From ee0687ecd36a5c361f47dc6f6d6615eee6827955 Mon Sep 17 00:00:00 2001 From: Aditya Toshniwal Date: Sat, 13 Apr 2024 15:47:18 +0530 Subject: [PATCH] Add a function getQueryAt to get the SQL query at given cursor position from a CodeMirror input --- .../ReactCodeMirror/CustomEditorView.js | 158 ++++++++++++++++-- 1 file changed, 148 insertions(+), 10 deletions(-) diff --git a/web/pgadmin/static/js/components/ReactCodeMirror/CustomEditorView.js b/web/pgadmin/static/js/components/ReactCodeMirror/CustomEditorView.js index e6c90ec6c..278a67a30 100644 --- a/web/pgadmin/static/js/components/ReactCodeMirror/CustomEditorView.js +++ b/web/pgadmin/static/js/components/ReactCodeMirror/CustomEditorView.js @@ -2,6 +2,7 @@ import { EditorView } from '@codemirror/view'; import { StateEffect, EditorState, EditorSelection } from '@codemirror/state'; +import { syntaxTree } from '@codemirror/language'; import { autocompletion } from '@codemirror/autocomplete'; import {undo, indentMore, indentLess, toggleComment} from '@codemirror/commands'; import { errorMarkerEffect } from './extensions/errorMarker'; @@ -33,7 +34,145 @@ export default class CustomEditorView extends EditorView { } return this.state.doc.toString(); } - + + /* Function to extract query based on position passed */ + getQueryAt(currPos) { + try { + if(typeof currPos == 'undefined') { + currPos = this.state.selection.main.head; + } + const tree = syntaxTree(this.state); + + let origLine = this.state.doc.lineAt(currPos); + let startPos = currPos; + + // Move the startPos a known node type or a space. + // We don't want to be in an unknown teritory + for(;startPos= 0) { + const currLine = this.state.doc.lineAt(startPos); + + // If empty line then start with prev line + // If empty line in between then that's it + if(currLine.text.trim() == '') { + if(origLine.number != currLine.number) { + startPos = currLine.to + 1; + break; + } + startPos = currLine.from - 1; + continue; + } + + // Script type doesn't give any info, better skip it. + const currChar = this.state.sliceDoc(startPos, startPos+1); + let node = tree.resolve(startPos); + if(node.type.name == 'Script' || (currChar == '\n')) { + startPos -= 1; + continue; + } + + // Skip the comments + if(node.type.name == 'LineComment' || node.type.name == 'BlockComment') { + startPos = node.from - 1; + // comments are valid text + validTextFound = true; + continue; + } + + // sometimes, node type is child of statement. + while(node.type.name != 'Statement' && node.parent) { + node = node.parent; + } + + // We already had found valid text + if(validTextFound) { + // continue till it reaches start so we can check for empty lines, etc. + if(statementStartPos > 0 && statementStartPos < startPos) { + startPos -= 1; + continue; + } + // don't go beyond this + startPos = node.to; + break; + } + + // statement found for the first time + if(node.type.name == 'Statement') { + statementStartPos = node.from; + maxEndPos = node.to; + + // if the statement is on the same line, jump to stmt start + if(node.from >= currLine.from) { + startPos = node.from; + } + } + + validTextFound = true; + startPos -= 1; + } + + // move forward from start position + let endPos = startPos+1; + maxEndPos = maxEndPos == -1 ? this.state.doc.length : maxEndPos; + while(endPos < maxEndPos) { + const currLine = this.state.doc.lineAt(endPos); + + // If empty line in between then that's it + if(currLine.text.trim() == '') { + break; + } + + let node = tree.resolve(endPos); + // Skip the comments + if(node.type.name == 'LineComment' || node.type.name == 'BlockComment') { + endPos = node.to + 1; + continue; + } + + // Skip any other types + if(node.type.name != 'Statement') { + endPos += 1; + continue; + } + + // can't go beyond a statement + if(node.type.name == 'Statement') { + maxEndPos = node.to; + } + + if(currLine.to < maxEndPos) { + endPos = currLine.to + 1; + } else { + endPos +=1; + } + } + + // make sure start and end are valid values; + if(startPos < 0) startPos = 0; + if(endPos > this.state.doc.length) endPos = this.state.doc.length; + + return this.state.sliceDoc(startPos, endPos).trim(); + } catch (error) { + console.error(error); + return this.getValue(); + } + } + setValue(newValue, markClean=false) { newValue = newValue || ''; if(markClean) { @@ -46,7 +185,7 @@ export default class CustomEditorView extends EditorView { changes: { from: 0, to: this.getValue().length, insert: newValue } }); } - + getSelection() { return this.state.sliceDoc(this.state.selection.main.from, this.state.selection.main.to) ?? ''; } @@ -63,7 +202,7 @@ export default class CustomEditorView extends EditorView { let line = this.state.doc.lineAt(offset); return {line: line.number, ch: offset - line.from}; } - + setCursor(lineNo, ch) { // line is 1-based; // ch is 0-based; @@ -79,15 +218,15 @@ export default class CustomEditorView extends EditorView { } this.dispatch({ selection: { anchor: pos, head: pos } }); } - + getCurrentLineNo() { return this.state.doc.lineAt(this.state.selection.main.head).number; } - + lineCount() { return this.state.doc.lines; } - + getLine(lineNo) { // line is 1-based; return this.state.doc.line(lineNo).text; @@ -141,7 +280,7 @@ export default class CustomEditorView extends EditorView { break; } } - + registerAutocomplete(completionFunc) { this.dispatch({ effects: StateEffect.appendConfig.of( @@ -160,13 +299,13 @@ export default class CustomEditorView extends EditorView { )) }); } - + setErrorMark(fromCursor, toCursor) { const from = this.state.doc.line(fromCursor.line).from + fromCursor.pos; const to = this.state.doc.line(toCursor.line).from + toCursor.pos; this.dispatch({ effects: errorMarkerEffect.of({ from, to }) }); } - + removeErrorMark() { this.dispatch({ effects: errorMarkerEffect.of({ clear: true }) }); } @@ -175,4 +314,3 @@ export default class CustomEditorView extends EditorView { this.dispatch({ effects: activeLineEffect.of({ from: line, to: line }) }); } } - \ No newline at end of file