From a15f36b0d24a860ed70eade9575995c2ea6e06ab Mon Sep 17 00:00:00 2001 From: Hannah Bast Date: Tue, 6 Feb 2024 01:34:43 +0100 Subject: [PATCH 1/3] No subject AC query for short prefix Without typing at least a few characters, it's more confusing than helpful to suggest a subject AC query to the user. It is currently hard-coded that at least 3 characters have to be typed, but this should be a configurable parameter. --- backend/static/js/codemirror/modes/sparql/sparql-hint.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/static/js/codemirror/modes/sparql/sparql-hint.js b/backend/static/js/codemirror/modes/sparql/sparql-hint.js index e4483461..f456afdb 100755 --- a/backend/static/js/codemirror/modes/sparql/sparql-hint.js +++ b/backend/static/js/codemirror/modes/sparql/sparql-hint.js @@ -420,8 +420,11 @@ function getDynamicSuggestions(context) { } } + // Do not launch AC query when current word starts with ? or for subject AC + // queries when the prefix has length < 3. + var sendSparql = !(word.startsWith('?')) + && !(words.length == 1 && words[0].length < 3); sparqlQuery = ""; - var sendSparql = !(word.startsWith('?')); var sparqlLines = ""; var mode1Query = ""; // mode 1 is context-insensitive var mode2Query = ""; // mode 2 is context-sensitive From 3403721d0366f462291bf5fe10d33cf8ca7eaaf8 Mon Sep 17 00:00:00 2001 From: Hannah Bast Date: Sun, 11 Feb 2024 23:57:44 +0100 Subject: [PATCH 2/3] Revert "No subject AC query for short prefix" PR merged for testing, but is not part of this PR --- backend/static/js/codemirror/modes/sparql/sparql-hint.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/backend/static/js/codemirror/modes/sparql/sparql-hint.js b/backend/static/js/codemirror/modes/sparql/sparql-hint.js index f456afdb..e4483461 100755 --- a/backend/static/js/codemirror/modes/sparql/sparql-hint.js +++ b/backend/static/js/codemirror/modes/sparql/sparql-hint.js @@ -420,11 +420,8 @@ function getDynamicSuggestions(context) { } } - // Do not launch AC query when current word starts with ? or for subject AC - // queries when the prefix has length < 3. - var sendSparql = !(word.startsWith('?')) - && !(words.length == 1 && words[0].length < 3); sparqlQuery = ""; + var sendSparql = !(word.startsWith('?')); var sparqlLines = ""; var mode1Query = ""; // mode 1 is context-insensitive var mode2Query = ""; // mode 2 is context-sensitive From 368327df84a938a14250ac13bd413ad698e51c62 Mon Sep 17 00:00:00 2001 From: Hannah Bast Date: Sun, 11 Feb 2024 23:58:25 +0100 Subject: [PATCH 3/3] Fix some of the overzealous UI behavior Some of the useful automatic features of the query editor went rogue under certain conditions. Two things are now fixed or at least better: 1. When the query did not contain `SELECT` in all upper case and one of the existing prefixes was typed, each cursor movement would be followed by a reveral of that movement, which made the UI unusable. This is now fixed. 2. So far, the query was "cleaned up" (blank lines removed, whitespace replaced by single spaces, etc.) after *every* cursor movement, which was not always desired. Now the query is only cleaned up in this way when the `TAB` key is pressed (which jumps between the different positions in the placeholder). 3. Some REGEXes were only looking for fully uppercases version of SPARQL keywords like WHERE, etc. Now the are case-insensitive. --- .../js/codemirror/modes/sparql/sparql.js | 44 +++++++++---------- backend/static/js/helper.js | 20 ++++++--- backend/static/js/qleverUI.js | 18 ++++---- 3 files changed, 46 insertions(+), 36 deletions(-) diff --git a/backend/static/js/codemirror/modes/sparql/sparql.js b/backend/static/js/codemirror/modes/sparql/sparql.js index 778558fc..955a1bda 100644 --- a/backend/static/js/codemirror/modes/sparql/sparql.js +++ b/backend/static/js/codemirror/modes/sparql/sparql.js @@ -74,14 +74,14 @@ const CONTEXTS = [ const COMPLEXTYPES = [ { name: 'PREFIX', - definition: /PREFIX (.*)/g, + definition: /PREFIX (.*)/gi, suggestions: [['PREFIX ', function (c) { return getPrefixSuggestions(c); }, '\n']], availableInContext: ['PrefixDecl', 'undefined'], }, { name: 'SELECT', - definition: /SELECT (.*)/g, + definition: /SELECT (.*)/gi, suggestions: [['SELECT * WHERE {\n \n}\n']], availableInContext: ['PrefixDecl', 'undefined', 'SubQuery'], onlyOnce: true, @@ -89,7 +89,7 @@ const COMPLEXTYPES = [ }, { name: 'DISTINCT', - definition: /DISTINCT|[?\w]+ [?\w]*/g, + definition: /DISTINCT|[?\w]+ [?\w]*/gi, suggestions: [['DISTINCT ']], availableInContext: ['SelectClause'], onlyOnce: true, @@ -113,70 +113,70 @@ const COMPLEXTYPES = [ }, { name: 'TEXT', - definition: /TEXT\((.*)\)/g, + definition: /TEXT\((.*)\)/gi, suggestions: [['TEXT(', function (c) { var a = []; $(getVariables(c, true, "text")).each(function (k, v) { a.push(v) }); return a; }, ') ']], availableInContext: ['SelectClause'], }, { name: 'SCORE', - definition: /SCORE\((.*)\)/g, + definition: /SCORE\((.*)\)/gi, suggestions: [['SCORE(', function (c) { var a = []; $(getVariables(c, true, "text")).each(function (k, v) { a.push(v) }); return a; }, ') ']], availableInContext: ['SelectClause', 'OrderCondition'], }, { name: 'MIN', - definition: /\(MIN\((.*)\) as (.*)\)/g, + definition: /\(MIN\((.*)\) as (.*)\)/gi, suggestions: [['(MIN(', function (c) { if (getContextByName('SolutionModifier') && getContextByName('SolutionModifier')['content'].indexOf('GROUP BY') != -1) { return getVariables(c, true); } else { return false; } }, ') as ?min_{[0]}) ']], availableInContext: ['SelectClause', 'OrderCondition'], }, { name: 'MAX', - definition: /\(MAX\((.*)\) as (.*)\)/g, + definition: /\(MAX\((.*)\) as (.*)\)/gi, suggestions: [['(MAX(', function (c) { if (getContextByName('SolutionModifier') && getContextByName('SolutionModifier')['content'].indexOf('GROUP BY') != -1) { return getVariables(c, true); } else { return false; } }, ') as ?max_{[0]}) ']], availableInContext: ['SelectClause', 'OrderCondition'], }, { name: 'SUM', - definition: /\(SUM\((.*)\) as (.*)\)/g, + definition: /\(SUM\((.*)\) as (.*)\)/gi, suggestions: [['(SUM(', ['DISTINCT ', ''], function (c) { if (getContextByName('SolutionModifier') && getContextByName('SolutionModifier')['content'].indexOf('GROUP BY') != -1) { return getVariables(c, true); } else { return false; } }, ') as ?sum_{[1]}) ']], availableInContext: ['SelectClause', 'OrderCondition'], }, { name: 'AVG', - definition: /\(AVG\((.*)\) as (.*)\)/g, + definition: /\(AVG\((.*)\) as (.*)\)/gi, suggestions: [['(AVG(', ['DISTINCT ', ''], function (c) { if (getContextByName('SolutionModifier') && getContextByName('SolutionModifier')['content'].indexOf('GROUP BY') != -1) { return getVariables(c, true); } else { return false; } }, ') as ?avg_{[1]}) ']], availableInContext: ['SelectClause', 'OrderCondition'], }, { name: 'SAMPLE', - definition: /\(SAMPLE\((.*)\) as (.*)\)/g, + definition: /\(SAMPLE\((.*)\) as (.*)\)/gi, suggestions: [['(SAMPLE(', function (c) { if (getContextByName('SolutionModifier') && getContextByName('SolutionModifier')['content'].indexOf('GROUP BY') != -1) { return getVariables(c, true); } else { return false; } }, ') as ?sample_{[0]}) ']], availableInContext: ['SelectClause'], }, { name: 'COUNT', - definition: /\(COUNT\((.*)\) as (.*)\)/g, + definition: /\(COUNT\((.*)\) as (.*)\)/gi, suggestions: [['(COUNT(', ['DISTINCT ', ''], function (c) { if (getContextByName('SolutionModifier') && getContextByName('SolutionModifier')['content'].indexOf('GROUP BY') != -1) { return getVariables(c, true); } else { return false; } }, ') as ?count_{[1]}) ']], availableInContext: ['SelectClause'], }, { name: 'GROUP_CONCAT', - definition: /\(GROUP_CONCAT\((.*)\) as (.*)\)/g, + definition: /\(GROUP_CONCAT\((.*)\) as (.*)\)/gi, suggestions: [['(GROUP_CONCAT(', ['DISTINCT ', ''], function (c) { if (getContextByName('SolutionModifier') && getContextByName('SolutionModifier')['content'].indexOf('GROUP BY') != -1) { return getVariables(c, true); } else { return false; } }, ') as ?concat_{[1]}) ']], availableInContext: ['SelectClause'], }, { name: 'LIMIT', - definition: /\bLIMIT ([0-9+])/g, + definition: /\bLIMIT ([0-9+])/gi, suggestions: [['LIMIT ', [1, 10, 100, 1000], '\n']], availableInContext: ['SolutionModifier'], onlyOnce: true, @@ -184,7 +184,7 @@ const COMPLEXTYPES = [ }, { name: 'TEXTLIMIT', - definition: /TEXTLIMIT ([0-9+])/g, + definition: /TEXTLIMIT ([0-9+])/gi, suggestions: [['TEXTLIMIT ', [2, 5, 10], '\n']], availableInContext: ['SolutionModifier'], onlyOnce: true, @@ -192,21 +192,21 @@ const COMPLEXTYPES = [ }, { name: 'ASC', - definition: /ASC(\?.*)/g, + definition: /ASC(\?.*)/gi, suggestions: [['ASC(', function (c) { var a = getVariables(c); for (var v of getVariables(c, undefined, "text")) { a.push("SCORE(" + v + ")"); }; return a; }, ') ']], availableInContext: ['OrderCondition'], }, { name: 'DESC', - definition: /DESC(\?.*)/g, + definition: /DESC(\?.*)/gi, suggestions: [['DESC(', function (c) { var a = getVariables(c); for (var v of getVariables(c, undefined, "text")) { a.push("SCORE(" + v + ")"); }; return a; }, ') ']], availableInContext: ['OrderCondition'], }, { name: 'ORDER BY', - definition: /ORDER BY .*/g, + definition: /ORDER BY .*/gi, suggestions: [['ORDER BY ']], availableInContext: ['SolutionModifier'], onlyOnce: true, @@ -214,7 +214,7 @@ const COMPLEXTYPES = [ }, { name: 'GROUP BY', - definition: /GROUP BY \?(.+)/g, + definition: /GROUP BY \?(.+)/gi, suggestions: [['GROUP BY ']], availableInContext: ['SolutionModifier'], onlyOnce: true, @@ -222,7 +222,7 @@ const COMPLEXTYPES = [ }, { name: 'HAVING', - definition: /HAVING\?(.+)/g, + definition: /HAVING\?(.+)/gi, suggestions: [['HAVING(', function (c) { return getVariables(c); }, ' ']], availableInContext: ['SolutionModifier'], onlyOnce: true, @@ -236,7 +236,7 @@ const COMPLEXTYPES = [ }, { name: 'REGEX', - definition: /REGEX(\?.*)/g, + definition: /REGEX(\?.*)/gi, suggestions: [['REGEX(', function (c) { return getVariables(c, true); }]], onlyOncePerVariation: true, availableInContext: ['Filter'], @@ -244,7 +244,7 @@ const COMPLEXTYPES = [ }, { name: 'FILTER', - definition: /FILTER\((.*)/g, + definition: /FILTER\((.*)/gi, suggestions: [['FILTER ']], availableInContext: ['WhereClause', 'OptionalClause', 'UnionClause'], requiresEmptyLine: true, @@ -253,7 +253,7 @@ const COMPLEXTYPES = [ }, { name: 'FILTER LANGUAGE', - definition: /LANG(.*)/g, + definition: /LANG(.*)/gi, suggestions: [['(LANG(', function (c) { var a = []; for (var v of getVariables(c, undefined, undefined, LANGUAGELITERAL)) { if (editor.getValue().indexOf("FILTER(LANG(" + v) == -1) { a.push(v); } }; return a; }, ') = ', LANGUAGES, ') .\n']], availableInContext: ['Filter'], }, diff --git a/backend/static/js/helper.js b/backend/static/js/helper.js index eef748b8..8c2bf9d3 100644 --- a/backend/static/js/helper.js +++ b/backend/static/js/helper.js @@ -219,7 +219,7 @@ function splitSparqlQueryIntoParts(query) { .replace(/\(\s+/g, "(").replace(/\s+\)/g, ")") .replace(/\{\s*/g, "{ ").replace(/\s*\.?\s*\}$/g, " }"); // console.log("SPLIT_SPARQL_QUERY_INTO_PARTS:", query_with_spaces_normalized) - const pattern = /^\s*(.*?)\s*SELECT\s+([^{]*\S)\s*WHERE\s*{\s*(\S.*\S)\s*}\s*(.*?)\s*$/m; + const pattern = /^\s*(.*?)\s*SELECT\s+([^{]*\S)\s*WHERE\s*{\s*(\S.*\S)\s*}\s*(.*?)\s*$/mi; var match = query_normalized.match(pattern); if (!match) { throw "ERROR: Query did not match regex for SELECT queries"; @@ -625,9 +625,9 @@ function cleanLines(cm) { cm.setSelection(selection.anchor, selection.head); } -// Triggered when using TAB +// Triggered when the `TAB` key is pressed; see `qleverUI`, search for +// `extraKeys` in `CodeMirror` initialization. function switchStates(cm) { - var cur = editor.getCursor(); // current cursor position var absolutePosition = editor.indexFromPos({ 'line': cur.line, 'ch': cur.ch + 1 }); // absolute cursor position in text @@ -635,7 +635,7 @@ function switchStates(cm) { var gaps = []; - var gap1 = /WHERE/g + var gap1 = /WHERE/gi while ((match = gap1.exec(content)) != null) { gaps.push(match.index + match[0].length - 5); } @@ -669,12 +669,13 @@ function switchStates(cm) { var newCursor = editor.posFromIndex(found); editor.setCursor(newCursor); + var line = cm.getLine(newCursor.line); indentWhitespaces = (" ".repeat((line.length - line.trimStart().length))) - if (line.slice(newCursor.ch, newCursor.ch + 5) == "WHERE") { - // add empty whitespace in select if not present + if (line.slice(newCursor.ch, newCursor.ch + 5).toUpperCase() == "WHERE") { + // Add empty whitespace in select if not present log("Found SELECT-Placeholder on postion " + found, 'other'); cm.setSelection({ 'line': newCursor.line, 'ch': line.length - 8 }, { 'line': newCursor.line, 'ch': line.length - 7 }); cm.replaceSelection(" "); @@ -702,6 +703,12 @@ function switchStates(cm) { cm.setSelection(cm.getCursor(), cm.getCursor()); + // Now that the cursor is set at a new position, clean up the query. + // + // NOTE: Previously, this was done after every cursor movement in + // `qleverUI.js`, where it says `editor.on("cursorActivity", ...)`. + cleanLines(cm); + window.setTimeout(function () { CodeMirror.commands.autocomplete(editor); }, 100); @@ -918,6 +925,7 @@ function getFormattedResultEntry(str, maxLength, column = undefined) { // the column header. // console.log("Check if \"" + str + "\" in column \"" + var_name + "\" is a float ..."); if (var_name.endsWith("?note") || var_name.endsWith("_note")) str = parseFloat(str).toFixed(2).toString(); + if (var_name.endsWith("?grade") || var_name.endsWith("_grade")) str = parseFloat(str).toFixed(2).toString(); if (var_name.endsWith("_per_paper")) str = parseFloat(str).toFixed(2).toString(); if (var_name.endsWith("_perc") || var_name.endsWith("percent")) str = parseFloat(str).toFixed(2).toString(); if (var_name.endsWith("?lp_proz")) str = parseFloat(str).toFixed(0).toString(); diff --git a/backend/static/js/qleverUI.js b/backend/static/js/qleverUI.js index 58bcdf6d..1d44ac86 100755 --- a/backend/static/js/qleverUI.js +++ b/backend/static/js/qleverUI.js @@ -87,10 +87,10 @@ $(document).ready(function () { // Initialization done. log('Editor initialized', 'other'); - // Do some custom activities on cursor activity + // When cursor moves, make sure that tooltips are closed. editor.on("cursorActivity", function (instance) { $('[data-tooltip=tooltip]').tooltip('hide'); - cleanLines(instance); + // cleanLines(instance); }); editor.on("update", function (instance, event) { @@ -107,10 +107,12 @@ $(document).ready(function () { // Do some custom activities (overwrite codemirror behaviour) editor.on("keyup", function (instance, event) { - // For each prefix in COLLECTEDPREFIXES, check whether it occurs somewhere - // in the query and if so, add it before the first SELECT (and move - // the cursor accordingly). - if (FILLPREFIXES) { + // For each prefix in `COLLECTEDPREFIXES`, check whether it occurs + // somewhere in the query and if so, add it before the first `SELECT` or + // `CONSTRUCT` (and move the cursor accordingly). If there is no `SELECT` + // or `CONSTRUCT`, do nothing. + let select_or_construct_regex = /(^| )(SELECT|CONSTRUCT)/mi; + if (FILLPREFIXES && select_or_construct_regex.test(editor.getValue())) { let queryString = editor.getValue(); let newCursor = editor.getCursor(); let linesAdded = 0; @@ -118,7 +120,7 @@ $(document).ready(function () { const fullPrefix = "PREFIX " + prefix + ": <" + COLLECTEDPREFIXES[prefix] + ">"; if (doesQueryFragmentContainPrefix(queryString, prefix) && queryString.indexOf(fullPrefix) == -1) { - queryString = queryString.replace(/(^| )(SELECT)/m, fullPrefix + "\n$1$2"); + queryString = queryString.replace(select_or_construct_regex, fullPrefix + "\n$1$2"); linesAdded += 1; } } @@ -180,7 +182,7 @@ $(document).ready(function () { for (var line of lines) { if (line.trim().startsWith("PREFIX")) { - var match = /PREFIX (.*): ?<(.*)>/g.exec(line.trim()); + var match = /PREFIX (.*): ?<(.*)>/gi.exec(line.trim()); if (match) { prefixes += line.trim() + '\n'; }