author | Carsten "Tomcat" Book <cbook@mozilla.com> |
Tue, 08 Apr 2014 15:39:46 +0200 | |
changeset 177568 | 568c9573340af6ebb129a1563b5a5662147c1ea8 |
parent 177567 | 2b61df0983d60ea336389200a3b27eddd48bd8ed (current diff) |
parent 177500 | 25aeb2bc79f24cb8a663ebf4a7787fc3b4e19237 (diff) |
child 177569 | a6ba4478e9d20c7136dbb0737930ed8ddea91488 |
push id | 26556 |
push user | ryanvm@gmail.com |
push date | Tue, 08 Apr 2014 22:16:57 +0000 |
treeherder | mozilla-central@5811efc11011 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
milestone | 31.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -14,17 +14,17 @@ <!--original fetch url was git://github.com/apitrace/--> <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/> <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/> <!-- Gonk specific things and forks --> <project name="platform_build" path="build" remote="b2g" revision="1ad48c4be51b279f7f63c1a13025b52fe087d231"> <copyfile dest="Makefile" src="core/root.mk"/> </project> <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/> - <project name="gaia.git" path="gaia" remote="mozillaorg" revision="1958454595b1fa0e061f0652ae965629993f5708"/> + <project name="gaia.git" path="gaia" remote="mozillaorg" revision="4645b8da5a9cf25795313b20a56bad67225786c5"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="d11f524d00cacf5ba0dfbf25e4aa2158b1c3a036"/> <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="9b393a454feef2967ff164ee33fcad3905baedef"/> <project name="moztt" path="external/moztt" remote="b2g" revision="70b698c2e8d1764a1e27527a102df6452e405b9a"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="89c5816399e71bda92a8959b5b771c04d6672ea3"/> <!-- Stock Android things --> <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -12,17 +12,17 @@ <!--original fetch url was https://git.mozilla.org/releases--> <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/> <!-- B2G specific things. --> <project name="platform_build" path="build" remote="b2g" revision="f6a198295f65ac38f8511803654a3583a1c666af"> <copyfile dest="Makefile" src="core/root.mk"/> </project> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> - <project name="gaia" path="gaia" remote="mozillaorg" revision="1958454595b1fa0e061f0652ae965629993f5708"/> + <project name="gaia" path="gaia" remote="mozillaorg" revision="4645b8da5a9cf25795313b20a56bad67225786c5"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/> <project name="moztt" path="external/moztt" remote="b2g" revision="70b698c2e8d1764a1e27527a102df6452e405b9a"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="89c5816399e71bda92a8959b5b771c04d6672ea3"/> <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/> <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/> <!-- Stock Android things --> <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/> <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -7,20 +7,20 @@ <remote fetch="https://git.mozilla.org/b2g" name="mozilla"/> <!--original fetch url was git://github.com/apitrace/--> <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/> <!--original fetch url was git://codeaurora.org/--> <remote fetch="https://git.mozilla.org/external/caf" name="caf"/> <!--original fetch url was https://git.mozilla.org/releases--> <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/> <!-- B2G specific things. --> - <project name="platform_build" path="build" remote="b2g" revision="a9e08b91e9cd1f0930f16cfc49ec72f63575d5fe"> + <project name="platform_build" path="build" remote="b2g" revision="52c909ccead537f8f9dbf634f3e6639078a8b0bd"> <copyfile dest="Makefile" src="core/root.mk"/> </project> - <project name="gaia" path="gaia" remote="mozillaorg" revision="1958454595b1fa0e061f0652ae965629993f5708"/> + <project name="gaia" path="gaia" remote="mozillaorg" revision="4645b8da5a9cf25795313b20a56bad67225786c5"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/> <project name="moztt" path="external/moztt" remote="b2g" revision="70b698c2e8d1764a1e27527a102df6452e405b9a"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/> <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="89c5816399e71bda92a8959b5b771c04d6672ea3"/> <!-- Stock Android things -->
--- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -14,17 +14,17 @@ <!--original fetch url was git://github.com/apitrace/--> <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/> <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/> <!-- Gonk specific things and forks --> <project name="platform_build" path="build" remote="b2g" revision="1ad48c4be51b279f7f63c1a13025b52fe087d231"> <copyfile dest="Makefile" src="core/root.mk"/> </project> <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/> - <project name="gaia.git" path="gaia" remote="mozillaorg" revision="1958454595b1fa0e061f0652ae965629993f5708"/> + <project name="gaia.git" path="gaia" remote="mozillaorg" revision="4645b8da5a9cf25795313b20a56bad67225786c5"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="d11f524d00cacf5ba0dfbf25e4aa2158b1c3a036"/> <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="9b393a454feef2967ff164ee33fcad3905baedef"/> <project name="moztt" path="external/moztt" remote="b2g" revision="70b698c2e8d1764a1e27527a102df6452e405b9a"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="89c5816399e71bda92a8959b5b771c04d6672ea3"/> <!-- Stock Android things --> <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/flame/sources.xml +++ b/b2g/config/flame/sources.xml @@ -13,17 +13,17 @@ <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/> <!-- B2G specific things. --> <project name="platform_build" path="build" remote="b2g" revision="f6a198295f65ac38f8511803654a3583a1c666af"> <copyfile dest="Makefile" src="core/root.mk"/> </project> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="librecovery" path="librecovery" remote="b2g" revision="1f6a1fe07f81c5bc5e1d079c9b60f7f78ca2bf4f"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> - <project name="gaia" path="gaia" remote="mozillaorg" revision="1958454595b1fa0e061f0652ae965629993f5708"/> + <project name="gaia" path="gaia" remote="mozillaorg" revision="4645b8da5a9cf25795313b20a56bad67225786c5"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/> <project name="moztt" path="external/moztt" remote="b2g" revision="70b698c2e8d1764a1e27527a102df6452e405b9a"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="89c5816399e71bda92a8959b5b771c04d6672ea3"/> <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/> <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/> <!-- Stock Android things --> <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="e95b4ce22c825da44d14299e1190ea39a5260bde"/> <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="471afab478649078ad7c75ec6b252481a59e19b8"/>
--- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -1,9 +1,9 @@ { "git": { "git_revision": "", "remote": "", "branch": "" }, - "revision": "65cb0c455f058c49aead7de9a245d90aa890276c", + "revision": "82aa7db46a22bc99998468aa433eafb33d739ed6", "repo_path": "/integration/gaia-central" }
--- a/b2g/config/hamachi/sources.xml +++ b/b2g/config/hamachi/sources.xml @@ -12,17 +12,17 @@ <!--original fetch url was git://github.com/apitrace/--> <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/> <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/> <!-- Gonk specific things and forks --> <project name="platform_build" path="build" remote="b2g" revision="1ad48c4be51b279f7f63c1a13025b52fe087d231"> <copyfile dest="Makefile" src="core/root.mk"/> </project> <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/> - <project name="gaia.git" path="gaia" remote="mozillaorg" revision="1958454595b1fa0e061f0652ae965629993f5708"/> + <project name="gaia.git" path="gaia" remote="mozillaorg" revision="4645b8da5a9cf25795313b20a56bad67225786c5"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="librecovery" path="librecovery" remote="b2g" revision="1f6a1fe07f81c5bc5e1d079c9b60f7f78ca2bf4f"/> <project name="moztt" path="external/moztt" remote="b2g" revision="70b698c2e8d1764a1e27527a102df6452e405b9a"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="89c5816399e71bda92a8959b5b771c04d6672ea3"/> <!-- Stock Android things --> <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/> <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/helix/sources.xml +++ b/b2g/config/helix/sources.xml @@ -10,17 +10,17 @@ <!--original fetch url was https://git.mozilla.org/releases--> <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/> <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/> <!-- Gonk specific things and forks --> <project name="platform_build" path="build" remote="b2g" revision="1ad48c4be51b279f7f63c1a13025b52fe087d231"> <copyfile dest="Makefile" src="core/root.mk"/> </project> <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/> - <project name="gaia.git" path="gaia" remote="mozillaorg" revision="1958454595b1fa0e061f0652ae965629993f5708"/> + <project name="gaia.git" path="gaia" remote="mozillaorg" revision="4645b8da5a9cf25795313b20a56bad67225786c5"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="librecovery" path="librecovery" remote="b2g" revision="1f6a1fe07f81c5bc5e1d079c9b60f7f78ca2bf4f"/> <project name="moztt" path="external/moztt" remote="b2g" revision="70b698c2e8d1764a1e27527a102df6452e405b9a"/> <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/> <!-- Stock Android things --> <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/> <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/inari/sources.xml +++ b/b2g/config/inari/sources.xml @@ -14,17 +14,17 @@ <!--original fetch url was git://github.com/apitrace/--> <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/> <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/> <!-- Gonk specific things and forks --> <project name="platform_build" path="build" remote="b2g" revision="1ad48c4be51b279f7f63c1a13025b52fe087d231"> <copyfile dest="Makefile" src="core/root.mk"/> </project> <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/> - <project name="gaia.git" path="gaia" remote="mozillaorg" revision="1958454595b1fa0e061f0652ae965629993f5708"/> + <project name="gaia.git" path="gaia" remote="mozillaorg" revision="4645b8da5a9cf25795313b20a56bad67225786c5"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="librecovery" path="librecovery" remote="b2g" revision="1f6a1fe07f81c5bc5e1d079c9b60f7f78ca2bf4f"/> <project name="moztt" path="external/moztt" remote="b2g" revision="70b698c2e8d1764a1e27527a102df6452e405b9a"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="89c5816399e71bda92a8959b5b771c04d6672ea3"/> <!-- Stock Android things --> <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/> <project name="platform/bionic" path="bionic" revision="cd5dfce80bc3f0139a56b58aca633202ccaee7f8"/>
--- a/b2g/config/leo/sources.xml +++ b/b2g/config/leo/sources.xml @@ -12,17 +12,17 @@ <!--original fetch url was git://github.com/apitrace/--> <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/> <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/> <!-- Gonk specific things and forks --> <project name="platform_build" path="build" remote="b2g" revision="1ad48c4be51b279f7f63c1a13025b52fe087d231"> <copyfile dest="Makefile" src="core/root.mk"/> </project> <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/> - <project name="gaia.git" path="gaia" remote="mozillaorg" revision="1958454595b1fa0e061f0652ae965629993f5708"/> + <project name="gaia.git" path="gaia" remote="mozillaorg" revision="4645b8da5a9cf25795313b20a56bad67225786c5"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="librecovery" path="librecovery" remote="b2g" revision="1f6a1fe07f81c5bc5e1d079c9b60f7f78ca2bf4f"/> <project name="moztt" path="external/moztt" remote="b2g" revision="70b698c2e8d1764a1e27527a102df6452e405b9a"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="89c5816399e71bda92a8959b5b771c04d6672ea3"/> <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/> <!-- Stock Android things --> <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
--- a/b2g/config/mako/sources.xml +++ b/b2g/config/mako/sources.xml @@ -12,17 +12,17 @@ <!--original fetch url was https://git.mozilla.org/releases--> <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/> <!-- B2G specific things. --> <project name="platform_build" path="build" remote="b2g" revision="f6a198295f65ac38f8511803654a3583a1c666af"> <copyfile dest="Makefile" src="core/root.mk"/> </project> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/> - <project name="gaia" path="gaia" remote="mozillaorg" revision="1958454595b1fa0e061f0652ae965629993f5708"/> + <project name="gaia" path="gaia" remote="mozillaorg" revision="4645b8da5a9cf25795313b20a56bad67225786c5"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/> <project name="moztt" path="external/moztt" remote="b2g" revision="70b698c2e8d1764a1e27527a102df6452e405b9a"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="89c5816399e71bda92a8959b5b771c04d6672ea3"/> <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/> <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/> <!-- Stock Android things --> <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/> <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/wasabi/sources.xml +++ b/b2g/config/wasabi/sources.xml @@ -12,17 +12,17 @@ <!--original fetch url was git://github.com/apitrace/--> <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/> <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/> <!-- Gonk specific things and forks --> <project name="platform_build" path="build" remote="b2g" revision="1ad48c4be51b279f7f63c1a13025b52fe087d231"> <copyfile dest="Makefile" src="core/root.mk"/> </project> <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/> - <project name="gaia.git" path="gaia" remote="mozillaorg" revision="1958454595b1fa0e061f0652ae965629993f5708"/> + <project name="gaia.git" path="gaia" remote="mozillaorg" revision="4645b8da5a9cf25795313b20a56bad67225786c5"/> <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="266bca6e60dad43e395f38b66edabe8bdc882334"/> <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/> <project name="librecovery" path="librecovery" remote="b2g" revision="1f6a1fe07f81c5bc5e1d079c9b60f7f78ca2bf4f"/> <project name="moztt" path="external/moztt" remote="b2g" revision="70b698c2e8d1764a1e27527a102df6452e405b9a"/> <project name="apitrace" path="external/apitrace" remote="apitrace" revision="89c5816399e71bda92a8959b5b771c04d6672ea3"/> <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/> <!-- Stock Android things --> <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
--- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -698,16 +698,18 @@ <image id="plugin-install-notification-icon" class="notification-anchor-icon" role="button"/> <image id="mixed-content-blocked-notification-icon" class="notification-anchor-icon" role="button"/> <image id="webRTC-shareDevices-notification-icon" class="notification-anchor-icon" role="button"/> <image id="webRTC-sharingDevices-notification-icon" class="notification-anchor-icon" role="button"/> <image id="webRTC-shareMicrophone-notification-icon" class="notification-anchor-icon" role="button"/> <image id="webRTC-sharingMicrophone-notification-icon" class="notification-anchor-icon" role="button"/> <image id="pointerLock-notification-icon" class="notification-anchor-icon" role="button"/> <image id="servicesInstall-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="translate-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="translated-notification-icon" class="notification-anchor-icon" role="button"/> </box> <!-- Use onclick instead of normal popup= syntax since the popup code fires onmousedown, and hence eats our favicon drag events. We only add the identity-box button to the tab order when the location bar has focus, otherwise pressing F6 focuses it instead of the location bar --> <box id="identity-box" role="button" align="center" onclick="gIdentityHandler.handleIdentityButtonEvent(event);"
--- a/browser/base/content/test/general/browser_popupNotification.js +++ b/browser/base/content/test/general/browser_popupNotification.js @@ -878,17 +878,17 @@ var tests = [ }, onHidden: function () {} }, { // Test #29 - Popup Notifications should catch exceptions from callbacks run: function () { let callbackCount = 0; this.testNotif1 = new basicNotification(); this.testNotif1.message += " 1"; - showNotification(this.testNotif1); + this.notification1 = showNotification(this.testNotif1); this.testNotif1.options.eventCallback = function (eventName) { info("notifyObj1.options.eventCallback: " + eventName); if (eventName == "dismissed") { throw new Error("Oops 1!"); if (++callbackCount == 2) { executeSoon(goNext); } } @@ -901,23 +901,26 @@ var tests = [ info("notifyObj2.options.eventCallback: " + eventName); if (eventName == "dismissed") { throw new Error("Oops 2!"); if (++callbackCount == 2) { executeSoon(goNext); } } }; - showNotification(this.testNotif2); + this.notification2 = showNotification(this.testNotif2); }, onShown: function (popup) { is(popup.childNodes.length, 2, "two notifications are shown"); dismissNotification(popup); }, - onHidden: function () {} + onHidden: function () { + this.notification1.remove(); + this.notification2.remove(); + } }, { // Test #30 - Popup Notifications main actions should catch exceptions from callbacks run: function () { this.testNotif = new errorNotification(); showNotification(this.testNotif); }, onShown: function (popup) { checkPopup(popup, this.testNotif); @@ -1012,20 +1015,20 @@ var tests = [ }); } }, { // Test #35 - Moving a tab to a new window should preserve swappable notifications. run: function() { gBrowser.selectedTab = gBrowser.addTab("about:blank"); let notifyObj = new basicNotification(); let originalCallback = notifyObj.options.eventCallback; - notifyObj.options.eventCallback = function (eventName) { - originalCallback(eventName); - return eventName == "swapping"; - }; + notifyObj.options.eventCallback = function (eventName) { + originalCallback(eventName); + return eventName == "swapping"; + }; showNotification(notifyObj); let win = gBrowser.replaceTabWithWindow(gBrowser.selectedTab); whenDelayedStartupFinished(win, function() { let [tab] = win.gBrowser.tabs; let anchor = win.document.getElementById("default-notification-icon"); win.PopupNotifications._reshowNotifications(anchor); checkPopup(win.PopupNotifications.panel, notifyObj); @@ -1035,53 +1038,73 @@ var tests = [ }); } }, { // Test #36 - the hideNotNow option run: function () { this.notifyObj = new basicNotification(); this.notifyObj.options.hideNotNow = true; this.notifyObj.mainAction.dismiss = true; - showNotification(this.notifyObj); + this.notification = showNotification(this.notifyObj); }, onShown: function (popup) { // checkPopup verifies that the Not Now item is hidden, and that no separator is added. checkPopup(popup, this.notifyObj); triggerMainCommand(popup); }, - onHidden: function (popup) { } + onHidden: function (popup) { + this.notification.remove(); + } }, { // Test #37 - the main action callback can keep the notification. run: function () { this.notifyObj = new basicNotification(); this.notifyObj.mainAction.dismiss = true; - showNotification(this.notifyObj); + this.notification = showNotification(this.notifyObj); }, onShown: function (popup) { checkPopup(popup, this.notifyObj); triggerMainCommand(popup); }, onHidden: function (popup) { ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback was triggered"); ok(!this.notifyObj.removedCallbackTriggered, "removed callback wasn't triggered"); + this.notification.remove(); } }, { // Test #38 - a secondary action callback can keep the notification. run: function () { this.notifyObj = new basicNotification(); this.notifyObj.secondaryActions[0].dismiss = true; - showNotification(this.notifyObj); + this.notification = showNotification(this.notifyObj); }, onShown: function (popup) { checkPopup(popup, this.notifyObj); triggerSecondaryCommand(popup, 0); }, onHidden: function (popup) { ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback was triggered"); ok(!this.notifyObj.removedCallbackTriggered, "removed callback wasn't triggered"); + this.notification.remove(); + } + }, + { // Test #39 - returning true in the showing callback should dismiss the notification. + run: function() { + let notifyObj = new basicNotification(); + let originalCallback = notifyObj.options.eventCallback; + notifyObj.options.eventCallback = function (eventName) { + originalCallback(eventName); + return eventName == "showing"; + }; + + let notification = showNotification(notifyObj); + ok(notifyObj.showingCallbackTriggered, "the showing callback was triggered"); + ok(!notifyObj.shownCallbackTriggered, "the shown callback wasn't triggered"); + notification.remove(); + goNext(); } } ]; function showNotification(notifyObj) { return PopupNotifications.show(notifyObj.browser, notifyObj.id, notifyObj.message,
--- a/browser/branding/aurora/Makefile.in +++ b/browser/branding/aurora/Makefile.in @@ -32,17 +32,17 @@ BRANDING_FILES := \ background.png \ firefox.icns \ disk.icns \ document.icns \ dsstore \ $(NULL) endif -ifeq ($(MOZ_WIDGET_TOOLKIT),gtk2) +ifdef MOZ_WIDGET_GTK BRANDING_FILES := \ default16.png \ default32.png \ default48.png \ mozicon128.png \ $(NULL) endif
--- a/browser/branding/official/Makefile.in +++ b/browser/branding/official/Makefile.in @@ -32,17 +32,17 @@ BRANDING_FILES := \ background.png \ firefox.icns \ disk.icns \ document.icns \ dsstore \ $(NULL) endif -ifeq ($(MOZ_WIDGET_TOOLKIT),gtk2) +ifdef MOZ_WIDGET_GTK BRANDING_FILES := \ default16.png \ default32.png \ default48.png \ mozicon128.png \ $(NULL) endif
--- a/browser/components/customizableui/content/panelUI.inc.xul +++ b/browser/components/customizableui/content/panelUI.inc.xul @@ -29,17 +29,17 @@ tooltiptext="&appMenuCustomize.tooltip;" exitTooltiptext="&appMenuCustomizeExit.tooltip;" closemenu="none" oncommand="gCustomizeMode.toggle();"/> <toolbarseparator/> <toolbarbutton id="PanelUI-help" label="&helpMenu.label;" closemenu="none" tooltiptext="&appMenuHelp.tooltip;" - oncommand="PanelUI.showHelpView(this.parentNode);"/> + oncommand="PanelUI.showHelpView(this);"/> <toolbarseparator/> <toolbarbutton id="PanelUI-quit" #ifdef XP_WIN label="&quitApplicationCmdWin.label;" tooltiptext="&quitApplicationCmdWin.tooltip;" #else #ifdef XP_MACOSX label="&quitApplicationCmdMac.label;"
--- a/browser/components/places/content/controller.js +++ b/browser/components/places/content/controller.js @@ -1,14 +1,13 @@ /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ForgetAboutSite", "resource://gre/modules/ForgetAboutSite.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); // XXXmano: we should move most/all of these constants to PlacesUtils const ORGANIZER_ROOT_BOOKMARKS = "place:folder=BOOKMARKS_MENU&excludeItems=1&queryType=1"; @@ -53,16 +52,18 @@ function InsertionPoint(aItemId, aIndex, this.dropNearItemId = aDropNearItemId; } InsertionPoint.prototype = { set index(val) { return this._index = val; }, + promiseGUID: function () PlacesUtils.promiseItemGUID(this.itemId), + get index() { if (this.dropNearItemId > 0) { // If dropNearItemId is set up we must calculate the real index of // the item near which we will drop. var index = PlacesUtils.bookmarks.getItemIndex(this.dropNearItemId); return this.orientation == Ci.nsITreeView.DROP_BEFORE ? index : index + 1; } return this._index; @@ -131,20 +132,17 @@ PlacesController.prototype = { case "cmd_copy": case "cmd_paste": case "cmd_delete": case "placesCmd_delete": case "placesCmd_moveBookmarks": case "cmd_paste": case "placesCmd_paste": case "placesCmd_new:folder": - case "placesCmd_new:livemark": case "placesCmd_new:bookmark": - case "placesCmd_new:separator": - case "placesCmd_sortBy:name": case "placesCmd_createBookmark": return false; } } switch (aCommand) { case "cmd_undo": if (!PlacesUIUtils.useAsyncTransactions) @@ -185,17 +183,16 @@ PlacesController.prototype = { } return false; case "placesCmd_open": case "placesCmd_open:window": case "placesCmd_open:tab": var selectedNode = this._view.selectedNode; return selectedNode && PlacesUtils.nodeIsURI(selectedNode); case "placesCmd_new:folder": - case "placesCmd_new:livemark": return this._canInsert(); case "placesCmd_new:bookmark": return this._canInsert(); case "placesCmd_new:separator": return this._canInsert() && !PlacesUtils.asQuery(this._view.result.root).queryOptions.excludeItems && this._view.result.sortingMode == Ci.nsINavHistoryQueryOptions.SORT_BY_NONE; @@ -219,20 +216,28 @@ PlacesController.prototype = { default: return false; } }, doCommand: function PC_doCommand(aCommand) { switch (aCommand) { case "cmd_undo": - PlacesUtils.transactionManager.undoTransaction(); + if (!PlacesUIUtils.useAsyncTransactions) { + PlacesUtils.transactionManager.undoTransaction(); + return; + } + PlacesTransactions.undo().then(null, Cu.reportError); break; case "cmd_redo": - PlacesUtils.transactionManager.redoTransaction(); + if (!PlacesUIUtils.useAsyncTransactions) { + PlacesUtils.transactionManager.redoTransaction(); + return; + } + PlacesTransactions.redo().then(null, Cu.reportError); break; case "cmd_cut": case "placesCmd_cut": this.cut(); break; case "cmd_copy": case "placesCmd_copy": this.copy(); @@ -268,33 +273,30 @@ PlacesController.prototype = { PlacesUIUtils.openNodeIn(this._view.selectedNode, "tab", this._view); break; case "placesCmd_new:folder": this.newItem("folder"); break; case "placesCmd_new:bookmark": this.newItem("bookmark"); break; - case "placesCmd_new:livemark": - this.newItem("livemark"); - break; case "placesCmd_new:separator": - this.newSeparator(); + this.newSeparator().then(null, Cu.reportError); break; case "placesCmd_show:info": this.showBookmarkPropertiesForSelection(); break; case "placesCmd_moveBookmarks": this.moveSelectedBookmarks(); break; case "placesCmd_reload": this.reloadSelectedLivemark(); break; case "placesCmd_sortBy:name": - this.sortFolderByName(); + this.sortFolderByName().then(null, Cu.reportError); break; case "placesCmd_createBookmark": let node = this._view.selectedNode; PlacesUIUtils.showBookmarkDialog({ action: "add" , type: "bookmark" , hiddenRows: [ "description" , "keyword" , "location" @@ -762,45 +764,61 @@ PlacesController.prototype = { .getIdForItemAt(ip.itemId, ip.index); this._view.selectItems([insertedNodeId], false); } }, /** * Create a new Bookmark separator somewhere. */ - newSeparator: function PC_newSeparator() { + newSeparator: Task.async(function* () { var ip = this._view.insertionPoint; if (!ip) throw Cr.NS_ERROR_NOT_AVAILABLE; - var txn = new PlacesCreateSeparatorTransaction(ip.itemId, ip.index); - PlacesUtils.transactionManager.doTransaction(txn); - // select the new item - var insertedNodeId = PlacesUtils.bookmarks - .getIdForItemAt(ip.itemId, ip.index); - this._view.selectItems([insertedNodeId], false); - }, + + if (!PlacesUIUtils.useAsyncTransactions) { + let txn = new PlacesCreateSeparatorTransaction(ip.itemId, ip.index); + PlacesUtils.transactionManager.doTransaction(txn); + // Select the new item. + let insertedNodeId = PlacesUtils.bookmarks + .getIdForItemAt(ip.itemId, ip.index); + this._view.selectItems([insertedNodeId], false); + return; + } + + let txn = PlacesTransactions.NewSeparator({ parentGUID: yield ip.promiseGUID() + , index: ip.index }); + let guid = yield PlacesTransactions.transact(txn); + let itemId = yield PlacesUtils.promiseItemId(guid); + // Select the new item. + this._view.selectItems([itemId], false); + }), /** * Opens a dialog for moving the selected nodes. */ moveSelectedBookmarks: function PC_moveBookmarks() { window.openDialog("chrome://browser/content/places/moveBookmarks.xul", "", "chrome, modal", this._view.selectedNodes); }, /** * Sort the selected folder by name */ - sortFolderByName: function PC_sortFolderByName() { - var itemId = PlacesUtils.getConcreteItemId(this._view.selectedNode); - var txn = new PlacesSortFolderByNameTransaction(itemId); - PlacesUtils.transactionManager.doTransaction(txn); - }, + sortFolderByName: Task.async(function* () { + let itemId = PlacesUtils.getConcreteItemId(this._view.selectedNode); + if (!PlacesUIUtils.useAsyncTransactions) { + var txn = new PlacesSortFolderByNameTransaction(itemId); + PlacesUtils.transactionManager.doTransaction(txn); + return; + } + let guid = yield PlacesUtils.promiseItemGUID(itemId); + yield PlacesTransactions.transact(PlacesTransactions.SortByName(guid)); + }), /** * Walk the list of folders we're removing in this delete operation, and * see if the selected node specified is already implicitly being removed * because it is a child of that folder. * @param node * Node to check for containment. * @param pastFolders @@ -1656,17 +1674,16 @@ function goUpdatePlacesCommands() { placesController.isCommandEnabled(aCommand)); } updatePlacesCommand("placesCmd_open"); updatePlacesCommand("placesCmd_open:window"); updatePlacesCommand("placesCmd_open:tab"); updatePlacesCommand("placesCmd_new:folder"); updatePlacesCommand("placesCmd_new:bookmark"); - updatePlacesCommand("placesCmd_new:livemark"); updatePlacesCommand("placesCmd_new:separator"); updatePlacesCommand("placesCmd_show:info"); updatePlacesCommand("placesCmd_moveBookmarks"); updatePlacesCommand("placesCmd_reload"); updatePlacesCommand("placesCmd_sortBy:name"); updatePlacesCommand("placesCmd_cut"); updatePlacesCommand("placesCmd_copy"); updatePlacesCommand("placesCmd_paste");
--- a/browser/components/places/content/placesOverlay.xul +++ b/browser/components/places/content/placesOverlay.xul @@ -19,18 +19,23 @@ src="chrome://browser/content/utilityOverlay.js"/> <script type="application/javascript"><![CDATA[ // TODO: Bug 406371. // A bunch of browser code depends on us defining these, sad but true :( var Cc = Components.classes; var Ci = Components.interfaces; var Cr = Components.results; + Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + Components.utils.import("resource://gre/modules/Task.jsm"); Components.utils.import("resource://gre/modules/PlacesUtils.jsm"); - Components.utils.import("resource:///modules/PlacesUIUtils.jsm"); + XPCOMUtils.defineLazyModuleGetter(window, + "PlacesUIUtils", "resource:///modules/PlacesUIUtils.jsm"); + XPCOMUtils.defineLazyModuleGetter(window, + "PlacesTransactions", "resource://gre/modules/PlacesTransactions.jsm"); ]]></script> <script type="application/javascript" src="chrome://browser/content/places/controller.js"/> <script type="application/javascript" src="chrome://browser/content/places/treeView.js"/> <!-- Bookmarks and history tooltip --> <tooltip id="bhTooltip" noautohide="true" @@ -49,18 +54,16 @@ oncommand="goDoPlacesCommand('placesCmd_open');"/> <command id="placesCmd_open:window" oncommand="goDoPlacesCommand('placesCmd_open:window');"/> <command id="placesCmd_open:tab" oncommand="goDoPlacesCommand('placesCmd_open:tab');"/> <command id="placesCmd_new:bookmark" oncommand="goDoPlacesCommand('placesCmd_new:bookmark');"/> - <command id="placesCmd_new:livemark" - oncommand="goDoPlacesCommand('placesCmd_new:livemark');"/> <command id="placesCmd_new:folder" oncommand="goDoPlacesCommand('placesCmd_new:folder');"/> <command id="placesCmd_new:separator" oncommand="goDoPlacesCommand('placesCmd_new:separator');"/> <command id="placesCmd_show:info" oncommand="goDoPlacesCommand('placesCmd_show:info');"/> <command id="placesCmd_rename" oncommand="goDoPlacesCommand('placesCmd_show:info');"
new file mode 100644 --- /dev/null +++ b/browser/components/translation/Translation.jsm @@ -0,0 +1,134 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["Translation"]; + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector", + "resource:///modules/translation/LanguageDetector.jsm"); + +/* Create an object keeping the information related to translation for + * a specific browser. This object is passed to the translation + * infobar so that it can initialize itself. The properties exposed to + * the infobar are: + * - supportedSourceLanguages, array of supported source language codes + * - supportedTargetLanguages, array of supported target language codes + * - detectedLanguage, code of the language detected on the web page. + * - defaultTargetLanguage, code of the language to use by default for + * translation. + * - state, the state in which the infobar should be displayed + * - STATE_{OFFER,TRANSLATING,TRANSLATED,ERROR} constants. + * - translatedFrom, if already translated, source language code. + * - translatedTo, if already translated, target language code. + * - translate, method starting the translation of the current page. + * - showOriginalContent, method showing the original page content. + * - showTranslatedContent, method showing the translation for an + * already translated page whose original content is shown. + * - originalShown, boolean indicating if the original or translated + * version of the page is shown. + */ +this.Translation = function(aBrowser) { + this.browser = aBrowser; +}; + +this.Translation.prototype = { + supportedSourceLanguages: ["en", "zh", "ja", "es", "de", "fr", "ru", "ar", "ko", "pt"], + supportedTargetLanguages: ["en", "pl", "tr", "vi"], + + STATE_OFFER: 0, + STATE_TRANSLATING: 1, + STATE_TRANSLATED: 2, + STATE_ERROR: 3, + + _defaultTargetLanguage: "", + get defaultTargetLanguage() { + if (!this._defaultTargetLanguage) { + this._defaultTargetLanguage = Cc["@mozilla.org/chrome/chrome-registry;1"] + .getService(Ci.nsIXULChromeRegistry) + .getSelectedLocale("global") + .split("-")[0]; + } + return this._defaultTargetLanguage; + }, + + get doc() this.browser.contentDocument, + + translate: function(aFrom, aTo) { + this.state = this.STATE_TRANSLATING; + this.translatedFrom = aFrom; + this.translatedTo = aTo; + }, + + showURLBarIcon: function(aTranslated) { + let chromeWin = this.browser.ownerGlobal; + let PopupNotifications = chromeWin.PopupNotifications; + let removeId = aTranslated ? "translate" : "translated"; + let notification = + PopupNotifications.getNotification(removeId, this.browser); + if (notification) + PopupNotifications.remove(notification); + + let callback = aTopic => { + if (aTopic != "showing") + return false; + if (!this.notificationBox.getNotificationWithValue("translation")) + this.showTranslationInfoBar(); + return true; + }; + + let addId = aTranslated ? "translated" : "translate"; + PopupNotifications.show(this.browser, addId, null, + addId + "-notification-icon", null, null, + {dismissed: true, eventCallback: callback}); + }, + + _state: 0, + get state() this._state, + set state(val) { + let notif = this.notificationBox.getNotificationWithValue("translation"); + if (notif) + notif.state = val; + this._state = val; + }, + + originalShown: true, + showOriginalContent: function() { + this.showURLBarIcon(); + this.originalShown = true; + }, + + showTranslatedContent: function() { + this.showURLBarIcon(true); + this.originalShown = false; + }, + + get notificationBox() this.browser.ownerGlobal.gBrowser.getNotificationBox(), + + showTranslationInfoBar: function() { + let notificationBox = this.notificationBox; + let notif = notificationBox.appendNotification("", "translation", null, + notificationBox.PRIORITY_INFO_HIGH); + notif.init(this); + return notif; + }, + + showTranslationUI: function(aDetectedLanguage) { + this.detectedLanguage = aDetectedLanguage; + + // Reset all values before showing a new translation infobar. + this.state = 0; + this.translatedFrom = ""; + this.translatedTo = ""; + this.originalShown = true; + + this.showURLBarIcon(); + return this.showTranslationInfoBar(); + } +};
--- a/browser/components/translation/moz.build +++ b/browser/components/translation/moz.build @@ -2,17 +2,18 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. JS_MODULES_PATH = 'modules/translation' EXTRA_JS_MODULES = [ 'cld2/cld-worker.js', 'cld2/cld-worker.js.mem', - 'LanguageDetector.jsm' + 'LanguageDetector.jsm', + 'Translation.jsm' ] JAR_MANIFESTS += ['jar.mn'] BROWSER_CHROME_MANIFESTS += [ 'test/browser.ini' ]
--- a/browser/components/translation/test/browser_translation_infobar.js +++ b/browser/components/translation/test/browser_translation_infobar.js @@ -1,155 +1,194 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // tests the translation infobar, using a fake 'Translation' implementation. -var Translation = { - supportedSourceLanguages: ["en", "zh", "ja", "es", "de", "fr", "ru", "ar", "ko", "pt"], - supportedTargetLanguages: ["en", "pl", "tr", "vi"], - defaultTargetLanguage: "en", +Components.utils.import("resource:///modules/translation/Translation.jsm"); - _translateFrom: "", - _translateTo: "", - _deferred: null, +function waitForCondition(condition, nextTest, errorMsg) { + var tries = 0; + var interval = setInterval(function() { + if (tries >= 30) { + ok(false, errorMsg); + moveOn(); + } + var conditionPassed; + try { + conditionPassed = condition(); + } catch (e) { + ok(false, e + "\n" + e.stack); + conditionPassed = false; + } + if (conditionPassed) { + moveOn(); + } + tries++; + }, 100); + var moveOn = function() { clearInterval(interval); nextTest(); }; +} + +var TranslationStub = { + __proto__: Translation.prototype, + translate: function(aFrom, aTo) { - this._translateFrom = aFrom; - this._translateTo = aTo; - this._deferred = Promise.defer(); - return this._deferred.promise; + this.state = this.STATE_TRANSLATING; + this.translatedFrom = aFrom; + this.translatedTo = aTo; }, _reset: function() { - this._translateFrom = ""; - this._translateTo = ""; - this._deferred = null; + this.translatedFrom = ""; + this.translatedTo = ""; }, failTranslation: function() { - this._deferred.reject(); + this.state = this.STATE_ERROR; this._reset(); }, finishTranslation: function() { - this._deferred.resolve(); + this.showTranslatedContent(); + this.state = this.STATE_TRANSLATED; this._reset(); - }, - - _showOriginalCalled: false, - showOriginalContent: function() { - this._showOriginalCalled = true; - }, - - _showTranslationCalled: false, - showTranslatedContent: function() { - this._showTranslationCalled = true; - }, - - showTranslationUI: function(aLanguage) { - let notificationBox = gBrowser.getNotificationBox(); - let notif = notificationBox.appendNotification("", "translation", null, - notificationBox.PRIORITY_INFO_HIGH); - notif.init(this); - notif.detectedLanguage = aLanguage; - return notif; } }; + function test() { waitForExplicitFinish(); - // Show an info bar saying the current page is in French - let notif = Translation.showTranslationUI("fr"); - is(notif.state, notif.STATE_OFFER, "the infobar is offering translation"); - is(notif._getAnonElt("detectedLanguage").value, "fr", "The detected language is displayed"); + let tab = gBrowser.addTab(); + gBrowser.selectedTab = tab; + tab.linkedBrowser.addEventListener("load", function onload() { + tab.linkedBrowser.removeEventListener("load", onload, true); + TranslationStub.browser = gBrowser.selectedBrowser; + registerCleanupFunction(function () { + gBrowser.removeTab(tab); + }); + run_tests(() => { + finish(); + }); + }, true); - // Click the "Translate" button - notif._getAnonElt("translate").click(); - is(notif.state, notif.STATE_TRANSLATING, "the infobar is in the translating state"); - ok(!!Translation._deferred, "Translation.translate has been called"); - is(Translation._translateFrom, "fr", "from language correct"); - is(Translation._translateTo, Translation.defaultTargetLanguage, "from language correct"); + content.location = "data:text/plain,test page"; +} + +function checkURLBarIcon(aExpectTranslated = false) { + is(!PopupNotifications.getNotification("translate"), aExpectTranslated, + "translate icon " + (aExpectTranslated ? "not " : "") + "shown"); + is(!!PopupNotifications.getNotification("translated"), aExpectTranslated, + "translated icon " + (aExpectTranslated ? "" : "not ") + "shown"); +} - // Make the translation fail and check we are in the error state. - Translation.failTranslation(); - is(notif.state, notif.STATE_ERROR, "infobar in the error state"); +function run_tests(aFinishCallback) { + info("Show an info bar saying the current page is in French"); + let notif = TranslationStub.showTranslationUI("fr"); + is(notif.state, TranslationStub.STATE_OFFER, "the infobar is offering translation"); + is(notif._getAnonElt("detectedLanguage").value, "fr", "The detected language is displayed"); + checkURLBarIcon(); - // Click the try again button + info("Click the 'Translate' button"); + notif._getAnonElt("translate").click(); + is(notif.state, TranslationStub.STATE_TRANSLATING, "the infobar is in the translating state"); + ok(!!TranslationStub.translatedFrom, "Translation.translate has been called"); + is(TranslationStub.translatedFrom, "fr", "from language correct"); + is(TranslationStub.translatedTo, TranslationStub.defaultTargetLanguage, "from language correct"); + checkURLBarIcon(); + + info("Make the translation fail and check we are in the error state."); + TranslationStub.failTranslation(); + is(notif.state, TranslationStub.STATE_ERROR, "infobar in the error state"); + checkURLBarIcon(); + + info("Click the try again button"); notif._getAnonElt("tryAgain").click(); - is(notif.state, notif.STATE_TRANSLATING, "infobar in the translating state"); - ok(!!Translation._deferred, "Translation.translate has been called"); - is(Translation._translateFrom, "fr", "from language correct"); - is(Translation._translateTo, Translation.defaultTargetLanguage, "to language correct"); + is(notif.state, TranslationStub.STATE_TRANSLATING, "infobar in the translating state"); + ok(!!TranslationStub.translatedFrom, "Translation.translate has been called"); + is(TranslationStub.translatedFrom, "fr", "from language correct"); + is(TranslationStub.translatedTo, TranslationStub.defaultTargetLanguage, "from language correct"); + checkURLBarIcon(); - // Make the translation succeed and check we are in the 'translated' state. - Translation.finishTranslation(); - is(notif.state, notif.STATE_TRANSLATED, "infobar in the translated state"); + info("Make the translation succeed and check we are in the 'translated' state."); + TranslationStub.finishTranslation(); + is(notif.state, TranslationStub.STATE_TRANSLATED, "infobar in the translated state"); + checkURLBarIcon(true); - // Test 'Show Original' / 'Show Translation' buttons. + info("Test 'Show original' / 'Show Translation' buttons."); // First check 'Show Original' is visible and 'Show Translation' is hidden. ok(!notif._getAnonElt("showOriginal").hidden, "'Show Original' button visible"); ok(notif._getAnonElt("showTranslation").hidden, "'Show Translation' button hidden"); // Click the button. notif._getAnonElt("showOriginal").click(); - // Check the correct function has been called. - ok(Translation._showOriginalCalled, "'Translation.showOriginalContent' called") - ok(!Translation._showTranslationCalled, "'Translation.showTranslatedContent' not called") - Translation._showOriginalCalled = false; + // Check that the url bar icon shows the original content is displayed. + checkURLBarIcon(); // And the 'Show Translation' button is now visible. ok(notif._getAnonElt("showOriginal").hidden, "'Show Original' button hidden"); ok(!notif._getAnonElt("showTranslation").hidden, "'Show Translation' button visible"); // Click the 'Show Translation' button notif._getAnonElt("showTranslation").click(); - // Check the correct function has been called. - ok(!Translation._showOriginalCalled, "'Translation.showOriginalContent' not called") - ok(Translation._showTranslationCalled, "'Translation.showTranslatedContent' called") - Translation._showTranslationCalled = false; + // Check that the url bar icon shows the page is translated. + checkURLBarIcon(true); // Check that the 'Show Original' button is visible again. ok(!notif._getAnonElt("showOriginal").hidden, "'Show Original' button visible"); ok(notif._getAnonElt("showTranslation").hidden, "'Show Translation' button hidden"); - // Check that changing the source language causes a re-translation + info("Check that changing the source language causes a re-translation"); let from = notif._getAnonElt("fromLanguage"); from.value = "es"; from.doCommand(); - is(notif.state, notif.STATE_TRANSLATING, "infobar in the translating state"); - ok(!!Translation._deferred, "Translation.translate has been called"); - is(Translation._translateFrom, "es", "from language correct"); - is(Translation._translateTo, Translation.defaultTargetLanguage, "to language correct"); - Translation.finishTranslation(); + is(notif.state, TranslationStub.STATE_TRANSLATING, "infobar in the translating state"); + ok(!!TranslationStub.translatedFrom, "Translation.translate has been called"); + is(TranslationStub.translatedFrom, "es", "from language correct"); + is(TranslationStub.translatedTo, TranslationStub.defaultTargetLanguage, "to language correct"); + // We want to show the 'translated' icon while re-translating, + // because we are still displaying the previous translation. + checkURLBarIcon(true); + TranslationStub.finishTranslation(); + checkURLBarIcon(true); - // Check that changing the target language causes a re-translation + info("Check that changing the target language causes a re-translation"); let to = notif._getAnonElt("toLanguage"); to.value = "pl"; to.doCommand(); - is(notif.state, notif.STATE_TRANSLATING, "infobar in the translating state"); - ok(!!Translation._deferred, "Translation.translate has been called"); - is(Translation._translateFrom, "es", "from language correct"); - is(Translation._translateTo, "pl", "to language correct"); - Translation.finishTranslation(); + is(notif.state, TranslationStub.STATE_TRANSLATING, "infobar in the translating state"); + ok(!!TranslationStub.translatedFrom, "Translation.translate has been called"); + is(TranslationStub.translatedFrom, "es", "from language correct"); + is(TranslationStub.translatedTo, "pl", "to language correct"); + checkURLBarIcon(true); + TranslationStub.finishTranslation(); + checkURLBarIcon(true); // Cleanup. notif.close(); - // Reopen the info bar to check that it's possible to override the detected language. - notif = Translation.showTranslationUI("fr"); - is(notif.state, notif.STATE_OFFER, "the infobar is offering translation"); + info("Reopen the info bar to check that it's possible to override the detected language."); + notif = TranslationStub.showTranslationUI("fr"); + is(notif.state, TranslationStub.STATE_OFFER, "the infobar is offering translation"); is(notif._getAnonElt("detectedLanguage").value, "fr", "The detected language is displayed"); // Change the language and click 'Translate' notif._getAnonElt("detectedLanguage").value = "ja"; notif._getAnonElt("translate").click(); - is(notif.state, notif.STATE_TRANSLATING, "the infobar is in the translating state"); - ok(!!Translation._deferred, "Translation.translate has been called"); - is(Translation._translateFrom, "ja", "from language correct"); + is(notif.state, TranslationStub.STATE_TRANSLATING, "the infobar is in the translating state"); + ok(!!TranslationStub.translatedFrom, "Translation.translate has been called"); + is(TranslationStub.translatedFrom, "ja", "from language correct"); notif.close(); - // Reopen one last time to check the 'Not Now' button closes the notification. - notif = Translation.showTranslationUI("fr"); - + info("Reopen to check the 'Not Now' button closes the notification."); + notif = TranslationStub.showTranslationUI("fr"); let notificationBox = gBrowser.getNotificationBox(); ok(!!notificationBox.getNotificationWithValue("translation"), "there's a 'translate' notification"); notif._getAnonElt("notNow").click(); ok(!notificationBox.getNotificationWithValue("translation"), "no 'translate' notification after clicking 'not now'"); - finish(); + info("Check that clicking the url bar icon reopens the info bar"); + checkURLBarIcon(); + // Clicking the anchor element causes a 'showing' event to be sent + // asynchronously to our callback that will then show the infobar. + PopupNotifications.getNotification("translate").anchorElement.click(); + waitForCondition(() => !!notificationBox.getNotificationWithValue("translation"), () => { + ok(!!notificationBox.getNotificationWithValue("translation"), + "there's a 'translate' notification"); + aFinishCallback(); + }, "timeout waiting for the info bar to reappear"); }
--- a/browser/components/translation/translation-infobar.xml +++ b/browser/components/translation/translation-infobar.xml @@ -81,117 +81,106 @@ <xul:toolbarbutton ondblclick="event.stopPropagation();" class="messageCloseButton close-icon tabbable" xbl:inherits="hidden=hideclose" tooltiptext="&closeNotification.tooltip;" oncommand="document.getBindingParent(this).close();"/> </xul:hbox> </content> <implementation> - <field name="STATE_OFFER" readonly="true">0</field> - <field name="STATE_TRANSLATING" readonly="true">1</field> - <field name="STATE_TRANSLATED" readonly="true">2</field> - <field name="STATE_ERROR" readonly="true">3</field> - <property name="state" onget="return this._getAnonElt('translationStates').selectedIndex;" onset="this._getAnonElt('translationStates').selectedIndex = val;"/> - <!-- Initialize the infobar with a translation object exposing these - properties: - - supportedSourceLanguages, array of supported source language codes - - supportedTargetLanguages, array of supported target language codes - - defaultTargetLanguage, code of the language to use by default for - translation. - - translate, method starting the translation of the current page. - Returns a promise. - - showOriginalContent, method showing the original page content. - - showTranslatedContent, method showing the translation for an - already translated page whose original content is shown. - --> <method name="init"> <parameter name="aTranslation"/> <body> <![CDATA[ this.translation = aTranslation; let bundle = Cc["@mozilla.org/intl/stringbundle;1"] .getService(Ci.nsIStringBundleService) .createBundle("chrome://global/locale/languageNames.properties"); + // Fill the lists of supported source languages. let detectedLanguage = this._getAnonElt("detectedLanguage"); let fromLanguage = this._getAnonElt("fromLanguage"); for (let code of this.translation.supportedSourceLanguages) { let name = bundle.GetStringFromName(code); detectedLanguage.appendItem(name, code); fromLanguage.appendItem(name, code); } + detectedLanguage.value = this.translation.detectedLanguage; + // translatedFrom is only set if we have already translated this page. + if (aTranslation.translatedFrom) + fromLanguage.value = aTranslation.translatedFrom; + + // Fill the list of supporter target languages. let toLanguage = this._getAnonElt("toLanguage"); for (let code of this.translation.supportedTargetLanguages) toLanguage.appendItem(bundle.GetStringFromName(code), code); + + if (aTranslation.translatedTo) + toLanguage.value = aTranslation.translatedTo; + + if (aTranslation.state) + this.state = aTranslation.state; + + this._handleButtonHiding(aTranslation.originalShown); ]]> </body> </method> <method name="_getAnonElt"> <parameter name="aAnonId"/> <body> return document.getAnonymousElementByAttribute(this, "anonid", aAnonId); </body> </method> - - <field name="_detectedLanguage">""</field> - <property name="detectedLanguage" onget="return this._detectedLanguage;"> - <setter><![CDATA[ - this._getAnonElt("detectedLanguage").value = val; - this._detectedLanguage = val; - return val; - ]]></setter> - </property> - <method name="translate"> <body> <![CDATA[ - if (this.state == this.STATE_OFFER) { + if (this.state == this.translation.STATE_OFFER) { this._getAnonElt("fromLanguage").value = this._getAnonElt("detectedLanguage").value; this._getAnonElt("toLanguage").value = this.translation.defaultTargetLanguage; } - this._getAnonElt("showOriginal").hidden = false; - this._getAnonElt("showTranslation").hidden = true; + this._handleButtonHiding(false); + this.translation.translate(this._getAnonElt("fromLanguage").value, + this._getAnonElt("toLanguage").value); + ]]> + </body> + </method> - this.state = this.STATE_TRANSLATING; - this.translation.translate(this._getAnonElt("fromLanguage").value, - this._getAnonElt("toLanguage").value) - .then(() => { this.state = this.STATE_TRANSLATED; }, - () => { this.state = this.STATE_ERROR; }); + <method name="_handleButtonHiding"> + <parameter name="aOriginalShown"/> + <body> + <![CDATA[ + this._getAnonElt("showOriginal").hidden = aOriginalShown; + this._getAnonElt("showTranslation").hidden = !aOriginalShown; ]]> </body> </method> <method name="showOriginal"> <body> <![CDATA[ - this._getAnonElt("showOriginal").hidden = true; - this._getAnonElt("showTranslation").hidden = false; - + this._handleButtonHiding(true); this.translation.showOriginalContent(); ]]> </body> </method> <method name="showTranslation"> <body> <![CDATA[ - this._getAnonElt("showOriginal").hidden = false; - this._getAnonElt("showTranslation").hidden = true; - + this._handleButtonHiding(false); this.translation.showTranslatedContent(); ]]> </body> </method> </implementation> </binding> </bindings>
--- a/browser/devtools/app-manager/device-store.js +++ b/browser/devtools/app-manager/device-store.js @@ -29,17 +29,17 @@ module.exports = DeviceStore = function( this._onStatusChanged = this._onStatusChanged.bind(this); this._connection = connection; this._connection.once(Connection.Events.DESTROYED, this.destroy); this._connection.on(Connection.Events.STATUS_CHANGED, this._onStatusChanged); this._onTabListChanged = this._onTabListChanged.bind(this); this._onStatusChanged(); return this; -} +}; DeviceStore.prototype = { destroy: function() { if (this._connection) { // While this.destroy is bound using .once() above, that event may not // have occurred when the DeviceStore client calls destroy, so we // manually remove it here. this._connection.off(Connection.Events.DESTROYED, this.destroy); @@ -52,39 +52,45 @@ DeviceStore.prototype = { _resetStore: function() { this.object.description = {}; this.object.permissions = []; this.object.tabs = []; }, _onStatusChanged: function() { if (this._connection.status == Connection.Status.CONNECTED) { + // Watch for changes to remote browser tabs + this._connection.client.addListener("tabListChanged", + this._onTabListChanged); this._listTabs(); } else { + if (this._connection.client) { + this._connection.client.removeListener("tabListChanged", + this._onTabListChanged); + } this._resetStore(); } }, _onTabListChanged: function() { this._listTabs(); }, _listTabs: function() { + if (!this._connection) { + return; + } this._connection.client.listTabs((resp) => { if (resp.error) { this._connection.disconnect(); return; } this._deviceFront = getDeviceFront(this._connection.client, resp); // Save remote browser's tabs this.object.tabs = resp.tabs; - // Add listener to update remote browser's tabs list in app-manager - // when it changes - this._connection.client.addListener( - 'tabListChanged', this._onTabListChanged); this._feedStore(); }); }, _feedStore: function() { this._getDeviceDescription(); this._getDevicePermissionsTable(); }, @@ -108,9 +114,9 @@ DeviceStore.prototype = { app: permissionsTable[name].app, privileged: permissionsTable[name].privileged, certified: permissionsTable[name].certified, }); } this.object.permissions = permissionsArray; }); } -} +};
--- a/browser/devtools/debugger/test/browser_dbg_addon-modules-unpacked.js +++ b/browser/devtools/debugger/test/browser_dbg_addon-modules-unpacked.js @@ -1,137 +1,48 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ // Make sure the add-on actor can see loaded JS Modules from an add-on -const ADDON5_URL = EXAMPLE_URL + "addon5.xpi"; - -let gAddon, gClient, gThreadClient, gDebugger, gSources, gTitle; - -function onMessage(event) { - try { - let json = JSON.parse(event.data); - switch (json.name) { - case "toolbox-title": - gTitle = json.data.value; - break; - } - } catch(e) { - DevToolsUtils.reportException("onMessage", e); - } -} +const ADDON_URL = EXAMPLE_URL + "addon5.xpi"; function test() { Task.spawn(function () { - if (!DebuggerServer.initialized) { - DebuggerServer.init(() => true); - DebuggerServer.addBrowserActors(); - } + let addon = yield addAddon(ADDON_URL); + let addonDebugger = yield initAddonDebugger(ADDON_URL); - gBrowser.selectedTab = gBrowser.addTab(); - let iframe = document.createElement("iframe"); - document.documentElement.appendChild(iframe); - - window.addEventListener("message", onMessage); + is(addonDebugger.title, "Debugger - Test unpacked add-on with JS Modules", "Saw the right toolbox title."); - let transport = DebuggerServer.connectPipe(); - gClient = new DebuggerClient(transport); - - let connected = promise.defer(); - gClient.connect(connected.resolve); - yield connected.promise; + // Check the inital list of sources is correct + let groups = yield addonDebugger.getSourceGroups(); + is(groups[0].name, "browser_dbg_addon5@tests.mozilla.org", "Add-on code should be the first group"); + is(groups.length, 1, "Should be only one group."); - yield installAddon(); - let debuggerPanel = yield initAddonDebugger(gClient, ADDON5_URL, iframe); - gDebugger = debuggerPanel.panelWin; - gThreadClient = gDebugger.gThreadClient; - gSources = gDebugger.DebuggerView.Sources; + let sources = groups[0].sources; + is(sources.length, 2, "Should be two sources"); + ok(sources[0].url.endsWith("/browser_dbg_addon5@tests.mozilla.org/bootstrap.js"), "correct url for bootstrap code") + is(sources[0].label, "bootstrap.js", "correct label for bootstrap code") + is(sources[1].url, "resource://browser_dbg_addon5/test.jsm", "correct url for addon code") + is(sources[1].label, "test.jsm", "correct label for addon code") - yield testSources(false); - + // Load a new module and check it appears in the list of sources Cu.import("resource://browser_dbg_addon5/test2.jsm", {}); - yield testSources(true); + groups = yield addonDebugger.getSourceGroups(); + is(groups[0].name, "browser_dbg_addon5@tests.mozilla.org", "Add-on code should be the first group"); + is(groups.length, 1, "Should be only one group."); + + sources = groups[0].sources; + is(sources.length, 3, "Should be three sources"); + ok(sources[0].url.endsWith("/browser_dbg_addon5@tests.mozilla.org/bootstrap.js"), "correct url for bootstrap code") + is(sources[0].label, "bootstrap.js", "correct label for bootstrap code") + is(sources[1].url, "resource://browser_dbg_addon5/test.jsm", "correct url for addon code") + is(sources[1].label, "test.jsm", "correct label for addon code") + is(sources[2].url, "resource://browser_dbg_addon5/test2.jsm", "correct url for addon code") + is(sources[2].label, "test2.jsm", "correct label for addon code") Cu.unload("resource://browser_dbg_addon5/test2.jsm"); - - yield uninstallAddon(); - yield closeConnection(); - yield debuggerPanel._toolbox.destroy(); - iframe.remove(); - window.removeEventListener("message", onMessage); + yield addonDebugger.destroy(); + yield removeAddon(addon); finish(); }); } - -function installAddon () { - return addAddon(ADDON5_URL).then(aAddon => { - gAddon = aAddon; - }); -} - -function testSources(expectSecondModule) { - let deferred = promise.defer(); - let foundAddonModule = false; - let foundAddonModule2 = false; - let foundAddonBootstrap = false; - - gThreadClient.getSources(({sources}) => { - ok(sources.length, "retrieved sources"); - - for (let source of sources) { - let url = source.url.split(" -> ").pop(); - let { label, group } = gSources.getItemByValue(source.url).attachment; - - if (url.indexOf("resource://browser_dbg_addon5/test.jsm") === 0) { - is(label, "test.jsm", "correct label for addon code"); - is(group, "browser_dbg_addon5@tests.mozilla.org", "addon module is in the add-on's group"); - foundAddonModule = true; - } else if (url.indexOf("resource://browser_dbg_addon5/test2.jsm") === 0) { - is(label, "test2.jsm", "correct label for addon code"); - is(group, "browser_dbg_addon5@tests.mozilla.org", "addon module is in the add-on's group"); - foundAddonModule2 = true; - } else if (url.endsWith("/browser_dbg_addon5@tests.mozilla.org/bootstrap.js")) { - is(label, "bootstrap.js", "correct label for bootstrap code"); - is(group, "browser_dbg_addon5@tests.mozilla.org", "addon bootstrap script is in the add-on's group"); - foundAddonBootstrap = true; - } else { - ok(false, "Saw an unexpected source: " + url); - } - } - - ok(foundAddonModule, "found JS module for the addon in the list"); - is(foundAddonModule2, expectSecondModule, "saw the second addon module"); - ok(foundAddonBootstrap, "found bootstrap script for the addon in the list"); - - is(gTitle, "Debugger - Test unpacked add-on with JS Modules", "Saw the right toolbox title."); - - let groups = gDebugger.document.querySelectorAll(".side-menu-widget-group-title .name"); - is(groups[0].value, "browser_dbg_addon5@tests.mozilla.org", "Add-on code should be the first group"); - is(groups.length, 1, "Should be only one group."); - - deferred.resolve(); - }); - - return deferred.promise; -} - -function uninstallAddon() { - return removeAddon(gAddon); -} - -function closeConnection () { - let deferred = promise.defer(); - gClient.close(deferred.resolve); - return deferred.promise; -} - -registerCleanupFunction(function() { - gClient = null; - gAddon = null; - gThreadClient = null; - gDebugger = null; - gSources = null; - while (gBrowser.tabs.length > 1) { - gBrowser.removeCurrentTab(); - } -});
--- a/browser/devtools/debugger/test/browser_dbg_addon-modules.js +++ b/browser/devtools/debugger/test/browser_dbg_addon-modules.js @@ -1,137 +1,48 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ // Make sure the add-on actor can see loaded JS Modules from an add-on -const ADDON4_URL = EXAMPLE_URL + "addon4.xpi"; - -let gAddon, gClient, gThreadClient, gDebugger, gSources, gTitle; - -function onMessage(event) { - try { - let json = JSON.parse(event.data); - switch (json.name) { - case "toolbox-title": - gTitle = json.data.value; - break; - } - } catch(e) { - DevToolsUtils.reportException("onMessage", e); - } -} +const ADDON_URL = EXAMPLE_URL + "addon4.xpi"; function test() { Task.spawn(function () { - if (!DebuggerServer.initialized) { - DebuggerServer.init(() => true); - DebuggerServer.addBrowserActors(); - } + let addon = yield addAddon(ADDON_URL); + let addonDebugger = yield initAddonDebugger(ADDON_URL); - gBrowser.selectedTab = gBrowser.addTab(); - let iframe = document.createElement("iframe"); - document.documentElement.appendChild(iframe); - - window.addEventListener("message", onMessage); + is(addonDebugger.title, "Debugger - Test add-on with JS Modules", "Saw the right toolbox title."); - let transport = DebuggerServer.connectPipe(); - gClient = new DebuggerClient(transport); - - let connected = promise.defer(); - gClient.connect(connected.resolve); - yield connected.promise; + // Check the inital list of sources is correct + let groups = yield addonDebugger.getSourceGroups(); + is(groups[0].name, "browser_dbg_addon4@tests.mozilla.org", "Add-on code should be the first group"); + is(groups.length, 1, "Should be only one group."); - yield installAddon(); - let debuggerPanel = yield initAddonDebugger(gClient, ADDON4_URL, iframe); - gDebugger = debuggerPanel.panelWin; - gThreadClient = gDebugger.gThreadClient; - gSources = gDebugger.DebuggerView.Sources; + let sources = groups[0].sources; + is(sources.length, 2, "Should be two sources"); + ok(sources[0].url.endsWith("/browser_dbg_addon4@tests.mozilla.org.xpi!/bootstrap.js"), "correct url for bootstrap code") + is(sources[0].label, "bootstrap.js", "correct label for bootstrap code") + is(sources[1].url, "resource://browser_dbg_addon4/test.jsm", "correct url for addon code") + is(sources[1].label, "test.jsm", "correct label for addon code") - yield testSources(false); - + // Load a new module and check it appears in the list of sources Cu.import("resource://browser_dbg_addon4/test2.jsm", {}); - yield testSources(true); + groups = yield addonDebugger.getSourceGroups(); + is(groups[0].name, "browser_dbg_addon4@tests.mozilla.org", "Add-on code should be the first group"); + is(groups.length, 1, "Should be only one group."); + + sources = groups[0].sources; + is(sources.length, 3, "Should be three sources"); + ok(sources[0].url.endsWith("/browser_dbg_addon4@tests.mozilla.org.xpi!/bootstrap.js"), "correct url for bootstrap code") + is(sources[0].label, "bootstrap.js", "correct label for bootstrap code") + is(sources[1].url, "resource://browser_dbg_addon4/test.jsm", "correct url for addon code") + is(sources[1].label, "test.jsm", "correct label for addon code") + is(sources[2].url, "resource://browser_dbg_addon4/test2.jsm", "correct url for addon code") + is(sources[2].label, "test2.jsm", "correct label for addon code") Cu.unload("resource://browser_dbg_addon4/test2.jsm"); - - yield uninstallAddon(); - yield closeConnection(); - yield debuggerPanel._toolbox.destroy(); - iframe.remove(); - window.removeEventListener("message", onMessage); + yield addonDebugger.destroy(); + yield removeAddon(addon); finish(); }); } - -function installAddon () { - return addAddon(ADDON4_URL).then(aAddon => { - gAddon = aAddon; - }); -} - -function testSources(expectSecondModule) { - let deferred = promise.defer(); - let foundAddonModule = false; - let foundAddonModule2 = false; - let foundAddonBootstrap = false; - - gThreadClient.getSources(({sources}) => { - ok(sources.length, "retrieved sources"); - - for (let source of sources) { - let url = source.url.split(" -> ").pop(); - let { label, group } = gSources.getItemByValue(source.url).attachment; - - if (url.indexOf("resource://browser_dbg_addon4/test.jsm") === 0) { - is(label, "test.jsm", "correct label for addon code"); - is(group, "browser_dbg_addon4@tests.mozilla.org", "addon module is in the add-on's group"); - foundAddonModule = true; - } else if (url.indexOf("resource://browser_dbg_addon4/test2.jsm") === 0) { - is(label, "test2.jsm", "correct label for addon code"); - is(group, "browser_dbg_addon4@tests.mozilla.org", "addon module is in the add-on's group"); - foundAddonModule2 = true; - } else if (url.endsWith("/browser_dbg_addon4@tests.mozilla.org.xpi!/bootstrap.js")) { - is(label, "bootstrap.js", "correct label for bootstrap code"); - is(group, "browser_dbg_addon4@tests.mozilla.org", "addon bootstrap script is in the add-on's group"); - foundAddonBootstrap = true; - } else { - ok(false, "Saw an unexpected source: " + url); - } - } - - ok(foundAddonModule, "found JS module for the addon in the list"); - is(foundAddonModule2, expectSecondModule, "saw the second addon module"); - ok(foundAddonBootstrap, "found bootstrap script for the addon in the list"); - - is(gTitle, "Debugger - Test add-on with JS Modules", "Saw the right toolbox title."); - - let groups = gDebugger.document.querySelectorAll(".side-menu-widget-group-title .name"); - is(groups[0].value, "browser_dbg_addon4@tests.mozilla.org", "Add-on code should be the first group"); - is(groups.length, 1, "Should be only one group."); - - deferred.resolve(); - }); - - return deferred.promise; -} - -function uninstallAddon() { - return removeAddon(gAddon); -} - -function closeConnection () { - let deferred = promise.defer(); - gClient.close(deferred.resolve); - return deferred.promise; -} - -registerCleanupFunction(function() { - gClient = null; - gAddon = null; - gThreadClient = null; - gDebugger = null; - gSources = null; - while (gBrowser.tabs.length > 1) { - gBrowser.removeCurrentTab(); - } -});
--- a/browser/devtools/debugger/test/browser_dbg_addon-panels.js +++ b/browser/devtools/debugger/test/browser_dbg_addon-panels.js @@ -1,95 +1,44 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ // Ensure that only panels that are relevant to the addon debugger // display in the toolbox -const ADDON3_URL = EXAMPLE_URL + "addon3.xpi"; +const ADDON_URL = EXAMPLE_URL + "addon3.xpi"; let gAddon, gClient, gThreadClient, gDebugger, gSources; let PREFS = [ "devtools.canvasdebugger.enabled", "devtools.shadereditor.enabled", "devtools.profiler.enabled", "devtools.netmonitor.enabled" ]; function test() { Task.spawn(function () { - if (!DebuggerServer.initialized) { - DebuggerServer.init(() => true); - DebuggerServer.addBrowserActors(); - } + let addon = yield addAddon(ADDON_URL); + let addonDebugger = yield initAddonDebugger(ADDON_URL); // Store and enable all optional dev tools panels let originalPrefs = PREFS.map(pref => { let original = Services.prefs.getBoolPref(pref); Services.prefs.setBoolPref(pref, true) return original; }); - gBrowser.selectedTab = gBrowser.addTab(); - let iframe = document.createElement("iframe"); - document.documentElement.appendChild(iframe); - - let transport = DebuggerServer.connectPipe(); - gClient = new DebuggerClient(transport); - - let connected = promise.defer(); - gClient.connect(connected.resolve); - yield connected.promise; + let tabs = addonDebugger.frame.contentDocument.getElementById("toolbox-tabs").children; + let expectedTabs = ["options", "jsdebugger"]; - yield installAddon(); - let debuggerPanel = yield initAddonDebugger(gClient, ADDON3_URL, iframe); - gDebugger = debuggerPanel.panelWin; - gThreadClient = gDebugger.gThreadClient; - gSources = gDebugger.DebuggerView.Sources; + is(tabs.length, 2, "displaying only 2 tabs in addon debugger"); + Array.forEach(tabs, (tab, i) => { + let toolName = expectedTabs[i]; + is(tab.getAttribute("toolid"), toolName, "displaying " + toolName); + }); - testPanels(iframe); - yield uninstallAddon(); - yield closeConnection(); - yield debuggerPanel._toolbox.destroy(); - iframe.remove(); + yield addonDebugger.destroy(); + yield removeAddon(addon); PREFS.forEach((pref, i) => Services.prefs.setBoolPref(pref, originalPrefs[i])); finish(); }); } - -function installAddon () { - return addAddon(ADDON3_URL).then(aAddon => { - gAddon = aAddon; - }); -} - -function testPanels(frame) { - let tabs = frame.contentDocument.getElementById("toolbox-tabs").children; - let expectedTabs = ["options", "jsdebugger"]; - - is(tabs.length, 2, "displaying only 2 tabs in addon debugger"); - Array.forEach(tabs, (tab, i) => { - let toolName = expectedTabs[i]; - is(tab.getAttribute("toolid"), toolName, "displaying " + toolName); - }); -} - -function uninstallAddon() { - return removeAddon(gAddon); -} - -function closeConnection () { - let deferred = promise.defer(); - gClient.close(deferred.resolve); - return deferred.promise; -} - -registerCleanupFunction(function() { - gClient = null; - gAddon = null; - gThreadClient = null; - gDebugger = null; - gSources = null; - while (gBrowser.tabs.length > 1) { - gBrowser.removeCurrentTab(); - } -});
--- a/browser/devtools/debugger/test/browser_dbg_addon-sources.js +++ b/browser/devtools/debugger/test/browser_dbg_addon-sources.js @@ -1,139 +1,35 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ // Ensure that the sources listed when debugging an addon are either from the // addon itself, or the SDK, with proper groups and labels. -const ADDON3_URL = EXAMPLE_URL + "addon3.xpi"; - -let gAddon, gClient, gThreadClient, gDebugger, gSources, gTitle; - -function onMessage(event) { - try { - let json = JSON.parse(event.data); - switch (json.name) { - case "toolbox-title": - gTitle = json.data.value; - break; - } - } catch(e) { - DevToolsUtils.reportException("onMessage", e); - } -} +const ADDON_URL = EXAMPLE_URL + "addon3.xpi"; function test() { Task.spawn(function () { - if (!DebuggerServer.initialized) { - DebuggerServer.init(() => true); - DebuggerServer.addBrowserActors(); - } + let addon = yield addAddon(ADDON_URL); + let addonDebugger = yield initAddonDebugger(ADDON_URL); + + is(addonDebugger.title, "Debugger - browser_dbg_addon3", "Saw the right toolbox title."); - gBrowser.selectedTab = gBrowser.addTab(); - let iframe = document.createElement("iframe"); - document.documentElement.appendChild(iframe); - - window.addEventListener("message", onMessage); - - let transport = DebuggerServer.connectPipe(); - gClient = new DebuggerClient(transport); + // Check the inital list of sources is correct + let groups = yield addonDebugger.getSourceGroups(); + is(groups[0].name, "jid1-ami3akps3baaeg@jetpack", "Add-on code should be the first group"); + is(groups[1].name, "Add-on SDK", "Add-on SDK should be the second group"); + is(groups.length, 2, "Should be only two groups."); - let connected = promise.defer(); - gClient.connect(connected.resolve); - yield connected.promise; + let sources = groups[0].sources; + is(sources.length, 2, "Should be two sources"); + ok(sources[0].url.endsWith("/jid1-ami3akps3baaeg@jetpack.xpi!/bootstrap.js"), "correct url for bootstrap code") + is(sources[0].label, "bootstrap.js", "correct label for bootstrap code") + is(sources[1].url, "resource://jid1-ami3akps3baaeg-at-jetpack/browser_dbg_addon3/lib/main.js", "correct url for add-on code") + is(sources[1].label, "resources/browser_dbg_addon3/lib/main.js", "correct label for add-on code") - yield installAddon(); - let debuggerPanel = yield initAddonDebugger(gClient, ADDON3_URL, iframe); - gDebugger = debuggerPanel.panelWin; - gThreadClient = gDebugger.gThreadClient; - gSources = gDebugger.DebuggerView.Sources; + ok(groups[1].sources.length > 10, "SDK modules are listed"); - yield testSources(); - yield uninstallAddon(); - yield closeConnection(); - yield debuggerPanel._toolbox.destroy(); - iframe.remove(); - window.removeEventListener("message", onMessage); + yield addonDebugger.destroy(); + yield removeAddon(addon); finish(); }); } - -function installAddon () { - return addAddon(ADDON3_URL).then(aAddon => { - gAddon = aAddon; - }); -} - -function testSources() { - let deferred = promise.defer(); - let foundAddonModule = false; - let foundSDKModule = 0; - let foundAddonBootstrap = false; - - gThreadClient.getSources(({sources}) => { - ok(sources.length, "retrieved sources"); - - for (let source of sources) { - let url = source.url.split(" -> ").pop(); - info(source.url + "\n\n\n" + url); - let { label, group } = gSources.getItemByValue(source.url).attachment; - - if (url.indexOf("resource://gre/modules/commonjs/") === 0) { - is(label, url.substring(32), "correct truncated label"); - is(group, "Add-on SDK", "correct SDK group"); - foundSDKModule++; - } else if (url.indexOf("resource://gre/modules/commonjs/method") === 0) { - is(label.indexOf("method/"), 0, "correct truncated label"); - is(group, "Add-on SDK", "correct SDK group"); - foundSDKModule++; - } else if (url.indexOf("resource://jid1-ami3akps3baaeg-at-jetpack") === 0) { - is(label, "resources/browser_dbg_addon3/lib/main.js", "correct label for addon code"); - is(group, "jid1-ami3akps3baaeg@jetpack", "addon code is in the add-on's group"); - foundAddonModule = true; - } else if (url.endsWith("/jid1-ami3akps3baaeg@jetpack.xpi!/bootstrap.js")) { - is(label, "bootstrap.js", "correct label for bootstrap script"); - is(group, "jid1-ami3akps3baaeg@jetpack", "addon code is in the add-on's group"); - foundAddonBootstrap = true; - } else { - ok(false, "Saw an unexpected source: " + url); - } - } - - ok(foundAddonModule, "found code for the addon in the list"); - ok(foundAddonBootstrap, "found bootstrap for the addon in the list"); - // Be flexible in this number, as SDK changes could change the exact number of - // built-in browser SDK modules - ok(foundSDKModule > 10, "SDK modules are listed"); - - is(gTitle, "Debugger - browser_dbg_addon3", "Saw the right toolbox title."); - - let groups = gDebugger.document.querySelectorAll(".side-menu-widget-group-title .name"); - is(groups[0].value, "jid1-ami3akps3baaeg@jetpack", "Add-on code should be the first group"); - is(groups[1].value, "Add-on SDK", "Add-on SDK should be the second group"); - is(groups.length, 2, "Should be only two groups."); - - deferred.resolve(); - }); - - return deferred.promise; -} - -function uninstallAddon() { - return removeAddon(gAddon); -} - -function closeConnection () { - let deferred = promise.defer(); - gClient.close(deferred.resolve); - return deferred.promise; -} - -registerCleanupFunction(function() { - gClient = null; - gAddon = null; - gThreadClient = null; - gDebugger = null; - gSources = null; - while (gBrowser.tabs.length > 1) { - gBrowser.removeCurrentTab(); - } -});
--- a/browser/devtools/debugger/test/head.js +++ b/browser/devtools/debugger/test/head.js @@ -466,16 +466,24 @@ function backspaceText(aElement, aTimes) function getTab(aTarget, aWindow) { if (aTarget instanceof XULElement) { return promise.resolve(aTarget); } else { return addTab(aTarget, aWindow); } } +function getSources(aClient) { + let deferred = promise.defer(); + + aClient.getSources(({sources}) => deferred.resolve(sources)); + + return deferred.promise; +} + function initDebugger(aTarget, aWindow) { info("Initializing a debugger panel."); return getTab(aTarget, aWindow).then(aTab => { info("Debugee tab added successfully: " + aTarget); let deferred = promise.defer(); let debuggee = aTab.linkedBrowser.contentWindow.wrappedJSObject; @@ -495,42 +503,146 @@ function initDebugger(aTarget, aWindow) deferred.resolve([aTab, debuggee, debuggerPanel, aWindow]); }); }); return deferred.promise; }); } -function initAddonDebugger(aClient, aUrl, aFrame) { - info("Initializing an addon debugger panel."); +// Creates an add-on debugger for a given add-on. The returned AddonDebugger +// object must be destroyed before finishing the test +function initAddonDebugger(aUrl) { + let addonDebugger = new AddonDebugger(); + return addonDebugger.init(aUrl).then(() => addonDebugger); +} + +function AddonDebugger() { + this._onMessage = this._onMessage.bind(this); +} + +AddonDebugger.prototype = { + init: Task.async(function*(aUrl) { + info("Initializing an addon debugger panel."); - return getAddonActorForUrl(aClient, aUrl).then((addonActor) => { + if (!DebuggerServer.initialized) { + DebuggerServer.init(() => true); + DebuggerServer.addBrowserActors(); + } + + this.frame = document.createElement("iframe"); + this.frame.setAttribute("height", 400); + document.documentElement.appendChild(this.frame); + window.addEventListener("message", this._onMessage); + + let transport = DebuggerServer.connectPipe(); + this.client = new DebuggerClient(transport); + + let connected = promise.defer(); + this.client.connect(connected.resolve); + yield connected.promise; + + let addonActor = yield getAddonActorForUrl(this.client, aUrl); + let targetOptions = { form: { addonActor: addonActor.actor, title: addonActor.name }, - client: aClient, + client: this.client, chrome: true }; let toolboxOptions = { - customIframe: aFrame + customIframe: this.frame }; let target = devtools.TargetFactory.forTab(targetOptions); - return gDevTools.showToolbox(target, "jsdebugger", devtools.Toolbox.HostType.CUSTOM, toolboxOptions); - }).then(aToolbox => { + let toolbox = yield gDevTools.showToolbox(target, "jsdebugger", devtools.Toolbox.HostType.CUSTOM, toolboxOptions); + info("Addon debugger panel shown successfully."); - let debuggerPanel = aToolbox.getCurrentPanel(); + this.debuggerPanel = toolbox.getCurrentPanel(); // Wait for the initial resume... - return waitForClientEvents(debuggerPanel, "resumed") - .then(() => prepareDebugger(debuggerPanel)) - .then(() => debuggerPanel); - }); + yield waitForClientEvents(this.debuggerPanel, "resumed"); + yield prepareDebugger(this.debuggerPanel); + }), + + destroy: Task.async(function*() { + let deferred = promise.defer(); + this.client.close(deferred.resolve); + yield deferred.promise; + yield this.debuggerPanel._toolbox.destroy(); + this.frame.remove(); + window.removeEventListener("message", this._onMessage); + }), + + /** + * Returns a list of the groups and sources in the UI. The returned array + * contains objects for each group with properties name and sources. The + * sources property contains an array with objects for each source for that + * group with properties label and url. + */ + getSourceGroups: Task.async(function*() { + let debuggerWin = this.debuggerPanel.panelWin; + let sources = yield getSources(debuggerWin.gThreadClient); + ok(sources.length, "retrieved sources"); + + // groups will be the return value, groupmap and the maps we put in it will + // be used as quick lookups to add the url information in below + let groups = []; + let groupmap = new Map(); + + let uigroups = this.debuggerPanel.panelWin.document.querySelectorAll(".side-menu-widget-group"); + for (let g of uigroups) { + let name = g.querySelector(".side-menu-widget-group-title .name").value; + let group = { + name: name, + sources: [] + }; + groups.push(group); + let labelmap = new Map(); + groupmap.set(name, labelmap); + + for (let l of g.querySelectorAll(".dbg-source-item")) { + let source = { + label: l.value, + url: null + }; + + labelmap.set(l.value, source); + group.sources.push(source); + } + } + + for (let source of sources) { + let { label, group } = debuggerWin.DebuggerView.Sources.getItemByValue(source.url).attachment; + + if (!groupmap.has(group)) { + ok(false, "Saw a source group not in the UI: " + group); + continue; + } + + if (!groupmap.get(group).has(label)) { + ok(false, "Saw a source label not in the UI: " + label); + continue; + } + + groupmap.get(group).get(label).url = source.url.split(" -> ").pop(); + } + + return groups; + }), + + _onMessage: function(event) { + let json = JSON.parse(event.data); + switch (json.name) { + case "toolbox-title": + this.title = json.data.value; + break; + } + } } function initChromeDebugger(aOnClose) { info("Initializing a chrome debugger process."); let deferred = promise.defer(); // Wait for the toolbox process to start...
--- a/browser/devtools/jar.mn +++ b/browser/devtools/jar.mn @@ -40,16 +40,17 @@ browser.jar: content/browser/devtools/codemirror/matchbrackets.js (sourceeditor/codemirror/matchbrackets.js) content/browser/devtools/codemirror/closebrackets.js (sourceeditor/codemirror/closebrackets.js) content/browser/devtools/codemirror/comment.js (sourceeditor/codemirror/comment.js) content/browser/devtools/codemirror/searchcursor.js (sourceeditor/codemirror/search/searchcursor.js) content/browser/devtools/codemirror/search.js (sourceeditor/codemirror/search/search.js) content/browser/devtools/codemirror/dialog.js (sourceeditor/codemirror/dialog/dialog.js) content/browser/devtools/codemirror/dialog.css (sourceeditor/codemirror/dialog/dialog.css) content/browser/devtools/codemirror/emacs.js (sourceeditor/codemirror/keymap/emacs.js) + content/browser/devtools/codemirror/sublime.js (sourceeditor/codemirror/keymap/sublime.js) content/browser/devtools/codemirror/vim.js (sourceeditor/codemirror/keymap/vim.js) content/browser/devtools/codemirror/foldcode.js (sourceeditor/codemirror/fold/foldcode.js) content/browser/devtools/codemirror/brace-fold.js (sourceeditor/codemirror/fold/brace-fold.js) content/browser/devtools/codemirror/comment-fold.js (sourceeditor/codemirror/fold/comment-fold.js) content/browser/devtools/codemirror/xml-fold.js (sourceeditor/codemirror/fold/xml-fold.js) content/browser/devtools/codemirror/foldgutter.js (sourceeditor/codemirror/fold/foldgutter.js) content/browser/devtools/codemirror/mozilla.css (sourceeditor/codemirror/mozilla.css) content/browser/devtools/debugger.xul (debugger/debugger.xul)
--- a/browser/devtools/sourceeditor/autocomplete.js +++ b/browser/devtools/sourceeditor/autocomplete.js @@ -98,20 +98,22 @@ function autoComplete({ ed, cm }) { popup.hidePopup(); ed.emit("after-suggest"); return; } // The cursor is at the end of the currently entered part of the token, like // "backgr|" but we need to open the popup at the beginning of the character // "b". Thus we need to calculate the width of the entered part of the token // ("backgr" here). 4 comes from the popup's left padding. + + let cursorElement = cm.display.cursorDiv.querySelector(".CodeMirror-cursor"); let left = suggestions[0].preLabel.length * cm.defaultCharWidth() + 4; popup.hidePopup(); popup.setItems(suggestions); - popup.openPopup(cm.display.cursor, -1 * left, 0); + popup.openPopup(cursorElement, -1 * left, 0); private.suggestionInsertedOnce = false; // This event is used in tests. ed.emit("after-suggest"); }); } /** * Cycles through provided suggestions by the popup in a top to bottom manner @@ -154,16 +156,24 @@ function cycleSuggestions(ed, reverse) { } /** * onkeydown handler for the editor instance to prevent autocompleting on some * keypresses. */ function onEditorKeypress({ ed, Editor }, event) { let private = privates.get(ed); + + // Do not try to autocomplete with multiple selections. + if (ed.hasMultipleSelections()) { + private.doNotAutocomplete = true; + private.popup.hidePopup(); + return; + } + switch (event.keyCode) { case event.DOM_VK_ESCAPE: if (private.popup.isOpen) event.preventDefault(); case event.DOM_VK_LEFT: case event.DOM_VK_RIGHT: case event.DOM_VK_HOME: case event.DOM_VK_END:
--- a/browser/devtools/sourceeditor/codemirror/README +++ b/browser/devtools/sourceeditor/codemirror/README @@ -1,16 +1,16 @@ This is the CodeMirror editor packaged for the Mozilla Project. CodeMirror is a JavaScript component that provides a code editor in the browser. When a mode is available for the language you are coding in, it will color your code, and optionally help with indentation. # Upgrade -Currently used version is 3.20. To upgrade, download a new version of +Currently used version is 4.0.3. To upgrade, download a new version of CodeMirror from the project's page [1] and replace all JavaScript and CSS files inside the codemirror directory [2]. To confirm the functionality run mochitests for the following components: * sourceeditor * scratchpad * debugger @@ -46,16 +46,17 @@ in the LICENSE file: * codemirror.css * codemirror.js * comment.js * activeline.js * dialog/dialog.css * dialog/dialog.js * keymap/emacs.js + * keymap/sublime.js * keymap/vim.js * fold/foldcode.js * fold/brace-fold.js * fold/comment-fold.js * fold/xml-fold.js * fold/foldgutter.js * xml.js * css.js @@ -65,55 +66,67 @@ in the LICENSE file: * matchbrackets.js * closebrackets.js * trailingspace.js * search/match-highlighter.js * search/search.js * search/searchcursor.js * test/codemirror.html * test/cm_comment_test.js + * test/cm_doc_test.js * test/cm_driver.js * test/cm_mode_javascript_test.js * test/cm_mode_test.css * test/cm_mode_test.js + * test/cm_multi_test.js + * test/cm_search_test.js + * test/cm_test.js + * test/cm_sublime_test.js * test/cm_vim_test.js * test/cm_emacs_test.js - * test/cm_test.js # Localization patches -diff --git a/browser/devtools/sourceeditor/codemirror/search/search.js b/browser/devtools/sourceeditor/codemirror/sea -index 049f72f..df4d95e 100644 +diff --git a/browser/devtools/sourceeditor/codemirror/search/search.js b/browser/devtools/sourceeditor/codemirror/search/search.js --- a/browser/devtools/sourceeditor/codemirror/search/search.js +++ b/browser/devtools/sourceeditor/codemirror/search/search.js -@@ -58,9 +58,22 @@ - var isRE = query.match(/^\/(.*)\/([a-z]*)$/); - return isRE ? new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i") : query; +@@ -62,19 +62,31 @@ + if (isRE) { + query = new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i"); + if (query.test("")) query = /x^/; + } else if (query == "") { + query = /x^/; + } + return query; } - var queryDialog = -- 'Search: <input type="text" style="width: 10em"/> <span style="color: #888">(Use /re/ syntax for regexp search)< +- 'Search: <input type="text" style="width: 10em"/> <span style="color: #888">(Use /re/ syntax for regexp search)</span>'; + var queryDialog; function doSearch(cm, rev) { + if (!queryDialog) { + let doc = cm.getWrapperElement().ownerDocument; + let inp = doc.createElement("input"); + let txt = doc.createTextNode(cm.l10n("findCmd.promptMessage")); + + inp.type = "text"; + inp.style.width = "10em"; + inp.style.MozMarginStart = "1em"; + + queryDialog = doc.createElement("div"); + queryDialog.appendChild(txt); + queryDialog.appendChild(inp); + } -+ 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, queryCaseInsensitive(state.query)); # Footnotes [1] http://codemirror.net [2] browser/devtools/sourceeditor/codemirror [3] browser/devtools/sourceeditor/test/browser_codemirror.js [4] browser/devtools/jar.mn [5] browser/devtools/sourceeditor/editor.js
--- a/browser/devtools/sourceeditor/codemirror/activeline.js +++ b/browser/devtools/sourceeditor/codemirror/activeline.js @@ -1,45 +1,66 @@ // Because sometimes you need to style the cursor's line. // // Adds an option 'styleActiveLine' which, when enabled, gives the // active line's wrapping <div> the CSS class "CodeMirror-activeline", // and gives its background <div> the class "CodeMirror-activeline-background". -(function() { +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { "use strict"; var WRAP_CLASS = "CodeMirror-activeline"; var BACK_CLASS = "CodeMirror-activeline-background"; CodeMirror.defineOption("styleActiveLine", false, function(cm, val, old) { var prev = old && old != CodeMirror.Init; if (val && !prev) { - updateActiveLine(cm, cm.getCursor().line); + cm.state.activeLines = []; + updateActiveLines(cm, cm.listSelections()); cm.on("beforeSelectionChange", selectionChange); } else if (!val && prev) { cm.off("beforeSelectionChange", selectionChange); - clearActiveLine(cm); - delete cm.state.activeLine; + clearActiveLines(cm); + delete cm.state.activeLines; } }); - function clearActiveLine(cm) { - if ("activeLine" in cm.state) { - cm.removeLineClass(cm.state.activeLine, "wrap", WRAP_CLASS); - cm.removeLineClass(cm.state.activeLine, "background", BACK_CLASS); + function clearActiveLines(cm) { + for (var i = 0; i < cm.state.activeLines.length; i++) { + cm.removeLineClass(cm.state.activeLines[i], "wrap", WRAP_CLASS); + cm.removeLineClass(cm.state.activeLines[i], "background", BACK_CLASS); } } - function updateActiveLine(cm, selectedLine) { - var line = cm.getLineHandleVisualStart(selectedLine); - if (cm.state.activeLine == line) return; + function sameArray(a, b) { + if (a.length != b.length) return false; + for (var i = 0; i < a.length; i++) + if (a[i] != b[i]) return false; + return true; + } + + function updateActiveLines(cm, ranges) { + var active = []; + for (var i = 0; i < ranges.length; i++) { + var line = cm.getLineHandleVisualStart(ranges[i].head.line); + if (active[active.length - 1] != line) active.push(line); + } + if (sameArray(cm.state.activeLines, active)) return; cm.operation(function() { - clearActiveLine(cm); - cm.addLineClass(line, "wrap", WRAP_CLASS); - cm.addLineClass(line, "background", BACK_CLASS); - cm.state.activeLine = line; + clearActiveLines(cm); + for (var i = 0; i < active.length; i++) { + cm.addLineClass(active[i], "wrap", WRAP_CLASS); + cm.addLineClass(active[i], "background", BACK_CLASS); + } + cm.state.activeLines = active; }); } function selectionChange(cm, sel) { - updateActiveLine(cm, sel.head.line); + updateActiveLines(cm, sel.ranges); } -})(); +});
--- a/browser/devtools/sourceeditor/codemirror/clike.js +++ b/browser/devtools/sourceeditor/codemirror/clike.js @@ -1,8 +1,18 @@ +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + CodeMirror.defineMode("clike", function(config, parserConfig) { var indentUnit = config.indentUnit, statementIndentUnit = parserConfig.statementIndentUnit || indentUnit, dontAlignCalls = parserConfig.dontAlignCalls, keywords = parserConfig.keywords || {}, builtin = parserConfig.builtin || {}, blockKeywords = parserConfig.blockKeywords || {}, atoms = parserConfig.atoms || {}, @@ -186,28 +196,65 @@ CodeMirror.defineMode("clike", function( stream.skipToEnd(); state.tokenize = null; break; } } return "meta"; } + function cpp11StringHook(stream, state) { + stream.backUp(1); + // Raw strings. + if (stream.match(/(R|u8R|uR|UR|LR)/)) { + var match = stream.match(/"(.{0,16})\(/); + if (!match) { + return false; + } + state.cpp11RawStringDelim = match[1]; + state.tokenize = tokenRawString; + return tokenRawString(stream, state); + } + // Unicode strings/chars. + if (stream.match(/(u8|u|U|L)/)) { + if (stream.match(/["']/, /* eat */ false)) { + return "string"; + } + return false; + } + // Ignore this hook. + stream.next(); + return false; + } + // C#-style strings where "" escapes a quote. function tokenAtString(stream, state) { var next; while ((next = stream.next()) != null) { if (next == '"' && !stream.eat('"')) { state.tokenize = null; break; } } return "string"; } + // C++11 raw string literal is <prefix>"<delim>( anything )<delim>", where + // <delim> can be a string up to 16 characters long. + function tokenRawString(stream, state) { + var closingSequence = new RegExp(".*?\\)" + state.cpp11RawStringDelim + '"'); + var match = stream.match(closingSequence); + if (match) { + state.tokenize = null; + } else { + stream.skipToEnd(); + } + return "string"; + } + function def(mimes, mode) { var words = []; function add(obj) { if (obj) for (var prop in obj) if (obj.hasOwnProperty(prop)) words.push(prop); } add(mode.keywords); add(mode.builtin); @@ -230,20 +277,27 @@ CodeMirror.defineMode("clike", function( modeProps: {fold: ["brace", "include"]} }); def(["text/x-c++src", "text/x-c++hdr"], { name: "clike", keywords: words(cKeywords + " asm dynamic_cast namespace reinterpret_cast try bool explicit new " + "static_cast typeid catch operator template typename class friend private " + "this using const_cast inline public throw virtual delete mutable protected " + - "wchar_t"), + "wchar_t alignas alignof constexpr decltype nullptr noexcept thread_local final " + + "static_assert override"), blockKeywords: words("catch class do else finally for if struct switch try while"), atoms: words("true false null"), - hooks: {"#": cppHook}, + hooks: { + "#": cppHook, + "u": cpp11StringHook, + "U": cpp11StringHook, + "L": cpp11StringHook, + "R": cpp11StringHook + }, modeProps: {fold: ["brace", "include"]} }); CodeMirror.defineMIME("text/x-java", { name: "clike", keywords: words("abstract assert boolean break byte case catch char class const continue default " + "do double else enum extends final finally float for goto if implements import " + "instanceof int interface long native new package private protected public " + "return short static strictfp super switch synchronized this throw throws transient " + @@ -374,8 +428,10 @@ CodeMirror.defineMode("clike", function( "gl_MaxVertexAttribs gl_MaxVertexUniformComponents gl_MaxVaryingFloats " + "gl_MaxVertexTextureImageUnits gl_MaxTextureImageUnits " + "gl_MaxFragmentUniformComponents gl_MaxCombineTextureImageUnits " + "gl_MaxDrawBuffers"), hooks: {"#": cppHook}, modeProps: {fold: ["brace", "include"]} }); }()); + +});
--- a/browser/devtools/sourceeditor/codemirror/closebrackets.js +++ b/browser/devtools/sourceeditor/codemirror/closebrackets.js @@ -1,9 +1,16 @@ -(function() { +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { var DEFAULT_BRACKETS = "()[]{}''\"\""; var DEFAULT_EXPLODE_ON_ENTER = "[]{}"; var SPACE_CHAR_REGEX = /\s/; CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) { if (old != CodeMirror.Init && old) cm.removeKeyMap("autoCloseBrackets"); if (!val) return; @@ -23,62 +30,94 @@ CodeMirror.Pos(pos.line, pos.ch + 1)); return str.length == 2 ? str : null; } function buildKeymap(pairs) { var map = { name : "autoCloseBrackets", Backspace: function(cm) { - if (cm.somethingSelected() || cm.getOption("disableInput")) return CodeMirror.Pass; - var cur = cm.getCursor(), around = charsAround(cm, cur); - if (around && pairs.indexOf(around) % 2 == 0) + if (cm.getOption("disableInput")) return CodeMirror.Pass; + var ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + if (!ranges[i].empty()) return CodeMirror.Pass; + var around = charsAround(cm, ranges[i].head); + if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass; + } + for (var i = ranges.length - 1; i >= 0; i--) { + var cur = ranges[i].head; cm.replaceRange("", CodeMirror.Pos(cur.line, cur.ch - 1), CodeMirror.Pos(cur.line, cur.ch + 1)); - else - return CodeMirror.Pass; + } } }; var closingBrackets = ""; for (var i = 0; i < pairs.length; i += 2) (function(left, right) { if (left != right) closingBrackets += right; - function surround(cm) { - var selection = cm.getSelection(); - cm.replaceSelection(left + selection + right); - } - function maybeOverwrite(cm) { - var cur = cm.getCursor(), ahead = cm.getRange(cur, CodeMirror.Pos(cur.line, cur.ch + 1)); - if (ahead != right || cm.somethingSelected()) return CodeMirror.Pass; - else cm.execCommand("goCharRight"); - } map["'" + left + "'"] = function(cm) { - if (left == "'" && cm.getTokenAt(cm.getCursor()).type == "comment" || - cm.getOption("disableInput")) - return CodeMirror.Pass; - if (cm.somethingSelected()) return surround(cm); - if (left == right && maybeOverwrite(cm) != CodeMirror.Pass) return; - var cur = cm.getCursor(), ahead = CodeMirror.Pos(cur.line, cur.ch + 1); - var line = cm.getLine(cur.line), nextChar = line.charAt(cur.ch), curChar = cur.ch > 0 ? line.charAt(cur.ch - 1) : ""; - if (left == right && CodeMirror.isWordChar(curChar)) - return CodeMirror.Pass; - if (line.length == cur.ch || closingBrackets.indexOf(nextChar) >= 0 || SPACE_CHAR_REGEX.test(nextChar)) - cm.replaceSelection(left + right, {head: ahead, anchor: ahead}); - else - return CodeMirror.Pass; + if (cm.getOption("disableInput")) return CodeMirror.Pass; + var ranges = cm.listSelections(), type, next; + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i], cur = range.head, curType; + if (left == "'" && cm.getTokenTypeAt(cur) == "comment") + return CodeMirror.Pass; + var next = cm.getRange(cur, CodeMirror.Pos(cur.line, cur.ch + 1)); + if (!range.empty()) + curType = "surround"; + else if (left == right && next == right) + curType = "skip"; + else if (left == right && CodeMirror.isWordChar(next)) + return CodeMirror.Pass; + else if (cm.getLine(cur.line).length == cur.ch || closingBrackets.indexOf(next) >= 0 || SPACE_CHAR_REGEX.test(next)) + curType = "both"; + else + return CodeMirror.Pass; + if (!type) type = curType; + else if (type != curType) return CodeMirror.Pass; + } + + if (type == "skip") { + cm.execCommand("goCharRight"); + } else if (type == "surround") { + var sels = cm.getSelections(); + for (var i = 0; i < sels.length; i++) + sels[i] = left + sels[i] + right; + cm.replaceSelections(sels, "around"); + } else if (type == "both") { + cm.replaceSelection(left + right, null); + cm.execCommand("goCharLeft"); + } }; - if (left != right) map["'" + right + "'"] = maybeOverwrite; + if (left != right) map["'" + right + "'"] = function(cm) { + var ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + if (!range.empty() || + cm.getRange(range.head, CodeMirror.Pos(range.head.line, range.head.ch + 1)) != right) + return CodeMirror.Pass; + } + cm.execCommand("goCharRight"); + }; })(pairs.charAt(i), pairs.charAt(i + 1)); return map; } function buildExplodeHandler(pairs) { return function(cm) { - var cur = cm.getCursor(), around = charsAround(cm, cur); - if (!around || pairs.indexOf(around) % 2 != 0 || cm.getOption("disableInput")) - return CodeMirror.Pass; + if (cm.getOption("disableInput")) return CodeMirror.Pass; + var ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + if (!ranges[i].empty()) return CodeMirror.Pass; + var around = charsAround(cm, ranges[i].head); + if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass; + } cm.operation(function() { - var newPos = CodeMirror.Pos(cur.line + 1, 0); - cm.replaceSelection("\n\n", {anchor: newPos, head: newPos}, "+input"); - cm.indentLine(cur.line + 1, null, true); - cm.indentLine(cur.line + 2, null, true); + cm.replaceSelection("\n\n", null); + cm.execCommand("goCharLeft"); + ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + var line = ranges[i].head.line; + cm.indentLine(line, null, true); + cm.indentLine(line + 1, null, true); + } }); }; } -})(); +});
--- a/browser/devtools/sourceeditor/codemirror/codemirror.css +++ b/browser/devtools/sourceeditor/codemirror/codemirror.css @@ -31,42 +31,47 @@ white-space: nowrap; } .CodeMirror-linenumbers {} .CodeMirror-linenumber { padding: 0 3px 0 5px; min-width: 20px; text-align: right; color: #999; + -moz-box-sizing: content-box; + box-sizing: content-box; } /* 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 {} +div.CodeMirror-overwrite div.CodeMirror-cursor {} .cm-tab { display: inline-block; } +.CodeMirror-ruler { + border-left: 1px solid #ccc; + position: absolute; +} + /* DEFAULT THEME */ -.cm-s-default .cm-keyword {color: #708;} +.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;} @@ -109,24 +114,28 @@ div.CodeMirror span.CodeMirror-nonmatchi 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; + padding-bottom: 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; + border-right: 30px solid transparent; + -moz-box-sizing: content-box; + box-sizing: content-box; } /* 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; @@ -152,16 +161,17 @@ div.CodeMirror span.CodeMirror-nonmatchi .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; } @@ -190,26 +200,17 @@ div.CodeMirror span.CodeMirror-nonmatchi 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; @@ -229,33 +230,41 @@ div.CodeMirror span.CodeMirror-nonmatchi 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 { + +div.CodeMirror-cursors { + visibility: hidden; + position: relative; + z-index: 1; +} +.CodeMirror-focused div.CodeMirror-cursors { 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; } +/* Used to force a border model for a node */ +.cm-force-border { padding-right: .1px; } + @media print { /* Hide the cursor when printing */ - .CodeMirror div.CodeMirror-cursor { + .CodeMirror div.CodeMirror-cursors { visibility: hidden; } }
--- a/browser/devtools/sourceeditor/codemirror/codemirror.js +++ b/browser/devtools/sourceeditor/codemirror/codemirror.js @@ -1,201 +1,242 @@ -// CodeMirror version 3.21 +// This is CodeMirror (http://codemirror.net), a code editor +// implemented in JavaScript on top of the browser's DOM. // -// CodeMirror is the only global var we claim -window.CodeMirror = (function() { +// You can find some technical background for some of the code below +// at http://marijnhaverbeke.nl/blog/#cm-internals . + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + module.exports = mod(); + else if (typeof define == "function" && define.amd) // AMD + return define([], mod); + else // Plain browser env + this.CodeMirror = mod(); +})(function() { "use strict"; // BROWSER SNIFFING - // Crude, but necessary to handle a number of hard-to-feature-detect - // bugs and behavior differences. + // Kludges for bugs and behavior differences that can't be feature + // detected are enabled based on userAgent etc sniffing. + 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; + // ie_uptoN means Internet Explorer version N or lower + var ie_upto10 = /MSIE \d/.test(navigator.userAgent); + var ie_upto7 = ie_upto10 && (document.documentMode == null || document.documentMode < 8); + var ie_upto8 = ie_upto10 && (document.documentMode == null || document.documentMode < 9); + var ie_upto9 = ie_upto10 && (document.documentMode == null || document.documentMode < 10); + var ie_11up = /Trident\/([7-9]|\d{2,})\./.test(navigator.userAgent); + var ie = ie_upto10 || ie_11up; 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 presto = /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; } + var presto_version = presto && navigator.userAgent.match(/Version\/(\d*\.\d*)/); + if (presto_version) presto_version = Number(presto_version[1]); + if (presto_version && presto_version >= 15) { presto = 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 flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11)); + var captureRightClick = gecko || (ie && !ie_upto8); + + // Optimize some code when these features are not used. var sawReadOnlySpans = false, sawCollapsedSpans = false; - // CONSTRUCTOR + // EDITOR CONSTRUCTOR + + // A CodeMirror instance represents an editor. This is the object + // that user code is usually dealing with. 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)) + for (var opt in defaults) if (!options.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); + var doc = options.value; + if (typeof doc == "string") doc = new Doc(doc, options.mode); + this.doc = doc; + + var display = this.display = new Display(place, doc); 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); + if (options.autofocus && !mobile) focusInput(this); + + this.state = { + keyMaps: [], // stores maps added by addKeyMap + overlays: [], // highlighting overlays, as added by addOverlay + modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info + overwrite: false, focused: false, + suppressEdits: false, // used to disable editing during key handlers when in readOnly mode + pasteIncoming: false, cutIncoming: false, // help recognize paste/cut edits in readInput + draggingText: false, + highlight: new Delayed() // stores highlight worker timeout + }; // Override magic textarea content restore that IE sometimes does // on our hidden textarea on reload - if (old_ie) setTimeout(bind(resetInput, this, true), 20); + if (ie_upto10) 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); - })(); + + var cm = this; + runInOp(this, function() { + cm.curOp.forceUpdate = true; + attachDoc(cm, doc); + + if ((options.autofocus && !mobile) || activeElt() == display.input) + setTimeout(bind(onFocus, cm), 20); + else + onBlur(cm); + + for (var opt in optionHandlers) if (optionHandlers.hasOwnProperty(opt)) + optionHandlers[opt](cm, options[opt], Init); + for (var i = 0; i < initHooks.length; ++i) initHooks[i](cm); + }); } // 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;"); + // The display handles the DOM integration, both for input reading + // and content drawing. It holds references to DOM nodes and + // display-related state. + + function Display(place, doc) { + var d = this; + + // The semihidden textarea that is focused when the editor is + // focused, and receives input. + var input = d.input = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none"); + // The textarea is kept positioned near the cursor to prevent the + // fact that it'll be scrolled into view on input from scrolling + // our fake cursor out of view. On webkit, when wrap=off, paste is + // very slow. So make the area wide instead. if (webkit) input.style.width = "1000px"; else input.setAttribute("wrap", "off"); - // if border: 0; -- iOS fails to open keyboard (issue #1287) + // 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"); + // The fake scrollbar elements. + d.scrollbarH = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar"); + d.scrollbarV = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar"); + // Covers bottom-right square when both scrollbars are present. d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler"); + // Covers bottom of gutter when coverGutterNextToScrollbar is on + // and h scrollbar is present. d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler"); - // DIVs containing the selection and the actual code + // Will contain the actual code, positioned to cover the viewport. d.lineDiv = elt("div", null, "CodeMirror-code"); + // Elements are added to these to represent selection and cursors. 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.cursorDiv = elt("div", null, "CodeMirror-cursors"); + // A visibility: hidden element used to find the size of things. d.measure = elt("div", null, "CodeMirror-measure"); + // When lines outside of the viewport are measured, they are drawn in this. + d.lineMeasure = 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.lineSpace = elt("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv], + 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 + // Set to the height of the document, allowing 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 + // Behavior of elts with overflow: auto and padding is + // inconsistent across browsers. This is used to ensure the + // scrollable area is big enough. d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerCutOff + "px; width: 1px;"); - // Will contain the gutters, if any + // Will contain the gutters, if any. d.gutters = elt("div", null, "CodeMirror-gutters"); d.lineGutter = null; - // Provides scrolling + // Actual scrollable element. 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); - + + // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported) + if (ie_upto7) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } // 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; + if (ie_upto7) d.scrollbarH.style.minHeight = d.scrollbarV.style.minWidth = "18px"; + + if (place.appendChild) place.appendChild(d.wrapper); + else place(d.wrapper); + + // Current rendered range (may be bigger than the view window). + d.viewFrom = d.viewTo = doc.first; + // Information about the rendered lines. + d.view = []; + // Holds info about a single rendered line when it was rendered + // for measurement, while not in view. + d.externalMeasured = null; + // Empty space (in pixels) above the view + d.viewOffset = 0; + d.lastSizeC = 0; + d.updateLineNumbers = null; // 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. + // Set to true when a non-horizontal-scrolling line widget is + // added. As an optimization, line widget aligning is skipped when + // this 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. + // Flag that indicates whether we expect input to appear real soon + // now (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; + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; // Tracks when resetInput has punted to just putting a short - // string instead of the (large) selection. + // string into the textarea instead of the full 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; + // True when shift is held down. + d.shift = false; } // 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); @@ -214,34 +255,42 @@ window.CodeMirror = (function() { } 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); + findMaxLine(cm); } estimateLineHeights(cm); regChange(cm); clearCaches(cm); setTimeout(function(){updateScrollbars(cm);}, 100); } + // Returns a function that estimates the height of a line, to use as + // first approximation until the line becomes visible (and is thus + // properly measurable). 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; + if (lineIsHidden(cm.doc, line)) return 0; + + var widgetsHeight = 0; + if (line.widgets) for (var i = 0; i < line.widgets.length; i++) { + if (line.widgets[i].height) widgetsHeight += line.widgets[i].height; + } + + if (wrapping) + return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th; else - return th; + return widgetsHeight + 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); @@ -261,55 +310,64 @@ window.CodeMirror = (function() { } function guttersChanged(cm) { updateGutters(cm); regChange(cm); setTimeout(function(){alignHorizontally(cm);}, 20); } + // Rebuild the gutter elements, ensure the margin to the left of the + // code matches their width. 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) { + var width = gutters.offsetWidth; + cm.display.sizer.style.marginLeft = width + "px"; + if (i) cm.display.scrollbarH.style.left = cm.options.fixedGutter ? width + "px" : 0; + } + + // Compute the character length of a line, taking into account + // collapsed ranges (see markText) that might hide parts, and join + // other lines onto it. + function lineLength(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); + var found = merged.find(0, true); + cur = found.from.line; len += found.from.ch - found.to.ch; } cur = line; while (merged = collapsedSpanAtEnd(cur)) { - var found = merged.find(); + var found = merged.find(0, true); len -= cur.text.length - found.from.ch; - cur = getLine(doc, found.to.line); + cur = found.to.line; len += cur.text.length - found.to.ch; } return len; } - function computeMaxLength(cm) { + // Find the longest line in the document. + function findMaxLine(cm) { var d = cm.display, doc = cm.doc; d.maxLine = getLine(doc, doc.first); - d.maxLineLength = lineLength(doc, d.maxLine); + d.maxLineLength = lineLength(d.maxLine); d.maxLineChanged = true; doc.iter(function(line) { - var len = lineLength(doc, line); + var len = lineLength(line); if (len > d.maxLineLength) { d.maxLineLength = len; d.maxLine = line; } }); } // Make sure the gutters options contains the element @@ -321,516 +379,911 @@ window.CodeMirror = (function() { } else if (found > -1 && !options.lineNumbers) { options.gutters = options.gutters.slice(0); options.gutters.splice(found, 1); } } // SCROLLBARS + // Prepare DOM reads needed to update the scrollbars. Done in one + // shot to minimize update/measure roundtrips. + function measureForScrollbars(cm) { + var scroll = cm.display.scroller; + return { + clientHeight: scroll.clientHeight, + barHeight: cm.display.scrollbarV.clientHeight, + scrollWidth: scroll.scrollWidth, clientWidth: scroll.clientWidth, + barWidth: cm.display.scrollbarH.clientWidth, + docHeight: Math.round(cm.doc.height + paddingVert(cm.display)) + }; + } + // 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); + // content. + function updateScrollbars(cm, measure) { + if (!measure) measure = measureForScrollbars(cm); + var d = cm.display; + var scrollHeight = measure.docHeight + scrollerCutOff; + var needsH = measure.scrollWidth > measure.clientWidth; + var needsV = scrollHeight > measure.clientHeight; if (needsV) { d.scrollbarV.style.display = "block"; d.scrollbarV.style.bottom = needsH ? scrollbarWidth(d.measure) + "px" : "0"; + // A bug in IE8 can cause this value to be negative, so guard it. d.scrollbarV.firstChild.style.height = - (scrollHeight - d.scroller.clientHeight + d.scrollbarV.clientHeight) + "px"; + Math.max(0, scrollHeight - measure.clientHeight + (measure.barHeight || 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"; + (measure.scrollWidth - measure.clientWidth + (measure.barWidth || 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"; - } - } - + var barMouseDown = function(e) { + if (e_target(e) != d.scrollbarV && e_target(e) != d.scrollbarH) + operation(cm, onMouseDown)(e); + }; + on(d.scrollbarV, "mousedown", barMouseDown); + on(d.scrollbarH, "mousedown", barMouseDown); + } + } + + // Compute the lines that are visible in a given viewport (defaults + // the the current scroll position). viewPort may contain top, + // height, and ensure (see op.scrollToPos) properties. 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;} + var top = viewPort && viewPort.top != null ? viewPort.top : display.scroller.scrollTop; top = Math.floor(top - paddingTop(display)); - var bottom = Math.ceil(top + height); - return {from: lineAtHeight(doc, top), to: lineAtHeight(doc, bottom)}; + var bottom = viewPort && viewPort.bottom != null ? viewPort.bottom : top + display.wrapper.clientHeight; + + var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom); + // Ensure is a {from: {line, ch}, to: {line, ch}} object, and + // forces those lines into the viewport (if possible). + if (viewPort && viewPort.ensure) { + var ensureFrom = viewPort.ensure.from.line, ensureTo = viewPort.ensure.to.line; + if (ensureFrom < from) + return {from: ensureFrom, + to: lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight)}; + if (Math.min(ensureTo, doc.lastLine()) >= to) + return {from: lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight), + to: ensureTo}; + } + return {from: from, to: to}; } // LINE NUMBERS + // Re-align line numbers and gutter marks to compensate for + // horizontal scrolling. function alignHorizontally(cm) { - var display = cm.display; + var display = cm.display, view = display.view; 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; + var gutterW = display.gutters.offsetWidth, left = comp + "px"; + for (var i = 0; i < view.length; i++) if (!view[i].hidden) { + if (cm.options.fixedGutter && view[i].gutter) + view[i].gutter.style.left = left; + var align = view[i].alignable; + if (align) for (var j = 0; j < align.length; j++) + align[j].style.left = left; } if (cm.options.fixedGutter) display.gutters.style.left = (comp + gutterW) + "px"; } + // Used to ensure that the line number gutter is still the right + // size for the current document size. Returns true when an update + // is needed. 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"; + var width = display.gutters.offsetWidth; + display.scrollbarH.style.left = cm.options.fixedGutter ? width + "px" : 0; + display.sizer.style.marginLeft = width + "px"; return true; } return false; } function lineNumberFor(options, i) { return String(options.lineNumberFormatter(i + options.firstLineNumber)); } + + // Computes display.scroller.scrollLeft + display.gutters.offsetWidth, + // but using getBoundingClientRect to get a sub-pixel-accurate + // result. function compensateForHScroll(display) { - return getRect(display.scroller).left - getRect(display.sizer).left; + return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left; } // DISPLAY DRAWING - function updateDisplay(cm, changes, viewPort, forced) { - var oldFrom = cm.display.showingFrom, oldTo = cm.display.showingTo, updated; + // Updates the display, selection, and scrollbars, using the + // information in display.view to find out which nodes are no longer + // up-to-date. Tries to bail out early when no changes are needed, + // unless forced is true. + // Returns true if an actual update happened, false otherwise. + function updateDisplay(cm, viewPort, forced) { + var oldFrom = cm.display.viewFrom, oldTo = cm.display.viewTo, 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; + if (!updateDisplayInner(cm, visible, forced)) break; updated = true; - changes = []; + + // If the max line changed since it was last measured, measure it, + // and ensure the document's width matches it. + if (cm.display.maxLineChanged && !cm.options.lineWrapping) + adjustContentWidth(cm); + + var barMeasure = measureForScrollbars(cm); updateSelection(cm); - updateScrollbars(cm); + setDocumentHeight(cm, barMeasure); + updateScrollbars(cm, barMeasure); 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); + // Clip forced viewport to actual scrollable area. + if (viewPort && viewPort.top != null) + viewPort = {top: Math.min(barMeasure.docHeight - scrollerCutOff - barMeasure.clientHeight, viewPort.top)}; + // Updated line heights might result in the drawn area not + // actually covering the viewport. Keep looping until it does. visible = visibleLines(cm.display, cm.doc, viewPort); - if (visible.from >= cm.display.showingFrom && visible.to <= cm.display.showingTo) + if (visible.from >= cm.display.viewFrom && visible.to <= cm.display.viewTo) break; } + cm.display.updateLineNumbers = null; 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); + if (cm.display.viewFrom != oldFrom || cm.display.viewTo != oldTo) + signalLater(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo); } 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) { + // Does the actual updating of the line display. Bails out + // (returning false) when there is nothing to be done and forced is + // false. + function updateDisplayInner(cm, visible, forced) { var display = cm.display, doc = cm.doc; if (!display.wrapper.offsetWidth) { - display.showingFrom = display.showingTo = doc.first; - display.viewOffset = 0; + resetView(cm); 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) + if (!forced && visible.from >= display.viewFrom && visible.to <= display.viewTo && + countDirtyView(cm) == 0) 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; } - + resetView(cm); + var dims = getDimensions(cm); + + // Compute a suitable new viewport (from & to) 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 (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom); + if (display.viewTo > to && display.viewTo - to < 20) to = Math.min(end, display.viewTo); 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 || + from = visualLineNo(cm.doc, from); + to = visualLineEndNo(cm.doc, to); + } + + var different = from != display.viewFrom || to != display.viewTo || display.lastSizeC != display.wrapper.clientHeight; - // This is just a bogus formula that detects when the editor is - // resized or the font size changes. + adjustView(cm, from, to); + + display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom)); + // Position the mover div to align with the current scroll position + cm.display.mover.style.top = display.viewOffset + "px"; + + var toUpdate = countDirtyView(cm); + if (!different && toUpdate == 0 && !forced) return; + + // For big changes, we hide the enclosing element during the + // update, since that speeds up the operations on most browsers. + var focused = activeElt(); + if (toUpdate > 4) display.lineDiv.style.display = "none"; + patchDisplay(cm, display.updateLineNumbers, dims); + if (toUpdate > 4) display.lineDiv.style.display = ""; + // There might have been a widget with a focused element that got + // hidden or updated, if so re-focus it. + if (focused && activeElt() != focused && focused.offsetHeight) focused.focus(); + + // Prevent selection and cursors from interfering with the scroll + // width. + removeChildren(display.cursorDiv); + removeChildren(display.selectionDiv); + 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 adjustContentWidth(cm) { + var display = cm.display; + var width = measureChar(cm, display.maxLine, display.maxLine.text.length).left; + display.maxLineChanged = false; + var minWidth = Math.max(0, width + 3); + var maxScrollLeft = Math.max(0, display.sizer.offsetLeft + minWidth + scrollerCutOff - display.scroller.clientWidth); + display.sizer.style.minWidth = minWidth + "px"; + if (maxScrollLeft < cm.doc.scrollLeft) + setScrollLeft(cm, Math.min(display.scroller.scrollLeft, maxScrollLeft), true); + } + + function setDocumentHeight(cm, measure) { + cm.display.sizer.style.minHeight = cm.display.heightForcer.style.top = measure.docHeight + "px"; + cm.display.gutters.style.height = Math.max(measure.docHeight, measure.clientHeight - scrollerCutOff) + "px"; + } + + // Read the actual heights of the rendered lines, and update their + // stored heights to match. 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; + for (var i = 0; i < display.view.length; i++) { + var cur = display.view[i], height; + if (cur.hidden) continue; + if (ie_upto7) { + var bot = cur.node.offsetTop + cur.node.offsetHeight; height = bot - prevBottom; prevBottom = bot; } else { - var box = getRect(node); + var box = cur.node.getBoundingClientRect(); height = box.bottom - box.top; } - var diff = node.lineObj.height - height; + var diff = cur.line.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; - } - + updateLineHeight(cur.line, height); + updateWidgetHeight(cur.line); + if (cur.rest) for (var j = 0; j < cur.rest.length; j++) + updateWidgetHeight(cur.rest[j]); + } + } + } + + // Read and store the height of line widgets associated with the + // given line. + function updateWidgetHeight(line) { + if (line.widgets) for (var i = 0; i < line.widgets.length; ++i) + line.widgets[i].height = line.widgets[i].node.offsetHeight; + } + + // Do a bulk-read of the DOM positions and sizes needed to draw the + // view, so that we don't interleave reading and writing to the DOM. 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); + // Sync the actual display DOM structure with display.view, removing + // nodes for lines that are no longer in view, and creating the ones + // that are not there yet, and updating the ones that are out of + // date. + function patchDisplay(cm, updateNumbersFrom, dims) { 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) { + // Works around a throw-scroll bug in OS X Webkit + if (webkit && mac && cm.display.currentWheelTarget == node) node.style.display = "none"; - node.lineObj = null; - } else { + 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); - } + var view = display.view, lineN = display.viewFrom; + // Loop over the elements in the view, syncing cur (the DOM nodes + // in display.lineDiv) with the view as we go. + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (lineView.hidden) { + } else if (!lineView.node) { // Not drawn yet + var node = buildLineElement(cm, lineView, lineN, dims); + container.insertBefore(node, cur); + } else { // Already drawn + while (cur != lineView.node) cur = rm(cur); + var updateNumber = lineNumbers && updateNumbersFrom != null && + updateNumbersFrom <= lineN && lineView.lineNumber; + if (lineView.changes) { + if (indexOf(lineView.changes, "gutter") > -1) updateNumber = false; + updateLineForChanges(cm, lineView, lineN, 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; + if (updateNumber) { + removeChildren(lineView.lineNumber); + lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN))); } - - lineNode.lineObj = line; - } - ++lineN; - }); + cur = lineView.node.nextSibling; + } + lineN += lineView.size; + } 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); + // When an aspect of a line changes, a string is added to + // lineView.changes. This updates the relevant part of the line's + // DOM structure. + function updateLineForChanges(cm, lineView, lineN, dims) { + for (var j = 0; j < lineView.changes.length; j++) { + var type = lineView.changes[j]; + if (type == "text") updateLineText(cm, lineView); + else if (type == "gutter") updateLineGutter(cm, lineView, lineN, dims); + else if (type == "class") updateLineClasses(lineView); + else if (type == "widget") updateLineWidgets(lineView, dims); + } + lineView.changes = null; + } + + // Lines with gutter elements, widgets or a background class need to + // be wrapped, and have the extra elements added to the wrapper div + function ensureLineWrapped(lineView) { + if (lineView.node == lineView.text) { + lineView.node = elt("div", null, null, "position: relative"); + if (lineView.text.parentNode) + lineView.text.parentNode.replaceChild(lineView.node, lineView.text); + lineView.node.appendChild(lineView.text); + if (ie_upto7) lineView.node.style.zIndex = 2; + } + return lineView.node; + } + + function updateLineBackground(lineView) { + var cls = lineView.bgClass ? lineView.bgClass + " " + (lineView.line.bgClass || "") : lineView.line.bgClass; + if (cls) cls += " CodeMirror-linebackground"; + if (lineView.background) { + if (cls) lineView.background.className = cls; + else { lineView.background.parentNode.removeChild(lineView.background); lineView.background = null; } + } else if (cls) { + var wrap = ensureLineWrapped(lineView); + lineView.background = wrap.insertBefore(elt("div", null, cls), wrap.firstChild); + } + } + + // Wrapper around buildLineContent which will reuse the structure + // in display.externalMeasured when possible. + function getLineContent(cm, lineView) { + var ext = cm.display.externalMeasured; + if (ext && ext.line == lineView.line) { + cm.display.externalMeasured = null; + lineView.measure = ext.measure; + return ext.built; + } + return buildLineContent(cm, lineView); + } + + // Redraw the line's text. Interacts with the background and text + // classes because the mode may output tokens that influence these + // classes. + function updateLineText(cm, lineView) { + var cls = lineView.text.className; + var built = getLineContent(cm, lineView); + if (lineView.text == lineView.node) lineView.node = built.pre; + lineView.text.parentNode.replaceChild(built.pre, lineView.text); + lineView.text = built.pre; + if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) { + lineView.bgClass = built.bgClass; + lineView.textClass = built.textClass; + updateLineClasses(lineView); + } else if (cls) { + lineView.text.className = cls; + } + } + + function updateLineClasses(lineView) { + updateLineBackground(lineView); + if (lineView.line.wrapClass) + ensureLineWrapped(lineView).className = lineView.line.wrapClass; + else if (lineView.node != lineView.text) + lineView.node.className = ""; + var textClass = lineView.textClass ? lineView.textClass + " " + (lineView.line.textClass || "") : lineView.line.textClass; + lineView.text.className = textClass || ""; + } + + function updateLineGutter(cm, lineView, lineN, dims) { + if (lineView.gutter) { + lineView.node.removeChild(lineView.gutter); + lineView.gutter = null; + } + var markers = lineView.line.gutterMarkers; 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); + var wrap = ensureLineWrapped(lineView); + var gutterWrap = lineView.gutter = + wrap.insertBefore(elt("div", null, "CodeMirror-gutter-wrapper", "position: absolute; left: " + + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px"), + lineView.text); if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"])) - wrap.lineNumber = gutterWrap.appendChild( - elt("div", lineNumberFor(cm.options, lineNo), + lineView.lineNumber = gutterWrap.appendChild( + elt("div", lineNumberFor(cm.options, lineN), "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) { + + cm.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")); + } + } + } + + function updateLineWidgets(lineView, dims) { + if (lineView.alignable) lineView.alignable = null; + for (var node = lineView.node.firstChild, next; node; node = next) { + var next = node.nextSibling; + if (node.className == "CodeMirror-linewidget") + lineView.node.removeChild(node); + } + insertLineWidgets(lineView, dims); + } + + // Build a line's DOM representation from scratch + function buildLineElement(cm, lineView, lineN, dims) { + var built = getLineContent(cm, lineView); + lineView.text = lineView.node = built.pre; + if (built.bgClass) lineView.bgClass = built.bgClass; + if (built.textClass) lineView.textClass = built.textClass; + + updateLineClasses(lineView); + updateLineGutter(cm, lineView, lineN, dims); + insertLineWidgets(lineView, dims); + return lineView.node; + } + + // A lineView may contain multiple logical lines (when merged by + // collapsed spans). The widgets for all of them need to be drawn. + function insertLineWidgets(lineView, dims) { + insertLineWidgetsFor(lineView.line, lineView, dims, true); + if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++) + insertLineWidgetsFor(lineView.rest[i], lineView, dims, false); + } + + function insertLineWidgetsFor(line, lineView, dims, allowAbove) { + if (!line.widgets) return; + var wrap = ensureLineWrapped(lineView); + 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); + positionLineWidget(widget, node, lineView, dims); + if (allowAbove && widget.above) + wrap.insertBefore(node, lineView.gutter || lineView.text); else wrap.appendChild(node); signalLater(widget, "redraw"); } - return wrap; - } - - function positionLineWidget(widget, node, wrap, dims) { + } + + function positionLineWidget(widget, node, lineView, dims) { if (widget.noHScroll) { - (wrap.alignable || (wrap.alignable = [])).push(node); + (lineView.alignable || (lineView.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"; } } + // POSITION OBJECT + + // A Pos instance represents a position within the text. + var Pos = CodeMirror.Pos = function(line, ch) { + if (!(this instanceof Pos)) return new Pos(line, ch); + this.line = line; this.ch = ch; + }; + + // Compare two positions, return 0 if they are the same, a negative + // number when a is less, and a positive number otherwise. + var cmp = CodeMirror.cmpPos = function(a, b) { return a.line - b.line || a.ch - b.ch; }; + + function copyPos(x) {return Pos(x.line, x.ch);} + function maxPos(a, b) { return cmp(a, b) < 0 ? b : a; } + function minPos(a, b) { return cmp(a, b) < 0 ? a : b; } + // SELECTION / CURSOR + // Selection objects are immutable. A new one is created every time + // the selection changes. A selection is one or more non-overlapping + // (and non-touching) ranges, sorted, and an integer that indicates + // which one is the primary selection (the one that's scrolled into + // view, that getCursor returns, etc). + function Selection(ranges, primIndex) { + this.ranges = ranges; + this.primIndex = primIndex; + } + + Selection.prototype = { + primary: function() { return this.ranges[this.primIndex]; }, + equals: function(other) { + if (other == this) return true; + if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) return false; + for (var i = 0; i < this.ranges.length; i++) { + var here = this.ranges[i], there = other.ranges[i]; + if (cmp(here.anchor, there.anchor) != 0 || cmp(here.head, there.head) != 0) return false; + } + return true; + }, + deepCopy: function() { + for (var out = [], i = 0; i < this.ranges.length; i++) + out[i] = new Range(copyPos(this.ranges[i].anchor), copyPos(this.ranges[i].head)); + return new Selection(out, this.primIndex); + }, + somethingSelected: function() { + for (var i = 0; i < this.ranges.length; i++) + if (!this.ranges[i].empty()) return true; + return false; + }, + contains: function(pos, end) { + if (!end) end = pos; + for (var i = 0; i < this.ranges.length; i++) { + var range = this.ranges[i]; + if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0) + return i; + } + return -1; + } + }; + + function Range(anchor, head) { + this.anchor = anchor; this.head = head; + } + + Range.prototype = { + from: function() { return minPos(this.anchor, this.head); }, + to: function() { return maxPos(this.anchor, this.head); }, + empty: function() { + return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch; + } + }; + + // Take an unsorted, potentially overlapping set of ranges, and + // build a selection out of it. 'Consumes' ranges array (modifying + // it). + function normalizeSelection(ranges, primIndex) { + var prim = ranges[primIndex]; + ranges.sort(function(a, b) { return cmp(a.from(), b.from()); }); + primIndex = indexOf(ranges, prim); + for (var i = 1; i < ranges.length; i++) { + var cur = ranges[i], prev = ranges[i - 1]; + if (cmp(prev.to(), cur.from()) >= 0) { + var from = minPos(prev.from(), cur.from()), to = maxPos(prev.to(), cur.to()); + var inv = prev.empty() ? cur.from() == cur.head : prev.from() == prev.head; + if (i <= primIndex) --primIndex; + ranges.splice(--i, 2, new Range(inv ? to : from, inv ? from : to)); + } + } + return new Selection(ranges, primIndex); + } + + function simpleSelection(anchor, head) { + return new Selection([new Range(anchor, head || anchor)], 0); + } + + // Most of the external API clips given positions to make sure they + // actually exist within the document. + 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;} + function clipPosArray(doc, array) { + for (var out = [], i = 0; i < array.length; i++) out[i] = clipPos(doc, array[i]); + return out; + } + + // SELECTION UPDATES + + // The 'scroll' parameter given to many of these indicated whether + // the new cursor position should be scrolled into view after + // modifying the selection. + + // If shift is held or the extend flag is set, extends a range to + // include a given position (and optionally a second position). + // Otherwise, simply returns the range between the given positions. + // Used for cursor motion and such. + function extendRange(doc, range, head, other) { + if (doc.cm && doc.cm.display.shift || doc.extend) { + var anchor = range.anchor; + if (other) { + var posBefore = cmp(head, anchor) < 0; + if (posBefore != (cmp(other, anchor) < 0)) { + anchor = head; + head = other; + } else if (posBefore != (cmp(head, other) < 0)) { + head = other; + } + } + return new Range(anchor, head); + } else { + return new Range(other || head, head); + } + } + + // Extend the primary selection range, discard the rest. + function extendSelection(doc, head, other, options) { + setSelection(doc, new Selection([extendRange(doc, doc.sel.primary(), head, other)], 0), options); + } + + // Extend all selections (pos is an array of selections with length + // equal the number of selections) + function extendSelections(doc, heads, options) { + for (var out = [], i = 0; i < doc.sel.ranges.length; i++) + out[i] = extendRange(doc, doc.sel.ranges[i], heads[i], null); + var newSel = normalizeSelection(out, doc.sel.primIndex); + setSelection(doc, newSel, options); + } + + // Updates a single range in the selection. + function replaceOneSelection(doc, i, range, options) { + var ranges = doc.sel.ranges.slice(0); + ranges[i] = range; + setSelection(doc, normalizeSelection(ranges, doc.sel.primIndex), options); + } + + // Reset the selection to a single range. + function setSimpleSelection(doc, anchor, head, options) { + setSelection(doc, simpleSelection(anchor, head), options); + } + + // Give beforeSelectionChange handlers a change to influence a + // selection update. + function filterSelectionChange(doc, sel) { + var obj = { + ranges: sel.ranges, + update: function(ranges) { + this.ranges = []; + for (var i = 0; i < ranges.length; i++) + this.ranges[i] = new Range(clipPos(doc, ranges[i].anchor), + clipPos(doc, ranges[i].head)); + } + }; + signal(doc, "beforeSelectionChange", doc, obj); + if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj); + if (obj.ranges != sel.ranges) return normalizeSelection(obj.ranges, obj.ranges.length - 1); + else return sel; + } + + function setSelectionReplaceHistory(doc, sel, options) { + var done = doc.history.done, last = lst(done); + if (last && last.ranges) { + done[done.length - 1] = sel; + setSelectionNoUndo(doc, sel, options); + } else { + setSelection(doc, sel, options); + } + } + + // Set a new selection. + function setSelection(doc, sel, options) { + setSelectionNoUndo(doc, sel, options); + addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options); + } + + function setSelectionNoUndo(doc, sel, options) { + if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) + sel = filterSelectionChange(doc, sel); + + var bias = cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1; + setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true)); + + if (!(options && options.scroll === false) && doc.cm) + ensureCursorVisible(doc.cm); + } + + function setSelectionInner(doc, sel) { + if (sel.equals(doc.sel)) return; + + doc.sel = sel; + + if (doc.cm) + doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = + doc.cm.curOp.cursorActivity = true; + signalLater(doc, "cursorActivity", doc); + } + + // Verify that the selection does not partially select any atomic + // marked ranges. + function reCheckSelection(doc) { + setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false), sel_dontScroll); + } + + // Return a selection that does not partially select any atomic + // ranges. + function skipAtomicInSelection(doc, sel, bias, mayClear) { + var out; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + var newAnchor = skipAtomic(doc, range.anchor, bias, mayClear); + var newHead = skipAtomic(doc, range.head, bias, mayClear); + if (out || newAnchor != range.anchor || newHead != range.head) { + if (!out) out = sel.ranges.slice(0, i); + out[i] = new Range(newAnchor, newHead); + } + } + return out ? normalizeSelection(out, sel.primIndex) : sel; + } + + // Ensure a given position is not inside an atomic range. + 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 ? -1 : 1); + if (cmp(newPos, curPos) == 0) { + 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; + } + } + + // SELECTION DRAWING + + // Redraw the selection and/or 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"; + var display = cm.display, doc = cm.doc; + var curFragment = document.createDocumentFragment(); + var selFragment = document.createDocumentFragment(); + + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + var collapsed = range.empty(); + if (collapsed || cm.options.showCursorWhenSelecting) + updateSelectionCursor(cm, range, curFragment); + if (!collapsed) + updateSelectionRange(cm, range, selFragment); + } // 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 = ""; + var headPos = cursorCoords(cm, doc.sel.primary().head, "div"); + var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect(); + var top = Math.max(0, Math.min(display.wrapper.clientHeight - 10, + headPos.top + lineOff.top - wrapOff.top)); + var left = Math.max(0, Math.min(display.wrapper.clientWidth - 10, + headPos.left + lineOff.left - wrapOff.left)); + display.inputDiv.style.top = top + "px"; + display.inputDiv.style.left = left + "px"; + } + + removeChildrenAndAdd(display.cursorDiv, curFragment); + removeChildrenAndAdd(display.selectionDiv, selFragment); + } + + // Draws a cursor for the given range + function updateSelectionCursor(cm, range, output) { + var pos = cursorCoords(cm, range.head, "div"); + + var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor")); + cursor.style.left = pos.left + "px"; + cursor.style.top = pos.top + "px"; + cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px"; 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; + // Secondary cursor, shown when on a 'jump' in bi-directional text + var otherCursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor")); + otherCursor.style.display = ""; + otherCursor.style.left = pos.other.left + "px"; + otherCursor.style.top = pos.other.top + "px"; + otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px"; + } + } + + // Draws the given range as a highlighted selection + function updateSelectionRange(cm, range, output) { + var display = cm.display, doc = cm.doc; var fragment = document.createDocumentFragment(); - var clientWidth = display.lineSpace.offsetWidth, pl = paddingLeft(cm.display); + var padding = paddingH(cm.display), leftSide = padding.left, rightSide = display.lineSpace.offsetWidth - padding.right; 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; top: " + top + "px; width: " + (width == null ? rightSide - 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) { @@ -843,109 +1296,103 @@ window.CodeMirror = (function() { 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 (fromArg == null && from == 0) left = leftSide; if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part add(left, leftPos.top, null, leftPos.bottom); - left = pl; + left = leftSide; if (leftPos.bottom < rightPos.top) add(left, leftPos.bottom, null, rightPos.top); } - if (toArg == null && to == lineLen) right = clientWidth; + if (toArg == null && to == lineLen) right = rightSide; 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; + if (left < leftSide + 1) left = leftSide; 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); + var sFrom = range.from(), sTo = range.to(); + if (sFrom.line == sTo.line) { + drawForLine(sFrom.line, sFrom.ch, sTo.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; + var fromLine = getLine(doc, sFrom.line), toLine = getLine(doc, sTo.line); + var singleVLine = visualLine(fromLine) == visualLine(toLine); + var leftEnd = drawForLine(sFrom.line, sFrom.ch, singleVLine ? fromLine.text.length + 1 : null).end; + var rightStart = drawForLine(sTo.line, singleVLine ? 0 : null, sTo.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); + add(leftSide, 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 = ""; + add(leftSide, leftEnd.bottom, null, rightStart.top); + } + + output.appendChild(fragment); } // 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.cursorDiv.style.visibility = ""; if (cm.options.cursorBlinkRate > 0) display.blinker = setInterval(function() { - display.cursor.style.visibility = display.otherCursor.style.visibility = (on = !on) ? "" : "hidden"; + display.cursorDiv.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) + if (cm.doc.mode.startState && cm.doc.frontier < cm.display.viewTo) 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; + if (doc.frontier >= cm.display.viewTo) 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 + + runInOp(cm, function() { + doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function(line) { + if (doc.frontier >= cm.display.viewFrom) { // 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}); - } + if (ischange) regLineChange(cm, doc.frontier, "text"); 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) { @@ -967,262 +1414,306 @@ window.CodeMirror = (function() { 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; + var save = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo; 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; + function paddingH(display) { + if (display.cachedPaddingH) return display.cachedPaddingH; + var e = removeChildrenAndAdd(display.measure, elt("pre", "x")); + var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle; + return display.cachedPaddingH = {left: parseInt(style.paddingLeft), + right: parseInt(style.paddingRight)}; + } + + // Ensure the lineView.wrapping.heights array is populated. This is + // an array of bottom offsets for the lines that make up a drawn + // line. When lineWrapping is on, there might be more than one + // height. + function ensureLineHeights(cm, lineView, rect) { + var wrapping = cm.options.lineWrapping; + var curWidth = wrapping && cm.display.scroller.clientWidth; + if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) { + var heights = lineView.measure.heights = []; + if (wrapping) { + lineView.measure.width = curWidth; + var rects = lineView.text.firstChild.getClientRects(); + for (var i = 0; i < rects.length - 1; i++) { + var cur = rects[i], next = rects[i + 1]; + if (Math.abs(cur.bottom - next.bottom) > 2) + heights.push((cur.bottom + next.top) / 2 - rect.top); } - 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; + } + heights.push(rect.bottom - rect.top); + } + } + + // Find a line map (mapping character offsets to text nodes) and a + // measurement cache for the given line number. (A line view might + // contain multiple lines when collapsed ranges are present.) + function mapFromLineView(lineView, line, lineN) { + if (lineView.line == line) + return {map: lineView.measure.map, cache: lineView.measure.cache}; + for (var i = 0; i < lineView.rest.length; i++) + if (lineView.rest[i] == line) + return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]}; + for (var i = 0; i < lineView.rest.length; i++) + if (lineNo(lineView.rest[i]) > lineN) + return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i], before: true}; + } + + // Render a line into the hidden node display.externalMeasured. Used + // when measurement is needed for a line that's not in the viewport. + function updateExternalMeasurement(cm, line) { + line = visualLine(line); + var lineN = lineNo(line); + var view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN); + view.lineN = lineN; + var built = view.built = buildLineContent(cm, view); + view.text = built.pre; + removeChildrenAndAdd(cm.display.lineMeasure, built.pre); + return view; + } + + // Get a {top, bottom, left, right} box (in line-local coordinates) + // for a given character. + function measureChar(cm, line, ch, bias) { + return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias); + } + + // Find a line view that corresponds to the given line number. + function findViewForLine(cm, lineN) { + if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo) + return cm.display.view[findViewIndex(cm, lineN)]; + var ext = cm.display.externalMeasured; + if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size) + return ext; + } + + // Measurement can be split in two steps, the set-up work that + // applies to the whole line, and the measurement of the actual + // character. Functions like coordsChar, that need to do a lot of + // measurements in a row, can thus ensure that the set-up work is + // only done once. + function prepareMeasureForLine(cm, line) { + var lineN = lineNo(line); + var view = findViewForLine(cm, lineN); + if (view && !view.text) + view = null; + else if (view && view.changes) + updateLineForChanges(cm, view, lineN, getDimensions(cm)); + if (!view) + view = updateExternalMeasurement(cm, line); + + var info = mapFromLineView(view, line, lineN); + return { + line: line, view: view, rect: null, + map: info.map, cache: info.cache, before: info.before, + hasHeights: false + }; + } + + // Given a prepared measurement object, measures the position of an + // actual character (or fetches it from the cache). + function measureCharPrepared(cm, prepared, ch, bias) { + if (prepared.before) ch = -1; + var key = ch + (bias || ""), found; + if (prepared.cache.hasOwnProperty(key)) { + found = prepared.cache[key]; + } else { + if (!prepared.rect) + prepared.rect = prepared.view.text.getBoundingClientRect(); + if (!prepared.hasHeights) { + ensureLineHeights(cm, prepared.view, prepared.rect); + prepared.hasHeights = true; + } + found = measureCharInner(cm, prepared, ch, bias); + if (!found.bogus) prepared.cache[key] = found; + } + return {left: found.left, right: found.right, top: found.top, bottom: found.bottom}; + } + + var nullRect = {left: 0, right: 0, top: 0, bottom: 0}; + + function measureCharInner(cm, prepared, ch, bias) { + var map = prepared.map; + + var node, start, end, collapse; + // First, search the line map for the text node corresponding to, + // or closest to, the target character. + for (var i = 0; i < map.length; i += 3) { + var mStart = map[i], mEnd = map[i + 1]; + if (ch < mStart) { + start = 0; end = 1; + collapse = "left"; + } else if (ch < mEnd) { + start = ch - mStart; + end = start + 1; + } else if (i == map.length - 3 || ch == mEnd && map[i + 3] > ch) { + end = mEnd - mStart; + start = end - 1; + if (ch >= mEnd) collapse = "right"; + } + if (start != null) { + node = map[i + 2]; + if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right")) + collapse = bias; + if (bias == "left" && start == 0) + while (i && map[i - 2] == map[i - 3] && map[i - 1].insertLeft) { + node = map[(i -= 3) + 2]; + collapse = "left"; + } + if (bias == "right" && start == mEnd - mStart) + while (i < map.length - 3 && map[i + 3] == map[i + 4] && !map[i + 5].insertLeft) { + node = map[(i += 3) + 2]; + collapse = "right"; + } + break; + } + } + + var rect; + if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates. + while (start && isExtendingChar(prepared.line.text.charAt(mStart + start))) --start; + while (mStart + end < mEnd && isExtendingChar(prepared.line.text.charAt(mStart + end))) ++end; + if (ie_upto8 && start == 0 && end == mEnd - mStart) { + rect = node.parentNode.getBoundingClientRect(); + } else if (ie && cm.options.lineWrapping) { + var rects = range(node, start, end).getClientRects(); + if (rects.length) + rect = rects[bias == "right" ? rects.length - 1 : 0]; + else + rect = nullRect; + } else { + rect = range(node, start, end).getBoundingClientRect(); + } + } else { // If it is a widget, simply get the box for the whole widget. + if (start > 0) collapse = bias = "right"; + var rects; + if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1) + rect = rects[bias == "right" ? rects.length - 1 : 0]; + else + rect = node.getBoundingClientRect(); + } + if (ie_upto8 && !start && (!rect || !rect.left && !rect.right)) { + var rSpan = node.parentNode.getClientRects()[0]; + if (rSpan) + rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom}; + else + rect = nullRect; + } + + var top, bot = (rect.bottom + rect.top) / 2 - prepared.rect.top; + var heights = prepared.view.measure.heights; + for (var i = 0; i < heights.length - 1; i++) + if (bot < heights[i]) break; + top = i ? heights[i - 1] : 0; bot = heights[i]; + var result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left, + right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left, + top: top, bottom: bot}; + if (!rect.left && !rect.right) result.bogus = true; + return result; + } + + function clearLineMeasurementCacheFor(lineView) { + if (lineView.measure) { + lineView.measure.cache = {}; + lineView.measure.heights = null; + if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++) + lineView.measure.caches[i] = {}; + } + } + + function clearLineMeasurementCache(cm) { + cm.display.externalMeasure = null; + removeChildren(cm.display.lineMeasure); + for (var i = 0; i < cm.display.view.length; i++) + clearLineMeasurementCacheFor(cm.display.view[i]); } function clearCaches(cm) { - cm.display.measureLineCache.length = cm.display.measureLineCachePos = 0; - cm.display.cachedCharWidth = cm.display.cachedTextHeight = null; + clearLineMeasurementCache(cm); + cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = 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" + // Converts a {top, bottom, left, right} box from line-local + // coordinates into another coordinate system. Context may be 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); + var yOff = heightAtLine(lineObj); if (context == "local") yOff += paddingTop(cm.display); else yOff -= cm.display.viewOffset; if (context == "page" || context == "window") { - var lOff = getRect(cm.display.lineSpace); + var lOff = cm.display.lineSpace.getBoundingClientRect(); 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 + // Coverts a box from "div" coords to another coordinate system. + // Context may be "window", "page", "div", or "local"/null. 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); + var localBox = cm.display.sizer.getBoundingClientRect(); left += localBox.left; top += localBox.top; } - var lineSpaceBox = getRect(cm.display.lineSpace); + var lineSpaceBox = cm.display.lineSpace.getBoundingClientRect(); 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) { + return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context); + } + + // Returns a box for a given cursor position, which may have an + // 'other' property containing the position of the secondary cursor + // on a bidi boundary. + function cursorCoords(cm, pos, context, lineObj, preparedMeasure) { lineObj = lineObj || getLine(cm.doc, pos.line); - if (!measurement) measurement = measureLine(cm, lineObj); + if (!preparedMeasure) preparedMeasure = prepareMeasureForLine(cm, lineObj); function get(ch, right) { - var m = measureChar(cm, lineObj, ch, measurement, right ? "right" : "left"); + var m = measureCharPrepared(cm, preparedMeasure, ch, 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); @@ -1238,53 +1729,69 @@ window.CodeMirror = (function() { 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; } + // Used to cheaply estimate the coordinates for a position. Used for + // intermediate scroll updates. + function estimateCoords(cm, pos) { + var left = 0, pos = clipPos(cm.doc, pos); + if (!cm.options.lineWrapping) left = charWidth(cm.display) * pos.ch; + var lineObj = getLine(cm.doc, pos.line); + var top = heightAtLine(lineObj) + paddingTop(cm.display); + return {left: left, right: left, top: top, bottom: top + lineObj.height}; + } + + // Positions returned by coordsChar contain some extra information. + // xRel is the relative x position of the input coordinates compared + // to the found position (so xRel > 0 means the coordinates are to + // the right of the character position, for example). When outside + // is true, that means the coordinates lie outside the line's + // vertical range. function PosWithInfo(line, ch, outside, xRel) { - var pos = new Pos(line, ch); + var pos = Pos(line, ch); pos.xRel = xRel; if (outside) pos.outside = true; return pos; } - // Coords must be lineSpace-local + // Compute the character position closest to the given coordinates. + // Input must be lineSpace-local ("div" coordinate system). 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) + var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1; + if (lineN > last) return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, true, 1); if (x < 0) x = 0; + var lineObj = getLine(doc, lineN); for (;;) { - var lineObj = getLine(doc, lineNo); - var found = coordsCharInner(cm, lineObj, lineNo, x, y); + var found = coordsCharInner(cm, lineObj, lineN, x, y); var merged = collapsedSpanAtEnd(lineObj); - var mergedPos = merged && merged.find(); + var mergedPos = merged && merged.find(0, true); if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0)) - lineNo = mergedPos.to.line; + lineN = lineNo(lineObj = mergedPos.to.line); else return found; } } function coordsCharInner(cm, lineObj, lineNo, x, y) { - var innerOff = y - heightAtLine(cm, lineObj); + var innerOff = y - heightAtLine(lineObj); var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth; - var measurement = measureLine(cm, lineObj); + var preparedMeasure = prepareMeasureForLine(cm, lineObj); function getX(ch) { - var sp = cursorCoords(cm, Pos(lineNo, ch), "line", - lineObj, measurement); + var sp = cursorCoords(cm, Pos(lineNo, ch), "line", lineObj, preparedMeasure); 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; @@ -1294,31 +1801,32 @@ window.CodeMirror = (function() { 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); + xDiff < -1 ? -1 : xDiff > 1 ? 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; + // Compute the default text height. 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")); @@ -1328,352 +1836,573 @@ window.CodeMirror = (function() { } removeChildrenAndAdd(display.measure, measureText); var height = measureText.offsetHeight / 50; if (height > 3) display.cachedTextHeight = height; removeChildren(display.measure); return height || 1; } + // Compute the default character width. function charWidth(display) { if (display.cachedCharWidth != null) return display.cachedCharWidth; - var anchor = elt("span", "x"); + var anchor = elt("span", "xxxxxxxxxx"); var pre = elt("pre", [anchor]); removeChildrenAndAdd(display.measure, pre); - var width = anchor.offsetWidth; + var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10; 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. + // Operations are used to wrap a series of changes to the editor + // state in such a way that each change won't have to update the + // cursor and display (which would be awkward, slow, and + // error-prone). Instead, display updates are batched and then all + // combined and executed at once. var nextOpId = 0; + // Start a new operation. 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 + viewChanged: false, // Flag that indicates that lines might need to be redrawn + startHeight: cm.doc.height, // Used to detect need to update scrollbar + forceUpdate: false, // Used to force a redraw + updateInput: null, // Whether to reset the input textarea + typing: false, // Whether this reset should be careful to leave existing text (for compositing) + changeObjs: null, // Accumulated changes, for firing change events + cursorActivity: false, // Whether to fire a cursorActivity event + selectionChanged: false, // Whether the selection needs to be redrawn + updateMaxLine: false, // Set when the widest line needs to be determined anew + scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet + scrollToPos: null, // Used to scroll to a specific position + id: ++nextOpId // Unique ID }; if (!delayedCallbackDepth++) delayedCallbacks = []; } + // Finish an operation, updating the display and signalling delayed events 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 (op.updateMaxLine) findMaxLine(cm); + + // If it looks like an update might be needed, call updateDisplay + if (op.viewChanged || op.forceUpdate || op.scrollTop != null || + op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom || + op.scrollToPos.to.line >= display.viewTo) || + display.maxLineChanged && cm.options.lineWrapping) { + var updated = updateDisplay(cm, {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate); if (cm.display.scroller.offsetHeight) cm.doc.scrollTop = cm.display.scroller.scrollTop; } + // If no update was run, but the selection changed, redraw that. 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)); + if (!updated && op.startHeight != cm.doc.height) updateScrollbars(cm); + + // Propagate the scroll position to the actual DOM scroller + if (op.scrollTop != null && display.scroller.scrollTop != op.scrollTop) { + var top = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, op.scrollTop)); display.scroller.scrollTop = display.scrollbarV.scrollTop = doc.scrollTop = top; + } + if (op.scrollLeft != null && display.scroller.scrollLeft != op.scrollLeft) { + var left = Math.max(0, Math.min(display.scroller.scrollWidth - display.scroller.clientWidth, op.scrollLeft)); 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 we need to scroll a specific position into view, do so. + if (op.scrollToPos) { + var coords = scrollPosIntoView(cm, clipPos(cm.doc, op.scrollToPos.from), + clipPos(cm.doc, op.scrollToPos.to), op.scrollToPos.margin); + if (op.scrollToPos.isCursor && cm.state.focused) maybeScrollWindow(cm, coords); + } + if (op.selectionChanged) restartBlink(cm); if (cm.state.focused && op.updateInput) - resetInput(cm, op.userSelChange); - + resetInput(cm, op.typing); + + // Fire events for markers that are hidden/unidden by editing or + // undoing 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); + // Fire change events, and delayed event handlers + if (op.changeObjs) { + for (var i = 0; i < op.changeObjs.length; i++) + signal(cm, "change", cm, op.changeObjs[i]); + signal(cm, "changes", cm, op.changeObjs); + } if (op.cursorActivity) signal(cm, "cursorActivity", cm); if (delayed) for (var i = 0; i < delayed.length; ++i) delayed[i](); } + // Run the given function in an operation + function runInOp(cm, f) { + if (cm.curOp) return f(); + startOperation(cm); + try { return f(); } + finally { endOperation(cm); } + } // Wraps a function in an operation. Returns the wrapped function. - function operation(cm1, f) { + function operation(cm, 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; + if (cm.curOp) return f.apply(cm, arguments); + startOperation(cm); + try { return f.apply(cm, arguments); } + finally { endOperation(cm); } }; } - function docOperation(f) { + // Used to add methods to editor and doc instances, wrapping them in + // operations. + function methodOp(f) { + return function() { + if (this.curOp) return f.apply(this, arguments); + startOperation(this); + try { return f.apply(this, arguments); } + finally { endOperation(this); } + }; + } + function docMethodOp(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; + var cm = this.cm; + if (!cm || cm.curOp) return f.apply(this, arguments); + startOperation(cm); + try { return f.apply(this, arguments); } + finally { endOperation(cm); } }; } - function runInOp(cm, f) { - var withOp = !cm.curOp, result; - if (withOp) startOperation(cm); - try { result = f(); } - finally { if (withOp) endOperation(cm); } - return result; - } - + + // VIEW TRACKING + + // These objects are used to represent the visible (currently drawn) + // part of the document. A LineView may correspond to multiple + // logical lines, if those are connected by collapsed ranges. + function LineView(doc, line, lineN) { + // The starting line + this.line = line; + // Continuing lines, if any + this.rest = visualLineContinued(line); + // Number of logical lines in this visual line + this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1; + this.node = this.text = null; + this.hidden = lineIsHidden(doc, line); + } + + // Create a range of LineView objects for the given lines. + function buildViewArray(cm, from, to) { + var array = [], nextPos; + for (var pos = from; pos < to; pos = nextPos) { + var view = new LineView(cm.doc, getLine(cm.doc, pos), pos); + nextPos = pos + view.size; + array.push(view); + } + return array; + } + + // Updates the display.view data structure for a given change to the + // document. From and to are in pre-change coordinates. Lendiff is + // the amount of lines added or subtracted by the change. This is + // used for changes that span multiple lines, or change the way + // lines are divided into visual lines. regLineChange (below) + // registers single-line changes. 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}); + if (!lendiff) lendiff = 0; + + var display = cm.display; + if (lendiff && to < display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers > from)) + display.updateLineNumbers = from; + + cm.curOp.viewChanged = true; + + if (from >= display.viewTo) { // Change after + if (sawCollapsedSpans && visualLineNo(cm.doc, from) < display.viewTo) + resetView(cm); + } else if (to <= display.viewFrom) { // Change before + if (sawCollapsedSpans && visualLineEndNo(cm.doc, to + lendiff) > display.viewFrom) { + resetView(cm); + } else { + display.viewFrom += lendiff; + display.viewTo += lendiff; + } + } else if (from <= display.viewFrom && to >= display.viewTo) { // Full overlap + resetView(cm); + } else if (from <= display.viewFrom) { // Top overlap + var cut = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cut) { + display.view = display.view.slice(cut.index); + display.viewFrom = cut.lineN; + display.viewTo += lendiff; + } else { + resetView(cm); + } + } else if (to >= display.viewTo) { // Bottom overlap + var cut = viewCuttingPoint(cm, from, from, -1); + if (cut) { + display.view = display.view.slice(0, cut.index); + display.viewTo = cut.lineN; + } else { + resetView(cm); + } + } else { // Gap in the middle + var cutTop = viewCuttingPoint(cm, from, from, -1); + var cutBot = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cutTop && cutBot) { + display.view = display.view.slice(0, cutTop.index) + .concat(buildViewArray(cm, cutTop.lineN, cutBot.lineN)) + .concat(display.view.slice(cutBot.index)); + display.viewTo += lendiff; + } else { + resetView(cm); + } + } + + var ext = display.externalMeasured; + if (ext) { + if (to < ext.lineN) + ext.lineN += lendiff; + else if (from < ext.lineN + ext.size) + display.externalMeasured = null; + } + } + + // Register a change to a single line. Type must be one of "text", + // "gutter", "class", "widget" + function regLineChange(cm, line, type) { + cm.curOp.viewChanged = true; + var display = cm.display, ext = cm.display.externalMeasured; + if (ext && line >= ext.lineN && line < ext.lineN + ext.size) + display.externalMeasured = null; + + if (line < display.viewFrom || line >= display.viewTo) return; + var lineView = display.view[findViewIndex(cm, line)]; + if (lineView.node == null) return; + var arr = lineView.changes || (lineView.changes = []); + if (indexOf(arr, type) == -1) arr.push(type); + } + + // Clear the view. + function resetView(cm) { + cm.display.viewFrom = cm.display.viewTo = cm.doc.first; + cm.display.view = []; + cm.display.viewOffset = 0; + } + + // Find the view element corresponding to a given line. Return null + // when the line isn't visible. + function findViewIndex(cm, n) { + if (n >= cm.display.viewTo) return null; + n -= cm.display.viewFrom; + if (n < 0) return null; + var view = cm.display.view; + for (var i = 0; i < view.length; i++) { + n -= view[i].size; + if (n < 0) return i; + } + } + + function viewCuttingPoint(cm, oldN, newN, dir) { + var index = findViewIndex(cm, oldN), diff, view = cm.display.view; + if (!sawCollapsedSpans) return {index: index, lineN: newN}; + for (var i = 0, n = cm.display.viewFrom; i < index; i++) + n += view[i].size; + if (n != oldN) { + if (dir > 0) { + if (index == view.length - 1) return null; + diff = (n + view[index].size) - oldN; + index++; + } else { + diff = n - oldN; + } + oldN += diff; newN += diff; + } + while (visualLineNo(cm.doc, newN) != newN) { + if (index == (dir < 0 ? 0 : view.length - 1)) return null; + newN += dir * view[index - (dir < 0 ? 1 : 0)].size; + index += dir; + } + return {index: index, lineN: newN}; + } + + // Force the view to cover a given range, adding empty view element + // or clipping off existing ones as needed. + function adjustView(cm, from, to) { + var display = cm.display, view = display.view; + if (view.length == 0 || from >= display.viewTo || to <= display.viewFrom) { + display.view = buildViewArray(cm, from, to); + display.viewFrom = from; + } else { + if (display.viewFrom > from) + display.view = buildViewArray(cm, from, display.viewFrom).concat(display.view); + else if (display.viewFrom < from) + display.view = display.view.slice(findViewIndex(cm, from)); + display.viewFrom = from; + if (display.viewTo < to) + display.view = display.view.concat(buildViewArray(cm, display.viewTo, to)); + else if (display.viewTo > to) + display.view = display.view.slice(0, findViewIndex(cm, to)); + } + display.viewTo = to; + } + + // Count the number of lines in the view whose DOM representation is + // out of date (or nonexistent). + function countDirtyView(cm) { + var view = cm.display.view, dirty = 0; + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (!lineView.hidden && (!lineView.node || lineView.changes)) ++dirty; + } + return dirty; } // INPUT HANDLING + // Poll for input changes, using the normal rate of polling. This + // runs as long as the editor is focused. function slowPoll(cm) { if (cm.display.pollingFast) return; cm.display.poll.set(cm.options.pollInterval, function() { readInput(cm); if (cm.state.focused) slowPoll(cm); }); } + // When an event has just come in that is likely to add or change + // something in the input textarea, we poll faster, to ensure that + // the change appears on the screen quickly. 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.) + // Read input from the textarea, and update the document to match. + // When something is selected, it is present in the textarea, and + // selected (unless it is huge, in which case a placeholder is + // used). When nothing is selected, the cursor sits after previously + // seen text (can be empty), which is stored in prevInput (we must + // not reset the textarea when typing, because that breaks IME). function readInput(cm) { - var input = cm.display.input, prevInput = cm.display.prevInput, doc = cm.doc, sel = doc.sel; + var input = cm.display.input, prevInput = cm.display.prevInput, doc = cm.doc; + // Since this is called a *lot*, try to bail out as cheaply as + // possible when it is clear that nothing happened. hasSelection + // will be the case when there is a lot of text in the textarea, + // in which case reading its value would be expensive. if (!cm.state.focused || hasSelection(input) || isReadOnly(cm) || cm.options.disableInput) 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); + // If nothing changed, bail. + if (text == prevInput && !cm.somethingSelected()) return false; + // Work around nonsensical selection resetting in IE9/10 + if (ie && !ie_upto8 && cm.display.inputHasSelection === text) { + resetInput(cm); return false; } var withOp = !cm.curOp; if (withOp) startOperation(cm); - sel.shift = false; + cm.display.shift = false; + + // Find the part of the input that is actually new 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"); + var inserted = text.slice(same), textLines = splitLines(inserted); + + // When pasing N lines into N selections, insert one line per selection + var multiPaste = cm.state.pasteIncoming && textLines.length > 1 && doc.sel.ranges.length == textLines.length; + + // Normal behavior is to insert the new text into every selection + for (var i = doc.sel.ranges.length - 1; i >= 0; i--) { + var range = doc.sel.ranges[i]; + var from = range.from(), to = range.to(); + // Handle deletion + if (same < prevInput.length) + from = Pos(from.line, from.ch - (prevInput.length - same)); + // Handle overwrite + else if (cm.state.overwrite && range.empty() && !cm.state.pasteIncoming) + to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length)); + var updateInput = cm.curOp.updateInput; + var changeEvent = {from: from, to: to, text: multiPaste ? [textLines[i]] : textLines, + origin: cm.state.pasteIncoming ? "paste" : cm.state.cutIncoming ? "cut" : "+input"}; + makeChange(cm.doc, changeEvent); + signalLater(cm, "inputRead", cm, changeEvent); + // When an 'electric' character is inserted, immediately trigger a reindent + if (inserted && !cm.state.pasteIncoming && cm.options.electricChars && + cm.options.smartIndent && range.head.ch < 100 && + (!i || doc.sel.ranges[i - 1].head.line != range.head.line)) { + var electric = cm.getModeAt(range.head).electricChars; + if (electric) for (var j = 0; j < electric.length; j++) + if (inserted.indexOf(electric.charAt(j)) > -1) { + indentLine(cm, range.head.line, "smart"); + break; + } + } + } + ensureCursorVisible(cm); 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; - } - } - + cm.curOp.typing = true; + + // Don't leave long text in the textarea, since it makes further polling slow 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) { + // Reset the input to correspond to the selection (or to be empty, + // when not typing and nothing is selected) + function resetInput(cm, typing) { var minimal, selected, doc = cm.doc; - if (!posEq(doc.sel.from, doc.sel.to)) { + if (cm.somethingSelected()) { cm.display.prevInput = ""; + var range = doc.sel.primary(); minimal = hasCopyEvent && - (doc.sel.to.line - doc.sel.from.line > 100 || (selected = cm.getSelection()).length > 1000); + (range.to().line - range.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) { + if (ie && !ie_upto8) cm.display.inputHasSelection = content; + } else if (!typing) { cm.display.prevInput = cm.display.input.value = ""; - if (ie && !ie_lt9) cm.display.inputHasSelection = null; + if (ie && !ie_upto8) cm.display.inputHasSelection = null; } cm.display.inaccurateSelection = minimal; } function focusInput(cm) { - if (cm.options.readOnly != "nocursor" && (!mobile || document.activeElement != cm.display.input)) + if (cm.options.readOnly != "nocursor" && (!mobile || activeElt() != cm.display.input)) cm.display.input.focus(); } + function ensureFocus(cm) { + if (!cm.state.focused) { focusInput(cm); onFocus(cm); } + } + function isReadOnly(cm) { return cm.options.readOnly || cm.doc.cantEdit; } // EVENT HANDLERS + // Attach the necessary event handlers when initializing the editor function registerEventHandlers(cm) { var d = cm.display; on(d.scroller, "mousedown", operation(cm, onMouseDown)); - if (old_ie) + // Older IE's will not fire a second mousedown for a double click + if (ie_upto10) 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); + var word = findWordAt(cm.doc, pos); + extendSelection(cm.doc, word.anchor, word.head); })); else on(d.scroller, "dblclick", function(e) { signalDOMEvent(cm, e) || e_preventDefault(e); }); + // Prevent normal selection in the editor (we handle our own) on(d.lineSpace, "selectstart", function(e) { if (!eventInWidget(d, e)) e_preventDefault(e); }); - // Gecko browsers fire contextmenu *after* opening the menu, at + // Some 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);}); - + // handled in onMouseDown for these browsers. + if (!captureRightClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);}); + + // Sync scrolling between fake scrollbars and real scrollable + // area, ensure viewport is updated when scrolling. 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); }); + // Listen to wheel events in order to try and update the viewport on time. on(d.scroller, "mousewheel", function(e){onScrollWheel(cm, e);}); on(d.scroller, "DOMMouseScroll", function(e){onScrollWheel(cm, e);}); + // Prevent clicks in the scrollbars from killing focus 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; }); + // When the window resizes, we need to refresh active editors. 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)); + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = knownScrollbarWidth = null; + cm.setSize(); }, 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. + // The 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); + if (contains(document.body, d.wrapper)) 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, "keyup", operation(cm, onKeyUp)); on(d.input, "input", function() { - if (ie && !ie_lt9 && cm.display.inputHasSelection) cm.display.inputHasSelection = null; + if (ie && !ie_upto8 && 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 (!signalDOMEvent(cm, e)) 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; + cm.state.pasteIncoming = true; 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; @@ -1682,156 +2411,250 @@ window.CodeMirror = (function() { } 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) on(d.sizer, "mouseup", function() { - if (document.activeElement == d.input) d.input.blur(); + if (activeElt() == d.input) d.input.blur(); focusInput(cm); }); } + // MOUSE EVENTS + + // Return true when the given mouse event happened in a widget 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) { + // Given a mouse event, find the corresponding position. If liberal + // is false, it checks whether a gutter or scrollbar was clicked, + // and returns null if it was. forRect is used by rectangular + // selections, and tries to estimate a character position even for + // coordinates beyond the right of the text. + function posFromMouse(cm, e, liberal, forRect) { 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 || + if (target == display.scrollbarH || target == display.scrollbarV || target == display.scrollbarFiller || target == display.gutterFiller) return null; } - var x, y, space = getRect(display.lineSpace); + var x, y, space = display.lineSpace.getBoundingClientRect(); // 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; + try { x = e.clientX - space.left; y = e.clientY - space.top; } + catch (e) { return null; } + var coords = coordsChar(cm, x, y), line; + if (forRect && coords.xRel == 1 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) { + var colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length; + coords = Pos(coords.line, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff); + } + return coords; + } + + // A mouse down can be a single click, double click, triple click, + // start of selection drag, start of text drag, new cursor + // (ctrl-click), rectangle drag (alt-drag), or xwin + // middle-click-paste. Or it might be a click on something we should + // not interfere with, such as a scrollbar or widget. function onMouseDown(e) { if (signalDOMEvent(this, e)) return; - var cm = this, display = cm.display, doc = cm.doc, sel = doc.sel; - sel.shift = e.shiftKey; + var cm = this, display = cm.display; + display.shift = e.shiftKey; if (eventInWidget(display, e)) { if (!webkit) { + // Briefly turn off draggability, to allow widgets to do + // normal dragging things. display.scroller.draggable = false; setTimeout(function(){display.scroller.draggable = true;}, 100); } return; } if (clickInGutter(cm, e)) return; var start = posFromMouse(cm, e); + window.focus(); switch (e_button(e)) { - case 3: - if (captureMiddleClick) onContextMenu.call(cm, cm, e); - return; + case 1: + if (start) + leftButtonDown(cm, e, start); + else if (e_target(e) == display.scroller) + e_preventDefault(e); + break; 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;} - - if (!cm.state.focused) onFocus(cm); - - var now = +new Date, type = "single"; - if (lastDoubleClick && lastDoubleClick.time > now - 400 && posEq(lastDoubleClick.pos, start)) { + break; + case 3: + if (captureRightClick) onContextMenu(cm, e); + break; + } + } + + var lastClick, lastDoubleClick; + function leftButtonDown(cm, e, start) { + setTimeout(bind(ensureFocus, cm), 0); + + var now = +new Date, type; + if (lastDoubleClick && lastDoubleClick.time > now - 400 && cmp(lastDoubleClick.pos, start) == 0) { 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)) { + } else if (lastClick && lastClick.time > now - 400 && cmp(lastClick.pos, start) == 0) { 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); + } else { + type = "single"; + lastClick = {time: now, pos: start}; + } + + var sel = cm.doc.sel, addNew = mac ? e.metaKey : e.ctrlKey; + if (cm.options.dragDrop && dragAndDrop && !addNew && !isReadOnly(cm) && + type == "single" && sel.contains(start) > -1 && sel.somethingSelected()) + leftButtonStartDrag(cm, e, start); + else + leftButtonSelect(cm, e, start, type, addNew); + } + + // Start a text drag. When it ends, see if any dragging actually + // happen, and treat as a click if it didn't. + function leftButtonStartDrag(cm, e, start) { + var display = cm.display; + 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 (ie_upto10 && !ie_upto8) + 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); + } + + // Normal selection, as opposed to text dragging. + function leftButtonSelect(cm, e, start, type, addNew) { + var display = cm.display, doc = cm.doc; + e_preventDefault(e); + + var ourRange, ourIndex, startSel = doc.sel; + if (addNew) { + ourIndex = doc.sel.contains(start); + if (ourIndex > -1) + ourRange = doc.sel.ranges[ourIndex]; + else + ourRange = new Range(start, start); + } else { + ourRange = doc.sel.primary(); + } + + if (e.altKey) { + type = "rect"; + if (!addNew) ourRange = new Range(start, start); + start = posFromMouse(cm, e, true, true); + ourIndex = -1; + } else if (type == "double") { + var word = findWordAt(doc, start); + if (cm.display.shift || doc.extend) + ourRange = extendRange(doc, ourRange, word.anchor, word.head); + else + ourRange = word; + } else if (type == "triple") { + var line = new Range(Pos(start.line, 0), clipPos(doc, Pos(start.line + 1, 0))); + if (cm.display.shift || doc.extend) + ourRange = extendRange(doc, ourRange, line.anchor, line.head); + else + ourRange = line; + } else { + ourRange = extendRange(doc, ourRange, start); + } + + if (!addNew) { + ourIndex = 0; + setSelection(doc, new Selection([ourRange], 0), sel_mouse); + } else if (ourIndex > -1) { + replaceOneSelection(doc, ourIndex, ourRange, sel_mouse); + } else { + ourIndex = doc.sel.ranges.length; + setSelection(doc, normalizeSelection(doc.sel.ranges.concat([ourRange]), ourIndex), + {scroll: false, origin: "*mouse"}); + } + + var lastPos = start; + function extendTo(pos) { + if (cmp(lastPos, pos) == 0) return; + lastPos = pos; + + if (type == "rect") { + var ranges = [], tabSize = cm.options.tabSize; + var startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize); + var posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize); + var left = Math.min(startCol, posCol), right = Math.max(startCol, posCol); + for (var line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line)); + line <= end; line++) { + var text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize); + if (left == right) + ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos))); + else if (text.length > leftPos) + ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize)))); } - }); - // 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); + if (!ranges.length) ranges.push(new Range(start, start)); + setSelection(doc, normalizeSelection(startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex), sel_mouse); + } else { + var oldRange = ourRange; + var anchor = oldRange.anchor, head = pos; + if (type != "single") { + if (type == "double") + var range = findWordAt(doc, pos); + else + var range = new Range(Pos(pos.line, 0), clipPos(doc, Pos(pos.line + 1, 0))); + if (cmp(range.anchor, anchor) > 0) { + head = range.head; + anchor = minPos(oldRange.from(), range.anchor); + } else { + head = range.anchor; + anchor = maxPos(oldRange.to(), range.head); + } + } + var ranges = startSel.ranges.slice(0); + ranges[ourIndex] = new Range(clipPos(doc, anchor), head); + setSelection(doc, normalizeSelection(ranges, ourIndex), sel_mouse); + } + } + + var editorSize = display.wrapper.getBoundingClientRect(); // 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); + var cur = posFromMouse(cm, e, true, type == "rect"); if (!cur) return; - if (!posEq(cur, last)) { - if (!cm.state.focused) onFocus(cm); - last = cur; - doSelect(cur); + if (cmp(cur, lastPos) != 0) { + ensureFocus(cm); + extendTo(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; @@ -1841,139 +2664,147 @@ window.CodeMirror = (function() { } function done(e) { counter = Infinity; e_preventDefault(e); focusInput(cm); off(document, "mousemove", move); off(document, "mouseup", up); + doc.history.lastSelOrigin = null; } var move = operation(cm, function(e) { - if (!old_ie && !e_button(e)) done(e); + if ((ie && !ie_upto9) ? !e.buttons : !e_button(e)) done(e); else extend(e); }); var up = operation(cm, done); on(document, "mousemove", move); on(document, "mouseup", up); } + // Determines whether an event happened in the gutter, and fires the + // handlers for the corresponding event. 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 (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) return false; if (prevent) e_preventDefault(e); var display = cm.display; - var lineBox = getRect(display.lineDiv); + var lineBox = display.lineDiv.getBoundingClientRect(); 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) { + if (g && g.getBoundingClientRect().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)))) + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return; e_preventDefault(e); - if (ie) lastDrop = +new Date; + if (ie_upto10) lastDrop = +new Date; var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files; if (!pos || isReadOnly(cm)) return; + // Might be a file drop, in which case we simply extract the text + // and insert it. 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"); + var change = {from: pos, to: pos, text: splitLines(text.join("\n")), origin: "paste"}; + makeChange(cm.doc, change); + setSelectionReplaceHistory(cm.doc, simpleSelection(pos, changeEnd(change))); } }; reader.readAsText(file); }; for (var i = 0; i < n; ++i) loadFile(files[i], i); - } else { + } else { // Normal drop // 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))) { + if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) { 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"); + var selected = cm.state.draggingText && cm.listSelections(); + setSelectionNoUndo(cm.doc, simpleSelection(pos, pos)); + if (selected) for (var i = 0; i < selected.length; ++i) + replaceRange(cm.doc, "", selected[i].anchor, selected[i].head, "drag"); + cm.replaceSelection(text, "around", "paste"); focusInput(cm); } } catch(e){} } } function onDragStart(cm, e) { - if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return; } + if (ie_upto10 && (!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); + e.dataTransfer.setData("Text", cm.getSelection()); // 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) { + if (presto) { 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); - } - } - + if (presto) img.parentNode.removeChild(img); + } + } + + // SCROLL EVENTS + + // Sync the scrollable area and scrollbars, ensure the viewport + // covers the visible area. function setScrollTop(cm, val) { if (Math.abs(cm.doc.scrollTop - val) < 2) return; cm.doc.scrollTop = val; - if (!gecko) updateDisplay(cm, [], val); + if (!gecko) updateDisplay(cm, {top: 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, []); + if (gecko) updateDisplay(cm); startWorker(cm, 100); } + // Sync scroller and scrollbar, ensure the gutter elements are + // aligned. 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; } @@ -1989,17 +2820,17 @@ window.CodeMirror = (function() { // 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; + 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; @@ -2010,45 +2841,49 @@ window.CodeMirror = (function() { 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; + outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) { + for (var i = 0; i < view.length; i++) { + if (view[i].node == cur) { + cm.display.currentWheelTarget = cur; + break outer; + } } } } // 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 (dx && !gecko && !presto && 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; } + // 'Project' the visible viewport to cover the area that is being + // scrolled into view (if we know enough to estimate it). 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}); + 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; @@ -2062,46 +2897,51 @@ window.CodeMirror = (function() { ++wheelSamples; }, 200); } else { display.wheelDX += dx; display.wheelDY += dy; } } } + // KEY EVENTS + + // Run a handler that was bound to a key. 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; + var prevShift = cm.display.shift, done = false; try { if (isReadOnly(cm)) cm.state.suppressEdits = true; - if (dropShift) doc.sel.shift = false; + if (dropShift) cm.display.shift = false; done = bound(cm) != Pass; } finally { - doc.sel.shift = prevShift; + cm.display.shift = prevShift; cm.state.suppressEdits = false; } return done; } + // Collect the currently active keymaps. 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; + // Handle a key from the keydown event. function handleKeyBinding(cm, e) { - // Handle auto keymap transitions + // Handle automatic 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); @@ -2121,202 +2961,234 @@ window.CodeMirror = (function() { }); } 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; } + // Handle a key from the keypress event 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; + ensureFocus(cm); + if (signalDOMEvent(cm, e)) return; // IE does strange things with escape. - cm.doc.sel.shift = code == 16 || e.shiftKey; - // First give onKeyEvent option a chance to handle this. + if (ie_upto10 && e.keyCode == 27) e.returnValue = false; + var code = e.keyCode; + cm.display.shift = code == 16 || e.shiftKey; var handled = handleKeyBinding(cm, e); - if (opera) { + if (presto) { 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(""); - } + cm.replaceSelection("", null, "cut"); + } + } + + function onKeyUp(e) { + if (signalDOMEvent(this, e)) return; + if (e.keyCode == 16) this.doc.sel.shift = false; } function onKeyPress(e) { var cm = this; - if (signalDOMEvent(cm, e) || cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return; + if (signalDOMEvent(cm, 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; + if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;} + if (((presto && (!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; + if (ie && !ie_upto8) cm.display.inputHasSelection = null; fastPoll(cm); } + // FOCUS/BLUR EVENTS + 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); + resetInput(cm); if (webkit) setTimeout(bind(resetInput, cm, true), 0); // Issue #1730 } } 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); - } + setTimeout(function() {if (!cm.state.focused) cm.display.shift = false;}, 150); + } + + // CONTEXT MENU HANDLING var detectingSelectAll; + // To make the context menu work, we need to briefly unhide the + // textarea (making it as unobtrusive as possible) to let the + // right-click take effect on it. function onContextMenu(cm, e) { if (signalDOMEvent(cm, e, "contextmenu")) return; - var display = cm.display, sel = cm.doc.sel; + var display = cm.display; if (eventInWidget(display, e) || contextMenuInGutter(cm, e)) return; var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop; - if (!pos || opera) return; // Opera is difficult. + if (!pos || presto) 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); + if (reset && cm.doc.sel.contains(pos) == -1) + operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll); 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);"; + "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: " + + (ie ? "rgba(255, 255, 255, .05)" : "transparent") + + "; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; focusInput(cm); - resetInput(cm, true); + resetInput(cm); // Adds "Select all" to context menu in FF - if (posEq(sel.from, sel.to)) display.input.value = display.prevInput = " "; - + if (!cm.somethingSelected()) display.input.value = display.prevInput = " "; + + // Select-all will be greyed out if there's nothing to select, so + // this adds a zero-width space so that we can later check whether + // it got selected. function prepareSelectAllHack() { if (display.input.selectionStart != null) { - var extval = display.input.value = "\u200b" + (posEq(sel.from, sel.to) ? "" : display.input.value); + var extval = display.input.value = "\u200b" + (cm.somethingSelected() ? display.input.value : ""); display.prevInput = "\u200b"; 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; + if (ie_upto8) display.scrollbarV.scrollTop = display.scroller.scrollTop = scrollPos; slowPoll(cm); // Try to detect the user choosing select-all if (display.input.selectionStart != null) { - if (!old_ie || ie_lt9) prepareSelectAllHack(); + if (!ie || ie_upto8) 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 (old_ie && !ie_lt9) prepareSelectAllHack(); - if (captureMiddleClick) { + if (ie && !ie_upto8) prepareSelectAllHack(); + if (captureRightClick) { e_stop(e); var mouseup = function() { off(window, "mouseup", mouseup); setTimeout(rehide, 20); }; on(window, "mouseup", mouseup); } else { setTimeout(rehide, 50); } } + function contextMenuInGutter(cm, e) { + if (!hasHandler(cm, "gutterContextMenu")) return false; + return gutterEvent(cm, e, "gutterContextMenu", false, signal); + } + // UPDATING + // Compute the position of the end of a change (its 'to' property + // refers to the pre-change end). 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)}; - } - + // Adjust a position to refer to the post-change position of the + // same text, or the end of the change if the change covers it. + function adjustForChange(pos, change) { + if (cmp(pos, change.from) < 0) return pos; + if (cmp(pos, change.to) <= 0) return changeEnd(change); + + var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch; + if (pos.line == change.to.line) ch += changeEnd(change).ch - change.to.ch; + return Pos(line, ch); + } + + function computeSelAfterChange(doc, change) { + var out = []; + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + out.push(new Range(adjustForChange(range.anchor, change), + adjustForChange(range.head, change))); + } + return normalizeSelection(out, doc.sel.primIndex); + } + + function offsetPos(pos, old, nw) { + if (pos.line == old.line) + return Pos(nw.line, pos.ch - old.ch + nw.ch); + else + return Pos(nw.line + (pos.line - old.line), pos.ch); + } + + // Used by replaceSelections to allow moving the selection to the + // start or around the replaced test. Hint may be "start" or "around". + function computeReplacedSel(doc, changes, hint) { + var out = []; + var oldPrev = Pos(doc.first, 0), newPrev = oldPrev; + for (var i = 0; i < changes.length; i++) { + var change = changes[i]; + var from = offsetPos(change.from, oldPrev, newPrev); + var to = offsetPos(changeEnd(change), oldPrev, newPrev); + oldPrev = change.to; + newPrev = to; + if (hint == "around") { + var range = doc.sel.ranges[i], inv = cmp(range.head, range.anchor) < 0; + out[i] = new Range(inv ? to : from, inv ? from : to); + } else { + out[i] = new Range(from, from); + } + } + return new Selection(out, doc.sel.primIndex); + } + + // Allow "beforeChange" event handlers to influence a change 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; } @@ -2329,107 +3201,135 @@ window.CodeMirror = (function() { }; 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) { + // Apply a change to a document, and add it to the document's + // history, and propagating it to all linked documents. + function makeChange(doc, change, ignoreReadOnly) { if (doc.cm) { - if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, selUpdate, ignoreReadOnly); + if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, 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); + for (var i = split.length - 1; i >= 0; --i) + makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text}); } 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); + makeChangeInner(doc, change); + } + } + + function makeChangeInner(doc, change) { + if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) return; + var selAfter = computeSelAfterChange(doc, change); + addChangeToHistory(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) { + // Revert a change stored in a document's history. + function makeChangeFromHistory(doc, type, allowSelectionOnly) { 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); + var hist = doc.history, event, selAfter = doc.sel; + var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done; + + // Verify that there is a useable event (so that ctrl-z won't + // needlessly clear selection events) + for (var i = 0; i < source.length; i++) { + event = source[i]; + if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges) + break; + } + if (i == source.length) return; + hist.lastOrigin = hist.lastSelOrigin = null; + + for (;;) { + event = source.pop(); + if (event.ranges) { + pushSelectionToHistory(event, dest); + if (allowSelectionOnly && !event.equals(doc.sel)) { + setSelection(doc, event, {clearRedo: false}); + return; + } + selAfter = event; + } + else break; + } + + // Build up a reverse change object to add to the opposite history + // stack (redo when undoing, and vice versa). + var antiChanges = []; + pushSelectionToHistory(selAfter, dest); + dest.push({changes: antiChanges, generation: hist.generation}); 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; + source.length = 0; return; } - anti.changes.push(historyChangeFromChange(doc, change)); - - var after = i ? computeSelAfterChange(doc, change, null) - : {anchor: event.anchorBefore, head: event.headBefore}; + antiChanges.push(historyChangeFromChange(doc, change)); + + var after = i ? computeSelAfterChange(doc, change, null) : lst(source); makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change)); + if (doc.cm) ensureCursorVisible(doc.cm); var rebased = []; + // Propagate to the linked documents 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)); }); } } + // Sub-views need their line numbers shifted when text is added + // above or below them in the parent document. 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); - } - + doc.sel = new Selection(map(doc.sel.ranges, function(range) { + return new Range(Pos(range.anchor.line + distance, range.anchor.ch), + Pos(range.head.line + distance, range.head.ch)); + }), doc.sel.primIndex); + if (doc.cm) regChange(doc.cm, doc.first, doc.first - distance, distance); + } + + // More lower-level change function, handling only a single document + // (not linked ones). 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; } @@ -2446,244 +3346,104 @@ window.CodeMirror = (function() { 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) { + if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans); + else updateDoc(doc, change, spans); + setSelectionNoUndo(doc, selAfter, sel_dontScroll); + } + + // Handle the interaction of a change to a document with the editor + // that this document is part of. + function makeChangeSingleDocInEditor(cm, change, spans) { 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))); + checkWidthStart = lineNo(visualLine(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)) + if (doc.sel.contains(change.from, change.to) > -1) cm.curOp.cursorActivity = true; - updateDoc(doc, change, spans, selAfter, estimateHeight(cm)); + updateDoc(doc, change, spans, estimateHeight(cm)); if (!cm.options.lineWrapping) { doc.iter(checkWidthStart, from.line + change.text.length, function(line) { - var len = lineLength(doc, line); + var len = lineLength(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; - } + if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change)) + regLineChange(cm, from.line, "text"); + else + regChange(cm, from.line, to.line + 1, lendiff); + + if (hasHandler(cm, "change") || hasHandler(cm, "changes")) + (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push({ + from: from, to: to, + text: change.text, + removed: change.removed, + origin: change.origin + }); } function replaceRange(doc, code, from, to, origin) { if (!to) to = from; - if (posLess(to, from)) { var tmp = to; to = from; from = tmp; } + if (cmp(to, from) < 0) { 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; - } - } - setSelection(doc, anchor, pos, bias); - } else { - setSelection(doc, pos, other || pos, bias); - } - if (doc.cm) doc.cm.curOp.userSelChange = 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; + makeChange(doc, {from: from, to: to, text: code, origin: origin}); + } + + // SCROLLING THINGS INTO VIEW + + // If an editor sits on the top or bottom of the window, partially + // scrolled out of view, this ensures that the cursor is visible. + function maybeScrollWindow(cm, coords) { + var display = cm.display, box = display.sizer.getBoundingClientRect(), 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.top - display.viewOffset - paddingTop(cm.display)) + "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); } } + // Scroll a given position into view (immediately), verifying that + // it actually became visible (as line heights are accurately + // measured, the position of something may 'drift' during drawing). 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), @@ -2696,72 +3456,114 @@ window.CodeMirror = (function() { if (scrollPos.scrollLeft != null) { setScrollLeft(cm, scrollPos.scrollLeft); if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) changed = true; } if (!changed) return coords; } } + // Scroll a given set of coordinates into view (immediately). 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); } + // Calculate a new scroll position needed to scroll the given + // rectangle into view. Returns an object with scrollTop and + // scrollLeft properties. When these are undefined, the + // vertical/horizontal position does not need to be adjusted. 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 screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop; + var screen = display.scroller.clientHeight - scrollerCutOff, 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; + var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft; + var screenw = display.scroller.clientWidth - scrollerCutOff; 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}; - } - + // Store a relative adjustment to the scroll position in the current + // operation (to be applied when the operation finishes). 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)); + if (left != null || top != null) resolveScrollToPos(cm); + if (left != null) + cm.curOp.scrollLeft = (cm.curOp.scrollLeft == null ? cm.doc.scrollLeft : cm.curOp.scrollLeft) + left; + if (top != null) + cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top; + } + + // Make sure that at the end of the operation the current cursor is + // shown. + function ensureCursorVisible(cm) { + resolveScrollToPos(cm); + var cur = cm.getCursor(), from = cur, to = cur; + if (!cm.options.lineWrapping) { + from = cur.ch ? Pos(cur.line, cur.ch - 1) : cur; + to = Pos(cur.line, cur.ch + 1); + } + cm.curOp.scrollToPos = {from: from, to: to, margin: cm.options.cursorScrollMargin, isCursor: true}; + } + + // When an operation has its scrollToPos property set, and another + // scroll action is applied before the end of the operation, this + // 'simulates' scrolling that position into view in a cheap way, so + // that the effect of intermediate scroll commands is not ignored. + function resolveScrollToPos(cm) { + var range = cm.curOp.scrollToPos; + if (range) { + cm.curOp.scrollToPos = null; + var from = estimateCoords(cm, range.from), to = estimateCoords(cm, range.to); + var sPos = calculateScrollPos(cm, Math.min(from.left, to.left), + Math.min(from.top, to.top) - range.margin, + Math.max(from.right, to.right), + Math.max(from.bottom, to.bottom) + range.margin); + cm.scrollTo(sPos.scrollLeft, sPos.scrollTop); + } } // API UTILITIES + // Indent the given line. The how parameter can be "smart", + // "add"/null, "subtract", or "prev". When aggressive is false + // (typically set to true for forced single-line indents), empty + // lines are not indented, and places where the mode returns Pass + // are left alone. function indentLine(cm, n, how, aggressive) { - var doc = cm.doc; + var doc = cm.doc, state; if (how == null) how = "add"; if (how == "smart") { + // Fall back to "prev" when the mode doesn't have an indentation + // method. if (!cm.doc.mode.indent) how = "prev"; - else var state = getStateBefore(cm, n); + else state = getStateBefore(cm, n); } var tabSize = cm.options.tabSize; var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize); + if (line.stateAfter) line.stateAfter = null; 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; @@ -2780,33 +3582,80 @@ window.CodeMirror = (function() { } 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) + 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); + } else { + // Ensure that, if the cursor was in the whitespace at the start + // of the line, it is moved to the end of that space. + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + if (range.head.line == n && range.head.ch < curSpaceString.length) { + var pos = Pos(n, curSpaceString.length); + replaceOneSelection(doc, i, new Range(pos, pos)); + break; + } + } + } line.stateAfter = null; } - function changeLine(cm, handle, op) { + // Utility for applying a change to a line by handle or number, + // returning the number and optionally registering the line as + // changed. + function changeLine(cm, handle, changeType, 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); + if (op(line, no)) regLineChange(cm, no, changeType); else return null; return line; } + // Helper for deleting text near the selection(s), used to implement + // backspace, delete, and similar functionality. + function deleteNearSelection(cm, compute) { + var ranges = cm.doc.sel.ranges, kill = []; + // Build up a set of ranges to kill first, merging overlapping + // ranges. + for (var i = 0; i < ranges.length; i++) { + var toKill = compute(ranges[i]); + while (kill.length && cmp(toKill.from, lst(kill).to) <= 0) { + var replaced = kill.pop(); + if (cmp(replaced.from, toKill.from) < 0) { + toKill.from = replaced.from; + break; + } + } + kill.push(toKill); + } + // Next, remove those actual ranges. + runInOp(cm, function() { + for (var i = kill.length - 1; i >= 0; i--) + replaceRange(cm.doc, "", kill[i].from, kill[i].to, "+delete"); + ensureCursorVisible(cm); + }); + } + + // Used for horizontal relative motion. Dir is -1 or 1 (left or + // right), unit can be "char", "column" (like char, but doesn't + // cross line boundaries), "word" (across next word), or "group" (to + // the start of next group of word or non-word-non-whitespace + // chars). The visually param controls whether, in right-to-left + // text, direction 1 means to move towards the next index in the + // string, or towards the character to the right of the current + // position. The resulting position will have a hitSide=true + // property if it reached the end of the document. 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; @@ -2826,32 +3675,37 @@ window.CodeMirror = (function() { 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 + : group && cur == "\n" ? "n" + : !group || /\s/.test(cur) ? null : "p"; + if (group && !first && !type) type = "s"; 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; } + // For relative vertical movement. Dir may be -1 or 1. Unit can be + // "page" or "line". The resulting position will have a hitSide=true + // property if it reached the end of the document. 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; } @@ -2859,38 +3713,41 @@ window.CodeMirror = (function() { 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) { + // Find the word at the given position (as returned by coordsChar). + function findWordAt(doc, pos) { + var line = getLine(doc, pos.line).text; 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' + return new Range(Pos(pos.line, start), Pos(pos.line, end)); + } + + // EDITOR METHODS + + // The publicly visible API. Note that methodOp(f) means + // 'wrap f in an operation, performed on its `this` parameter'. + + // This is not the complete set of editor methods. Most of the + // methods defined on the Doc type are also injected into + // CodeMirror.prototype, for backwards compatibility and + // convenience. 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; @@ -2909,48 +3766,59 @@ window.CodeMirror = (function() { 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) { + addOverlay: methodOp(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) { + removeOverlay: methodOp(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) { + indentLine: methodOp(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); + indentSelection: methodOp(function(how) { + var ranges = this.doc.sel.ranges, end = -1; + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + if (!range.empty()) { + var start = Math.max(end, range.from().line); + var to = range.to(); + end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1; + for (var j = start; j < end; ++j) + indentLine(this, j, how); + } else if (range.head.line > end) { + indentLine(this, range.head.line, how, true); + end = range.head.line; + if (i == this.doc.sel.primIndex) ensureCursorVisible(this); + } + } }), // 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; @@ -2958,17 +3826,16 @@ window.CodeMirror = (function() { 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; @@ -3017,20 +3884,20 @@ window.CodeMirror = (function() { 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; + var pos, range = this.doc.sel.primary(); + if (start == null) pos = range.head; else if (typeof start == "object") pos = clipPos(this.doc, start); - else pos = start ? sel.from : sel.to; + else pos = start ? range.from() : range.to(); return cursorCoords(this, pos, mode || "page"); }, charCoords: function(pos, mode) { return charCoords(this, clipPos(this.doc, pos), mode || "page"); }, coordsChar: function(coords, mode) { @@ -3042,71 +3909,71 @@ window.CodeMirror = (function() { 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); + return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page").top + + (end ? this.doc.height - heightAtLine(lineObj) : 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) { + setGutterMarker: methodOp(function(line, gutterID, value) { + return changeLine(this, line, "gutter", 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) { + clearGutter: methodOp(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); + regLineChange(cm, i, "gutter"); if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null; } ++i; }); }), - addLineClass: operation(null, function(handle, where, cls) { - return changeLine(this, handle, function(line) { + addLineClass: methodOp(function(handle, where, cls) { + return changeLine(this, handle, "class", 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) { + removeLineClass: methodOp(function(handle, where, cls) { + return changeLine(this, handle, "class", 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) { + addLineWidget: methodOp(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; @@ -3117,17 +3984,17 @@ window.CodeMirror = (function() { 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};}, + getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo};}, 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") { @@ -3152,168 +4019,189 @@ window.CodeMirror = (function() { 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), + triggerOnKeyDown: methodOp(onKeyDown), + triggerOnKeyPress: methodOp(onKeyPress), + triggerOnKeyUp: methodOp(onKeyUp), execCommand: function(cmd) { if (commands.hasOwnProperty(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); + moveH: methodOp(function(dir, unit) { + var cm = this; + cm.extendSelectionsBy(function(range) { + if (cm.display.shift || cm.doc.extend || range.empty()) + return findPosH(cm.doc, range.head, dir, unit, cm.options.rtlMoveVisually); + else + return dir < 0 ? range.from() : range.to(); + }, sel_move); + }), + + deleteH: methodOp(function(dir, unit) { + var sel = this.doc.sel, doc = this.doc; + if (sel.somethingSelected()) + doc.replaceSelection("", null, "+delete"); 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; + deleteNearSelection(this, function(range) { + var other = findPosH(doc, range.head, dir, unit, false); + return dir < 0 ? {from: other, to: range.head} : {from: range.head, to: other}; + }); }), 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, 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; - } - extendSelection(this.doc, target, target, dir); - if (goal != null) sel.goalColumn = goal; + moveV: methodOp(function(dir, unit) { + var cm = this, doc = this.doc, goals = []; + var collapse = !cm.display.shift && !doc.extend && doc.sel.somethingSelected(); + doc.extendSelectionsBy(function(range) { + if (collapse) + return dir < 0 ? range.from() : range.to(); + var headPos = cursorCoords(cm, range.head, "div"); + if (range.goalColumn != null) headPos.left = range.goalColumn; + goals.push(headPos.left); + var pos = findPosV(cm, headPos, dir, unit); + if (unit == "page" && range == doc.sel.primary()) + addToScrollPos(cm, null, charCoords(cm, pos, "div").top - headPos.top); + return pos; + }, sel_move); + if (goals.length) for (var i = 0; i < doc.sel.ranges.length; i++) + doc.sel.ranges[i].goalColumn = goals[i]; }), toggleOverwrite: function(value) { if (value != null && value == this.state.overwrite) return; if (this.state.overwrite = !this.state.overwrite) - this.display.cursor.className += " CodeMirror-overwrite"; + this.display.cursorDiv.className += " CodeMirror-overwrite"; else - this.display.cursor.className = this.display.cursor.className.replace(" CodeMirror-overwrite", ""); + this.display.cursorDiv.className = this.display.cursorDiv.className.replace(" CodeMirror-overwrite", ""); + + signal(this, "overwriteToggle", this, this.state.overwrite); }, - hasFocus: function() { return this.state.focused; }, - - scrollTo: operation(null, function(x, y) { - updateScrollPos(this, x, y); + hasFocus: function() { return activeElt() == this.display.input; }, + + scrollTo: methodOp(function(x, y) { + if (x != null || y != null) resolveScrollToPos(this); + if (x != null) this.curOp.scrollLeft = x; + if (y != null) this.curOp.scrollTop = 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}; + scrollIntoView: methodOp(function(range, margin) { + if (range == null) { + range = {from: this.doc.sel.primary().head, to: null}; + if (margin == null) margin = this.options.cursorScrollMargin; + } 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; + range.margin = margin || 0; + 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)}; - } - 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); + resolveScrollToPos(this); + this.curOp.scrollToPos = range; + } else { + var sPos = calculateScrollPos(this, Math.min(range.from.left, range.to.left), + Math.min(range.from.top, range.to.top) - range.margin, + Math.max(range.from.right, range.to.right), + Math.max(range.from.bottom, range.to.bottom) + range.margin); + this.scrollTo(sPos.scrollLeft, sPos.scrollTop); + } }), - setSize: operation(null, function(width, height) { + setSize: methodOp(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; + if (this.options.lineWrapping) clearLineMeasurementCache(this); this.curOp.forceUpdate = true; + signal(this, "refresh", this); }), operation: function(f){return runInOp(this, f);}, - refresh: operation(null, function() { - var badHeight = this.display.cachedTextHeight == null; - clearCaches(this); - updateScrollPos(this, this.doc.scrollLeft, this.doc.scrollTop); + refresh: methodOp(function() { + var oldHeight = this.display.cachedTextHeight; regChange(this); - if (badHeight) estimateLineHeights(this); + clearCaches(this); + this.scrollTo(this.doc.scrollLeft, this.doc.scrollTop); + if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5) + estimateLineHeights(this); + signal(this, "refresh", this); }), - swapDoc: operation(null, function(doc) { + swapDoc: methodOp(function(doc) { var old = this.doc; old.cm = null; attachDoc(this, doc); clearCaches(this); - resetInput(this, true); - updateScrollPos(this, doc.scrollLeft, doc.scrollTop); + resetInput(this); + this.scrollTo(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. var defaults = CodeMirror.defaults = {}; + // Functions to run when options are changed. + var optionHandlers = CodeMirror.optionHandlers = {}; 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; } + // Passed to option handlers when there is no old value. 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) { @@ -3340,19 +4228,16 @@ window.CodeMirror = (function() { 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(); @@ -3370,76 +4255,83 @@ window.CodeMirror = (function() { 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); + if (!val) resetInput(cm); } }); - option("disableInput", false, function(cm, val) {if (!val) resetInput(cm, true);}, true); + option("disableInput", false, function(cm, val) {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, 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("undoDepth", 200, function(cm, val){cm.doc.history.undoDepth = val;}); + option("historyEventDelay", 1250); 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 = {}; + // Extra arguments are stored as the mode's dependencies, which is + // used by (legacy) mechanisms like loadmode.js to automatically + // load a mode. (Preferred mechanism is the require/define calls.) 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; }; + // Given a MIME type, a {name, ...options} config object, or a name + // string, return a mode config object. 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]; + if (typeof found == "string") found = {name: found}; 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"}; }; + // Given a mode spec (anything that resolveMode accepts), find and + // initialize an actual mode object. 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) { @@ -3451,21 +4343,24 @@ window.CodeMirror = (function() { 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; }; + // Minimal default mode. CodeMirror.defineMode("null", function() { return {token: function(stream) {stream.skipToEnd();}}; }); CodeMirror.defineMIME("text/plain", "null"); + // This can be used to attach properties to mode objects from + // outside the actual mode definition. var modeExtensions = CodeMirror.modeExtensions = {}; CodeMirror.extendMode = function(mode, properties) { var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {}); copyObj(properties, exts); }; // EXTENSIONS @@ -3485,97 +4380,118 @@ window.CodeMirror = (function() { 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}); }; - // 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) { + // Utility functions for working with state. Exported because nested + // modes need to do this for their inner modes. + + var copyState = CodeMirror.copyState = function(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) { + }; + + var startState = CodeMirror.startState = function(mode, a1, a2) { return mode.startState ? mode.startState(a1, a2) : true; - } - CodeMirror.startState = startState; - + }; + + // Given a mode and a state (for that mode), find the inner mode and + // state at the position that the state refers to. 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 + // Commands are parameter-less actions that can be performed on an + // editor, mostly used for keybindings. var commands = CodeMirror.commands = { - selectAll: function(cm) {cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()));}, + selectAll: function(cm) {cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll);}, + singleSelection: function(cm) { + cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll); + }, 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"); + deleteNearSelection(cm, function(range) { + if (range.empty()) { + var len = getLine(cm.doc, range.head.line).text.length; + if (range.head.ch == len && range.head.line < cm.lastLine()) + return {from: range.head, to: Pos(range.head.line + 1, 0)}; + else + return {from: range.head, to: Pos(range.head.line, len)}; + } else { + return {from: range.from(), to: range.to()}; + } + }); }, deleteLine: function(cm) { - var l = cm.getCursor().line; - cm.replaceRange("", Pos(l, 0), Pos(l), "+delete"); + deleteNearSelection(cm, function(range) { + return {from: Pos(range.from().line, 0), + to: clipPos(cm.doc, Pos(range.to().line + 1, 0))}; + }); }, delLineLeft: function(cm) { - var cur = cm.getCursor(); - cm.replaceRange("", Pos(cur.line, 0), cur, "+delete"); + deleteNearSelection(cm, function(range) { + return {from: Pos(range.from().line, 0), to: range.from()}; + }); }, undo: function(cm) {cm.undo();}, redo: function(cm) {cm.redo();}, + undoSelection: function(cm) {cm.undoSelection();}, + redoSelection: function(cm) {cm.redoSelection();}, 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)); + cm.extendSelectionsBy(function(range) { return lineStart(cm, range.head.line); }, sel_move); }, 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); + cm.extendSelectionsBy(function(range) { + var start = lineStart(cm, range.head.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 = range.head.line == start.line && range.head.ch <= firstNonWS && range.head.ch; + return Pos(start.line, inWS ? 0 : firstNonWS); + } + return start; + }, sel_move); }, goLineEnd: function(cm) { - cm.extendSelection(lineEnd(cm, cm.getCursor().line)); + cm.extendSelectionsBy(function(range) { return lineEnd(cm, range.head.line); }, sel_move); }, 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")); + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); + }, sel_move); }, goLineLeft: function(cm) { - var top = cm.charCoords(cm.getCursor(), "div").top + 5; - cm.extendSelection(cm.coordsChar({left: 0, top: top}, "div")); + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + return cm.coordsChar({left: 0, top: top}, "div"); + }, sel_move); }, 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");}, @@ -3588,140 +4504,157 @@ window.CodeMirror = (function() { 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"); - }, + insertTab: function(cm) {cm.replaceSelection("\t");}, defaultTab: function(cm) { if (cm.somethingSelected()) cm.indentSelection("add"); - else cm.replaceSelection("\t", "end", "+input"); + else cm.execCommand("insertTab"); }, 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)); + runInOp(cm, function() { + var ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text; + 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); - })(); + runInOp(cm, function() { + var len = cm.listSelections().length; + for (var i = 0; i < len; i++) { + var range = cm.listSelections()[i]; + cm.replaceRange("\n", range.anchor, range.head, "+input"); + cm.indentLine(range.from().line + 1, null, true); + ensureCursorVisible(cm); + } + }); }, 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" + "Enter": "newlineAndIndent", "Insert": "toggleOverwrite", + "Esc": "singleSelection" }; // Note that the save and find-related commands aren't defined by - // default. Unknown commands are simply ignored. + // default. User code or addons can define them. 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-Home": "goDocStart", "Ctrl-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", + "Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection", 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", + "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", fallthrough: ["basic", "emacsy"] }; - keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; + // Very basic readline/emacs-style bindings, which are standard on Mac. 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["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; // KEYMAP DISPATCH function getKeyMap(val) { if (typeof val == "string") return keyMap[val]; else return val; } - function lookupKey(name, maps, handle) { + // Given an array of keymaps and a key name, call handle on any + // bindings found, until that returns a truthy value, at which point + // we consider the key handled. Implements things like binding a key + // to false stopping further handling and keymap fallthrough. + var lookupKey = CodeMirror.lookupKey = function(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) { + for (var i = 0; i < fallthrough.length; ++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) { + }; + + // Modifier key presses don't count as 'real' key presses for the + // purpose of keymap fallthrough. + var isModifierKey = CodeMirror.isModifierKey = function(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; + }; + + // Look up the name of a key as indicated by an event object. + var keyName = CodeMirror.keyName = function(event, noShift) { + if (presto && 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) {} + var hasFocus = activeElt(); 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. @@ -3757,24 +4690,23 @@ window.CodeMirror = (function() { 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) { + var StringStream = CodeMirror.StringStream = function(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 == 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++); @@ -3829,96 +4761,122 @@ window.CodeMirror = (function() { }, 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; // TEXTMARKERS - function TextMarker(doc, type) { + // Created with markText and setBookmark methods. A TextMarker is a + // handle that can be used to clear or find a marked position in the + // document. Line objects hold arrays (markedSpans) containing + // {from, to, marker} object pointing to such marker objects, and + // indicating that such a marker is present on that line. Multiple + // lines may point to the same marker when it spans across lines. + // The spans will have null for their from/to properties when the + // marker continues beyond the start/end of the line. Markers have + // links back to the lines they currently touch. + + var TextMarker = CodeMirror.TextMarker = function(doc, type) { this.lines = []; this.type = type; this.doc = doc; - } - CodeMirror.TextMarker = TextMarker; + }; eventMixin(TextMarker); + // Clear the marker. 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); + if (cm && !this.collapsed) regLineChange(cm, lineNo(line), "text"); + else if (cm) { + if (span.to != null) max = lineNo(line); + if (span.from != null) min = lineNo(line); + } line.markedSpans = removeMarkedSpan(line.markedSpans, span); - if (span.from != null) - min = lineNo(line); - else if (this.collapsed && !lineIsHidden(this.doc, line) && cm) + if (span.from == null && 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); + var visual = visualLine(this.lines[i]), len = lineLength(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); + if (min != null && cm && this.collapsed) 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 (cm) reCheckSelection(cm.doc); + } + if (cm) signalLater(cm, "markerCleared", cm, this); if (withOp) endOperation(cm); }; - TextMarker.prototype.find = function(bothSides) { + // Find the position of the marker in the document. Returns a {from, + // to} object by default. Side can be passed to get a specific side + // -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the + // Pos objects returned contain a line object, rather than a line + // number (used to prevent looking up the same line twice). + TextMarker.prototype.find = function(side, lineObj) { + if (side == null && this.type == "bookmark") side = 1; 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; + if (span.from != null) { + from = Pos(lineObj ? line : lineNo(line), span.from); + if (side == -1) return from; + } + if (span.to != null) { + to = Pos(lineObj ? line : lineNo(line), span.to); + if (side == 1) return to; + } + } return from && {from: from, to: to}; }; + // Signals that the marker's widget changed, and surrounding layout + // should be recomputed. TextMarker.prototype.changed = function() { - var pos = this.find(), cm = this.doc.cm; + var pos = this.find(-1, true), widget = this, 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; - }); - } + runInOp(cm, function() { + var line = pos.line, lineN = lineNo(pos.line); + var view = findViewForLine(cm, lineN); + if (view) { + clearLineMeasurementCacheFor(view); + cm.curOp.selectionChanged = cm.curOp.forceUpdate = true; + } + cm.curOp.updateMaxLine = true; + if (!lineIsHidden(widget.doc, line) && widget.height != null) { + var oldHeight = widget.height; + widget.height = null; + var dHeight = widgetHeight(widget) - oldHeight; + if (dHeight) + updateLineHeight(line, line.height + dHeight); + } + }); }; 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); } @@ -3927,168 +4885,199 @@ window.CodeMirror = (function() { 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); } }; + // Collapsed markers have unique ids, in order to be able to order + // them, which is needed for uniquely determining an outer marker + // when they overlap (they may nest, but not partially overlap). var nextMarkerId = 0; + // Create a marker, wire it up to the right lines, and function markText(doc, from, to, options, type) { + // Shared markers (across linked documents) are handled separately + // (markTextShared will call out to this again, once per + // document). if (options && options.shared) return markTextShared(doc, from, to, options, type); + // Ensure we are in an operation. if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type); - var marker = new TextMarker(doc, type); + var marker = new TextMarker(doc, type), diff = cmp(from, to); if (options) copyObj(options, marker); - if (posLess(to, from) || posEq(from, to) && marker.clearWhenEmpty !== false) + // Don't connect empty markers unless clearWhenEmpty is false + if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false) return marker; if (marker.replacedWith) { + // Showing up as a widget implies collapsed (widget replaces text) marker.collapsed = true; - marker.replacedWith = elt("span", [marker.replacedWith], "CodeMirror-widget"); - if (!options.handleMouseEvents) marker.replacedWith.ignoreEvents = true; + marker.widgetNode = elt("span", [marker.replacedWith], "CodeMirror-widget"); + if (!options.handleMouseEvents) marker.widgetNode.ignoreEvents = true; + if (options.insertLeft) marker.widgetNode.insertLeft = 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); + addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, 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) + if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(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); + addMarkedSpan(line, new MarkedSpan(marker, + curLine == from.line ? from.ch : null, + curLine == to.line ? to.ch : null)); ++curLine; }); + // lineIsHidden depends on the presence of the spans, so needs a second pass 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) { + // Sync editor state if (updateMaxLine) cm.curOp.updateMaxLine = true; - if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.collapsed) + if (marker.collapsed) regChange(cm, from.line, to.line + 1); - if (marker.atomic) reCheckSelection(cm); + else if (marker.className || marker.title || marker.startStyle || marker.endStyle) + for (var i = from.line; i <= to.line; i++) regLineChange(cm, i, "text"); + if (marker.atomic) reCheckSelection(cm.doc); + signalLater(cm, "markerAdded", cm, marker); } return marker; } // SHARED TEXTMARKERS - function SharedTextMarker(markers, primary) { + // A shared marker spans multiple linked documents. It is + // implemented as a meta-marker-object controlling multiple normal + // markers. + var SharedTextMarker = CodeMirror.SharedTextMarker = function(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(); + SharedTextMarker.prototype.find = function(side, lineObj) { + return this.primary.find(side, lineObj); }; 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; + var widget = options.widgetNode; linkedDocs(doc, function(doc) { - if (widget) options.replacedWith = widget.cloneNode(true); + if (widget) options.widgetNode = 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 MarkedSpan(marker, from, to) { + this.marker = marker; + this.from = from; this.to = to; + } + + // Search an array of spans for a span matching the given marker. function getMarkedSpanFor(spans, marker) { if (spans) for (var i = 0; i < spans.length; ++i) { var span = spans[i]; if (span.marker == marker) return span; } } + // Remove a span from an array, returning undefined if no spans are + // left (we don't store arrays for lines without spans). function removeMarkedSpan(spans, span) { for (var r, i = 0; i < spans.length; ++i) if (spans[i] != span) (r || (r = [])).push(spans[i]); return r; } + // Add a span to a line. function addMarkedSpan(line, span) { line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]; span.marker.attachLine(line); } + // Used for the algorithm that adjusts markers for a change in the + // document. These functions cut an array of spans at a given + // character position, returning an array of remaining chunks (or + // undefined if nothing remains). 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}); + (nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to)); } } 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}); + (nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh, + span.to == null ? null : span.to - endCh)); } } return nw; } + // Given a change object, compute the new set of marker spans that + // cover the line in which the change took place. Removes spans + // entirely within the change, reconnects spans belonging to the + // same marker that appear on both sides of the change, and cuts off + // spans partially within the change. Returns an array of span + // arrays with one element for each line in (after) the change. 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); + var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0; // 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 @@ -4124,34 +5113,40 @@ window.CodeMirror = (function() { 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}); + (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i].marker, null, null)); for (var i = 0; i < gap; ++i) newMarkers.push(gapMarkers); newMarkers.push(last); } return newMarkers; } + // Remove spans that are empty and don't have a clearWhenEmpty + // option of false. 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; } + // Used for un/re-doing changes from the history. Combines the + // result of computing the existing spans with the set of spans that + // existed in the history (so that deleting around a span and then + // undoing brings back the span). 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]; @@ -4164,193 +5159,244 @@ window.CodeMirror = (function() { } } else if (stretchCur) { old[i] = stretchCur; } } return old; } + // Used to 'clip' out readOnly ranges when making a change. 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(); + var mk = markers[i], m = mk.find(0); 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)) + if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) continue; + var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to); + if (dfrom < 0 || !mk.inclusiveLeft && !dfrom) newParts.push({from: p.from, to: m.from}); - if (posLess(m.to, p.to) || !mk.inclusiveRight && posEq(p.to, m.to)) + if (dto > 0 || !mk.inclusiveRight && !dto) newParts.push({from: m.to, to: p.to}); parts.splice.apply(parts, newParts); j += newParts.length - 1; } } return parts; } + // Connect or disconnect spans from a line. + 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; + } + + // Helpers used when computing which overlapping collapsed span + // counts as the larger one. function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0; } function extraRight(marker) { return marker.inclusiveRight ? 1 : 0; } + // Returns a number indicating which of two overlapping collapsed + // spans is larger (and thus includes the other). Falls back to + // comparing ids when the spans cover exactly the same range. 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; } + // Find out whether a line ends or starts in a collapsed span. If + // so, return the marker for that span. 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); } + // Test whether there exists a collapsed span that partially + // overlaps (covers the start or end, but not both) of a new span. + // Such overlap is not allowed. 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 found = sp.marker.find(0); 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) { + // A visual line is a line as drawn on the screen. Folding, for + // example, can cause multiple logical lines to appear on the same + // visual line. This finds the start of the visual line that the + // given line is part of (usually that is the line itself). + function visualLine(line) { var merged; while (merged = collapsedSpanAtStart(line)) - line = getLine(doc, merged.find().from.line); + line = merged.find(-1, true).line; return line; } + // Returns an array of logical lines that continue the visual line + // started by the argument, or undefined if there are no such lines. + function visualLineContinued(line) { + var merged, lines; + while (merged = collapsedSpanAtEnd(line)) { + line = merged.find(1, true).line; + (lines || (lines = [])).push(line); + } + return lines; + } + + // Get the line number of the start of the visual line that the + // given line number is part of. + function visualLineNo(doc, lineN) { + var line = getLine(doc, lineN), vis = visualLine(line); + if (line == vis) return lineN; + return lineNo(vis); + } + // Get the line number of the start of the next visual line after + // the given line. + function visualLineEndNo(doc, lineN) { + if (lineN > doc.lastLine()) return lineN; + var line = getLine(doc, lineN), merged; + if (!lineIsHidden(doc, line)) return lineN; + while (merged = collapsedSpanAtEnd(line)) + line = merged.find(1, true).line; + return lineNo(line) + 1; + } + + // Compute whether a line is hidden. Lines count as hidden when they + // are part of a visual line that starts with another line, or when + // they are entirely covered by collapsed, non-widget span. 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.marker.widgetNode) 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)); + var end = span.marker.find(1, true); + return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.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 && + if (sp.marker.collapsed && !sp.marker.widgetNode && 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 + // Line widgets are block elements displayed above or below a line. + 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); + + function adjustScrollWhenAboveVisible(cm, line, diff) { + if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop)) + addToScrollPos(cm, null, diff); + } + + LineWidget.prototype.clear = function() { + var cm = this.cm, ws = this.line.widgets, line = this.line, no = lineNo(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; + if (!ws.length) line.widgets = null; + var height = widgetHeight(this); + runInOp(cm, function() { + adjustScrollWhenAboveVisible(cm, line, -height); + regLineChange(cm, no, "widget"); + updateLineHeight(line, Math.max(0, line.height - height)); + }); + }; + LineWidget.prototype.changed = function() { + var oldH = this.height, cm = this.cm, line = this.line; 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); - }); + runInOp(cm, function() { + cm.curOp.forceUpdate = true; + adjustScrollWhenAboveVisible(cm, line, diff); + updateLineHeight(line, line.height + diff); + }); + }; function widgetHeight(widget) { if (widget.height != null) return widget.height; - if (!widget.node.parentNode || widget.node.parentNode.nodeType != 1) + if (!contains(document.body, widget.node)) 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) { + changeLine(cm, handle, "widget", 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; + if (!lineIsHidden(cm.doc, line)) { + var aboveVisible = heightAtLine(line) < cm.doc.scrollTop; updateLineHeight(line, line.height + widgetHeight(widget)); - if (aboveVisible) addToScrollPos(cm, 0, widget.height); + if (aboveVisible) addToScrollPos(cm, null, widget.height); + cm.curOp.forceUpdate = true; } return true; }); return widget; } // LINE DATA STRUCTURE @@ -4359,35 +5405,37 @@ window.CodeMirror = (funct