From 0bb30b621021fdc169f6b5637ba963d63e9db697 Mon Sep 17 00:00:00 2001 From: Paul McKeown Date: Sat, 9 Mar 2024 18:26:16 +1300 Subject: [PATCH 1/2] Grade caching code merged into development branch (#205) * Added caching of grader results. Uses a Moolde file cache to store results of checked questions. The key is made up of the question id, template id, random seed, student answser and attachements (if any). * Response caching code for testing on a real server. Code still under development. * Response caching code for testing on a real server. Code still under development. * Minor update to allow running on php < v8... * Updated some tests to make them work in php < 8 Mainly replacing calls to grade_response to grade_response($response, false, false) instead of grade_response($response, usecache:false) * Little bugfix... * Trial with grade caching at jobesandbox.php level. * Small tweaks while testing jobe caching. * Tidied up code ready for trial on a real server * Minor updates, removing useless bits and pieces. * Updated behat tests to initialise jobe sandbox properly each time May still need to change behat_coderunner.php to make it more general. That is make it possible to change the jobeserver address in different situations, eg, when running in docker locally or docker in CI tests on github. * Reverted behat fixtures to roughly what they were with minor improvements. Removed unneccesary Given jobe sandbox enabled lines. Removed the hack in jobesandbox.php in prep for merging with branch that has fixed the problem of reverting back to coderunner's default jobe2 server (jobe2.cosc.canterbury.ac.nz) during bhat tests. * Reverted behat fixtures to roughly what they were with minor improvements. Removed unneccesary Given jobe sandbox enabled lines. Removed the hack in jobesandbox.php in prep for merging with branch that has fixed the problem of reverting back to coderunner's default jobe2 server (jobe2.cosc.canterbury.ac.nz) during bhat tests. * Added attribute definitions to qtype_coderunner_combinator_grader_outcome class to adhere to new PHP8.2 standard. Also fixed minor bug caused by missed merge conflict * Updated grade caching so that questions with different question text still use the cached outcome. That is, something has to change in the actual job being run to force a real regrade. --- .gitignore | 2 +- bulktestindex.php | 2 +- classes/combinator_grader_outcome.php | 9 + classes/jobesandbox.php | 187 +++++++++++------- classes/jobrunner.php | 56 ++++-- db/caches.php | 40 ++++ edit_coderunner_form.php | 5 +- lang/en/qtype_coderunner.php | 4 + question.php | 98 ++++++--- settings.php | 11 ++ .../ace_scratchpad_compatibility.feature | 4 +- tests/behat/attachmentimportexport.feature | 1 - tests/behat/backup_and_restore.feature | 2 +- tests/behat/behat_coderunner.php | 25 ++- tests/behat/sandbox_webservice.feature | 19 +- tests/behat/scratchpad_ui.feature | 4 +- tests/behat/scratchpad_ui_params.feature | 4 +- tests/grader_test.php | 18 +- tests/pythonquestions_test.php | 45 ++++- tests/template_test.php | 12 +- 20 files changed, 397 insertions(+), 151 deletions(-) create mode 100644 db/caches.php diff --git a/.gitignore b/.gitignore index 67f0e8831..11b445d92 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,4 @@ NonRepoFiles/* /amd/src/ui_blockly.json /amd/src/.eslintrc.js .grunt - +.vscode/ diff --git a/bulktestindex.php b/bulktestindex.php index e092c22f7..a4cb26dc1 100644 --- a/bulktestindex.php +++ b/bulktestindex.php @@ -150,7 +150,7 @@ expander.innerHTML = 'Collapse'; expander.nextSibling.style.display = 'inline'; } else { - expander.innerTHML = 'Expand'; + expander.innerHTML = 'Expand'; expander.nextSibling.style.display = 'none'; } }); diff --git a/classes/combinator_grader_outcome.php b/classes/combinator_grader_outcome.php index db025b00f..c61cc1da4 100644 --- a/classes/combinator_grader_outcome.php +++ b/classes/combinator_grader_outcome.php @@ -47,6 +47,15 @@ class qtype_coderunner_combinator_grader_outcome extends qtype_coderunner_testin /** @var bool If true, is used when the question is to be used only to display the output and perhaps images from a run, with no mark. */ public $showoutputonly; + /** @var array Array where each item is a rows of test result table */ + public $testresults; + + /** @var ?string The feedback for a given question attempt */ + public $feedbackhtml; + + /** @var bool Whether or no show differences is selected */ + public $showdifferences; + // A list of the allowed attributes in the combinator template grader return value. public $allowedfields = ['fraction', 'prologuehtml', 'testresults', 'epiloguehtml', 'feedbackhtml', 'columnformats', 'showdifferences', diff --git a/classes/jobesandbox.php b/classes/jobesandbox.php index 5a0017123..78056e9ee 100644 --- a/classes/jobesandbox.php +++ b/classes/jobesandbox.php @@ -30,6 +30,9 @@ global $CFG; require_once($CFG->libdir . '/filelib.php'); // Needed when run as web service. +const READ_FROM_CACHE = true; +const WRITE_TO_CACHE = true; + class qtype_coderunner_jobesandbox extends qtype_coderunner_sandbox { const DEBUGGING = 0; const HTTP_GET = 1; @@ -197,57 +200,86 @@ public function execute($sourcecode, $language, $input, $files = null, $params = $this->apikey = $params['jobeapikey']; } } - - $postbody = ['run_spec' => $runspec]; + // QUESTION: Do we need this when using cached result? $this->currentjobid = sprintf('%08x', mt_rand()); - // Try submitting the job. If we get a 404, try again after - // putting all the files on the server. Anything else is an error. - $httpcode = $this->submit($postbody); - if ($httpcode == 404) { // If it's a file not found error ... - foreach ($files as $filename => $contents) { - if (($httpcode = $this->put_file($contents)) != 204) { - break; - } - } - if ($httpcode == 204) { - // Try again if put_files all worked. - $httpcode = $this->submit($postbody); + $cache = cache::make('qtype_coderunner', 'coderunner_grading_cache'); + $runresult = null; + if (READ_FROM_CACHE) { + // NOTE: Changing jobeserver setting will effectively flush the cache + // eg, adding another jobeserver to a list of servers will mean the + // jobeserver parameter has changed and therefore the key will change. + + // QUESTION: Do we want the cache to ignore the jobeserver setting? + // eg, adding a new, presumeably equal jobeserver to the mix shouldn't + // change the result (unless it isn't equal!) + // But, remember that the server is chosen at random from the pool! + + $key = hash("md5", serialize($runspec)); + // Debugger: echo '
' . serialize($runspec) . '
';. + $runresult = $cache->get($key); // Unserializes the returned value :) false if not found. + if ($runresult) { + // echo $key . '-----------> FOUND' . '
'; . } } - $runresult = []; - $runresult['sandboxinfo'] = [ - 'jobeserver' => $this->jobeserver, - 'jobeapikey' => $this->apikey, - ]; + if (!$runresult) { // if cache read failed regrade to be safe + $postbody = ['run_spec' => $runspec]; + // Try submitting the job. If we get a 404, try again after + // putting all the files on the server. Anything else is an error. + $httpcode = $this->submit($postbody); + if ($httpcode == 404) { // If it's a file not found error ... + foreach ($files as $filename => $contents) { + if (($httpcode = $this->put_file($contents)) != 204) { + break; + } + } + if ($httpcode == 204) { + // Try again if put_files all worked. + $httpcode = $this->submit($postbody); + } + } - $okresponse = in_array($httpcode, [200, 203]); // Allow 203, which can result from an intevening proxy server. - if ( - !$okresponse // If it's not an OK response... - || !is_object($this->response) // ... or there's any sort of broken ... - || !isset($this->response->outcome) - ) { // ... communication with server. - // Return with errorcode set and as much extra info as possible in stderr. - $errorcode = $okresponse ? self::UNKNOWN_SERVER_ERROR : $this->get_error_code($httpcode); - $this->currentjobid = null; - $runresult['error'] = $errorcode; - $runresult['stderr'] = "HTTP response from Jobe ({$this->jobeserver}) was $httpcode: " . json_encode($this->response); - } else if ($this->response->outcome == self::RESULT_SERVER_OVERLOAD) { - $runresult['error'] = self::SERVER_OVERLOAD; - } else { - $stderr = $this->filter_file_path($this->response->stderr); - // Any stderr output is treated as a runtime error. - if (trim($stderr ?? '') !== '') { - $this->response->outcome = self::RESULT_RUNTIME_ERROR; + $runresult = []; + $runresult['sandboxinfo'] = [ + 'jobeserver' => $this->jobeserver, + 'jobeapikey' => $this->apikey, + ]; + + $okresponse = in_array($httpcode, [200, 203]); // Allow 203, which can result from an intevening proxy server. + if ( + !$okresponse // If it's not an OK response... + || !is_object($this->response) // ... or there's any sort of broken ... + || !isset($this->response->outcome) + ) { // ... communication with server. + // Return with errorcode set and as much extra info as possible in stderr. + $errorcode = $okresponse ? self::UNKNOWN_SERVER_ERROR : $this->get_error_code($httpcode); + $this->currentjobid = null; + $runresult['error'] = $errorcode; + $runresult['stderr'] = "HTTP response from Jobe was $httpcode: " . json_encode($this->response); + } else if ($this->response->outcome == self::RESULT_SERVER_OVERLOAD) { + $runresult['error'] = self::SERVER_OVERLOAD; + } else { + $stderr = $this->filter_file_path($this->response->stderr); + // Any stderr output is treated as a runtime error. + if (trim($stderr ?? '') !== '') { + $this->response->outcome = self::RESULT_RUNTIME_ERROR; + } + $this->currentjobid = null; + $runresult['error'] = self::OK; + $runresult['stderr'] = $stderr; + $runresult['result'] = $this->response->outcome; + $runresult['signal'] = 0; // Jobe doesn't return signals. + $runresult['cmpinfo'] = $this->response->cmpinfo; + $runresult['output'] = $this->filter_file_path($this->response->stdout); + + // Got a useable result from Jobe server so cache it if required. + if (WRITE_TO_CACHE) { + $key = hash("md5", serialize($runspec)); + $cache->set($key, $runresult); // set serializes the result, get will unserialize. + // echo 'CACHE WRITE for ---> ' . $key . '
'; + } } - $this->currentjobid = null; - $runresult['error'] = self::OK; - $runresult['stderr'] = $stderr; - $runresult['result'] = $this->response->outcome; - $runresult['signal'] = 0; // Jobe doesn't return signals. - $runresult['cmpinfo'] = $this->response->cmpinfo; - $runresult['output'] = $this->filter_file_path($this->response->stdout); } return (object) $runresult; } @@ -256,58 +288,61 @@ public function execute($sourcecode, $language, $input, $files = null, $params = // such class found. Removes comments, strings and nested code and then // uses a regexp to find a public class. private function get_main_class($prog) { - // filter out comments and strings + // Filter out comments and strings. $prog = $prog . ' '; - $filteredProg = array(); - $skipTo = -1; + $filteredprog = []; + $skipto = -1; for ($i = 0; $i < strlen($prog) - 1; $i++) { - if ($skipTo == false) break; // an unclosed comment/string - bail out - if ($i < $skipTo) continue; - - // skip "//" comments - if ($prog[$i].$prog[$i+1] == '//') { - $skipTo = strpos($prog, "\n", $i + 2); + if ($skipto == false) { + break; // An unclosed comment/string - bail out. } - - // skip "/**/" comments - else if ($prog[$i].$prog[$i+1] == '/*') { - $skipTo = strpos($prog, '*/', $i + 2) + 2; - $filteredProg[] = ' '; // '/**/' is a token delimiter + if ($i < $skipto) { + continue; } - - // skip strings - else if ($prog[$i] == '"') { - // matches the whole string + // Skip "//" comments. + if ($prog[$i] . $prog[$i + 1] == '//') { + $skipto = strpos($prog, "\n", $i + 2); + // Skip "/**/" comments. + } else if ($prog[$i] . $prog[$i + 1] == '/*') { + $skipto = strpos($prog, '*/', $i + 2) + 2; + $filteredprog[] = ' '; // The string '/**/' is a token delimiter. + // Skip strings. + } else if ($prog[$i] == '"') { + // Matches the whole string. if (preg_match('/"((\\.)|[^\\"])*"/', $prog, $matches, 0, $i)) { - $skipTo = $i + strlen($matches[0]); + $skipto = $i + strlen($matches[0]); + } else { + $skipto = false; } - else $skipTo = false; + // Copy everything else. + } else { + $filteredprog[] = $prog[$i]; } - - // copy everything else - else $filteredProg[] = $prog[$i]; } - // remove nested code + // Remove nested code. $depth = 0; - for ($i = 0; $i < count($filteredProg); $i++) { - if ($filteredProg[$i] == '{') $depth++; - if ($filteredProg[$i] == '}') $depth--; - if ($filteredProg[$i] != "\n" && $depth > 0 && !($depth == 1 && $filteredProg[$i] == '{')) { - $filteredProg[$i] = ' '; + for ($i = 0; $i < count($filteredprog); $i++) { + if ($filteredprog[$i] == '{') { + $depth++; + } + if ($filteredprog[$i] == '}') { + $depth--; + } + if ($filteredprog[$i] != "\n" && $depth > 0 && !($depth == 1 && $filteredprog[$i] == '{')) { + $filteredprog[$i] = ' '; } } - // search for a public class - if (preg_match('/public\s(\w*\s)*class\s*(\w+)[^\w]/', implode('', $filteredProg), $matches) !== 1) { + // Search for a public class. + if (preg_match('/public\s(\w*\s)*class\s*(\w+)[^\w]/', implode('', $filteredprog), $matches) !== 1) { return false; } else { return $matches[2]; } } - // Return the sandbox error code corresponding to the given httpcode. private function get_error_code($httpcode) { diff --git a/classes/jobrunner.php b/classes/jobrunner.php index 40d7efda7..713e116ad 100644 --- a/classes/jobrunner.php +++ b/classes/jobrunner.php @@ -36,6 +36,7 @@ class qtype_coderunner_jobrunner { private $testcases = null; // The testcases (a subset of those in the question). private $allruns = null; // Array of the source code for all runs. + /** @var ?array Array of sandbox params. */ private $sandboxparams = null; @@ -48,15 +49,32 @@ class qtype_coderunner_jobrunner { /** @var bool True if this grading is occurring because the student clicked the precheck button. */ private $isprecheck = false; - // Check the correctness of a student's code and possible extra attachments - // as an answer to the given - // question and and a given set of test cases (which may be empty or a - // subset of the question's set of testcases. $isprecheck is true if - // this is a run triggered by the student clicking the Precheck button. - // $answerlanguage will be the empty string except for multilanguage questions, - // when it is the language selected in the language drop-down menu. - // Returns a TestingOutcome object. - public function run_tests($question, $code, $attachments, $testcases, $isprecheck, $answerlanguage) { + + + /** + * Check the correctness of a student's code and possible extra attachments + * as an answer to the given question and and a given set of test cases (which may be empty or a + * subset of the question's set of testcases. + * @param qtype_coderunner_question $question object relevant to this step of the attempt + * @param string $code is the JSON repr of the code + * @param array $attachments is the array of attachments given by student, if any + * @param + * @param boolean $isprecheck is true if + * this is a run triggered by the student clicking the Precheck button. + * @param string $answerlanguage will be the empty string except for multilanguage questions, + * when it is the language selected in the language drop-down menu. + * @return qtype_coderunner_combinator_grader_outcome $testoutcome that contains the outcome + * of the grading. + */ + public function run_tests( + $question, + $code, + $attachments, + $testcases, + $isprecheck, + $answerlanguage + ) { + if (empty($question->prototype)) { // Missing prototype. We can't run this question. $outcome = new qtype_coderunner_testing_outcome(0, 0, false); @@ -69,7 +87,8 @@ public function run_tests($question, $code, $attachments, $testcases, $isprechec ['crtype' => $question->coderunnertype] ); } - $outcome->set_status(qtype_coderunner_testing_outcome::STATUS_MISSING_PROTOTYPE, $message); + $status = qtype_coderunner_testing_outcome::STATUS_MISSING_PROTOTYPE; + $outcome->set_status($status, $message); return $outcome; } @@ -131,10 +150,11 @@ public function run_tests($question, $code, $attachments, $testcases, $isprechec if ($question->get_show_source()) { $outcome->sourcecodelist = $this->allruns; } + + return $outcome; } - // If the template is a combinator, try running all the tests in a single // go. // @@ -143,6 +163,13 @@ public function run_tests($question, $code, $attachments, $testcases, $isprechec // a list of all the test cases and QUESTION, the original question object. // Return the testing outcome object if successful else null. private function run_combinator($isprecheck) { + // Remove id and questionid keys+values from testcases so they don't + // affect caching. For example the questionid will change each time + // the question is saved thanks to question versioning - urgh! + foreach ($this->testcases as $tc) { + unset($tc->id); + unset($tc->questionid); + } $numtests = count($this->testcases); $this->templateparams['TESTCASES'] = $this->testcases; $maxmark = $this->maximum_possible_mark(); @@ -217,6 +244,13 @@ private function run_tests_singly($isprecheck) { if ($maxmark == 0) { $maxmark = 1; // Something silly is happening. Probably running a prototype with no tests. } + // Remove id and questionid keys+values from testcases so they don't + // affect caching. For example the questionid will change each time + // the question is saved thanks to question versioning - urgh! + foreach ($this->testcases as $tc) { + unset($tc->id); + unset($tc->questionid); + } $numtests = count($this->testcases); $outcome = new qtype_coderunner_testing_outcome($maxmark, $numtests, $isprecheck); $question = $this->question; diff --git a/db/caches.php b/db/caches.php new file mode 100644 index 000000000..7d0f403d1 --- /dev/null +++ b/db/caches.php @@ -0,0 +1,40 @@ +. + +/** + * Defines cache used to store results of quiz attempt steps. + * If a jobe submission is cached we don't need to call jobe again + * as the result will be known :) + * + * @package qtype + * @subpackage coderunner + * @copyright 2023 Paul McKeown, University of Canterbury + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$definitions = [ + 'coderunner_grading_cache' => [ + 'mode' => cache_store::MODE_APPLICATION, + 'maxsize' => 50000000, + 'simplekeys' => true, + 'simpledata' => false, + 'canuselocalstore' => true, + 'staticacceleration' => true, + 'staticaccelerationsize' => 1000000, + ], +]; diff --git a/edit_coderunner_form.php b/edit_coderunner_form.php index e9aba7d0f..5a930b413 100644 --- a/edit_coderunner_form.php +++ b/edit_coderunner_form.php @@ -1633,7 +1633,10 @@ private function validate_sample_answer() { if ($error) { return $error; } - [$mark, , $cachedata] = $this->formquestion->grade_response($response); + [$mark, $state, $cachedata] = $this->formquestion->grade_response( + $response, + false // Not a precheck. + ); } catch (Exception $e) { return $e->getMessage(); } diff --git a/lang/en/qtype_coderunner.php b/lang/en/qtype_coderunner.php index 2da4b4af5..14611f986 100644 --- a/lang/en/qtype_coderunner.php +++ b/lang/en/qtype_coderunner.php @@ -1343,3 +1343,7 @@ function should be applied, e.g. {{STUDENT_ANSWER | e(\'py\')}} is $string['wssubmissionrateexceeded'] = 'You have exceeded the maximum hourly \'Try it!\' submission rate. Request denied.'; $string['xmlcoderunnerformaterror'] = 'XML format error in coderunner question'; +$string['coderunner_grading_cache'] = 'Caches grading results so we can avoid going to Jobe so often'; +$string['cachedef_coderunner_grading_cache'] = 'Caches grading results so we can avoid going to Jobe so often'; +$string['cachegradingresultsenable'] = 'Cache results when grading answers.'; +$string['cachegradingresults_desc'] = 'Uses a local Moodle cache (currently file cache) to store results of grading questions. Mainly to speed up regrading by using cached results for steps where the same quesiton and answer have already been graded.'; diff --git a/question.php b/question.php index b6d6e1413..623c6d23a 100644 --- a/question.php +++ b/question.php @@ -35,6 +35,7 @@ /** * Represents a 'CodeRunner' question. */ + #[AllowDynamicProperties] class qtype_coderunner_question extends question_graded_automatically { public $testcases = null; // Array of testcases. @@ -219,6 +220,9 @@ class qtype_coderunner_question extends question_graded_automatically { /** @var int questionid. */ public $questionid; + /** @var int randomseed in case we want to see the seed for a question */ + public $randomseed; + /** * Start a new attempt at this question, storing any information that will * be needed later in the step. It is retrieved and applied by @@ -250,6 +254,7 @@ public function start_attempt(question_attempt_step $step = null, $variant = nul $step->set_qt_var('_mtrandseed', $seed); } $this->evaluate_question_for_display($seed, $step); + $this->randomseed = $seed; // so we can see it when checking } // Retrieve the saved random number seed and reconstruct the template @@ -265,6 +270,7 @@ public function apply_attempt_state(question_attempt_step $step) { $seed = mt_rand(); } $this->evaluate_question_for_display($seed, $step); + $this->randomseed = $seed; // so we can see it when checking } @@ -329,6 +335,19 @@ public function evaluate_question_for_display($seed, $step) { * md5 hash of the template parameters within the question attempt step * record in the database, re-evaluating only if the hash changes. * + * + * QUESTION + * With question versioning the question's template paremters can't + * change between steps because the question id will be fixed. + * So, do we need the md5 for the question's template params? + * I'm not sure if the prototype id is fixed at the start of the question + * attempt. If so then we wouldn't be able to change the template + * during an attempt either + * + * Of course, doing a regrade will potentially change everything but + * this will work it's way through as the regrade will start from the + * first step and work through again... + * * If the prototype is missing, process just the template parameters from * this question; an error message will be given later. * @param int $seed The random number seed to set for Twig randomisation @@ -357,6 +376,9 @@ public function evaluate_merged_parameters($seed, $step = null) { /** * Evaluate the template parameter field for this question alone (i.e. * not including its prototype). + * Note: the prototype is also a question and will cache it's own jsontemplateparams + * eg, we call $prototype->template_params_json(..,.., '_prototype_template_params') in + * the evaulate_merged_parameters method. * * @param int $seed the random number seed for this instance of the question * @param question_attempt_step $step the current attempt step @@ -369,6 +391,7 @@ public function template_params_json($seed = 0, $step = null, $qtvar = '') { $params = $this->templateparams; $lang = $this->templateparamslang; if ($step === null) { + // Step is null when validating question so evaluate params. $jsontemplateparams = $this->evaluate_template_params($params, $lang, $seed); } else { $previousparamsmd5 = $step->get_qt_var($qtvar . '_md5'); @@ -399,31 +422,33 @@ public function template_params_json($seed = 0, $step = null, $qtvar = '') { * valid json). */ public function evaluate_template_params($templateparams, $lang, $seed) { - $lang = strtolower($lang); // Just in case some old legacy DB entries escaped. - if (empty($templateparams)) { - $jsontemplateparams = '{}'; - } else if ( + if ( isset($this->cachedfuncparams) && $this->cachedfuncparams === ['lang' => $lang, 'seed' => $seed] ) { // Use previously cached result if possible. $jsontemplateparams = $this->cachedevaldtemplateparams; - } else if ($lang == 'none') { - $jsontemplateparams = $templateparams; - } else if ($lang == 'twig') { - try { - $jsontemplateparams = $this->twig_render_with_seed($templateparams, $seed); - } catch (\Twig\Error\Error $e) { - throw new qtype_coderunner_bad_json_exception($e->getMessage()); - } - } else if (!$this->templateparamsevalpertry && !empty($this->templateparamsevald)) { - $jsontemplateparams = $this->templateparamsevald; } else { - $jsontemplateparams = $this->evaluate_template_params_on_jobe($templateparams, $lang, $seed); + $lang = strtolower($lang); // Just in case some old legacy DB entries escaped. + if (empty($templateparams)) { + $jsontemplateparams = '{}'; + } else if ($lang == 'none') { + $jsontemplateparams = $templateparams; + } else if ($lang == 'twig') { + try { + $jsontemplateparams = $this->twig_render_with_seed($templateparams, $seed); + } catch (\Twig\Error\Error $e) { + throw new qtype_coderunner_bad_json_exception($e->getMessage()); + } + } else if (!$this->templateparamsevalpertry && !empty($this->templateparamsevald)) { + $jsontemplateparams = $this->templateparamsevald; + } else { + $jsontemplateparams = $this->evaluate_template_params_on_jobe($templateparams, $lang, $seed); + } + // Cache in this to avoid multiple evaluations during question editing and validation. + $this->cachedfuncparams = ['lang' => $lang, 'seed' => $seed]; + $this->cachedevaldtemplateparams = $jsontemplateparams; } - // Cache in this to avoid multiple evaluations during question editing and validation. - $this->cachedfuncparams = ['lang' => $lang, 'seed' => $seed]; - $this->cachedevaldtemplateparams = $jsontemplateparams; return $jsontemplateparams; } @@ -828,8 +853,13 @@ public function grade_response(array $response, bool $isprecheck = false) { if ($isprecheck && empty($this->precheck)) { throw new coding_exception("Unexpected precheck"); } + $language = empty($response['language']) ? '' : $response['language']; $gradingreqd = true; + $testoutcomeserial = false; + + // Use _testoutcome if it's already in $response. + // This should be even quicker than the file cache. if (!empty($response['_testoutcome'])) { $testoutcomeserial = $response['_testoutcome']; $testoutcome = unserialize($testoutcomeserial); @@ -847,19 +877,30 @@ public function grade_response(array $response, bool $isprecheck = false) { // filenames and values being file contents. $code = $response['answer']; $attachments = $this->get_attached_files($response); + $this->stepinfo = self::step_info($response); + $this->stepinfo->graderstate = $response['graderstate'] ?? ""; $testcases = $this->filter_testcases($isprecheck, $this->precheck); $runner = new qtype_coderunner_jobrunner(); - $this->stepinfo = self::step_info($response); + // QUESTION why are we reading the graderstate?? if (isset($response['graderstate'])) { $this->stepinfo->graderstate = $response['graderstate']; } else { $this->stepinfo->graderstate = ''; } - $testoutcome = $runner->run_tests($this, $code, $attachments, $testcases, $isprecheck, $language); + $testoutcome = $runner->run_tests( + $this, + $code, + $attachments, + $testcases, + $isprecheck, + $language + ); $testoutcomeserial = serialize($testoutcome); } - + // To be saved in question step data. + // Note: This is used to render test results too so it's not just a cache. $datatocache = ['_testoutcome' => $testoutcomeserial]; + if ($testoutcome->run_failed()) { return [0, question_state::$invalid, $datatocache]; } else if ($testoutcome->all_correct()) { @@ -875,8 +916,6 @@ public function grade_response(array $response, bool $isprecheck = false) { question_state::$gradedpartial, $datatocache]; } } - - // Return a map from filename to file contents for all the attached files // in the given response. private function get_attached_files($response) { @@ -1024,23 +1063,24 @@ public function twig_expand($text, $context = []) { protected function filter_testcases($isprecheckrun, $prechecksetting) { if (!$isprecheckrun) { if ($prechecksetting != constants::PRECHECK_SELECTED) { - return $this->testcases; + $relevanttestcases = $this->testcases; } else { - return $this->selected_testcases(false); + $relevanttestcases = $this->selected_testcases(false); } } else { // This is a precheck run. if ($prechecksetting == constants::PRECHECK_EMPTY) { - return [$this->empty_testcase()]; + $relevanttestcases = [$this->empty_testcase()]; } else if ($prechecksetting == constants::PRECHECK_EXAMPLES) { - return $this->example_testcases(); + $relevanttestcases = $this->example_testcases(); } else if ($prechecksetting == constants::PRECHECK_SELECTED) { - return $this->selected_testcases(true); + $relevanttestcases = $this->selected_testcases(true); } else if ($prechecksetting == constants::PRECHECK_ALL) { - return $this->testcases; + $relevanttestcases = $this->testcases; } else { throw new coding_exception('Precheck clicked but no precheck button?!'); } } + return $relevanttestcases; } diff --git a/settings.php b/settings.php index ad3ee03dc..39c40f44c 100644 --- a/settings.php +++ b/settings.php @@ -134,3 +134,14 @@ get_string('wsmaxcputime_desc', 'qtype_coderunner'), '5' )); + +/* +Currently left out so we can test + +$settings->add(new admin_setting_configcheckbox( + "qtype_coderunner/cachegradingresults", + get_string('cachegradingresultsenable', 'qtype_coderunner'), + get_string('cachegradingresults_desc', 'qtype_coderunner'), + false +)); +*/ diff --git a/tests/behat/ace_scratchpad_compatibility.feature b/tests/behat/ace_scratchpad_compatibility.feature index d29805534..8e4075376 100644 --- a/tests/behat/ace_scratchpad_compatibility.feature +++ b/tests/behat/ace_scratchpad_compatibility.feature @@ -5,7 +5,8 @@ Feature: Ace UI convert to Scratchpad UI questions with one click I should be able to change a question from using Ace to Scratchpad in one click Background: - Given the following "users" exist: + Given the CodeRunner scratchpad is enabled + And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@asd.com | And the following "courses" exist: @@ -20,7 +21,6 @@ Feature: Ace UI convert to Scratchpad UI questions with one click And the following "questions" exist: | questioncategory | qtype | name | | Test questions | coderunner | Square function | - And the CodeRunner sandbox is enabled When I am on the "Square function" "core_question > edit" page logged in as teacher1 And I set the following fields to these values: diff --git a/tests/behat/attachmentimportexport.feature b/tests/behat/attachmentimportexport.feature index 10e88dcec..1150d4c56 100644 --- a/tests/behat/attachmentimportexport.feature +++ b/tests/behat/attachmentimportexport.feature @@ -20,7 +20,6 @@ Feature: Test importing and exporting of question with attachments And the following "questions" exist: | questioncategory | qtype | name | | Test questions | coderunner | Square function | - And the CodeRunner sandbox is enabled And I am on the "Square function" "core_question > edit" page logged in as teacher And I click on "a[aria-controls='id_attachmentoptionscontainer']" "css_element" And I set the field "Answer" to "from sqrmodule import sqr" diff --git a/tests/behat/backup_and_restore.feature b/tests/behat/backup_and_restore.feature index 870658339..8d3d73552 100644 --- a/tests/behat/backup_and_restore.feature +++ b/tests/behat/backup_and_restore.feature @@ -5,7 +5,7 @@ Feature: Duplicate a course containing a CodeRunner question I need to be able to back them up and restore them Background: - And the following "courses" exist: + Given the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "question categories" exist: diff --git a/tests/behat/behat_coderunner.php b/tests/behat/behat_coderunner.php index 2ecaa8c20..55d713d25 100644 --- a/tests/behat/behat_coderunner.php +++ b/tests/behat/behat_coderunner.php @@ -30,14 +30,33 @@ class behat_coderunner extends behat_base { /** * Sets the webserver sandbox to enabled for testing purposes. * - * @Given /^the CodeRunner sandbox is enabled/ + * @Given /^the CodeRunner jobe sandbox is enabled/ */ public function the_coderunner_sandbox_is_enabled() { - set_config('wsenabled', 1, 'qtype_coderunner'); set_config('jobesandbox_enabled', 1, 'qtype_coderunner'); - set_config('jobe_host', '172.17.0.1:4000', 'qtype_coderunner'); } + + /** + * Sets the webserver scratchpad to enabled for testing purposes. + * + * @Given /^the CodeRunner scratchpad is enabled/ + */ + public function the_coderunner_scratchpad_is_enabled() { + set_config('wsenabled', 1, 'qtype_coderunner'); + } + + + /** + * Sets the webserver scratchpad to disabled for testing purposes. + * + * @Given /^the CodeRunner scratchpad is disabled/ + */ + public function the_coderunner_scratchpad_is_disabled() { + set_config('wsenabled', 0, 'qtype_coderunner'); + } + + /** * Checks that a given string appears within answer textarea. * Intended for checking UI serialization diff --git a/tests/behat/sandbox_webservice.feature b/tests/behat/sandbox_webservice.feature index f013dd353..abc03e328 100644 --- a/tests/behat/sandbox_webservice.feature +++ b/tests/behat/sandbox_webservice.feature @@ -30,19 +30,22 @@ Feature: Test sandbox web service @javascript Scenario: As a student if I try to initiate a WS request I get an error if the service is disabled. - When I am on the "Quiz 1" "mod_quiz > View" page logged in as student + Given the CodeRunner scratchpad is disabled + And I am on the "Quiz 1" "mod_quiz > View" page logged in as student And I press "Attempt quiz" And I press "Click me" - Then I should see "ERROR: qtype_coderunner/Sandbox web service disabled." + #Then I should see "ERROR: qtype_coderunner/Sandbox web service disabled." + Then I should see "ERROR: qtype_coderunner/Sandbox web service disabled. Talk to a sysadmin" @javascript Scenario: As a student I can initiate a WS request and see the outcome if the service is enabled. - When I log in as "admin" - And I navigate to "Plugins > CodeRunner" in site administration - And I set the following fields to these values: - | Enable sandbox web service | Yes | - And I press "Save changes" - And I log out +# When I log in as "admin" +# And I navigate to "Plugins > CodeRunner" in site administration +# And I set the following fields to these values: +# | Enable sandbox web service | Yes | +# And I press "Save changes" +# And I log out + Given the CodeRunner scratchpad is enabled When I am on the "Quiz 1" "mod_quiz > View" page logged in as student And I press "Attempt quiz" And I press "Click me" diff --git a/tests/behat/scratchpad_ui.feature b/tests/behat/scratchpad_ui.feature index 9397521a5..54e55701c 100644 --- a/tests/behat/scratchpad_ui.feature +++ b/tests/behat/scratchpad_ui.feature @@ -5,7 +5,8 @@ Feature: Test the Scratchpad UI I should be able specify the required html in either globalextra or prototypeextra Background: - Given the following "users" exist: + Given the CodeRunner scratchpad is enabled + And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@asd.com | And the following "courses" exist: @@ -20,7 +21,6 @@ Feature: Test the Scratchpad UI And the following "questions" exist: | questioncategory | qtype | name | template | | Test questions | coderunner | Print answer | printans | - And the CodeRunner sandbox is enabled Scenario: Edit a CodeRunner question into a Scratchpad UI question When I am on the "Print answer" "core_question > edit" page logged in as teacher1 diff --git a/tests/behat/scratchpad_ui_params.feature b/tests/behat/scratchpad_ui_params.feature index 2dd3d8111..312f48894 100644 --- a/tests/behat/scratchpad_ui_params.feature +++ b/tests/behat/scratchpad_ui_params.feature @@ -5,7 +5,8 @@ Feature: Test the Scratchpad UI, UI Params I should be able specify the UI Parameters to change the Scratchpad UI Background: - Given the following "users" exist: + Given the CodeRunner scratchpad is enabled + And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@asd.com | And the following "courses" exist: @@ -20,7 +21,6 @@ Feature: Test the Scratchpad UI, UI Params And the following "questions" exist: | questioncategory | qtype | name | template | | Test questions | coderunner | Print answer | printans | - And the CodeRunner sandbox is enabled And I am on the "Print answer" "core_question > edit" page logged in as teacher1 And I set the field "id_validateonsave" to "" diff --git a/tests/grader_test.php b/tests/grader_test.php index 5f7a1eed8..711859c04 100644 --- a/tests/grader_test.php +++ b/tests/grader_test.php @@ -63,7 +63,11 @@ public function test_regex_grader() { pass EOCODE; $response = ['answer' => $code]; - $result = $q->grade_response($response); + // Note: Moodle's test helper question always uses an id of zero! + $result = $q->grade_response( + $response, + false, // Not a precheck. + ); [$mark, $grade, $cache] = $result; $testoutcome = unserialize($cache['_testoutcome']); // For debugging test. $this->assertEquals(1, $mark); @@ -94,7 +98,11 @@ public function test_nearequality_grader_right_answer() { pass EOCODE; $response = ['answer' => $code]; - $result = $q->grade_response($response); + // Note: Moodle's test helper question always uses an id of zero! + $result = $q->grade_response( + $response, + false, // Not a precheck. + ); [$mark, $grade, $cache] = $result; $testoutcome = unserialize($cache['_testoutcome']); // For debugging test. $this->assertEquals(1, $mark); @@ -125,7 +133,11 @@ public function test_nearequality_grader_wrong_answer() { pass EOCODE; $response = ['answer' => $code]; - $result = $q->grade_response($response); + // Note: Moodle's test helper question always uses an id of zero! + $result = $q->grade_response( + $response, + false, // Not a precheck. + ); [$mark, $grade, $cache] = $result; $testoutcome = unserialize($cache['_testoutcome']); // For debugging test. $this->assertEquals(0, $mark); diff --git a/tests/pythonquestions_test.php b/tests/pythonquestions_test.php index ed633bded..6667cc0cc 100644 --- a/tests/pythonquestions_test.php +++ b/tests/pythonquestions_test.php @@ -38,7 +38,6 @@ * @coversNothing */ class pythonquestions_test extends \qtype_coderunner_testcase { - /** @var string */ private $goodcode; @@ -215,7 +214,11 @@ public function test_timeout() { $q = $this->make_question('timeout'); $code = "def timeout():\n while (1):\n pass"; $response = ['answer' => $code]; - $result = $q->grade_response($response); + // NOTE: qid of zero is reused with different question! + $result = $q->grade_response( + $response, + false, // Not a precheck. + ); [$mark, $grade, $cache] = $result; $this->assertEquals(0, $mark); $this->assertEquals(\question_state::$gradedwrong, $grade); @@ -231,7 +234,11 @@ public function test_exceptions() { $q = $this->make_question('exceptions'); $code = "def checkOdd(n):\n if n & 1:\n raise ValueError()"; $response = ['answer' => $code]; - $result = $q->grade_response($response); + // NOTE: qid of zero is reused with different question! + $result = $q->grade_response( + $response, + false, // Not a precheck. + ); [$mark, $grade, $cache] = $result; $this->assertEquals(1, $mark); $this->assertEquals(\question_state::$gradedright, $grade); @@ -257,21 +264,33 @@ public function test_partial_mark_question() { $code = "def sqr(n):\n return 0"; // Passes first test only. $response = ['answer' => $code]; - $result = $q->grade_response($response); + // NOTE: qid of zero is reused with different question! + $result = $q->grade_response( + $response, + false, // Not a precheck. + ); [$mark, $grade, $cache] = $result; $this->assertEquals(\question_state::$gradedpartial, $grade); $this->assertTrue(abs($mark - 0.5 / 7.5) < 0.00001); $code = "def sqr(n):\n return n * n if n <= 0 else -17.995"; // Passes first test and last two only. $response = ['answer' => $code]; - $result = $q->grade_response($response); + // NOTE: qid of zero is reused with different question! + $result = $q->grade_response( + $response, + false, // Not a precheck. + ); [$mark, $grade, $cache] = $result; $this->assertEquals(\question_state::$gradedpartial, $grade); $this->assertTrue(abs($mark - 5.0 / 7.5) < 0.00001); $code = "def sqr(n):\n return n * n if n <= 0 else 1 / 0"; // Passes first test then aborts. $response = ['answer' => $code]; - $result = $q->grade_response($response); + // NOTE: qid of zero is reused with different question! + $result = $q->grade_response( + $response, + false, // Not a precheck. + ); [$mark, $grade, $cache] = $result; $this->assertEquals(\question_state::$gradedpartial, $grade); $this->assertTrue(abs($mark - 0.5 / 7.5) < 0.00001); @@ -285,11 +304,21 @@ public function test_customised_timeout() { print("Hello Python") EOT; $response = ['answer' => $slowsquare]; // Should time out. - [$mark, $grade, $cache] = $q->grade_response($response); + // NOTE: qid of zero is reused with different question! + $result = $q->grade_response( + $response, + false, // Not a precheck. + ); + [$mark, $grade, $cache] = $result; $this->assertEquals(0, $mark); $this->assertEquals(\question_state::$gradedwrong, $grade); $q->cputimelimitsecs = 20; // This should fix it. - [$mark, $grade, $cache] = $q->grade_response($response); + // NOTE: qid of zero is reused with different question! + $result = $q->grade_response( + $response, + false, // Not a precheck. + ); + [$mark, $grade, $cache] = $result; $this->assertEquals(1, $mark); $this->assertEquals(\question_state::$gradedright, $grade); } diff --git a/tests/template_test.php b/tests/template_test.php index d821311bb..8739b4ea3 100644 --- a/tests/template_test.php +++ b/tests/template_test.php @@ -111,11 +111,19 @@ public function test_grading_template() { $q->allornothing = false; $code = "def sqr(n): return n * n\n"; $response = ['answer' => $code]; - $result = $q->grade_response($response); + // Note: qid of zero is reused with different question! + $result = $q->grade_response( + $response, + false // Not a precheck. + ); [$mark, $grade, $cache] = $result; $this->assertTrue(abs($mark - 24.0 / 31.0) < 0.000001); $q->allornothing = true; - $result = $q->grade_response($response); + // Note: qid of zero is reused with different question! + $result = $q->grade_response( + $response, + false // Not a precheck. + ); [$mark, $grade, $cache] = $result; $this->assertTrue($mark == 0.0); } From c6d744417bdb032f72ef5abd028b446420c5650c Mon Sep 17 00:00:00 2001 From: chrahe <87469367+chrahe@users.noreply.github.com> Date: Thu, 14 Mar 2024 08:56:16 +0100 Subject: [PATCH 2/2] Fix issue/196 (#197) * fix for issue #196 * updated ui_ace_gapfiller minified versions to address issue #196 --- amd/build/ui_ace_gapfiller.min.js | 2 +- amd/build/ui_ace_gapfiller.min.js.map | 2 +- amd/src/ui_ace_gapfiller.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/amd/build/ui_ace_gapfiller.min.js b/amd/build/ui_ace_gapfiller.min.js index 3ee068865..927f2d7b2 100644 --- a/amd/build/ui_ace_gapfiller.min.js +++ b/amd/build/ui_ace_gapfiller.min.js @@ -38,6 +38,6 @@ * @copyright Matthew Toohey, 2021, The University of Canterbury * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define("qtype_coderunner/ui_ace_gapfiller",["jquery"],(function($){var Range;const validChars=/[ !"#$%&'()*+,`\-./0-9\p{L}:;<=>?@\[\]\\^_{}|~]/u;function AceGapfillerUi(textareaId,w,h,uiParams){this.textArea=$(document.getElementById(textareaId));var wrapper=$(document.getElementById(textareaId+"_wrapper")),focused=this.textArea[0]===document.activeElement,lang=uiParams.lang,t=this;let code="";this.uiParams=uiParams,this.gaps=[],this.source=uiParams.ui_source||"globalextra",this.nextGapIndex=0,"globalextra"!==this.source&&"test0"!==this.source&&(alert("Invalid source for code in ui_ace_gapfiller"),this.source="globalextra"),code="globalextra"==this.source?this.textArea.attr("data-globalextra"):this.textArea.attr("data-test0");try{window.ace.require("ace/ext/language_tools"),Range=window.ace.require("ace/range").Range,this.modelist=window.ace.require("ace/ext/modelist"),this.enabled=!1,this.contents_changed=!1,this.capturingTab=!1,this.clickInProgress=!1,this.editNode=$("
"),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),this.textArea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({displayIndentGuides:!1,dragEnabled:!1,enableBasicAutocompletion:!0,newLineMode:"unix"}),this.editor.$blockScrolling=1/0,uiParams.theme?this.editor.setTheme("ace/theme/"+uiParams.theme):this.editor.setTheme("ace/theme/textmate"),this.setLanguage(lang),this.setEventHandlers(this.textArea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.createGaps(code),this.editor.commands.on("exec",(function(e){let cursor=t.editor.selection.getCursor(),commandName=e.command.name,selectionRange=t.editor.getSelectionRange(),gap=t.findCursorGap(cursor);if(commandName.startsWith("go")){if(null===gap||"gotoright"!==commandName||cursor.column!==gap.range.start.column+gap.textSize)return;t.editor.moveCursorTo(cursor.row,gap.range.end.column+1)}if(null===gap)"selectall"===commandName&&t.editor.selection.selectAll();else if("indent"===commandName){let nextGap=t.gaps[(gap.index+1)%t.gaps.length];t.editor.moveCursorTo(nextGap.range.start.row,nextGap.range.start.column+nextGap.textSize),t.editor.selection.clearSelection()}else if("selectall"===commandName)t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1);else if(t.editor.selection.isEmpty()){if("insertstring"===commandName){let char=e.args;validChars.test(char)&&gap.insertChar(t.gaps,cursor,char)}else"backspace"===commandName?cursor.column>gap.range.start.column&&gap.textSize>0&&gap.deleteChar(t.gaps,{row:cursor.row,column:cursor.column-1}):"del"===commandName&&cursor.column0&&gap.deleteChar(t.gaps,cursor);t.editor.selection.clearSelection()}else if(!t.editor.selection.isEmpty()&&gap.cursorInGap(selectionRange.start)&&gap.cursorInGap(selectionRange.end)&&("insertstring"!==commandName&&"backspace"!==commandName&&"del"!==commandName&&"paste"!==commandName&&"cut"!==commandName||(gap.deleteRange(t.gaps,selectionRange.start.column,selectionRange.end.column),t.editor.selection.clearSelection()),"insertstring"===commandName)){let char=e.args;validChars.test(char)&&gap.insertChar(t.gaps,selectionRange.start,char)}null!==gap&&"paste"===commandName&&gap.insertText(t.gaps,selectionRange.start.column,e.args.text),e.preventDefault(),e.stopPropagation()})),t.editor.selection.on("changeCursor",(function(){let cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&cursor.column>gap.range.start.column+gap.textSize&&t.editor.moveCursorTo(gap.range.start.row,gap.range.start.column+gap.textSize)})),this.gapToSelect=null,this.editor.on("tripleclick",(function(e){let cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&(t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1),t.gapToSelect=gap,e.preventDefault(),e.stopPropagation())})),this.editor.on("click",(function(e){t.gapToSelect&&(t.editor.moveCursorTo(t.gapToSelect.range.start.row,t.gapToSelect.range.start.column+t.gapToSelect.textSize),t.gapToSelect=null,e.preventDefault(),e.stopPropagation())})),this.fail=!1,this.reload()}catch(err){this.fail=!0}}function Gap(editor,row,column,minWidth){let maxWidth=arguments.length>4&&void 0!==arguments[4]?arguments[4]:1/0;this.editor=editor,this.minWidth=minWidth,this.maxWidth=maxWidth,this.range=new Range(row,column,row,column+minWidth),this.textSize=0,this.editor.session.addMarker(this.range,"ace-gap-outline","text",!0),this.editor.session.addMarker(this.range,"ace-gap-background","text",!1)}return AceGapfillerUi.prototype.createGaps=function(code){function reEscape(s){for(var c,result="",i=0;i1?parseInt(values[1]):1/0,gap=new Gap(this.editor,i,columnPos,minWidth,maxWidth);gap.index=this.nextGapIndex,this.nextGapIndex+=1,this.gaps.push(gap),columnPos+=minWidth,editorContent+=" ".repeat(minWidth),j+12,AceGapfillerUi.prototype.reload=function(){let content=this.textArea.val();if(content)try{let values=JSON.parse(content);for(let i=0;i=this.range.start.row&&cursor.column>=this.range.start.column&&cursor.row<=this.range.end.row&&cursor.column<=this.range.end.column},Gap.prototype.getWidth=function(){return this.range.end.column-this.range.start.column},Gap.prototype.changeWidth=function(gaps,delta){this.range.end.column+=delta;for(let i=0;ithis.range.end.column&&(other.range.start.column+=delta,other.range.end.column+=delta)}this.editor.$onChangeBackMarker(),this.editor.$onChangeFrontMarker()},Gap.prototype.insertChar=function(gaps,pos,char){this.textSize===this.getWidth()&&this.getWidth()=this.minWidth?this.changeWidth(gaps,-1):this.editor.session.insert({row:pos.row,column:this.range.end.column-1}," ")},Gap.prototype.deleteRange=function(gaps,start,end){for(let i=start;i?@\[\]\\^_{}|~]/u;function AceGapfillerUi(textareaId,w,h,uiParams){this.textArea=$(document.getElementById(textareaId));var wrapper=$(document.getElementById(textareaId+"_wrapper")),focused=this.textArea[0]===document.activeElement,lang=uiParams.lang,t=this;let code="";this.uiParams=uiParams,this.gaps=[],this.source=uiParams.ui_source||"globalextra",this.nextGapIndex=0,"globalextra"!==this.source&&"test0"!==this.source&&(alert("Invalid source for code in ui_ace_gapfiller"),this.source="globalextra"),code="globalextra"==this.source?this.textArea.attr("data-globalextra"):this.textArea.attr("data-test0");try{window.ace.require("ace/ext/language_tools"),Range=window.ace.require("ace/range").Range,this.modelist=window.ace.require("ace/ext/modelist"),this.enabled=!1,this.contents_changed=!1,this.capturingTab=!1,this.clickInProgress=!1,this.editNode=$("
"),this.editNode.css({resize:"none",height:h,width:"100%"}),this.editor=window.ace.edit(this.editNode.get(0)),this.textArea.prop("readonly")&&this.editor.setReadOnly(!0),this.editor.setOptions({displayIndentGuides:!1,dragEnabled:!1,enableBasicAutocompletion:!0,newLineMode:"unix"}),this.editor.$blockScrolling=1/0,uiParams.theme?this.editor.setTheme("ace/theme/"+uiParams.theme):this.editor.setTheme("ace/theme/textmate"),this.setLanguage(lang),this.setEventHandlers(this.textArea),this.captureTab(),this.editor.renderer.on("afterRender",(function(){var gutter=wrapper.find(".ace_gutter");gutter.hasClass("moodle-has-zindex")||(gutter.addClass("moodle-has-zindex"),focused&&(t.editor.focus(),t.editor.navigateFileEnd()),t.aceLabel=wrapper.find(".answerprompt"),t.aceLabel.attr("for","ace_"+textareaId),t.aceTextarea=wrapper.find(".ace_text-input"),t.aceTextarea.attr("id","ace_"+textareaId))})),this.createGaps(code),this.editor.commands.on("exec",(function(e){let cursor=t.editor.selection.getCursor(),commandName=e.command.name,selectionRange=t.editor.getSelectionRange(),gap=t.findCursorGap(cursor);if(commandName.startsWith("go")){if(null===gap||"gotoright"!==commandName||cursor.column!==gap.range.start.column+gap.textSize)return;t.editor.moveCursorTo(cursor.row,gap.range.end.column+1)}if(null===gap)"selectall"===commandName&&t.editor.selection.selectAll();else if("indent"===commandName){let nextGap=t.gaps[(gap.index+1)%t.gaps.length];t.editor.moveCursorTo(nextGap.range.start.row,nextGap.range.start.column+nextGap.textSize),t.editor.selection.clearSelection()}else if("selectall"===commandName)t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1);else if(t.editor.selection.isEmpty()){if("insertstring"===commandName){let char=e.args;validChars.test(char)&&gap.insertChar(t.gaps,cursor,char)}else"backspace"===commandName?cursor.column>gap.range.start.column&&gap.textSize>0&&gap.deleteChar(t.gaps,{row:cursor.row,column:cursor.column-1}):"del"===commandName&&cursor.column0&&gap.deleteChar(t.gaps,cursor);t.editor.selection.clearSelection()}else if(!t.editor.selection.isEmpty()&&gap.cursorInGap(selectionRange.start)&&gap.cursorInGap(selectionRange.end)&&("insertstring"!==commandName&&"backspace"!==commandName&&"del"!==commandName&&"paste"!==commandName&&"cut"!==commandName||(gap.deleteRange(t.gaps,selectionRange.start.column,selectionRange.end.column),t.editor.selection.clearSelection()),"insertstring"===commandName)){let char=e.args;validChars.test(char)&&gap.insertChar(t.gaps,selectionRange.start,char)}null!==gap&&"paste"===commandName&&gap.insertText(t.gaps,selectionRange.start.column,e.args.text),e.preventDefault(),e.stopPropagation()})),t.editor.selection.on("changeCursor",(function(){let cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&cursor.column>gap.range.start.column+gap.textSize&&t.editor.moveCursorTo(gap.range.start.row,gap.range.start.column+gap.textSize)})),this.gapToSelect=null,this.editor.on("tripleclick",(function(e){let cursor=t.editor.selection.getCursor(),gap=t.findCursorGap(cursor);null!==gap&&(t.editor.selection.setSelectionRange(new Range(gap.range.start.row,gap.range.start.column,gap.range.start.row,gap.range.end.column),!1),t.gapToSelect=gap,e.preventDefault(),e.stopPropagation())})),this.editor.on("click",(function(e){t.gapToSelect&&(t.editor.moveCursorTo(t.gapToSelect.range.start.row,t.gapToSelect.range.start.column+t.gapToSelect.textSize),t.gapToSelect=null,e.preventDefault(),e.stopPropagation())})),this.fail=!1,this.reload()}catch(err){this.fail=!0}}function Gap(editor,row,column,minWidth){let maxWidth=arguments.length>4&&void 0!==arguments[4]?arguments[4]:1/0;this.editor=editor,this.minWidth=minWidth,this.maxWidth=maxWidth,this.range=new Range(row,column,row,column+minWidth),this.textSize=0,this.editor.session.addMarker(this.range,"ace-gap-outline","text",!0),this.editor.session.addMarker(this.range,"ace-gap-background","text",!1)}return AceGapfillerUi.prototype.createGaps=function(code){function reEscape(s){for(var c,result="",i=0;i1?parseInt(values[1]):1/0,gap=new Gap(this.editor,i,columnPos,minWidth,maxWidth);gap.index=this.nextGapIndex,this.nextGapIndex+=1,this.gaps.push(gap),columnPos+=minWidth,editorContent+=" ".repeat(minWidth),j+12,AceGapfillerUi.prototype.reload=function(){let content=this.textArea.val();if(content)try{let values=JSON.parse(content);for(let i=0;i=this.range.start.row&&cursor.column>=this.range.start.column&&cursor.row<=this.range.end.row&&cursor.column<=this.range.end.column},Gap.prototype.getWidth=function(){return this.range.end.column-this.range.start.column},Gap.prototype.changeWidth=function(gaps,delta){this.range.end.column+=delta;for(let i=0;ithis.range.start.column&&(other.range.start.column+=delta,other.range.end.column+=delta)}this.editor.$onChangeBackMarker(),this.editor.$onChangeFrontMarker()},Gap.prototype.insertChar=function(gaps,pos,char){this.textSize===this.getWidth()&&this.getWidth()=this.minWidth?this.changeWidth(gaps,-1):this.editor.session.insert({row:pos.row,column:this.range.end.column-1}," ")},Gap.prototype.deleteRange=function(gaps,start,end){for(let i=start;i.\n\n/**\n * Implementation of the ace_gapfiller_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin uses the usual ace editor but only makes some portions of the text editable.\n * The pre-formatted text is supplied by the question author in either the\n * \"globalextra\" field or the testcode field of the first test case, according\n * to the ui parameter ui_source (default: globalextra).\n * Editable \"gaps\" are inserted into the ace editor at specified points.\n * It is intended primarily for use with coding questions where the answerbox presents\n * the students with code that has smallish bits missing.\n *\n * The locations within the globalextra text at which the gaps are\n * to be inserted are denoted by \"tags\" of the form\n *\n * {[ size ]}\n *\n * or\n *\n * {[ size-maxSize ]}\n *\n * where size and maxSize are integer literals. These respectively inject a \"gap\" into\n * the editor of the specified size and maxSize. If maxSize is not specified then the\n * \"gap\" has no maximum size and can grow without bound.\n *\n * The serialisation of the answer box contents, i.e. the text that\n * copied back into the textarea for submissions\n * as the answer, is simply a list of all the field values (strings), in order.\n *\n * As a special case of the serialisation, if the value list is empty, the\n * serialisation itself is the empty string.\n *\n * The delimiters for the gap tags are by default '{[' and\n * ']}'.\n *\n * @module qtype_coderunner/ui_ace_gapfiller\n * @copyright Richard Lobb, 2019, The University of Canterbury\n * @copyright Matthew Toohey, 2021, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n\n var Range; // Can't load this until ace has loaded.\n const fillChar = \" \";\n const validChars = /[ !\"#$%&'()*+,`\\-./0-9\\p{L}:;<=>?@\\[\\]\\\\^_{}|~]/u;\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n /**\n * Constructor for the Ace interface object\n * @param {string} textareaId The ID of the textarea html element.\n * @param {int} w The width of the text area in pixels.\n * @param {int} h The height of the text area in pixels.\n * @param {object} uiParams The UI parameter specifier object.\n */\n function AceGapfillerUi(textareaId, w, h, uiParams) {\n this.textArea = $(document.getElementById(textareaId));\n var wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = this.textArea[0] === document.activeElement,\n lang = uiParams.lang,\n t = this; // For embedded callbacks.\n\n let code = \"\";\n this.uiParams = uiParams;\n this.gaps = [];\n this.source = uiParams.ui_source || 'globalextra';\n this.nextGapIndex = 0;\n if (this.source !== 'globalextra' && this.source !== 'test0') {\n alert('Invalid source for code in ui_ace_gapfiller');\n this.source = 'globalextra';\n }\n if (this.source == 'globalextra') {\n code = this.textArea.attr('data-globalextra');\n } else {\n code = this.textArea.attr('data-test0');\n }\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n Range = window.ace.require(\"ace/range\").Range;\n this.modelist = window.ace.require('ace/ext/modelist');\n\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"
\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (this.textArea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n displayIndentGuides: false,\n dragEnabled: false,\n enableBasicAutocompletion: true,\n newLineMode: \"unix\",\n });\n this.editor.$blockScrolling = Infinity;\n\n // Use the uiParams theme if provided else use light.\n if (uiParams.theme) {\n this.editor.setTheme(\"ace/theme/\" + uiParams.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.setLanguage(lang);\n\n this.setEventHandlers(this.textArea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.createGaps(code);\n\n // Intercept commands sent to ace.\n this.editor.commands.on(\"exec\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let commandName = e.command.name;\n let selectionRange = t.editor.getSelectionRange();\n\n let gap = t.findCursorGap(cursor);\n\n if (commandName.startsWith(\"go\")) { // If command just moves the cursor then do nothing.\n if (gap !== null && commandName === \"gotoright\" && cursor.column === gap.range.start.column+gap.textSize) {\n // In this case we jump out of gap over the empty space that contains nothing that the user has entered.\n t.editor.moveCursorTo(cursor.row, gap.range.end.column+1);\n } else {\n return;\n }\n }\n\n if (gap === null) {\n // Not in a gap\n if (commandName === \"selectall\") {\n t.editor.selection.selectAll();\n }\n\n } else if (commandName === \"indent\") {\n // Instead of indenting, move to next gap.\n let nextGap = t.gaps[(gap.index+1) % t.gaps.length];\n t.editor.moveCursorTo(nextGap.range.start.row, nextGap.range.start.column+nextGap.textSize);\n t.editor.selection.clearSelection(); // Clear selection.\n\n } else if (commandName === \"selectall\") {\n // Select all text in a gap if we are in a gap.\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n\n } else if (t.editor.selection.isEmpty()) {\n // User is not selecting multiple characters.\n if (commandName === \"insertstring\") {\n let char = e.args;\n // Only allow user to insert 'valid' chars.\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, cursor, char);\n }\n } else if (commandName === \"backspace\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column > gap.range.start.column && gap.textSize > 0) {\n gap.deleteChar(t.gaps, {row: cursor.row, column: cursor.column-1});\n }\n } else if (commandName === \"del\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column < gap.range.start.column + gap.textSize && gap.textSize > 0) {\n gap.deleteChar(t.gaps, cursor);\n }\n }\n t.editor.selection.clearSelection(); // Keep selection clear.\n\n } else if (!t.editor.selection.isEmpty() && gap.cursorInGap(selectionRange.start)\n && gap.cursorInGap(selectionRange.end)) {\n // User is selecting multiple characters and is in a gap.\n\n // These are the commands that remove the selected text.\n if (commandName === \"insertstring\" || commandName === \"backspace\"\n || commandName === \"del\" || commandName === \"paste\"\n || commandName === \"cut\") {\n\n gap.deleteRange(t.gaps, selectionRange.start.column, selectionRange.end.column);\n t.editor.selection.clearSelection(); // Clear selection.\n }\n\n if (commandName === \"insertstring\") {\n let char = e.args;\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, selectionRange.start, char);\n }\n }\n }\n\n // Paste text into gap.\n if (gap !== null && commandName === \"paste\") {\n gap.insertText(t.gaps, selectionRange.start.column, e.args.text);\n }\n\n e.preventDefault();\n e.stopPropagation();\n });\n\n // Move cursor to where it should be if we click on a gap.\n t.editor.selection.on('changeCursor', function() {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n if (cursor.column > gap.range.start.column+gap.textSize) {\n t.editor.moveCursorTo(gap.range.start.row, gap.range.start.column+gap.textSize);\n }\n }\n });\n\n this.gapToSelect = null; // Stores gap that has been selected with triple click.\n\n // Select all text in gap on triple click within gap.\n this.editor.on(\"tripleclick\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n t.gapToSelect = gap;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n // Annoying hack to ensure the tripple click thing works.\n this.editor.on(\"click\", function(e) {\n if (t.gapToSelect) {\n t.editor.moveCursorTo(t.gapToSelect.range.start.row, t.gapToSelect.range.start.column+t.gapToSelect.textSize);\n t.gapToSelect = null;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n this.fail = false;\n this.reload();\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n /**\n * The method that creates the gaps at all places containing the appropriate\n * marker (default {[ ... ]}).\n * Do not call until after this.editor has been instantiated.\n * @param {string} code The initial raw text code\n */\n AceGapfillerUi.prototype.createGaps = function(code) {\n this.gaps = [];\n /**\n * Escape special characters in a given string.\n * @param {string} s The input string.\n * @returns {string} The updated string, with escaped specials.\n */\n function reEscape(s) {\n var c, specials = '{[(*+\\\\', result='';\n for (var i = 0; i < s.length; i++) {\n c = s[i];\n for (var j = 0; j < specials.length; j++) {\n if (c === specials[j]) {\n c = '\\\\' + c;\n }\n }\n result += c;\n }\n return result;\n }\n\n let lines = code.split(/\\r?\\n/);\n\n let sepLeft = reEscape('{[');\n let sepRight = reEscape(']}');\n let splitter = new RegExp(sepLeft + ' *((?:\\\\d+)|(?:\\\\d+- *\\\\d+)) *' + sepRight);\n\n let editorContent = \"\";\n for (let i = 0; i < lines.length; i++) {\n let bits = lines[i].split(splitter);\n editorContent += bits[0];\n\n let columnPos = bits[0].length;\n for (let j = 1; j < bits.length; j += 2) {\n let values = bits[j].split('-');\n let minWidth = parseInt(values[0]);\n let maxWidth = (values.length > 1 ? parseInt(values[1]) : Infinity);\n\n // Create new gap.\n let gap = new Gap(this.editor, i, columnPos, minWidth, maxWidth);\n gap.index = this.nextGapIndex;\n this.nextGapIndex += 1;\n this.gaps.push(gap);\n\n columnPos += minWidth;\n editorContent += ' '.repeat(minWidth);\n if (j + 1 < bits.length) {\n editorContent += bits[j+1];\n columnPos += bits[j+1].length;\n }\n\n }\n\n if (i < lines.length-1) {\n editorContent += '\\n';\n }\n }\n this.editor.session.setValue(editorContent);\n };\n\n /**\n * Return the gap that the cursor is in. This will actually return a gap if\n * the cursor is 1 outside the gap as this will be needed for\n * backspace/insertion to work. Rigth now this is done as a simple\n * linear search but could be improved later.\n * @param {object} cursor The ace editor cursor position.\n * @returns {object} The gap that the cursor is current in, or null otherwise.\n */\n AceGapfillerUi.prototype.findCursorGap = function(cursor) {\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n if (gap.cursorInGap(cursor)) {\n return gap;\n }\n }\n return null;\n };\n\n AceGapfillerUi.prototype.failed = function() {\n return this.fail;\n };\n\n AceGapfillerUi.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n\n // Sync to TextArea\n AceGapfillerUi.prototype.sync = function() {\n if (this.fail) {\n return; // Leave the text area alone if Ace load failed.\n }\n let serialisation = []; // A list of field values.\n let empty = true;\n\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n let value = gap.getText();\n serialisation.push(value);\n if (value !== \"\") {\n empty = false;\n }\n }\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n // Sync every 2 seconds in case quiz closes automatically without user\n // action.\n AceGapfillerUi.prototype.syncIntervalSecs = (() => 2);\n\n // Reload the HTML fields from the given serialisation.\n AceGapfillerUi.prototype.reload = function() {\n let content = this.textArea.val();\n if (content) {\n try {\n let values = JSON.parse(content);\n for (let i = 0; i < this.gaps.length; i++) {\n let value = i < values.length ? values[i]: '???';\n this.gaps[i].insertText(this.gaps, this.gaps[i].range.start.column, value);\n }\n } catch(e) {\n // Just ignore errors\n }\n }\n };\n\n AceGapfillerUi.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceGapfillerUi.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceGapfillerUi.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceGapfillerUi.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n AceGapfillerUi.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textArea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceGapfillerUi.prototype.destroy = function () {\n this.sync();\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n }\n };\n\n AceGapfillerUi.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceGapfillerUi.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceGapfillerUi.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n /**\n * Constructor for the Gap object that represents a gap in the source code\n * that the user is expected to fill.\n * @param {object} editor The Ace Editor object.\n * @param {int} row The row within the text of the gap.\n * @param {int} column The column within the text of the gap.\n * @param {int} minWidth The minimum width (in characters) of the gap.\n * @param {int} maxWidth The maximum width (in characters) of the gap.\n */\n function Gap(editor, row, column, minWidth, maxWidth=Infinity) {\n this.editor = editor;\n\n this.minWidth = minWidth;\n this.maxWidth = maxWidth;\n\n this.range = new Range(row, column, row, column+minWidth);\n this.textSize = 0;\n\n // Create markers\n this.editor.session.addMarker(this.range, \"ace-gap-outline\", \"text\", true);\n this.editor.session.addMarker(this.range, \"ace-gap-background\", \"text\", false);\n }\n\n Gap.prototype.cursorInGap = function(cursor) {\n return (cursor.row >= this.range.start.row && cursor.column >= this.range.start.column &&\n cursor.row <= this.range.end.row && cursor.column <= this.range.end.column);\n };\n\n Gap.prototype.getWidth = function() {\n return (this.range.end.column-this.range.start.column);\n };\n\n Gap.prototype.changeWidth = function(gaps, delta) {\n this.range.end.column += delta;\n\n // Update any gaps that come after this one on the same line\n for (let i=0; i < gaps.length; i++) {\n let other = gaps[i];\n if (other.range.start.row === this.range.start.row && other.range.start.column > this.range.end.column) {\n other.range.start.column += delta;\n other.range.end.column += delta;\n }\n }\n\n this.editor.$onChangeBackMarker();\n this.editor.$onChangeFrontMarker();\n };\n\n Gap.prototype.insertChar = function(gaps, pos, char) {\n if (this.textSize === this.getWidth() && this.getWidth() < this.maxWidth) { // Grow the size of gap and insert char.\n this.changeWidth(gaps, 1);\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n } else if (this.textSize < this.maxWidth) { // Insert char.\n this.editor.session.remove(new Range(pos.row, this.range.end.column-1, pos.row, this.range.end.column));\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n }\n };\n\n Gap.prototype.deleteChar = function(gaps, pos) {\n this.textSize -= 1;\n this.editor.session.remove(new Range(pos.row, pos.column, pos.row, pos.column+1));\n\n if (this.textSize >= this.minWidth) {\n this.changeWidth(gaps, -1); // Shrink the size of the gap.\n } else {\n // Put new space at end so everything is shifted across.\n this.editor.session.insert({row: pos.row, column: this.range.end.column-1}, fillChar);\n }\n };\n\n Gap.prototype.deleteRange = function(gaps, start, end) {\n for (let i = start; i < end; i++) {\n if (start < this.range.start.column+this.textSize) {\n this.deleteChar(gaps, {row: this.range.start.row, column: start});\n }\n }\n };\n\n Gap.prototype.insertText = function(gaps, start, text) {\n for (let i = 0; i < text.length; i++) {\n if (start+i < this.range.start.column+this.maxWidth) {\n this.insertChar(gaps, {row: this.range.start.row, column: start+i}, text[i]);\n }\n }\n };\n\n Gap.prototype.getText = function() {\n return this.editor.session.getTextRange(new Range(this.range.start.row, this.range.start.column,\n this.range.end.row, this.range.start.column+this.textSize));\n\n };\n\n return {\n Constructor: AceGapfillerUi\n };\n});\n"],"names":["define","$","Range","validChars","AceGapfillerUi","textareaId","w","h","uiParams","textArea","document","getElementById","wrapper","focused","this","activeElement","lang","t","code","gaps","source","ui_source","nextGapIndex","alert","attr","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","displayIndentGuides","dragEnabled","enableBasicAutocompletion","newLineMode","$blockScrolling","Infinity","theme","setTheme","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","aceTextarea","createGaps","commands","e","cursor","selection","getCursor","commandName","command","name","selectionRange","getSelectionRange","gap","findCursorGap","startsWith","column","range","start","textSize","moveCursorTo","row","end","selectAll","nextGap","index","length","clearSelection","setSelectionRange","isEmpty","char","args","test","insertChar","deleteChar","cursorInGap","deleteRange","insertText","text","preventDefault","stopPropagation","gapToSelect","fail","reload","err","Gap","minWidth","maxWidth","session","addMarker","prototype","reEscape","s","c","result","i","j","lines","split","sepLeft","sepRight","splitter","RegExp","editorContent","bits","columnPos","values","parseInt","push","repeat","setValue","failed","failMessage","sync","serialisation","empty","value","getText","val","JSON","stringify","syncIntervalSecs","content","parse","language","getSession","mode","findMode","setMode","getElement","bindKeys","releaseTab","trigger","container","addEventListener","undefined","which","keyCode","ctrlKey","altKey","shiftKey","destroy","isFocused","remove","selectionStart","hasFocus","candidate","filename","candidates","nameMap","toLowerCase","replace","modesByName","getModeForPath","outerHeight","outerWidth","getWidth","changeWidth","delta","other","$onChangeBackMarker","$onChangeFrontMarker","pos","insert","getTextRange","Constructor"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"ui_ace_gapfiller.min.js","sources":["../src/ui_ace_gapfiller.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more util.details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Implementation of the ace_gapfiller_ui user interface plugin. For overall details\n * of the UI plugin architecture, see userinterfacewrapper.js.\n *\n * This plugin uses the usual ace editor but only makes some portions of the text editable.\n * The pre-formatted text is supplied by the question author in either the\n * \"globalextra\" field or the testcode field of the first test case, according\n * to the ui parameter ui_source (default: globalextra).\n * Editable \"gaps\" are inserted into the ace editor at specified points.\n * It is intended primarily for use with coding questions where the answerbox presents\n * the students with code that has smallish bits missing.\n *\n * The locations within the globalextra text at which the gaps are\n * to be inserted are denoted by \"tags\" of the form\n *\n * {[ size ]}\n *\n * or\n *\n * {[ size-maxSize ]}\n *\n * where size and maxSize are integer literals. These respectively inject a \"gap\" into\n * the editor of the specified size and maxSize. If maxSize is not specified then the\n * \"gap\" has no maximum size and can grow without bound.\n *\n * The serialisation of the answer box contents, i.e. the text that\n * copied back into the textarea for submissions\n * as the answer, is simply a list of all the field values (strings), in order.\n *\n * As a special case of the serialisation, if the value list is empty, the\n * serialisation itself is the empty string.\n *\n * The delimiters for the gap tags are by default '{[' and\n * ']}'.\n *\n * @module qtype_coderunner/ui_ace_gapfiller\n * @copyright Richard Lobb, 2019, The University of Canterbury\n * @copyright Matthew Toohey, 2021, The University of Canterbury\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n\n var Range; // Can't load this until ace has loaded.\n const fillChar = \" \";\n const validChars = /[ !\"#$%&'()*+,`\\-./0-9\\p{L}:;<=>?@\\[\\]\\\\^_{}|~]/u;\n const ACE_LIGHT_THEME = 'ace/theme/textmate';\n\n /**\n * Constructor for the Ace interface object\n * @param {string} textareaId The ID of the textarea html element.\n * @param {int} w The width of the text area in pixels.\n * @param {int} h The height of the text area in pixels.\n * @param {object} uiParams The UI parameter specifier object.\n */\n function AceGapfillerUi(textareaId, w, h, uiParams) {\n this.textArea = $(document.getElementById(textareaId));\n var wrapper = $(document.getElementById(textareaId + '_wrapper')),\n focused = this.textArea[0] === document.activeElement,\n lang = uiParams.lang,\n t = this; // For embedded callbacks.\n\n let code = \"\";\n this.uiParams = uiParams;\n this.gaps = [];\n this.source = uiParams.ui_source || 'globalextra';\n this.nextGapIndex = 0;\n if (this.source !== 'globalextra' && this.source !== 'test0') {\n alert('Invalid source for code in ui_ace_gapfiller');\n this.source = 'globalextra';\n }\n if (this.source == 'globalextra') {\n code = this.textArea.attr('data-globalextra');\n } else {\n code = this.textArea.attr('data-test0');\n }\n\n try {\n window.ace.require(\"ace/ext/language_tools\");\n Range = window.ace.require(\"ace/range\").Range;\n this.modelist = window.ace.require('ace/ext/modelist');\n\n this.enabled = false;\n this.contents_changed = false;\n this.capturingTab = false;\n this.clickInProgress = false;\n\n this.editNode = $(\"
\"); // Ace editor manages this\n this.editNode.css({\n resize: 'none',\n height: h,\n width: \"100%\"\n });\n\n this.editor = window.ace.edit(this.editNode.get(0));\n if (this.textArea.prop('readonly')) {\n this.editor.setReadOnly(true);\n }\n\n this.editor.setOptions({\n displayIndentGuides: false,\n dragEnabled: false,\n enableBasicAutocompletion: true,\n newLineMode: \"unix\",\n });\n this.editor.$blockScrolling = Infinity;\n\n // Use the uiParams theme if provided else use light.\n if (uiParams.theme) {\n this.editor.setTheme(\"ace/theme/\" + uiParams.theme);\n } else {\n this.editor.setTheme(ACE_LIGHT_THEME);\n }\n\n this.setLanguage(lang);\n\n this.setEventHandlers(this.textArea);\n this.captureTab();\n\n // Try to tell Moodle about parts of the editor with z-index.\n // It is hard to be sure if this is complete. ACE adds all its CSS using JavaScript.\n // Here, we just deal with things that are known to cause a problem.\n // Can't do these operations until editor has rendered. So ...\n this.editor.renderer.on('afterRender', function() {\n var gutter = wrapper.find('.ace_gutter');\n if (gutter.hasClass('moodle-has-zindex')) {\n return; // So we only do what follows once.\n }\n gutter.addClass('moodle-has-zindex');\n\n if (focused) {\n t.editor.focus();\n t.editor.navigateFileEnd();\n }\n t.aceLabel = wrapper.find('.answerprompt');\n t.aceLabel.attr('for', 'ace_' + textareaId);\n\n t.aceTextarea = wrapper.find('.ace_text-input');\n t.aceTextarea.attr('id', 'ace_' + textareaId);\n });\n\n this.createGaps(code);\n\n // Intercept commands sent to ace.\n this.editor.commands.on(\"exec\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let commandName = e.command.name;\n let selectionRange = t.editor.getSelectionRange();\n\n let gap = t.findCursorGap(cursor);\n\n if (commandName.startsWith(\"go\")) { // If command just moves the cursor then do nothing.\n if (gap !== null && commandName === \"gotoright\" && cursor.column === gap.range.start.column+gap.textSize) {\n // In this case we jump out of gap over the empty space that contains nothing that the user has entered.\n t.editor.moveCursorTo(cursor.row, gap.range.end.column+1);\n } else {\n return;\n }\n }\n\n if (gap === null) {\n // Not in a gap\n if (commandName === \"selectall\") {\n t.editor.selection.selectAll();\n }\n\n } else if (commandName === \"indent\") {\n // Instead of indenting, move to next gap.\n let nextGap = t.gaps[(gap.index+1) % t.gaps.length];\n t.editor.moveCursorTo(nextGap.range.start.row, nextGap.range.start.column+nextGap.textSize);\n t.editor.selection.clearSelection(); // Clear selection.\n\n } else if (commandName === \"selectall\") {\n // Select all text in a gap if we are in a gap.\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n\n } else if (t.editor.selection.isEmpty()) {\n // User is not selecting multiple characters.\n if (commandName === \"insertstring\") {\n let char = e.args;\n // Only allow user to insert 'valid' chars.\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, cursor, char);\n }\n } else if (commandName === \"backspace\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column > gap.range.start.column && gap.textSize > 0) {\n gap.deleteChar(t.gaps, {row: cursor.row, column: cursor.column-1});\n }\n } else if (commandName === \"del\") {\n // Only delete chars that are actually in the gap.\n if (cursor.column < gap.range.start.column + gap.textSize && gap.textSize > 0) {\n gap.deleteChar(t.gaps, cursor);\n }\n }\n t.editor.selection.clearSelection(); // Keep selection clear.\n\n } else if (!t.editor.selection.isEmpty() && gap.cursorInGap(selectionRange.start)\n && gap.cursorInGap(selectionRange.end)) {\n // User is selecting multiple characters and is in a gap.\n\n // These are the commands that remove the selected text.\n if (commandName === \"insertstring\" || commandName === \"backspace\"\n || commandName === \"del\" || commandName === \"paste\"\n || commandName === \"cut\") {\n\n gap.deleteRange(t.gaps, selectionRange.start.column, selectionRange.end.column);\n t.editor.selection.clearSelection(); // Clear selection.\n }\n\n if (commandName === \"insertstring\") {\n let char = e.args;\n if (validChars.test(char)) {\n gap.insertChar(t.gaps, selectionRange.start, char);\n }\n }\n }\n\n // Paste text into gap.\n if (gap !== null && commandName === \"paste\") {\n gap.insertText(t.gaps, selectionRange.start.column, e.args.text);\n }\n\n e.preventDefault();\n e.stopPropagation();\n });\n\n // Move cursor to where it should be if we click on a gap.\n t.editor.selection.on('changeCursor', function() {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n if (cursor.column > gap.range.start.column+gap.textSize) {\n t.editor.moveCursorTo(gap.range.start.row, gap.range.start.column+gap.textSize);\n }\n }\n });\n\n this.gapToSelect = null; // Stores gap that has been selected with triple click.\n\n // Select all text in gap on triple click within gap.\n this.editor.on(\"tripleclick\", function(e) {\n let cursor = t.editor.selection.getCursor();\n let gap = t.findCursorGap(cursor);\n if (gap !== null) {\n t.editor.selection.setSelectionRange(new Range(gap.range.start.row,\n gap.range.start.column,\n gap.range.start.row,\n gap.range.end.column), false);\n t.gapToSelect = gap;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n // Annoying hack to ensure the tripple click thing works.\n this.editor.on(\"click\", function(e) {\n if (t.gapToSelect) {\n t.editor.moveCursorTo(t.gapToSelect.range.start.row, t.gapToSelect.range.start.column+t.gapToSelect.textSize);\n t.gapToSelect = null;\n e.preventDefault();\n e.stopPropagation();\n }\n });\n\n this.fail = false;\n this.reload();\n }\n catch(err) {\n // Something ugly happened. Probably ace editor hasn't been loaded\n this.fail = true;\n }\n }\n\n /**\n * The method that creates the gaps at all places containing the appropriate\n * marker (default {[ ... ]}).\n * Do not call until after this.editor has been instantiated.\n * @param {string} code The initial raw text code\n */\n AceGapfillerUi.prototype.createGaps = function(code) {\n this.gaps = [];\n /**\n * Escape special characters in a given string.\n * @param {string} s The input string.\n * @returns {string} The updated string, with escaped specials.\n */\n function reEscape(s) {\n var c, specials = '{[(*+\\\\', result='';\n for (var i = 0; i < s.length; i++) {\n c = s[i];\n for (var j = 0; j < specials.length; j++) {\n if (c === specials[j]) {\n c = '\\\\' + c;\n }\n }\n result += c;\n }\n return result;\n }\n\n let lines = code.split(/\\r?\\n/);\n\n let sepLeft = reEscape('{[');\n let sepRight = reEscape(']}');\n let splitter = new RegExp(sepLeft + ' *((?:\\\\d+)|(?:\\\\d+- *\\\\d+)) *' + sepRight);\n\n let editorContent = \"\";\n for (let i = 0; i < lines.length; i++) {\n let bits = lines[i].split(splitter);\n editorContent += bits[0];\n\n let columnPos = bits[0].length;\n for (let j = 1; j < bits.length; j += 2) {\n let values = bits[j].split('-');\n let minWidth = parseInt(values[0]);\n let maxWidth = (values.length > 1 ? parseInt(values[1]) : Infinity);\n\n // Create new gap.\n let gap = new Gap(this.editor, i, columnPos, minWidth, maxWidth);\n gap.index = this.nextGapIndex;\n this.nextGapIndex += 1;\n this.gaps.push(gap);\n\n columnPos += minWidth;\n editorContent += ' '.repeat(minWidth);\n if (j + 1 < bits.length) {\n editorContent += bits[j+1];\n columnPos += bits[j+1].length;\n }\n\n }\n\n if (i < lines.length-1) {\n editorContent += '\\n';\n }\n }\n this.editor.session.setValue(editorContent);\n };\n\n /**\n * Return the gap that the cursor is in. This will actually return a gap if\n * the cursor is 1 outside the gap as this will be needed for\n * backspace/insertion to work. Rigth now this is done as a simple\n * linear search but could be improved later.\n * @param {object} cursor The ace editor cursor position.\n * @returns {object} The gap that the cursor is current in, or null otherwise.\n */\n AceGapfillerUi.prototype.findCursorGap = function(cursor) {\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n if (gap.cursorInGap(cursor)) {\n return gap;\n }\n }\n return null;\n };\n\n AceGapfillerUi.prototype.failed = function() {\n return this.fail;\n };\n\n AceGapfillerUi.prototype.failMessage = function() {\n return 'ace_ui_notready';\n };\n\n\n // Sync to TextArea\n AceGapfillerUi.prototype.sync = function() {\n if (this.fail) {\n return; // Leave the text area alone if Ace load failed.\n }\n let serialisation = []; // A list of field values.\n let empty = true;\n\n for (let i=0; i < this.gaps.length; i++) {\n let gap = this.gaps[i];\n let value = gap.getText();\n serialisation.push(value);\n if (value !== \"\") {\n empty = false;\n }\n }\n if (empty) {\n this.textArea.val('');\n } else {\n this.textArea.val(JSON.stringify(serialisation));\n }\n };\n\n // Sync every 2 seconds in case quiz closes automatically without user\n // action.\n AceGapfillerUi.prototype.syncIntervalSecs = (() => 2);\n\n // Reload the HTML fields from the given serialisation.\n AceGapfillerUi.prototype.reload = function() {\n let content = this.textArea.val();\n if (content) {\n try {\n let values = JSON.parse(content);\n for (let i = 0; i < this.gaps.length; i++) {\n let value = i < values.length ? values[i]: '???';\n this.gaps[i].insertText(this.gaps, this.gaps[i].range.start.column, value);\n }\n } catch(e) {\n // Just ignore errors\n }\n }\n };\n\n AceGapfillerUi.prototype.setLanguage = function(language) {\n var session = this.editor.getSession(),\n mode = this.findMode(language);\n if (mode) {\n session.setMode(mode.mode);\n }\n };\n\n AceGapfillerUi.prototype.getElement = function() {\n return this.editNode;\n };\n\n AceGapfillerUi.prototype.captureTab = function () {\n this.capturingTab = true;\n this.editor.commands.bindKeys({'Tab': 'indent', 'Shift-Tab': 'outdent'});\n };\n\n AceGapfillerUi.prototype.releaseTab = function () {\n this.capturingTab = false;\n this.editor.commands.bindKeys({'Tab': null, 'Shift-Tab': null});\n };\n\n AceGapfillerUi.prototype.setEventHandlers = function () {\n var TAB = 9,\n ESC = 27,\n KEY_M = 77,\n t = this;\n\n this.editor.getSession().on('change', function() {\n t.contents_changed = true;\n });\n\n this.editor.on('blur', function() {\n if (t.contents_changed) {\n t.textArea.trigger('change');\n }\n });\n\n this.editor.on('mousedown', function() {\n // Event order seems to be (\\ is where the mouse button is pressed, / released):\n // Chrome: \\ mousedown, mouseup, focusin / click.\n // Firefox/IE: \\ mousedown, focusin / mouseup, click.\n t.clickInProgress = true;\n });\n\n this.editor.on('focus', function() {\n if (t.clickInProgress) {\n t.captureTab();\n } else {\n t.releaseTab();\n }\n });\n\n this.editor.on('click', function() {\n t.clickInProgress = false;\n });\n\n this.editor.container.addEventListener('keydown', function(e) {\n if (e.which === undefined || e.which !== 0) { // Normal keypress?\n if (e.keyCode === KEY_M && e.ctrlKey && !e.altKey) {\n if (t.capturingTab) {\n t.releaseTab();\n } else {\n t.captureTab();\n }\n e.preventDefault(); // Firefox uses this for mute audio in current browser tab.\n }\n else if (e.keyCode === ESC) {\n t.releaseTab();\n }\n else if (!(e.shiftKey || e.ctrlKey || e.altKey || e.keyCode == TAB)) {\n t.captureTab();\n }\n }\n }, true);\n };\n\n AceGapfillerUi.prototype.destroy = function () {\n this.sync();\n var focused;\n if (!this.fail) {\n // Proceed only if this wrapper was correctly constructed\n focused = this.editor.isFocused();\n this.editor.destroy();\n $(this.editNode).remove();\n if (focused) {\n this.textArea.focus();\n this.textArea[0].selectionStart = this.textArea[0].value.length;\n }\n }\n };\n\n AceGapfillerUi.prototype.hasFocus = function() {\n return this.editor.isFocused();\n };\n\n AceGapfillerUi.prototype.findMode = function (language) {\n var candidate,\n filename,\n result,\n candidates = [], // List of candidate modes.\n nameMap = {\n 'octave': 'matlab',\n 'nodejs': 'javascript',\n 'c#': 'cs'\n };\n\n if (typeof language !== 'string') {\n return undefined;\n }\n if (language.toLowerCase() in nameMap) {\n language = nameMap[language.toLowerCase()];\n }\n\n candidates = [language, language.replace(/\\d+$/, \"\")];\n for (var i = 0; i < candidates.length; i++) {\n candidate = candidates[i];\n filename = \"input.\" + candidate;\n result = this.modelist.modesByName[candidate] ||\n this.modelist.modesByName[candidate.toLowerCase()] ||\n this.modelist.getModeForPath(filename) ||\n this.modelist.getModeForPath(filename.toLowerCase());\n\n if (result && result.name !== 'text') {\n return result;\n }\n }\n return undefined;\n };\n\n AceGapfillerUi.prototype.resize = function(w, h) {\n this.editNode.outerHeight(h);\n this.editNode.outerWidth(w);\n this.editor.resize();\n };\n\n /**\n * Constructor for the Gap object that represents a gap in the source code\n * that the user is expected to fill.\n * @param {object} editor The Ace Editor object.\n * @param {int} row The row within the text of the gap.\n * @param {int} column The column within the text of the gap.\n * @param {int} minWidth The minimum width (in characters) of the gap.\n * @param {int} maxWidth The maximum width (in characters) of the gap.\n */\n function Gap(editor, row, column, minWidth, maxWidth=Infinity) {\n this.editor = editor;\n\n this.minWidth = minWidth;\n this.maxWidth = maxWidth;\n\n this.range = new Range(row, column, row, column+minWidth);\n this.textSize = 0;\n\n // Create markers\n this.editor.session.addMarker(this.range, \"ace-gap-outline\", \"text\", true);\n this.editor.session.addMarker(this.range, \"ace-gap-background\", \"text\", false);\n }\n\n Gap.prototype.cursorInGap = function(cursor) {\n return (cursor.row >= this.range.start.row && cursor.column >= this.range.start.column &&\n cursor.row <= this.range.end.row && cursor.column <= this.range.end.column);\n };\n\n Gap.prototype.getWidth = function() {\n return (this.range.end.column-this.range.start.column);\n };\n\n Gap.prototype.changeWidth = function(gaps, delta) {\n this.range.end.column += delta;\n\n // Update any gaps that come after this one on the same line\n for (let i=0; i < gaps.length; i++) {\n let other = gaps[i];\n if (other.range.start.row === this.range.start.row && other.range.start.column > this.range.start.column) {\n other.range.start.column += delta;\n other.range.end.column += delta;\n }\n }\n\n this.editor.$onChangeBackMarker();\n this.editor.$onChangeFrontMarker();\n };\n\n Gap.prototype.insertChar = function(gaps, pos, char) {\n if (this.textSize === this.getWidth() && this.getWidth() < this.maxWidth) { // Grow the size of gap and insert char.\n this.changeWidth(gaps, 1);\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n } else if (this.textSize < this.maxWidth) { // Insert char.\n this.editor.session.remove(new Range(pos.row, this.range.end.column-1, pos.row, this.range.end.column));\n this.textSize += 1; // Important to record that texSize has increased before insertion.\n this.editor.session.insert(pos, char);\n }\n };\n\n Gap.prototype.deleteChar = function(gaps, pos) {\n this.textSize -= 1;\n this.editor.session.remove(new Range(pos.row, pos.column, pos.row, pos.column+1));\n\n if (this.textSize >= this.minWidth) {\n this.changeWidth(gaps, -1); // Shrink the size of the gap.\n } else {\n // Put new space at end so everything is shifted across.\n this.editor.session.insert({row: pos.row, column: this.range.end.column-1}, fillChar);\n }\n };\n\n Gap.prototype.deleteRange = function(gaps, start, end) {\n for (let i = start; i < end; i++) {\n if (start < this.range.start.column+this.textSize) {\n this.deleteChar(gaps, {row: this.range.start.row, column: start});\n }\n }\n };\n\n Gap.prototype.insertText = function(gaps, start, text) {\n for (let i = 0; i < text.length; i++) {\n if (start+i < this.range.start.column+this.maxWidth) {\n this.insertChar(gaps, {row: this.range.start.row, column: start+i}, text[i]);\n }\n }\n };\n\n Gap.prototype.getText = function() {\n return this.editor.session.getTextRange(new Range(this.range.start.row, this.range.start.column,\n this.range.end.row, this.range.start.column+this.textSize));\n\n };\n\n return {\n Constructor: AceGapfillerUi\n };\n});\n"],"names":["define","$","Range","validChars","AceGapfillerUi","textareaId","w","h","uiParams","textArea","document","getElementById","wrapper","focused","this","activeElement","lang","t","code","gaps","source","ui_source","nextGapIndex","alert","attr","window","ace","require","modelist","enabled","contents_changed","capturingTab","clickInProgress","editNode","css","resize","height","width","editor","edit","get","prop","setReadOnly","setOptions","displayIndentGuides","dragEnabled","enableBasicAutocompletion","newLineMode","$blockScrolling","Infinity","theme","setTheme","setLanguage","setEventHandlers","captureTab","renderer","on","gutter","find","hasClass","addClass","focus","navigateFileEnd","aceLabel","aceTextarea","createGaps","commands","e","cursor","selection","getCursor","commandName","command","name","selectionRange","getSelectionRange","gap","findCursorGap","startsWith","column","range","start","textSize","moveCursorTo","row","end","selectAll","nextGap","index","length","clearSelection","setSelectionRange","isEmpty","char","args","test","insertChar","deleteChar","cursorInGap","deleteRange","insertText","text","preventDefault","stopPropagation","gapToSelect","fail","reload","err","Gap","minWidth","maxWidth","session","addMarker","prototype","reEscape","s","c","result","i","j","lines","split","sepLeft","sepRight","splitter","RegExp","editorContent","bits","columnPos","values","parseInt","push","repeat","setValue","failed","failMessage","sync","serialisation","empty","value","getText","val","JSON","stringify","syncIntervalSecs","content","parse","language","getSession","mode","findMode","setMode","getElement","bindKeys","releaseTab","trigger","container","addEventListener","undefined","which","keyCode","ctrlKey","altKey","shiftKey","destroy","isFocused","remove","selectionStart","hasFocus","candidate","filename","candidates","nameMap","toLowerCase","replace","modesByName","getModeForPath","outerHeight","outerWidth","getWidth","changeWidth","delta","other","$onChangeBackMarker","$onChangeFrontMarker","pos","insert","getTextRange","Constructor"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/ui_ace_gapfiller.js b/amd/src/ui_ace_gapfiller.js index 101c1c69c..d6d81b872 100644 --- a/amd/src/ui_ace_gapfiller.js +++ b/amd/src/ui_ace_gapfiller.js @@ -600,7 +600,7 @@ define(['jquery'], function($) { // Update any gaps that come after this one on the same line for (let i=0; i < gaps.length; i++) { let other = gaps[i]; - if (other.range.start.row === this.range.start.row && other.range.start.column > this.range.end.column) { + if (other.range.start.row === this.range.start.row && other.range.start.column > this.range.start.column) { other.range.start.column += delta; other.range.end.column += delta; }