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
3
AUTHORS
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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'])
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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 ####################################
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,10 @@ define [
|
||||
super()
|
||||
@savingNotification = new NotificationView.Mini
|
||||
title: gettext('Saving…')
|
||||
@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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}})
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -56,6 +56,7 @@ class TestMongoModuleStore(object):
|
||||
def teardownClass(cls):
|
||||
if cls.connection:
|
||||
cls.connection.drop_database(DB)
|
||||
cls.connection.close()
|
||||
|
||||
@staticmethod
|
||||
def initdb():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -20,6 +20,7 @@ if Backbone?
|
||||
@template(templateData)
|
||||
|
||||
render: ->
|
||||
@$el.addClass("response_" + @model.get("id"))
|
||||
@$el.html(@renderTemplate())
|
||||
@delegateEvents()
|
||||
|
||||
|
||||
72
common/static/js/vendor/CodeMirror/accessible.diff
vendored
Normal 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) {
|
||||
93
common/static/js/vendor/CodeMirror/addons/closetag.js
vendored
Normal 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;
|
||||
}
|
||||
})();
|
||||
149
common/static/js/vendor/CodeMirror/addons/comment.js
vendored
Normal 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;
|
||||
});
|
||||
})();
|
||||
32
common/static/js/vendor/CodeMirror/addons/diff.js
vendored
Normal 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");
|
||||
114
common/static/js/vendor/CodeMirror/addons/formatting.js
vendored
Normal 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));
|
||||
});
|
||||
});
|
||||
})();
|
||||
71
common/static/js/vendor/CodeMirror/addons/htmlembedded.js
vendored
Normal 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"});
|
||||
102
common/static/js/vendor/CodeMirror/addons/htmlmixed.js
vendored
Normal 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");
|
||||
630
common/static/js/vendor/CodeMirror/addons/javascript.js
vendored
Normal 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 });
|
||||
91
common/static/js/vendor/CodeMirror/addons/match-highlighter.js
vendored
Normal 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();
|
||||
}};
|
||||
}
|
||||
})();
|
||||
374
common/static/js/vendor/CodeMirror/addons/python.js
vendored
Normal 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")
|
||||
});
|
||||
})();
|
||||
146
common/static/js/vendor/CodeMirror/addons/search.js
vendored
Normal 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);};
|
||||
})();
|
||||
167
common/static/js/vendor/CodeMirror/addons/searchcursor.js
vendored
Normal 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);
|
||||
});
|
||||
})();
|
||||
332
common/static/js/vendor/CodeMirror/addons/xml.js
vendored
Normal 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});
|
||||
97
common/static/js/vendor/CodeMirror/addons/yaml.js
vendored
Normal 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");
|
||||
263
common/static/js/vendor/CodeMirror/codemirror-3.21.0.css
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
5828
common/static/js/vendor/CodeMirror/codemirror-accessible.js
vendored
Normal 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;
|
||||
})();
|
||||
8162
common/static/js/vendor/CodeMirror/codemirror.js
vendored
@@ -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"> </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"> </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/> ");
|
||||
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;
|
||||
})();
|
||||
|
||||
455
common/static/js/vendor/codemirror-compressed.js
vendored
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
))
|
||||
|
||||
@@ -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"))
|
||||
|
||||
BIN
docs/en_us/course_authors/source/Images/AIScoredResponse.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
docs/en_us/course_authors/source/Images/AI_ScoredResponse.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
docs/en_us/course_authors/source/Images/AddNewComponent.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
docs/en_us/course_authors/source/Images/AdditionalFeedback.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
docs/en_us/course_authors/source/Images/AdvancedComponent.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
docs/en_us/course_authors/source/Images/AdvancedModulesEmpty.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
docs/en_us/course_authors/source/Images/AnatomyOfExercise1.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
docs/en_us/course_authors/source/Images/AnatomyOfExercise2.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
docs/en_us/course_authors/source/Images/AnatomyOfExercise3.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
BIN
docs/en_us/course_authors/source/Images/AnnotationExample.png
Normal file
|
After Width: | Height: | Size: 201 KiB |
BIN
docs/en_us/course_authors/source/Images/CITL_AssmtTypes.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
docs/en_us/course_authors/source/Images/CITL_SA_Rubric.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
docs/en_us/course_authors/source/Images/CITLsample.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
docs/en_us/course_authors/source/Images/CheckboxExample.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 214 KiB |