mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-23 20:55:41 +00:00
fix: clean up whitespace and formatting in linkAware functions fix rediff Feature/cmd click on links (#6132) * Allow ctrl/cmd + click to open URLs * fix for when user does cmd+tab, then comes back without it * refactored the community contribution to match Autocomplete's implementation * updated the code to resolve issues caused during merge conflict resolution with the use of makeLinkAware * fix: updated the code to use lodash's debounce and removed redundant undefined checks * fix: correct debouncing test expectation in linkAware.spec.js The test was incorrectly expecting 3 setTimeout calls when debouncing should only result in one active timeout. Updated the test to verify debouncing behavior correctly by checking that setTimeout is called with the correct delay, and that only one execution happens after the debounce delay. * fix: fixed merge issues in linkAware.js * fix: fixed CodeMirror assignment to this.editor * fix: formatting fixes * fix: formatting fix --------- Co-authored-by: abansal21 <37215457+abansal21@users.noreply.github.com> Co-authored-by: Chirag Chandrashekhar <chirag@usebruno.com>
330 lines
11 KiB
JavaScript
330 lines
11 KiB
JavaScript
/**
|
|
* Copyright (c) 2021 GraphQL Contributors.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
import React from 'react';
|
|
import { isEqual, escapeRegExp } from 'lodash';
|
|
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
|
import { setupAutoComplete } from 'utils/codemirror/autocomplete';
|
|
import StyledWrapper from './StyledWrapper';
|
|
import * as jsonlint from '@prantlf/jsonlint';
|
|
import { JSHINT } from 'jshint';
|
|
import stripJsonComments from 'strip-json-comments';
|
|
import { getAllVariables } from 'utils/collections';
|
|
import { setupLinkAware } from 'utils/codemirror/linkAware';
|
|
import CodeMirrorSearch from 'components/CodeMirrorSearch';
|
|
|
|
const CodeMirror = require('codemirror');
|
|
window.jsonlint = jsonlint;
|
|
window.JSHINT = JSHINT;
|
|
|
|
const TAB_SIZE = 2;
|
|
|
|
export default class CodeEditor extends React.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
// Keep a cached version of the value, this cache will be updated when the
|
|
// editor is updated, which can later be used to protect the editor from
|
|
// unnecessary updates during the update lifecycle.
|
|
this.cachedValue = props.value || '';
|
|
this.variables = {};
|
|
this.searchResultsCountElementId = 'search-results-count';
|
|
|
|
this.lintOptions = {
|
|
esversion: 11,
|
|
expr: true,
|
|
asi: true
|
|
};
|
|
|
|
this.state = {
|
|
searchBarVisible: false
|
|
};
|
|
}
|
|
|
|
componentDidMount() {
|
|
const variables = getAllVariables(this.props.collection, this.props.item);
|
|
|
|
const editor = (this.editor = CodeMirror(this._node, {
|
|
value: this.props.value || '',
|
|
lineNumbers: true,
|
|
lineWrapping: this.props.enableLineWrapping ?? true,
|
|
tabSize: TAB_SIZE,
|
|
mode: this.props.mode || 'application/ld+json',
|
|
brunoVarInfo: this.props.enableBrunoVarInfo !== false ? {
|
|
variables,
|
|
collection: this.props.collection,
|
|
item: this.props.item
|
|
} : false,
|
|
keyMap: 'sublime',
|
|
autoCloseBrackets: true,
|
|
matchBrackets: true,
|
|
showCursorWhenSelecting: true,
|
|
foldGutter: true,
|
|
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
|
|
lint: this.lintOptions,
|
|
readOnly: this.props.readOnly,
|
|
scrollbarStyle: 'overlay',
|
|
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
|
extraKeys: {
|
|
'Cmd-Enter': () => {
|
|
if (this.props.onRun) {
|
|
this.props.onRun();
|
|
}
|
|
},
|
|
'Ctrl-Enter': () => {
|
|
if (this.props.onRun) {
|
|
this.props.onRun();
|
|
}
|
|
},
|
|
'Cmd-S': () => {
|
|
if (this.props.onSave) {
|
|
this.props.onSave();
|
|
}
|
|
},
|
|
'Ctrl-S': () => {
|
|
if (this.props.onSave) {
|
|
this.props.onSave();
|
|
}
|
|
},
|
|
'Cmd-F': (cm) => {
|
|
if (!this.state.searchBarVisible) {
|
|
this.setState({ searchBarVisible: true });
|
|
}
|
|
},
|
|
'Ctrl-F': (cm) => {
|
|
if (!this.state.searchBarVisible) {
|
|
this.setState({ searchBarVisible: true });
|
|
}
|
|
},
|
|
'Cmd-H': 'replace',
|
|
'Ctrl-H': 'replace',
|
|
Tab: function (cm) {
|
|
cm.getSelection().includes('\n') || editor.getLine(cm.getCursor().line) == cm.getSelection()
|
|
? cm.execCommand('indentMore')
|
|
: cm.replaceSelection(' ', 'end');
|
|
},
|
|
'Shift-Tab': 'indentLess',
|
|
'Ctrl-Space': 'autocomplete',
|
|
'Cmd-Space': 'autocomplete',
|
|
'Ctrl-Y': 'foldAll',
|
|
'Cmd-Y': 'foldAll',
|
|
'Ctrl-I': 'unfoldAll',
|
|
'Cmd-I': 'unfoldAll',
|
|
'Ctrl-/': () => {
|
|
if (['application/ld+json', 'application/json'].includes(this.props.mode)) {
|
|
this.editor.toggleComment({ lineComment: '//', blockComment: '/*' });
|
|
} else {
|
|
this.editor.toggleComment();
|
|
}
|
|
},
|
|
'Cmd-/': () => {
|
|
if (['application/ld+json', 'application/json'].includes(this.props.mode)) {
|
|
this.editor.toggleComment({ lineComment: '//', blockComment: '/*' });
|
|
} else {
|
|
this.editor.toggleComment();
|
|
}
|
|
},
|
|
'Esc': () => {
|
|
if (this.state.searchBarVisible) {
|
|
this.setState({ searchBarVisible: false });
|
|
}
|
|
}
|
|
},
|
|
foldOptions: {
|
|
widget: (from, to) => {
|
|
var count = undefined;
|
|
var internal = this.editor.getRange(from, to);
|
|
if (this.props.mode == 'application/ld+json') {
|
|
if (this.editor.getLine(from.line).endsWith('[')) {
|
|
var toParse = '[' + internal + ']';
|
|
} else var toParse = '{' + internal + '}';
|
|
try {
|
|
count = Object.keys(JSON.parse(toParse)).length;
|
|
} catch (e) {}
|
|
} else if (this.props.mode == 'application/xml') {
|
|
var doc = new DOMParser();
|
|
try {
|
|
//add header element and remove prefix namespaces for DOMParser
|
|
var dcm = doc.parseFromString(
|
|
'<a> ' + internal.replace(/(?<=\<|<\/)\w+:/g, '') + '</a>',
|
|
'application/xml'
|
|
);
|
|
count = dcm.documentElement.children.length;
|
|
} catch (e) {}
|
|
}
|
|
return count ? `\u21A4${count}\u21A6` : '\u2194';
|
|
}
|
|
}
|
|
}));
|
|
CodeMirror.registerHelper('lint', 'json', function (text) {
|
|
let found = [];
|
|
if (!window.jsonlint) {
|
|
if (window.console) {
|
|
window.console.error('Error: window.jsonlint not defined, CodeMirror JSON linting cannot run.');
|
|
}
|
|
return found;
|
|
}
|
|
let jsonlint = window.jsonlint.parser || window.jsonlint;
|
|
try {
|
|
jsonlint.parse(stripJsonComments(text.replace(/(?<!"[^":{]*){{[^}]*}}(?![^"},]*")/g, '1')));
|
|
} catch (error) {
|
|
const { message, location } = error;
|
|
const line = location?.start?.line;
|
|
const column = location?.start?.column;
|
|
if (line && column) {
|
|
found.push({
|
|
from: CodeMirror.Pos(line - 1, column),
|
|
to: CodeMirror.Pos(line - 1, column),
|
|
message
|
|
});
|
|
}
|
|
}
|
|
return found;
|
|
});
|
|
|
|
if (editor) {
|
|
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
|
editor.on('change', this._onEdit);
|
|
editor.on('scroll', this.onScroll);
|
|
editor.scrollTo(null, this.props.initialScroll);
|
|
this.addOverlay();
|
|
|
|
const getAllVariablesHandler = () => getAllVariables(this.props.collection, this.props.item);
|
|
|
|
// Setup AutoComplete Helper for all modes
|
|
const autoCompleteOptions = {
|
|
showHintsFor: this.props.showHintsFor,
|
|
getAllVariables: getAllVariablesHandler
|
|
};
|
|
|
|
this.brunoAutoCompleteCleanup = setupAutoComplete(
|
|
editor,
|
|
autoCompleteOptions
|
|
);
|
|
|
|
setupLinkAware(editor);
|
|
}
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
// Ensure the changes caused by this update are not interpreted as
|
|
// user-input changes which could otherwise result in an infinite
|
|
// event loop.
|
|
this.ignoreChangeEvent = true;
|
|
if (this.props.schema !== prevProps.schema && this.editor) {
|
|
this.editor.options.lint.schema = this.props.schema;
|
|
this.editor.options.hintOptions.schema = this.props.schema;
|
|
this.editor.options.info.schema = this.props.schema;
|
|
this.editor.options.jump.schema = this.props.schema;
|
|
CodeMirror.signal(this.editor, 'change', this.editor);
|
|
}
|
|
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
|
this.cachedValue = this.props.value;
|
|
this.editor.setValue(this.props.value);
|
|
}
|
|
|
|
if (this.editor) {
|
|
let variables = getAllVariables(this.props.collection, this.props.item);
|
|
if (!isEqual(variables, this.variables)) {
|
|
this.addOverlay();
|
|
}
|
|
|
|
// Update collection and item when they change
|
|
if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) {
|
|
if (!isEqual(this.props.collection, this.editor.options.brunoVarInfo.collection)) {
|
|
this.editor.options.brunoVarInfo.collection = this.props.collection;
|
|
}
|
|
if (!isEqual(this.props.item, this.editor.options.brunoVarInfo.item)) {
|
|
this.editor.options.brunoVarInfo.item = this.props.item;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.props.theme !== prevProps.theme && this.editor) {
|
|
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
|
}
|
|
|
|
if (this.props.initialScroll !== prevProps.initialScroll) {
|
|
this.editor.scrollTo(null, this.props.initialScroll);
|
|
}
|
|
|
|
if (this.props.enableLineWrapping !== prevProps.enableLineWrapping) {
|
|
this.editor.setOption('lineWrapping', this.props.enableLineWrapping);
|
|
}
|
|
|
|
if (this.props.mode !== prevProps.mode) {
|
|
this.editor.setOption('mode', this.props.mode);
|
|
}
|
|
|
|
if (this.props.readOnly !== prevProps.readOnly && this.editor) {
|
|
this.editor.setOption('readOnly', this.props.readOnly);
|
|
}
|
|
|
|
this.ignoreChangeEvent = false;
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
if (this.editor) {
|
|
this.editor?._destroyLinkAware?.();
|
|
this.editor.off('change', this._onEdit);
|
|
this.editor.off('scroll', this.onScroll);
|
|
this.editor = null;
|
|
}
|
|
}
|
|
|
|
render() {
|
|
if (this.editor) {
|
|
this.editor.refresh();
|
|
}
|
|
return (
|
|
<StyledWrapper
|
|
className={`h-full w-full flex flex-col relative graphiql-container ${this.props.readOnly ? 'read-only' : ''}`}
|
|
aria-label="Code Editor"
|
|
font={this.props.font}
|
|
fontSize={this.props.fontSize}
|
|
>
|
|
<CodeMirrorSearch
|
|
visible={this.state.searchBarVisible}
|
|
editor={this.editor}
|
|
onClose={() => this.setState({ searchBarVisible: false })}
|
|
/>
|
|
<div
|
|
className={`editor-container${this.state.searchBarVisible ? ' search-bar-visible' : ''}`}
|
|
ref={(node) => { this._node = node; }}
|
|
style={{ height: '100%', width: '100%' }}
|
|
/>
|
|
</StyledWrapper>
|
|
);
|
|
}
|
|
|
|
addOverlay = () => {
|
|
const mode = this.props.mode || 'application/ld+json';
|
|
let variables = getAllVariables(this.props.collection, this.props.item);
|
|
this.variables = variables;
|
|
|
|
// Update brunoVarInfo with latest variables
|
|
if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) {
|
|
this.editor.options.brunoVarInfo.variables = variables;
|
|
}
|
|
|
|
defineCodeMirrorBrunoVariablesMode(variables, mode, false, this.props.enableVariableHighlighting);
|
|
this.editor.setOption('mode', 'brunovariables');
|
|
};
|
|
|
|
onScroll = (event) => this.props.onScroll?.(event);
|
|
|
|
_onEdit = () => {
|
|
if (!this.ignoreChangeEvent && this.editor) {
|
|
this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
|
this.cachedValue = this.editor.getValue();
|
|
if (this.props.onEdit) {
|
|
this.props.onEdit(this.cachedValue);
|
|
}
|
|
}
|
|
};
|
|
}
|