mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
317 lines
8.7 KiB
JavaScript
317 lines
8.7 KiB
JavaScript
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';
|
|
import { activeLineEffect, activeLineField } from './extensions/activeLineMarker';
|
|
import { clearBreakpoints, hasBreakpoint, toggleBreakpoint } from './extensions/breakpointGutter';
|
|
|
|
|
|
function getAutocompLoading({ bottom, left }, dom) {
|
|
const cmRect = dom.getBoundingClientRect();
|
|
const div = document.createElement('div');
|
|
div.classList.add('cm-tooltip', 'pg-autocomp-loader');
|
|
div.innerText = 'Loading...';
|
|
div.style.position = 'absolute';
|
|
div.style.top = (bottom - cmRect.top) + 'px';
|
|
div.style.left = (left - cmRect.left) + 'px';
|
|
dom?.appendChild(div);
|
|
return div;
|
|
}
|
|
|
|
export default class CustomEditorView extends EditorView {
|
|
constructor(...args) {
|
|
super(...args);
|
|
this._cleanDoc = this.state.doc;
|
|
}
|
|
|
|
getValue(tillCursor=false) {
|
|
if(tillCursor) {
|
|
return this.state.sliceDoc(0, this.state.selection.main.head);
|
|
}
|
|
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<origLine.to; startPos++) {
|
|
let node = tree.resolve(startPos);
|
|
if(node.type.name != 'Script') {
|
|
break;
|
|
}
|
|
const currChar = this.state.sliceDoc(startPos, startPos+1);
|
|
if(currChar == ' ' || currChar == '\t') {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let maxEndPos = this.state.doc.length;
|
|
let statementStartPos = -1;
|
|
let validTextFound = false;
|
|
|
|
// we'll go in reverse direction to get the start position.
|
|
while(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) {
|
|
// create a new doc with new value to make it clean
|
|
this._cleanDoc = EditorState.create({
|
|
doc: newValue
|
|
}).doc;
|
|
}
|
|
this.dispatch({
|
|
changes: { from: 0, to: this.getValue().length, insert: newValue }
|
|
});
|
|
}
|
|
|
|
getSelection() {
|
|
return this.state.sliceDoc(this.state.selection.main.from, this.state.selection.main.to) ?? '';
|
|
}
|
|
|
|
replaceSelection(newValue) {
|
|
this.dispatch(this.state.changeByRange(range => ({
|
|
changes: { from: range.from, to: range.to, insert: newValue },
|
|
range: EditorSelection.range(range.from, range.to)
|
|
})));
|
|
}
|
|
|
|
getCursor() {
|
|
let offset = this.state.selection.main.head;
|
|
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;
|
|
let pos = 0;
|
|
if(lineNo > this.state.doc.lines) {
|
|
pos = this.state.doc.length;
|
|
} else {
|
|
const line = this.state.doc.line(lineNo);
|
|
pos = line.from + ch;
|
|
if(ch == -1 || pos > line.to) {
|
|
pos = line.to;
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
|
|
getActiveLine() {
|
|
const activeLineChunk = this.state.field(activeLineField).chunkPos;
|
|
if(activeLineChunk.length > 0) {
|
|
return this.state.doc.lineAt(activeLineChunk[0]).number;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
hasBreakpoint(lineNo) {
|
|
const line = this.state.doc.line(lineNo);
|
|
return hasBreakpoint(this, line.from);
|
|
}
|
|
|
|
toggleBreakpoint(lineNo, silent, val) {
|
|
const line = this.state.doc.line(lineNo);
|
|
toggleBreakpoint(this, line.from, silent, val);
|
|
}
|
|
|
|
clearBreakpoints() {
|
|
clearBreakpoints(this);
|
|
}
|
|
|
|
markClean() {
|
|
this._cleanDoc = this.state.doc;
|
|
}
|
|
|
|
isDirty() {
|
|
return !this._cleanDoc.eq(this.state.doc);
|
|
}
|
|
|
|
fireDOMEvent(event) {
|
|
this.contentDOM.dispatchEvent(event);
|
|
}
|
|
|
|
execCommand(cmd) {
|
|
switch (cmd) {
|
|
case 'undo': undo(this);
|
|
break;
|
|
case 'indentMore': indentMore(this);
|
|
break;
|
|
case 'indentLess': indentLess(this);
|
|
break;
|
|
case 'toggleComment': toggleComment(this);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
registerAutocomplete(completionFunc) {
|
|
this.dispatch({
|
|
effects: StateEffect.appendConfig.of(
|
|
autocompletion({
|
|
override: [(context) => {
|
|
this.loadingDiv?.remove();
|
|
this.loadingDiv = getAutocompLoading(this.coordsAtPos(context.pos), this.dom);
|
|
context.addEventListener('abort', () => {
|
|
this.loadingDiv?.remove();
|
|
});
|
|
return Promise.resolve(completionFunc(context, () => {
|
|
this.loadingDiv?.remove();
|
|
}));
|
|
}]
|
|
}
|
|
))
|
|
});
|
|
}
|
|
|
|
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 }) });
|
|
}
|
|
|
|
setActiveLine(line) {
|
|
this.dispatch({ effects: activeLineEffect.of({ from: line, to: line }) });
|
|
}
|
|
}
|