Merge remote-tracking branch 'origin/master' into release, conflicts resolved

Conflicts:
	cms/envs/common.py
	common/lib/xmodule/xmodule/seq_module.py
	lms/envs/common.py
	requirements/edx/edx-private.txt
This commit is contained in:
Han Su Kim
2014-03-06 16:56:16 -05:00
225 changed files with 17128 additions and 4270 deletions

View File

@@ -121,7 +121,7 @@ Carson Gee <x@carsongee.com>
Oleg Marshev <oleh.marshev@gmail.com>
Sylvia Pearce <spearce@edx.org>
Olga Stroilova <olga@edx.org>
Paul-Olivier Dehaye <pdehaye@gmail.com>
Paul-Olivier Dehaye <paulolivier@gmail.com>
Feanil Patel <feanil@edx.org>
Zubair Afzal <zubair.afzal@arbisoft.com>
Juho Kim <juhokim@edx.org>
@@ -134,3 +134,4 @@ Avinash Sajjanshetty <avinashsajjan@gmail.com>
David Glance <david.glance@gmail.com>
Nimisha Asthagiri <nasthagiri@edx.org>
Martyn James <mjames@edx.org>
Han Su Kim <hkim823@gmail.com>

View File

@@ -5,6 +5,11 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.
Blades: Show start time or starting position on slider and VCR. BLD-823.
Common: Upgraded CodeMirror to 3.21.0 with an accessibility patch applied.
LMS-1802
Studio: Add new container page that can display nested xblocks. STUD-1244.
Blades: Allow multiple transcripts with video. BLD-642.

View File

@@ -3,10 +3,9 @@
from lettuce import world, step
from nose.tools import assert_false, assert_equal, assert_regexp_matches # pylint: disable=E0611
from common import type_in_codemirror, press_the_notification_button
from common import type_in_codemirror, press_the_notification_button, get_codemirror_value
KEY_CSS = '.key input.policy-key'
VALUE_CSS = 'textarea.json'
DISPLAY_NAME_KEY = "display_name"
DISPLAY_NAME_VALUE = '"Robot Super Course"'
@@ -101,7 +100,7 @@ def assert_policy_entries(expected_keys, expected_values):
for key, value in zip(expected_keys, expected_values):
index = get_index_of(key)
assert_false(index == -1, "Could not find key: {key}".format(key=key))
found_value = world.css_find(VALUE_CSS)[index].value
found_value = get_codemirror_value(index)
assert_equal(
value, found_value,
"Expected {} to have value {} but found {}".format(key, value, found_value)
@@ -120,15 +119,13 @@ def get_index_of(expected_key):
def get_display_name_value():
index = get_index_of(DISPLAY_NAME_KEY)
return world.css_value(VALUE_CSS, index=index)
return get_codemirror_value(index)
def change_display_name_value(step, new_value):
change_value(step, DISPLAY_NAME_KEY, new_value)
def change_value(step, key, new_value):
type_in_codemirror(get_index_of(key), new_value)
world.wait(0.5)
index = get_index_of(key)
type_in_codemirror(index, new_value)
press_the_notification_button(step, "Save")
world.wait_for_ajax_complete()

View File

@@ -319,20 +319,18 @@ def i_am_shown_a_notification(step):
def type_in_codemirror(index, text):
world.wait(1) # For now, slow this down so that it works. TODO: fix it.
world.css_click("div.CodeMirror-lines", index=index)
world.browser.execute_script("$('div.CodeMirror.CodeMirror-focused > div').css('overflow', '')")
g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea")
if world.is_mac():
g._element.send_keys(Keys.COMMAND + 'a')
else:
g._element.send_keys(Keys.CONTROL + 'a')
g._element.send_keys(Keys.DELETE)
g._element.send_keys(text)
if world.is_firefox():
world.trigger_event('div.CodeMirror', index=index, event='blur')
script = """
var cm = $('div.CodeMirror:eq({})').get(0).CodeMirror;
cm.getInputField().focus();
cm.setValue(arguments[0]);
cm.getInputField().blur();""".format(index)
world.browser.driver.execute_script(script, str(text))
world.wait_for_ajax_complete()
def get_codemirror_value(index=0):
return world.browser.driver.execute_script("""
return $('div.CodeMirror:eq({})').get(0).CodeMirror.getValue();
""".format(index))
def upload_file(filename):
path = os.path.join(TEST_ROOT, filename)

View File

@@ -3,7 +3,7 @@
from lettuce import world, step
from selenium.webdriver.common.keys import Keys
from common import type_in_codemirror
from common import type_in_codemirror, get_codemirror_value
from nose.tools import assert_in # pylint: disable=E0611
@@ -74,7 +74,7 @@ def change_date(_step, new_date):
@step(u'I should see the date "([^"]*)"$')
def check_date(_step, date):
date_css = 'span.date-display'
assert date == world.css_html(date_css)
assert_in(date, world.css_html(date_css))
@step(u'I modify the handout to "([^"]*)"$')
@@ -87,7 +87,7 @@ def edit_handouts(_step, text):
@step(u'I see the handout "([^"]*)"$')
def check_handout(_step, handout):
handout_css = 'div.handouts-content'
assert handout in world.css_html(handout_css)
assert_in(handout, world.css_html(handout_css))
@step(u'I see the handout error text')
@@ -127,6 +127,6 @@ def change_text(text):
def verify_text_in_editor_and_update(button_css, before, after):
world.css_click(button_css)
text = world.css_find(".cm-string").html
assert before in text
text = get_codemirror_value()
assert_in(before, text)
change_text(after)

View File

@@ -19,3 +19,9 @@ Feature: CMS.HTML Editor
Given I have created an E-text Written in LaTeX
When I edit and select Settings
Then Edit High Level Source is visible
Scenario: TinyMCE image plugin sets urls correctly
Given I have created a Blank HTML Page
When I edit the page and select the Visual Editor
And I add an image with a static link via the Image Plugin Icon
Then the image static link is rewritten to translate the path

View File

@@ -2,6 +2,7 @@
#pylint: disable=C0111
from lettuce import world, step
from nose.tools import assert_in # pylint: disable=no-name-in-module
@step('I have created a Blank HTML Page$')
@@ -28,3 +29,43 @@ def i_created_etext_in_latex(step):
category='html',
component_type='E-text Written in LaTeX'
)
@step('I edit the page and select the Visual Editor')
def i_click_on_edit_icon(step):
world.edit_component()
world.wait_for(lambda _driver: world.css_visible('a.visual-tab'))
world.css_click('a.visual-tab')
@step('I add an image with a static link via the Image Plugin Icon')
def i_click_on_image_plugin_icon(step):
# Click on image plugin button
world.wait_for(lambda _driver: world.css_visible('a.mce_image'))
world.css_click('a.mce_image')
# Change to the non-modal TinyMCE Image window
# keeping parent window so we can go back to it.
parent_window = world.browser.current_window
for window in world.browser.windows:
world.browser.switch_to_window(window) # Switch to a different window
if world.browser.title == 'Insert/Edit Image':
# This is the Image window so find the url text box,
# enter text in it then hit Insert button.
url_elem = world.browser.find_by_id("src")
url_elem.fill('/static/image.jpg')
world.browser.find_by_id('insert').click()
world.browser.switch_to_window(parent_window) # Switch back to the main window
@step('the image static link is rewritten to translate the path')
def image_static_link_is_rewritten(step):
# Find the TinyMCE iframe within the main window
with world.browser.get_iframe('mce_0_ifr') as tinymce:
image = tinymce.find_by_tag('img').first
# Test onExecCommandHandler set the url to absolute.
assert_in('c4x/MITx/999/asset/image.jpg', image['src'])

View File

@@ -173,7 +173,7 @@ def cancel_does_not_save_changes(step):
def enable_latex_compiler(step):
url = world.browser.url
step.given("I select the Advanced Settings")
change_value(step, 'use_latex_compiler', True)
change_value(step, 'use_latex_compiler', 'true')
world.visit(url)
world.wait_for_xmodule()

View File

@@ -1003,7 +1003,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# We had a bug where orphaned draft nodes caused export to fail. This is here to cover that case.
vertical.location = mongo.draft.as_draft(vertical.location.replace(name='no_references'))
draft_store.save_xmodule(vertical)
draft_store.update_item(vertical, allow_not_found=True)
orphan_vertical = draft_store.get_item(vertical.location)
self.assertEqual(orphan_vertical.location.name, 'no_references')
@@ -1020,13 +1020,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# now create a new/different private (draft only) vertical
vertical.location = mongo.draft.as_draft(Location(['i4x', 'edX', 'toy', 'vertical', 'a_private_vertical', None]))
draft_store.save_xmodule(vertical)
draft_store.update_item(vertical, allow_not_found=True)
private_vertical = draft_store.get_item(vertical.location)
vertical = None # blank out b/c i destructively manipulated its location 2 lines above
# add the new private to list of children
sequential = module_store.get_item(Location(['i4x', 'edX', 'toy',
'sequential', 'vertical_sequential', None]))
sequential = module_store.get_item(
Location('i4x', 'edX', 'toy', 'sequential', 'vertical_sequential', None)
)
private_location_no_draft = private_vertical.location.replace(revision=None)
sequential.children.append(private_location_no_draft.url())
module_store.update_item(sequential, self.user.id)

View File

@@ -17,6 +17,7 @@ from .utils import CourseTestCase
import contentstore.git_export_utils as git_export_utils
from xmodule.contentstore.django import _CONTENTSTORE
from xmodule.modulestore.django import modulestore
from contentstore.utils import get_modulestore
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex
@@ -70,7 +71,7 @@ class TestExportGit(CourseTestCase):
Test failed course export response.
"""
self.course_module.giturl = 'foobar'
modulestore().save_xmodule(self.course_module)
get_modulestore(self.course_module.location).update_item(self.course_module)
response = self.client.get('{}?action=push'.format(self.test_url))
self.assertIn('Export Failed:', response.content)
@@ -93,7 +94,7 @@ class TestExportGit(CourseTestCase):
self.populateCourse()
self.course_module.giturl = 'file://{}'.format(bare_repo_dir)
modulestore().save_xmodule(self.course_module)
get_modulestore(self.course_module.location).update_item(self.course_module)
response = self.client.get('{}?action=push'.format(self.test_url))
self.assertIn('Export Succeeded', response.content)

View File

@@ -23,11 +23,10 @@ from xblock.exceptions import NoSuchHandlerError
from xblock.fields import Scope
from xblock.plugin import PluginMissingError
from xblock.runtime import Mixologist
from xmodule.x_module import prefer_xmodules
from lms.lib.xblock.runtime import unquote_slashes
from contentstore.utils import get_lms_link_for_item, compute_unit_state, UnitState
from contentstore.utils import get_lms_link_for_item, compute_unit_state, UnitState, get_modulestore
from contentstore.views.helpers import get_parent_xblock
from models.settings.course_grading import CourseGradingModel
@@ -310,13 +309,20 @@ def container_handler(request, tag=None, package_id=None, branch=None, version_g
old_location, course, xblock, __ = _get_item_in_course(request, locator)
except ItemNotFoundError:
return HttpResponseBadRequest()
parent_xblock = get_parent_xblock(xblock)
ancestor_xblocks = []
parent = get_parent_xblock(xblock)
while parent and parent.category != 'sequential':
ancestor_xblocks.append(parent)
parent = get_parent_xblock(parent)
ancestor_xblocks.reverse()
return render_to_response('container.html', {
'context_course': course,
'xblock': xblock,
'xblock_locator': locator,
'parent_xblock': parent_xblock,
'ancestor_xblocks': ancestor_xblocks,
})
else:
return HttpResponseBadRequest("Only supports html requests")
@@ -359,7 +365,7 @@ def component_handler(request, usage_id, handler, suffix=''):
location = unquote_slashes(usage_id)
descriptor = modulestore().get_item(location)
descriptor = get_modulestore(location).get_item(location)
# Let the module handle the AJAX
req = django_to_webob_request(request)
@@ -370,6 +376,8 @@ def component_handler(request, usage_id, handler, suffix=''):
log.info("XBlock %s attempted to access missing handler %r", descriptor, handler, exc_info=True)
raise Http404
modulestore().save_xmodule(descriptor)
# unintentional update to handle any side effects of handle call; so, request user didn't author
# the change
get_modulestore(location).update_item(descriptor, None)
return webob_to_django_response(resp)

View File

@@ -127,6 +127,12 @@ def xblock_handler(request, tag=None, package_id=None, branch=None, version_guid
return _delete_item_at_location(old_location, delete_children, delete_all_versions, request.user)
else: # Since we have a package_id, we are updating an existing xblock.
if block == 'handouts' and old_location is None:
# update handouts location in loc_mapper
course_location = loc_mapper().translate_locator_to_location(locator, get_course=True)
old_location = course_location.replace(category='course_info', name=block)
locator = loc_mapper().translate_location(course_location.course_id, old_location)
return _save_item(
request,
locator,
@@ -202,16 +208,16 @@ def xblock_view_handler(request, package_id, view_name, tag=None, branch=None, v
log.debug("unable to render studio_view for %r", component, exc_info=True)
fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)}))
store.save_xmodule(component)
# change not authored by requestor but by xblocks.
store.update_item(component, None)
elif view_name == 'student_view' and component.has_children:
# For non-leaf xblocks on the unit page, show the special rendering
# which links to the new container page.
course_location = loc_mapper().translate_locator_to_location(locator, True)
course = store.get_item(course_location)
html = render_to_string('unit_container_xblock_component.html', {
'course': course,
html = render_to_string('container_xblock_component.html', {
'xblock': component,
'locator': locator
'locator': locator,
'reordering_enabled': True,
})
return JsonResponse({
'html': html,
@@ -521,8 +527,8 @@ def orphan_handler(request, tag=None, package_id=None, branch=None, version_guid
if request.method == 'DELETE':
if request.user.is_staff:
items = modulestore().get_orphans(old_location, 'draft')
for item in items:
modulestore('draft').delete_item(item, delete_all_versions=True)
for itemloc in items:
modulestore('draft').delete_item(itemloc, delete_all_versions=True)
return JsonResponse({'deleted': items})
else:
raise PermissionDenied()

View File

@@ -179,6 +179,8 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
}
if xblock.category == 'vertical':
template = 'studio_vertical_wrapper.html'
elif xblock.location != context.get('root_xblock').location and xblock.has_children:
template = 'container_xblock_component.html'
else:
template = 'studio_xblock_wrapper.html'
html = render_to_string(template, template_context)

View File

@@ -26,8 +26,44 @@ class ContainerViewTestCase(CourseTestCase):
category="video", display_name="My Video")
def test_container_html(self):
url = xblock_studio_url(self.child_vertical)
self._test_html_content(
self.child_vertical,
expected_section_tag='<section class="wrapper-xblock level-page" data-locator="MITx.999.Robot_Super_Course/branch/published/block/Child_Vertical"/>',
expected_breadcrumbs=(
r'<a href="/unit/MITx.999.Robot_Super_Course/branch/published/block/Unit"\s*'
r'class="navigation-link navigation-parent">Unit</a>\s*'
r'<a href="#" class="navigation-link navigation-current">Child Vertical</a>'),
)
def test_container_on_container_html(self):
"""
Create the scenario of an xblock with children (non-vertical) on the container page.
This should create a container page that is a child of another container page.
"""
xblock_with_child = ItemFactory.create(parent_location=self.child_vertical.location,
category="wrapper", display_name="Wrapper")
ItemFactory.create(parent_location=xblock_with_child.location,
category="html", display_name="Child HTML")
self._test_html_content(
xblock_with_child,
expected_section_tag='<section class="wrapper-xblock level-page" data-locator="MITx.999.Robot_Super_Course/branch/published/block/Wrapper"/>',
expected_breadcrumbs=(
r'<a href="/unit/MITx.999.Robot_Super_Course/branch/published/block/Unit"\s*'
r'class="navigation-link navigation-parent">Unit</a>\s*'
r'<a href="/container/MITx.999.Robot_Super_Course/branch/published/block/Child_Vertical"\s*'
r'class="navigation-link navigation-parent">Child Vertical</a>\s*'
r'<a href="#" class="navigation-link navigation-current">Wrapper</a>'),
)
def _test_html_content(self, xblock, expected_section_tag, expected_breadcrumbs):
"""
Get the HTML for a container page and verify the section tag is correct
and the breadcrumbs trail is correct.
"""
url = xblock_studio_url(xblock, self.course)
resp = self.client.get_html(url)
self.assertEqual(resp.status_code, 200)
html = resp.content
self.assertIn('<section class="wrapper-xblock level-page" data-locator="MITx.999.Robot_Super_Course/branch/published/block/Child_Vertical"/>', html)
self.assertIn(expected_section_tag, html)
# Verify the navigation link at the top of the page is correct.
self.assertRegexpMatches(html, expected_breadcrumbs)

View File

@@ -230,7 +230,8 @@ class CourseUpdateTest(CourseTestCase):
def test_post_course_update(self):
"""
Test that a user can successfully post on course updates of a course whose location in not in loc_mapper
Test that a user can successfully post on course updates and handouts of a course
whose location in not in loc_mapper
"""
# create a course via the view handler
course_location = Location(['i4x', 'Org_1', 'Course_1', 'course', 'Run_1'])
@@ -270,3 +271,19 @@ class CourseUpdateTest(CourseTestCase):
updates_locator = loc_mapper().translate_location(course_location.course_id, updates_location)
self.assertTrue(isinstance(updates_locator, BlockUsageLocator))
self.assertEqual(updates_locator.block_id, block)
# check posting on handouts
block = u'handouts'
handouts_locator = BlockUsageLocator(
package_id=updates_locator.package_id, branch=updates_locator.branch, version_guid=version, block_id=block
)
course_handouts_url = handouts_locator.url_reverse('xblock')
content = u"Sample handout"
payload = {"data": content}
resp = self.client.ajax_post(course_handouts_url, payload)
# check that response status is 200 not 500
self.assertEqual(resp.status_code, 200)
payload = json.loads(resp.content)
self.assertHTMLEqual(payload['data'], content)

View File

@@ -132,6 +132,31 @@ class GetItem(ItemTest):
# Verify that the Studio element wrapper has been added
self.assertIn('level-element', html)
def test_get_container_nested_container_fragment(self):
"""
Test the case of the container page containing a link to another container page.
"""
# Add a wrapper with child beneath a child vertical
root_locator = self._create_vertical()
resp = self.create_xblock(parent_locator=root_locator, category="wrapper")
self.assertEqual(resp.status_code, 200)
wrapper_locator = self.response_locator(resp)
resp = self.create_xblock(parent_locator=wrapper_locator, category='problem', boilerplate='multiplechoice.yaml')
self.assertEqual(resp.status_code, 200)
# Get the preview HTML and verify the View -> link is present.
html, __ = self._get_container_preview(root_locator)
self.assertIn('wrapper-xblock', html)
self.assertRegexpMatches(
html,
# The instance of the wrapper class will have an auto-generated ID (wrapperxxx). Allow anything
# for the 3 characters after wrapper.
(r'"/container/MITx.999.Robot_Super_Course/branch/published/block/wrapper.{3}" class="action-button">\s*'
'<span class="action-button-text">View</span>')
)
class DeleteItem(ItemTest):
"""Tests for '/xblock' DELETE url."""
@@ -636,11 +661,11 @@ class TestComponentHandler(TestCase):
def setUp(self):
self.request_factory = RequestFactory()
patcher = patch('contentstore.views.component.modulestore')
self.modulestore = patcher.start()
patcher = patch('contentstore.views.component.get_modulestore')
self.get_modulestore = patcher.start()
self.addCleanup(patcher.stop)
self.descriptor = self.modulestore.return_value.get_item.return_value
self.descriptor = self.get_modulestore.return_value.get_item.return_value
self.usage_id = 'dummy_usage_id'

View File

@@ -194,7 +194,7 @@ class CourseDetails(object):
result = None
if video_key:
result = '<iframe width="560" height="315" src="//www.youtube.com/embed/' + \
video_key + '?autoplay=1&rel=0" frameborder="0" allowfullscreen=""></iframe>'
video_key + '?rel=0" frameborder="0" allowfullscreen=""></iframe>'
return result

View File

@@ -16,7 +16,7 @@ os.environ['SERVICE_VARIANT'] = 'bok_choy'
os.environ['CONFIG_ROOT'] = path(__file__).abspath().dirname() #pylint: disable=E1120
from .aws import * # pylint: disable=W0401, W0614
from xmodule.x_module import prefer_xmodules
from xmodule.modulestore import prefer_xmodules
######################### Testing overrides ####################################

View File

@@ -296,7 +296,7 @@ PIPELINE_CSS = {
'css/vendor/normalize.css',
'css/vendor/font-awesome.css',
'css/vendor/html5-input-polyfills/number-polyfill.css',
'js/vendor/CodeMirror/codemirror.css',
'js/vendor/CodeMirror/codemirror-3.21.0.css',
'css/vendor/ui-lightness/jquery-ui-1.8.22.custom.css',
'css/vendor/jquery.qtip.min.css',
'js/vendor/markitup/skins/simple/style.css',

View File

@@ -16,6 +16,7 @@ from .common import *
import os
from path import path
from warnings import filterwarnings
from xmodule.modulestore import prefer_xmodules
# Nose Test Runner
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
@@ -158,7 +159,6 @@ filterwarnings('ignore', message='No request passed to the backend, unable to ra
################################# XBLOCK ######################################
from xmodule.x_module import prefer_xmodules
XBLOCK_SELECT_FUNCTION = prefer_xmodules

View File

@@ -20,6 +20,10 @@ define [
super()
@savingNotification = new NotificationView.Mini
title: gettext('Saving&hellip;')
@alert = new NotificationView.Error
title: "OpenAssessment Save Error",
closeIcon: false,
shown: false
handlerUrl: (element, handlerName, suffix, query, thirdparty) ->
uri = URI("/xblock").segment($(element).data('usage-id'))
@@ -41,11 +45,15 @@ define [
# Starting to save, so show the "Saving..." notification
if data.state == 'start'
@_hideEditor()
@savingNotification.show()
# Finished saving, so hide the "Saving..." notification
else if data.state == 'end'
# Hide the editor *after* we finish saving in case there are validation
# errors that the user needs to correct.
@_hideEditor()
$('.component.editing').removeClass('editing')
@savingNotification.hide()
@@ -54,7 +62,8 @@ define [
else if name == 'error'
if 'msg' of data
@_showAlert(data.msg)
@alert.options.message = data.msg
@alert.show()
_hideEditor: () ->
# This will close all open component editors, which works
@@ -64,9 +73,6 @@ define [
el.find('.component-editor').slideUp(150)
ModalUtils.hideModalCover()
_showAlert: (msg) ->
new NotificationView.Error({
title: "OpenAssessment Save Error",
message: msg,
closeIcon: false
}).show()
# Hide any alerts that are being shown
if @alert.options.shown
@alert.hide()

View File

@@ -7,9 +7,10 @@ define(["codemirror", 'js/utils/handle_iframe_binding', "utility"],
mode: "text/html",
lineNumbers: true,
lineWrapping: true,
onChange: function () {
autoCloseTags: true
});
$codeMirror.on('change', function () {
$('.save-button').removeClass('is-disabled');
}
});
$codeMirror.setValue(content);
$codeMirror.clearHistory();

View File

@@ -47,9 +47,11 @@ var AdvancedView = ValidatingView.extend({
var self = this;
var oldValue = $(textarea).val();
CodeMirror.fromTextArea(textarea, {
mode: "application/json", lineNumbers: false, lineWrapping: false,
onChange: function(instance, changeobj) {
var cm = CodeMirror.fromTextArea(textarea, {
mode: "application/json",
lineNumbers: false,
lineWrapping: false});
cm.on('change', function(instance, changeobj) {
instance.save();
// this event's being called even when there's no change :-(
if (instance.getValue() !== oldValue) {
@@ -58,11 +60,11 @@ var AdvancedView = ValidatingView.extend({
_.bind(self.saveView, self),
_.bind(self.revertView, self));
}
},
onFocus : function(mirror) {
});
cm.on('focus', function(mirror) {
$(textarea).parent().children('label').addClass("is-focused");
},
onBlur: function (mirror) {
});
cm.on('blur', function (mirror) {
$(textarea).parent().children('label').removeClass("is-focused");
var key = $(mirror.getWrapperElement()).closest('.field-group').children('.key').attr('id');
var stringValue = $.trim(mirror.getValue());
@@ -91,8 +93,7 @@ var AdvancedView = ValidatingView.extend({
if (JSONValue !== undefined) {
self.model.set(key, JSONValue);
}
}
});
});
},
saveView : function() {
// TODO one last verification scan:

View File

@@ -206,15 +206,14 @@ var DetailsView = ValidatingView.extend({
var cachethis = this;
var field = this.selectorToField[thisTarget.id];
this.codeMirrors[thisTarget.id] = CodeMirror.fromTextArea(thisTarget, {
mode: "text/html", lineNumbers: true, lineWrapping: true,
onChange: function (mirror) {
mode: "text/html", lineNumbers: true, lineWrapping: true});
this.codeMirrors[thisTarget.id].on('change', function (mirror) {
mirror.save();
cachethis.clearValidationErrors();
var newVal = mirror.getValue();
if (cachethis.model.get(field) != newVal) {
cachethis.setAndValidate(field, newVal);
}
}
});
}
},

View File

@@ -334,11 +334,12 @@ p, ul, ol, dl {
.navigation-link {
@extend %cont-truncated;
display: inline-block;
max-width: 150px;
max-width: 250px;
&.navigation-current {
@extend %ui-disabled;
color: $gray;
max-width: 250px;
&:before {
color: $gray;

View File

@@ -55,7 +55,7 @@
}
// UI: xblock is collapsible
.wrapper-xblock.is-collapsible {
.wrapper-xblock.is-collapsible, .wrapper-xblock.xblock-type-container {
[class^="icon-"] {
font-style: normal;

View File

@@ -831,19 +831,15 @@
font-family: 'Open Sans', sans-serif;
color: $baseFontColor;
outline: 0;
height: auto;
min-height: ($baseline*2.25);
max-height: ($baseline*10);
&.CodeMirror-focused {
@include linear-gradient($paleYellow, tint($paleYellow, 90%));
outline: 0;
}
.CodeMirror-scroll {
overflow: hidden;
height: auto;
min-height: ($baseline*1.5);
max-height: ($baseline*10);
}
// editor color changes just for JSON
.CodeMirror-lines {

View File

@@ -16,7 +16,7 @@ from django.utils.translation import ugettext as _
<%
xblock_info = {
'id': str(xblock_locator),
'display-name': xblock.display_name,
'display-name': xblock.display_name_with_default,
'category': xblock.category,
};
%>
@@ -47,14 +47,16 @@ xblock_info = {
<header class="mast has-actions has-navigation">
<h1 class="page-header">
<small class="navigation navigation-parents">
<%
parent_url = xblock_studio_url(parent_xblock, context_course)
%>
% if parent_url:
<a href="${parent_url}"
class="navigation-link navigation-parent">${parent_xblock.display_name | h}</a>
% endif
<a href="#" class="navigation-link navigation-current">${xblock.display_name | h}</a>
% for ancestor in ancestor_xblocks:
<%
ancestor_url = xblock_studio_url(ancestor, context_course)
%>
% if ancestor_url:
<a href="${ancestor_url}"
class="navigation-link navigation-parent">${ancestor.display_name_with_default | h}</a>
% endif
% endfor
<a href="#" class="navigation-link navigation-current">${xblock.display_name_with_default | h}</a>
</small>
</h1>

View File

@@ -7,12 +7,12 @@ from contentstore.views.helpers import xblock_studio_url
<section class="wrapper-xblock xblock-type-container level-element" data-locator="${locator}">
<header class="xblock-header">
<div class="header-details">
${xblock.display_name}
${xblock.display_name_with_default}
</div>
<div class="header-actions">
<ul class="actions-list">
<li class="action-item action-view">
<a href="${xblock_studio_url(xblock, course)}" class="action-button">
<a href="${xblock_studio_url(xblock)}" class="action-button">
## Translators: this is a verb describing the action of viewing more details
<span class="action-button-text">${_('View')}</span>
<i class="icon-arrow-right"></i>
@@ -21,5 +21,8 @@ from contentstore.views.helpers import xblock_studio_url
</ul>
</div>
</header>
<span data-tooltip="${_("Drag to reorder")}" class="drag-handle"></span>
## We currently support reordering only on the unit page.
% if reordering_enabled:
<span data-tooltip="${_("Drag to reorder")}" class="drag-handle"></span>
% endif
</section>

View File

@@ -8,7 +8,7 @@
<i class="icon-caret-down ui-toggle-expansion"></i>
<span class="sr">${_('Expand or Collapse')}</span>
</a>
<span>${xblock.display_name | h}</span>
<span>${xblock.display_name_with_default | h}</span>
</div>
<div class="header-actions">
<ul class="actions-list">

View File

@@ -8,7 +8,7 @@
% endif
<header class="xblock-header">
<div class="header-details">
${xblock.display_name | h}
${xblock.display_name_with_default | h}
</div>
<div class="header-actions">
<ul class="actions-list">

View File

@@ -97,7 +97,7 @@ require(["jquery", "jquery.leanModal", "codemirror/stex"], function($) {
});
// resize the codemirror box
var h = el.height();
el.find('.CodeMirror-scroll').height(h - 100);
el.find('.CodeMirror').height(h - 160);
}
// compile & save button

View File

@@ -29,6 +29,7 @@ from django.dispatch import receiver, Signal
import django.dispatch
from django.forms import ModelForm, forms
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import ugettext_noop
from django_countries import CountryField
from track import contexts
from track.views import server_track
@@ -189,7 +190,12 @@ class UserProfile(models.Model):
this_year = datetime.now(UTC).year
VALID_YEARS = range(this_year, this_year - 120, -1)
year_of_birth = models.IntegerField(blank=True, null=True, db_index=True)
GENDER_CHOICES = (('m', 'Male'), ('f', 'Female'), ('o', 'Other'))
GENDER_CHOICES = (
('m', ugettext_noop('Male')),
('f', ugettext_noop('Female')),
# Translators: 'Other' refers to the student's gender
('o', ugettext_noop('Other'))
)
gender = models.CharField(
blank=True, null=True, max_length=6, db_index=True, choices=GENDER_CHOICES
)
@@ -199,15 +205,17 @@ class UserProfile(models.Model):
# ('p_se', 'Doctorate in science or engineering'),
# ('p_oth', 'Doctorate in another field'),
LEVEL_OF_EDUCATION_CHOICES = (
('p', 'Doctorate'),
('m', "Master's or professional degree"),
('b', "Bachelor's degree"),
('a', "Associate's degree"),
('hs', "Secondary/high school"),
('jhs', "Junior secondary/junior high/middle school"),
('el', "Elementary/primary school"),
('none', "None"),
('other', "Other")
('p', ugettext_noop('Doctorate')),
('m', ugettext_noop("Master's or professional degree")),
('b', ugettext_noop("Bachelor's degree")),
('a', ugettext_noop("Associate's degree")),
('hs', ugettext_noop("Secondary/high school")),
('jhs', ugettext_noop("Junior secondary/junior high/middle school")),
('el', ugettext_noop("Elementary/primary school")),
# Translators: 'None' refers to the student's level of education
('none', ugettext_noop("None")),
# Translators: 'Other' refers to the student's level of education
('other', ugettext_noop("Other"))
)
level_of_education = models.CharField(
blank=True, null=True, max_length=6, db_index=True,

View File

@@ -1,11 +1,16 @@
<%! from django.utils.translation import ugettext as _ %>
<section id="textbox_${id}" class="textbox">
<textarea rows="${rows}" cols="${cols}" name="input_${id}" aria-describedby="answer_${id}" id="input_${id}"
<textarea rows="${rows}" cols="${cols}" name="input_${id}"
aria-label="${_("{programming_language} editor").format(programming_language=mode)}"
aria-describedby="answer_${id}"
id="input_${id}"
tabindex="0"
% if hidden:
style="display:none;"
% endif
>${value|h}</textarea>
<div class="grader-status">
<div class="grader-status" tabindex="1">
% if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: </span>Unanswered</span>
% elif status == 'correct':
@@ -44,13 +49,16 @@
tabSize: "${tabsize}",
indentWithTabs: false,
extraKeys: {
"Esc": function(cm) {
$('.grader-status').focus();
return false;
},
"Tab": function(cm) {
cm.replaceSelection("${' '*tabsize}", "end");
}
},
smartIndent: false
});
$("#textbox_${id}").find('.CodeMirror-scroll').height(${int(13.5*eval(rows))});
});
</script>
</section>

View File

@@ -31,3 +31,10 @@ class SerializationError(Exception):
def __init__(self, location, msg):
super(SerializationError, self).__init__(msg)
self.location = location
class UndefinedContext(Exception):
"""
Tried to access an xmodule field which needs a different context (runtime) to have a value.
"""
pass

View File

@@ -183,7 +183,7 @@ class WeightedSubsectionsGrader(CourseGrader):
subgrade_result = subgrader.grade(grade_sheet, generate_random_scores)
weighted_percent = subgrade_result['percent'] * weight
section_detail = u"{0} = {1:.1%} of a possible {2:.0%}".format(category, weighted_percent, weight)
section_detail = u"{0} = {1:.2%} of a possible {2:.2%}".format(category, weighted_percent, weight)
total_percent += weighted_percent
section_breakdown += subgrade_result['section_breakdown']

View File

@@ -253,8 +253,8 @@
return state;
};
jasmine.initializePlayerYouTube = function () {
jasmine.initializePlayerYouTube = function (params) {
// "video.html" contains HTML template for a YouTube video.
return jasmine.initializePlayer('video.html');
return jasmine.initializePlayer('video.html', params);
};
}).call(this, window.jQuery);

View File

@@ -89,17 +89,15 @@ describe 'HTMLEditingDescriptor', ->
@descriptor.showingVisualEditor = false
visualEditorStub =
isNotDirty: false
content: 'not set'
startContent: 'not set',
focus: () -> true
isDirty: () -> not @isNotDirty
isDirty: () -> false
setContent: (x) -> @content = x
getContent: -> @content
@descriptor.showVisualEditor(visualEditorStub)
expect(@descriptor.showingVisualEditor).toEqual(true)
expect(visualEditorStub.isDirty()).toEqual(false)
expect(visualEditorStub.getContent()).toEqual('Advanced Editor Text')
expect(visualEditorStub.startContent).toEqual('Advanced Editor Text')
it 'When switching to visual editor links are rewritten to c4x format', ->
@@ -109,16 +107,14 @@ describe 'HTMLEditingDescriptor', ->
@descriptor.showingVisualEditor = false
visualEditorStub =
isNotDirty: false
content: 'not set'
startContent: 'not set',
focus: () -> true
isDirty: () -> not @isNotDirty
isDirty: () -> false
setContent: (x) -> @content = x
getContent: -> @content
@descriptor.showVisualEditor(visualEditorStub)
expect(@descriptor.showingVisualEditor).toEqual(true)
expect(visualEditorStub.isDirty()).toEqual(false)
expect(visualEditorStub.getContent()).toEqual('Advanced Editor Text with link /c4x/foo/bar/asset/dummy.jpg')
expect(visualEditorStub.startContent).toEqual('Advanced Editor Text with link /c4x/foo/bar/asset/dummy.jpg')

View File

@@ -104,6 +104,451 @@
});
});
describe('constructor with start-time', function () {
it('saved position is 0, timer slider and VCR set to start-time', function () {
var duration;
runs(function () {
state = jasmine.initializePlayer({
start: 10,
savedVideoPosition: 0
});
spyOn(state.videoPlayer, 'duration').andReturn(60);
});
waitsFor(function () {
duration = state.videoPlayer.duration();
return isFinite(duration) && duration > 0 &&
isFinite(state.videoPlayer.startTime);
}, 'duration is set', WAIT_TIMEOUT);
runs(function () {
expect($('.video-controls').find('.vidtime'))
.toHaveText('0:10 / 1:00');
expect(state.videoProgressSlider.slider.slider('option', 'value')).toBe(10);
state.storage.clear();
});
});
it('saved position is after start-time, timer slider and VCR set to saved position', function () {
var duration;
runs(function () {
state = jasmine.initializePlayer({
start: 10,
savedVideoPosition: 15
});
spyOn(state.videoPlayer, 'duration').andReturn(60);
});
waitsFor(function () {
duration = state.videoPlayer.duration();
return isFinite(duration) && duration > 0 &&
isFinite(state.videoPlayer.startTime);
}, 'duration is set', WAIT_TIMEOUT);
runs(function () {
expect($('.video-controls').find('.vidtime'))
.toHaveText('0:15 / 1:00');
expect(state.videoProgressSlider.slider.slider('option', 'value')).toBe(15);
state.storage.clear();
});
});
it('saved position is negative, timer slider and VCR set to start-time', function () {
var duration;
runs(function () {
state = jasmine.initializePlayer({
start: 10,
savedVideoPosition: -15
});
spyOn(state.videoPlayer, 'duration').andReturn(60);
});
waitsFor(function () {
duration = state.videoPlayer.duration();
return isFinite(duration) && duration > 0 &&
isFinite(state.videoPlayer.startTime);
}, 'duration is set', WAIT_TIMEOUT);
runs(function () {
expect($('.video-controls').find('.vidtime'))
.toHaveText('0:10 / 1:00');
expect(state.videoProgressSlider.slider.slider('option', 'value')).toBe(10);
state.storage.clear();
});
});
it('saved position is not a number, timer slider and VCR set to start-time', function () {
var duration;
runs(function () {
state = jasmine.initializePlayer({
start: 10,
savedVideoPosition: 'a'
});
spyOn(state.videoPlayer, 'duration').andReturn(60);
});
waitsFor(function () {
duration = state.videoPlayer.duration();
return isFinite(duration) && duration > 0 &&
isFinite(state.videoPlayer.startTime);
}, 'duration is set', WAIT_TIMEOUT);
runs(function () {
expect($('.video-controls').find('.vidtime'))
.toHaveText('0:10 / 1:00');
expect(state.videoProgressSlider.slider.slider('option', 'value')).toBe(10);
state.storage.clear();
});
});
it('saved position is greater than end-time, timer slider and VCR set to start-time', function () {
var duration;
runs(function () {
state = jasmine.initializePlayer({
start: 10,
savedVideoPosition: 10000
});
spyOn(state.videoPlayer, 'duration').andReturn(60);
});
waitsFor(function () {
duration = state.videoPlayer.duration();
return isFinite(duration) && duration > 0 &&
isFinite(state.videoPlayer.startTime);
}, 'duration is set', WAIT_TIMEOUT);
runs(function () {
expect($('.video-controls').find('.vidtime'))
.toHaveText('0:10 / 1:00');
expect(state.videoProgressSlider.slider.slider('option', 'value')).toBe(10);
state.storage.clear();
});
});
});
describe('constructor with end-time', function () {
it('saved position is 0, timer slider and VCR set to 0:00', function () {
var duration;
runs(function () {
state = jasmine.initializePlayer({
end: 20,
savedVideoPosition: 0
});
spyOn(state.videoPlayer, 'duration').andReturn(60);
});
waitsFor(function () {
duration = state.videoPlayer.duration();
return isFinite(duration) && duration > 0 &&
isFinite(state.videoPlayer.startTime);
}, 'duration is set', WAIT_TIMEOUT);
runs(function () {
expect($('.video-controls').find('.vidtime'))
.toHaveText('0:00 / 1:00');
expect(state.videoProgressSlider.slider.slider('option', 'value')).toBe(0);
state.storage.clear();
});
});
it('saved position is after start-time, timer slider and VCR set to saved position', function () {
var duration;
runs(function () {
state = jasmine.initializePlayer({
end: 20,
savedVideoPosition: 15
});
spyOn(state.videoPlayer, 'duration').andReturn(60);
});
waitsFor(function () {
duration = state.videoPlayer.duration();
return isFinite(duration) && duration > 0 &&
isFinite(state.videoPlayer.startTime);
}, 'duration is set', WAIT_TIMEOUT);
runs(function () {
expect($('.video-controls').find('.vidtime'))
.toHaveText('0:15 / 1:00');
expect(state.videoProgressSlider.slider.slider('option', 'value')).toBe(15);
state.storage.clear();
});
});
// TODO: Fix!
it('saved position is negative, timer slider and VCR set to 0:00', function () {
var duration, c1 = 0;
runs(function () {
state = jasmine.initializePlayer({
end: 20,
savedVideoPosition: -15
});
spyOn(state.videoPlayer, 'duration').andReturn(60);
});
waitsFor(function () {
c1 += 1;
console.log('c1 = ', c1);
duration = state.videoPlayer.duration();
return isFinite(duration) && duration > 0 &&
isFinite(state.videoPlayer.startTime);
}, 'duration is set', WAIT_TIMEOUT);
runs(function () {
console.log('oiooio');
console.log(state.videoProgressSlider.slider);
console.log('0000');
expect($('.video-controls').find('.vidtime'))
.toHaveText('0:00 / 1:00');
console.log('1111');
expect(true).toBe(true);
console.log('1111');
// expect(state.videoProgressSlider.slider.slider('option', 'value')).toBe(0);
state.storage.clear();
});
});
it('saved position is not a number, timer slider and VCR set to 0:00', function () {
var duration;
runs(function () {
state = jasmine.initializePlayer({
end: 20,
savedVideoPosition: 'a'
});
spyOn(state.videoPlayer, 'duration').andReturn(60);
});
waitsFor(function () {
duration = state.videoPlayer.duration();
return isFinite(duration) && duration > 0 &&
isFinite(state.videoPlayer.startTime);
}, 'duration is set', WAIT_TIMEOUT);
runs(function () {
expect($('.video-controls').find('.vidtime'))
.toHaveText('0:00 / 1:00');
expect(state.videoProgressSlider.slider.slider('option', 'value')).toBe(0);
state.storage.clear();
});
});
// TODO: Fix!
it('saved position is greater than end-time, timer slider and VCR set to 0:00', function () {
var duration;
runs(function () {
state = jasmine.initializePlayer({
end: 20,
savedVideoPosition: 10000
});
spyOn(state.videoPlayer, 'duration').andReturn(60);
});
waitsFor(function () {
duration = state.videoPlayer.duration();
return isFinite(duration) && duration > 0 &&
isFinite(state.videoPlayer.startTime);
}, 'duration is set', WAIT_TIMEOUT);
runs(function () {
expect($('.video-controls').find('.vidtime'))
.toHaveText('0:00 / 1:00');
expect(state.videoProgressSlider.slider.slider('option', 'value')).toBe(0);
state.storage.clear();
});
});
});
describe('constructor with start-time and end-time', function () {
it('saved position is 0, timer slider and VCR set to start-time', function () {
var duration;
runs(function () {
state = jasmine.initializePlayer({
start: 10,
end: 20,
savedVideoPosition: 0
});
spyOn(state.videoPlayer, 'duration').andReturn(60);
});
waitsFor(function () {
duration = state.videoPlayer.duration();
return isFinite(duration) && duration > 0 &&
isFinite(state.videoPlayer.startTime);
}, 'duration is set', WAIT_TIMEOUT);
runs(function () {
expect($('.video-controls').find('.vidtime'))
.toHaveText('0:10 / 1:00');
expect(state.videoProgressSlider.slider.slider('option', 'value')).toBe(10);
state.storage.clear();
});
});
it('saved position is after start-time, timer slider and VCR set to saved position', function () {
var duration;
runs(function () {
state = jasmine.initializePlayer({
start: 10,
end: 20,
savedVideoPosition: 15
});
spyOn(state.videoPlayer, 'duration').andReturn(60);
});
waitsFor(function () {
duration = state.videoPlayer.duration();
return isFinite(duration) && duration > 0 &&
isFinite(state.videoPlayer.startTime);
}, 'duration is set', WAIT_TIMEOUT);
runs(function () {
expect($('.video-controls').find('.vidtime'))
.toHaveText('0:15 / 1:00');
expect(state.videoProgressSlider.slider.slider('option', 'value')).toBe(15);
state.storage.clear();
});
});
it('saved position is negative, timer slider and VCR set to start-time', function () {
var duration;
runs(function () {
state = jasmine.initializePlayer({
start: 10,
end: 20,
savedVideoPosition: -15
});
spyOn(state.videoPlayer, 'duration').andReturn(60);
});
waitsFor(function () {
duration = state.videoPlayer.duration();
return isFinite(duration) && duration > 0 &&
isFinite(state.videoPlayer.startTime);
}, 'duration is set', WAIT_TIMEOUT);
runs(function () {
expect($('.video-controls').find('.vidtime'))
.toHaveText('0:10 / 1:00');
expect(state.videoProgressSlider.slider.slider('option', 'value')).toBe(10);
state.storage.clear();
});
});
it('saved position is not a number, timer slider and VCR set to start-time', function () {
var duration;
runs(function () {
state = jasmine.initializePlayer({
start: 10,
end: 20,
savedVideoPosition: 'a'
});
spyOn(state.videoPlayer, 'duration').andReturn(60);
});
waitsFor(function () {
duration = state.videoPlayer.duration();
return isFinite(duration) && duration > 0 &&
isFinite(state.videoPlayer.startTime);
}, 'duration is set', WAIT_TIMEOUT);
runs(function () {
expect($('.video-controls').find('.vidtime'))
.toHaveText('0:10 / 1:00');
expect(state.videoProgressSlider.slider.slider('option', 'value')).toBe(10);
state.storage.clear();
});
});
it('saved position is greater than end-time, timer slider and VCR set to start-time', function () {
var duration;
runs(function () {
state = jasmine.initializePlayer({
start: 10,
end: 20,
savedVideoPosition: 10000
});
spyOn(state.videoPlayer, 'duration').andReturn(60);
});
waitsFor(function () {
duration = state.videoPlayer.duration();
return isFinite(duration) && duration > 0 &&
isFinite(state.videoPlayer.startTime);
}, 'duration is set', WAIT_TIMEOUT);
runs(function () {
expect($('.video-controls').find('.vidtime'))
.toHaveText('0:10 / 1:00');
expect(state.videoProgressSlider.slider.slider('option', 'value')).toBe(10);
state.storage.clear();
});
});
});
describe('play', function () {
beforeEach(function () {
state = jasmine.initializePlayer();

View File

@@ -538,10 +538,8 @@ function (VideoPlayer) {
describe('updatePlayTime', function () {
beforeEach(function () {
state = jasmine.initializePlayer();
state = jasmine.initializePlayerYouTube();
state.videoEl = $('video, iframe');
spyOn(state.videoCaption, 'updatePlayTime').andCallThrough();
spyOn(state.videoProgressSlider, 'updatePlayTime').andCallThrough();
});
@@ -560,27 +558,10 @@ function (VideoPlayer) {
}, 'Video is fully loaded.', WAIT_TIMEOUT);
runs(function () {
var htmlStr;
state.videoPlayer.goToStartTime = false;
state.videoPlayer.updatePlayTime(60);
htmlStr = $('.vidtime').html();
// We resort to this trickery because Firefox and Chrome
// round the total time a bit differently.
if (
htmlStr.match('1:00 / 1:01') ||
htmlStr.match('1:00 / 1:00')
) {
expect(true).toBe(true);
} else {
expect(true).toBe(false);
}
// The below test has been replaced by above trickery:
//
// expect($('.vidtime')).toHaveHtml('1:00 / 1:01');
expect($('.vidtime')).toHaveHtml('1:00 / 1:00');
});
});
@@ -691,7 +672,9 @@ function (VideoPlayer) {
endTime: undefined,
player: {
seekTo: function () {}
}
},
figureOutStartEndTime: jasmine.createSpy(),
figureOutStartingTime: jasmine.createSpy().andReturn(0)
},
config: {
savedVideoPosition: 0,
@@ -712,6 +695,11 @@ function (VideoPlayer) {
it('invalid endTime is reset to null', function () {
VideoPlayer.prototype.updatePlayTime.call(state, 0);
expect(state.videoPlayer.figureOutStartingTime).toHaveBeenCalled();
VideoPlayer.prototype.figureOutStartEndTime.call(state, 60);
VideoPlayer.prototype.figureOutStartingTime.call(state, 60);
expect(state.videoPlayer.endTime).toBe(null);
});
});

View File

@@ -42,7 +42,7 @@ class @HTMLEditingDescriptor
# Disable visual aid on borderless table.
visual:false,
# We may want to add "styleselect" when we collect all styles used throughout the LMS
theme_advanced_buttons1 : "formatselect,fontselect,bold,italic,underline,forecolor,|,bullist,numlist,outdent,indent,|,blockquote,wrapAsCode,|,link,unlink,|,image,",
theme_advanced_buttons1 : "formatselect,fontselect,bold,italic,underline,forecolor,|,bullist,numlist,outdent,indent,|,link,unlink,image,|,blockquote,wrapAsCode",
theme_advanced_toolbar_location : "top",
theme_advanced_toolbar_align : "left",
theme_advanced_statusbar_location : "none",
@@ -80,6 +80,15 @@ class @HTMLEditingDescriptor
)
@visualEditor = ed
ed.onExecCommand.add(@onExecCommandHandler)
# Intended to run after the "image" plugin is used so that static urls are set
# correctly in the Visual editor immediately after command use.
onExecCommandHandler: (ed, cmd, ui, val) =>
if cmd == 'mceInsertContent' and val.match(/^<img/)
content = rewriteStaticLinks(ed.getContent(), '/static/', @base_asset_url)
ed.setContent(content)
onSwitchEditor: (e) =>
e.preventDefault();
@@ -114,7 +123,7 @@ class @HTMLEditingDescriptor
# both the startContent must be sync'ed up and the dirty flag set to false.
content = rewriteStaticLinks(@advanced_editor.getValue(), '/static/', @base_asset_url)
visualEditor.setContent(content)
visualEditor.startContent = content
visualEditor.startContent = visualEditor.getContent({format : 'raw'})
@focusVisualEditor(visualEditor)
@showingVisualEditor = true
@@ -124,8 +133,6 @@ class @HTMLEditingDescriptor
focusVisualEditor: (visualEditor) =>
visualEditor.focus()
# Need to mark editor as not dirty both when it is initially created and when we switch back to it.
visualEditor.isNotDirty = true
if not @$mceToolbar?
@$mceToolbar = $(@element).find('table.mceToolbar')

View File

@@ -35,6 +35,8 @@ function (HTML5Video, Resizer) {
play: play,
setPlaybackRate: setPlaybackRate,
update: update,
figureOutStartEndTime: figureOutStartEndTime,
figureOutStartingTime: figureOutStartingTime,
updatePlayTime: updatePlayTime
};
@@ -62,7 +64,7 @@ function (HTML5Video, Resizer) {
// via the 'state' object. Much easier to work this way - you don't
// have to do repeated jQuery element selects.
function _initialize(state) {
var youTubeId, player, duration;
var youTubeId, player;
// The function is called just once to apply pre-defined configurations
// by student before video starts playing. Waits until the video's
@@ -134,22 +136,7 @@ function (HTML5Video, Resizer) {
_resize(state, videoWidth, videoHeight);
duration = state.videoPlayer.duration();
state.trigger(
'videoControl.updateVcrVidTime',
{
time: 0,
duration: duration
}
);
state.trigger(
'videoProgressSlider.updateStartEndTimeRegion',
{
duration: duration
}
);
_updateVcrAndRegion(state);
}, false);
} else { // if (state.videoType === 'youtube') {
@@ -200,22 +187,34 @@ function (HTML5Video, Resizer) {
}
function _updateVcrAndRegion(state) {
var duration = state.videoPlayer.duration();
var duration = state.videoPlayer.duration(),
time;
time = state.videoPlayer.figureOutStartingTime(duration);
// Update the VCR.
state.trigger(
'videoControl.updateVcrVidTime',
{
time: 0,
time: time,
duration: duration
}
);
// Update the time slider.
state.trigger(
'videoProgressSlider.updateStartEndTimeRegion',
{
duration: duration
}
);
state.trigger(
'videoProgressSlider.updatePlayTime',
{
time: time,
duration: duration
}
);
}
function _resize(state, videoWidth, videoHeight) {
@@ -642,62 +641,46 @@ function (HTML5Video, Resizer) {
}
}
function updatePlayTime(time) {
var videoPlayer = this.videoPlayer,
duration = this.videoPlayer.duration(),
savedVideoPosition = this.config.savedVideoPosition,
youTubeId, startTime, endTime;
function figureOutStartEndTime(duration) {
var videoPlayer = this.videoPlayer;
if (duration > 0 && videoPlayer.goToStartTime) {
videoPlayer.goToStartTime = false;
videoPlayer.startTime = this.config.startTime;
if (videoPlayer.startTime >= duration) {
videoPlayer.startTime = 0;
} else if (this.currentPlayerMode === 'flash') {
videoPlayer.startTime /= Number(this.speed);
}
videoPlayer.startTime = this.config.startTime;
if (videoPlayer.startTime >= duration) {
videoPlayer.startTime = 0;
} else if (this.currentPlayerMode === 'flash') {
videoPlayer.startTime /= Number(this.speed);
}
videoPlayer.endTime = this.config.endTime;
if (
videoPlayer.endTime <= videoPlayer.startTime ||
videoPlayer.endTime >= duration
) {
videoPlayer.stopAtEndTime = false;
videoPlayer.endTime = null;
} else if (this.currentPlayerMode === 'flash') {
videoPlayer.endTime /= Number(this.speed);
}
}
videoPlayer.endTime = this.config.endTime;
function figureOutStartingTime(duration) {
var savedVideoPosition = this.config.savedVideoPosition,
// Default starting time is 0. This is the case when
// there is not start-time, no previously saved position,
// or one (or both) of those values is incorrect.
time = 0,
startTime, endTime;
this.videoPlayer.figureOutStartEndTime(duration);
startTime = this.videoPlayer.startTime;
endTime = this.videoPlayer.endTime;
if (startTime > 0) {
if (
videoPlayer.endTime <= videoPlayer.startTime ||
videoPlayer.endTime >= duration
) {
videoPlayer.stopAtEndTime = false;
videoPlayer.endTime = null;
} else if (this.currentPlayerMode === 'flash') {
videoPlayer.endTime /= Number(this.speed);
}
this.trigger(
'videoProgressSlider.updateStartEndTimeRegion',
{
duration: duration
}
);
startTime = videoPlayer.startTime;
endTime = videoPlayer.endTime;
if (startTime) {
if (
startTime < savedVideoPosition &&
(endTime > savedVideoPosition || endTime === null) &&
// We do not want to jump to the end of the video.
// We subtract 1 from the duration for a 1 second
// safety net.
savedVideoPosition < duration - 1
) {
time = savedVideoPosition;
// When the video finishes playing, we will start from the
// start-time, rather than from the remembered position
this.config.savedVideoPosition = 0;
} else {
time = startTime;
}
} else if (
startTime < savedVideoPosition &&
(endTime > savedVideoPosition || endTime === null) &&
// We do not want to jump to the end of the video.
@@ -706,13 +689,47 @@ function (HTML5Video, Resizer) {
savedVideoPosition < duration - 1
) {
time = savedVideoPosition;
// When the video finishes playing, we will start from the
// start-time, rather than from the remembered position
this.config.savedVideoPosition = 0;
} else {
time = 0;
time = startTime;
}
} else if (
savedVideoPosition > 0 &&
(endTime > savedVideoPosition || endTime === null) &&
// We do not want to jump to the end of the video.
// We subtract 1 from the duration for a 1 second
// safety net.
savedVideoPosition < duration - 1
) {
time = savedVideoPosition;
}
return time;
}
function updatePlayTime(time) {
var videoPlayer = this.videoPlayer,
duration = this.videoPlayer.duration(),
youTubeId;
if (duration > 0 && videoPlayer.goToStartTime) {
videoPlayer.goToStartTime = false;
// The duration might have changed. Update the start-end time region to
// reflect this fact.
this.trigger(
'videoProgressSlider.updateStartEndTimeRegion',
{
duration: duration
}
);
time = videoPlayer.figureOutStartingTime(duration);
// When the video finishes playing, we will start from the
// start-time, or from the beginning (rather than from the remembered
// position).
this.config.savedVideoPosition = 0;
if (time > 0) {
// After a bug came up (BLD-708: "In Firefox YouTube video with

View File

@@ -7,11 +7,15 @@ import logging
import re
from collections import namedtuple
import collections
from abc import ABCMeta, abstractmethod
from xblock.plugin import default_select
from .exceptions import InvalidLocationError, InsufficientSpecificationError
from xmodule.errortracker import make_error_tracker
from xblock.runtime import Mixologist
from xblock.core import XBlock
log = logging.getLogger('edx.modulestore')
@@ -447,7 +451,7 @@ class ModuleStoreWrite(ModuleStoreRead):
pass
@abstractmethod
def delete_item(self, location, user_id=None, delete_all_versions=False, delete_children=False, force=False):
def delete_item(self, location, user_id=None, **kwargs):
"""
Delete an item from persistence. Pass the user's unique id which the persistent store
should save with the update if it has that ability.
@@ -475,7 +479,9 @@ class ModuleStoreReadBase(ModuleStoreRead):
metadata_inheritance_cache_subsystem=None, request_cache=None,
modulestore_update_signal=None, xblock_mixins=(), xblock_select=None,
# temporary parms to enable backward compatibility. remove once all envs migrated
db=None, collection=None, host=None, port=None, tz_aware=True, user=None, password=None
db=None, collection=None, host=None, port=None, tz_aware=True, user=None, password=None,
# allow lower level init args to pass harmlessly
** kwargs
):
'''
Set up the error-tracking logic.
@@ -529,9 +535,75 @@ class ModuleStoreReadBase(ModuleStoreRead):
return c
return None
def update_item(self, xblock, user_id=None, allow_not_found=False, force=False):
"""
Update the given xblock's persisted repr. Pass the user's unique id which the persistent store
should save with the update if it has that ability.
:param allow_not_found: whether this method should raise an exception if the given xblock
has not been persisted before.
:param force: fork the structure and don't update the course draftVersion if there's a version
conflict (only applicable to version tracking and conflict detecting persistence stores)
:raises VersionConflictError: if package_id and version_guid given and the current
version head != version_guid and force is not True. (only applicable to version tracking stores)
"""
raise NotImplementedError
def delete_item(self, location, user_id=None, delete_all_versions=False, delete_children=False, force=False):
"""
Delete an item from persistence. Pass the user's unique id which the persistent store
should save with the update if it has that ability.
:param delete_all_versions: removes both the draft and published version of this item from
the course if using draft and old mongo. Split may or may not implement this.
:param force: fork the structure and don't update the course draftVersion if there's a version
conflict (only applicable to version tracking and conflict detecting persistence stores)
:raises VersionConflictError: if package_id and version_guid given and the current
version head != version_guid and force is not True. (only applicable to version tracking stores)
"""
raise NotImplementedError
class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
'''
Implement interface functionality that can be shared.
'''
pass
def __init__(self, **kwargs):
super(ModuleStoreWriteBase, self).__init__(**kwargs)
# TODO: Don't have a runtime just to generate the appropriate mixin classes (cpennington)
# This is only used by partition_fields_by_scope, which is only needed because
# the split mongo store is used for item creation as well as item persistence
self.mixologist = Mixologist(self.xblock_mixins)
def partition_fields_by_scope(self, category, fields):
"""
Return dictionary of {scope: {field1: val, ..}..} for the fields of this potential xblock
:param category: the xblock category
:param fields: the dictionary of {fieldname: value}
"""
if fields is None:
return {}
cls = self.mixologist.mix(XBlock.load_class(category, select=prefer_xmodules))
result = collections.defaultdict(dict)
for field_name, value in fields.iteritems():
field = getattr(cls, field_name)
result[field.scope][field_name] = value
return result
def only_xmodules(identifier, entry_points):
"""Only use entry_points that are supplied by the xmodule package"""
from_xmodule = [entry_point for entry_point in entry_points if entry_point.dist.key == 'xmodule']
return default_select(identifier, from_xmodule)
def prefer_xmodules(identifier, entry_points):
"""Prefer entry_points from the xmodule package"""
from_xmodule = [entry_point for entry_point in entry_points if entry_point.dist.key == 'xmodule']
if from_xmodule:
return default_select(identifier, from_xmodule)
else:
return default_select(identifier, entry_points)

View File

@@ -119,7 +119,8 @@ class LocMapperStore(object):
return package_id
def translate_location(self, old_style_course_id, location, published=True, add_entry_if_missing=True):
def translate_location(self, old_style_course_id, location, published=True,
add_entry_if_missing=True, passed_block_id=None):
"""
Translate the given module location to a Locator. If the mapping has the run id in it, then you
should provide old_style_course_id with that run id in it to disambiguate the mapping if there exists more
@@ -137,6 +138,8 @@ class LocMapperStore(object):
:param add_entry_if_missing: a boolean as to whether to raise ItemNotFoundError or to create an entry if
the course
or block is not found in the map.
:param passed_block_id: what block_id to assign and save if none is found
(only if add_entry_if_missing)
NOTE: unlike old mongo, draft branches contain the whole course; so, it applies to all category
of locations including course.
@@ -158,7 +161,7 @@ class LocMapperStore(object):
self.create_map_entry(course_location)
entry = self.location_map.find_one(location_id)
else:
raise ItemNotFoundError()
raise ItemNotFoundError(location)
elif len(maps) == 1:
entry = maps[0]
else:
@@ -172,7 +175,9 @@ class LocMapperStore(object):
block_id = entry['block_map'].get(self.encode_key_for_mongo(location.name))
if block_id is None:
if add_entry_if_missing:
block_id = self._add_to_block_map(location, location_id, entry['block_map'])
block_id = self._add_to_block_map(
location, location_id, entry['block_map'], passed_block_id
)
else:
raise ItemNotFoundError(location)
elif isinstance(block_id, dict):
@@ -188,7 +193,7 @@ class LocMapperStore(object):
elif add_entry_if_missing:
block_id = self._add_to_block_map(location, location_id, entry['block_map'])
else:
raise ItemNotFoundError()
raise ItemNotFoundError(location)
else:
raise InvalidLocationError()
@@ -297,7 +302,7 @@ class LocMapperStore(object):
maps = self.location_map.find(location_id)
maps = list(maps)
if len(maps) == 0:
raise ItemNotFoundError()
raise ItemNotFoundError(location)
elif len(maps) == 1:
entry = maps[0]
else:
@@ -315,18 +320,19 @@ class LocMapperStore(object):
else:
return draft_course_locator
def _add_to_block_map(self, location, location_id, block_map):
def _add_to_block_map(self, location, location_id, block_map, block_id=None):
'''add the given location to the block_map and persist it'''
if self._block_id_is_guid(location.name):
# This makes the ids more meaningful with a small probability of name collision.
# The downside is that if there's more than one course mapped to from the same org/course root
# the block ids will likely be out of sync and collide from an id perspective. HOWEVER,
# if there are few == org/course roots or their content is unrelated, this will work well.
block_id = self._verify_uniqueness(location.category + location.name[:3], block_map)
else:
# if 2 different category locations had same name, then they'll collide. Make the later
# mapped ones unique
block_id = self._verify_uniqueness(location.name, block_map)
if block_id is None:
if self._block_id_is_guid(location.name):
# This makes the ids more meaningful with a small probability of name collision.
# The downside is that if there's more than one course mapped to from the same org/course root
# the block ids will likely be out of sync and collide from an id perspective. HOWEVER,
# if there are few == org/course roots or their content is unrelated, this will work well.
block_id = self._verify_uniqueness(location.category + location.name[:3], block_map)
else:
# if 2 different category locations had same name, then they'll collide. Make the later
# mapped ones unique
block_id = self._verify_uniqueness(location.name, block_map)
encoded_location_name = self.encode_key_for_mongo(location.name)
block_map.setdefault(encoded_location_name, {})[location.category] = block_id
self.location_map.update(location_id, {'$set': {'block_map': block_map}})

View File

@@ -51,6 +51,12 @@ class Locator(object):
def __eq__(self, other):
return self.__dict__ == other.__dict__
def __hash__(self):
"""
Hash on contents.
"""
return hash(unicode(self))
def __repr__(self):
'''
repr(self) returns something like this: CourseLocator("mit.eecs.6002x")
@@ -198,16 +204,14 @@ class CourseLocator(Locator):
"""
Return a string representing this location.
"""
parts = []
if self.package_id:
result = unicode(self.package_id)
parts.append(unicode(self.package_id))
if self.branch:
result += '/' + BRANCH_PREFIX + self.branch
return result
elif self.version_guid:
return u"{prefix}{guid}".format(prefix=VERSION_PREFIX, guid=self.version_guid)
else:
# raise InsufficientSpecificationError("missing package_id or version_guid")
return '<InsufficientSpecificationError: missing package_id or version_guid>'
parts.append(u"{prefix}{branch}".format(prefix=BRANCH_PREFIX, branch=self.branch))
if self.version_guid:
parts.append(u"{prefix}{guid}".format(prefix=VERSION_PREFIX, guid=self.version_guid))
return u"/".join(parts)
def url(self):
"""
@@ -432,22 +436,25 @@ class BlockUsageLocator(CourseLocator):
def version_agnostic(self):
"""
Returns a copy of itself.
If both version_guid and package_id are known, use a blank package_id in the copy.
We don't care if the locator's version is not the current head; so, avoid version conflict
by reducing info.
Returns a copy of itself without any version info.
:param block_locator:
:raises: ValueError if the block locator has no package_id
"""
if self.version_guid:
return BlockUsageLocator(version_guid=self.version_guid,
branch=self.branch,
block_id=self.block_id)
else:
return BlockUsageLocator(package_id=self.package_id,
branch=self.branch,
block_id=self.block_id)
return BlockUsageLocator(package_id=self.package_id,
branch=self.branch,
block_id=self.block_id)
def course_agnostic(self):
"""
We only care about the locator's version not its course.
Returns a copy of itself without any course info.
:raises: ValueError if the block locator has no version_guid
"""
return BlockUsageLocator(version_guid=self.version_guid,
block_id=self.block_id)
def set_block_id(self, new):
"""

View File

@@ -5,47 +5,42 @@ In this way, courses can be served up both - say - XMLModuleStore or MongoModule
"""
import logging
from . import ModuleStoreWriteBase
from xmodule.modulestore.django import create_modulestore_instance, loc_mapper
import logging
from xmodule.modulestore import Location
from xblock.fields import Reference, ReferenceList, String
from xmodule.modulestore.locator import CourseLocator, Locator, BlockUsageLocator
from xmodule.modulestore.exceptions import InsufficientSpecificationError, ItemNotFoundError
from xmodule.modulestore.parsers import ALLOWED_ID_CHARS
import re
from xmodule.modulestore import Location, SPLIT_MONGO_MODULESTORE_TYPE, XML_MODULESTORE_TYPE
from xmodule.modulestore.locator import CourseLocator, Locator
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from uuid import uuid4
from xmodule.modulestore.mongo.base import MongoModuleStore
from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore
from xmodule.exceptions import UndefinedContext
log = logging.getLogger(__name__)
class MixedModuleStore(ModuleStoreWriteBase):
"""
ModuleStore knows how to route requests to the right persistence ms and how to convert any
references in the xblocks to the type required by the app and the persistence layer.
ModuleStore knows how to route requests to the right persistence ms
"""
def __init__(self, mappings, stores, reference_type=None, i18n_service=None, **kwargs):
def __init__(self, mappings, stores, i18n_service=None, **kwargs):
"""
Initialize a MixedModuleStore. Here we look into our passed in kwargs which should be a
collection of other modulestore configuration informations
:param reference_type: either Location or Locator to indicate what type of reference this app
uses.
"""
super(MixedModuleStore, self).__init__(**kwargs)
self.modulestores = {}
self.mappings = mappings
# temporary code for transition period
if reference_type is None:
log.warn("reference_type not specified in MixedModuleStore settings. %s",
"Will default temporarily to the to-be-deprecated Location.")
self.use_locations = (reference_type != 'Locator')
if 'default' not in stores:
raise Exception('Missing a default modulestore in the MixedModuleStore __init__ method.')
for key, store in stores.items():
for key, store in stores.iteritems():
is_xml = 'XMLModuleStore' in store['ENGINE']
if is_xml:
# restrict xml to only load courses in mapping
store['OPTIONS']['course_ids'] = [
course_id
for course_id, store_key in self.mappings.iteritems()
@@ -58,137 +53,22 @@ class MixedModuleStore(ModuleStoreWriteBase):
store['OPTIONS'],
i18n_service=i18n_service,
)
# If and when locations can identify their course, we won't need
# these loc maps. They're needed for figuring out which store owns these locations.
if is_xml:
self.ensure_loc_maps_exist(key)
def _get_modulestore_for_courseid(self, course_id):
"""
For a given course_id, look in the mapping table and see if it has been pinned
to a particular modulestore
"""
# TODO when this becomes a router capable of handling more than one r/w backend
# we'll need to generalize this to handle mappings from old Locations w/o full
# course_id in much the same way as loc_mapper().translate_location does.
mapping = self.mappings.get(course_id, 'default')
return self.modulestores[mapping]
def _locator_to_location(self, reference):
"""
Convert the referenced locator to a location casting to and from a string as necessary
"""
stringify = isinstance(reference, basestring)
if stringify:
reference = BlockUsageLocator(url=reference)
location = loc_mapper().translate_locator_to_location(reference)
return location.url() if stringify else location
def _location_to_locator(self, course_id, reference):
"""
Convert the referenced location to a locator casting to and from a string as necessary
"""
stringify = isinstance(reference, basestring)
if stringify:
reference = Location(reference)
locator = loc_mapper().translate_location(course_id, reference, reference.revision == 'draft', True)
return unicode(locator) if stringify else locator
def _incoming_reference_adaptor(self, store, course_id, reference):
"""
Convert the reference to the type the persistence layer wants
"""
if issubclass(store.reference_type, Location if self.use_locations else Locator):
return reference
if store.reference_type == Location:
return self._locator_to_location(reference)
return self._location_to_locator(course_id, reference)
def _outgoing_reference_adaptor(self, store, course_id, reference):
"""
Convert the reference to the type the application wants
"""
if issubclass(store.reference_type, Location if self.use_locations else Locator):
return reference
if store.reference_type == Location:
return self._location_to_locator(course_id, reference)
return self._locator_to_location(reference)
def _xblock_adaptor_iterator(self, adaptor, string_converter, store, course_id, xblock):
"""
Change all reference fields in this xblock to the type expected by the receiving layer
"""
for field in xblock.fields.itervalues():
if field.is_set_on(xblock):
if isinstance(field, Reference):
field.write_to(
xblock,
adaptor(store, course_id, field.read_from(xblock))
)
elif isinstance(field, ReferenceList):
field.write_to(
xblock,
[
adaptor(store, course_id, ref)
for ref in field.read_from(xblock)
]
)
elif isinstance(field, String):
# replace links within the string
string_converter(field, xblock)
return xblock
def _incoming_xblock_adaptor(self, store, course_id, xblock):
"""
Change all reference fields in this xblock to the type expected by the persistence layer
"""
string_converter = self._get_string_converter(
course_id, store.reference_type, xblock.location
)
return self._xblock_adaptor_iterator(
self._incoming_reference_adaptor, string_converter, store, course_id, xblock
)
def _outgoing_xblock_adaptor(self, store, course_id, xblock):
"""
Change all reference fields in this xblock to the type expected by the persistence layer
"""
string_converter = self._get_string_converter(
course_id, xblock.location.__class__, xblock.location
)
return self._xblock_adaptor_iterator(
self._outgoing_reference_adaptor, string_converter, store, course_id, xblock
)
CONVERT_RE = re.compile(r"/jump_to_id/({}+)".format(ALLOWED_ID_CHARS))
def _get_string_converter(self, course_id, reference_type, from_base_addr):
"""
Return a closure which finds and replaces all embedded links in a string field
with the correct rewritten link for the target type
"""
if self.use_locations and reference_type == Location:
return lambda field, xblock: None
if not self.use_locations and issubclass(reference_type, Locator):
return lambda field, xblock: None
if isinstance(from_base_addr, Location):
def mapper(found_id):
"""
Convert the found id to BlockUsageLocator block_id
"""
location = from_base_addr.replace(category=None, name=found_id)
# NOTE without category, it cannot create a new mapping if there's not one already
return loc_mapper().translate_location(course_id, location).block_id
else:
def mapper(found_id):
"""
Convert the found id to Location block_id
"""
locator = BlockUsageLocator.make_relative(from_base_addr, found_id)
return loc_mapper().translate_locator_to_location(locator).name
def converter(field, xblock):
"""
Find all of the ids in the block and replace them w/ their mapped values
"""
value = field.read_from(xblock)
self.CONVERT_RE.sub(mapper, value)
field.write_to(xblock, value)
return converter
def has_item(self, course_id, reference):
"""
Does the course include the xblock who's id is reference?
@@ -197,21 +77,19 @@ class MixedModuleStore(ModuleStoreWriteBase):
:param reference: a Location or BlockUsageLocator
"""
store = self._get_modulestore_for_courseid(course_id)
decoded_ref = self._incoming_reference_adaptor(store, course_id, reference)
return store.has_item(course_id, decoded_ref)
return store.has_item(course_id, reference)
def get_item(self, location, depth=0):
"""
This method is explicitly not implemented as we need a course_id to disambiguate
We should be able to fix this when the data-model rearchitecting is done
"""
# Although we shouldn't have both get_item and get_instance imho
raise NotImplementedError
def get_instance(self, course_id, location, depth=0):
store = self._get_modulestore_for_courseid(course_id)
decoded_ref = self._incoming_reference_adaptor(store, course_id, location)
xblock = store.get_instance(course_id, decoded_ref, depth)
return self._outgoing_xblock_adaptor(store, course_id, xblock)
return store.get_instance(course_id, location, depth)
def get_items(self, location, course_id=None, depth=0, qualifiers=None):
"""
@@ -224,70 +102,55 @@ class MixedModuleStore(ModuleStoreWriteBase):
a Locator with at least a package_id and branch but possibly no block_id.
depth: An argument that some module stores may use to prefetch
descendents of the queried modules for more efficient results later
descendants of the queried modules for more efficient results later
in the request. The depth is counted in the number of calls to
get_children() to cache. None indicates to cache all descendents
get_children() to cache. None indicates to cache all descendants
"""
if not (course_id or hasattr(location, 'package_id')):
raise Exception("Must pass in a course_id when calling get_items()")
store = self._get_modulestore_for_courseid(course_id or getattr(location, 'package_id'))
# translate won't work w/ missing fields so work around it
if store.reference_type == Location:
if not self.use_locations:
if getattr(location, 'block_id', False):
location = self._incoming_reference_adaptor(store, course_id, location)
else:
# get the course's location
location = loc_mapper().translate_locator_to_location(location, get_course=True)
# now remove the unknowns
location = location.replace(
category=qualifiers.get('category', None),
name=None
)
else:
if self.use_locations:
if not isinstance(location, Location):
location = Location(location)
try:
location.ensure_fully_specified()
location = loc_mapper().translate_location(
course_id, location, location.revision == 'published', True
)
except InsufficientSpecificationError:
# construct the Locator by hand
if location.category is not None and qualifiers.get('category', False):
qualifiers['category'] = location.category
location = loc_mapper().translate_location_to_course_locator(
course_id, location, location.revision == 'published'
)
xblocks = store.get_items(location, course_id, depth, qualifiers)
xblocks = [self._outgoing_xblock_adaptor(store, course_id, xblock) for xblock in xblocks]
return xblocks
return store.get_items(location, course_id, depth, qualifiers)
def _get_course_id_from_course_location(self, course_location):
"""
Get the proper course_id based on the type of course_location
"""
return getattr(course_location, 'course_id', None) or getattr(course_location, 'package_id', None)
def get_courses(self):
'''
Returns a list containing the top level XModuleDescriptors of the courses
in this modulestore.
'''
courses = []
for key in self.modulestores:
store_courses = self.modulestores[key].get_courses()
# If the store has not been labeled as 'default' then we should
# only surface courses that have a mapping entry, for example the XMLModuleStore will
# slurp up anything that is on disk, however, we don't want to surface those to
# consumers *unless* there is an explicit mapping in the configuration
if key != 'default':
for course in store_courses:
# make sure that the courseId is mapped to the store in question
if key == self.mappings.get(course.location.course_id, 'default'):
courses = courses + ([course])
else:
# if we're the 'default' store provider, then we surface all courses hosted in
# that store provider
courses = courses + (store_courses)
# order the modulestores and ensure no dupes (default may be a dupe of a named store)
# remove 'draft' as we know it's a functional dupe of 'direct' (ugly hardcoding)
stores = set([value for key, value in self.modulestores.iteritems() if key != 'draft'])
stores = sorted(stores, cmp=_compare_stores)
return courses
courses = {} # a dictionary of stringified course locations to course objects
has_locators = any(issubclass(CourseLocator, store.reference_type) for store in stores)
for store in stores:
store_courses = store.get_courses()
# filter out ones which were fetched from earlier stores but locations may not be ==
for course in store_courses:
course_location = unicode(course.location)
if course_location not in courses:
if has_locators and isinstance(course.location, Location):
# see if a locator version of course is in the result
try:
# if there's no existing mapping, then the course can't have been in split
course_locator = loc_mapper().translate_location(
course.location.course_id, course.location, add_entry_if_missing=False
)
if unicode(course_locator) not in courses:
courses[course_location] = course
except ItemNotFoundError:
courses[course_location] = course
else:
courses[course_location] = course
return courses.values()
def get_course(self, course_id):
"""
@@ -297,44 +160,19 @@ class MixedModuleStore(ModuleStoreWriteBase):
:param course_id: must be either a string course_id or a CourseLocator
"""
store = self._get_modulestore_for_courseid(
course_id.package_id if hasattr(course_id, 'package_id') else course_id)
course_id.package_id if hasattr(course_id, 'package_id') else course_id
)
try:
# translate won't work w/ missing fields so work around it
if store.reference_type == Location:
# takes the course_id: figure out if this is old or new style
if not self.use_locations:
if isinstance(course_id, basestring):
course_id = CourseLocator(package_id=course_id, branch='published')
course_location = loc_mapper().translate_locator_to_location(course_id, get_course=True)
course_id = course_location.course_id
xblock = store.get_course(course_id)
else:
# takes a courseLocator
if isinstance(course_id, CourseLocator):
location = course_id
course_id = None # not an old style course_id; so, don't use it further
elif '/' in course_id:
location = loc_mapper().translate_location_to_course_locator(course_id, None, True)
else:
location = CourseLocator(package_id=course_id, branch='published')
course_id = None # not an old style course_id; so, don't use it further
xblock = store.get_course(location)
return store.get_course(course_id)
except ItemNotFoundError:
return None
if xblock is not None:
return self._outgoing_xblock_adaptor(store, course_id, xblock)
else:
return None
def get_parent_locations(self, location, course_id):
"""
returns the parent locations for a given location and course_id
"""
store = self._get_modulestore_for_courseid(course_id)
decoded_ref = self._incoming_reference_adaptor(store, course_id, location)
parents = store.get_parent_locations(decoded_ref, course_id)
return [self._outgoing_reference_adaptor(store, course_id, reference)
for reference in parents]
return store.get_parent_locations(location, course_id)
def get_modulestore_type(self, course_id):
"""
@@ -352,10 +190,9 @@ class MixedModuleStore(ModuleStoreWriteBase):
usually orphaned. NOTE: may include xblocks which still have references via xblocks which don't
use children to point to their dependents.
"""
course_id = getattr(course_location, 'course_id', getattr(course_location, 'package_id', None))
course_id = self._get_course_id_from_course_location(course_location)
store = self._get_modulestore_for_courseid(course_id)
decoded_ref = self._incoming_reference_adaptor(store, course_id, course_location)
return store.get_orphans(decoded_ref, branch)
return store.get_orphans(course_location, branch)
def get_errored_courses(self):
"""
@@ -367,31 +204,242 @@ class MixedModuleStore(ModuleStoreWriteBase):
errs.update(store.get_errored_courses())
return errs
def _get_course_id_from_block(self, block, store):
"""
Get the course_id from the block or from asking its store. Expensive.
"""
try:
return block.course_id
except UndefinedContext:
pass
try:
course = store._get_course_for_item(block.scope_ids.usage_id)
if course is not None:
return course.scope_ids.usage_id.course_id
except Exception: # sorry, that method just raises vanilla Exception if it doesn't find course
pass
def _infer_course_id_try(self, location):
"""
Create, Update, Delete operations don't require a fully-specified course_id, but
there's no complete & sound general way to compute the course_id except via the
proper modulestore. This method attempts several sound but not complete methods.
:param location: an old style Location
"""
if isinstance(location, CourseLocator):
return location.package_id
elif isinstance(location, basestring):
try:
location = Location(location)
except InvalidLocationError:
# try to parse as a course_id
try:
Location.parse_course_id(location)
# it's already a course_id
return location
except ValueError:
# cannot interpret the location
return None
# location is a Location at this point
if location.category == 'course': # easiest case
return location.course_id
# try finding in loc_mapper
try:
# see if the loc mapper knows the course id (requires double translation)
locator = loc_mapper().translate_location_to_course_locator(None, location)
location = loc_mapper().translate_locator_to_location(locator, get_course=True)
return location.course_id
except ItemNotFoundError:
pass
# expensive query against all location-based modulestores to look for location.
for store in self.modulestores.itervalues():
if isinstance(location, store.reference_type):
try:
xblock = store.get_item(location)
course_id = self._get_course_id_from_block(xblock, store)
if course_id is not None:
return course_id
except NotImplementedError:
blocks = store.get_items(location)
if len(blocks) == 1:
block = blocks[0]
try:
return block.course_id
except UndefinedContext:
pass
except ItemNotFoundError:
pass
# if we get here, it must be in a Locator based store, but we won't be able to find
# it.
return None
def create_course(self, course_id, user_id=None, store_name='default', **kwargs):
"""
Creates and returns the course.
:param org: the org
:param fields: a dict of xblock field name - value pairs for the course module.
:param metadata: the old way of setting fields by knowing which ones are scope.settings v scope.content
:param definition_data: the complement to metadata which is also a subset of fields
:param id_root: the split-mongo course_id starting value (see split.create_course)
:param pretty_id: a field split.create_course uses and may quit using
:returns: course xblock
"""
store = self.modulestores[store_name]
if not hasattr(store, 'create_course'):
raise NotImplementedError(u"Cannot create a course on store %s" % store_name)
if store.get_modulestore_type(course_id) == SPLIT_MONGO_MODULESTORE_TYPE:
id_root = kwargs.get('id_root')
try:
course_dict = Location.parse_course_id(course_id)
org = course_dict['org']
if id_root is None:
id_root = "{org}.{course}.{name}".format(**course_dict)
except ValueError:
org = None
if id_root is None:
id_root = course_id
org = kwargs.pop('org', org)
pretty_id = kwargs.pop('pretty_id', id_root)
fields = kwargs.pop('fields', {})
fields.update(kwargs.pop('metadata', {}))
fields.update(kwargs.pop('definition_data', {}))
course = store.create_course(org, pretty_id, user_id, id_root=id_root, fields=fields, **kwargs)
else: # assume mongo
course = store.create_course(course_id, **kwargs)
return course
def create_item(self, course_or_parent_loc, category, user_id=None, **kwargs):
"""
Create and return the item. If parent_loc is a specific location v a course id,
it installs the new item as a child of the parent (if the parent_loc is a specific
xblock reference).
:param course_or_parent_loc: Can be a course_id (org/course/run), CourseLocator,
Location, or BlockUsageLocator but must be what the persistence modulestore expects
"""
# find the store for the course
course_id = self._infer_course_id_try(course_or_parent_loc)
if course_id is None:
raise ItemNotFoundError(u"Cannot find modulestore for %s" % course_or_parent_loc)
store = self._get_modulestore_for_courseid(course_id)
location = kwargs.pop('location', None)
# invoke its create_item
if isinstance(store, MongoModuleStore):
block_id = kwargs.pop('block_id', getattr(location, 'name', uuid4().hex))
# convert parent loc if it's legit
if isinstance(course_or_parent_loc, basestring):
parent_loc = None
if location is None:
loc_dict = Location.parse_course_id(course_id)
loc_dict['name'] = block_id
location = Location(category=category, **loc_dict)
else:
parent_loc = course_or_parent_loc
# must have a legitimate location, compute if appropriate
if location is None:
location = parent_loc.replace(category=category, name=block_id)
# do the actual creation
xblock = store.create_and_save_xmodule(location, **kwargs)
# don't forget to attach to parent
if parent_loc is not None and not 'detached' in xblock._class_tags:
parent = store.get_item(parent_loc)
parent.children.append(location.url())
store.update_item(parent)
elif isinstance(store, SplitMongoModuleStore):
if isinstance(course_or_parent_loc, basestring): # course_id
course_or_parent_loc = loc_mapper().translate_location_to_course_locator(
# hardcode draft version until we figure out how we're handling branches from app
course_or_parent_loc, None, published=False
)
elif not isinstance(course_or_parent_loc, CourseLocator):
raise ValueError(u"Cannot create a child of {} in split. Wrong repr.".format(course_or_parent_loc))
# split handles all the fields in one dict not separated by scope
fields = kwargs.get('fields', {})
fields.update(kwargs.pop('metadata', {}))
fields.update(kwargs.pop('definition_data', {}))
kwargs['fields'] = fields
xblock = store.create_item(course_or_parent_loc, category, user_id, **kwargs)
else:
raise NotImplementedError(u"Cannot create an item on store %s" % store)
return xblock
def update_item(self, xblock, user_id, allow_not_found=False):
"""
Update the xblock persisted to be the same as the given for all types of fields
(content, children, and metadata) attribute the change to the given user.
"""
if self.use_locations:
raise NotImplementedError
locator = xblock.location
course_id = locator.package_id
course_id = self._infer_course_id_try(xblock.scope_ids.usage_id)
if course_id is None:
raise ItemNotFoundError(u"Cannot find modulestore for %s" % xblock.scope_ids.usage_id)
store = self._get_modulestore_for_courseid(course_id)
return store.update_item(xblock, user_id)
# if an xblock, convert its contents to correct addr scheme
xblock = self._incoming_xblock_adaptor(store, course_id, xblock)
xblock = store.update_item(xblock, user_id)
return self._outgoing_xblock_adaptor(store, course_id, xblock)
def delete_item(self, location, **kwargs):
def delete_item(self, location, user_id=None, **kwargs):
"""
Delete the given item from persistence.
Delete the given item from persistence. kwargs allow modulestore specific parameters.
"""
if self.use_locations:
raise NotImplementedError
course_id = self._infer_course_id_try(location)
if course_id is None:
raise ItemNotFoundError(u"Cannot find modulestore for %s" % location)
store = self._get_modulestore_for_courseid(course_id)
return store.delete_item(location, user_id=user_id, **kwargs)
store = self._get_modulestore_for_courseid(location.package_id)
decoded_ref = self._incoming_reference_adaptor(store, location.package_id, location)
return store.delete_item(decoded_ref, **kwargs)
def close_all_connections(self):
"""
Close all db connections
"""
for mstore in self.modulestores.itervalues():
if hasattr(mstore, 'database'):
mstore.database.connection.close()
elif hasattr(mstore, 'db'):
mstore.db.connection.close()
def ensure_loc_maps_exist(self, store_name):
"""
Ensure location maps exist for every course in the modulestore whose
name is the given name (mostly used for 'xml'). It creates maps for any
missing ones.
NOTE: will only work if the given store is Location based. If it's not,
it raises NotImplementedError
"""
store = self.modulestores[store_name]
if store.reference_type != Location:
raise ValueError(u"Cannot create maps from %s" % store.reference_type)
for course in store.get_courses():
loc_mapper().translate_location(course.location.course_id, course.location)
def _compare_stores(left, right):
"""
Order stores via precedence: if a course is found in an earlier store, it shadows the later store.
xml stores take precedence b/c they only contain hardcoded mappings, then Locator-based ones,
then others. Locators before Locations because if some courses may be in both,
the ones in the Locator-based stores shadow the others.
"""
if left.get_modulestore_type(None) == XML_MODULESTORE_TYPE:
if right.get_modulestore_type(None) == XML_MODULESTORE_TYPE:
return 0
else:
return -1
elif right.get_modulestore_type(None) == XML_MODULESTORE_TYPE:
return 1
if issubclass(left.reference_type, Locator):
if issubclass(right.reference_type, Locator):
return 0
else:
return -1
elif issubclass(right.reference_type, Locator):
return 1
return 0

View File

@@ -625,7 +625,22 @@ class MongoModuleStore(ModuleStoreWriteBase):
modules = self._load_items(list(items), depth)
return modules
def create_xmodule(self, location, definition_data=None, metadata=None, system=None):
def create_course(self, course_id, definition_data=None, metadata=None, runtime=None):
"""
Create a course with the given course_id.
"""
if isinstance(course_id, Location):
location = course_id
if location.category != 'course':
raise ValueError(u"Course roots must be of category 'course': {}".format(unicode(location)))
else:
course_dict = Location.parse_course_id(course_id)
course_dict['category'] = 'course'
course_dict['tag'] = 'i4x'
location = Location(course_dict)
return self.create_and_save_xmodule(location, definition_data, metadata, runtime)
def create_xmodule(self, location, definition_data=None, metadata=None, system=None, fields={}):
"""
Create the new xmodule but don't save it. Returns the new module.
@@ -672,36 +687,18 @@ class MongoModuleStore(ModuleStoreWriteBase):
ScopeIds(None, location.category, location, location),
dbmodel,
)
for key, value in fields.iteritems():
setattr(xmodule, key, value)
# decache any pending field settings from init
xmodule.save()
return xmodule
def save_xmodule(self, xmodule):
"""
Save the given xmodule (will either create or update based on whether id already exists).
Pulls out the data definition v metadata v children locally but saves it all.
:param xmodule:
"""
# Save any changes to the xmodule to the MongoKeyValueStore
xmodule.save()
self.collection.save({
'_id': namedtuple_to_son(xmodule.location),
'metadata': own_metadata(xmodule),
'definition': {
'data': xmodule.get_explicitly_set_fields_by_scope(Scope.content),
'children': xmodule.children if xmodule.has_children else []
}
})
# recompute (and update) the metadata inheritance tree which is cached
self.refresh_cached_metadata_inheritance_tree(xmodule.location)
self.fire_updated_modulestore_signal(get_course_id_no_run(xmodule.location), xmodule.location)
def create_and_save_xmodule(self, location, definition_data=None, metadata=None, system=None):
def create_and_save_xmodule(self, location, definition_data=None, metadata=None, system=None,
fields={}):
"""
Create the new xmodule and save it. Does not return the new module because if the caller
will insert it as a child, it's inherited metadata will completely change. The difference
between this and just doing create_xmodule and save_xmodule is this ensures static_tabs get
between this and just doing create_xmodule and update_item is this ensures static_tabs get
pointed to by the course.
:param location: a Location--must have a category
@@ -711,9 +708,9 @@ class MongoModuleStore(ModuleStoreWriteBase):
"""
# differs from split mongo in that I believe most of this logic should be above the persistence
# layer but added it here to enable quick conversion. I'll need to reconcile these.
new_object = self.create_xmodule(location, definition_data, metadata, system)
new_object = self.create_xmodule(location, definition_data, metadata, system, fields)
location = new_object.location
self.save_xmodule(new_object)
self.update_item(new_object, allow_not_found=True)
# VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so
# if we add one then we need to also add it to the policy information (i.e. metadata)
@@ -728,9 +725,9 @@ class MongoModuleStore(ModuleStoreWriteBase):
'url_slug': new_object.location.name
})
course.tabs = existing_tabs
# Save any changes to the course to the MongoKeyValueStore
course.save()
self.update_item(course, '**replace_user**')
self.update_item(course)
return new_object
def fire_updated_modulestore_signal(self, course_id, location):
"""
@@ -787,7 +784,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
if result['n'] == 0:
raise ItemNotFoundError(location)
def update_item(self, xblock, user, allow_not_found=False):
def update_item(self, xblock, user=None, allow_not_found=False):
"""
Update the persisted version of xblock to reflect its current values.
@@ -861,7 +858,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
location = Location.ensure_fully_specified(location)
items = self.collection.find({'definition.children': location.url()},
{'_id': True})
return [i['_id'] for i in items]
return [Location(i['_id']) for i in items]
def get_modulestore_type(self, course_id):
"""

View File

@@ -92,7 +92,7 @@ class DraftModuleStore(MongoModuleStore):
except ItemNotFoundError:
return wrap_draft(super(DraftModuleStore, self).get_instance(course_id, location, depth=depth))
def create_xmodule(self, location, definition_data=None, metadata=None, system=None):
def create_xmodule(self, location, definition_data=None, metadata=None, system=None, fields={}):
"""
Create the new xmodule but don't save it. Returns the new module with a draft locator
@@ -104,22 +104,7 @@ class DraftModuleStore(MongoModuleStore):
draft_loc = as_draft(location)
if draft_loc.category in DIRECT_ONLY_CATEGORIES:
raise InvalidVersionError(location)
return super(DraftModuleStore, self).create_xmodule(draft_loc, definition_data, metadata, system)
def save_xmodule(self, xmodule):
"""
Save the given xmodule (will either create or update based on whether id already exists).
Pulls out the data definition v metadata v children locally but saves it all.
:param xmodule:
"""
orig_location = xmodule.location
xmodule.location = as_draft(orig_location)
try:
super(DraftModuleStore, self).save_xmodule(xmodule)
finally:
xmodule.location = orig_location
return super(DraftModuleStore, self).create_xmodule(draft_loc, definition_data, metadata, system, fields)
def get_items(self, location, course_id=None, depth=0, qualifiers=None):
"""
@@ -159,7 +144,7 @@ class DraftModuleStore(MongoModuleStore):
if draft_location.category in DIRECT_ONLY_CATEGORIES:
raise InvalidVersionError(source_location)
if not original:
raise ItemNotFoundError
raise ItemNotFoundError(source_location)
original['_id'] = namedtuple_to_son(draft_location)
try:
self.collection.insert(original)
@@ -171,7 +156,7 @@ class DraftModuleStore(MongoModuleStore):
return self._load_items([original])[0]
def update_item(self, xblock, user, allow_not_found=False):
def update_item(self, xblock, user=None, allow_not_found=False):
"""
Save the current values to persisted version of the xblock
@@ -187,7 +172,7 @@ class DraftModuleStore(MongoModuleStore):
raise
xblock.location = draft_loc
super(DraftModuleStore, self).update_item(xblock, user)
super(DraftModuleStore, self).update_item(xblock, user, allow_not_found)
# don't allow locations to truly represent themselves as draft outside of this file
xblock.location = as_published(xblock.location)

View File

@@ -51,14 +51,13 @@ import logging
import re
from importlib import import_module
from path import path
import collections
import copy
from pytz import UTC
from xmodule.errortracker import null_error_tracker
from xmodule.x_module import prefer_xmodules
from xmodule.modulestore.locator import (
BlockUsageLocator, DefinitionLocator, CourseLocator, VersionTree, LocalId, Locator
BlockUsageLocator, DefinitionLocator, CourseLocator, VersionTree,
LocalId, Locator
)
from xmodule.modulestore.exceptions import InsufficientSpecificationError, VersionConflictError, DuplicateItemError
from xmodule.modulestore import inheritance, ModuleStoreWriteBase, Location, SPLIT_MONGO_MODULESTORE_TYPE
@@ -67,7 +66,6 @@ from ..exceptions import ItemNotFoundError
from .definition_lazy_loader import DefinitionLazyLoader
from .caching_descriptor_system import CachingDescriptorSystem
from xblock.fields import Scope
from xblock.runtime import Mixologist
from bson.objectid import ObjectId
from xmodule.modulestore.split_mongo.mongo_connection import MongoConnection
from xblock.core import XBlock
@@ -132,11 +130,6 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
self.render_template = render_template
self.i18n_service = i18n_service
# TODO: Don't have a runtime just to generate the appropriate mixin classes (cpennington)
# This is only used by _partition_fields_by_scope, which is only needed because
# the split mongo store is used for item creation as well as item persistence
self.mixologist = Mixologist(self.xblock_mixins)
def cache_items(self, system, base_block_ids, depth=0, lazy=True):
'''
Handles caching of items once inheritance and any other one time
@@ -281,7 +274,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
}
return envelope
def get_courses(self, branch='published', qualifiers=None):
def get_courses(self, branch='draft', qualifiers=None):
'''
Returns a list of course descriptors matching any given qualifiers.
@@ -291,7 +284,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
Note, this is to find the current head of the named branch type
(e.g., 'draft'). To get specific versions via guid use get_course.
:param branch: the branch for which to return courses. Default value is 'published'.
:param branch: the branch for which to return courses. Default value is 'draft'.
:param qualifiers: a optional dict restricting which elements should match
'''
if qualifiers is None:
@@ -563,8 +556,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
The block's history tracks its explicit changes but not the changes in its children.
'''
# version_agnostic means we don't care if the head and version don't align, trust the version
course_struct = self._lookup_course(block_locator.version_agnostic())['structure']
# course_agnostic means we don't care if the head and version don't align, trust the version
course_struct = self._lookup_course(block_locator.course_agnostic())['structure']
block_id = block_locator.block_id
update_version_field = 'blocks.{}.edit_info.update_version'.format(block_id)
all_versions_with_block = self.db_connection.find_matching_structures({'original_version': course_struct['original_version'],
@@ -759,7 +752,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
index_entry = self._get_index_if_valid(course_or_parent_locator, force, continue_version)
structure = self._lookup_course(course_or_parent_locator)['structure']
partitioned_fields = self._partition_fields_by_scope(category, fields)
partitioned_fields = self.partition_fields_by_scope(category, fields)
new_def_data = partitioned_fields.get(Scope.content, {})
# persist the definition if persisted != passed
if (definition_locator is None or isinstance(definition_locator.definition_id, LocalId)):
@@ -822,14 +815,19 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
if index_entry is not None:
if not continue_version:
self._update_head(index_entry, course_or_parent_locator.branch, new_id)
course_parent = course_or_parent_locator.as_course_locator()
item_loc = BlockUsageLocator(
package_id=course_or_parent_locator.package_id,
branch=course_or_parent_locator.branch,
block_id=new_block_id,
)
else:
course_parent = None
item_loc = BlockUsageLocator(
block_id=new_block_id,
version_guid=new_id,
)
# reconstruct the new_item from the cache
return self.get_item(BlockUsageLocator(package_id=course_parent,
block_id=new_block_id,
version_guid=new_id))
return self.get_item(item_loc)
def create_course(
self, org, prettyid, user_id, id_root=None, fields=None,
@@ -867,7 +865,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
provide any fields overrides, see above). if not provided, will create a mostly empty course
structure with just a category course root xblock.
"""
partitioned_fields = self._partition_fields_by_scope(root_category, fields)
partitioned_fields = self.partition_fields_by_scope(root_category, fields)
block_fields = partitioned_fields.setdefault(Scope.settings, {})
if Scope.children in partitioned_fields:
block_fields.update(partitioned_fields[Scope.children])
@@ -1287,7 +1285,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
if index is None:
raise ItemNotFoundError(package_id)
# this is the only real delete in the system. should it do something else?
log.info("deleting course from split-mongo: %s", package_id)
log.info(u"deleting course from split-mongo: %s", package_id)
self.db_connection.delete_course_index(index['_id'])
def get_errored_courses(self):
@@ -1494,22 +1492,6 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
index_entry['versions'][branch] = new_id
self.db_connection.update_course_index(index_entry)
def _partition_fields_by_scope(self, category, fields):
"""
Return dictionary of {scope: {field1: val, ..}..} for the fields of this potential xblock
:param category: the xblock category
:param fields: the dictionary of {fieldname: value}
"""
if fields is None:
return {}
cls = self.mixologist.mix(XBlock.load_class(category, select=self.xblock_select))
result = collections.defaultdict(dict)
for field_name, value in fields.iteritems():
field = getattr(cls, field_name)
result[field.scope][field_name] = value
return result
def _filter_special_fields(self, fields):
"""
Remove any fields which split or its kvs computes or adds but does not want persisted.

View File

@@ -33,7 +33,6 @@ def mixed_store_config(data_dir, mappings):
'ENGINE': 'xmodule.modulestore.mixed.MixedModuleStore',
'OPTIONS': {
'mappings': mappings,
'reference_type': 'Location',
'stores': {
'default': mongo_config['default'],
'xml': xml_config['default']
@@ -219,13 +218,21 @@ class ModuleStoreTestCase(TestCase):
# even if we're using a mixed modulestore
store = editable_modulestore()
if hasattr(store, 'collection'):
connection = store.collection.database.connection
store.collection.drop()
connection.close()
elif hasattr(store, 'close_all_connections'):
store.close_all_connections()
if contentstore().fs_files:
db = contentstore().fs_files.database
db.connection.drop_database(db)
db.connection.close()
location_mapper = loc_mapper()
if location_mapper.db:
location_mapper.location_map.drop()
location_mapper.db.connection.close()
@classmethod
def setUpClass(cls):

View File

@@ -2,8 +2,7 @@ from factory import Factory, lazy_attribute_sequence, lazy_attribute
from factory.containers import CyclicDefinitionError
from uuid import uuid4
from xmodule.modulestore import Location
from xmodule.x_module import prefer_xmodules
from xmodule.modulestore import Location, prefer_xmodules
from xblock.core import XBlock
@@ -58,7 +57,7 @@ class CourseFactory(XModuleFactory):
setattr(new_course, k, v)
# Update the data in the mongo datastore
store.save_xmodule(new_course)
store.update_item(new_course)
return new_course
@@ -159,7 +158,7 @@ class ItemFactory(XModuleFactory):
setattr(module, attr, val)
module.save()
store.save_xmodule(module)
store.update_item(module)
if 'detached' not in module._class_tags:
parent.children.append(location.url())

View File

@@ -1,13 +1,23 @@
from xmodule.modulestore.django import modulestore
from xmodule.course_module import CourseDescriptor
from xmodule.x_module import XModuleDescriptor
import factory
from factory.helpers import lazy_attribute
# [dhm] I'm not sure why we're using factory_boy if we're not following its pattern. If anyone
# assumes they can call build, it will completely fail, for example.
# pylint: disable=W0232
class PersistentCourseFactory(factory.Factory):
class SplitFactory(factory.Factory):
"""
Abstracted superclass which defines modulestore so that there's no dependency on django
if the caller passes modulestore in kwargs
"""
@lazy_attribute
def modulestore(self):
# Delayed import so that we only depend on django if the caller
# hasn't provided their own modulestore
from xmodule.modulestore.django import modulestore
return modulestore('split')
class PersistentCourseFactory(SplitFactory):
"""
Create a new course (not a new version of a course, but a whole new index entry).
@@ -23,12 +33,15 @@ class PersistentCourseFactory(factory.Factory):
# pylint: disable=W0613
@classmethod
def _create(cls, target_class, org='testX', prettyid='999', user_id='test_user', master_branch='draft', **kwargs):
def _create(cls, target_class, org='testX', prettyid='999', user_id='test_user',
master_branch='draft', id_root=None, **kwargs):
modulestore = kwargs.pop('modulestore')
root_block_id = kwargs.pop('root_block_id', 'course')
# Write the data to the mongo datastore
new_course = modulestore('split').create_course(
org, prettyid, user_id, fields=kwargs, id_root=prettyid,
master_branch=master_branch)
new_course = modulestore.create_course(
org, prettyid, user_id, fields=kwargs, id_root=id_root or prettyid,
master_branch=master_branch, root_block_id=root_block_id)
return new_course
@@ -37,7 +50,7 @@ class PersistentCourseFactory(factory.Factory):
raise NotImplementedError()
class ItemFactory(factory.Factory):
class ItemFactory(SplitFactory):
FACTORY_FOR = XModuleDescriptor
display_name = factory.LazyAttributeSequence(lambda o, n: "{} {}".format(o.category, n))
@@ -45,7 +58,8 @@ class ItemFactory(factory.Factory):
# pylint: disable=W0613
@classmethod
def _create(cls, target_class, parent_location, category='chapter',
user_id='test_user', definition_locator=None, **kwargs):
user_id='test_user', block_id=None, definition_locator=None, force=False,
continue_version=False, **kwargs):
"""
passes *kwargs* as the new item's field values:
@@ -55,8 +69,10 @@ class ItemFactory(factory.Factory):
:param definition_locator (optional): the DescriptorLocator for the definition this uses or branches
"""
return modulestore('split').create_item(
parent_location, category, user_id, definition_locator, fields=kwargs
modulestore = kwargs.pop('modulestore')
return modulestore.create_item(
parent_location, category, user_id, definition_locator=definition_locator,
block_id=block_id, force=force, continue_version=continue_version, fields=kwargs
)
@classmethod

View File

@@ -12,11 +12,11 @@ from xmodule.modulestore.loc_mapper_store import LocMapperStore
from mock import Mock
class TestLocationMapper(unittest.TestCase):
class LocMapperSetupSansDjango(unittest.TestCase):
"""
Test the location to locator mapper
Create and destroy a loc mapper for each test
"""
loc_store = None
def setUp(self):
modulestore_options = {
'host': 'localhost',
@@ -27,14 +27,19 @@ class TestLocationMapper(unittest.TestCase):
cache_standin = TrivialCache()
self.instrumented_cache = Mock(spec=cache_standin, wraps=cache_standin)
# pylint: disable=W0142
TestLocationMapper.loc_store = LocMapperStore(self.instrumented_cache, **modulestore_options)
LocMapperSetupSansDjango.loc_store = LocMapperStore(self.instrumented_cache, **modulestore_options)
def tearDown(self):
dbref = TestLocationMapper.loc_store.db
dbref.drop_collection(TestLocationMapper.loc_store.location_map)
dbref.connection.close()
TestLocationMapper.loc_store = None
self.loc_store = None
class TestLocationMapper(LocMapperSetupSansDjango):
"""
Test the location to locator mapper
"""
def test_create_map(self):
org = 'foo_org'
course = 'bar_course'
@@ -125,7 +130,7 @@ class TestLocationMapper(unittest.TestCase):
)
test_problem_locn = Location('i4x', org, course, 'problem', 'abc123')
# only one course matches
self.translate_n_check(test_problem_locn, old_style_course_id, new_style_package_id, 'problem2', 'published')
# look for w/ only the Location (works b/c there's only one possible course match). Will force
# cache as default translation for this problemid
self.translate_n_check(test_problem_locn, None, new_style_package_id, 'problem2', 'published')
@@ -389,7 +394,7 @@ def loc_mapper():
"""
Mocks the global location mapper.
"""
return TestLocationMapper.loc_store
return LocMapperSetupSansDjango.loc_store
def render_to_template_mock(*_args):

View File

@@ -200,13 +200,14 @@ class LocatorTest(TestCase):
expected_id = 'mit.eecs.6002x'
expected_branch = 'published'
expected_block_ref = 'HW3'
testobj = BlockUsageLocator(package_id=testurn)
testobj = BlockUsageLocator(url=testurn)
self.check_block_locn_fields(testobj, 'test_block constructor',
package_id=expected_id,
branch=expected_branch,
block=expected_block_ref)
self.assertEqual(str(testobj), testurn)
self.assertEqual(testobj.url(), 'edx://' + testurn)
testobj = BlockUsageLocator(url=testurn, version_guid=ObjectId())
agnostic = testobj.version_agnostic()
self.assertIsNone(agnostic.version_guid)
self.check_block_locn_fields(agnostic, 'test_block constructor',
@@ -225,7 +226,7 @@ class LocatorTest(TestCase):
block='lab2',
version_guid=ObjectId(test_id_loc)
)
agnostic = testobj.version_agnostic()
agnostic = testobj.course_agnostic()
self.check_block_locn_fields(
agnostic, 'error parsing URL with version and block',
block='lab2',

View File

@@ -1,286 +1,407 @@
# pylint: disable=E0611
from nose.tools import assert_equals, assert_raises, assert_false, \
assert_true, assert_not_equals, assert_in, assert_not_in
# pylint: enable=E0611
import pymongo
from uuid import uuid4
import ddt
from mock import patch, Mock
from importlib import import_module
from xmodule.tests import DATA_DIR
from xmodule.modulestore import Location, MONGO_MODULESTORE_TYPE, XML_MODULESTORE_TYPE
from xmodule.modulestore import Location, MONGO_MODULESTORE_TYPE, SPLIT_MONGO_MODULESTORE_TYPE, \
XML_MODULESTORE_TYPE
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.locator import BlockUsageLocator, CourseLocator
from xmodule.modulestore.tests.test_location_mapper import LocMapperSetupSansDjango, loc_mapper
# Mixed modulestore depends on django, so we'll manually configure some django settings
# before importing the module
from django.conf import settings
import unittest
import copy
if not settings.configured:
settings.configure()
from xmodule.modulestore.mixed import MixedModuleStore
HOST = 'localhost'
PORT = 27017
DB = 'test_mongo_%s' % uuid4().hex[:5]
COLLECTION = 'modulestore'
FS_ROOT = DATA_DIR
DEFAULT_CLASS = 'xmodule.raw_module.RawDescriptor'
RENDER_TEMPLATE = lambda t_n, d, ctx = None, nsp = 'main': ''
@ddt.ddt
class TestMixedModuleStore(LocMapperSetupSansDjango):
"""
Quasi-superclass which tests Location based apps against both split and mongo dbs (Locator and
Location-based dbs)
"""
HOST = 'localhost'
PORT = 27017
DB = 'test_mongo_%s' % uuid4().hex[:5]
COLLECTION = 'modulestore'
FS_ROOT = DATA_DIR
DEFAULT_CLASS = 'xmodule.raw_module.RawDescriptor'
RENDER_TEMPLATE = lambda t_n, d, ctx = None, nsp = 'main': ''
IMPORT_COURSEID = 'MITx/999/2013_Spring'
XML_COURSEID1 = 'edX/toy/2012_Fall'
XML_COURSEID2 = 'edX/simple/2012_Fall'
MONGO_COURSEID = 'MITx/999/2013_Spring'
XML_COURSEID1 = 'edX/toy/2012_Fall'
XML_COURSEID2 = 'edX/simple/2012_Fall'
OPTIONS = {
'mappings': {
XML_COURSEID1: 'xml',
XML_COURSEID2: 'xml',
IMPORT_COURSEID: 'default'
},
'reference_type': 'Location',
'stores': {
'xml': {
'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
'OPTIONS': {
'data_dir': DATA_DIR,
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
}
modulestore_options = {
'default_class': DEFAULT_CLASS,
'fs_root': DATA_DIR,
'render_template': RENDER_TEMPLATE,
}
DOC_STORE_CONFIG = {
'host': HOST,
'db': DB,
'collection': COLLECTION,
}
OPTIONS = {
'mappings': {
XML_COURSEID1: 'xml',
XML_COURSEID2: 'xml',
MONGO_COURSEID: 'default'
},
'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'DOC_STORE_CONFIG': {
'host': HOST,
'db': DB,
'collection': COLLECTION,
'stores': {
'xml': {
'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
'OPTIONS': {
'data_dir': DATA_DIR,
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
}
},
'OPTIONS': {
'default_class': DEFAULT_CLASS,
'fs_root': DATA_DIR,
'render_template': RENDER_TEMPLATE,
'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
'OPTIONS': modulestore_options
},
'draft': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
'OPTIONS': modulestore_options
},
'split': {
'ENGINE': 'xmodule.modulestore.split_mongo.SplitMongoModuleStore',
'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
'OPTIONS': modulestore_options
}
}
}
}
def _compareIgnoreVersion(self, loc1, loc2, msg=None):
"""
AssertEqual replacement for CourseLocator
"""
if not (loc1.package_id == loc2.package_id and loc1.branch == loc2.branch and loc1.block_id == loc2.block_id):
self.fail(self._formatMessage(msg, u"{} != {}".format(unicode(loc1), unicode(loc2))))
class TestMixedModuleStore(object):
'''Tests!'''
@classmethod
def setupClass(cls):
def setUp(self):
"""
Set up the database for testing
"""
cls.connection = pymongo.MongoClient(
host=HOST,
port=PORT,
self.options = getattr(self, 'options', self.OPTIONS)
self.connection = pymongo.MongoClient(
host=self.HOST,
port=self.PORT,
tz_aware=True,
)
cls.connection.drop_database(DB)
cls.fake_location = Location('i4x', 'foo', 'bar', 'vertical', 'baz')
import_course_dict = Location.parse_course_id(IMPORT_COURSEID)
cls.import_org = import_course_dict['org']
cls.import_course = import_course_dict['course']
cls.import_run = import_course_dict['name']
# NOTE: Creating a single db for all the tests to save time. This
# is ok only as long as none of the tests modify the db.
# If (when!) that changes, need to either reload the db, or load
# once and copy over to a tmp db for each test.
cls.store = cls.initdb()
self.connection.drop_database(self.DB)
self.addCleanup(self.connection.drop_database, self.DB)
self.addCleanup(self.connection.close)
super(TestMixedModuleStore, self).setUp()
@classmethod
def teardownClass(cls):
"""
Clear out database after test has completed
"""
cls.destroy_db(cls.connection)
@staticmethod
def initdb():
"""
Initialize the database and import one test course into it
"""
# connect to the db
_options = {}
_options.update(OPTIONS)
store = MixedModuleStore(**_options)
import_from_xml(
store._get_modulestore_for_courseid(IMPORT_COURSEID),
DATA_DIR,
['toy'],
target_location_namespace=Location(
'i4x',
TestMixedModuleStore.import_org,
TestMixedModuleStore.import_course,
'course',
TestMixedModuleStore.import_run
)
patcher = patch.multiple(
'xmodule.modulestore.mixed',
loc_mapper=Mock(return_value=LocMapperSetupSansDjango.loc_store),
create_modulestore_instance=create_modulestore_instance,
)
patcher.start()
self.addCleanup(patcher.stop)
self.addTypeEqualityFunc(BlockUsageLocator, '_compareIgnoreVersion')
# define attrs which get set in initdb to quell pylint
self.import_chapter_location = self.store = self.fake_location = self.xml_chapter_location = None
self.course_locations = []
return store
@staticmethod
def destroy_db(connection):
# pylint: disable=invalid-name
def _create_course(self, default, course_id):
"""
Destroy the test db.
Create a course w/ one item in the persistence store using the given course & item location.
"""
connection.drop_database(DB)
course = self.store.create_course(course_id, store_name=default)
category = self.import_chapter_location.category
block_id = self.import_chapter_location.name
chapter = self.store.create_item(
# don't use course_location as it may not be the repr
course.location, category, location=self.import_chapter_location, block_id=block_id
)
if isinstance(course.location, CourseLocator):
self.course_locations[self.MONGO_COURSEID] = course.location.version_agnostic()
self.import_chapter_location = chapter.location.version_agnostic()
else:
self.assertEqual(course.location.course_id, course_id)
self.assertEqual(chapter.location, self.import_chapter_location)
def setUp(self):
# make a copy for convenience
self.connection = TestMixedModuleStore.connection
def initdb(self, default):
"""
Initialize the database and create one test course in it
"""
# set the default modulestore
self.options['stores']['default'] = self.options['stores'][default]
self.store = MixedModuleStore(**self.options)
self.addCleanup(self.store.close_all_connections)
def tearDown(self):
pass
self.course_locations = {
course_id: generate_location(course_id)
for course_id in [self.MONGO_COURSEID, self.XML_COURSEID1, self.XML_COURSEID2]
}
self.fake_location = Location('i4x', 'foo', 'bar', 'vertical', 'baz')
self.import_chapter_location = self.course_locations[self.MONGO_COURSEID].replace(
category='chapter', name='Overview'
)
self.xml_chapter_location = self.course_locations[self.XML_COURSEID1].replace(
category='chapter', name='Overview'
)
# get Locators and set up the loc mapper if app is Locator based
if default == 'split':
self.fake_location = loc_mapper().translate_location('foo/bar/2012_Fall', self.fake_location)
def test_get_modulestore_type(self):
self._create_course(default, self.MONGO_COURSEID)
@ddt.data('direct', 'split')
def test_get_modulestore_type(self, default_ms):
"""
Make sure we get back the store type we expect for given mappings
"""
assert_equals(self.store.get_modulestore_type(XML_COURSEID1), XML_MODULESTORE_TYPE)
assert_equals(self.store.get_modulestore_type(XML_COURSEID2), XML_MODULESTORE_TYPE)
assert_equals(self.store.get_modulestore_type(IMPORT_COURSEID), MONGO_MODULESTORE_TYPE)
self.initdb(default_ms)
self.assertEqual(self.store.get_modulestore_type(self.XML_COURSEID1), XML_MODULESTORE_TYPE)
self.assertEqual(self.store.get_modulestore_type(self.XML_COURSEID2), XML_MODULESTORE_TYPE)
mongo_ms_type = MONGO_MODULESTORE_TYPE if default_ms == 'direct' else SPLIT_MONGO_MODULESTORE_TYPE
self.assertEqual(self.store.get_modulestore_type(self.MONGO_COURSEID), mongo_ms_type)
# try an unknown mapping, it should be the 'default' store
assert_equals(self.store.get_modulestore_type('foo/bar/2012_Fall'), MONGO_MODULESTORE_TYPE)
self.assertEqual(self.store.get_modulestore_type('foo/bar/2012_Fall'), mongo_ms_type)
def test_has_item(self):
assert_true(self.store.has_item(
IMPORT_COURSEID, Location(['i4x', self.import_org, self.import_course, 'course', self.import_run])
))
assert_true(self.store.has_item(
XML_COURSEID1, Location(['i4x', 'edX', 'toy', 'course', '2012_Fall'])
))
@ddt.data('direct', 'split')
def test_has_item(self, default_ms):
self.initdb(default_ms)
for course_id, course_locn in self.course_locations.iteritems():
self.assertTrue(self.store.has_item(course_id, course_locn))
# try negative cases
assert_false(self.store.has_item(
XML_COURSEID1, Location(['i4x', self.import_org, self.import_course, 'course', self.import_run])
))
assert_false(self.store.has_item(
IMPORT_COURSEID, Location(['i4x', 'edX', 'toy', 'course', '2012_Fall'])
self.assertFalse(self.store.has_item(
self.XML_COURSEID1,
self.course_locations[self.XML_COURSEID1].replace(name='not_findable', category='problem')
))
self.assertFalse(self.store.has_item(self.MONGO_COURSEID, self.fake_location))
def test_get_item(self):
with assert_raises(NotImplementedError):
@ddt.data('direct', 'split')
def test_get_item(self, default_ms):
self.initdb(default_ms)
with self.assertRaises(NotImplementedError):
self.store.get_item(self.fake_location)
def test_get_instance(self):
module = self.store.get_instance(
IMPORT_COURSEID, Location(['i4x', self.import_org, self.import_course, 'course', self.import_run])
)
assert_not_equals(module, None)
module = self.store.get_instance(
XML_COURSEID1, Location(['i4x', 'edX', 'toy', 'course', '2012_Fall'])
)
assert_not_equals(module, None)
@ddt.data('direct', 'split')
def test_get_instance(self, default_ms):
self.initdb(default_ms)
for course_id, course_locn in self.course_locations.iteritems():
self.assertIsNotNone(self.store.get_instance(course_id, course_locn))
# try negative cases
with assert_raises(ItemNotFoundError):
with self.assertRaises(ItemNotFoundError):
self.store.get_instance(
XML_COURSEID1, Location(['i4x', self.import_org, self.import_course, 'course', self.import_run])
self.XML_COURSEID1,
self.course_locations[self.XML_COURSEID1].replace(name='not_findable', category='problem')
)
with self.assertRaises(ItemNotFoundError):
self.store.get_instance(self.MONGO_COURSEID, self.fake_location)
with assert_raises(ItemNotFoundError):
self.store.get_instance(
IMPORT_COURSEID, Location(['i4x', 'edX', 'toy', 'course', '2012_Fall'])
)
@ddt.data('direct', 'split')
def test_get_items(self, default_ms):
self.initdb(default_ms)
for course_id, course_locn in self.course_locations.iteritems():
if hasattr(course_locn, 'as_course_locator'):
locn = course_locn.as_course_locator()
else:
locn = course_locn.replace(org=None, course=None, name=None)
# NOTE: use get_course if you just want the course. get_items is expensive
modules = self.store.get_items(locn, course_id, qualifiers={'category': 'course'})
self.assertEqual(len(modules), 1)
self.assertEqual(modules[0].location, course_locn)
def test_get_items(self):
# NOTE: use get_course if you just want the course. get_items only allows wildcarding of category and name
modules = self.store.get_items(Location('i4x', None, None, 'course', None), IMPORT_COURSEID)
assert_equals(len(modules), 1)
assert_equals(modules[0].location.course, self.import_course)
@ddt.data('direct', 'split')
def test_update_item(self, default_ms):
"""
Update should fail for r/o dbs and succeed for r/w ones
"""
self.initdb(default_ms)
course_id = self.XML_COURSEID1
course = self.store.get_course(course_id)
# if following raised, then the test is really a noop, change it
self.assertFalse(course.show_calculator, "Default changed making test meaningless")
course.show_calculator = True
with self.assertRaises(NotImplementedError):
self.store.update_item(course, None)
# now do it for a r/w db
# get_course api's are inconsistent: one takes Locators the other an old style course id
if hasattr(self.course_locations[self.MONGO_COURSEID], 'as_course_locator'):
locn = self.course_locations[self.MONGO_COURSEID]
else:
locn = self.MONGO_COURSEID
course = self.store.get_course(locn)
# if following raised, then the test is really a noop, change it
self.assertFalse(course.show_calculator, "Default changed making test meaningless")
course.show_calculator = True
self.store.update_item(course, None)
course = self.store.get_course(locn)
self.assertTrue(course.show_calculator)
modules = self.store.get_items(Location('i4x', None, None, 'course', None), XML_COURSEID1)
assert_equals(len(modules), 1)
assert_equals(modules[0].location.course, 'toy')
@ddt.data('direct', 'split')
def test_delete_item(self, default_ms):
"""
Delete should reject on r/o db and work on r/w one
"""
self.initdb(default_ms)
# r/o try deleting the course
with self.assertRaises(NotImplementedError):
self.store.delete_item(self.xml_chapter_location)
self.store.delete_item(self.import_chapter_location, '**replace_user**')
# verify it's gone
with self.assertRaises(ItemNotFoundError):
self.store.get_instance(self.MONGO_COURSEID, self.import_chapter_location)
modules = self.store.get_items(Location('i4x', 'edX', 'simple', 'course', None), XML_COURSEID2)
assert_equals(len(modules), 1)
assert_equals(modules[0].location.course, 'simple')
def test_update_item(self):
# FIXME update
with assert_raises(NotImplementedError):
self.store.update_item(self.fake_location, '**replace_user**')
def test_delete_item(self):
with assert_raises(NotImplementedError):
self.store.delete_item(self.fake_location)
def test_get_courses(self):
# we should have 3 total courses aggregated
@ddt.data('direct', 'split')
def test_get_courses(self, default_ms):
self.initdb(default_ms)
# we should have 3 total courses across all stores
courses = self.store.get_courses()
assert_equals(len(courses), 3)
course_ids = []
for course in courses:
course_ids.append(course.location.course_id)
assert_true(IMPORT_COURSEID in course_ids)
assert_true(XML_COURSEID1 in course_ids)
assert_true(XML_COURSEID2 in course_ids)
course_ids = [
course.location.version_agnostic()
if hasattr(course.location, 'version_agnostic') else course.location
for course in courses
]
self.assertEqual(len(courses), 3, "Not 3 courses: {}".format(course_ids))
self.assertIn(self.course_locations[self.MONGO_COURSEID], course_ids)
self.assertIn(self.course_locations[self.XML_COURSEID1], course_ids)
self.assertIn(self.course_locations[self.XML_COURSEID2], course_ids)
def test_xml_get_courses(self):
"""
Test that the xml modulestore only loaded the courses from the maps.
"""
self.initdb('direct')
courses = self.store.modulestores['xml'].get_courses()
assert_equals(len(courses), 2)
self.assertEqual(len(courses), 2)
course_ids = [course.location.course_id for course in courses]
assert_in(XML_COURSEID1, course_ids)
assert_in(XML_COURSEID2, course_ids)
self.assertIn(self.XML_COURSEID1, course_ids)
self.assertIn(self.XML_COURSEID2, course_ids)
# this course is in the directory from which we loaded courses but not in the map
assert_not_in("edX/toy/TT_2012_Fall", course_ids)
self.assertNotIn("edX/toy/TT_2012_Fall", course_ids)
def test_get_course(self):
module = self.store.get_course(IMPORT_COURSEID)
assert_equals(module.location.course, self.import_course)
def test_xml_no_write(self):
"""
Test that the xml modulestore doesn't allow write ops.
"""
self.initdb('direct')
with self.assertRaises(NotImplementedError):
self.store.create_course("org/course/run", store_name='xml')
module = self.store.get_course(XML_COURSEID1)
assert_equals(module.location.course, 'toy')
@ddt.data('direct', 'split')
def test_get_course(self, default_ms):
self.initdb(default_ms)
for course_locn in self.course_locations.itervalues():
if hasattr(course_locn, 'as_course_locator'):
locn = course_locn.as_course_locator()
else:
locn = course_locn.course_id
# NOTE: use get_course if you just want the course. get_items is expensive
course = self.store.get_course(locn)
self.assertIsNotNone(course)
self.assertEqual(course.location, course_locn)
module = self.store.get_course(XML_COURSEID2)
assert_equals(module.location.course, 'simple')
# pylint: disable=E1101
def test_get_parent_locations(self):
@ddt.data('direct', 'split')
def test_get_parent_locations(self, default_ms):
self.initdb(default_ms)
parents = self.store.get_parent_locations(
Location(['i4x', self.import_org, self.import_course, 'chapter', 'Overview']),
IMPORT_COURSEID
self.import_chapter_location,
self.MONGO_COURSEID
)
assert_equals(len(parents), 1)
assert_equals(Location(parents[0]).org, self.import_org)
assert_equals(Location(parents[0]).course, self.import_course)
assert_equals(Location(parents[0]).name, self.import_run)
self.assertEqual(len(parents), 1)
self.assertEqual(parents[0], self.course_locations[self.MONGO_COURSEID])
parents = self.store.get_parent_locations(
Location(['i4x', 'edX', 'toy', 'chapter', 'Overview']),
XML_COURSEID1
self.xml_chapter_location,
self.XML_COURSEID1
)
assert_equals(len(parents), 1)
assert_equals(Location(parents[0]).org, 'edX')
assert_equals(Location(parents[0]).course, 'toy')
assert_equals(Location(parents[0]).name, '2012_Fall')
self.assertEqual(len(parents), 1)
self.assertEqual(parents[0], self.course_locations[self.XML_COURSEID1])
class TestMixedMSInit(unittest.TestCase):
"""
Test initializing w/o a reference_type
"""
def setUp(self):
unittest.TestCase.setUp(self)
options = copy.copy(OPTIONS)
del options['reference_type']
self.connection = pymongo.MongoClient(
host=HOST,
port=PORT,
tz_aware=True,
@ddt.data('direct', 'split')
def test_get_orphans(self, default_ms):
self.initdb(default_ms)
# create an orphan
if default_ms == 'split':
course_id = self.course_locations[self.MONGO_COURSEID].as_course_locator()
branch = course_id.branch
else:
course_id = self.MONGO_COURSEID
branch = None
orphan = self.store.create_item(course_id, 'problem', block_id='orphan')
found_orphans = self.store.get_orphans(self.course_locations[self.MONGO_COURSEID], branch)
if default_ms == 'split':
self.assertEqual(found_orphans, [orphan.location.version_agnostic()])
else:
self.assertEqual(found_orphans, [unicode(orphan.location)])
@ddt.data('split')
def test_create_item_from_course_id(self, default_ms):
"""
Test code paths missed by the above:
* passing an old-style course_id which has a loc map to split's create_item
"""
self.initdb(default_ms)
# create loc_map entry
loc_mapper().translate_location(self.MONGO_COURSEID, generate_location(self.MONGO_COURSEID))
orphan = self.store.create_item(self.MONGO_COURSEID, 'problem', block_id='orphan')
self.assertEqual(
orphan.location.version_agnostic().as_course_locator(),
self.course_locations[self.MONGO_COURSEID].as_course_locator()
)
self.store = MixedModuleStore(**options)
def test_use_locations(self):
@ddt.data('direct')
def test_create_item_from_parent_location(self, default_ms):
"""
Test that use_locations defaulted correctly
Test a code path missed by the above: passing an old-style location as parent but no
new location for the child
"""
self.assertTrue(self.store.use_locations)
self.initdb(default_ms)
self.store.create_item(self.course_locations[self.MONGO_COURSEID], 'problem', block_id='orphan')
orphans = self.store.get_orphans(self.course_locations[self.MONGO_COURSEID], None)
self.assertEqual(len(orphans), 0, "unexpected orphans: {}".format(orphans))
#=============================================================================================================
# General utils for not using django settings
#=============================================================================================================
def load_function(path):
"""
Load a function by name.
path is a string of the form "path.to.module.function"
returns the imported python object `function` from `path.to.module`
"""
module_path, _, name = path.rpartition('.')
return getattr(import_module(module_path), name)
# pylint: disable=unused-argument
def create_modulestore_instance(engine, doc_store_config, options, i18n_service=None):
"""
This will return a new instance of a modulestore given an engine and options
"""
class_ = load_function(engine)
return class_(
doc_store_config=doc_store_config,
**options
)
def generate_location(course_id):
"""
Generate the locations for the given ids
"""
course_dict = Location.parse_course_id(course_id)
course_dict['tag'] = 'i4x'
course_dict['category'] = 'course'
return Location(course_dict)

View File

@@ -56,6 +56,7 @@ class TestMongoModuleStore(object):
def teardownClass(cls):
if cls.connection:
cls.connection.drop_database(DB)
cls.connection.close()
@staticmethod
def initdb():

View File

@@ -163,8 +163,6 @@ class SplitModuleCourseTests(SplitModuleTest):
"children")
_verify_published_course(modulestore().get_courses(branch='published'))
# default for branch is 'published'.
_verify_published_course(modulestore().get_courses())
def test_search_qualifiers(self):
# query w/ search criteria

View File

@@ -11,10 +11,10 @@ from mock import Mock, patch
from django.utils.timezone import UTC
from xmodule.xml_module import is_pointer_tag
from xmodule.modulestore import Location
from xmodule.modulestore import Location, only_xmodules
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore, LocationReader
from xmodule.modulestore.inheritance import compute_inherited_metadata
from xmodule.x_module import XModuleMixin, only_xmodules
from xmodule.x_module import XModuleMixin
from xmodule.fields import Date
from xmodule.tests import DATA_DIR
from xmodule.modulestore.inheritance import InheritanceMixin

View File

@@ -9,7 +9,7 @@ from factory import Factory, lazy_attribute, post_generation, Sequence
from lxml import etree
from xmodule.modulestore.inheritance import InheritanceMixin
from xmodule.x_module import only_xmodules
from xmodule.modulestore import only_xmodules
class XmlImportData(object):

View File

@@ -342,7 +342,7 @@ class VideoModule(VideoFields, XModule):
try:
transcript = self.translation(request.GET.get('videoId'))
except TranscriptException as ex:
except (TranscriptException, NotFoundError) as ex:
log.info(ex.message)
response = Response(status=404)
else:
@@ -414,6 +414,9 @@ class VideoModule(VideoFields, XModule):
Filenames naming:
en: subs_videoid.srt.sjson
non_en: uk_subs_videoid.srt.sjson
Raises:
NotFoundError if for 'en' subtitles no asset is uploaded.
"""
if self.transcript_language == 'en':
return asset(self.location, subs_id).data

View File

@@ -26,6 +26,7 @@ from xmodule.errortracker import exc_info_to_str
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError, InsufficientSpecificationError, InvalidLocationError
from xmodule.modulestore.locator import BlockUsageLocator
from xmodule.exceptions import UndefinedContext
log = logging.getLogger(__name__)
@@ -605,22 +606,6 @@ class ResourceTemplates(object):
return template
def prefer_xmodules(identifier, entry_points):
"""Prefer entry_points from the xmodule package"""
from_xmodule = [entry_point for entry_point in entry_points if entry_point.dist.key == 'xmodule']
if from_xmodule:
return default_select(identifier, from_xmodule)
else:
return default_select(identifier, entry_points)
def only_xmodules(identifier, entry_points):
"""Only use entry_points that are supplied by the xmodule package"""
from_xmodule = [entry_point for entry_point in entry_points if entry_point.dist.key == 'xmodule']
return default_select(identifier, from_xmodule)
@XBlock.needs("i18n")
class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
"""
@@ -836,7 +821,8 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
Returns the XModule corresponding to this descriptor. Expects that the system
already supports all of the attributes needed by xmodules
"""
assert self.xmodule_runtime is not None
if self.xmodule_runtime is None:
raise UndefinedContext()
assert self.xmodule_runtime.error_descriptor_class is not None
if self.xmodule_runtime.xmodule_instance is None:
try:

View File

@@ -20,6 +20,7 @@ if Backbone?
@template(templateData)
render: ->
@$el.addClass("response_" + @model.get("id"))
@$el.html(@renderTemplate())
@delegateEvents()

View File

@@ -0,0 +1,72 @@
diff --git a/codemirror-accessible.js b/codemirror-accessible.js
index 1d0d996..bd37cfb 100644
--- a/codemirror-accessible.js
+++ b/codemirror-accessible.js
@@ -1443,7 +1443,7 @@ window.CodeMirror = (function() {
// supported or compatible enough yet to rely on.)
function readInput(cm) {
var input = cm.display.input, prevInput = cm.display.prevInput, doc = cm.doc, sel = doc.sel;
- if (!cm.state.focused || hasSelection(input) || isReadOnly(cm) || cm.options.disableInput) return false;
+ if (!cm.state.focused || hasSelection(input) || isReadOnly(cm) || cm.options.disableInput || cm.state.accessibleTextareaWaiting) return false;
var text = input.value;
if (text == prevInput && posEq(sel.from, sel.to)) return false;
if (ie && !ie_lt9 && cm.display.inputHasSelection === text) {
@@ -1480,13 +1480,13 @@ window.CodeMirror = (function() {
var minimal, selected, doc = cm.doc;
if (!posEq(doc.sel.from, doc.sel.to)) {
cm.display.prevInput = "";
- minimal = hasCopyEvent &&
+ minimal = false && hasCopyEvent &&
(doc.sel.to.line - doc.sel.from.line > 100 || (selected = cm.getSelection()).length > 1000);
var content = minimal ? "-" : selected || cm.getSelection();
cm.display.input.value = content;
if (cm.state.focused) selectInput(cm.display.input);
if (ie && !ie_lt9) cm.display.inputHasSelection = content;
- } else if (user) {
+ } else if (user && !cm.state.accessibleTextareaWaiting) {
cm.display.prevInput = cm.display.input.value = "";
if (ie && !ie_lt9) cm.display.inputHasSelection = null;
}
@@ -2069,6 +2069,12 @@ window.CodeMirror = (function() {
cm.doc.sel.shift = code == 16 || e.shiftKey;
// First give onKeyEvent option a chance to handle this.
var handled = handleKeyBinding(cm, e);
+
+ // On text input if value was temporaritly set for a screenreader, clear it out.
+ if (!handled && cm.state.accessibleTextareaWaiting) {
+ clearAccessibleTextarea(cm);
+ }
+
if (opera) {
lastStoppedKey = handled ? code : null;
// Opera has no cut event... we try to at least catch the key combo
@@ -2473,6 +2479,29 @@ window.CodeMirror = (function() {
setSelection(doc, pos, other || pos, bias);
}
if (doc.cm) doc.cm.curOp.userSelChange = true;
+
+ if (doc.cm) {
+ var from = doc.sel.from;
+ var to = doc.sel.to;
+
+ if (posEq(from, to) && doc.cm.display.input.setSelectionRange) {
+ clearTimeout(doc.cm.state.accessibleTextareaTimeout);
+ doc.cm.state.accessibleTextareaWaiting = true;
+
+ doc.cm.display.input.value = doc.getLine(from.line) + "\n";
+ doc.cm.display.input.setSelectionRange(from.ch, from.ch);
+
+ doc.cm.state.accessibleTextareaTimeout = setTimeout(function() {
+ clearAccessibleTextarea(doc.cm);
+ }, 80);
+ }
+ }
+ }
+
+ function clearAccessibleTextarea(cm) {
+ clearTimeout(cm.state.accessibleTextareaTimeout);
+ cm.state.accessibleTextareaWaiting = false;
+ resetInput(cm, true);
}
function filterSelectionChange(doc, anchor, head) {

View File

@@ -0,0 +1,93 @@
/**
* Tag-closer extension for CodeMirror.
*
* This extension adds an "autoCloseTags" option that can be set to
* either true to get the default behavior, or an object to further
* configure its behavior.
*
* These are supported options:
*
* `whenClosing` (default true)
* Whether to autoclose when the '/' of a closing tag is typed.
* `whenOpening` (default true)
* Whether to autoclose the tag when the final '>' of an opening
* tag is typed.
* `dontCloseTags` (default is empty tags for HTML, none for XML)
* An array of tag names that should not be autoclosed.
* `indentTags` (default is block tags for HTML, none for XML)
* An array of tag names that should, when opened, cause a
* blank line to be added inside the tag, and the blank line and
* closing line to be indented.
*
* See demos/closetag.html for a usage example.
*/
(function() {
CodeMirror.defineOption("autoCloseTags", false, function(cm, val, old) {
if (old != CodeMirror.Init && old)
cm.removeKeyMap("autoCloseTags");
if (!val) return;
var map = {name: "autoCloseTags"};
if (typeof val != "object" || val.whenClosing)
map["'/'"] = function(cm) { return autoCloseSlash(cm); };
if (typeof val != "object" || val.whenOpening)
map["'>'"] = function(cm) { return autoCloseGT(cm); };
cm.addKeyMap(map);
});
var htmlDontClose = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param",
"source", "track", "wbr"];
var htmlIndent = ["applet", "blockquote", "body", "button", "div", "dl", "fieldset", "form", "frameset", "h1", "h2", "h3", "h4",
"h5", "h6", "head", "html", "iframe", "layer", "legend", "object", "ol", "p", "select", "table", "ul"];
function autoCloseGT(cm) {
var pos = cm.getCursor(), tok = cm.getTokenAt(pos);
var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state;
if (inner.mode.name != "xml" || !state.tagName || cm.getOption("disableInput")) return CodeMirror.Pass;
var opt = cm.getOption("autoCloseTags"), html = inner.mode.configuration == "html";
var dontCloseTags = (typeof opt == "object" && opt.dontCloseTags) || (html && htmlDontClose);
var indentTags = (typeof opt == "object" && opt.indentTags) || (html && htmlIndent);
var tagName = state.tagName;
if (tok.end > pos.ch) tagName = tagName.slice(0, tagName.length - tok.end + pos.ch);
var lowerTagName = tagName.toLowerCase();
// Don't process the '>' at the end of an end-tag or self-closing tag
if (!tagName ||
tok.type == "string" && (tok.end != pos.ch || !/[\"\']/.test(tok.string.charAt(tok.string.length - 1)) || tok.string.length == 1) ||
tok.type == "tag" && state.type == "closeTag" ||
tok.string.indexOf("/") == (tok.string.length - 1) || // match something like <someTagName />
dontCloseTags && indexOf(dontCloseTags, lowerTagName) > -1 ||
CodeMirror.scanForClosingTag && CodeMirror.scanForClosingTag(cm, pos, tagName,
Math.min(cm.lastLine() + 1, pos.line + 50)))
return CodeMirror.Pass;
var doIndent = indentTags && indexOf(indentTags, lowerTagName) > -1;
var curPos = doIndent ? CodeMirror.Pos(pos.line + 1, 0) : CodeMirror.Pos(pos.line, pos.ch + 1);
cm.replaceSelection(">" + (doIndent ? "\n\n" : "") + "</" + tagName + ">",
{head: curPos, anchor: curPos});
if (doIndent) {
cm.indentLine(pos.line + 1, null, true);
cm.indentLine(pos.line + 2, null);
}
}
function autoCloseSlash(cm) {
var pos = cm.getCursor(), tok = cm.getTokenAt(pos);
var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state;
if (tok.type == "string" || tok.string.charAt(0) != "<" ||
tok.start != pos.ch - 1 || inner.mode.name != "xml" ||
cm.getOption("disableInput"))
return CodeMirror.Pass;
var tagName = state.context && state.context.tagName;
if (tagName) cm.replaceSelection("/" + tagName + ">", "end");
}
function indexOf(collection, elt) {
if (collection.indexOf) return collection.indexOf(elt);
for (var i = 0, e = collection.length; i < e; ++i)
if (collection[i] == elt) return i;
return -1;
}
})();

View File

@@ -0,0 +1,149 @@
(function() {
"use strict";
var noOptions = {};
var nonWS = /[^\s\u00a0]/;
var Pos = CodeMirror.Pos;
function firstNonWS(str) {
var found = str.search(nonWS);
return found == -1 ? 0 : found;
}
CodeMirror.commands.toggleComment = function(cm) {
var from = cm.getCursor("start"), to = cm.getCursor("end");
cm.uncomment(from, to) || cm.lineComment(from, to);
};
CodeMirror.defineExtension("lineComment", function(from, to, options) {
if (!options) options = noOptions;
var self = this, mode = self.getModeAt(from);
var commentString = options.lineComment || mode.lineComment;
if (!commentString) {
if (options.blockCommentStart || mode.blockCommentStart) {
options.fullLines = true;
self.blockComment(from, to, options);
}
return;
}
var firstLine = self.getLine(from.line);
if (firstLine == null) return;
var end = Math.min(to.ch != 0 || to.line == from.line ? to.line + 1 : to.line, self.lastLine() + 1);
var pad = options.padding == null ? " " : options.padding;
var blankLines = options.commentBlankLines || from.line == to.line;
self.operation(function() {
if (options.indent) {
var baseString = firstLine.slice(0, firstNonWS(firstLine));
for (var i = from.line; i < end; ++i) {
var line = self.getLine(i), cut = baseString.length;
if (!blankLines && !nonWS.test(line)) continue;
if (line.slice(0, cut) != baseString) cut = firstNonWS(line);
self.replaceRange(baseString + commentString + pad, Pos(i, 0), Pos(i, cut));
}
} else {
for (var i = from.line; i < end; ++i) {
if (blankLines || nonWS.test(self.getLine(i)))
self.replaceRange(commentString + pad, Pos(i, 0));
}
}
});
});
CodeMirror.defineExtension("blockComment", function(from, to, options) {
if (!options) options = noOptions;
var self = this, mode = self.getModeAt(from);
var startString = options.blockCommentStart || mode.blockCommentStart;
var endString = options.blockCommentEnd || mode.blockCommentEnd;
if (!startString || !endString) {
if ((options.lineComment || mode.lineComment) && options.fullLines != false)
self.lineComment(from, to, options);
return;
}
var end = Math.min(to.line, self.lastLine());
if (end != from.line && to.ch == 0 && nonWS.test(self.getLine(end))) --end;
var pad = options.padding == null ? " " : options.padding;
if (from.line > end) return;
self.operation(function() {
if (options.fullLines != false) {
var lastLineHasText = nonWS.test(self.getLine(end));
self.replaceRange(pad + endString, Pos(end));
self.replaceRange(startString + pad, Pos(from.line, 0));
var lead = options.blockCommentLead || mode.blockCommentLead;
if (lead != null) for (var i = from.line + 1; i <= end; ++i)
if (i != end || lastLineHasText)
self.replaceRange(lead + pad, Pos(i, 0));
} else {
self.replaceRange(endString, to);
self.replaceRange(startString, from);
}
});
});
CodeMirror.defineExtension("uncomment", function(from, to, options) {
if (!options) options = noOptions;
var self = this, mode = self.getModeAt(from);
var end = Math.min(to.line, self.lastLine()), start = Math.min(from.line, end);
// Try finding line comments
var lineString = options.lineComment || mode.lineComment, lines = [];
var pad = options.padding == null ? " " : options.padding, didSomething;
lineComment: {
if (!lineString) break lineComment;
for (var i = start; i <= end; ++i) {
var line = self.getLine(i);
var found = line.indexOf(lineString);
if (found > -1 && !/comment/.test(self.getTokenTypeAt(Pos(i, found + 1)))) found = -1;
if (found == -1 && (i != end || i == start) && nonWS.test(line)) break lineComment;
if (found > -1 && nonWS.test(line.slice(0, found))) break lineComment;
lines.push(line);
}
self.operation(function() {
for (var i = start; i <= end; ++i) {
var line = lines[i - start];
var pos = line.indexOf(lineString), endPos = pos + lineString.length;
if (pos < 0) continue;
if (line.slice(endPos, endPos + pad.length) == pad) endPos += pad.length;
didSomething = true;
self.replaceRange("", Pos(i, pos), Pos(i, endPos));
}
});
if (didSomething) return true;
}
// Try block comments
var startString = options.blockCommentStart || mode.blockCommentStart;
var endString = options.blockCommentEnd || mode.blockCommentEnd;
if (!startString || !endString) return false;
var lead = options.blockCommentLead || mode.blockCommentLead;
var startLine = self.getLine(start), endLine = end == start ? startLine : self.getLine(end);
var open = startLine.indexOf(startString), close = endLine.lastIndexOf(endString);
if (close == -1 && start != end) {
endLine = self.getLine(--end);
close = endLine.lastIndexOf(endString);
}
if (open == -1 || close == -1 ||
!/comment/.test(self.getTokenTypeAt(Pos(start, open + 1))) ||
!/comment/.test(self.getTokenTypeAt(Pos(end, close + 1))))
return false;
self.operation(function() {
self.replaceRange("", Pos(end, close - (pad && endLine.slice(close - pad.length, close) == pad ? pad.length : 0)),
Pos(end, close + endString.length));
var openEnd = open + startString.length;
if (pad && startLine.slice(openEnd, openEnd + pad.length) == pad) openEnd += pad.length;
self.replaceRange("", Pos(start, open), Pos(start, openEnd));
if (lead) for (var i = start + 1; i <= end; ++i) {
var line = self.getLine(i), found = line.indexOf(lead);
if (found == -1 || nonWS.test(line.slice(0, found))) continue;
var foundEnd = found + lead.length;
if (pad && line.slice(foundEnd, foundEnd + pad.length) == pad) foundEnd += pad.length;
self.replaceRange("", Pos(i, found), Pos(i, foundEnd));
}
});
return true;
});
})();

View File

@@ -0,0 +1,32 @@
CodeMirror.defineMode("diff", function() {
var TOKEN_NAMES = {
'+': 'positive',
'-': 'negative',
'@': 'meta'
};
return {
token: function(stream) {
var tw_pos = stream.string.search(/[\t ]+?$/);
if (!stream.sol() || tw_pos === 0) {
stream.skipToEnd();
return ("error " + (
TOKEN_NAMES[stream.string.charAt(0)] || '')).replace(/ $/, '');
}
var token_name = TOKEN_NAMES[stream.peek()] || stream.skipToEnd();
if (tw_pos === -1) {
stream.skipToEnd();
} else {
stream.pos = tw_pos;
}
return token_name;
}
};
});
CodeMirror.defineMIME("text/x-diff", "diff");

View File

@@ -0,0 +1,114 @@
(function() {
CodeMirror.extendMode("css", {
commentStart: "/*",
commentEnd: "*/",
newlineAfterToken: function(_type, content) {
return /^[;{}]$/.test(content);
}
});
CodeMirror.extendMode("javascript", {
commentStart: "/*",
commentEnd: "*/",
// FIXME semicolons inside of for
newlineAfterToken: function(_type, content, textAfter, state) {
if (this.jsonMode) {
return /^[\[,{]$/.test(content) || /^}/.test(textAfter);
} else {
if (content == ";" && state.lexical && state.lexical.type == ")") return false;
return /^[;{}]$/.test(content) && !/^;/.test(textAfter);
}
}
});
var inlineElements = /^(a|abbr|acronym|area|base|bdo|big|br|button|caption|cite|code|col|colgroup|dd|del|dfn|em|frame|hr|iframe|img|input|ins|kbd|label|legend|link|map|object|optgroup|option|param|q|samp|script|select|small|span|strong|sub|sup|textarea|tt|var)$/;
CodeMirror.extendMode("xml", {
commentStart: "<!--",
commentEnd: "-->",
newlineAfterToken: function(type, content, textAfter, state) {
var inline = false;
if (this.configuration == "html")
inline = state.context ? inlineElements.test(state.context.tagName) : false;
return !inline && ((type == "tag" && />$/.test(content) && state.context) ||
/^</.test(textAfter));
}
});
// Comment/uncomment the specified range
CodeMirror.defineExtension("commentRange", function (isComment, from, to) {
var cm = this, curMode = CodeMirror.innerMode(cm.getMode(), cm.getTokenAt(from).state).mode;
cm.operation(function() {
if (isComment) { // Comment range
cm.replaceRange(curMode.commentEnd, to);
cm.replaceRange(curMode.commentStart, from);
if (from.line == to.line && from.ch == to.ch) // An empty comment inserted - put cursor inside
cm.setCursor(from.line, from.ch + curMode.commentStart.length);
} else { // Uncomment range
var selText = cm.getRange(from, to);
var startIndex = selText.indexOf(curMode.commentStart);
var endIndex = selText.lastIndexOf(curMode.commentEnd);
if (startIndex > -1 && endIndex > -1 && endIndex > startIndex) {
// Take string till comment start
selText = selText.substr(0, startIndex)
// From comment start till comment end
+ selText.substring(startIndex + curMode.commentStart.length, endIndex)
// From comment end till string end
+ selText.substr(endIndex + curMode.commentEnd.length);
}
cm.replaceRange(selText, from, to);
}
});
});
// Applies automatic mode-aware indentation to the specified range
CodeMirror.defineExtension("autoIndentRange", function (from, to) {
var cmInstance = this;
this.operation(function () {
for (var i = from.line; i <= to.line; i++) {
cmInstance.indentLine(i, "smart");
}
});
});
// Applies automatic formatting to the specified range
CodeMirror.defineExtension("autoFormatRange", function (from, to) {
var cm = this;
var outer = cm.getMode(), text = cm.getRange(from, to).split("\n");
var state = CodeMirror.copyState(outer, cm.getTokenAt(from).state);
var tabSize = cm.getOption("tabSize");
var out = "", lines = 0, atSol = from.ch == 0;
function newline() {
out += "\n";
atSol = true;
++lines;
}
for (var i = 0; i < text.length; ++i) {
var stream = new CodeMirror.StringStream(text[i], tabSize);
while (!stream.eol()) {
var inner = CodeMirror.innerMode(outer, state);
var style = outer.token(stream, state), cur = stream.current();
stream.start = stream.pos;
if (!atSol || /\S/.test(cur)) {
out += cur;
atSol = false;
}
if (!atSol && inner.mode.newlineAfterToken &&
inner.mode.newlineAfterToken(style, cur, stream.string.slice(stream.pos) || text[i+1] || "", inner.state))
newline();
}
if (!stream.pos && outer.blankLine) outer.blankLine(state);
if (!atSol && i < text.length - 1) newline();
}
cm.operation(function () {
cm.replaceRange(out, from, to);
for (var cur = from.line + 1, end = from.line + lines; cur <= end; ++cur)
cm.indentLine(cur, "smart");
cm.setSelection(from, cm.getCursor(false));
});
});
})();

View File

@@ -0,0 +1,71 @@
CodeMirror.defineMode("htmlembedded", function(config, parserConfig) {
//config settings
var scriptStartRegex = parserConfig.scriptStartRegex || /^<%/i,
scriptEndRegex = parserConfig.scriptEndRegex || /^%>/i;
//inner modes
var scriptingMode, htmlMixedMode;
//tokenizer when in html mode
function htmlDispatch(stream, state) {
if (stream.match(scriptStartRegex, false)) {
state.token=scriptingDispatch;
return scriptingMode.token(stream, state.scriptState);
}
else
return htmlMixedMode.token(stream, state.htmlState);
}
//tokenizer when in scripting mode
function scriptingDispatch(stream, state) {
if (stream.match(scriptEndRegex, false)) {
state.token=htmlDispatch;
return htmlMixedMode.token(stream, state.htmlState);
}
else
return scriptingMode.token(stream, state.scriptState);
}
return {
startState: function() {
scriptingMode = scriptingMode || CodeMirror.getMode(config, parserConfig.scriptingModeSpec);
htmlMixedMode = htmlMixedMode || CodeMirror.getMode(config, "htmlmixed");
return {
token : parserConfig.startOpen ? scriptingDispatch : htmlDispatch,
htmlState : CodeMirror.startState(htmlMixedMode),
scriptState : CodeMirror.startState(scriptingMode)
};
},
token: function(stream, state) {
return state.token(stream, state);
},
indent: function(state, textAfter) {
if (state.token == htmlDispatch)
return htmlMixedMode.indent(state.htmlState, textAfter);
else if (scriptingMode.indent)
return scriptingMode.indent(state.scriptState, textAfter);
},
copyState: function(state) {
return {
token : state.token,
htmlState : CodeMirror.copyState(htmlMixedMode, state.htmlState),
scriptState : CodeMirror.copyState(scriptingMode, state.scriptState)
};
},
innerMode: function(state) {
if (state.token == scriptingDispatch) return {state: state.scriptState, mode: scriptingMode};
else return {state: state.htmlState, mode: htmlMixedMode};
}
};
}, "htmlmixed");
CodeMirror.defineMIME("application/x-ejs", { name: "htmlembedded", scriptingModeSpec:"javascript"});
CodeMirror.defineMIME("application/x-aspx", { name: "htmlembedded", scriptingModeSpec:"text/x-csharp"});
CodeMirror.defineMIME("application/x-jsp", { name: "htmlembedded", scriptingModeSpec:"text/x-java"});
CodeMirror.defineMIME("application/x-erb", { name: "htmlembedded", scriptingModeSpec:"ruby"});

View File

@@ -0,0 +1,102 @@
CodeMirror.defineMode("htmlmixed", function(config, parserConfig) {
var htmlMode = CodeMirror.getMode(config, {name: "xml", htmlMode: true});
var cssMode = CodeMirror.getMode(config, "css");
var scriptTypes = [], scriptTypesConf = parserConfig && parserConfig.scriptTypes;
scriptTypes.push({matches: /^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^$/i,
mode: CodeMirror.getMode(config, "javascript")});
if (scriptTypesConf) for (var i = 0; i < scriptTypesConf.length; ++i) {
var conf = scriptTypesConf[i];
scriptTypes.push({matches: conf.matches, mode: conf.mode && CodeMirror.getMode(config, conf.mode)});
}
scriptTypes.push({matches: /./,
mode: CodeMirror.getMode(config, "text/plain")});
function html(stream, state) {
var tagName = state.htmlState.tagName;
var style = htmlMode.token(stream, state.htmlState);
if (tagName == "script" && /\btag\b/.test(style) && stream.current() == ">") {
// Script block: mode to change to depends on type attribute
var scriptType = stream.string.slice(Math.max(0, stream.pos - 100), stream.pos).match(/\btype\s*=\s*("[^"]+"|'[^']+'|\S+)[^<]*$/i);
scriptType = scriptType ? scriptType[1] : "";
if (scriptType && /[\"\']/.test(scriptType.charAt(0))) scriptType = scriptType.slice(1, scriptType.length - 1);
for (var i = 0; i < scriptTypes.length; ++i) {
var tp = scriptTypes[i];
if (typeof tp.matches == "string" ? scriptType == tp.matches : tp.matches.test(scriptType)) {
if (tp.mode) {
state.token = script;
state.localMode = tp.mode;
state.localState = tp.mode.startState && tp.mode.startState(htmlMode.indent(state.htmlState, ""));
}
break;
}
}
} else if (tagName == "style" && /\btag\b/.test(style) && stream.current() == ">") {
state.token = css;
state.localMode = cssMode;
state.localState = cssMode.startState(htmlMode.indent(state.htmlState, ""));
}
return style;
}
function maybeBackup(stream, pat, style) {
var cur = stream.current();
var close = cur.search(pat), m;
if (close > -1) stream.backUp(cur.length - close);
else if (m = cur.match(/<\/?$/)) {
stream.backUp(cur.length);
if (!stream.match(pat, false)) stream.match(cur);
}
return style;
}
function script(stream, state) {
if (stream.match(/^<\/\s*script\s*>/i, false)) {
state.token = html;
state.localState = state.localMode = null;
return html(stream, state);
}
return maybeBackup(stream, /<\/\s*script\s*>/,
state.localMode.token(stream, state.localState));
}
function css(stream, state) {
if (stream.match(/^<\/\s*style\s*>/i, false)) {
state.token = html;
state.localState = state.localMode = null;
return html(stream, state);
}
return maybeBackup(stream, /<\/\s*style\s*>/,
cssMode.token(stream, state.localState));
}
return {
startState: function() {
var state = htmlMode.startState();
return {token: html, localMode: null, localState: null, htmlState: state};
},
copyState: function(state) {
if (state.localState)
var local = CodeMirror.copyState(state.localMode, state.localState);
return {token: state.token, localMode: state.localMode, localState: local,
htmlState: CodeMirror.copyState(htmlMode, state.htmlState)};
},
token: function(stream, state) {
return state.token(stream, state);
},
indent: function(state, textAfter) {
if (!state.localMode || /^\s*<\//.test(textAfter))
return htmlMode.indent(state.htmlState, textAfter);
else if (state.localMode.indent)
return state.localMode.indent(state.localState, textAfter);
else
return CodeMirror.Pass;
},
innerMode: function(state) {
return {state: state.localState || state.htmlState, mode: state.localMode || htmlMode};
}
};
}, "xml", "javascript", "css");
CodeMirror.defineMIME("text/html", "htmlmixed");

View File

@@ -0,0 +1,630 @@
// TODO actually recognize syntax of TypeScript constructs
CodeMirror.defineMode("javascript", function(config, parserConfig) {
var indentUnit = config.indentUnit;
var statementIndent = parserConfig.statementIndent;
var jsonMode = parserConfig.json;
var isTS = parserConfig.typescript;
// Tokenizer
var keywords = function(){
function kw(type) {return {type: type, style: "keyword"};}
var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c");
var operator = kw("operator"), atom = {type: "atom", style: "atom"};
var jsKeywords = {
"if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B,
"return": C, "break": C, "continue": C, "new": C, "delete": C, "throw": C, "debugger": C,
"var": kw("var"), "const": kw("var"), "let": kw("var"),
"function": kw("function"), "catch": kw("catch"),
"for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"),
"in": operator, "typeof": operator, "instanceof": operator,
"true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom,
"this": kw("this"), "module": kw("module"), "class": kw("class"), "super": kw("atom"),
"yield": C, "export": kw("export"), "import": kw("import"), "extends": C
};
// Extend the 'normal' keywords with the TypeScript language extensions
if (isTS) {
var type = {type: "variable", style: "variable-3"};
var tsKeywords = {
// object-like things
"interface": kw("interface"),
"extends": kw("extends"),
"constructor": kw("constructor"),
// scope modifiers
"public": kw("public"),
"private": kw("private"),
"protected": kw("protected"),
"static": kw("static"),
// types
"string": type, "number": type, "bool": type, "any": type
};
for (var attr in tsKeywords) {
jsKeywords[attr] = tsKeywords[attr];
}
}
return jsKeywords;
}();
var isOperatorChar = /[+\-*&%=<>!?|~^]/;
function readRegexp(stream) {
var escaped = false, next, inSet = false;
while ((next = stream.next()) != null) {
if (!escaped) {
if (next == "/" && !inSet) return;
if (next == "[") inSet = true;
else if (inSet && next == "]") inSet = false;
}
escaped = !escaped && next == "\\";
}
}
// Used as scratch variables to communicate multiple values without
// consing up tons of objects.
var type, content;
function ret(tp, style, cont) {
type = tp; content = cont;
return style;
}
function tokenBase(stream, state) {
var ch = stream.next();
if (ch == '"' || ch == "'") {
state.tokenize = tokenString(ch);
return state.tokenize(stream, state);
} else if (ch == "." && stream.match(/^\d+(?:[eE][+\-]?\d+)?/)) {
return ret("number", "number");
} else if (ch == "." && stream.match("..")) {
return ret("spread", "meta");
} else if (/[\[\]{}\(\),;\:\.]/.test(ch)) {
return ret(ch);
} else if (ch == "=" && stream.eat(">")) {
return ret("=>", "operator");
} else if (ch == "0" && stream.eat(/x/i)) {
stream.eatWhile(/[\da-f]/i);
return ret("number", "number");
} else if (/\d/.test(ch)) {
stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/);
return ret("number", "number");
} else if (ch == "/") {
if (stream.eat("*")) {
state.tokenize = tokenComment;
return tokenComment(stream, state);
} else if (stream.eat("/")) {
stream.skipToEnd();
return ret("comment", "comment");
} else if (state.lastType == "operator" || state.lastType == "keyword c" ||
state.lastType == "sof" || /^[\[{}\(,;:]$/.test(state.lastType)) {
readRegexp(stream);
stream.eatWhile(/[gimy]/); // 'y' is "sticky" option in Mozilla
return ret("regexp", "string-2");
} else {
stream.eatWhile(isOperatorChar);
return ret("operator", "operator", stream.current());
}
} else if (ch == "`") {
state.tokenize = tokenQuasi;
return tokenQuasi(stream, state);
} else if (ch == "#") {
stream.skipToEnd();
return ret("error", "error");
} else if (isOperatorChar.test(ch)) {
stream.eatWhile(isOperatorChar);
return ret("operator", "operator", stream.current());
} else {
stream.eatWhile(/[\w\$_]/);
var word = stream.current(), known = keywords.propertyIsEnumerable(word) && keywords[word];
return (known && state.lastType != ".") ? ret(known.type, known.style, word) :
ret("variable", "variable", word);
}
}
function tokenString(quote) {
return function(stream, state) {
var escaped = false, next;
while ((next = stream.next()) != null) {
if (next == quote && !escaped) break;
escaped = !escaped && next == "\\";
}
if (!escaped) state.tokenize = tokenBase;
return ret("string", "string");
};
}
function tokenComment(stream, state) {
var maybeEnd = false, ch;
while (ch = stream.next()) {
if (ch == "/" && maybeEnd) {
state.tokenize = tokenBase;
break;
}
maybeEnd = (ch == "*");
}
return ret("comment", "comment");
}
function tokenQuasi(stream, state) {
var escaped = false, next;
while ((next = stream.next()) != null) {
if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) {
state.tokenize = tokenBase;
break;
}
escaped = !escaped && next == "\\";
}
return ret("quasi", "string-2", stream.current());
}
var brackets = "([{}])";
// This is a crude lookahead trick to try and notice that we're
// parsing the argument patterns for a fat-arrow function before we
// actually hit the arrow token. It only works if the arrow is on
// the same line as the arguments and there's no strange noise
// (comments) in between. Fallback is to only notice when we hit the
// arrow, and not declare the arguments as locals for the arrow
// body.
function findFatArrow(stream, state) {
if (state.fatArrowAt) state.fatArrowAt = null;
var arrow = stream.string.indexOf("=>", stream.start);
if (arrow < 0) return;
var depth = 0, sawSomething = false;
for (var pos = arrow - 1; pos >= 0; --pos) {
var ch = stream.string.charAt(pos);
var bracket = brackets.indexOf(ch);
if (bracket >= 0 && bracket < 3) {
if (!depth) { ++pos; break; }
if (--depth == 0) break;
} else if (bracket >= 3 && bracket < 6) {
++depth;
} else if (/[$\w]/.test(ch)) {
sawSomething = true;
} else if (sawSomething && !depth) {
++pos;
break;
}
}
if (sawSomething && !depth) state.fatArrowAt = pos;
}
// Parser
var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true};
function JSLexical(indented, column, type, align, prev, info) {
this.indented = indented;
this.column = column;
this.type = type;
this.prev = prev;
this.info = info;
if (align != null) this.align = align;
}
function inScope(state, varname) {
for (var v = state.localVars; v; v = v.next)
if (v.name == varname) return true;
for (var cx = state.context; cx; cx = cx.prev) {
for (var v = cx.vars; v; v = v.next)
if (v.name == varname) return true;
}
}
function parseJS(state, style, type, content, stream) {
var cc = state.cc;
// Communicate our context to the combinators.
// (Less wasteful than consing up a hundred closures on every call.)
cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc;
if (!state.lexical.hasOwnProperty("align"))
state.lexical.align = true;
while(true) {
var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement;
if (combinator(type, content)) {
while(cc.length && cc[cc.length - 1].lex)
cc.pop()();
if (cx.marked) return cx.marked;
if (type == "variable" && inScope(state, content)) return "variable-2";
return style;
}
}
}
// Combinator utils
var cx = {state: null, column: null, marked: null, cc: null};
function pass() {
for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]);
}
function cont() {
pass.apply(null, arguments);
return true;
}
function register(varname) {
function inList(list) {
for (var v = list; v; v = v.next)
if (v.name == varname) return true;
return false;
}
var state = cx.state;
if (state.context) {
cx.marked = "def";
if (inList(state.localVars)) return;
state.localVars = {name: varname, next: state.localVars};
} else {
if (inList(state.globalVars)) return;
if (parserConfig.globalVars)
state.globalVars = {name: varname, next: state.globalVars};
}
}
// Combinators
var defaultVars = {name: "this", next: {name: "arguments"}};
function pushcontext() {
cx.state.context = {prev: cx.state.context, vars: cx.state.localVars};
cx.state.localVars = defaultVars;
}
function popcontext() {
cx.state.localVars = cx.state.context.vars;
cx.state.context = cx.state.context.prev;
}
function pushlex(type, info) {
var result = function() {
var state = cx.state, indent = state.indented;
if (state.lexical.type == "stat") indent = state.lexical.indented;
state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info);
};
result.lex = true;
return result;
}
function poplex() {
var state = cx.state;
if (state.lexical.prev) {
if (state.lexical.type == ")")
state.indented = state.lexical.indented;
state.lexical = state.lexical.prev;
}
}
poplex.lex = true;
function expect(wanted) {
return function(type) {
if (type == wanted) return cont();
else if (wanted == ";") return pass();
else return cont(arguments.callee);
};
}
function statement(type, value) {
if (type == "var") return cont(pushlex("vardef", value.length), vardef, expect(";"), poplex);
if (type == "keyword a") return cont(pushlex("form"), expression, statement, poplex);
if (type == "keyword b") return cont(pushlex("form"), statement, poplex);
if (type == "{") return cont(pushlex("}"), block, poplex);
if (type == ";") return cont();
if (type == "if") return cont(pushlex("form"), expression, statement, poplex, maybeelse);
if (type == "function") return cont(functiondef);
if (type == "for") return cont(pushlex("form"), forspec, statement, poplex);
if (type == "variable") return cont(pushlex("stat"), maybelabel);
if (type == "switch") return cont(pushlex("form"), expression, pushlex("}", "switch"), expect("{"),
block, poplex, poplex);
if (type == "case") return cont(expression, expect(":"));
if (type == "default") return cont(expect(":"));
if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"),
statement, poplex, popcontext);
if (type == "module") return cont(pushlex("form"), pushcontext, afterModule, popcontext, poplex);
if (type == "class") return cont(pushlex("form"), className, objlit, poplex);
if (type == "export") return cont(pushlex("form"), afterExport, poplex);
if (type == "import") return cont(pushlex("form"), afterImport, poplex);
return pass(pushlex("stat"), expression, expect(";"), poplex);
}
function expression(type) {
return expressionInner(type, false);
}
function expressionNoComma(type) {
return expressionInner(type, true);
}
function expressionInner(type, noComma) {
if (cx.state.fatArrowAt == cx.stream.start) {
var body = noComma ? arrowBodyNoComma : arrowBody;
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(pattern, ")"), poplex, expect("=>"), body, popcontext);
else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext);
}
var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma;
if (atomicTypes.hasOwnProperty(type)) return cont(maybeop);
if (type == "function") return cont(functiondef);
if (type == "keyword c") return cont(noComma ? maybeexpressionNoComma : maybeexpression);
if (type == "(") return cont(pushlex(")"), maybeexpression, comprehension, expect(")"), poplex, maybeop);
if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression);
if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop);
if (type == "{") return contCommasep(objprop, "}", null, maybeop);
return cont();
}
function maybeexpression(type) {
if (type.match(/[;\}\)\],]/)) return pass();
return pass(expression);
}
function maybeexpressionNoComma(type) {
if (type.match(/[;\}\)\],]/)) return pass();
return pass(expressionNoComma);
}
function maybeoperatorComma(type, value) {
if (type == ",") return cont(expression);
return maybeoperatorNoComma(type, value, false);
}
function maybeoperatorNoComma(type, value, noComma) {
var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma;
var expr = noComma == false ? expression : expressionNoComma;
if (value == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext);
if (type == "operator") {
if (/\+\+|--/.test(value)) return cont(me);
if (value == "?") return cont(expression, expect(":"), expr);
return cont(expr);
}
if (type == "quasi") { cx.cc.push(me); return quasi(value); }
if (type == ";") return;
if (type == "(") return contCommasep(expressionNoComma, ")", "call", me);
if (type == ".") return cont(property, me);
if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me);
}
function quasi(value) {
if (value.slice(value.length - 2) != "${") return cont();
return cont(expression, continueQuasi);
}
function continueQuasi(type) {
if (type == "}") {
cx.marked = "string-2";
cx.state.tokenize = tokenQuasi;
return cont();
}
}
function arrowBody(type) {
findFatArrow(cx.stream, cx.state);
if (type == "{") return pass(statement);
return pass(expression);
}
function arrowBodyNoComma(type) {
findFatArrow(cx.stream, cx.state);
if (type == "{") return pass(statement);
return pass(expressionNoComma);
}
function maybelabel(type) {
if (type == ":") return cont(poplex, statement);
return pass(maybeoperatorComma, expect(";"), poplex);
}
function property(type) {
if (type == "variable") {cx.marked = "property"; return cont();}
}
function objprop(type, value) {
if (type == "variable") {
cx.marked = "property";
if (value == "get" || value == "set") return cont(getterSetter);
} else if (type == "number" || type == "string") {
cx.marked = type + " property";
} else if (type == "[") {
return cont(expression, expect("]"), afterprop);
}
if (atomicTypes.hasOwnProperty(type)) return cont(afterprop);
}
function getterSetter(type) {
if (type != "variable") return pass(afterprop);
cx.marked = "property";
return cont(functiondef);
}
function afterprop(type) {
if (type == ":") return cont(expressionNoComma);
if (type == "(") return pass(functiondef);
}
function commasep(what, end) {
function proceed(type) {
if (type == ",") {
var lex = cx.state.lexical;
if (lex.info == "call") lex.pos = (lex.pos || 0) + 1;
return cont(what, proceed);
}
if (type == end) return cont();
return cont(expect(end));
}
return function(type) {
if (type == end) return cont();
return pass(what, proceed);
};
}
function contCommasep(what, end, info) {
for (var i = 3; i < arguments.length; i++)
cx.cc.push(arguments[i]);
return cont(pushlex(end, info), commasep(what, end), poplex);
}
function block(type) {
if (type == "}") return cont();
return pass(statement, block);
}
function maybetype(type) {
if (isTS && type == ":") return cont(typedef);
}
function typedef(type) {
if (type == "variable"){cx.marked = "variable-3"; return cont();}
}
function vardef() {
return pass(pattern, maybetype, maybeAssign, vardefCont);
}
function pattern(type, value) {
if (type == "variable") { register(value); return cont(); }
if (type == "[") return contCommasep(pattern, "]");
if (type == "{") return contCommasep(proppattern, "}");
}
function proppattern(type, value) {
if (type == "variable" && !cx.stream.match(/^\s*:/, false)) {
register(value);
return cont(maybeAssign);
}
if (type == "variable") cx.marked = "property";
return cont(expect(":"), pattern, maybeAssign);
}
function maybeAssign(_type, value) {
if (value == "=") return cont(expressionNoComma);
}
function vardefCont(type) {
if (type == ",") return cont(vardef);
}
function maybeelse(type, value) {
if (type == "keyword b" && value == "else") return cont(pushlex("form"), statement, poplex);
}
function forspec(type) {
if (type == "(") return cont(pushlex(")"), forspec1, expect(")"), poplex);
}
function forspec1(type) {
if (type == "var") return cont(vardef, expect(";"), forspec2);
if (type == ";") return cont(forspec2);
if (type == "variable") return cont(formaybeinof);
return pass(expression, expect(";"), forspec2);
}
function formaybeinof(_type, value) {
if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); }
return cont(maybeoperatorComma, forspec2);
}
function forspec2(type, value) {
if (type == ";") return cont(forspec3);
if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); }
return pass(expression, expect(";"), forspec3);
}
function forspec3(type) {
if (type != ")") cont(expression);
}
function functiondef(type, value) {
if (value == "*") {cx.marked = "keyword"; return cont(functiondef);}
if (type == "variable") {register(value); return cont(functiondef);}
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, statement, popcontext);
}
function funarg(type) {
if (type == "spread") return cont(funarg);
return pass(pattern, maybetype);
}
function className(type, value) {
if (type == "variable") {register(value); return cont(classNameAfter);}
}
function classNameAfter(_type, value) {
if (value == "extends") return cont(expression);
}
function objlit(type) {
if (type == "{") return contCommasep(objprop, "}");
}
function afterModule(type, value) {
if (type == "string") return cont(statement);
if (type == "variable") { register(value); return cont(maybeFrom); }
}
function afterExport(_type, value) {
if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); }
if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); }
return pass(statement);
}
function afterImport(type) {
if (type == "string") return cont();
return pass(importSpec, maybeFrom);
}
function importSpec(type, value) {
if (type == "{") return contCommasep(importSpec, "}");
if (type == "variable") register(value);
return cont();
}
function maybeFrom(_type, value) {
if (value == "from") { cx.marked = "keyword"; return cont(expression); }
}
function arrayLiteral(type) {
if (type == "]") return cont();
return pass(expressionNoComma, maybeArrayComprehension);
}
function maybeArrayComprehension(type) {
if (type == "for") return pass(comprehension, expect("]"));
if (type == ",") return cont(commasep(expressionNoComma, "]"));
return pass(commasep(expressionNoComma, "]"));
}
function comprehension(type) {
if (type == "for") return cont(forspec, comprehension);
if (type == "if") return cont(expression, comprehension);
}
// Interface
return {
startState: function(basecolumn) {
var state = {
tokenize: tokenBase,
lastType: "sof",
cc: [],
lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false),
localVars: parserConfig.localVars,
context: parserConfig.localVars && {vars: parserConfig.localVars},
indented: 0
};
if (parserConfig.globalVars) state.globalVars = parserConfig.globalVars;
return state;
},
token: function(stream, state) {
if (stream.sol()) {
if (!state.lexical.hasOwnProperty("align"))
state.lexical.align = false;
state.indented = stream.indentation();
findFatArrow(stream, state);
}
if (state.tokenize != tokenComment && stream.eatSpace()) return null;
var style = state.tokenize(stream, state);
if (type == "comment") return style;
state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type;
return parseJS(state, style, type, content, stream);
},
indent: function(state, textAfter) {
if (state.tokenize == tokenComment) return CodeMirror.Pass;
if (state.tokenize != tokenBase) return 0;
var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical;
// Kludge to prevent 'maybelse' from blocking lexical scope pops
for (var i = state.cc.length - 1; i >= 0; --i) {
var c = state.cc[i];
if (c == poplex) lexical = lexical.prev;
else if (c != maybeelse) break;
}
if (lexical.type == "stat" && firstChar == "}") lexical = lexical.prev;
if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat")
lexical = lexical.prev;
var type = lexical.type, closing = firstChar == type;
if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info + 1 : 0);
else if (type == "form" && firstChar == "{") return lexical.indented;
else if (type == "form") return lexical.indented + indentUnit;
else if (type == "stat")
return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? statementIndent || indentUnit : 0);
else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false)
return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit);
else if (lexical.align) return lexical.column + (closing ? 0 : 1);
else return lexical.indented + (closing ? 0 : indentUnit);
},
electricChars: ":{}",
blockCommentStart: jsonMode ? null : "/*",
blockCommentEnd: jsonMode ? null : "*/",
lineComment: jsonMode ? null : "//",
fold: "brace",
helperType: jsonMode ? "json" : "javascript",
jsonMode: jsonMode
};
});
CodeMirror.defineMIME("text/javascript", "javascript");
CodeMirror.defineMIME("text/ecmascript", "javascript");
CodeMirror.defineMIME("application/javascript", "javascript");
CodeMirror.defineMIME("application/ecmascript", "javascript");
CodeMirror.defineMIME("application/json", {name: "javascript", json: true});
CodeMirror.defineMIME("application/x-json", {name: "javascript", json: true});
CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true });
CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true });

View File

@@ -0,0 +1,91 @@
// Highlighting text that matches the selection
//
// Defines an option highlightSelectionMatches, which, when enabled,
// will style strings that match the selection throughout the
// document.
//
// The option can be set to true to simply enable it, or to a
// {minChars, style, showToken} object to explicitly configure it.
// minChars is the minimum amount of characters that should be
// selected for the behavior to occur, and style is the token style to
// apply to the matches. This will be prefixed by "cm-" to create an
// actual CSS class name. showToken, when enabled, will cause the
// current token to be highlighted when nothing is selected.
(function() {
var DEFAULT_MIN_CHARS = 2;
var DEFAULT_TOKEN_STYLE = "matchhighlight";
var DEFAULT_DELAY = 100;
function State(options) {
if (typeof options == "object") {
this.minChars = options.minChars;
this.style = options.style;
this.showToken = options.showToken;
this.delay = options.delay;
}
if (this.style == null) this.style = DEFAULT_TOKEN_STYLE;
if (this.minChars == null) this.minChars = DEFAULT_MIN_CHARS;
if (this.delay == null) this.delay = DEFAULT_DELAY;
this.overlay = this.timeout = null;
}
CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) {
if (old && old != CodeMirror.Init) {
var over = cm.state.matchHighlighter.overlay;
if (over) cm.removeOverlay(over);
clearTimeout(cm.state.matchHighlighter.timeout);
cm.state.matchHighlighter = null;
cm.off("cursorActivity", cursorActivity);
}
if (val) {
cm.state.matchHighlighter = new State(val);
highlightMatches(cm);
cm.on("cursorActivity", cursorActivity);
}
});
function cursorActivity(cm) {
var state = cm.state.matchHighlighter;
clearTimeout(state.timeout);
state.timeout = setTimeout(function() {highlightMatches(cm);}, state.delay);
}
function highlightMatches(cm) {
cm.operation(function() {
var state = cm.state.matchHighlighter;
if (state.overlay) {
cm.removeOverlay(state.overlay);
state.overlay = null;
}
if (!cm.somethingSelected() && state.showToken) {
var re = state.showToken === true ? /[\w$]/ : state.showToken;
var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start;
while (start && re.test(line.charAt(start - 1))) --start;
while (end < line.length && re.test(line.charAt(end))) ++end;
if (start < end)
cm.addOverlay(state.overlay = makeOverlay(line.slice(start, end), re, state.style));
return;
}
if (cm.getCursor("head").line != cm.getCursor("anchor").line) return;
var selection = cm.getSelection().replace(/^\s+|\s+$/g, "");
if (selection.length >= state.minChars)
cm.addOverlay(state.overlay = makeOverlay(selection, false, state.style));
});
}
function boundariesAround(stream, re) {
return (!stream.start || !re.test(stream.string.charAt(stream.start - 1))) &&
(stream.pos == stream.string.length || !re.test(stream.string.charAt(stream.pos)));
}
function makeOverlay(query, hasBoundary, style) {
return {token: function(stream) {
if (stream.match(query) &&
(!hasBoundary || boundariesAround(stream, hasBoundary)))
return style;
stream.next();
stream.skipTo(query.charAt(0)) || stream.skipToEnd();
}};
}
})();

View File

@@ -0,0 +1,374 @@
CodeMirror.defineMode("python", function(conf, parserConf) {
var ERRORCLASS = 'error';
function wordRegexp(words) {
return new RegExp("^((" + words.join(")|(") + "))\\b");
}
var singleOperators = parserConf.singleOperators || new RegExp("^[\\+\\-\\*/%&|\\^~<>!]");
var singleDelimiters = parserConf.singleDelimiters || new RegExp('^[\\(\\)\\[\\]\\{\\}@,:`=;\\.]');
var doubleOperators = parserConf.doubleOperators || new RegExp("^((==)|(!=)|(<=)|(>=)|(<>)|(<<)|(>>)|(//)|(\\*\\*))");
var doubleDelimiters = parserConf.doubleDelimiters || new RegExp("^((\\+=)|(\\-=)|(\\*=)|(%=)|(/=)|(&=)|(\\|=)|(\\^=))");
var tripleDelimiters = parserConf.tripleDelimiters || new RegExp("^((//=)|(>>=)|(<<=)|(\\*\\*=))");
var identifiers = parserConf.identifiers|| new RegExp("^[_A-Za-z][_A-Za-z0-9]*");
var hangingIndent = parserConf.hangingIndent || parserConf.indentUnit;
var wordOperators = wordRegexp(['and', 'or', 'not', 'is', 'in']);
var commonkeywords = ['as', 'assert', 'break', 'class', 'continue',
'def', 'del', 'elif', 'else', 'except', 'finally',
'for', 'from', 'global', 'if', 'import',
'lambda', 'pass', 'raise', 'return',
'try', 'while', 'with', 'yield'];
var commonBuiltins = ['abs', 'all', 'any', 'bin', 'bool', 'bytearray', 'callable', 'chr',
'classmethod', 'compile', 'complex', 'delattr', 'dict', 'dir', 'divmod',
'enumerate', 'eval', 'filter', 'float', 'format', 'frozenset',
'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id',
'input', 'int', 'isinstance', 'issubclass', 'iter', 'len',
'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next',
'object', 'oct', 'open', 'ord', 'pow', 'property', 'range',
'repr', 'reversed', 'round', 'set', 'setattr', 'slice',
'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple',
'type', 'vars', 'zip', '__import__', 'NotImplemented',
'Ellipsis', '__debug__'];
var py2 = {'builtins': ['apply', 'basestring', 'buffer', 'cmp', 'coerce', 'execfile',
'file', 'intern', 'long', 'raw_input', 'reduce', 'reload',
'unichr', 'unicode', 'xrange', 'False', 'True', 'None'],
'keywords': ['exec', 'print']};
var py3 = {'builtins': ['ascii', 'bytes', 'exec', 'print'],
'keywords': ['nonlocal', 'False', 'True', 'None']};
if(parserConf.extra_keywords != undefined){
commonkeywords = commonkeywords.concat(parserConf.extra_keywords);
}
if(parserConf.extra_builtins != undefined){
commonBuiltins = commonBuiltins.concat(parserConf.extra_builtins);
}
if (!!parserConf.version && parseInt(parserConf.version, 10) === 3) {
commonkeywords = commonkeywords.concat(py3.keywords);
commonBuiltins = commonBuiltins.concat(py3.builtins);
var stringPrefixes = new RegExp("^(([rb]|(br))?('{3}|\"{3}|['\"]))", "i");
} else {
commonkeywords = commonkeywords.concat(py2.keywords);
commonBuiltins = commonBuiltins.concat(py2.builtins);
var stringPrefixes = new RegExp("^(([rub]|(ur)|(br))?('{3}|\"{3}|['\"]))", "i");
}
var keywords = wordRegexp(commonkeywords);
var builtins = wordRegexp(commonBuiltins);
var indentInfo = null;
// tokenizers
function tokenBase(stream, state) {
// Handle scope changes
if (stream.sol()) {
var scopeOffset = state.scopes[0].offset;
if (stream.eatSpace()) {
var lineOffset = stream.indentation();
if (lineOffset > scopeOffset) {
indentInfo = 'indent';
} else if (lineOffset < scopeOffset) {
indentInfo = 'dedent';
}
return null;
} else {
if (scopeOffset > 0) {
dedent(stream, state);
}
}
}
if (stream.eatSpace()) {
return null;
}
var ch = stream.peek();
// Handle Comments
if (ch === '#') {
stream.skipToEnd();
return 'comment';
}
// Handle Number Literals
if (stream.match(/^[0-9\.]/, false)) {
var floatLiteral = false;
// Floats
if (stream.match(/^\d*\.\d+(e[\+\-]?\d+)?/i)) { floatLiteral = true; }
if (stream.match(/^\d+\.\d*/)) { floatLiteral = true; }
if (stream.match(/^\.\d+/)) { floatLiteral = true; }
if (floatLiteral) {
// Float literals may be "imaginary"
stream.eat(/J/i);
return 'number';
}
// Integers
var intLiteral = false;
// Hex
if (stream.match(/^0x[0-9a-f]+/i)) { intLiteral = true; }
// Binary
if (stream.match(/^0b[01]+/i)) { intLiteral = true; }
// Octal
if (stream.match(/^0o[0-7]+/i)) { intLiteral = true; }
// Decimal
if (stream.match(/^[1-9]\d*(e[\+\-]?\d+)?/)) {
// Decimal literals may be "imaginary"
stream.eat(/J/i);
// TODO - Can you have imaginary longs?
intLiteral = true;
}
// Zero by itself with no other piece of number.
if (stream.match(/^0(?![\dx])/i)) { intLiteral = true; }
if (intLiteral) {
// Integer literals may be "long"
stream.eat(/L/i);
return 'number';
}
}
// Handle Strings
if (stream.match(stringPrefixes)) {
state.tokenize = tokenStringFactory(stream.current());
return state.tokenize(stream, state);
}
// Handle operators and Delimiters
if (stream.match(tripleDelimiters) || stream.match(doubleDelimiters)) {
return null;
}
if (stream.match(doubleOperators)
|| stream.match(singleOperators)
|| stream.match(wordOperators)) {
return 'operator';
}
if (stream.match(singleDelimiters)) {
return null;
}
if (stream.match(keywords)) {
return 'keyword';
}
if (stream.match(builtins)) {
return 'builtin';
}
if (stream.match(identifiers)) {
if (state.lastToken == 'def' || state.lastToken == 'class') {
return 'def';
}
return 'variable';
}
// Handle non-detected items
stream.next();
return ERRORCLASS;
}
function tokenStringFactory(delimiter) {
while ('rub'.indexOf(delimiter.charAt(0).toLowerCase()) >= 0) {
delimiter = delimiter.substr(1);
}
var singleline = delimiter.length == 1;
var OUTCLASS = 'string';
function tokenString(stream, state) {
while (!stream.eol()) {
stream.eatWhile(/[^'"\\]/);
if (stream.eat('\\')) {
stream.next();
if (singleline && stream.eol()) {
return OUTCLASS;
}
} else if (stream.match(delimiter)) {
state.tokenize = tokenBase;
return OUTCLASS;
} else {
stream.eat(/['"]/);
}
}
if (singleline) {
if (parserConf.singleLineStringErrors) {
return ERRORCLASS;
} else {
state.tokenize = tokenBase;
}
}
return OUTCLASS;
}
tokenString.isString = true;
return tokenString;
}
function indent(stream, state, type) {
type = type || 'py';
var indentUnit = 0;
if (type === 'py') {
if (state.scopes[0].type !== 'py') {
state.scopes[0].offset = stream.indentation();
return;
}
for (var i = 0; i < state.scopes.length; ++i) {
if (state.scopes[i].type === 'py') {
indentUnit = state.scopes[i].offset + conf.indentUnit;
break;
}
}
} else if (stream.match(/\s*($|#)/, false)) {
// An open paren/bracket/brace with only space or comments after it
// on the line will indent the next line a fixed amount, to make it
// easier to put arguments, list items, etc. on their own lines.
indentUnit = stream.indentation() + hangingIndent;
} else {
indentUnit = stream.column() + stream.current().length;
}
state.scopes.unshift({
offset: indentUnit,
type: type
});
}
function dedent(stream, state, type) {
type = type || 'py';
if (state.scopes.length == 1) return;
if (state.scopes[0].type === 'py') {
var _indent = stream.indentation();
var _indent_index = -1;
for (var i = 0; i < state.scopes.length; ++i) {
if (_indent === state.scopes[i].offset) {
_indent_index = i;
break;
}
}
if (_indent_index === -1) {
return true;
}
while (state.scopes[0].offset !== _indent) {
state.scopes.shift();
}
return false;
} else {
if (type === 'py') {
state.scopes[0].offset = stream.indentation();
return false;
} else {
if (state.scopes[0].type != type) {
return true;
}
state.scopes.shift();
return false;
}
}
}
function tokenLexer(stream, state) {
indentInfo = null;
var style = state.tokenize(stream, state);
var current = stream.current();
// Handle '.' connected identifiers
if (current === '.') {
style = stream.match(identifiers, false) ? null : ERRORCLASS;
if (style === null && state.lastStyle === 'meta') {
// Apply 'meta' style to '.' connected identifiers when
// appropriate.
style = 'meta';
}
return style;
}
// Handle decorators
if (current === '@') {
return stream.match(identifiers, false) ? 'meta' : ERRORCLASS;
}
if ((style === 'variable' || style === 'builtin')
&& state.lastStyle === 'meta') {
style = 'meta';
}
// Handle scope changes.
if (current === 'pass' || current === 'return') {
state.dedent += 1;
}
if (current === 'lambda') state.lambda = true;
if ((current === ':' && !state.lambda && state.scopes[0].type == 'py')
|| indentInfo === 'indent') {
indent(stream, state);
}
var delimiter_index = '[({'.indexOf(current);
if (delimiter_index !== -1) {
indent(stream, state, '])}'.slice(delimiter_index, delimiter_index+1));
}
if (indentInfo === 'dedent') {
if (dedent(stream, state)) {
return ERRORCLASS;
}
}
delimiter_index = '])}'.indexOf(current);
if (delimiter_index !== -1) {
if (dedent(stream, state, current)) {
return ERRORCLASS;
}
}
if (state.dedent > 0 && stream.eol() && state.scopes[0].type == 'py') {
if (state.scopes.length > 1) state.scopes.shift();
state.dedent -= 1;
}
return style;
}
var external = {
startState: function(basecolumn) {
return {
tokenize: tokenBase,
scopes: [{offset:basecolumn || 0, type:'py'}],
lastStyle: null,
lastToken: null,
lambda: false,
dedent: 0
};
},
token: function(stream, state) {
var style = tokenLexer(stream, state);
state.lastStyle = style;
var current = stream.current();
if (current && style) {
state.lastToken = current;
}
if (stream.eol() && state.lambda) {
state.lambda = false;
}
return style;
},
indent: function(state) {
if (state.tokenize != tokenBase) {
return state.tokenize.isString ? CodeMirror.Pass : 0;
}
return state.scopes[0].offset;
},
lineComment: "#",
fold: "indent"
};
return external;
});
CodeMirror.defineMIME("text/x-python", "python");
(function() {
"use strict";
var words = function(str){return str.split(' ');};
CodeMirror.defineMIME("text/x-cython", {
name: "python",
extra_keywords: words("by cdef cimport cpdef ctypedef enum except"+
"extern gil include nogil property public"+
"readonly struct union DEF IF ELIF ELSE")
});
})();

View File

@@ -0,0 +1,146 @@
// Define search commands. Depends on dialog.js or another
// implementation of the openDialog method.
// Replace works a little oddly -- it will do the replace on the next
// Ctrl-G (or whatever is bound to findNext) press. You prevent a
// replace by making sure the match is no longer selected when hitting
// Ctrl-G.
(function() {
function searchOverlay(query, caseInsensitive) {
var startChar;
if (typeof query == "string") {
startChar = query.charAt(0);
query = new RegExp("^" + query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"),
caseInsensitive ? "i" : "");
} else {
query = new RegExp("^(?:" + query.source + ")", query.ignoreCase ? "i" : "");
}
if (typeof query == "string") return {token: function(stream) {
if (stream.match(query)) return "searching";
stream.next();
stream.skipTo(query.charAt(0)) || stream.skipToEnd();
}};
return {token: function(stream) {
if (stream.match(query)) return "searching";
while (!stream.eol()) {
stream.next();
if (startChar)
stream.skipTo(startChar) || stream.skipToEnd();
if (stream.match(query, false)) break;
}
}};
}
function SearchState() {
this.posFrom = this.posTo = this.query = null;
this.overlay = null;
}
function getSearchState(cm) {
return cm.state.search || (cm.state.search = new SearchState());
}
function queryCaseInsensitive(query) {
return typeof query == "string" && query == query.toLowerCase();
}
function getSearchCursor(cm, query, pos) {
// Heuristic: if the query string is all lowercase, do a case insensitive search.
return cm.getSearchCursor(query, pos, queryCaseInsensitive(query));
}
function dialog(cm, text, shortText, deflt, f) {
if (cm.openDialog) cm.openDialog(text, f, {value: deflt});
else f(prompt(shortText, deflt));
}
function confirmDialog(cm, text, shortText, fs) {
if (cm.openConfirm) cm.openConfirm(text, fs);
else if (confirm(shortText)) fs[0]();
}
function parseQuery(query) {
var isRE = query.match(/^\/(.*)\/([a-z]*)$/);
return isRE ? new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i") : query;
}
var queryDialog =
'Search: <input type="text" style="width: 10em"/> <span style="color: #888">(Use /re/ syntax for regexp search)</span>';
function doSearch(cm, rev) {
var state = getSearchState(cm);
if (state.query) return findNext(cm, rev);
dialog(cm, queryDialog, "Search for:", cm.getSelection(), function(query) {
cm.operation(function() {
if (!query || state.query) return;
state.query = parseQuery(query);
cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
state.overlay = searchOverlay(state.query);
cm.addOverlay(state.overlay);
state.posFrom = state.posTo = cm.getCursor();
findNext(cm, rev);
});
});
}
function findNext(cm, rev) {cm.operation(function() {
var state = getSearchState(cm);
var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo);
if (!cursor.find(rev)) {
cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0));
if (!cursor.find(rev)) return;
}
cm.setSelection(cursor.from(), cursor.to());
cm.scrollIntoView({from: cursor.from(), to: cursor.to()});
state.posFrom = cursor.from(); state.posTo = cursor.to();
});}
function clearSearch(cm) {cm.operation(function() {
var state = getSearchState(cm);
if (!state.query) return;
state.query = null;
cm.removeOverlay(state.overlay);
});}
var replaceQueryDialog =
'Replace: <input type="text" style="width: 10em"/> <span style="color: #888">(Use /re/ syntax for regexp search)</span>';
var replacementQueryDialog = 'With: <input type="text" style="width: 10em"/>';
var doReplaceConfirm = "Replace? <button>Yes</button> <button>No</button> <button>Stop</button>";
function replace(cm, all) {
dialog(cm, replaceQueryDialog, "Replace:", cm.getSelection(), function(query) {
if (!query) return;
query = parseQuery(query);
dialog(cm, replacementQueryDialog, "Replace with:", "", function(text) {
if (all) {
cm.operation(function() {
for (var cursor = getSearchCursor(cm, query); cursor.findNext();) {
if (typeof query != "string") {
var match = cm.getRange(cursor.from(), cursor.to()).match(query);
cursor.replace(text.replace(/\$(\d)/, function(_, i) {return match[i];}));
} else cursor.replace(text);
}
});
} else {
clearSearch(cm);
var cursor = getSearchCursor(cm, query, cm.getCursor());
var advance = function() {
var start = cursor.from(), match;
if (!(match = cursor.findNext())) {
cursor = getSearchCursor(cm, query);
if (!(match = cursor.findNext()) ||
(start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return;
}
cm.setSelection(cursor.from(), cursor.to());
cm.scrollIntoView({from: cursor.from(), to: cursor.to()});
confirmDialog(cm, doReplaceConfirm, "Replace?",
[function() {doReplace(match);}, advance]);
};
var doReplace = function(match) {
cursor.replace(typeof query == "string" ? text :
text.replace(/\$(\d)/, function(_, i) {return match[i];}));
advance();
};
advance();
}
});
});
}
CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);};
CodeMirror.commands.findNext = doSearch;
CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);};
CodeMirror.commands.clearSearch = clearSearch;
CodeMirror.commands.replace = replace;
CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);};
})();

View File

@@ -0,0 +1,167 @@
(function(){
var Pos = CodeMirror.Pos;
function SearchCursor(doc, query, pos, caseFold) {
this.atOccurrence = false; this.doc = doc;
if (caseFold == null && typeof query == "string") caseFold = false;
pos = pos ? doc.clipPos(pos) : Pos(0, 0);
this.pos = {from: pos, to: pos};
// The matches method is filled in based on the type of query.
// It takes a position and a direction, and returns an object
// describing the next occurrence of the query, or null if no
// more matches were found.
if (typeof query != "string") { // Regexp match
if (!query.global) query = new RegExp(query.source, query.ignoreCase ? "ig" : "g");
this.matches = function(reverse, pos) {
if (reverse) {
query.lastIndex = 0;
var line = doc.getLine(pos.line).slice(0, pos.ch), cutOff = 0, match, start;
for (;;) {
query.lastIndex = cutOff;
var newMatch = query.exec(line);
if (!newMatch) break;
match = newMatch;
start = match.index;
cutOff = match.index + (match[0].length || 1);
if (cutOff == line.length) break;
}
var matchLen = (match && match[0].length) || 0;
if (!matchLen) {
if (start == 0 && line.length == 0) {match = undefined;}
else if (start != doc.getLine(pos.line).length) {
matchLen++;
}
}
} else {
query.lastIndex = pos.ch;
var line = doc.getLine(pos.line), match = query.exec(line);
var matchLen = (match && match[0].length) || 0;
var start = match && match.index;
if (start + matchLen != line.length && !matchLen) matchLen = 1;
}
if (match && matchLen)
return {from: Pos(pos.line, start),
to: Pos(pos.line, start + matchLen),
match: match};
};
} else { // String query
var origQuery = query;
if (caseFold) query = query.toLowerCase();
var fold = caseFold ? function(str){return str.toLowerCase();} : function(str){return str;};
var target = query.split("\n");
// Different methods for single-line and multi-line queries
if (target.length == 1) {
if (!query.length) {
// Empty string would match anything and never progress, so
// we define it to match nothing instead.
this.matches = function() {};
} else {
this.matches = function(reverse, pos) {
if (reverse) {
var orig = doc.getLine(pos.line).slice(0, pos.ch), line = fold(orig);
var match = line.lastIndexOf(query);
if (match > -1) {
match = adjustPos(orig, line, match);
return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
}
} else {
var orig = doc.getLine(pos.line).slice(pos.ch), line = fold(orig);
var match = line.indexOf(query);
if (match > -1) {
match = adjustPos(orig, line, match) + pos.ch;
return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
}
}
};
}
} else {
var origTarget = origQuery.split("\n");
this.matches = function(reverse, pos) {
var last = target.length - 1;
if (reverse) {
if (pos.line - (target.length - 1) < doc.firstLine()) return;
if (fold(doc.getLine(pos.line).slice(0, origTarget[last].length)) != target[target.length - 1]) return;
var to = Pos(pos.line, origTarget[last].length);
for (var ln = pos.line - 1, i = last - 1; i >= 1; --i, --ln)
if (target[i] != fold(doc.getLine(ln))) return;
var line = doc.getLine(ln), cut = line.length - origTarget[0].length;
if (fold(line.slice(cut)) != target[0]) return;
return {from: Pos(ln, cut), to: to};
} else {
if (pos.line + (target.length - 1) > doc.lastLine()) return;
var line = doc.getLine(pos.line), cut = line.length - origTarget[0].length;
if (fold(line.slice(cut)) != target[0]) return;
var from = Pos(pos.line, cut);
for (var ln = pos.line + 1, i = 1; i < last; ++i, ++ln)
if (target[i] != fold(doc.getLine(ln))) return;
if (doc.getLine(ln).slice(0, origTarget[last].length) != target[last]) return;
return {from: from, to: Pos(ln, origTarget[last].length)};
}
};
}
}
}
SearchCursor.prototype = {
findNext: function() {return this.find(false);},
findPrevious: function() {return this.find(true);},
find: function(reverse) {
var self = this, pos = this.doc.clipPos(reverse ? this.pos.from : this.pos.to);
function savePosAndFail(line) {
var pos = Pos(line, 0);
self.pos = {from: pos, to: pos};
self.atOccurrence = false;
return false;
}
for (;;) {
if (this.pos = this.matches(reverse, pos)) {
this.atOccurrence = true;
return this.pos.match || true;
}
if (reverse) {
if (!pos.line) return savePosAndFail(0);
pos = Pos(pos.line-1, this.doc.getLine(pos.line-1).length);
}
else {
var maxLine = this.doc.lineCount();
if (pos.line == maxLine - 1) return savePosAndFail(maxLine);
pos = Pos(pos.line + 1, 0);
}
}
},
from: function() {if (this.atOccurrence) return this.pos.from;},
to: function() {if (this.atOccurrence) return this.pos.to;},
replace: function(newText) {
if (!this.atOccurrence) return;
var lines = CodeMirror.splitLines(newText);
this.doc.replaceRange(lines, this.pos.from, this.pos.to);
this.pos.to = Pos(this.pos.from.line + lines.length - 1,
lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0));
}
};
// Maps a position in a case-folded line back to a position in the original line
// (compensating for codepoints increasing in number during folding)
function adjustPos(orig, folded, pos) {
if (orig.length == folded.length) return pos;
for (var pos1 = Math.min(pos, orig.length);;) {
var len1 = orig.slice(0, pos1).toLowerCase().length;
if (len1 < pos) ++pos1;
else if (len1 > pos) --pos1;
else return pos1;
}
}
CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) {
return new SearchCursor(this.doc, query, pos, caseFold);
});
CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) {
return new SearchCursor(this, query, pos, caseFold);
});
})();

View File

@@ -0,0 +1,332 @@
CodeMirror.defineMode("xml", function(config, parserConfig) {
var indentUnit = config.indentUnit;
var multilineTagIndentFactor = parserConfig.multilineTagIndentFactor || 1;
var multilineTagIndentPastTag = parserConfig.multilineTagIndentPastTag || true;
var Kludges = parserConfig.htmlMode ? {
autoSelfClosers: {'area': true, 'base': true, 'br': true, 'col': true, 'command': true,
'embed': true, 'frame': true, 'hr': true, 'img': true, 'input': true,
'keygen': true, 'link': true, 'meta': true, 'param': true, 'source': true,
'track': true, 'wbr': true},
implicitlyClosed: {'dd': true, 'li': true, 'optgroup': true, 'option': true, 'p': true,
'rp': true, 'rt': true, 'tbody': true, 'td': true, 'tfoot': true,
'th': true, 'tr': true},
contextGrabbers: {
'dd': {'dd': true, 'dt': true},
'dt': {'dd': true, 'dt': true},
'li': {'li': true},
'option': {'option': true, 'optgroup': true},
'optgroup': {'optgroup': true},
'p': {'address': true, 'article': true, 'aside': true, 'blockquote': true, 'dir': true,
'div': true, 'dl': true, 'fieldset': true, 'footer': true, 'form': true,
'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true,
'header': true, 'hgroup': true, 'hr': true, 'menu': true, 'nav': true, 'ol': true,
'p': true, 'pre': true, 'section': true, 'table': true, 'ul': true},
'rp': {'rp': true, 'rt': true},
'rt': {'rp': true, 'rt': true},
'tbody': {'tbody': true, 'tfoot': true},
'td': {'td': true, 'th': true},
'tfoot': {'tbody': true},
'th': {'td': true, 'th': true},
'thead': {'tbody': true, 'tfoot': true},
'tr': {'tr': true}
},
doNotIndent: {"pre": true},
allowUnquoted: true,
allowMissing: true
} : {
autoSelfClosers: {},
implicitlyClosed: {},
contextGrabbers: {},
doNotIndent: {},
allowUnquoted: false,
allowMissing: false
};
var alignCDATA = parserConfig.alignCDATA;
// Return variables for tokenizers
var tagName, type, setStyle;
function inText(stream, state) {
function chain(parser) {
state.tokenize = parser;
return parser(stream, state);
}
var ch = stream.next();
if (ch == "<") {
if (stream.eat("!")) {
if (stream.eat("[")) {
if (stream.match("CDATA[")) return chain(inBlock("atom", "]]>"));
else return null;
} else if (stream.match("--")) {
return chain(inBlock("comment", "-->"));
} else if (stream.match("DOCTYPE", true, true)) {
stream.eatWhile(/[\w\._\-]/);
return chain(doctype(1));
} else {
return null;
}
} else if (stream.eat("?")) {
stream.eatWhile(/[\w\._\-]/);
state.tokenize = inBlock("meta", "?>");
return "meta";
} else {
var isClose = stream.eat("/");
tagName = "";
var c;
while ((c = stream.eat(/[^\s\u00a0=<>\"\'\/?]/))) tagName += c;
if (!tagName) return "tag error";
type = isClose ? "closeTag" : "openTag";
state.tokenize = inTag;
return "tag";
}
} else if (ch == "&") {
var ok;
if (stream.eat("#")) {
if (stream.eat("x")) {
ok = stream.eatWhile(/[a-fA-F\d]/) && stream.eat(";");
} else {
ok = stream.eatWhile(/[\d]/) && stream.eat(";");
}
} else {
ok = stream.eatWhile(/[\w\.\-:]/) && stream.eat(";");
}
return ok ? "atom" : "error";
} else {
stream.eatWhile(/[^&<]/);
return null;
}
}
function inTag(stream, state) {
var ch = stream.next();
if (ch == ">" || (ch == "/" && stream.eat(">"))) {
state.tokenize = inText;
type = ch == ">" ? "endTag" : "selfcloseTag";
return "tag";
} else if (ch == "=") {
type = "equals";
return null;
} else if (ch == "<") {
state.tokenize = inText;
state.state = baseState;
state.tagName = state.tagStart = null;
var next = state.tokenize(stream, state);
return next ? next + " error" : "error";
} else if (/[\'\"]/.test(ch)) {
state.tokenize = inAttribute(ch);
state.stringStartCol = stream.column();
return state.tokenize(stream, state);
} else {
stream.eatWhile(/[^\s\u00a0=<>\"\']/);
return "word";
}
}
function inAttribute(quote) {
var closure = function(stream, state) {
while (!stream.eol()) {
if (stream.next() == quote) {
state.tokenize = inTag;
break;
}
}
return "string";
};
closure.isInAttribute = true;
return closure;
}
function inBlock(style, terminator) {
return function(stream, state) {
while (!stream.eol()) {
if (stream.match(terminator)) {
state.tokenize = inText;
break;
}
stream.next();
}
return style;
};
}
function doctype(depth) {
return function(stream, state) {
var ch;
while ((ch = stream.next()) != null) {
if (ch == "<") {
state.tokenize = doctype(depth + 1);
return state.tokenize(stream, state);
} else if (ch == ">") {
if (depth == 1) {
state.tokenize = inText;
break;
} else {
state.tokenize = doctype(depth - 1);
return state.tokenize(stream, state);
}
}
}
return "meta";
};
}
function Context(state, tagName, startOfLine) {
this.prev = state.context;
this.tagName = tagName;
this.indent = state.indented;
this.startOfLine = startOfLine;
if (Kludges.doNotIndent.hasOwnProperty(tagName) || (state.context && state.context.noIndent))
this.noIndent = true;
}
function popContext(state) {
if (state.context) state.context = state.context.prev;
}
function maybePopContext(state, nextTagName) {
var parentTagName;
while (true) {
if (!state.context) {
return;
}
parentTagName = state.context.tagName.toLowerCase();
if (!Kludges.contextGrabbers.hasOwnProperty(parentTagName) ||
!Kludges.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) {
return;
}
popContext(state);
}
}
function baseState(type, stream, state) {
if (type == "openTag") {
state.tagName = tagName;
state.tagStart = stream.column();
return attrState;
} else if (type == "closeTag") {
var err = false;
if (state.context) {
if (state.context.tagName != tagName) {
if (Kludges.implicitlyClosed.hasOwnProperty(state.context.tagName.toLowerCase()))
popContext(state);
err = !state.context || state.context.tagName != tagName;
}
} else {
err = true;
}
if (err) setStyle = "error";
return err ? closeStateErr : closeState;
} else {
return baseState;
}
}
function closeState(type, _stream, state) {
if (type != "endTag") {
setStyle = "error";
return closeState;
}
popContext(state);
return baseState;
}
function closeStateErr(type, stream, state) {
setStyle = "error";
return closeState(type, stream, state);
}
function attrState(type, _stream, state) {
if (type == "word") {
setStyle = "attribute";
return attrEqState;
} else if (type == "endTag" || type == "selfcloseTag") {
var tagName = state.tagName, tagStart = state.tagStart;
state.tagName = state.tagStart = null;
if (type == "selfcloseTag" ||
Kludges.autoSelfClosers.hasOwnProperty(tagName.toLowerCase())) {
maybePopContext(state, tagName.toLowerCase());
} else {
maybePopContext(state, tagName.toLowerCase());
state.context = new Context(state, tagName, tagStart == state.indented);
}
return baseState;
}
setStyle = "error";
return attrState;
}
function attrEqState(type, stream, state) {
if (type == "equals") return attrValueState;
if (!Kludges.allowMissing) setStyle = "error";
return attrState(type, stream, state);
}
function attrValueState(type, stream, state) {
if (type == "string") return attrContinuedState;
if (type == "word" && Kludges.allowUnquoted) {setStyle = "string"; return attrState;}
setStyle = "error";
return attrState(type, stream, state);
}
function attrContinuedState(type, stream, state) {
if (type == "string") return attrContinuedState;
return attrState(type, stream, state);
}
return {
startState: function() {
return {tokenize: inText,
state: baseState,
indented: 0,
tagName: null, tagStart: null,
context: null};
},
token: function(stream, state) {
if (!state.tagName && stream.sol())
state.indented = stream.indentation();
if (stream.eatSpace()) return null;
tagName = type = null;
var style = state.tokenize(stream, state);
if ((style || type) && style != "comment") {
setStyle = null;
state.state = state.state(type || style, stream, state);
if (setStyle)
style = setStyle == "error" ? style + " error" : setStyle;
}
return style;
},
indent: function(state, textAfter, fullLine) {
var context = state.context;
// Indent multi-line strings (e.g. css).
if (state.tokenize.isInAttribute) {
return state.stringStartCol + 1;
}
if (context && context.noIndent) return CodeMirror.Pass;
if (state.tokenize != inTag && state.tokenize != inText)
return fullLine ? fullLine.match(/^(\s*)/)[0].length : 0;
// Indent the starts of attribute names.
if (state.tagName) {
if (multilineTagIndentPastTag)
return state.tagStart + state.tagName.length + 2;
else
return state.tagStart + indentUnit * multilineTagIndentFactor;
}
if (alignCDATA && /<!\[CDATA\[/.test(textAfter)) return 0;
if (context && /^<\//.test(textAfter))
context = context.prev;
while (context && !context.startOfLine)
context = context.prev;
if (context) return context.indent + indentUnit;
else return 0;
},
electricChars: "/",
blockCommentStart: "<!--",
blockCommentEnd: "-->",
configuration: parserConfig.htmlMode ? "html" : "xml",
helperType: parserConfig.htmlMode ? "html" : "xml"
};
});
CodeMirror.defineMIME("text/xml", "xml");
CodeMirror.defineMIME("application/xml", "xml");
if (!CodeMirror.mimeModes.hasOwnProperty("text/html"))
CodeMirror.defineMIME("text/html", {name: "xml", htmlMode: true});

View File

@@ -0,0 +1,97 @@
CodeMirror.defineMode("yaml", function() {
var cons = ['true', 'false', 'on', 'off', 'yes', 'no'];
var keywordRegex = new RegExp("\\b(("+cons.join(")|(")+"))$", 'i');
return {
token: function(stream, state) {
var ch = stream.peek();
var esc = state.escaped;
state.escaped = false;
/* comments */
if (ch == "#" && (stream.pos == 0 || /\s/.test(stream.string.charAt(stream.pos - 1)))) {
stream.skipToEnd(); return "comment";
}
if (state.literal && stream.indentation() > state.keyCol) {
stream.skipToEnd(); return "string";
} else if (state.literal) { state.literal = false; }
if (stream.sol()) {
state.keyCol = 0;
state.pair = false;
state.pairStart = false;
/* document start */
if(stream.match(/---/)) { return "def"; }
/* document end */
if (stream.match(/\.\.\./)) { return "def"; }
/* array list item */
if (stream.match(/\s*-\s+/)) { return 'meta'; }
}
/* inline pairs/lists */
if (stream.match(/^(\{|\}|\[|\])/)) {
if (ch == '{')
state.inlinePairs++;
else if (ch == '}')
state.inlinePairs--;
else if (ch == '[')
state.inlineList++;
else
state.inlineList--;
return 'meta';
}
/* list seperator */
if (state.inlineList > 0 && !esc && ch == ',') {
stream.next();
return 'meta';
}
/* pairs seperator */
if (state.inlinePairs > 0 && !esc && ch == ',') {
state.keyCol = 0;
state.pair = false;
state.pairStart = false;
stream.next();
return 'meta';
}
/* start of value of a pair */
if (state.pairStart) {
/* block literals */
if (stream.match(/^\s*(\||\>)\s*/)) { state.literal = true; return 'meta'; };
/* references */
if (stream.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i)) { return 'variable-2'; }
/* numbers */
if (state.inlinePairs == 0 && stream.match(/^\s*-?[0-9\.\,]+\s?$/)) { return 'number'; }
if (state.inlinePairs > 0 && stream.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/)) { return 'number'; }
/* keywords */
if (stream.match(keywordRegex)) { return 'keyword'; }
}
/* pairs (associative arrays) -> key */
if (!state.pair && stream.match(/^\s*\S+(?=\s*:($|\s))/i)) {
state.pair = true;
state.keyCol = stream.indentation();
return "atom";
}
if (state.pair && stream.match(/^:\s*/)) { state.pairStart = true; return 'meta'; }
/* nothing found, continue */
state.pairStart = false;
state.escaped = (ch == '\\');
stream.next();
return null;
},
startState: function() {
return {
pair: false,
pairStart: false,
keyCol: 0,
inlinePairs: 0,
inlineList: 0,
literal: false,
escaped: false
};
}
};
});
CodeMirror.defineMIME("text/x-yaml", "yaml");

View File

@@ -0,0 +1,263 @@
/* BASICS */
.CodeMirror {
/* Set height, width, borders, and global font properties here */
font-family: monospace;
height: 300px;
}
.CodeMirror-scroll {
/* Set scrolling behaviour here */
overflow: auto;
}
/* PADDING */
.CodeMirror-lines {
padding: 4px 0; /* Vertical padding around content */
}
.CodeMirror pre {
padding: 0 4px; /* Horizontal padding of content */
}
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
background-color: white; /* The little square between H and V scrollbars */
}
/* GUTTER */
.CodeMirror-gutters {
border-right: 1px solid #ddd;
background-color: #f7f7f7;
white-space: nowrap;
}
.CodeMirror-linenumbers {}
.CodeMirror-linenumber {
padding: 0 3px 0 5px;
min-width: 20px;
text-align: right;
color: #999;
}
/* CURSOR */
.CodeMirror div.CodeMirror-cursor {
border-left: 1px solid black;
z-index: 3;
}
/* Shown when moving in bi-directional text */
.CodeMirror div.CodeMirror-secondarycursor {
border-left: 1px solid silver;
}
.CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursor {
width: auto;
border: 0;
background: #7e7;
z-index: 1;
}
/* Can style cursor different in overwrite (non-insert) mode */
.CodeMirror div.CodeMirror-cursor.CodeMirror-overwrite {}
.cm-tab { display: inline-block; }
/* DEFAULT THEME */
.cm-s-default .cm-keyword {color: #708;}
.cm-s-default .cm-atom {color: #219;}
.cm-s-default .cm-number {color: #164;}
.cm-s-default .cm-def {color: #00f;}
.cm-s-default .cm-variable {color: black;}
.cm-s-default .cm-variable-2 {color: #05a;}
.cm-s-default .cm-variable-3 {color: #085;}
.cm-s-default .cm-property {color: black;}
.cm-s-default .cm-operator {color: black;}
.cm-s-default .cm-comment {color: #a50;}
.cm-s-default .cm-string {color: #a11;}
.cm-s-default .cm-string-2 {color: #f50;}
.cm-s-default .cm-meta {color: #555;}
.cm-s-default .cm-qualifier {color: #555;}
.cm-s-default .cm-builtin {color: #30a;}
.cm-s-default .cm-bracket {color: #997;}
.cm-s-default .cm-tag {color: #170;}
.cm-s-default .cm-attribute {color: #00c;}
.cm-s-default .cm-header {color: blue;}
.cm-s-default .cm-quote {color: #090;}
.cm-s-default .cm-hr {color: #999;}
.cm-s-default .cm-link {color: #00c;}
.cm-negative {color: #d44;}
.cm-positive {color: #292;}
.cm-header, .cm-strong {font-weight: bold;}
.cm-em {font-style: italic;}
.cm-link {text-decoration: underline;}
.cm-s-default .cm-error {color: #f00;}
.cm-invalidchar {color: #f00;}
div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
.CodeMirror-activeline-background {background: #e8f2ff;}
/* STOP */
/* The rest of this file contains styles related to the mechanics of
the editor. You probably shouldn't touch them. */
.CodeMirror {
line-height: 1;
position: relative;
overflow: hidden;
background: white;
color: black;
}
.CodeMirror-scroll {
/* 30px is the magic margin used to hide the element's real scrollbars */
/* See overflow: hidden in .CodeMirror */
margin-bottom: -30px; margin-right: -30px;
padding-bottom: 30px; padding-right: 30px;
height: 100%;
outline: none; /* Prevent dragging from highlighting the element */
position: relative;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.CodeMirror-sizer {
position: relative;
}
/* The fake, visible scrollbars. Used to force redraw during scrolling
before actuall scrolling happens, thus preventing shaking and
flickering artifacts. */
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
position: absolute;
z-index: 6;
display: none;
}
.CodeMirror-vscrollbar {
right: 0; top: 0;
overflow-x: hidden;
overflow-y: scroll;
}
.CodeMirror-hscrollbar {
bottom: 0; left: 0;
overflow-y: hidden;
overflow-x: scroll;
}
.CodeMirror-scrollbar-filler {
right: 0; bottom: 0;
}
.CodeMirror-gutter-filler {
left: 0; bottom: 0;
}
.CodeMirror-gutters {
position: absolute; left: 0; top: 0;
padding-bottom: 30px;
z-index: 3;
}
.CodeMirror-gutter {
white-space: normal;
height: 100%;
-moz-box-sizing: content-box;
box-sizing: content-box;
padding-bottom: 30px;
margin-bottom: -32px;
display: inline-block;
/* Hack to make IE7 behave */
*zoom:1;
*display:inline;
}
.CodeMirror-gutter-elt {
position: absolute;
cursor: default;
z-index: 4;
}
.CodeMirror-lines {
cursor: text;
}
.CodeMirror pre {
/* Reset some styles that the rest of the page might have set */
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
border-width: 0;
background: transparent;
font-family: inherit;
font-size: inherit;
margin: 0;
white-space: pre;
word-wrap: normal;
line-height: inherit;
color: inherit;
z-index: 2;
position: relative;
overflow: visible;
}
.CodeMirror-wrap pre {
word-wrap: break-word;
white-space: pre-wrap;
word-break: normal;
}
.CodeMirror-code pre {
border-right: 30px solid transparent;
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
}
.CodeMirror-wrap .CodeMirror-code pre {
border-right: none;
width: auto;
}
.CodeMirror-linebackground {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
z-index: 0;
}
.CodeMirror-linewidget {
position: relative;
z-index: 2;
overflow: auto;
}
.CodeMirror-widget {}
.CodeMirror-wrap .CodeMirror-scroll {
overflow-x: hidden;
}
.CodeMirror-measure {
position: absolute;
width: 100%;
height: 0;
overflow: hidden;
visibility: hidden;
}
.CodeMirror-measure pre { position: static; }
.CodeMirror div.CodeMirror-cursor {
position: absolute;
visibility: hidden;
border-right: none;
width: 0;
}
.CodeMirror-focused div.CodeMirror-cursor {
visibility: visible;
}
.CodeMirror-selected { background: #d9d9d9; }
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
.cm-searching {
background: #ffa;
background: rgba(255, 255, 0, .4);
}
/* IE7 hack to prevent it from returning funny offsetTops on the spans */
.CodeMirror span { *vertical-align: text-bottom; }
@media print {
/* Hide the cursor when printing */
.CodeMirror div.CodeMirror-cursor {
visibility: hidden;
}
}

View File

@@ -0,0 +1,5828 @@
// CodeMirror version 3.15
//
// CodeMirror is the only global var we claim
window.CodeMirror = (function() {
"use strict";
// BROWSER SNIFFING
// Crude, but necessary to handle a number of hard-to-feature-detect
// bugs and behavior differences.
var gecko = /gecko\/\d/i.test(navigator.userAgent);
var ie = /MSIE \d/.test(navigator.userAgent);
var ie_lt8 = ie && (document.documentMode == null || document.documentMode < 8);
var ie_lt9 = ie && (document.documentMode == null || document.documentMode < 9);
var webkit = /WebKit\//.test(navigator.userAgent);
var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent);
var chrome = /Chrome\//.test(navigator.userAgent);
var opera = /Opera\//.test(navigator.userAgent);
var safari = /Apple Computer/.test(navigator.vendor);
var khtml = /KHTML\//.test(navigator.userAgent);
var mac_geLion = /Mac OS X 1\d\D([7-9]|\d\d)\D/.test(navigator.userAgent);
var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(navigator.userAgent);
var phantom = /PhantomJS/.test(navigator.userAgent);
var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent);
// This is woefully incomplete. Suggestions for alternative methods welcome.
var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent);
var mac = ios || /Mac/.test(navigator.platform);
var windows = /windows/i.test(navigator.platform);
var opera_version = opera && navigator.userAgent.match(/Version\/(\d*\.\d*)/);
if (opera_version) opera_version = Number(opera_version[1]);
if (opera_version && opera_version >= 15) { opera = false; webkit = true; }
// Some browsers use the wrong event properties to signal cmd/ctrl on OS X
var flipCtrlCmd = mac && (qtwebkit || opera && (opera_version == null || opera_version < 12.11));
var captureMiddleClick = gecko || (ie && !ie_lt9);
// Optimize some code when these features are not used
var sawReadOnlySpans = false, sawCollapsedSpans = false;
// CONSTRUCTOR
function CodeMirror(place, options) {
if (!(this instanceof CodeMirror)) return new CodeMirror(place, options);
this.options = options = options || {};
// Determine effective options based on given values and defaults.
for (var opt in defaults) if (!options.hasOwnProperty(opt) && defaults.hasOwnProperty(opt))
options[opt] = defaults[opt];
setGuttersForLineNumbers(options);
var docStart = typeof options.value == "string" ? 0 : options.value.first;
var display = this.display = makeDisplay(place, docStart);
display.wrapper.CodeMirror = this;
updateGutters(this);
if (options.autofocus && !mobile) focusInput(this);
this.state = {keyMaps: [],
overlays: [],
modeGen: 0,
overwrite: false, focused: false,
suppressEdits: false, pasteIncoming: false,
draggingText: false,
highlight: new Delayed()};
themeChanged(this);
if (options.lineWrapping)
this.display.wrapper.className += " CodeMirror-wrap";
var doc = options.value;
if (typeof doc == "string") doc = new Doc(options.value, options.mode);
operation(this, attachDoc)(this, doc);
// Override magic textarea content restore that IE sometimes does
// on our hidden textarea on reload
if (ie) setTimeout(bind(resetInput, this, true), 20);
registerEventHandlers(this);
// IE throws unspecified error in certain cases, when
// trying to access activeElement before onload
var hasFocus; try { hasFocus = (document.activeElement == display.input); } catch(e) { }
if (hasFocus || (options.autofocus && !mobile)) setTimeout(bind(onFocus, this), 20);
else onBlur(this);
operation(this, function() {
for (var opt in optionHandlers)
if (optionHandlers.propertyIsEnumerable(opt))
optionHandlers[opt](this, options[opt], Init);
for (var i = 0; i < initHooks.length; ++i) initHooks[i](this);
})();
}
// DISPLAY CONSTRUCTOR
function makeDisplay(place, docStart) {
var d = {};
var input = d.input = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none; font-size: 4px;");
if (webkit) input.style.width = "1000px";
else input.setAttribute("wrap", "off");
// if border: 0; -- iOS fails to open keyboard (issue #1287)
if (ios) input.style.border = "1px solid black";
input.setAttribute("autocorrect", "off"); input.setAttribute("autocapitalize", "off"); input.setAttribute("spellcheck", "false");
// Wraps and hides input textarea
d.inputDiv = elt("div", [input], null, "overflow: hidden; position: relative; width: 3px; height: 0px;");
// The actual fake scrollbars.
d.scrollbarH = elt("div", [elt("div", null, null, "height: 1px")], "CodeMirror-hscrollbar");
d.scrollbarV = elt("div", [elt("div", null, null, "width: 1px")], "CodeMirror-vscrollbar");
d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler");
d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler");
// DIVs containing the selection and the actual code
d.lineDiv = elt("div", null, "CodeMirror-code");
d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1");
// Blinky cursor, and element used to ensure cursor fits at the end of a line
d.cursor = elt("div", "\u00a0", "CodeMirror-cursor");
// Secondary cursor, shown when on a 'jump' in bi-directional text
d.otherCursor = elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor");
// Used to measure text size
d.measure = elt("div", null, "CodeMirror-measure");
// Wraps everything that needs to exist inside the vertically-padded coordinate system
d.lineSpace = elt("div", [d.measure, d.selectionDiv, d.lineDiv, d.cursor, d.otherCursor],
null, "position: relative; outline: none");
// Moved around its parent to cover visible view
d.mover = elt("div", [elt("div", [d.lineSpace], "CodeMirror-lines")], null, "position: relative");
// Set to the height of the text, causes scrolling
d.sizer = elt("div", [d.mover], "CodeMirror-sizer");
// D is needed because behavior of elts with overflow: auto and padding is inconsistent across browsers
d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerCutOff + "px; width: 1px;");
// Will contain the gutters, if any
d.gutters = elt("div", null, "CodeMirror-gutters");
d.lineGutter = null;
// Provides scrolling
d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll");
d.scroller.setAttribute("tabIndex", "-1");
// The element in which the editor lives.
d.wrapper = elt("div", [d.inputDiv, d.scrollbarH, d.scrollbarV,
d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror");
// Work around IE7 z-index bug
if (ie_lt8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; }
if (place.appendChild) place.appendChild(d.wrapper); else place(d.wrapper);
// Needed to hide big blue blinking cursor on Mobile Safari
if (ios) input.style.width = "0px";
if (!webkit) d.scroller.draggable = true;
// Needed to handle Tab key in KHTML
if (khtml) { d.inputDiv.style.height = "1px"; d.inputDiv.style.position = "absolute"; }
// Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8).
else if (ie_lt8) d.scrollbarH.style.minWidth = d.scrollbarV.style.minWidth = "18px";
// Current visible range (may be bigger than the view window).
d.viewOffset = d.lastSizeC = 0;
d.showingFrom = d.showingTo = docStart;
// Used to only resize the line number gutter when necessary (when
// the amount of lines crosses a boundary that makes its width change)
d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null;
// See readInput and resetInput
d.prevInput = "";
// Set to true when a non-horizontal-scrolling widget is added. As
// an optimization, widget aligning is skipped when d is false.
d.alignWidgets = false;
// Flag that indicates whether we currently expect input to appear
// (after some event like 'keypress' or 'input') and are polling
// intensively.
d.pollingFast = false;
// Self-resetting timeout for the poller
d.poll = new Delayed();
d.cachedCharWidth = d.cachedTextHeight = null;
d.measureLineCache = [];
d.measureLineCachePos = 0;
// Tracks when resetInput has punted to just putting a short
// string instead of the (large) selection.
d.inaccurateSelection = false;
// Tracks the maximum line length so that the horizontal scrollbar
// can be kept static when scrolling.
d.maxLine = null;
d.maxLineLength = 0;
d.maxLineChanged = false;
// Used for measuring wheel scrolling granularity
d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null;
return d;
}
// STATE UPDATES
// Used to get the editor into a consistent state again when options change.
function loadMode(cm) {
cm.doc.mode = CodeMirror.getMode(cm.options, cm.doc.modeOption);
cm.doc.iter(function(line) {
if (line.stateAfter) line.stateAfter = null;
if (line.styles) line.styles = null;
});
cm.doc.frontier = cm.doc.first;
startWorker(cm, 100);
cm.state.modeGen++;
if (cm.curOp) regChange(cm);
}
function wrappingChanged(cm) {
if (cm.options.lineWrapping) {
cm.display.wrapper.className += " CodeMirror-wrap";
cm.display.sizer.style.minWidth = "";
} else {
cm.display.wrapper.className = cm.display.wrapper.className.replace(" CodeMirror-wrap", "");
computeMaxLength(cm);
}
estimateLineHeights(cm);
regChange(cm);
clearCaches(cm);
setTimeout(function(){updateScrollbars(cm);}, 100);
}
function estimateHeight(cm) {
var th = textHeight(cm.display), wrapping = cm.options.lineWrapping;
var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3);
return function(line) {
if (lineIsHidden(cm.doc, line))
return 0;
else if (wrapping)
return (Math.ceil(line.text.length / perLine) || 1) * th;
else
return th;
};
}
function estimateLineHeights(cm) {
var doc = cm.doc, est = estimateHeight(cm);
doc.iter(function(line) {
var estHeight = est(line);
if (estHeight != line.height) updateLineHeight(line, estHeight);
});
}
function keyMapChanged(cm) {
var map = keyMap[cm.options.keyMap], style = map.style;
cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-keymap-\S+/g, "") +
(style ? " cm-keymap-" + style : "");
cm.state.disableInput = map.disableInput;
}
function themeChanged(cm) {
cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") +
cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-");
clearCaches(cm);
}
function guttersChanged(cm) {
updateGutters(cm);
regChange(cm);
setTimeout(function(){alignHorizontally(cm);}, 20);
}
function updateGutters(cm) {
var gutters = cm.display.gutters, specs = cm.options.gutters;
removeChildren(gutters);
for (var i = 0; i < specs.length; ++i) {
var gutterClass = specs[i];
var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass));
if (gutterClass == "CodeMirror-linenumbers") {
cm.display.lineGutter = gElt;
gElt.style.width = (cm.display.lineNumWidth || 1) + "px";
}
}
gutters.style.display = i ? "" : "none";
}
function lineLength(doc, line) {
if (line.height == 0) return 0;
var len = line.text.length, merged, cur = line;
while (merged = collapsedSpanAtStart(cur)) {
var found = merged.find();
cur = getLine(doc, found.from.line);
len += found.from.ch - found.to.ch;
}
cur = line;
while (merged = collapsedSpanAtEnd(cur)) {
var found = merged.find();
len -= cur.text.length - found.from.ch;
cur = getLine(doc, found.to.line);
len += cur.text.length - found.to.ch;
}
return len;
}
function computeMaxLength(cm) {
var d = cm.display, doc = cm.doc;
d.maxLine = getLine(doc, doc.first);
d.maxLineLength = lineLength(doc, d.maxLine);
d.maxLineChanged = true;
doc.iter(function(line) {
var len = lineLength(doc, line);
if (len > d.maxLineLength) {
d.maxLineLength = len;
d.maxLine = line;
}
});
}
// Make sure the gutters options contains the element
// "CodeMirror-linenumbers" when the lineNumbers option is true.
function setGuttersForLineNumbers(options) {
var found = false;
for (var i = 0; i < options.gutters.length; ++i) {
if (options.gutters[i] == "CodeMirror-linenumbers") {
if (options.lineNumbers) found = true;
else options.gutters.splice(i--, 1);
}
}
if (!found && options.lineNumbers)
options.gutters.push("CodeMirror-linenumbers");
}
// SCROLLBARS
// Re-synchronize the fake scrollbars with the actual size of the
// content. Optionally force a scrollTop.
function updateScrollbars(cm) {
var d = cm.display, docHeight = cm.doc.height;
var totalHeight = docHeight + paddingVert(d);
d.sizer.style.minHeight = d.heightForcer.style.top = totalHeight + "px";
d.gutters.style.height = Math.max(totalHeight, d.scroller.clientHeight - scrollerCutOff) + "px";
var scrollHeight = Math.max(totalHeight, d.scroller.scrollHeight);
var needsH = d.scroller.scrollWidth > (d.scroller.clientWidth + 1);
var needsV = scrollHeight > (d.scroller.clientHeight + 1);
if (needsV) {
d.scrollbarV.style.display = "block";
d.scrollbarV.style.bottom = needsH ? scrollbarWidth(d.measure) + "px" : "0";
d.scrollbarV.firstChild.style.height =
(scrollHeight - d.scroller.clientHeight + d.scrollbarV.clientHeight) + "px";
} else d.scrollbarV.style.display = "";
if (needsH) {
d.scrollbarH.style.display = "block";
d.scrollbarH.style.right = needsV ? scrollbarWidth(d.measure) + "px" : "0";
d.scrollbarH.firstChild.style.width =
(d.scroller.scrollWidth - d.scroller.clientWidth + d.scrollbarH.clientWidth) + "px";
} else d.scrollbarH.style.display = "";
if (needsH && needsV) {
d.scrollbarFiller.style.display = "block";
d.scrollbarFiller.style.height = d.scrollbarFiller.style.width = scrollbarWidth(d.measure) + "px";
} else d.scrollbarFiller.style.display = "";
if (needsH && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) {
d.gutterFiller.style.display = "block";
d.gutterFiller.style.height = scrollbarWidth(d.measure) + "px";
d.gutterFiller.style.width = d.gutters.offsetWidth + "px";
} else d.gutterFiller.style.display = "";
if (mac_geLion && scrollbarWidth(d.measure) === 0)
d.scrollbarV.style.minWidth = d.scrollbarH.style.minHeight = mac_geMountainLion ? "18px" : "12px";
}
function visibleLines(display, doc, viewPort) {
var top = display.scroller.scrollTop, height = display.wrapper.clientHeight;
if (typeof viewPort == "number") top = viewPort;
else if (viewPort) {top = viewPort.top; height = viewPort.bottom - viewPort.top;}
top = Math.floor(top - paddingTop(display));
var bottom = Math.ceil(top + height);
return {from: lineAtHeight(doc, top), to: lineAtHeight(doc, bottom)};
}
// LINE NUMBERS
function alignHorizontally(cm) {
var display = cm.display;
if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) return;
var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft;
var gutterW = display.gutters.offsetWidth, l = comp + "px";
for (var n = display.lineDiv.firstChild; n; n = n.nextSibling) if (n.alignable) {
for (var i = 0, a = n.alignable; i < a.length; ++i) a[i].style.left = l;
}
if (cm.options.fixedGutter)
display.gutters.style.left = (comp + gutterW) + "px";
}
function maybeUpdateLineNumberWidth(cm) {
if (!cm.options.lineNumbers) return false;
var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display;
if (last.length != display.lineNumChars) {
var test = display.measure.appendChild(elt("div", [elt("div", last)],
"CodeMirror-linenumber CodeMirror-gutter-elt"));
var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW;
display.lineGutter.style.width = "";
display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding);
display.lineNumWidth = display.lineNumInnerWidth + padding;
display.lineNumChars = display.lineNumInnerWidth ? last.length : -1;
display.lineGutter.style.width = display.lineNumWidth + "px";
return true;
}
return false;
}
function lineNumberFor(options, i) {
return String(options.lineNumberFormatter(i + options.firstLineNumber));
}
function compensateForHScroll(display) {
return getRect(display.scroller).left - getRect(display.sizer).left;
}
// DISPLAY DRAWING
function updateDisplay(cm, changes, viewPort, forced) {
var oldFrom = cm.display.showingFrom, oldTo = cm.display.showingTo, updated;
var visible = visibleLines(cm.display, cm.doc, viewPort);
for (;;) {
if (!updateDisplayInner(cm, changes, visible, forced)) break;
forced = false;
updated = true;
updateSelection(cm);
updateScrollbars(cm);
// Clip forced viewport to actual scrollable area
if (viewPort)
viewPort = Math.min(cm.display.scroller.scrollHeight - cm.display.scroller.clientHeight,
typeof viewPort == "number" ? viewPort : viewPort.top);
visible = visibleLines(cm.display, cm.doc, viewPort);
if (visible.from >= cm.display.showingFrom && visible.to <= cm.display.showingTo)
break;
changes = [];
}
if (updated) {
signalLater(cm, "update", cm);
if (cm.display.showingFrom != oldFrom || cm.display.showingTo != oldTo)
signalLater(cm, "viewportChange", cm, cm.display.showingFrom, cm.display.showingTo);
}
return updated;
}
// Uses a set of changes plus the current scroll position to
// determine which DOM updates have to be made, and makes the
// updates.
function updateDisplayInner(cm, changes, visible, forced) {
var display = cm.display, doc = cm.doc;
if (!display.wrapper.clientWidth) {
display.showingFrom = display.showingTo = doc.first;
display.viewOffset = 0;
return;
}
// Bail out if the visible area is already rendered and nothing changed.
if (!forced && changes.length == 0 &&
visible.from > display.showingFrom && visible.to < display.showingTo)
return;
if (maybeUpdateLineNumberWidth(cm))
changes = [{from: doc.first, to: doc.first + doc.size}];
var gutterW = display.sizer.style.marginLeft = display.gutters.offsetWidth + "px";
display.scrollbarH.style.left = cm.options.fixedGutter ? gutterW : "0";
// Used to determine which lines need their line numbers updated
var positionsChangedFrom = Infinity;
if (cm.options.lineNumbers)
for (var i = 0; i < changes.length; ++i)
if (changes[i].diff) { positionsChangedFrom = changes[i].from; break; }
var end = doc.first + doc.size;
var from = Math.max(visible.from - cm.options.viewportMargin, doc.first);
var to = Math.min(end, visible.to + cm.options.viewportMargin);
if (display.showingFrom < from && from - display.showingFrom < 20) from = Math.max(doc.first, display.showingFrom);
if (display.showingTo > to && display.showingTo - to < 20) to = Math.min(end, display.showingTo);
if (sawCollapsedSpans) {
from = lineNo(visualLine(doc, getLine(doc, from)));
while (to < end && lineIsHidden(doc, getLine(doc, to))) ++to;
}
// Create a range of theoretically intact lines, and punch holes
// in that using the change info.
var intact = [{from: Math.max(display.showingFrom, doc.first),
to: Math.min(display.showingTo, end)}];
if (intact[0].from >= intact[0].to) intact = [];
else intact = computeIntact(intact, changes);
// When merged lines are present, we might have to reduce the
// intact ranges because changes in continued fragments of the
// intact lines do require the lines to be redrawn.
if (sawCollapsedSpans)
for (var i = 0; i < intact.length; ++i) {
var range = intact[i], merged;
while (merged = collapsedSpanAtEnd(getLine(doc, range.to - 1))) {
var newTo = merged.find().from.line;
if (newTo > range.from) range.to = newTo;
else { intact.splice(i--, 1); break; }
}
}
// Clip off the parts that won't be visible
var intactLines = 0;
for (var i = 0; i < intact.length; ++i) {
var range = intact[i];
if (range.from < from) range.from = from;
if (range.to > to) range.to = to;
if (range.from >= range.to) intact.splice(i--, 1);
else intactLines += range.to - range.from;
}
if (!forced && intactLines == to - from && from == display.showingFrom && to == display.showingTo) {
updateViewOffset(cm);
return;
}
intact.sort(function(a, b) {return a.from - b.from;});
// Avoid crashing on IE's "unspecified error" when in iframes
try {
var focused = document.activeElement;
} catch(e) {}
if (intactLines < (to - from) * .7) display.lineDiv.style.display = "none";
patchDisplay(cm, from, to, intact, positionsChangedFrom);
display.lineDiv.style.display = "";
if (focused && document.activeElement != focused && focused.offsetHeight) focused.focus();
var different = from != display.showingFrom || to != display.showingTo ||
display.lastSizeC != display.wrapper.clientHeight;
// This is just a bogus formula that detects when the editor is
// resized or the font size changes.
if (different) {
display.lastSizeC = display.wrapper.clientHeight;
startWorker(cm, 400);
}
display.showingFrom = from; display.showingTo = to;
updateHeightsInViewport(cm);
updateViewOffset(cm);
return true;
}
function updateHeightsInViewport(cm) {
var display = cm.display;
var prevBottom = display.lineDiv.offsetTop;
for (var node = display.lineDiv.firstChild, height; node; node = node.nextSibling) if (node.lineObj) {
if (ie_lt8) {
var bot = node.offsetTop + node.offsetHeight;
height = bot - prevBottom;
prevBottom = bot;
} else {
var box = getRect(node);
height = box.bottom - box.top;
}
var diff = node.lineObj.height - height;
if (height < 2) height = textHeight(display);
if (diff > .001 || diff < -.001) {
updateLineHeight(node.lineObj, height);
var widgets = node.lineObj.widgets;
if (widgets) for (var i = 0; i < widgets.length; ++i)
widgets[i].height = widgets[i].node.offsetHeight;
}
}
}
function updateViewOffset(cm) {
var off = cm.display.viewOffset = heightAtLine(cm, getLine(cm.doc, cm.display.showingFrom));
// Position the mover div to align with the current virtual scroll position
cm.display.mover.style.top = off + "px";
}
function computeIntact(intact, changes) {
for (var i = 0, l = changes.length || 0; i < l; ++i) {
var change = changes[i], intact2 = [], diff = change.diff || 0;
for (var j = 0, l2 = intact.length; j < l2; ++j) {
var range = intact[j];
if (change.to <= range.from && change.diff) {
intact2.push({from: range.from + diff, to: range.to + diff});
} else if (change.to <= range.from || change.from >= range.to) {
intact2.push(range);
} else {
if (change.from > range.from)
intact2.push({from: range.from, to: change.from});
if (change.to < range.to)
intact2.push({from: change.to + diff, to: range.to + diff});
}
}
intact = intact2;
}
return intact;
}
function getDimensions(cm) {
var d = cm.display, left = {}, width = {};
for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) {
left[cm.options.gutters[i]] = n.offsetLeft;
width[cm.options.gutters[i]] = n.offsetWidth;
}
return {fixedPos: compensateForHScroll(d),
gutterTotalWidth: d.gutters.offsetWidth,
gutterLeft: left,
gutterWidth: width,
wrapperWidth: d.wrapper.clientWidth};
}
function patchDisplay(cm, from, to, intact, updateNumbersFrom) {
var dims = getDimensions(cm);
var display = cm.display, lineNumbers = cm.options.lineNumbers;
if (!intact.length && (!webkit || !cm.display.currentWheelTarget))
removeChildren(display.lineDiv);
var container = display.lineDiv, cur = container.firstChild;
function rm(node) {
var next = node.nextSibling;
if (webkit && mac && cm.display.currentWheelTarget == node) {
node.style.display = "none";
node.lineObj = null;
} else {
node.parentNode.removeChild(node);
}
return next;
}
var nextIntact = intact.shift(), lineN = from;
cm.doc.iter(from, to, function(line) {
if (nextIntact && nextIntact.to == lineN) nextIntact = intact.shift();
if (lineIsHidden(cm.doc, line)) {
if (line.height != 0) updateLineHeight(line, 0);
if (line.widgets && cur.previousSibling) for (var i = 0; i < line.widgets.length; ++i) {
var w = line.widgets[i];
if (w.showIfHidden) {
var prev = cur.previousSibling;
if (/pre/i.test(prev.nodeName)) {
var wrap = elt("div", null, null, "position: relative");
prev.parentNode.replaceChild(wrap, prev);
wrap.appendChild(prev);
prev = wrap;
}
var wnode = prev.appendChild(elt("div", [w.node], "CodeMirror-linewidget"));
if (!w.handleMouseEvents) wnode.ignoreEvents = true;
positionLineWidget(w, wnode, prev, dims);
}
}
} else if (nextIntact && nextIntact.from <= lineN && nextIntact.to > lineN) {
// This line is intact. Skip to the actual node. Update its
// line number if needed.
while (cur.lineObj != line) cur = rm(cur);
if (lineNumbers && updateNumbersFrom <= lineN && cur.lineNumber)
setTextContent(cur.lineNumber, lineNumberFor(cm.options, lineN));
cur = cur.nextSibling;
} else {
// For lines with widgets, make an attempt to find and reuse
// the existing element, so that widgets aren't needlessly
// removed and re-inserted into the dom
if (line.widgets) for (var j = 0, search = cur, reuse; search && j < 20; ++j, search = search.nextSibling)
if (search.lineObj == line && /div/i.test(search.nodeName)) { reuse = search; break; }
// This line needs to be generated.
var lineNode = buildLineElement(cm, line, lineN, dims, reuse);
if (lineNode != reuse) {
container.insertBefore(lineNode, cur);
} else {
while (cur != reuse) cur = rm(cur);
cur = cur.nextSibling;
}
lineNode.lineObj = line;
}
++lineN;
});
while (cur) cur = rm(cur);
}
function buildLineElement(cm, line, lineNo, dims, reuse) {
var lineElement = lineContent(cm, line);
var markers = line.gutterMarkers, display = cm.display, wrap;
if (!cm.options.lineNumbers && !markers && !line.bgClass && !line.wrapClass && !line.widgets)
return lineElement;
// Lines with gutter elements, widgets or a background class need
// to be wrapped again, and have the extra elements added to the
// wrapper div
if (reuse) {
reuse.alignable = null;
var isOk = true, widgetsSeen = 0, insertBefore = null;
for (var n = reuse.firstChild, next; n; n = next) {
next = n.nextSibling;
if (!/\bCodeMirror-linewidget\b/.test(n.className)) {
reuse.removeChild(n);
} else {
for (var i = 0; i < line.widgets.length; ++i) {
var widget = line.widgets[i];
if (widget.node == n.firstChild) {
if (!widget.above && !insertBefore) insertBefore = n;
positionLineWidget(widget, n, reuse, dims);
++widgetsSeen;
break;
}
}
if (i == line.widgets.length) { isOk = false; break; }
}
}
reuse.insertBefore(lineElement, insertBefore);
if (isOk && widgetsSeen == line.widgets.length) {
wrap = reuse;
reuse.className = line.wrapClass || "";
}
}
if (!wrap) {
wrap = elt("div", null, line.wrapClass, "position: relative");
wrap.appendChild(lineElement);
}
// Kludge to make sure the styled element lies behind the selection (by z-index)
if (line.bgClass)
wrap.insertBefore(elt("div", null, line.bgClass + " CodeMirror-linebackground"), wrap.firstChild);
if (cm.options.lineNumbers || markers) {
var gutterWrap = wrap.insertBefore(elt("div", null, null, "position: absolute; left: " +
(cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px"),
wrap.firstChild);
if (cm.options.fixedGutter) (wrap.alignable || (wrap.alignable = [])).push(gutterWrap);
if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"]))
wrap.lineNumber = gutterWrap.appendChild(
elt("div", lineNumberFor(cm.options, lineNo),
"CodeMirror-linenumber CodeMirror-gutter-elt",
"left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: "
+ display.lineNumInnerWidth + "px"));
if (markers)
for (var k = 0; k < cm.options.gutters.length; ++k) {
var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id];
if (found)
gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", "left: " +
dims.gutterLeft[id] + "px; width: " + dims.gutterWidth[id] + "px"));
}
}
if (ie_lt8) wrap.style.zIndex = 2;
if (line.widgets && wrap != reuse) for (var i = 0, ws = line.widgets; i < ws.length; ++i) {
var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget");
if (!widget.handleMouseEvents) node.ignoreEvents = true;
positionLineWidget(widget, node, wrap, dims);
if (widget.above)
wrap.insertBefore(node, cm.options.lineNumbers && line.height != 0 ? gutterWrap : lineElement);
else
wrap.appendChild(node);
signalLater(widget, "redraw");
}
return wrap;
}
function positionLineWidget(widget, node, wrap, dims) {
if (widget.noHScroll) {
(wrap.alignable || (wrap.alignable = [])).push(node);
var width = dims.wrapperWidth;
node.style.left = dims.fixedPos + "px";
if (!widget.coverGutter) {
width -= dims.gutterTotalWidth;
node.style.paddingLeft = dims.gutterTotalWidth + "px";
}
node.style.width = width + "px";
}
if (widget.coverGutter) {
node.style.zIndex = 5;
node.style.position = "relative";
if (!widget.noHScroll) node.style.marginLeft = -dims.gutterTotalWidth + "px";
}
}
// SELECTION / CURSOR
function updateSelection(cm) {
var display = cm.display;
var collapsed = posEq(cm.doc.sel.from, cm.doc.sel.to);
if (collapsed || cm.options.showCursorWhenSelecting)
updateSelectionCursor(cm);
else
display.cursor.style.display = display.otherCursor.style.display = "none";
if (!collapsed)
updateSelectionRange(cm);
else
display.selectionDiv.style.display = "none";
// Move the hidden textarea near the cursor to prevent scrolling artifacts
if (cm.options.moveInputWithCursor) {
var headPos = cursorCoords(cm, cm.doc.sel.head, "div");
var wrapOff = getRect(display.wrapper), lineOff = getRect(display.lineDiv);
display.inputDiv.style.top = Math.max(0, Math.min(display.wrapper.clientHeight - 10,
headPos.top + lineOff.top - wrapOff.top)) + "px";
display.inputDiv.style.left = Math.max(0, Math.min(display.wrapper.clientWidth - 10,
headPos.left + lineOff.left - wrapOff.left)) + "px";
}
}
// No selection, plain cursor
function updateSelectionCursor(cm) {
var display = cm.display, pos = cursorCoords(cm, cm.doc.sel.head, "div");
display.cursor.style.left = pos.left + "px";
display.cursor.style.top = pos.top + "px";
display.cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px";
display.cursor.style.display = "";
if (pos.other) {
display.otherCursor.style.display = "";
display.otherCursor.style.left = pos.other.left + "px";
display.otherCursor.style.top = pos.other.top + "px";
display.otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px";
} else { display.otherCursor.style.display = "none"; }
}
// Highlight selection
function updateSelectionRange(cm) {
var display = cm.display, doc = cm.doc, sel = cm.doc.sel;
var fragment = document.createDocumentFragment();
var clientWidth = display.lineSpace.offsetWidth, pl = paddingLeft(cm.display);
function add(left, top, width, bottom) {
if (top < 0) top = 0;
fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left +
"px; top: " + top + "px; width: " + (width == null ? clientWidth - left : width) +
"px; height: " + (bottom - top) + "px"));
}
function drawForLine(line, fromArg, toArg) {
var lineObj = getLine(doc, line);
var lineLen = lineObj.text.length;
var start, end;
function coords(ch, bias) {
return charCoords(cm, Pos(line, ch), "div", lineObj, bias);
}
iterateBidiSections(getOrder(lineObj), fromArg || 0, toArg == null ? lineLen : toArg, function(from, to, dir) {
var leftPos = coords(from, "left"), rightPos, left, right;
if (from == to) {
rightPos = leftPos;
left = right = leftPos.left;
} else {
rightPos = coords(to - 1, "right");
if (dir == "rtl") { var tmp = leftPos; leftPos = rightPos; rightPos = tmp; }
left = leftPos.left;
right = rightPos.right;
}
if (fromArg == null && from == 0) left = pl;
if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part
add(left, leftPos.top, null, leftPos.bottom);
left = pl;
if (leftPos.bottom < rightPos.top) add(left, leftPos.bottom, null, rightPos.top);
}
if (toArg == null && to == lineLen) right = clientWidth;
if (!start || leftPos.top < start.top || leftPos.top == start.top && leftPos.left < start.left)
start = leftPos;
if (!end || rightPos.bottom > end.bottom || rightPos.bottom == end.bottom && rightPos.right > end.right)
end = rightPos;
if (left < pl + 1) left = pl;
add(left, rightPos.top, right - left, rightPos.bottom);
});
return {start: start, end: end};
}
if (sel.from.line == sel.to.line) {
drawForLine(sel.from.line, sel.from.ch, sel.to.ch);
} else {
var fromLine = getLine(doc, sel.from.line), toLine = getLine(doc, sel.to.line);
var singleVLine = visualLine(doc, fromLine) == visualLine(doc, toLine);
var leftEnd = drawForLine(sel.from.line, sel.from.ch, singleVLine ? fromLine.text.length : null).end;
var rightStart = drawForLine(sel.to.line, singleVLine ? 0 : null, sel.to.ch).start;
if (singleVLine) {
if (leftEnd.top < rightStart.top - 2) {
add(leftEnd.right, leftEnd.top, null, leftEnd.bottom);
add(pl, rightStart.top, rightStart.left, rightStart.bottom);
} else {
add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom);
}
}
if (leftEnd.bottom < rightStart.top)
add(pl, leftEnd.bottom, null, rightStart.top);
}
removeChildrenAndAdd(display.selectionDiv, fragment);
display.selectionDiv.style.display = "";
}
// Cursor-blinking
function restartBlink(cm) {
if (!cm.state.focused) return;
var display = cm.display;
clearInterval(display.blinker);
var on = true;
display.cursor.style.visibility = display.otherCursor.style.visibility = "";
display.blinker = setInterval(function() {
display.cursor.style.visibility = display.otherCursor.style.visibility = (on = !on) ? "" : "hidden";
}, cm.options.cursorBlinkRate);
}
// HIGHLIGHT WORKER
function startWorker(cm, time) {
if (cm.doc.mode.startState && cm.doc.frontier < cm.display.showingTo)
cm.state.highlight.set(time, bind(highlightWorker, cm));
}
function highlightWorker(cm) {
var doc = cm.doc;
if (doc.frontier < doc.first) doc.frontier = doc.first;
if (doc.frontier >= cm.display.showingTo) return;
var end = +new Date + cm.options.workTime;
var state = copyState(doc.mode, getStateBefore(cm, doc.frontier));
var changed = [], prevChange;
doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.showingTo + 500), function(line) {
if (doc.frontier >= cm.display.showingFrom) { // Visible
var oldStyles = line.styles;
line.styles = highlightLine(cm, line, state);
var ischange = !oldStyles || oldStyles.length != line.styles.length;
for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i];
if (ischange) {
if (prevChange && prevChange.end == doc.frontier) prevChange.end++;
else changed.push(prevChange = {start: doc.frontier, end: doc.frontier + 1});
}
line.stateAfter = copyState(doc.mode, state);
} else {
processLine(cm, line, state);
line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null;
}
++doc.frontier;
if (+new Date > end) {
startWorker(cm, cm.options.workDelay);
return true;
}
});
if (changed.length)
operation(cm, function() {
for (var i = 0; i < changed.length; ++i)
regChange(this, changed[i].start, changed[i].end);
})();
}
// Finds the line to start with when starting a parse. Tries to
// find a line with a stateAfter, so that it can start with a
// valid state. If that fails, it returns the line with the
// smallest indentation, which tends to need the least context to
// parse correctly.
function findStartLine(cm, n, precise) {
var minindent, minline, doc = cm.doc;
for (var search = n, lim = n - 100; search > lim; --search) {
if (search <= doc.first) return doc.first;
var line = getLine(doc, search - 1);
if (line.stateAfter && (!precise || search <= doc.frontier)) return search;
var indented = countColumn(line.text, null, cm.options.tabSize);
if (minline == null || minindent > indented) {
minline = search - 1;
minindent = indented;
}
}
return minline;
}
function getStateBefore(cm, n, precise) {
var doc = cm.doc, display = cm.display;
if (!doc.mode.startState) return true;
var pos = findStartLine(cm, n, precise), state = pos > doc.first && getLine(doc, pos-1).stateAfter;
if (!state) state = startState(doc.mode);
else state = copyState(doc.mode, state);
doc.iter(pos, n, function(line) {
processLine(cm, line, state);
var save = pos == n - 1 || pos % 5 == 0 || pos >= display.showingFrom && pos < display.showingTo;
line.stateAfter = save ? copyState(doc.mode, state) : null;
++pos;
});
return state;
}
// POSITION MEASUREMENT
function paddingTop(display) {return display.lineSpace.offsetTop;}
function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight;}
function paddingLeft(display) {
var e = removeChildrenAndAdd(display.measure, elt("pre", null, null, "text-align: left")).appendChild(elt("span", "x"));
return e.offsetLeft;
}
function measureChar(cm, line, ch, data, bias) {
var dir = -1;
data = data || measureLine(cm, line);
for (var pos = ch;; pos += dir) {
var r = data[pos];
if (r) break;
if (dir < 0 && pos == 0) dir = 1;
}
bias = pos > ch ? "left" : pos < ch ? "right" : bias;
if (bias == "left" && r.leftSide) r = r.leftSide;
else if (bias == "right" && r.rightSide) r = r.rightSide;
return {left: pos < ch ? r.right : r.left,
right: pos > ch ? r.left : r.right,
top: r.top,
bottom: r.bottom};
}
function findCachedMeasurement(cm, line) {
var cache = cm.display.measureLineCache;
for (var i = 0; i < cache.length; ++i) {
var memo = cache[i];
if (memo.text == line.text && memo.markedSpans == line.markedSpans &&
cm.display.scroller.clientWidth == memo.width &&
memo.classes == line.textClass + "|" + line.bgClass + "|" + line.wrapClass)
return memo;
}
}
function clearCachedMeasurement(cm, line) {
var exists = findCachedMeasurement(cm, line);
if (exists) exists.text = exists.measure = exists.markedSpans = null;
}
function measureLine(cm, line) {
// First look in the cache
var cached = findCachedMeasurement(cm, line);
if (cached) return cached.measure;
// Failing that, recompute and store result in cache
var measure = measureLineInner(cm, line);
var cache = cm.display.measureLineCache;
var memo = {text: line.text, width: cm.display.scroller.clientWidth,
markedSpans: line.markedSpans, measure: measure,
classes: line.textClass + "|" + line.bgClass + "|" + line.wrapClass};
if (cache.length == 16) cache[++cm.display.measureLineCachePos % 16] = memo;
else cache.push(memo);
return measure;
}
function measureLineInner(cm, line) {
var display = cm.display, measure = emptyArray(line.text.length);
var pre = lineContent(cm, line, measure, true);
// IE does not cache element positions of inline elements between
// calls to getBoundingClientRect. This makes the loop below,
// which gathers the positions of all the characters on the line,
// do an amount of layout work quadratic to the number of
// characters. When line wrapping is off, we try to improve things
// by first subdividing the line into a bunch of inline blocks, so
// that IE can reuse most of the layout information from caches
// for those blocks. This does interfere with line wrapping, so it
// doesn't work when wrapping is on, but in that case the
// situation is slightly better, since IE does cache line-wrapping
// information and only recomputes per-line.
if (ie && !ie_lt8 && !cm.options.lineWrapping && pre.childNodes.length > 100) {
var fragment = document.createDocumentFragment();
var chunk = 10, n = pre.childNodes.length;
for (var i = 0, chunks = Math.ceil(n / chunk); i < chunks; ++i) {
var wrap = elt("div", null, null, "display: inline-block");
for (var j = 0; j < chunk && n; ++j) {
wrap.appendChild(pre.firstChild);
--n;
}
fragment.appendChild(wrap);
}
pre.appendChild(fragment);
}
removeChildrenAndAdd(display.measure, pre);
var outer = getRect(display.lineDiv);
var vranges = [], data = emptyArray(line.text.length), maxBot = pre.offsetHeight;
// Work around an IE7/8 bug where it will sometimes have randomly
// replaced our pre with a clone at this point.
if (ie_lt9 && display.measure.first != pre)
removeChildrenAndAdd(display.measure, pre);
function measureRect(rect) {
var top = rect.top - outer.top, bot = rect.bottom - outer.top;
if (bot > maxBot) bot = maxBot;
if (top < 0) top = 0;
for (var i = vranges.length - 2; i >= 0; i -= 2) {
var rtop = vranges[i], rbot = vranges[i+1];
if (rtop > bot || rbot < top) continue;
if (rtop <= top && rbot >= bot ||
top <= rtop && bot >= rbot ||
Math.min(bot, rbot) - Math.max(top, rtop) >= (bot - top) >> 1) {
vranges[i] = Math.min(top, rtop);
vranges[i+1] = Math.max(bot, rbot);
break;
}
}
if (i < 0) { i = vranges.length; vranges.push(top, bot); }
return {left: rect.left - outer.left,
right: rect.right - outer.left,
top: i, bottom: null};
}
function finishRect(rect) {
rect.bottom = vranges[rect.top+1];
rect.top = vranges[rect.top];
}
for (var i = 0, cur; i < measure.length; ++i) if (cur = measure[i]) {
var node = cur, rect = null;
// A widget might wrap, needs special care
if (/\bCodeMirror-widget\b/.test(cur.className) && cur.getClientRects) {
if (cur.firstChild.nodeType == 1) node = cur.firstChild;
var rects = node.getClientRects();
if (rects.length > 1) {
rect = data[i] = measureRect(rects[0]);
rect.rightSide = measureRect(rects[rects.length - 1]);
}
}
if (!rect) rect = data[i] = measureRect(getRect(node));
if (cur.measureRight) rect.right = getRect(cur.measureRight).left;
if (cur.leftSide) rect.leftSide = measureRect(getRect(cur.leftSide));
}
for (var i = 0, cur; i < data.length; ++i) if (cur = data[i]) {
finishRect(cur);
if (cur.leftSide) finishRect(cur.leftSide);
if (cur.rightSide) finishRect(cur.rightSide);
}
return data;
}
function measureLineWidth(cm, line) {
var hasBadSpan = false;
if (line.markedSpans) for (var i = 0; i < line.markedSpans; ++i) {
var sp = line.markedSpans[i];
if (sp.collapsed && (sp.to == null || sp.to == line.text.length)) hasBadSpan = true;
}
var cached = !hasBadSpan && findCachedMeasurement(cm, line);
if (cached) return measureChar(cm, line, line.text.length, cached.measure, "right").right;
var pre = lineContent(cm, line, null, true);
var end = pre.appendChild(zeroWidthElement(cm.display.measure));
removeChildrenAndAdd(cm.display.measure, pre);
return getRect(end).right - getRect(cm.display.lineDiv).left;
}
function clearCaches(cm) {
cm.display.measureLineCache.length = cm.display.measureLineCachePos = 0;
cm.display.cachedCharWidth = cm.display.cachedTextHeight = null;
if (!cm.options.lineWrapping) cm.display.maxLineChanged = true;
cm.display.lineNumChars = null;
}
function pageScrollX() { return window.pageXOffset || (document.documentElement || document.body).scrollLeft; }
function pageScrollY() { return window.pageYOffset || (document.documentElement || document.body).scrollTop; }
// Context is one of "line", "div" (display.lineDiv), "local"/null (editor), or "page"
function intoCoordSystem(cm, lineObj, rect, context) {
if (lineObj.widgets) for (var i = 0; i < lineObj.widgets.length; ++i) if (lineObj.widgets[i].above) {
var size = widgetHeight(lineObj.widgets[i]);
rect.top += size; rect.bottom += size;
}
if (context == "line") return rect;
if (!context) context = "local";
var yOff = heightAtLine(cm, lineObj);
if (context == "local") yOff += paddingTop(cm.display);
else yOff -= cm.display.viewOffset;
if (context == "page" || context == "window") {
var lOff = getRect(cm.display.lineSpace);
yOff += lOff.top + (context == "window" ? 0 : pageScrollY());
var xOff = lOff.left + (context == "window" ? 0 : pageScrollX());
rect.left += xOff; rect.right += xOff;
}
rect.top += yOff; rect.bottom += yOff;
return rect;
}
// Context may be "window", "page", "div", or "local"/null
// Result is in "div" coords
function fromCoordSystem(cm, coords, context) {
if (context == "div") return coords;
var left = coords.left, top = coords.top;
// First move into "page" coordinate system
if (context == "page") {
left -= pageScrollX();
top -= pageScrollY();
} else if (context == "local" || !context) {
var localBox = getRect(cm.display.sizer);
left += localBox.left;
top += localBox.top;
}
var lineSpaceBox = getRect(cm.display.lineSpace);
return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top};
}
function charCoords(cm, pos, context, lineObj, bias) {
if (!lineObj) lineObj = getLine(cm.doc, pos.line);
return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, null, bias), context);
}
function cursorCoords(cm, pos, context, lineObj, measurement) {
lineObj = lineObj || getLine(cm.doc, pos.line);
if (!measurement) measurement = measureLine(cm, lineObj);
function get(ch, right) {
var m = measureChar(cm, lineObj, ch, measurement, right ? "right" : "left");
if (right) m.left = m.right; else m.right = m.left;
return intoCoordSystem(cm, lineObj, m, context);
}
function getBidi(ch, partPos) {
var part = order[partPos], right = part.level % 2;
if (ch == bidiLeft(part) && partPos && part.level < order[partPos - 1].level) {
part = order[--partPos];
ch = bidiRight(part) - (part.level % 2 ? 0 : 1);
right = true;
} else if (ch == bidiRight(part) && partPos < order.length - 1 && part.level < order[partPos + 1].level) {
part = order[++partPos];
ch = bidiLeft(part) - part.level % 2;
right = false;
}
if (right && ch == part.to && ch > part.from) return get(ch - 1);
return get(ch, right);
}
var order = getOrder(lineObj), ch = pos.ch;
if (!order) return get(ch);
var partPos = getBidiPartAt(order, ch);
var val = getBidi(ch, partPos);
if (bidiOther != null) val.other = getBidi(ch, bidiOther);
return val;
}
function PosWithInfo(line, ch, outside, xRel) {
var pos = new Pos(line, ch);
pos.xRel = xRel;
if (outside) pos.outside = true;
return pos;
}
// Coords must be lineSpace-local
function coordsChar(cm, x, y) {
var doc = cm.doc;
y += cm.display.viewOffset;
if (y < 0) return PosWithInfo(doc.first, 0, true, -1);
var lineNo = lineAtHeight(doc, y), last = doc.first + doc.size - 1;
if (lineNo > last)
return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, true, 1);
if (x < 0) x = 0;
for (;;) {
var lineObj = getLine(doc, lineNo);
var found = coordsCharInner(cm, lineObj, lineNo, x, y);
var merged = collapsedSpanAtEnd(lineObj);
var mergedPos = merged && merged.find();
if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0))
lineNo = mergedPos.to.line;
else
return found;
}
}
function coordsCharInner(cm, lineObj, lineNo, x, y) {
var innerOff = y - heightAtLine(cm, lineObj);
var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth;
var measurement = measureLine(cm, lineObj);
function getX(ch) {
var sp = cursorCoords(cm, Pos(lineNo, ch), "line",
lineObj, measurement);
wrongLine = true;
if (innerOff > sp.bottom) return sp.left - adjust;
else if (innerOff < sp.top) return sp.left + adjust;
else wrongLine = false;
return sp.left;
}
var bidi = getOrder(lineObj), dist = lineObj.text.length;
var from = lineLeft(lineObj), to = lineRight(lineObj);
var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine;
if (x > toX) return PosWithInfo(lineNo, to, toOutside, 1);
// Do a binary search between these bounds.
for (;;) {
if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) {
var ch = x < fromX || x - fromX <= toX - x ? from : to;
var xDiff = x - (ch == from ? fromX : toX);
while (isExtendingChar.test(lineObj.text.charAt(ch))) ++ch;
var pos = PosWithInfo(lineNo, ch, ch == from ? fromOutside : toOutside,
xDiff < 0 ? -1 : xDiff ? 1 : 0);
return pos;
}
var step = Math.ceil(dist / 2), middle = from + step;
if (bidi) {
middle = from;
for (var i = 0; i < step; ++i) middle = moveVisually(lineObj, middle, 1);
}
var middleX = getX(middle);
if (middleX > x) {to = middle; toX = middleX; if (toOutside = wrongLine) toX += 1000; dist = step;}
else {from = middle; fromX = middleX; fromOutside = wrongLine; dist -= step;}
}
}
var measureText;
function textHeight(display) {
if (display.cachedTextHeight != null) return display.cachedTextHeight;
if (measureText == null) {
measureText = elt("pre");
// Measure a bunch of lines, for browsers that compute
// fractional heights.
for (var i = 0; i < 49; ++i) {
measureText.appendChild(document.createTextNode("x"));
measureText.appendChild(elt("br"));
}
measureText.appendChild(document.createTextNode("x"));
}
removeChildrenAndAdd(display.measure, measureText);
var height = measureText.offsetHeight / 50;
if (height > 3) display.cachedTextHeight = height;
removeChildren(display.measure);
return height || 1;
}
function charWidth(display) {
if (display.cachedCharWidth != null) return display.cachedCharWidth;
var anchor = elt("span", "x");
var pre = elt("pre", [anchor]);
removeChildrenAndAdd(display.measure, pre);
var width = anchor.offsetWidth;
if (width > 2) display.cachedCharWidth = width;
return width || 10;
}
// OPERATIONS
// Operations are used to wrap changes in such a way that each
// change won't have to update the cursor and display (which would
// be awkward, slow, and error-prone), but instead updates are
// batched and then all combined and executed at once.
var nextOpId = 0;
function startOperation(cm) {
cm.curOp = {
// An array of ranges of lines that have to be updated. See
// updateDisplay.
changes: [],
forceUpdate: false,
updateInput: null,
userSelChange: null,
textChanged: null,
selectionChanged: false,
cursorActivity: false,
updateMaxLine: false,
updateScrollPos: false,
id: ++nextOpId
};
if (!delayedCallbackDepth++) delayedCallbacks = [];
}
function endOperation(cm) {
var op = cm.curOp, doc = cm.doc, display = cm.display;
cm.curOp = null;
if (op.updateMaxLine) computeMaxLength(cm);
if (display.maxLineChanged && !cm.options.lineWrapping && display.maxLine) {
var width = measureLineWidth(cm, display.maxLine);
display.sizer.style.minWidth = Math.max(0, width + 3 + scrollerCutOff) + "px";
display.maxLineChanged = false;
var maxScrollLeft = Math.max(0, display.sizer.offsetLeft + display.sizer.offsetWidth - display.scroller.clientWidth);
if (maxScrollLeft < doc.scrollLeft && !op.updateScrollPos)
setScrollLeft(cm, Math.min(display.scroller.scrollLeft, maxScrollLeft), true);
}
var newScrollPos, updated;
if (op.updateScrollPos) {
newScrollPos = op.updateScrollPos;
} else if (op.selectionChanged && display.scroller.clientHeight) { // don't rescroll if not visible
var coords = cursorCoords(cm, doc.sel.head);
newScrollPos = calculateScrollPos(cm, coords.left, coords.top, coords.left, coords.bottom);
}
if (op.changes.length || op.forceUpdate || newScrollPos && newScrollPos.scrollTop != null) {
updated = updateDisplay(cm, op.changes, newScrollPos && newScrollPos.scrollTop, op.forceUpdate);
if (cm.display.scroller.offsetHeight) cm.doc.scrollTop = cm.display.scroller.scrollTop;
}
if (!updated && op.selectionChanged) updateSelection(cm);
if (op.updateScrollPos) {
display.scroller.scrollTop = display.scrollbarV.scrollTop = doc.scrollTop = newScrollPos.scrollTop;
display.scroller.scrollLeft = display.scrollbarH.scrollLeft = doc.scrollLeft = newScrollPos.scrollLeft;
alignHorizontally(cm);
if (op.scrollToPos)
scrollPosIntoView(cm, clipPos(cm.doc, op.scrollToPos), op.scrollToPosMargin);
} else if (newScrollPos) {
scrollCursorIntoView(cm);
}
if (op.selectionChanged) restartBlink(cm);
if (cm.state.focused && op.updateInput)
resetInput(cm, op.userSelChange);
var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers;
if (hidden) for (var i = 0; i < hidden.length; ++i)
if (!hidden[i].lines.length) signal(hidden[i], "hide");
if (unhidden) for (var i = 0; i < unhidden.length; ++i)
if (unhidden[i].lines.length) signal(unhidden[i], "unhide");
var delayed;
if (!--delayedCallbackDepth) {
delayed = delayedCallbacks;
delayedCallbacks = null;
}
if (op.textChanged)
signal(cm, "change", cm, op.textChanged);
if (op.cursorActivity) signal(cm, "cursorActivity", cm);
if (delayed) for (var i = 0; i < delayed.length; ++i) delayed[i]();
}
// Wraps a function in an operation. Returns the wrapped function.
function operation(cm1, f) {
return function() {
var cm = cm1 || this, withOp = !cm.curOp;
if (withOp) startOperation(cm);
try { var result = f.apply(cm, arguments); }
finally { if (withOp) endOperation(cm); }
return result;
};
}
function docOperation(f) {
return function() {
var withOp = this.cm && !this.cm.curOp, result;
if (withOp) startOperation(this.cm);
try { result = f.apply(this, arguments); }
finally { if (withOp) endOperation(this.cm); }
return result;
};
}
function runInOp(cm, f) {
var withOp = !cm.curOp, result;
if (withOp) startOperation(cm);
try { result = f(); }
finally { if (withOp) endOperation(cm); }
return result;
}
function regChange(cm, from, to, lendiff) {
if (from == null) from = cm.doc.first;
if (to == null) to = cm.doc.first + cm.doc.size;
cm.curOp.changes.push({from: from, to: to, diff: lendiff});
}
// INPUT HANDLING
function slowPoll(cm) {
if (cm.display.pollingFast) return;
cm.display.poll.set(cm.options.pollInterval, function() {
readInput(cm);
if (cm.state.focused) slowPoll(cm);
});
}
function fastPoll(cm) {
var missed = false;
cm.display.pollingFast = true;
function p() {
var changed = readInput(cm);
if (!changed && !missed) {missed = true; cm.display.poll.set(60, p);}
else {cm.display.pollingFast = false; slowPoll(cm);}
}
cm.display.poll.set(20, p);
}
// prevInput is a hack to work with IME. If we reset the textarea
// on every change, that breaks IME. So we look for changes
// compared to the previous content instead. (Modern browsers have
// events that indicate IME taking place, but these are not widely
// supported or compatible enough yet to rely on.)
function readInput(cm) {
var input = cm.display.input, prevInput = cm.display.prevInput, doc = cm.doc, sel = doc.sel;
if (!cm.state.focused || hasSelection(input) || isReadOnly(cm) || cm.state.disableInput || cm.state.accessibleTextareaWaiting) return false;
var text = input.value;
if (text == prevInput && posEq(sel.from, sel.to)) return false;
if (ie && !ie_lt9 && cm.display.inputHasSelection === text) {
resetInput(cm, true);
return false;
}
var withOp = !cm.curOp;
if (withOp) startOperation(cm);
sel.shift = false;
var same = 0, l = Math.min(prevInput.length, text.length);
while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same;
var from = sel.from, to = sel.to;
if (same < prevInput.length)
from = Pos(from.line, from.ch - (prevInput.length - same));
else if (cm.state.overwrite && posEq(from, to) && !cm.state.pasteIncoming)
to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + (text.length - same)));
var updateInput = cm.curOp.updateInput;
var changeEvent = {from: from, to: to, text: splitLines(text.slice(same)),
origin: cm.state.pasteIncoming ? "paste" : "+input"};
makeChange(cm.doc, changeEvent, "end");
cm.curOp.updateInput = updateInput;
signalLater(cm, "inputRead", cm, changeEvent);
if (text.length > 1000 || text.indexOf("\n") > -1) input.value = cm.display.prevInput = "";
else cm.display.prevInput = text;
if (withOp) endOperation(cm);
cm.state.pasteIncoming = false;
return true;
}
function resetInput(cm, user) {
var minimal, selected, doc = cm.doc;
if (!posEq(doc.sel.from, doc.sel.to)) {
cm.display.prevInput = "";
minimal = false && hasCopyEvent &&
(doc.sel.to.line - doc.sel.from.line > 100 || (selected = cm.getSelection()).length > 1000);
var content = minimal ? "-" : selected || cm.getSelection();
cm.display.input.value = content;
if (cm.state.focused) selectInput(cm.display.input);
if (ie && !ie_lt9) cm.display.inputHasSelection = content;
} else if (user && !cm.state.accessibleTextareaWaiting) {
cm.display.prevInput = cm.display.input.value = "";
if (ie && !ie_lt9) cm.display.inputHasSelection = null;
}
cm.display.inaccurateSelection = minimal;
}
function focusInput(cm) {
if (cm.options.readOnly != "nocursor" && (!mobile || document.activeElement != cm.display.input))
cm.display.input.focus();
}
function isReadOnly(cm) {
return cm.options.readOnly || cm.doc.cantEdit;
}
// EVENT HANDLERS
function registerEventHandlers(cm) {
var d = cm.display;
on(d.scroller, "mousedown", operation(cm, onMouseDown));
if (ie)
on(d.scroller, "dblclick", operation(cm, function(e) {
if (signalDOMEvent(cm, e)) return;
var pos = posFromMouse(cm, e);
if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) return;
e_preventDefault(e);
var word = findWordAt(getLine(cm.doc, pos.line).text, pos);
extendSelection(cm.doc, word.from, word.to);
}));
else
on(d.scroller, "dblclick", function(e) { signalDOMEvent(cm, e) || e_preventDefault(e); });
on(d.lineSpace, "selectstart", function(e) {
if (!eventInWidget(d, e)) e_preventDefault(e);
});
// Gecko browsers fire contextmenu *after* opening the menu, at
// which point we can't mess with it anymore. Context menu is
// handled in onMouseDown for Gecko.
if (!captureMiddleClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);});
on(d.scroller, "scroll", function() {
if (d.scroller.clientHeight) {
setScrollTop(cm, d.scroller.scrollTop);
setScrollLeft(cm, d.scroller.scrollLeft, true);
signal(cm, "scroll", cm);
}
});
on(d.scrollbarV, "scroll", function() {
if (d.scroller.clientHeight) setScrollTop(cm, d.scrollbarV.scrollTop);
});
on(d.scrollbarH, "scroll", function() {
if (d.scroller.clientHeight) setScrollLeft(cm, d.scrollbarH.scrollLeft);
});
on(d.scroller, "mousewheel", function(e){onScrollWheel(cm, e);});
on(d.scroller, "DOMMouseScroll", function(e){onScrollWheel(cm, e);});
function reFocus() { if (cm.state.focused) setTimeout(bind(focusInput, cm), 0); }
on(d.scrollbarH, "mousedown", reFocus);
on(d.scrollbarV, "mousedown", reFocus);
// Prevent wrapper from ever scrolling
on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; });
var resizeTimer;
function onResize() {
if (resizeTimer == null) resizeTimer = setTimeout(function() {
resizeTimer = null;
// Might be a text scaling operation, clear size caches.
d.cachedCharWidth = d.cachedTextHeight = knownScrollbarWidth = null;
clearCaches(cm);
runInOp(cm, bind(regChange, cm));
}, 100);
}
on(window, "resize", onResize);
// Above handler holds on to the editor and its data structures.
// Here we poll to unregister it when the editor is no longer in
// the document, so that it can be garbage-collected.
function unregister() {
for (var p = d.wrapper.parentNode; p && p != document.body; p = p.parentNode) {}
if (p) setTimeout(unregister, 5000);
else off(window, "resize", onResize);
}
setTimeout(unregister, 5000);
on(d.input, "keyup", operation(cm, function(e) {
if (signalDOMEvent(cm, e) || cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return;
if (e.keyCode == 16) cm.doc.sel.shift = false;
}));
on(d.input, "input", bind(fastPoll, cm));
on(d.input, "keydown", operation(cm, onKeyDown));
on(d.input, "keypress", operation(cm, onKeyPress));
on(d.input, "focus", bind(onFocus, cm));
on(d.input, "blur", bind(onBlur, cm));
function drag_(e) {
if (signalDOMEvent(cm, e) || cm.options.onDragEvent && cm.options.onDragEvent(cm, addStop(e))) return;
e_stop(e);
}
if (cm.options.dragDrop) {
on(d.scroller, "dragstart", function(e){onDragStart(cm, e);});
on(d.scroller, "dragenter", drag_);
on(d.scroller, "dragover", drag_);
on(d.scroller, "drop", operation(cm, onDrop));
}
on(d.scroller, "paste", function(e){
if (eventInWidget(d, e)) return;
focusInput(cm);
fastPoll(cm);
});
on(d.input, "paste", function() {
cm.state.pasteIncoming = true;
fastPoll(cm);
});
function prepareCopy() {
if (d.inaccurateSelection) {
d.prevInput = "";
d.inaccurateSelection = false;
d.input.value = cm.getSelection();
selectInput(d.input);
}
}
on(d.input, "cut", prepareCopy);
on(d.input, "copy", prepareCopy);
// Needed to handle Tab key in KHTML
if (khtml) on(d.sizer, "mouseup", function() {
if (document.activeElement == d.input) d.input.blur();
focusInput(cm);
});
}
function eventInWidget(display, e) {
for (var n = e_target(e); n != display.wrapper; n = n.parentNode) {
if (!n || n.ignoreEvents || n.parentNode == display.sizer && n != display.mover) return true;
}
}
function posFromMouse(cm, e, liberal) {
var display = cm.display;
if (!liberal) {
var target = e_target(e);
if (target == display.scrollbarH || target == display.scrollbarH.firstChild ||
target == display.scrollbarV || target == display.scrollbarV.firstChild ||
target == display.scrollbarFiller || target == display.gutterFiller) return null;
}
var x, y, space = getRect(display.lineSpace);
// Fails unpredictably on IE[67] when mouse is dragged around quickly.
try { x = e.clientX; y = e.clientY; } catch (e) { return null; }
return coordsChar(cm, x - space.left, y - space.top);
}
var lastClick, lastDoubleClick;
function onMouseDown(e) {
if (signalDOMEvent(this, e)) return;
var cm = this, display = cm.display, doc = cm.doc, sel = doc.sel;
sel.shift = e.shiftKey;
if (eventInWidget(display, e)) {
if (!webkit) {
display.scroller.draggable = false;
setTimeout(function(){display.scroller.draggable = true;}, 100);
}
return;
}
if (clickInGutter(cm, e)) return;
var start = posFromMouse(cm, e);
switch (e_button(e)) {
case 3:
if (captureMiddleClick) onContextMenu.call(cm, cm, e);
return;
case 2:
if (start) extendSelection(cm.doc, start);
setTimeout(bind(focusInput, cm), 20);
e_preventDefault(e);
return;
}
// For button 1, if it was clicked inside the editor
// (posFromMouse returning non-null), we have to adjust the
// selection.
if (!start) {if (e_target(e) == display.scroller) e_preventDefault(e); return;}
if (!cm.state.focused) onFocus(cm);
var now = +new Date, type = "single";
if (lastDoubleClick && lastDoubleClick.time > now - 400 && posEq(lastDoubleClick.pos, start)) {
type = "triple";
e_preventDefault(e);
setTimeout(bind(focusInput, cm), 20);
selectLine(cm, start.line);
} else if (lastClick && lastClick.time > now - 400 && posEq(lastClick.pos, start)) {
type = "double";
lastDoubleClick = {time: now, pos: start};
e_preventDefault(e);
var word = findWordAt(getLine(doc, start.line).text, start);
extendSelection(cm.doc, word.from, word.to);
} else { lastClick = {time: now, pos: start}; }
var last = start;
if (cm.options.dragDrop && dragAndDrop && !isReadOnly(cm) && !posEq(sel.from, sel.to) &&
!posLess(start, sel.from) && !posLess(sel.to, start) && type == "single") {
var dragEnd = operation(cm, function(e2) {
if (webkit) display.scroller.draggable = false;
cm.state.draggingText = false;
off(document, "mouseup", dragEnd);
off(display.scroller, "drop", dragEnd);
if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) {
e_preventDefault(e2);
extendSelection(cm.doc, start);
focusInput(cm);
}
});
// Let the drag handler handle this.
if (webkit) display.scroller.draggable = true;
cm.state.draggingText = dragEnd;
// IE's approach to draggable
if (display.scroller.dragDrop) display.scroller.dragDrop();
on(document, "mouseup", dragEnd);
on(display.scroller, "drop", dragEnd);
return;
}
e_preventDefault(e);
if (type == "single") extendSelection(cm.doc, clipPos(doc, start));
var startstart = sel.from, startend = sel.to, lastPos = start;
function doSelect(cur) {
if (posEq(lastPos, cur)) return;
lastPos = cur;
if (type == "single") {
extendSelection(cm.doc, clipPos(doc, start), cur);
return;
}
startstart = clipPos(doc, startstart);
startend = clipPos(doc, startend);
if (type == "double") {
var word = findWordAt(getLine(doc, cur.line).text, cur);
if (posLess(cur, startstart)) extendSelection(cm.doc, word.from, startend);
else extendSelection(cm.doc, startstart, word.to);
} else if (type == "triple") {
if (posLess(cur, startstart)) extendSelection(cm.doc, startend, clipPos(doc, Pos(cur.line, 0)));
else extendSelection(cm.doc, startstart, clipPos(doc, Pos(cur.line + 1, 0)));
}
}
var editorSize = getRect(display.wrapper);
// Used to ensure timeout re-tries don't fire when another extend
// happened in the meantime (clearTimeout isn't reliable -- at
// least on Chrome, the timeouts still happen even when cleared,
// if the clear happens after their scheduled firing time).
var counter = 0;
function extend(e) {
var curCount = ++counter;
var cur = posFromMouse(cm, e, true);
if (!cur) return;
if (!posEq(cur, last)) {
if (!cm.state.focused) onFocus(cm);
last = cur;
doSelect(cur);
var visible = visibleLines(display, doc);
if (cur.line >= visible.to || cur.line < visible.from)
setTimeout(operation(cm, function(){if (counter == curCount) extend(e);}), 150);
} else {
var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0;
if (outside) setTimeout(operation(cm, function() {
if (counter != curCount) return;
display.scroller.scrollTop += outside;
extend(e);
}), 50);
}
}
function done(e) {
counter = Infinity;
e_preventDefault(e);
focusInput(cm);
off(document, "mousemove", move);
off(document, "mouseup", up);
}
var move = operation(cm, function(e) {
if (!ie && !e_button(e)) done(e);
else extend(e);
});
var up = operation(cm, done);
on(document, "mousemove", move);
on(document, "mouseup", up);
}
function clickInGutter(cm, e) {
var display = cm.display;
try { var mX = e.clientX, mY = e.clientY; }
catch(e) { return false; }
if (mX >= Math.floor(getRect(display.gutters).right)) return false;
e_preventDefault(e);
if (!hasHandler(cm, "gutterClick")) return true;
var lineBox = getRect(display.lineDiv);
if (mY > lineBox.bottom) return true;
mY -= lineBox.top - display.viewOffset;
for (var i = 0; i < cm.options.gutters.length; ++i) {
var g = display.gutters.childNodes[i];
if (g && getRect(g).right >= mX) {
var line = lineAtHeight(cm.doc, mY);
var gutter = cm.options.gutters[i];
signalLater(cm, "gutterClick", cm, line, gutter, e);
break;
}
}
return true;
}
// Kludge to work around strange IE behavior where it'll sometimes
// re-fire a series of drag-related events right after the drop (#1551)
var lastDrop = 0;
function onDrop(e) {
var cm = this;
if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e) || (cm.options.onDragEvent && cm.options.onDragEvent(cm, addStop(e))))
return;
e_preventDefault(e);
if (ie) lastDrop = +new Date;
var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files;
if (!pos || isReadOnly(cm)) return;
if (files && files.length && window.FileReader && window.File) {
var n = files.length, text = Array(n), read = 0;
var loadFile = function(file, i) {
var reader = new FileReader;
reader.onload = function() {
text[i] = reader.result;
if (++read == n) {
pos = clipPos(cm.doc, pos);
makeChange(cm.doc, {from: pos, to: pos, text: splitLines(text.join("\n")), origin: "paste"}, "around");
}
};
reader.readAsText(file);
};
for (var i = 0; i < n; ++i) loadFile(files[i], i);
} else {
// Don't do a replace if the drop happened inside of the selected text.
if (cm.state.draggingText && !(posLess(pos, cm.doc.sel.from) || posLess(cm.doc.sel.to, pos))) {
cm.state.draggingText(e);
// Ensure the editor is re-focused
setTimeout(bind(focusInput, cm), 20);
return;
}
try {
var text = e.dataTransfer.getData("Text");
if (text) {
var curFrom = cm.doc.sel.from, curTo = cm.doc.sel.to;
setSelection(cm.doc, pos, pos);
if (cm.state.draggingText) replaceRange(cm.doc, "", curFrom, curTo, "paste");
cm.replaceSelection(text, null, "paste");
focusInput(cm);
onFocus(cm);
}
}
catch(e){}
}
}
function onDragStart(cm, e) {
if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return; }
if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return;
var txt = cm.getSelection();
e.dataTransfer.setData("Text", txt);
// Use dummy image instead of default browsers image.
// Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there.
if (e.dataTransfer.setDragImage && !safari) {
var img = elt("img", null, null, "position: fixed; left: 0; top: 0;");
if (opera) {
img.width = img.height = 1;
cm.display.wrapper.appendChild(img);
// Force a relayout, or Opera won't use our image for some obscure reason
img._top = img.offsetTop;
}
e.dataTransfer.setDragImage(img, 0, 0);
if (opera) img.parentNode.removeChild(img);
}
}
function setScrollTop(cm, val) {
if (Math.abs(cm.doc.scrollTop - val) < 2) return;
cm.doc.scrollTop = val;
if (!gecko) updateDisplay(cm, [], val);
if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val;
if (cm.display.scrollbarV.scrollTop != val) cm.display.scrollbarV.scrollTop = val;
if (gecko) updateDisplay(cm, []);
startWorker(cm, 100);
}
function setScrollLeft(cm, val, isScroller) {
if (isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) return;
val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth);
cm.doc.scrollLeft = val;
alignHorizontally(cm);
if (cm.display.scroller.scrollLeft != val) cm.display.scroller.scrollLeft = val;
if (cm.display.scrollbarH.scrollLeft != val) cm.display.scrollbarH.scrollLeft = val;
}
// Since the delta values reported on mouse wheel events are
// unstandardized between browsers and even browser versions, and
// generally horribly unpredictable, this code starts by measuring
// the scroll effect that the first few mouse wheel events have,
// and, from that, detects the way it can convert deltas to pixel
// offsets afterwards.
//
// The reason we want to know the amount a wheel event will scroll
// is that it gives us a chance to update the display before the
// actual scrolling happens, reducing flickering.
var wheelSamples = 0, wheelPixelsPerUnit = null;
// Fill in a browser-detected starting value on browsers where we
// know one. These don't have to be accurate -- the result of them
// being wrong would just be a slight flicker on the first wheel
// scroll (if it is large enough).
if (ie) wheelPixelsPerUnit = -.53;
else if (gecko) wheelPixelsPerUnit = 15;
else if (chrome) wheelPixelsPerUnit = -.7;
else if (safari) wheelPixelsPerUnit = -1/3;
function onScrollWheel(cm, e) {
var dx = e.wheelDeltaX, dy = e.wheelDeltaY;
if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) dx = e.detail;
if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) dy = e.detail;
else if (dy == null) dy = e.wheelDelta;
var display = cm.display, scroll = display.scroller;
// Quit if there's nothing to scroll here
if (!(dx && scroll.scrollWidth > scroll.clientWidth ||
dy && scroll.scrollHeight > scroll.clientHeight)) return;
// Webkit browsers on OS X abort momentum scrolls when the target
// of the scroll event is removed from the scrollable element.
// This hack (see related code in patchDisplay) makes sure the
// element is kept around.
if (dy && mac && webkit) {
for (var cur = e.target; cur != scroll; cur = cur.parentNode) {
if (cur.lineObj) {
cm.display.currentWheelTarget = cur;
break;
}
}
}
// On some browsers, horizontal scrolling will cause redraws to
// happen before the gutter has been realigned, causing it to
// wriggle around in a most unseemly way. When we have an
// estimated pixels/delta value, we just handle horizontal
// scrolling entirely here. It'll be slightly off from native, but
// better than glitching out.
if (dx && !gecko && !opera && wheelPixelsPerUnit != null) {
if (dy)
setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight)));
setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth)));
e_preventDefault(e);
display.wheelStartX = null; // Abort measurement, if in progress
return;
}
if (dy && wheelPixelsPerUnit != null) {
var pixels = dy * wheelPixelsPerUnit;
var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight;
if (pixels < 0) top = Math.max(0, top + pixels - 50);
else bot = Math.min(cm.doc.height, bot + pixels + 50);
updateDisplay(cm, [], {top: top, bottom: bot});
}
if (wheelSamples < 20) {
if (display.wheelStartX == null) {
display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop;
display.wheelDX = dx; display.wheelDY = dy;
setTimeout(function() {
if (display.wheelStartX == null) return;
var movedX = scroll.scrollLeft - display.wheelStartX;
var movedY = scroll.scrollTop - display.wheelStartY;
var sample = (movedY && display.wheelDY && movedY / display.wheelDY) ||
(movedX && display.wheelDX && movedX / display.wheelDX);
display.wheelStartX = display.wheelStartY = null;
if (!sample) return;
wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1);
++wheelSamples;
}, 200);
} else {
display.wheelDX += dx; display.wheelDY += dy;
}
}
}
function doHandleBinding(cm, bound, dropShift) {
if (typeof bound == "string") {
bound = commands[bound];
if (!bound) return false;
}
// Ensure previous input has been read, so that the handler sees a
// consistent view of the document
if (cm.display.pollingFast && readInput(cm)) cm.display.pollingFast = false;
var doc = cm.doc, prevShift = doc.sel.shift, done = false;
try {
if (isReadOnly(cm)) cm.state.suppressEdits = true;
if (dropShift) doc.sel.shift = false;
done = bound(cm) != Pass;
} finally {
doc.sel.shift = prevShift;
cm.state.suppressEdits = false;
}
return done;
}
function allKeyMaps(cm) {
var maps = cm.state.keyMaps.slice(0);
if (cm.options.extraKeys) maps.push(cm.options.extraKeys);
maps.push(cm.options.keyMap);
return maps;
}
var maybeTransition;
function handleKeyBinding(cm, e) {
// Handle auto keymap transitions
var startMap = getKeyMap(cm.options.keyMap), next = startMap.auto;
clearTimeout(maybeTransition);
if (next && !isModifierKey(e)) maybeTransition = setTimeout(function() {
if (getKeyMap(cm.options.keyMap) == startMap) {
cm.options.keyMap = (next.call ? next.call(null, cm) : next);
keyMapChanged(cm);
}
}, 50);
var name = keyName(e, true), handled = false;
if (!name) return false;
var keymaps = allKeyMaps(cm);
if (e.shiftKey) {
// First try to resolve full name (including 'Shift-'). Failing
// that, see if there is a cursor-motion command (starting with
// 'go') bound to the keyname without 'Shift-'.
handled = lookupKey("Shift-" + name, keymaps, function(b) {return doHandleBinding(cm, b, true);})
|| lookupKey(name, keymaps, function(b) {
if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion)
return doHandleBinding(cm, b);
});
} else {
handled = lookupKey(name, keymaps, function(b) { return doHandleBinding(cm, b); });
}
if (handled) {
e_preventDefault(e);
restartBlink(cm);
if (ie_lt9) { e.oldKeyCode = e.keyCode; e.keyCode = 0; }
signalLater(cm, "keyHandled", cm, name, e);
}
return handled;
}
function handleCharBinding(cm, e, ch) {
var handled = lookupKey("'" + ch + "'", allKeyMaps(cm),
function(b) { return doHandleBinding(cm, b, true); });
if (handled) {
e_preventDefault(e);
restartBlink(cm);
signalLater(cm, "keyHandled", cm, "'" + ch + "'", e);
}
return handled;
}
var lastStoppedKey = null;
function onKeyDown(e) {
var cm = this;
if (!cm.state.focused) onFocus(cm);
if (ie && e.keyCode == 27) { e.returnValue = false; }
if (signalDOMEvent(cm, e) || cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return;
var code = e.keyCode;
// IE does strange things with escape.
cm.doc.sel.shift = code == 16 || e.shiftKey;
// First give onKeyEvent option a chance to handle this.
var handled = handleKeyBinding(cm, e);
// On text input if value was temporaritly set for a screenreader, clear it out.
if (!handled && cm.state.accessibleTextareaWaiting) {
clearAccessibleTextarea(cm);
}
if (opera) {
lastStoppedKey = handled ? code : null;
// Opera has no cut event... we try to at least catch the key combo
if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey))
cm.replaceSelection("");
}
}
function onKeyPress(e) {
var cm = this;
if (signalDOMEvent(cm, e) || cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return;
var keyCode = e.keyCode, charCode = e.charCode;
if (opera && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;}
if (((opera && (!e.which || e.which < 10)) || khtml) && handleKeyBinding(cm, e)) return;
var ch = String.fromCharCode(charCode == null ? keyCode : charCode);
if (this.options.electricChars && this.doc.mode.electricChars &&
this.options.smartIndent && !isReadOnly(this) &&
this.doc.mode.electricChars.indexOf(ch) > -1)
setTimeout(operation(cm, function() {indentLine(cm, cm.doc.sel.to.line, "smart");}), 75);
if (handleCharBinding(cm, e, ch)) return;
if (ie && !ie_lt9) cm.display.inputHasSelection = null;
fastPoll(cm);
}
function onFocus(cm) {
if (cm.options.readOnly == "nocursor") return;
if (!cm.state.focused) {
signal(cm, "focus", cm);
cm.state.focused = true;
if (cm.display.wrapper.className.search(/\bCodeMirror-focused\b/) == -1)
cm.display.wrapper.className += " CodeMirror-focused";
resetInput(cm, true);
}
slowPoll(cm);
restartBlink(cm);
}
function onBlur(cm) {
if (cm.state.focused) {
signal(cm, "blur", cm);
cm.state.focused = false;
cm.display.wrapper.className = cm.display.wrapper.className.replace(" CodeMirror-focused", "");
}
clearInterval(cm.display.blinker);
setTimeout(function() {if (!cm.state.focused) cm.doc.sel.shift = false;}, 150);
}
var detectingSelectAll;
function onContextMenu(cm, e) {
if (signalDOMEvent(cm, e, "contextmenu")) return;
var display = cm.display, sel = cm.doc.sel;
if (eventInWidget(display, e)) return;
var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop;
if (!pos || opera) return; // Opera is difficult.
if (posEq(sel.from, sel.to) || posLess(pos, sel.from) || !posLess(pos, sel.to))
operation(cm, setSelection)(cm.doc, pos, pos);
var oldCSS = display.input.style.cssText;
display.inputDiv.style.position = "absolute";
display.input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) +
"px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: white; outline: none;" +
"border-width: 0; outline: none; overflow: hidden; opacity: .05; -ms-opacity: .05; filter: alpha(opacity=5);";
focusInput(cm);
resetInput(cm, true);
// Adds "Select all" to context menu in FF
if (posEq(sel.from, sel.to)) display.input.value = display.prevInput = " ";
function prepareSelectAllHack() {
if (display.input.selectionStart != null) {
var extval = display.input.value = " " + (posEq(sel.from, sel.to) ? "" : display.input.value);
display.prevInput = " ";
display.input.selectionStart = 1; display.input.selectionEnd = extval.length;
}
}
function rehide() {
display.inputDiv.style.position = "relative";
display.input.style.cssText = oldCSS;
if (ie_lt9) display.scrollbarV.scrollTop = display.scroller.scrollTop = scrollPos;
slowPoll(cm);
// Try to detect the user choosing select-all
if (display.input.selectionStart != null) {
if (!ie || ie_lt9) prepareSelectAllHack();
clearTimeout(detectingSelectAll);
var i = 0, poll = function(){
if (display.prevInput == " " && display.input.selectionStart == 0)
operation(cm, commands.selectAll)(cm);
else if (i++ < 10) detectingSelectAll = setTimeout(poll, 500);
else resetInput(cm);
};
detectingSelectAll = setTimeout(poll, 200);
}
}
if (ie && !ie_lt9) prepareSelectAllHack();
if (captureMiddleClick) {
e_stop(e);
var mouseup = function() {
off(window, "mouseup", mouseup);
setTimeout(rehide, 20);
};
on(window, "mouseup", mouseup);
} else {
setTimeout(rehide, 50);
}
}
// UPDATING
var changeEnd = CodeMirror.changeEnd = function(change) {
if (!change.text) return change.to;
return Pos(change.from.line + change.text.length - 1,
lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0));
};
// Make sure a position will be valid after the given change.
function clipPostChange(doc, change, pos) {
if (!posLess(change.from, pos)) return clipPos(doc, pos);
var diff = (change.text.length - 1) - (change.to.line - change.from.line);
if (pos.line > change.to.line + diff) {
var preLine = pos.line - diff, lastLine = doc.first + doc.size - 1;
if (preLine > lastLine) return Pos(lastLine, getLine(doc, lastLine).text.length);
return clipToLen(pos, getLine(doc, preLine).text.length);
}
if (pos.line == change.to.line + diff)
return clipToLen(pos, lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0) +
getLine(doc, change.to.line).text.length - change.to.ch);
var inside = pos.line - change.from.line;
return clipToLen(pos, change.text[inside].length + (inside ? 0 : change.from.ch));
}
// Hint can be null|"end"|"start"|"around"|{anchor,head}
function computeSelAfterChange(doc, change, hint) {
if (hint && typeof hint == "object") // Assumed to be {anchor, head} object
return {anchor: clipPostChange(doc, change, hint.anchor),
head: clipPostChange(doc, change, hint.head)};
if (hint == "start") return {anchor: change.from, head: change.from};
var end = changeEnd(change);
if (hint == "around") return {anchor: change.from, head: end};
if (hint == "end") return {anchor: end, head: end};
// hint is null, leave the selection alone as much as possible
var adjustPos = function(pos) {
if (posLess(pos, change.from)) return pos;
if (!posLess(change.to, pos)) return end;
var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch;
if (pos.line == change.to.line) ch += end.ch - change.to.ch;
return Pos(line, ch);
};
return {anchor: adjustPos(doc.sel.anchor), head: adjustPos(doc.sel.head)};
}
function filterChange(doc, change, update) {
var obj = {
canceled: false,
from: change.from,
to: change.to,
text: change.text,
origin: change.origin,
cancel: function() { this.canceled = true; }
};
if (update) obj.update = function(from, to, text, origin) {
if (from) this.from = clipPos(doc, from);
if (to) this.to = clipPos(doc, to);
if (text) this.text = text;
if (origin !== undefined) this.origin = origin;
};
signal(doc, "beforeChange", doc, obj);
if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj);
if (obj.canceled) return null;
return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin};
}
// Replace the range from from to to by the strings in replacement.
// change is a {from, to, text [, origin]} object
function makeChange(doc, change, selUpdate, ignoreReadOnly) {
if (doc.cm) {
if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, selUpdate, ignoreReadOnly);
if (doc.cm.state.suppressEdits) return;
}
if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) {
change = filterChange(doc, change, true);
if (!change) return;
}
// Possibly split or suppress the update based on the presence
// of read-only spans in its range.
var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to);
if (split) {
for (var i = split.length - 1; i >= 1; --i)
makeChangeNoReadonly(doc, {from: split[i].from, to: split[i].to, text: [""]});
if (split.length)
makeChangeNoReadonly(doc, {from: split[0].from, to: split[0].to, text: change.text}, selUpdate);
} else {
makeChangeNoReadonly(doc, change, selUpdate);
}
}
function makeChangeNoReadonly(doc, change, selUpdate) {
var selAfter = computeSelAfterChange(doc, change, selUpdate);
addToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN);
makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change));
var rebased = [];
linkedDocs(doc, function(doc, sharedHist) {
if (!sharedHist && indexOf(rebased, doc.history) == -1) {
rebaseHist(doc.history, change);
rebased.push(doc.history);
}
makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change));
});
}
function makeChangeFromHistory(doc, type) {
if (doc.cm && doc.cm.state.suppressEdits) return;
var hist = doc.history;
var event = (type == "undo" ? hist.done : hist.undone).pop();
if (!event) return;
var anti = {changes: [], anchorBefore: event.anchorAfter, headBefore: event.headAfter,
anchorAfter: event.anchorBefore, headAfter: event.headBefore,
generation: hist.generation};
(type == "undo" ? hist.undone : hist.done).push(anti);
hist.generation = event.generation || ++hist.maxGeneration;
var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange");
for (var i = event.changes.length - 1; i >= 0; --i) {
var change = event.changes[i];
change.origin = type;
if (filter && !filterChange(doc, change, false)) {
(type == "undo" ? hist.done : hist.undone).length = 0;
return;
}
anti.changes.push(historyChangeFromChange(doc, change));
var after = i ? computeSelAfterChange(doc, change, null)
: {anchor: event.anchorBefore, head: event.headBefore};
makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change));
var rebased = [];
linkedDocs(doc, function(doc, sharedHist) {
if (!sharedHist && indexOf(rebased, doc.history) == -1) {
rebaseHist(doc.history, change);
rebased.push(doc.history);
}
makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change));
});
}
}
function shiftDoc(doc, distance) {
function shiftPos(pos) {return Pos(pos.line + distance, pos.ch);}
doc.first += distance;
if (doc.cm) regChange(doc.cm, doc.first, doc.first, distance);
doc.sel.head = shiftPos(doc.sel.head); doc.sel.anchor = shiftPos(doc.sel.anchor);
doc.sel.from = shiftPos(doc.sel.from); doc.sel.to = shiftPos(doc.sel.to);
}
function makeChangeSingleDoc(doc, change, selAfter, spans) {
if (doc.cm && !doc.cm.curOp)
return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans);
if (change.to.line < doc.first) {
shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line));
return;
}
if (change.from.line > doc.lastLine()) return;
// Clip the change to the size of this doc
if (change.from.line < doc.first) {
var shift = change.text.length - 1 - (doc.first - change.from.line);
shiftDoc(doc, shift);
change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch),
text: [lst(change.text)], origin: change.origin};
}
var last = doc.lastLine();
if (change.to.line > last) {
change = {from: change.from, to: Pos(last, getLine(doc, last).text.length),
text: [change.text[0]], origin: change.origin};
}
change.removed = getBetween(doc, change.from, change.to);
if (!selAfter) selAfter = computeSelAfterChange(doc, change, null);
if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans, selAfter);
else updateDoc(doc, change, spans, selAfter);
}
function makeChangeSingleDocInEditor(cm, change, spans, selAfter) {
var doc = cm.doc, display = cm.display, from = change.from, to = change.to;
var recomputeMaxLength = false, checkWidthStart = from.line;
if (!cm.options.lineWrapping) {
checkWidthStart = lineNo(visualLine(doc, getLine(doc, from.line)));
doc.iter(checkWidthStart, to.line + 1, function(line) {
if (line == display.maxLine) {
recomputeMaxLength = true;
return true;
}
});
}
if (!posLess(doc.sel.head, change.from) && !posLess(change.to, doc.sel.head))
cm.curOp.cursorActivity = true;
updateDoc(doc, change, spans, selAfter, estimateHeight(cm));
if (!cm.options.lineWrapping) {
doc.iter(checkWidthStart, from.line + change.text.length, function(line) {
var len = lineLength(doc, line);
if (len > display.maxLineLength) {
display.maxLine = line;
display.maxLineLength = len;
display.maxLineChanged = true;
recomputeMaxLength = false;
}
});
if (recomputeMaxLength) cm.curOp.updateMaxLine = true;
}
// Adjust frontier, schedule worker
doc.frontier = Math.min(doc.frontier, from.line);
startWorker(cm, 400);
var lendiff = change.text.length - (to.line - from.line) - 1;
// Remember that these lines changed, for updating the display
regChange(cm, from.line, to.line + 1, lendiff);
if (hasHandler(cm, "change")) {
var changeObj = {from: from, to: to,
text: change.text,
removed: change.removed,
origin: change.origin};
if (cm.curOp.textChanged) {
for (var cur = cm.curOp.textChanged; cur.next; cur = cur.next) {}
cur.next = changeObj;
} else cm.curOp.textChanged = changeObj;
}
}
function replaceRange(doc, code, from, to, origin) {
if (!to) to = from;
if (posLess(to, from)) { var tmp = to; to = from; from = tmp; }
if (typeof code == "string") code = splitLines(code);
makeChange(doc, {from: from, to: to, text: code, origin: origin}, null);
}
// POSITION OBJECT
function Pos(line, ch) {
if (!(this instanceof Pos)) return new Pos(line, ch);
this.line = line; this.ch = ch;
}
CodeMirror.Pos = Pos;
function posEq(a, b) {return a.line == b.line && a.ch == b.ch;}
function posLess(a, b) {return a.line < b.line || (a.line == b.line && a.ch < b.ch);}
function copyPos(x) {return Pos(x.line, x.ch);}
// SELECTION
function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1));}
function clipPos(doc, pos) {
if (pos.line < doc.first) return Pos(doc.first, 0);
var last = doc.first + doc.size - 1;
if (pos.line > last) return Pos(last, getLine(doc, last).text.length);
return clipToLen(pos, getLine(doc, pos.line).text.length);
}
function clipToLen(pos, linelen) {
var ch = pos.ch;
if (ch == null || ch > linelen) return Pos(pos.line, linelen);
else if (ch < 0) return Pos(pos.line, 0);
else return pos;
}
function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size;}
// If shift is held, this will move the selection anchor. Otherwise,
// it'll set the whole selection.
function extendSelection(doc, pos, other, bias) {
if (doc.sel.shift || doc.sel.extend) {
var anchor = doc.sel.anchor;
if (other) {
var posBefore = posLess(pos, anchor);
if (posBefore != posLess(other, anchor)) {
anchor = pos;
pos = other;
} else if (posBefore != posLess(pos, other)) {
pos = other;
}
}
setSelection(doc, anchor, pos, bias);
} else {
setSelection(doc, pos, other || pos, bias);
}
if (doc.cm) doc.cm.curOp.userSelChange = true;
if (doc.cm) {
var from = doc.sel.from;
var to = doc.sel.to;
if (posEq(from, to) && doc.cm.display.input.setSelectionRange) {
clearTimeout(doc.cm.state.accessibleTextareaTimeout);
doc.cm.state.accessibleTextareaWaiting = true;
doc.cm.display.input.value = doc.getLine(from.line) + "\n";
doc.cm.display.input.setSelectionRange(from.ch, from.ch);
doc.cm.state.accessibleTextareaTimeout = setTimeout(function() {
clearAccessibleTextarea(doc.cm);
}, 80);
}
}
}
function clearAccessibleTextarea(cm) {
clearTimeout(cm.state.accessibleTextareaTimeout);
cm.state.accessibleTextareaWaiting = false;
resetInput(cm, true);
}
function filterSelectionChange(doc, anchor, head) {
var obj = {anchor: anchor, head: head};
signal(doc, "beforeSelectionChange", doc, obj);
if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj);
obj.anchor = clipPos(doc, obj.anchor); obj.head = clipPos(doc, obj.head);
return obj;
}
// Update the selection. Last two args are only used by
// updateDoc, since they have to be expressed in the line
// numbers before the update.
function setSelection(doc, anchor, head, bias, checkAtomic) {
if (!checkAtomic && hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) {
var filtered = filterSelectionChange(doc, anchor, head);
head = filtered.head;
anchor = filtered.anchor;
}
var sel = doc.sel;
sel.goalColumn = null;
// Skip over atomic spans.
if (checkAtomic || !posEq(anchor, sel.anchor))
anchor = skipAtomic(doc, anchor, bias, checkAtomic != "push");
if (checkAtomic || !posEq(head, sel.head))
head = skipAtomic(doc, head, bias, checkAtomic != "push");
if (posEq(sel.anchor, anchor) && posEq(sel.head, head)) return;
sel.anchor = anchor; sel.head = head;
var inv = posLess(head, anchor);
sel.from = inv ? head : anchor;
sel.to = inv ? anchor : head;
if (doc.cm)
doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged =
doc.cm.curOp.cursorActivity = true;
signalLater(doc, "cursorActivity", doc);
}
function reCheckSelection(cm) {
setSelection(cm.doc, cm.doc.sel.from, cm.doc.sel.to, null, "push");
}
function skipAtomic(doc, pos, bias, mayClear) {
var flipped = false, curPos = pos;
var dir = bias || 1;
doc.cantEdit = false;
search: for (;;) {
var line = getLine(doc, curPos.line);
if (line.markedSpans) {
for (var i = 0; i < line.markedSpans.length; ++i) {
var sp = line.markedSpans[i], m = sp.marker;
if ((sp.from == null || (m.inclusiveLeft ? sp.from <= curPos.ch : sp.from < curPos.ch)) &&
(sp.to == null || (m.inclusiveRight ? sp.to >= curPos.ch : sp.to > curPos.ch))) {
if (mayClear) {
signal(m, "beforeCursorEnter");
if (m.explicitlyCleared) {
if (!line.markedSpans) break;
else {--i; continue;}
}
}
if (!m.atomic) continue;
var newPos = m.find()[dir < 0 ? "from" : "to"];
if (posEq(newPos, curPos)) {
newPos.ch += dir;
if (newPos.ch < 0) {
if (newPos.line > doc.first) newPos = clipPos(doc, Pos(newPos.line - 1));
else newPos = null;
} else if (newPos.ch > line.text.length) {
if (newPos.line < doc.first + doc.size - 1) newPos = Pos(newPos.line + 1, 0);
else newPos = null;
}
if (!newPos) {
if (flipped) {
// Driven in a corner -- no valid cursor position found at all
// -- try again *with* clearing, if we didn't already
if (!mayClear) return skipAtomic(doc, pos, bias, true);
// Otherwise, turn off editing until further notice, and return the start of the doc
doc.cantEdit = true;
return Pos(doc.first, 0);
}
flipped = true; newPos = pos; dir = -dir;
}
}
curPos = newPos;
continue search;
}
}
}
return curPos;
}
}
// SCROLLING
function scrollCursorIntoView(cm) {
var coords = scrollPosIntoView(cm, cm.doc.sel.head, cm.options.cursorScrollMargin);
if (!cm.state.focused) return;
var display = cm.display, box = getRect(display.sizer), doScroll = null;
if (coords.top + box.top < 0) doScroll = true;
else if (coords.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false;
if (doScroll != null && !phantom) {
var hidden = display.cursor.style.display == "none";
if (hidden) {
display.cursor.style.display = "";
display.cursor.style.left = coords.left + "px";
display.cursor.style.top = (coords.top - display.viewOffset) + "px";
}
display.cursor.scrollIntoView(doScroll);
if (hidden) display.cursor.style.display = "none";
}
}
function scrollPosIntoView(cm, pos, margin) {
if (margin == null) margin = 0;
for (;;) {
var changed = false, coords = cursorCoords(cm, pos);
var scrollPos = calculateScrollPos(cm, coords.left, coords.top - margin, coords.left, coords.bottom + margin);
var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft;
if (scrollPos.scrollTop != null) {
setScrollTop(cm, scrollPos.scrollTop);
if (Math.abs(cm.doc.scrollTop - startTop) > 1) changed = true;
}
if (scrollPos.scrollLeft != null) {
setScrollLeft(cm, scrollPos.scrollLeft);
if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) changed = true;
}
if (!changed) return coords;
}
}
function scrollIntoView(cm, x1, y1, x2, y2) {
var scrollPos = calculateScrollPos(cm, x1, y1, x2, y2);
if (scrollPos.scrollTop != null) setScrollTop(cm, scrollPos.scrollTop);
if (scrollPos.scrollLeft != null) setScrollLeft(cm, scrollPos.scrollLeft);
}
function calculateScrollPos(cm, x1, y1, x2, y2) {
var display = cm.display, snapMargin = textHeight(cm.display);
if (y1 < 0) y1 = 0;
var screen = display.scroller.clientHeight - scrollerCutOff, screentop = display.scroller.scrollTop, result = {};
var docBottom = cm.doc.height + paddingVert(display);
var atTop = y1 < snapMargin, atBottom = y2 > docBottom - snapMargin;
if (y1 < screentop) {
result.scrollTop = atTop ? 0 : y1;
} else if (y2 > screentop + screen) {
var newTop = Math.min(y1, (atBottom ? docBottom : y2) - screen);
if (newTop != screentop) result.scrollTop = newTop;
}
var screenw = display.scroller.clientWidth - scrollerCutOff, screenleft = display.scroller.scrollLeft;
x1 += display.gutters.offsetWidth; x2 += display.gutters.offsetWidth;
var gutterw = display.gutters.offsetWidth;
var atLeft = x1 < gutterw + 10;
if (x1 < screenleft + gutterw || atLeft) {
if (atLeft) x1 = 0;
result.scrollLeft = Math.max(0, x1 - 10 - gutterw);
} else if (x2 > screenw + screenleft - 3) {
result.scrollLeft = x2 + 10 - screenw;
}
return result;
}
function updateScrollPos(cm, left, top) {
cm.curOp.updateScrollPos = {scrollLeft: left == null ? cm.doc.scrollLeft : left,
scrollTop: top == null ? cm.doc.scrollTop : top};
}
function addToScrollPos(cm, left, top) {
var pos = cm.curOp.updateScrollPos || (cm.curOp.updateScrollPos = {scrollLeft: cm.doc.scrollLeft, scrollTop: cm.doc.scrollTop});
var scroll = cm.display.scroller;
pos.scrollTop = Math.max(0, Math.min(scroll.scrollHeight - scroll.clientHeight, pos.scrollTop + top));
pos.scrollLeft = Math.max(0, Math.min(scroll.scrollWidth - scroll.clientWidth, pos.scrollLeft + left));
}
// API UTILITIES
function indentLine(cm, n, how, aggressive) {
var doc = cm.doc;
if (how == null) how = "add";
if (how == "smart") {
if (!cm.doc.mode.indent) how = "prev";
else var state = getStateBefore(cm, n);
}
var tabSize = cm.options.tabSize;
var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize);
var curSpaceString = line.text.match(/^\s*/)[0], indentation;
if (how == "smart") {
indentation = cm.doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text);
if (indentation == Pass) {
if (!aggressive) return;
how = "prev";
}
}
if (how == "prev") {
if (n > doc.first) indentation = countColumn(getLine(doc, n-1).text, null, tabSize);
else indentation = 0;
} else if (how == "add") {
indentation = curSpace + cm.options.indentUnit;
} else if (how == "subtract") {
indentation = curSpace - cm.options.indentUnit;
} else if (typeof how == "number") {
indentation = curSpace + how;
}
indentation = Math.max(0, indentation);
var indentString = "", pos = 0;
if (cm.options.indentWithTabs)
for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";}
if (pos < indentation) indentString += spaceStr(indentation - pos);
if (indentString != curSpaceString)
replaceRange(cm.doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input");
line.stateAfter = null;
}
function changeLine(cm, handle, op) {
var no = handle, line = handle, doc = cm.doc;
if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle));
else no = lineNo(handle);
if (no == null) return null;
if (op(line, no)) regChange(cm, no, no + 1);
else return null;
return line;
}
function findPosH(doc, pos, dir, unit, visually) {
var line = pos.line, ch = pos.ch, origDir = dir;
var lineObj = getLine(doc, line);
var possible = true;
function findNextLine() {
var l = line + dir;
if (l < doc.first || l >= doc.first + doc.size) return (possible = false);
line = l;
return lineObj = getLine(doc, l);
}
function moveOnce(boundToLine) {
var next = (visually ? moveVisually : moveLogically)(lineObj, ch, dir, true);
if (next == null) {
if (!boundToLine && findNextLine()) {
if (visually) ch = (dir < 0 ? lineRight : lineLeft)(lineObj);
else ch = dir < 0 ? lineObj.text.length : 0;
} else return (possible = false);
} else ch = next;
return true;
}
if (unit == "char") moveOnce();
else if (unit == "column") moveOnce(true);
else if (unit == "word" || unit == "group") {
var sawType = null, group = unit == "group";
for (var first = true;; first = false) {
if (dir < 0 && !moveOnce(!first)) break;
var cur = lineObj.text.charAt(ch) || "\n";
var type = isWordChar(cur) ? "w"
: !group ? null
: /\s/.test(cur) ? null
: "p";
if (sawType && sawType != type) {
if (dir < 0) {dir = 1; moveOnce();}
break;
}
if (type) sawType = type;
if (dir > 0 && !moveOnce(!first)) break;
}
}
var result = skipAtomic(doc, Pos(line, ch), origDir, true);
if (!possible) result.hitSide = true;
return result;
}
function findPosV(cm, pos, dir, unit) {
var doc = cm.doc, x = pos.left, y;
if (unit == "page") {
var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight);
y = pos.top + dir * (pageSize - (dir < 0 ? 1.5 : .5) * textHeight(cm.display));
} else if (unit == "line") {
y = dir > 0 ? pos.bottom + 3 : pos.top - 3;
}
for (;;) {
var target = coordsChar(cm, x, y);
if (!target.outside) break;
if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break; }
y += dir * 5;
}
return target;
}
function findWordAt(line, pos) {
var start = pos.ch, end = pos.ch;
if (line) {
if ((pos.xRel < 0 || end == line.length) && start) --start; else ++end;
var startChar = line.charAt(start);
var check = isWordChar(startChar) ? isWordChar
: /\s/.test(startChar) ? function(ch) {return /\s/.test(ch);}
: function(ch) {return !/\s/.test(ch) && !isWordChar(ch);};
while (start > 0 && check(line.charAt(start - 1))) --start;
while (end < line.length && check(line.charAt(end))) ++end;
}
return {from: Pos(pos.line, start), to: Pos(pos.line, end)};
}
function selectLine(cm, line) {
extendSelection(cm.doc, Pos(line, 0), clipPos(cm.doc, Pos(line + 1, 0)));
}
// PROTOTYPE
// The publicly visible API. Note that operation(null, f) means
// 'wrap f in an operation, performed on its `this` parameter'
CodeMirror.prototype = {
constructor: CodeMirror,
focus: function(){window.focus(); focusInput(this); onFocus(this); fastPoll(this);},
setOption: function(option, value) {
var options = this.options, old = options[option];
if (options[option] == value && option != "mode") return;
options[option] = value;
if (optionHandlers.hasOwnProperty(option))
operation(this, optionHandlers[option])(this, value, old);
},
getOption: function(option) {return this.options[option];},
getDoc: function() {return this.doc;},
addKeyMap: function(map, bottom) {
this.state.keyMaps[bottom ? "push" : "unshift"](map);
},
removeKeyMap: function(map) {
var maps = this.state.keyMaps;
for (var i = 0; i < maps.length; ++i)
if (maps[i] == map || (typeof maps[i] != "string" && maps[i].name == map)) {
maps.splice(i, 1);
return true;
}
},
addOverlay: operation(null, function(spec, options) {
var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec);
if (mode.startState) throw new Error("Overlays may not be stateful.");
this.state.overlays.push({mode: mode, modeSpec: spec, opaque: options && options.opaque});
this.state.modeGen++;
regChange(this);
}),
removeOverlay: operation(null, function(spec) {
var overlays = this.state.overlays;
for (var i = 0; i < overlays.length; ++i) {
var cur = overlays[i].modeSpec;
if (cur == spec || typeof spec == "string" && cur.name == spec) {
overlays.splice(i, 1);
this.state.modeGen++;
regChange(this);
return;
}
}
}),
indentLine: operation(null, function(n, dir, aggressive) {
if (typeof dir != "string" && typeof dir != "number") {
if (dir == null) dir = this.options.smartIndent ? "smart" : "prev";
else dir = dir ? "add" : "subtract";
}
if (isLine(this.doc, n)) indentLine(this, n, dir, aggressive);
}),
indentSelection: operation(null, function(how) {
var sel = this.doc.sel;
if (posEq(sel.from, sel.to)) return indentLine(this, sel.from.line, how);
var e = sel.to.line - (sel.to.ch ? 0 : 1);
for (var i = sel.from.line; i <= e; ++i) indentLine(this, i, how);
}),
// Fetch the parser token for a given character. Useful for hacks
// that want to inspect the mode state (say, for completion).
getTokenAt: function(pos, precise) {
var doc = this.doc;
pos = clipPos(doc, pos);
var state = getStateBefore(this, pos.line, precise), mode = this.doc.mode;
var line = getLine(doc, pos.line);
var stream = new StringStream(line.text, this.options.tabSize);
while (stream.pos < pos.ch && !stream.eol()) {
stream.start = stream.pos;
var style = mode.token(stream, state);
}
return {start: stream.start,
end: stream.pos,
string: stream.current(),
className: style || null, // Deprecated, use 'type' instead
type: style || null,
state: state};
},
getTokenTypeAt: function(pos) {
pos = clipPos(this.doc, pos);
var styles = getLineStyles(this, getLine(this.doc, pos.line));
var before = 0, after = (styles.length - 1) / 2, ch = pos.ch;
if (ch == 0) return styles[2];
for (;;) {
var mid = (before + after) >> 1;
if ((mid ? styles[mid * 2 - 1] : 0) >= ch) after = mid;
else if (styles[mid * 2 + 1] < ch) before = mid + 1;
else return styles[mid * 2 + 2];
}
},
getModeAt: function(pos) {
var mode = this.doc.mode;
if (!mode.innerMode) return mode;
return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode;
},
getHelper: function(pos, type) {
if (!helpers.hasOwnProperty(type)) return;
var help = helpers[type], mode = this.getModeAt(pos);
return mode[type] && help[mode[type]] ||
mode.helperType && help[mode.helperType] ||
help[mode.name];
},
getStateAfter: function(line, precise) {
var doc = this.doc;
line = clipLine(doc, line == null ? doc.first + doc.size - 1: line);
return getStateBefore(this, line + 1, precise);
},
cursorCoords: function(start, mode) {
var pos, sel = this.doc.sel;
if (start == null) pos = sel.head;
else if (typeof start == "object") pos = clipPos(this.doc, start);
else pos = start ? sel.from : sel.to;
return cursorCoords(this, pos, mode || "page");
},
charCoords: function(pos, mode) {
return charCoords(this, clipPos(this.doc, pos), mode || "page");
},
coordsChar: function(coords, mode) {
coords = fromCoordSystem(this, coords, mode || "page");
return coordsChar(this, coords.left, coords.top);
},
lineAtHeight: function(height, mode) {
height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top;
return lineAtHeight(this.doc, height + this.display.viewOffset);
},
heightAtLine: function(line, mode) {
var end = false, last = this.doc.first + this.doc.size - 1;
if (line < this.doc.first) line = this.doc.first;
else if (line > last) { line = last; end = true; }
var lineObj = getLine(this.doc, line);
return intoCoordSystem(this, getLine(this.doc, line), {top: 0, left: 0}, mode || "page").top +
(end ? lineObj.height : 0);
},
defaultTextHeight: function() { return textHeight(this.display); },
defaultCharWidth: function() { return charWidth(this.display); },
setGutterMarker: operation(null, function(line, gutterID, value) {
return changeLine(this, line, function(line) {
var markers = line.gutterMarkers || (line.gutterMarkers = {});
markers[gutterID] = value;
if (!value && isEmpty(markers)) line.gutterMarkers = null;
return true;
});
}),
clearGutter: operation(null, function(gutterID) {
var cm = this, doc = cm.doc, i = doc.first;
doc.iter(function(line) {
if (line.gutterMarkers && line.gutterMarkers[gutterID]) {
line.gutterMarkers[gutterID] = null;
regChange(cm, i, i + 1);
if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null;
}
++i;
});
}),
addLineClass: operation(null, function(handle, where, cls) {
return changeLine(this, handle, function(line) {
var prop = where == "text" ? "textClass" : where == "background" ? "bgClass" : "wrapClass";
if (!line[prop]) line[prop] = cls;
else if (new RegExp("(?:^|\\s)" + cls + "(?:$|\\s)").test(line[prop])) return false;
else line[prop] += " " + cls;
return true;
});
}),
removeLineClass: operation(null, function(handle, where, cls) {
return changeLine(this, handle, function(line) {
var prop = where == "text" ? "textClass" : where == "background" ? "bgClass" : "wrapClass";
var cur = line[prop];
if (!cur) return false;
else if (cls == null) line[prop] = null;
else {
var found = cur.match(new RegExp("(?:^|\\s+)" + cls + "(?:$|\\s+)"));
if (!found) return false;
var end = found.index + found[0].length;
line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null;
}
return true;
});
}),
addLineWidget: operation(null, function(handle, node, options) {
return addLineWidget(this, handle, node, options);
}),
removeLineWidget: function(widget) { widget.clear(); },
lineInfo: function(line) {
if (typeof line == "number") {
if (!isLine(this.doc, line)) return null;
var n = line;
line = getLine(this.doc, line);
if (!line) return null;
} else {
var n = lineNo(line);
if (n == null) return null;
}
return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers,
textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass,
widgets: line.widgets};
},
getViewport: function() { return {from: this.display.showingFrom, to: this.display.showingTo};},
addWidget: function(pos, node, scroll, vert, horiz) {
var display = this.display;
pos = cursorCoords(this, clipPos(this.doc, pos));
var top = pos.bottom, left = pos.left;
node.style.position = "absolute";
display.sizer.appendChild(node);
if (vert == "over") {
top = pos.top;
} else if (vert == "above" || vert == "near") {
var vspace = Math.max(display.wrapper.clientHeight, this.doc.height),
hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth);
// Default to positioning above (if specified and possible); otherwise default to positioning below
if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight)
top = pos.top - node.offsetHeight;
else if (pos.bottom + node.offsetHeight <= vspace)
top = pos.bottom;
if (left + node.offsetWidth > hspace)
left = hspace - node.offsetWidth;
}
node.style.top = top + "px";
node.style.left = node.style.right = "";
if (horiz == "right") {
left = display.sizer.clientWidth - node.offsetWidth;
node.style.right = "0px";
} else {
if (horiz == "left") left = 0;
else if (horiz == "middle") left = (display.sizer.clientWidth - node.offsetWidth) / 2;
node.style.left = left + "px";
}
if (scroll)
scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight);
},
triggerOnKeyDown: operation(null, onKeyDown),
execCommand: function(cmd) {return commands[cmd](this);},
findPosH: function(from, amount, unit, visually) {
var dir = 1;
if (amount < 0) { dir = -1; amount = -amount; }
for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) {
cur = findPosH(this.doc, cur, dir, unit, visually);
if (cur.hitSide) break;
}
return cur;
},
moveH: operation(null, function(dir, unit) {
var sel = this.doc.sel, pos;
if (sel.shift || sel.extend || posEq(sel.from, sel.to))
pos = findPosH(this.doc, sel.head, dir, unit, this.options.rtlMoveVisually);
else
pos = dir < 0 ? sel.from : sel.to;
extendSelection(this.doc, pos, pos, dir);
}),
deleteH: operation(null, function(dir, unit) {
var sel = this.doc.sel;
if (!posEq(sel.from, sel.to)) replaceRange(this.doc, "", sel.from, sel.to, "+delete");
else replaceRange(this.doc, "", sel.from, findPosH(this.doc, sel.head, dir, unit, false), "+delete");
this.curOp.userSelChange = true;
}),
findPosV: function(from, amount, unit, goalColumn) {
var dir = 1, x = goalColumn;
if (amount < 0) { dir = -1; amount = -amount; }
for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) {
var coords = cursorCoords(this, cur, "div");
if (x == null) x = coords.left;
else coords.left = x;
cur = findPosV(this, coords, dir, unit);
if (cur.hitSide) break;
}
return cur;
},
moveV: operation(null, function(dir, unit) {
var sel = this.doc.sel;
var pos = cursorCoords(this, sel.head, "div");
if (sel.goalColumn != null) pos.left = sel.goalColumn;
var target = findPosV(this, pos, dir, unit);
if (unit == "page") addToScrollPos(this, 0, charCoords(this, target, "div").top - pos.top);
extendSelection(this.doc, target, target, dir);
sel.goalColumn = pos.left;
}),
toggleOverwrite: function(value) {
if (value != null && value == this.state.overwrite) return;
if (this.state.overwrite = !this.state.overwrite)
this.display.cursor.className += " CodeMirror-overwrite";
else
this.display.cursor.className = this.display.cursor.className.replace(" CodeMirror-overwrite", "");
},
hasFocus: function() { return this.state.focused; },
scrollTo: operation(null, function(x, y) {
updateScrollPos(this, x, y);
}),
getScrollInfo: function() {
var scroller = this.display.scroller, co = scrollerCutOff;
return {left: scroller.scrollLeft, top: scroller.scrollTop,
height: scroller.scrollHeight - co, width: scroller.scrollWidth - co,
clientHeight: scroller.clientHeight - co, clientWidth: scroller.clientWidth - co};
},
scrollIntoView: operation(null, function(pos, margin) {
if (typeof pos == "number") pos = Pos(pos, 0);
if (!margin) margin = 0;
var coords = pos;
if (!pos || pos.line != null) {
this.curOp.scrollToPos = pos ? clipPos(this.doc, pos) : this.doc.sel.head;
this.curOp.scrollToPosMargin = margin;
coords = cursorCoords(this, this.curOp.scrollToPos);
}
var sPos = calculateScrollPos(this, coords.left, coords.top - margin, coords.right, coords.bottom + margin);
updateScrollPos(this, sPos.scrollLeft, sPos.scrollTop);
}),
setSize: operation(null, function(width, height) {
function interpret(val) {
return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val;
}
if (width != null) this.display.wrapper.style.width = interpret(width);
if (height != null) this.display.wrapper.style.height = interpret(height);
if (this.options.lineWrapping)
this.display.measureLineCache.length = this.display.measureLineCachePos = 0;
this.curOp.forceUpdate = true;
}),
operation: function(f){return runInOp(this, f);},
refresh: operation(null, function() {
clearCaches(this);
updateScrollPos(this, this.doc.scrollLeft, this.doc.scrollTop);
regChange(this);
}),
swapDoc: operation(null, function(doc) {
var old = this.doc;
old.cm = null;
attachDoc(this, doc);
clearCaches(this);
resetInput(this, true);
updateScrollPos(this, doc.scrollLeft, doc.scrollTop);
return old;
}),
getInputField: function(){return this.display.input;},
getWrapperElement: function(){return this.display.wrapper;},
getScrollerElement: function(){return this.display.scroller;},
getGutterElement: function(){return this.display.gutters;}
};
eventMixin(CodeMirror);
// OPTION DEFAULTS
var optionHandlers = CodeMirror.optionHandlers = {};
// The default configuration options.
var defaults = CodeMirror.defaults = {};
function option(name, deflt, handle, notOnInit) {
CodeMirror.defaults[name] = deflt;
if (handle) optionHandlers[name] =
notOnInit ? function(cm, val, old) {if (old != Init) handle(cm, val, old);} : handle;
}
var Init = CodeMirror.Init = {toString: function(){return "CodeMirror.Init";}};
// These two are, on init, called from the constructor because they
// have to be initialized before the editor can start at all.
option("value", "", function(cm, val) {
cm.setValue(val);
}, true);
option("mode", null, function(cm, val) {
cm.doc.modeOption = val;
loadMode(cm);
}, true);
option("indentUnit", 2, loadMode, true);
option("indentWithTabs", false);
option("smartIndent", true);
option("tabSize", 4, function(cm) {
loadMode(cm);
clearCaches(cm);
regChange(cm);
}, true);
option("electricChars", true);
option("rtlMoveVisually", !windows);
option("theme", "default", function(cm) {
themeChanged(cm);
guttersChanged(cm);
}, true);
option("keyMap", "default", keyMapChanged);
option("extraKeys", null);
option("onKeyEvent", null);
option("onDragEvent", null);
option("lineWrapping", false, wrappingChanged, true);
option("gutters", [], function(cm) {
setGuttersForLineNumbers(cm.options);
guttersChanged(cm);
}, true);
option("fixedGutter", true, function(cm, val) {
cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0";
cm.refresh();
}, true);
option("coverGutterNextToScrollbar", false, updateScrollbars, true);
option("lineNumbers", false, function(cm) {
setGuttersForLineNumbers(cm.options);
guttersChanged(cm);
}, true);
option("firstLineNumber", 1, guttersChanged, true);
option("lineNumberFormatter", function(integer) {return integer;}, guttersChanged, true);
option("showCursorWhenSelecting", false, updateSelection, true);
option("readOnly", false, function(cm, val) {
if (val == "nocursor") {onBlur(cm); cm.display.input.blur();}
else if (!val) resetInput(cm, true);
});
option("dragDrop", true);
option("cursorBlinkRate", 530);
option("cursorScrollMargin", 0);
option("cursorHeight", 1);
option("workTime", 100);
option("workDelay", 100);
option("flattenSpans", true);
option("pollInterval", 100);
option("undoDepth", 40, function(cm, val){cm.doc.history.undoDepth = val;});
option("historyEventDelay", 500);
option("viewportMargin", 10, function(cm){cm.refresh();}, true);
option("maxHighlightLength", 10000, function(cm){loadMode(cm); cm.refresh();}, true);
option("moveInputWithCursor", true, function(cm, val) {
if (!val) cm.display.inputDiv.style.top = cm.display.inputDiv.style.left = 0;
});
option("tabindex", null, function(cm, val) {
cm.display.input.tabIndex = val || "";
});
option("autofocus", null);
// MODE DEFINITION AND QUERYING
// Known modes, by name and by MIME
var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {};
CodeMirror.defineMode = function(name, mode) {
if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name;
if (arguments.length > 2) {
mode.dependencies = [];
for (var i = 2; i < arguments.length; ++i) mode.dependencies.push(arguments[i]);
}
modes[name] = mode;
};
CodeMirror.defineMIME = function(mime, spec) {
mimeModes[mime] = spec;
};
CodeMirror.resolveMode = function(spec) {
if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) {
spec = mimeModes[spec];
} else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) {
var found = mimeModes[spec.name];
spec = createObj(found, spec);
spec.name = found.name;
} else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) {
return CodeMirror.resolveMode("application/xml");
}
if (typeof spec == "string") return {name: spec};
else return spec || {name: "null"};
};
CodeMirror.getMode = function(options, spec) {
var spec = CodeMirror.resolveMode(spec);
var mfactory = modes[spec.name];
if (!mfactory) return CodeMirror.getMode(options, "text/plain");
var modeObj = mfactory(options, spec);
if (modeExtensions.hasOwnProperty(spec.name)) {
var exts = modeExtensions[spec.name];
for (var prop in exts) {
if (!exts.hasOwnProperty(prop)) continue;
if (modeObj.hasOwnProperty(prop)) modeObj["_" + prop] = modeObj[prop];
modeObj[prop] = exts[prop];
}
}
modeObj.name = spec.name;
return modeObj;
};
CodeMirror.defineMode("null", function() {
return {token: function(stream) {stream.skipToEnd();}};
});
CodeMirror.defineMIME("text/plain", "null");
var modeExtensions = CodeMirror.modeExtensions = {};
CodeMirror.extendMode = function(mode, properties) {
var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {});
copyObj(properties, exts);
};
// EXTENSIONS
CodeMirror.defineExtension = function(name, func) {
CodeMirror.prototype[name] = func;
};
CodeMirror.defineDocExtension = function(name, func) {
Doc.prototype[name] = func;
};
CodeMirror.defineOption = option;
var initHooks = [];
CodeMirror.defineInitHook = function(f) {initHooks.push(f);};
var helpers = CodeMirror.helpers = {};
CodeMirror.registerHelper = function(type, name, value) {
if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {};
helpers[type][name] = value;
};
// UTILITIES
CodeMirror.isWordChar = isWordChar;
// MODE STATE HANDLING
// Utility functions for working with state. Exported because modes
// sometimes need to do this.
function copyState(mode, state) {
if (state === true) return state;
if (mode.copyState) return mode.copyState(state);
var nstate = {};
for (var n in state) {
var val = state[n];
if (val instanceof Array) val = val.concat([]);
nstate[n] = val;
}
return nstate;
}
CodeMirror.copyState = copyState;
function startState(mode, a1, a2) {
return mode.startState ? mode.startState(a1, a2) : true;
}
CodeMirror.startState = startState;
CodeMirror.innerMode = function(mode, state) {
while (mode.innerMode) {
var info = mode.innerMode(state);
if (!info || info.mode == mode) break;
state = info.state;
mode = info.mode;
}
return info || {mode: mode, state: state};
};
// STANDARD COMMANDS
var commands = CodeMirror.commands = {
selectAll: function(cm) {cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()));},
killLine: function(cm) {
var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
if (!sel && cm.getLine(from.line).length == from.ch)
cm.replaceRange("", from, Pos(from.line + 1, 0), "+delete");
else cm.replaceRange("", from, sel ? to : Pos(from.line), "+delete");
},
deleteLine: function(cm) {
var l = cm.getCursor().line;
cm.replaceRange("", Pos(l, 0), Pos(l), "+delete");
},
delLineLeft: function(cm) {
var cur = cm.getCursor();
cm.replaceRange("", Pos(cur.line, 0), cur, "+delete");
},
undo: function(cm) {cm.undo();},
redo: function(cm) {cm.redo();},
goDocStart: function(cm) {cm.extendSelection(Pos(cm.firstLine(), 0));},
goDocEnd: function(cm) {cm.extendSelection(Pos(cm.lastLine()));},
goLineStart: function(cm) {
cm.extendSelection(lineStart(cm, cm.getCursor().line));
},
goLineStartSmart: function(cm) {
var cur = cm.getCursor(), start = lineStart(cm, cur.line);
var line = cm.getLineHandle(start.line);
var order = getOrder(line);
if (!order || order[0].level == 0) {
var firstNonWS = Math.max(0, line.text.search(/\S/));
var inWS = cur.line == start.line && cur.ch <= firstNonWS && cur.ch;
cm.extendSelection(Pos(start.line, inWS ? 0 : firstNonWS));
} else cm.extendSelection(start);
},
goLineEnd: function(cm) {
cm.extendSelection(lineEnd(cm, cm.getCursor().line));
},
goLineRight: function(cm) {
var top = cm.charCoords(cm.getCursor(), "div").top + 5;
cm.extendSelection(cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"));
},
goLineLeft: function(cm) {
var top = cm.charCoords(cm.getCursor(), "div").top + 5;
cm.extendSelection(cm.coordsChar({left: 0, top: top}, "div"));
},
goLineUp: function(cm) {cm.moveV(-1, "line");},
goLineDown: function(cm) {cm.moveV(1, "line");},
goPageUp: function(cm) {cm.moveV(-1, "page");},
goPageDown: function(cm) {cm.moveV(1, "page");},
goCharLeft: function(cm) {cm.moveH(-1, "char");},
goCharRight: function(cm) {cm.moveH(1, "char");},
goColumnLeft: function(cm) {cm.moveH(-1, "column");},
goColumnRight: function(cm) {cm.moveH(1, "column");},
goWordLeft: function(cm) {cm.moveH(-1, "word");},
goGroupRight: function(cm) {cm.moveH(1, "group");},
goGroupLeft: function(cm) {cm.moveH(-1, "group");},
goWordRight: function(cm) {cm.moveH(1, "word");},
delCharBefore: function(cm) {cm.deleteH(-1, "char");},
delCharAfter: function(cm) {cm.deleteH(1, "char");},
delWordBefore: function(cm) {cm.deleteH(-1, "word");},
delWordAfter: function(cm) {cm.deleteH(1, "word");},
delGroupBefore: function(cm) {cm.deleteH(-1, "group");},
delGroupAfter: function(cm) {cm.deleteH(1, "group");},
indentAuto: function(cm) {cm.indentSelection("smart");},
indentMore: function(cm) {cm.indentSelection("add");},
indentLess: function(cm) {cm.indentSelection("subtract");},
insertTab: function(cm) {cm.replaceSelection("\t", "end", "+input");},
defaultTab: function(cm) {
if (cm.somethingSelected()) cm.indentSelection("add");
else cm.replaceSelection("\t", "end", "+input");
},
transposeChars: function(cm) {
var cur = cm.getCursor(), line = cm.getLine(cur.line);
if (cur.ch > 0 && cur.ch < line.length - 1)
cm.replaceRange(line.charAt(cur.ch) + line.charAt(cur.ch - 1),
Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1));
},
newlineAndIndent: function(cm) {
operation(cm, function() {
cm.replaceSelection("\n", "end", "+input");
cm.indentLine(cm.getCursor().line, null, true);
})();
},
toggleOverwrite: function(cm) {cm.toggleOverwrite();}
};
// STANDARD KEYMAPS
var keyMap = CodeMirror.keyMap = {};
keyMap.basic = {
"Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown",
"End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown",
"Delete": "delCharAfter", "Backspace": "delCharBefore", "Tab": "defaultTab", "Shift-Tab": "indentAuto",
"Enter": "newlineAndIndent", "Insert": "toggleOverwrite"
};
// Note that the save and find-related commands aren't defined by
// default. Unknown commands are simply ignored.
keyMap.pcDefault = {
"Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo",
"Ctrl-Home": "goDocStart", "Alt-Up": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Down": "goDocEnd",
"Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd",
"Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find",
"Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll",
"Ctrl-[": "indentLess", "Ctrl-]": "indentMore",
fallthrough: "basic"
};
keyMap.macDefault = {
"Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo",
"Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft",
"Alt-Right": "goGroupRight", "Cmd-Left": "goLineStart", "Cmd-Right": "goLineEnd", "Alt-Backspace": "delGroupBefore",
"Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find",
"Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll",
"Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delLineLeft",
fallthrough: ["basic", "emacsy"]
};
keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault;
keyMap.emacsy = {
"Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown",
"Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd",
"Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore",
"Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars"
};
// KEYMAP DISPATCH
function getKeyMap(val) {
if (typeof val == "string") return keyMap[val];
else return val;
}
function lookupKey(name, maps, handle) {
function lookup(map) {
map = getKeyMap(map);
var found = map[name];
if (found === false) return "stop";
if (found != null && handle(found)) return true;
if (map.nofallthrough) return "stop";
var fallthrough = map.fallthrough;
if (fallthrough == null) return false;
if (Object.prototype.toString.call(fallthrough) != "[object Array]")
return lookup(fallthrough);
for (var i = 0, e = fallthrough.length; i < e; ++i) {
var done = lookup(fallthrough[i]);
if (done) return done;
}
return false;
}
for (var i = 0; i < maps.length; ++i) {
var done = lookup(maps[i]);
if (done) return done != "stop";
}
}
function isModifierKey(event) {
var name = keyNames[event.keyCode];
return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod";
}
function keyName(event, noShift) {
if (opera && event.keyCode == 34 && event["char"]) return false;
var name = keyNames[event.keyCode];
if (name == null || event.altGraphKey) return false;
if (event.altKey) name = "Alt-" + name;
if (flipCtrlCmd ? event.metaKey : event.ctrlKey) name = "Ctrl-" + name;
if (flipCtrlCmd ? event.ctrlKey : event.metaKey) name = "Cmd-" + name;
if (!noShift && event.shiftKey) name = "Shift-" + name;
return name;
}
CodeMirror.lookupKey = lookupKey;
CodeMirror.isModifierKey = isModifierKey;
CodeMirror.keyName = keyName;
// FROMTEXTAREA
CodeMirror.fromTextArea = function(textarea, options) {
if (!options) options = {};
options.value = textarea.value;
if (!options.tabindex && textarea.tabindex)
options.tabindex = textarea.tabindex;
if (!options.placeholder && textarea.placeholder)
options.placeholder = textarea.placeholder;
// Set autofocus to true if this textarea is focused, or if it has
// autofocus and no other element is focused.
if (options.autofocus == null) {
var hasFocus = document.body;
// doc.activeElement occasionally throws on IE
try { hasFocus = document.activeElement; } catch(e) {}
options.autofocus = hasFocus == textarea ||
textarea.getAttribute("autofocus") != null && hasFocus == document.body;
}
function save() {textarea.value = cm.getValue();}
if (textarea.form) {
on(textarea.form, "submit", save);
// Deplorable hack to make the submit method do the right thing.
if (!options.leaveSubmitMethodAlone) {
var form = textarea.form, realSubmit = form.submit;
try {
var wrappedSubmit = form.submit = function() {
save();
form.submit = realSubmit;
form.submit();
form.submit = wrappedSubmit;
};
} catch(e) {}
}
}
textarea.style.display = "none";
var cm = CodeMirror(function(node) {
textarea.parentNode.insertBefore(node, textarea.nextSibling);
}, options);
cm.save = save;
cm.getTextArea = function() { return textarea; };
cm.toTextArea = function() {
save();
textarea.parentNode.removeChild(cm.getWrapperElement());
textarea.style.display = "";
if (textarea.form) {
off(textarea.form, "submit", save);
if (typeof textarea.form.submit == "function")
textarea.form.submit = realSubmit;
}
};
return cm;
};
// STRING STREAM
// Fed to the mode parsers, provides helper functions to make
// parsers more succinct.
// The character stream used by a mode's parser.
function StringStream(string, tabSize) {
this.pos = this.start = 0;
this.string = string;
this.tabSize = tabSize || 8;
this.lastColumnPos = this.lastColumnValue = 0;
}
StringStream.prototype = {
eol: function() {return this.pos >= this.string.length;},
sol: function() {return this.pos == 0;},
peek: function() {return this.string.charAt(this.pos) || undefined;},
next: function() {
if (this.pos < this.string.length)
return this.string.charAt(this.pos++);
},
eat: function(match) {
var ch = this.string.charAt(this.pos);
if (typeof match == "string") var ok = ch == match;
else var ok = ch && (match.test ? match.test(ch) : match(ch));
if (ok) {++this.pos; return ch;}
},
eatWhile: function(match) {
var start = this.pos;
while (this.eat(match)){}
return this.pos > start;
},
eatSpace: function() {
var start = this.pos;
while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos;
return this.pos > start;
},
skipToEnd: function() {this.pos = this.string.length;},
skipTo: function(ch) {
var found = this.string.indexOf(ch, this.pos);
if (found > -1) {this.pos = found; return true;}
},
backUp: function(n) {this.pos -= n;},
column: function() {
if (this.lastColumnPos < this.start) {
this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue);
this.lastColumnPos = this.start;
}
return this.lastColumnValue;
},
indentation: function() {return countColumn(this.string, null, this.tabSize);},
match: function(pattern, consume, caseInsensitive) {
if (typeof pattern == "string") {
var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;};
var substr = this.string.substr(this.pos, pattern.length);
if (cased(substr) == cased(pattern)) {
if (consume !== false) this.pos += pattern.length;
return true;
}
} else {
var match = this.string.slice(this.pos).match(pattern);
if (match && match.index > 0) return null;
if (match && consume !== false) this.pos += match[0].length;
return match;
}
},
current: function(){return this.string.slice(this.start, this.pos);}
};
CodeMirror.StringStream = StringStream;
// TEXTMARKERS
function TextMarker(doc, type) {
this.lines = [];
this.type = type;
this.doc = doc;
}
CodeMirror.TextMarker = TextMarker;
eventMixin(TextMarker);
TextMarker.prototype.clear = function() {
if (this.explicitlyCleared) return;
var cm = this.doc.cm, withOp = cm && !cm.curOp;
if (withOp) startOperation(cm);
if (hasHandler(this, "clear")) {
var found = this.find();
if (found) signalLater(this, "clear", found.from, found.to);
}
var min = null, max = null;
for (var i = 0; i < this.lines.length; ++i) {
var line = this.lines[i];
var span = getMarkedSpanFor(line.markedSpans, this);
if (span.to != null) max = lineNo(line);
line.markedSpans = removeMarkedSpan(line.markedSpans, span);
if (span.from != null)
min = lineNo(line);
else if (this.collapsed && !lineIsHidden(this.doc, line) && cm)
updateLineHeight(line, textHeight(cm.display));
}
if (cm && this.collapsed && !cm.options.lineWrapping) for (var i = 0; i < this.lines.length; ++i) {
var visual = visualLine(cm.doc, this.lines[i]), len = lineLength(cm.doc, visual);
if (len > cm.display.maxLineLength) {
cm.display.maxLine = visual;
cm.display.maxLineLength = len;
cm.display.maxLineChanged = true;
}
}
if (min != null && cm) regChange(cm, min, max + 1);
this.lines.length = 0;
this.explicitlyCleared = true;
if (this.atomic && this.doc.cantEdit) {
this.doc.cantEdit = false;
if (cm) reCheckSelection(cm);
}
if (withOp) endOperation(cm);
};
TextMarker.prototype.find = function() {
var from, to;
for (var i = 0; i < this.lines.length; ++i) {
var line = this.lines[i];
var span = getMarkedSpanFor(line.markedSpans, this);
if (span.from != null || span.to != null) {
var found = lineNo(line);
if (span.from != null) from = Pos(found, span.from);
if (span.to != null) to = Pos(found, span.to);
}
}
if (this.type == "bookmark") return from;
return from && {from: from, to: to};
};
TextMarker.prototype.changed = function() {
var pos = this.find(), cm = this.doc.cm;
if (!pos || !cm) return;
var line = getLine(this.doc, pos.from.line);
clearCachedMeasurement(cm, line);
if (pos.from.line >= cm.display.showingFrom && pos.from.line < cm.display.showingTo) {
for (var node = cm.display.lineDiv.firstChild; node; node = node.nextSibling) if (node.lineObj == line) {
if (node.offsetHeight != line.height) updateLineHeight(line, node.offsetHeight);
break;
}
runInOp(cm, function() {
cm.curOp.selectionChanged = cm.curOp.forceUpdate = cm.curOp.updateMaxLine = true;
});
}
};
TextMarker.prototype.attachLine = function(line) {
if (!this.lines.length && this.doc.cm) {
var op = this.doc.cm.curOp;
if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1)
(op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this);
}
this.lines.push(line);
};
TextMarker.prototype.detachLine = function(line) {
this.lines.splice(indexOf(this.lines, line), 1);
if (!this.lines.length && this.doc.cm) {
var op = this.doc.cm.curOp;
(op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this);
}
};
function markText(doc, from, to, options, type) {
if (options && options.shared) return markTextShared(doc, from, to, options, type);
if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type);
var marker = new TextMarker(doc, type);
if (type == "range" && !posLess(from, to)) return marker;
if (options) copyObj(options, marker);
if (marker.replacedWith) {
marker.collapsed = true;
marker.replacedWith = elt("span", [marker.replacedWith], "CodeMirror-widget");
if (!options.handleMouseEvents) marker.replacedWith.ignoreEvents = true;
}
if (marker.collapsed) sawCollapsedSpans = true;
if (marker.addToHistory)
addToHistory(doc, {from: from, to: to, origin: "markText"},
{head: doc.sel.head, anchor: doc.sel.anchor}, NaN);
var curLine = from.line, size = 0, collapsedAtStart, collapsedAtEnd, cm = doc.cm, updateMaxLine;
doc.iter(curLine, to.line + 1, function(line) {
if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(doc, line) == cm.display.maxLine)
updateMaxLine = true;
var span = {from: null, to: null, marker: marker};
size += line.text.length;
if (curLine == from.line) {span.from = from.ch; size -= from.ch;}
if (curLine == to.line) {span.to = to.ch; size -= line.text.length - to.ch;}
if (marker.collapsed) {
if (curLine == to.line) collapsedAtEnd = collapsedSpanAt(line, to.ch);
if (curLine == from.line) collapsedAtStart = collapsedSpanAt(line, from.ch);
else updateLineHeight(line, 0);
}
addMarkedSpan(line, span);
++curLine;
});
if (marker.collapsed) doc.iter(from.line, to.line + 1, function(line) {
if (lineIsHidden(doc, line)) updateLineHeight(line, 0);
});
if (marker.clearOnEnter) on(marker, "beforeCursorEnter", function() { marker.clear(); });
if (marker.readOnly) {
sawReadOnlySpans = true;
if (doc.history.done.length || doc.history.undone.length)
doc.clearHistory();
}
if (marker.collapsed) {
if (collapsedAtStart != collapsedAtEnd)
throw new Error("Inserting collapsed marker overlapping an existing one");
marker.size = size;
marker.atomic = true;
}
if (cm) {
if (updateMaxLine) cm.curOp.updateMaxLine = true;
if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.collapsed)
regChange(cm, from.line, to.line + 1);
if (marker.atomic) reCheckSelection(cm);
}
return marker;
}
// SHARED TEXTMARKERS
function SharedTextMarker(markers, primary) {
this.markers = markers;
this.primary = primary;
for (var i = 0, me = this; i < markers.length; ++i) {
markers[i].parent = this;
on(markers[i], "clear", function(){me.clear();});
}
}
CodeMirror.SharedTextMarker = SharedTextMarker;
eventMixin(SharedTextMarker);
SharedTextMarker.prototype.clear = function() {
if (this.explicitlyCleared) return;
this.explicitlyCleared = true;
for (var i = 0; i < this.markers.length; ++i)
this.markers[i].clear();
signalLater(this, "clear");
};
SharedTextMarker.prototype.find = function() {
return this.primary.find();
};
function markTextShared(doc, from, to, options, type) {
options = copyObj(options);
options.shared = false;
var markers = [markText(doc, from, to, options, type)], primary = markers[0];
var widget = options.replacedWith;
linkedDocs(doc, function(doc) {
if (widget) options.replacedWith = widget.cloneNode(true);
markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type));
for (var i = 0; i < doc.linked.length; ++i)
if (doc.linked[i].isParent) return;
primary = lst(markers);
});
return new SharedTextMarker(markers, primary);
}
// TEXTMARKER SPANS
function getMarkedSpanFor(spans, marker) {
if (spans) for (var i = 0; i < spans.length; ++i) {
var span = spans[i];
if (span.marker == marker) return span;
}
}
function removeMarkedSpan(spans, span) {
for (var r, i = 0; i < spans.length; ++i)
if (spans[i] != span) (r || (r = [])).push(spans[i]);
return r;
}
function addMarkedSpan(line, span) {
line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span];
span.marker.attachLine(line);
}
function markedSpansBefore(old, startCh, isInsert) {
if (old) for (var i = 0, nw; i < old.length; ++i) {
var span = old[i], marker = span.marker;
var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh);
if (startsBefore || marker.type == "bookmark" && span.from == startCh && (!isInsert || !span.marker.insertLeft)) {
var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh);
(nw || (nw = [])).push({from: span.from,
to: endsAfter ? null : span.to,
marker: marker});
}
}
return nw;
}
function markedSpansAfter(old, endCh, isInsert) {
if (old) for (var i = 0, nw; i < old.length; ++i) {
var span = old[i], marker = span.marker;
var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh);
if (endsAfter || marker.type == "bookmark" && span.from == endCh && (!isInsert || span.marker.insertLeft)) {
var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh);
(nw || (nw = [])).push({from: startsBefore ? null : span.from - endCh,
to: span.to == null ? null : span.to - endCh,
marker: marker});
}
}
return nw;
}
function stretchSpansOverChange(doc, change) {
var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans;
var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans;
if (!oldFirst && !oldLast) return null;
var startCh = change.from.ch, endCh = change.to.ch, isInsert = posEq(change.from, change.to);
// Get the spans that 'stick out' on both sides
var first = markedSpansBefore(oldFirst, startCh, isInsert);
var last = markedSpansAfter(oldLast, endCh, isInsert);
// Next, merge those two ends
var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0);
if (first) {
// Fix up .to properties of first
for (var i = 0; i < first.length; ++i) {
var span = first[i];
if (span.to == null) {
var found = getMarkedSpanFor(last, span.marker);
if (!found) span.to = startCh;
else if (sameLine) span.to = found.to == null ? null : found.to + offset;
}
}
}
if (last) {
// Fix up .from in last (or move them into first in case of sameLine)
for (var i = 0; i < last.length; ++i) {
var span = last[i];
if (span.to != null) span.to += offset;
if (span.from == null) {
var found = getMarkedSpanFor(first, span.marker);
if (!found) {
span.from = offset;
if (sameLine) (first || (first = [])).push(span);
}
} else {
span.from += offset;
if (sameLine) (first || (first = [])).push(span);
}
}
}
if (sameLine && first) {
// Make sure we didn't create any zero-length spans
for (var i = 0; i < first.length; ++i)
if (first[i].from != null && first[i].from == first[i].to && first[i].marker.type != "bookmark")
first.splice(i--, 1);
if (!first.length) first = null;
}
var newMarkers = [first];
if (!sameLine) {
// Fill gap with whole-line-spans
var gap = change.text.length - 2, gapMarkers;
if (gap > 0 && first)
for (var i = 0; i < first.length; ++i)
if (first[i].to == null)
(gapMarkers || (gapMarkers = [])).push({from: null, to: null, marker: first[i].marker});
for (var i = 0; i < gap; ++i)
newMarkers.push(gapMarkers);
newMarkers.push(last);
}
return newMarkers;
}
function mergeOldSpans(doc, change) {
var old = getOldSpans(doc, change);
var stretched = stretchSpansOverChange(doc, change);
if (!old) return stretched;
if (!stretched) return old;
for (var i = 0; i < old.length; ++i) {
var oldCur = old[i], stretchCur = stretched[i];
if (oldCur && stretchCur) {
spans: for (var j = 0; j < stretchCur.length; ++j) {
var span = stretchCur[j];
for (var k = 0; k < oldCur.length; ++k)
if (oldCur[k].marker == span.marker) continue spans;
oldCur.push(span);
}
} else if (stretchCur) {
old[i] = stretchCur;
}
}
return old;
}
function removeReadOnlyRanges(doc, from, to) {
var markers = null;
doc.iter(from.line, to.line + 1, function(line) {
if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) {
var mark = line.markedSpans[i].marker;
if (mark.readOnly && (!markers || indexOf(markers, mark) == -1))
(markers || (markers = [])).push(mark);
}
});
if (!markers) return null;
var parts = [{from: from, to: to}];
for (var i = 0; i < markers.length; ++i) {
var mk = markers[i], m = mk.find();
for (var j = 0; j < parts.length; ++j) {
var p = parts[j];
if (posLess(p.to, m.from) || posLess(m.to, p.from)) continue;
var newParts = [j, 1];
if (posLess(p.from, m.from) || !mk.inclusiveLeft && posEq(p.from, m.from))
newParts.push({from: p.from, to: m.from});
if (posLess(m.to, p.to) || !mk.inclusiveRight && posEq(p.to, m.to))
newParts.push({from: m.to, to: p.to});
parts.splice.apply(parts, newParts);
j += newParts.length - 1;
}
}
return parts;
}
function collapsedSpanAt(line, ch) {
var sps = sawCollapsedSpans && line.markedSpans, found;
if (sps) for (var sp, i = 0; i < sps.length; ++i) {
sp = sps[i];
if (!sp.marker.collapsed) continue;
if ((sp.from == null || sp.from < ch) &&
(sp.to == null || sp.to > ch) &&
(!found || found.width < sp.marker.width))
found = sp.marker;
}
return found;
}
function collapsedSpanAtStart(line) { return collapsedSpanAt(line, -1); }
function collapsedSpanAtEnd(line) { return collapsedSpanAt(line, line.text.length + 1); }
function visualLine(doc, line) {
var merged;
while (merged = collapsedSpanAtStart(line))
line = getLine(doc, merged.find().from.line);
return line;
}
function lineIsHidden(doc, line) {
var sps = sawCollapsedSpans && line.markedSpans;
if (sps) for (var sp, i = 0; i < sps.length; ++i) {
sp = sps[i];
if (!sp.marker.collapsed) continue;
if (sp.from == null) return true;
if (sp.marker.replacedWith) continue;
if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp))
return true;
}
}
function lineIsHiddenInner(doc, line, span) {
if (span.to == null) {
var end = span.marker.find().to, endLine = getLine(doc, end.line);
return lineIsHiddenInner(doc, endLine, getMarkedSpanFor(endLine.markedSpans, span.marker));
}
if (span.marker.inclusiveRight && span.to == line.text.length)
return true;
for (var sp, i = 0; i < line.markedSpans.length; ++i) {
sp = line.markedSpans[i];
if (sp.marker.collapsed && !sp.marker.replacedWith && sp.from == span.to &&
(sp.marker.inclusiveLeft || span.marker.inclusiveRight) &&
lineIsHiddenInner(doc, line, sp)) return true;
}
}
function detachMarkedSpans(line) {
var spans = line.markedSpans;
if (!spans) return;
for (var i = 0; i < spans.length; ++i)
spans[i].marker.detachLine(line);
line.markedSpans = null;
}
function attachMarkedSpans(line, spans) {
if (!spans) return;
for (var i = 0; i < spans.length; ++i)
spans[i].marker.attachLine(line);
line.markedSpans = spans;
}
// LINE WIDGETS
var LineWidget = CodeMirror.LineWidget = function(cm, node, options) {
if (options) for (var opt in options) if (options.hasOwnProperty(opt))
this[opt] = options[opt];
this.cm = cm;
this.node = node;
};
eventMixin(LineWidget);
function widgetOperation(f) {
return function() {
var withOp = !this.cm.curOp;
if (withOp) startOperation(this.cm);
try {var result = f.apply(this, arguments);}
finally {if (withOp) endOperation(this.cm);}
return result;
};
}
LineWidget.prototype.clear = widgetOperation(function() {
var ws = this.line.widgets, no = lineNo(this.line);
if (no == null || !ws) return;
for (var i = 0; i < ws.length; ++i) if (ws[i] == this) ws.splice(i--, 1);
if (!ws.length) this.line.widgets = null;
var aboveVisible = heightAtLine(this.cm, this.line) < this.cm.doc.scrollTop;
updateLineHeight(this.line, Math.max(0, this.line.height - widgetHeight(this)));
if (aboveVisible) addToScrollPos(this.cm, 0, -this.height);
regChange(this.cm, no, no + 1);
});
LineWidget.prototype.changed = widgetOperation(function() {
var oldH = this.height;
this.height = null;
var diff = widgetHeight(this) - oldH;
if (!diff) return;
updateLineHeight(this.line, this.line.height + diff);
var no = lineNo(this.line);
regChange(this.cm, no, no + 1);
});
function widgetHeight(widget) {
if (widget.height != null) return widget.height;
if (!widget.node.parentNode || widget.node.parentNode.nodeType != 1)
removeChildrenAndAdd(widget.cm.display.measure, elt("div", [widget.node], null, "position: relative"));
return widget.height = widget.node.offsetHeight;
}
function addLineWidget(cm, handle, node, options) {
var widget = new LineWidget(cm, node, options);
if (widget.noHScroll) cm.display.alignWidgets = true;
changeLine(cm, handle, function(line) {
var widgets = line.widgets || (line.widgets = []);
if (widget.insertAt == null) widgets.push(widget);
else widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget);
widget.line = line;
if (!lineIsHidden(cm.doc, line) || widget.showIfHidden) {
var aboveVisible = heightAtLine(cm, line) < cm.doc.scrollTop;
updateLineHeight(line, line.height + widgetHeight(widget));
if (aboveVisible) addToScrollPos(cm, 0, widget.height);
}
return true;
});
return widget;
}
// LINE DATA STRUCTURE
// Line objects. These hold state related to a line, including
// highlighting info (the styles array).
var Line = CodeMirror.Line = function(text, markedSpans, estimateHeight) {
this.text = text;
attachMarkedSpans(this, markedSpans);
this.height = estimateHeight ? estimateHeight(this) : 1;
};
eventMixin(Line);
function updateLine(line, text, markedSpans, estimateHeight) {
line.text = text;
if (line.stateAfter) line.stateAfter = null;
if (line.styles) line.styles = null;
if (line.order != null) line.order = null;
detachMarkedSpans(line);
attachMarkedSpans(line, markedSpans);
var estHeight = estimateHeight ? estimateHeight(line) : 1;
if (estHeight != line.height) updateLineHeight(line, estHeight);
}
function cleanUpLine(line) {
line.parent = null;
detachMarkedSpans(line);
}
// Run the given mode's parser over a line, update the styles
// array, which contains alternating fragments of text and CSS
// classes.
function runMode(cm, text, mode, state, f) {
var flattenSpans = mode.flattenSpans;
if (flattenSpans == null) flattenSpans = cm.options.flattenSpans;
var curStart = 0, curStyle = null;
var stream = new StringStream(text, cm.options.tabSize), style;
if (text == "" && mode.blankLine) mode.blankLine(state);
while (!stream.eol()) {
if (stream.pos > cm.options.maxHighlightLength) {
flattenSpans = false;
// Webkit seems to refuse to render text nodes longer than 57444 characters
stream.pos = Math.min(text.length, stream.start + 50000);
style = null;
} else {
style = mode.token(stream, state);
}
if (!flattenSpans || curStyle != style) {
if (curStart < stream.start) f(stream.start, curStyle);
curStart = stream.start; curStyle = style;
}
stream.start = stream.pos;
}
if (curStart < stream.pos) f(stream.pos, curStyle);
}
function highlightLine(cm, line, state) {
// A styles array always starts with a number identifying the
// mode/overlays that it is based on (for easy invalidation).
var st = [cm.state.modeGen];
// Compute the base array of styles
runMode(cm, line.text, cm.doc.mode, state, function(end, style) {st.push(end, style);});
// Run overlays, adjust style array.
for (var o = 0; o < cm.state.overlays.length; ++o) {
var overlay = cm.state.overlays[o], i = 1, at = 0;
runMode(cm, line.text, overlay.mode, true, function(end, style) {
var start = i;
// Ensure there's a token end at the current position, and that i points at it
while (at < end) {
var i_end = st[i];
if (i_end > end)
st.splice(i, 1, end, st[i+1], i_end);
i += 2;
at = Math.min(end, i_end);
}
if (!style) return;
if (overlay.opaque) {
st.splice(start, i - start, end, style);
i = start + 2;
} else {
for (; start < i; start += 2) {
var cur = st[start+1];
st[start+1] = cur ? cur + " " + style : style;
}
}
});
}
return st;
}
function getLineStyles(cm, line) {
if (!line.styles || line.styles[0] != cm.state.modeGen)
line.styles = highlightLine(cm, line, line.stateAfter = getStateBefore(cm, lineNo(line)));
return line.styles;
}
// Lightweight form of highlight -- proceed over this line and
// update state, but don't save a style array.
function processLine(cm, line, state) {
var mode = cm.doc.mode;
var stream = new StringStream(line.text, cm.options.tabSize);
if (line.text == "" && mode.blankLine) mode.blankLine(state);
while (!stream.eol() && stream.pos <= cm.options.maxHighlightLength) {
mode.token(stream, state);
stream.start = stream.pos;
}
}
var styleToClassCache = {};
function styleToClass(style) {
if (!style) return null;
return styleToClassCache[style] ||
(styleToClassCache[style] = "cm-" + style.replace(/ +/g, " cm-"));
}
function lineContent(cm, realLine, measure, copyWidgets) {
var merged, line = realLine, empty = true;
while (merged = collapsedSpanAtStart(line))
line = getLine(cm.doc, merged.find().from.line);
var builder = {pre: elt("pre"), col: 0, pos: 0,
measure: null, measuredSomething: false, cm: cm,
copyWidgets: copyWidgets};
if (line.textClass) builder.pre.className = line.textClass;
do {
if (line.text) empty = false;
builder.measure = line == realLine && measure;
builder.pos = 0;
builder.addToken = builder.measure ? buildTokenMeasure : buildToken;
if ((ie || webkit) && cm.getOption("lineWrapping"))
builder.addToken = buildTokenSplitSpaces(builder.addToken);
var next = insertLineContent(line, builder, getLineStyles(cm, line));
if (measure && line == realLine && !builder.measuredSomething) {
measure[0] = builder.pre.appendChild(zeroWidthElement(cm.display.measure));
builder.measuredSomething = true;
}
if (next) line = getLine(cm.doc, next.to.line);
} while (next);
if (measure && !builder.measuredSomething && !measure[0])
measure[0] = builder.pre.appendChild(empty ? elt("span", "\u00a0") : zeroWidthElement(cm.display.measure));
if (!builder.pre.firstChild && !lineIsHidden(cm.doc, realLine))
builder.pre.appendChild(document.createTextNode("\u00a0"));
var order;
// Work around problem with the reported dimensions of single-char
// direction spans on IE (issue #1129). See also the comment in
// cursorCoords.
if (measure && ie && (order = getOrder(line))) {
var l = order.length - 1;
if (order[l].from == order[l].to) --l;
var last = order[l], prev = order[l - 1];
if (last.from + 1 == last.to && prev && last.level < prev.level) {
var span = measure[builder.pos - 1];
if (span) span.parentNode.insertBefore(span.measureRight = zeroWidthElement(cm.display.measure),
span.nextSibling);
}
}
signal(cm, "renderLine", cm, realLine, builder.pre);
return builder.pre;
}
var tokenSpecialChars = /[\t\u0000-\u0019\u00ad\u200b\u2028\u2029\uFEFF]/g;
function buildToken(builder, text, style, startStyle, endStyle, title) {
if (!text) return;
if (!tokenSpecialChars.test(text)) {
builder.col += text.length;
var content = document.createTextNode(text);
} else {
var content = document.createDocumentFragment(), pos = 0;
while (true) {
tokenSpecialChars.lastIndex = pos;
var m = tokenSpecialChars.exec(text);
var skipped = m ? m.index - pos : text.length - pos;
if (skipped) {
content.appendChild(document.createTextNode(text.slice(pos, pos + skipped)));
builder.col += skipped;
}
if (!m) break;
pos += skipped + 1;
if (m[0] == "\t") {
var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize;
content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab"));
builder.col += tabWidth;
} else {
var token = elt("span", "\u2022", "cm-invalidchar");
token.title = "\\u" + m[0].charCodeAt(0).toString(16);
content.appendChild(token);
builder.col += 1;
}
}
}
if (style || startStyle || endStyle || builder.measure) {
var fullStyle = style || "";
if (startStyle) fullStyle += startStyle;
if (endStyle) fullStyle += endStyle;
var token = elt("span", [content], fullStyle);
if (title) token.title = title;
return builder.pre.appendChild(token);
}
builder.pre.appendChild(content);
}
function buildTokenMeasure(builder, text, style, startStyle, endStyle) {
var wrapping = builder.cm.options.lineWrapping;
for (var i = 0; i < text.length; ++i) {
var ch = text.charAt(i), start = i == 0;
if (ch >= "\ud800" && ch < "\udbff" && i < text.length - 1) {
ch = text.slice(i, i + 2);
++i;
} else if (i && wrapping && spanAffectsWrapping(text, i)) {
builder.pre.appendChild(elt("wbr"));
}
var old = builder.measure[builder.pos];
var span = builder.measure[builder.pos] =
buildToken(builder, ch, style,
start && startStyle, i == text.length - 1 && endStyle);
if (old) span.leftSide = old.leftSide || old;
// In IE single-space nodes wrap differently than spaces
// embedded in larger text nodes, except when set to
// white-space: normal (issue #1268).
if (ie && wrapping && ch == " " && i && !/\s/.test(text.charAt(i - 1)) &&
i < text.length - 1 && !/\s/.test(text.charAt(i + 1)))
span.style.whiteSpace = "normal";
builder.pos += ch.length;
}
if (text.length) builder.measuredSomething = true;
}
function buildTokenSplitSpaces(inner) {
function split(old) {
var out = " ";
for (var i = 0; i < old.length - 2; ++i) out += i % 2 ? " " : "\u00a0";
out += " ";
return out;
}
return function(builder, text, style, startStyle, endStyle, title) {
return inner(builder, text.replace(/ {3,}/, split), style, startStyle, endStyle, title);
};
}
function buildCollapsedSpan(builder, size, marker, ignoreWidget) {
var widget = !ignoreWidget && marker.replacedWith;
if (widget) {
if (builder.copyWidgets) widget = widget.cloneNode(true);
builder.pre.appendChild(widget);
if (builder.measure) {
if (size) {
builder.measure[builder.pos] = widget;
} else {
var elt = builder.measure[builder.pos] = zeroWidthElement(builder.cm.display.measure);
if (marker.type != "bookmark" || marker.insertLeft)
builder.pre.insertBefore(elt, widget);
else
builder.pre.appendChild(elt);
}
builder.measuredSomething = true;
}
}
builder.pos += size;
}
// Outputs a number of spans to make up a line, taking highlighting
// and marked text into account.
function insertLineContent(line, builder, styles) {
var spans = line.markedSpans, allText = line.text, at = 0;
if (!spans) {
for (var i = 1; i < styles.length; i+=2)
builder.addToken(builder, allText.slice(at, at = styles[i]), styleToClass(styles[i+1]));
return;
}
var len = allText.length, pos = 0, i = 1, text = "", style;
var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed;
for (;;) {
if (nextChange == pos) { // Update current marker set
spanStyle = spanEndStyle = spanStartStyle = title = "";
collapsed = null; nextChange = Infinity;
var foundBookmark = null;
for (var j = 0; j < spans.length; ++j) {
var sp = spans[j], m = sp.marker;
if (sp.from <= pos && (sp.to == null || sp.to > pos)) {
if (sp.to != null && nextChange > sp.to) { nextChange = sp.to; spanEndStyle = ""; }
if (m.className) spanStyle += " " + m.className;
if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle;
if (m.endStyle && sp.to == nextChange) spanEndStyle += " " + m.endStyle;
if (m.title && !title) title = m.title;
if (m.collapsed && (!collapsed || collapsed.marker.size < m.size))
collapsed = sp;
} else if (sp.from > pos && nextChange > sp.from) {
nextChange = sp.from;
}
if (m.type == "bookmark" && sp.from == pos && m.replacedWith) foundBookmark = m;
}
if (collapsed && (collapsed.from || 0) == pos) {
buildCollapsedSpan(builder, (collapsed.to == null ? len : collapsed.to) - pos,
collapsed.marker, collapsed.from == null);
if (collapsed.to == null) return collapsed.marker.find();
}
if (foundBookmark && !collapsed) buildCollapsedSpan(builder, 0, foundBookmark);
}
if (pos >= len) break;
var upto = Math.min(len, nextChange);
while (true) {
if (text) {
var end = pos + text.length;
if (!collapsed) {
var tokenText = end > upto ? text.slice(0, upto - pos) : text;
builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle,
spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", title);
}
if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;}
pos = end;
spanStartStyle = "";
}
text = allText.slice(at, at = styles[i++]);
style = styleToClass(styles[i++]);
}
}
}
// DOCUMENT DATA STRUCTURE
function updateDoc(doc, change, markedSpans, selAfter, estimateHeight) {
function spansFor(n) {return markedSpans ? markedSpans[n] : null;}
function update(line, text, spans) {
updateLine(line, text, spans, estimateHeight);
signalLater(line, "change", line, change);
}
var from = change.from, to = change.to, text = change.text;
var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line);
var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line;
// First adjust the line structure
if (from.ch == 0 && to.ch == 0 && lastText == "") {
// This is a whole-line replace. Treated specially to make
// sure line objects move the way they are supposed to.
for (var i = 0, e = text.length - 1, added = []; i < e; ++i)
added.push(new Line(text[i], spansFor(i), estimateHeight));
update(lastLine, lastLine.text, lastSpans);
if (nlines) doc.remove(from.line, nlines);
if (added.length) doc.insert(from.line, added);
} else if (firstLine == lastLine) {
if (text.length == 1) {
update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans);
} else {
for (var added = [], i = 1, e = text.length - 1; i < e; ++i)
added.push(new Line(text[i], spansFor(i), estimateHeight));
added.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight));
update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0));
doc.insert(from.line + 1, added);
}
} else if (text.length == 1) {
update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0));
doc.remove(from.line + 1, nlines);
} else {
update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0));
update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans);
for (var i = 1, e = text.length - 1, added = []; i < e; ++i)
added.push(new Line(text[i], spansFor(i), estimateHeight));
if (nlines > 1) doc.remove(from.line + 1, nlines - 1);
doc.insert(from.line + 1, added);
}
signalLater(doc, "change", doc, change);
setSelection(doc, selAfter.anchor, selAfter.head, null, true);
}
function LeafChunk(lines) {
this.lines = lines;
this.parent = null;
for (var i = 0, e = lines.length, height = 0; i < e; ++i) {
lines[i].parent = this;
height += lines[i].height;
}
this.height = height;
}
LeafChunk.prototype = {
chunkSize: function() { return this.lines.length; },
removeInner: function(at, n) {
for (var i = at, e = at + n; i < e; ++i) {
var line = this.lines[i];
this.height -= line.height;
cleanUpLine(line);
signalLater(line, "delete");
}
this.lines.splice(at, n);
},
collapse: function(lines) {
lines.splice.apply(lines, [lines.length, 0].concat(this.lines));
},
insertInner: function(at, lines, height) {
this.height += height;
this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at));
for (var i = 0, e = lines.length; i < e; ++i) lines[i].parent = this;
},
iterN: function(at, n, op) {
for (var e = at + n; at < e; ++at)
if (op(this.lines[at])) return true;
}
};
function BranchChunk(children) {
this.children = children;
var size = 0, height = 0;
for (var i = 0, e = children.length; i < e; ++i) {
var ch = children[i];
size += ch.chunkSize(); height += ch.height;
ch.parent = this;
}
this.size = size;
this.height = height;
this.parent = null;
}
BranchChunk.prototype = {
chunkSize: function() { return this.size; },
removeInner: function(at, n) {
this.size -= n;
for (var i = 0; i < this.children.length; ++i) {
var child = this.children[i], sz = child.chunkSize();
if (at < sz) {
var rm = Math.min(n, sz - at), oldHeight = child.height;
child.removeInner(at, rm);
this.height -= oldHeight - child.height;
if (sz == rm) { this.children.splice(i--, 1); child.parent = null; }
if ((n -= rm) == 0) break;
at = 0;
} else at -= sz;
}
if (this.size - n < 25) {
var lines = [];
this.collapse(lines);
this.children = [new LeafChunk(lines)];
this.children[0].parent = this;
}
},
collapse: function(lines) {
for (var i = 0, e = this.children.length; i < e; ++i) this.children[i].collapse(lines);
},
insertInner: function(at, lines, height) {
this.size += lines.length;
this.height += height;
for (var i = 0, e = this.children.length; i < e; ++i) {
var child = this.children[i], sz = child.chunkSize();
if (at <= sz) {
child.insertInner(at, lines, height);
if (child.lines && child.lines.length > 50) {
while (child.lines.length > 50) {
var spilled = child.lines.splice(child.lines.length - 25, 25);
var newleaf = new LeafChunk(spilled);
child.height -= newleaf.height;
this.children.splice(i + 1, 0, newleaf);
newleaf.parent = this;
}
this.maybeSpill();
}
break;
}
at -= sz;
}
},
maybeSpill: function() {
if (this.children.length <= 10) return;
var me = this;
do {
var spilled = me.children.splice(me.children.length - 5, 5);
var sibling = new BranchChunk(spilled);
if (!me.parent) { // Become the parent node
var copy = new BranchChunk(me.children);
copy.parent = me;
me.children = [copy, sibling];
me = copy;
} else {
me.size -= sibling.size;
me.height -= sibling.height;
var myIndex = indexOf(me.parent.children, me);
me.parent.children.splice(myIndex + 1, 0, sibling);
}
sibling.parent = me.parent;
} while (me.children.length > 10);
me.parent.maybeSpill();
},
iterN: function(at, n, op) {
for (var i = 0, e = this.children.length; i < e; ++i) {
var child = this.children[i], sz = child.chunkSize();
if (at < sz) {
var used = Math.min(n, sz - at);
if (child.iterN(at, used, op)) return true;
if ((n -= used) == 0) break;
at = 0;
} else at -= sz;
}
}
};
var nextDocId = 0;
var Doc = CodeMirror.Doc = function(text, mode, firstLine) {
if (!(this instanceof Doc)) return new Doc(text, mode, firstLine);
if (firstLine == null) firstLine = 0;
BranchChunk.call(this, [new LeafChunk([new Line("", null)])]);
this.first = firstLine;
this.scrollTop = this.scrollLeft = 0;
this.cantEdit = false;
this.history = makeHistory();
this.cleanGeneration = 1;
this.frontier = firstLine;
var start = Pos(firstLine, 0);
this.sel = {from: start, to: start, head: start, anchor: start, shift: false, extend: false, goalColumn: null};
this.id = ++nextDocId;
this.modeOption = mode;
if (typeof text == "string") text = splitLines(text);
updateDoc(this, {from: start, to: start, text: text}, null, {head: start, anchor: start});
};
Doc.prototype = createObj(BranchChunk.prototype, {
constructor: Doc,
iter: function(from, to, op) {
if (op) this.iterN(from - this.first, to - from, op);
else this.iterN(this.first, this.first + this.size, from);
},
insert: function(at, lines) {
var height = 0;
for (var i = 0, e = lines.length; i < e; ++i) height += lines[i].height;
this.insertInner(at - this.first, lines, height);
},
remove: function(at, n) { this.removeInner(at - this.first, n); },
getValue: function(lineSep) {
var lines = getLines(this, this.first, this.first + this.size);
if (lineSep === false) return lines;
return lines.join(lineSep || "\n");
},
setValue: function(code) {
var top = Pos(this.first, 0), last = this.first + this.size - 1;
makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length),
text: splitLines(code), origin: "setValue"},
{head: top, anchor: top}, true);
},
replaceRange: function(code, from, to, origin) {
from = clipPos(this, from);
to = to ? clipPos(this, to) : from;
replaceRange(this, code, from, to, origin);
},
getRange: function(from, to, lineSep) {
var lines = getBetween(this, clipPos(this, from), clipPos(this, to));
if (lineSep === false) return lines;
return lines.join(lineSep || "\n");
},
getLine: function(line) {var l = this.getLineHandle(line); return l && l.text;},
setLine: function(line, text) {
if (isLine(this, line))
replaceRange(this, text, Pos(line, 0), clipPos(this, Pos(line)));
},
removeLine: function(line) {
if (line) replaceRange(this, "", clipPos(this, Pos(line - 1)), clipPos(this, Pos(line)));
else replaceRange(this, "", Pos(0, 0), clipPos(this, Pos(1, 0)));
},
getLineHandle: function(line) {if (isLine(this, line)) return getLine(this, line);},
getLineNumber: function(line) {return lineNo(line);},
getLineHandleVisualStart: function(line) {
if (typeof line == "number") line = getLine(this, line);
return visualLine(this, line);
},
lineCount: function() {return this.size;},
firstLine: function() {return this.first;},
lastLine: function() {return this.first + this.size - 1;},
clipPos: function(pos) {return clipPos(this, pos);},
getCursor: function(start) {
var sel = this.sel, pos;
if (start == null || start == "head") pos = sel.head;
else if (start == "anchor") pos = sel.anchor;
else if (start == "end" || start === false) pos = sel.to;
else pos = sel.from;
return copyPos(pos);
},
somethingSelected: function() {return !posEq(this.sel.head, this.sel.anchor);},
setCursor: docOperation(function(line, ch, extend) {
var pos = clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line);
if (extend) extendSelection(this, pos);
else setSelection(this, pos, pos);
}),
setSelection: docOperation(function(anchor, head) {
setSelection(this, clipPos(this, anchor), clipPos(this, head || anchor));
}),
extendSelection: docOperation(function(from, to) {
extendSelection(this, clipPos(this, from), to && clipPos(this, to));
}),
getSelection: function(lineSep) {return this.getRange(this.sel.from, this.sel.to, lineSep);},
replaceSelection: function(code, collapse, origin) {
makeChange(this, {from: this.sel.from, to: this.sel.to, text: splitLines(code), origin: origin}, collapse || "around");
},
undo: docOperation(function() {makeChangeFromHistory(this, "undo");}),
redo: docOperation(function() {makeChangeFromHistory(this, "redo");}),
setExtending: function(val) {this.sel.extend = val;},
historySize: function() {
var hist = this.history;
return {undo: hist.done.length, redo: hist.undone.length};
},
clearHistory: function() {this.history = makeHistory(this.history.maxGeneration);},
markClean: function() {
this.cleanGeneration = this.changeGeneration();
},
changeGeneration: function() {
this.history.lastOp = this.history.lastOrigin = null;
return this.history.generation;
},
isClean: function (gen) {
return this.history.generation == (gen || this.cleanGeneration);
},
getHistory: function() {
return {done: copyHistoryArray(this.history.done),
undone: copyHistoryArray(this.history.undone)};
},
setHistory: function(histData) {
var hist = this.history = makeHistory(this.history.maxGeneration);
hist.done = histData.done.slice(0);
hist.undone = histData.undone.slice(0);
},
markText: function(from, to, options) {
return markText(this, clipPos(this, from), clipPos(this, to), options, "range");
},
setBookmark: function(pos, options) {
var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options),
insertLeft: options && options.insertLeft};
pos = clipPos(this, pos);
return markText(this, pos, pos, realOpts, "bookmark");
},
findMarksAt: function(pos) {
pos = clipPos(this, pos);
var markers = [], spans = getLine(this, pos.line).markedSpans;
if (spans) for (var i = 0; i < spans.length; ++i) {
var span = spans[i];
if ((span.from == null || span.from <= pos.ch) &&
(span.to == null || span.to >= pos.ch))
markers.push(span.marker.parent || span.marker);
}
return markers;
},
getAllMarks: function() {
var markers = [];
this.iter(function(line) {
var sps = line.markedSpans;
if (sps) for (var i = 0; i < sps.length; ++i)
if (sps[i].from != null) markers.push(sps[i].marker);
});
return markers;
},
posFromIndex: function(off) {
var ch, lineNo = this.first;
this.iter(function(line) {
var sz = line.text.length + 1;
if (sz > off) { ch = off; return true; }
off -= sz;
++lineNo;
});
return clipPos(this, Pos(lineNo, ch));
},
indexFromPos: function (coords) {
coords = clipPos(this, coords);
var index = coords.ch;
if (coords.line < this.first || coords.ch < 0) return 0;
this.iter(this.first, coords.line, function (line) {
index += line.text.length + 1;
});
return index;
},
copy: function(copyHistory) {
var doc = new Doc(getLines(this, this.first, this.first + this.size), this.modeOption, this.first);
doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft;
doc.sel = {from: this.sel.from, to: this.sel.to, head: this.sel.head, anchor: this.sel.anchor,
shift: this.sel.shift, extend: false, goalColumn: this.sel.goalColumn};
if (copyHistory) {
doc.history.undoDepth = this.history.undoDepth;
doc.setHistory(this.getHistory());
}
return doc;
},
linkedDoc: function(options) {
if (!options) options = {};
var from = this.first, to = this.first + this.size;
if (options.from != null && options.from > from) from = options.from;
if (options.to != null && options.to < to) to = options.to;
var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from);
if (options.sharedHist) copy.history = this.history;
(this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist});
copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}];
return copy;
},
unlinkDoc: function(other) {
if (other instanceof CodeMirror) other = other.doc;
if (this.linked) for (var i = 0; i < this.linked.length; ++i) {
var link = this.linked[i];
if (link.doc != other) continue;
this.linked.splice(i, 1);
other.unlinkDoc(this);
break;
}
// If the histories were shared, split them again
if (other.history == this.history) {
var splitIds = [other.id];
linkedDocs(other, function(doc) {splitIds.push(doc.id);}, true);
other.history = makeHistory();
other.history.done = copyHistoryArray(this.history.done, splitIds);
other.history.undone = copyHistoryArray(this.history.undone, splitIds);
}
},
iterLinkedDocs: function(f) {linkedDocs(this, f);},
getMode: function() {return this.mode;},
getEditor: function() {return this.cm;}
});
Doc.prototype.eachLine = Doc.prototype.iter;
// The Doc methods that should be available on CodeMirror instances
var dontDelegate = "iter insert remove copy getEditor".split(" ");
for (var prop in Doc.prototype) if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0)
CodeMirror.prototype[prop] = (function(method) {
return function() {return method.apply(this.doc, arguments);};
})(Doc.prototype[prop]);
eventMixin(Doc);
function linkedDocs(doc, f, sharedHistOnly) {
function propagate(doc, skip, sharedHist) {
if (doc.linked) for (var i = 0; i < doc.linked.length; ++i) {
var rel = doc.linked[i];
if (rel.doc == skip) continue;
var shared = sharedHist && rel.sharedHist;
if (sharedHistOnly && !shared) continue;
f(rel.doc, shared);
propagate(rel.doc, doc, shared);
}
}
propagate(doc, null, true);
}
function attachDoc(cm, doc) {
if (doc.cm) throw new Error("This document is already in use.");
cm.doc = doc;
doc.cm = cm;
estimateLineHeights(cm);
loadMode(cm);
if (!cm.options.lineWrapping) computeMaxLength(cm);
cm.options.mode = doc.modeOption;
regChange(cm);
}
// LINE UTILITIES
function getLine(chunk, n) {
n -= chunk.first;
while (!chunk.lines) {
for (var i = 0;; ++i) {
var child = chunk.children[i], sz = child.chunkSize();
if (n < sz) { chunk = child; break; }
n -= sz;
}
}
return chunk.lines[n];
}
function getBetween(doc, start, end) {
var out = [], n = start.line;
doc.iter(start.line, end.line + 1, function(line) {
var text = line.text;
if (n == end.line) text = text.slice(0, end.ch);
if (n == start.line) text = text.slice(start.ch);
out.push(text);
++n;
});
return out;
}
function getLines(doc, from, to) {
var out = [];
doc.iter(from, to, function(line) { out.push(line.text); });
return out;
}
function updateLineHeight(line, height) {
var diff = height - line.height;
for (var n = line; n; n = n.parent) n.height += diff;
}
function lineNo(line) {
if (line.parent == null) return null;
var cur = line.parent, no = indexOf(cur.lines, line);
for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) {
for (var i = 0;; ++i) {
if (chunk.children[i] == cur) break;
no += chunk.children[i].chunkSize();
}
}
return no + cur.first;
}
function lineAtHeight(chunk, h) {
var n = chunk.first;
outer: do {
for (var i = 0, e = chunk.children.length; i < e; ++i) {
var child = chunk.children[i], ch = child.height;
if (h < ch) { chunk = child; continue outer; }
h -= ch;
n += child.chunkSize();
}
return n;
} while (!chunk.lines);
for (var i = 0, e = chunk.lines.length; i < e; ++i) {
var line = chunk.lines[i], lh = line.height;
if (h < lh) break;
h -= lh;
}
return n + i;
}
function heightAtLine(cm, lineObj) {
lineObj = visualLine(cm.doc, lineObj);
var h = 0, chunk = lineObj.parent;
for (var i = 0; i < chunk.lines.length; ++i) {
var line = chunk.lines[i];
if (line == lineObj) break;
else h += line.height;
}
for (var p = chunk.parent; p; chunk = p, p = chunk.parent) {
for (var i = 0; i < p.children.length; ++i) {
var cur = p.children[i];
if (cur == chunk) break;
else h += cur.height;
}
}
return h;
}
function getOrder(line) {
var order = line.order;
if (order == null) order = line.order = bidiOrdering(line.text);
return order;
}
// HISTORY
function makeHistory(startGen) {
return {
// Arrays of history events. Doing something adds an event to
// done and clears undo. Undoing moves events from done to
// undone, redoing moves them in the other direction.
done: [], undone: [], undoDepth: Infinity,
// Used to track when changes can be merged into a single undo
// event
lastTime: 0, lastOp: null, lastOrigin: null,
// Used by the isClean() method
generation: startGen || 1, maxGeneration: startGen || 1
};
}
function attachLocalSpans(doc, change, from, to) {
var existing = change["spans_" + doc.id], n = 0;
doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function(line) {
if (line.markedSpans)
(existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans;
++n;
});
}
function historyChangeFromChange(doc, change) {
var from = { line: change.from.line, ch: change.from.ch };
var histChange = {from: from, to: changeEnd(change), text: getBetween(doc, change.from, change.to)};
attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);
linkedDocs(doc, function(doc) {attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);}, true);
return histChange;
}
function addToHistory(doc, change, selAfter, opId) {
var hist = doc.history;
hist.undone.length = 0;
var time = +new Date, cur = lst(hist.done);
if (cur &&
(hist.lastOp == opId ||
hist.lastOrigin == change.origin && change.origin &&
((change.origin.charAt(0) == "+" && doc.cm && hist.lastTime > time - doc.cm.options.historyEventDelay) ||
change.origin.charAt(0) == "*"))) {
// Merge this change into the last event
var last = lst(cur.changes);
if (posEq(change.from, change.to) && posEq(change.from, last.to)) {
// Optimized case for simple insertion -- don't want to add
// new changesets for every character typed
last.to = changeEnd(change);
} else {
// Add new sub-event
cur.changes.push(historyChangeFromChange(doc, change));
}
cur.anchorAfter = selAfter.anchor; cur.headAfter = selAfter.head;
} else {
// Can not be merged, start a new event.
cur = {changes: [historyChangeFromChange(doc, change)],
generation: hist.generation,
anchorBefore: doc.sel.anchor, headBefore: doc.sel.head,
anchorAfter: selAfter.anchor, headAfter: selAfter.head};
hist.done.push(cur);
hist.generation = ++hist.maxGeneration;
while (hist.done.length > hist.undoDepth)
hist.done.shift();
}
hist.lastTime = time;
hist.lastOp = opId;
hist.lastOrigin = change.origin;
}
function removeClearedSpans(spans) {
if (!spans) return null;
for (var i = 0, out; i < spans.length; ++i) {
if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i); }
else if (out) out.push(spans[i]);
}
return !out ? spans : out.length ? out : null;
}
function getOldSpans(doc, change) {
var found = change["spans_" + doc.id];
if (!found) return null;
for (var i = 0, nw = []; i < change.text.length; ++i)
nw.push(removeClearedSpans(found[i]));
return nw;
}
// Used both to provide a JSON-safe object in .getHistory, and, when
// detaching a document, to split the history in two
function copyHistoryArray(events, newGroup) {
for (var i = 0, copy = []; i < events.length; ++i) {
var event = events[i], changes = event.changes, newChanges = [];
copy.push({changes: newChanges, anchorBefore: event.anchorBefore, headBefore: event.headBefore,
anchorAfter: event.anchorAfter, headAfter: event.headAfter});
for (var j = 0; j < changes.length; ++j) {
var change = changes[j], m;
newChanges.push({from: change.from, to: change.to, text: change.text});
if (newGroup) for (var prop in change) if (m = prop.match(/^spans_(\d+)$/)) {
if (indexOf(newGroup, Number(m[1])) > -1) {
lst(newChanges)[prop] = change[prop];
delete change[prop];
}
}
}
}
return copy;
}
// Rebasing/resetting history to deal with externally-sourced changes
function rebaseHistSel(pos, from, to, diff) {
if (to < pos.line) {
pos.line += diff;
} else if (from < pos.line) {
pos.line = from;
pos.ch = 0;
}
}
// Tries to rebase an array of history events given a change in the
// document. If the change touches the same lines as the event, the
// event, and everything 'behind' it, is discarded. If the change is
// before the event, the event's positions are updated. Uses a
// copy-on-write scheme for the positions, to avoid having to
// reallocate them all on every rebase, but also avoid problems with
// shared position objects being unsafely updated.
function rebaseHistArray(array, from, to, diff) {
for (var i = 0; i < array.length; ++i) {
var sub = array[i], ok = true;
for (var j = 0; j < sub.changes.length; ++j) {
var cur = sub.changes[j];
if (!sub.copied) { cur.from = copyPos(cur.from); cur.to = copyPos(cur.to); }
if (to < cur.from.line) {
cur.from.line += diff;
cur.to.line += diff;
} else if (from <= cur.to.line) {
ok = false;
break;
}
}
if (!sub.copied) {
sub.anchorBefore = copyPos(sub.anchorBefore); sub.headBefore = copyPos(sub.headBefore);
sub.anchorAfter = copyPos(sub.anchorAfter); sub.readAfter = copyPos(sub.headAfter);
sub.copied = true;
}
if (!ok) {
array.splice(0, i + 1);
i = 0;
} else {
rebaseHistSel(sub.anchorBefore); rebaseHistSel(sub.headBefore);
rebaseHistSel(sub.anchorAfter); rebaseHistSel(sub.headAfter);
}
}
}
function rebaseHist(hist, change) {
var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1;
rebaseHistArray(hist.done, from, to, diff);
rebaseHistArray(hist.undone, from, to, diff);
}
// EVENT OPERATORS
function stopMethod() {e_stop(this);}
// Ensure an event has a stop method.
function addStop(event) {
if (!event.stop) event.stop = stopMethod;
return event;
}
function e_preventDefault(e) {
if (e.preventDefault) e.preventDefault();
else e.returnValue = false;
}
function e_stopPropagation(e) {
if (e.stopPropagation) e.stopPropagation();
else e.cancelBubble = true;
}
function e_defaultPrevented(e) {
return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false;
}
function e_stop(e) {e_preventDefault(e); e_stopPropagation(e);}
CodeMirror.e_stop = e_stop;
CodeMirror.e_preventDefault = e_preventDefault;
CodeMirror.e_stopPropagation = e_stopPropagation;
function e_target(e) {return e.target || e.srcElement;}
function e_button(e) {
var b = e.which;
if (b == null) {
if (e.button & 1) b = 1;
else if (e.button & 2) b = 3;
else if (e.button & 4) b = 2;
}
if (mac && e.ctrlKey && b == 1) b = 3;
return b;
}
// EVENT HANDLING
function on(emitter, type, f) {
if (emitter.addEventListener)
emitter.addEventListener(type, f, false);
else if (emitter.attachEvent)
emitter.attachEvent("on" + type, f);
else {
var map = emitter._handlers || (emitter._handlers = {});
var arr = map[type] || (map[type] = []);
arr.push(f);
}
}
function off(emitter, type, f) {
if (emitter.removeEventListener)
emitter.removeEventListener(type, f, false);
else if (emitter.detachEvent)
emitter.detachEvent("on" + type, f);
else {
var arr = emitter._handlers && emitter._handlers[type];
if (!arr) return;
for (var i = 0; i < arr.length; ++i)
if (arr[i] == f) { arr.splice(i, 1); break; }
}
}
function signal(emitter, type /*, values...*/) {
var arr = emitter._handlers && emitter._handlers[type];
if (!arr) return;
var args = Array.prototype.slice.call(arguments, 2);
for (var i = 0; i < arr.length; ++i) arr[i].apply(null, args);
}
var delayedCallbacks, delayedCallbackDepth = 0;
function signalLater(emitter, type /*, values...*/) {
var arr = emitter._handlers && emitter._handlers[type];
if (!arr) return;
var args = Array.prototype.slice.call(arguments, 2);
if (!delayedCallbacks) {
++delayedCallbackDepth;
delayedCallbacks = [];
setTimeout(fireDelayed, 0);
}
function bnd(f) {return function(){f.apply(null, args);};};
for (var i = 0; i < arr.length; ++i)
delayedCallbacks.push(bnd(arr[i]));
}
function signalDOMEvent(cm, e, override) {
signal(cm, override || e.type, cm, e);
return e_defaultPrevented(e) || e.codemirrorIgnore;
}
function fireDelayed() {
--delayedCallbackDepth;
var delayed = delayedCallbacks;
delayedCallbacks = null;
for (var i = 0; i < delayed.length; ++i) delayed[i]();
}
function hasHandler(emitter, type) {
var arr = emitter._handlers && emitter._handlers[type];
return arr && arr.length > 0;
}
CodeMirror.on = on; CodeMirror.off = off; CodeMirror.signal = signal;
function eventMixin(ctor) {
ctor.prototype.on = function(type, f) {on(this, type, f);};
ctor.prototype.off = function(type, f) {off(this, type, f);};
}
// MISC UTILITIES
// Number of pixels added to scroller and sizer to hide scrollbar
var scrollerCutOff = 30;
// Returned or thrown by various protocols to signal 'I'm not
// handling this'.
var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}};
function Delayed() {this.id = null;}
Delayed.prototype = {set: function(ms, f) {clearTimeout(this.id); this.id = setTimeout(f, ms);}};
// Counts the column offset in a string, taking tabs into account.
// Used mostly to find indentation.
function countColumn(string, end, tabSize, startIndex, startValue) {
if (end == null) {
end = string.search(/[^\s\u00a0]/);
if (end == -1) end = string.length;
}
for (var i = startIndex || 0, n = startValue || 0; i < end; ++i) {
if (string.charAt(i) == "\t") n += tabSize - (n % tabSize);
else ++n;
}
return n;
}
CodeMirror.countColumn = countColumn;
var spaceStrs = [""];
function spaceStr(n) {
while (spaceStrs.length <= n)
spaceStrs.push(lst(spaceStrs) + " ");
return spaceStrs[n];
}
function lst(arr) { return arr[arr.length-1]; }
function selectInput(node) {
if (ios) { // Mobile Safari apparently has a bug where select() is broken.
node.selectionStart = 0;
node.selectionEnd = node.value.length;
} else {
// Suppress mysterious IE10 errors
try { node.select(); }
catch(_e) {}
}
}
function indexOf(collection, elt) {
if (collection.indexOf) return collection.indexOf(elt);
for (var i = 0, e = collection.length; i < e; ++i)
if (collection[i] == elt) return i;
return -1;
}
function createObj(base, props) {
function Obj() {}
Obj.prototype = base;
var inst = new Obj();
if (props) copyObj(props, inst);
return inst;
}
function copyObj(obj, target) {
if (!target) target = {};
for (var prop in obj) if (obj.hasOwnProperty(prop)) target[prop] = obj[prop];
return target;
}
function emptyArray(size) {
for (var a = [], i = 0; i < size; ++i) a.push(undefined);
return a;
}
function bind(f) {
var args = Array.prototype.slice.call(arguments, 1);
return function(){return f.apply(null, args);};
}
var nonASCIISingleCaseWordChar = /[\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/;
function isWordChar(ch) {
return /\w/.test(ch) || ch > "\x80" &&
(ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch));
}
function isEmpty(obj) {
for (var n in obj) if (obj.hasOwnProperty(n) && obj[n]) return false;
return true;
}
var isExtendingChar = /[\u0300-\u036F\u0483-\u0487\u0488-\u0489\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\uA66F\uA670-\uA672\uA674-\uA67D\uA69F\udc00-\udfff]/;
// DOM UTILITIES
function elt(tag, content, className, style) {
var e = document.createElement(tag);
if (className) e.className = className;
if (style) e.style.cssText = style;
if (typeof content == "string") setTextContent(e, content);
else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]);
return e;
}
function removeChildren(e) {
for (var count = e.childNodes.length; count > 0; --count)
e.removeChild(e.firstChild);
return e;
}
function removeChildrenAndAdd(parent, e) {
return removeChildren(parent).appendChild(e);
}
function setTextContent(e, str) {
if (ie_lt9) {
e.innerHTML = "";
e.appendChild(document.createTextNode(str));
} else e.textContent = str;
}
function getRect(node) {
return node.getBoundingClientRect();
}
CodeMirror.replaceGetRect = function(f) { getRect = f; };
// FEATURE DETECTION
// Detect drag-and-drop
var dragAndDrop = function() {
// There is *some* kind of drag-and-drop support in IE6-8, but I
// couldn't get it to work yet.
if (ie_lt9) return false;
var div = elt('div');
return "draggable" in div || "dragDrop" in div;
}();
// For a reason I have yet to figure out, some browsers disallow
// word wrapping between certain characters *only* if a new inline
// element is started between them. This makes it hard to reliably
// measure the position of things, since that requires inserting an
// extra span. This terribly fragile set of tests matches the
// character combinations that suffer from this phenomenon on the
// various browsers.
function spanAffectsWrapping() { return false; }
if (gecko) // Only for "$'"
spanAffectsWrapping = function(str, i) {
return str.charCodeAt(i - 1) == 36 && str.charCodeAt(i) == 39;
};
else if (safari && !/Version\/([6-9]|\d\d)\b/.test(navigator.userAgent))
spanAffectsWrapping = function(str, i) {
return /\-[^ \-?]|\?[^ !\'\"\),.\-\/:;\?\]\}]/.test(str.slice(i - 1, i + 1));
};
else if (webkit && !/Chrome\/(?:29|[3-9]\d|\d\d\d)\./.test(navigator.userAgent))
spanAffectsWrapping = function(str, i) {
if (i > 1 && str.charCodeAt(i - 1) == 45) {
if (/\w/.test(str.charAt(i - 2)) && /[^\-?\.]/.test(str.charAt(i))) return true;
if (i > 2 && /[\d\.,]/.test(str.charAt(i - 2)) && /[\d\.,]/.test(str.charAt(i))) return false;
}
return /[~!#%&*)=+}\]|\"\.>,:;][({[<]|-[^\-?\.\u2010-\u201f\u2026]|\?[\w~`@#$%\^&*(_=+{[|><]|…[\w~`@#$%\^&*(_=+{[><]/.test(str.slice(i - 1, i + 1));
};
var knownScrollbarWidth;
function scrollbarWidth(measure) {
if (knownScrollbarWidth != null) return knownScrollbarWidth;
var test = elt("div", null, null, "width: 50px; height: 50px; overflow-x: scroll");
removeChildrenAndAdd(measure, test);
if (test.offsetWidth)
knownScrollbarWidth = test.offsetHeight - test.clientHeight;
return knownScrollbarWidth || 0;
}
var zwspSupported;
function zeroWidthElement(measure) {
if (zwspSupported == null) {
var test = elt("span", "\u200b");
removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")]));
if (measure.firstChild.offsetHeight != 0)
zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !ie_lt8;
}
if (zwspSupported) return elt("span", "\u200b");
else return elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px");
}
// See if "".split is the broken IE version, if so, provide an
// alternative way to split lines.
var splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) {
var pos = 0, result = [], l = string.length;
while (pos <= l) {
var nl = string.indexOf("\n", pos);
if (nl == -1) nl = string.length;
var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl);
var rt = line.indexOf("\r");
if (rt != -1) {
result.push(line.slice(0, rt));
pos += rt + 1;
} else {
result.push(line);
pos = nl + 1;
}
}
return result;
} : function(string){return string.split(/\r\n?|\n/);};
CodeMirror.splitLines = splitLines;
var hasSelection = window.getSelection ? function(te) {
try { return te.selectionStart != te.selectionEnd; }
catch(e) { return false; }
} : function(te) {
try {var range = te.ownerDocument.selection.createRange();}
catch(e) {}
if (!range || range.parentElement() != te) return false;
return range.compareEndPoints("StartToEnd", range) != 0;
};
var hasCopyEvent = (function() {
var e = elt("div");
if ("oncopy" in e) return true;
e.setAttribute("oncopy", "return;");
return typeof e.oncopy == 'function';
})();
// KEY NAMING
var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt",
19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End",
36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert",
46: "Delete", 59: ";", 91: "Mod", 92: "Mod", 93: "Mod", 109: "-", 107: "=", 127: "Delete",
186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\",
221: "]", 222: "'", 63276: "PageUp", 63277: "PageDown", 63275: "End", 63273: "Home",
63234: "Left", 63232: "Up", 63235: "Right", 63233: "Down", 63302: "Insert", 63272: "Delete"};
CodeMirror.keyNames = keyNames;
(function() {
// Number keys
for (var i = 0; i < 10; i++) keyNames[i + 48] = String(i);
// Alphabetic keys
for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i);
// Function keys
for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i;
})();
// BIDI HELPERS
function iterateBidiSections(order, from, to, f) {
if (!order) return f(from, to, "ltr");
var found = false;
for (var i = 0; i < order.length; ++i) {
var part = order[i];
if (part.from < to && part.to > from || from == to && part.to == from) {
f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr");
found = true;
}
}
if (!found) f(from, to, "ltr");
}
function bidiLeft(part) { return part.level % 2 ? part.to : part.from; }
function bidiRight(part) { return part.level % 2 ? part.from : part.to; }
function lineLeft(line) { var order = getOrder(line); return order ? bidiLeft(order[0]) : 0; }
function lineRight(line) {
var order = getOrder(line);
if (!order) return line.text.length;
return bidiRight(lst(order));
}
function lineStart(cm, lineN) {
var line = getLine(cm.doc, lineN);
var visual = visualLine(cm.doc, line);
if (visual != line) lineN = lineNo(visual);
var order = getOrder(visual);
var ch = !order ? 0 : order[0].level % 2 ? lineRight(visual) : lineLeft(visual);
return Pos(lineN, ch);
}
function lineEnd(cm, lineN) {
var merged, line;
while (merged = collapsedSpanAtEnd(line = getLine(cm.doc, lineN)))
lineN = merged.find().to.line;
var order = getOrder(line);
var ch = !order ? line.text.length : order[0].level % 2 ? lineLeft(line) : lineRight(line);
return Pos(lineN, ch);
}
function compareBidiLevel(order, a, b) {
var linedir = order[0].level;
if (a == linedir) return true;
if (b == linedir) return false;
return a < b;
}
var bidiOther;
function getBidiPartAt(order, pos) {
for (var i = 0, found; i < order.length; ++i) {
var cur = order[i];
if (cur.from < pos && cur.to > pos) { bidiOther = null; return i; }
if (cur.from == pos || cur.to == pos) {
if (found == null) {
found = i;
} else if (compareBidiLevel(order, cur.level, order[found].level)) {
bidiOther = found;
return i;
} else {
bidiOther = i;
return found;
}
}
}
bidiOther = null;
return found;
}
function moveInLine(line, pos, dir, byUnit) {
if (!byUnit) return pos + dir;
do pos += dir;
while (pos > 0 && isExtendingChar.test(line.text.charAt(pos)));
return pos;
}
// This is somewhat involved. It is needed in order to move
// 'visually' through bi-directional text -- i.e., pressing left
// should make the cursor go left, even when in RTL text. The
// tricky part is the 'jumps', where RTL and LTR text touch each
// other. This often requires the cursor offset to move more than
// one unit, in order to visually move one unit.
function moveVisually(line, start, dir, byUnit) {
var bidi = getOrder(line);
if (!bidi) return moveLogically(line, start, dir, byUnit);
var pos = getBidiPartAt(bidi, start), part = bidi[pos];
var target = moveInLine(line, start, part.level % 2 ? -dir : dir, byUnit);
for (;;) {
if (target > part.from && target < part.to) return target;
if (target == part.from || target == part.to) {
if (getBidiPartAt(bidi, target) == pos) return target;
part = bidi[pos += dir];
return (dir > 0) == part.level % 2 ? part.to : part.from;
} else {
part = bidi[pos += dir];
if (!part) return null;
if ((dir > 0) == part.level % 2)
target = moveInLine(line, part.to, -1, byUnit);
else
target = moveInLine(line, part.from, 1, byUnit);
}
}
}
function moveLogically(line, start, dir, byUnit) {
var target = start + dir;
if (byUnit) while (target > 0 && isExtendingChar.test(line.text.charAt(target))) target += dir;
return target < 0 || target > line.text.length ? null : target;
}
// Bidirectional ordering algorithm
// See http://unicode.org/reports/tr9/tr9-13.html for the algorithm
// that this (partially) implements.
// One-char codes used for character types:
// L (L): Left-to-Right
// R (R): Right-to-Left
// r (AL): Right-to-Left Arabic
// 1 (EN): European Number
// + (ES): European Number Separator
// % (ET): European Number Terminator
// n (AN): Arabic Number
// , (CS): Common Number Separator
// m (NSM): Non-Spacing Mark
// b (BN): Boundary Neutral
// s (B): Paragraph Separator
// t (S): Segment Separator
// w (WS): Whitespace
// N (ON): Other Neutrals
// Returns null if characters are ordered as they appear
// (left-to-right), or an array of sections ({from, to, level}
// objects) in the order in which they occur visually.
var bidiOrdering = (function() {
// Character types for codepoints 0 to 0xff
var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLL";
// Character types for codepoints 0x600 to 0x6ff
var arabicTypes = "rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmmrrrrrrrrrrrrrrrrrr";
function charType(code) {
if (code <= 0xff) return lowTypes.charAt(code);
else if (0x590 <= code && code <= 0x5f4) return "R";
else if (0x600 <= code && code <= 0x6ff) return arabicTypes.charAt(code - 0x600);
else if (0x700 <= code && code <= 0x8ac) return "r";
else return "L";
}
var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/;
var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/;
// Browsers seem to always treat the boundaries of block elements as being L.
var outerType = "L";
return function(str) {
if (!bidiRE.test(str)) return false;
var len = str.length, types = [];
for (var i = 0, type; i < len; ++i)
types.push(type = charType(str.charCodeAt(i)));
// W1. Examine each non-spacing mark (NSM) in the level run, and
// change the type of the NSM to the type of the previous
// character. If the NSM is at the start of the level run, it will
// get the type of sor.
for (var i = 0, prev = outerType; i < len; ++i) {
var type = types[i];
if (type == "m") types[i] = prev;
else prev = type;
}
// W2. Search backwards from each instance of a European number
// until the first strong type (R, L, AL, or sor) is found. If an
// AL is found, change the type of the European number to Arabic
// number.
// W3. Change all ALs to R.
for (var i = 0, cur = outerType; i < len; ++i) {
var type = types[i];
if (type == "1" && cur == "r") types[i] = "n";
else if (isStrong.test(type)) { cur = type; if (type == "r") types[i] = "R"; }
}
// W4. A single European separator between two European numbers
// changes to a European number. A single common separator between
// two numbers of the same type changes to that type.
for (var i = 1, prev = types[0]; i < len - 1; ++i) {
var type = types[i];
if (type == "+" && prev == "1" && types[i+1] == "1") types[i] = "1";
else if (type == "," && prev == types[i+1] &&
(prev == "1" || prev == "n")) types[i] = prev;
prev = type;
}
// W5. A sequence of European terminators adjacent to European
// numbers changes to all European numbers.
// W6. Otherwise, separators and terminators change to Other
// Neutral.
for (var i = 0; i < len; ++i) {
var type = types[i];
if (type == ",") types[i] = "N";
else if (type == "%") {
for (var end = i + 1; end < len && types[end] == "%"; ++end) {}
var replace = (i && types[i-1] == "!") || (end < len - 1 && types[end] == "1") ? "1" : "N";
for (var j = i; j < end; ++j) types[j] = replace;
i = end - 1;
}
}
// W7. Search backwards from each instance of a European number
// until the first strong type (R, L, or sor) is found. If an L is
// found, then change the type of the European number to L.
for (var i = 0, cur = outerType; i < len; ++i) {
var type = types[i];
if (cur == "L" && type == "1") types[i] = "L";
else if (isStrong.test(type)) cur = type;
}
// N1. A sequence of neutrals takes the direction of the
// surrounding strong text if the text on both sides has the same
// direction. European and Arabic numbers act as if they were R in
// terms of their influence on neutrals. Start-of-level-run (sor)
// and end-of-level-run (eor) are used at level run boundaries.
// N2. Any remaining neutrals take the embedding direction.
for (var i = 0; i < len; ++i) {
if (isNeutral.test(types[i])) {
for (var end = i + 1; end < len && isNeutral.test(types[end]); ++end) {}
var before = (i ? types[i-1] : outerType) == "L";
var after = (end < len - 1 ? types[end] : outerType) == "L";
var replace = before || after ? "L" : "R";
for (var j = i; j < end; ++j) types[j] = replace;
i = end - 1;
}
}
// Here we depart from the documented algorithm, in order to avoid
// building up an actual levels array. Since there are only three
// levels (0, 1, 2) in an implementation that doesn't take
// explicit embedding into account, we can build up the order on
// the fly, without following the level-based algorithm.
var order = [], m;
for (var i = 0; i < len;) {
if (countsAsLeft.test(types[i])) {
var start = i;
for (++i; i < len && countsAsLeft.test(types[i]); ++i) {}
order.push({from: start, to: i, level: 0});
} else {
var pos = i, at = order.length;
for (++i; i < len && types[i] != "L"; ++i) {}
for (var j = pos; j < i;) {
if (countsAsNum.test(types[j])) {
if (pos < j) order.splice(at, 0, {from: pos, to: j, level: 1});
var nstart = j;
for (++j; j < i && countsAsNum.test(types[j]); ++j) {}
order.splice(at, 0, {from: nstart, to: j, level: 2});
pos = j;
} else ++j;
}
if (pos < i) order.splice(at, 0, {from: pos, to: i, level: 1});
}
}
if (order[0].level == 1 && (m = str.match(/^\s+/))) {
order[0].from = m[0].length;
order.unshift({from: 0, to: m[0].length, level: 0});
}
if (lst(order).level == 1 && (m = str.match(/\s+$/))) {
lst(order).to -= m[0].length;
order.push({from: len - m[0].length, to: len, level: 0});
}
if (order[0].level != lst(order).level)
order.push({from: len, to: len, level: order[0].level});
return order;
};
})();
// THE END
CodeMirror.version = "3.15.0";
return CodeMirror;
})();

View File

@@ -1,2150 +1,3527 @@
// CodeMirror version 2.23 (with edits)
//
// All functions that need access to the editor's state live inside
// the CodeMirror function. Below that, at the bottom of the file,
// some utilities are defined.
// CodeMirror is the only global var we claim
var CodeMirror = (function() {
// This is the function that produces an editor instance. Its
// closure is used to store the editor state.
function CodeMirror(place, givenOptions) {
// Determine effective options based on given values and defaults.
var options = {}, defaults = CodeMirror.defaults;
for (var opt in defaults)
if (defaults.hasOwnProperty(opt))
options[opt] = (givenOptions && givenOptions.hasOwnProperty(opt) ? givenOptions : defaults)[opt];
window.CodeMirror = (function() {
"use strict";
// BROWSER SNIFFING
// Crude, but necessary to handle a number of hard-to-feature-detect
// bugs and behavior differences.
var gecko = /gecko\/\d/i.test(navigator.userAgent);
// IE11 currently doesn't count as 'ie', since it has almost none of
// the same bugs as earlier versions. Use ie_gt10 to handle
// incompatibilities in that version.
var old_ie = /MSIE \d/.test(navigator.userAgent);
var ie_lt8 = old_ie && (document.documentMode == null || document.documentMode < 8);
var ie_lt9 = old_ie && (document.documentMode == null || document.documentMode < 9);
var ie_gt10 = /Trident\/([7-9]|\d{2,})\./.test(navigator.userAgent);
var ie = old_ie || ie_gt10;
var webkit = /WebKit\//.test(navigator.userAgent);
var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent);
var chrome = /Chrome\//.test(navigator.userAgent);
var opera = /Opera\//.test(navigator.userAgent);
var safari = /Apple Computer/.test(navigator.vendor);
var khtml = /KHTML\//.test(navigator.userAgent);
var mac_geLion = /Mac OS X 1\d\D([7-9]|\d\d)\D/.test(navigator.userAgent);
var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(navigator.userAgent);
var phantom = /PhantomJS/.test(navigator.userAgent);
var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent);
// This is woefully incomplete. Suggestions for alternative methods welcome.
var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent);
var mac = ios || /Mac/.test(navigator.platform);
var windows = /win/i.test(navigator.platform);
var opera_version = opera && navigator.userAgent.match(/Version\/(\d*\.\d*)/);
if (opera_version) opera_version = Number(opera_version[1]);
if (opera_version && opera_version >= 15) { opera = false; webkit = true; }
// Some browsers use the wrong event properties to signal cmd/ctrl on OS X
var flipCtrlCmd = mac && (qtwebkit || opera && (opera_version == null || opera_version < 12.11));
var captureMiddleClick = gecko || (old_ie && !ie_lt9);
// Optimize some code when these features are not used
var sawReadOnlySpans = false, sawCollapsedSpans = false;
// CONSTRUCTOR
function CodeMirror(place, options) {
if (!(this instanceof CodeMirror)) return new CodeMirror(place, options);
this.options = options = options || {};
// Determine effective options based on given values and defaults.
for (var opt in defaults) if (!options.hasOwnProperty(opt) && defaults.hasOwnProperty(opt))
options[opt] = defaults[opt];
setGuttersForLineNumbers(options);
var docStart = typeof options.value == "string" ? 0 : options.value.first;
var display = this.display = makeDisplay(place, docStart);
display.wrapper.CodeMirror = this;
updateGutters(this);
if (options.autofocus && !mobile) focusInput(this);
this.state = {keyMaps: [],
overlays: [],
modeGen: 0,
overwrite: false, focused: false,
suppressEdits: false,
pasteIncoming: false, cutIncoming: false,
draggingText: false,
highlight: new Delayed()};
themeChanged(this);
if (options.lineWrapping)
this.display.wrapper.className += " CodeMirror-wrap";
var doc = options.value;
if (typeof doc == "string") doc = new Doc(options.value, options.mode);
operation(this, attachDoc)(this, doc);
// Override magic textarea content restore that IE sometimes does
// on our hidden textarea on reload
if (old_ie) setTimeout(bind(resetInput, this, true), 20);
registerEventHandlers(this);
// IE throws unspecified error in certain cases, when
// trying to access activeElement before onload
var hasFocus; try { hasFocus = (document.activeElement == display.input); } catch(e) { }
if (hasFocus || (options.autofocus && !mobile)) setTimeout(bind(onFocus, this), 20);
else onBlur(this);
operation(this, function() {
for (var opt in optionHandlers)
if (optionHandlers.propertyIsEnumerable(opt))
optionHandlers[opt](this, options[opt], Init);
for (var i = 0; i < initHooks.length; ++i) initHooks[i](this);
})();
}
// DISPLAY CONSTRUCTOR
function makeDisplay(place, docStart) {
var d = {};
var input = d.input = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none; font-size: 4px;");
if (webkit) input.style.width = "1000px";
else input.setAttribute("wrap", "off");
// if border: 0; -- iOS fails to open keyboard (issue #1287)
if (ios) input.style.border = "1px solid black";
input.setAttribute("autocorrect", "off"); input.setAttribute("autocapitalize", "off"); input.setAttribute("spellcheck", "false");
// Wraps and hides input textarea
d.inputDiv = elt("div", [input], null, "overflow: hidden; position: relative; width: 3px; height: 0px;");
// The actual fake scrollbars.
d.scrollbarH = elt("div", [elt("div", null, null, "height: 1px")], "CodeMirror-hscrollbar");
d.scrollbarV = elt("div", [elt("div", null, null, "width: 1px")], "CodeMirror-vscrollbar");
d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler");
d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler");
// DIVs containing the selection and the actual code
d.lineDiv = elt("div", null, "CodeMirror-code");
d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1");
// Blinky cursor, and element used to ensure cursor fits at the end of a line
d.cursor = elt("div", "\u00a0", "CodeMirror-cursor");
// Secondary cursor, shown when on a 'jump' in bi-directional text
d.otherCursor = elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor");
// Used to measure text size
d.measure = elt("div", null, "CodeMirror-measure");
// Wraps everything that needs to exist inside the vertically-padded coordinate system
d.lineSpace = elt("div", [d.measure, d.selectionDiv, d.lineDiv, d.cursor, d.otherCursor],
null, "position: relative; outline: none");
// Moved around its parent to cover visible view
d.mover = elt("div", [elt("div", [d.lineSpace], "CodeMirror-lines")], null, "position: relative");
// Set to the height of the text, causes scrolling
d.sizer = elt("div", [d.mover], "CodeMirror-sizer");
// D is needed because behavior of elts with overflow: auto and padding is inconsistent across browsers
d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerCutOff + "px; width: 1px;");
// Will contain the gutters, if any
d.gutters = elt("div", null, "CodeMirror-gutters");
d.lineGutter = null;
// Provides scrolling
d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll");
d.scroller.setAttribute("tabIndex", "-1");
// The element in which the editor lives.
var wrapper = document.createElement("div");
wrapper.className = "CodeMirror" + (options.lineWrapping ? " CodeMirror-wrap" : "");
// This mess creates the base DOM structure for the editor.
wrapper.innerHTML =
'<div style="overflow: hidden; position: relative; width: 3px; height: 0px;">' + // Wraps and hides input textarea
'<textarea style="position: absolute; padding: 0; width: 1px; height: 1em" wrap="off" ' +
'autocorrect="off" autocapitalize="off"></textarea></div>' +
'<div class="CodeMirror-scroll" tabindex="-1">' +
'<div style="position: relative">' + // Set to the height of the text, causes scrolling
'<div style="position: relative">' + // Moved around its parent to cover visible view
'<div class="CodeMirror-gutter"><div class="CodeMirror-gutter-text"></div></div>' +
// Provides positioning relative to (visible) text origin
'<div class="CodeMirror-lines"><div style="position: relative; z-index: 0">' +
'<div style="position: absolute; width: 100%; height: 0; overflow: hidden; visibility: hidden;"></div>' +
'<pre class="CodeMirror-cursor">&#160;</pre>' + // Absolutely positioned blinky cursor
'<div style="position: relative; z-index: -1"></div><div></div>' + // DIVs containing the selection and the actual code
'</div></div></div></div></div>';
if (place.appendChild) place.appendChild(wrapper); else place(wrapper);
// I've never seen more elegant code in my life.
var inputDiv = wrapper.firstChild, input = inputDiv.firstChild,
scroller = wrapper.lastChild, code = scroller.firstChild,
mover = code.firstChild, gutter = mover.firstChild, gutterText = gutter.firstChild,
lineSpace = gutter.nextSibling.firstChild, measure = lineSpace.firstChild,
cursor = measure.nextSibling, selectionDiv = cursor.nextSibling,
lineDiv = selectionDiv.nextSibling;
themeChanged();
d.wrapper = elt("div", [d.inputDiv, d.scrollbarH, d.scrollbarV,
d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror");
// Work around IE7 z-index bug
if (ie_lt8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; }
if (place.appendChild) place.appendChild(d.wrapper); else place(d.wrapper);
// Needed to hide big blue blinking cursor on Mobile Safari
if (ios) input.style.width = "0px";
if (!webkit) lineSpace.draggable = true;
lineSpace.style.outline = "none";
if (options.tabindex != null) input.tabIndex = options.tabindex;
if (options.autofocus) focusInput();
if (!options.gutter && !options.lineNumbers) gutter.style.display = "none";
if (!webkit) d.scroller.draggable = true;
// Needed to handle Tab key in KHTML
if (khtml) inputDiv.style.height = "1px", inputDiv.style.position = "absolute";
if (khtml) { d.inputDiv.style.height = "1px"; d.inputDiv.style.position = "absolute"; }
// Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8).
else if (ie_lt8) d.scrollbarH.style.minWidth = d.scrollbarV.style.minWidth = "18px";
// Check for problem with IE innerHTML not working when we have a
// P (or similar) parent node.
try { stringWidth("x"); }
catch (e) {
if (e.message.match(/runtime/i))
e = new Error("A CodeMirror inside a P-style element does not work in Internet Explorer. (innerHTML bug)");
throw e;
}
// Delayed object wrap timeouts, making sure only one is active. blinker holds an interval.
var poll = new Delayed(), highlight = new Delayed(), blinker;
// mode holds a mode API object. doc is the tree of Line objects,
// work an array of lines that should be parsed, and history the
// undo history (instance of History constructor).
var mode, doc = new BranchChunk([new LeafChunk([new Line("")])]), work, focused;
loadMode();
// The selection. These are always maintained to point at valid
// positions. Inverted is used to remember that the user is
// selecting bottom-to-top.
var sel = {from: {line: 0, ch: 0}, to: {line: 0, ch: 0}, inverted: false};
// Selection-related flags. shiftSelecting obviously tracks
// whether the user is holding shift.
var shiftSelecting, lastClick, lastDoubleClick, lastScrollPos = 0, draggingText,
overwrite = false, suppressEdits = false;
// Variables used by startOperation/endOperation to track what
// happened during the operation.
var updateInput, userSelChange, changes, textChanged, selectionChanged, leaveInputAlone,
gutterDirty, callbacks;
// Current visible range (may be bigger than the view window).
var displayOffset = 0, showingFrom = 0, showingTo = 0, lastSizeC = 0;
// bracketHighlighted is used to remember that a bracket has been
// marked.
var bracketHighlighted;
d.viewOffset = d.lastSizeC = 0;
d.showingFrom = d.showingTo = docStart;
// Used to only resize the line number gutter when necessary (when
// the amount of lines crosses a boundary that makes its width change)
d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null;
// See readInput and resetInput
d.prevInput = "";
// Set to true when a non-horizontal-scrolling widget is added. As
// an optimization, widget aligning is skipped when d is false.
d.alignWidgets = false;
// Flag that indicates whether we currently expect input to appear
// (after some event like 'keypress' or 'input') and are polling
// intensively.
d.pollingFast = false;
// Self-resetting timeout for the poller
d.poll = new Delayed();
d.cachedCharWidth = d.cachedTextHeight = null;
d.measureLineCache = [];
d.measureLineCachePos = 0;
// Tracks when resetInput has punted to just putting a short
// string instead of the (large) selection.
d.inaccurateSelection = false;
// Tracks the maximum line length so that the horizontal scrollbar
// can be kept static when scrolling.
var maxLine = "", maxWidth;
var tabCache = {};
d.maxLine = null;
d.maxLineLength = 0;
d.maxLineChanged = false;
// Initialize the content.
operation(function(){setValue(options.value || ""); updateInput = false;})();
var history = new History();
// Used for measuring wheel scrolling granularity
d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null;
// Register our event handlers.
connect(scroller, "mousedown", operation(onMouseDown));
connect(scroller, "dblclick", operation(onDoubleClick));
connect(lineSpace, "dragstart", onDragStart);
connect(lineSpace, "selectstart", e_preventDefault);
return d;
}
// STATE UPDATES
// Used to get the editor into a consistent state again when options change.
function loadMode(cm) {
cm.doc.mode = CodeMirror.getMode(cm.options, cm.doc.modeOption);
resetModeState(cm);
}
function resetModeState(cm) {
cm.doc.iter(function(line) {
if (line.stateAfter) line.stateAfter = null;
if (line.styles) line.styles = null;
});
cm.doc.frontier = cm.doc.first;
startWorker(cm, 100);
cm.state.modeGen++;
if (cm.curOp) regChange(cm);
}
function wrappingChanged(cm) {
if (cm.options.lineWrapping) {
cm.display.wrapper.className += " CodeMirror-wrap";
cm.display.sizer.style.minWidth = "";
} else {
cm.display.wrapper.className = cm.display.wrapper.className.replace(" CodeMirror-wrap", "");
computeMaxLength(cm);
}
estimateLineHeights(cm);
regChange(cm);
clearCaches(cm);
setTimeout(function(){updateScrollbars(cm);}, 100);
}
function estimateHeight(cm) {
var th = textHeight(cm.display), wrapping = cm.options.lineWrapping;
var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3);
return function(line) {
if (lineIsHidden(cm.doc, line))
return 0;
else if (wrapping)
return (Math.ceil(line.text.length / perLine) || 1) * th;
else
return th;
};
}
function estimateLineHeights(cm) {
var doc = cm.doc, est = estimateHeight(cm);
doc.iter(function(line) {
var estHeight = est(line);
if (estHeight != line.height) updateLineHeight(line, estHeight);
});
}
function keyMapChanged(cm) {
var map = keyMap[cm.options.keyMap], style = map.style;
cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-keymap-\S+/g, "") +
(style ? " cm-keymap-" + style : "");
}
function themeChanged(cm) {
cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") +
cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-");
clearCaches(cm);
}
function guttersChanged(cm) {
updateGutters(cm);
regChange(cm);
setTimeout(function(){alignHorizontally(cm);}, 20);
}
function updateGutters(cm) {
var gutters = cm.display.gutters, specs = cm.options.gutters;
removeChildren(gutters);
for (var i = 0; i < specs.length; ++i) {
var gutterClass = specs[i];
var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass));
if (gutterClass == "CodeMirror-linenumbers") {
cm.display.lineGutter = gElt;
gElt.style.width = (cm.display.lineNumWidth || 1) + "px";
}
}
gutters.style.display = i ? "" : "none";
}
function lineLength(doc, line) {
if (line.height == 0) return 0;
var len = line.text.length, merged, cur = line;
while (merged = collapsedSpanAtStart(cur)) {
var found = merged.find();
cur = getLine(doc, found.from.line);
len += found.from.ch - found.to.ch;
}
cur = line;
while (merged = collapsedSpanAtEnd(cur)) {
var found = merged.find();
len -= cur.text.length - found.from.ch;
cur = getLine(doc, found.to.line);
len += cur.text.length - found.to.ch;
}
return len;
}
function computeMaxLength(cm) {
var d = cm.display, doc = cm.doc;
d.maxLine = getLine(doc, doc.first);
d.maxLineLength = lineLength(doc, d.maxLine);
d.maxLineChanged = true;
doc.iter(function(line) {
var len = lineLength(doc, line);
if (len > d.maxLineLength) {
d.maxLineLength = len;
d.maxLine = line;
}
});
}
// Make sure the gutters options contains the element
// "CodeMirror-linenumbers" when the lineNumbers option is true.
function setGuttersForLineNumbers(options) {
var found = indexOf(options.gutters, "CodeMirror-linenumbers");
if (found == -1 && options.lineNumbers) {
options.gutters = options.gutters.concat(["CodeMirror-linenumbers"]);
} else if (found > -1 && !options.lineNumbers) {
options.gutters = options.gutters.slice(0);
options.gutters.splice(found, 1);
}
}
// SCROLLBARS
// Re-synchronize the fake scrollbars with the actual size of the
// content. Optionally force a scrollTop.
function updateScrollbars(cm) {
var d = cm.display, docHeight = cm.doc.height;
var totalHeight = docHeight + paddingVert(d);
d.sizer.style.minHeight = d.heightForcer.style.top = totalHeight + "px";
d.gutters.style.height = Math.max(totalHeight, d.scroller.clientHeight - scrollerCutOff) + "px";
var scrollHeight = Math.max(totalHeight, d.scroller.scrollHeight);
var needsH = d.scroller.scrollWidth > (d.scroller.clientWidth + 1);
var needsV = scrollHeight > (d.scroller.clientHeight + 1);
if (needsV) {
d.scrollbarV.style.display = "block";
d.scrollbarV.style.bottom = needsH ? scrollbarWidth(d.measure) + "px" : "0";
d.scrollbarV.firstChild.style.height =
(scrollHeight - d.scroller.clientHeight + d.scrollbarV.clientHeight) + "px";
} else {
d.scrollbarV.style.display = "";
d.scrollbarV.firstChild.style.height = "0";
}
if (needsH) {
d.scrollbarH.style.display = "block";
d.scrollbarH.style.right = needsV ? scrollbarWidth(d.measure) + "px" : "0";
d.scrollbarH.firstChild.style.width =
(d.scroller.scrollWidth - d.scroller.clientWidth + d.scrollbarH.clientWidth) + "px";
} else {
d.scrollbarH.style.display = "";
d.scrollbarH.firstChild.style.width = "0";
}
if (needsH && needsV) {
d.scrollbarFiller.style.display = "block";
d.scrollbarFiller.style.height = d.scrollbarFiller.style.width = scrollbarWidth(d.measure) + "px";
} else d.scrollbarFiller.style.display = "";
if (needsH && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) {
d.gutterFiller.style.display = "block";
d.gutterFiller.style.height = scrollbarWidth(d.measure) + "px";
d.gutterFiller.style.width = d.gutters.offsetWidth + "px";
} else d.gutterFiller.style.display = "";
if (mac_geLion && scrollbarWidth(d.measure) === 0) {
d.scrollbarV.style.minWidth = d.scrollbarH.style.minHeight = mac_geMountainLion ? "18px" : "12px";
d.scrollbarV.style.pointerEvents = d.scrollbarH.style.pointerEvents = "none";
}
}
function visibleLines(display, doc, viewPort) {
var top = display.scroller.scrollTop, height = display.wrapper.clientHeight;
if (typeof viewPort == "number") top = viewPort;
else if (viewPort) {top = viewPort.top; height = viewPort.bottom - viewPort.top;}
top = Math.floor(top - paddingTop(display));
var bottom = Math.ceil(top + height);
return {from: lineAtHeight(doc, top), to: lineAtHeight(doc, bottom)};
}
// LINE NUMBERS
function alignHorizontally(cm) {
var display = cm.display;
if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) return;
var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft;
var gutterW = display.gutters.offsetWidth, l = comp + "px";
for (var n = display.lineDiv.firstChild; n; n = n.nextSibling) if (n.alignable) {
for (var i = 0, a = n.alignable; i < a.length; ++i) a[i].style.left = l;
}
if (cm.options.fixedGutter)
display.gutters.style.left = (comp + gutterW) + "px";
}
function maybeUpdateLineNumberWidth(cm) {
if (!cm.options.lineNumbers) return false;
var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display;
if (last.length != display.lineNumChars) {
var test = display.measure.appendChild(elt("div", [elt("div", last)],
"CodeMirror-linenumber CodeMirror-gutter-elt"));
var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW;
display.lineGutter.style.width = "";
display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding);
display.lineNumWidth = display.lineNumInnerWidth + padding;
display.lineNumChars = display.lineNumInnerWidth ? last.length : -1;
display.lineGutter.style.width = display.lineNumWidth + "px";
return true;
}
return false;
}
function lineNumberFor(options, i) {
return String(options.lineNumberFormatter(i + options.firstLineNumber));
}
function compensateForHScroll(display) {
return getRect(display.scroller).left - getRect(display.sizer).left;
}
// DISPLAY DRAWING
function updateDisplay(cm, changes, viewPort, forced) {
var oldFrom = cm.display.showingFrom, oldTo = cm.display.showingTo, updated;
var visible = visibleLines(cm.display, cm.doc, viewPort);
for (var first = true;; first = false) {
var oldWidth = cm.display.scroller.clientWidth;
if (!updateDisplayInner(cm, changes, visible, forced)) break;
updated = true;
changes = [];
updateSelection(cm);
updateScrollbars(cm);
if (first && cm.options.lineWrapping && oldWidth != cm.display.scroller.clientWidth) {
forced = true;
continue;
}
forced = false;
// Clip forced viewport to actual scrollable area
if (viewPort)
viewPort = Math.min(cm.display.scroller.scrollHeight - cm.display.scroller.clientHeight,
typeof viewPort == "number" ? viewPort : viewPort.top);
visible = visibleLines(cm.display, cm.doc, viewPort);
if (visible.from >= cm.display.showingFrom && visible.to <= cm.display.showingTo)
break;
}
if (updated) {
signalLater(cm, "update", cm);
if (cm.display.showingFrom != oldFrom || cm.display.showingTo != oldTo)
signalLater(cm, "viewportChange", cm, cm.display.showingFrom, cm.display.showingTo);
}
return updated;
}
// Uses a set of changes plus the current scroll position to
// determine which DOM updates have to be made, and makes the
// updates.
function updateDisplayInner(cm, changes, visible, forced) {
var display = cm.display, doc = cm.doc;
if (!display.wrapper.offsetWidth) {
display.showingFrom = display.showingTo = doc.first;
display.viewOffset = 0;
return;
}
// Bail out if the visible area is already rendered and nothing changed.
if (!forced && changes.length == 0 &&
visible.from > display.showingFrom && visible.to < display.showingTo)
return;
if (maybeUpdateLineNumberWidth(cm))
changes = [{from: doc.first, to: doc.first + doc.size}];
var gutterW = display.sizer.style.marginLeft = display.gutters.offsetWidth + "px";
display.scrollbarH.style.left = cm.options.fixedGutter ? gutterW : "0";
// Used to determine which lines need their line numbers updated
var positionsChangedFrom = Infinity;
if (cm.options.lineNumbers)
for (var i = 0; i < changes.length; ++i)
if (changes[i].diff && changes[i].from < positionsChangedFrom) { positionsChangedFrom = changes[i].from; }
var end = doc.first + doc.size;
var from = Math.max(visible.from - cm.options.viewportMargin, doc.first);
var to = Math.min(end, visible.to + cm.options.viewportMargin);
if (display.showingFrom < from && from - display.showingFrom < 20) from = Math.max(doc.first, display.showingFrom);
if (display.showingTo > to && display.showingTo - to < 20) to = Math.min(end, display.showingTo);
if (sawCollapsedSpans) {
from = lineNo(visualLine(doc, getLine(doc, from)));
while (to < end && lineIsHidden(doc, getLine(doc, to))) ++to;
}
// Create a range of theoretically intact lines, and punch holes
// in that using the change info.
var intact = [{from: Math.max(display.showingFrom, doc.first),
to: Math.min(display.showingTo, end)}];
if (intact[0].from >= intact[0].to) intact = [];
else intact = computeIntact(intact, changes);
// When merged lines are present, we might have to reduce the
// intact ranges because changes in continued fragments of the
// intact lines do require the lines to be redrawn.
if (sawCollapsedSpans)
for (var i = 0; i < intact.length; ++i) {
var range = intact[i], merged;
while (merged = collapsedSpanAtEnd(getLine(doc, range.to - 1))) {
var newTo = merged.find().from.line;
if (newTo > range.from) range.to = newTo;
else { intact.splice(i--, 1); break; }
}
}
// Clip off the parts that won't be visible
var intactLines = 0;
for (var i = 0; i < intact.length; ++i) {
var range = intact[i];
if (range.from < from) range.from = from;
if (range.to > to) range.to = to;
if (range.from >= range.to) intact.splice(i--, 1);
else intactLines += range.to - range.from;
}
if (!forced && intactLines == to - from && from == display.showingFrom && to == display.showingTo) {
updateViewOffset(cm);
return;
}
intact.sort(function(a, b) {return a.from - b.from;});
// Avoid crashing on IE's "unspecified error" when in iframes
try {
var focused = document.activeElement;
} catch(e) {}
if (intactLines < (to - from) * .7) display.lineDiv.style.display = "none";
patchDisplay(cm, from, to, intact, positionsChangedFrom);
display.lineDiv.style.display = "";
if (focused && document.activeElement != focused && focused.offsetHeight) focused.focus();
var different = from != display.showingFrom || to != display.showingTo ||
display.lastSizeC != display.wrapper.clientHeight;
// This is just a bogus formula that detects when the editor is
// resized or the font size changes.
if (different) {
display.lastSizeC = display.wrapper.clientHeight;
startWorker(cm, 400);
}
display.showingFrom = from; display.showingTo = to;
display.gutters.style.height = "";
updateHeightsInViewport(cm);
updateViewOffset(cm);
return true;
}
function updateHeightsInViewport(cm) {
var display = cm.display;
var prevBottom = display.lineDiv.offsetTop;
for (var node = display.lineDiv.firstChild, height; node; node = node.nextSibling) if (node.lineObj) {
if (ie_lt8) {
var bot = node.offsetTop + node.offsetHeight;
height = bot - prevBottom;
prevBottom = bot;
} else {
var box = getRect(node);
height = box.bottom - box.top;
}
var diff = node.lineObj.height - height;
if (height < 2) height = textHeight(display);
if (diff > .001 || diff < -.001) {
updateLineHeight(node.lineObj, height);
var widgets = node.lineObj.widgets;
if (widgets) for (var i = 0; i < widgets.length; ++i)
widgets[i].height = widgets[i].node.offsetHeight;
}
}
}
function updateViewOffset(cm) {
var off = cm.display.viewOffset = heightAtLine(cm, getLine(cm.doc, cm.display.showingFrom));
// Position the mover div to align with the current virtual scroll position
cm.display.mover.style.top = off + "px";
}
function computeIntact(intact, changes) {
for (var i = 0, l = changes.length || 0; i < l; ++i) {
var change = changes[i], intact2 = [], diff = change.diff || 0;
for (var j = 0, l2 = intact.length; j < l2; ++j) {
var range = intact[j];
if (change.to <= range.from && change.diff) {
intact2.push({from: range.from + diff, to: range.to + diff});
} else if (change.to <= range.from || change.from >= range.to) {
intact2.push(range);
} else {
if (change.from > range.from)
intact2.push({from: range.from, to: change.from});
if (change.to < range.to)
intact2.push({from: change.to + diff, to: range.to + diff});
}
}
intact = intact2;
}
return intact;
}
function getDimensions(cm) {
var d = cm.display, left = {}, width = {};
for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) {
left[cm.options.gutters[i]] = n.offsetLeft;
width[cm.options.gutters[i]] = n.offsetWidth;
}
return {fixedPos: compensateForHScroll(d),
gutterTotalWidth: d.gutters.offsetWidth,
gutterLeft: left,
gutterWidth: width,
wrapperWidth: d.wrapper.clientWidth};
}
function patchDisplay(cm, from, to, intact, updateNumbersFrom) {
var dims = getDimensions(cm);
var display = cm.display, lineNumbers = cm.options.lineNumbers;
if (!intact.length && (!webkit || !cm.display.currentWheelTarget))
removeChildren(display.lineDiv);
var container = display.lineDiv, cur = container.firstChild;
function rm(node) {
var next = node.nextSibling;
if (webkit && mac && cm.display.currentWheelTarget == node) {
node.style.display = "none";
node.lineObj = null;
} else {
node.parentNode.removeChild(node);
}
return next;
}
var nextIntact = intact.shift(), lineN = from;
cm.doc.iter(from, to, function(line) {
if (nextIntact && nextIntact.to == lineN) nextIntact = intact.shift();
if (lineIsHidden(cm.doc, line)) {
if (line.height != 0) updateLineHeight(line, 0);
if (line.widgets && cur && cur.previousSibling) for (var i = 0; i < line.widgets.length; ++i) {
var w = line.widgets[i];
if (w.showIfHidden) {
var prev = cur.previousSibling;
if (/pre/i.test(prev.nodeName)) {
var wrap = elt("div", null, null, "position: relative");
prev.parentNode.replaceChild(wrap, prev);
wrap.appendChild(prev);
prev = wrap;
}
var wnode = prev.appendChild(elt("div", [w.node], "CodeMirror-linewidget"));
if (!w.handleMouseEvents) wnode.ignoreEvents = true;
positionLineWidget(w, wnode, prev, dims);
}
}
} else if (nextIntact && nextIntact.from <= lineN && nextIntact.to > lineN) {
// This line is intact. Skip to the actual node. Update its
// line number if needed.
while (cur.lineObj != line) cur = rm(cur);
if (lineNumbers && updateNumbersFrom <= lineN && cur.lineNumber)
setTextContent(cur.lineNumber, lineNumberFor(cm.options, lineN));
cur = cur.nextSibling;
} else {
// For lines with widgets, make an attempt to find and reuse
// the existing element, so that widgets aren't needlessly
// removed and re-inserted into the dom
if (line.widgets) for (var j = 0, search = cur, reuse; search && j < 20; ++j, search = search.nextSibling)
if (search.lineObj == line && /div/i.test(search.nodeName)) { reuse = search; break; }
// This line needs to be generated.
var lineNode = buildLineElement(cm, line, lineN, dims, reuse);
if (lineNode != reuse) {
container.insertBefore(lineNode, cur);
} else {
while (cur != reuse) cur = rm(cur);
cur = cur.nextSibling;
}
lineNode.lineObj = line;
}
++lineN;
});
while (cur) cur = rm(cur);
}
function buildLineElement(cm, line, lineNo, dims, reuse) {
var built = buildLineContent(cm, line), lineElement = built.pre;
var markers = line.gutterMarkers, display = cm.display, wrap;
var bgClass = built.bgClass ? built.bgClass + " " + (line.bgClass || "") : line.bgClass;
if (!cm.options.lineNumbers && !markers && !bgClass && !line.wrapClass && !line.widgets)
return lineElement;
// Lines with gutter elements, widgets or a background class need
// to be wrapped again, and have the extra elements added to the
// wrapper div
if (reuse) {
reuse.alignable = null;
var isOk = true, widgetsSeen = 0, insertBefore = null;
for (var n = reuse.firstChild, next; n; n = next) {
next = n.nextSibling;
if (!/\bCodeMirror-linewidget\b/.test(n.className)) {
reuse.removeChild(n);
} else {
for (var i = 0; i < line.widgets.length; ++i) {
var widget = line.widgets[i];
if (widget.node == n.firstChild) {
if (!widget.above && !insertBefore) insertBefore = n;
positionLineWidget(widget, n, reuse, dims);
++widgetsSeen;
break;
}
}
if (i == line.widgets.length) { isOk = false; break; }
}
}
reuse.insertBefore(lineElement, insertBefore);
if (isOk && widgetsSeen == line.widgets.length) {
wrap = reuse;
reuse.className = line.wrapClass || "";
}
}
if (!wrap) {
wrap = elt("div", null, line.wrapClass, "position: relative");
wrap.appendChild(lineElement);
}
// Kludge to make sure the styled element lies behind the selection (by z-index)
if (bgClass)
wrap.insertBefore(elt("div", null, bgClass + " CodeMirror-linebackground"), wrap.firstChild);
if (cm.options.lineNumbers || markers) {
var gutterWrap = wrap.insertBefore(elt("div", null, "CodeMirror-gutter-wrapper", "position: absolute; left: " +
(cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px"),
lineElement);
if (cm.options.fixedGutter) (wrap.alignable || (wrap.alignable = [])).push(gutterWrap);
if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"]))
wrap.lineNumber = gutterWrap.appendChild(
elt("div", lineNumberFor(cm.options, lineNo),
"CodeMirror-linenumber CodeMirror-gutter-elt",
"left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: "
+ display.lineNumInnerWidth + "px"));
if (markers)
for (var k = 0; k < cm.options.gutters.length; ++k) {
var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id];
if (found)
gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", "left: " +
dims.gutterLeft[id] + "px; width: " + dims.gutterWidth[id] + "px"));
}
}
if (ie_lt8) wrap.style.zIndex = 2;
if (line.widgets && wrap != reuse) for (var i = 0, ws = line.widgets; i < ws.length; ++i) {
var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget");
if (!widget.handleMouseEvents) node.ignoreEvents = true;
positionLineWidget(widget, node, wrap, dims);
if (widget.above)
wrap.insertBefore(node, cm.options.lineNumbers && line.height != 0 ? gutterWrap : lineElement);
else
wrap.appendChild(node);
signalLater(widget, "redraw");
}
return wrap;
}
function positionLineWidget(widget, node, wrap, dims) {
if (widget.noHScroll) {
(wrap.alignable || (wrap.alignable = [])).push(node);
var width = dims.wrapperWidth;
node.style.left = dims.fixedPos + "px";
if (!widget.coverGutter) {
width -= dims.gutterTotalWidth;
node.style.paddingLeft = dims.gutterTotalWidth + "px";
}
node.style.width = width + "px";
}
if (widget.coverGutter) {
node.style.zIndex = 5;
node.style.position = "relative";
if (!widget.noHScroll) node.style.marginLeft = -dims.gutterTotalWidth + "px";
}
}
// SELECTION / CURSOR
function updateSelection(cm) {
var display = cm.display;
var collapsed = posEq(cm.doc.sel.from, cm.doc.sel.to);
if (collapsed || cm.options.showCursorWhenSelecting)
updateSelectionCursor(cm);
else
display.cursor.style.display = display.otherCursor.style.display = "none";
if (!collapsed)
updateSelectionRange(cm);
else
display.selectionDiv.style.display = "none";
// Move the hidden textarea near the cursor to prevent scrolling artifacts
if (cm.options.moveInputWithCursor) {
var headPos = cursorCoords(cm, cm.doc.sel.head, "div");
var wrapOff = getRect(display.wrapper), lineOff = getRect(display.lineDiv);
display.inputDiv.style.top = Math.max(0, Math.min(display.wrapper.clientHeight - 10,
headPos.top + lineOff.top - wrapOff.top)) + "px";
display.inputDiv.style.left = Math.max(0, Math.min(display.wrapper.clientWidth - 10,
headPos.left + lineOff.left - wrapOff.left)) + "px";
}
}
// No selection, plain cursor
function updateSelectionCursor(cm) {
var display = cm.display, pos = cursorCoords(cm, cm.doc.sel.head, "div");
display.cursor.style.left = pos.left + "px";
display.cursor.style.top = pos.top + "px";
display.cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px";
display.cursor.style.display = "";
if (pos.other) {
display.otherCursor.style.display = "";
display.otherCursor.style.left = pos.other.left + "px";
display.otherCursor.style.top = pos.other.top + "px";
display.otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px";
} else { display.otherCursor.style.display = "none"; }
}
// Highlight selection
function updateSelectionRange(cm) {
var display = cm.display, doc = cm.doc, sel = cm.doc.sel;
var fragment = document.createDocumentFragment();
var clientWidth = display.lineSpace.offsetWidth, pl = paddingLeft(cm.display);
function add(left, top, width, bottom) {
if (top < 0) top = 0;
fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left +
"px; top: " + top + "px; width: " + (width == null ? clientWidth - left : width) +
"px; height: " + (bottom - top) + "px"));
}
function drawForLine(line, fromArg, toArg) {
var lineObj = getLine(doc, line);
var lineLen = lineObj.text.length;
var start, end;
function coords(ch, bias) {
return charCoords(cm, Pos(line, ch), "div", lineObj, bias);
}
iterateBidiSections(getOrder(lineObj), fromArg || 0, toArg == null ? lineLen : toArg, function(from, to, dir) {
var leftPos = coords(from, "left"), rightPos, left, right;
if (from == to) {
rightPos = leftPos;
left = right = leftPos.left;
} else {
rightPos = coords(to - 1, "right");
if (dir == "rtl") { var tmp = leftPos; leftPos = rightPos; rightPos = tmp; }
left = leftPos.left;
right = rightPos.right;
}
if (fromArg == null && from == 0) left = pl;
if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part
add(left, leftPos.top, null, leftPos.bottom);
left = pl;
if (leftPos.bottom < rightPos.top) add(left, leftPos.bottom, null, rightPos.top);
}
if (toArg == null && to == lineLen) right = clientWidth;
if (!start || leftPos.top < start.top || leftPos.top == start.top && leftPos.left < start.left)
start = leftPos;
if (!end || rightPos.bottom > end.bottom || rightPos.bottom == end.bottom && rightPos.right > end.right)
end = rightPos;
if (left < pl + 1) left = pl;
add(left, rightPos.top, right - left, rightPos.bottom);
});
return {start: start, end: end};
}
if (sel.from.line == sel.to.line) {
drawForLine(sel.from.line, sel.from.ch, sel.to.ch);
} else {
var fromLine = getLine(doc, sel.from.line), toLine = getLine(doc, sel.to.line);
var singleVLine = visualLine(doc, fromLine) == visualLine(doc, toLine);
var leftEnd = drawForLine(sel.from.line, sel.from.ch, singleVLine ? fromLine.text.length : null).end;
var rightStart = drawForLine(sel.to.line, singleVLine ? 0 : null, sel.to.ch).start;
if (singleVLine) {
if (leftEnd.top < rightStart.top - 2) {
add(leftEnd.right, leftEnd.top, null, leftEnd.bottom);
add(pl, rightStart.top, rightStart.left, rightStart.bottom);
} else {
add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom);
}
}
if (leftEnd.bottom < rightStart.top)
add(pl, leftEnd.bottom, null, rightStart.top);
}
removeChildrenAndAdd(display.selectionDiv, fragment);
display.selectionDiv.style.display = "";
}
// Cursor-blinking
function restartBlink(cm) {
if (!cm.state.focused) return;
var display = cm.display;
clearInterval(display.blinker);
var on = true;
display.cursor.style.visibility = display.otherCursor.style.visibility = "";
if (cm.options.cursorBlinkRate > 0)
display.blinker = setInterval(function() {
display.cursor.style.visibility = display.otherCursor.style.visibility = (on = !on) ? "" : "hidden";
}, cm.options.cursorBlinkRate);
}
// HIGHLIGHT WORKER
function startWorker(cm, time) {
if (cm.doc.mode.startState && cm.doc.frontier < cm.display.showingTo)
cm.state.highlight.set(time, bind(highlightWorker, cm));
}
function highlightWorker(cm) {
var doc = cm.doc;
if (doc.frontier < doc.first) doc.frontier = doc.first;
if (doc.frontier >= cm.display.showingTo) return;
var end = +new Date + cm.options.workTime;
var state = copyState(doc.mode, getStateBefore(cm, doc.frontier));
var changed = [], prevChange;
doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.showingTo + 500), function(line) {
if (doc.frontier >= cm.display.showingFrom) { // Visible
var oldStyles = line.styles;
line.styles = highlightLine(cm, line, state, true);
var ischange = !oldStyles || oldStyles.length != line.styles.length;
for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i];
if (ischange) {
if (prevChange && prevChange.end == doc.frontier) prevChange.end++;
else changed.push(prevChange = {start: doc.frontier, end: doc.frontier + 1});
}
line.stateAfter = copyState(doc.mode, state);
} else {
processLine(cm, line.text, state);
line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null;
}
++doc.frontier;
if (+new Date > end) {
startWorker(cm, cm.options.workDelay);
return true;
}
});
if (changed.length)
operation(cm, function() {
for (var i = 0; i < changed.length; ++i)
regChange(this, changed[i].start, changed[i].end);
})();
}
// Finds the line to start with when starting a parse. Tries to
// find a line with a stateAfter, so that it can start with a
// valid state. If that fails, it returns the line with the
// smallest indentation, which tends to need the least context to
// parse correctly.
function findStartLine(cm, n, precise) {
var minindent, minline, doc = cm.doc;
var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100);
for (var search = n; search > lim; --search) {
if (search <= doc.first) return doc.first;
var line = getLine(doc, search - 1);
if (line.stateAfter && (!precise || search <= doc.frontier)) return search;
var indented = countColumn(line.text, null, cm.options.tabSize);
if (minline == null || minindent > indented) {
minline = search - 1;
minindent = indented;
}
}
return minline;
}
function getStateBefore(cm, n, precise) {
var doc = cm.doc, display = cm.display;
if (!doc.mode.startState) return true;
var pos = findStartLine(cm, n, precise), state = pos > doc.first && getLine(doc, pos-1).stateAfter;
if (!state) state = startState(doc.mode);
else state = copyState(doc.mode, state);
doc.iter(pos, n, function(line) {
processLine(cm, line.text, state);
var save = pos == n - 1 || pos % 5 == 0 || pos >= display.showingFrom && pos < display.showingTo;
line.stateAfter = save ? copyState(doc.mode, state) : null;
++pos;
});
if (precise) doc.frontier = pos;
return state;
}
// POSITION MEASUREMENT
function paddingTop(display) {return display.lineSpace.offsetTop;}
function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight;}
function paddingLeft(display) {
var e = removeChildrenAndAdd(display.measure, elt("pre", null, null, "text-align: left")).appendChild(elt("span", "x"));
return e.offsetLeft;
}
function measureChar(cm, line, ch, data, bias) {
var dir = -1;
data = data || measureLine(cm, line);
if (data.crude) {
var left = data.left + ch * data.width;
return {left: left, right: left + data.width, top: data.top, bottom: data.bottom};
}
for (var pos = ch;; pos += dir) {
var r = data[pos];
if (r) break;
if (dir < 0 && pos == 0) dir = 1;
}
bias = pos > ch ? "left" : pos < ch ? "right" : bias;
if (bias == "left" && r.leftSide) r = r.leftSide;
else if (bias == "right" && r.rightSide) r = r.rightSide;
return {left: pos < ch ? r.right : r.left,
right: pos > ch ? r.left : r.right,
top: r.top,
bottom: r.bottom};
}
function findCachedMeasurement(cm, line) {
var cache = cm.display.measureLineCache;
for (var i = 0; i < cache.length; ++i) {
var memo = cache[i];
if (memo.text == line.text && memo.markedSpans == line.markedSpans &&
cm.display.scroller.clientWidth == memo.width &&
memo.classes == line.textClass + "|" + line.wrapClass)
return memo;
}
}
function clearCachedMeasurement(cm, line) {
var exists = findCachedMeasurement(cm, line);
if (exists) exists.text = exists.measure = exists.markedSpans = null;
}
function measureLine(cm, line) {
// First look in the cache
var cached = findCachedMeasurement(cm, line);
if (cached) return cached.measure;
// Failing that, recompute and store result in cache
var measure = measureLineInner(cm, line);
var cache = cm.display.measureLineCache;
var memo = {text: line.text, width: cm.display.scroller.clientWidth,
markedSpans: line.markedSpans, measure: measure,
classes: line.textClass + "|" + line.wrapClass};
if (cache.length == 16) cache[++cm.display.measureLineCachePos % 16] = memo;
else cache.push(memo);
return measure;
}
function measureLineInner(cm, line) {
if (!cm.options.lineWrapping && line.text.length >= cm.options.crudeMeasuringFrom)
return crudelyMeasureLine(cm, line);
var display = cm.display, measure = emptyArray(line.text.length);
var pre = buildLineContent(cm, line, measure, true).pre;
// IE does not cache element positions of inline elements between
// calls to getBoundingClientRect. This makes the loop below,
// which gathers the positions of all the characters on the line,
// do an amount of layout work quadratic to the number of
// characters. When line wrapping is off, we try to improve things
// by first subdividing the line into a bunch of inline blocks, so
// that IE can reuse most of the layout information from caches
// for those blocks. This does interfere with line wrapping, so it
// doesn't work when wrapping is on, but in that case the
// situation is slightly better, since IE does cache line-wrapping
// information and only recomputes per-line.
if (old_ie && !ie_lt8 && !cm.options.lineWrapping && pre.childNodes.length > 100) {
var fragment = document.createDocumentFragment();
var chunk = 10, n = pre.childNodes.length;
for (var i = 0, chunks = Math.ceil(n / chunk); i < chunks; ++i) {
var wrap = elt("div", null, null, "display: inline-block");
for (var j = 0; j < chunk && n; ++j) {
wrap.appendChild(pre.firstChild);
--n;
}
fragment.appendChild(wrap);
}
pre.appendChild(fragment);
}
removeChildrenAndAdd(display.measure, pre);
var outer = getRect(display.lineDiv);
var vranges = [], data = emptyArray(line.text.length), maxBot = pre.offsetHeight;
// Work around an IE7/8 bug where it will sometimes have randomly
// replaced our pre with a clone at this point.
if (ie_lt9 && display.measure.first != pre)
removeChildrenAndAdd(display.measure, pre);
function measureRect(rect) {
var top = rect.top - outer.top, bot = rect.bottom - outer.top;
if (bot > maxBot) bot = maxBot;
if (top < 0) top = 0;
for (var i = vranges.length - 2; i >= 0; i -= 2) {
var rtop = vranges[i], rbot = vranges[i+1];
if (rtop > bot || rbot < top) continue;
if (rtop <= top && rbot >= bot ||
top <= rtop && bot >= rbot ||
Math.min(bot, rbot) - Math.max(top, rtop) >= (bot - top) >> 1) {
vranges[i] = Math.min(top, rtop);
vranges[i+1] = Math.max(bot, rbot);
break;
}
}
if (i < 0) { i = vranges.length; vranges.push(top, bot); }
return {left: rect.left - outer.left,
right: rect.right - outer.left,
top: i, bottom: null};
}
function finishRect(rect) {
rect.bottom = vranges[rect.top+1];
rect.top = vranges[rect.top];
}
for (var i = 0, cur; i < measure.length; ++i) if (cur = measure[i]) {
var node = cur, rect = null;
// A widget might wrap, needs special care
if (/\bCodeMirror-widget\b/.test(cur.className) && cur.getClientRects) {
if (cur.firstChild.nodeType == 1) node = cur.firstChild;
var rects = node.getClientRects();
if (rects.length > 1) {
rect = data[i] = measureRect(rects[0]);
rect.rightSide = measureRect(rects[rects.length - 1]);
}
}
if (!rect) rect = data[i] = measureRect(getRect(node));
if (cur.measureRight) rect.right = getRect(cur.measureRight).left - outer.left;
if (cur.leftSide) rect.leftSide = measureRect(getRect(cur.leftSide));
}
removeChildren(cm.display.measure);
for (var i = 0, cur; i < data.length; ++i) if (cur = data[i]) {
finishRect(cur);
if (cur.leftSide) finishRect(cur.leftSide);
if (cur.rightSide) finishRect(cur.rightSide);
}
return data;
}
function crudelyMeasureLine(cm, line) {
var copy = new Line(line.text.slice(0, 100), null);
if (line.textClass) copy.textClass = line.textClass;
var measure = measureLineInner(cm, copy);
var left = measureChar(cm, copy, 0, measure, "left");
var right = measureChar(cm, copy, 99, measure, "right");
return {crude: true, top: left.top, left: left.left, bottom: left.bottom, width: (right.right - left.left) / 100};
}
function measureLineWidth(cm, line) {
var hasBadSpan = false;
if (line.markedSpans) for (var i = 0; i < line.markedSpans; ++i) {
var sp = line.markedSpans[i];
if (sp.collapsed && (sp.to == null || sp.to == line.text.length)) hasBadSpan = true;
}
var cached = !hasBadSpan && findCachedMeasurement(cm, line);
if (cached || line.text.length >= cm.options.crudeMeasuringFrom)
return measureChar(cm, line, line.text.length, cached && cached.measure, "right").right;
var pre = buildLineContent(cm, line, null, true).pre;
var end = pre.appendChild(zeroWidthElement(cm.display.measure));
removeChildrenAndAdd(cm.display.measure, pre);
return getRect(end).right - getRect(cm.display.lineDiv).left;
}
function clearCaches(cm) {
cm.display.measureLineCache.length = cm.display.measureLineCachePos = 0;
cm.display.cachedCharWidth = cm.display.cachedTextHeight = null;
if (!cm.options.lineWrapping) cm.display.maxLineChanged = true;
cm.display.lineNumChars = null;
}
function pageScrollX() { return window.pageXOffset || (document.documentElement || document.body).scrollLeft; }
function pageScrollY() { return window.pageYOffset || (document.documentElement || document.body).scrollTop; }
// Context is one of "line", "div" (display.lineDiv), "local"/null (editor), or "page"
function intoCoordSystem(cm, lineObj, rect, context) {
if (lineObj.widgets) for (var i = 0; i < lineObj.widgets.length; ++i) if (lineObj.widgets[i].above) {
var size = widgetHeight(lineObj.widgets[i]);
rect.top += size; rect.bottom += size;
}
if (context == "line") return rect;
if (!context) context = "local";
var yOff = heightAtLine(cm, lineObj);
if (context == "local") yOff += paddingTop(cm.display);
else yOff -= cm.display.viewOffset;
if (context == "page" || context == "window") {
var lOff = getRect(cm.display.lineSpace);
yOff += lOff.top + (context == "window" ? 0 : pageScrollY());
var xOff = lOff.left + (context == "window" ? 0 : pageScrollX());
rect.left += xOff; rect.right += xOff;
}
rect.top += yOff; rect.bottom += yOff;
return rect;
}
// Context may be "window", "page", "div", or "local"/null
// Result is in "div" coords
function fromCoordSystem(cm, coords, context) {
if (context == "div") return coords;
var left = coords.left, top = coords.top;
// First move into "page" coordinate system
if (context == "page") {
left -= pageScrollX();
top -= pageScrollY();
} else if (context == "local" || !context) {
var localBox = getRect(cm.display.sizer);
left += localBox.left;
top += localBox.top;
}
var lineSpaceBox = getRect(cm.display.lineSpace);
return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top};
}
function charCoords(cm, pos, context, lineObj, bias) {
if (!lineObj) lineObj = getLine(cm.doc, pos.line);
return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, null, bias), context);
}
function cursorCoords(cm, pos, context, lineObj, measurement) {
lineObj = lineObj || getLine(cm.doc, pos.line);
if (!measurement) measurement = measureLine(cm, lineObj);
function get(ch, right) {
var m = measureChar(cm, lineObj, ch, measurement, right ? "right" : "left");
if (right) m.left = m.right; else m.right = m.left;
return intoCoordSystem(cm, lineObj, m, context);
}
function getBidi(ch, partPos) {
var part = order[partPos], right = part.level % 2;
if (ch == bidiLeft(part) && partPos && part.level < order[partPos - 1].level) {
part = order[--partPos];
ch = bidiRight(part) - (part.level % 2 ? 0 : 1);
right = true;
} else if (ch == bidiRight(part) && partPos < order.length - 1 && part.level < order[partPos + 1].level) {
part = order[++partPos];
ch = bidiLeft(part) - part.level % 2;
right = false;
}
if (right && ch == part.to && ch > part.from) return get(ch - 1);
return get(ch, right);
}
var order = getOrder(lineObj), ch = pos.ch;
if (!order) return get(ch);
var partPos = getBidiPartAt(order, ch);
var val = getBidi(ch, partPos);
if (bidiOther != null) val.other = getBidi(ch, bidiOther);
return val;
}
function PosWithInfo(line, ch, outside, xRel) {
var pos = new Pos(line, ch);
pos.xRel = xRel;
if (outside) pos.outside = true;
return pos;
}
// Coords must be lineSpace-local
function coordsChar(cm, x, y) {
var doc = cm.doc;
y += cm.display.viewOffset;
if (y < 0) return PosWithInfo(doc.first, 0, true, -1);
var lineNo = lineAtHeight(doc, y), last = doc.first + doc.size - 1;
if (lineNo > last)
return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, true, 1);
if (x < 0) x = 0;
for (;;) {
var lineObj = getLine(doc, lineNo);
var found = coordsCharInner(cm, lineObj, lineNo, x, y);
var merged = collapsedSpanAtEnd(lineObj);
var mergedPos = merged && merged.find();
if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0))
lineNo = mergedPos.to.line;
else
return found;
}
}
function coordsCharInner(cm, lineObj, lineNo, x, y) {
var innerOff = y - heightAtLine(cm, lineObj);
var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth;
var measurement = measureLine(cm, lineObj);
function getX(ch) {
var sp = cursorCoords(cm, Pos(lineNo, ch), "line",
lineObj, measurement);
wrongLine = true;
if (innerOff > sp.bottom) return sp.left - adjust;
else if (innerOff < sp.top) return sp.left + adjust;
else wrongLine = false;
return sp.left;
}
var bidi = getOrder(lineObj), dist = lineObj.text.length;
var from = lineLeft(lineObj), to = lineRight(lineObj);
var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine;
if (x > toX) return PosWithInfo(lineNo, to, toOutside, 1);
// Do a binary search between these bounds.
for (;;) {
if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) {
var ch = x < fromX || x - fromX <= toX - x ? from : to;
var xDiff = x - (ch == from ? fromX : toX);
while (isExtendingChar(lineObj.text.charAt(ch))) ++ch;
var pos = PosWithInfo(lineNo, ch, ch == from ? fromOutside : toOutside,
xDiff < 0 ? -1 : xDiff ? 1 : 0);
return pos;
}
var step = Math.ceil(dist / 2), middle = from + step;
if (bidi) {
middle = from;
for (var i = 0; i < step; ++i) middle = moveVisually(lineObj, middle, 1);
}
var middleX = getX(middle);
if (middleX > x) {to = middle; toX = middleX; if (toOutside = wrongLine) toX += 1000; dist = step;}
else {from = middle; fromX = middleX; fromOutside = wrongLine; dist -= step;}
}
}
var measureText;
function textHeight(display) {
if (display.cachedTextHeight != null) return display.cachedTextHeight;
if (measureText == null) {
measureText = elt("pre");
// Measure a bunch of lines, for browsers that compute
// fractional heights.
for (var i = 0; i < 49; ++i) {
measureText.appendChild(document.createTextNode("x"));
measureText.appendChild(elt("br"));
}
measureText.appendChild(document.createTextNode("x"));
}
removeChildrenAndAdd(display.measure, measureText);
var height = measureText.offsetHeight / 50;
if (height > 3) display.cachedTextHeight = height;
removeChildren(display.measure);
return height || 1;
}
function charWidth(display) {
if (display.cachedCharWidth != null) return display.cachedCharWidth;
var anchor = elt("span", "x");
var pre = elt("pre", [anchor]);
removeChildrenAndAdd(display.measure, pre);
var width = anchor.offsetWidth;
if (width > 2) display.cachedCharWidth = width;
return width || 10;
}
// OPERATIONS
// Operations are used to wrap changes in such a way that each
// change won't have to update the cursor and display (which would
// be awkward, slow, and error-prone), but instead updates are
// batched and then all combined and executed at once.
var nextOpId = 0;
function startOperation(cm) {
cm.curOp = {
// An array of ranges of lines that have to be updated. See
// updateDisplay.
changes: [],
forceUpdate: false,
updateInput: null,
userSelChange: null,
textChanged: null,
selectionChanged: false,
cursorActivity: false,
updateMaxLine: false,
updateScrollPos: false,
id: ++nextOpId
};
if (!delayedCallbackDepth++) delayedCallbacks = [];
}
function endOperation(cm) {
var op = cm.curOp, doc = cm.doc, display = cm.display;
cm.curOp = null;
if (op.updateMaxLine) computeMaxLength(cm);
if (display.maxLineChanged && !cm.options.lineWrapping && display.maxLine) {
var width = measureLineWidth(cm, display.maxLine);
display.sizer.style.minWidth = Math.max(0, width + 3 + scrollerCutOff) + "px";
display.maxLineChanged = false;
var maxScrollLeft = Math.max(0, display.sizer.offsetLeft + display.sizer.offsetWidth - display.scroller.clientWidth);
if (maxScrollLeft < doc.scrollLeft && !op.updateScrollPos)
setScrollLeft(cm, Math.min(display.scroller.scrollLeft, maxScrollLeft), true);
}
var newScrollPos, updated;
if (op.updateScrollPos) {
newScrollPos = op.updateScrollPos;
} else if (op.selectionChanged && display.scroller.clientHeight) { // don't rescroll if not visible
var coords = cursorCoords(cm, doc.sel.head);
newScrollPos = calculateScrollPos(cm, coords.left, coords.top, coords.left, coords.bottom);
}
if (op.changes.length || op.forceUpdate || newScrollPos && newScrollPos.scrollTop != null) {
updated = updateDisplay(cm, op.changes, newScrollPos && newScrollPos.scrollTop, op.forceUpdate);
if (cm.display.scroller.offsetHeight) cm.doc.scrollTop = cm.display.scroller.scrollTop;
}
if (!updated && op.selectionChanged) updateSelection(cm);
if (op.updateScrollPos) {
var top = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, newScrollPos.scrollTop));
var left = Math.max(0, Math.min(display.scroller.scrollWidth - display.scroller.clientWidth, newScrollPos.scrollLeft));
display.scroller.scrollTop = display.scrollbarV.scrollTop = doc.scrollTop = top;
display.scroller.scrollLeft = display.scrollbarH.scrollLeft = doc.scrollLeft = left;
alignHorizontally(cm);
if (op.scrollToPos)
scrollPosIntoView(cm, clipPos(cm.doc, op.scrollToPos.from),
clipPos(cm.doc, op.scrollToPos.to), op.scrollToPos.margin);
} else if (newScrollPos) {
scrollCursorIntoView(cm);
}
if (op.selectionChanged) restartBlink(cm);
if (cm.state.focused && op.updateInput)
resetInput(cm, op.userSelChange);
var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers;
if (hidden) for (var i = 0; i < hidden.length; ++i)
if (!hidden[i].lines.length) signal(hidden[i], "hide");
if (unhidden) for (var i = 0; i < unhidden.length; ++i)
if (unhidden[i].lines.length) signal(unhidden[i], "unhide");
var delayed;
if (!--delayedCallbackDepth) {
delayed = delayedCallbacks;
delayedCallbacks = null;
}
if (op.textChanged)
signal(cm, "change", cm, op.textChanged);
if (op.cursorActivity) signal(cm, "cursorActivity", cm);
if (delayed) for (var i = 0; i < delayed.length; ++i) delayed[i]();
}
// Wraps a function in an operation. Returns the wrapped function.
function operation(cm1, f) {
return function() {
var cm = cm1 || this, withOp = !cm.curOp;
if (withOp) startOperation(cm);
try { var result = f.apply(cm, arguments); }
finally { if (withOp) endOperation(cm); }
return result;
};
}
function docOperation(f) {
return function() {
var withOp = this.cm && !this.cm.curOp, result;
if (withOp) startOperation(this.cm);
try { result = f.apply(this, arguments); }
finally { if (withOp) endOperation(this.cm); }
return result;
};
}
function runInOp(cm, f) {
var withOp = !cm.curOp, result;
if (withOp) startOperation(cm);
try { result = f(); }
finally { if (withOp) endOperation(cm); }
return result;
}
function regChange(cm, from, to, lendiff) {
if (from == null) from = cm.doc.first;
if (to == null) to = cm.doc.first + cm.doc.size;
cm.curOp.changes.push({from: from, to: to, diff: lendiff});
}
// INPUT HANDLING
function slowPoll(cm) {
if (cm.display.pollingFast) return;
cm.display.poll.set(cm.options.pollInterval, function() {
readInput(cm);
if (cm.state.focused) slowPoll(cm);
});
}
function fastPoll(cm) {
var missed = false;
cm.display.pollingFast = true;
function p() {
var changed = readInput(cm);
if (!changed && !missed) {missed = true; cm.display.poll.set(60, p);}
else {cm.display.pollingFast = false; slowPoll(cm);}
}
cm.display.poll.set(20, p);
}
// prevInput is a hack to work with IME. If we reset the textarea
// on every change, that breaks IME. So we look for changes
// compared to the previous content instead. (Modern browsers have
// events that indicate IME taking place, but these are not widely
// supported or compatible enough yet to rely on.)
function readInput(cm) {
var input = cm.display.input, prevInput = cm.display.prevInput, doc = cm.doc, sel = doc.sel;
if (!cm.state.focused || hasSelection(input) || isReadOnly(cm) || cm.options.disableInput || cm.state.accessibleTextareaWaiting) return false;
if (cm.state.pasteIncoming && cm.state.fakedLastChar) {
input.value = input.value.substring(0, input.value.length - 1);
cm.state.fakedLastChar = false;
}
var text = input.value;
if (text == prevInput && posEq(sel.from, sel.to)) return false;
if (ie && !ie_lt9 && cm.display.inputHasSelection === text) {
resetInput(cm, true);
return false;
}
var withOp = !cm.curOp;
if (withOp) startOperation(cm);
sel.shift = false;
var same = 0, l = Math.min(prevInput.length, text.length);
while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same;
var from = sel.from, to = sel.to;
var inserted = text.slice(same);
if (same < prevInput.length)
from = Pos(from.line, from.ch - (prevInput.length - same));
else if (cm.state.overwrite && posEq(from, to) && !cm.state.pasteIncoming)
to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + inserted.length));
var updateInput = cm.curOp.updateInput;
var changeEvent = {from: from, to: to, text: splitLines(inserted),
origin: cm.state.pasteIncoming ? "paste" : cm.state.cutIncoming ? "cut" : "+input"};
makeChange(cm.doc, changeEvent, "end");
cm.curOp.updateInput = updateInput;
signalLater(cm, "inputRead", cm, changeEvent);
if (inserted && !cm.state.pasteIncoming && cm.options.electricChars &&
cm.options.smartIndent && sel.head.ch < 100) {
var electric = cm.getModeAt(sel.head).electricChars;
if (electric) for (var i = 0; i < electric.length; i++)
if (inserted.indexOf(electric.charAt(i)) > -1) {
indentLine(cm, sel.head.line, "smart");
break;
}
}
if (text.length > 1000 || text.indexOf("\n") > -1) input.value = cm.display.prevInput = "";
else cm.display.prevInput = text;
if (withOp) endOperation(cm);
cm.state.pasteIncoming = cm.state.cutIncoming = false;
return true;
}
function resetInput(cm, user) {
var minimal, selected, doc = cm.doc;
if (!posEq(doc.sel.from, doc.sel.to)) {
cm.display.prevInput = "";
minimal = false && hasCopyEvent &&
(doc.sel.to.line - doc.sel.from.line > 100 || (selected = cm.getSelection()).length > 1000);
var content = minimal ? "-" : selected || cm.getSelection();
cm.display.input.value = content;
if (cm.state.focused) selectInput(cm.display.input);
if (ie && !ie_lt9) cm.display.inputHasSelection = content;
} else if (user && !cm.state.accessibleTextareaWaiting) {
cm.display.prevInput = cm.display.input.value = "";
if (ie && !ie_lt9) cm.display.inputHasSelection = null;
}
cm.display.inaccurateSelection = minimal;
}
function focusInput(cm) {
if (cm.options.readOnly != "nocursor" && (!mobile || document.activeElement != cm.display.input))
cm.display.input.focus();
}
function isReadOnly(cm) {
return cm.options.readOnly || cm.doc.cantEdit;
}
// EVENT HANDLERS
function registerEventHandlers(cm) {
var d = cm.display;
on(d.scroller, "mousedown", operation(cm, onMouseDown));
if (old_ie)
on(d.scroller, "dblclick", operation(cm, function(e) {
if (signalDOMEvent(cm, e)) return;
var pos = posFromMouse(cm, e);
if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) return;
e_preventDefault(e);
var word = findWordAt(getLine(cm.doc, pos.line).text, pos);
extendSelection(cm.doc, word.from, word.to);
}));
else
on(d.scroller, "dblclick", function(e) { signalDOMEvent(cm, e) || e_preventDefault(e); });
on(d.lineSpace, "selectstart", function(e) {
if (!eventInWidget(d, e)) e_preventDefault(e);
});
// Gecko browsers fire contextmenu *after* opening the menu, at
// which point we can't mess with it anymore. Context menu is
// handled in onMouseDown for Gecko.
if (!gecko) connect(scroller, "contextmenu", onContextMenu);
connect(scroller, "scroll", function() {
lastScrollPos = scroller.scrollTop;
updateDisplay([]);
if (options.fixedGutter) gutter.style.left = scroller.scrollLeft + "px";
if (options.onScroll) options.onScroll(instance);
});
connect(window, "resize", function() {updateDisplay(true);});
connect(input, "keyup", operation(onKeyUp));
connect(input, "input", fastPoll);
connect(input, "keydown", operation(onKeyDown));
connect(input, "keypress", operation(onKeyPress));
connect(input, "focus", onFocus);
connect(input, "blur", onBlur);
if (!captureMiddleClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);});
connect(scroller, "dragenter", e_stop);
connect(scroller, "dragover", e_stop);
connect(scroller, "drop", operation(onDrop));
connect(scroller, "paste", function(){focusInput(); fastPoll();});
connect(input, "paste", fastPoll);
connect(input, "cut", operation(function(){
if (!options.readOnly) replaceSelection("");
on(d.scroller, "scroll", function() {
if (d.scroller.clientHeight) {
setScrollTop(cm, d.scroller.scrollTop);
setScrollLeft(cm, d.scroller.scrollLeft, true);
signal(cm, "scroll", cm);
}
});
on(d.scrollbarV, "scroll", function() {
if (d.scroller.clientHeight) setScrollTop(cm, d.scrollbarV.scrollTop);
});
on(d.scrollbarH, "scroll", function() {
if (d.scroller.clientHeight) setScrollLeft(cm, d.scrollbarH.scrollLeft);
});
on(d.scroller, "mousewheel", function(e){onScrollWheel(cm, e);});
on(d.scroller, "DOMMouseScroll", function(e){onScrollWheel(cm, e);});
function reFocus() { if (cm.state.focused) setTimeout(bind(focusInput, cm), 0); }
on(d.scrollbarH, "mousedown", reFocus);
on(d.scrollbarV, "mousedown", reFocus);
// Prevent wrapper from ever scrolling
on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; });
var resizeTimer;
function onResize() {
if (resizeTimer == null) resizeTimer = setTimeout(function() {
resizeTimer = null;
// Might be a text scaling operation, clear size caches.
d.cachedCharWidth = d.cachedTextHeight = knownScrollbarWidth = null;
clearCaches(cm);
runInOp(cm, bind(regChange, cm));
}, 100);
}
on(window, "resize", onResize);
// Above handler holds on to the editor and its data structures.
// Here we poll to unregister it when the editor is no longer in
// the document, so that it can be garbage-collected.
function unregister() {
for (var p = d.wrapper.parentNode; p && p != document.body; p = p.parentNode) {}
if (p) setTimeout(unregister, 5000);
else off(window, "resize", onResize);
}
setTimeout(unregister, 5000);
on(d.input, "keyup", operation(cm, function(e) {
if (signalDOMEvent(cm, e) || cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return;
if (e.keyCode == 16) cm.doc.sel.shift = false;
}));
on(d.input, "input", function() {
if (ie && !ie_lt9 && cm.display.inputHasSelection) cm.display.inputHasSelection = null;
fastPoll(cm);
});
on(d.input, "keydown", operation(cm, onKeyDown));
on(d.input, "keypress", operation(cm, onKeyPress));
on(d.input, "focus", bind(onFocus, cm));
on(d.input, "blur", bind(onBlur, cm));
function drag_(e) {
if (signalDOMEvent(cm, e) || cm.options.onDragEvent && cm.options.onDragEvent(cm, addStop(e))) return;
e_stop(e);
}
if (cm.options.dragDrop) {
on(d.scroller, "dragstart", function(e){onDragStart(cm, e);});
on(d.scroller, "dragenter", drag_);
on(d.scroller, "dragover", drag_);
on(d.scroller, "drop", operation(cm, onDrop));
}
on(d.scroller, "paste", function(e) {
if (eventInWidget(d, e)) return;
focusInput(cm);
fastPoll(cm);
});
on(d.input, "paste", function() {
// Workaround for webkit bug https://bugs.webkit.org/show_bug.cgi?id=90206
// Add a char to the end of textarea before paste occur so that
// selection doesn't span to the end of textarea.
if (webkit && !cm.state.fakedLastChar && !(new Date - cm.state.lastMiddleDown < 200)) {
var start = d.input.selectionStart, end = d.input.selectionEnd;
d.input.value += "$";
d.input.selectionStart = start;
d.input.selectionEnd = end;
cm.state.fakedLastChar = true;
}
cm.state.pasteIncoming = true;
fastPoll(cm);
});
function prepareCopy(e) {
if (d.inaccurateSelection) {
d.prevInput = "";
d.inaccurateSelection = false;
d.input.value = cm.getSelection();
selectInput(d.input);
}
if (e.type == "cut") cm.state.cutIncoming = true;
}
on(d.input, "cut", prepareCopy);
on(d.input, "copy", prepareCopy);
// Needed to handle Tab key in KHTML
if (khtml) connect(code, "mouseup", function() {
if (document.activeElement == input) input.blur();
focusInput();
if (khtml) on(d.sizer, "mouseup", function() {
if (document.activeElement == d.input) d.input.blur();
focusInput(cm);
});
}
// IE throws unspecified error in certain cases, when
// trying to access activeElement before onload
var hasFocus; try { hasFocus = (document.activeElement == input); } catch(e) { }
if (hasFocus || options.autofocus) setTimeout(onFocus, 20);
else onBlur();
function isLine(l) {return l >= 0 && l < doc.size;}
// The instance object that we'll return. Mostly calls out to
// local functions in the CodeMirror function. Some do some extra
// range checking and/or clipping. operation is used to wrap the
// call so that changes it makes are tracked, and the display is
// updated afterwards.
var instance = wrapper.CodeMirror = {
getValue: getValue,
setValue: operation(setValue),
getSelection: getSelection,
replaceSelection: operation(replaceSelection),
focus: function(){window.focus(); focusInput(); onFocus(); fastPoll();},
setOption: function(option, value) {
var oldVal = options[option];
options[option] = value;
if (option == "mode" || option == "indentUnit") loadMode();
else if (option == "readOnly" && value == "nocursor") {onBlur(); input.blur();}
else if (option == "readOnly" && !value) {resetInput(true);}
else if (option == "theme") themeChanged();
else if (option == "lineWrapping" && oldVal != value) operation(wrappingChanged)();
else if (option == "tabSize") updateDisplay(true);
if (option == "lineNumbers" || option == "gutter" || option == "firstLineNumber" || option == "theme") {
gutterChanged();
updateDisplay(true);
}
},
getOption: function(option) {return options[option];},
undo: operation(undo),
redo: operation(redo),
indentLine: operation(function(n, dir) {
if (typeof dir != "string") {
if (dir == null) dir = options.smartIndent ? "smart" : "prev";
else dir = dir ? "add" : "subtract";
}
if (isLine(n)) indentLine(n, dir);
}),
indentSelection: operation(indentSelected),
historySize: function() {return {undo: history.done.length, redo: history.undone.length};},
clearHistory: function() {history = new History();},
matchBrackets: operation(function(){matchBrackets(true);}),
getTokenAt: operation(function(pos) {
pos = clipPos(pos);
return getLine(pos.line).getTokenAt(mode, getStateBefore(pos.line), pos.ch);
}),
getStateAfter: function(line) {
line = clipLine(line == null ? doc.size - 1: line);
return getStateBefore(line + 1);
},
cursorCoords: function(start, mode) {
if (start == null) start = sel.inverted;
return this.charCoords(start ? sel.from : sel.to, mode);
},
charCoords: function(pos, mode) {
pos = clipPos(pos);
if (mode == "local") return localCoords(pos, false);
if (mode == "div") return localCoords(pos, true);
return pageCoords(pos);
},
coordsChar: function(coords) {
var off = eltOffset(lineSpace);
return coordsChar(coords.x - off.left, coords.y - off.top);
},
markText: operation(markText),
setBookmark: setBookmark,
findMarksAt: findMarksAt,
setMarker: operation(addGutterMarker),
clearMarker: operation(removeGutterMarker),
setLineClass: operation(setLineClass),
hideLine: operation(function(h) {return setLineHidden(h, true);}),
showLine: operation(function(h) {return setLineHidden(h, false);}),
onDeleteLine: function(line, f) {
if (typeof line == "number") {
if (!isLine(line)) return null;
line = getLine(line);
}
(line.handlers || (line.handlers = [])).push(f);
return line;
},
lineInfo: lineInfo,
addWidget: function(pos, node, scroll, vert, horiz) {
pos = localCoords(clipPos(pos));
var top = pos.yBot, left = pos.x;
node.style.position = "absolute";
code.appendChild(node);
if (vert == "over") top = pos.y;
else if (vert == "near") {
var vspace = Math.max(scroller.offsetHeight, doc.height * textHeight()),
hspace = Math.max(code.clientWidth, lineSpace.clientWidth) - paddingLeft();
if (pos.yBot + node.offsetHeight > vspace && pos.y > node.offsetHeight)
top = pos.y - node.offsetHeight;
if (left + node.offsetWidth > hspace)
left = hspace - node.offsetWidth;
}
node.style.top = (top + paddingTop()) + "px";
node.style.left = node.style.right = "";
if (horiz == "right") {
left = code.clientWidth - node.offsetWidth;
node.style.right = "0px";
} else {
if (horiz == "left") left = 0;
else if (horiz == "middle") left = (code.clientWidth - node.offsetWidth) / 2;
node.style.left = (left + paddingLeft()) + "px";
}
if (scroll)
scrollIntoView(left, top, left + node.offsetWidth, top + node.offsetHeight);
},
lineCount: function() {return doc.size;},
clipPos: clipPos,
getCursor: function(start) {
if (start == null) start = sel.inverted;
return copyPos(start ? sel.from : sel.to);
},
somethingSelected: function() {return !posEq(sel.from, sel.to);},
setCursor: operation(function(line, ch, user) {
if (ch == null && typeof line.line == "number") setCursor(line.line, line.ch, user);
else setCursor(line, ch, user);
}),
setSelection: operation(function(from, to, user) {
(user ? setSelectionUser : setSelection)(clipPos(from), clipPos(to || from));
}),
getLine: function(line) {if (isLine(line)) return getLine(line).text;},
getLineHandle: function(line) {if (isLine(line)) return getLine(line);},
setLine: operation(function(line, text) {
if (isLine(line)) replaceRange(text, {line: line, ch: 0}, {line: line, ch: getLine(line).text.length});
}),
removeLine: operation(function(line) {
if (isLine(line)) replaceRange("", {line: line, ch: 0}, clipPos({line: line+1, ch: 0}));
}),
replaceRange: operation(replaceRange),
getRange: function(from, to) {return getRange(clipPos(from), clipPos(to));},
triggerOnKeyDown: operation(onKeyDown),
execCommand: function(cmd) {return commands[cmd](instance);},
// Stuff used by commands, probably not much use to outside code.
moveH: operation(moveH),
deleteH: operation(deleteH),
moveV: operation(moveV),
toggleOverwrite: function() {
if(overwrite){
overwrite = false;
cursor.className = cursor.className.replace(" CodeMirror-overwrite", "");
} else {
overwrite = true;
cursor.className += " CodeMirror-overwrite";
}
},
posFromIndex: function(off) {
var lineNo = 0, ch;
doc.iter(0, doc.size, function(line) {
var sz = line.text.length + 1;
if (sz > off) { ch = off; return true; }
off -= sz;
++lineNo;
});
return clipPos({line: lineNo, ch: ch});
},
indexFromPos: function (coords) {
if (coords.line < 0 || coords.ch < 0) return 0;
var index = coords.ch;
doc.iter(0, coords.line, function (line) {
index += line.text.length + 1;
});
return index;
},
scrollTo: function(x, y) {
if (x != null) scroller.scrollLeft = x;
if (y != null) scroller.scrollTop = y;
updateDisplay([]);
},
operation: function(f){return operation(f)();},
refresh: function(){
updateDisplay(true);
if (scroller.scrollHeight > lastScrollPos)
scroller.scrollTop = lastScrollPos;
},
getInputField: function(){return input;},
getWrapperElement: function(){return wrapper;},
getScrollerElement: function(){return scroller;},
getGutterElement: function(){return gutter;}
};
function getLine(n) { return getLineAt(doc, n); }
function updateLineHeight(line, height) {
gutterDirty = true;
var diff = height - line.height;
for (var n = line; n; n = n.parent) n.height += diff;
function eventInWidget(display, e) {
for (var n = e_target(e); n != display.wrapper; n = n.parentNode) {
if (!n || n.ignoreEvents || n.parentNode == display.sizer && n != display.mover) return true;
}
}
function setValue(code) {
var top = {line: 0, ch: 0};
updateLines(top, {line: doc.size - 1, ch: getLine(doc.size-1).text.length},
splitLines(code), top, top);
updateInput = true;
function posFromMouse(cm, e, liberal) {
var display = cm.display;
if (!liberal) {
var target = e_target(e);
if (target == display.scrollbarH || target == display.scrollbarH.firstChild ||
target == display.scrollbarV || target == display.scrollbarV.firstChild ||
target == display.scrollbarFiller || target == display.gutterFiller) return null;
}
function getValue(code) {
var text = [];
doc.iter(0, doc.size, function(line) { text.push(line.text); });
return text.join("\n");
var x, y, space = getRect(display.lineSpace);
// Fails unpredictably on IE[67] when mouse is dragged around quickly.
try { x = e.clientX; y = e.clientY; } catch (e) { return null; }
return coordsChar(cm, x - space.left, y - space.top);
}
var lastClick, lastDoubleClick;
function onMouseDown(e) {
if (signalDOMEvent(this, e)) return;
var cm = this, display = cm.display, doc = cm.doc, sel = doc.sel;
sel.shift = e.shiftKey;
if (eventInWidget(display, e)) {
if (!webkit) {
display.scroller.draggable = false;
setTimeout(function(){display.scroller.draggable = true;}, 100);
}
return;
}
if (clickInGutter(cm, e)) return;
var start = posFromMouse(cm, e);
function onMouseDown(e) {
setShift(e_prop(e, "shiftKey"));
// Check whether this is a click in a widget
for (var n = e_target(e); n != wrapper; n = n.parentNode)
if (n.parentNode == code && n != mover) return;
switch (e_button(e)) {
case 3:
if (captureMiddleClick) onContextMenu.call(cm, cm, e);
return;
case 2:
if (webkit) cm.state.lastMiddleDown = +new Date;
if (start) extendSelection(cm.doc, start);
setTimeout(bind(focusInput, cm), 20);
e_preventDefault(e);
return;
}
// For button 1, if it was clicked inside the editor
// (posFromMouse returning non-null), we have to adjust the
// selection.
if (!start) {if (e_target(e) == display.scroller) e_preventDefault(e); return;}
// See if this is a click in the gutter
for (var n = e_target(e); n != wrapper; n = n.parentNode)
if (n.parentNode == gutterText) {
if (options.onGutterClick)
options.onGutterClick(instance, indexOf(gutterText.childNodes, n) + showingFrom, e);
return e_preventDefault(e);
if (!cm.state.focused) onFocus(cm);
var now = +new Date, type = "single";
if (lastDoubleClick && lastDoubleClick.time > now - 400 && posEq(lastDoubleClick.pos, start)) {
type = "triple";
e_preventDefault(e);
setTimeout(bind(focusInput, cm), 20);
selectLine(cm, start.line);
} else if (lastClick && lastClick.time > now - 400 && posEq(lastClick.pos, start)) {
type = "double";
lastDoubleClick = {time: now, pos: start};
e_preventDefault(e);
var word = findWordAt(getLine(doc, start.line).text, start);
extendSelection(cm.doc, word.from, word.to);
} else { lastClick = {time: now, pos: start}; }
var last = start;
if (cm.options.dragDrop && dragAndDrop && !isReadOnly(cm) && !posEq(sel.from, sel.to) &&
!posLess(start, sel.from) && !posLess(sel.to, start) && type == "single") {
var dragEnd = operation(cm, function(e2) {
if (webkit) display.scroller.draggable = false;
cm.state.draggingText = false;
off(document, "mouseup", dragEnd);
off(display.scroller, "drop", dragEnd);
if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) {
e_preventDefault(e2);
extendSelection(cm.doc, start);
focusInput(cm);
// Work around unexplainable focus problem in IE9 (#2127)
if (old_ie && !ie_lt9)
setTimeout(function() {document.body.focus(); focusInput(cm);}, 20);
}
});
// Let the drag handler handle this.
if (webkit) display.scroller.draggable = true;
cm.state.draggingText = dragEnd;
// IE's approach to draggable
if (display.scroller.dragDrop) display.scroller.dragDrop();
on(document, "mouseup", dragEnd);
on(display.scroller, "drop", dragEnd);
return;
}
e_preventDefault(e);
if (type == "single") extendSelection(cm.doc, clipPos(doc, start));
var start = posFromMouse(e);
var startstart = sel.from, startend = sel.to, lastPos = start;
switch (e_button(e)) {
case 3:
if (gecko && !mac) onContextMenu(e);
return;
case 2:
if (start) setCursor(start.line, start.ch, true);
function doSelect(cur) {
if (posEq(lastPos, cur)) return;
lastPos = cur;
if (type == "single") {
extendSelection(cm.doc, clipPos(doc, start), cur);
return;
}
// For button 1, if it was clicked inside the editor
// (posFromMouse returning non-null), we have to adjust the
// selection.
if (!start) {if (e_target(e) == scroller) e_preventDefault(e); return;}
if (!focused) onFocus();
startstart = clipPos(doc, startstart);
startend = clipPos(doc, startend);
if (type == "double") {
var word = findWordAt(getLine(doc, cur.line).text, cur);
if (posLess(cur, startstart)) extendSelection(cm.doc, word.from, startend);
else extendSelection(cm.doc, startstart, word.to);
} else if (type == "triple") {
if (posLess(cur, startstart)) extendSelection(cm.doc, startend, clipPos(doc, Pos(cur.line, 0)));
else extendSelection(cm.doc, startstart, clipPos(doc, Pos(cur.line + 1, 0)));
}
}
var now = +new Date;
if (lastDoubleClick && lastDoubleClick.time > now - 400 && posEq(lastDoubleClick.pos, start)) {
e_preventDefault(e);
setTimeout(focusInput, 20);
return selectLine(start.line);
} else if (lastClick && lastClick.time > now - 400 && posEq(lastClick.pos, start)) {
lastDoubleClick = {time: now, pos: start};
e_preventDefault(e);
return selectWordAt(start);
} else { lastClick = {time: now, pos: start}; }
var editorSize = getRect(display.wrapper);
// Used to ensure timeout re-tries don't fire when another extend
// happened in the meantime (clearTimeout isn't reliable -- at
// least on Chrome, the timeouts still happen even when cleared,
// if the clear happens after their scheduled firing time).
var counter = 0;
var last = start, going;
if (dragAndDrop && !options.readOnly && !posEq(sel.from, sel.to) &&
!posLess(start, sel.from) && !posLess(sel.to, start)) {
// Let the drag handler handle this.
if (webkit) lineSpace.draggable = true;
var up = connect(document, "mouseup", operation(function(e2) {
if (webkit) lineSpace.draggable = false;
draggingText = false;
up();
if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) {
e_preventDefault(e2);
setCursor(start.line, start.ch, true);
focusInput();
function extend(e) {
var curCount = ++counter;
var cur = posFromMouse(cm, e, true);
if (!cur) return;
if (!posEq(cur, last)) {
if (!cm.state.focused) onFocus(cm);
last = cur;
doSelect(cur);
var visible = visibleLines(display, doc);
if (cur.line >= visible.to || cur.line < visible.from)
setTimeout(operation(cm, function(){if (counter == curCount) extend(e);}), 150);
} else {
var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0;
if (outside) setTimeout(operation(cm, function() {
if (counter != curCount) return;
display.scroller.scrollTop += outside;
extend(e);
}), 50);
}
}
function done(e) {
counter = Infinity;
e_preventDefault(e);
focusInput(cm);
off(document, "mousemove", move);
off(document, "mouseup", up);
}
var move = operation(cm, function(e) {
if (!old_ie && !e_button(e)) done(e);
else extend(e);
});
var up = operation(cm, done);
on(document, "mousemove", move);
on(document, "mouseup", up);
}
function gutterEvent(cm, e, type, prevent, signalfn) {
try { var mX = e.clientX, mY = e.clientY; }
catch(e) { return false; }
if (mX >= Math.floor(getRect(cm.display.gutters).right)) return false;
if (prevent) e_preventDefault(e);
var display = cm.display;
var lineBox = getRect(display.lineDiv);
if (mY > lineBox.bottom || !hasHandler(cm, type)) return e_defaultPrevented(e);
mY -= lineBox.top - display.viewOffset;
for (var i = 0; i < cm.options.gutters.length; ++i) {
var g = display.gutters.childNodes[i];
if (g && getRect(g).right >= mX) {
var line = lineAtHeight(cm.doc, mY);
var gutter = cm.options.gutters[i];
signalfn(cm, type, cm, line, gutter, e);
return e_defaultPrevented(e);
}
}
}
function contextMenuInGutter(cm, e) {
if (!hasHandler(cm, "gutterContextMenu")) return false;
return gutterEvent(cm, e, "gutterContextMenu", false, signal);
}
function clickInGutter(cm, e) {
return gutterEvent(cm, e, "gutterClick", true, signalLater);
}
// Kludge to work around strange IE behavior where it'll sometimes
// re-fire a series of drag-related events right after the drop (#1551)
var lastDrop = 0;
function onDrop(e) {
var cm = this;
if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e) || (cm.options.onDragEvent && cm.options.onDragEvent(cm, addStop(e))))
return;
e_preventDefault(e);
if (ie) lastDrop = +new Date;
var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files;
if (!pos || isReadOnly(cm)) return;
if (files && files.length && window.FileReader && window.File) {
var n = files.length, text = Array(n), read = 0;
var loadFile = function(file, i) {
var reader = new FileReader;
reader.onload = function() {
text[i] = reader.result;
if (++read == n) {
pos = clipPos(cm.doc, pos);
makeChange(cm.doc, {from: pos, to: pos, text: splitLines(text.join("\n")), origin: "paste"}, "around");
}
}), true);
draggingText = true;
// IE's approach to draggable
if (lineSpace.dragDrop) lineSpace.dragDrop();
};
reader.readAsText(file);
};
for (var i = 0; i < n; ++i) loadFile(files[i], i);
} else {
// Don't do a replace if the drop happened inside of the selected text.
if (cm.state.draggingText && !(posLess(pos, cm.doc.sel.from) || posLess(cm.doc.sel.to, pos))) {
cm.state.draggingText(e);
// Ensure the editor is re-focused
setTimeout(bind(focusInput, cm), 20);
return;
}
e_preventDefault(e);
setCursor(start.line, start.ch, true);
function extend(e) {
var cur = posFromMouse(e, true);
if (cur && !posEq(cur, last)) {
if (!focused) onFocus();
last = cur;
setSelectionUser(start, cur);
updateInput = false;
var visible = visibleLines();
if (cur.line >= visible.to || cur.line < visible.from)
going = setTimeout(operation(function(){extend(e);}), 150);
}
}
function done(e) {
clearTimeout(going);
var cur = posFromMouse(e);
if (cur) setSelectionUser(start, cur);
e_preventDefault(e);
focusInput();
updateInput = true;
move(); up();
}
var move = connect(document, "mousemove", operation(function(e) {
clearTimeout(going);
e_preventDefault(e);
if (!ie && !e_button(e)) done(e);
else extend(e);
}), true);
var up = connect(document, "mouseup", operation(done), true);
}
function onDoubleClick(e) {
for (var n = e_target(e); n != wrapper; n = n.parentNode)
if (n.parentNode == gutterText) return e_preventDefault(e);
var start = posFromMouse(e);
if (!start) return;
lastDoubleClick = {time: +new Date, pos: start};
e_preventDefault(e);
selectWordAt(start);
}
function onDrop(e) {
e.preventDefault();
var pos = posFromMouse(e, true), files = e.dataTransfer.files;
if (!pos || options.readOnly) return;
if (files && files.length && window.FileReader && window.File) {
function loadFile(file, i) {
var reader = new FileReader;
reader.onload = function() {
text[i] = reader.result;
if (++read == n) {
pos = clipPos(pos);
operation(function() {
var end = replaceRange(text.join(""), pos, pos);
setSelectionUser(pos, end);
})();
}
};
reader.readAsText(file);
}
var n = files.length, text = Array(n), read = 0;
for (var i = 0; i < n; ++i) loadFile(files[i], i);
}
else {
try {
var text = e.dataTransfer.getData("Text");
if (text) {
var curFrom = sel.from, curTo = sel.to;
setSelectionUser(pos, pos);
if (draggingText) replaceRange("", curFrom, curTo);
replaceSelection(text);
focusInput();
}
}
catch(e){}
}
}
function onDragStart(e) {
var txt = getSelection();
e.dataTransfer.setData("Text", txt);
// Use dummy image instead of default browsers image.
if (gecko || chrome) {
var img = document.createElement('img');
img.scr = 'data:image/gif;base64,R0lGODdhAgACAIAAAAAAAP///ywAAAAAAgACAAACAoRRADs='; //1x1 image
e.dataTransfer.setDragImage(img, 0, 0);
}
}
function doHandleBinding(bound, dropShift) {
if (typeof bound == "string") {
bound = commands[bound];
if (!bound) return false;
}
var prevShift = shiftSelecting;
try {
if (options.readOnly) suppressEdits = true;
if (dropShift) shiftSelecting = null;
bound(instance);
} catch(e) {
if (e != Pass) throw e;
return false;
} finally {
shiftSelecting = prevShift;
suppressEdits = false;
}
return true;
}
function handleKeyBinding(e) {
// Handle auto keymap transitions
var startMap = getKeyMap(options.keyMap), next = startMap.auto;
clearTimeout(maybeTransition);
if (next && !isModifierKey(e)) maybeTransition = setTimeout(function() {
if (getKeyMap(options.keyMap) == startMap) {
options.keyMap = (next.call ? next.call(null, instance) : next);
var text = e.dataTransfer.getData("Text");
if (text) {
var curFrom = cm.doc.sel.from, curTo = cm.doc.sel.to;
setSelection(cm.doc, pos, pos);
if (cm.state.draggingText) replaceRange(cm.doc, "", curFrom, curTo, "paste");
cm.replaceSelection(text, null, "paste");
focusInput(cm);
}
}, 50);
}
catch(e){}
}
}
var name = keyNames[e_prop(e, "keyCode")], handled = false;
if (name == null || e.altGraphKey) return false;
if (e_prop(e, "altKey")) name = "Alt-" + name;
if (e_prop(e, "ctrlKey")) name = "Ctrl-" + name;
if (e_prop(e, "metaKey")) name = "Cmd-" + name;
function onDragStart(cm, e) {
if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return; }
if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return;
if (e_prop(e, "shiftKey")) {
handled = lookupKey("Shift-" + name, options.extraKeys, options.keyMap,
function(b) {return doHandleBinding(b, true);})
|| lookupKey(name, options.extraKeys, options.keyMap, function(b) {
if (typeof b == "string" && /^go[A-Z]/.test(b)) return doHandleBinding(b);
});
var txt = cm.getSelection();
e.dataTransfer.setData("Text", txt);
// Use dummy image instead of default browsers image.
// Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there.
if (e.dataTransfer.setDragImage && !safari) {
var img = elt("img", null, null, "position: fixed; left: 0; top: 0;");
img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
if (opera) {
img.width = img.height = 1;
cm.display.wrapper.appendChild(img);
// Force a relayout, or Opera won't use our image for some obscure reason
img._top = img.offsetTop;
}
e.dataTransfer.setDragImage(img, 0, 0);
if (opera) img.parentNode.removeChild(img);
}
}
function setScrollTop(cm, val) {
if (Math.abs(cm.doc.scrollTop - val) < 2) return;
cm.doc.scrollTop = val;
if (!gecko) updateDisplay(cm, [], val);
if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val;
if (cm.display.scrollbarV.scrollTop != val) cm.display.scrollbarV.scrollTop = val;
if (gecko) updateDisplay(cm, []);
startWorker(cm, 100);
}
function setScrollLeft(cm, val, isScroller) {
if (isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) return;
val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth);
cm.doc.scrollLeft = val;
alignHorizontally(cm);
if (cm.display.scroller.scrollLeft != val) cm.display.scroller.scrollLeft = val;
if (cm.display.scrollbarH.scrollLeft != val) cm.display.scrollbarH.scrollLeft = val;
}
// Since the delta values reported on mouse wheel events are
// unstandardized between browsers and even browser versions, and
// generally horribly unpredictable, this code starts by measuring
// the scroll effect that the first few mouse wheel events have,
// and, from that, detects the way it can convert deltas to pixel
// offsets afterwards.
//
// The reason we want to know the amount a wheel event will scroll
// is that it gives us a chance to update the display before the
// actual scrolling happens, reducing flickering.
var wheelSamples = 0, wheelPixelsPerUnit = null;
// Fill in a browser-detected starting value on browsers where we
// know one. These don't have to be accurate -- the result of them
// being wrong would just be a slight flicker on the first wheel
// scroll (if it is large enough).
if (old_ie) wheelPixelsPerUnit = -.53;
else if (gecko) wheelPixelsPerUnit = 15;
else if (chrome) wheelPixelsPerUnit = -.7;
else if (safari) wheelPixelsPerUnit = -1/3;
function onScrollWheel(cm, e) {
var dx = e.wheelDeltaX, dy = e.wheelDeltaY;
if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) dx = e.detail;
if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) dy = e.detail;
else if (dy == null) dy = e.wheelDelta;
var display = cm.display, scroll = display.scroller;
// Quit if there's nothing to scroll here
if (!(dx && scroll.scrollWidth > scroll.clientWidth ||
dy && scroll.scrollHeight > scroll.clientHeight)) return;
// Webkit browsers on OS X abort momentum scrolls when the target
// of the scroll event is removed from the scrollable element.
// This hack (see related code in patchDisplay) makes sure the
// element is kept around.
if (dy && mac && webkit) {
for (var cur = e.target; cur != scroll; cur = cur.parentNode) {
if (cur.lineObj) {
cm.display.currentWheelTarget = cur;
break;
}
}
}
// On some browsers, horizontal scrolling will cause redraws to
// happen before the gutter has been realigned, causing it to
// wriggle around in a most unseemly way. When we have an
// estimated pixels/delta value, we just handle horizontal
// scrolling entirely here. It'll be slightly off from native, but
// better than glitching out.
if (dx && !gecko && !opera && wheelPixelsPerUnit != null) {
if (dy)
setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight)));
setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth)));
e_preventDefault(e);
display.wheelStartX = null; // Abort measurement, if in progress
return;
}
if (dy && wheelPixelsPerUnit != null) {
var pixels = dy * wheelPixelsPerUnit;
var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight;
if (pixels < 0) top = Math.max(0, top + pixels - 50);
else bot = Math.min(cm.doc.height, bot + pixels + 50);
updateDisplay(cm, [], {top: top, bottom: bot});
}
if (wheelSamples < 20) {
if (display.wheelStartX == null) {
display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop;
display.wheelDX = dx; display.wheelDY = dy;
setTimeout(function() {
if (display.wheelStartX == null) return;
var movedX = scroll.scrollLeft - display.wheelStartX;
var movedY = scroll.scrollTop - display.wheelStartY;
var sample = (movedY && display.wheelDY && movedY / display.wheelDY) ||
(movedX && display.wheelDX && movedX / display.wheelDX);
display.wheelStartX = display.wheelStartY = null;
if (!sample) return;
wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1);
++wheelSamples;
}, 200);
} else {
handled = lookupKey(name, options.extraKeys, options.keyMap, doHandleBinding);
}
if (handled) {
e_preventDefault(e);
if (ie) { e.oldKeyCode = e.keyCode; e.keyCode = 0; }
}
return handled;
}
function handleCharBinding(e, ch) {
var handled = lookupKey("'" + ch + "'", options.extraKeys,
options.keyMap, doHandleBinding);
if (handled) e_preventDefault(e);
return handled;
}
var lastStoppedKey = null, maybeTransition;
function onKeyDown(e) {
if (!focused) onFocus();
if (ie && e.keyCode == 27) { e.returnValue = false; }
if (pollingFast) { if (readInput()) pollingFast = false; }
if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return;
var code = e_prop(e, "keyCode");
// IE does strange things with escape.
setShift(code == 16 || e_prop(e, "shiftKey"));
// First give onKeyEvent option a chance to handle this.
var handled = handleKeyBinding(e);
if (window.opera) {
lastStoppedKey = handled ? code : null;
// Opera has no cut event... we try to at least catch the key combo
if (!handled && code == 88 && e_prop(e, mac ? "metaKey" : "ctrlKey"))
replaceSelection("");
display.wheelDX += dx; display.wheelDY += dy;
}
}
function onKeyPress(e) {
if (pollingFast) readInput();
if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return;
var keyCode = e_prop(e, "keyCode"), charCode = e_prop(e, "charCode");
if (window.opera && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;}
if (((window.opera && !e.which) || khtml) && handleKeyBinding(e)) return;
var ch = String.fromCharCode(charCode == null ? keyCode : charCode);
if (options.electricChars && mode.electricChars && options.smartIndent && !options.readOnly) {
if (mode.electricChars.indexOf(ch) > -1)
setTimeout(operation(function() {indentLine(sel.to.line, "smart");}), 75);
}
function doHandleBinding(cm, bound, dropShift) {
if (typeof bound == "string") {
bound = commands[bound];
if (!bound) return false;
}
// Ensure previous input has been read, so that the handler sees a
// consistent view of the document
if (cm.display.pollingFast && readInput(cm)) cm.display.pollingFast = false;
var doc = cm.doc, prevShift = doc.sel.shift, done = false;
try {
if (isReadOnly(cm)) cm.state.suppressEdits = true;
if (dropShift) doc.sel.shift = false;
done = bound(cm) != Pass;
} finally {
doc.sel.shift = prevShift;
cm.state.suppressEdits = false;
}
return done;
}
function allKeyMaps(cm) {
var maps = cm.state.keyMaps.slice(0);
if (cm.options.extraKeys) maps.push(cm.options.extraKeys);
maps.push(cm.options.keyMap);
return maps;
}
var maybeTransition;
function handleKeyBinding(cm, e) {
// Handle auto keymap transitions
var startMap = getKeyMap(cm.options.keyMap), next = startMap.auto;
clearTimeout(maybeTransition);
if (next && !isModifierKey(e)) maybeTransition = setTimeout(function() {
if (getKeyMap(cm.options.keyMap) == startMap) {
cm.options.keyMap = (next.call ? next.call(null, cm) : next);
keyMapChanged(cm);
}
if (handleCharBinding(e, ch)) return;
fastPoll();
}
function onKeyUp(e) {
if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return;
if (e_prop(e, "keyCode") == 16) shiftSelecting = null;
}, 50);
var name = keyName(e, true), handled = false;
if (!name) return false;
var keymaps = allKeyMaps(cm);
if (e.shiftKey) {
// First try to resolve full name (including 'Shift-'). Failing
// that, see if there is a cursor-motion command (starting with
// 'go') bound to the keyname without 'Shift-'.
handled = lookupKey("Shift-" + name, keymaps, function(b) {return doHandleBinding(cm, b, true);})
|| lookupKey(name, keymaps, function(b) {
if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion)
return doHandleBinding(cm, b);
});
} else {
handled = lookupKey(name, keymaps, function(b) { return doHandleBinding(cm, b); });
}
function onFocus() {
if (options.readOnly == "nocursor") return;
if (!focused) {
if (options.onFocus) options.onFocus(instance);
focused = true;
if (wrapper.className.search(/\bCodeMirror-focused\b/) == -1)
wrapper.className += " CodeMirror-focused";
if (!leaveInputAlone) resetInput(true);
if (handled) {
e_preventDefault(e);
restartBlink(cm);
if (ie_lt9) { e.oldKeyCode = e.keyCode; e.keyCode = 0; }
signalLater(cm, "keyHandled", cm, name, e);
}
return handled;
}
function handleCharBinding(cm, e, ch) {
var handled = lookupKey("'" + ch + "'", allKeyMaps(cm),
function(b) { return doHandleBinding(cm, b, true); });
if (handled) {
e_preventDefault(e);
restartBlink(cm);
signalLater(cm, "keyHandled", cm, "'" + ch + "'", e);
}
return handled;
}
var lastStoppedKey = null;
function onKeyDown(e) {
var cm = this;
if (!cm.state.focused) onFocus(cm);
if (signalDOMEvent(cm, e) || cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return;
if (old_ie && e.keyCode == 27) e.returnValue = false;
var code = e.keyCode;
// IE does strange things with escape.
cm.doc.sel.shift = code == 16 || e.shiftKey;
// First give onKeyEvent option a chance to handle this.
var handled = handleKeyBinding(cm, e);
// On text input if value was temporaritly set for a screenreader, clear it out.
if (!handled && cm.state.accessibleTextareaWaiting) {
clearAccessibleTextarea(cm);
}
if (opera) {
lastStoppedKey = handled ? code : null;
// Opera has no cut event... we try to at least catch the key combo
if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey))
cm.replaceSelection("");
}
}
function onKeyPress(e) {
var cm = this;
if (signalDOMEvent(cm, e) || cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return;
var keyCode = e.keyCode, charCode = e.charCode;
if (opera && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;}
if (((opera && (!e.which || e.which < 10)) || khtml) && handleKeyBinding(cm, e)) return;
var ch = String.fromCharCode(charCode == null ? keyCode : charCode);
if (handleCharBinding(cm, e, ch)) return;
if (ie && !ie_lt9) cm.display.inputHasSelection = null;
fastPoll(cm);
}
function onFocus(cm) {
if (cm.options.readOnly == "nocursor") return;
if (!cm.state.focused) {
signal(cm, "focus", cm);
cm.state.focused = true;
if (cm.display.wrapper.className.search(/\bCodeMirror-focused\b/) == -1)
cm.display.wrapper.className += " CodeMirror-focused";
if (!cm.curOp) {
resetInput(cm, true);
if (webkit) setTimeout(bind(resetInput, cm, true), 0); // Issue #1730
}
slowPoll();
restartBlink();
}
function onBlur() {
if (focused) {
if (options.onBlur) options.onBlur(instance);
focused = false;
if (bracketHighlighted)
operation(function(){
if (bracketHighlighted) { bracketHighlighted(); bracketHighlighted = null; }
})();
wrapper.className = wrapper.className.replace(" CodeMirror-focused", "");
slowPoll(cm);
restartBlink(cm);
}
function onBlur(cm) {
if (cm.state.focused) {
signal(cm, "blur", cm);
cm.state.focused = false;
cm.display.wrapper.className = cm.display.wrapper.className.replace(" CodeMirror-focused", "");
}
clearInterval(cm.display.blinker);
setTimeout(function() {if (!cm.state.focused) cm.doc.sel.shift = false;}, 150);
}
var detectingSelectAll;
function onContextMenu(cm, e) {
if (signalDOMEvent(cm, e, "contextmenu")) return;
var display = cm.display, sel = cm.doc.sel;
if (eventInWidget(display, e) || contextMenuInGutter(cm, e)) return;
var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop;
if (!pos || opera) return; // Opera is difficult.
// Reset the current text selection only if the click is done outside of the selection
// and 'resetSelectionOnContextMenu' option is true.
var reset = cm.options.resetSelectionOnContextMenu;
if (reset && (posEq(sel.from, sel.to) || posLess(pos, sel.from) || !posLess(pos, sel.to)))
operation(cm, setSelection)(cm.doc, pos, pos);
var oldCSS = display.input.style.cssText;
display.inputDiv.style.position = "absolute";
display.input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) +
"px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: transparent; outline: none;" +
"border-width: 0; outline: none; overflow: hidden; opacity: .05; -ms-opacity: .05; filter: alpha(opacity=5);";
focusInput(cm);
resetInput(cm, true);
// Adds "Select all" to context menu in FF
if (posEq(sel.from, sel.to)) display.input.value = display.prevInput = " ";
function prepareSelectAllHack() {
if (display.input.selectionStart != null) {
var extval = display.input.value = "\u200b" + (posEq(sel.from, sel.to) ? "" : display.input.value);
display.prevInput = "\u200b";
display.input.selectionStart = 1; display.input.selectionEnd = extval.length;
}
clearInterval(blinker);
setTimeout(function() {if (!focused) shiftSelecting = null;}, 150);
}
function rehide() {
display.inputDiv.style.position = "relative";
display.input.style.cssText = oldCSS;
if (ie_lt9) display.scrollbarV.scrollTop = display.scroller.scrollTop = scrollPos;
slowPoll(cm);
// Replace the range from from to to by the strings in newText.
// Afterwards, set the selection to selFrom, selTo.
function updateLines(from, to, newText, selFrom, selTo) {
if (suppressEdits) return;
//This block ensures that widget lines don't have any text inserted on the same line.
if (from.ch > 0 && (newText[0] != '' || newText.length == 1) && getLine(from.line).widgetFunction) {
newText.unshift('');
var widgetLine = getLine(from.line);
function moveSel(sel) {
if (sel.line == from.line && sel.ch > 0){
return {line: sel.line + 1, ch: sel.ch - widgetLine.text.length};
} else if (sel.line > from.line) {
return {line: sel.line + 1, ch: sel.ch};
}
}
selFrom = moveSel(selFrom, widgetLine);
selTo = moveSel(selTo, widgetLine);
// Try to detect the user choosing select-all
if (display.input.selectionStart != null) {
if (!old_ie || ie_lt9) prepareSelectAllHack();
clearTimeout(detectingSelectAll);
var i = 0, poll = function(){
if (display.prevInput == "\u200b" && display.input.selectionStart == 0)
operation(cm, commands.selectAll)(cm);
else if (i++ < 10) detectingSelectAll = setTimeout(poll, 500);
else resetInput(cm);
};
detectingSelectAll = setTimeout(poll, 200);
}
if (to.ch == 0 && (newText[newText.length - 1] != '' || newText.length == 1) && getLine(to.line).widgetFunction) {
newText.push('');
}
if (old_ie && !ie_lt9) prepareSelectAllHack();
if (captureMiddleClick) {
e_stop(e);
var mouseup = function() {
off(window, "mouseup", mouseup);
setTimeout(rehide, 20);
};
on(window, "mouseup", mouseup);
} else {
setTimeout(rehide, 50);
}
}
// UPDATING
var changeEnd = CodeMirror.changeEnd = function(change) {
if (!change.text) return change.to;
return Pos(change.from.line + change.text.length - 1,
lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0));
};
// Make sure a position will be valid after the given change.
function clipPostChange(doc, change, pos) {
if (!posLess(change.from, pos)) return clipPos(doc, pos);
var diff = (change.text.length - 1) - (change.to.line - change.from.line);
if (pos.line > change.to.line + diff) {
var preLine = pos.line - diff, lastLine = doc.first + doc.size - 1;
if (preLine > lastLine) return Pos(lastLine, getLine(doc, lastLine).text.length);
return clipToLen(pos, getLine(doc, preLine).text.length);
}
if (pos.line == change.to.line + diff)
return clipToLen(pos, lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0) +
getLine(doc, change.to.line).text.length - change.to.ch);
var inside = pos.line - change.from.line;
return clipToLen(pos, change.text[inside].length + (inside ? 0 : change.from.ch));
}
// Hint can be null|"end"|"start"|"around"|{anchor,head}
function computeSelAfterChange(doc, change, hint) {
if (hint && typeof hint == "object") // Assumed to be {anchor, head} object
return {anchor: clipPostChange(doc, change, hint.anchor),
head: clipPostChange(doc, change, hint.head)};
if (hint == "start") return {anchor: change.from, head: change.from};
var end = changeEnd(change);
if (hint == "around") return {anchor: change.from, head: end};
if (hint == "end") return {anchor: end, head: end};
// hint is null, leave the selection alone as much as possible
var adjustPos = function(pos) {
if (posLess(pos, change.from)) return pos;
if (!posLess(change.to, pos)) return end;
var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch;
if (pos.line == change.to.line) ch += end.ch - change.to.ch;
return Pos(line, ch);
};
return {anchor: adjustPos(doc.sel.anchor), head: adjustPos(doc.sel.head)};
}
function filterChange(doc, change, update) {
var obj = {
canceled: false,
from: change.from,
to: change.to,
text: change.text,
origin: change.origin,
cancel: function() { this.canceled = true; }
};
if (update) obj.update = function(from, to, text, origin) {
if (from) this.from = clipPos(doc, from);
if (to) this.to = clipPos(doc, to);
if (text) this.text = text;
if (origin !== undefined) this.origin = origin;
};
signal(doc, "beforeChange", doc, obj);
if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj);
if (obj.canceled) return null;
return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin};
}
// Replace the range from from to to by the strings in replacement.
// change is a {from, to, text [, origin]} object
function makeChange(doc, change, selUpdate, ignoreReadOnly) {
if (doc.cm) {
if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, selUpdate, ignoreReadOnly);
if (doc.cm.state.suppressEdits) return;
}
if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) {
change = filterChange(doc, change, true);
if (!change) return;
}
// Possibly split or suppress the update based on the presence
// of read-only spans in its range.
var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to);
if (split) {
for (var i = split.length - 1; i >= 1; --i)
makeChangeNoReadonly(doc, {from: split[i].from, to: split[i].to, text: [""]});
if (split.length)
makeChangeNoReadonly(doc, {from: split[0].from, to: split[0].to, text: change.text}, selUpdate);
} else {
makeChangeNoReadonly(doc, change, selUpdate);
}
}
function makeChangeNoReadonly(doc, change, selUpdate) {
if (change.text.length == 1 && change.text[0] == "" && posEq(change.from, change.to)) return;
var selAfter = computeSelAfterChange(doc, change, selUpdate);
addToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN);
makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change));
var rebased = [];
linkedDocs(doc, function(doc, sharedHist) {
if (!sharedHist && indexOf(rebased, doc.history) == -1) {
rebaseHist(doc.history, change);
rebased.push(doc.history);
}
if (history) {
var old = [];
doc.iter(from.line, to.line + 1, function(line) { old.push(line.text); });
history.addChange(from.line, newText.length, old);
while (history.done.length > options.undoDepth) history.done.shift();
}
updateLinesNoUndo(from, to, newText, selFrom, selTo);
}
function unredoHelper(from, to) {
if (!from.length) return;
var set = from.pop(), out = [];
for (var i = set.length - 1; i >= 0; i -= 1) {
var change = set[i];
var replaced = [], end = change.start + change.added;
doc.iter(change.start, end, function(line) { replaced.push(line.text); });
out.push({start: change.start, added: change.old.length, old: replaced});
var pos = clipPos({line: change.start + change.old.length - 1,
ch: editEnd(replaced[replaced.length-1], change.old[change.old.length-1])});
updateLinesNoUndo({line: change.start, ch: 0}, {line: end - 1, ch: getLine(end-1).text.length}, change.old, pos, pos);
}
updateInput = true;
to.push(out);
}
function undo() {unredoHelper(history.done, history.undone);}
function redo() {unredoHelper(history.undone, history.done);}
makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change));
});
}
function updateLinesNoUndo(from, to, newText, selFrom, selTo) {
if (suppressEdits) return;
var recomputeMaxLength = false, maxLineLength = maxLine.length;
if (!options.lineWrapping)
doc.iter(from.line, to.line, function(line) {
if (line.text.length == maxLineLength) {recomputeMaxLength = true; return true;}
});
if (from.line != to.line || newText.length > 1) gutterDirty = true;
function makeChangeFromHistory(doc, type) {
if (doc.cm && doc.cm.state.suppressEdits) return;
var nlines = to.line - from.line, firstLine = getLine(from.line), lastLine = getLine(to.line);
// First adjust the line structure, taking some care to leave highlighting intact.
if (from.ch == 0 && to.ch == 0 && newText[newText.length - 1] == "") {
// This is a whole-line replace. Treated specially to make
// sure line objects move the way they are supposed to.
var added = [], prevLine = null;
if (from.line) {
prevLine = getLine(from.line - 1);
prevLine.fixMarkEnds(lastLine);
} else lastLine.fixMarkStarts();
for (var i = 0, e = newText.length - 1; i < e; ++i)
added.push(Line.inheritMarks(newText[i], prevLine));
if (nlines) doc.remove(from.line, nlines, callbacks);
if (added.length) doc.insert(from.line, added);
} else if (firstLine == lastLine) {
if (newText.length == 1)
firstLine.replace(from.ch, to.ch, newText[0]);
else {
lastLine = firstLine.split(to.ch, newText[newText.length-1]);
firstLine.replace(from.ch, null, newText[0]);
firstLine.fixMarkEnds(lastLine);
var added = [];
for (var i = 1, e = newText.length - 1; i < e; ++i)
added.push(Line.inheritMarks(newText[i], firstLine));
added.push(lastLine);
doc.insert(from.line + 1, added);
}
} else if (newText.length == 1) {
firstLine.replace(from.ch, null, newText[0]);
lastLine.replace(null, to.ch, "");
firstLine.append(lastLine);
doc.remove(from.line + 1, nlines, callbacks);
} else {
var added = [];
firstLine.replace(from.ch, null, newText[0]);
lastLine.replace(null, to.ch, newText[newText.length-1]);
firstLine.fixMarkEnds(lastLine);
for (var i = 1, e = newText.length - 1; i < e; ++i)
added.push(Line.inheritMarks(newText[i], firstLine));
if (nlines > 1) doc.remove(from.line + 1, nlines - 1, callbacks);
doc.insert(from.line + 1, added);
}
var hist = doc.history;
var event = (type == "undo" ? hist.done : hist.undone).pop();
if (!event) return;
// Add these lines to the work array, so that they will be
// highlighted. Adjust work lines if lines were added/removed.
var newWork = [], lendiff = newText.length - nlines - 1;
for (var j = 0, l = work.length; j < l; ++j) {
var task = work[j];
if (task < from.line) newWork.push(task);
else if (task > to.line) newWork.push(task + lendiff);
}
var hlEnd = from.line + Math.min(newText.length, 500);
highlightLines(from.line, hlEnd);
newWork.push(hlEnd);
work = newWork;
startWorker(100);
// Remember that these lines changed, for updating the display
changes.push({from: from.line, to: to.line + 1, diff: lendiff});
var changeObj = {from: from, to: to, text: newText};
if (textChanged) {
for (var cur = textChanged; cur.next; cur = cur.next) {}
cur.next = changeObj;
} else textChanged = changeObj;
var anti = {changes: [], anchorBefore: event.anchorAfter, headBefore: event.headAfter,
anchorAfter: event.anchorBefore, headAfter: event.headBefore,
generation: hist.generation};
(type == "undo" ? hist.undone : hist.done).push(anti);
hist.generation = event.generation || ++hist.maxGeneration;
if (options.lineWrapping) {
var perLine = scroller.clientWidth / charWidth() - 3;
doc.iter(from.line, from.line + newText.length, function(line) {
if (line.hidden) return;
var guess = Math.ceil(line.text.length / perLine) || 1;
if (line.widgetFunction)
guess = line.widgetFunction.size(line.text).height / textHeight();
if (guess != line.height) updateLineHeight(line, guess);
});
} else {
doc.iter(from.line, from.line + newText.length, function(line) {
var l = line.text;
if (l.length > maxLineLength) {
maxLine = l; maxLineLength = l.length; maxWidth = null;
recomputeMaxLength = false;
}
if (line.widgetFunction) {
var guess = line.widgetFunction.size(line.text).height / textHeight();
if (guess != line.height) updateLineHeight(line, guess);
} else if (line.height != 1) {
updateLineHeight(line, 1);
}
});
if (recomputeMaxLength) {
maxLineLength = 0; maxLine = ""; maxWidth = null;
doc.iter(0, doc.size, function(line) {
var l = line.text;
if (l.length > maxLineLength) {
maxLineLength = l.length; maxLine = l;
}
});
}
}
var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange");
// Update the selection
function updateLine(n) {return n <= Math.min(to.line, to.line + lendiff) ? n : n + lendiff;}
setSelection(selFrom, selTo, updateLine(sel.from.line), updateLine(sel.to.line));
// Make sure the scroll-size div has the correct height.
if (scroller.clientHeight)
code.style.height = (doc.height * textHeight() + 2 * paddingTop()) + "px";
}
function replaceRange(code, from, to) {
from = clipPos(from);
if (!to) to = from; else to = clipPos(to);
code = splitLines(code);
function adjustPos(pos) {
if (posLess(pos, from)) return pos;
if (!posLess(to, pos)) return end;
var line = pos.line + code.length - (to.line - from.line) - 1;
var ch = pos.ch;
if (pos.line == to.line)
ch += code[code.length-1].length - (to.ch - (to.line == from.line ? from.ch : 0));
return {line: line, ch: ch};
}
var end;
replaceRange1(code, from, to, function(end1) {
end = end1;
return {from: adjustPos(sel.from), to: adjustPos(sel.to)};
});
return end;
}
function replaceSelection(code, collapse) {
replaceRange1(splitLines(code), sel.from, sel.to, function(end) {
if (collapse == "end") return {from: end, to: end};
else if (collapse == "start") return {from: sel.from, to: sel.from};
else return {from: sel.from, to: end};
});
}
function replaceRange1(code, from, to, computeSel) {
var endch = code.length == 1 ? code[0].length + from.ch : code[code.length-1].length;
var newSel = computeSel({line: from.line + code.length - 1, ch: endch});
updateLines(from, to, code, newSel.from, newSel.to);
}
function getRange(from, to) {
var l1 = from.line, l2 = to.line;
if (l1 == l2) return getLine(l1).text.slice(from.ch, to.ch);
var code = [getLine(l1).text.slice(from.ch)];
doc.iter(l1 + 1, l2, function(line) { code.push(line.text); });
code.push(getLine(l2).text.slice(0, to.ch));
return code.join("\n");
}
function getSelection() {
return getRange(sel.from, sel.to);
}
var pollingFast = false; // Ensures slowPoll doesn't cancel fastPoll
function slowPoll() {
if (pollingFast) return;
poll.set(options.pollInterval, function() {
startOperation();
readInput();
if (focused) slowPoll();
endOperation();
});
}
function fastPoll() {
var missed = false;
pollingFast = true;
function p() {
startOperation();
var changed = readInput();
if (!changed && !missed) {missed = true; poll.set(60, p);}
else {pollingFast = false; slowPoll();}
endOperation();
}
poll.set(20, p);
}
// Previnput is a hack to work with IME. If we reset the textarea
// on every change, that breaks IME. So we look for changes
// compared to the previous content instead. (Modern browsers have
// events that indicate IME taking place, but these are not widely
// supported or compatible enough yet to rely on.)
var prevInput = "";
function readInput() {
if (leaveInputAlone || !focused || hasSelection(input) || options.readOnly) return false;
var text = input.value;
if (text == prevInput) return false;
shiftSelecting = null;
var same = 0, l = Math.min(prevInput.length, text.length);
while (same < l && prevInput[same] == text[same]) ++same;
if (same < prevInput.length)
sel.from = {line: sel.from.line, ch: sel.from.ch - (prevInput.length - same)};
else if (overwrite && posEq(sel.from, sel.to))
sel.to = {line: sel.to.line, ch: Math.min(getLine(sel.to.line).text.length, sel.to.ch + (text.length - same))};
replaceSelection(text.slice(same), "end");
prevInput = text;
return true;
}
function resetInput(user) {
if (!posEq(sel.from, sel.to)) {
prevInput = "";
input.value = getSelection();
selectInput(input);
} else if (user) prevInput = input.value = "";
}
function focusInput() {
if (options.readOnly != "nocursor") input.focus();
}
function scrollEditorIntoView() {
if (!cursor.getBoundingClientRect) return;
var rect = cursor.getBoundingClientRect();
// IE returns bogus coordinates when the instance sits inside of an iframe and the cursor is hidden
if (ie && rect.top == rect.bottom) return;
var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight);
if (rect.top < 0 || rect.bottom > winH) cursor.scrollIntoView();
}
function scrollCursorIntoView() {
var cursor = localCoords(sel.inverted ? sel.from : sel.to);
var x = options.lineWrapping ? Math.min(cursor.x, lineSpace.offsetWidth) : cursor.x;
return scrollIntoView(x, cursor.y, x, cursor.yBot);
}
function scrollIntoView(x1, y1, x2, y2) {
var pl = paddingLeft(), pt = paddingTop();
y1 += pt; y2 += pt; x1 += pl; x2 += pl;
var screen = scroller.clientHeight, screentop = scroller.scrollTop, scrolled = false, result = true;
if (y1 < screentop) {scroller.scrollTop = Math.max(0, y1); scrolled = true;}
else if (y2 > screentop + screen) {scroller.scrollTop = y2 - screen; scrolled = true;}
var screenw = scroller.clientWidth, screenleft = scroller.scrollLeft;
var gutterw = options.fixedGutter ? gutter.clientWidth : 0;
if (x1 < screenleft + gutterw) {
if (x1 < 50) x1 = 0;
scroller.scrollLeft = Math.max(0, x1 - 10 - gutterw);
scrolled = true;
}
else if (x2 > screenw + screenleft - 3) {
scroller.scrollLeft = x2 + 10 - screenw;
scrolled = true;
if (x2 > code.clientWidth) result = false;
}
if (scrolled && options.onScroll) options.onScroll(instance);
return result;
}
function visibleLines() {
var lh = textHeight(), top = scroller.scrollTop - paddingTop();
var from_height = Math.max(0, Math.floor(top / lh));
var to_height = Math.ceil((top + scroller.clientHeight) / lh);
return {from: lineAtHeight(doc, from_height),
to: lineAtHeight(doc, to_height)};
}
// Uses a set of changes plus the current scroll position to
// determine which DOM updates have to be made, and makes the
// updates.
function updateDisplay(changes, suppressCallback) {
if (!scroller.clientWidth) {
showingFrom = showingTo = displayOffset = 0;
for (var i = event.changes.length - 1; i >= 0; --i) {
var change = event.changes[i];
change.origin = type;
if (filter && !filterChange(doc, change, false)) {
(type == "undo" ? hist.done : hist.undone).length = 0;
return;
}
// Compute the new visible window
var visible = visibleLines();
// Bail out if the visible area is already rendered and nothing changed.
if (changes !== true && changes.length == 0 && visible.from > showingFrom && visible.to < showingTo) return;
var from = Math.max(visible.from - 100, 0), to = Math.min(doc.size, visible.to + 100);
if (showingFrom < from && from - showingFrom < 20) from = showingFrom;
if (showingTo > to && showingTo - to < 20) to = Math.min(doc.size, showingTo);
// Create a range of theoretically intact lines, and punch holes
// in that using the change info.
var intact = changes === true ? [] :
computeIntact([{from: showingFrom, to: showingTo, domStart: 0}], changes);
// Clip off the parts that won't be visible
var intactLines = 0;
for (var i = 0; i < intact.length; ++i) {
var range = intact[i];
if (range.from < from) {range.domStart += (from - range.from); range.from = from;}
if (range.to > to) range.to = to;
if (range.from >= range.to) intact.splice(i--, 1);
else intactLines += range.to - range.from;
}
if (intactLines == to - from) return;
intact.sort(function(a, b) {return a.domStart - b.domStart;});
anti.changes.push(historyChangeFromChange(doc, change));
var th = textHeight(), gutterDisplay = gutter.style.display;
lineDiv.style.display = "none";
patchDisplay(from, to, intact);
lineDiv.style.display = gutter.style.display = "";
var after = i ? computeSelAfterChange(doc, change, null)
: {anchor: event.anchorBefore, head: event.headBefore};
makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change));
var rebased = [];
// Position the mover div to align with the lines it's supposed
// to be showing (which will cover the visible display)
var different = from != showingFrom || to != showingTo || lastSizeC != scroller.clientHeight + th;
// This is just a bogus formula that detects when the editor is
// resized or the font size changes.
if (different) lastSizeC = scroller.clientHeight + th;
showingFrom = from; showingTo = to;
displayOffset = heightAtLine(doc, from);
mover.style.top = (displayOffset * th) + "px";
if (scroller.clientHeight)
code.style.height = (doc.height * th + 2 * paddingTop()) + "px";
// Since this is all rather error prone, it is honoured with the
// only assertion in the whole file.
if (lineDiv.childNodes.length != showingTo - showingFrom)
throw new Error("BAD PATCH! " + JSON.stringify(intact) + " size=" + (showingTo - showingFrom) +
" nodes=" + lineDiv.childNodes.length);
if (options.lineWrapping) {
maxWidth = scroller.clientWidth;
var curNode = lineDiv.firstChild, heightChanged = false;
doc.iter(showingFrom, showingTo, function(line) {
if (!line.hidden) {
var height = Math.round(curNode.offsetHeight / th) || 1;
if (line.widgetFunction) height = line.widgetFunction.size(line.text).height / textHeight();
if (line.height != height) {
updateLineHeight(line, height);
gutterDirty = heightChanged = true;
}
}
curNode = curNode.nextSibling;
});
if (heightChanged)
code.style.height = (doc.height * th + 2 * paddingTop()) + "px";
} else {
if (maxWidth == null) maxWidth = stringWidth(maxLine);
if (maxWidth > scroller.clientWidth) {
lineSpace.style.width = maxWidth + "px";
// Needed to prevent odd wrapping/hiding of widgets placed in here.
code.style.width = "";
code.style.width = scroller.scrollWidth + "px";
} else {
lineSpace.style.width = code.style.width = "";
linkedDocs(doc, function(doc, sharedHist) {
if (!sharedHist && indexOf(rebased, doc.history) == -1) {
rebaseHist(doc.history, change);
rebased.push(doc.history);
}
}
gutter.style.display = gutterDisplay;
if (different || gutterDirty) updateGutter();
updateSelection();
if (!suppressCallback && options.onUpdate) options.onUpdate(instance);
return true;
}
function computeIntact(intact, changes) {
for (var i = 0, l = changes.length || 0; i < l; ++i) {
var change = changes[i], intact2 = [], diff = change.diff || 0;
for (var j = 0, l2 = intact.length; j < l2; ++j) {
var range = intact[j];
if (change.to <= range.from && change.diff)
intact2.push({from: range.from + diff, to: range.to + diff,
domStart: range.domStart});
else if (change.to <= range.from || change.from >= range.to)
intact2.push(range);
else {
if (change.from > range.from)
intact2.push({from: range.from, to: change.from, domStart: range.domStart});
if (change.to < range.to)
intact2.push({from: change.to + diff, to: range.to + diff,
domStart: range.domStart + (change.to - range.from)});
}
}
intact = intact2;
}
return intact;
}
function patchDisplay(from, to, intact) {
// The first pass removes the DOM nodes that aren't intact.
if (!intact.length) lineDiv.innerHTML = "";
else {
function killNode(node) {
var tmp = node.nextSibling;
node.parentNode.removeChild(node);
return tmp;
}
var domPos = 0, curNode = lineDiv.firstChild, n;
for (var i = 0; i < intact.length; ++i) {
var cur = intact[i];
while (cur.domStart > domPos) {curNode = killNode(curNode); domPos++;}
for (var j = 0, e = cur.to - cur.from; j < e; ++j) {curNode = curNode.nextSibling; domPos++;}
}
while (curNode) curNode = killNode(curNode);
}
// This pass fills in the lines that actually changed.
var nextIntact = intact.shift(), curNode = lineDiv.firstChild, j = from;
var scratch = document.createElement("div");
doc.iter(from, to, function(line) {
if (nextIntact && nextIntact.to == j) nextIntact = intact.shift();
if (!nextIntact || nextIntact.from > j) {
if (line.hidden) var html = scratch.innerHTML = "<pre></pre>";
else {
var html = line.getHTML(makeTab);
if (!line.widgetFunction) {
html = '<pre' + (line.className ? ' class="' + line.className + '"' : '') + '>' + html + '</pre>';
}
// Kludge to make sure the styled element lies behind the selection (by z-index)
if (line.bgClassName)
html = '<div style="position: relative"><pre class="' + line.bgClassName +
'" style="position: absolute; left: 0; right: 0; top: 0; bottom: 0; z-index: -2">&#160;</pre>' + html + "</div>";
}
scratch.innerHTML = html;
var insertChild = scratch.firstChild;
lineDiv.insertBefore(insertChild, curNode);
line.nodeAdded(insertChild);
} else {
curNode = curNode.nextSibling;
}
++j;
makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change));
});
}
}
function updateGutter() {
if (!options.gutter && !options.lineNumbers) return;
var hText = mover.offsetHeight, hEditor = scroller.clientHeight;
gutter.style.height = (hText - hEditor < 2 ? hEditor : hText) + "px";
var html = [], i = showingFrom, normalNode;
doc.iter(showingFrom, Math.max(showingTo, showingFrom + 1), function(line) {
if (line.hidden) {
html.push("<pre></pre>");
} else {
var marker = line.gutterMarker;
var text = options.lineNumbers ? i + options.firstLineNumber : null;
if (marker && marker.text)
text = marker.text.replace("%N%", text != null ? text : "");
else if (text == null)
text = "\u00a0";
html.push((marker && marker.style ? '<pre class="' + marker.style + '">' : "<pre>"), text);
for (var j = 1; j < line.height; ++j) html.push("<br/>&#160;");
html.push("</pre>");
if (!marker) normalNode = i;
}
++i;
});
gutter.style.display = "none";
gutterText.innerHTML = html.join("");
// Make sure scrolling doesn't cause number gutter size to pop
if (normalNode != null) {
var node = gutterText.childNodes[normalNode - showingFrom];
var minwidth = String(doc.size).length, val = eltText(node), pad = "";
while (val.length + pad.length < minwidth) pad += "\u00a0";
if (pad) node.insertBefore(document.createTextNode(pad), node.firstChild);
}
gutter.style.display = "";
lineSpace.style.marginLeft = gutter.offsetWidth + "px";
gutterDirty = false;
function shiftDoc(doc, distance) {
function shiftPos(pos) {return Pos(pos.line + distance, pos.ch);}
doc.first += distance;
if (doc.cm) regChange(doc.cm, doc.first, doc.first, distance);
doc.sel.head = shiftPos(doc.sel.head); doc.sel.anchor = shiftPos(doc.sel.anchor);
doc.sel.from = shiftPos(doc.sel.from); doc.sel.to = shiftPos(doc.sel.to);
}
function makeChangeSingleDoc(doc, change, selAfter, spans) {
if (doc.cm && !doc.cm.curOp)
return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans);
if (change.to.line < doc.first) {
shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line));
return;
}
function updateSelection() {
var collapsed = posEq(sel.from, sel.to);
var fromPos = localCoords(sel.from, true);
var toPos = collapsed ? fromPos : localCoords(sel.to, true);
var headPos = sel.inverted ? fromPos : toPos, th = textHeight();
var wrapOff = eltOffset(wrapper), lineOff = eltOffset(lineDiv);
inputDiv.style.top = Math.max(0, Math.min(scroller.offsetHeight, headPos.y + lineOff.top - wrapOff.top)) + "px";
inputDiv.style.left = Math.max(0, Math.min(scroller.offsetWidth, headPos.x + lineOff.left - wrapOff.left)) + "px";
if (collapsed) {
cursor.style.top = headPos.y + "px";
cursor.style.left = (options.lineWrapping ? Math.min(headPos.x, lineSpace.offsetWidth) : headPos.x) + "px";
cursor.style.display = "";
selectionDiv.style.display = "none";
} else {
var sameLine = fromPos.y == toPos.y, html = "";
function add(left, top, right, height) {
html += '<div class="CodeMirror-selected" style="position: absolute; left: ' + left +
'px; top: ' + top + 'px; right: ' + right + 'px; height: ' + height + 'px"></div>';
}
var clientWidth = lineSpace.clientWidth || lineSpace.offsetWidth;
var clientHeight = lineSpace.clientHeight || lineSpace.offsetHeight;
if (sel.from.ch && fromPos.y >= 0) {
var right = sameLine ? clientWidth - toPos.x : 0;
add(fromPos.x, fromPos.y, right, th);
}
var middleStart = Math.max(0, fromPos.y + (sel.from.ch ? th : 0));
var middleHeight = Math.min(toPos.y, clientHeight) - middleStart;
if (middleHeight > 0.2 * th)
add(0, middleStart, 0, middleHeight);
if ((!sameLine || !sel.from.ch) && toPos.y < clientHeight - .5 * th)
add(0, toPos.y, clientWidth - toPos.x, th);
selectionDiv.innerHTML = html;
cursor.style.display = "none";
selectionDiv.style.display = "";
}
if (change.from.line > doc.lastLine()) return;
// Clip the change to the size of this doc
if (change.from.line < doc.first) {
var shift = change.text.length - 1 - (doc.first - change.from.line);
shiftDoc(doc, shift);
change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch),
text: [lst(change.text)], origin: change.origin};
}
var last = doc.lastLine();
if (change.to.line > last) {
change = {from: change.from, to: Pos(last, getLine(doc, last).text.length),
text: [change.text[0]], origin: change.origin};
}
function setShift(val) {
if (val) shiftSelecting = shiftSelecting || (sel.inverted ? sel.to : sel.from);
else shiftSelecting = null;
}
function setSelectionUser(from, to) {
var sh = shiftSelecting && clipPos(shiftSelecting);
if (sh) {
if (posLess(sh, from)) from = sh;
else if (posLess(to, sh)) to = sh;
}
setSelection(from, to);
userSelChange = true;
}
// Update the selection. Last two args are only used by
// updateLines, since they have to be expressed in the line
// numbers before the update.
function setSelection(from, to, oldFrom, oldTo) {
goalColumn = null;
if (oldFrom == null) {oldFrom = sel.from.line; oldTo = sel.to.line;}
if (posEq(sel.from, from) && posEq(sel.to, to)) return;
if (posLess(to, from)) {var tmp = to; to = from; from = tmp;}
change.removed = getBetween(doc, change.from, change.to);
// Skip over hidden lines.
if (from.line != oldFrom) {
var from1 = skipHidden(from, oldFrom, sel.from.ch);
// If there is no non-hidden line left, force visibility on current line
if (!from1) setLineHidden(from.line, false);
else from = from1;
}
if (to.line != oldTo) to = skipHidden(to, oldTo, sel.to.ch);
if (!selAfter) selAfter = computeSelAfterChange(doc, change, null);
if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans, selAfter);
else updateDoc(doc, change, spans, selAfter);
}
if (posEq(from, to)) sel.inverted = false;
else if (posEq(from, sel.to)) sel.inverted = false;
else if (posEq(to, sel.from)) sel.inverted = true;
function makeChangeSingleDocInEditor(cm, change, spans, selAfter) {
var doc = cm.doc, display = cm.display, from = change.from, to = change.to;
if (options.autoClearEmptyLines && posEq(sel.from, sel.to)) {
var head = sel.inverted ? from : to;
if (head.line != sel.from.line && sel.from.line < doc.size) {
var oldLine = getLine(sel.from.line);
if (/^\s+$/.test(oldLine.text))
setTimeout(operation(function() {
if (oldLine.parent && /^\s+$/.test(oldLine.text)) {
var no = lineNo(oldLine);
replaceRange("", {line: no, ch: 0}, {line: no, ch: oldLine.text.length});
}
}, 10));
}
}
sel.from = from; sel.to = to;
selectionChanged = true;
}
function skipHidden(pos, oldLine, oldCh) {
function getNonHidden(dir) {
var lNo = pos.line + dir, end = dir == 1 ? doc.size : -1;
while (lNo != end) {
var line = getLine(lNo);
if (!line.hidden) {
var ch = pos.ch;
if (ch > oldCh || ch > line.text.length) ch = line.text.length;
return {line: lNo, ch: ch};
}
lNo += dir;
}
}
var line = getLine(pos.line);
if (!line.hidden) return pos;
if (pos.line >= oldLine) return getNonHidden(1) || getNonHidden(-1);
else return getNonHidden(-1) || getNonHidden(1);
}
function setCursor(line, ch, user) {
var pos = clipPos({line: line, ch: ch || 0});
(user ? setSelectionUser : setSelection)(pos, pos);
}
function clipLine(n) {return Math.max(0, Math.min(n, doc.size-1));}
function clipPos(pos) {
if (pos.line < 0) return {line: 0, ch: 0};
if (pos.line >= doc.size) return {line: doc.size-1, ch: getLine(doc.size-1).text.length};
var ch = pos.ch, line = getLine(pos.line), linelen =line.text.length;
if (line.widgetFunction && ch != 0) return {line: pos.line, ch: linelen};
if (ch == null || ch > linelen) return {line: pos.line, ch: linelen};
else if (ch < 0) return {line: pos.line, ch: 0};
else return pos;
}
function findPosH(dir, unit) {
var end = sel.inverted ? sel.from : sel.to, line = end.line, ch = end.ch;
var lineObj = getLine(line);
function findNextLine() {
for (var l = line + dir, e = dir < 0 ? -1 : doc.size; l != e; l += dir) {
var lo = getLine(l);
if (!lo.hidden) { line = l; lineObj = lo; return true; }
}
}
function moveOnce(boundToLine) {
if (ch == (dir < 0 ? 0 : lineObj.text.length)) {
if (!boundToLine && findNextLine()) ch = dir < 0 ? lineObj.text.length : 0;
else return false;
} else if (lineObj.widgetFunction) { //Select the entire line
ch = dir < 0 ? 0 : lineObj.text.length;
} else ch += dir;
return true;
}
if (unit == "char") moveOnce();
else if (unit == "column") moveOnce(true);
else if (unit == "word") {
var sawWord = false;
for (;;) {
if (dir < 0) if (!moveOnce()) break;
if (isWordChar(lineObj.text.charAt(ch))) sawWord = true;
else if (sawWord) {if (dir < 0) {dir = 1; moveOnce();} break;}
if (dir > 0) if (!moveOnce()) break;
}
}
return {line: line, ch: ch};
}
function moveH(dir, unit) {
var pos = dir < 0 ? sel.from : sel.to;
if (shiftSelecting || posEq(sel.from, sel.to)) pos = findPosH(dir, unit);
setCursor(pos.line, pos.ch, true);
}
function deleteH(dir, unit) {
var from = sel.from;
var to = sel.to;
if (posEq(sel.from, sel.to)) {
if (dir < 0) {
from = findPosH(dir, unit);
if (getLine(from.line).widgetFunction) from.ch = 0;
}
else {
to = findPosH(dir, unit);
if (getLine(to.line).widgetFunction) to.ch = getLine(to.line).text.length;
}
}
replaceRange("", from, to);
userSelChange = true;
}
var goalColumn = null;
function moveV(dir, unit) {
var dist = 0, pos = sel.inverted ? sel.from : sel.to, loc = localCoords(pos, true);
if (goalColumn != null) pos.x = goalColumn;
if (unit == "page") dist = Math.min(scroller.clientHeight, window.innerHeight || document.documentElement.clientHeight);
else if (unit == "line") {
var line = getLine(pos.line);
if (dir > 0 && line.widgetFunction) dist = Math.ceil(getLine(pos.line).height);
else dist = 1;
dist *= textHeight();
}
var target = coordsChar(loc.x, loc.y + dist * dir + 2);
if (unit == "page") scroller.scrollTop += localCoords(target, true).y - loc.y;
setCursor(target.line, target.ch, true);
goalColumn = loc.x;
}
function selectWordAt(pos) {
var line = getLine(pos.line).text;
var start = pos.ch, end = pos.ch;
while (start > 0 && isWordChar(line.charAt(start - 1))) --start;
while (end < line.length && isWordChar(line.charAt(end))) ++end;
setSelectionUser({line: pos.line, ch: start}, {line: pos.line, ch: end});
}
function selectLine(line) {
setSelectionUser({line: line, ch: 0}, clipPos({line: line + 1, ch: 0}));
}
function indentSelected(mode) {
if (posEq(sel.from, sel.to)) return indentLine(sel.from.line, mode);
var e = sel.to.line - (sel.to.ch ? 0 : 1);
for (var i = sel.from.line; i <= e; ++i) indentLine(i, mode);
}
function indentLine(n, how) {
if (!how) how = "add";
if (how == "smart") {
if (!mode.indent) how = "prev";
else var state = getStateBefore(n);
}
var line = getLine(n), curSpace = line.indentation(options.tabSize),
curSpaceString = line.text.match(/^\s*/)[0], indentation;
if (how == "prev") {
if (n) indentation = getLine(n-1).indentation(options.tabSize);
else indentation = 0;
}
else if (how == "smart") indentation = mode.indent(state, line.text.slice(curSpaceString.length), line.text);
else if (how == "add") indentation = curSpace + options.indentUnit;
else if (how == "subtract") indentation = curSpace - options.indentUnit;
indentation = Math.max(0, indentation);
var diff = indentation - curSpace;
if (!diff) {
if (sel.from.line != n && sel.to.line != n) return;
var indentString = curSpaceString;
}
else {
var indentString = "", pos = 0;
if (options.indentWithTabs)
for (var i = Math.floor(indentation / options.tabSize); i; --i) {pos += options.tabSize; indentString += "\t";}
while (pos < indentation) {++pos; indentString += " ";}
}
replaceRange(indentString, {line: n, ch: 0}, {line: n, ch: curSpaceString.length});
}
function loadMode() {
mode = CodeMirror.getMode(options, options.mode);
doc.iter(0, doc.size, function(line) { line.stateAfter = null; });
work = [0];
startWorker();
}
function gutterChanged() {
var visible = options.gutter || options.lineNumbers;
gutter.style.display = visible ? "" : "none";
if (visible) gutterDirty = true;
else lineDiv.parentNode.style.marginLeft = 0;
}
function wrappingChanged(from, to) {
if (options.lineWrapping) {
wrapper.className += " CodeMirror-wrap";
var perLine = scroller.clientWidth / charWidth() - 3;
doc.iter(0, doc.size, function(line) {
if (line.hidden) return;
var guess = Math.ceil(line.text.length / perLine) || 1;
if (guess != 1) updateLineHeight(line, guess);
});
lineSpace.style.width = code.style.width = "";
} else {
wrapper.className = wrapper.className.replace(" CodeMirror-wrap", "");
maxWidth = null; maxLine = "";
doc.iter(0, doc.size, function(line) {
if (line.height != 1 && !line.hidden && !line.widgetFunction) updateLineHeight(line, 1);
if (line.text.length > maxLine.length) maxLine = line.text;
});
}
changes.push({from: 0, to: doc.size});
}
function makeTab(col) {
var w = options.tabSize - col % options.tabSize, cached = tabCache[w];
if (cached) return cached;
for (var str = '<span class="cm-tab">', i = 0; i < w; ++i) str += " ";
return (tabCache[w] = {html: str + "</span>", width: w});
}
function themeChanged() {
scroller.className = scroller.className.replace(/\s*cm-s-\w+/g, "") +
options.theme.replace(/(^|\s)\s*/g, " cm-s-");
}
function TextMarker() { this.set = []; }
TextMarker.prototype.clear = operation(function() {
var min = Infinity, max = -Infinity;
for (var i = 0, e = this.set.length; i < e; ++i) {
var line = this.set[i], mk = line.marked;
if (!mk || !line.parent) continue;
var lineN = lineNo(line);
min = Math.min(min, lineN); max = Math.max(max, lineN);
for (var j = 0; j < mk.length; ++j)
if (mk[j].marker == this) mk.splice(j--, 1);
}
if (min != Infinity)
changes.push({from: min, to: max + 1});
});
TextMarker.prototype.find = function() {
var from, to;
for (var i = 0, e = this.set.length; i < e; ++i) {
var line = this.set[i], mk = line.marked;
for (var j = 0; j < mk.length; ++j) {
var mark = mk[j];
if (mark.marker == this) {
if (mark.from != null || mark.to != null) {
var found = lineNo(line);
if (found != null) {
if (mark.from != null) from = {line: found, ch: mark.from};
if (mark.to != null) to = {line: found, ch: mark.to};
}
}
}
}
}
return {from: from, to: to};
};
function markText(from, to, className) {
from = clipPos(from); to = clipPos(to);
var tm = new TextMarker();
if (!posLess(from, to)) return tm;
function add(line, from, to, className) {
getLine(line).addMark(new MarkedText(from, to, className, tm));
}
if (from.line == to.line) add(from.line, from.ch, to.ch, className);
else {
add(from.line, from.ch, null, className);
for (var i = from.line + 1, e = to.line; i < e; ++i)
add(i, null, null, className);
add(to.line, null, to.ch, className);
}
changes.push({from: from.line, to: to.line + 1});
return tm;
}
function setBookmark(pos) {
pos = clipPos(pos);
var bm = new Bookmark(pos.ch);
getLine(pos.line).addMark(bm);
return bm;
}
function findMarksAt(pos) {
pos = clipPos(pos);
var markers = [], marked = getLine(pos.line).marked;
if (!marked) return markers;
for (var i = 0, e = marked.length; i < e; ++i) {
var m = marked[i];
if ((m.from == null || m.from <= pos.ch) &&
(m.to == null || m.to >= pos.ch))
markers.push(m.marker || m);
}
return markers;
}
function addGutterMarker(line, text, className) {
if (typeof line == "number") line = getLine(clipLine(line));
line.gutterMarker = {text: text, style: className};
gutterDirty = true;
return line;
}
function removeGutterMarker(line) {
if (typeof line == "number") line = getLine(clipLine(line));
line.gutterMarker = null;
gutterDirty = true;
}
function changeLine(handle, op) {
var no = handle, line = handle;
if (typeof handle == "number") line = getLine(clipLine(handle));
else no = lineNo(handle);
if (no == null) return null;
if (op(line, no)) changes.push({from: no, to: no + 1});
else return null;
return line;
}
function setLineClass(handle, className, bgClassName) {
return changeLine(handle, function(line) {
if (line.className != className || line.bgClassName != bgClassName) {
line.className = className;
line.bgClassName = bgClassName;
var recomputeMaxLength = false, checkWidthStart = from.line;
if (!cm.options.lineWrapping) {
checkWidthStart = lineNo(visualLine(doc, getLine(doc, from.line)));
doc.iter(checkWidthStart, to.line + 1, function(line) {
if (line == display.maxLine) {
recomputeMaxLength = true;
return true;
}
});
}
function setLineHidden(handle, hidden) {
return changeLine(handle, function(line, no) {
if (line.hidden != hidden) {
line.hidden = hidden;
updateLineHeight(line, hidden ? 0 : 1);
var fline = sel.from.line, tline = sel.to.line;
if (hidden && (fline == no || tline == no)) {
var from = fline == no ? skipHidden({line: fline, ch: 0}, fline, 0) : sel.from;
var to = tline == no ? skipHidden({line: tline, ch: 0}, tline, 0) : sel.to;
// Can't hide the last visible line, we'd have no place to put the cursor
if (!to) return;
setSelection(from, to);
}
return (gutterDirty = true);
if (!posLess(doc.sel.head, change.from) && !posLess(change.to, doc.sel.head))
cm.curOp.cursorActivity = true;
updateDoc(doc, change, spans, selAfter, estimateHeight(cm));
if (!cm.options.lineWrapping) {
doc.iter(checkWidthStart, from.line + change.text.length, function(line) {
var len = lineLength(doc, line);
if (len > display.maxLineLength) {
display.maxLine = line;
display.maxLineLength = len;
display.maxLineChanged = true;
recomputeMaxLength = false;
}
});
if (recomputeMaxLength) cm.curOp.updateMaxLine = true;
}
function lineInfo(line) {
if (typeof line == "number") {
if (!isLine(line)) return null;
var n = line;
line = getLine(line);
if (!line) return null;
// Adjust frontier, schedule worker
doc.frontier = Math.min(doc.frontier, from.line);
startWorker(cm, 400);
var lendiff = change.text.length - (to.line - from.line) - 1;
// Remember that these lines changed, for updating the display
regChange(cm, from.line, to.line + 1, lendiff);
if (hasHandler(cm, "change")) {
var changeObj = {from: from, to: to,
text: change.text,
removed: change.removed,
origin: change.origin};
if (cm.curOp.textChanged) {
for (var cur = cm.curOp.textChanged; cur.next; cur = cur.next) {}
cur.next = changeObj;
} else cm.curOp.textChanged = changeObj;
}
}
function replaceRange(doc, code, from, to, origin) {
if (!to) to = from;
if (posLess(to, from)) { var tmp = to; to = from; from = tmp; }
if (typeof code == "string") code = splitLines(code);
makeChange(doc, {from: from, to: to, text: code, origin: origin}, null);
}
// POSITION OBJECT
function Pos(line, ch) {
if (!(this instanceof Pos)) return new Pos(line, ch);
this.line = line; this.ch = ch;
}
CodeMirror.Pos = Pos;
function posEq(a, b) {return a.line == b.line && a.ch == b.ch;}
function posLess(a, b) {return a.line < b.line || (a.line == b.line && a.ch < b.ch);}
function cmp(a, b) {return a.line - b.line || a.ch - b.ch;}
function copyPos(x) {return Pos(x.line, x.ch);}
// SELECTION
function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1));}
function clipPos(doc, pos) {
if (pos.line < doc.first) return Pos(doc.first, 0);
var last = doc.first + doc.size - 1;
if (pos.line > last) return Pos(last, getLine(doc, last).text.length);
return clipToLen(pos, getLine(doc, pos.line).text.length);
}
function clipToLen(pos, linelen) {
var ch = pos.ch;
if (ch == null || ch > linelen) return Pos(pos.line, linelen);
else if (ch < 0) return Pos(pos.line, 0);
else return pos;
}
function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size;}
// If shift is held, this will move the selection anchor. Otherwise,
// it'll set the whole selection.
function extendSelection(doc, pos, other, bias) {
if (doc.sel.shift || doc.sel.extend) {
var anchor = doc.sel.anchor;
if (other) {
var posBefore = posLess(pos, anchor);
if (posBefore != posLess(other, anchor)) {
anchor = pos;
pos = other;
} else if (posBefore != posLess(pos, other)) {
pos = other;
}
}
else {
setSelection(doc, anchor, pos, bias);
} else {
setSelection(doc, pos, other || pos, bias);
}
if (doc.cm) doc.cm.curOp.userSelChange = true;
if (doc.cm) {
var from = doc.sel.from;
var to = doc.sel.to;
if (posEq(from, to) && doc.cm.display.input.setSelectionRange) {
clearTimeout(doc.cm.state.accessibleTextareaTimeout);
doc.cm.state.accessibleTextareaWaiting = true;
doc.cm.display.input.value = doc.getLine(from.line) + "\n";
doc.cm.display.input.setSelectionRange(from.ch, from.ch);
doc.cm.state.accessibleTextareaTimeout = setTimeout(function() {
clearAccessibleTextarea(doc.cm);
}, 80);
}
}
}
function clearAccessibleTextarea(cm) {
clearTimeout(cm.state.accessibleTextareaTimeout);
cm.state.accessibleTextareaWaiting = false;
resetInput(cm, true);
}
function filterSelectionChange(doc, anchor, head) {
var obj = {anchor: anchor, head: head};
signal(doc, "beforeSelectionChange", doc, obj);
if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj);
obj.anchor = clipPos(doc, obj.anchor); obj.head = clipPos(doc, obj.head);
return obj;
}
// Update the selection. Last two args are only used by
// updateDoc, since they have to be expressed in the line
// numbers before the update.
function setSelection(doc, anchor, head, bias, checkAtomic) {
if (!checkAtomic && hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) {
var filtered = filterSelectionChange(doc, anchor, head);
head = filtered.head;
anchor = filtered.anchor;
}
var sel = doc.sel;
sel.goalColumn = null;
if (bias == null) bias = posLess(head, sel.head) ? -1 : 1;
// Skip over atomic spans.
if (checkAtomic || !posEq(anchor, sel.anchor))
anchor = skipAtomic(doc, anchor, bias, checkAtomic != "push");
if (checkAtomic || !posEq(head, sel.head))
head = skipAtomic(doc, head, bias, checkAtomic != "push");
if (posEq(sel.anchor, anchor) && posEq(sel.head, head)) return;
sel.anchor = anchor; sel.head = head;
var inv = posLess(head, anchor);
sel.from = inv ? head : anchor;
sel.to = inv ? anchor : head;
if (doc.cm)
doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged =
doc.cm.curOp.cursorActivity = true;
signalLater(doc, "cursorActivity", doc);
}
function reCheckSelection(cm) {
setSelection(cm.doc, cm.doc.sel.from, cm.doc.sel.to, null, "push");
}
function skipAtomic(doc, pos, bias, mayClear) {
var flipped = false, curPos = pos;
var dir = bias || 1;
doc.cantEdit = false;
search: for (;;) {
var line = getLine(doc, curPos.line);
if (line.markedSpans) {
for (var i = 0; i < line.markedSpans.length; ++i) {
var sp = line.markedSpans[i], m = sp.marker;
if ((sp.from == null || (m.inclusiveLeft ? sp.from <= curPos.ch : sp.from < curPos.ch)) &&
(sp.to == null || (m.inclusiveRight ? sp.to >= curPos.ch : sp.to > curPos.ch))) {
if (mayClear) {
signal(m, "beforeCursorEnter");
if (m.explicitlyCleared) {
if (!line.markedSpans) break;
else {--i; continue;}
}
}
if (!m.atomic) continue;
var newPos = m.find()[dir < 0 ? "from" : "to"];
if (posEq(newPos, curPos)) {
newPos.ch += dir;
if (newPos.ch < 0) {
if (newPos.line > doc.first) newPos = clipPos(doc, Pos(newPos.line - 1));
else newPos = null;
} else if (newPos.ch > line.text.length) {
if (newPos.line < doc.first + doc.size - 1) newPos = Pos(newPos.line + 1, 0);
else newPos = null;
}
if (!newPos) {
if (flipped) {
// Driven in a corner -- no valid cursor position found at all
// -- try again *with* clearing, if we didn't already
if (!mayClear) return skipAtomic(doc, pos, bias, true);
// Otherwise, turn off editing until further notice, and return the start of the doc
doc.cantEdit = true;
return Pos(doc.first, 0);
}
flipped = true; newPos = pos; dir = -dir;
}
}
curPos = newPos;
continue search;
}
}
}
return curPos;
}
}
// SCROLLING
function scrollCursorIntoView(cm) {
var coords = scrollPosIntoView(cm, cm.doc.sel.head, null, cm.options.cursorScrollMargin);
if (!cm.state.focused) return;
var display = cm.display, box = getRect(display.sizer), doScroll = null;
if (coords.top + box.top < 0) doScroll = true;
else if (coords.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false;
if (doScroll != null && !phantom) {
var scrollNode = elt("div", "\u200b", null, "position: absolute; top: " +
(coords.top - display.viewOffset) + "px; height: " +
(coords.bottom - coords.top + scrollerCutOff) + "px; left: " +
coords.left + "px; width: 2px;");
cm.display.lineSpace.appendChild(scrollNode);
scrollNode.scrollIntoView(doScroll);
cm.display.lineSpace.removeChild(scrollNode);
}
}
function scrollPosIntoView(cm, pos, end, margin) {
if (margin == null) margin = 0;
for (;;) {
var changed = false, coords = cursorCoords(cm, pos);
var endCoords = !end || end == pos ? coords : cursorCoords(cm, end);
var scrollPos = calculateScrollPos(cm, Math.min(coords.left, endCoords.left),
Math.min(coords.top, endCoords.top) - margin,
Math.max(coords.left, endCoords.left),
Math.max(coords.bottom, endCoords.bottom) + margin);
var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft;
if (scrollPos.scrollTop != null) {
setScrollTop(cm, scrollPos.scrollTop);
if (Math.abs(cm.doc.scrollTop - startTop) > 1) changed = true;
}
if (scrollPos.scrollLeft != null) {
setScrollLeft(cm, scrollPos.scrollLeft);
if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) changed = true;
}
if (!changed) return coords;
}
}
function scrollIntoView(cm, x1, y1, x2, y2) {
var scrollPos = calculateScrollPos(cm, x1, y1, x2, y2);
if (scrollPos.scrollTop != null) setScrollTop(cm, scrollPos.scrollTop);
if (scrollPos.scrollLeft != null) setScrollLeft(cm, scrollPos.scrollLeft);
}
function calculateScrollPos(cm, x1, y1, x2, y2) {
var display = cm.display, snapMargin = textHeight(cm.display);
if (y1 < 0) y1 = 0;
var screen = display.scroller.clientHeight - scrollerCutOff, screentop = display.scroller.scrollTop, result = {};
var docBottom = cm.doc.height + paddingVert(display);
var atTop = y1 < snapMargin, atBottom = y2 > docBottom - snapMargin;
if (y1 < screentop) {
result.scrollTop = atTop ? 0 : y1;
} else if (y2 > screentop + screen) {
var newTop = Math.min(y1, (atBottom ? docBottom : y2) - screen);
if (newTop != screentop) result.scrollTop = newTop;
}
var screenw = display.scroller.clientWidth - scrollerCutOff, screenleft = display.scroller.scrollLeft;
x1 += display.gutters.offsetWidth; x2 += display.gutters.offsetWidth;
var gutterw = display.gutters.offsetWidth;
var atLeft = x1 < gutterw + 10;
if (x1 < screenleft + gutterw || atLeft) {
if (atLeft) x1 = 0;
result.scrollLeft = Math.max(0, x1 - 10 - gutterw);
} else if (x2 > screenw + screenleft - 3) {
result.scrollLeft = x2 + 10 - screenw;
}
return result;
}
function updateScrollPos(cm, left, top) {
cm.curOp.updateScrollPos = {scrollLeft: left == null ? cm.doc.scrollLeft : left,
scrollTop: top == null ? cm.doc.scrollTop : top};
}
function addToScrollPos(cm, left, top) {
var pos = cm.curOp.updateScrollPos || (cm.curOp.updateScrollPos = {scrollLeft: cm.doc.scrollLeft, scrollTop: cm.doc.scrollTop});
var scroll = cm.display.scroller;
pos.scrollTop = Math.max(0, Math.min(scroll.scrollHeight - scroll.clientHeight, pos.scrollTop + top));
pos.scrollLeft = Math.max(0, Math.min(scroll.scrollWidth - scroll.clientWidth, pos.scrollLeft + left));
}
// API UTILITIES
function indentLine(cm, n, how, aggressive) {
var doc = cm.doc;
if (how == null) how = "add";
if (how == "smart") {
if (!cm.doc.mode.indent) how = "prev";
else var state = getStateBefore(cm, n);
}
var tabSize = cm.options.tabSize;
var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize);
var curSpaceString = line.text.match(/^\s*/)[0], indentation;
if (!aggressive && !/\S/.test(line.text)) {
indentation = 0;
how = "not";
} else if (how == "smart") {
indentation = cm.doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text);
if (indentation == Pass) {
if (!aggressive) return;
how = "prev";
}
}
if (how == "prev") {
if (n > doc.first) indentation = countColumn(getLine(doc, n-1).text, null, tabSize);
else indentation = 0;
} else if (how == "add") {
indentation = curSpace + cm.options.indentUnit;
} else if (how == "subtract") {
indentation = curSpace - cm.options.indentUnit;
} else if (typeof how == "number") {
indentation = curSpace + how;
}
indentation = Math.max(0, indentation);
var indentString = "", pos = 0;
if (cm.options.indentWithTabs)
for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";}
if (pos < indentation) indentString += spaceStr(indentation - pos);
if (indentString != curSpaceString)
replaceRange(cm.doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input");
else if (doc.sel.head.line == n && doc.sel.head.ch < curSpaceString.length)
setSelection(doc, Pos(n, curSpaceString.length), Pos(n, curSpaceString.length), 1);
line.stateAfter = null;
}
function changeLine(cm, handle, op) {
var no = handle, line = handle, doc = cm.doc;
if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle));
else no = lineNo(handle);
if (no == null) return null;
if (op(line, no)) regChange(cm, no, no + 1);
else return null;
return line;
}
function findPosH(doc, pos, dir, unit, visually) {
var line = pos.line, ch = pos.ch, origDir = dir;
var lineObj = getLine(doc, line);
var possible = true;
function findNextLine() {
var l = line + dir;
if (l < doc.first || l >= doc.first + doc.size) return (possible = false);
line = l;
return lineObj = getLine(doc, l);
}
function moveOnce(boundToLine) {
var next = (visually ? moveVisually : moveLogically)(lineObj, ch, dir, true);
if (next == null) {
if (!boundToLine && findNextLine()) {
if (visually) ch = (dir < 0 ? lineRight : lineLeft)(lineObj);
else ch = dir < 0 ? lineObj.text.length : 0;
} else return (possible = false);
} else ch = next;
return true;
}
if (unit == "char") moveOnce();
else if (unit == "column") moveOnce(true);
else if (unit == "word" || unit == "group") {
var sawType = null, group = unit == "group";
for (var first = true;; first = false) {
if (dir < 0 && !moveOnce(!first)) break;
var cur = lineObj.text.charAt(ch) || "\n";
var type = isWordChar(cur) ? "w"
: !group ? null
: /\s/.test(cur) ? null
: "p";
if (sawType && sawType != type) {
if (dir < 0) {dir = 1; moveOnce();}
break;
}
if (type) sawType = type;
if (dir > 0 && !moveOnce(!first)) break;
}
}
var result = skipAtomic(doc, Pos(line, ch), origDir, true);
if (!possible) result.hitSide = true;
return result;
}
function findPosV(cm, pos, dir, unit) {
var doc = cm.doc, x = pos.left, y;
if (unit == "page") {
var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight);
y = pos.top + dir * (pageSize - (dir < 0 ? 1.5 : .5) * textHeight(cm.display));
} else if (unit == "line") {
y = dir > 0 ? pos.bottom + 3 : pos.top - 3;
}
for (;;) {
var target = coordsChar(cm, x, y);
if (!target.outside) break;
if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break; }
y += dir * 5;
}
return target;
}
function findWordAt(line, pos) {
var start = pos.ch, end = pos.ch;
if (line) {
if ((pos.xRel < 0 || end == line.length) && start) --start; else ++end;
var startChar = line.charAt(start);
var check = isWordChar(startChar) ? isWordChar
: /\s/.test(startChar) ? function(ch) {return /\s/.test(ch);}
: function(ch) {return !/\s/.test(ch) && !isWordChar(ch);};
while (start > 0 && check(line.charAt(start - 1))) --start;
while (end < line.length && check(line.charAt(end))) ++end;
}
return {from: Pos(pos.line, start), to: Pos(pos.line, end)};
}
function selectLine(cm, line) {
extendSelection(cm.doc, Pos(line, 0), clipPos(cm.doc, Pos(line + 1, 0)));
}
// PROTOTYPE
// The publicly visible API. Note that operation(null, f) means
// 'wrap f in an operation, performed on its `this` parameter'
CodeMirror.prototype = {
constructor: CodeMirror,
focus: function(){window.focus(); focusInput(this); fastPoll(this);},
setOption: function(option, value) {
var options = this.options, old = options[option];
if (options[option] == value && option != "mode") return;
options[option] = value;
if (optionHandlers.hasOwnProperty(option))
operation(this, optionHandlers[option])(this, value, old);
},
getOption: function(option) {return this.options[option];},
getDoc: function() {return this.doc;},
addKeyMap: function(map, bottom) {
this.state.keyMaps[bottom ? "push" : "unshift"](map);
},
removeKeyMap: function(map) {
var maps = this.state.keyMaps;
for (var i = 0; i < maps.length; ++i)
if (maps[i] == map || (typeof maps[i] != "string" && maps[i].name == map)) {
maps.splice(i, 1);
return true;
}
},
addOverlay: operation(null, function(spec, options) {
var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec);
if (mode.startState) throw new Error("Overlays may not be stateful.");
this.state.overlays.push({mode: mode, modeSpec: spec, opaque: options && options.opaque});
this.state.modeGen++;
regChange(this);
}),
removeOverlay: operation(null, function(spec) {
var overlays = this.state.overlays;
for (var i = 0; i < overlays.length; ++i) {
var cur = overlays[i].modeSpec;
if (cur == spec || typeof spec == "string" && cur.name == spec) {
overlays.splice(i, 1);
this.state.modeGen++;
regChange(this);
return;
}
}
}),
indentLine: operation(null, function(n, dir, aggressive) {
if (typeof dir != "string" && typeof dir != "number") {
if (dir == null) dir = this.options.smartIndent ? "smart" : "prev";
else dir = dir ? "add" : "subtract";
}
if (isLine(this.doc, n)) indentLine(this, n, dir, aggressive);
}),
indentSelection: operation(null, function(how) {
var sel = this.doc.sel;
if (posEq(sel.from, sel.to)) return indentLine(this, sel.from.line, how, true);
var e = sel.to.line - (sel.to.ch ? 0 : 1);
for (var i = sel.from.line; i <= e; ++i) indentLine(this, i, how);
}),
// Fetch the parser token for a given character. Useful for hacks
// that want to inspect the mode state (say, for completion).
getTokenAt: function(pos, precise) {
var doc = this.doc;
pos = clipPos(doc, pos);
var state = getStateBefore(this, pos.line, precise), mode = this.doc.mode;
var line = getLine(doc, pos.line);
var stream = new StringStream(line.text, this.options.tabSize);
while (stream.pos < pos.ch && !stream.eol()) {
stream.start = stream.pos;
var style = mode.token(stream, state);
}
return {start: stream.start,
end: stream.pos,
string: stream.current(),
className: style || null, // Deprecated, use 'type' instead
type: style || null,
state: state};
},
getTokenTypeAt: function(pos) {
pos = clipPos(this.doc, pos);
var styles = getLineStyles(this, getLine(this.doc, pos.line));
var before = 0, after = (styles.length - 1) / 2, ch = pos.ch;
if (ch == 0) return styles[2];
for (;;) {
var mid = (before + after) >> 1;
if ((mid ? styles[mid * 2 - 1] : 0) >= ch) after = mid;
else if (styles[mid * 2 + 1] < ch) before = mid + 1;
else return styles[mid * 2 + 2];
}
},
getModeAt: function(pos) {
var mode = this.doc.mode;
if (!mode.innerMode) return mode;
return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode;
},
getHelper: function(pos, type) {
return this.getHelpers(pos, type)[0];
},
getHelpers: function(pos, type) {
var found = [];
if (!helpers.hasOwnProperty(type)) return helpers;
var help = helpers[type], mode = this.getModeAt(pos);
if (typeof mode[type] == "string") {
if (help[mode[type]]) found.push(help[mode[type]]);
} else if (mode[type]) {
for (var i = 0; i < mode[type].length; i++) {
var val = help[mode[type][i]];
if (val) found.push(val);
}
} else if (mode.helperType && help[mode.helperType]) {
found.push(help[mode.helperType]);
} else if (help[mode.name]) {
found.push(help[mode.name]);
}
for (var i = 0; i < help._global.length; i++) {
var cur = help._global[i];
if (cur.pred(mode, this) && indexOf(found, cur.val) == -1)
found.push(cur.val);
}
return found;
},
getStateAfter: function(line, precise) {
var doc = this.doc;
line = clipLine(doc, line == null ? doc.first + doc.size - 1: line);
return getStateBefore(this, line + 1, precise);
},
cursorCoords: function(start, mode) {
var pos, sel = this.doc.sel;
if (start == null) pos = sel.head;
else if (typeof start == "object") pos = clipPos(this.doc, start);
else pos = start ? sel.from : sel.to;
return cursorCoords(this, pos, mode || "page");
},
charCoords: function(pos, mode) {
return charCoords(this, clipPos(this.doc, pos), mode || "page");
},
coordsChar: function(coords, mode) {
coords = fromCoordSystem(this, coords, mode || "page");
return coordsChar(this, coords.left, coords.top);
},
lineAtHeight: function(height, mode) {
height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top;
return lineAtHeight(this.doc, height + this.display.viewOffset);
},
heightAtLine: function(line, mode) {
var end = false, last = this.doc.first + this.doc.size - 1;
if (line < this.doc.first) line = this.doc.first;
else if (line > last) { line = last; end = true; }
var lineObj = getLine(this.doc, line);
return intoCoordSystem(this, getLine(this.doc, line), {top: 0, left: 0}, mode || "page").top +
(end ? lineObj.height : 0);
},
defaultTextHeight: function() { return textHeight(this.display); },
defaultCharWidth: function() { return charWidth(this.display); },
setGutterMarker: operation(null, function(line, gutterID, value) {
return changeLine(this, line, function(line) {
var markers = line.gutterMarkers || (line.gutterMarkers = {});
markers[gutterID] = value;
if (!value && isEmpty(markers)) line.gutterMarkers = null;
return true;
});
}),
clearGutter: operation(null, function(gutterID) {
var cm = this, doc = cm.doc, i = doc.first;
doc.iter(function(line) {
if (line.gutterMarkers && line.gutterMarkers[gutterID]) {
line.gutterMarkers[gutterID] = null;
regChange(cm, i, i + 1);
if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null;
}
++i;
});
}),
addLineClass: operation(null, function(handle, where, cls) {
return changeLine(this, handle, function(line) {
var prop = where == "text" ? "textClass" : where == "background" ? "bgClass" : "wrapClass";
if (!line[prop]) line[prop] = cls;
else if (new RegExp("(?:^|\\s)" + cls + "(?:$|\\s)").test(line[prop])) return false;
else line[prop] += " " + cls;
return true;
});
}),
removeLineClass: operation(null, function(handle, where, cls) {
return changeLine(this, handle, function(line) {
var prop = where == "text" ? "textClass" : where == "background" ? "bgClass" : "wrapClass";
var cur = line[prop];
if (!cur) return false;
else if (cls == null) line[prop] = null;
else {
var found = cur.match(new RegExp("(?:^|\\s+)" + cls + "(?:$|\\s+)"));
if (!found) return false;
var end = found.index + found[0].length;
line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null;
}
return true;
});
}),
addLineWidget: operation(null, function(handle, node, options) {
return addLineWidget(this, handle, node, options);
}),
removeLineWidget: function(widget) { widget.clear(); },
lineInfo: function(line) {
if (typeof line == "number") {
if (!isLine(this.doc, line)) return null;
var n = line;
line = getLine(this.doc, line);
if (!line) return null;
} else {
var n = lineNo(line);
if (n == null) return null;
}
var marker = line.gutterMarker;
return {line: n, handle: line, text: line.text, markerText: marker && marker.text,
markerClass: marker && marker.style, lineClass: line.className, bgClass: line.bgClassName};
}
return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers,
textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass,
widgets: line.widgets};
},
function stringWidth(str) {
measure.innerHTML = "<pre><span>x</span></pre>";
measure.firstChild.firstChild.firstChild.nodeValue = str;
return measure.firstChild.firstChild.offsetWidth || 10;
}
// These are used to go from pixel positions to character
// positions, taking varying character widths into account.
function charFromX(line, x) {
if (x <= 0) return 0;
var lineObj = getLine(line), text = lineObj.text;
function getX(len) {
measure.innerHTML = "<pre><span>" + lineObj.getHTML(makeTab, len) + "</span></pre>";
return measure.firstChild.firstChild.offsetWidth;
}
var from = 0, fromX = 0, to = text.length, toX;
// Guess a suitable upper bound for our search.
var estimated = Math.min(to, Math.ceil(x / charWidth()));
for (;;) {
var estX = getX(estimated);
if (estX <= x && estimated < to) estimated = Math.min(to, Math.ceil(estimated * 1.2));
else {toX = estX; to = estimated; break;}
}
if (x > toX) return to;
// Try to guess a suitable lower bound as well.
estimated = Math.floor(to * 0.8); estX = getX(estimated);
if (estX < x) {from = estimated; fromX = estX;}
// Do a binary search between these bounds.
for (;;) {
if (to - from <= 1) return (toX - x > x - fromX) ? from : to;
var middle = Math.ceil((from + to) / 2), middleX = getX(middle);
if (middleX > x) {to = middle; toX = middleX;}
else {from = middle; fromX = middleX;}
}
}
getViewport: function() { return {from: this.display.showingFrom, to: this.display.showingTo};},
var tempId = Math.floor(Math.random() * 0xffffff).toString(16);
function measureLine(line, ch) {
if (ch == 0) return {top: 0, left: 0};
if (line.widgetFunction) {
var size = line.widgetFunction.size(line.text);
return {top: -1, left: size.width};
}
var extra = "";
// Include extra text at the end to make sure the measured line is wrapped in the right way.
if (options.lineWrapping) {
var end = line.text.indexOf(" ", ch + 6);
extra = htmlEscape(line.text.slice(ch + 1, end < 0 ? line.text.length : end + (ie ? 5 : 0)));
addWidget: function(pos, node, scroll, vert, horiz) {
var display = this.display;
pos = cursorCoords(this, clipPos(this.doc, pos));
var top = pos.bottom, left = pos.left;
node.style.position = "absolute";
display.sizer.appendChild(node);
if (vert == "over") {
top = pos.top;
} else if (vert == "above" || vert == "near") {
var vspace = Math.max(display.wrapper.clientHeight, this.doc.height),
hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth);
// Default to positioning above (if specified and possible); otherwise default to positioning below
if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight)
top = pos.top - node.offsetHeight;
else if (pos.bottom + node.offsetHeight <= vspace)
top = pos.bottom;
if (left + node.offsetWidth > hspace)
left = hspace - node.offsetWidth;
}
measure.innerHTML = "<pre>" + line.getHTML(makeTab, ch) +
'<span id="CodeMirror-temp-' + tempId + '">' + htmlEscape(line.text.charAt(ch) || " ") + "</span>" +
extra + "</pre>";
var elt = document.getElementById("CodeMirror-temp-" + tempId);
var top = elt.offsetTop, left = elt.offsetLeft;
// Older IEs report zero offsets for spans directly after a wrap
if (ie && top == 0 && left == 0) {
var backup = document.createElement("span");
backup.innerHTML = "x";
elt.parentNode.insertBefore(backup, elt.nextSibling);
top = backup.offsetTop;
}
return {top: top, left: left};
}
function localCoords(pos, inLineWrap) {
var x, lh = textHeight(), y = lh * (heightAtLine(doc, pos.line) - (inLineWrap ? displayOffset : 0));
if (pos.ch == 0) x = 0;
else {
var sp = measureLine(getLine(pos.line), pos.ch);
x = sp.left;
if (options.lineWrapping) y += Math.max(0, sp.top);
}
return {x: x, y: y, yBot: y + lh};
}
// Coords must be lineSpace-local
function coordsChar(x, y) {
if (y < 0) y = 0;
var th = textHeight(), cw = charWidth(), heightPos = displayOffset + y / th;
var lineNo = lineAtHeight(doc, heightPos);
if (lineNo >= doc.size) return {line: doc.size - 1, ch: getLine(doc.size - 1).text.length};
var lineObj = getLine(lineNo), text = lineObj.text;
var tw = options.lineWrapping, innerOff = tw ? Math.floor(heightPos - heightAtLine(doc, lineNo)) : 0;
if (x <= 0 && innerOff == 0) return {line: lineNo, ch: 0};
function getX(len) {
var sp = measureLine(lineObj, len);
if (tw) {
var off = Math.round(sp.top / th);
return Math.max(0, sp.left + (off - innerOff) * scroller.clientWidth);
}
return sp.left;
}
var from = 0, fromX = 0, to = text.length, toX;
// Guess a suitable upper bound for our search.
var estimated = Math.min(to, Math.ceil((x + innerOff * scroller.clientWidth * .9) / cw));
for (;;) {
var estX = getX(estimated);
if (estX <= x && estimated < to) estimated = Math.min(to, Math.ceil(estimated * 1.2));
else {toX = estX; to = estimated; break;}
}
if (x > toX) return {line: lineNo, ch: to};
// Try to guess a suitable lower bound as well.
estimated = Math.floor(to * 0.8); estX = getX(estimated);
if (estX < x) {from = estimated; fromX = estX;}
// Do a binary search between these bounds.
for (;;) {
if (to - from <= 1) return {line: lineNo, ch: (toX - x > x - fromX) ? from : to};
var middle = Math.ceil((from + to) / 2), middleX = getX(middle);
if (middleX > x) {to = middle; toX = middleX;}
else {from = middle; fromX = middleX;}
}
}
function pageCoords(pos) {
var local = localCoords(pos, true), off = eltOffset(lineSpace);
return {x: off.left + local.x, y: off.top + local.y, yBot: off.top + local.yBot};
}
var cachedHeight, cachedHeightFor, measureText;
function textHeight() {
if (measureText == null) {
measureText = "<pre>";
for (var i = 0; i < 49; ++i) measureText += "x<br/>";
measureText += "x</pre>";
}
var offsetHeight = lineDiv.clientHeight;
if (offsetHeight == cachedHeightFor) return cachedHeight;
cachedHeightFor = offsetHeight;
measure.innerHTML = measureText;
cachedHeight = measure.firstChild.offsetHeight / 50 || 1;
measure.innerHTML = "";
return cachedHeight;
}
var cachedWidth, cachedWidthFor = 0;
function charWidth() {
if (scroller.clientWidth == cachedWidthFor) return cachedWidth;
cachedWidthFor = scroller.clientWidth;
return (cachedWidth = stringWidth("x"));
}
function paddingTop() {return lineSpace.offsetTop;}
function paddingLeft() {return lineSpace.offsetLeft;}
function posFromMouse(e, liberal) {
var offW = eltOffset(scroller, true), x, y;
// Fails unpredictably on IE[67] when mouse is dragged around quickly.
try { x = e.clientX; y = e.clientY; } catch (e) { return null; }
// This is a mess of a heuristic to try and determine whether a
// scroll-bar was clicked or not, and to return null if one was
// (and !liberal).
if (!liberal && (x - offW.left > scroller.clientWidth || y - offW.top > scroller.clientHeight))
return null;
var offL = eltOffset(lineSpace, true);
return coordsChar(x - offL.left, y - offL.top);
}
function onContextMenu(e) {
var pos = posFromMouse(e), scrollPos = scroller.scrollTop;
if (!pos || window.opera) return; // Opera is difficult.
if (posEq(sel.from, sel.to) || posLess(pos, sel.from) || !posLess(pos, sel.to))
operation(setCursor)(pos.line, pos.ch);
var oldCSS = input.style.cssText;
inputDiv.style.position = "absolute";
input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) +
"px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: white; " +
"border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);";
leaveInputAlone = true;
var val = input.value = getSelection();
focusInput();
selectInput(input);
function rehide() {
var newVal = splitLines(input.value).join("\n");
if (newVal != val) operation(replaceSelection)(newVal, "end");
inputDiv.style.position = "relative";
input.style.cssText = oldCSS;
if (ie_lt9) scroller.scrollTop = scrollPos;
leaveInputAlone = false;
resetInput(true);
slowPoll();
}
if (gecko) {
e_stop(e);
var mouseup = connect(window, "mouseup", function() {
mouseup();
setTimeout(rehide, 20);
}, true);
node.style.top = top + "px";
node.style.left = node.style.right = "";
if (horiz == "right") {
left = display.sizer.clientWidth - node.offsetWidth;
node.style.right = "0px";
} else {
setTimeout(rehide, 50);
if (horiz == "left") left = 0;
else if (horiz == "middle") left = (display.sizer.clientWidth - node.offsetWidth) / 2;
node.style.left = left + "px";
}
}
if (scroll)
scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight);
},
// Cursor-blinking
function restartBlink() {
clearInterval(blinker);
var on = true;
cursor.style.visibility = "";
blinker = setInterval(function() {
cursor.style.visibility = (on = !on) ? "" : "hidden";
}, 650);
}
triggerOnKeyDown: operation(null, onKeyDown),
var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"};
function matchBrackets(autoclear) {
var head = sel.inverted ? sel.from : sel.to, line = getLine(head.line), pos = head.ch - 1;
var match = (pos >= 0 && matching[line.text.charAt(pos)]) || matching[line.text.charAt(++pos)];
if (!match) return;
var ch = match.charAt(0), forward = match.charAt(1) == ">", d = forward ? 1 : -1, st = line.styles;
for (var off = pos + 1, i = 0, e = st.length; i < e; i+=2)
if ((off -= st[i].length) <= 0) {var style = st[i+1]; break;}
execCommand: function(cmd) {
if (commands.hasOwnProperty(cmd))
return commands[cmd](this);
},
var stack = [line.text.charAt(pos)], re = /[(){}[\]]/;
function scan(line, from, to) {
if (!line.text) return;
var st = line.styles, pos = forward ? 0 : line.text.length - 1, cur;
for (var i = forward ? 0 : st.length - 2, e = forward ? st.length : -2; i != e; i += 2*d) {
var text = st[i];
if (st[i+1] != null && st[i+1] != style) {pos += d * text.length; continue;}
for (var j = forward ? 0 : text.length - 1, te = forward ? text.length : -1; j != te; j += d, pos+=d) {
if (pos >= from && pos < to && re.test(cur = text.charAt(j))) {
var match = matching[cur];
if (match.charAt(1) == ">" == forward) stack.push(cur);
else if (stack.pop() != match.charAt(0)) return {pos: pos, match: false};
else if (!stack.length) return {pos: pos, match: true};
}
}
}
findPosH: function(from, amount, unit, visually) {
var dir = 1;
if (amount < 0) { dir = -1; amount = -amount; }
for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) {
cur = findPosH(this.doc, cur, dir, unit, visually);
if (cur.hitSide) break;
}
for (var i = head.line, e = forward ? Math.min(i + 100, doc.size) : Math.max(-1, i - 100); i != e; i+=d) {
var line = getLine(i), first = i == head.line;
var found = scan(line, first && forward ? pos + 1 : 0, first && !forward ? pos : line.text.length);
if (found) break;
return cur;
},
moveH: operation(null, function(dir, unit) {
var sel = this.doc.sel, pos;
if (sel.shift || sel.extend || posEq(sel.from, sel.to))
pos = findPosH(this.doc, sel.head, dir, unit, this.options.rtlMoveVisually);
else
pos = dir < 0 ? sel.from : sel.to;
extendSelection(this.doc, pos, pos, dir);
}),
deleteH: operation(null, function(dir, unit) {
var sel = this.doc.sel;
if (!posEq(sel.from, sel.to)) replaceRange(this.doc, "", sel.from, sel.to, "+delete");
else replaceRange(this.doc, "", sel.from, findPosH(this.doc, sel.head, dir, unit, false), "+delete");
this.curOp.userSelChange = true;
}),
findPosV: function(from, amount, unit, goalColumn) {
var dir = 1, x = goalColumn;
if (amount < 0) { dir = -1; amount = -amount; }
for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) {
var coords = cursorCoords(this, cur, "div");
if (x == null) x = coords.left;
else coords.left = x;
cur = findPosV(this, coords, dir, unit);
if (cur.hitSide) break;
}
if (!found) found = {pos: null, match: false};
var style = found.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket";
var one = markText({line: head.line, ch: pos}, {line: head.line, ch: pos+1}, style),
two = found.pos != null && markText({line: i, ch: found.pos}, {line: i, ch: found.pos + 1}, style);
var clear = operation(function(){one.clear(); two && two.clear();});
if (autoclear) setTimeout(clear, 800);
else bracketHighlighted = clear;
}
return cur;
},
// Finds the line to start with when starting a parse. Tries to
// find a line with a stateAfter, so that it can start with a
// valid state. If that fails, it returns the line with the
// smallest indentation, which tends to need the least context to
// parse correctly.
function findStartLine(n) {
var minindent, minline;
for (var search = n, lim = n - 40; search > lim; --search) {
if (search == 0) return 0;
var line = getLine(search-1);
if (line.stateAfter) return search;
var indented = line.indentation(options.tabSize);
if (minline == null || minindent > indented) {
minline = search - 1;
minindent = indented;
}
moveV: operation(null, function(dir, unit) {
var sel = this.doc.sel, target, goal;
if (sel.shift || sel.extend || posEq(sel.from, sel.to)) {
var pos = cursorCoords(this, sel.head, "div");
if (sel.goalColumn != null) pos.left = sel.goalColumn;
target = findPosV(this, pos, dir, unit);
if (unit == "page") addToScrollPos(this, 0, charCoords(this, target, "div").top - pos.top);
goal = pos.left;
} else {
target = dir < 0 ? sel.from : sel.to;
}
return minline;
}
function getStateBefore(n) {
var start = findStartLine(n), state = start && getLine(start-1).stateAfter;
if (!state) state = startState(mode);
else state = copyState(mode, state);
doc.iter(start, n, function(line) {
line.highlight(mode, state, options.tabSize);
line.stateAfter = copyState(mode, state);
});
if (start < n) changes.push({from: start, to: n});
if (n < doc.size && !getLine(n).stateAfter) work.push(n);
return state;
}
function highlightLines(start, end) {
var state = getStateBefore(start);
doc.iter(start, end, function(line) {
line.highlight(mode, state, options.tabSize);
line.stateAfter = copyState(mode, state);
});
}
function highlightWorker() {
var end = +new Date + options.workTime;
var foundWork = work.length;
while (work.length) {
if (!getLine(showingFrom).stateAfter) var task = showingFrom;
else var task = work.pop();
if (task >= doc.size) continue;
var start = findStartLine(task), state = start && getLine(start-1).stateAfter;
if (state) state = copyState(mode, state);
else state = startState(mode);
extendSelection(this.doc, target, target, dir);
if (goal != null) sel.goalColumn = goal;
}),
var unchanged = 0, compare = mode.compareStates, realChange = false,
i = start, bail = false;
doc.iter(i, doc.size, function(line) {
var hadState = line.stateAfter;
if (+new Date > end) {
work.push(i);
startWorker(options.workDelay);
if (realChange) changes.push({from: task, to: i + 1});
return (bail = true);
}
var changed = line.highlight(mode, state, options.tabSize);
if (changed) realChange = true;
line.stateAfter = copyState(mode, state);
if (compare) {
if (hadState && compare(hadState, state)) return true;
} else {
if (changed !== false || !hadState) unchanged = 0;
else if (++unchanged > 3 && (!mode.indent || mode.indent(hadState, "") == mode.indent(state, "")))
return true;
}
++i;
});
if (bail) return;
if (realChange) changes.push({from: task, to: i + 1});
toggleOverwrite: function(value) {
if (value != null && value == this.state.overwrite) return;
if (this.state.overwrite = !this.state.overwrite)
this.display.cursor.className += " CodeMirror-overwrite";
else
this.display.cursor.className = this.display.cursor.className.replace(" CodeMirror-overwrite", "");
},
hasFocus: function() { return this.state.focused; },
scrollTo: operation(null, function(x, y) {
updateScrollPos(this, x, y);
}),
getScrollInfo: function() {
var scroller = this.display.scroller, co = scrollerCutOff;
return {left: scroller.scrollLeft, top: scroller.scrollTop,
height: scroller.scrollHeight - co, width: scroller.scrollWidth - co,
clientHeight: scroller.clientHeight - co, clientWidth: scroller.clientWidth - co};
},
scrollIntoView: operation(null, function(range, margin) {
if (range == null) range = {from: this.doc.sel.head, to: null};
else if (typeof range == "number") range = {from: Pos(range, 0), to: null};
else if (range.from == null) range = {from: range, to: null};
if (!range.to) range.to = range.from;
if (!margin) margin = 0;
var coords = range;
if (range.from.line != null) {
this.curOp.scrollToPos = {from: range.from, to: range.to, margin: margin};
coords = {from: cursorCoords(this, range.from),
to: cursorCoords(this, range.to)};
}
if (foundWork && options.onHighlightComplete)
options.onHighlightComplete(instance);
}
function startWorker(time) {
if (!work.length) return;
highlight.set(time, operation(highlightWorker));
}
var sPos = calculateScrollPos(this, Math.min(coords.from.left, coords.to.left),
Math.min(coords.from.top, coords.to.top) - margin,
Math.max(coords.from.right, coords.to.right),
Math.max(coords.from.bottom, coords.to.bottom) + margin);
updateScrollPos(this, sPos.scrollLeft, sPos.scrollTop);
}),
// Operations are used to wrap changes in such a way that each
// change won't have to update the cursor and display (which would
// be awkward, slow, and error-prone), but instead updates are
// batched and then all combined and executed at once.
function startOperation() {
updateInput = userSelChange = textChanged = null;
changes = []; selectionChanged = false; callbacks = [];
}
function endOperation() {
var reScroll = false, updated;
if (selectionChanged) reScroll = !scrollCursorIntoView();
if (changes.length) updated = updateDisplay(changes, true);
else {
if (selectionChanged) updateSelection();
if (gutterDirty) updateGutter();
setSize: operation(null, function(width, height) {
function interpret(val) {
return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val;
}
if (reScroll) scrollCursorIntoView();
if (selectionChanged) {scrollEditorIntoView(); restartBlink();}
if (width != null) this.display.wrapper.style.width = interpret(width);
if (height != null) this.display.wrapper.style.height = interpret(height);
if (this.options.lineWrapping)
this.display.measureLineCache.length = this.display.measureLineCachePos = 0;
this.curOp.forceUpdate = true;
}),
if (focused && !leaveInputAlone &&
(updateInput === true || (updateInput !== false && selectionChanged)))
resetInput(userSelChange);
operation: function(f){return runInOp(this, f);},
if (selectionChanged && options.matchBrackets)
setTimeout(operation(function() {
if (bracketHighlighted) {bracketHighlighted(); bracketHighlighted = null;}
if (posEq(sel.from, sel.to)) matchBrackets(false);
}), 20);
var tc = textChanged, cbs = callbacks; // these can be reset by callbacks
if (selectionChanged && options.onCursorActivity)
options.onCursorActivity(instance);
if (tc && options.onChange && instance)
options.onChange(instance, tc);
for (var i = 0; i < cbs.length; ++i) cbs[i](instance);
if (updated && options.onUpdate) options.onUpdate(instance);
}
var nestedOperation = 0;
function operation(f) {
return function() {
if (!nestedOperation++) startOperation();
try {var result = f.apply(this, arguments);}
finally {if (!--nestedOperation) endOperation();}
return result;
};
}
refresh: operation(null, function() {
var badHeight = this.display.cachedTextHeight == null;
clearCaches(this);
updateScrollPos(this, this.doc.scrollLeft, this.doc.scrollTop);
regChange(this);
if (badHeight) estimateLineHeights(this);
}),
for (var ext in extensions)
if (extensions.propertyIsEnumerable(ext) &&
!instance.propertyIsEnumerable(ext))
instance[ext] = extensions[ext];
return instance;
} // (end of function CodeMirror)
swapDoc: operation(null, function(doc) {
var old = this.doc;
old.cm = null;
attachDoc(this, doc);
clearCaches(this);
resetInput(this, true);
updateScrollPos(this, doc.scrollLeft, doc.scrollTop);
signalLater(this, "swapDoc", this, old);
return old;
}),
getInputField: function(){return this.display.input;},
getWrapperElement: function(){return this.display.wrapper;},
getScrollerElement: function(){return this.display.scroller;},
getGutterElement: function(){return this.display.gutters;}
};
eventMixin(CodeMirror);
// OPTION DEFAULTS
var optionHandlers = CodeMirror.optionHandlers = {};
// The default configuration options.
CodeMirror.defaults = {
value: "",
mode: null,
theme: "default",
indentUnit: 2,
indentWithTabs: false,
smartIndent: true,
tabSize: 4,
keyMap: "default",
extraKeys: null,
electricChars: true,
autoClearEmptyLines: false,
onKeyEvent: null,
lineWrapping: false,
lineNumbers: false,
gutter: false,
fixedGutter: false,
firstLineNumber: 1,
readOnly: false,
onChange: null,
onCursorActivity: null,
onGutterClick: null,
onHighlightComplete: null,
onUpdate: null,
onFocus: null, onBlur: null, onScroll: null,
matchBrackets: false,
workTime: 100,
workDelay: 200,
pollInterval: 100,
undoDepth: 40,
tabindex: null,
autofocus: null
};
var defaults = CodeMirror.defaults = {};
var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent);
var mac = ios || /Mac/.test(navigator.platform);
var win = /Win/.test(navigator.platform);
function option(name, deflt, handle, notOnInit) {
CodeMirror.defaults[name] = deflt;
if (handle) optionHandlers[name] =
notOnInit ? function(cm, val, old) {if (old != Init) handle(cm, val, old);} : handle;
}
var Init = CodeMirror.Init = {toString: function(){return "CodeMirror.Init";}};
// These two are, on init, called from the constructor because they
// have to be initialized before the editor can start at all.
option("value", "", function(cm, val) {
cm.setValue(val);
}, true);
option("mode", null, function(cm, val) {
cm.doc.modeOption = val;
loadMode(cm);
}, true);
option("indentUnit", 2, loadMode, true);
option("indentWithTabs", false);
option("smartIndent", true);
option("tabSize", 4, function(cm) {
resetModeState(cm);
clearCaches(cm);
regChange(cm);
}, true);
option("specialChars", /[\t\u0000-\u0019\u00ad\u200b\u2028\u2029\ufeff]/g, function(cm, val) {
cm.options.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g");
cm.refresh();
}, true);
option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function(cm) {cm.refresh();}, true);
option("electricChars", true);
option("rtlMoveVisually", !windows);
option("wholeLineUpdateBefore", true);
option("theme", "default", function(cm) {
themeChanged(cm);
guttersChanged(cm);
}, true);
option("keyMap", "default", keyMapChanged);
option("extraKeys", null);
option("onKeyEvent", null);
option("onDragEvent", null);
option("lineWrapping", false, wrappingChanged, true);
option("gutters", [], function(cm) {
setGuttersForLineNumbers(cm.options);
guttersChanged(cm);
}, true);
option("fixedGutter", true, function(cm, val) {
cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0";
cm.refresh();
}, true);
option("coverGutterNextToScrollbar", false, updateScrollbars, true);
option("lineNumbers", false, function(cm) {
setGuttersForLineNumbers(cm.options);
guttersChanged(cm);
}, true);
option("firstLineNumber", 1, guttersChanged, true);
option("lineNumberFormatter", function(integer) {return integer;}, guttersChanged, true);
option("showCursorWhenSelecting", false, updateSelection, true);
option("resetSelectionOnContextMenu", true);
option("readOnly", false, function(cm, val) {
if (val == "nocursor") {
onBlur(cm);
cm.display.input.blur();
cm.display.disabled = true;
} else {
cm.display.disabled = false;
if (!val) resetInput(cm, true);
}
});
option("disableInput", false, function(cm, val) {if (!val) resetInput(cm, true);}, true);
option("dragDrop", true);
option("cursorBlinkRate", 530);
option("cursorScrollMargin", 0);
option("cursorHeight", 1);
option("workTime", 100);
option("workDelay", 100);
option("flattenSpans", true, resetModeState, true);
option("addModeClass", false, resetModeState, true);
option("pollInterval", 100);
option("undoDepth", 40, function(cm, val){cm.doc.history.undoDepth = val;});
option("historyEventDelay", 500);
option("viewportMargin", 10, function(cm){cm.refresh();}, true);
option("maxHighlightLength", 10000, resetModeState, true);
option("crudeMeasuringFrom", 10000);
option("moveInputWithCursor", true, function(cm, val) {
if (!val) cm.display.inputDiv.style.top = cm.display.inputDiv.style.left = 0;
});
option("tabindex", null, function(cm, val) {
cm.display.input.tabIndex = val || "";
});
option("autofocus", null);
// MODE DEFINITION AND QUERYING
// Known modes, by name and by MIME
var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {};
CodeMirror.defineMode = function(name, mode) {
if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name;
if (arguments.length > 2) {
mode.dependencies = [];
for (var i = 2; i < arguments.length; ++i) mode.dependencies.push(arguments[i]);
}
modes[name] = mode;
};
CodeMirror.defineMIME = function(mime, spec) {
mimeModes[mime] = spec;
};
CodeMirror.resolveMode = function(spec) {
if (typeof spec == "string" && mimeModes.hasOwnProperty(spec))
if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) {
spec = mimeModes[spec];
else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec))
} else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) {
var found = mimeModes[spec.name];
spec = createObj(found, spec);
spec.name = found.name;
} else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) {
return CodeMirror.resolveMode("application/xml");
}
if (typeof spec == "string") return {name: spec};
else return spec || {name: "null"};
};
CodeMirror.getMode = function(options, spec) {
var spec = CodeMirror.resolveMode(spec);
var mfactory = modes[spec.name];
if (!mfactory) {
if (window.console) console.warn("No mode " + spec.name + " found, falling back to plain text.");
return CodeMirror.getMode(options, "text/plain");
if (!mfactory) return CodeMirror.getMode(options, "text/plain");
var modeObj = mfactory(options, spec);
if (modeExtensions.hasOwnProperty(spec.name)) {
var exts = modeExtensions[spec.name];
for (var prop in exts) {
if (!exts.hasOwnProperty(prop)) continue;
if (modeObj.hasOwnProperty(prop)) modeObj["_" + prop] = modeObj[prop];
modeObj[prop] = exts[prop];
}
}
return mfactory(options, spec);
};
CodeMirror.listModes = function() {
var list = [];
for (var m in modes)
if (modes.propertyIsEnumerable(m)) list.push(m);
return list;
};
CodeMirror.listMIMEs = function() {
var list = [];
for (var m in mimeModes)
if (mimeModes.propertyIsEnumerable(m)) list.push({mime: m, mode: mimeModes[m]});
return list;
modeObj.name = spec.name;
if (spec.helperType) modeObj.helperType = spec.helperType;
if (spec.modeProps) for (var prop in spec.modeProps)
modeObj[prop] = spec.modeProps[prop];
return modeObj;
};
var extensions = CodeMirror.extensions = {};
CodeMirror.defineMode("null", function() {
return {token: function(stream) {stream.skipToEnd();}};
});
CodeMirror.defineMIME("text/plain", "null");
var modeExtensions = CodeMirror.modeExtensions = {};
CodeMirror.extendMode = function(mode, properties) {
var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {});
copyObj(properties, exts);
};
// EXTENSIONS
CodeMirror.defineExtension = function(name, func) {
extensions[name] = func;
CodeMirror.prototype[name] = func;
};
CodeMirror.defineDocExtension = function(name, func) {
Doc.prototype[name] = func;
};
CodeMirror.defineOption = option;
var initHooks = [];
CodeMirror.defineInitHook = function(f) {initHooks.push(f);};
var helpers = CodeMirror.helpers = {};
CodeMirror.registerHelper = function(type, name, value) {
if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {_global: []};
helpers[type][name] = value;
};
CodeMirror.registerGlobalHelper = function(type, name, predicate, value) {
CodeMirror.registerHelper(type, name, value);
helpers[type]._global.push({pred: predicate, val: value});
};
var commands = CodeMirror.commands = {
selectAll: function(cm) {cm.setSelection({line: 0, ch: 0}, {line: cm.lineCount() - 1});},
killLine: function(cm) {
var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
if (!sel && cm.getLine(from.line).length == from.ch) cm.replaceRange("", from, {line: from.line + 1, ch: 0});
else cm.replaceRange("", from, sel ? to : {line: from.line});
},
deleteLine: function(cm) {var l = cm.getCursor().line; cm.replaceRange("", {line: l, ch: 0}, {line: l});},
undo: function(cm) {cm.undo();},
redo: function(cm) {cm.redo();},
goDocStart: function(cm) {cm.setCursor(0, 0, true);},
goDocEnd: function(cm) {cm.setSelection({line: cm.lineCount() - 1}, null, true);},
goLineStart: function(cm) {cm.setCursor(cm.getCursor().line, 0, true);},
goLineStartSmart: function(cm) {
var cur = cm.getCursor();
var text = cm.getLine(cur.line), firstNonWS = Math.max(0, text.search(/\S/));
cm.setCursor(cur.line, cur.ch <= firstNonWS && cur.ch ? 0 : firstNonWS, true);
},
goLineEnd: function(cm) {cm.setSelection({line: cm.getCursor().line}, null, true);},
goLineUp: function(cm) {cm.moveV(-1, "line");},
goLineDown: function(cm) {cm.moveV(1, "line");},
goPageUp: function(cm) {cm.moveV(-1, "page");},
goPageDown: function(cm) {cm.moveV(1, "page");},
goCharLeft: function(cm) {cm.moveH(-1, "char");},
goCharRight: function(cm) {cm.moveH(1, "char");},
goColumnLeft: function(cm) {cm.moveH(-1, "column");},
goColumnRight: function(cm) {cm.moveH(1, "column");},
goWordLeft: function(cm) {cm.moveH(-1, "word");},
goWordRight: function(cm) {cm.moveH(1, "word");},
delCharLeft: function(cm) {cm.deleteH(-1, "char");},
delCharRight: function(cm) {cm.deleteH(1, "char");},
delWordLeft: function(cm) {cm.deleteH(-1, "word");},
delWordRight: function(cm) {cm.deleteH(1, "word");},
indentAuto: function(cm) {cm.indentSelection("smart");},
indentMore: function(cm) {cm.indentSelection("add");},
indentLess: function(cm) {cm.indentSelection("subtract");},
insertTab: function(cm) {cm.replaceSelection("\t", "end");},
transposeChars: function(cm) {
var cur = cm.getCursor(), line = cm.getLine(cur.line);
if (cur.ch > 0 && cur.ch < line.length - 1)
cm.replaceRange(line.charAt(cur.ch) + line.charAt(cur.ch - 1),
{line: cur.line, ch: cur.ch - 1}, {line: cur.line, ch: cur.ch + 1});
},
newlineAndIndent: function(cm) {
cm.replaceSelection("\n", "end");
cm.indentLine(cm.getCursor().line);
},
toggleOverwrite: function(cm) {cm.toggleOverwrite();}
};
// UTILITIES
var keyMap = CodeMirror.keyMap = {};
keyMap.basic = {
"Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown",
"End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown",
"Delete": "delCharRight", "Backspace": "delCharLeft", "Tab": "insertTab", "Shift-Tab": "indentAuto",
"Enter": "newlineAndIndent", "Insert": "toggleOverwrite"
};
// Note that the save and find-related commands aren't defined by
// default. Unknown commands are simply ignored.
keyMap.pcDefault = {
"Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo",
"Ctrl-Home": "goDocStart", "Alt-Up": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Down": "goDocEnd",
"Ctrl-Left": "goWordLeft", "Ctrl-Right": "goWordRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd",
"Ctrl-Backspace": "delWordLeft", "Ctrl-Delete": "delWordRight", "Ctrl-S": "save", "Ctrl-F": "find",
"Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll",
"Ctrl-[": "indentLess", "Ctrl-]": "indentMore",
fallthrough: "basic"
};
keyMap.macDefault = {
"Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo",
"Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goWordLeft",
"Alt-Right": "goWordRight", "Cmd-Left": "goLineStart", "Cmd-Right": "goLineEnd", "Alt-Backspace": "delWordLeft",
"Ctrl-Alt-Backspace": "delWordRight", "Alt-Delete": "delWordRight", "Cmd-S": "save", "Cmd-F": "find",
"Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll",
"Cmd-[": "indentLess", "Cmd-]": "indentMore",
fallthrough: ["basic", "emacsy"]
};
keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault;
keyMap.emacsy = {
"Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown",
"Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd",
"Ctrl-V": "goPageUp", "Shift-Ctrl-V": "goPageDown", "Ctrl-D": "delCharRight", "Ctrl-H": "delCharLeft",
"Alt-D": "delWordRight", "Alt-Backspace": "delWordLeft", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars"
};
CodeMirror.isWordChar = isWordChar;
function getKeyMap(val) {
if (typeof val == "string") return keyMap[val];
else return val;
}
function lookupKey(name, extraMap, map, handle) {
function lookup(map) {
map = getKeyMap(map);
var found = map[name];
if (found != null && handle(found)) return true;
if (map.catchall) return handle(map.catchall);
var fallthrough = map.fallthrough;
if (fallthrough == null) return false;
if (Object.prototype.toString.call(fallthrough) != "[object Array]")
return lookup(fallthrough);
for (var i = 0, e = fallthrough.length; i < e; ++i) {
if (lookup(fallthrough[i])) return true;
}
return false;
}
if (extraMap && lookup(extraMap)) return true;
return lookup(map);
}
function isModifierKey(event) {
var name = keyNames[e_prop(event, "keyCode")];
return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod";
}
CodeMirror.fromTextArea = function(textarea, options) {
if (!options) options = {};
options.value = textarea.value;
if (!options.tabindex && textarea.tabindex)
options.tabindex = textarea.tabindex;
if (options.autofocus == null && textarea.getAttribute("autofocus") != null)
options.autofocus = true;
function save() {textarea.value = instance.getValue();}
if (textarea.form) {
// Deplorable hack to make the submit method do the right thing.
var rmSubmit = connect(textarea.form, "submit", save, true);
if (typeof textarea.form.submit == "function") {
var realSubmit = textarea.form.submit;
function wrappedSubmit() {
save();
textarea.form.submit = realSubmit;
textarea.form.submit();
textarea.form.submit = wrappedSubmit;
}
textarea.form.submit = wrappedSubmit;
}
}
textarea.style.display = "none";
var instance = CodeMirror(function(node) {
textarea.parentNode.insertBefore(node, textarea.nextSibling);
}, options);
instance.save = save;
instance.getTextArea = function() { return textarea; };
instance.toTextArea = function() {
save();
textarea.parentNode.removeChild(instance.getWrapperElement());
textarea.style.display = "";
if (textarea.form) {
rmSubmit();
if (typeof textarea.form.submit == "function")
textarea.form.submit = realSubmit;
}
};
return instance;
};
// MODE STATE HANDLING
// Utility functions for working with state. Exported because modes
// sometimes need to do this.
@@ -2160,21 +3537,271 @@ var CodeMirror = (function() {
return nstate;
}
CodeMirror.copyState = copyState;
function startState(mode, a1, a2) {
return mode.startState ? mode.startState(a1, a2) : true;
}
CodeMirror.startState = startState;
CodeMirror.innerMode = function(mode, state) {
while (mode.innerMode) {
var info = mode.innerMode(state);
if (!info || info.mode == mode) break;
state = info.state;
mode = info.mode;
}
return info || {mode: mode, state: state};
};
// STANDARD COMMANDS
var commands = CodeMirror.commands = {
selectAll: function(cm) {cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()));},
killLine: function(cm) {
var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
if (!sel && cm.getLine(from.line).length == from.ch)
cm.replaceRange("", from, Pos(from.line + 1, 0), "+delete");
else cm.replaceRange("", from, sel ? to : Pos(from.line), "+delete");
},
deleteLine: function(cm) {
var l = cm.getCursor().line;
cm.replaceRange("", Pos(l, 0), Pos(l), "+delete");
},
delLineLeft: function(cm) {
var cur = cm.getCursor();
cm.replaceRange("", Pos(cur.line, 0), cur, "+delete");
},
undo: function(cm) {cm.undo();},
redo: function(cm) {cm.redo();},
goDocStart: function(cm) {cm.extendSelection(Pos(cm.firstLine(), 0));},
goDocEnd: function(cm) {cm.extendSelection(Pos(cm.lastLine()));},
goLineStart: function(cm) {
cm.extendSelection(lineStart(cm, cm.getCursor().line));
},
goLineStartSmart: function(cm) {
var cur = cm.getCursor(), start = lineStart(cm, cur.line);
var line = cm.getLineHandle(start.line);
var order = getOrder(line);
if (!order || order[0].level == 0) {
var firstNonWS = Math.max(0, line.text.search(/\S/));
var inWS = cur.line == start.line && cur.ch <= firstNonWS && cur.ch;
cm.extendSelection(Pos(start.line, inWS ? 0 : firstNonWS));
} else cm.extendSelection(start);
},
goLineEnd: function(cm) {
cm.extendSelection(lineEnd(cm, cm.getCursor().line));
},
goLineRight: function(cm) {
var top = cm.charCoords(cm.getCursor(), "div").top + 5;
cm.extendSelection(cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"));
},
goLineLeft: function(cm) {
var top = cm.charCoords(cm.getCursor(), "div").top + 5;
cm.extendSelection(cm.coordsChar({left: 0, top: top}, "div"));
},
goLineUp: function(cm) {cm.moveV(-1, "line");},
goLineDown: function(cm) {cm.moveV(1, "line");},
goPageUp: function(cm) {cm.moveV(-1, "page");},
goPageDown: function(cm) {cm.moveV(1, "page");},
goCharLeft: function(cm) {cm.moveH(-1, "char");},
goCharRight: function(cm) {cm.moveH(1, "char");},
goColumnLeft: function(cm) {cm.moveH(-1, "column");},
goColumnRight: function(cm) {cm.moveH(1, "column");},
goWordLeft: function(cm) {cm.moveH(-1, "word");},
goGroupRight: function(cm) {cm.moveH(1, "group");},
goGroupLeft: function(cm) {cm.moveH(-1, "group");},
goWordRight: function(cm) {cm.moveH(1, "word");},
delCharBefore: function(cm) {cm.deleteH(-1, "char");},
delCharAfter: function(cm) {cm.deleteH(1, "char");},
delWordBefore: function(cm) {cm.deleteH(-1, "word");},
delWordAfter: function(cm) {cm.deleteH(1, "word");},
delGroupBefore: function(cm) {cm.deleteH(-1, "group");},
delGroupAfter: function(cm) {cm.deleteH(1, "group");},
indentAuto: function(cm) {cm.indentSelection("smart");},
indentMore: function(cm) {cm.indentSelection("add");},
indentLess: function(cm) {cm.indentSelection("subtract");},
insertTab: function(cm) {
cm.replaceSelection("\t", "end", "+input");
},
defaultTab: function(cm) {
if (cm.somethingSelected()) cm.indentSelection("add");
else cm.replaceSelection("\t", "end", "+input");
},
transposeChars: function(cm) {
var cur = cm.getCursor(), line = cm.getLine(cur.line);
if (cur.ch > 0 && cur.ch < line.length - 1)
cm.replaceRange(line.charAt(cur.ch) + line.charAt(cur.ch - 1),
Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1));
},
newlineAndIndent: function(cm) {
operation(cm, function() {
cm.replaceSelection("\n", "end", "+input");
cm.indentLine(cm.getCursor().line, null, true);
})();
},
toggleOverwrite: function(cm) {cm.toggleOverwrite();}
};
// STANDARD KEYMAPS
var keyMap = CodeMirror.keyMap = {};
keyMap.basic = {
"Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown",
"End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown",
"Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore",
"Tab": "defaultTab", "Shift-Tab": "indentAuto",
"Enter": "newlineAndIndent", "Insert": "toggleOverwrite"
};
// Note that the save and find-related commands aren't defined by
// default. Unknown commands are simply ignored.
keyMap.pcDefault = {
"Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo",
"Ctrl-Home": "goDocStart", "Alt-Up": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Down": "goDocEnd",
"Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd",
"Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find",
"Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll",
"Ctrl-[": "indentLess", "Ctrl-]": "indentMore",
fallthrough: "basic"
};
keyMap.macDefault = {
"Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo",
"Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft",
"Alt-Right": "goGroupRight", "Cmd-Left": "goLineStart", "Cmd-Right": "goLineEnd", "Alt-Backspace": "delGroupBefore",
"Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find",
"Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll",
"Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delLineLeft",
fallthrough: ["basic", "emacsy"]
};
keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault;
keyMap.emacsy = {
"Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown",
"Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd",
"Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore",
"Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars"
};
// KEYMAP DISPATCH
function getKeyMap(val) {
if (typeof val == "string") return keyMap[val];
else return val;
}
function lookupKey(name, maps, handle) {
function lookup(map) {
map = getKeyMap(map);
var found = map[name];
if (found === false) return "stop";
if (found != null && handle(found)) return true;
if (map.nofallthrough) return "stop";
var fallthrough = map.fallthrough;
if (fallthrough == null) return false;
if (Object.prototype.toString.call(fallthrough) != "[object Array]")
return lookup(fallthrough);
for (var i = 0, e = fallthrough.length; i < e; ++i) {
var done = lookup(fallthrough[i]);
if (done) return done;
}
return false;
}
for (var i = 0; i < maps.length; ++i) {
var done = lookup(maps[i]);
if (done) return done != "stop";
}
}
function isModifierKey(event) {
var name = keyNames[event.keyCode];
return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod";
}
function keyName(event, noShift) {
if (opera && event.keyCode == 34 && event["char"]) return false;
var name = keyNames[event.keyCode];
if (name == null || event.altGraphKey) return false;
if (event.altKey) name = "Alt-" + name;
if (flipCtrlCmd ? event.metaKey : event.ctrlKey) name = "Ctrl-" + name;
if (flipCtrlCmd ? event.ctrlKey : event.metaKey) name = "Cmd-" + name;
if (!noShift && event.shiftKey) name = "Shift-" + name;
return name;
}
CodeMirror.lookupKey = lookupKey;
CodeMirror.isModifierKey = isModifierKey;
CodeMirror.keyName = keyName;
// FROMTEXTAREA
CodeMirror.fromTextArea = function(textarea, options) {
if (!options) options = {};
options.value = textarea.value;
if (!options.tabindex && textarea.tabindex)
options.tabindex = textarea.tabindex;
if (!options.placeholder && textarea.placeholder)
options.placeholder = textarea.placeholder;
// Set autofocus to true if this textarea is focused, or if it has
// autofocus and no other element is focused.
if (options.autofocus == null) {
var hasFocus = document.body;
// doc.activeElement occasionally throws on IE
try { hasFocus = document.activeElement; } catch(e) {}
options.autofocus = hasFocus == textarea ||
textarea.getAttribute("autofocus") != null && hasFocus == document.body;
}
function save() {textarea.value = cm.getValue();}
if (textarea.form) {
on(textarea.form, "submit", save);
// Deplorable hack to make the submit method do the right thing.
if (!options.leaveSubmitMethodAlone) {
var form = textarea.form, realSubmit = form.submit;
try {
var wrappedSubmit = form.submit = function() {
save();
form.submit = realSubmit;
form.submit();
form.submit = wrappedSubmit;
};
} catch(e) {}
}
}
textarea.style.display = "none";
var cm = CodeMirror(function(node) {
textarea.parentNode.insertBefore(node, textarea.nextSibling);
}, options);
cm.save = save;
cm.getTextArea = function() { return textarea; };
cm.toTextArea = function() {
save();
textarea.parentNode.removeChild(cm.getWrapperElement());
textarea.style.display = "";
if (textarea.form) {
off(textarea.form, "submit", save);
if (typeof textarea.form.submit == "function")
textarea.form.submit = realSubmit;
}
};
return cm;
};
// STRING STREAM
// Fed to the mode parsers, provides helper functions to make
// parsers more succinct.
// The character stream used by a mode's parser.
function StringStream(string, tabSize) {
this.pos = this.start = 0;
this.string = string;
this.tabSize = tabSize || 8;
this.lastColumnPos = this.lastColumnValue = 0;
this.lineStart = 0;
}
StringStream.prototype = {
eol: function() {return this.pos >= this.string.length;},
sol: function() {return this.pos == 0;},
peek: function() {return this.string.charAt(this.pos);},
sol: function() {return this.pos == this.lineStart;},
peek: function() {return this.string.charAt(this.pos) || undefined;},
next: function() {
if (this.pos < this.string.length)
return this.string.charAt(this.pos++);
@@ -2201,370 +3828,963 @@ var CodeMirror = (function() {
if (found > -1) {this.pos = found; return true;}
},
backUp: function(n) {this.pos -= n;},
column: function() {return countColumn(this.string, this.start, this.tabSize);},
indentation: function() {return countColumn(this.string, null, this.tabSize);},
column: function() {
if (this.lastColumnPos < this.start) {
this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue);
this.lastColumnPos = this.start;
}
return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0);
},
indentation: function() {
return countColumn(this.string, null, this.tabSize) -
(this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0);
},
match: function(pattern, consume, caseInsensitive) {
if (typeof pattern == "string") {
function cased(str) {return caseInsensitive ? str.toLowerCase() : str;}
if (cased(this.string).indexOf(cased(pattern), this.pos) == this.pos) {
var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;};
var substr = this.string.substr(this.pos, pattern.length);
if (cased(substr) == cased(pattern)) {
if (consume !== false) this.pos += pattern.length;
return true;
}
}
else {
} else {
var match = this.string.slice(this.pos).match(pattern);
if (match && match.index > 0) return null;
if (match && consume !== false) this.pos += match[0].length;
return match;
}
},
current: function(){return this.string.slice(this.start, this.pos);}
current: function(){return this.string.slice(this.start, this.pos);},
hideFirstChars: function(n, inner) {
this.lineStart += n;
try { return inner(); }
finally { this.lineStart -= n; }
}
};
CodeMirror.StringStream = StringStream;
function MarkedText(from, to, className, marker) {
this.from = from; this.to = to; this.style = className; this.marker = marker;
}
MarkedText.prototype = {
attach: function(line) { this.marker.set.push(line); },
detach: function(line) {
var ix = indexOf(this.marker.set, line);
if (ix > -1) this.marker.set.splice(ix, 1);
},
split: function(pos, lenBefore) {
if (this.to <= pos && this.to != null) return null;
var from = this.from < pos || this.from == null ? null : this.from - pos + lenBefore;
var to = this.to == null ? null : this.to - pos + lenBefore;
return new MarkedText(from, to, this.style, this.marker);
},
dup: function() { return new MarkedText(null, null, this.style, this.marker); },
clipTo: function(fromOpen, from, toOpen, to, diff) {
if (fromOpen && to > this.from && (to < this.to || this.to == null))
this.from = null;
else if (this.from != null && this.from >= from)
this.from = Math.max(to, this.from) + diff;
if (toOpen && (from < this.to || this.to == null) && (from > this.from || this.from == null))
this.to = null;
else if (this.to != null && this.to > from)
this.to = to < this.to ? this.to + diff : from;
},
isDead: function() { return this.from != null && this.to != null && this.from >= this.to; },
sameSet: function(x) { return this.marker == x.marker; }
};
// TEXTMARKERS
function Bookmark(pos) {
this.from = pos; this.to = pos; this.line = null;
function TextMarker(doc, type) {
this.lines = [];
this.type = type;
this.doc = doc;
}
Bookmark.prototype = {
attach: function(line) { this.line = line; },
detach: function(line) { if (this.line == line) this.line = null; },
split: function(pos, lenBefore) {
if (pos < this.from) {
this.from = this.to = (this.from - pos) + lenBefore;
return this;
}
},
isDead: function() { return this.from > this.to; },
clipTo: function(fromOpen, from, toOpen, to, diff) {
if ((fromOpen || from < this.from) && (toOpen || to > this.to)) {
this.from = 0; this.to = -1;
} else if (this.from > from) {
this.from = this.to = Math.max(to, this.from) + diff;
}
},
sameSet: function(x) { return false; },
find: function() {
if (!this.line || !this.line.parent) return null;
return {line: lineNo(this.line), ch: this.from};
},
clear: function() {
if (this.line) {
var found = indexOf(this.line.marked, this);
if (found != -1) this.line.marked.splice(found, 1);
this.line = null;
CodeMirror.TextMarker = TextMarker;
eventMixin(TextMarker);
TextMarker.prototype.clear = function() {
if (this.explicitlyCleared) return;
var cm = this.doc.cm, withOp = cm && !cm.curOp;
if (withOp) startOperation(cm);
if (hasHandler(this, "clear")) {
var found = this.find();
if (found) signalLater(this, "clear", found.from, found.to);
}
var min = null, max = null;
for (var i = 0; i < this.lines.length; ++i) {
var line = this.lines[i];
var span = getMarkedSpanFor(line.markedSpans, this);
if (span.to != null) max = lineNo(line);
line.markedSpans = removeMarkedSpan(line.markedSpans, span);
if (span.from != null)
min = lineNo(line);
else if (this.collapsed && !lineIsHidden(this.doc, line) && cm)
updateLineHeight(line, textHeight(cm.display));
}
if (cm && this.collapsed && !cm.options.lineWrapping) for (var i = 0; i < this.lines.length; ++i) {
var visual = visualLine(cm.doc, this.lines[i]), len = lineLength(cm.doc, visual);
if (len > cm.display.maxLineLength) {
cm.display.maxLine = visual;
cm.display.maxLineLength = len;
cm.display.maxLineChanged = true;
}
}
if (min != null && cm) regChange(cm, min, max + 1);
this.lines.length = 0;
this.explicitlyCleared = true;
if (this.atomic && this.doc.cantEdit) {
this.doc.cantEdit = false;
if (cm) reCheckSelection(cm);
}
if (withOp) endOperation(cm);
};
TextMarker.prototype.find = function(bothSides) {
var from, to;
for (var i = 0; i < this.lines.length; ++i) {
var line = this.lines[i];
var span = getMarkedSpanFor(line.markedSpans, this);
if (span.from != null || span.to != null) {
var found = lineNo(line);
if (span.from != null) from = Pos(found, span.from);
if (span.to != null) to = Pos(found, span.to);
}
}
if (this.type == "bookmark" && !bothSides) return from;
return from && {from: from, to: to};
};
TextMarker.prototype.changed = function() {
var pos = this.find(), cm = this.doc.cm;
if (!pos || !cm) return;
if (this.type != "bookmark") pos = pos.from;
var line = getLine(this.doc, pos.line);
clearCachedMeasurement(cm, line);
if (pos.line >= cm.display.showingFrom && pos.line < cm.display.showingTo) {
for (var node = cm.display.lineDiv.firstChild; node; node = node.nextSibling) if (node.lineObj == line) {
if (node.offsetHeight != line.height) updateLineHeight(line, node.offsetHeight);
break;
}
runInOp(cm, function() {
cm.curOp.selectionChanged = cm.curOp.forceUpdate = cm.curOp.updateMaxLine = true;
});
}
};
TextMarker.prototype.attachLine = function(line) {
if (!this.lines.length && this.doc.cm) {
var op = this.doc.cm.curOp;
if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1)
(op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this);
}
this.lines.push(line);
};
TextMarker.prototype.detachLine = function(line) {
this.lines.splice(indexOf(this.lines, line), 1);
if (!this.lines.length && this.doc.cm) {
var op = this.doc.cm.curOp;
(op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this);
}
};
var nextMarkerId = 0;
function markText(doc, from, to, options, type) {
if (options && options.shared) return markTextShared(doc, from, to, options, type);
if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type);
var marker = new TextMarker(doc, type);
if (options) copyObj(options, marker);
if (posLess(to, from) || posEq(from, to) && marker.clearWhenEmpty !== false)
return marker;
if (marker.replacedWith) {
marker.collapsed = true;
marker.replacedWith = elt("span", [marker.replacedWith], "CodeMirror-widget");
if (!options.handleMouseEvents) marker.replacedWith.ignoreEvents = true;
}
if (marker.collapsed) {
if (conflictingCollapsedRange(doc, from.line, from, to, marker) ||
from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker))
throw new Error("Inserting collapsed marker partially overlapping an existing one");
sawCollapsedSpans = true;
}
if (marker.addToHistory)
addToHistory(doc, {from: from, to: to, origin: "markText"},
{head: doc.sel.head, anchor: doc.sel.anchor}, NaN);
var curLine = from.line, cm = doc.cm, updateMaxLine;
doc.iter(curLine, to.line + 1, function(line) {
if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(doc, line) == cm.display.maxLine)
updateMaxLine = true;
var span = {from: null, to: null, marker: marker};
if (curLine == from.line) span.from = from.ch;
if (curLine == to.line) span.to = to.ch;
if (marker.collapsed && curLine != from.line) updateLineHeight(line, 0);
addMarkedSpan(line, span);
++curLine;
});
if (marker.collapsed) doc.iter(from.line, to.line + 1, function(line) {
if (lineIsHidden(doc, line)) updateLineHeight(line, 0);
});
if (marker.clearOnEnter) on(marker, "beforeCursorEnter", function() { marker.clear(); });
if (marker.readOnly) {
sawReadOnlySpans = true;
if (doc.history.done.length || doc.history.undone.length)
doc.clearHistory();
}
if (marker.collapsed) {
marker.id = ++nextMarkerId;
marker.atomic = true;
}
if (cm) {
if (updateMaxLine) cm.curOp.updateMaxLine = true;
if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.collapsed)
regChange(cm, from.line, to.line + 1);
if (marker.atomic) reCheckSelection(cm);
}
return marker;
}
// SHARED TEXTMARKERS
function SharedTextMarker(markers, primary) {
this.markers = markers;
this.primary = primary;
for (var i = 0, me = this; i < markers.length; ++i) {
markers[i].parent = this;
on(markers[i], "clear", function(){me.clear();});
}
}
CodeMirror.SharedTextMarker = SharedTextMarker;
eventMixin(SharedTextMarker);
SharedTextMarker.prototype.clear = function() {
if (this.explicitlyCleared) return;
this.explicitlyCleared = true;
for (var i = 0; i < this.markers.length; ++i)
this.markers[i].clear();
signalLater(this, "clear");
};
SharedTextMarker.prototype.find = function() {
return this.primary.find();
};
function markTextShared(doc, from, to, options, type) {
options = copyObj(options);
options.shared = false;
var markers = [markText(doc, from, to, options, type)], primary = markers[0];
var widget = options.replacedWith;
linkedDocs(doc, function(doc) {
if (widget) options.replacedWith = widget.cloneNode(true);
markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type));
for (var i = 0; i < doc.linked.length; ++i)
if (doc.linked[i].isParent) return;
primary = lst(markers);
});
return new SharedTextMarker(markers, primary);
}
// TEXTMARKER SPANS
function getMarkedSpanFor(spans, marker) {
if (spans) for (var i = 0; i < spans.length; ++i) {
var span = spans[i];
if (span.marker == marker) return span;
}
}
function removeMarkedSpan(spans, span) {
for (var r, i = 0; i < spans.length; ++i)
if (spans[i] != span) (r || (r = [])).push(spans[i]);
return r;
}
function addMarkedSpan(line, span) {
line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span];
span.marker.attachLine(line);
}
function markedSpansBefore(old, startCh, isInsert) {
if (old) for (var i = 0, nw; i < old.length; ++i) {
var span = old[i], marker = span.marker;
var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh);
if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) {
var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh);
(nw || (nw = [])).push({from: span.from,
to: endsAfter ? null : span.to,
marker: marker});
}
}
return nw;
}
function markedSpansAfter(old, endCh, isInsert) {
if (old) for (var i = 0, nw; i < old.length; ++i) {
var span = old[i], marker = span.marker;
var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh);
if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) {
var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh);
(nw || (nw = [])).push({from: startsBefore ? null : span.from - endCh,
to: span.to == null ? null : span.to - endCh,
marker: marker});
}
}
return nw;
}
function stretchSpansOverChange(doc, change) {
var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans;
var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans;
if (!oldFirst && !oldLast) return null;
var startCh = change.from.ch, endCh = change.to.ch, isInsert = posEq(change.from, change.to);
// Get the spans that 'stick out' on both sides
var first = markedSpansBefore(oldFirst, startCh, isInsert);
var last = markedSpansAfter(oldLast, endCh, isInsert);
// Next, merge those two ends
var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0);
if (first) {
// Fix up .to properties of first
for (var i = 0; i < first.length; ++i) {
var span = first[i];
if (span.to == null) {
var found = getMarkedSpanFor(last, span.marker);
if (!found) span.to = startCh;
else if (sameLine) span.to = found.to == null ? null : found.to + offset;
}
}
}
if (last) {
// Fix up .from in last (or move them into first in case of sameLine)
for (var i = 0; i < last.length; ++i) {
var span = last[i];
if (span.to != null) span.to += offset;
if (span.from == null) {
var found = getMarkedSpanFor(first, span.marker);
if (!found) {
span.from = offset;
if (sameLine) (first || (first = [])).push(span);
}
} else {
span.from += offset;
if (sameLine) (first || (first = [])).push(span);
}
}
}
// Make sure we didn't create any zero-length spans
if (first) first = clearEmptySpans(first);
if (last && last != first) last = clearEmptySpans(last);
var newMarkers = [first];
if (!sameLine) {
// Fill gap with whole-line-spans
var gap = change.text.length - 2, gapMarkers;
if (gap > 0 && first)
for (var i = 0; i < first.length; ++i)
if (first[i].to == null)
(gapMarkers || (gapMarkers = [])).push({from: null, to: null, marker: first[i].marker});
for (var i = 0; i < gap; ++i)
newMarkers.push(gapMarkers);
newMarkers.push(last);
}
return newMarkers;
}
function clearEmptySpans(spans) {
for (var i = 0; i < spans.length; ++i) {
var span = spans[i];
if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false)
spans.splice(i--, 1);
}
if (!spans.length) return null;
return spans;
}
function mergeOldSpans(doc, change) {
var old = getOldSpans(doc, change);
var stretched = stretchSpansOverChange(doc, change);
if (!old) return stretched;
if (!stretched) return old;
for (var i = 0; i < old.length; ++i) {
var oldCur = old[i], stretchCur = stretched[i];
if (oldCur && stretchCur) {
spans: for (var j = 0; j < stretchCur.length; ++j) {
var span = stretchCur[j];
for (var k = 0; k < oldCur.length; ++k)
if (oldCur[k].marker == span.marker) continue spans;
oldCur.push(span);
}
} else if (stretchCur) {
old[i] = stretchCur;
}
}
return old;
}
function removeReadOnlyRanges(doc, from, to) {
var markers = null;
doc.iter(from.line, to.line + 1, function(line) {
if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) {
var mark = line.markedSpans[i].marker;
if (mark.readOnly && (!markers || indexOf(markers, mark) == -1))
(markers || (markers = [])).push(mark);
}
});
if (!markers) return null;
var parts = [{from: from, to: to}];
for (var i = 0; i < markers.length; ++i) {
var mk = markers[i], m = mk.find();
for (var j = 0; j < parts.length; ++j) {
var p = parts[j];
if (posLess(p.to, m.from) || posLess(m.to, p.from)) continue;
var newParts = [j, 1];
if (posLess(p.from, m.from) || !mk.inclusiveLeft && posEq(p.from, m.from))
newParts.push({from: p.from, to: m.from});
if (posLess(m.to, p.to) || !mk.inclusiveRight && posEq(p.to, m.to))
newParts.push({from: m.to, to: p.to});
parts.splice.apply(parts, newParts);
j += newParts.length - 1;
}
}
return parts;
}
function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0; }
function extraRight(marker) { return marker.inclusiveRight ? 1 : 0; }
function compareCollapsedMarkers(a, b) {
var lenDiff = a.lines.length - b.lines.length;
if (lenDiff != 0) return lenDiff;
var aPos = a.find(), bPos = b.find();
var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b);
if (fromCmp) return -fromCmp;
var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b);
if (toCmp) return toCmp;
return b.id - a.id;
}
function collapsedSpanAtSide(line, start) {
var sps = sawCollapsedSpans && line.markedSpans, found;
if (sps) for (var sp, i = 0; i < sps.length; ++i) {
sp = sps[i];
if (sp.marker.collapsed && (start ? sp.from : sp.to) == null &&
(!found || compareCollapsedMarkers(found, sp.marker) < 0))
found = sp.marker;
}
return found;
}
function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true); }
function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false); }
function conflictingCollapsedRange(doc, lineNo, from, to, marker) {
var line = getLine(doc, lineNo);
var sps = sawCollapsedSpans && line.markedSpans;
if (sps) for (var i = 0; i < sps.length; ++i) {
var sp = sps[i];
if (!sp.marker.collapsed) continue;
var found = sp.marker.find(true);
var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker);
var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker);
if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) continue;
if (fromCmp <= 0 && (cmp(found.to, from) || extraRight(sp.marker) - extraLeft(marker)) > 0 ||
fromCmp >= 0 && (cmp(found.from, to) || extraLeft(sp.marker) - extraRight(marker)) < 0)
return true;
}
}
function visualLine(doc, line) {
var merged;
while (merged = collapsedSpanAtStart(line))
line = getLine(doc, merged.find().from.line);
return line;
}
function lineIsHidden(doc, line) {
var sps = sawCollapsedSpans && line.markedSpans;
if (sps) for (var sp, i = 0; i < sps.length; ++i) {
sp = sps[i];
if (!sp.marker.collapsed) continue;
if (sp.from == null) return true;
if (sp.marker.replacedWith) continue;
if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp))
return true;
}
}
function lineIsHiddenInner(doc, line, span) {
if (span.to == null) {
var end = span.marker.find().to, endLine = getLine(doc, end.line);
return lineIsHiddenInner(doc, endLine, getMarkedSpanFor(endLine.markedSpans, span.marker));
}
if (span.marker.inclusiveRight && span.to == line.text.length)
return true;
for (var sp, i = 0; i < line.markedSpans.length; ++i) {
sp = line.markedSpans[i];
if (sp.marker.collapsed && !sp.marker.replacedWith && sp.from == span.to &&
(sp.to == null || sp.to != span.from) &&
(sp.marker.inclusiveLeft || span.marker.inclusiveRight) &&
lineIsHiddenInner(doc, line, sp)) return true;
}
}
function detachMarkedSpans(line) {
var spans = line.markedSpans;
if (!spans) return;
for (var i = 0; i < spans.length; ++i)
spans[i].marker.detachLine(line);
line.markedSpans = null;
}
function attachMarkedSpans(line, spans) {
if (!spans) return;
for (var i = 0; i < spans.length; ++i)
spans[i].marker.attachLine(line);
line.markedSpans = spans;
}
// LINE WIDGETS
var LineWidget = CodeMirror.LineWidget = function(cm, node, options) {
if (options) for (var opt in options) if (options.hasOwnProperty(opt))
this[opt] = options[opt];
this.cm = cm;
this.node = node;
};
eventMixin(LineWidget);
function widgetOperation(f) {
return function() {
var withOp = !this.cm.curOp;
if (withOp) startOperation(this.cm);
try {var result = f.apply(this, arguments);}
finally {if (withOp) endOperation(this.cm);}
return result;
};
}
LineWidget.prototype.clear = widgetOperation(function() {
var ws = this.line.widgets, no = lineNo(this.line);
if (no == null || !ws) return;
for (var i = 0; i < ws.length; ++i) if (ws[i] == this) ws.splice(i--, 1);
if (!ws.length) this.line.widgets = null;
var aboveVisible = heightAtLine(this.cm, this.line) < this.cm.doc.scrollTop;
updateLineHeight(this.line, Math.max(0, this.line.height - widgetHeight(this)));
if (aboveVisible) addToScrollPos(this.cm, 0, -this.height);
regChange(this.cm, no, no + 1);
});
LineWidget.prototype.changed = widgetOperation(function() {
var oldH = this.height;
this.height = null;
var diff = widgetHeight(this) - oldH;
if (!diff) return;
updateLineHeight(this.line, this.line.height + diff);
var no = lineNo(this.line);
regChange(this.cm, no, no + 1);
});
function widgetHeight(widget) {
if (widget.height != null) return widget.height;
if (!widget.node.parentNode || widget.node.parentNode.nodeType != 1)
removeChildrenAndAdd(widget.cm.display.measure, elt("div", [widget.node], null, "position: relative"));
return widget.height = widget.node.offsetHeight;
}
function addLineWidget(cm, handle, node, options) {
var widget = new LineWidget(cm, node, options);
if (widget.noHScroll) cm.display.alignWidgets = true;
changeLine(cm, handle, function(line) {
var widgets = line.widgets || (line.widgets = []);
if (widget.insertAt == null) widgets.push(widget);
else widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget);
widget.line = line;
if (!lineIsHidden(cm.doc, line) || widget.showIfHidden) {
var aboveVisible = heightAtLine(cm, line) < cm.doc.scrollTop;
updateLineHeight(line, line.height + widgetHeight(widget));
if (aboveVisible) addToScrollPos(cm, 0, widget.height);
}
return true;
});
return widget;
}
// LINE DATA STRUCTURE
// Line objects. These hold state related to a line, including
// highlighting info (the styles array).
function Line(text, styles) {
this.styles = styles || [text, null];
var Line = CodeMirror.Line = function(text, markedSpans, estimateHeight) {
this.text = text;
this.height = 1;
this.marked = this.gutterMarker = this.className = this.bgClassName = this.handlers = null;
this.stateAfter = this.parent = this.hidden = null;
this.widgetFunction = null;
}
Line.inheritMarks = function(text, orig) {
var ln = new Line(text), mk = orig && orig.marked;
if (mk) {
for (var i = 0; i < mk.length; ++i) {
if (mk[i].to == null && mk[i].style) {
var newmk = ln.marked || (ln.marked = []), mark = mk[i];
var nmark = mark.dup(); newmk.push(nmark); nmark.attach(ln);
}
}
}
return ln;
}
Line.prototype = {
// Replace a piece of a line, keeping the styles around it intact.
replace: function(from, to_, text) {
var st = [], mk = this.marked, to = to_ == null ? this.text.length : to_;
copyStyles(0, from, this.styles, st);
if (text) st.push(text, null);
copyStyles(to, this.text.length, this.styles, st);
this.styles = st;
this.text = this.text.slice(0, from) + text + this.text.slice(to);
this.stateAfter = null;
if (mk) {
var diff = text.length - (to - from);
for (var i = 0; i < mk.length; ++i) {
var mark = mk[i];
mark.clipTo(from == null, from || 0, to_ == null, to, diff);
if (mark.isDead()) {mark.detach(this); mk.splice(i--, 1);}
}
}
},
// Split a part off a line, keeping styles and markers intact.
split: function(pos, textBefore) {
var st = [textBefore, null], mk = this.marked;
copyStyles(pos, this.text.length, this.styles, st);
var taken = new Line(textBefore + this.text.slice(pos), st);
if (mk) {
for (var i = 0; i < mk.length; ++i) {
var mark = mk[i];
var newmark = mark.split(pos, textBefore.length);
if (newmark) {
if (!taken.marked) taken.marked = [];
taken.marked.push(newmark); newmark.attach(taken);
if (newmark == mark) mk.splice(i--, 1);
}
}
}
return taken;
},
append: function(line) {
var mylen = this.text.length, mk = line.marked, mymk = this.marked;
this.text += line.text;
copyStyles(0, line.text.length, line.styles, this.styles);
if (mymk) {
for (var i = 0; i < mymk.length; ++i)
if (mymk[i].to == null) mymk[i].to = mylen;
}
if (mk && mk.length) {
if (!mymk) this.marked = mymk = [];
outer: for (var i = 0; i < mk.length; ++i) {
var mark = mk[i];
if (!mark.from) {
for (var j = 0; j < mymk.length; ++j) {
var mymark = mymk[j];
if (mymark.to == mylen && mymark.sameSet(mark)) {
mymark.to = mark.to == null ? null : mark.to + mylen;
if (mymark.isDead()) {
mymark.detach(this);
mk.splice(i--, 1);
}
continue outer;
}
}
}
mymk.push(mark);
mark.attach(this);
mark.from += mylen;
if (mark.to != null) mark.to += mylen;
}
}
},
fixMarkEnds: function(other) {
var mk = this.marked, omk = other.marked;
if (!mk) return;
for (var i = 0; i < mk.length; ++i) {
var mark = mk[i], close = mark.to == null;
if (close && omk) {
for (var j = 0; j < omk.length; ++j)
if (omk[j].sameSet(mark)) {close = false; break;}
}
if (close) mark.to = this.text.length;
}
},
fixMarkStarts: function() {
var mk = this.marked;
if (!mk) return;
for (var i = 0; i < mk.length; ++i)
if (mk[i].from == null) mk[i].from = 0;
},
addMark: function(mark) {
mark.attach(this);
if (this.marked == null) this.marked = [];
this.marked.push(mark);
this.marked.sort(function(a, b){return (a.from || 0) - (b.from || 0);});
},
// Run the given mode's parser over a line, update the styles
// array, which contains alternating fragments of text and CSS
// classes.
highlight: function(mode, state, tabSize) {
var stream = new StringStream(this.text, tabSize), st = this.styles, pos = 0;
var changed = false, curWord = st[0], prevWord;
if (this.text == "" && mode.blankLine) mode.blankLine(state);
while (!stream.eol()) {
var style = mode.token(stream, state);
var substr = this.text.slice(stream.start, stream.pos);
stream.start = stream.pos;
if (pos && st[pos-1] == style)
st[pos-2] += substr;
else if (substr) {
if (!changed && (st[pos+1] != style || (pos && st[pos-2] != prevWord))) changed = true;
st[pos++] = substr; st[pos++] = style;
prevWord = curWord; curWord = st[pos];
}
// Give up when line is ridiculously long
if (stream.pos > 5000) {
st[pos++] = this.text.slice(stream.pos); st[pos++] = null;
break;
}
}
if (st.length != pos) {st.length = pos; changed = true;}
if (pos && st[pos-2] != prevWord) changed = true;
if (st.length == 2 && typeof st[1] == 'object') {
this.widgetFunction = st[1];
st[1] = null;
} else {
this.widgetFunction = null;
}
// Short lines with simple highlights return null, and are
// counted as changed by the driver because they are likely to
// highlight the same way in various contexts.
return changed || (st.length < 5 && this.text.length < 10 ? null : false);
},
nodeAdded: function(node) {
if (this.widgetFunction) this.widgetFunction.callback(node, this);
},
// Fetch the parser token for a given character. Useful for hacks
// that want to inspect the mode state (say, for completion).
getTokenAt: function(mode, state, ch) {
var txt = this.text, stream = new StringStream(txt);
while (stream.pos < ch && !stream.eol()) {
stream.start = stream.pos;
var style = mode.token(stream, state);
}
return {start: stream.start,
end: stream.pos,
string: stream.current(),
className: style || null,
state: state};
},
indentation: function(tabSize) {return countColumn(this.text, null, tabSize);},
// Produces an HTML fragment for the line, taking selection,
// marking, and highlighting into account.
getHTML: function(makeTab, endAt) {
var html = [], first = true, col = 0;
function span(text, style) {
if (!text) return;
// Work around a bug where, in some compat modes, IE ignores leading spaces
if (first && ie && text.charAt(0) == " ") text = "\u00a0" + text.slice(1);
first = false;
if (text.indexOf("\t") == -1) {
col += text.length;
var escaped = htmlEscape(text);
} else {
var escaped = "";
for (var pos = 0;;) {
var idx = text.indexOf("\t", pos);
if (idx == -1) {
escaped += htmlEscape(text.slice(pos));
col += text.length - pos;
break;
} else {
col += idx - pos;
var tab = makeTab(col);
escaped += htmlEscape(text.slice(pos, idx)) + tab.html;
col += tab.width;
pos = idx + 1;
}
}
}
if (style) html.push('<span class="', style, '">', escaped, "</span>");
else html.push(escaped);
}
var st = this.styles, allText = this.text, marked = this.marked;
var len = allText.length;
if (this.widgetFunction) return this.widgetFunction.creator(allText);
if (endAt != null) len = Math.min(endAt, len);
function styleToClass(style) {
if (!style) return null;
return "cm-" + style.replace(/ +/g, " cm-");
}
if (!allText && endAt == null)
span(" ");
else if (!marked || !marked.length)
for (var i = 0, ch = 0; ch < len; i+=2) {
var str = st[i], style = st[i+1], l = str.length;
if (ch + l > len) str = str.slice(0, len - ch);
ch += l;
span(str, styleToClass(style));
}
else {
var pos = 0, i = 0, text = "", style, sg = 0;
var nextChange = marked[0].from || 0, marks = [], markpos = 0;
function advanceMarks() {
var m;
while (markpos < marked.length &&
((m = marked[markpos]).from == pos || m.from == null)) {
if (m.style != null) marks.push(m);
++markpos;
}
nextChange = markpos < marked.length ? marked[markpos].from : Infinity;
for (var i = 0; i < marks.length; ++i) {
var to = marks[i].to || Infinity;
if (to == pos) marks.splice(i--, 1);
else nextChange = Math.min(to, nextChange);
}
}
var m = 0;
while (pos < len) {
if (nextChange == pos) advanceMarks();
var upto = Math.min(len, nextChange);
while (true) {
if (text) {
var end = pos + text.length;
var appliedStyle = style;
for (var j = 0; j < marks.length; ++j)
appliedStyle = (appliedStyle ? appliedStyle + " " : "") + marks[j].style;
span(end > upto ? text.slice(0, upto - pos) : text, appliedStyle);
if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;}
pos = end;
}
text = st[i++]; style = styleToClass(st[i++]);
}
}
}
return html.join("");
},
cleanUp: function() {
this.parent = null;
if (this.marked)
for (var i = 0, e = this.marked.length; i < e; ++i) this.marked[i].detach(this);
}
attachMarkedSpans(this, markedSpans);
this.height = estimateHeight ? estimateHeight(this) : 1;
};
// Utility used by replace and split above
function copyStyles(from, to, source, dest) {
for (var i = 0, pos = 0, state = 0; pos < to; i+=2) {
var part = source[i], end = pos + part.length;
if (state == 0) {
if (end > from) dest.push(part.slice(from - pos, Math.min(part.length, to - pos)), source[i+1]);
if (end >= from) state = 1;
eventMixin(Line);
Line.prototype.lineNo = function() { return lineNo(this); };
function updateLine(line, text, markedSpans, estimateHeight) {
line.text = text;
if (line.stateAfter) line.stateAfter = null;
if (line.styles) line.styles = null;
if (line.order != null) line.order = null;
detachMarkedSpans(line);
attachMarkedSpans(line, markedSpans);
var estHeight = estimateHeight ? estimateHeight(line) : 1;
if (estHeight != line.height) updateLineHeight(line, estHeight);
}
function cleanUpLine(line) {
line.parent = null;
detachMarkedSpans(line);
}
// Run the given mode's parser over a line, update the styles
// array, which contains alternating fragments of text and CSS
// classes.
function runMode(cm, text, mode, state, f, forceToEnd) {
var flattenSpans = mode.flattenSpans;
if (flattenSpans == null) flattenSpans = cm.options.flattenSpans;
var curStart = 0, curStyle = null;
var stream = new StringStream(text, cm.options.tabSize), style;
if (text == "" && mode.blankLine) mode.blankLine(state);
while (!stream.eol()) {
if (stream.pos > cm.options.maxHighlightLength) {
flattenSpans = false;
if (forceToEnd) processLine(cm, text, state, stream.pos);
stream.pos = text.length;
style = null;
} else {
style = mode.token(stream, state);
}
else if (state == 1) {
if (end > to) dest.push(part.slice(0, to - pos), source[i+1]);
else dest.push(part, source[i+1]);
if (cm.options.addModeClass) {
var mName = CodeMirror.innerMode(mode, state).mode.name;
if (mName) style = "m-" + (style ? mName + " " + style : mName);
}
pos = end;
if (!flattenSpans || curStyle != style) {
if (curStart < stream.start) f(stream.start, curStyle);
curStart = stream.start; curStyle = style;
}
stream.start = stream.pos;
}
while (curStart < stream.pos) {
// Webkit seems to refuse to render text nodes longer than 57444 characters
var pos = Math.min(stream.pos, curStart + 50000);
f(pos, curStyle);
curStart = pos;
}
}
// Data structure that holds the sequence of lines.
function highlightLine(cm, line, state, forceToEnd) {
// A styles array always starts with a number identifying the
// mode/overlays that it is based on (for easy invalidation).
var st = [cm.state.modeGen];
// Compute the base array of styles
runMode(cm, line.text, cm.doc.mode, state, function(end, style) {
st.push(end, style);
}, forceToEnd);
// Run overlays, adjust style array.
for (var o = 0; o < cm.state.overlays.length; ++o) {
var overlay = cm.state.overlays[o], i = 1, at = 0;
runMode(cm, line.text, overlay.mode, true, function(end, style) {
var start = i;
// Ensure there's a token end at the current position, and that i points at it
while (at < end) {
var i_end = st[i];
if (i_end > end)
st.splice(i, 1, end, st[i+1], i_end);
i += 2;
at = Math.min(end, i_end);
}
if (!style) return;
if (overlay.opaque) {
st.splice(start, i - start, end, style);
i = start + 2;
} else {
for (; start < i; start += 2) {
var cur = st[start+1];
st[start+1] = cur ? cur + " " + style : style;
}
}
});
}
return st;
}
function getLineStyles(cm, line) {
if (!line.styles || line.styles[0] != cm.state.modeGen)
line.styles = highlightLine(cm, line, line.stateAfter = getStateBefore(cm, lineNo(line)));
return line.styles;
}
// Lightweight form of highlight -- proceed over this line and
// update state, but don't save a style array.
function processLine(cm, text, state, startAt) {
var mode = cm.doc.mode;
var stream = new StringStream(text, cm.options.tabSize);
stream.start = stream.pos = startAt || 0;
if (text == "" && mode.blankLine) mode.blankLine(state);
while (!stream.eol() && stream.pos <= cm.options.maxHighlightLength) {
mode.token(stream, state);
stream.start = stream.pos;
}
}
var styleToClassCache = {}, styleToClassCacheWithMode = {};
function interpretTokenStyle(style, builder) {
if (!style) return null;
for (;;) {
var lineClass = style.match(/(?:^|\s)line-(background-)?(\S+)/);
if (!lineClass) break;
style = style.slice(0, lineClass.index) + style.slice(lineClass.index + lineClass[0].length);
var prop = lineClass[1] ? "bgClass" : "textClass";
if (builder[prop] == null)
builder[prop] = lineClass[2];
else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(builder[prop]))
builder[prop] += " " + lineClass[2];
}
var cache = builder.cm.options.addModeClass ? styleToClassCacheWithMode : styleToClassCache;
return cache[style] ||
(cache[style] = "cm-" + style.replace(/ +/g, " cm-"));
}
function buildLineContent(cm, realLine, measure, copyWidgets) {
var merged, line = realLine, empty = true;
while (merged = collapsedSpanAtStart(line))
line = getLine(cm.doc, merged.find().from.line);
var builder = {pre: elt("pre"), col: 0, pos: 0,
measure: null, measuredSomething: false, cm: cm,
copyWidgets: copyWidgets};
do {
if (line.text) empty = false;
builder.measure = line == realLine && measure;
builder.pos = 0;
builder.addToken = builder.measure ? buildTokenMeasure : buildToken;
if ((old_ie || webkit) && cm.getOption("lineWrapping"))
builder.addToken = buildTokenSplitSpaces(builder.addToken);
var next = insertLineContent(line, builder, getLineStyles(cm, line));
if (measure && line == realLine && !builder.measuredSomething) {
measure[0] = builder.pre.appendChild(zeroWidthElement(cm.display.measure));
builder.measuredSomething = true;
}
if (next) line = getLine(cm.doc, next.to.line);
} while (next);
if (measure && !builder.measuredSomething && !measure[0])
measure[0] = builder.pre.appendChild(empty ? elt("span", "\u00a0") : zeroWidthElement(cm.display.measure));
if (!builder.pre.firstChild && !lineIsHidden(cm.doc, realLine))
builder.pre.appendChild(document.createTextNode("\u00a0"));
var order;
// Work around problem with the reported dimensions of single-char
// direction spans on IE (issue #1129). See also the comment in
// cursorCoords.
if (measure && ie && (order = getOrder(line))) {
var l = order.length - 1;
if (order[l].from == order[l].to) --l;
var last = order[l], prev = order[l - 1];
if (last.from + 1 == last.to && prev && last.level < prev.level) {
var span = measure[builder.pos - 1];
if (span) span.parentNode.insertBefore(span.measureRight = zeroWidthElement(cm.display.measure),
span.nextSibling);
}
}
var textClass = builder.textClass ? builder.textClass + " " + (realLine.textClass || "") : realLine.textClass;
if (textClass) builder.pre.className = textClass;
signal(cm, "renderLine", cm, realLine, builder.pre);
return builder;
}
function defaultSpecialCharPlaceholder(ch) {
var token = elt("span", "\u2022", "cm-invalidchar");
token.title = "\\u" + ch.charCodeAt(0).toString(16);
return token;
}
function buildToken(builder, text, style, startStyle, endStyle, title) {
if (!text) return;
var special = builder.cm.options.specialChars;
if (!special.test(text)) {
builder.col += text.length;
var content = document.createTextNode(text);
} else {
var content = document.createDocumentFragment(), pos = 0;
while (true) {
special.lastIndex = pos;
var m = special.exec(text);
var skipped = m ? m.index - pos : text.length - pos;
if (skipped) {
content.appendChild(document.createTextNode(text.slice(pos, pos + skipped)));
builder.col += skipped;
}
if (!m) break;
pos += skipped + 1;
if (m[0] == "\t") {
var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize;
content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab"));
builder.col += tabWidth;
} else {
var token = builder.cm.options.specialCharPlaceholder(m[0]);
content.appendChild(token);
builder.col += 1;
}
}
}
if (style || startStyle || endStyle || builder.measure) {
var fullStyle = style || "";
if (startStyle) fullStyle += startStyle;
if (endStyle) fullStyle += endStyle;
var token = elt("span", [content], fullStyle);
if (title) token.title = title;
return builder.pre.appendChild(token);
}
builder.pre.appendChild(content);
}
function buildTokenMeasure(builder, text, style, startStyle, endStyle) {
var wrapping = builder.cm.options.lineWrapping;
for (var i = 0; i < text.length; ++i) {
var start = i == 0, to = i + 1;
while (to < text.length && isExtendingChar(text.charAt(to))) ++to;
var ch = text.slice(i, to);
i = to - 1;
if (i && wrapping && spanAffectsWrapping(text, i))
builder.pre.appendChild(elt("wbr"));
var old = builder.measure[builder.pos];
var span = builder.measure[builder.pos] =
buildToken(builder, ch, style,
start && startStyle, i == text.length - 1 && endStyle);
if (old) span.leftSide = old.leftSide || old;
// In IE single-space nodes wrap differently than spaces
// embedded in larger text nodes, except when set to
// white-space: normal (issue #1268).
if (old_ie && wrapping && ch == " " && i && !/\s/.test(text.charAt(i - 1)) &&
i < text.length - 1 && !/\s/.test(text.charAt(i + 1)))
span.style.whiteSpace = "normal";
builder.pos += ch.length;
}
if (text.length) builder.measuredSomething = true;
}
function buildTokenSplitSpaces(inner) {
function split(old) {
var out = " ";
for (var i = 0; i < old.length - 2; ++i) out += i % 2 ? " " : "\u00a0";
out += " ";
return out;
}
return function(builder, text, style, startStyle, endStyle, title) {
return inner(builder, text.replace(/ {3,}/g, split), style, startStyle, endStyle, title);
};
}
function buildCollapsedSpan(builder, size, marker, ignoreWidget) {
var widget = !ignoreWidget && marker.replacedWith;
if (widget) {
if (builder.copyWidgets) widget = widget.cloneNode(true);
builder.pre.appendChild(widget);
if (builder.measure) {
if (size) {
builder.measure[builder.pos] = widget;
} else {
var elt = zeroWidthElement(builder.cm.display.measure);
if (marker.type == "bookmark" && !marker.insertLeft)
builder.measure[builder.pos] = builder.pre.appendChild(elt);
else if (builder.measure[builder.pos])
return;
else
builder.measure[builder.pos] = builder.pre.insertBefore(elt, widget);
}
builder.measuredSomething = true;
}
}
builder.pos += size;
}
// Outputs a number of spans to make up a line, taking highlighting
// and marked text into account.
function insertLineContent(line, builder, styles) {
var spans = line.markedSpans, allText = line.text, at = 0;
if (!spans) {
for (var i = 1; i < styles.length; i+=2)
builder.addToken(builder, allText.slice(at, at = styles[i]), interpretTokenStyle(styles[i+1], builder));
return;
}
var len = allText.length, pos = 0, i = 1, text = "", style;
var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed;
for (;;) {
if (nextChange == pos) { // Update current marker set
spanStyle = spanEndStyle = spanStartStyle = title = "";
collapsed = null; nextChange = Infinity;
var foundBookmarks = [];
for (var j = 0; j < spans.length; ++j) {
var sp = spans[j], m = sp.marker;
if (sp.from <= pos && (sp.to == null || sp.to > pos)) {
if (sp.to != null && nextChange > sp.to) { nextChange = sp.to; spanEndStyle = ""; }
if (m.className) spanStyle += " " + m.className;
if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle;
if (m.endStyle && sp.to == nextChange) spanEndStyle += " " + m.endStyle;
if (m.title && !title) title = m.title;
if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0))
collapsed = sp;
} else if (sp.from > pos && nextChange > sp.from) {
nextChange = sp.from;
}
if (m.type == "bookmark" && sp.from == pos && m.replacedWith) foundBookmarks.push(m);
}
if (collapsed && (collapsed.from || 0) == pos) {
buildCollapsedSpan(builder, (collapsed.to == null ? len : collapsed.to) - pos,
collapsed.marker, collapsed.from == null);
if (collapsed.to == null) return collapsed.marker.find();
}
if (!collapsed && foundBookmarks.length) for (var j = 0; j < foundBookmarks.length; ++j)
buildCollapsedSpan(builder, 0, foundBookmarks[j]);
}
if (pos >= len) break;
var upto = Math.min(len, nextChange);
while (true) {
if (text) {
var end = pos + text.length;
if (!collapsed) {
var tokenText = end > upto ? text.slice(0, upto - pos) : text;
builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle,
spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", title);
}
if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;}
pos = end;
spanStartStyle = "";
}
text = allText.slice(at, at = styles[i++]);
style = interpretTokenStyle(styles[i++], builder);
}
}
}
// DOCUMENT DATA STRUCTURE
function updateDoc(doc, change, markedSpans, selAfter, estimateHeight) {
function spansFor(n) {return markedSpans ? markedSpans[n] : null;}
function update(line, text, spans) {
updateLine(line, text, spans, estimateHeight);
signalLater(line, "change", line, change);
}
var from = change.from, to = change.to, text = change.text;
var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line);
var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line;
// First adjust the line structure
if (from.ch == 0 && to.ch == 0 && lastText == "" &&
(!doc.cm || doc.cm.options.wholeLineUpdateBefore)) {
// This is a whole-line replace. Treated specially to make
// sure line objects move the way they are supposed to.
for (var i = 0, e = text.length - 1, added = []; i < e; ++i)
added.push(new Line(text[i], spansFor(i), estimateHeight));
update(lastLine, lastLine.text, lastSpans);
if (nlines) doc.remove(from.line, nlines);
if (added.length) doc.insert(from.line, added);
} else if (firstLine == lastLine) {
if (text.length == 1) {
update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans);
} else {
for (var added = [], i = 1, e = text.length - 1; i < e; ++i)
added.push(new Line(text[i], spansFor(i), estimateHeight));
added.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight));
update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0));
doc.insert(from.line + 1, added);
}
} else if (text.length == 1) {
update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0));
doc.remove(from.line + 1, nlines);
} else {
update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0));
update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans);
for (var i = 1, e = text.length - 1, added = []; i < e; ++i)
added.push(new Line(text[i], spansFor(i), estimateHeight));
if (nlines > 1) doc.remove(from.line + 1, nlines - 1);
doc.insert(from.line + 1, added);
}
signalLater(doc, "change", doc, change);
setSelection(doc, selAfter.anchor, selAfter.head, null, true);
}
function LeafChunk(lines) {
this.lines = lines;
this.parent = null;
@@ -2574,24 +4794,24 @@ var CodeMirror = (function() {
}
this.height = height;
}
LeafChunk.prototype = {
chunkSize: function() { return this.lines.length; },
remove: function(at, n, callbacks) {
removeInner: function(at, n) {
for (var i = at, e = at + n; i < e; ++i) {
var line = this.lines[i];
this.height -= line.height;
line.cleanUp();
if (line.handlers)
for (var j = 0; j < line.handlers.length; ++j) callbacks.push(line.handlers[j]);
cleanUpLine(line);
signalLater(line, "delete");
}
this.lines.splice(at, n);
},
collapse: function(lines) {
lines.splice.apply(lines, [lines.length, 0].concat(this.lines));
},
insertHeight: function(at, lines, height) {
insertInner: function(at, lines, height) {
this.height += height;
this.lines.splice.apply(this.lines, [at, 0].concat(lines));
this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at));
for (var i = 0, e = lines.length; i < e; ++i) lines[i].parent = this;
},
iterN: function(at, n, op) {
@@ -2599,6 +4819,7 @@ var CodeMirror = (function() {
if (op(this.lines[at])) return true;
}
};
function BranchChunk(children) {
this.children = children;
var size = 0, height = 0;
@@ -2611,15 +4832,16 @@ var CodeMirror = (function() {
this.height = height;
this.parent = null;
}
BranchChunk.prototype = {
chunkSize: function() { return this.size; },
remove: function(at, n, callbacks) {
removeInner: function(at, n) {
this.size -= n;
for (var i = 0; i < this.children.length; ++i) {
var child = this.children[i], sz = child.chunkSize();
if (at < sz) {
var rm = Math.min(n, sz - at), oldHeight = child.height;
child.remove(at, rm, callbacks);
child.removeInner(at, rm);
this.height -= oldHeight - child.height;
if (sz == rm) { this.children.splice(i--, 1); child.parent = null; }
if ((n -= rm) == 0) break;
@@ -2636,18 +4858,13 @@ var CodeMirror = (function() {
collapse: function(lines) {
for (var i = 0, e = this.children.length; i < e; ++i) this.children[i].collapse(lines);
},
insert: function(at, lines) {
var height = 0;
for (var i = 0, e = lines.length; i < e; ++i) height += lines[i].height;
this.insertHeight(at, lines, height);
},
insertHeight: function(at, lines, height) {
insertInner: function(at, lines, height) {
this.size += lines.length;
this.height += height;
for (var i = 0, e = this.children.length; i < e; ++i) {
var child = this.children[i], sz = child.chunkSize();
if (at <= sz) {
child.insertHeight(at, lines, height);
child.insertInner(at, lines, height);
if (child.lines && child.lines.length > 50) {
while (child.lines.length > 50) {
var spilled = child.lines.splice(child.lines.length - 25, 25);
@@ -2684,7 +4901,6 @@ var CodeMirror = (function() {
} while (me.children.length > 10);
me.parent.maybeSpill();
},
iter: function(from, to, op) { this.iterN(from, to - from, op); },
iterN: function(at, n, op) {
for (var i = 0, e = this.children.length; i < e; ++i) {
var child = this.children[i], sz = child.chunkSize();
@@ -2698,7 +4914,284 @@ var CodeMirror = (function() {
}
};
function getLineAt(chunk, n) {
var nextDocId = 0;
var Doc = CodeMirror.Doc = function(text, mode, firstLine) {
if (!(this instanceof Doc)) return new Doc(text, mode, firstLine);
if (firstLine == null) firstLine = 0;
BranchChunk.call(this, [new LeafChunk([new Line("", null)])]);
this.first = firstLine;
this.scrollTop = this.scrollLeft = 0;
this.cantEdit = false;
this.history = makeHistory();
this.cleanGeneration = 1;
this.frontier = firstLine;
var start = Pos(firstLine, 0);
this.sel = {from: start, to: start, head: start, anchor: start, shift: false, extend: false, goalColumn: null};
this.id = ++nextDocId;
this.modeOption = mode;
if (typeof text == "string") text = splitLines(text);
updateDoc(this, {from: start, to: start, text: text}, null, {head: start, anchor: start});
};
Doc.prototype = createObj(BranchChunk.prototype, {
constructor: Doc,
iter: function(from, to, op) {
if (op) this.iterN(from - this.first, to - from, op);
else this.iterN(this.first, this.first + this.size, from);
},
insert: function(at, lines) {
var height = 0;
for (var i = 0, e = lines.length; i < e; ++i) height += lines[i].height;
this.insertInner(at - this.first, lines, height);
},
remove: function(at, n) { this.removeInner(at - this.first, n); },
getValue: function(lineSep) {
var lines = getLines(this, this.first, this.first + this.size);
if (lineSep === false) return lines;
return lines.join(lineSep || "\n");
},
setValue: function(code) {
var top = Pos(this.first, 0), last = this.first + this.size - 1;
makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length),
text: splitLines(code), origin: "setValue"},
{head: top, anchor: top}, true);
},
replaceRange: function(code, from, to, origin) {
from = clipPos(this, from);
to = to ? clipPos(this, to) : from;
replaceRange(this, code, from, to, origin);
},
getRange: function(from, to, lineSep) {
var lines = getBetween(this, clipPos(this, from), clipPos(this, to));
if (lineSep === false) return lines;
return lines.join(lineSep || "\n");
},
getLine: function(line) {var l = this.getLineHandle(line); return l && l.text;},
setLine: function(line, text) {
if (isLine(this, line))
replaceRange(this, text, Pos(line, 0), clipPos(this, Pos(line)));
},
removeLine: function(line) {
if (line) replaceRange(this, "", clipPos(this, Pos(line - 1)), clipPos(this, Pos(line)));
else replaceRange(this, "", Pos(0, 0), clipPos(this, Pos(1, 0)));
},
getLineHandle: function(line) {if (isLine(this, line)) return getLine(this, line);},
getLineNumber: function(line) {return lineNo(line);},
getLineHandleVisualStart: function(line) {
if (typeof line == "number") line = getLine(this, line);
return visualLine(this, line);
},
lineCount: function() {return this.size;},
firstLine: function() {return this.first;},
lastLine: function() {return this.first + this.size - 1;},
clipPos: function(pos) {return clipPos(this, pos);},
getCursor: function(start) {
var sel = this.sel, pos;
if (start == null || start == "head") pos = sel.head;
else if (start == "anchor") pos = sel.anchor;
else if (start == "end" || start === false) pos = sel.to;
else pos = sel.from;
return copyPos(pos);
},
somethingSelected: function() {return !posEq(this.sel.head, this.sel.anchor);},
setCursor: docOperation(function(line, ch, extend) {
var pos = clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line);
if (extend) extendSelection(this, pos);
else setSelection(this, pos, pos);
}),
setSelection: docOperation(function(anchor, head, bias) {
setSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), bias);
}),
extendSelection: docOperation(function(from, to, bias) {
extendSelection(this, clipPos(this, from), to && clipPos(this, to), bias);
}),
getSelection: function(lineSep) {return this.getRange(this.sel.from, this.sel.to, lineSep);},
replaceSelection: function(code, collapse, origin) {
makeChange(this, {from: this.sel.from, to: this.sel.to, text: splitLines(code), origin: origin}, collapse || "around");
},
undo: docOperation(function() {makeChangeFromHistory(this, "undo");}),
redo: docOperation(function() {makeChangeFromHistory(this, "redo");}),
setExtending: function(val) {this.sel.extend = val;},
historySize: function() {
var hist = this.history;
return {undo: hist.done.length, redo: hist.undone.length};
},
clearHistory: function() {this.history = makeHistory(this.history.maxGeneration);},
markClean: function() {
this.cleanGeneration = this.changeGeneration(true);
},
changeGeneration: function(forceSplit) {
if (forceSplit)
this.history.lastOp = this.history.lastOrigin = null;
return this.history.generation;
},
isClean: function (gen) {
return this.history.generation == (gen || this.cleanGeneration);
},
getHistory: function() {
return {done: copyHistoryArray(this.history.done),
undone: copyHistoryArray(this.history.undone)};
},
setHistory: function(histData) {
var hist = this.history = makeHistory(this.history.maxGeneration);
hist.done = histData.done.slice(0);
hist.undone = histData.undone.slice(0);
},
markText: function(from, to, options) {
return markText(this, clipPos(this, from), clipPos(this, to), options, "range");
},
setBookmark: function(pos, options) {
var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options),
insertLeft: options && options.insertLeft,
clearWhenEmpty: false};
pos = clipPos(this, pos);
return markText(this, pos, pos, realOpts, "bookmark");
},
findMarksAt: function(pos) {
pos = clipPos(this, pos);
var markers = [], spans = getLine(this, pos.line).markedSpans;
if (spans) for (var i = 0; i < spans.length; ++i) {
var span = spans[i];
if ((span.from == null || span.from <= pos.ch) &&
(span.to == null || span.to >= pos.ch))
markers.push(span.marker.parent || span.marker);
}
return markers;
},
getAllMarks: function() {
var markers = [];
this.iter(function(line) {
var sps = line.markedSpans;
if (sps) for (var i = 0; i < sps.length; ++i)
if (sps[i].from != null) markers.push(sps[i].marker);
});
return markers;
},
posFromIndex: function(off) {
var ch, lineNo = this.first;
this.iter(function(line) {
var sz = line.text.length + 1;
if (sz > off) { ch = off; return true; }
off -= sz;
++lineNo;
});
return clipPos(this, Pos(lineNo, ch));
},
indexFromPos: function (coords) {
coords = clipPos(this, coords);
var index = coords.ch;
if (coords.line < this.first || coords.ch < 0) return 0;
this.iter(this.first, coords.line, function (line) {
index += line.text.length + 1;
});
return index;
},
copy: function(copyHistory) {
var doc = new Doc(getLines(this, this.first, this.first + this.size), this.modeOption, this.first);
doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft;
doc.sel = {from: this.sel.from, to: this.sel.to, head: this.sel.head, anchor: this.sel.anchor,
shift: this.sel.shift, extend: false, goalColumn: this.sel.goalColumn};
if (copyHistory) {
doc.history.undoDepth = this.history.undoDepth;
doc.setHistory(this.getHistory());
}
return doc;
},
linkedDoc: function(options) {
if (!options) options = {};
var from = this.first, to = this.first + this.size;
if (options.from != null && options.from > from) from = options.from;
if (options.to != null && options.to < to) to = options.to;
var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from);
if (options.sharedHist) copy.history = this.history;
(this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist});
copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}];
return copy;
},
unlinkDoc: function(other) {
if (other instanceof CodeMirror) other = other.doc;
if (this.linked) for (var i = 0; i < this.linked.length; ++i) {
var link = this.linked[i];
if (link.doc != other) continue;
this.linked.splice(i, 1);
other.unlinkDoc(this);
break;
}
// If the histories were shared, split them again
if (other.history == this.history) {
var splitIds = [other.id];
linkedDocs(other, function(doc) {splitIds.push(doc.id);}, true);
other.history = makeHistory();
other.history.done = copyHistoryArray(this.history.done, splitIds);
other.history.undone = copyHistoryArray(this.history.undone, splitIds);
}
},
iterLinkedDocs: function(f) {linkedDocs(this, f);},
getMode: function() {return this.mode;},
getEditor: function() {return this.cm;}
});
Doc.prototype.eachLine = Doc.prototype.iter;
// The Doc methods that should be available on CodeMirror instances
var dontDelegate = "iter insert remove copy getEditor".split(" ");
for (var prop in Doc.prototype) if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0)
CodeMirror.prototype[prop] = (function(method) {
return function() {return method.apply(this.doc, arguments);};
})(Doc.prototype[prop]);
eventMixin(Doc);
function linkedDocs(doc, f, sharedHistOnly) {
function propagate(doc, skip, sharedHist) {
if (doc.linked) for (var i = 0; i < doc.linked.length; ++i) {
var rel = doc.linked[i];
if (rel.doc == skip) continue;
var shared = sharedHist && rel.sharedHist;
if (sharedHistOnly && !shared) continue;
f(rel.doc, shared);
propagate(rel.doc, doc, shared);
}
}
propagate(doc, null, true);
}
function attachDoc(cm, doc) {
if (doc.cm) throw new Error("This document is already in use.");
cm.doc = doc;
doc.cm = cm;
estimateLineHeights(cm);
loadMode(cm);
if (!cm.options.lineWrapping) computeMaxLength(cm);
cm.options.mode = doc.modeOption;
regChange(cm);
}
// LINE UTILITIES
function getLine(chunk, n) {
n -= chunk.first;
while (!chunk.lines) {
for (var i = 0;; ++i) {
var child = chunk.children[i], sz = child.chunkSize();
@@ -2708,19 +5201,43 @@ var CodeMirror = (function() {
}
return chunk.lines[n];
}
function getBetween(doc, start, end) {
var out = [], n = start.line;
doc.iter(start.line, end.line + 1, function(line) {
var text = line.text;
if (n == end.line) text = text.slice(0, end.ch);
if (n == start.line) text = text.slice(start.ch);
out.push(text);
++n;
});
return out;
}
function getLines(doc, from, to) {
var out = [];
doc.iter(from, to, function(line) { out.push(line.text); });
return out;
}
function updateLineHeight(line, height) {
var diff = height - line.height;
for (var n = line; n; n = n.parent) n.height += diff;
}
function lineNo(line) {
if (line.parent == null) return null;
var cur = line.parent, no = indexOf(cur.lines, line);
for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) {
for (var i = 0, e = chunk.children.length; ; ++i) {
for (var i = 0;; ++i) {
if (chunk.children[i] == cur) break;
no += chunk.children[i].chunkSize();
}
}
return no;
return no + cur.first;
}
function lineAtHeight(chunk, h) {
var n = 0;
var n = chunk.first;
outer: do {
for (var i = 0, e = chunk.children.length; i < e; ++i) {
var child = chunk.children[i], ch = child.height;
@@ -2737,55 +5254,194 @@ var CodeMirror = (function() {
}
return n + i;
}
function heightAtLine(chunk, n) {
var h = 0;
outer: do {
for (var i = 0, e = chunk.children.length; i < e; ++i) {
var child = chunk.children[i], sz = child.chunkSize();
if (n < sz) { chunk = child; continue outer; }
n -= sz;
h += child.height;
function heightAtLine(cm, lineObj) {
lineObj = visualLine(cm.doc, lineObj);
var h = 0, chunk = lineObj.parent;
for (var i = 0; i < chunk.lines.length; ++i) {
var line = chunk.lines[i];
if (line == lineObj) break;
else h += line.height;
}
for (var p = chunk.parent; p; chunk = p, p = chunk.parent) {
for (var i = 0; i < p.children.length; ++i) {
var cur = p.children[i];
if (cur == chunk) break;
else h += cur.height;
}
return h;
} while (!chunk.lines);
for (var i = 0; i < n; ++i) h += chunk.lines[i].height;
}
return h;
}
// The history object 'chunks' changes that are made close together
// and at almost the same time into bigger undoable units.
function History() {
this.time = 0;
this.done = []; this.undone = [];
function getOrder(line) {
var order = line.order;
if (order == null) order = line.order = bidiOrdering(line.text);
return order;
}
History.prototype = {
addChange: function(start, added, old) {
this.undone.length = 0;
var time = +new Date, cur = this.done[this.done.length - 1], last = cur && cur[cur.length - 1];
var dtime = time - this.time;
if (dtime > 400 || !last) {
this.done.push([{start: start, added: added, old: old}]);
} else if (last.start > start + old.length || last.start + last.added < start - last.added + last.old.length) {
cur.push({start: start, added: added, old: old});
// HISTORY
function makeHistory(startGen) {
return {
// Arrays of history events. Doing something adds an event to
// done and clears undo. Undoing moves events from done to
// undone, redoing moves them in the other direction.
done: [], undone: [], undoDepth: Infinity,
// Used to track when changes can be merged into a single undo
// event
lastTime: 0, lastOp: null, lastOrigin: null,
// Used by the isClean() method
generation: startGen || 1, maxGeneration: startGen || 1
};
}
function attachLocalSpans(doc, change, from, to) {
var existing = change["spans_" + doc.id], n = 0;
doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function(line) {
if (line.markedSpans)
(existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans;
++n;
});
}
function historyChangeFromChange(doc, change) {
var from = { line: change.from.line, ch: change.from.ch };
var histChange = {from: from, to: changeEnd(change), text: getBetween(doc, change.from, change.to)};
attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);
linkedDocs(doc, function(doc) {attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);}, true);
return histChange;
}
function addToHistory(doc, change, selAfter, opId) {
var hist = doc.history;
hist.undone.length = 0;
var time = +new Date, cur = lst(hist.done);
if (cur &&
(hist.lastOp == opId ||
hist.lastOrigin == change.origin && change.origin &&
((change.origin.charAt(0) == "+" && doc.cm && hist.lastTime > time - doc.cm.options.historyEventDelay) ||
change.origin.charAt(0) == "*"))) {
// Merge this change into the last event
var last = lst(cur.changes);
if (posEq(change.from, change.to) && posEq(change.from, last.to)) {
// Optimized case for simple insertion -- don't want to add
// new changesets for every character typed
last.to = changeEnd(change);
} else {
var oldoff = 0;
if (start < last.start) {
for (var i = last.start - start - 1; i >= 0; --i)
last.old.unshift(old[i]);
oldoff = Math.min(0, added - old.length);
last.added += last.start - start + oldoff;
last.start = start;
} else if (last.start < start) {
oldoff = start - last.start;
added += oldoff;
}
for (var i = last.added - oldoff, e = old.length; i < e; ++i)
last.old.push(old[i]);
if (last.added < added) last.added = added;
// Add new sub-event
cur.changes.push(historyChangeFromChange(doc, change));
}
this.time = time;
cur.anchorAfter = selAfter.anchor; cur.headAfter = selAfter.head;
} else {
// Can not be merged, start a new event.
cur = {changes: [historyChangeFromChange(doc, change)],
generation: hist.generation,
anchorBefore: doc.sel.anchor, headBefore: doc.sel.head,
anchorAfter: selAfter.anchor, headAfter: selAfter.head};
hist.done.push(cur);
while (hist.done.length > hist.undoDepth)
hist.done.shift();
}
};
hist.generation = ++hist.maxGeneration;
hist.lastTime = time;
hist.lastOp = opId;
hist.lastOrigin = change.origin;
}
function removeClearedSpans(spans) {
if (!spans) return null;
for (var i = 0, out; i < spans.length; ++i) {
if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i); }
else if (out) out.push(spans[i]);
}
return !out ? spans : out.length ? out : null;
}
function getOldSpans(doc, change) {
var found = change["spans_" + doc.id];
if (!found) return null;
for (var i = 0, nw = []; i < change.text.length; ++i)
nw.push(removeClearedSpans(found[i]));
return nw;
}
// Used both to provide a JSON-safe object in .getHistory, and, when
// detaching a document, to split the history in two
function copyHistoryArray(events, newGroup) {
for (var i = 0, copy = []; i < events.length; ++i) {
var event = events[i], changes = event.changes, newChanges = [];
copy.push({changes: newChanges, anchorBefore: event.anchorBefore, headBefore: event.headBefore,
anchorAfter: event.anchorAfter, headAfter: event.headAfter});
for (var j = 0; j < changes.length; ++j) {
var change = changes[j], m;
newChanges.push({from: change.from, to: change.to, text: change.text});
if (newGroup) for (var prop in change) if (m = prop.match(/^spans_(\d+)$/)) {
if (indexOf(newGroup, Number(m[1])) > -1) {
lst(newChanges)[prop] = change[prop];
delete change[prop];
}
}
}
}
return copy;
}
// Rebasing/resetting history to deal with externally-sourced changes
function rebaseHistSel(pos, from, to, diff) {
if (to < pos.line) {
pos.line += diff;
} else if (from < pos.line) {
pos.line = from;
pos.ch = 0;
}
}
// Tries to rebase an array of history events given a change in the
// document. If the change touches the same lines as the event, the
// event, and everything 'behind' it, is discarded. If the change is
// before the event, the event's positions are updated. Uses a
// copy-on-write scheme for the positions, to avoid having to
// reallocate them all on every rebase, but also avoid problems with
// shared position objects being unsafely updated.
function rebaseHistArray(array, from, to, diff) {
for (var i = 0; i < array.length; ++i) {
var sub = array[i], ok = true;
for (var j = 0; j < sub.changes.length; ++j) {
var cur = sub.changes[j];
if (!sub.copied) { cur.from = copyPos(cur.from); cur.to = copyPos(cur.to); }
if (to < cur.from.line) {
cur.from.line += diff;
cur.to.line += diff;
} else if (from <= cur.to.line) {
ok = false;
break;
}
}
if (!sub.copied) {
sub.anchorBefore = copyPos(sub.anchorBefore); sub.headBefore = copyPos(sub.headBefore);
sub.anchorAfter = copyPos(sub.anchorAfter); sub.readAfter = copyPos(sub.headAfter);
sub.copied = true;
}
if (!ok) {
array.splice(0, i + 1);
i = 0;
} else {
rebaseHistSel(sub.anchorBefore); rebaseHistSel(sub.headBefore);
rebaseHistSel(sub.anchorAfter); rebaseHistSel(sub.headAfter);
}
}
}
function rebaseHist(hist, change) {
var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1;
rebaseHistArray(hist.done, from, to, diff);
rebaseHistArray(hist.undone, from, to, diff);
}
// EVENT OPERATORS
function stopMethod() {e_stop(this);}
// Ensure an event has a stop method.
@@ -2802,6 +5458,9 @@ var CodeMirror = (function() {
if (e.stopPropagation) e.stopPropagation();
else e.cancelBubble = true;
}
function e_defaultPrevented(e) {
return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false;
}
function e_stop(e) {e_preventDefault(e); e_stopPropagation(e);}
CodeMirror.e_stop = e_stop;
CodeMirror.e_preventDefault = e_preventDefault;
@@ -2809,164 +5468,134 @@ var CodeMirror = (function() {
function e_target(e) {return e.target || e.srcElement;}
function e_button(e) {
if (e.which) return e.which;
else if (e.button & 1) return 1;
else if (e.button & 2) return 3;
else if (e.button & 4) return 2;
}
// Allow 3rd-party code to override event properties by adding an override
// object to an event object.
function e_prop(e, prop) {
var overridden = e.override && e.override.hasOwnProperty(prop);
return overridden ? e.override[prop] : e[prop];
}
// Event handler registration. If disconnect is true, it'll return a
// function that unregisters the handler.
function connect(node, type, handler, disconnect) {
if (typeof node.addEventListener == "function") {
node.addEventListener(type, handler, false);
if (disconnect) return function() {node.removeEventListener(type, handler, false);};
var b = e.which;
if (b == null) {
if (e.button & 1) b = 1;
else if (e.button & 2) b = 3;
else if (e.button & 4) b = 2;
}
if (mac && e.ctrlKey && b == 1) b = 3;
return b;
}
// EVENT HANDLING
function on(emitter, type, f) {
if (emitter.addEventListener)
emitter.addEventListener(type, f, false);
else if (emitter.attachEvent)
emitter.attachEvent("on" + type, f);
else {
var wrapHandler = function(event) {handler(event || window.event);};
node.attachEvent("on" + type, wrapHandler);
if (disconnect) return function() {node.detachEvent("on" + type, wrapHandler);};
var map = emitter._handlers || (emitter._handlers = {});
var arr = map[type] || (map[type] = []);
arr.push(f);
}
}
CodeMirror.connect = connect;
function off(emitter, type, f) {
if (emitter.removeEventListener)
emitter.removeEventListener(type, f, false);
else if (emitter.detachEvent)
emitter.detachEvent("on" + type, f);
else {
var arr = emitter._handlers && emitter._handlers[type];
if (!arr) return;
for (var i = 0; i < arr.length; ++i)
if (arr[i] == f) { arr.splice(i, 1); break; }
}
}
function signal(emitter, type /*, values...*/) {
var arr = emitter._handlers && emitter._handlers[type];
if (!arr) return;
var args = Array.prototype.slice.call(arguments, 2);
for (var i = 0; i < arr.length; ++i) arr[i].apply(null, args);
}
var delayedCallbacks, delayedCallbackDepth = 0;
function signalLater(emitter, type /*, values...*/) {
var arr = emitter._handlers && emitter._handlers[type];
if (!arr) return;
var args = Array.prototype.slice.call(arguments, 2);
if (!delayedCallbacks) {
++delayedCallbackDepth;
delayedCallbacks = [];
setTimeout(fireDelayed, 0);
}
function bnd(f) {return function(){f.apply(null, args);};};
for (var i = 0; i < arr.length; ++i)
delayedCallbacks.push(bnd(arr[i]));
}
function signalDOMEvent(cm, e, override) {
signal(cm, override || e.type, cm, e);
return e_defaultPrevented(e) || e.codemirrorIgnore;
}
function fireDelayed() {
--delayedCallbackDepth;
var delayed = delayedCallbacks;
delayedCallbacks = null;
for (var i = 0; i < delayed.length; ++i) delayed[i]();
}
function hasHandler(emitter, type) {
var arr = emitter._handlers && emitter._handlers[type];
return arr && arr.length > 0;
}
CodeMirror.on = on; CodeMirror.off = off; CodeMirror.signal = signal;
function eventMixin(ctor) {
ctor.prototype.on = function(type, f) {on(this, type, f);};
ctor.prototype.off = function(type, f) {off(this, type, f);};
}
// MISC UTILITIES
// Number of pixels added to scroller and sizer to hide scrollbar
var scrollerCutOff = 30;
// Returned or thrown by various protocols to signal 'I'm not
// handling this'.
var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}};
function Delayed() {this.id = null;}
Delayed.prototype = {set: function(ms, f) {clearTimeout(this.id); this.id = setTimeout(f, ms);}};
var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}};
var gecko = /gecko\/\d{7}/i.test(navigator.userAgent);
var ie = /MSIE \d/.test(navigator.userAgent);
var ie_lt9 = /MSIE [1-8]\b/.test(navigator.userAgent);
var webkit = /WebKit\//.test(navigator.userAgent);
var chrome = /Chrome\//.test(navigator.userAgent);
var khtml = /KHTML\//.test(navigator.userAgent);
// Detect drag-and-drop
var dragAndDrop = function() {
// There is *some* kind of drag-and-drop support in IE6-8, but I
// couldn't get it to work yet.
if (ie_lt9) return false;
var div = document.createElement('div');
return "draggable" in div || "dragDrop" in div;
}();
var lineSep = "\n";
// Feature-detect whether newlines in textareas are converted to \r\n
(function () {
var te = document.createElement("textarea");
te.value = "foo\nbar";
if (te.value.indexOf("\r") > -1) lineSep = "\r\n";
}());
// Counts the column offset in a string, taking tabs into account.
// Used mostly to find indentation.
function countColumn(string, end, tabSize) {
function countColumn(string, end, tabSize, startIndex, startValue) {
if (end == null) {
end = string.search(/[^\s\u00a0]/);
if (end == -1) end = string.length;
}
for (var i = 0, n = 0; i < end; ++i) {
for (var i = startIndex || 0, n = startValue || 0; i < end; ++i) {
if (string.charAt(i) == "\t") n += tabSize - (n % tabSize);
else ++n;
}
return n;
}
CodeMirror.countColumn = countColumn;
function computedStyle(elt) {
if (elt.currentStyle) return elt.currentStyle;
return window.getComputedStyle(elt, null);
var spaceStrs = [""];
function spaceStr(n) {
while (spaceStrs.length <= n)
spaceStrs.push(lst(spaceStrs) + " ");
return spaceStrs[n];
}
// Find the position of an element by following the offsetParent chain.
// If screen==true, it returns screen (rather than page) coordinates.
function eltOffset(node, screen) {
var bod = node.ownerDocument.body;
var x = 0, y = 0, skipBody = false;
for (var n = node; n; n = n.offsetParent) {
var ol = n.offsetLeft, ot = n.offsetTop;
// Firefox reports weird inverted offsets when the body has a border.
if (n == bod) { x += Math.abs(ol); y += Math.abs(ot); }
else { x += ol, y += ot; }
if (screen && computedStyle(n).position == "fixed")
skipBody = true;
}
var e = screen && !skipBody ? null : bod;
for (var n = node.parentNode; n != e; n = n.parentNode)
if (n.scrollLeft != null) { x -= n.scrollLeft; y -= n.scrollTop;}
return {left: x, top: y};
}
// Use the faster and saner getBoundingClientRect method when possible.
if (document.documentElement.getBoundingClientRect != null) eltOffset = function(node, screen) {
// Take the parts of bounding client rect that we are interested in so we are able to edit if need be,
// since the returned value cannot be changed externally (they are kept in sync as the element moves within the page)
try { var box = node.getBoundingClientRect(); box = { top: box.top, left: box.left }; }
catch(e) { box = {top: 0, left: 0}; }
if (!screen) {
// Get the toplevel scroll, working around browser differences.
if (window.pageYOffset == null) {
var t = document.documentElement || document.body.parentNode;
if (t.scrollTop == null) t = document.body;
box.top += t.scrollTop; box.left += t.scrollLeft;
} else {
box.top += window.pageYOffset; box.left += window.pageXOffset;
}
}
return box;
};
function lst(arr) { return arr[arr.length-1]; }
// Get a node's text content.
function eltText(node) {
return node.textContent || node.innerText || node.nodeValue || "";
}
function selectInput(node) {
if (ios) { // Mobile Safari apparently has a bug where select() is broken.
node.selectionStart = 0;
node.selectionEnd = node.value.length;
} else node.select();
}
// Operations on {line, ch} objects.
function posEq(a, b) {return a.line == b.line && a.ch == b.ch;}
function posLess(a, b) {return a.line < b.line || (a.line == b.line && a.ch < b.ch);}
function copyPos(x) {return {line: x.line, ch: x.ch};}
var escapeElement = document.createElement("pre");
function htmlEscape(str) {
escapeElement.textContent = str;
return escapeElement.innerHTML;
}
// Recent (late 2011) Opera betas insert bogus newlines at the start
// of the textContent, so we strip those.
if (htmlEscape("a") == "\na")
htmlEscape = function(str) {
escapeElement.textContent = str;
return escapeElement.innerHTML.slice(1);
};
// Some IEs don't preserve tabs through innerHTML
else if (htmlEscape("\t") != "\t")
htmlEscape = function(str) {
escapeElement.innerHTML = "";
escapeElement.appendChild(document.createTextNode(str));
return escapeElement.innerHTML;
};
CodeMirror.htmlEscape = htmlEscape;
// Used to position the cursor after an undo/redo by finding the
// last edited character.
function editEnd(from, to) {
if (!to) return 0;
if (!from) return to.length;
for (var i = from.length, j = to.length; i >= 0 && j >= 0; --i, --j)
if (from.charAt(i) != to.charAt(j)) break;
return j + 1;
} else {
// Suppress mysterious IE10 errors
try { node.select(); }
catch(_e) {}
}
}
function indexOf(collection, elt) {
@@ -2975,21 +5604,160 @@ var CodeMirror = (function() {
if (collection[i] == elt) return i;
return -1;
}
function createObj(base, props) {
function Obj() {}
Obj.prototype = base;
var inst = new Obj();
if (props) copyObj(props, inst);
return inst;
}
function copyObj(obj, target) {
if (!target) target = {};
for (var prop in obj) if (obj.hasOwnProperty(prop)) target[prop] = obj[prop];
return target;
}
function emptyArray(size) {
for (var a = [], i = 0; i < size; ++i) a.push(undefined);
return a;
}
function bind(f) {
var args = Array.prototype.slice.call(arguments, 1);
return function(){return f.apply(null, args);};
}
var nonASCIISingleCaseWordChar = /[\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/;
function isWordChar(ch) {
return /\w/.test(ch) || ch.toUpperCase() != ch.toLowerCase();
return /\w/.test(ch) || ch > "\x80" &&
(ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch));
}
function isEmpty(obj) {
for (var n in obj) if (obj.hasOwnProperty(n) && obj[n]) return false;
return true;
}
var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/;
function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch); }
// DOM UTILITIES
function elt(tag, content, className, style) {
var e = document.createElement(tag);
if (className) e.className = className;
if (style) e.style.cssText = style;
if (typeof content == "string") setTextContent(e, content);
else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]);
return e;
}
function removeChildren(e) {
for (var count = e.childNodes.length; count > 0; --count)
e.removeChild(e.firstChild);
return e;
}
function removeChildrenAndAdd(parent, e) {
return removeChildren(parent).appendChild(e);
}
function setTextContent(e, str) {
if (ie_lt9) {
e.innerHTML = "";
e.appendChild(document.createTextNode(str));
} else e.textContent = str;
}
function getRect(node) {
return node.getBoundingClientRect();
}
CodeMirror.replaceGetRect = function(f) { getRect = f; };
// FEATURE DETECTION
// Detect drag-and-drop
var dragAndDrop = function() {
// There is *some* kind of drag-and-drop support in IE6-8, but I
// couldn't get it to work yet.
if (ie_lt9) return false;
var div = elt('div');
return "draggable" in div || "dragDrop" in div;
}();
// For a reason I have yet to figure out, some browsers disallow
// word wrapping between certain characters *only* if a new inline
// element is started between them. This makes it hard to reliably
// measure the position of things, since that requires inserting an
// extra span. This terribly fragile set of tests matches the
// character combinations that suffer from this phenomenon on the
// various browsers.
function spanAffectsWrapping() { return false; }
if (gecko) // Only for "$'"
spanAffectsWrapping = function(str, i) {
return str.charCodeAt(i - 1) == 36 && str.charCodeAt(i) == 39;
};
else if (safari && !/Version\/([6-9]|\d\d)\b/.test(navigator.userAgent))
spanAffectsWrapping = function(str, i) {
return /\-[^ \-?]|\?[^ !\'\"\),.\-\/:;\?\]\}]/.test(str.slice(i - 1, i + 1));
};
else if (webkit && /Chrome\/(?:29|[3-9]\d|\d\d\d)\./.test(navigator.userAgent))
spanAffectsWrapping = function(str, i) {
var code = str.charCodeAt(i - 1);
return code >= 8208 && code <= 8212;
};
else if (webkit)
spanAffectsWrapping = function(str, i) {
if (i > 1 && str.charCodeAt(i - 1) == 45) {
if (/\w/.test(str.charAt(i - 2)) && /[^\-?\.]/.test(str.charAt(i))) return true;
if (i > 2 && /[\d\.,]/.test(str.charAt(i - 2)) && /[\d\.,]/.test(str.charAt(i))) return false;
}
return /[~!#%&*)=+}\]\\|\"\.>,:;][({[<]|-[^\-?\.\u2010-\u201f\u2026]|\?[\w~`@#$%\^&*(_=+{[|><]|\u2026[\w~`@#$%\^&*(_=+{[><]/.test(str.slice(i - 1, i + 1));
};
var knownScrollbarWidth;
function scrollbarWidth(measure) {
if (knownScrollbarWidth != null) return knownScrollbarWidth;
var test = elt("div", null, null, "width: 50px; height: 50px; overflow-x: scroll");
removeChildrenAndAdd(measure, test);
if (test.offsetWidth)
knownScrollbarWidth = test.offsetHeight - test.clientHeight;
return knownScrollbarWidth || 0;
}
var zwspSupported;
function zeroWidthElement(measure) {
if (zwspSupported == null) {
var test = elt("span", "\u200b");
removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")]));
if (measure.firstChild.offsetHeight != 0)
zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !ie_lt8;
}
if (zwspSupported) return elt("span", "\u200b");
else return elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px");
}
// See if "".split is the broken IE version, if so, provide an
// alternative way to split lines.
var splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) {
var pos = 0, nl, result = [];
while ((nl = string.indexOf("\n", pos)) > -1) {
result.push(string.slice(pos, string.charAt(nl-1) == "\r" ? nl - 1 : nl));
pos = nl + 1;
var pos = 0, result = [], l = string.length;
while (pos <= l) {
var nl = string.indexOf("\n", pos);
if (nl == -1) nl = string.length;
var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl);
var rt = line.indexOf("\r");
if (rt != -1) {
result.push(line.slice(0, rt));
pos += rt + 1;
} else {
result.push(line);
pos = nl + 1;
}
}
result.push(string.slice(pos));
return result;
} : function(string){return string.split(/\r?\n/);};
} : function(string){return string.split(/\r\n?|\n/);};
CodeMirror.splitLines = splitLines;
var hasSelection = window.getSelection ? function(te) {
@@ -3002,27 +5770,307 @@ var CodeMirror = (function() {
return range.compareEndPoints("StartToEnd", range) != 0;
};
CodeMirror.defineMode("null", function() {
return {token: function(stream) {stream.skipToEnd();}};
});
CodeMirror.defineMIME("text/plain", "null");
var hasCopyEvent = (function() {
var e = elt("div");
if ("oncopy" in e) return true;
e.setAttribute("oncopy", "return;");
return typeof e.oncopy == 'function';
})();
// KEY NAMING
var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt",
19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End",
36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert",
46: "Delete", 59: ";", 91: "Mod", 92: "Mod", 93: "Mod", 127: "Delete", 186: ";", 187: "=", 188: ",",
189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", 221: "]", 222: "'", 63276: "PageUp",
63277: "PageDown", 63275: "End", 63273: "Home", 63234: "Left", 63232: "Up", 63235: "Right",
63233: "Down", 63302: "Insert", 63272: "Delete"};
46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", 107: "=", 109: "-", 127: "Delete",
173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\",
221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete",
63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert"};
CodeMirror.keyNames = keyNames;
(function() {
// Number keys
for (var i = 0; i < 10; i++) keyNames[i + 48] = String(i);
for (var i = 0; i < 10; i++) keyNames[i + 48] = keyNames[i + 96] = String(i);
// Alphabetic keys
for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i);
// Function keys
for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i;
})();
// BIDI HELPERS
function iterateBidiSections(order, from, to, f) {
if (!order) return f(from, to, "ltr");
var found = false;
for (var i = 0; i < order.length; ++i) {
var part = order[i];
if (part.from < to && part.to > from || from == to && part.to == from) {
f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr");
found = true;
}
}
if (!found) f(from, to, "ltr");
}
function bidiLeft(part) { return part.level % 2 ? part.to : part.from; }
function bidiRight(part) { return part.level % 2 ? part.from : part.to; }
function lineLeft(line) { var order = getOrder(line); return order ? bidiLeft(order[0]) : 0; }
function lineRight(line) {
var order = getOrder(line);
if (!order) return line.text.length;
return bidiRight(lst(order));
}
function lineStart(cm, lineN) {
var line = getLine(cm.doc, lineN);
var visual = visualLine(cm.doc, line);
if (visual != line) lineN = lineNo(visual);
var order = getOrder(visual);
var ch = !order ? 0 : order[0].level % 2 ? lineRight(visual) : lineLeft(visual);
return Pos(lineN, ch);
}
function lineEnd(cm, lineN) {
var merged, line;
while (merged = collapsedSpanAtEnd(line = getLine(cm.doc, lineN)))
lineN = merged.find().to.line;
var order = getOrder(line);
var ch = !order ? line.text.length : order[0].level % 2 ? lineLeft(line) : lineRight(line);
return Pos(lineN, ch);
}
function compareBidiLevel(order, a, b) {
var linedir = order[0].level;
if (a == linedir) return true;
if (b == linedir) return false;
return a < b;
}
var bidiOther;
function getBidiPartAt(order, pos) {
bidiOther = null;
for (var i = 0, found; i < order.length; ++i) {
var cur = order[i];
if (cur.from < pos && cur.to > pos) return i;
if ((cur.from == pos || cur.to == pos)) {
if (found == null) {
found = i;
} else if (compareBidiLevel(order, cur.level, order[found].level)) {
if (cur.from != cur.to) bidiOther = found;
return i;
} else {
if (cur.from != cur.to) bidiOther = i;
return found;
}
}
}
return found;
}
function moveInLine(line, pos, dir, byUnit) {
if (!byUnit) return pos + dir;
do pos += dir;
while (pos > 0 && isExtendingChar(line.text.charAt(pos)));
return pos;
}
// This is somewhat involved. It is needed in order to move
// 'visually' through bi-directional text -- i.e., pressing left
// should make the cursor go left, even when in RTL text. The
// tricky part is the 'jumps', where RTL and LTR text touch each
// other. This often requires the cursor offset to move more than
// one unit, in order to visually move one unit.
function moveVisually(line, start, dir, byUnit) {
var bidi = getOrder(line);
if (!bidi) return moveLogically(line, start, dir, byUnit);
var pos = getBidiPartAt(bidi, start), part = bidi[pos];
var target = moveInLine(line, start, part.level % 2 ? -dir : dir, byUnit);
for (;;) {
if (target > part.from && target < part.to) return target;
if (target == part.from || target == part.to) {
if (getBidiPartAt(bidi, target) == pos) return target;
part = bidi[pos += dir];
return (dir > 0) == part.level % 2 ? part.to : part.from;
} else {
part = bidi[pos += dir];
if (!part) return null;
if ((dir > 0) == part.level % 2)
target = moveInLine(line, part.to, -1, byUnit);
else
target = moveInLine(line, part.from, 1, byUnit);
}
}
}
function moveLogically(line, start, dir, byUnit) {
var target = start + dir;
if (byUnit) while (target > 0 && isExtendingChar(line.text.charAt(target))) target += dir;
return target < 0 || target > line.text.length ? null : target;
}
// Bidirectional ordering algorithm
// See http://unicode.org/reports/tr9/tr9-13.html for the algorithm
// that this (partially) implements.
// One-char codes used for character types:
// L (L): Left-to-Right
// R (R): Right-to-Left
// r (AL): Right-to-Left Arabic
// 1 (EN): European Number
// + (ES): European Number Separator
// % (ET): European Number Terminator
// n (AN): Arabic Number
// , (CS): Common Number Separator
// m (NSM): Non-Spacing Mark
// b (BN): Boundary Neutral
// s (B): Paragraph Separator
// t (S): Segment Separator
// w (WS): Whitespace
// N (ON): Other Neutrals
// Returns null if characters are ordered as they appear
// (left-to-right), or an array of sections ({from, to, level}
// objects) in the order in which they occur visually.
var bidiOrdering = (function() {
// Character types for codepoints 0 to 0xff
var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLL";
// Character types for codepoints 0x600 to 0x6ff
var arabicTypes = "rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmmrrrrrrrrrrrrrrrrrr";
function charType(code) {
if (code <= 0xff) return lowTypes.charAt(code);
else if (0x590 <= code && code <= 0x5f4) return "R";
else if (0x600 <= code && code <= 0x6ff) return arabicTypes.charAt(code - 0x600);
else if (0x700 <= code && code <= 0x8ac) return "r";
else return "L";
}
var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/;
var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/;
// Browsers seem to always treat the boundaries of block elements as being L.
var outerType = "L";
return function(str) {
if (!bidiRE.test(str)) return false;
var len = str.length, types = [];
for (var i = 0, type; i < len; ++i)
types.push(type = charType(str.charCodeAt(i)));
// W1. Examine each non-spacing mark (NSM) in the level run, and
// change the type of the NSM to the type of the previous
// character. If the NSM is at the start of the level run, it will
// get the type of sor.
for (var i = 0, prev = outerType; i < len; ++i) {
var type = types[i];
if (type == "m") types[i] = prev;
else prev = type;
}
// W2. Search backwards from each instance of a European number
// until the first strong type (R, L, AL, or sor) is found. If an
// AL is found, change the type of the European number to Arabic
// number.
// W3. Change all ALs to R.
for (var i = 0, cur = outerType; i < len; ++i) {
var type = types[i];
if (type == "1" && cur == "r") types[i] = "n";
else if (isStrong.test(type)) { cur = type; if (type == "r") types[i] = "R"; }
}
// W4. A single European separator between two European numbers
// changes to a European number. A single common separator between
// two numbers of the same type changes to that type.
for (var i = 1, prev = types[0]; i < len - 1; ++i) {
var type = types[i];
if (type == "+" && prev == "1" && types[i+1] == "1") types[i] = "1";
else if (type == "," && prev == types[i+1] &&
(prev == "1" || prev == "n")) types[i] = prev;
prev = type;
}
// W5. A sequence of European terminators adjacent to European
// numbers changes to all European numbers.
// W6. Otherwise, separators and terminators change to Other
// Neutral.
for (var i = 0; i < len; ++i) {
var type = types[i];
if (type == ",") types[i] = "N";
else if (type == "%") {
for (var end = i + 1; end < len && types[end] == "%"; ++end) {}
var replace = (i && types[i-1] == "!") || (end < len && types[end] == "1") ? "1" : "N";
for (var j = i; j < end; ++j) types[j] = replace;
i = end - 1;
}
}
// W7. Search backwards from each instance of a European number
// until the first strong type (R, L, or sor) is found. If an L is
// found, then change the type of the European number to L.
for (var i = 0, cur = outerType; i < len; ++i) {
var type = types[i];
if (cur == "L" && type == "1") types[i] = "L";
else if (isStrong.test(type)) cur = type;
}
// N1. A sequence of neutrals takes the direction of the
// surrounding strong text if the text on both sides has the same
// direction. European and Arabic numbers act as if they were R in
// terms of their influence on neutrals. Start-of-level-run (sor)
// and end-of-level-run (eor) are used at level run boundaries.
// N2. Any remaining neutrals take the embedding direction.
for (var i = 0; i < len; ++i) {
if (isNeutral.test(types[i])) {
for (var end = i + 1; end < len && isNeutral.test(types[end]); ++end) {}
var before = (i ? types[i-1] : outerType) == "L";
var after = (end < len ? types[end] : outerType) == "L";
var replace = before || after ? "L" : "R";
for (var j = i; j < end; ++j) types[j] = replace;
i = end - 1;
}
}
// Here we depart from the documented algorithm, in order to avoid
// building up an actual levels array. Since there are only three
// levels (0, 1, 2) in an implementation that doesn't take
// explicit embedding into account, we can build up the order on
// the fly, without following the level-based algorithm.
var order = [], m;
for (var i = 0; i < len;) {
if (countsAsLeft.test(types[i])) {
var start = i;
for (++i; i < len && countsAsLeft.test(types[i]); ++i) {}
order.push({from: start, to: i, level: 0});
} else {
var pos = i, at = order.length;
for (++i; i < len && types[i] != "L"; ++i) {}
for (var j = pos; j < i;) {
if (countsAsNum.test(types[j])) {
if (pos < j) order.splice(at, 0, {from: pos, to: j, level: 1});
var nstart = j;
for (++j; j < i && countsAsNum.test(types[j]); ++j) {}
order.splice(at, 0, {from: nstart, to: j, level: 2});
pos = j;
} else ++j;
}
if (pos < i) order.splice(at, 0, {from: pos, to: i, level: 1});
}
}
if (order[0].level == 1 && (m = str.match(/^\s+/))) {
order[0].from = m[0].length;
order.unshift({from: 0, to: m[0].length, level: 0});
}
if (lst(order).level == 1 && (m = str.match(/\s+$/))) {
lst(order).to -= m[0].length;
order.push({from: len - m[0].length, to: len, level: 0});
}
if (order[0].level != lst(order).level)
order.push({from: len, to: len, level: order[0].level});
return order;
};
})();
// THE END
CodeMirror.version = "3.21.0";
return CodeMirror;
})();

File diff suppressed because one or more lines are too long

View File

@@ -224,9 +224,6 @@ class CourseFixture(StudioApiFixture):
"""
self._create_course()
# Remove once STUD-1248 is resolved
self._update_loc_map()
self._install_course_updates()
self._install_course_handouts()
self._configure_course()
@@ -362,20 +359,6 @@ class CourseFixture(StudioApiFixture):
"Could not add update to course: {0}. Status was {1}".format(
update, response.status_code))
def _update_loc_map(self):
"""
Force update of the location map.
"""
# We perform a GET request to force Studio to update the course location map.
# This is a (minor) bug in the Studio RESTful API: STUD-1248
url = "{base}/course_info/{course}".format(base=STUDIO_BASE_URL, course=self._course_loc)
response = self.session.get(url, headers={'Accept': 'text/html'})
if not response.ok:
raise CourseFixtureError(
"Could not load Studio dashboard to trigger location map update. Status was {0}".format(
response.status_code))
def _create_xblock_children(self, parent_loc, xblock_descriptions):
"""
Recursively create XBlock children.

View File

@@ -72,9 +72,28 @@ class DiscussionSingleThreadPage(CoursePage):
self.css_map(selector, lambda el: el.visible)[0]
)
def is_response_editor_visible(self, response_id):
"""Returns true if the response editor is present, false otherwise"""
return self._is_element_visible(".response_{} .edit-post-body".format(response_id))
def start_response_edit(self, response_id):
"""Click the edit button for the response, loading the editing view"""
self.css_click(".response_{} .discussion-response .action-edit".format(response_id))
fulfill(EmptyPromise(
lambda: self.is_response_editor_visible(response_id),
"Response edit started"
))
def is_add_comment_visible(self, response_id):
"""Returns true if the "add comment" form is visible for a response"""
return self._is_element_visible(".response_{} .new-comment".format(response_id))
def is_comment_visible(self, comment_id):
"""Returns true if the comment is viewable onscreen"""
return self._is_element_visible("#comment_{}".format(comment_id))
return self._is_element_visible("#comment_{} .response-body".format(comment_id))
def get_comment_body(self, comment_id):
return self._get_element_text("#comment_{} .response-body".format(comment_id))
def is_comment_deletable(self, comment_id):
"""Returns true if the delete comment button is present, false otherwise"""
@@ -87,3 +106,56 @@ class DiscussionSingleThreadPage(CoursePage):
lambda: not self.is_comment_visible(comment_id),
"Deleted comment was removed"
))
def is_comment_editable(self, comment_id):
"""Returns true if the edit comment button is present, false otherwise"""
return self._is_element_visible("#comment_{} .action-edit".format(comment_id))
def is_comment_editor_visible(self, comment_id):
"""Returns true if the comment editor is present, false otherwise"""
return self._is_element_visible("#comment_{} .edit-comment-body".format(comment_id))
def _get_comment_editor_value(self, comment_id):
return self.css_value("#comment_{} .wmd-input".format(comment_id))[0]
def start_comment_edit(self, comment_id):
"""Click the edit button for the comment, loading the editing view"""
old_body = self.get_comment_body(comment_id)
self.css_click("#comment_{} .action-edit".format(comment_id))
fulfill(EmptyPromise(
lambda: (
self.is_comment_editor_visible(comment_id) and
not self.is_comment_visible(comment_id) and
self._get_comment_editor_value(comment_id) == old_body
),
"Comment edit started"
))
def set_comment_editor_value(self, comment_id, new_body):
"""Replace the contents of the comment editor"""
self.css_fill("#comment_{} .wmd-input".format(comment_id), new_body)
def submit_comment_edit(self, comment_id):
"""Click the submit button on the comment editor"""
new_body = self._get_comment_editor_value(comment_id)
self.css_click("#comment_{} .post-update".format(comment_id))
fulfill(EmptyPromise(
lambda: (
not self.is_comment_editor_visible(comment_id) and
self.is_comment_visible(comment_id) and
self.get_comment_body(comment_id) == new_body
),
"Comment edit succeeded"
))
def cancel_comment_edit(self, comment_id, original_body):
"""Click the cancel button on the comment editor"""
self.css_click("#comment_{} .post-cancel".format(comment_id))
fulfill(EmptyPromise(
lambda: (
not self.is_comment_editor_visible(comment_id) and
self.is_comment_visible(comment_id) and
self.get_comment_body(comment_id) == original_body
),
"Comment edit was canceled"
))

View File

@@ -135,3 +135,91 @@ class DiscussionCommentDeletionTest(UniqueCourseTest):
self.assertTrue(page.is_comment_deletable("comment_other_author"))
page.delete_comment("comment_self_author")
page.delete_comment("comment_other_author")
class DiscussionCommentEditTest(UniqueCourseTest):
"""
Tests for editing comments displayed beneath responses in the single thread view.
"""
def setUp(self):
super(DiscussionCommentEditTest, self).setUp()
# Create a course to register for
CourseFixture(**self.course_info).install()
def setup_user(self, roles=[]):
roles_str = ','.join(roles)
self.user_id = AutoAuthPage(self.browser, course_id=self.course_id, roles=roles_str).visit().get_user_id()
def setup_view(self):
view = SingleThreadViewFixture(Thread(id="comment_edit_test_thread"))
view.addResponse(
Response(id="response1"),
[Comment(id="comment_other_author", user_id="other"), Comment(id="comment_self_author", user_id=self.user_id)])
view.push()
def edit_comment(self, page, comment_id):
page.start_comment_edit(comment_id)
page.set_comment_editor_value(comment_id, "edited body")
page.submit_comment_edit(comment_id)
def test_edit_comment_as_student(self):
self.setup_user()
self.setup_view()
page = DiscussionSingleThreadPage(self.browser, self.course_id, "comment_edit_test_thread")
page.visit()
self.assertTrue(page.is_comment_editable("comment_self_author"))
self.assertTrue(page.is_comment_visible("comment_other_author"))
self.assertFalse(page.is_comment_editable("comment_other_author"))
self.edit_comment(page, "comment_self_author")
def test_edit_comment_as_moderator(self):
self.setup_user(roles=["Moderator"])
self.setup_view()
page = DiscussionSingleThreadPage(self.browser, self.course_id, "comment_edit_test_thread")
page.visit()
self.assertTrue(page.is_comment_editable("comment_self_author"))
self.assertTrue(page.is_comment_editable("comment_other_author"))
self.edit_comment(page, "comment_self_author")
self.edit_comment(page, "comment_other_author")
def test_cancel_comment_edit(self):
self.setup_user()
self.setup_view()
page = DiscussionSingleThreadPage(self.browser, self.course_id, "comment_edit_test_thread")
page.visit()
self.assertTrue(page.is_comment_editable("comment_self_author"))
original_body = page.get_comment_body("comment_self_author")
page.start_comment_edit("comment_self_author")
page.set_comment_editor_value("comment_self_author", "edited body")
page.cancel_comment_edit("comment_self_author", original_body)
def test_editor_visibility(self):
"""Only one editor should be visible at a time within a single response"""
self.setup_user(roles=["Moderator"])
self.setup_view()
page = DiscussionSingleThreadPage(self.browser, self.course_id, "comment_edit_test_thread")
page.visit()
self.assertTrue(page.is_comment_editable("comment_self_author"))
self.assertTrue(page.is_comment_editable("comment_other_author"))
self.assertTrue(page.is_add_comment_visible("response1"))
original_body = page.get_comment_body("comment_self_author")
page.start_comment_edit("comment_self_author")
self.assertFalse(page.is_add_comment_visible("response1"))
self.assertTrue(page.is_comment_editor_visible("comment_self_author"))
page.set_comment_editor_value("comment_self_author", "edited body")
page.start_comment_edit("comment_other_author")
self.assertFalse(page.is_comment_editor_visible("comment_self_author"))
self.assertTrue(page.is_comment_editor_visible("comment_other_author"))
self.assertEqual(page.get_comment_body("comment_self_author"), original_body)
page.start_response_edit("response1")
self.assertFalse(page.is_comment_editor_visible("comment_other_author"))
self.assertTrue(page.is_response_editor_visible("response1"))
original_body = page.get_comment_body("comment_self_author")
page.start_comment_edit("comment_self_author")
self.assertFalse(page.is_response_editor_visible("response1"))
self.assertTrue(page.is_comment_editor_visible("comment_self_author"))
page.cancel_comment_edit("comment_self_author", original_body)
self.assertFalse(page.is_comment_editor_visible("comment_self_author"))
self.assertTrue(page.is_add_comment_visible("response1"))

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Some files were not shown because too many files have changed in this diff Show More