diff --git a/accounts/migrations/0014_alter_user_editor_mode.py b/accounts/migrations/0014_alter_user_editor_mode.py new file mode 100644 index 000000000..510e92196 --- /dev/null +++ b/accounts/migrations/0014_alter_user_editor_mode.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.14 on 2024-07-24 19:16 + +from django.db import migrations, models + + +def switch_sublime_to_default(apps, schema_editor): + User = apps.get_model("accounts", "User") + User.objects.filter(editor_mode="sublime").update(editor_mode="default") + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0013_alter_user_email_alter_user_username'), + ] + + operations = [ + migrations.RunPython(switch_sublime_to_default), + migrations.AlterField( + model_name='user', + name='editor_mode', + field=models.CharField(choices=[('default', 'Default'), ('emacs', 'Emacs'), ('vim', 'Vim')], default='default', help_text="Which key bindings you prefer when editing larger amounts of text or code. (If you do not understand what this means, leave it as 'Default'.)", max_length=20, verbose_name='Editor mode'), + ), + ] diff --git a/accounts/models.py b/accounts/models.py index b9a044db1..f11f733b4 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -113,7 +113,6 @@ class User(AbstractBaseUser, PermissionsMixin): "leave it as 'Default'.)"), choices=( ("default", _("Default")), - ("sublime", "Sublime text"), ("emacs", "Emacs"), ("vim", "Vim"), ), diff --git a/course/exam.py b/course/exam.py index ac4eb126c..763ac3a0a 100644 --- a/course/exam.py +++ b/course/exam.py @@ -302,8 +302,7 @@ def __init__(self, course, editor_mode, *args, **kwargs): from course.utils import get_codemirror_widget cm_widget, _cm_help_text = get_codemirror_widget( - language_mode={"name": "markdown", "xml": True}, - dependencies=("xml",), + language_mode="markdown", interaction_mode=editor_mode) help_text = (gettext('Enter . " "See RELATE documentation for automatic computation of point " "count from [pts:N/N] and [pts:N]. " - "Use Ctrl-Space/Ctrl-Shift-Space " + "Use Ctrl-Semicolon/Ctrl-Shift-Semicolon " "to move between [pts:] fields. ") + cm_help_text), label=_("Feedback text (Ctrl+Shift+F)")) diff --git a/course/page/code.py b/course/page/code.py index 550bbe0f3..45a5c6e83 100644 --- a/course/page/code.py +++ b/course/page/code.py @@ -155,7 +155,6 @@ def __init__(self, read_only, interaction_mode, initial_code, cm_widget, cm_help_text = get_codemirror_widget( language_mode=language_mode, interaction_mode=interaction_mode, - read_only=read_only, # Automatically focus the text field once there has # been some input. @@ -167,10 +166,9 @@ def __init__(self, read_only, interaction_mode, initial_code, initial=initial_code, help_text=cm_help_text, widget=cm_widget, + disabled=read_only, label=_("Answer")) - self.style_codemirror_widget() - def clean(self): # FIXME Should try compilation pass diff --git a/course/page/text.py b/course/page/text.py index 740e70f0e..85daed287 100644 --- a/course/page/text.py +++ b/course/page/text.py @@ -49,7 +49,7 @@ class TextAnswerForm(StyledForm): use_required_attribute = False @staticmethod - def get_text_widget(widget_type, read_only=False, check_only=False, + def get_text_widget(widget_type, check_only=False, interaction_mode=None, initial_text=None): """Returns None if no widget found.""" @@ -59,8 +59,6 @@ def get_text_widget(widget_type, read_only=False, check_only=False, widget = forms.TextInput() widget.attrs["autofocus"] = None - if read_only: - widget.attrs["readonly"] = None return widget, None elif widget_type == "textarea": @@ -68,9 +66,7 @@ def get_text_widget(widget_type, read_only=False, check_only=False, return True widget = forms.Textarea() - # widget.attrs["autofocus"] = None - if read_only: - widget.attrs["readonly"] = None + widget.attrs["autofocus"] = None return widget, None elif widget_type.startswith("editor:"): @@ -80,8 +76,7 @@ def get_text_widget(widget_type, read_only=False, check_only=False, from course.utils import get_codemirror_widget cm_widget, cm_help_text = get_codemirror_widget( language_mode=widget_type[widget_type.find(":")+1:], - interaction_mode=interaction_mode, - read_only=read_only) + interaction_mode=interaction_mode) return cm_widget, cm_help_text @@ -94,18 +89,17 @@ def __init__(self, read_only, interaction_mode, validators, *args, **kwargs): super().__init__(*args, **kwargs) widget, help_text = self.get_text_widget( - widget_type, read_only, + widget_type, interaction_mode=interaction_mode) self.validators = validators self.fields["answer"] = forms.CharField( required=True, initial=initial_text, + disabled=read_only, widget=widget, help_text=help_text, label=_("Answer")) - self.style_codemirror_widget() - def clean(self): cleaned_data = super().clean() diff --git a/course/sandbox.py b/course/sandbox.py index f3e4eea58..b1b678f23 100644 --- a/course/sandbox.py +++ b/course/sandbox.py @@ -67,7 +67,8 @@ def __init__(self, initial_text: str, from course.utils import get_codemirror_widget cm_widget, cm_help_text = get_codemirror_widget( language_mode=language_mode, - interaction_mode=interaction_mode) + interaction_mode=interaction_mode, + autofocus=True) self.fields["content"] = forms.CharField( required=False, diff --git a/course/templates/course/flow-page.html b/course/templates/course/flow-page.html index 32d30e2d8..c2a7ee466 100644 --- a/course/templates/course/flow-page.html +++ b/course/templates/course/flow-page.html @@ -400,22 +400,6 @@ { var input_changed = false; - // {{{ listen for codemirror changes - - function on_cm_change(cm, change_obj) - { - input_changed = true; - } - - $("div.CodeMirror").each( - function () - { - var cm = this.CodeMirror; - cm.on("change", on_cm_change); - }); - - // }}} - // {{{ listen for other input changes function on_input_change(evt) @@ -434,7 +418,7 @@ $(window).on('beforeunload', function() { - if (input_changed) + if (input_changed || (rlCodemirror && rlCodemirror.anyEditorChanged())) return "{% trans 'You have unsaved changes on this page.' %}"; }); @@ -559,25 +543,6 @@ {# }}} #} - {# {{{ codemirror resizing, save #} - - - - {# }}} #} - {% endblock %} {% block page_bottom_javascript_extra %} diff --git a/course/templates/course/grade-flow-page.html b/course/templates/course/grade-flow-page.html index a7b1470f9..ee2aea81a 100644 --- a/course/templates/course/grade-flow-page.html +++ b/course/templates/course/grade-flow-page.html @@ -232,28 +232,25 @@

{% trans "Grading" %}: {{ flow_identifier}} - {{ page_data.group_id }}/ {# }}} #} diff --git a/course/utils.py b/course/utils.py index f01d3d1b8..f60a7723a 100644 --- a/course/utils.py +++ b/course/utils.py @@ -25,11 +25,15 @@ import datetime from contextlib import ContextDecorator +from dataclasses import dataclass from typing import ( - TYPE_CHECKING, Any, Iterable, cast, + TYPE_CHECKING, + Any, + Iterable, + cast, ) -from django import http +from django import forms, http from django.core.exceptions import ObjectDoesNotExist from django.shortcuts import get_object_or_404, render from django.utils import translation @@ -37,9 +41,16 @@ from course.constants import flow_permission, flow_rule_kind from course.content import ( - CourseCommitSHADoesNotExist, FlowDesc, FlowPageDesc, FlowSessionAccessRuleDesc, - FlowSessionGradingRuleDesc, FlowSessionStartRuleDesc, get_course_commit_sha, - get_course_repo, get_flow_desc, parse_date_spec, + CourseCommitSHADoesNotExist, + FlowDesc, + FlowPageDesc, + FlowSessionAccessRuleDesc, + FlowSessionGradingRuleDesc, + FlowSessionStartRuleDesc, + get_course_commit_sha, + get_course_repo, + get_flow_desc, + parse_date_spec, ) from course.page.base import PageBase, PageContext from relate.utils import string_concat @@ -48,11 +59,13 @@ # {{{ mypy if TYPE_CHECKING: - from codemirror import CodeMirrorJavascript, CodeMirrorTextarea # noqa - from course.content import Repo_ish from course.models import ( - Course, ExamTicket, FlowPageData, FlowSession, Participation, + Course, + ExamTicket, + FlowPageData, + FlowSession, + Participation, ) from relate.utils import Repo_ish # noqa @@ -880,110 +893,116 @@ def get_page(self, group_id, page_id, commit_sha): # {{{ codemirror config +@dataclass(frozen=True) +class JsLiteral: + js: str + + +def repr_js(obj: Any) -> str: + if isinstance(obj, list): + return "[%s]" % ", ".join(repr_js(ch) for ch in obj) + elif isinstance(obj, dict): + return "{%s}" % ", ".join(f"{k}: {repr_js(v)}" for k, v in obj.items()) + elif isinstance(obj, bool): + return repr(obj).lower() + elif isinstance(obj, (int, float)): + return repr(obj) + elif isinstance(obj, str): + return repr(obj) + elif isinstance(obj, JsLiteral): + return obj.js + else: + raise ValueError(f"unsupported object type: {type(obj)}") + + +class CodeMirrorTextarea(forms.Textarea): + @property + def media(self): + return forms.Media(js=["bundle-codemirror.js"]) + + def __init__(self, attrs=None, + *, + language_mode=None, interaction_mode, + indent_unit: int, + autofocus: bool, + additional_keys: dict[str, JsLiteral], + **kwargs): + super().__init__(attrs, **kwargs) + self.language_mode = language_mode + self.interaction_mode = interaction_mode + self.indent_unit = indent_unit + self.autofocus = autofocus + self.additional_keys = additional_keys + + # TODO: Maybe add VSCode keymap? + # https://github.com/replit/codemirror-vscode-keymap + def render(self, name, value, attrs=None, renderer=None): + # based on + # https://github.com/codemirror/basic-setup/blob/b3be7cd30496ee578005bd11b1fa6a8b21fcbece/src/codemirror.ts + extensions = [ + JsLiteral(f"rlCodemirror.indentUnit.of({' ' * self.indent_unit !r})"), + ] + + if self.interaction_mode == "vim": + extensions.insert(0, JsLiteral("rlCodemirror.vim()")) + elif self.interaction_mode == "emacs": + extensions.insert(0, JsLiteral("rlCodemirror.emacs()")) + else: + pass + + if self.language_mode is not None: + extensions.append(JsLiteral(f"rlCodemirror.{self.language_mode}()")) + + additional_keys = [ + { + "key": key, + "run": func, + } + for key, func in self.additional_keys.items() + ] + output = [super().render( + name, value, attrs, renderer), + f""" + + """] + + return "\n".join(output) + + def get_codemirror_widget( language_mode: str, interaction_mode: str | None, config: dict | None = None, - addon_css: tuple = (), - addon_js: tuple = (), - dependencies: tuple = (), - read_only: bool = False, autofocus: bool = False, - additional_keys: dict[str, str | CodeMirrorJavascript] | None = None, - attrs: dict[str, str] | None = None, + additional_keys: dict[str, JsLiteral] | None = None, ) -> tuple[CodeMirrorTextarea, str]: - from codemirror import CodeMirrorJavascript, CodeMirrorTextarea if additional_keys is None: additional_keys = {} - theme = "default" - if read_only: - theme += " relate-readonly" - from django.urls import reverse - help_text = (_("Press F9 to toggle full-screen mode. ") + help_text = (_("Press Esc then Tab to leave the editor. ") + _("Set editor mode in user profile.") % reverse("relate-user_profile")) - actual_addon_css = ("dialog/dialog", "display/fullscreen", *addon_css) - actual_addon_js = ( - "search/searchcursor", - "dialog/dialog", - "search/search", - "comment/comment", - "edit/matchbrackets", - "display/fullscreen", - "selection/active-line", - "edit/trailingspace", - *addon_js) - - if language_mode == "python": + if language_mode in ["python", "yaml"]: indent_unit = 4 else: indent_unit = 2 - extra_keys = { - "Ctrl-/": "toggleComment", - "Tab": CodeMirrorJavascript("""function(cm) - { - // from https://github.com/codemirror/CodeMirror/issues/988 - - if (cm.doc.somethingSelected()) { - return CodeMirror.Pass; - } - var spacesPerTab = cm.getOption("indentUnit"); - var spacesToInsert = ( - spacesPerTab - - (cm.doc.getCursor("start").ch % spacesPerTab)); - var spaces = Array(spacesToInsert + 1).join(" "); - cm.replaceSelection(spaces, "end", "+input"); - }"""), - "Shift-Tab": "indentLess", - "F9": CodeMirrorJavascript("""function(cm) { - cm.setOption("fullScreen", - !cm.getOption("fullScreen")); - }"""), - } - extra_keys.update(additional_keys) - - actual_config = { - "fixedGutter": True, - "matchBrackets": True, - "styleActiveLine": True, - "showTrailingSpace": True, - "indentUnit": indent_unit, - "readOnly": read_only, - "extraKeys": extra_keys, - } - - if autofocus: - actual_config["autofocus"] = True - - if interaction_mode == "vim": - actual_config["vimMode"] = True - actual_addon_js += ("../keymap/vim",) - elif interaction_mode == "emacs": - actual_config["keyMap"] = "emacs" - actual_addon_js += ("../keymap/emacs",) - elif interaction_mode == "sublime": - actual_config["keyMap"] = "sublime" - actual_addon_js += ("../keymap/sublime",) - # every other interaction mode goes to default - - if config is not None: - actual_config.update(config) - - if attrs is None: - attrs = {} - return CodeMirrorTextarea( - mode=language_mode, - dependencies=dependencies, - theme=theme, - addon_css=actual_addon_css, - addon_js=actual_addon_js, - config=actual_config, - attrs=attrs), help_text + language_mode=language_mode, + interaction_mode=interaction_mode, + indent_unit=indent_unit, + autofocus=autofocus, + additional_keys=additional_keys, + ), help_text # }}} diff --git a/package-lock.json b/package-lock.json index 3b2232768..026b950e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,15 +5,27 @@ "packages": { "": { "dependencies": { + "@codemirror/autocomplete": "^6.9.0", + "@codemirror/commands": "^6.2.4", + "@codemirror/lang-markdown": "^6.2.0", + "@codemirror/lang-python": "^6.1.3", + "@codemirror/lang-yaml": "^6.1.1", + "@codemirror/language": "^6.8.0", + "@codemirror/legacy-modes": "^6.3.3", + "@codemirror/search": "^6.5.0", + "@codemirror/state": "^6.4.1", + "@codemirror/view": "^6.29", "@fullcalendar/core": "^5.10.1", "@fullcalendar/daygrid": "^5.10.1", "@fullcalendar/list": "^5.10.1", "@fullcalendar/timegrid": "^5.10.1", + "@lezer/highlight": "^1.1.6", "@popperjs/core": "^2.11.2", + "@replit/codemirror-emacs": "^6.0.1", + "@replit/codemirror-vim": "^6.0.14", "blueimp-tmpl": "^3.11.0", "bootstrap": "^5.3.1", "bootstrap-icons": "^1.8.1", - "codemirror": "^5.58.2", "datatables-i18n": "github:dzhuang/datatables-i18n", "datatables.net": "^1.13.5", "datatables.net-bs5": "^1.13.5", @@ -91,6 +103,169 @@ "node": ">=6.9.0" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.9.0.tgz", + "integrity": "sha512-Fbwm0V/Wn3BkEJZRhr0hi5BhCo5a7eBL6LYaliPjOSwCyfOpnjXY59HruSxOUNV+1OYer0Tgx1zRNQttjXyDog==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.6.0", + "@lezer/common": "^1.0.0" + }, + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.2.4.tgz", + "integrity": "sha512-42lmDqVH0ttfilLShReLXsDfASKLXzfyC36bzwcqzox9PlHulMcsUOfHXNo2X2aFMVNUoQ7j+d4q5bnfseYoOA==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.2.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.2.0.tgz", + "integrity": "sha512-oyIdJM29AyRPM3+PPq1I2oIk8NpUfEN3kAM05XWDDs6o3gSneIKaVJifT2P+fqONLou2uIgXynFyMUDQvo/szA==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.5.tgz", + "integrity": "sha512-dUCSxkIw2G+chaUfw3Gfu5kkN83vJQN8gfQDp9iEHsIZluMJA0YJveT12zg/28BJx+uPsbQ6VimKCgx3oJrZxA==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.2.2", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.0" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.1.9.tgz", + "integrity": "sha512-z3jdkcqOEBT2txn2a87A0jSy6Te3679wg/U8QzMeftFt+4KA6QooMwfdFzJiuC3L6fXKfTXZcDocoaxMYfGz0w==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-markdown": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.2.0.tgz", + "integrity": "sha512-deKegEQVzfBAcLPqsJEa+IxotqPVwWZi90UOEvQbfa01NTAw8jNinrykuYPTULGUj+gha0ZG2HBsn4s5d64Qrg==", + "dependencies": { + "@codemirror/autocomplete": "^6.7.1", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.3.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/markdown": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-python": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.3.tgz", + "integrity": "sha512-S9w2Jl74hFlD5nqtUMIaXAq9t5WlM0acCkyuQWUUSvZclk1sV+UfnpFiZzuZSG+hfEaOmxKR5UxY/Uxswn7EhQ==", + "dependencies": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.8.0", + "@lezer/python": "^1.1.4" + } + }, + "node_modules/@codemirror/lang-yaml": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.1.tgz", + "integrity": "sha512-HV2NzbK9bbVnjWxwObuZh5FuPCowx51mEfoFT9y3y+M37fA3+pbxx4I7uePuygFzDsAmCTwQSc/kXh/flab4uw==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/yaml": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.8.0.tgz", + "integrity": "sha512-r1paAyWOZkfY0RaYEZj3Kul+MiQTEbDvYqf8gPGaRvNneHXCmfSaAVFjwRUPlgxS8yflMxw2CTu6uCMp8R8A2g==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.3.3.tgz", + "integrity": "sha512-X0Z48odJ0KIoh/HY8Ltz75/4tDYc9msQf1E/2trlxFaFFhgjpVHjZ/BCXe1Lk7s4Gd67LL/CeEEHNI+xHOiESg==", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.4.0.tgz", + "integrity": "sha512-6VZ44Ysh/Zn07xrGkdtNfmHCbGSHZzFBdzWi0pbd7chAQ/iUcpLGX99NYRZTa7Ugqg4kEHCqiHhcZnH0gLIgSg==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.0.tgz", + "integrity": "sha512-64/M40YeJPToKvGO6p3fijo2vwUEj4nACEAXElCaYQ50HrXSvRaK+NHEhSh73WFBGdvIdhrV+lL9PdJy2RfCYA==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", + "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" + }, + "node_modules/@codemirror/view": { + "version": "6.29.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.29.0.tgz", + "integrity": "sha512-ED4ims4fkf7eOA+HYLVP8VVg3NMllt1FPm9PEJBfYFnidKlRITBaua38u68L1F60eNtw2YNcDN5jsIzhKZwWQA==", + "dependencies": { + "@codemirror/state": "^6.4.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@eslint/eslintrc": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.4.tgz", @@ -315,6 +490,83 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@lezer/common": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", + "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==" + }, + "node_modules/@lezer/css": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.3.tgz", + "integrity": "sha512-SjSM4pkQnQdJDVc80LYzEaMiNy9txsFbI7HsMgeVF28NdLaAdHNtQ+kB/QqDUzRBV/75NTXjJ/R5IdC8QQGxMg==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.0.tgz", + "integrity": "sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/html": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.6.tgz", + "integrity": "sha512-Kk9HJARZTc0bAnMQUqbtuhFVsB4AnteR2BFUWfZV7L/x1H0aAKz6YabrfJ2gk/BEgjh9L3hg5O4y2IDZRBdzuQ==", + "dependencies": { + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.5.tgz", + "integrity": "sha512-FmBUHz8K1V22DgjTd6SrIG9owbzOYZ1t3rY6vGEmw+e2RVBd7sqjM8uXEVRFmfxKFn1Mx2ABJehHjrN3G2ZpmA==", + "dependencies": { + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.1.tgz", + "integrity": "sha512-CHsKq8DMKBf9b3yXPDIU4DbH+ZJd/sJdYOW2llbW/HudP5u0VS6Bfq1hLYfgU7uAYGFIyGGQIsSOXGPEErZiJw==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/markdown": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.0.5.tgz", + "integrity": "sha512-J0LRA0l21Ec6ZroaOxjxsWWm+swCOFHcnOU85Z7aH9nj3eJx5ORmtzVkWzs9e21SZrdvyIzM1gt+YF/HnqbvnA==", + "dependencies": { + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@lezer/python": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.8.tgz", + "integrity": "sha512-1T/XsmeF57ijrjpC0Zmrf9YeO5mn2zC1XeSNrOnc0KB+6PgxJ5m7kWKt0CnwyS74oHQXbJxUUL+QDQJR26c1Gw==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/yaml": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.3.tgz", + "integrity": "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, "node_modules/@npmcli/fs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", @@ -374,6 +626,30 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@replit/codemirror-emacs": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@replit/codemirror-emacs/-/codemirror-emacs-6.0.1.tgz", + "integrity": "sha512-2WYkODZGH1QVAXWuOxTMCwktkoZyv/BjYdJi2A5w4fRrmOQFuIACzb6pO9dgU3J+Pm2naeiX2C8veZr/3/r6AA==", + "peerDependencies": { + "@codemirror/autocomplete": "^6.0.2", + "@codemirror/commands": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.1", + "@codemirror/view": "^6.3.0" + } + }, + "node_modules/@replit/codemirror-vim": { + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/@replit/codemirror-vim/-/codemirror-vim-6.0.14.tgz", + "integrity": "sha512-wwhqhvL76FdRTdwfUWpKCbv0hkp2fvivfMosDVlL/popqOiNLtUhL02ThgHZH8mus/NkVr5Mj582lyFZqQrjOA==", + "peerDependencies": { + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.1.0", + "@codemirror/search": "^6.2.0", + "@codemirror/state": "^6.0.1", + "@codemirror/view": "^6.0.3" + } + }, "node_modules/@rollup/plugin-commonjs": { "version": "17.1.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-17.1.0.tgz", @@ -1615,11 +1891,6 @@ "node": ">=8" } }, - "node_modules/codemirror": { - "version": "5.63.1", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.63.1.tgz", - "integrity": "sha512-baivaNZreZOGh1/tYyTvCupC9NeWk7qlZeGUDi4nFKj/J0JU8FYKZND4QqLw70P7HOttlCt4JJAOj9GoIhHEkA==" - }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -1783,6 +2054,11 @@ "node": ">=8" } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, "node_modules/cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -7327,6 +7603,11 @@ "node": ">=0.10.0" } }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" + }, "node_modules/stylehacks": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.0.tgz", @@ -7671,6 +7952,11 @@ "global": "^4.3.1" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -8022,6 +8308,163 @@ "regenerator-runtime": "^0.13.4" } }, + "@codemirror/autocomplete": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.9.0.tgz", + "integrity": "sha512-Fbwm0V/Wn3BkEJZRhr0hi5BhCo5a7eBL6LYaliPjOSwCyfOpnjXY59HruSxOUNV+1OYer0Tgx1zRNQttjXyDog==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.6.0", + "@lezer/common": "^1.0.0" + } + }, + "@codemirror/commands": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.2.4.tgz", + "integrity": "sha512-42lmDqVH0ttfilLShReLXsDfASKLXzfyC36bzwcqzox9PlHulMcsUOfHXNo2X2aFMVNUoQ7j+d4q5bnfseYoOA==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.2.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "@codemirror/lang-css": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.2.0.tgz", + "integrity": "sha512-oyIdJM29AyRPM3+PPq1I2oIk8NpUfEN3kAM05XWDDs6o3gSneIKaVJifT2P+fqONLou2uIgXynFyMUDQvo/szA==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.0.0" + } + }, + "@codemirror/lang-html": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.5.tgz", + "integrity": "sha512-dUCSxkIw2G+chaUfw3Gfu5kkN83vJQN8gfQDp9iEHsIZluMJA0YJveT12zg/28BJx+uPsbQ6VimKCgx3oJrZxA==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.2.2", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.0" + } + }, + "@codemirror/lang-javascript": { + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.1.9.tgz", + "integrity": "sha512-z3jdkcqOEBT2txn2a87A0jSy6Te3679wg/U8QzMeftFt+4KA6QooMwfdFzJiuC3L6fXKfTXZcDocoaxMYfGz0w==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "@codemirror/lang-markdown": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.2.0.tgz", + "integrity": "sha512-deKegEQVzfBAcLPqsJEa+IxotqPVwWZi90UOEvQbfa01NTAw8jNinrykuYPTULGUj+gha0ZG2HBsn4s5d64Qrg==", + "requires": { + "@codemirror/autocomplete": "^6.7.1", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.3.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/markdown": "^1.0.0" + } + }, + "@codemirror/lang-python": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.3.tgz", + "integrity": "sha512-S9w2Jl74hFlD5nqtUMIaXAq9t5WlM0acCkyuQWUUSvZclk1sV+UfnpFiZzuZSG+hfEaOmxKR5UxY/Uxswn7EhQ==", + "requires": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.8.0", + "@lezer/python": "^1.1.4" + } + }, + "@codemirror/lang-yaml": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.1.tgz", + "integrity": "sha512-HV2NzbK9bbVnjWxwObuZh5FuPCowx51mEfoFT9y3y+M37fA3+pbxx4I7uePuygFzDsAmCTwQSc/kXh/flab4uw==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/yaml": "^1.0.0" + } + }, + "@codemirror/language": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.8.0.tgz", + "integrity": "sha512-r1paAyWOZkfY0RaYEZj3Kul+MiQTEbDvYqf8gPGaRvNneHXCmfSaAVFjwRUPlgxS8yflMxw2CTu6uCMp8R8A2g==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "@codemirror/legacy-modes": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.3.3.tgz", + "integrity": "sha512-X0Z48odJ0KIoh/HY8Ltz75/4tDYc9msQf1E/2trlxFaFFhgjpVHjZ/BCXe1Lk7s4Gd67LL/CeEEHNI+xHOiESg==", + "requires": { + "@codemirror/language": "^6.0.0" + } + }, + "@codemirror/lint": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.4.0.tgz", + "integrity": "sha512-6VZ44Ysh/Zn07xrGkdtNfmHCbGSHZzFBdzWi0pbd7chAQ/iUcpLGX99NYRZTa7Ugqg4kEHCqiHhcZnH0gLIgSg==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "@codemirror/search": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.0.tgz", + "integrity": "sha512-64/M40YeJPToKvGO6p3fijo2vwUEj4nACEAXElCaYQ50HrXSvRaK+NHEhSh73WFBGdvIdhrV+lL9PdJy2RfCYA==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "@codemirror/state": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", + "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" + }, + "@codemirror/view": { + "version": "6.29.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.29.0.tgz", + "integrity": "sha512-ED4ims4fkf7eOA+HYLVP8VVg3NMllt1FPm9PEJBfYFnidKlRITBaua38u68L1F60eNtw2YNcDN5jsIzhKZwWQA==", + "requires": { + "@codemirror/state": "^6.4.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "@eslint/eslintrc": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.4.tgz", @@ -8209,6 +8652,83 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "@lezer/common": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", + "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==" + }, + "@lezer/css": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.3.tgz", + "integrity": "sha512-SjSM4pkQnQdJDVc80LYzEaMiNy9txsFbI7HsMgeVF28NdLaAdHNtQ+kB/QqDUzRBV/75NTXjJ/R5IdC8QQGxMg==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/highlight": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.0.tgz", + "integrity": "sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==", + "requires": { + "@lezer/common": "^1.0.0" + } + }, + "@lezer/html": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.6.tgz", + "integrity": "sha512-Kk9HJARZTc0bAnMQUqbtuhFVsB4AnteR2BFUWfZV7L/x1H0aAKz6YabrfJ2gk/BEgjh9L3hg5O4y2IDZRBdzuQ==", + "requires": { + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/javascript": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.5.tgz", + "integrity": "sha512-FmBUHz8K1V22DgjTd6SrIG9owbzOYZ1t3rY6vGEmw+e2RVBd7sqjM8uXEVRFmfxKFn1Mx2ABJehHjrN3G2ZpmA==", + "requires": { + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "@lezer/lr": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.1.tgz", + "integrity": "sha512-CHsKq8DMKBf9b3yXPDIU4DbH+ZJd/sJdYOW2llbW/HudP5u0VS6Bfq1hLYfgU7uAYGFIyGGQIsSOXGPEErZiJw==", + "requires": { + "@lezer/common": "^1.0.0" + } + }, + "@lezer/markdown": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.0.5.tgz", + "integrity": "sha512-J0LRA0l21Ec6ZroaOxjxsWWm+swCOFHcnOU85Z7aH9nj3eJx5ORmtzVkWzs9e21SZrdvyIzM1gt+YF/HnqbvnA==", + "requires": { + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "@lezer/python": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.8.tgz", + "integrity": "sha512-1T/XsmeF57ijrjpC0Zmrf9YeO5mn2zC1XeSNrOnc0KB+6PgxJ5m7kWKt0CnwyS74oHQXbJxUUL+QDQJR26c1Gw==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/yaml": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.3.tgz", + "integrity": "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==", + "requires": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, "@npmcli/fs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", @@ -8254,6 +8774,18 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" }, + "@replit/codemirror-emacs": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@replit/codemirror-emacs/-/codemirror-emacs-6.0.1.tgz", + "integrity": "sha512-2WYkODZGH1QVAXWuOxTMCwktkoZyv/BjYdJi2A5w4fRrmOQFuIACzb6pO9dgU3J+Pm2naeiX2C8veZr/3/r6AA==", + "requires": {} + }, + "@replit/codemirror-vim": { + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/@replit/codemirror-vim/-/codemirror-vim-6.0.14.tgz", + "integrity": "sha512-wwhqhvL76FdRTdwfUWpKCbv0hkp2fvivfMosDVlL/popqOiNLtUhL02ThgHZH8mus/NkVr5Mj582lyFZqQrjOA==", + "requires": {} + }, "@rollup/plugin-commonjs": { "version": "17.1.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-17.1.0.tgz", @@ -9161,11 +9693,6 @@ } } }, - "codemirror": { - "version": "5.63.1", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.63.1.tgz", - "integrity": "sha512-baivaNZreZOGh1/tYyTvCupC9NeWk7qlZeGUDi4nFKj/J0JU8FYKZND4QqLw70P7HOttlCt4JJAOj9GoIhHEkA==" - }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -9309,6 +9836,11 @@ } } }, + "crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -13438,6 +13970,11 @@ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true }, + "style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" + }, "stylehacks": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.0.tgz", @@ -13710,6 +14247,11 @@ "global": "^4.3.1" } }, + "w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/package.json b/package.json index 9b045d327..ffd5a6a83 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,22 @@ "@popperjs/core": "^2.11.2", "blueimp-tmpl": "^3.11.0", "bootstrap": "^5.3.1", - "codemirror": "^5.58.2", + "@codemirror/state": "^6.4.1", + "@codemirror/view": "^6.29", + "@codemirror/commands": "^6.2.4", + "@codemirror/autocomplete": "^6.9.0", + "@codemirror/search": "^6.5.0", + "@codemirror/autocomplete": "^6.9.0", + "@codemirror/language": "^6.8.0", + "@codemirror/lang-python": "^6.1.3", + "@codemirror/lang-markdown": "^6.2.0", + "@codemirror/lang-yaml": "^6.1.1", + "@codemirror/legacy-modes": "^6.3.3", + "@replit/codemirror-vim": "^6.0.14", + "@replit/codemirror-emacs": "^6.0.1", + "@lezer/highlight": "^1.1.6", + "@replit/codemirror-vim": "^6.0.14", + "@replit/codemirror-emacs": "^6.0.1", "datatables-i18n": "github:dzhuang/datatables-i18n", "datatables.net": "^1.13.5", "datatables.net-bs5": "^1.13.5", @@ -43,4 +58,4 @@ "build": "rollup --config", "dev": "rollup --config --watch" } -} +} \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 68d68498e..172eefe54 100644 --- a/poetry.lock +++ b/poetry.lock @@ -608,16 +608,6 @@ files = [ celery = ">=5.2.7,<6.0" Django = ">=3.2.18" -[[package]] -name = "django-codemirror-widget" -version = "0.5.0" -description = "Django form widget library for using CodeMirror on textarea" -optional = false -python-versions = "*" -files = [ - {file = "django-codemirror-widget-0.5.0.tar.gz", hash = "sha256:a2f7facf299f3f51dff7227cc9850905b899e08485698d3f4fad110a7ad628be"}, -] - [[package]] name = "django-crispy-forms" version = "2.3" @@ -1844,13 +1834,13 @@ s2repoze = ["paste", "repoze.who", "zope.interface"] [[package]] name = "pytest" -version = "8.3.1" +version = "8.3.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.3.1-py3-none-any.whl", hash = "sha256:e9600ccf4f563976e2c99fa02c7624ab938296551f280835ee6516df8bc4ae8c"}, - {file = "pytest-8.3.1.tar.gz", hash = "sha256:7e8e5c5abd6e93cb1cc151f23e57adc31fcf8cfd2a3ff2da63e23f732de35db6"}, + {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, + {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, ] [package.dependencies] @@ -2207,29 +2197,29 @@ files = [ [[package]] name = "ruff" -version = "0.5.4" +version = "0.5.5" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.5.4-py3-none-linux_armv6l.whl", hash = "sha256:82acef724fc639699b4d3177ed5cc14c2a5aacd92edd578a9e846d5b5ec18ddf"}, - {file = "ruff-0.5.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:da62e87637c8838b325e65beee485f71eb36202ce8e3cdbc24b9fcb8b99a37be"}, - {file = "ruff-0.5.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e98ad088edfe2f3b85a925ee96da652028f093d6b9b56b76fc242d8abb8e2059"}, - {file = "ruff-0.5.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c55efbecc3152d614cfe6c2247a3054cfe358cefbf794f8c79c8575456efe19"}, - {file = "ruff-0.5.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9b85eaa1f653abd0a70603b8b7008d9e00c9fa1bbd0bf40dad3f0c0bdd06793"}, - {file = "ruff-0.5.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cf497a47751be8c883059c4613ba2f50dd06ec672692de2811f039432875278"}, - {file = "ruff-0.5.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:09c14ed6a72af9ccc8d2e313d7acf7037f0faff43cde4b507e66f14e812e37f7"}, - {file = "ruff-0.5.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:628f6b8f97b8bad2490240aa84f3e68f390e13fabc9af5c0d3b96b485921cd60"}, - {file = "ruff-0.5.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3520a00c0563d7a7a7c324ad7e2cde2355733dafa9592c671fb2e9e3cd8194c1"}, - {file = "ruff-0.5.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93789f14ca2244fb91ed481456f6d0bb8af1f75a330e133b67d08f06ad85b516"}, - {file = "ruff-0.5.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:029454e2824eafa25b9df46882f7f7844d36fd8ce51c1b7f6d97e2615a57bbcc"}, - {file = "ruff-0.5.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9492320eed573a13a0bc09a2957f17aa733fff9ce5bf00e66e6d4a88ec33813f"}, - {file = "ruff-0.5.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a6e1f62a92c645e2919b65c02e79d1f61e78a58eddaebca6c23659e7c7cb4ac7"}, - {file = "ruff-0.5.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:768fa9208df2bec4b2ce61dbc7c2ddd6b1be9fb48f1f8d3b78b3332c7d71c1ff"}, - {file = "ruff-0.5.4-py3-none-win32.whl", hash = "sha256:e1e7393e9c56128e870b233c82ceb42164966f25b30f68acbb24ed69ce9c3a4e"}, - {file = "ruff-0.5.4-py3-none-win_amd64.whl", hash = "sha256:58b54459221fd3f661a7329f177f091eb35cf7a603f01d9eb3eb11cc348d38c4"}, - {file = "ruff-0.5.4-py3-none-win_arm64.whl", hash = "sha256:bd53da65f1085fb5b307c38fd3c0829e76acf7b2a912d8d79cadcdb4875c1eb7"}, - {file = "ruff-0.5.4.tar.gz", hash = "sha256:2795726d5f71c4f4e70653273d1c23a8182f07dd8e48c12de5d867bfb7557eed"}, + {file = "ruff-0.5.5-py3-none-linux_armv6l.whl", hash = "sha256:605d589ec35d1da9213a9d4d7e7a9c761d90bba78fc8790d1c5e65026c1b9eaf"}, + {file = "ruff-0.5.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00817603822a3e42b80f7c3298c8269e09f889ee94640cd1fc7f9329788d7bf8"}, + {file = "ruff-0.5.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:187a60f555e9f865a2ff2c6984b9afeffa7158ba6e1eab56cb830404c942b0f3"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe26fc46fa8c6e0ae3f47ddccfbb136253c831c3289bba044befe68f467bfb16"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad25dd9c5faac95c8e9efb13e15803cd8bbf7f4600645a60ffe17c73f60779b"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f70737c157d7edf749bcb952d13854e8f745cec695a01bdc6e29c29c288fc36e"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:cfd7de17cef6ab559e9f5ab859f0d3296393bc78f69030967ca4d87a541b97a0"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a09b43e02f76ac0145f86a08e045e2ea452066f7ba064fd6b0cdccb486f7c3e7"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0b856cb19c60cd40198be5d8d4b556228e3dcd545b4f423d1ad812bfdca5884"}, + {file = "ruff-0.5.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3687d002f911e8a5faf977e619a034d159a8373514a587249cc00f211c67a091"}, + {file = "ruff-0.5.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ac9dc814e510436e30d0ba535f435a7f3dc97f895f844f5b3f347ec8c228a523"}, + {file = "ruff-0.5.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:af9bdf6c389b5add40d89b201425b531e0a5cceb3cfdcc69f04d3d531c6be74f"}, + {file = "ruff-0.5.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d40a8533ed545390ef8315b8e25c4bb85739b90bd0f3fe1280a29ae364cc55d8"}, + {file = "ruff-0.5.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cab904683bf9e2ecbbe9ff235bfe056f0eba754d0168ad5407832928d579e7ab"}, + {file = "ruff-0.5.5-py3-none-win32.whl", hash = "sha256:696f18463b47a94575db635ebb4c178188645636f05e934fdf361b74edf1bb2d"}, + {file = "ruff-0.5.5-py3-none-win_amd64.whl", hash = "sha256:50f36d77f52d4c9c2f1361ccbfbd09099a1b2ea5d2b2222c586ab08885cf3445"}, + {file = "ruff-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3191317d967af701f1b73a31ed5788795936e423b7acce82a2b63e26eb3e89d6"}, + {file = "ruff-0.5.5.tar.gz", hash = "sha256:cc5516bdb4858d972fbc31d246bdb390eab8df1a26e2353be2dbc0c2d7f5421a"}, ] [[package]] @@ -2857,4 +2847,4 @@ postgres = ["psycopg2"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "e7e8c8a38174ddbc413bd58ce3ff9ce7d71c430fa5171f8e89af37aee6884227" +content-hash = "3b8a0eb65dfed21db2bed3efda21ded6719195696ea386e421e44c98c7c58f2b" diff --git a/pyproject.toml b/pyproject.toml index d2b858e3f..6f87855ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,8 +67,6 @@ slixmpp = "^1.8.3" # To manage web dependencies django-npm = "^1.0.0" -# For comfortable code entry -django-codemirror-widget = ">=0.5" # For code isolation in code questions # 7.1 bumps the minimum API version to 1.24, which is unsupported by podman docker = "^7.1.0" diff --git a/relate/static/css/base.scss b/relate/static/css/base.scss index 796b3c745..0ee58b7d7 100644 --- a/relate/static/css/base.scss +++ b/relate/static/css/base.scss @@ -9,3 +9,6 @@ $bootstrap-icons-font-file: "./fonts/bootstrap-icons"; @import 'node_modules/bootstrap-icons/font/bootstrap-icons.scss'; @import "./style.scss"; + +// ideally, this should be separate, but it uses bootstrap classes +@import "./codemirror.scss"; \ No newline at end of file diff --git a/relate/static/css/codemirror.scss b/relate/static/css/codemirror.scss new file mode 100644 index 000000000..45eabec81 --- /dev/null +++ b/relate/static/css/codemirror.scss @@ -0,0 +1,50 @@ +.cm-editor .cm-gutters { + @extend .border-end; + @extend .border-secondary-subtle; +} + +.cm-editor .cm-gutter { + @extend .bg-secondary-subtle; + @extend .text-secondary-emphasis; +} + +.cm-editor .cm-activeLineGutter { + @extend .bg-secondary; + @extend .text-white; + background-color: yellow; +} + +.cm-editor .cm-panels { + @extend .bg-body-secondary; + @extend .text-body; +} + +.cm-editor .cm-selectionBackground { + @extend .bg-body-secondary; +} + +/* Based on https://discuss.codemirror.net/t/dynamic-light-mode-dark-mode-how/4709/5 */ + +.cm-editor, .cm-view { + + .cmt-atom {color: #221199;} + .cmt-comment {color: #AA5500;} + .cmt-keyword {color: #8959A8;} + .cmt-literal {color: #4271AE;} + .cmt-number {color: #F5871F;} + .cmt-operator {color: #008803;} + .cmt-separator {color: #990033;} + .cmt-string {color: #FF5500;} + + @media (prefers-color-scheme: dark) { + + .cmt-atom {color: #F78C6C;} + .cmt-comment {color: #545454;} + .cmt-keyword {color: #C792EA;} + .cmt-literal {color: #FFCB6B;} + .cmt-number {color: #FF5370;} + .cmt-operator {color: #89DDFF;} + .cmt-separator {color: #FF7DE9;} + .cmt-string {color: #F07178;} + } +} diff --git a/relate/static/css/style.scss b/relate/static/css/style.scss index 7a1600d91..3743b942f 100644 --- a/relate/static/css/style.scss +++ b/relate/static/css/style.scss @@ -259,32 +259,6 @@ div.progress:hover span.stats-percentage{ color: #7cf; } -/* {{{ codemirror */ - -.cm-trailingspace { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAYAAAB/qH1jAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QUXCToH00Y1UgAAACFJREFUCNdjPMDBUc/AwNDAAAFMTAwMDA0OP34wQgX/AQBYgwYEx4f9lQAAAABJRU5ErkJggg==); - background-position: bottom left; - background-repeat: repeat-x; -} -.cm-tab { - background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAYAAAAkuj5RAAAAAXNSR0IArs4c6QAAAGFJREFUSMft1LsRQFAQheHPowAKoACx3IgEKtaEHujDjORSgWTH/ZOdnZOcM/sgk/kFFWY0qV8foQwS4MKBCS3qR6ixBJvElOobYAtivseIE120FaowJPN75GMu8j/LfMwNjh4HUpwg4LUAAAAASUVORK5CYII=); - background-position: right; - background-repeat: no-repeat; -} - -.CodeMirror.cm-s-relate-readonly { - background-color: #e8e8e8; -} - -div.relate-codemirror-container div.col-lg-8 { - width: 100%; -} -div.relate-codemirror-container div.CodeMirror { - height: calc(max(40vh, 40ex)); -} - -/* }}} */ - .relate-btn-sm-vert-spaced { margin-bottom: 3px; } diff --git a/relate/static/js/codemirror.js b/relate/static/js/codemirror.js new file mode 100644 index 000000000..b246e524b --- /dev/null +++ b/relate/static/js/codemirror.js @@ -0,0 +1,161 @@ +import { Compartment, EditorState } from '@codemirror/state'; +import { + EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter, + drawSelection, rectangularSelection, dropCursor, highlightSpecialChars, + highlightTrailingWhitespace, +} from '@codemirror/view'; +import { + defaultKeymap, history, historyKeymap, indentWithTab, +} from '@codemirror/commands'; +import { searchKeymap, highlightSelectionMatches } from '@codemirror/search'; +import { + HighlightStyle, + syntaxHighlighting, indentOnInput, bracketMatching, + foldGutter, foldKeymap, indentUnit, +} from '@codemirror/language'; +import { + autocompletion, completionKeymap, + closeBrackets, closeBracketsKeymap, +} from '@codemirror/autocomplete'; +import { python } from '@codemirror/lang-python'; +import { markdown } from '@codemirror/lang-markdown'; +import { yaml } from '@codemirror/lang-yaml'; + +import { tags } from '@lezer/highlight'; + +import { vim, Vim } from '@replit/codemirror-vim'; +import { emacs } from '@replit/codemirror-emacs'; + +let anyEditorChangedFlag = false; + +export function anyEditorChanged() { + return anyEditorChangedFlag; +} + +const myListener = new Compartment(); + +const rlDefaultKeymap = [ + ...closeBracketsKeymap, + ...defaultKeymap, + ...searchKeymap, + ...historyKeymap, + ...completionKeymap, + ...foldKeymap, + indentWithTab, +]; + +// Based on https://discuss.codemirror.net/t/dynamic-light-mode-dark-mode-how/4709/5 +// Use a class highlight style, so we can handle things in CSS. +const highlightStyle = HighlightStyle.define([ + { tag: tags.atom, class: 'cmt-atom' }, + { tag: tags.comment, class: 'cmt-comment' }, + { tag: tags.keyword, class: 'cmt-keyword' }, + { tag: tags.literal, class: 'cmt-literal' }, + { tag: tags.number, class: 'cmt-number' }, + { tag: tags.operator, class: 'cmt-operator' }, + { tag: tags.separator, class: 'cmt-separator' }, + { tag: tags.string, class: 'cmt-string' }, +]); + +const defaultExtensionsBase = [ + lineNumbers(), + history(), + foldGutter(), + indentOnInput(), + drawSelection(), + EditorState.allowMultipleSelections.of(true), + dropCursor(), + syntaxHighlighting(highlightStyle, { fallback: true }), + bracketMatching(), + closeBrackets(), + autocompletion(), + rectangularSelection(), + highlightActiveLine(), + highlightActiveLineGutter(), + highlightSelectionMatches(), + highlightSpecialChars(), + highlightTrailingWhitespace(), +]; + +// based on https://codemirror.net/docs/migration/ +export function editorFromTextArea(textarea, extensions, autofocus, additionalKeys) { + // vim/emacs must come before other extensions + extensions.push( + ...defaultExtensionsBase, + keymap.of([ + ...rlDefaultKeymap, + ...additionalKeys, + ]), + EditorView.updateListener.of((viewUpdate) => { + if (viewUpdate.docChanged) { + anyEditorChangedFlag = true; + } + }), + myListener.of(EditorView.updateListener.of( + () => { }, + )), + ); + + if (textarea.disabled) { + extensions.push( + EditorState.readOnly.of(true), + EditorView.editable.of(false), + ); + } + + const view = new EditorView({ doc: textarea.value, extensions }); + + textarea.parentNode.insertBefore(view.dom, textarea); + // eslint-disable-next-line no-param-reassign + textarea.style.display = 'none'; + if (textarea.form) { + textarea.form.addEventListener('submit', () => { + // eslint-disable-next-line no-param-reassign + textarea.value = view.state.doc.toString(); + }); + } + textarea.classList.add('rl-managed-by-codemirror'); + + if (autofocus) { + document.addEventListener('DOMContentLoaded', () => { + view.focus(); + }); + } + + return view; +} + +export function setListener(view, fn) { + view.dispatch({ + effects: myListener.reconfigure( + EditorView.updateListener.of(fn), + ), + }); +} + +Vim.defineEx('write', 'w', () => { + const textarea = document.querySelector('textarea.rl-managed-by-codemirror'); + if (textarea.form) { + const { form } = textarea; + + // prefer 'submit' over 'save' on flow pages + let submitButton = form.querySelector("input[type='submit'][name='submit']"); + if (submitButton) { + submitButton.click(); + return; + } + + submitButton = form.querySelector("input[type='submit']"); + if (submitButton) { + submitButton.click(); + } + } +}); + +export { + EditorState, + EditorView, + indentUnit, + vim, emacs, + python, markdown, yaml, +}; diff --git a/relate/static/js/rlUtils.js b/relate/static/js/rlUtils.js index b78633756..51587cc6c 100644 --- a/relate/static/js/rlUtils.js +++ b/relate/static/js/rlUtils.js @@ -81,52 +81,61 @@ export function enablePreviewForFileUpload() { // based on https://codemirror.net/addon/search/searchcursor.js (MIT) -export function goToNextPointsField(cm) { - const regexp = /\[pts:/g; - for (let { line, ch } = cm.getCursor(), last = cm.lastLine(); line <= last; line += 1, ch = 0) { - regexp.lastIndex = ch; - const string = cm.getLine(line); - const match = regexp.exec(string); - if (match) { - cm.setCursor({ line, ch: match.index + match[0].length }); - return; - } +const pointsRegexp = /\[pts:/g; + +export function goToNextPointsField(view) { + pointsRegexp.lastIndex = view.state.selection.main.head; + const match = pointsRegexp.exec(view.state.doc.toString()); + if (match) { + view.dispatch({ selection: { anchor: match.index + match[0].length } }); + return true; } + return false; } -function lastMatchIn(string, regexp, endMargin) { +// based on https://stackoverflow.com/a/274094 +function regexLastMatch(string, regex, startpos) { + if (!regex.global) { + throw new Error('Passed regex not global'); + } + + let start; + if (typeof (startpos) === 'undefined') { + start = string.length; + } else if (startpos < 0) { + start = 0; + } else { + start = startpos; + } + + const stringToWorkWith = string.substring(0, start); let match; - let from = 0; - while (from <= string.length) { + let lastMatch = null; + // eslint-disable-next-line no-param-reassign + regex.lastIndex = 0; + + // eslint-disable-next-line no-cond-assign + while ((match = regex.exec(stringToWorkWith)) != null) { + lastMatch = match; // eslint-disable-next-line no-param-reassign - regexp.lastIndex = from; - const newMatch = regexp.exec(string); - if (!newMatch) break; - const end = newMatch.index + newMatch[0].length; - if (end > string.length - endMargin) break; - if (!match || end > match.index + match[0].length) { - match = newMatch; - } - from = newMatch.index + 1; + regex.lastIndex = match.index + 1; } - return match; + return lastMatch; } -export function goToPreviousPointsField(cm) { - const cursor = cm.getCursor(); - const regexp = /\[pts:/g; - - for (let { line, ch } = cursor, first = cm.firstLine(); line >= first; line -= 1, ch = -1) { - const string = cm.getLine(line); - const match = lastMatchIn(string, regexp, ch < 0 ? 0 : string.length - ch); - if (match) { - const newCh = match.index + match[0].length; - if (line !== cursor.line || ch !== newCh) { - cm.setCursor({ line, ch: newCh }); - return; - } - } +export function goToPreviousPointsField(view) { + const match = regexLastMatch( + view.state.doc.toString(), + pointsRegexp, + // "[pts:" is five characters + view.state.selection.main.head - 5, + ); + + if (match) { + view.dispatch({ selection: { anchor: match.index + match[0].length } }); + return true; } + return false; } // }}} diff --git a/relate/utils.py b/relate/utils.py index f532980eb..01b4c6aa2 100644 --- a/relate/utils.py +++ b/relate/utils.py @@ -55,18 +55,6 @@ def _configure_helper(self) -> None: self.helper.label_class = "col-lg-2" self.helper.field_class = "col-lg-8" - def style_codemirror_widget(self): - from codemirror import CodeMirrorTextarea - from crispy_forms.layout import Div - - if self.helper.layout is None: - from crispy_forms.helper import FormHelper - self.helper = FormHelper(self) - self._configure_helper() - - self.helper.filter_by_widget(CodeMirrorTextarea).wrap( - Div, css_class="relate-codemirror-container") - class StyledModelForm(forms.ModelForm): def __init__(self, *args, **kwargs) -> None: diff --git a/rollup.config.js b/rollup.config.js index 8080b020f..9f0b446a1 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -75,4 +75,14 @@ export default [ }, plugins: defaultPlugins, }, + { + input: 'relate/static/js/codemirror.js', + output: { + file: 'frontend-dist/bundle-codemirror.js', + format: 'iife', + sourcemap: true, + name: 'rlCodemirror', + }, + plugins: defaultPlugins, + }, ]; diff --git a/tests/test_utils.py b/tests/test_utils.py index b19b80757..ebb8ac595 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1746,67 +1746,6 @@ def test_not_iterale(self): self.assertIn(expected_error_msg, str(cm.exception)) -class GetCodemirrorWidgetTest(unittest.TestCase): - # test utils.get_codemirror_widget (for cases not covered by other tests) - def setUp(self): - self.language_mode = "python" - fake_code_mirror_textarea = mock.patch("codemirror.CodeMirrorTextarea") - self.mock_code_mirror_textarea = fake_code_mirror_textarea.start() - self.addCleanup(fake_code_mirror_textarea.stop) - - def test_interaction_mode_vim(self): - interaction_mode = "vim" - utils.get_codemirror_widget( - self.language_mode, interaction_mode=interaction_mode) - - addon_js = self.mock_code_mirror_textarea.call_args[1]["addon_js"] - self.assertIn("../keymap/vim", addon_js) - - config = self.mock_code_mirror_textarea.call_args[1]["config"] - self.assertEqual(config["vimMode"], True) - - def test_interaction_mode_emacs(self): - interaction_mode = "emacs" - utils.get_codemirror_widget( - self.language_mode, interaction_mode=interaction_mode) - - addon_js = self.mock_code_mirror_textarea.call_args[1]["addon_js"] - self.assertIn("../keymap/emacs", addon_js) - - config = self.mock_code_mirror_textarea.call_args[1]["config"] - self.assertEqual(config["keyMap"], "emacs") - - def test_interaction_mode_sublime(self): - interaction_mode = "sublime" - utils.get_codemirror_widget( - self.language_mode, interaction_mode=interaction_mode) - - addon_js = self.mock_code_mirror_textarea.call_args[1]["addon_js"] - self.assertIn("../keymap/sublime", addon_js) - - config = self.mock_code_mirror_textarea.call_args[1]["config"] - self.assertEqual(config["keyMap"], "sublime") - - def test_interaction_mode_other(self): - # just ensure no errors - interaction_mode = "other" - utils.get_codemirror_widget( - self.language_mode, interaction_mode=interaction_mode) - - def test_update_config(self): - interaction_mode = "vim" - utils.get_codemirror_widget( - self.language_mode, interaction_mode=interaction_mode, - config={"foo": "bar"}) - - addon_js = self.mock_code_mirror_textarea.call_args[1]["addon_js"] - self.assertIn("../keymap/vim", addon_js) - - config = self.mock_code_mirror_textarea.call_args[1]["config"] - self.assertEqual(config["vimMode"], True) - self.assertEqual(config["foo"], "bar") - - class WillUseMaskedProfileForEmailTest(SingleCourseTestMixin, TestCase): # test utils.will_use_masked_profile_for_email def test_no_recipient_email(self):