changeset 2877:d6ac3baaa15a

merged beta into stable
author Marcin Kuzminski <marcin@python-works.com>
date Fri, 28 Sep 2012 23:28:10 +0200
parents 79fdfd0afdf8 (current diff) b5b21af9ad28 (diff)
children 3f5d40b9dd99
files CONTRIBUTORS development.ini docs/changelog.rst production.ini rhodecode/__init__.py rhodecode/config/deployment.ini_tmpl rhodecode/lib/helpers.py rhodecode/lib/hooks.py rhodecode/lib/middleware/simplegit.py rhodecode/lib/middleware/simplehg.py rhodecode/model/user.py rhodecode/public/css/style.css setup.py
diffstat 24 files changed, 1369 insertions(+), 1176 deletions(-) [+]
line wrap: on
line diff
--- a/CONTRIBUTORS	Wed Sep 19 23:02:36 2012 +0200
+++ b/CONTRIBUTORS	Fri Sep 28 23:28:10 2012 +0200
@@ -24,4 +24,7 @@
     Indra Talip <indra.talip@gmail.com>
     James Rhodes <jrhodes@redpointsoftware.com.au>
     Dominik Ruf <dominikruf@gmail.com>
-    xpol <xpolife@gmail.com>
\ No newline at end of file
+    xpol <xpolife@gmail.com>
+    Vincent Caron <vcaron@bearstech.com>
+    Zachary Auclair <zach101@gmail.com>
+    Stefan Engel <mail@engel-stefan.de>
--- a/development.ini	Wed Sep 19 23:02:36 2012 +0200
+++ b/development.ini	Fri Sep 28 23:28:10 2012 +0200
@@ -108,6 +108,16 @@
 
 issue_prefix = #
 
+## issue_pat, issue_server_link, issue_prefix can have suffixes to specify
+## multiple patterns, to other issues server, wiki or others
+## below an example how to create a wiki pattern 
+#  #wiki-some-id -> https://mywiki.com/some-id
+
+#issue_pat_wiki = (?:wiki-)(.+)
+#issue_server_link_wiki = https://mywiki.com/{id}
+#issue_prefix_wiki = WIKI-
+
+
 ## instance-id prefix
 ## a prefix key for this instance used for cache invalidation when running 
 ## multiple instances of rhodecode, make sure it's globally unique for 
--- a/docs/changelog.rst	Wed Sep 19 23:02:36 2012 +0200
+++ b/docs/changelog.rst	Fri Sep 28 23:28:10 2012 +0200
@@ -5,6 +5,24 @@
 =========
 
 
+1.4.3 (**2012-09-28**)
+----------------------
+
+news
+++++
+
+- #558 Added config file to hooks extra data
+- bumped mercurial version to 2.3.1
+- #518 added possibility of specifying multiple patterns for issues
+
+fixes
++++++
+
+- fixed #570 explicit users group permissions can overwrite owner permissions
+- fixed #578 set proper PATH with current Python for Git
+  hooks to execute within same Python as RhodeCode 
+- fixed issue with Git bare repos that ends with .git in name
+
 1.4.2 (**2012-09-12**)
 ----------------------
 
--- a/production.ini	Wed Sep 19 23:02:36 2012 +0200
+++ b/production.ini	Fri Sep 28 23:28:10 2012 +0200
@@ -108,6 +108,16 @@
 
 issue_prefix = #
 
+## issue_pat, issue_server_link, issue_prefix can have suffixes to specify
+## multiple patterns, to other issues server, wiki or others
+## below an example how to create a wiki pattern 
+#  #wiki-some-id -> https://mywiki.com/some-id
+
+#issue_pat_wiki = (?:wiki-)(.+)
+#issue_server_link_wiki = https://mywiki.com/{id}
+#issue_prefix_wiki = WIKI-
+
+
 ## instance-id prefix
 ## a prefix key for this instance used for cache invalidation when running 
 ## multiple instances of rhodecode, make sure it's globally unique for 
--- a/rhodecode/__init__.py	Wed Sep 19 23:02:36 2012 +0200
+++ b/rhodecode/__init__.py	Fri Sep 28 23:28:10 2012 +0200
@@ -26,7 +26,7 @@
 import sys
 import platform
 
-VERSION = (1, 4, 2)
+VERSION = (1, 4, 3)
 
 try:
     from rhodecode.lib import get_current_revision
--- a/rhodecode/config/deployment.ini_tmpl	Wed Sep 19 23:02:36 2012 +0200
+++ b/rhodecode/config/deployment.ini_tmpl	Fri Sep 28 23:28:10 2012 +0200
@@ -108,6 +108,16 @@
 
 issue_prefix = #
 
+## issue_pat, issue_server_link, issue_prefix can have suffixes to specify
+## multiple patterns, to other issues server, wiki or others
+## below an example how to create a wiki pattern 
+#  #wiki-some-id -> https://mywiki.com/some-id
+
+#issue_pat_wiki = (?:wiki-)(.+)
+#issue_server_link_wiki = https://mywiki.com/{id}
+#issue_prefix_wiki = WIKI-
+
+
 ## instance-id prefix
 ## a prefix key for this instance used for cache invalidation when running 
 ## multiple instances of rhodecode, make sure it's globally unique for 
--- a/rhodecode/controllers/pullrequests.py	Wed Sep 19 23:02:36 2012 +0200
+++ b/rhodecode/controllers/pullrequests.py	Fri Sep 28 23:28:10 2012 +0200
@@ -50,6 +50,7 @@
 from rhodecode.model.comment import ChangesetCommentsModel
 from rhodecode.model.changeset_status import ChangesetStatusModel
 from rhodecode.model.forms import PullRequestForm
+from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
 
 log = logging.getLogger(__name__)
 
@@ -107,6 +108,13 @@
             log.error('Review not available for GIT REPOS')
             raise HTTPNotFound
 
+        try:
+            org_repo.scm_instance.get_changeset()
+        except EmptyRepositoryError, e:
+            h.flash(h.literal(_('There are no changesets yet')),
+                    category='warning')
+            redirect(url('summary_home', repo_name=org_repo.repo_name))
+
         other_repos_info = {}
 
         c.org_refs = self._get_repo_refs(c.rhodecode_repo)
Binary file rhodecode/i18n/zh_CN/LC_MESSAGES/rhodecode.mo has changed
--- a/rhodecode/i18n/zh_CN/LC_MESSAGES/rhodecode.po	Wed Sep 19 23:02:36 2012 +0200
+++ b/rhodecode/i18n/zh_CN/LC_MESSAGES/rhodecode.po	Fri Sep 28 23:28:10 2012 +0200
@@ -8,8 +8,8 @@
 msgstr ""
 "Project-Id-Version: RhodeCode 1.2.0\n"
 "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2012-09-02 20:30+0200\n"
-"PO-Revision-Date: 2012-09-19 13:27+0800\n"
+"POT-Creation-Date: 2012-09-21 14:39+0800\n"
+"PO-Revision-Date: 2012-09-21 15:20+0800\n"
 "Last-Translator: xpol <xpolife@gmail.com>\n"
 "Language-Team: mikespook\n"
 "Plural-Forms: nplurals=1; plural=0;\n"
@@ -19,9 +19,8 @@
 "Generated-By: Babel 0.9.6\n"
 "X-Generator: Poedit 1.5.3\n"
 "X-Poedit-Basepath: E:\\home\\rhodecode\n"
-"X-Poedit-SourceCharset: UTF-8\n"
-
-#: rhodecode/controllers/changelog.py:94
+
+#: rhodecode/controllers/changelog.py:95
 msgid "All Branches"
 msgstr "所有分支"
 
@@ -39,17 +38,24 @@
 msgstr "%s 行上下文"
 
 #: rhodecode/controllers/changeset.py:333
-#: rhodecode/controllers/changeset.py:348 rhodecode/lib/diffs.py:70
+#: rhodecode/controllers/changeset.py:348 rhodecode/lib/diffs.py:71
 msgid "binary file"
 msgstr "二进制文件"
 
-#: rhodecode/controllers/changeset.py:408
+#: rhodecode/controllers/changeset.py:381
+#: rhodecode/controllers/pullrequests.py:368
+#, python-format
+#| msgid "Last change"
+msgid "Status change -> %s"
+msgstr "状态改变 -> %s"
+
+#: rhodecode/controllers/changeset.py:412
 msgid ""
 "Changing status on a changeset associated witha closed pull request is not "
 "allowed"
 msgstr "不允许修改已关闭拉取请求的修订集状态"
 
-#: rhodecode/controllers/compare.py:69
+#: rhodecode/controllers/compare.py:72
 msgid "There are no changesets yet"
 msgstr "还没有修订集"
 
@@ -90,7 +96,12 @@
 msgid "%s %s feed"
 msgstr "%s %s 订阅"
 
-#: rhodecode/controllers/feed.py:75
+#: rhodecode/controllers/feed.py:67
+#: rhodecode/templates/changeset/changeset.html:119
+msgid "Changeset was too big and was cut off..."
+msgstr "修订集太大已被截断......"
+
+#: rhodecode/controllers/feed.py:81
 msgid "commited on"
 msgstr "提交于"
 
@@ -163,16 +174,16 @@
 msgstr "修订集"
 
 #: rhodecode/controllers/files.py:495 rhodecode/controllers/pullrequests.py:72
-#: rhodecode/controllers/summary.py:232 rhodecode/model/scm.py:543
+#: rhodecode/controllers/summary.py:234 rhodecode/model/scm.py:543
 msgid "Branches"
 msgstr "分支"
 
 #: rhodecode/controllers/files.py:496 rhodecode/controllers/pullrequests.py:76
-#: rhodecode/controllers/summary.py:233 rhodecode/model/scm.py:554
+#: rhodecode/controllers/summary.py:235 rhodecode/model/scm.py:554
 msgid "Tags"
 msgstr "标签"
 
-#: rhodecode/controllers/forks.py:73 rhodecode/controllers/admin/repos.py:90
+#: rhodecode/controllers/forks.py:74 rhodecode/controllers/admin/repos.py:90
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from the "
@@ -181,7 +192,7 @@
 "版本库 %s 没有映射到数据库,可能是从文件系统创建或者重命名,请重启 RhodeCode "
 "以重新扫描版本库"
 
-#: rhodecode/controllers/forks.py:133 rhodecode/controllers/settings.py:72
+#: rhodecode/controllers/forks.py:134 rhodecode/controllers/settings.py:73
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from the "
@@ -190,22 +201,22 @@
 " 版本库 %s 没有映射到数据库,可能是从文件系统创建或者重命名,请重启 "
 "RhodeCode 以重新扫描版本库"
 
-#: rhodecode/controllers/forks.py:167
+#: rhodecode/controllers/forks.py:168
 #, python-format
 msgid "forked %s repository as %s"
 msgstr "版本库 %s 被分支到 %s"
 
-#: rhodecode/controllers/forks.py:181
+#: rhodecode/controllers/forks.py:182
 #, python-format
 msgid "An error occurred during repository forking %s"
 msgstr "在分支版本库 %s 的时候发生错误"
 
-#: rhodecode/controllers/journal.py:202 rhodecode/controllers/journal.py:239
+#: rhodecode/controllers/journal.py:203 rhodecode/controllers/journal.py:240
 msgid "public journal"
 msgstr "公共日志"
 
-#: rhodecode/controllers/journal.py:206 rhodecode/controllers/journal.py:243
-#: rhodecode/templates/base/base.html:220
+#: rhodecode/controllers/journal.py:207 rhodecode/controllers/journal.py:244
+#: rhodecode/templates/base/base.html:229
 msgid "journal"
 msgstr "日志"
 
@@ -226,51 +237,51 @@
 msgid "Bookmarks"
 msgstr "书签"
 
-#: rhodecode/controllers/pullrequests.py:158
+#: rhodecode/controllers/pullrequests.py:174
 msgid "Pull request requires a title with min. 3 chars"
 msgstr "拉取请求的标题至少 3 个字符"
 
-#: rhodecode/controllers/pullrequests.py:160
+#: rhodecode/controllers/pullrequests.py:176
 msgid "error during creation of pull request"
 msgstr "提交拉取请求时发生错误"
 
-#: rhodecode/controllers/pullrequests.py:181
+#: rhodecode/controllers/pullrequests.py:197
 msgid "Successfully opened new pull request"
 msgstr "成功提交拉取请求"
 
-#: rhodecode/controllers/pullrequests.py:184
+#: rhodecode/controllers/pullrequests.py:200
 msgid "Error occurred during sending pull request"
 msgstr "提交拉取请求时发生错误"
 
-#: rhodecode/controllers/pullrequests.py:217
+#: rhodecode/controllers/pullrequests.py:233
 msgid "Successfully deleted pull request"
 msgstr "成功删除拉取请求"
 
-#: rhodecode/controllers/search.py:131
+#: rhodecode/controllers/search.py:132
 msgid "Invalid search query. Try quoting it."
 msgstr "错误的搜索。请尝试用引号包含它。"
 
-#: rhodecode/controllers/search.py:136
+#: rhodecode/controllers/search.py:137
 msgid "There is no index to search in. Please run whoosh indexer"
 msgstr "没有索引用于搜索。请运行 whoosh 索引器"
 
-#: rhodecode/controllers/search.py:140
+#: rhodecode/controllers/search.py:141
 msgid "An error occurred during this search operation"
 msgstr "在搜索操作中发生异常"
 
-#: rhodecode/controllers/settings.py:107
+#: rhodecode/controllers/settings.py:108
 #: rhodecode/controllers/admin/repos.py:266
 #, python-format
 msgid "Repository %s updated successfully"
 msgstr "版本库 %s 成功更新"
 
-#: rhodecode/controllers/settings.py:125
+#: rhodecode/controllers/settings.py:126
 #: rhodecode/controllers/admin/repos.py:284
 #, python-format
 msgid "error occurred during update of repository %s"
 msgstr "在更新版本库 %s 的时候发生错误"
 
-#: rhodecode/controllers/settings.py:143
+#: rhodecode/controllers/settings.py:144
 #: rhodecode/controllers/admin/repos.py:302
 #, python-format
 msgid ""
@@ -280,19 +291,40 @@
 "版本库 %s 没有映射到数据库,可能是从文件系统创建或者重命名,请重启 RhodeCode "
 "以重新扫描版本库"
 
-#: rhodecode/controllers/settings.py:155
+#: rhodecode/controllers/settings.py:156
 #: rhodecode/controllers/admin/repos.py:314
 #, python-format
 msgid "deleted repository %s"
 msgstr "已经删除版本库 %s"
 
-#: rhodecode/controllers/settings.py:159
+#: rhodecode/controllers/settings.py:160
 #: rhodecode/controllers/admin/repos.py:324
 #: rhodecode/controllers/admin/repos.py:330
 #, python-format
 msgid "An error occurred during deletion of %s"
 msgstr "在删除 %s 的时候发生错误"
 
+#: rhodecode/controllers/settings.py:179
+#| msgid "unlock"
+msgid "unlocked"
+msgstr "未锁"
+
+#: rhodecode/controllers/settings.py:182
+#| msgid "unlock"
+msgid "locked"
+msgstr "已锁"
+
+#: rhodecode/controllers/settings.py:184
+#, python-format
+#| msgid "forked %s repository as %s"
+msgid "Repository has been %s"
+msgstr "版本库已经 %s"
+
+#: rhodecode/controllers/settings.py:188
+#: rhodecode/controllers/admin/repos.py:422
+msgid "An error occurred during unlocking"
+msgstr "解锁时发生错误"
+
 #: rhodecode/controllers/summary.py:138
 msgid "No data loaded yet"
 msgstr "数据未加载"
@@ -389,9 +421,9 @@
 #: rhodecode/templates/admin/users_groups/users_group_edit.html:9
 #: rhodecode/templates/admin/users_groups/users_groups.html:9
 #: rhodecode/templates/base/base.html:197
-#: rhodecode/templates/base/base.html:337
-#: rhodecode/templates/base/base.html:339
-#: rhodecode/templates/base/base.html:341
+#: rhodecode/templates/base/base.html:346
+#: rhodecode/templates/base/base.html:348
+#: rhodecode/templates/base/base.html:350
 msgid "Admin"
 msgstr "管理"
 
@@ -465,10 +497,6 @@
 msgid "An error occurred during cache invalidation"
 msgstr "清除缓存时发生错误"
 
-#: rhodecode/controllers/admin/repos.py:422
-msgid "An error occurred during unlocking"
-msgstr "解锁时发生错误"
-
 #: rhodecode/controllers/admin/repos.py:442
 msgid "Updated repository visibility in public journal"
 msgstr "成功更新在公共日志中的可见性"
@@ -477,7 +505,7 @@
 msgid "An error occurred during setting this repository in public journal"
 msgstr "设置版本库到公共日志时发生错误"
 
-#: rhodecode/controllers/admin/repos.py:451 rhodecode/model/validators.py:299
+#: rhodecode/controllers/admin/repos.py:451 rhodecode/model/validators.py:300
 msgid "Token mismatch"
 msgstr "令牌不匹配"
 
@@ -502,109 +530,109 @@
 msgid "An error occurred during this operation"
 msgstr "在搜索操作中发生错误"
 
-#: rhodecode/controllers/admin/repos_groups.py:116
+#: rhodecode/controllers/admin/repos_groups.py:117
 #, python-format
 msgid "created repos group %s"
 msgstr "建立版本库组 %s"
 
-#: rhodecode/controllers/admin/repos_groups.py:129
+#: rhodecode/controllers/admin/repos_groups.py:130
 #, python-format
 msgid "error occurred during creation of repos group %s"
 msgstr "创建版本库组时发生错误 %s"
 
-#: rhodecode/controllers/admin/repos_groups.py:163
+#: rhodecode/controllers/admin/repos_groups.py:164
 #, python-format
 msgid "updated repos group %s"
 msgstr "更新版本库组 %s"
 
-#: rhodecode/controllers/admin/repos_groups.py:176
+#: rhodecode/controllers/admin/repos_groups.py:177
 #, python-format
 msgid "error occurred during update of repos group %s"
 msgstr "更新版本库组时发生错误 %s"
 
-#: rhodecode/controllers/admin/repos_groups.py:194
+#: rhodecode/controllers/admin/repos_groups.py:195
 #, python-format
 msgid "This group contains %s repositores and cannot be deleted"
 msgstr "这个组内有 %s 个版本库因而无法删除"
 
-#: rhodecode/controllers/admin/repos_groups.py:202
+#: rhodecode/controllers/admin/repos_groups.py:203
 #, python-format
 msgid "removed repos group %s"
 msgstr "移除版本库组 %s"
 
-#: rhodecode/controllers/admin/repos_groups.py:208
+#: rhodecode/controllers/admin/repos_groups.py:209
 msgid "Cannot delete this group it still contains subgroups"
 msgstr "不能删除包含子组的组"
 
-#: rhodecode/controllers/admin/repos_groups.py:213
-#: rhodecode/controllers/admin/repos_groups.py:218
+#: rhodecode/controllers/admin/repos_groups.py:214
+#: rhodecode/controllers/admin/repos_groups.py:219
 #, python-format
 msgid "error occurred during deletion of repos group %s"
 msgstr "删除版本库组时发生错误 %s"
 
-#: rhodecode/controllers/admin/repos_groups.py:238
+#: rhodecode/controllers/admin/repos_groups.py:240
 msgid "An error occurred during deletion of group user"
 msgstr "删除组用户时发生错误"
 
-#: rhodecode/controllers/admin/repos_groups.py:258
+#: rhodecode/controllers/admin/repos_groups.py:261
 msgid "An error occurred during deletion of group users groups"
 msgstr "删除版本库组的用户组时发生错误"
 
-#: rhodecode/controllers/admin/settings.py:121
+#: rhodecode/controllers/admin/settings.py:122
 #, python-format
 msgid "Repositories successfully rescanned added: %s,removed: %s"
 msgstr "重新扫描版本库成功,增加 %s, 移除 %s"
 
-#: rhodecode/controllers/admin/settings.py:129
+#: rhodecode/controllers/admin/settings.py:130
 msgid "Whoosh reindex task scheduled"
 msgstr "Whoosh 重新索引任务调度"
 
-#: rhodecode/controllers/admin/settings.py:160
+#: rhodecode/controllers/admin/settings.py:161
 msgid "Updated application settings"
 msgstr "更新应用设置"
 
-#: rhodecode/controllers/admin/settings.py:164
-#: rhodecode/controllers/admin/settings.py:275
+#: rhodecode/controllers/admin/settings.py:165
+#: rhodecode/controllers/admin/settings.py:293
 msgid "error occurred during updating application settings"
 msgstr "更新设置时发生错误"
 
-#: rhodecode/controllers/admin/settings.py:200
+#: rhodecode/controllers/admin/settings.py:201
 msgid "Updated visualisation settings"
 msgstr "成功更新可视化设置"
 
-#: rhodecode/controllers/admin/settings.py:205
+#: rhodecode/controllers/admin/settings.py:206
 msgid "error occurred during updating visualisation settings"
 msgstr "更新可视化设置时发生错误"
 
-#: rhodecode/controllers/admin/settings.py:271
+#: rhodecode/controllers/admin/settings.py:289
 msgid "Updated VCS settings"
 msgstr "成功更新版本控制系统设置"
 
-#: rhodecode/controllers/admin/settings.py:285
+#: rhodecode/controllers/admin/settings.py:303
 msgid "Added new hook"
 msgstr "新建钩子"
 
-#: rhodecode/controllers/admin/settings.py:297
+#: rhodecode/controllers/admin/settings.py:315
 msgid "Updated hooks"
 msgstr "更新钩子"
 
-#: rhodecode/controllers/admin/settings.py:301
+#: rhodecode/controllers/admin/settings.py:319
 msgid "error occurred during hook creation"
 msgstr "创建钩子时发生错误"
 
-#: rhodecode/controllers/admin/settings.py:320
+#: rhodecode/controllers/admin/settings.py:338
 msgid "Email task created"
 msgstr "已创建电子邮件任务"
 
-#: rhodecode/controllers/admin/settings.py:375
+#: rhodecode/controllers/admin/settings.py:393
 msgid "You can't edit this user since it's crucial for entire application"
 msgstr "由于是系统帐号,无法编辑该用户"
 
-#: rhodecode/controllers/admin/settings.py:406
+#: rhodecode/controllers/admin/settings.py:424
 msgid "Your account was updated successfully"
 msgstr "你的帐号已经更新完成"
 
-#: rhodecode/controllers/admin/settings.py:421
+#: rhodecode/controllers/admin/settings.py:439
 #: rhodecode/controllers/admin/users.py:191
 #, python-format
 msgid "error occurred during update of user %s"
@@ -722,198 +750,198 @@
 msgid "You need to be a signed in to view this page"
 msgstr "必须登录才能访问该页面"
 
-#: rhodecode/lib/diffs.py:86
+#: rhodecode/lib/diffs.py:87
 msgid ""
 "Changeset was too big and was cut off, use diff menu to display this diff"
 msgstr "修订集因过大而被截断,可查看原始修订集作为替代"
 
-#: rhodecode/lib/diffs.py:96
+#: rhodecode/lib/diffs.py:97
 msgid "No changes detected"
 msgstr "未发现差异"
 
-#: rhodecode/lib/helpers.py:372
+#: rhodecode/lib/helpers.py:373
 #, python-format
 msgid "%a, %d %b %Y %H:%M:%S"
 msgstr "%Y/%b/%d %H:%M:%S %a"
 
-#: rhodecode/lib/helpers.py:484
+#: rhodecode/lib/helpers.py:485
 msgid "True"
 msgstr "是"
 
-#: rhodecode/lib/helpers.py:488
+#: rhodecode/lib/helpers.py:489
 msgid "False"
 msgstr "否"
 
-#: rhodecode/lib/helpers.py:532
+#: rhodecode/lib/helpers.py:533
 msgid "Changeset not found"
 msgstr "未找到修订集"
 
-#: rhodecode/lib/helpers.py:555
+#: rhodecode/lib/helpers.py:556
 #, python-format
 msgid "Show all combined changesets %s->%s"
 msgstr "显示合并的修订集 %s->%s"
 
-#: rhodecode/lib/helpers.py:561
+#: rhodecode/lib/helpers.py:562
 msgid "compare view"
 msgstr "比较显示"
 
-#: rhodecode/lib/helpers.py:581
+#: rhodecode/lib/helpers.py:582
 msgid "and"
 msgstr "还有"
 
-#: rhodecode/lib/helpers.py:582
+#: rhodecode/lib/helpers.py:583
 #, python-format
 msgid "%s more"
 msgstr "%s 个"
 
-#: rhodecode/lib/helpers.py:583
+#: rhodecode/lib/helpers.py:584
 #: rhodecode/templates/changelog/changelog.html:48
 msgid "revisions"
 msgstr "修订"
 
-#: rhodecode/lib/helpers.py:606
+#: rhodecode/lib/helpers.py:607
 msgid "fork name "
 msgstr "分支名称"
 
-#: rhodecode/lib/helpers.py:620
+#: rhodecode/lib/helpers.py:621
 #: rhodecode/templates/pullrequests/pullrequest_show.html:4
 #: rhodecode/templates/pullrequests/pullrequest_show.html:12
 #, python-format
 msgid "Pull request #%s"
 msgstr "拉取请求 #%s"
 
-#: rhodecode/lib/helpers.py:626
+#: rhodecode/lib/helpers.py:627
 msgid "[deleted] repository"
-msgstr "[删除] 版本库"
-
-#: rhodecode/lib/helpers.py:628 rhodecode/lib/helpers.py:638
+msgstr "[删除]版本库"
+
+#: rhodecode/lib/helpers.py:629 rhodecode/lib/helpers.py:639
 msgid "[created] repository"
-msgstr "[创建] 版本库"
-
-#: rhodecode/lib/helpers.py:630
+msgstr "[创建]版本库"
+
+#: rhodecode/lib/helpers.py:631
 msgid "[created] repository as fork"
-msgstr "[创建] 分支版本库"
-
-#: rhodecode/lib/helpers.py:632 rhodecode/lib/helpers.py:640
+msgstr "[创建]分支版本库"
+
+#: rhodecode/lib/helpers.py:633 rhodecode/lib/helpers.py:641
 msgid "[forked] repository"
-msgstr "[分支] 版本库"
-
-#: rhodecode/lib/helpers.py:634 rhodecode/lib/helpers.py:642
+msgstr "[分支]版本库"
+
+#: rhodecode/lib/helpers.py:635 rhodecode/lib/helpers.py:643
 msgid "[updated] repository"
-msgstr "[更新] 版本库"
-
-#: rhodecode/lib/helpers.py:636
+msgstr "[更新]版本库"
+
+#: rhodecode/lib/helpers.py:637
 msgid "[delete] repository"
-msgstr "[删除] 版本库"
-
-#: rhodecode/lib/helpers.py:644
+msgstr "[删除]版本库"
+
+#: rhodecode/lib/helpers.py:645
 msgid "[created] user"
-msgstr "[创建] 用户"
-
-#: rhodecode/lib/helpers.py:646
+msgstr "[创建]用户"
+
+#: rhodecode/lib/helpers.py:647
 msgid "[updated] user"
-msgstr "[更新] 用户"
-
-#: rhodecode/lib/helpers.py:648
+msgstr "[更新]用户"
+
+#: rhodecode/lib/helpers.py:649
 msgid "[created] users group"
-msgstr "[创建] 用户组"
-
-#: rhodecode/lib/helpers.py:650
+msgstr "[创建]用户组"
+
+#: rhodecode/lib/helpers.py:651
 msgid "[updated] users group"
-msgstr "[更新] 用户组"
-
-#: rhodecode/lib/helpers.py:652
+msgstr "[更新]用户组"
+
+#: rhodecode/lib/helpers.py:653
 msgid "[commented] on revision in repository"
-msgstr "[评论] 了版本库中的修订"
-
-#: rhodecode/lib/helpers.py:654
+msgstr "[评论]了版本库中的修订"
+
+#: rhodecode/lib/helpers.py:655
 msgid "[commented] on pull request for"
-msgstr "[评论] 拉取请求"
-
-#: rhodecode/lib/helpers.py:656
+msgstr "[评论]拉取请求"
+
+#: rhodecode/lib/helpers.py:657
 msgid "[closed] pull request for"
 msgstr "[关闭] 拉取请求"
 
-#: rhodecode/lib/helpers.py:658
+#: rhodecode/lib/helpers.py:659
 msgid "[pushed] into"
-msgstr "[推送] 到"
-
-#: rhodecode/lib/helpers.py:660
+msgstr "[推送]到"
+
+#: rhodecode/lib/helpers.py:661
 msgid "[committed via RhodeCode] into repository"
-msgstr "[通过 RhodeCode 提交] 到版本库"
-
-#: rhodecode/lib/helpers.py:662
+msgstr "[通过 RhodeCode 提交]到版本库"
+
+#: rhodecode/lib/helpers.py:663
 msgid "[pulled from remote] into repository"
-msgstr "[远程拉取] 到版本库"
-
-#: rhodecode/lib/helpers.py:664
+msgstr "[远程拉取]到版本库"
+
+#: rhodecode/lib/helpers.py:665
 msgid "[pulled] from"
-msgstr "[拉取] 自"
-
-#: rhodecode/lib/helpers.py:666
+msgstr "[拉取]自"
+
+#: rhodecode/lib/helpers.py:667
 msgid "[started following] repository"
-msgstr "[开始关注] 版本库"
-
-#: rhodecode/lib/helpers.py:668
+msgstr "[开始关注]版本库"
+
+#: rhodecode/lib/helpers.py:669
 msgid "[stopped following] repository"
-msgstr "[停止关注] 版本库"
-
-#: rhodecode/lib/helpers.py:840
+msgstr "[停止关注]版本库"
+
+#: rhodecode/lib/helpers.py:845
 #, python-format
 msgid " and %s more"
 msgstr "还有 %s 个"
 
-#: rhodecode/lib/helpers.py:844
+#: rhodecode/lib/helpers.py:849
 msgid "No Files"
 msgstr "没有文件"
 
-#: rhodecode/lib/utils2.py:335
+#: rhodecode/lib/utils2.py:352
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
 msgstr[0] "%d 年"
 
-#: rhodecode/lib/utils2.py:336
+#: rhodecode/lib/utils2.py:353
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
 msgstr[0] "%d 月"
 
-#: rhodecode/lib/utils2.py:337
+#: rhodecode/lib/utils2.py:354
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
 msgstr[0] "%d 天"
 
-#: rhodecode/lib/utils2.py:338
+#: rhodecode/lib/utils2.py:355
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
 msgstr[0] "%d 小时"
 
-#: rhodecode/lib/utils2.py:339
+#: rhodecode/lib/utils2.py:356
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
 msgstr[0] "%d 分钟"
 
-#: rhodecode/lib/utils2.py:340
+#: rhodecode/lib/utils2.py:357
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
 msgstr[0] "%d 秒"
 
-#: rhodecode/lib/utils2.py:355
+#: rhodecode/lib/utils2.py:372
 #, python-format
 msgid "%s ago"
 msgstr "%s 之前"
 
-#: rhodecode/lib/utils2.py:357
+#: rhodecode/lib/utils2.py:374
 #, python-format
 msgid "%s and %s ago"
 msgstr "%s 零 %s 之前"
 
-#: rhodecode/lib/utils2.py:360
+#: rhodecode/lib/utils2.py:377
 msgid "just now"
 msgstr "刚才"
 
@@ -926,87 +954,87 @@
 msgid "on line %s"
 msgstr "在 %s 行"
 
-#: rhodecode/model/comment.py:157
+#: rhodecode/model/comment.py:173
 msgid "[Mention]"
 msgstr "[提及]"
 
-#: rhodecode/model/db.py:1140
+#: rhodecode/model/db.py:1164
 msgid "Repository no access"
 msgstr "无版本库访问权限"
 
-#: rhodecode/model/db.py:1141
+#: rhodecode/model/db.py:1165
 msgid "Repository read access"
 msgstr "版本库读取权限"
 
-#: rhodecode/model/db.py:1142
+#: rhodecode/model/db.py:1166
 msgid "Repository write access"
 msgstr "版本库写入权限"
 
-#: rhodecode/model/db.py:1143
+#: rhodecode/model/db.py:1167
 msgid "Repository admin access"
 msgstr "版本库管理权限"
 
-#: rhodecode/model/db.py:1145
+#: rhodecode/model/db.py:1169
 msgid "Repositories Group no access"
 msgstr "无版本库组访问权限"
 
-#: rhodecode/model/db.py:1146
+#: rhodecode/model/db.py:1170
 msgid "Repositories Group read access"
 msgstr "版本库组读取权限"
 
-#: rhodecode/model/db.py:1147
+#: rhodecode/model/db.py:1171
 msgid "Repositories Group write access"
 msgstr "版本库组写入"
 
-#: rhodecode/model/db.py:1148
+#: rhodecode/model/db.py:1172
 msgid "Repositories Group admin access"
 msgstr "版本库组管理权限"
 
-#: rhodecode/model/db.py:1150
+#: rhodecode/model/db.py:1174
 msgid "RhodeCode Administrator"
 msgstr "RhodeCode 管理员"
 
-#: rhodecode/model/db.py:1151
+#: rhodecode/model/db.py:1175
 msgid "Repository creation disabled"
 msgstr "禁用创建版本库"
 
-#: rhodecode/model/db.py:1152
+#: rhodecode/model/db.py:1176
 msgid "Repository creation enabled"
 msgstr "允许创建版本库"
 
-#: rhodecode/model/db.py:1153
+#: rhodecode/model/db.py:1177
 msgid "Repository forking disabled"
 msgstr "禁用分支 版本库"
 
-#: rhodecode/model/db.py:1154
+#: rhodecode/model/db.py:1178
 msgid "Repository forking enabled"
 msgstr "允许分支版本库"
 
-#: rhodecode/model/db.py:1155
+#: rhodecode/model/db.py:1179
 msgid "Register disabled"
 msgstr "禁用注册"
 
-#: rhodecode/model/db.py:1156
+#: rhodecode/model/db.py:1180
 msgid "Register new user with RhodeCode with manual activation"
 msgstr "用手动激活注册新用户"
 
-#: rhodecode/model/db.py:1159
+#: rhodecode/model/db.py:1183
 msgid "Register new user with RhodeCode with auto activation"
 msgstr "用自动激活注册新用户"
 
-#: rhodecode/model/db.py:1579
+#: rhodecode/model/db.py:1611
 msgid "Not Reviewed"
 msgstr "未检视"
 
-#: rhodecode/model/db.py:1580
+#: rhodecode/model/db.py:1612
 msgid "Approved"
 msgstr "已批准"
 
-#: rhodecode/model/db.py:1581
+#: rhodecode/model/db.py:1613
 msgid "Rejected"
 msgstr "驳回"
 
-#: rhodecode/model/db.py:1582
+#: rhodecode/model/db.py:1614
 msgid "Under Review"
 msgstr "检视中"
 
@@ -1052,7 +1080,7 @@
 msgid "commented on pull request"
 msgstr "评论了拉取请求"
 
-#: rhodecode/model/pull_request.py:84
+#: rhodecode/model/pull_request.py:89
 #, python-format
 msgid "%(user)s wants you to review pull request #%(pr_id)s"
 msgstr "%(user)s 想要你检视拉取请求 #%(pr_id)s"
@@ -1083,138 +1111,143 @@
 "由于用户 \"%s\" 拥有版本库 %s 因而无法删除,请修改版本库所有者或删除版本"
 "库。%s"
 
-#: rhodecode/model/validators.py:35 rhodecode/model/validators.py:36
+#: rhodecode/model/validators.py:36 rhodecode/model/validators.py:37
 msgid "Value cannot be an empty list"
 msgstr "值不能为空"
 
-#: rhodecode/model/validators.py:82
+#: rhodecode/model/validators.py:83
 #, python-format
 msgid "Username \"%(username)s\" already exists"
 msgstr "用户名称 %(username)s 已经存在"
 
-#: rhodecode/model/validators.py:84
+#: rhodecode/model/validators.py:85
 #, python-format
 msgid "Username \"%(username)s\" is forbidden"
 msgstr "不允许用户名 \"%(username)s\""
 
-#: rhodecode/model/validators.py:86
+#: rhodecode/model/validators.py:87
 msgid ""
 "Username may only contain alphanumeric characters underscores, periods or "
 "dashes and must begin with alphanumeric character"
 msgstr ""
 "只能使用字母、数字、下划线、小数点或减号作为用户名,且必须由数字或字母开头"
 
-#: rhodecode/model/validators.py:114
+#: rhodecode/model/validators.py:115
 #, python-format
 msgid "Username %(username)s is not valid"
 msgstr "用户名称 %(username)s 无效"
 
-#: rhodecode/model/validators.py:133
+#: rhodecode/model/validators.py:134
 msgid "Invalid users group name"
 msgstr "无效的用户组名"
 
-#: rhodecode/model/validators.py:134
+#: rhodecode/model/validators.py:135
 #, python-format
 msgid "Users group \"%(usersgroup)s\" already exists"
 msgstr "用户组 \"%(usersgroup)s\" 已经存在"
 
-#: rhodecode/model/validators.py:136
+#: rhodecode/model/validators.py:137
 msgid ""
 "users group name may only contain  alphanumeric characters underscores, "
 "periods or dashes and must begin with alphanumeric character"
 msgstr ""
 "只能使用字母、数字、下划线、小数点或减号作为用户组名,且必须由数字或字母开头"
 
-#: rhodecode/model/validators.py:174
+#: rhodecode/model/validators.py:175
 msgid "Cannot assign this group as parent"
 msgstr "不能将这个组作为 parent"
 
-#: rhodecode/model/validators.py:175
+#: rhodecode/model/validators.py:176
 #, python-format
 msgid "Group \"%(group_name)s\" already exists"
 msgstr "组 \"%(group_name)s\" 已经存在"
 
-#: rhodecode/model/validators.py:177
+#: rhodecode/model/validators.py:178
 #, python-format
 msgid "Repository with name \"%(group_name)s\" already exists"
 msgstr "已经存在名为 \"%(group_name)s\" 的版本库"
 
-#: rhodecode/model/validators.py:235
+#: rhodecode/model/validators.py:236
 msgid "Invalid characters (non-ascii) in password"
 msgstr "密码含有无效(非ASCII)字符"
 
-#: rhodecode/model/validators.py:250
+#: rhodecode/model/validators.py:251
 msgid "Passwords do not match"
 msgstr "密码不符"
 
-#: rhodecode/model/validators.py:267
+#: rhodecode/model/validators.py:268
 msgid "invalid password"
 msgstr "无效密码"
 
-#: rhodecode/model/validators.py:268
+#: rhodecode/model/validators.py:269
 msgid "invalid user name"
 msgstr "无效用户名"
 
-#: rhodecode/model/validators.py:269
+#: rhodecode/model/validators.py:270
 msgid "Your account is disabled"
 msgstr "该帐号已被禁用"
 
-#: rhodecode/model/validators.py:313
+#: rhodecode/model/validators.py:314
 #, python-format
 msgid "Repository name %(repo)s is disallowed"
 msgstr "版本库名称不能为 %(repo)s"
 
-#: rhodecode/model/validators.py:315
+#: rhodecode/model/validators.py:316
 #, python-format
 msgid "Repository named %(repo)s already exists"
 msgstr "已经存在版本库 %(repo)s"
 
-#: rhodecode/model/validators.py:316
+#: rhodecode/model/validators.py:317
 #, python-format
 msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
 msgstr "版本库组 \"%(group)s\" 中已经存在版本库 \"%(repo)s\""
 
-#: rhodecode/model/validators.py:318
+#: rhodecode/model/validators.py:319
 #, python-format
 msgid "Repositories group with name \"%(repo)s\" already exists"
 msgstr "已经存在名为 \"%(repo)s\" 的版本库组"
 
-#: rhodecode/model/validators.py:431
+#: rhodecode/model/validators.py:432
 msgid "invalid clone url"
 msgstr "无效的克隆地址"
 
-#: rhodecode/model/validators.py:432
+#: rhodecode/model/validators.py:433
 msgid "Invalid clone url, provide a valid clone http(s)/svn+http(s) url"
 msgstr "无效的克隆地址,提供一个有效的克隆 http(s) 或 svn+http(s) 地址"
 
-#: rhodecode/model/validators.py:457
+#: rhodecode/model/validators.py:458
 msgid "Fork have to be the same type as parent"
 msgstr "分支必须使用和父版本库相同的类型"
 
-#: rhodecode/model/validators.py:478
+#: rhodecode/model/validators.py:473
+#| msgid "You don't have permission to view this page"
+msgid "You don't have permissions to create repository in this group"
+msgstr "没有在这个组里面创建版本库的权限"
+
+#: rhodecode/model/validators.py:498
 msgid "This username or users group name is not valid"
 msgstr "用户或用户组名称无效"
 
-#: rhodecode/model/validators.py:562
+#: rhodecode/model/validators.py:582
 msgid "This is not a valid path"
 msgstr "不是一个合法的路径"
 
-#: rhodecode/model/validators.py:577
+#: rhodecode/model/validators.py:597
 msgid "This e-mail address is already taken"
 msgstr "该邮件地址已被使用"
 
-#: rhodecode/model/validators.py:597
+#: rhodecode/model/validators.py:617
 #, python-format
 msgid "e-mail \"%(email)s\" does not exist."
 msgstr "邮件地址  \"%(email)s\" 不存在"
 
-#: rhodecode/model/validators.py:634
+#: rhodecode/model/validators.py:654
 msgid ""
 "The LDAP Login attribute of the CN must be specified - this is the name of "
 "the attribute that is equivalent to \"username\""
 msgstr "LDAP 登陆属性的 CN 必须指定 - 这个名字作为用户名"
 
-#: rhodecode/model/validators.py:653
+#: rhodecode/model/validators.py:673
 #, python-format
 msgid "Revisions %(revs)s are already part of pull request or have set status"
 msgstr "修订 %(revs)s 已经包含在拉取请求中或者或者已经设置状态"
@@ -1237,9 +1270,9 @@
 
 #: rhodecode/templates/index_base.html:6
 #: rhodecode/templates/admin/repos/repos.html:9
-#: rhodecode/templates/base/base.html:221
+#: rhodecode/templates/base/base.html:230
 msgid "repositories"
-msgstr "个版本库"
+msgstr "版本库"
 
 #: rhodecode/templates/index_base.html:13
 #: rhodecode/templates/index_base.html:15
@@ -1551,7 +1584,7 @@
 #: rhodecode/templates/admin/admin.html:5
 #: rhodecode/templates/admin/admin.html:9
 msgid "Admin journal"
-msgstr "管理员日志"
+msgstr "系统日志"
 
 #: rhodecode/templates/admin/admin_log.html:6
 #: rhodecode/templates/admin/repos/repos.html:74
@@ -1681,8 +1714,8 @@
 msgstr "评论"
 
 #: rhodecode/templates/admin/notifications/notifications.html:31
-#: rhodecode/templates/base/base.html:254
-#: rhodecode/templates/base/base.html:256
+#: rhodecode/templates/base/base.html:263
+#: rhodecode/templates/base/base.html:265
 msgid "Pull requests"
 msgstr "拉取请求"
 
@@ -1753,7 +1786,7 @@
 msgstr "版本库分支"
 
 #: rhodecode/templates/admin/permissions/permissions.html:78
-#: rhodecode/templates/admin/repos/repo_edit.html:241
+#: rhodecode/templates/admin/repos/repo_edit.html:255
 msgid "set"
 msgstr "设置"
 
@@ -1835,7 +1868,7 @@
 msgid ""
 "Private repositories are only visible to people explicitly added as "
 "collaborators."
-msgstr "私有版本库只对显示添加的合作者可见。"
+msgstr "私有版本库只对成员可见。"
 
 #: rhodecode/templates/admin/repos/repo_add_base.html:73
 msgid "add"
@@ -1964,79 +1997,89 @@
 msgid "Confirm to invalidate repository cache"
 msgstr "确认清除版本库缓存"
 
-#: rhodecode/templates/admin/repos/repo_edit.html:195
-#: rhodecode/templates/base/base.html:318
-#: rhodecode/templates/base/base.html:320
-#: rhodecode/templates/base/base.html:322
+#: rhodecode/templates/admin/repos/repo_edit.html:193
+msgid ""
+"Manually invalidate cache for this repository. On first access repository "
+"will be cached again"
+msgstr "手动清除版本库缓存。之后第一次访问的时候将重建缓存"
+
+#: rhodecode/templates/admin/repos/repo_edit.html:198
+msgid "List of cached values"
+msgstr "缓存数据列表"
+
+#: rhodecode/templates/admin/repos/repo_edit.html:209
+#: rhodecode/templates/base/base.html:327
+#: rhodecode/templates/base/base.html:329
+#: rhodecode/templates/base/base.html:331
 msgid "Public journal"
 msgstr "公共日志"
 
-#: rhodecode/templates/admin/repos/repo_edit.html:201
+#: rhodecode/templates/admin/repos/repo_edit.html:215
 msgid "Remove from public journal"
 msgstr "从公共日志删除"
 
-#: rhodecode/templates/admin/repos/repo_edit.html:203
+#: rhodecode/templates/admin/repos/repo_edit.html:217
 msgid "Add to public journal"
 msgstr "添加到公共日志"
 
-#: rhodecode/templates/admin/repos/repo_edit.html:208
+#: rhodecode/templates/admin/repos/repo_edit.html:222
 msgid ""
 "All actions made on this repository will be accessible to everyone in public "
 "journal"
 msgstr "任何人都可以在公共日志上看到这个版本库上的所有动作"
 
-#: rhodecode/templates/admin/repos/repo_edit.html:215
+#: rhodecode/templates/admin/repos/repo_edit.html:229
 msgid "Locking"
 msgstr "锁定"
 
-#: rhodecode/templates/admin/repos/repo_edit.html:220
+#: rhodecode/templates/admin/repos/repo_edit.html:234
 msgid "Unlock locked repo"
 msgstr "解锁版本库"
 
-#: rhodecode/templates/admin/repos/repo_edit.html:220
+#: rhodecode/templates/admin/repos/repo_edit.html:234
 msgid "Confirm to unlock repository"
 msgstr "确认解锁版本库"
 
-#: rhodecode/templates/admin/repos/repo_edit.html:223
+#: rhodecode/templates/admin/repos/repo_edit.html:237
 msgid "lock repo"
 msgstr "锁定版本库"
 
-#: rhodecode/templates/admin/repos/repo_edit.html:223
+#: rhodecode/templates/admin/repos/repo_edit.html:237
 msgid "Confirm to lock repository"
 msgstr "确认锁定版本库"
 
-#: rhodecode/templates/admin/repos/repo_edit.html:224
+#: rhodecode/templates/admin/repos/repo_edit.html:238
 msgid "Repository is not locked"
 msgstr "版本库未锁定"
 
-#: rhodecode/templates/admin/repos/repo_edit.html:229
+#: rhodecode/templates/admin/repos/repo_edit.html:243
 msgid ""
 "Force locking on repository. Works only when anonymous access is disabled"
 msgstr "强制锁定版本库。只有在禁止匿名访问时候才有效"
 
-#: rhodecode/templates/admin/repos/repo_edit.html:236
+#: rhodecode/templates/admin/repos/repo_edit.html:250
 msgid "Set as fork of"
 msgstr "设置 fork 自"
 
-#: rhodecode/templates/admin/repos/repo_edit.html:245
+#: rhodecode/templates/admin/repos/repo_edit.html:259
 msgid "Manually set this repository as a fork of another from the list"
 msgstr "从列表中手动设置这个版本库 fork 自另一版本库"
 
-#: rhodecode/templates/admin/repos/repo_edit.html:251
+#: rhodecode/templates/admin/repos/repo_edit.html:265
 #: rhodecode/templates/changeset/changeset_file_comment.html:26
 msgid "Delete"
 msgstr "删除"
 
-#: rhodecode/templates/admin/repos/repo_edit.html:255
+#: rhodecode/templates/admin/repos/repo_edit.html:269
 msgid "Remove this repository"
 msgstr "删除版本库"
 
-#: rhodecode/templates/admin/repos/repo_edit.html:255
+#: rhodecode/templates/admin/repos/repo_edit.html:269
 #: rhodecode/templates/journal/journal.html:84
 msgid "Confirm to delete this repository"
 msgstr "确认删除版本库"
 
-#: rhodecode/templates/admin/repos/repo_edit.html:259
+#: rhodecode/templates/admin/repos/repo_edit.html:273
 msgid ""
 "This repository will be renamed in a special way in order to be unaccesible "
 "for RhodeCode and VCS systems.\n"
@@ -2064,9 +2107,9 @@
 #: rhodecode/templates/admin/repos/repo_edit_perms.html:6
 #: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:6
 #: rhodecode/templates/admin/users/users.html:85
-#: rhodecode/templates/base/base.html:217
+#: rhodecode/templates/base/base.html:226
 msgid "admin"
-msgstr "管理员"
+msgstr "管理"
 
 #: rhodecode/templates/admin/repos/repo_edit_perms.html:7
 #: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:7
@@ -2099,12 +2142,12 @@
 msgstr "添加成员"
 
 #: rhodecode/templates/admin/repos/repo_edit_perms.html:97
-#: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:81
+#: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:87
 msgid "Failed to remove user"
 msgstr "删除用户失败"
 
 #: rhodecode/templates/admin/repos/repo_edit_perms.html:112
-#: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:96
+#: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:103
 msgid "Failed to remove users group"
 msgstr "删除用户组失败"
 
@@ -2112,11 +2155,45 @@
 msgid "Repositories administration"
 msgstr "版本库管理员"
 
-#: rhodecode/templates/admin/repos_groups/repos_groups.html:8
-msgid "Groups"
-msgstr "组"
-
-#: rhodecode/templates/admin/repos_groups/repos_groups.html:12
+#: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:73
+msgid "apply to children"
+msgstr "应用到成员"
+
+#: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:74
+msgid ""
+"Set or revoke permission to all children of that group, including "
+"repositories and other groups"
+msgstr "授予或者撤销权限所有组成员,包括子组和子版本库"
+
+#: rhodecode/templates/admin/repos_groups/repos_groups.html:9
+#: rhodecode/templates/base/base.html:122
+#: rhodecode/templates/base/base.html:309
+#: rhodecode/templates/base/base.html:311
+#: rhodecode/templates/base/base.html:313
+#: rhodecode/templates/bookmarks/bookmarks.html:11
+#: rhodecode/templates/branches/branches.html:10
+#: rhodecode/templates/changelog/changelog.html:10
+#: rhodecode/templates/changeset/changeset.html:10
+#: rhodecode/templates/changeset/changeset_range.html:9
+#: rhodecode/templates/compare/compare_diff.html:9
+#: rhodecode/templates/files/file_diff.html:8
+#: rhodecode/templates/files/files.html:8
+#: rhodecode/templates/files/files_add.html:15
+#: rhodecode/templates/files/files_edit.html:15
+#: rhodecode/templates/followers/followers.html:9
+#: rhodecode/templates/forks/fork.html:9
+#: rhodecode/templates/forks/forks.html:9
+#: rhodecode/templates/pullrequests/pullrequest.html:8
+#: rhodecode/templates/pullrequests/pullrequest_show.html:8
+#: rhodecode/templates/pullrequests/pullrequest_show_all.html:8
+#: rhodecode/templates/settings/repo_settings.html:9
+#: rhodecode/templates/shortlog/shortlog.html:10
+#: rhodecode/templates/summary/summary.html:8
+#: rhodecode/templates/tags/tags.html:11
+msgid "Home"
+msgstr "首页"
+
+#: rhodecode/templates/admin/repos_groups/repos_groups.html:13
 msgid "with"
 msgstr "有"
 
@@ -2198,7 +2275,7 @@
 #: rhodecode/templates/admin/settings/hooks.html:5
 #: rhodecode/templates/admin/settings/settings.html:5
 msgid "Settings administration"
-msgstr "设置管理员"
+msgstr "系统设置"
 
 #: rhodecode/templates/admin/settings/hooks.html:9
 #: rhodecode/templates/admin/settings/settings.html:9
@@ -2389,6 +2466,7 @@
 msgstr "这是一个关键设置。如果确认修改该项设置,请重启服务以便设置生效。"
 
 #: rhodecode/templates/admin/settings/settings.html:251
+#: rhodecode/templates/base/base.html:218
 msgid "unlock"
 msgstr "解锁"
 
@@ -2616,7 +2694,7 @@
 msgstr "用户管理员"
 
 #: rhodecode/templates/admin/users/users.html:9
-#: rhodecode/templates/base/base.html:223
+#: rhodecode/templates/base/base.html:232
 msgid "users"
 msgstr "用户"
 
@@ -2646,7 +2724,7 @@
 msgstr "启用"
 
 #: rhodecode/templates/admin/users/users.html:86
-#: rhodecode/templates/base/base.html:226
+#: rhodecode/templates/base/base.html:235
 msgid "ldap"
 msgstr "LDAP"
 
@@ -2737,37 +2815,10 @@
 msgid "Inbox"
 msgstr "收件箱"
 
-#: rhodecode/templates/base/base.html:122
-#: rhodecode/templates/base/base.html:300
-#: rhodecode/templates/base/base.html:302
-#: rhodecode/templates/base/base.html:304
-#: rhodecode/templates/bookmarks/bookmarks.html:11
-#: rhodecode/templates/branches/branches.html:10
-#: rhodecode/templates/changelog/changelog.html:10
-#: rhodecode/templates/changeset/changeset.html:10
-#: rhodecode/templates/changeset/changeset_range.html:9
-#: rhodecode/templates/compare/compare_diff.html:9
-#: rhodecode/templates/files/file_diff.html:8
-#: rhodecode/templates/files/files.html:8
-#: rhodecode/templates/files/files_add.html:15
-#: rhodecode/templates/files/files_edit.html:15
-#: rhodecode/templates/followers/followers.html:9
-#: rhodecode/templates/forks/fork.html:9
-#: rhodecode/templates/forks/forks.html:9
-#: rhodecode/templates/pullrequests/pullrequest.html:8
-#: rhodecode/templates/pullrequests/pullrequest_show.html:8
-#: rhodecode/templates/pullrequests/pullrequest_show_all.html:8
-#: rhodecode/templates/settings/repo_settings.html:9
-#: rhodecode/templates/shortlog/shortlog.html:10
-#: rhodecode/templates/summary/summary.html:8
-#: rhodecode/templates/tags/tags.html:11
-msgid "Home"
-msgstr "首页"
-
 #: rhodecode/templates/base/base.html:123
-#: rhodecode/templates/base/base.html:309
-#: rhodecode/templates/base/base.html:311
-#: rhodecode/templates/base/base.html:313
+#: rhodecode/templates/base/base.html:318
+#: rhodecode/templates/base/base.html:320
+#: rhodecode/templates/base/base.html:322
 #: rhodecode/templates/journal/journal.html:4
 #: rhodecode/templates/journal/journal.html:21
 #: rhodecode/templates/journal/public_journal.html:4
@@ -2832,50 +2883,59 @@
 
 #: rhodecode/templates/base/base.html:204
 #: rhodecode/templates/base/base.html:206
-#: rhodecode/templates/base/base.html:227
-msgid "settings"
-msgstr "设置"
-
-#: rhodecode/templates/base/base.html:209
+#| msgid "Repository creation"
+msgid "repository settings"
+msgstr "版本库设置"
+
+#: rhodecode/templates/base/base.html:210
 #: rhodecode/templates/data_table/_dt_elements.html:80
 #: rhodecode/templates/forks/fork.html:13
 msgid "fork"
 msgstr "分支"
 
-#: rhodecode/templates/base/base.html:211
+#: rhodecode/templates/base/base.html:212
 #: rhodecode/templates/changelog/changelog.html:40
 msgid "Open new pull request"
 msgstr "新建拉取请求"
 
-#: rhodecode/templates/base/base.html:213
+#: rhodecode/templates/base/base.html:214
 msgid "search"
 msgstr "搜索"
 
-#: rhodecode/templates/base/base.html:222
+#: rhodecode/templates/base/base.html:220
+#| msgid "unlock"
+msgid "lock"
+msgstr "锁定"
+
+#: rhodecode/templates/base/base.html:231
 msgid "repositories groups"
 msgstr "版本库组"
 
-#: rhodecode/templates/base/base.html:224
+#: rhodecode/templates/base/base.html:233
 msgid "users groups"
 msgstr "用户组"
 
-#: rhodecode/templates/base/base.html:225
+#: rhodecode/templates/base/base.html:234
 msgid "permissions"
 msgstr "权限"
 
-#: rhodecode/templates/base/base.html:238
-#: rhodecode/templates/base/base.html:240
+#: rhodecode/templates/base/base.html:236
+msgid "settings"
+msgstr "设置"
+
+#: rhodecode/templates/base/base.html:247
+#: rhodecode/templates/base/base.html:249
 msgid "Followers"
 msgstr "关注者"
 
-#: rhodecode/templates/base/base.html:246
-#: rhodecode/templates/base/base.html:248
+#: rhodecode/templates/base/base.html:255
+#: rhodecode/templates/base/base.html:257
 msgid "Forks"
 msgstr "分支"
 
-#: rhodecode/templates/base/base.html:327
-#: rhodecode/templates/base/base.html:329
-#: rhodecode/templates/base/base.html:331
+#: rhodecode/templates/base/base.html:336
+#: rhodecode/templates/base/base.html:338
+#: rhodecode/templates/base/base.html:340
 #: rhodecode/templates/search/search.html:52
 msgid "Search"
 msgstr "搜索"
@@ -3107,10 +3167,6 @@
 msgid "%s files affected with %s insertions and %s deletions:"
 msgstr "%s 个文件受影响包括 %s 行插入和 %s 行删除:"
 
-#: rhodecode/templates/changeset/changeset.html:119
-msgid "Changeset was too big and was cut off..."
-msgstr "修订集太大已被截断......"
-
 #: rhodecode/templates/changeset/changeset_file_comment.html:42
 msgid "Submitting..."
 msgstr "提交中……"
@@ -3615,6 +3671,11 @@
 msgid "Closed %s"
 msgstr "关闭于 %s"
 
+#: rhodecode/templates/pullrequests/pullrequest_show.html:23
+#, python-format
+msgid "with status %s"
+msgstr "状态%s"
+
 #: rhodecode/templates/pullrequests/pullrequest_show.html:31
 msgid "Status"
 msgstr "状态"
@@ -3657,11 +3718,10 @@
 msgid "Closed"
 msgstr "已关闭"
 
-# 中文中 repo name 在前面 serch term在后面
 #: rhodecode/templates/search/search.html:6
 #, python-format
 msgid "Search \"%s\" in repository: %s"
-msgstr "在版本库 %2s 中搜索 \"%1s\""
+msgstr "搜索 \"%s\" 于版本库 %s 中"
 
 #: rhodecode/templates/search/search.html:8
 #, python-format
@@ -3892,3 +3952,6 @@
 #, python-format
 msgid "%s Tags"
 msgstr "%s 标签"
+
+#~ msgid "Groups"
+#~ msgstr "组"
--- a/rhodecode/lib/helpers.py	Wed Sep 19 23:02:36 2012 +0200
+++ b/rhodecode/lib/helpers.py	Fri Sep 28 23:28:10 2012 +0200
@@ -1000,16 +1000,32 @@
         return ''.join(links)
 
     # urlify changesets - extrac revisions and make link out of them
-    text_ = urlify_changesets(escaper(text_), repository)
+    newtext = urlify_changesets(escaper(text_), repository)
 
     try:
         conf = config['app_conf']
 
-        URL_PAT = re.compile(r'%s' % conf.get('issue_pat'))
+        # allow multiple issue servers to be used
+        valid_indices = [
+            x.group(1)
+            for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
+            if x and 'issue_server_link%s' % x.group(1) in conf
+            and 'issue_prefix%s' % x.group(1) in conf
+        ]
+
+        log.debug('found issue server suffixes `%s` during valuation of: %s'
+                  % (','.join(valid_indices), newtext))
 
-        if URL_PAT:
-            ISSUE_SERVER_LNK = conf.get('issue_server_link')
-            ISSUE_PREFIX = conf.get('issue_prefix')
+        for pattern_index in valid_indices:
+            ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
+            ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
+            ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
+
+            log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s'
+                      % (pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
+                         ISSUE_PREFIX))
+
+            URL_PAT = re.compile(r'%s' % ISSUE_PATTERN)
 
             def url_func(match_obj):
                 pref = ''
@@ -1027,6 +1043,7 @@
                     url = url.replace('{repo}', repository)
                     repo_name = repository.split(URL_SEP)[-1]
                     url = url.replace('{repo_name}', repo_name)
+
                 return tmpl % {
                      'pref': pref,
                      'cls': 'issue-tracker-link',
@@ -1035,9 +1052,11 @@
                      'issue-prefix': ISSUE_PREFIX,
                      'serv': ISSUE_SERVER_LNK,
                 }
+            newtext = URL_PAT.sub(url_func, newtext)
+            log.debug('processed prefix:`%s` => %s' % (pattern_index, newtext))
 
-            newtext = URL_PAT.sub(url_func, text_)
-
+        # if we actually did something above
+        if valid_indices:
             if link_:
                 # wrap not links into final link => link_
                 newtext = linkify_others(newtext, link_)
@@ -1047,7 +1066,7 @@
         log.error(traceback.format_exc())
         pass
 
-    return text_
+    return newtext
 
 
 def rst(source):
--- a/rhodecode/lib/hooks.py	Wed Sep 19 23:02:36 2012 +0200
+++ b/rhodecode/lib/hooks.py	Fri Sep 28 23:28:10 2012 +0200
@@ -317,8 +317,9 @@
     from rhodecode.model import init_model
     from rhodecode.model.db import RhodeCodeUi
     from rhodecode.lib.utils import make_ui
+    extras = json.loads(env['RHODECODE_EXTRAS'])
 
-    path, ini_name = os.path.split(env['RHODECODE_CONFIG_FILE'])
+    path, ini_name = os.path.split(extras['config'])
     conf = appconfig('config:%s' % ini_name, relative_to=path)
     load_environment(conf.global_conf, conf.local_conf)
 
@@ -327,8 +328,8 @@
 
     baseui = make_ui('db')
     # fix if it's not a bare repo
-    if repo_path.endswith('.git'):
-        repo_path = repo_path[:-4]
+    if repo_path.endswith(os.sep + '.git'):
+        repo_path = repo_path[:-5]
 
     repo = Repository.get_by_full_path(repo_path)
     if not repo:
@@ -337,7 +338,6 @@
 
     _hooks = dict(baseui.configitems('hooks')) or {}
 
-    extras = json.loads(env['RHODECODE_EXTRAS'])
     for k, v in extras.items():
         baseui.setconfig('rhodecode_extras', k, v)
     repo = repo.scm_instance
--- a/rhodecode/lib/middleware/pygrack.py	Wed Sep 19 23:02:36 2012 +0200
+++ b/rhodecode/lib/middleware/pygrack.py	Fri Sep 28 23:28:10 2012 +0200
@@ -2,6 +2,7 @@
 import socket
 import logging
 import subprocess
+import traceback
 
 from webob import Request, Response, exc
 
@@ -90,7 +91,7 @@
                 ]
             )
         except EnvironmentError, e:
-            log.exception(e)
+            log.error(traceback.format_exc())
             raise exc.HTTPExpectationFailed()
         resp = Response()
         resp.content_type = 'application/x-%s-advertisement' % str(git_command)
@@ -118,34 +119,33 @@
 
         try:
             gitenv = os.environ
-            from rhodecode import CONFIG
             from rhodecode.lib.compat import json
             gitenv['RHODECODE_EXTRAS'] = json.dumps(self.extras)
             # forget all configs
             gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
-            # we need current .ini file used to later initialize rhodecode
-            # env and connect to db
-            gitenv['RHODECODE_CONFIG_FILE'] = CONFIG['__file__']
             opts = dict(
                 env=gitenv,
                 cwd=os.getcwd()
             )
+            cmd = r'git %s --stateless-rpc "%s"' % (git_command[4:],
+                                                    self.content_path),
+            log.debug('handling cmd %s' % cmd)
             out = subprocessio.SubprocessIOChunker(
-                r'git %s --stateless-rpc "%s"' % (git_command[4:],
-                                                  self.content_path),
+                cmd,
                 inputstream=inputstream,
                 **opts
             )
         except EnvironmentError, e:
-            log.exception(e)
+            log.error(traceback.format_exc())
             raise exc.HTTPExpectationFailed()
 
         if git_command in [u'git-receive-pack']:
             # updating refs manually after each push.
             # Needed for pre-1.7.0.4 git clients using regular HTTP mode.
-            subprocess.call(u'git --git-dir "%s" '
-                            'update-server-info' % self.content_path,
-                            shell=True)
+            cmd = (u'git --git-dir "%s" '
+                    'update-server-info' % self.content_path)
+            log.debug('handling cmd %s' % cmd)
+            subprocess.call(cmd, shell=True)
 
         resp = Response()
         resp.content_type = 'application/x-%s-result' % git_command.encode('utf8')
@@ -164,9 +164,9 @@
             resp = app(request, environ)
         except exc.HTTPException, e:
             resp = e
-            log.exception(e)
+            log.error(traceback.format_exc())
         except Exception, e:
-            log.exception(e)
+            log.error(traceback.format_exc())
             resp = exc.HTTPInternalServerError()
         return resp(environ, start_response)
 
--- a/rhodecode/lib/middleware/simplegit.py	Wed Sep 19 23:02:36 2012 +0200
+++ b/rhodecode/lib/middleware/simplegit.py	Fri Sep 28 23:28:10 2012 +0200
@@ -79,7 +79,7 @@
 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
     HTTPBadRequest, HTTPNotAcceptable
 
-from rhodecode.lib.utils2 import safe_str
+from rhodecode.lib.utils2 import safe_str, fix_PATH
 from rhodecode.lib.base import BaseVCSController
 from rhodecode.lib.auth import get_container_username
 from rhodecode.lib.utils import is_valid_repo, make_ui
@@ -188,12 +188,14 @@
 
         # extras are injected into UI object and later available
         # in hooks executed by rhodecode
+        from rhodecode import CONFIG
         extras = {
             'ip': ipaddr,
             'username': username,
             'action': action,
             'repository': repo_name,
             'scm': 'git',
+            'config': CONFIG['__file__'],
             'make_lock': None,
             'locked_by': [None, None]
         }
@@ -218,6 +220,7 @@
                            'locked_by': locked_by})
         # set the environ variables for this request
         os.environ['RC_SCM_DATA'] = json.dumps(extras)
+        fix_PATH()
         log.debug('HOOKS extras is %s' % extras)
         baseui = make_ui('db')
         self.__inject_extras(repo_path, baseui, extras)
--- a/rhodecode/lib/middleware/simplehg.py	Wed Sep 19 23:02:36 2012 +0200
+++ b/rhodecode/lib/middleware/simplehg.py	Fri Sep 28 23:28:10 2012 +0200
@@ -27,7 +27,6 @@
 import os
 import logging
 import traceback
-import urllib
 
 from mercurial.error import RepoError
 from mercurial.hgweb import hgweb_mod
@@ -36,7 +35,7 @@
 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
     HTTPBadRequest, HTTPNotAcceptable
 
-from rhodecode.lib.utils2 import safe_str
+from rhodecode.lib.utils2 import safe_str, fix_PATH
 from rhodecode.lib.base import BaseVCSController
 from rhodecode.lib.auth import get_container_username
 from rhodecode.lib.utils import make_ui, is_valid_repo, ui_sections
@@ -152,12 +151,14 @@
 
         # extras are injected into mercurial UI object and later available
         # in hg hooks executed by rhodecode
+        from rhodecode import CONFIG
         extras = {
             'ip': ipaddr,
             'username': username,
             'action': action,
             'repository': repo_name,
             'scm': 'hg',
+            'config': CONFIG['__file__'],
             'make_lock': None,
             'locked_by': [None, None]
         }
@@ -182,6 +183,7 @@
 
         # set the environ variables for this request
         os.environ['RC_SCM_DATA'] = json.dumps(extras)
+        fix_PATH()
         log.debug('HOOKS extras is %s' % extras)
         baseui = make_ui('db')
         self.__inject_extras(repo_path, baseui, extras)
--- a/rhodecode/lib/utils2.py	Wed Sep 19 23:02:36 2012 +0200
+++ b/rhodecode/lib/utils2.py	Fri Sep 28 23:28:10 2012 +0200
@@ -481,3 +481,19 @@
         return self.get(attr, None)
     __setattr__ = dict.__setitem__
     __delattr__ = dict.__delitem__
+
+
+def fix_PATH(os_=None):
+    """
+    Get current active python path, and append it to PATH variable to fix issues
+    of subprocess calls and different python versions
+    """
+    import sys
+    if os_ is None:
+        import os
+    else:
+        os = os_
+
+    cur_path = os.path.split(sys.executable)[0]
+    if not os.environ['PATH'].startswith(cur_path):
+        os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
--- a/rhodecode/model/user.py	Wed Sep 19 23:02:36 2012 +0200
+++ b/rhodecode/model/user.py	Fri Sep 28 23:28:10 2012 +0200
@@ -524,8 +524,12 @@
             p = perm.Permission.permission_name
             cur_perm = user.permissions[RK][r_k]
             # overwrite permission only if it's greater than permission
-            # given from other sources
+            # given from other sources - disabled with `or 1` now
             if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm] or 1:  # disable check
+                if perm.Repository.user_id == uid:
+                    # set admin if owner
+                    p = 'repository.admin'
+
                 user.permissions[RK][r_k] = p
 
         # user explicit permissions for repositories
--- a/rhodecode/public/css/codemirror.css	Wed Sep 19 23:02:36 2012 +0200
+++ b/rhodecode/public/css/codemirror.css	Fri Sep 28 23:28:10 2012 +0200
@@ -9,8 +9,7 @@
 }
 
 .CodeMirror-scroll {
-  overflow-x: auto;
-  overflow-y: hidden;
+  overflow: auto;
   height: 300px;
   /* This is needed to prevent an IE[67] bug where the scrolled content
      is visible outside of the scrolling box. */
@@ -20,13 +19,11 @@
 
 /* Vertical scrollbar */
 .CodeMirror-scrollbar {
-  float: right;
+  position: absolute;
+  right: 0; top: 0;
   overflow-x: hidden;
   overflow-y: scroll;
-
-  /* This corrects for the 1px gap introduced to the left of the scrollbar
-     by the rule for .CodeMirror-scrollbar-inner. */
-  margin-left: -1px;
+  z-index: 5;
 }
 .CodeMirror-scrollbar-inner {
   /* This needs to have a nonzero width in order for the scrollbar to appear
@@ -62,16 +59,13 @@
   text-align: right;
   padding: .4em .2em .4em .4em;
   white-space: pre !important;
+  cursor: default;
 }
 .CodeMirror-lines {
   padding: .4em;
   white-space: pre;
   cursor: text;
 }
-.CodeMirror-lines * {
-  /* Necessary for throw-scrolling to decelerate properly on Safari. */
-  pointer-events: none;
-}
 
 .CodeMirror pre {
   -moz-border-radius: 0;
@@ -151,7 +145,7 @@
 .cm-s-default span.cm-error {color: #f00;}
 .cm-s-default span.cm-qualifier {color: #555;}
 .cm-s-default span.cm-builtin {color: #30a;}
-.cm-s-default span.cm-bracket {color: #cc7;}
+.cm-s-default span.cm-bracket {color: #997;}
 .cm-s-default span.cm-tag {color: #170;}
 .cm-s-default span.cm-attribute {color: #00c;}
 .cm-s-default span.cm-header {color: blue;}
@@ -164,5 +158,16 @@
 span.cm-emstrong {font-style: italic; font-weight: bold;}
 span.cm-link {text-decoration: underline;}
 
+span.cm-invalidchar {color: #f00;}
+
 div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
 div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
+
+@media print {
+
+  /* Hide the cursor when printing */
+  .CodeMirror pre.CodeMirror-cursor {
+    visibility: hidden;
+  }
+
+}
--- a/rhodecode/public/css/style.css	Wed Sep 19 23:02:36 2012 +0200
+++ b/rhodecode/public/css/style.css	Fri Sep 28 23:28:10 2012 +0200
@@ -4262,10 +4262,23 @@
 }
 
 div.comment-inline-form {
-    margin-top: 5px;
-    padding:2px 6px 8px 6px;
-
-}
+    padding:4px 0px 6px 0px;
+}
+
+
+tr.hl-comment{
+/*
+	background-color: #FFFFCC !important;
+*/
+}
+
+/*
+tr.hl-comment pre {
+	border-top: 2px solid #FFEE33;
+	border-left: 2px solid #FFEE33;
+	border-right: 2px solid #FFEE33;
+}
+*/
 
 .comment-inline-form strong {
     display: block;
--- a/rhodecode/public/js/codemirror.js	Wed Sep 19 23:02:36 2012 +0200
+++ b/rhodecode/public/js/codemirror.js	Fri Sep 28 23:28:10 2012 +0200
@@ -3,7 +3,8 @@
 // some utilities are defined.
 
 // CodeMirror is the only global var we claim
-var CodeMirror = (function() {
+window.CodeMirror = (function() {
+  "use strict";
   // This is the function that produces an editor instance. Its
   // closure is used to store the editor state.
   function CodeMirror(place, givenOptions) {
@@ -13,38 +14,33 @@
       if (defaults.hasOwnProperty(opt))
         options[opt] = (givenOptions && givenOptions.hasOwnProperty(opt) ? givenOptions : defaults)[opt];
 
+    var input = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em");
+    input.setAttribute("wrap", "off"); input.setAttribute("autocorrect", "off"); input.setAttribute("autocapitalize", "off");
+    // Wraps and hides input textarea
+    var inputDiv = elt("div", [input], null, "overflow: hidden; position: relative; width: 3px; height: 0px;");
+    // The empty scrollbar content, used solely for managing the scrollbar thumb.
+    var scrollbarInner = elt("div", null, "CodeMirror-scrollbar-inner");
+    // The vertical scrollbar. Horizontal scrolling is handled by the scroller itself.
+    var scrollbar = elt("div", [scrollbarInner], "CodeMirror-scrollbar");
+    // DIVs containing the selection and the actual code
+    var lineDiv = elt("div"), 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
+    var cursor = elt("pre", "\u00a0", "CodeMirror-cursor"), widthForcer = elt("pre", "\u00a0", "CodeMirror-cursor", "visibility: hidden");
+    // Used to measure text size
+    var measure = elt("div", null, null, "position: absolute; width: 100%; height: 0px; overflow: hidden; visibility: hidden;");
+    var lineSpace = elt("div", [measure, cursor, widthForcer, selectionDiv, lineDiv], null, "position: relative; z-index: 0");
+    var gutterText = elt("div", null, "CodeMirror-gutter-text"), gutter = elt("div", [gutterText], "CodeMirror-gutter");
+    // Moved around its parent to cover visible view
+    var mover = elt("div", [gutter, elt("div", [lineSpace], "CodeMirror-lines")], null, "position: relative");
+    // Set to the height of the text, causes scrolling
+    var sizer = elt("div", [mover], null, "position: relative");
+    // Provides scrolling
+    var scroller = elt("div", [sizer], "CodeMirror-scroll");
+    scroller.setAttribute("tabIndex", "-1");
     // The element in which the editor lives.
-    var wrapper = document.createElement("div");
-    wrapper.className = "CodeMirror" + (options.lineWrapping ? " CodeMirror-wrap" : "");
-    // This mess creates the base DOM structure for the editor.
-    wrapper.innerHTML =
-      '<div style="overflow: hidden; position: relative; width: 3px; height: 0px;">' + // Wraps and hides input textarea
-        '<textarea style="position: absolute; padding: 0; width: 1px; height: 1em" wrap="off" ' +
-          'autocorrect="off" autocapitalize="off"></textarea></div>' +
-      '<div class="CodeMirror-scrollbar">' + // The vertical scrollbar. Horizontal scrolling is handled by the scroller itself.
-        '<div class="CodeMirror-scrollbar-inner">' + // The empty scrollbar content, used solely for managing the scrollbar thumb.
-      '</div></div>' + // This must be before the scroll area because it's float-right.
-      '<div class="CodeMirror-scroll" tabindex="-1">' +
-        '<div style="position: relative">' + // Set to the height of the text, causes scrolling
-          '<div style="position: relative">' + // Moved around its parent to cover visible view
-            '<div class="CodeMirror-gutter"><div class="CodeMirror-gutter-text"></div></div>' +
-            // Provides positioning relative to (visible) text origin
-            '<div class="CodeMirror-lines"><div style="position: relative; z-index: 0">' +
-              // Used to measure text size
-              '<div style="position: absolute; width: 100%; height: 0; overflow: hidden; visibility: hidden;"></div>' +
-              '<pre class="CodeMirror-cursor">&#160;</pre>' + // Absolutely positioned blinky cursor
-              '<pre class="CodeMirror-cursor" style="visibility: hidden">&#160;</pre>' + // Used to force a width
-              '<div style="position: relative; z-index: -1"></div><div></div>' + // DIVs containing the selection and the actual code
-            '</div></div></div></div></div>';
+    var wrapper = elt("div", [inputDiv, scrollbar, scroller], "CodeMirror" + (options.lineWrapping ? " CodeMirror-wrap" : ""));
     if (place.appendChild) place.appendChild(wrapper); else place(wrapper);
-    // I've never seen more elegant code in my life.
-    var inputDiv = wrapper.firstChild, input = inputDiv.firstChild,
-        scroller = wrapper.lastChild, code = scroller.firstChild,
-        mover = code.firstChild, gutter = mover.firstChild, gutterText = gutter.firstChild,
-        lineSpace = gutter.nextSibling.firstChild, measure = lineSpace.firstChild,
-        cursor = measure.nextSibling, widthForcer = cursor.nextSibling,
-        selectionDiv = widthForcer.nextSibling, lineDiv = selectionDiv.nextSibling,
-        scrollbar = inputDiv.nextSibling, scrollbarInner = scrollbar.firstChild;
+
     themeChanged(); keyMapChanged();
     // Needed to hide big blue blinking cursor on Mobile Safari
     if (ios) input.style.width = "0px";
@@ -56,33 +52,21 @@
     // Needed to handle Tab key in KHTML
     if (khtml) inputDiv.style.height = "1px", inputDiv.style.position = "absolute";
 
-    // Check for OS X >= 10.7. If so, we need to force a width on the scrollbar, and 
-    // make it overlap the content. (But we only do this if the scrollbar doesn't already
-    // have a natural width. If the mouse is plugged in or the user sets the system pref
-    // to always show scrollbars, the scrollbar shouldn't overlap.)
-    if (mac_geLion) {
-      scrollbar.className += (overlapScrollbars() ? " cm-sb-overlap" : " cm-sb-nonoverlap");
-    } else if (ie_lt8) {
-      // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8).
-      scrollbar.className += " cm-sb-ie7";
-    }
-
-    // Check for problem with IE innerHTML not working when we have a
-    // P (or similar) parent node.
-    try { stringWidth("x"); }
-    catch (e) {
-      if (e.message.match(/runtime/i))
-        e = new Error("A CodeMirror inside a P-style element does not work in Internet Explorer. (innerHTML bug)");
-      throw e;
-    }
+    // Check for OS X >= 10.7. This has transparent scrollbars, so the
+    // overlaying of one scrollbar with another won't work. This is a
+    // temporary hack to simply turn off the overlay scrollbar. See
+    // issue #727.
+    if (mac_geLion) { scrollbar.style.zIndex = -2; scrollbar.style.visibility = "hidden"; }
+    // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8).
+    else if (ie_lt8) scrollbar.style.minWidth = "18px";
 
     // Delayed object wrap timeouts, making sure only one is active. blinker holds an interval.
     var poll = new Delayed(), highlight = new Delayed(), blinker;
 
     // mode holds a mode API object. doc is the tree of Line objects,
-    // work an array of lines that should be parsed, and history the
-    // undo history (instance of History constructor).
-    var mode, doc = new BranchChunk([new LeafChunk([new Line("")])]), work, focused;
+    // frontier is the point up to which the content has been parsed,
+    // and history the undo history (instance of History constructor).
+    var mode, doc = new BranchChunk([new LeafChunk([new Line("")])]), frontier = 0, focused;
     loadMode();
     // The selection. These are always maintained to point at valid
     // positions. Inverted is used to remember that the user is
@@ -90,11 +74,11 @@
     var sel = {from: {line: 0, ch: 0}, to: {line: 0, ch: 0}, inverted: false};
     // Selection-related flags. shiftSelecting obviously tracks
     // whether the user is holding shift.
-    var shiftSelecting, lastClick, lastDoubleClick, lastScrollTop = 0, lastScrollLeft = 0, draggingText,
+    var shiftSelecting, lastClick, lastDoubleClick, lastScrollTop = 0, draggingText,
         overwrite = false, suppressEdits = false;
     // Variables used by startOperation/endOperation to track what
     // happened during the operation.
-    var updateInput, userSelChange, changes, textChanged, selectionChanged, leaveInputAlone,
+    var updateInput, userSelChange, changes, textChanged, selectionChanged,
         gutterDirty, callbacks;
     // Current visible range (may be bigger than the view window).
     var displayOffset = 0, showingFrom = 0, showingTo = 0, lastSizeC = 0;
@@ -103,8 +87,9 @@
     var bracketHighlighted;
     // Tracks the maximum line length so that the horizontal scrollbar
     // can be kept static when scrolling.
-    var maxLine = "", updateMaxLine = false, maxLineChanged = true;
-    var tabCache = {};
+    var maxLine = getLine(0), updateMaxLine = false, maxLineChanged = true;
+    var pollingFast = false; // Ensures slowPoll doesn't cancel fastPoll
+    var goalColumn = null;
 
     // Initialize the content.
     operation(function(){setValue(options.value || ""); updateInput = false;})();
@@ -118,12 +103,13 @@
     // which point we can't mess with it anymore. Context menu is
     // handled in onMouseDown for Gecko.
     if (!gecko) connect(scroller, "contextmenu", onContextMenu);
-    connect(scroller, "scroll", onScroll);
-    connect(scrollbar, "scroll", onScroll);
-    connect(scrollbar, "mousedown", function() {setTimeout(focusInput, 0);});
-    connect(scroller, "mousewheel", onMouseWheel);
-    connect(scroller, "DOMMouseScroll", onMouseWheel);
-    connect(window, "resize", function() {updateDisplay(true);});
+    connect(scroller, "scroll", onScrollMain);
+    connect(scrollbar, "scroll", onScrollBar);
+    connect(scrollbar, "mousedown", function() {if (focused) setTimeout(focusInput, 0);});
+    var resizeHandler = connect(window, "resize", function() {
+      if (wrapper.parentNode) updateDisplay(true);
+      else resizeHandler();
+    }, true);
     connect(input, "keyup", operation(onKeyUp));
     connect(input, "input", fastPoll);
     connect(input, "keydown", operation(onKeyDown));
@@ -131,12 +117,12 @@
     connect(input, "focus", onFocus);
     connect(input, "blur", onBlur);
 
+    function drag_(e) {
+      if (options.onDragEvent && options.onDragEvent(instance, addStop(e))) return;
+      e_stop(e);
+    }
     if (options.dragDrop) {
       connect(scroller, "dragstart", onDragStart);
-      function drag_(e) {
-        if (options.onDragEvent && options.onDragEvent(instance, addStop(e))) return;
-        e_stop(e);
-      }
       connect(scroller, "dragenter", drag_);
       connect(scroller, "dragover", drag_);
       connect(scroller, "drop", operation(onDrop));
@@ -148,7 +134,7 @@
     }));
 
     // Needed to handle Tab key in KHTML
-    if (khtml) connect(code, "mouseup", function() {
+    if (khtml) connect(sizer, "mouseup", function() {
         if (document.activeElement == input) input.blur();
         focusInput();
     });
@@ -181,12 +167,14 @@
         else if (option == "lineWrapping" && oldVal != value) operation(wrappingChanged)();
         else if (option == "tabSize") updateDisplay(true);
         else if (option == "keyMap") keyMapChanged();
-        if (option == "lineNumbers" || option == "gutter" || option == "firstLineNumber" || option == "theme") {
+        if (option == "lineNumbers" || option == "gutter" || option == "firstLineNumber" ||
+            option == "theme" || option == "lineNumberFormatter") {
           gutterChanged();
           updateDisplay(true);
         }
       },
       getOption: function(option) {return options[option];},
+      getMode: function() {return mode;},
       undo: operation(undo),
       redo: operation(redo),
       indentLine: operation(function(n, dir) {
@@ -199,10 +187,29 @@
       indentSelection: operation(indentSelected),
       historySize: function() {return {undo: history.done.length, redo: history.undone.length};},
       clearHistory: function() {history = new History();},
+      setHistory: function(histData) {
+        history = new History();
+        history.done = histData.done;
+        history.undone = histData.undone;
+      },
+      getHistory: function() {
+        function cp(arr) {
+          for (var i = 0, nw = [], nwelt; i < arr.length; ++i) {
+            nw.push(nwelt = []);
+            for (var j = 0, elt = arr[i]; j < elt.length; ++j) {
+              var old = [], cur = elt[j];
+              nwelt.push({start: cur.start, added: cur.added, old: old});
+              for (var k = 0; k < cur.old.length; ++k) old.push(hlText(cur.old[k]));
+            }
+          }
+          return nw;
+        }
+        return {done: cp(history.done), undone: cp(history.undone)};
+      },
       matchBrackets: operation(function(){matchBrackets(true);}),
       getTokenAt: operation(function(pos) {
         pos = clipPos(pos);
-        return getLine(pos.line).getTokenAt(mode, getStateBefore(pos.line), pos.ch);
+        return getLine(pos.line).getTokenAt(mode, getStateBefore(pos.line), options.tabSize, pos.ch);
       }),
       getStateAfter: function(line) {
         line = clipLine(line == null ? doc.size - 1: line);
@@ -239,15 +246,16 @@
         return line;
       },
       lineInfo: lineInfo,
+      getViewport: function() { return {from: showingFrom, to: showingTo};},
       addWidget: function(pos, node, scroll, vert, horiz) {
         pos = localCoords(clipPos(pos));
         var top = pos.yBot, left = pos.x;
         node.style.position = "absolute";
-        code.appendChild(node);
+        sizer.appendChild(node);
         if (vert == "over") top = pos.y;
         else if (vert == "near") {
           var vspace = Math.max(scroller.offsetHeight, doc.height * textHeight()),
-              hspace = Math.max(code.clientWidth, lineSpace.clientWidth) - paddingLeft();
+              hspace = Math.max(sizer.clientWidth, lineSpace.clientWidth) - paddingLeft();
           if (pos.yBot + node.offsetHeight > vspace && pos.y > node.offsetHeight)
             top = pos.y - node.offsetHeight;
           if (left + node.offsetWidth > hspace)
@@ -256,11 +264,11 @@
         node.style.top = (top + paddingTop()) + "px";
         node.style.left = node.style.right = "";
         if (horiz == "right") {
-          left = code.clientWidth - node.offsetWidth;
+          left = sizer.clientWidth - node.offsetWidth;
           node.style.right = "0px";
         } else {
           if (horiz == "left") left = 0;
-          else if (horiz == "middle") left = (code.clientWidth - node.offsetWidth) / 2;
+          else if (horiz == "middle") left = (sizer.clientWidth - node.offsetWidth) / 2;
           node.style.left = (left + paddingLeft()) + "px";
         }
         if (scroll)
@@ -290,7 +298,7 @@
         if (isLine(line)) replaceRange("", {line: line, ch: 0}, clipPos({line: line+1, ch: 0}));
       }),
       replaceRange: operation(replaceRange),
-      getRange: function(from, to) {return getRange(clipPos(from), clipPos(to));},
+      getRange: function(from, to, lineSep) {return getRange(clipPos(from), clipPos(to), lineSep);},
 
       triggerOnKeyDown: operation(onKeyDown),
       execCommand: function(cmd) {return commands[cmd](instance);},
@@ -328,18 +336,27 @@
       },
       scrollTo: function(x, y) {
         if (x != null) scroller.scrollLeft = x;
-        if (y != null) scrollbar.scrollTop = y;
+        if (y != null) scrollbar.scrollTop = scroller.scrollTop = y;
         updateDisplay([]);
       },
       getScrollInfo: function() {
         return {x: scroller.scrollLeft, y: scrollbar.scrollTop,
                 height: scrollbar.scrollHeight, width: scroller.scrollWidth};
       },
+      setSize: function(width, height) {
+        function interpret(val) {
+          val = String(val);
+          return /^\d+$/.test(val) ? val + "px" : val;
+        }
+        if (width != null) wrapper.style.width = interpret(width);
+        if (height != null) scroller.style.height = interpret(height);
+        instance.refresh();
+      },
 
       operation: function(f){return operation(f)();},
       compoundChange: function(f){return compoundChange(f);},
       refresh: function(){
-        updateDisplay(true);
+        updateDisplay(true, null, lastScrollTop);
         if (scrollbar.scrollHeight > lastScrollTop)
           scrollbar.scrollTop = lastScrollTop;
       },
@@ -356,33 +373,48 @@
       for (var n = line; n; n = n.parent) n.height += diff;
     }
 
+    function lineContent(line, wrapAt) {
+      if (!line.styles)
+        line.highlight(mode, line.stateAfter = getStateBefore(lineNo(line)), options.tabSize);
+      return line.getContent(options.tabSize, wrapAt, options.lineWrapping);
+    }
+
     function setValue(code) {
       var top = {line: 0, ch: 0};
       updateLines(top, {line: doc.size - 1, ch: getLine(doc.size-1).text.length},
                   splitLines(code), top, top);
       updateInput = true;
     }
-    function getValue() {
+    function getValue(lineSep) {
       var text = [];
       doc.iter(0, doc.size, function(line) { text.push(line.text); });
-      return text.join("\n");
+      return text.join(lineSep || "\n");
     }
 
-    function onScroll(e) {
-      if (lastScrollTop != scrollbar.scrollTop || lastScrollLeft != scroller.scrollLeft) {
-        lastScrollTop = scrollbar.scrollTop;
-        lastScrollLeft = scroller.scrollLeft;
+    function onScrollBar(e) {
+      if (scrollbar.scrollTop != lastScrollTop) {
+        lastScrollTop = scroller.scrollTop = scrollbar.scrollTop;
         updateDisplay([]);
-        if (options.fixedGutter) gutter.style.left = scroller.scrollLeft + "px";
-        if (options.onScroll) options.onScroll(instance);
       }
     }
 
+    function onScrollMain(e) {
+      if (options.fixedGutter && gutter.style.left != scroller.scrollLeft + "px")
+        gutter.style.left = scroller.scrollLeft + "px";
+      if (scroller.scrollTop != lastScrollTop) {
+        lastScrollTop = scroller.scrollTop;
+        if (scrollbar.scrollTop != lastScrollTop)
+          scrollbar.scrollTop = lastScrollTop;
+        updateDisplay([]);
+      }
+      if (options.onScroll) options.onScroll(instance);
+    }
+
     function onMouseDown(e) {
       setShift(e_prop(e, "shiftKey"));
       // Check whether this is a click in a widget
       for (var n = e_target(e); n != wrapper; n = n.parentNode)
-        if (n.parentNode == code && n != mover) return;
+        if (n.parentNode == sizer && n != mover) return;
 
       // See if this is a click in the gutter
       for (var n = e_target(e); n != wrapper; n = n.parentNode)
@@ -396,7 +428,7 @@
 
       switch (e_button(e)) {
       case 3:
-        if (gecko && !mac) onContextMenu(e);
+        if (gecko) onContextMenu(e);
         return;
       case 2:
         if (start) setCursor(start.line, start.ch, true);
@@ -411,32 +443,35 @@
 
       if (!focused) onFocus();
 
-      var now = +new Date;
+      var now = +new Date, type = "single";
       if (lastDoubleClick && lastDoubleClick.time > now - 400 && posEq(lastDoubleClick.pos, start)) {
+        type = "triple";
         e_preventDefault(e);
         setTimeout(focusInput, 20);
-        return selectLine(start.line);
+        selectLine(start.line);
       } else if (lastClick && lastClick.time > now - 400 && posEq(lastClick.pos, start)) {
+        type = "double";
         lastDoubleClick = {time: now, pos: start};
         e_preventDefault(e);
-        return selectWordAt(start);
+        var word = findWordAt(start);
+        setSelectionUser(word.from, word.to);
       } else { lastClick = {time: now, pos: start}; }
 
+      function dragEnd(e2) {
+        if (webkit) scroller.draggable = false;
+        draggingText = false;
+        up(); drop();
+        if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) {
+          e_preventDefault(e2);
+          setCursor(start.line, start.ch, true);
+          focusInput();
+        }
+      }
       var last = start, going;
       if (options.dragDrop && dragAndDrop && !options.readOnly && !posEq(sel.from, sel.to) &&
-          !posLess(start, sel.from) && !posLess(sel.to, start)) {
+          !posLess(start, sel.from) && !posLess(sel.to, start) && type == "single") {
         // Let the drag handler handle this.
         if (webkit) scroller.draggable = true;
-        function dragEnd(e2) {
-          if (webkit) scroller.draggable = false;
-          draggingText = false;
-          up(); drop();
-          if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) {
-            e_preventDefault(e2);
-            setCursor(start.line, start.ch, true);
-            focusInput();
-          }
-        }
         var up = connect(document, "mouseup", operation(dragEnd), true);
         var drop = connect(scroller, "drop", operation(dragEnd), true);
         draggingText = true;
@@ -445,14 +480,29 @@
         return;
       }
       e_preventDefault(e);
-      setCursor(start.line, start.ch, true);
+      if (type == "single") setCursor(start.line, start.ch, true);
+
+      var startstart = sel.from, startend = sel.to;
+
+      function doSelect(cur) {
+        if (type == "single") {
+          setSelectionUser(start, cur);
+        } else if (type == "double") {
+          var word = findWordAt(cur);
+          if (posLess(cur, startstart)) setSelectionUser(word.from, startend);
+          else setSelectionUser(startstart, word.to);
+        } else if (type == "triple") {
+          if (posLess(cur, startstart)) setSelectionUser(startend, clipPos({line: cur.line, ch: 0}));
+          else setSelectionUser(startstart, clipPos({line: cur.line + 1, ch: 0}));
+        }
+      }
 
       function extend(e) {
         var cur = posFromMouse(e, true);
         if (cur && !posEq(cur, last)) {
           if (!focused) onFocus();
           last = cur;
-          setSelectionUser(start, cur);
+          doSelect(cur);
           updateInput = false;
           var visible = visibleLines();
           if (cur.line >= visible.to || cur.line < visible.from)
@@ -463,7 +513,7 @@
       function done(e) {
         clearTimeout(going);
         var cur = posFromMouse(e);
-        if (cur) setSelectionUser(start, cur);
+        if (cur) doSelect(cur);
         e_preventDefault(e);
         focusInput();
         updateInput = true;
@@ -480,19 +530,16 @@
     function onDoubleClick(e) {
       for (var n = e_target(e); n != wrapper; n = n.parentNode)
         if (n.parentNode == gutterText) return e_preventDefault(e);
-      var start = posFromMouse(e);
-      if (!start) return;
-      lastDoubleClick = {time: +new Date, pos: start};
       e_preventDefault(e);
-      selectWordAt(start);
     }
     function onDrop(e) {
       if (options.onDragEvent && options.onDragEvent(instance, addStop(e))) return;
-      e.preventDefault();
+      e_preventDefault(e);
       var pos = posFromMouse(e, true), files = e.dataTransfer.files;
       if (!pos || options.readOnly) return;
       if (files && files.length && window.FileReader && window.File) {
-        function loadFile(file, i) {
+        var n = files.length, text = Array(n), read = 0;
+        var loadFile = function(file, i) {
           var reader = new FileReader;
           reader.onload = function() {
             text[i] = reader.result;
@@ -505,8 +552,7 @@
             }
           };
           reader.readAsText(file);
-        }
-        var n = files.length, text = Array(n), read = 0;
+        };
         for (var i = 0; i < n; ++i) loadFile(files[i], i);
       } else {
         // Don't do a replace if the drop happened inside of the selected text.
@@ -529,13 +575,10 @@
     function onDragStart(e) {
       var txt = getSelection();
       e.dataTransfer.setData("Text", txt);
-      
+
       // Use dummy image instead of default browsers image.
-      if (gecko || chrome || opera) {
-        var img = document.createElement('img');
-        img.scr = 'data:image/gif;base64,R0lGODdhAgACAIAAAAAAAP///ywAAAAAAgACAAACAoRRADs='; //1x1 image
-        e.dataTransfer.setDragImage(img, 0, 0);
-      }
+      if (e.dataTransfer.setDragImage)
+        e.dataTransfer.setDragImage(elt('img'), 0, 0);
     }
 
     function doHandleBinding(bound, dropShift) {
@@ -557,6 +600,7 @@
       }
       return true;
     }
+    var maybeTransition;
     function handleKeyBinding(e) {
       // Handle auto keymap transitions
       var startMap = getKeyMap(options.keyMap), next = startMap.auto;
@@ -568,10 +612,11 @@
       }, 50);
 
       var name = keyNames[e_prop(e, "keyCode")], handled = false;
+      var flipCtrlCmd = opera && mac;
       if (name == null || e.altGraphKey) return false;
       if (e_prop(e, "altKey")) name = "Alt-" + name;
-      if (e_prop(e, "ctrlKey")) name = "Ctrl-" + name;
-      if (e_prop(e, "metaKey")) name = "Cmd-" + name;
+      if (e_prop(e, flipCtrlCmd ? "metaKey" : "ctrlKey")) name = "Ctrl-" + name;
+      if (e_prop(e, flipCtrlCmd ? "ctrlKey" : "metaKey")) name = "Cmd-" + name;
 
       var stopped = false;
       function stop() { stopped = true; }
@@ -603,7 +648,7 @@
       return handled;
     }
 
-    var lastStoppedKey = null, maybeTransition;
+    var lastStoppedKey = null;
     function onKeyDown(e) {
       if (!focused) onFocus();
       if (ie && e.keyCode == 27) { e.returnValue = false; }
@@ -647,7 +692,6 @@
         focused = true;
         if (scroller.className.search(/\bCodeMirror-focused\b/) == -1)
           scroller.className += " CodeMirror-focused";
-        if (!leaveInputAlone) resetInput(true);
       }
       slowPoll();
       restartBlink();
@@ -666,53 +710,20 @@
       setTimeout(function() {if (!focused) shiftSelecting = null;}, 150);
     }
 
-    function chopDelta(delta) {
-      // Make sure we always scroll a little bit for any nonzero delta.
-      if (delta > 0.0 && delta < 1.0) return 1;
-      else if (delta > -1.0 && delta < 0.0) return -1;
-      else return Math.round(delta);
-    }
-
-    function onMouseWheel(e) {
-      var deltaX = 0, deltaY = 0;
-      if (e.type == "DOMMouseScroll") { // Firefox
-        var delta = -e.detail * 8.0;
-        if (e.axis == e.HORIZONTAL_AXIS) deltaX = delta;
-        else if (e.axis == e.VERTICAL_AXIS) deltaY = delta;
-      } else if (e.wheelDeltaX !== undefined && e.wheelDeltaY !== undefined) { // WebKit
-        deltaX = e.wheelDeltaX / 3.0;
-        deltaY = e.wheelDeltaY / 3.0;
-      } else if (e.wheelDelta !== undefined) { // IE or Opera
-        deltaY = e.wheelDelta / 3.0;
-      }
-
-      var scrolled = false;
-      deltaX = chopDelta(deltaX);
-      deltaY = chopDelta(deltaY);
-      if ((deltaX > 0 && scroller.scrollLeft > 0) ||
-          (deltaX < 0 && scroller.scrollLeft + scroller.clientWidth < scroller.scrollWidth)) {
-        scroller.scrollLeft -= deltaX;
-        scrolled = true;
-      }
-      if ((deltaY > 0 && scrollbar.scrollTop > 0) ||
-          (deltaY < 0 && scrollbar.scrollTop + scrollbar.clientHeight < scrollbar.scrollHeight)) {
-        scrollbar.scrollTop -= deltaY;
-        scrolled = true;
-      }
-      if (scrolled) e_stop(e);
-    }
-
     // Replace the range from from to to by the strings in newText.
     // Afterwards, set the selection to selFrom, selTo.
     function updateLines(from, to, newText, selFrom, selTo) {
       if (suppressEdits) return;
+      var old = [];
+      doc.iter(from.line, to.line + 1, function(line) {
+        old.push(newHL(line.text, line.markedSpans));
+      });
       if (history) {
-        var old = [];
-        doc.iter(from.line, to.line + 1, function(line) { old.push(line.text); });
         history.addChange(from.line, newText.length, old);
         while (history.done.length > options.undoDepth) history.done.shift();
       }
-      updateLinesNoUndo(from, to, newText, selFrom, selTo);
+      var lines = updateMarkedSpans(hlSpans(old[0]), hlSpans(lst(old)), from.ch, to.ch, newText);
+      updateLinesNoUndo(from, to, lines, selFrom, selTo);
     }
     function unredoHelper(from, to) {
       if (!from.length) return;
@@ -720,11 +731,12 @@
       for (var i = set.length - 1; i >= 0; i -= 1) {
         var change = set[i];
         var replaced = [], end = change.start + change.added;
-        doc.iter(change.start, end, function(line) { replaced.push(line.text); });
+        doc.iter(change.start, end, function(line) { replaced.push(newHL(line.text, line.markedSpans)); });
         out.push({start: change.start, added: change.old.length, old: replaced});
         var pos = {line: change.start + change.old.length - 1,
-                   ch: editEnd(replaced[replaced.length-1], change.old[change.old.length-1])};
-        updateLinesNoUndo({line: change.start, ch: 0}, {line: end - 1, ch: getLine(end-1).text.length}, change.old, pos, pos);
+                   ch: editEnd(hlText(lst(replaced)), hlText(lst(change.old)))};
+        updateLinesNoUndo({line: change.start, ch: 0}, {line: end - 1, ch: getLine(end-1).text.length},
+                          change.old, pos, pos);
       }
       updateInput = true;
       to.push(out);
@@ -732,95 +744,86 @@
     function undo() {unredoHelper(history.done, history.undone);}
     function redo() {unredoHelper(history.undone, history.done);}
 
-    function updateLinesNoUndo(from, to, newText, selFrom, selTo) {
+    function updateLinesNoUndo(from, to, lines, selFrom, selTo) {
       if (suppressEdits) return;
-      var recomputeMaxLength = false, maxLineLength = maxLine.length;
+      var recomputeMaxLength = false, maxLineLength = maxLine.text.length;
       if (!options.lineWrapping)
         doc.iter(from.line, to.line + 1, function(line) {
           if (!line.hidden && line.text.length == maxLineLength) {recomputeMaxLength = true; return true;}
         });
-      if (from.line != to.line || newText.length > 1) gutterDirty = true;
+      if (from.line != to.line || lines.length > 1) gutterDirty = true;
 
       var nlines = to.line - from.line, firstLine = getLine(from.line), lastLine = getLine(to.line);
-      // First adjust the line structure, taking some care to leave highlighting intact.
-      if (from.ch == 0 && to.ch == 0 && newText[newText.length - 1] == "") {
+      var lastHL = lst(lines);
+
+      // First adjust the line structure
+      if (from.ch == 0 && to.ch == 0 && hlText(lastHL) == "") {
         // This is a whole-line replace. Treated specially to make
         // sure line objects move the way they are supposed to.
         var added = [], prevLine = null;
-        if (from.line) {
-          prevLine = getLine(from.line - 1);
-          prevLine.fixMarkEnds(lastLine);
-        } else lastLine.fixMarkStarts();
-        for (var i = 0, e = newText.length - 1; i < e; ++i)
-          added.push(Line.inheritMarks(newText[i], prevLine));
+        for (var i = 0, e = lines.length - 1; i < e; ++i)
+          added.push(new Line(hlText(lines[i]), hlSpans(lines[i])));
+        lastLine.update(lastLine.text, hlSpans(lastHL));
         if (nlines) doc.remove(from.line, nlines, callbacks);
         if (added.length) doc.insert(from.line, added);
       } else if (firstLine == lastLine) {
-        if (newText.length == 1)
-          firstLine.replace(from.ch, to.ch, newText[0]);
-        else {
-          lastLine = firstLine.split(to.ch, newText[newText.length-1]);
-          firstLine.replace(from.ch, null, newText[0]);
-          firstLine.fixMarkEnds(lastLine);
-          var added = [];
-          for (var i = 1, e = newText.length - 1; i < e; ++i)
-            added.push(Line.inheritMarks(newText[i], firstLine));
-          added.push(lastLine);
+        if (lines.length == 1) {
+          firstLine.update(firstLine.text.slice(0, from.ch) + hlText(lines[0]) + firstLine.text.slice(to.ch), hlSpans(lines[0]));
+        } else {
+          for (var added = [], i = 1, e = lines.length - 1; i < e; ++i)
+            added.push(new Line(hlText(lines[i]), hlSpans(lines[i])));
+          added.push(new Line(hlText(lastHL) + firstLine.text.slice(to.ch), hlSpans(lastHL)));
+          firstLine.update(firstLine.text.slice(0, from.ch) + hlText(lines[0]), hlSpans(lines[0]));
           doc.insert(from.line + 1, added);
         }
-      } else if (newText.length == 1) {
-        firstLine.replace(from.ch, null, newText[0]);
-        lastLine.replace(null, to.ch, "");
-        firstLine.append(lastLine);
+      } else if (lines.length == 1) {
+        firstLine.update(firstLine.text.slice(0, from.ch) + hlText(lines[0]) + lastLine.text.slice(to.ch), hlSpans(lines[0]));
         doc.remove(from.line + 1, nlines, callbacks);
       } else {
         var added = [];
-        firstLine.replace(from.ch, null, newText[0]);
-        lastLine.replace(null, to.ch, newText[newText.length-1]);
-        firstLine.fixMarkEnds(lastLine);
-        for (var i = 1, e = newText.length - 1; i < e; ++i)
-          added.push(Line.inheritMarks(newText[i], firstLine));
+        firstLine.update(firstLine.text.slice(0, from.ch) + hlText(lines[0]), hlSpans(lines[0]));
+        lastLine.update(hlText(lastHL) + lastLine.text.slice(to.ch), hlSpans(lastHL));
+        for (var i = 1, e = lines.length - 1; i < e; ++i)
+          added.push(new Line(hlText(lines[i]), hlSpans(lines[i])));
         if (nlines > 1) doc.remove(from.line + 1, nlines - 1, callbacks);
         doc.insert(from.line + 1, added);
       }
       if (options.lineWrapping) {
         var perLine = Math.max(5, scroller.clientWidth / charWidth() - 3);
-        doc.iter(from.line, from.line + newText.length, function(line) {
+        doc.iter(from.line, from.line + lines.length, function(line) {
           if (line.hidden) return;
           var guess = Math.ceil(line.text.length / perLine) || 1;
           if (guess != line.height) updateLineHeight(line, guess);
         });
       } else {
-        doc.iter(from.line, from.line + newText.length, function(line) {
+        doc.iter(from.line, from.line + lines.length, function(line) {
           var l = line.text;
           if (!line.hidden && l.length > maxLineLength) {
-            maxLine = l; maxLineLength = l.length; maxLineChanged = true;
+            maxLine = line; maxLineLength = l.length; maxLineChanged = true;
             recomputeMaxLength = false;
           }
         });
         if (recomputeMaxLength) updateMaxLine = true;
       }
 
-      // Add these lines to the work array, so that they will be
-      // highlighted. Adjust work lines if lines were added/removed.
-      var newWork = [], lendiff = newText.length - nlines - 1;
-      for (var i = 0, l = work.length; i < l; ++i) {
-        var task = work[i];
-        if (task < from.line) newWork.push(task);
-        else if (task > to.line) newWork.push(task + lendiff);
-      }
-      var hlEnd = from.line + Math.min(newText.length, 500);
-      highlightLines(from.line, hlEnd);
-      newWork.push(hlEnd);
-      work = newWork;
-      startWorker(100);
+      // Adjust frontier, schedule worker
+      frontier = Math.min(frontier, from.line);
+      startWorker(400);
+
+      var lendiff = lines.length - nlines - 1;
       // Remember that these lines changed, for updating the display
       changes.push({from: from.line, to: to.line + 1, diff: lendiff});
-      var changeObj = {from: from, to: to, text: newText};
-      if (textChanged) {
-        for (var cur = textChanged; cur.next; cur = cur.next) {}
-        cur.next = changeObj;
-      } else textChanged = changeObj;
+      if (options.onChange) {
+        // Normalize lines to contain only strings, since that's what
+        // the change event handler expects
+        for (var i = 0; i < lines.length; ++i)
+          if (typeof lines[i] != "string") lines[i] = lines[i].text;
+        var changeObj = {from: from, to: to, text: lines};
+        if (textChanged) {
+          for (var cur = textChanged; cur.next; cur = cur.next) {}
+          cur.next = changeObj;
+        } else textChanged = changeObj;
+      }
 
       // Update the selection
       function updateLine(n) {return n <= Math.min(to.line, to.line + lendiff) ? n : n + lendiff;}
@@ -828,42 +831,45 @@
                    updateLine(sel.from.line), updateLine(sel.to.line));
     }
 
-    function updateVerticalScroll(scrollTop) {
-      var th = textHeight(), virtualHeight = Math.floor(doc.height * th + 2 * paddingTop()), scrollbarHeight = scroller.clientHeight;
-      scrollbar.style.height = scrollbarHeight + "px";
-      if (scroller.clientHeight)
-        scrollbarInner.style.height = virtualHeight + "px";
-      // Position the mover div to align with the current virtual scroll position
-      if (scrollTop != null) scrollbar.scrollTop = scrollTop;
-      mover.style.top = (displayOffset * th - scrollbar.scrollTop) + "px";
-      scrollbar.style.display = (virtualHeight > scrollbarHeight) ? "block" : "none";
+    function needsScrollbar() {
+      var realHeight = doc.height * textHeight() + 2 * paddingTop();
+      return realHeight * .99 > scroller.offsetHeight ? realHeight : false;
     }
-  
-    // On Mac OS X Lion and up, detect whether the mouse is plugged in by measuring 
-    // the width of a div with a scrollbar in it. If the width is <= 1, then
-    // the mouse isn't plugged in and scrollbars should overlap the content.
-    function overlapScrollbars() {
-      var tmpSb = document.createElement('div'),
-          tmpSbInner = document.createElement('div');
-      tmpSb.className = "CodeMirror-scrollbar";
-      tmpSb.style.cssText = "position: absolute; left: -9999px; height: 100px;";
-      tmpSbInner.className = "CodeMirror-scrollbar-inner";
-      tmpSbInner.style.height = "200px";
-      tmpSb.appendChild(tmpSbInner);
 
-      document.body.appendChild(tmpSb);
-      var result = (tmpSb.offsetWidth <= 1);
-      document.body.removeChild(tmpSb);
-      return result;
+    function updateVerticalScroll(scrollTop) {
+      var scrollHeight = needsScrollbar();
+      scrollbar.style.display = scrollHeight ? "block" : "none";
+      if (scrollHeight) {
+        scrollbarInner.style.height = sizer.style.minHeight = scrollHeight + "px";
+        scrollbar.style.height = scroller.clientHeight + "px";
+        if (scrollTop != null) {
+          scrollbar.scrollTop = scroller.scrollTop = scrollTop;
+          // 'Nudge' the scrollbar to work around a Webkit bug where,
+          // in some situations, we'd end up with a scrollbar that
+          // reported its scrollTop (and looked) as expected, but
+          // *behaved* as if it was still in a previous state (i.e.
+          // couldn't scroll up, even though it appeared to be at the
+          // bottom).
+          if (webkit) setTimeout(function() {
+            if (scrollbar.scrollTop != scrollTop) return;
+            scrollbar.scrollTop = scrollTop + (scrollTop ? -1 : 1);
+            scrollbar.scrollTop = scrollTop;
+          }, 0);
+        }
+      } else {
+        sizer.style.minHeight = "";
+      }
+      // Position the mover div to align with the current virtual scroll position
+      mover.style.top = displayOffset * textHeight() + "px";
     }
 
     function computeMaxLength() {
-      var maxLineLength = 0; 
-      maxLine = ""; maxLineChanged = true;
-      doc.iter(0, doc.size, function(line) {
+      maxLine = getLine(0); maxLineChanged = true;
+      var maxLineLength = maxLine.text.length;
+      doc.iter(1, doc.size, function(line) {
         var l = line.text;
         if (!line.hidden && l.length > maxLineLength) {
-          maxLineLength = l.length; maxLine = l;
+          maxLineLength = l.length; maxLine = line;
         }
       });
       updateMaxLine = false;
@@ -879,7 +885,7 @@
         var line = pos.line + code.length - (to.line - from.line) - 1;
         var ch = pos.ch;
         if (pos.line == to.line)
-          ch += code[code.length-1].length - (to.ch - (to.line == from.line ? from.ch : 0));
+          ch += lst(code).length - (to.ch - (to.line == from.line ? from.ch : 0));
         return {line: line, ch: ch};
       }
       var end;
@@ -897,42 +903,37 @@
       });
     }
     function replaceRange1(code, from, to, computeSel) {
-      var endch = code.length == 1 ? code[0].length + from.ch : code[code.length-1].length;
+      var endch = code.length == 1 ? code[0].length + from.ch : lst(code).length;
       var newSel = computeSel({line: from.line + code.length - 1, ch: endch});
       updateLines(from, to, code, newSel.from, newSel.to);
     }
 
-    function getRange(from, to) {
+    function getRange(from, to, lineSep) {
       var l1 = from.line, l2 = to.line;
       if (l1 == l2) return getLine(l1).text.slice(from.ch, to.ch);
       var code = [getLine(l1).text.slice(from.ch)];
       doc.iter(l1 + 1, l2, function(line) { code.push(line.text); });
       code.push(getLine(l2).text.slice(0, to.ch));
-      return code.join("\n");
+      return code.join(lineSep || "\n");
     }
-    function getSelection() {
-      return getRange(sel.from, sel.to);
+    function getSelection(lineSep) {
+      return getRange(sel.from, sel.to, lineSep);
     }
 
-    var pollingFast = false; // Ensures slowPoll doesn't cancel fastPoll
     function slowPoll() {
       if (pollingFast) return;
       poll.set(options.pollInterval, function() {
-        startOperation();
         readInput();
         if (focused) slowPoll();
-        endOperation();
       });
     }
     function fastPoll() {
       var missed = false;
       pollingFast = true;
       function p() {
-        startOperation();
         var changed = readInput();
         if (!changed && !missed) {missed = true; poll.set(60, p);}
         else {pollingFast = false; slowPoll();}
-        endOperation();
       }
       poll.set(20, p);
     }
@@ -944,9 +945,10 @@
     // supported or compatible enough yet to rely on.)
     var prevInput = "";
     function readInput() {
-      if (leaveInputAlone || !focused || hasSelection(input) || options.readOnly) return false;
+      if (!focused || hasSelection(input) || options.readOnly) return false;
       var text = input.value;
       if (text == prevInput) return false;
+      if (!nestedOperation) startOperation();
       shiftSelecting = null;
       var same = 0, l = Math.min(prevInput.length, text.length);
       while (same < l && prevInput[same] == text[same]) ++same;
@@ -957,13 +959,14 @@
       replaceSelection(text.slice(same), "end");
       if (text.length > 1000) { input.value = prevInput = ""; }
       else prevInput = text;
+      if (!nestedOperation) endOperation();
       return true;
     }
     function resetInput(user) {
       if (!posEq(sel.from, sel.to)) {
         prevInput = "";
         input.value = getSelection();
-        selectInput(input);
+        if (focused) selectInput(input);
       } else if (user) prevInput = input.value = "";
     }
 
@@ -971,17 +974,23 @@
       if (options.readOnly != "nocursor") input.focus();
     }
 
-    function scrollEditorIntoView() {
-      if (!cursor.getBoundingClientRect) return;
-      var rect = cursor.getBoundingClientRect();
-      // IE returns bogus coordinates when the instance sits inside of an iframe and the cursor is hidden
-      if (ie && rect.top == rect.bottom) return;
-      var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight);
-      if (rect.top < 0 || rect.bottom > winH) scrollCursorIntoView();
-    }
     function scrollCursorIntoView() {
       var coords = calculateCursorCoords();
-      return scrollIntoView(coords.x, coords.y, coords.x, coords.yBot);
+      scrollIntoView(coords.x, coords.y, coords.x, coords.yBot);
+      if (!focused) return;
+      var box = sizer.getBoundingClientRect(), doScroll = null;
+      if (coords.y + box.top < 0) doScroll = true;
+      else if (coords.y + box.top + textHeight() > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false;
+      if (doScroll != null) {
+        var hidden = cursor.style.display == "none";
+        if (hidden) {
+          cursor.style.display = "";
+          cursor.style.left = coords.x + "px";
+          cursor.style.top = (coords.y - displayOffset) + "px";
+        }
+        cursor.scrollIntoView(doScroll);
+        if (hidden) cursor.style.display = "none";
+      }
     }
     function calculateCursorCoords() {
       var cursor = localCoords(sel.inverted ? sel.from : sel.to);
@@ -989,18 +998,18 @@
       return {x: x, y: cursor.y, yBot: cursor.yBot};
     }
     function scrollIntoView(x1, y1, x2, y2) {
-      var scrollPos = calculateScrollPos(x1, y1, x2, y2), scrolled = false;
-      if (scrollPos.scrollLeft != null) {scroller.scrollLeft = scrollPos.scrollLeft; scrolled = true;}
-      if (scrollPos.scrollTop != null) {scrollbar.scrollTop = scrollPos.scrollTop; scrolled = true;}
-      if (scrolled && options.onScroll) options.onScroll(instance);
+      var scrollPos = calculateScrollPos(x1, y1, x2, y2);
+      if (scrollPos.scrollLeft != null) {scroller.scrollLeft = scrollPos.scrollLeft;}
+      if (scrollPos.scrollTop != null) {scrollbar.scrollTop = scroller.scrollTop = scrollPos.scrollTop;}
     }
     function calculateScrollPos(x1, y1, x2, y2) {
       var pl = paddingLeft(), pt = paddingTop();
       y1 += pt; y2 += pt; x1 += pl; x2 += pl;
       var screen = scroller.clientHeight, screentop = scrollbar.scrollTop, result = {};
-      var atTop = y1 < paddingTop() + 10;
+      var docBottom = needsScrollbar() || Infinity;
+      var atTop = y1 < pt + 10, atBottom = y2 + pt > docBottom - 10;
       if (y1 < screentop) result.scrollTop = atTop ? 0 : Math.max(0, y1);
-      else if (y2 > screentop + screen) result.scrollTop = y2 - screen;
+      else if (y2 > screentop + screen) result.scrollTop = (atBottom ? docBottom : y2) - screen;
 
       var screenw = scroller.clientWidth, screenleft = scroller.scrollLeft;
       var gutterw = options.fixedGutter ? gutter.clientWidth : 0;
@@ -1070,8 +1079,13 @@
       // This is just a bogus formula that detects when the editor is
       // resized or the font size changes.
       if (different) lastSizeC = scroller.clientHeight + th;
+      if (from != showingFrom || to != showingTo && options.onViewportChange)
+        setTimeout(function(){
+          if (options.onViewportChange) options.onViewportChange(instance, from, to);
+        });
       showingFrom = from; showingTo = to;
       displayOffset = heightAtLine(doc, from);
+      startWorker(100);
 
       // Since this is all rather error prone, it is honoured with the
       // only assertion in the whole file.
@@ -1082,6 +1096,10 @@
       function checkHeights() {
         var curNode = lineDiv.firstChild, heightChanged = false;
         doc.iter(showingFrom, showingTo, function(line) {
+          // Work around bizarro IE7 bug where, sometimes, our curNode
+          // is magically replaced with a new node in the DOM, leaving
+          // us with a reference to an orphan (nextSibling-less) node.
+          if (!curNode) return;
           if (!line.hidden) {
             var height = Math.round(curNode.offsetHeight / th) || 1;
             if (line.height != height) {
@@ -1094,23 +1112,15 @@
         return heightChanged;
       }
 
-      if (options.lineWrapping) {
-        // Guess whether we're going to need the scrollbar, so that we don't end up changing the linewrapping
-        // after the scrollbar appears (during updateVerticalScroll()). Only do this if the scrollbar is
-        // appearing (if it's disappearing, we don't have to worry about the scroll position, and there are
-        // issues on IE7 if we turn it off too early).
-        var virtualHeight = Math.floor(doc.height * th + 2 * paddingTop()), scrollbarHeight = scroller.clientHeight;
-        if (virtualHeight > scrollbarHeight) scrollbar.style.display = "block";
-        checkHeights();
-      }
+      if (options.lineWrapping) checkHeights();
 
       gutter.style.display = gutterDisplay;
       if (different || gutterDirty) {
         // If the gutter grew in size, re-check heights. If those changed, re-draw gutter.
         updateGutter() && options.lineWrapping && checkHeights() && updateGutter();
       }
+      updateVerticalScroll(scrollTop);
       updateSelection();
-      updateVerticalScroll(scrollTop);
       if (!suppressCallback && options.onUpdate) options.onUpdate(instance);
       return true;
     }
@@ -1139,14 +1149,14 @@
     }
 
     function patchDisplay(from, to, intact) {
+      function killNode(node) {
+        var tmp = node.nextSibling;
+        node.parentNode.removeChild(node);
+        return tmp;
+      }
       // The first pass removes the DOM nodes that aren't intact.
-      if (!intact.length) lineDiv.innerHTML = "";
+      if (!intact.length) removeChildren(lineDiv);
       else {
-        function killNode(node) {
-          var tmp = node.nextSibling;
-          node.parentNode.removeChild(node);
-          return tmp;
-        }
         var domPos = 0, curNode = lineDiv.firstChild, n;
         for (var i = 0; i < intact.length; ++i) {
           var cur = intact[i];
@@ -1157,21 +1167,20 @@
       }
       // This pass fills in the lines that actually changed.
       var nextIntact = intact.shift(), curNode = lineDiv.firstChild, j = from;
-      var scratch = document.createElement("div");
       doc.iter(from, to, function(line) {
         if (nextIntact && nextIntact.to == j) nextIntact = intact.shift();
         if (!nextIntact || nextIntact.from > j) {
-          if (line.hidden) var html = scratch.innerHTML = "<pre></pre>";
+          if (line.hidden) var lineElement = elt("pre");
           else {
-            var html = '<pre' + (line.className ? ' class="' + line.className + '"' : '') + '>'
-              + line.getHTML(makeTab) + '</pre>';
+            var lineElement = lineContent(line);
+            if (line.className) lineElement.className = line.className;
             // Kludge to make sure the styled element lies behind the selection (by z-index)
-            if (line.bgClassName)
-              html = '<div style="position: relative"><pre class="' + line.bgClassName +
-              '" style="position: absolute; left: 0; right: 0; top: 0; bottom: 0; z-index: -2">&#160;</pre>' + html + "</div>";
+            if (line.bgClassName) {
+              var pre = elt("pre", "\u00a0", line.bgClassName, "position: absolute; left: 0; right: 0; top: 0; bottom: 0; z-index: -2");
+              lineElement = elt("div", [pre, lineElement], null, "position: relative");
+            }
           }
-          scratch.innerHTML = html;
-          lineDiv.insertBefore(scratch.firstChild, curNode);
+          lineDiv.insertBefore(lineElement, curNode);
         } else {
           curNode = curNode.nextSibling;
         }
@@ -1183,26 +1192,29 @@
       if (!options.gutter && !options.lineNumbers) return;
       var hText = mover.offsetHeight, hEditor = scroller.clientHeight;
       gutter.style.height = (hText - hEditor < 2 ? hEditor : hText) + "px";
-      var html = [], i = showingFrom, normalNode;
+      var fragment = document.createDocumentFragment(), i = showingFrom, normalNode;
       doc.iter(showingFrom, Math.max(showingTo, showingFrom + 1), function(line) {
         if (line.hidden) {
-          html.push("<pre></pre>");
+          fragment.appendChild(elt("pre"));
         } else {
           var marker = line.gutterMarker;
-          var text = options.lineNumbers ? i + options.firstLineNumber : null;
+          var text = options.lineNumbers ? options.lineNumberFormatter(i + options.firstLineNumber) : null;
           if (marker && marker.text)
             text = marker.text.replace("%N%", text != null ? text : "");
           else if (text == null)
             text = "\u00a0";
-          html.push((marker && marker.style ? '<pre class="' + marker.style + '">' : "<pre>"), text);
-          for (var j = 1; j < line.height; ++j) html.push("<br/>&#160;");
-          html.push("</pre>");
+          var markerElement = fragment.appendChild(elt("pre", null, marker && marker.style));
+          markerElement.innerHTML = text;
+          for (var j = 1; j < line.height; ++j) {
+            markerElement.appendChild(elt("br"));
+            markerElement.appendChild(document.createTextNode("\u00a0"));
+          }
           if (!marker) normalNode = i;
         }
         ++i;
       });
       gutter.style.display = "none";
-      gutterText.innerHTML = html.join("");
+      removeChildrenAndAdd(gutterText, fragment);
       // Make sure scrolling doesn't cause number gutter size to pop
       if (normalNode != null && options.lineNumbers) {
         var node = gutterText.childNodes[normalNode - showingFrom];
@@ -1230,15 +1242,15 @@
         cursor.style.display = "";
         selectionDiv.style.display = "none";
       } else {
-        var sameLine = fromPos.y == toPos.y, html = "";
+        var sameLine = fromPos.y == toPos.y, fragment = document.createDocumentFragment();
         var clientWidth = lineSpace.clientWidth || lineSpace.offsetWidth;
         var clientHeight = lineSpace.clientHeight || lineSpace.offsetHeight;
-        function add(left, top, right, height) {
+        var add = function(left, top, right, height) {
           var rstyle = quirksMode ? "width: " + (!right ? clientWidth : clientWidth - right - left) + "px"
                                   : "right: " + right + "px";
-          html += '<div class="CodeMirror-selected" style="position: absolute; left: ' + left +
-            'px; top: ' + top + 'px; ' + rstyle + '; height: ' + height + 'px"></div>';
-        }
+          fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left +
+                                   "px; top: " + top + "px; " + rstyle + "; height: " + height + "px"));
+        };
         if (sel.from.ch && fromPos.y >= 0) {
           var right = sameLine ? clientWidth - toPos.x : 0;
           add(fromPos.x, fromPos.y, right, th);
@@ -1249,7 +1261,7 @@
           add(0, middleStart, 0, middleHeight);
         if ((!sameLine || !sel.from.ch) && toPos.y < clientHeight - .5 * th)
           add(0, toPos.y, clientWidth - toPos.x, th);
-        selectionDiv.innerHTML = html;
+        removeChildrenAndAdd(selectionDiv, fragment);
         cursor.style.display = "none";
         selectionDiv.style.display = "";
       }
@@ -1381,24 +1393,34 @@
       else replaceRange("", sel.from, findPosH(dir, unit));
       userSelChange = true;
     }
-    var goalColumn = null;
     function moveV(dir, unit) {
       var dist = 0, pos = localCoords(sel.inverted ? sel.from : sel.to, true);
       if (goalColumn != null) pos.x = goalColumn;
-      if (unit == "page") dist = Math.min(scroller.clientHeight, window.innerHeight || document.documentElement.clientHeight);
-      else if (unit == "line") dist = textHeight();
-      var target = coordsChar(pos.x, pos.y + dist * dir + 2);
+      if (unit == "page") {
+        var screen = Math.min(scroller.clientHeight, window.innerHeight || document.documentElement.clientHeight);
+        var target = coordsChar(pos.x, pos.y + screen * dir);
+      } else if (unit == "line") {
+        var th = textHeight();
+        var target = coordsChar(pos.x, pos.y + .5 * th + dir * th);
+      }
       if (unit == "page") scrollbar.scrollTop += localCoords(target, true).y - pos.y;
       setCursor(target.line, target.ch, true);
       goalColumn = pos.x;
     }
 
-    function selectWordAt(pos) {
+    function findWordAt(pos) {
       var line = getLine(pos.line).text;
       var start = pos.ch, end = pos.ch;
-      while (start > 0 && isWordChar(line.charAt(start - 1))) --start;
-      while (end < line.length && isWordChar(line.charAt(end))) ++end;
-      setSelectionUser({line: pos.line, ch: start}, {line: pos.line, ch: end});
+      if (line) {
+        if (pos.after === false || end == line.length) --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: {line: pos.line, ch: start}, to: {line: pos.line, ch: end}};
     }
     function selectLine(line) {
       setSelectionUser({line: line, ch: 0}, clipPos({line: line + 1, ch: 0}));
@@ -1431,24 +1453,20 @@
       indentation = Math.max(0, indentation);
       var diff = indentation - curSpace;
 
-      if (!diff) {
-        if (sel.from.line != n && sel.to.line != n) return;
-        var indentString = curSpaceString;
-      } else {
-        var indentString = "", pos = 0;
-        if (options.indentWithTabs)
-          for (var i = Math.floor(indentation / options.tabSize); i; --i) {pos += options.tabSize; indentString += "\t";}
-        while (pos < indentation) {++pos; indentString += " ";}
-      }
+      var indentString = "", pos = 0;
+      if (options.indentWithTabs)
+        for (var i = Math.floor(indentation / options.tabSize); i; --i) {pos += options.tabSize; indentString += "\t";}
+      if (pos < indentation) indentString += spaceStr(indentation - pos);
 
-      replaceRange(indentString, {line: n, ch: 0}, {line: n, ch: curSpaceString.length});
+      if (indentString != curSpaceString)
+        replaceRange(indentString, {line: n, ch: 0}, {line: n, ch: curSpaceString.length});
     }
 
     function loadMode() {
       mode = CodeMirror.getMode(options, options.mode);
       doc.iter(0, doc.size, function(line) { line.stateAfter = null; });
-      work = [0];
-      startWorker();
+      frontier = 0;
+      startWorker(100);
     }
     function gutterChanged() {
       var visible = options.gutter || options.lineNumbers;
@@ -1465,24 +1483,16 @@
           var guess = Math.ceil(line.text.length / perLine) || 1;
           if (guess != 1) updateLineHeight(line, guess);
         });
-        lineSpace.style.width = code.style.width = "";
-        widthForcer.style.left = "";
+        lineSpace.style.minWidth = widthForcer.style.left = "";
       } else {
         wrapper.className = wrapper.className.replace(" CodeMirror-wrap", "");
-        maxLine = ""; maxLineChanged = true;
+        computeMaxLength();
         doc.iter(0, doc.size, function(line) {
           if (line.height != 1 && !line.hidden) updateLineHeight(line, 1);
-          if (line.text.length > maxLine.length) maxLine = line.text;
         });
       }
       changes.push({from: 0, to: doc.size});
     }
-    function makeTab(col) {
-      var w = options.tabSize - col % options.tabSize, cached = tabCache[w];
-      if (cached) return cached;
-      for (var str = '<span class="cm-tab">', i = 0; i < w; ++i) str += " ";
-      return (tabCache[w] = {html: str + "</span>", width: w});
-    }
     function themeChanged() {
       scroller.className = scroller.className.replace(/\s*cm-s-\S+/g, "") +
         options.theme.replace(/(^|\s)\s*/g, " cm-s-");
@@ -1493,74 +1503,71 @@
         (style ? " cm-keymap-" + style : "");
     }
 
-    function TextMarker() { this.set = []; }
+    function TextMarker(type, style) { this.lines = []; this.type = type; if (style) this.style = style; }
     TextMarker.prototype.clear = operation(function() {
       var min = Infinity, max = -Infinity;
-      for (var i = 0, e = this.set.length; i < e; ++i) {
-        var line = this.set[i], mk = line.marked;
-        if (!mk || !line.parent) continue;
-        var lineN = lineNo(line);
-        min = Math.min(min, lineN); max = Math.max(max, lineN);
-        for (var j = 0; j < mk.length; ++j)
-          if (mk[j].marker == this) mk.splice(j--, 1);
+      for (var i = 0; i < this.lines.length; ++i) {
+        var line = this.lines[i];
+        var span = getMarkedSpanFor(line.markedSpans, this, true);
+        if (span.from != null || span.to != null) {
+          var lineN = lineNo(line);
+          min = Math.min(min, lineN); max = Math.max(max, lineN);
+        }
       }
       if (min != Infinity)
         changes.push({from: min, to: max + 1});
+      this.lines.length = 0;
     });
     TextMarker.prototype.find = function() {
       var from, to;
-      for (var i = 0, e = this.set.length; i < e; ++i) {
-        var line = this.set[i], mk = line.marked;
-        for (var j = 0; j < mk.length; ++j) {
-          var mark = mk[j];
-          if (mark.marker == this) {
-            if (mark.from != null || mark.to != null) {
-              var found = lineNo(line);
-              if (found != null) {
-                if (mark.from != null) from = {line: found, ch: mark.from};
-                if (mark.to != null) to = {line: found, ch: mark.to};
-              }
-            }
-          }
+      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 = {line: found, ch: span.from};
+          if (span.to != null) to = {line: found, ch: span.to};
         }
       }
-      return {from: from, to: to};
+      if (this.type == "bookmark") return from;
+      return from && {from: from, to: to};
     };
 
-    function markText(from, to, className) {
+    function markText(from, to, className, options) {
       from = clipPos(from); to = clipPos(to);
-      var tm = new TextMarker();
-      if (!posLess(from, to)) return tm;
-      function add(line, from, to, className) {
-        getLine(line).addMark(new MarkedText(from, to, className, tm));
-      }
-      if (from.line == to.line) add(from.line, from.ch, to.ch, className);
-      else {
-        add(from.line, from.ch, null, className);
-        for (var i = from.line + 1, e = to.line; i < e; ++i)
-          add(i, null, null, className);
-        add(to.line, null, to.ch, className);
-      }
+      var marker = new TextMarker("range", className);
+      if (options) for (var opt in options) if (options.hasOwnProperty(opt))
+        marker[opt] = options[opt];
+      var curLine = from.line;
+      doc.iter(curLine, to.line + 1, function(line) {
+        var span = {from: curLine == from.line ? from.ch : null,
+                    to: curLine == to.line ? to.ch : null,
+                    marker: marker};
+        (line.markedSpans || (line.markedSpans = [])).push(span);
+        marker.lines.push(line);
+        ++curLine;
+      });
       changes.push({from: from.line, to: to.line + 1});
-      return tm;
+      return marker;
     }
 
     function setBookmark(pos) {
       pos = clipPos(pos);
-      var bm = new Bookmark(pos.ch);
-      getLine(pos.line).addMark(bm);
-      return bm;
+      var marker = new TextMarker("bookmark"), line = getLine(pos.line);
+      var span = {from: pos.ch, to: pos.ch, marker: marker};
+      (line.markedSpans || (line.markedSpans = [])).push(span);
+      marker.lines.push(line);
+      return marker;
     }
 
     function findMarksAt(pos) {
       pos = clipPos(pos);
-      var markers = [], marked = getLine(pos.line).marked;
-      if (!marked) return markers;
-      for (var i = 0, e = marked.length; i < e; ++i) {
-        var m = marked[i];
-        if ((m.from == null || m.from <= pos.ch) &&
-            (m.to == null || m.to >= pos.ch))
-          markers.push(m.marker || m);
+      var markers = [], spans = getLine(pos.line).markedSpans;
+      if (spans) for (var i = 0; i < spans.length; ++i) {
+        var span = spans[i];
+        if ((span.from == null || span.from <= pos.ch) &&
+            (span.to == null || span.to >= pos.ch))
+          markers.push(span.marker);
       }
       return markers;
     }
@@ -1600,11 +1607,10 @@
         if (line.hidden != hidden) {
           line.hidden = hidden;
           if (!options.lineWrapping) {
-            var l = line.text;
-            if (hidden && l.length == maxLine.length) {
+            if (hidden && line.text.length == maxLine.text.length) {
               updateMaxLine = true;
-            } else if (!hidden && l.length > maxLine.length) {
-              maxLine = l; maxWidth = null; updateMaxLine = false;
+            } else if (!hidden && line.text.length > maxLine.text.length) {
+              maxLine = line; updateMaxLine = false;
             }
           }
           updateLineHeight(line, hidden ? 0 : 1);
@@ -1636,53 +1642,18 @@
               markerClass: marker && marker.style, lineClass: line.className, bgClass: line.bgClassName};
     }
 
-    function stringWidth(str) {
-      measure.innerHTML = "<pre><span>x</span></pre>";
-      measure.firstChild.firstChild.firstChild.nodeValue = str;
-      return measure.firstChild.firstChild.offsetWidth || 10;
-    }
-    // These are used to go from pixel positions to character
-    // positions, taking varying character widths into account.
-    function charFromX(line, x) {
-      if (x <= 0) return 0;
-      var lineObj = getLine(line), text = lineObj.text;
-      function getX(len) {
-        return measureLine(lineObj, len).left;
-      }
-      var from = 0, fromX = 0, to = text.length, toX;
-      // Guess a suitable upper bound for our search.
-      var estimated = Math.min(to, Math.ceil(x / charWidth()));
-      for (;;) {
-        var estX = getX(estimated);
-        if (estX <= x && estimated < to) estimated = Math.min(to, Math.ceil(estimated * 1.2));
-        else {toX = estX; to = estimated; break;}
-      }
-      if (x > toX) return to;
-      // Try to guess a suitable lower bound as well.
-      estimated = Math.floor(to * 0.8); estX = getX(estimated);
-      if (estX < x) {from = estimated; fromX = estX;}
-      // Do a binary search between these bounds.
-      for (;;) {
-        if (to - from <= 1) return (toX - x > x - fromX) ? from : to;
-        var middle = Math.ceil((from + to) / 2), middleX = getX(middle);
-        if (middleX > x) {to = middle; toX = middleX;}
-        else {from = middle; fromX = middleX;}
-      }
-    }
-
-    var tempId = "CodeMirror-temp-" + Math.floor(Math.random() * 0xffffff).toString(16);
     function measureLine(line, ch) {
       if (ch == 0) return {top: 0, left: 0};
       var wbr = options.lineWrapping && ch < line.text.length &&
                 spanAffectsWrapping.test(line.text.slice(ch - 1, ch + 1));
-      measure.innerHTML = "<pre>" + line.getHTML(makeTab, ch, tempId, wbr) + "</pre>";
-      var elt = document.getElementById(tempId);
-      var top = elt.offsetTop, left = elt.offsetLeft;
+      var pre = lineContent(line, ch);
+      removeChildrenAndAdd(measure, pre);
+      var anchor = pre.anchor;
+      var top = anchor.offsetTop, left = anchor.offsetLeft;
       // Older IEs report zero offsets for spans directly after a wrap
       if (ie && top == 0 && left == 0) {
-        var backup = document.createElement("span");
-        backup.innerHTML = "x";
-        elt.parentNode.insertBefore(backup, elt.nextSibling);
+        var backup = elt("span", "x");
+        anchor.parentNode.insertBefore(backup, anchor.nextSibling);
         top = backup.offsetTop;
       }
       return {top: top, left: left};
@@ -1699,17 +1670,19 @@
     }
     // Coords must be lineSpace-local
     function coordsChar(x, y) {
-      if (y < 0) y = 0;
       var th = textHeight(), cw = charWidth(), heightPos = displayOffset + Math.floor(y / th);
+      if (heightPos < 0) return {line: 0, ch: 0};
       var lineNo = lineAtHeight(doc, heightPos);
       if (lineNo >= doc.size) return {line: doc.size - 1, ch: getLine(doc.size - 1).text.length};
       var lineObj = getLine(lineNo), text = lineObj.text;
       var tw = options.lineWrapping, innerOff = tw ? heightPos - heightAtLine(doc, lineNo) : 0;
       if (x <= 0 && innerOff == 0) return {line: lineNo, ch: 0};
+      var wrongLine = false;
       function getX(len) {
         var sp = measureLine(lineObj, len);
         if (tw) {
           var off = Math.round(sp.top / th);
+          wrongLine = off != innerOff;
           return Math.max(0, sp.left + (off - innerOff) * scroller.clientWidth);
         }
         return sp.left;
@@ -1728,9 +1701,12 @@
       if (estX < x) {from = estimated; fromX = estX;}
       // Do a binary search between these bounds.
       for (;;) {
-        if (to - from <= 1) return {line: lineNo, ch: (toX - x > x - fromX) ? from : to};
+        if (to - from <= 1) {
+          var after = x - fromX < toX - x;
+          return {line: lineNo, ch: after ? from : to, after: after};
+        }
         var middle = Math.ceil((from + to) / 2), middleX = getX(middle);
-        if (middleX > x) {to = middle; toX = middleX;}
+        if (middleX > x) {to = middle; toX = middleX; if (wrongLine) toX += 1000; }
         else {from = middle; fromX = middleX;}
       }
     }
@@ -1739,26 +1715,32 @@
       return {x: off.left + local.x, y: off.top + local.y, yBot: off.top + local.yBot};
     }
 
-    var cachedHeight, cachedHeightFor, measureText;
+    var cachedHeight, cachedHeightFor, measurePre;
     function textHeight() {
-      if (measureText == null) {
-        measureText = "<pre>";
-        for (var i = 0; i < 49; ++i) measureText += "x<br/>";
-        measureText += "x</pre>";
+      if (measurePre == null) {
+        measurePre = elt("pre");
+        for (var i = 0; i < 49; ++i) {
+          measurePre.appendChild(document.createTextNode("x"));
+          measurePre.appendChild(elt("br"));
+        }
+        measurePre.appendChild(document.createTextNode("x"));
       }
       var offsetHeight = lineDiv.clientHeight;
       if (offsetHeight == cachedHeightFor) return cachedHeight;
       cachedHeightFor = offsetHeight;
-      measure.innerHTML = measureText;
+      removeChildrenAndAdd(measure, measurePre.cloneNode(true));
       cachedHeight = measure.firstChild.offsetHeight / 50 || 1;
-      measure.innerHTML = "";
+      removeChildren(measure);
       return cachedHeight;
     }
     var cachedWidth, cachedWidthFor = 0;
     function charWidth() {
       if (scroller.clientWidth == cachedWidthFor) return cachedWidth;
       cachedWidthFor = scroller.clientWidth;
-      return (cachedWidth = stringWidth("x"));
+      var anchor = elt("span", "x");
+      var pre = elt("pre", [anchor]);
+      removeChildrenAndAdd(measure, pre);
+      return (cachedWidth = anchor.offsetWidth || 10);
     }
     function paddingTop() {return lineSpace.offsetTop;}
     function paddingLeft() {return lineSpace.offsetLeft;}
@@ -1775,6 +1757,7 @@
       var offL = eltOffset(lineSpace, true);
       return coordsChar(x - offL.left, y - offL.top);
     }
+    var detectingSelectAll;
     function onContextMenu(e) {
       var pos = posFromMouse(e), scrollPos = scrollbar.scrollTop;
       if (!pos || opera) return; // Opera is difficult.
@@ -1786,19 +1769,30 @@
       input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) +
         "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: white; " +
         "border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);";
-      leaveInputAlone = true;
-      var val = input.value = getSelection();
       focusInput();
-      selectInput(input);
+      resetInput(true);
+      // Adds "Select all" to context menu in FF
+      if (posEq(sel.from, sel.to)) input.value = prevInput = " ";
+
       function rehide() {
-        var newVal = splitLines(input.value).join("\n");
-        if (newVal != val && !options.readOnly) operation(replaceSelection)(newVal, "end");
         inputDiv.style.position = "relative";
         input.style.cssText = oldCSS;
         if (ie_lt9) scrollbar.scrollTop = scrollPos;
-        leaveInputAlone = false;
-        resetInput(true);
         slowPoll();
+
+        // Try to detect the user choosing select-all 
+        if (input.selectionStart != null) {
+          clearTimeout(detectingSelectAll);
+          var extval = input.value = " " + (posEq(sel.from, sel.to) ? "" : input.value), i = 0;
+          prevInput = " ";
+          input.selectionStart = 1; input.selectionEnd = extval.length;
+          detectingSelectAll = setTimeout(function poll(){
+            if (prevInput == " " && input.selectionStart == 0)
+              operation(commands.selectAll)(instance);
+            else if (i++ < 10) detectingSelectAll = setTimeout(poll, 500);
+            else resetInput();
+          }, 200);
+        }
       }
 
       if (gecko) {
@@ -1819,7 +1813,7 @@
       cursor.style.visibility = "";
       blinker = setInterval(function() {
         cursor.style.visibility = (on = !on) ? "" : "hidden";
-      }, 650);
+      }, options.cursorBlinkRate);
     }
 
     var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"};
@@ -1882,70 +1876,39 @@
       return minline;
     }
     function getStateBefore(n) {
-      var start = findStartLine(n), state = start && getLine(start-1).stateAfter;
+      var pos = findStartLine(n), state = pos && getLine(pos-1).stateAfter;
       if (!state) state = startState(mode);
       else state = copyState(mode, state);
-      doc.iter(start, n, function(line) {
-        line.highlight(mode, state, options.tabSize);
-        line.stateAfter = copyState(mode, state);
+      doc.iter(pos, n, function(line) {
+        line.process(mode, state, options.tabSize);
+        line.stateAfter = (pos == n - 1 || pos % 5 == 0) ? copyState(mode, state) : null;
       });
-      if (start < n) changes.push({from: start, to: n});
-      if (n < doc.size && !getLine(n).stateAfter) work.push(n);
       return state;
     }
-    function highlightLines(start, end) {
-      var state = getStateBefore(start);
-      doc.iter(start, end, function(line) {
-        line.highlight(mode, state, options.tabSize);
-        line.stateAfter = copyState(mode, state);
-      });
-    }
     function highlightWorker() {
-      var end = +new Date + options.workTime;
-      var foundWork = work.length;
-      while (work.length) {
-        if (!getLine(showingFrom).stateAfter) var task = showingFrom;
-        else var task = work.pop();
-        if (task >= doc.size) continue;
-        var start = findStartLine(task), state = start && getLine(start-1).stateAfter;
-        if (state) state = copyState(mode, state);
-        else state = startState(mode);
-
-        var unchanged = 0, compare = mode.compareStates, realChange = false,
-            i = start, bail = false;
-        doc.iter(i, doc.size, function(line) {
-          var hadState = line.stateAfter;
-          if (+new Date > end) {
-            work.push(i);
-            startWorker(options.workDelay);
-            if (realChange) changes.push({from: task, to: i + 1});
-            return (bail = true);
-          }
-          var changed = line.highlight(mode, state, options.tabSize);
-          if (changed) realChange = true;
+      if (frontier >= showingTo) return;
+      var end = +new Date + options.workTime, state = copyState(mode, getStateBefore(frontier));
+      var startFrontier = frontier;
+      doc.iter(frontier, showingTo, function(line) {
+        if (frontier >= showingFrom) { // Visible
+          line.highlight(mode, state, options.tabSize);
           line.stateAfter = copyState(mode, state);
-          var done = null;
-          if (compare) {
-            var same = hadState && compare(hadState, state);
-            if (same != Pass) done = !!same;
-          }
-          if (done == null) {
-            if (changed !== false || !hadState) unchanged = 0;
-            else if (++unchanged > 3 && (!mode.indent || mode.indent(hadState, "") == mode.indent(state, "")))
-              done = true;
-          }
-          if (done) return true;
-          ++i;
-        });
-        if (bail) return;
-        if (realChange) changes.push({from: task, to: i + 1});
-      }
-      if (foundWork && options.onHighlightComplete)
-        options.onHighlightComplete(instance);
+        } else {
+          line.process(mode, state, options.tabSize);
+          line.stateAfter = frontier % 5 == 0 ? copyState(mode, state) : null;
+        }
+        ++frontier;
+        if (+new Date > end) {
+          startWorker(options.workDelay);
+          return true;
+        }
+      });
+      if (showingTo > startFrontier && frontier >= showingFrom)
+        operation(function() {changes.push({from: startFrontier, to: frontier});})();
     }
     function startWorker(time) {
-      if (!work.length) return;
-      highlight.set(time, operation(highlightWorker));
+      if (frontier < showingTo)
+        highlight.set(time, highlightWorker);
     }
 
     // Operations are used to wrap changes in such a way that each
@@ -1959,7 +1922,11 @@
     function endOperation() {
       if (updateMaxLine) computeMaxLength();
       if (maxLineChanged && !options.lineWrapping) {
-        widthForcer.style.left = stringWidth(maxLine) + "px";
+        var cursorWidth = widthForcer.offsetWidth, left = measureLine(maxLine, maxLine.text.length).left;
+        if (!ie_lt8) {
+          widthForcer.style.left = left + "px";
+          lineSpace.style.minWidth = (left + cursorWidth) + "px";
+        }
         maxLineChanged = false;
       }
       var newScrollPos, updated;
@@ -1967,16 +1934,16 @@
         var coords = calculateCursorCoords();
         newScrollPos = calculateScrollPos(coords.x, coords.y, coords.x, coords.yBot);
       }
-      if (changes.length) updated = updateDisplay(changes, true, (newScrollPos ? newScrollPos.scrollTop : null));
-      else {
+      if (changes.length || newScrollPos && newScrollPos.scrollTop != null)
+        updated = updateDisplay(changes, true, newScrollPos && newScrollPos.scrollTop);
+      if (!updated) {
         if (selectionChanged) updateSelection();
         if (gutterDirty) updateGutter();
       }
       if (newScrollPos) scrollCursorIntoView();
-      if (selectionChanged) {scrollEditorIntoView(); restartBlink();}
+      if (selectionChanged) restartBlink();
 
-      if (focused && !leaveInputAlone &&
-          (updateInput === true || (updateInput !== false && selectionChanged)))
+      if (focused && (updateInput === true || (updateInput !== false && selectionChanged)))
         resetInput(userSelChange);
 
       if (selectionChanged && options.matchBrackets)
@@ -2038,17 +2005,19 @@
     dragDrop: true,
     onChange: null,
     onCursorActivity: null,
+    onViewportChange: null,
     onGutterClick: null,
-    onHighlightComplete: null,
     onUpdate: null,
     onFocus: null, onBlur: null, onScroll: null,
     matchBrackets: false,
+    cursorBlinkRate: 530,
     workTime: 100,
     workDelay: 200,
     pollInterval: 100,
     undoDepth: 40,
     tabindex: null,
-    autofocus: null
+    autofocus: null,
+    lineNumberFormatter: function(integer) { return integer; }
   };
 
   var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent);
@@ -2080,7 +2049,13 @@
     var spec = CodeMirror.resolveMode(spec);
     var mfactory = modes[spec.name];
     if (!mfactory) return CodeMirror.getMode(options, "text/plain");
-    return mfactory(options, spec);
+    var modeObj = mfactory(options, spec);
+    if (modeExtensions.hasOwnProperty(spec.name)) {
+      var exts = modeExtensions[spec.name];
+      for (var prop in exts) if (exts.hasOwnProperty(prop)) modeObj[prop] = exts[prop];
+    }
+    modeObj.name = spec.name;
+    return modeObj;
   };
   CodeMirror.listModes = function() {
     var list = [];
@@ -2100,6 +2075,13 @@
     extensions[name] = func;
   };
 
+  var modeExtensions = CodeMirror.modeExtensions = {};
+  CodeMirror.extendMode = function(mode, properties) {
+    var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {});
+    for (var prop in properties) if (properties.hasOwnProperty(prop))
+      exts[prop] = properties[prop];
+  };
+
   var commands = CodeMirror.commands = {
     selectAll: function(cm) {cm.setSelection({line: 0, ch: 0}, {line: cm.lineCount() - 1});},
     killLine: function(cm) {
@@ -2197,6 +2179,10 @@
     function lookup(map) {
       map = getKeyMap(map);
       var found = map[name];
+      if (found === false) {
+        if (stop) stop();
+        return true;
+      }
       if (found != null && handle(found)) return true;
       if (map.nofallthrough) {
         if (stop) stop();
@@ -2224,8 +2210,15 @@
     options.value = textarea.value;
     if (!options.tabindex && textarea.tabindex)
       options.tabindex = textarea.tabindex;
-    if (options.autofocus == null && textarea.getAttribute("autofocus") != null)
-      options.autofocus = true;
+    // Set autofocus to true if this textarea is focused, or if it has
+    // autofocus and no other element is focused.
+    if (options.autofocus == null) {
+      var hasFocus = document.body;
+      // doc.activeElement occasionally throws on IE
+      try { hasFocus = document.activeElement; } catch(e) {}
+      options.autofocus = hasFocus == textarea ||
+        textarea.getAttribute("autofocus") != null && hasFocus == document.body;
+    }
 
     function save() {textarea.value = instance.getValue();}
     if (textarea.form) {
@@ -2233,13 +2226,12 @@
       var rmSubmit = connect(textarea.form, "submit", save, true);
       if (typeof textarea.form.submit == "function") {
         var realSubmit = textarea.form.submit;
-        function wrappedSubmit() {
+        textarea.form.submit = function wrappedSubmit() {
           save();
           textarea.form.submit = realSubmit;
           textarea.form.submit();
           textarea.form.submit = wrappedSubmit;
-        }
-        textarea.form.submit = wrappedSubmit;
+        };
       }
     }
 
@@ -2262,6 +2254,18 @@
     return instance;
   };
 
+  var gecko = /gecko\/\d{7}/i.test(navigator.userAgent);
+  var ie = /MSIE \d/.test(navigator.userAgent);
+  var ie_lt8 = /MSIE [1-7]\b/.test(navigator.userAgent);
+  var ie_lt9 = /MSIE [1-8]\b/.test(navigator.userAgent);
+  var quirksMode = ie && document.documentMode == 5;
+  var webkit = /WebKit\//.test(navigator.userAgent);
+  var chrome = /Chrome\//.test(navigator.userAgent);
+  var opera = /Opera\//.test(navigator.userAgent);
+  var safari = /Apple Computer/.test(navigator.vendor);
+  var khtml = /KHTML\//.test(navigator.userAgent);
+  var mac_geLion = /Mac OS X 10\D([7-9]|\d\d)\D/.test(navigator.userAgent);
+
   // Utility functions for working with state. Exported because modes
   // sometimes need to do this.
   function copyState(mode, state) {
@@ -2280,6 +2284,14 @@
     return mode.startState ? mode.startState(a1, a2) : true;
   }
   CodeMirror.startState = startState;
+  CodeMirror.innerMode = function(mode, state) {
+    while (mode.innerMode) {
+      var info = mode.innerMode(state);
+      state = info.state;
+      mode = info.mode;
+    }
+    return info || {mode: mode, state: state};
+  };
 
   // The character stream used by a mode's parser.
   function StringStream(string, tabSize) {
@@ -2290,7 +2302,7 @@
   StringStream.prototype = {
     eol: function() {return this.pos >= this.string.length;},
     sol: function() {return this.pos == 0;},
-    peek: function() {return this.string.charAt(this.pos);},
+    peek: function() {return this.string.charAt(this.pos) || undefined;},
     next: function() {
       if (this.pos < this.string.length)
         return this.string.charAt(this.pos++);
@@ -2321,13 +2333,14 @@
     indentation: function() {return countColumn(this.string, null, this.tabSize);},
     match: function(pattern, consume, caseInsensitive) {
       if (typeof pattern == "string") {
-        function cased(str) {return caseInsensitive ? str.toLowerCase() : str;}
+        var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;};
         if (cased(this.string).indexOf(cased(pattern), this.pos) == this.pos) {
           if (consume !== false) this.pos += pattern.length;
           return true;
         }
       } else {
         var match = this.string.slice(this.pos).match(pattern);
+        if (match && match.index > 0) return null;
         if (match && consume !== false) this.pos += match[0].length;
         return match;
       }
@@ -2336,201 +2349,162 @@
   };
   CodeMirror.StringStream = StringStream;
 
-  function MarkedText(from, to, className, marker) {
-    this.from = from; this.to = to; this.style = className; this.marker = marker;
+  function MarkedSpan(from, to, marker) {
+    this.from = from; this.to = to; this.marker = marker;
   }
-  MarkedText.prototype = {
-    attach: function(line) { this.marker.set.push(line); },
-    detach: function(line) {
-      var ix = indexOf(this.marker.set, line);
-      if (ix > -1) this.marker.set.splice(ix, 1);
-    },
-    split: function(pos, lenBefore) {
-      if (this.to <= pos && this.to != null) return null;
-      var from = this.from < pos || this.from == null ? null : this.from - pos + lenBefore;
-      var to = this.to == null ? null : this.to - pos + lenBefore;
-      return new MarkedText(from, to, this.style, this.marker);
-    },
-    dup: function() { return new MarkedText(null, null, this.style, this.marker); },
-    clipTo: function(fromOpen, from, toOpen, to, diff) {
-      if (fromOpen && to > this.from && (to < this.to || this.to == null))
-        this.from = null;
-      else if (this.from != null && this.from >= from)
-        this.from = Math.max(to, this.from) + diff;
-      if (toOpen && (from < this.to || this.to == null) && (from > this.from || this.from == null))
-        this.to = null;
-      else if (this.to != null && this.to > from)
-        this.to = to < this.to ? this.to + diff : from;
-    },
-    isDead: function() { return this.from != null && this.to != null && this.from >= this.to; },
-    sameSet: function(x) { return this.marker == x.marker; }
-  };
 
-  function Bookmark(pos) {
-    this.from = pos; this.to = pos; this.line = null;
-  }
-  Bookmark.prototype = {
-    attach: function(line) { this.line = line; },
-    detach: function(line) { if (this.line == line) this.line = null; },
-    split: function(pos, lenBefore) {
-      if (pos < this.from) {
-        this.from = this.to = (this.from - pos) + lenBefore;
-        return this;
+  function getMarkedSpanFor(spans, marker, del) {
+    if (spans) for (var i = 0; i < spans.length; ++i) {
+      var span = spans[i];
+      if (span.marker == marker) {
+        if (del) spans.splice(i, 1);
+        return span;
       }
-    },
-    isDead: function() { return this.from > this.to; },
-    clipTo: function(fromOpen, from, toOpen, to, diff) {
-      if ((fromOpen || from < this.from) && (toOpen || to > this.to)) {
-        this.from = 0; this.to = -1;
-      } else if (this.from > from) {
-        this.from = this.to = Math.max(to, this.from) + diff;
-      }
-    },
-    sameSet: function(x) { return false; },
-    find: function() {
-      if (!this.line || !this.line.parent) return null;
-      return {line: lineNo(this.line), ch: this.from};
-    },
-    clear: function() {
-      if (this.line) {
-        var found = indexOf(this.line.marked, this);
-        if (found != -1) this.line.marked.splice(found, 1);
-        this.line = null;
+    }
+  }
+
+  function markedSpansBefore(old, startCh, endCh) {
+    if (old) for (var i = 0, nw; i < old.length; ++i) {
+      var span = old[i], marker = span.marker;
+      var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh);
+      if (startsBefore || marker.type == "bookmark" && span.from == startCh && span.from != endCh) {
+        var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh);
+        (nw || (nw = [])).push({from: span.from,
+                                to: endsAfter ? null : span.to,
+                                marker: marker});
       }
     }
-  };
+    return nw;
+  }
+
+  function markedSpansAfter(old, endCh) {
+    if (old) for (var i = 0, nw; i < old.length; ++i) {
+      var span = old[i], marker = span.marker;
+      var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh);
+      if (endsAfter || marker.type == "bookmark" && span.from == endCh) {
+        var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh);
+        (nw || (nw = [])).push({from: startsBefore ? null : span.from - endCh,
+                                to: span.to == null ? null : span.to - endCh,
+                                marker: marker});
+      }
+    }
+    return nw;
+  }
 
-  // Line objects. These hold state related to a line, including
-  // highlighting info (the styles array).
-  function Line(text, styles) {
-    this.styles = styles || [text, null];
-    this.text = text;
-    this.height = 1;
-    this.marked = this.gutterMarker = this.className = this.bgClassName = this.handlers = null;
-    this.stateAfter = this.parent = this.hidden = null;
-  }
-  Line.inheritMarks = function(text, orig) {
-    var ln = new Line(text), mk = orig && orig.marked;
-    if (mk) {
-      for (var i = 0; i < mk.length; ++i) {
-        if (mk[i].to == null && mk[i].style) {
-          var newmk = ln.marked || (ln.marked = []), mark = mk[i];
-          var nmark = mark.dup(); newmk.push(nmark); nmark.attach(ln);
+  function updateMarkedSpans(oldFirst, oldLast, startCh, endCh, newText) {
+    if (!oldFirst && !oldLast) return newText;
+    // Get the spans that 'stick out' on both sides
+    var first = markedSpansBefore(oldFirst, startCh);
+    var last = markedSpansAfter(oldLast, endCh);
+
+    // Next, merge those two ends
+    var sameLine = newText.length == 1, offset = lst(newText).length + (sameLine ? startCh : 0);
+    if (first) {
+      // Fix up .to properties of first
+      for (var i = 0; i < first.length; ++i) {
+        var span = first[i];
+        if (span.to == null) {
+          var found = getMarkedSpanFor(last, span.marker);
+          if (!found) span.to = startCh;
+          else if (sameLine) span.to = found.to == null ? null : found.to + offset;
         }
       }
     }
-    return ln;
-  }
-  Line.prototype = {
-    // Replace a piece of a line, keeping the styles around it intact.
-    replace: function(from, to_, text) {
-      var st = [], mk = this.marked, to = to_ == null ? this.text.length : to_;
-      copyStyles(0, from, this.styles, st);
-      if (text) st.push(text, null);
-      copyStyles(to, this.text.length, this.styles, st);
-      this.styles = st;
-      this.text = this.text.slice(0, from) + text + this.text.slice(to);
-      this.stateAfter = null;
-      if (mk) {
-        var diff = text.length - (to - from);
-        for (var i = 0; i < mk.length; ++i) {
-          var mark = mk[i];
-          mark.clipTo(from == null, from || 0, to_ == null, to, diff);
-          if (mark.isDead()) {mark.detach(this); mk.splice(i--, 1);}
-        }
-      }
-    },
-    // Split a part off a line, keeping styles and markers intact.
-    split: function(pos, textBefore) {
-      var st = [textBefore, null], mk = this.marked;
-      copyStyles(pos, this.text.length, this.styles, st);
-      var taken = new Line(textBefore + this.text.slice(pos), st);
-      if (mk) {
-        for (var i = 0; i < mk.length; ++i) {
-          var mark = mk[i];
-          var newmark = mark.split(pos, textBefore.length);
-          if (newmark) {
-            if (!taken.marked) taken.marked = [];
-            taken.marked.push(newmark); newmark.attach(taken);
-            if (newmark == mark) mk.splice(i--, 1);
+    if (last) {
+      // Fix up .from in last (or move them into first in case of sameLine)
+      for (var i = 0; i < last.length; ++i) {
+        var span = last[i];
+        if (span.to != null) span.to += offset;
+        if (span.from == null) {
+          var found = getMarkedSpanFor(first, span.marker);
+          if (!found) {
+            span.from = offset;
+            if (sameLine) (first || (first = [])).push(span);
           }
+        } else {
+          span.from += offset;
+          if (sameLine) (first || (first = [])).push(span);
         }
       }
-      return taken;
-    },
-    append: function(line) {
-      var mylen = this.text.length, mk = line.marked, mymk = this.marked;
-      this.text += line.text;
-      copyStyles(0, line.text.length, line.styles, this.styles);
-      if (mymk) {
-        for (var i = 0; i < mymk.length; ++i)
-          if (mymk[i].to == null) mymk[i].to = mylen;
-      }
-      if (mk && mk.length) {
-        if (!mymk) this.marked = mymk = [];
-        outer: for (var i = 0; i < mk.length; ++i) {
-          var mark = mk[i];
-          if (!mark.from) {
-            for (var j = 0; j < mymk.length; ++j) {
-              var mymark = mymk[j];
-              if (mymark.to == mylen && mymark.sameSet(mark)) {
-                mymark.to = mark.to == null ? null : mark.to + mylen;
-                if (mymark.isDead()) {
-                  mymark.detach(this);
-                  mk.splice(i--, 1);
-                }
-                continue outer;
-              }
-            }
-          }
-          mymk.push(mark);
-          mark.attach(this);
-          mark.from += mylen;
-          if (mark.to != null) mark.to += mylen;
-        }
-      }
-    },
-    fixMarkEnds: function(other) {
-      var mk = this.marked, omk = other.marked;
-      if (!mk) return;
-      for (var i = 0; i < mk.length; ++i) {
-        var mark = mk[i], close = mark.to == null;
-        if (close && omk) {
-          for (var j = 0; j < omk.length; ++j)
-            if (omk[j].sameSet(mark)) {close = false; break;}
-        }
-        if (close) mark.to = this.text.length;
-      }
-    },
-    fixMarkStarts: function() {
-      var mk = this.marked;
-      if (!mk) return;
-      for (var i = 0; i < mk.length; ++i)
-        if (mk[i].from == null) mk[i].from = 0;
-    },
-    addMark: function(mark) {
-      mark.attach(this);
-      if (this.marked == null) this.marked = [];
-      this.marked.push(mark);
-      this.marked.sort(function(a, b){return (a.from || 0) - (b.from || 0);});
+    }
+
+    var newMarkers = [newHL(newText[0], first)];
+    if (!sameLine) {
+      // Fill gap with whole-line-spans
+      var gap = newText.length - 2, gapMarkers;
+      if (gap > 0 && first)
+        for (var i = 0; i < first.length; ++i)
+          if (first[i].to == null)
+            (gapMarkers || (gapMarkers = [])).push({from: null, to: null, marker: first[i].marker});
+      for (var i = 0; i < gap; ++i)
+        newMarkers.push(newHL(newText[i+1], gapMarkers));
+      newMarkers.push(newHL(lst(newText), last));
+    }
+    return newMarkers;
+  }
+
+  // hl stands for history-line, a data structure that can be either a
+  // string (line without markers) or a {text, markedSpans} object.
+  function hlText(val) { return typeof val == "string" ? val : val.text; }
+  function hlSpans(val) { return typeof val == "string" ? null : val.markedSpans; }
+  function newHL(text, spans) { return spans ? {text: text, markedSpans: spans} : text; }
+
+  function detachMarkedSpans(line) {
+    var spans = line.markedSpans;
+    if (!spans) return;
+    for (var i = 0; i < spans.length; ++i) {
+      var lines = spans[i].marker.lines;
+      var ix = indexOf(lines, line);
+      lines.splice(ix, 1);
+    }
+    line.markedSpans = null;
+  }
+
+  function attachMarkedSpans(line, spans) {
+    if (!spans) return;
+    for (var i = 0; i < spans.length; ++i)
+      var marker = spans[i].marker.lines.push(line);
+    line.markedSpans = spans;
+  }
+
+  // When measuring the position of the end of a line, different
+  // browsers require different approaches. If an empty span is added,
+  // many browsers report bogus offsets. Of those, some (Webkit,
+  // recent IE) will accept a space without moving the whole span to
+  // the next line when wrapping it, others work with a zero-width
+  // space.
+  var eolSpanContent = " ";
+  if (gecko || (ie && !ie_lt8)) eolSpanContent = "\u200b";
+  else if (opera) eolSpanContent = "";
+
+  // Line objects. These hold state related to a line, including
+  // highlighting info (the styles array).
+  function Line(text, markedSpans) {
+    this.text = text;
+    this.height = 1;
+    attachMarkedSpans(this, markedSpans);
+  }
+  Line.prototype = {
+    update: function(text, markedSpans) {
+      this.text = text;
+      this.stateAfter = this.styles = null;
+      detachMarkedSpans(this);
+      attachMarkedSpans(this, markedSpans);
     },
     // Run the given mode's parser over a line, update the styles
     // array, which contains alternating fragments of text and CSS
     // classes.
     highlight: function(mode, state, tabSize) {
-      var stream = new StringStream(this.text, tabSize), st = this.styles, pos = 0;
-      var changed = false, curWord = st[0], prevWord;
+      var stream = new StringStream(this.text, tabSize), st = this.styles || (this.styles = []);
+      var pos = st.length = 0;
       if (this.text == "" && mode.blankLine) mode.blankLine(state);
       while (!stream.eol()) {
-        var style = mode.token(stream, state);
-        var substr = this.text.slice(stream.start, stream.pos);
+        var style = mode.token(stream, state), substr = stream.current();
         stream.start = stream.pos;
-        if (pos && st[pos-1] == style)
+        if (pos && st[pos-1] == style) {
           st[pos-2] += substr;
-        else if (substr) {
-          if (!changed && (st[pos+1] != style || (pos && st[pos-2] != prevWord))) changed = true;
+        } else if (substr) {
           st[pos++] = substr; st[pos++] = style;
-          prevWord = curWord; curWord = st[pos];
         }
         // Give up when line is ridiculously long
         if (stream.pos > 5000) {
@@ -2538,17 +2512,19 @@
           break;
         }
       }
-      if (st.length != pos) {st.length = pos; changed = true;}
-      if (pos && st[pos-2] != prevWord) changed = true;
-      // Short lines with simple highlights return null, and are
-      // counted as changed by the driver because they are likely to
-      // highlight the same way in various contexts.
-      return changed || (st.length < 5 && this.text.length < 10 ? null : false);
+    },
+    process: function(mode, state, tabSize) {
+      var stream = new StringStream(this.text, tabSize);
+      if (this.text == "" && mode.blankLine) mode.blankLine(state);
+      while (!stream.eol() && stream.pos <= 5000) {
+        mode.token(stream, state);
+        stream.start = stream.pos;
+      }
     },
     // Fetch the parser token for a given character. Useful for hacks
     // that want to inspect the mode state (say, for completion).
-    getTokenAt: function(mode, state, ch) {
-      var txt = this.text, stream = new StringStream(txt);
+    getTokenAt: function(mode, state, tabSize, ch) {
+      var txt = this.text, stream = new StringStream(txt, tabSize);
       while (stream.pos < ch && !stream.eol()) {
         stream.start = stream.pos;
         var style = mode.token(stream, state);
@@ -2562,98 +2538,108 @@
     indentation: function(tabSize) {return countColumn(this.text, null, tabSize);},
     // Produces an HTML fragment for the line, taking selection,
     // marking, and highlighting into account.
-    getHTML: function(makeTab, wrapAt, wrapId, wrapWBR) {
-      var html = [], first = true, col = 0;
-      function span_(text, style) {
+    getContent: function(tabSize, wrapAt, compensateForWrapping) {
+      var first = true, col = 0, specials = /[\t\u0000-\u0019\u200b\u2028\u2029\uFEFF]/g;
+      var pre = elt("pre");
+      function span_(html, text, style) {
         if (!text) return;
         // Work around a bug where, in some compat modes, IE ignores leading spaces
         if (first && ie && text.charAt(0) == " ") text = "\u00a0" + text.slice(1);
         first = false;
-        if (text.indexOf("\t") == -1) {
+        if (!specials.test(text)) {
           col += text.length;
-          var escaped = htmlEscape(text);
+          var content = document.createTextNode(text);
         } else {
-          var escaped = "";
-          for (var pos = 0;;) {
-            var idx = text.indexOf("\t", pos);
-            if (idx == -1) {
-              escaped += htmlEscape(text.slice(pos));
-              col += text.length - pos;
-              break;
+          var content = document.createDocumentFragment(), pos = 0;
+          while (true) {
+            specials.lastIndex = pos;
+            var m = specials.exec(text);
+            var skipped = m ? m.index - pos : text.length - pos;
+            if (skipped) {
+              content.appendChild(document.createTextNode(text.slice(pos, pos + skipped)));
+              col += skipped;
+            }
+            if (!m) break;
+            pos += skipped + 1;
+            if (m[0] == "\t") {
+              var tabWidth = tabSize - col % tabSize;
+              content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab"));
+              col += tabWidth;
             } else {
-              col += idx - pos;
-              var tab = makeTab(col);
-              escaped += htmlEscape(text.slice(pos, idx)) + tab.html;
-              col += tab.width;
-              pos = idx + 1;
+              var token = elt("span", "\u2022", "cm-invalidchar");
+              token.title = "\\u" + m[0].charCodeAt(0).toString(16);
+              content.appendChild(token);
+              col += 1;
             }
           }
         }
-        if (style) html.push('<span class="', style, '">', escaped, "</span>");
-        else html.push(escaped);
+        if (style) html.appendChild(elt("span", [content], style));
+        else html.appendChild(content);
       }
       var span = span_;
       if (wrapAt != null) {
-        var outPos = 0, open = "<span id=\"" + wrapId + "\">";
-        span = function(text, style) {
+        var outPos = 0, anchor = pre.anchor = elt("span");
+        span = function(html, text, style) {
           var l = text.length;
           if (wrapAt >= outPos && wrapAt < outPos + l) {
             if (wrapAt > outPos) {
-              span_(text.slice(0, wrapAt - outPos), style);
+              span_(html, text.slice(0, wrapAt - outPos), style);
               // See comment at the definition of spanAffectsWrapping
-              if (wrapWBR) html.push("<wbr>");
+              if (compensateForWrapping) html.appendChild(elt("wbr"));
             }
-            html.push(open);
+            html.appendChild(anchor);
             var cut = wrapAt - outPos;
-            span_(opera ? text.slice(cut, cut + 1) : text.slice(cut), style);
-            html.push("</span>");
-            if (opera) span_(text.slice(cut + 1), style);
+            span_(anchor, opera ? text.slice(cut, cut + 1) : text.slice(cut), style);
+            if (opera) span_(html, text.slice(cut + 1), style);
             wrapAt--;
             outPos += l;
           } else {
             outPos += l;
-            span_(text, style);
-            // Output empty wrapper when at end of line
-            if (outPos == wrapAt && outPos == len) html.push(open + " </span>");
+            span_(html, text, style);
+            if (outPos == wrapAt && outPos == len) {
+              setTextContent(anchor, eolSpanContent);
+              html.appendChild(anchor);
+            }
             // Stop outputting HTML when gone sufficiently far beyond measure
             else if (outPos > wrapAt + 10 && /\s/.test(text)) span = function(){};
           }
-        }
+        };
       }
 
-      var st = this.styles, allText = this.text, marked = this.marked;
+      var st = this.styles, allText = this.text, marked = this.markedSpans;
       var len = allText.length;
       function styleToClass(style) {
         if (!style) return null;
         return "cm-" + style.replace(/ +/g, " cm-");
       }
-
       if (!allText && wrapAt == null) {
-        span(" ");
+        span(pre, " ");
       } else if (!marked || !marked.length) {
         for (var i = 0, ch = 0; ch < len; i+=2) {
           var str = st[i], style = st[i+1], l = str.length;
           if (ch + l > len) str = str.slice(0, len - ch);
           ch += l;
-          span(str, styleToClass(style));
+          span(pre, str, styleToClass(style));
         }
       } else {
+        marked.sort(function(a, b) { return a.from - b.from; });
         var pos = 0, i = 0, text = "", style, sg = 0;
         var nextChange = marked[0].from || 0, marks = [], markpos = 0;
-        function advanceMarks() {
+        var advanceMarks = function() {
           var m;
           while (markpos < marked.length &&
                  ((m = marked[markpos]).from == pos || m.from == null)) {
-            if (m.style != null) marks.push(m);
+            if (m.marker.type == "range") marks.push(m);
             ++markpos;
           }
           nextChange = markpos < marked.length ? marked[markpos].from : Infinity;
           for (var i = 0; i < marks.length; ++i) {
-            var to = marks[i].to || Infinity;
+            var to = marks[i].to;
+            if (to == null) to = Infinity;
             if (to == pos) marks.splice(i--, 1);
             else nextChange = Math.min(to, nextChange);
           }
-        }
+        };
         var m = 0;
         while (pos < len) {
           if (nextChange == pos) advanceMarks();
@@ -2662,9 +2648,13 @@
             if (text) {
               var end = pos + text.length;
               var appliedStyle = style;
-              for (var j = 0; j < marks.length; ++j)
-                appliedStyle = (appliedStyle ? appliedStyle + " " : "") + marks[j].style;
-              span(end > upto ? text.slice(0, upto - pos) : text, appliedStyle);
+              for (var j = 0; j < marks.length; ++j) {
+                var mark = marks[j];
+                appliedStyle = (appliedStyle ? appliedStyle + " " : "") + mark.marker.style;
+                if (mark.marker.endStyle && mark.to === Math.min(end, upto)) appliedStyle += " " + mark.marker.endStyle;
+                if (mark.marker.startStyle && mark.from === pos) appliedStyle += " " + mark.marker.startStyle;
+              }
+              span(pre, end > upto ? text.slice(0, upto - pos) : text, appliedStyle);
               if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;}
               pos = end;
             }
@@ -2672,28 +2662,13 @@
           }
         }
       }
-      return html.join("");
+      return pre;
     },
     cleanUp: function() {
       this.parent = null;
-      if (this.marked)
-        for (var i = 0, e = this.marked.length; i < e; ++i) this.marked[i].detach(this);
+      detachMarkedSpans(this);
     }
   };
-  // Utility used by replace and split above
-  function copyStyles(from, to, source, dest) {
-    for (var i = 0, pos = 0, state = 0; pos < to; i+=2) {
-      var part = source[i], end = pos + part.length;
-      if (state == 0) {
-        if (end > from) dest.push(part.slice(from - pos, Math.min(part.length, to - pos)), source[i+1]);
-        if (end >= from) state = 1;
-      } else if (state == 1) {
-        if (end > to) dest.push(part.slice(0, to - pos), source[i+1]);
-        else dest.push(part, source[i+1]);
-      }
-      pos = end;
-    }
-  }
 
   // Data structure that holds the sequence of lines.
   function LeafChunk(lines) {
@@ -2894,7 +2869,7 @@
   History.prototype = {
     addChange: function(start, added, old) {
       this.undone.length = 0;
-      var time = +new Date, cur = this.done[this.done.length - 1], last = cur && cur[cur.length - 1];
+      var time = +new Date, cur = lst(this.done), last = cur && lst(cur);
       var dtime = time - this.time;
 
       if (this.compound && cur && !this.closed) {
@@ -2943,10 +2918,14 @@
 
   function e_target(e) {return e.target || e.srcElement;}
   function e_button(e) {
-    if (e.which) return e.which;
-    else if (e.button & 1) return 1;
-    else if (e.button & 2) return 3;
-    else if (e.button & 4) return 2;
+    var b = e.which;
+    if (b == null) {
+      if (e.button & 1) b = 1;
+      else if (e.button & 2) b = 3;
+      else if (e.button & 4) b = 2;
+    }
+    if (mac && e.ctrlKey && b == 1) b = 3;
+    return b;
   }
 
   // Allow 3rd-party code to override event properties by adding an override
@@ -2975,30 +2954,18 @@
 
   var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}};
 
-  var gecko = /gecko\/\d{7}/i.test(navigator.userAgent);
-  var ie = /MSIE \d/.test(navigator.userAgent);
-  var ie_lt8 = /MSIE [1-7]\b/.test(navigator.userAgent);
-  var ie_lt9 = /MSIE [1-8]\b/.test(navigator.userAgent);
-  var quirksMode = ie && document.documentMode == 5;
-  var webkit = /WebKit\//.test(navigator.userAgent);
-  var chrome = /Chrome\//.test(navigator.userAgent);
-  var opera = /Opera\//.test(navigator.userAgent);
-  var safari = /Apple Computer/.test(navigator.vendor);
-  var khtml = /KHTML\//.test(navigator.userAgent);
-  var mac_geLion = /Mac OS X 10\D([7-9]|\d\d)\D/.test(navigator.userAgent);
-
   // Detect drag-and-drop
   var dragAndDrop = function() {
     // There is *some* kind of drag-and-drop support in IE6-8, but I
     // couldn't get it to work yet.
     if (ie_lt9) return false;
-    var div = document.createElement('div');
+    var div = elt('div');
     return "draggable" in div || "dragDrop" in div;
   }();
 
   // Feature-detect whether newlines in textareas are converted to \r\n
   var lineSep = function () {
-    var te = document.createElement("textarea");
+    var te = elt("textarea");
     te.value = "foo\nbar";
     if (te.value.indexOf("\r") > -1) return "\r\n";
     return "\n";
@@ -3030,31 +2997,7 @@
     return n;
   }
 
-  function computedStyle(elt) {
-    if (elt.currentStyle) return elt.currentStyle;
-    return window.getComputedStyle(elt, null);
-  }
-
-  // Find the position of an element by following the offsetParent chain.
-  // If screen==true, it returns screen (rather than page) coordinates.
   function eltOffset(node, screen) {
-    var bod = node.ownerDocument.body;
-    var x = 0, y = 0, skipBody = false;
-    for (var n = node; n; n = n.offsetParent) {
-      var ol = n.offsetLeft, ot = n.offsetTop;
-      // Firefox reports weird inverted offsets when the body has a border.
-      if (n == bod) { x += Math.abs(ol); y += Math.abs(ot); }
-      else { x += ol, y += ot; }
-      if (screen && computedStyle(n).position == "fixed")
-        skipBody = true;
-    }
-    var e = screen && !skipBody ? null : bod;
-    for (var n = node.parentNode; n != e; n = n.parentNode)
-      if (n.scrollLeft != null) { x -= n.scrollLeft; y -= n.scrollTop;}
-    return {left: x, top: y};
-  }
-  // Use the faster and saner getBoundingClientRect method when possible.
-  if (document.documentElement.getBoundingClientRect != null) eltOffset = function(node, screen) {
     // Take the parts of bounding client rect that we are interested in so we are able to edit if need be,
     // since the returned value cannot be changed externally (they are kept in sync as the element moves within the page)
     try { var box = node.getBoundingClientRect(); box = { top: box.top, left: box.left }; }
@@ -3070,12 +3013,21 @@
       }
     }
     return box;
-  };
+  }
 
-  // Get a node's text content.
   function eltText(node) {
     return node.textContent || node.innerText || node.nodeValue || "";
   }
+
+  var spaceStrs = [""];
+  function spaceStr(n) {
+    while (spaceStrs.length <= n)
+      spaceStrs.push(lst(spaceStrs) + " ");
+    return spaceStrs[n];
+  }
+
+  function lst(arr) { return arr[arr.length-1]; }
+
   function selectInput(node) {
     if (ios) { // Mobile Safari apparently has a bug where select() is broken.
       node.selectionStart = 0;
@@ -3088,27 +3040,27 @@
   function posLess(a, b) {return a.line < b.line || (a.line == b.line && a.ch < b.ch);}
   function copyPos(x) {return {line: x.line, ch: x.ch};}
 
-  var escapeElement = document.createElement("pre");
-  function htmlEscape(str) {
-    escapeElement.textContent = str;
-    return escapeElement.innerHTML;
+  function elt(tag, content, className, style) {
+    var e = document.createElement(tag);
+    if (className) e.className = className;
+    if (style) e.style.cssText = style;
+    if (typeof content == "string") setTextContent(e, content);
+    else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]);
+    return e;
   }
-  // Recent (late 2011) Opera betas insert bogus newlines at the start
-  // of the textContent, so we strip those.
-  if (htmlEscape("a") == "\na") {
-    htmlEscape = function(str) {
-      escapeElement.textContent = str;
-      return escapeElement.innerHTML.slice(1);
-    };
-  // Some IEs don't preserve tabs through innerHTML
-  } else if (htmlEscape("\t") != "\t") {
-    htmlEscape = function(str) {
-      escapeElement.innerHTML = "";
-      escapeElement.appendChild(document.createTextNode(str));
-      return escapeElement.innerHTML;
-    };
+  function removeChildren(e) {
+    e.innerHTML = "";
+    return e;
+  }
+  function removeChildrenAndAdd(parent, e) {
+    removeChildren(parent).appendChild(e);
   }
-  CodeMirror.htmlEscape = htmlEscape;
+  function setTextContent(e, str) {
+    if (ie_lt9) {
+      e.innerHTML = "";
+      e.appendChild(document.createTextNode(str));
+    } else e.textContent = str;
+  }
 
   // Used to position the cursor after an undo/redo by finding the
   // last edited character.
@@ -3133,14 +3085,22 @@
   // See if "".split is the broken IE version, if so, provide an
   // alternative way to split lines.
   var splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) {
-    var pos = 0, nl, result = [];
-    while ((nl = string.indexOf("\n", pos)) > -1) {
-      result.push(string.slice(pos, string.charAt(nl-1) == "\r" ? nl - 1 : nl));
-      pos = nl + 1;
+    var pos = 0, result = [], l = string.length;
+    while (pos <= l) {
+      var nl = string.indexOf("\n", pos);
+      if (nl == -1) nl = string.length;
+      var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl);
+      var rt = line.indexOf("\r");
+      if (rt != -1) {
+        result.push(line.slice(0, rt));
+        pos += rt + 1;
+      } else {
+        result.push(line);
+        pos = nl + 1;
+      }
     }
-    result.push(string.slice(pos));
     return result;
-  } : function(string){return string.split(/\r?\n/);};
+  } : function(string){return string.split(/\r\n?|\n/);};
   CodeMirror.splitLines = splitLines;
 
   var hasSelection = window.getSelection ? function(te) {
@@ -3175,5 +3135,7 @@
     for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i;
   })();
 
+  CodeMirror.version = "2.34";
+
   return CodeMirror;
 })();
--- a/rhodecode/public/js/rhodecode.js	Wed Sep 19 23:02:36 2012 +0200
+++ b/rhodecode/public/js/rhodecode.js	Fri Sep 28 23:28:10 2012 +0200
@@ -417,6 +417,7 @@
 		}
 		removeInlineForm(newtr);
 		YUD.removeClass(parent_tr, 'form-open');
+		YUD.removeClass(parent_tr, 'hl-comment');
 		
 	});
 	
@@ -438,6 +439,7 @@
 		  return
 	  }	
 	  YUD.addClass(tr,'form-open');
+	  YUD.addClass(tr,'hl-comment');
 	  var node = YUD.getElementsByClassName('full_f_path',null,tr.parentNode.parentNode.parentNode)[0];
 	  var f_path = YUD.getAttribute(node,'path');
 	  var lineno = getLineNo(tr);
--- a/rhodecode/templates/files/files.html	Wed Sep 19 23:02:36 2012 +0200
+++ b/rhodecode/templates/files/files.html	Fri Sep 28 23:28:10 2012 +0200
@@ -89,6 +89,8 @@
     ypjax_links();
     tooltip_activate();
     fileBrowserListeners(State.url, State.data.node_list_url, State.data.url_base);
+    YUE.on('hlcode','mouseup',getSelectionLink("${_('Selection link')}"));
+
     // Inform Google Analytics of the change
     if ( typeof window.pageTracker !== 'undefined' ) {
         window.pageTracker._trackPageview(State.url);
--- a/rhodecode/templates/pullrequests/pullrequest.html	Wed Sep 19 23:02:36 2012 +0200
+++ b/rhodecode/templates/pullrequests/pullrequest.html	Fri Sep 28 23:28:10 2012 +0200
@@ -21,14 +21,7 @@
     </div>
     ${h.form(url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')}
     <div style="float:left;padding:0px 30px 30px 30px">
-       <div style="padding:0px 5px 5px 5px">
-         <span>
-           <a id="refresh" href="#">
-             <img class="icon" title="${_('Refresh')}" alt="${_('Refresh')}" src="${h.url('/images/icons/arrow_refresh.png')}"/>
-             ${_('refresh overview')}
-           </a>
-         </span>
-       </div>
+
         ##ORG
         <div style="float:left">
             <div class="fork_user">
@@ -55,6 +48,11 @@
                 <span style="font-size: 20px">
                 ${h.select('other_repo',c.default_pull_request ,c.other_repos,class_='refs')}:${h.select('other_ref',c.default_pull_request_rev,c.default_revs,class_='refs')}
                 </span>
+         <span style="padding:3px">
+           <a id="refresh" href="#" class="tooltip" title="${h.tooltip(_('refresh overview'))}">
+             <img style="margin:3px" class="icon" title="${_('Refresh')}" alt="${_('Refresh')}" src="${h.url('/images/icons/arrow_refresh.png')}"/>
+           </a>
+         </span>
                  <div id="other_repo_desc" style="padding:5px 3px 3px 42px;"></div>
             </div>
             <div style="clear:both;padding-top: 10px"></div>
--- a/rhodecode/tests/models/test_permissions.py	Wed Sep 19 23:02:36 2012 +0200
+++ b/rhodecode/tests/models/test_permissions.py	Fri Sep 28 23:28:10 2012 +0200
@@ -10,7 +10,7 @@
 from rhodecode.model.meta import Session
 from rhodecode.model.users_group import UsersGroupModel
 from rhodecode.lib.auth import AuthUser
-
+from rhodecode.tests.api.api_base import create_repo
 
 
 class TestPermissions(unittest.TestCase):
@@ -40,6 +40,7 @@
     def tearDown(self):
         if hasattr(self, 'test_repo'):
             RepoModel().delete(repo=self.test_repo)
+
         UserModel().delete(self.u1)
         UserModel().delete(self.u2)
         UserModel().delete(self.u3)
@@ -425,3 +426,47 @@
                          set(['hg.create.repository', 'hg.fork.repository',
                               'hg.register.manual_activate',
                               'repository.read']))
+
+    def test_owner_permissions_doesnot_get_overwritten_by_group(self):
+        #create repo as USER,
+        self.test_repo = repo = RepoModel().create_repo(repo_name='myownrepo',
+                                repo_type='hg',
+                                description='desc',
+                                owner=self.u1)
+
+        Session().commit()
+        #he has permissions of admin as owner
+        u1_auth = AuthUser(user_id=self.u1.user_id)
+        self.assertEqual(u1_auth.permissions['repositories']['myownrepo'],
+                         'repository.admin')
+        #set his permission as users group, he should still be admin
+        self.ug1 = UsersGroupModel().create('G1')
+        # add user to group
+        UsersGroupModel().add_user_to_group(self.ug1, self.u1)
+        RepoModel().grant_users_group_permission(repo, group_name=self.ug1,
+                                                 perm='repository.none')
+
+        Session().commit()
+        u1_auth = AuthUser(user_id=self.u1.user_id)
+        self.assertEqual(u1_auth.permissions['repositories']['myownrepo'],
+                         'repository.admin')
+
+    def test_owner_permissions_doesnot_get_overwritten_by_others(self):
+        #create repo as USER,
+        self.test_repo = repo = RepoModel().create_repo(repo_name='myownrepo',
+                                repo_type='hg',
+                                description='desc',
+                                owner=self.u1)
+
+        Session().commit()
+        #he has permissions of admin as owner
+        u1_auth = AuthUser(user_id=self.u1.user_id)
+        self.assertEqual(u1_auth.permissions['repositories']['myownrepo'],
+                         'repository.admin')
+        #set his permission as user, he should still be admin
+        RepoModel().grant_user_permission(repo, user=self.u1,
+                                          perm='repository.none')
+        Session().commit()
+        u1_auth = AuthUser(user_id=self.u1.user_id)
+        self.assertEqual(u1_auth.permissions['repositories']['myownrepo'],
+                         'repository.admin')
--- a/setup.py	Wed Sep 19 23:02:36 2012 +0200
+++ b/setup.py	Fri Sep 28 23:28:10 2012 +0200
@@ -60,10 +60,10 @@
     requirements.append("unittest2")
 
 if is_windows:
-    requirements.append("mercurial==2.3.0")
+    requirements.append("mercurial==2.3.1")
 else:
     requirements.append("py-bcrypt")
-    requirements.append("mercurial==2.3.0")
+    requirements.append("mercurial==2.3.1")
 
 
 dependency_links = [