mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-23 12:45:38 +00:00
codemirror api/variables autocomplete refactor
This commit is contained in:
@@ -8,123 +8,19 @@
|
||||
import React from 'react';
|
||||
import { isEqual, escapeRegExp } from 'lodash';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import { getMockDataHints } from 'utils/codemirror/mock-data-hints';
|
||||
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';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
const CodeMirror = require('codemirror');
|
||||
window.jsonlint = jsonlint;
|
||||
window.JSHINT = JSHINT;
|
||||
|
||||
const TAB_SIZE = 2;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
window.jsonlint = jsonlint;
|
||||
window.JSHINT = JSHINT;
|
||||
//This should be done dynamically if possible
|
||||
const hintWords = [
|
||||
'res',
|
||||
'res.status',
|
||||
'res.statusText',
|
||||
'res.headers',
|
||||
'res.body',
|
||||
'res.responseTime',
|
||||
'res.getStatus()',
|
||||
'res.getStatusText()',
|
||||
'res.getHeader(name)',
|
||||
'res.getHeaders()',
|
||||
'res.getBody()',
|
||||
'res.setBody(data)',
|
||||
'res.getResponseTime()',
|
||||
'req',
|
||||
'req.url',
|
||||
'req.method',
|
||||
'req.headers',
|
||||
'req.body',
|
||||
'req.timeout',
|
||||
'req.getUrl()',
|
||||
'req.setUrl(url)',
|
||||
'req.getMethod()',
|
||||
'req.getAuthMode()',
|
||||
'req.setMethod(method)',
|
||||
'req.getHeader(name)',
|
||||
'req.getHeaders()',
|
||||
'req.setHeader(name, value)',
|
||||
'req.setHeaders(data)',
|
||||
'req.getBody()',
|
||||
'req.setBody(data)',
|
||||
'req.setMaxRedirects(maxRedirects)',
|
||||
'req.getTimeout()',
|
||||
'req.setTimeout(timeout)',
|
||||
'req.getExecutionMode()',
|
||||
'req.getName()',
|
||||
'bru',
|
||||
'bru.cwd()',
|
||||
'bru.getEnvName()',
|
||||
'bru.getProcessEnv(key)',
|
||||
'bru.hasEnvVar(key)',
|
||||
'bru.getEnvVar(key)',
|
||||
'bru.getFolderVar(key)',
|
||||
'bru.getCollectionVar(key)',
|
||||
'bru.setEnvVar(key,value)',
|
||||
'bru.deleteEnvVar(key)',
|
||||
'bru.hasVar(key)',
|
||||
'bru.getVar(key)',
|
||||
'bru.setVar(key,value)',
|
||||
'bru.deleteVar(key)',
|
||||
'bru.deleteAllVars()',
|
||||
'bru.setNextRequest(requestName)',
|
||||
'req.disableParsingResponseJson()',
|
||||
'bru.getRequestVar(key)',
|
||||
'bru.runRequest(requestPathName)',
|
||||
'bru.getAssertionResults()',
|
||||
'bru.getTestResults()',
|
||||
'bru.sleep(ms)',
|
||||
'bru.getCollectionName()',
|
||||
'bru.getGlobalEnvVar(key)',
|
||||
'bru.setGlobalEnvVar(key, value)',
|
||||
'bru.runner',
|
||||
'bru.runner.setNextRequest(requestName)',
|
||||
'bru.runner.skipRequest()',
|
||||
'bru.runner.stopExecution()',
|
||||
'bru.interpolate(str)'
|
||||
];
|
||||
|
||||
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
|
||||
const cursor = editor.getCursor();
|
||||
const currentLine = editor.getLine(cursor.line);
|
||||
let startBru = cursor.ch;
|
||||
let endBru = startBru;
|
||||
while (endBru < currentLine.length && /[\w.]/.test(currentLine.charAt(endBru))) ++endBru;
|
||||
while (startBru && /[\w.]/.test(currentLine.charAt(startBru - 1))) --startBru;
|
||||
let curWordBru = startBru != endBru && currentLine.slice(startBru, endBru);
|
||||
|
||||
let start = cursor.ch;
|
||||
let end = start;
|
||||
while (end < currentLine.length && /[\w]/.test(currentLine.charAt(end))) ++end;
|
||||
while (start && /[\w]/.test(currentLine.charAt(start - 1))) --start;
|
||||
const jsHinter = CodeMirror.hint.javascript;
|
||||
let result = jsHinter(editor) || { list: [] };
|
||||
result.to = CodeMirror.Pos(cursor.line, end);
|
||||
result.from = CodeMirror.Pos(cursor.line, start);
|
||||
if (curWordBru) {
|
||||
hintWords.forEach((h) => {
|
||||
if (h.includes('.') == curWordBru.includes('.') && h.startsWith(curWordBru)) {
|
||||
result.list.push(curWordBru.includes('.') ? h.split('.')?.at(-1) : h);
|
||||
}
|
||||
});
|
||||
result.list?.sort();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
CodeMirror.commands.autocomplete = (cm, hint, options) => {
|
||||
cm.showHint({ hint, ...options });
|
||||
};
|
||||
}
|
||||
|
||||
export default class CodeEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -290,52 +186,21 @@ export default class CodeEditor extends React.Component {
|
||||
if (editor) {
|
||||
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
||||
editor.on('change', this._onEdit);
|
||||
editor.on('keyup', this._onKeyUpMockDataHints);
|
||||
this.addOverlay();
|
||||
}
|
||||
|
||||
if (this.props.mode == 'javascript') {
|
||||
editor.on('keyup', function (cm, event) {
|
||||
const cursor = editor.getCursor();
|
||||
const currentLine = editor.getLine(cursor.line);
|
||||
let start = cursor.ch;
|
||||
let end = start;
|
||||
while (end < currentLine.length && /[^{}();\s\[\]\,]/.test(currentLine.charAt(end))) ++end;
|
||||
while (start && /[^{}();\s\[\]\,]/.test(currentLine.charAt(start - 1))) --start;
|
||||
let curWord = start != end && currentLine.slice(start, end);
|
||||
// Qualify if autocomplete will be shown
|
||||
if (
|
||||
/^(?!Shift|Tab|Enter|Escape|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Meta|Alt|Home|End\s)\w*/.test(event.key) &&
|
||||
curWord.length > 0 &&
|
||||
!/\/\/|\/\*|.*{{|`[^$]*{|`[^{]*$/.test(currentLine.slice(0, end)) &&
|
||||
/(?<!\d)[a-zA-Z\._]$/.test(curWord)
|
||||
) {
|
||||
CodeMirror.commands.autocomplete(cm, CodeMirror.hint.brunoJS, { completeSingle: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Setup AutoComplete Helper for all modes
|
||||
const autoCompleteOptions = {
|
||||
showHintsFor: this.props.showHintsFor
|
||||
};
|
||||
|
||||
_onKeyUpMockDataHints(cm, event) {
|
||||
// This prevents triggering hints for non-character keys (e.g., Arrow keys, Meta).
|
||||
if (
|
||||
!/^(?!Shift|Tab|Enter|Escape|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Meta|Alt|Home|End\s)\w*/.test(event?.key)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const getVariables = () => getAllVariables(this.props.collection, this.props.item);
|
||||
|
||||
const hints = getMockDataHints(cm);
|
||||
if (!hints) {
|
||||
if (cm.state.completionActive) {
|
||||
cm.state.completionActive.close();
|
||||
}
|
||||
return;
|
||||
this.brunoAutoCompleteCleanup = setupAutoComplete(
|
||||
editor,
|
||||
getVariables,
|
||||
autoCompleteOptions
|
||||
);
|
||||
}
|
||||
|
||||
cm.showHint({
|
||||
hint: () => hints,
|
||||
completeSingle: false
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@@ -371,11 +236,13 @@ export default class CodeEditor extends React.Component {
|
||||
componentWillUnmount() {
|
||||
if (this.editor) {
|
||||
this.editor.off('change', this._onEdit);
|
||||
this.editor.off('keyup', this._onKeyUpMockDataHints);
|
||||
this.editor = null;
|
||||
}
|
||||
|
||||
this._unbindSearchHandler();
|
||||
if (this.brunoAutoCompleteCleanup) {
|
||||
this.brunoAutoCompleteCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -41,65 +41,11 @@ const setupEditorWithRef = () => {
|
||||
return { ref, rerender };
|
||||
};
|
||||
|
||||
describe('CodeEditor Autocomplete', () => {
|
||||
describe('CodeEditor', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
it('shows autocomplete suggestions when typing {{$ra', () => {
|
||||
// Setup
|
||||
const { ref } = setupEditorWithRef();
|
||||
const editorInstance = ref.current;
|
||||
expect(editorInstance).toBeTruthy();
|
||||
|
||||
const editor = editorInstance.editor;
|
||||
expect(editor).toBeTruthy();
|
||||
|
||||
// Configure editor state
|
||||
setupEditorState(editor, {
|
||||
value: '{{$r',
|
||||
cursorPosition: 4
|
||||
});
|
||||
|
||||
// Trigger autocomplete
|
||||
const _onKeyUpMockDataHints = editor._onKeyUpMockDataHints;
|
||||
expect(typeof _onKeyUpMockDataHints).toBe('function');
|
||||
|
||||
act(() => {
|
||||
_onKeyUpMockDataHints(editor, { text: ['a'], origin: '+input' });
|
||||
});
|
||||
|
||||
// Assertions
|
||||
expect(editor.showHint).toHaveBeenCalled();
|
||||
const call = editor.showHint.mock.calls[0][0];
|
||||
expect(typeof call.hint).toBe('function');
|
||||
|
||||
const hints = call.hint();
|
||||
expect(Array.isArray(hints.list)).toBe(true);
|
||||
expect(hints.list.some((s) => s.startsWith('$'))).toBe(true);
|
||||
expect(hints.list.every((match) => match.startsWith('$ra'))).toBe(true);
|
||||
});
|
||||
|
||||
it('does not show hints for regular text input', () => {
|
||||
// Setup
|
||||
const { ref } = setupEditorWithRef();
|
||||
const editor = ref.current.editor;
|
||||
|
||||
// Configure editor state
|
||||
setupEditorState(editor, {
|
||||
value: 'regular text',
|
||||
cursorPosition: 11
|
||||
});
|
||||
|
||||
// Trigger input
|
||||
const _onKeyUpMockDataHints = editor._onKeyUpMockDataHints;
|
||||
|
||||
act(() => {
|
||||
_onKeyUpMockDataHints(editor, { text: ['x'], origin: '+input' });
|
||||
});
|
||||
|
||||
// Assert no hints shown for regular text
|
||||
expect(editor.showHint).not.toHaveBeenCalled();
|
||||
});
|
||||
it("add CodeEditor related tests here", () => {});
|
||||
});
|
||||
@@ -53,6 +53,7 @@ const Script = ({ collection }) => {
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'bru']}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 mt-6">
|
||||
@@ -66,6 +67,7 @@ const Script = ({ collection }) => {
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ const Tests = ({ collection }) => {
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
|
||||
<div className="mt-6">
|
||||
|
||||
@@ -9,6 +9,7 @@ import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions'
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
const Headers = ({ collection, folder }) => {
|
||||
@@ -117,6 +118,7 @@ const Headers = ({ collection, folder }) => {
|
||||
}
|
||||
collection={collection}
|
||||
item={folder}
|
||||
autocomplete={MimeTypes}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -55,6 +55,7 @@ const Script = ({ collection, folder }) => {
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'bru']}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col flex-1 mt-2 gap-y-2">
|
||||
@@ -68,6 +69,7 @@ const Script = ({ collection, folder }) => {
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ const Tests = ({ collection, folder }) => {
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
|
||||
<div className="mt-6">
|
||||
|
||||
@@ -2,14 +2,10 @@ import React, { Component } from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { getAllVariables } from 'utils/collections';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import { setupAutoComplete } from 'utils/codemirror/autocomplete';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
}
|
||||
const CodeMirror = require('codemirror');
|
||||
|
||||
class MultiLineEditor extends Component {
|
||||
constructor(props) {
|
||||
@@ -78,14 +74,21 @@ class MultiLineEditor extends Component {
|
||||
'Shift-Tab': false
|
||||
}
|
||||
});
|
||||
if (this.props.autocomplete) {
|
||||
this.editor.on('keyup', (cm, event) => {
|
||||
if (!cm.state.completionActive /*Enables keyboard navigation in autocomplete list*/ && event.keyCode != 13) {
|
||||
/*Enter - do not open autocomplete list just after item has been selected in it*/
|
||||
CodeMirror.commands.autocomplete(cm, CodeMirror.hint.anyword, { autocomplete: this.props.autocomplete });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Setup AutoComplete Helper
|
||||
const autoCompleteOptions = {
|
||||
showHintsFor: ['variables'],
|
||||
anywordAutocompleteHints: this.props.autocomplete
|
||||
};
|
||||
|
||||
const getVariables = () => getAllVariables(this.props.collection, this.props.item);
|
||||
|
||||
this.brunoAutoCompleteCleanup = setupAutoComplete(
|
||||
this.editor,
|
||||
getVariables,
|
||||
autoCompleteOptions
|
||||
);
|
||||
|
||||
this.editor.setValue(String(this.props.value) || '');
|
||||
this.editor.on('change', this._onEdit);
|
||||
this.addOverlay(variables);
|
||||
@@ -125,6 +128,9 @@ class MultiLineEditor extends Component {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.brunoAutoCompleteCleanup) {
|
||||
this.brunoAutoCompleteCleanup();
|
||||
}
|
||||
this.editor.getWrapperElement().remove();
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
enableVariableHighlighting={true}
|
||||
showHintsFor={['variables']}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -18,12 +18,7 @@ import { IconWand } from '@tabler/icons';
|
||||
|
||||
import onHasCompletion from './onHasCompletion';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
}
|
||||
const CodeMirror = require('codemirror');
|
||||
|
||||
const md = new MD();
|
||||
const AUTO_COMPLETE_AFTER_KEY = /^[a-zA-Z0-9_@(]$/;
|
||||
|
||||
@@ -59,6 +59,7 @@ const RequestBody = ({ item, collection }) => {
|
||||
onSave={onSave}
|
||||
mode={codeMirrorMode[bodyMode]}
|
||||
enableVariableHighlighting={true}
|
||||
showHintsFor={['variables']}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -52,6 +52,7 @@ const Script = ({ item, collection }) => {
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
showHintsFor={['req', 'bru']}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col flex-1 mt-2 gap-y-2">
|
||||
@@ -66,6 +67,7 @@ const Script = ({ item, collection }) => {
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -37,6 +37,7 @@ const Tests = ({ item, collection }) => {
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -68,6 +68,7 @@ const CodeView = ({ language, item }) => {
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
theme={displayedTheme}
|
||||
mode={lang}
|
||||
showHintsFor={['variables']}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
</>
|
||||
|
||||
@@ -2,16 +2,11 @@ import React, { Component } from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { getAllVariables } from 'utils/collections';
|
||||
import { defineCodeMirrorBrunoVariablesMode, MaskedEditor } from 'utils/common/codemirror';
|
||||
import { getMockDataHints } from 'utils/codemirror/mock-data-hints';
|
||||
import { setupAutoComplete } from 'utils/codemirror/autocomplete';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconEye, IconEyeOff } from '@tabler/icons';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
}
|
||||
const CodeMirror = require('codemirror');
|
||||
|
||||
class SingleLineEditor extends Component {
|
||||
constructor(props) {
|
||||
@@ -78,16 +73,20 @@ class SingleLineEditor extends Component {
|
||||
}
|
||||
});
|
||||
|
||||
if (this.props.autocomplete) {
|
||||
this.editor.on('keyup', (cm, event) => {
|
||||
if (!cm.state.completionActive /*Enables keyboard navigation in autocomplete list*/ && event.key !== 'Enter') {
|
||||
/*Enter - do not open autocomplete list just after item has been selected in it*/
|
||||
CodeMirror.commands.autocomplete(cm, CodeMirror.hint.anyword, { autocomplete: this.props.autocomplete });
|
||||
}
|
||||
});
|
||||
}
|
||||
this.editor.on('keyup', this._onKeyUpMockDataHints);
|
||||
// Setup AutoComplete Helper
|
||||
const autoCompleteOptions = {
|
||||
showHintsFor: ['variables'],
|
||||
anywordAutocompleteHints: this.props.autocomplete
|
||||
};
|
||||
|
||||
const getVariables = () => getAllVariables(this.props.collection, this.props.item);
|
||||
|
||||
this.brunoAutoCompleteCleanup = setupAutoComplete(
|
||||
this.editor,
|
||||
getVariables,
|
||||
autoCompleteOptions
|
||||
);
|
||||
|
||||
this.editor.setValue(String(this.props.value ?? ''));
|
||||
this.editor.on('change', this._onEdit);
|
||||
this.addOverlay(variables);
|
||||
@@ -117,26 +116,6 @@ class SingleLineEditor extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
_onKeyUpMockDataHints(cm, event) {
|
||||
// This prevents triggering hints for non-character keys (e.g., Arrow keys, Meta).
|
||||
if (!/^(?!Shift|Tab|Enter|Escape|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Meta|Alt|Home|End\s)\w*/.test(event?.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hints = getMockDataHints(cm);
|
||||
if (!hints) {
|
||||
if (cm.state.completionActive) {
|
||||
cm.state.completionActive.close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
cm.showHint({
|
||||
hint: () => hints,
|
||||
completeSingle: false
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
// Ensure the changes caused by this update are not interpreted as
|
||||
// user-input changes which could otherwise result in an infinite
|
||||
@@ -167,10 +146,12 @@ class SingleLineEditor extends Component {
|
||||
componentWillUnmount() {
|
||||
if (this.editor) {
|
||||
this.editor.off('change', this._onEdit);
|
||||
this.editor.off('keyup', this._onKeyUpMockDataHints);
|
||||
this.editor.getWrapperElement().remove();
|
||||
this.editor = null;
|
||||
}
|
||||
if (this.brunoAutoCompleteCleanup) {
|
||||
this.brunoAutoCompleteCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
addOverlay = (variables) => {
|
||||
|
||||
@@ -10,40 +10,37 @@ import 'codemirror/theme/material.css';
|
||||
import 'codemirror/theme/monokai.css';
|
||||
import 'codemirror/addon/scroll/simplescrollbars.css';
|
||||
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
if (!SERVER_RENDERED) {
|
||||
require('codemirror/mode/javascript/javascript');
|
||||
require('codemirror/mode/xml/xml');
|
||||
require('codemirror/mode/sparql/sparql');
|
||||
require('codemirror/addon/comment/comment');
|
||||
require('codemirror/addon/dialog/dialog');
|
||||
require('codemirror/addon/edit/closebrackets');
|
||||
require('codemirror/addon/edit/matchbrackets');
|
||||
require('codemirror/addon/fold/brace-fold');
|
||||
require('codemirror/addon/fold/foldgutter');
|
||||
require('codemirror/addon/fold/xml-fold');
|
||||
require('codemirror/addon/hint/javascript-hint');
|
||||
require('codemirror/addon/hint/show-hint');
|
||||
require('codemirror/addon/lint/lint');
|
||||
require('codemirror/addon/lint/json-lint');
|
||||
require('codemirror/addon/mode/overlay');
|
||||
require('codemirror/addon/scroll/simplescrollbars');
|
||||
require('codemirror/addon/search/jump-to-line');
|
||||
require('codemirror/addon/search/search');
|
||||
require('codemirror/addon/search/searchcursor');
|
||||
require('codemirror/addon/display/placeholder');
|
||||
require('codemirror/keymap/sublime');
|
||||
require('codemirror/mode/javascript/javascript');
|
||||
require('codemirror/mode/xml/xml');
|
||||
require('codemirror/mode/sparql/sparql');
|
||||
require('codemirror/addon/comment/comment');
|
||||
require('codemirror/addon/dialog/dialog');
|
||||
require('codemirror/addon/edit/closebrackets');
|
||||
require('codemirror/addon/edit/matchbrackets');
|
||||
require('codemirror/addon/fold/brace-fold');
|
||||
require('codemirror/addon/fold/foldgutter');
|
||||
require('codemirror/addon/fold/xml-fold');
|
||||
require('codemirror/addon/hint/javascript-hint');
|
||||
require('codemirror/addon/hint/show-hint');
|
||||
require('codemirror/addon/lint/lint');
|
||||
require('codemirror/addon/lint/json-lint');
|
||||
require('codemirror/addon/mode/overlay');
|
||||
require('codemirror/addon/scroll/simplescrollbars');
|
||||
require('codemirror/addon/search/jump-to-line');
|
||||
require('codemirror/addon/search/search');
|
||||
require('codemirror/addon/search/searchcursor');
|
||||
require('codemirror/addon/display/placeholder');
|
||||
require('codemirror/keymap/sublime');
|
||||
|
||||
require('codemirror-graphql/hint');
|
||||
require('codemirror-graphql/info');
|
||||
require('codemirror-graphql/jump');
|
||||
require('codemirror-graphql/lint');
|
||||
require('codemirror-graphql/mode');
|
||||
require('codemirror-graphql/hint');
|
||||
require('codemirror-graphql/info');
|
||||
require('codemirror-graphql/jump');
|
||||
require('codemirror-graphql/lint');
|
||||
require('codemirror-graphql/mode');
|
||||
|
||||
require('utils/codemirror/brunoVarInfo');
|
||||
require('utils/codemirror/javascript-lint');
|
||||
require('utils/codemirror/autocomplete');
|
||||
}
|
||||
require('utils/codemirror/brunoVarInfo');
|
||||
require('utils/codemirror/javascript-lint');
|
||||
require('utils/codemirror/autocomplete');
|
||||
|
||||
export default function Main() {
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
|
||||
@@ -1,40 +1,589 @@
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
import { mockDataFunctions } from '@usebruno/common';
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
CodeMirror.registerHelper('hint', 'anyword', (editor, options) => {
|
||||
const word = /[\w$-]+/;
|
||||
const wordlist = (options && options.autocomplete) || [];
|
||||
let cur = editor.getCursor(),
|
||||
curLine = editor.getLine(cur.line);
|
||||
let end = cur.ch,
|
||||
start = end;
|
||||
while (start && word.test(curLine.charAt(start - 1))) --start;
|
||||
let curWord = start != end && curLine.slice(start, end);
|
||||
const CodeMirror = require('codemirror');
|
||||
|
||||
// Check if curWord is a valid string before proceeding
|
||||
if (typeof curWord !== 'string' || curWord.length < 3) {
|
||||
return null; // Abort the hint
|
||||
// Static API hints - Bruno JavaScript API (subgrouped by category)
|
||||
const STATIC_API_HINTS = {
|
||||
req: [
|
||||
'req',
|
||||
'req.url',
|
||||
'req.method',
|
||||
'req.headers',
|
||||
'req.body',
|
||||
'req.timeout',
|
||||
'req.getUrl()',
|
||||
'req.setUrl(url)',
|
||||
'req.getMethod()',
|
||||
'req.getAuthMode()',
|
||||
'req.setMethod(method)',
|
||||
'req.getHeader(name)',
|
||||
'req.getHeaders()',
|
||||
'req.setHeader(name, value)',
|
||||
'req.setHeaders(data)',
|
||||
'req.getBody()',
|
||||
'req.setBody(data)',
|
||||
'req.setMaxRedirects(maxRedirects)',
|
||||
'req.getTimeout()',
|
||||
'req.setTimeout(timeout)',
|
||||
'req.getExecutionMode()',
|
||||
'req.getName()',
|
||||
'req.disableParsingResponseJson()'
|
||||
],
|
||||
res: [
|
||||
'res',
|
||||
'res.status',
|
||||
'res.statusText',
|
||||
'res.headers',
|
||||
'res.body',
|
||||
'res.responseTime',
|
||||
'res.getStatus()',
|
||||
'res.getStatusText()',
|
||||
'res.getHeader(name)',
|
||||
'res.getHeaders()',
|
||||
'res.getBody()',
|
||||
'res.setBody(data)',
|
||||
'res.getResponseTime()'
|
||||
],
|
||||
bru: [
|
||||
'bru',
|
||||
'bru.cwd()',
|
||||
'bru.getEnvName()',
|
||||
'bru.getProcessEnv(key)',
|
||||
'bru.hasEnvVar(key)',
|
||||
'bru.getEnvVar(key)',
|
||||
'bru.getFolderVar(key)',
|
||||
'bru.getCollectionVar(key)',
|
||||
'bru.setEnvVar(key,value)',
|
||||
'bru.deleteEnvVar(key)',
|
||||
'bru.hasVar(key)',
|
||||
'bru.getVar(key)',
|
||||
'bru.setVar(key,value)',
|
||||
'bru.deleteVar(key)',
|
||||
'bru.deleteAllVars()',
|
||||
'bru.setNextRequest(requestName)',
|
||||
'bru.getRequestVar(key)',
|
||||
'bru.runRequest(requestPathName)',
|
||||
'bru.getAssertionResults()',
|
||||
'bru.getTestResults()',
|
||||
'bru.sleep(ms)',
|
||||
'bru.getCollectionName()',
|
||||
'bru.getGlobalEnvVar(key)',
|
||||
'bru.setGlobalEnvVar(key, value)',
|
||||
'bru.runner',
|
||||
'bru.runner.setNextRequest(requestName)',
|
||||
'bru.runner.skipRequest()',
|
||||
'bru.runner.stopExecution()',
|
||||
'bru.interpolate(str)'
|
||||
]
|
||||
};
|
||||
|
||||
// Mock data functions - prefixed with $
|
||||
const MOCK_DATA_HINTS = Object.keys(mockDataFunctions).map(key => `$${key}`);
|
||||
|
||||
// Constants for word pattern matching
|
||||
const WORD_PATTERN = /[\w.$-/]/;
|
||||
const VARIABLE_PATTERN = /\{\{([\w$.-]*)$/;
|
||||
const NON_CHARACTER_KEYS = /^(?!Shift|Tab|Enter|Escape|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Meta|Alt|Home|End\s)\w*/;
|
||||
|
||||
/**
|
||||
* Generate progressive hints for a given full hint
|
||||
* @param {string} fullHint - The complete hint string
|
||||
* @returns {string[]} Array of progressive hints
|
||||
*/
|
||||
const generateProgressiveHints = (fullHint) => {
|
||||
const parts = fullHint.split('.');
|
||||
const progressiveHints = [];
|
||||
|
||||
for (let i = 1; i <= parts.length; i++) {
|
||||
progressiveHints.push(parts.slice(0, i).join('.'));
|
||||
}
|
||||
|
||||
return progressiveHints;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a variable key should be skipped
|
||||
* @param {string} key - The variable key to check
|
||||
* @returns {boolean} True if the key should be skipped
|
||||
*/
|
||||
const shouldSkipVariableKey = (key) => {
|
||||
return key === 'pathParams' || key === 'maskedEnvVariables' || key === 'process';
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform variables object into flat hint list
|
||||
* @param {Object} allVariables - All available variables
|
||||
* @returns {string[]} Array of variable hints
|
||||
*/
|
||||
const transformVariablesToHints = (allVariables = {}) => {
|
||||
const hints = [];
|
||||
|
||||
// Process all variables without type-specific handling
|
||||
Object.keys(allVariables).forEach(key => {
|
||||
if (!shouldSkipVariableKey(key)) {
|
||||
hints.push(key);
|
||||
}
|
||||
|
||||
const list = (options && options.list) || [];
|
||||
const re = new RegExp(word.source, 'g');
|
||||
for (let dir = -1; dir <= 1; dir += 2) {
|
||||
let line = cur.line,
|
||||
endLine = Math.min(Math.max(line + dir * 500, editor.firstLine()), editor.lastLine()) + dir;
|
||||
for (; line != endLine; line += dir) {
|
||||
let text = editor.getLine(line),
|
||||
m;
|
||||
while ((m = re.exec(text))) {
|
||||
if (line == cur.line && curWord.length < 3) continue;
|
||||
list.push(...wordlist.filter((el) => el.toLowerCase().startsWith(curWord.toLowerCase())));
|
||||
}
|
||||
}
|
||||
}
|
||||
return { list: [...new Set(list)], from: CodeMirror.Pos(cur.line, start), to: CodeMirror.Pos(cur.line, end) };
|
||||
});
|
||||
|
||||
// Handle process environment variables
|
||||
if (allVariables.process && allVariables.process.env) {
|
||||
Object.keys(allVariables.process.env).forEach(key => {
|
||||
hints.push(`process.env.${key}`);
|
||||
});
|
||||
}
|
||||
|
||||
return hints;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add API hints to categorized hints based on showHintsFor configuration
|
||||
* @param {Set} apiHints - Set to add API hints to
|
||||
* @param {string[]} showHintsFor - Array of hint types to show
|
||||
*/
|
||||
const addApiHintsToSet = (apiHints, showHintsFor) => {
|
||||
const apiTypes = ['req', 'res', 'bru'];
|
||||
|
||||
apiTypes.forEach(apiType => {
|
||||
if (showHintsFor.includes(apiType)) {
|
||||
STATIC_API_HINTS[apiType].forEach(hint => {
|
||||
generateProgressiveHints(hint).forEach(h => apiHints.add(h));
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add variable hints to categorized hints
|
||||
* @param {Set} variableHints - Set to add variable hints to
|
||||
* @param {Object} allVariables - All available variables
|
||||
*/
|
||||
const addVariableHintsToSet = (variableHints, allVariables) => {
|
||||
// Add mock data hints
|
||||
MOCK_DATA_HINTS.forEach(hint => {
|
||||
generateProgressiveHints(hint).forEach(h => variableHints.add(h));
|
||||
});
|
||||
|
||||
// Add variable hints with progressive hints
|
||||
const variableHintsList = transformVariablesToHints(allVariables);
|
||||
variableHintsList.forEach(hint => {
|
||||
generateProgressiveHints(hint).forEach(h => variableHints.add(h));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add custom hints to categorized hints
|
||||
* @param {Set} anywordHints - Set to add custom hints to
|
||||
* @param {Object} options - Options containing custom hints
|
||||
*/
|
||||
const addCustomHintsToSet = (anywordHints, options) => {
|
||||
if (options.anywordAutocompleteHints && Array.isArray(options.anywordAutocompleteHints)) {
|
||||
options.anywordAutocompleteHints.forEach(hint => {
|
||||
generateProgressiveHints(hint).forEach(h => anywordHints.add(h));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Build categorized hints list from all sources
|
||||
* @param {Object} allVariables - All available variables
|
||||
* @param {Object} options - Configuration options
|
||||
* @returns {Object} Categorized hints object
|
||||
*/
|
||||
const buildCategorizedHintsList = (allVariables = {}, options = {}) => {
|
||||
const categorizedHints = {
|
||||
api: new Set(),
|
||||
variables: new Set(),
|
||||
anyword: new Set()
|
||||
};
|
||||
|
||||
const showHintsFor = options.showHintsFor || [];
|
||||
|
||||
// Add different types of hints
|
||||
addApiHintsToSet(categorizedHints.api, showHintsFor);
|
||||
addVariableHintsToSet(categorizedHints.variables, allVariables);
|
||||
addCustomHintsToSet(categorizedHints.anyword, options);
|
||||
|
||||
return {
|
||||
api: Array.from(categorizedHints.api).sort(),
|
||||
variables: Array.from(categorizedHints.variables).sort(),
|
||||
anyword: Array.from(categorizedHints.anyword).sort()
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate replacement positions for variable context
|
||||
* @param {Object} cursor - Current cursor position
|
||||
* @param {Object} startPos - Start position of variable
|
||||
* @param {string} wordMatch - The matched word
|
||||
* @returns {Object} From and to positions for replacement
|
||||
*/
|
||||
const calculateVariableReplacementPositions = (cursor, startPos, wordMatch) => {
|
||||
let replaceFrom, replaceTo;
|
||||
|
||||
if (wordMatch.endsWith('.')) {
|
||||
replaceFrom = cursor;
|
||||
replaceTo = cursor;
|
||||
} else {
|
||||
const lastDotIndex = wordMatch.lastIndexOf('.');
|
||||
if (lastDotIndex !== -1) {
|
||||
replaceFrom = { line: cursor.line, ch: startPos.ch + lastDotIndex + 1 };
|
||||
replaceTo = cursor;
|
||||
} else {
|
||||
replaceFrom = startPos;
|
||||
replaceTo = cursor;
|
||||
}
|
||||
}
|
||||
|
||||
return { replaceFrom, replaceTo };
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate replacement positions for regular word context
|
||||
* @param {Object} cursor - Current cursor position
|
||||
* @param {number} start - Start position of word
|
||||
* @param {number} end - End position of word
|
||||
* @param {string} word - The matched word
|
||||
* @returns {Object} From and to positions for replacement
|
||||
*/
|
||||
const calculateWordReplacementPositions = (cursor, start, end, word) => {
|
||||
let replaceFrom, replaceTo;
|
||||
|
||||
if (word.endsWith('.')) {
|
||||
replaceFrom = { line: cursor.line, ch: end };
|
||||
replaceTo = cursor;
|
||||
} else {
|
||||
const lastDotIndex = word.lastIndexOf('.');
|
||||
if (lastDotIndex !== -1) {
|
||||
replaceFrom = { line: cursor.line, ch: start + lastDotIndex + 1 };
|
||||
replaceTo = { line: cursor.line, ch: end };
|
||||
} else {
|
||||
replaceFrom = { line: cursor.line, ch: start };
|
||||
replaceTo = { line: cursor.line, ch: end };
|
||||
}
|
||||
}
|
||||
|
||||
return { replaceFrom, replaceTo };
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine context based on word prefix
|
||||
* @param {string} word - The word to analyze
|
||||
* @returns {string} The determined context
|
||||
*/
|
||||
const determineWordContext = (word) => {
|
||||
if (word.startsWith('req') || word.startsWith('res') || word.startsWith('bru')) {
|
||||
return 'api';
|
||||
}
|
||||
return 'anyword';
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract word from current line with boundaries
|
||||
* @param {string} currentLine - The current line content
|
||||
* @param {number} cursorPosition - Current cursor position
|
||||
* @returns {Object|null} Word information or null if no word found
|
||||
*/
|
||||
const extractWordFromLine = (currentLine, cursorPosition) => {
|
||||
let start = cursorPosition;
|
||||
let end = start;
|
||||
|
||||
while (end < currentLine.length && WORD_PATTERN.test(currentLine.charAt(end))) {
|
||||
++end;
|
||||
}
|
||||
while (start && WORD_PATTERN.test(currentLine.charAt(start - 1))) {
|
||||
--start;
|
||||
}
|
||||
|
||||
if (start === end) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
word: currentLine.slice(start, end),
|
||||
start,
|
||||
end
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get current word being typed at cursor position with context information
|
||||
* @param {Object} cm - CodeMirror instance
|
||||
* @returns {Object|null} Word information with context or null
|
||||
*/
|
||||
const getCurrentWordWithContext = (cm) => {
|
||||
const cursor = cm.getCursor();
|
||||
const currentLine = cm.getLine(cursor.line);
|
||||
const currentString = cm.getRange({ line: cursor.line, ch: 0 }, cursor);
|
||||
|
||||
// Check for variable pattern {{word
|
||||
const variableMatch = currentString.match(VARIABLE_PATTERN);
|
||||
if (variableMatch) {
|
||||
const wordMatch = variableMatch[1];
|
||||
const startPos = { line: cursor.line, ch: currentString.lastIndexOf('{{') + 2 };
|
||||
const { replaceFrom, replaceTo } = calculateVariableReplacementPositions(cursor, startPos, wordMatch);
|
||||
|
||||
return {
|
||||
word: wordMatch,
|
||||
from: replaceFrom,
|
||||
to: replaceTo,
|
||||
context: 'variables',
|
||||
requiresBraces: true
|
||||
};
|
||||
}
|
||||
|
||||
// Check for regular word
|
||||
const wordInfo = extractWordFromLine(currentLine, cursor.ch);
|
||||
if (!wordInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { word, start, end } = wordInfo;
|
||||
const { replaceFrom, replaceTo } = calculateWordReplacementPositions(cursor, start, end, word);
|
||||
const context = determineWordContext(word);
|
||||
|
||||
return {
|
||||
word,
|
||||
from: replaceFrom,
|
||||
to: replaceTo,
|
||||
context,
|
||||
requiresBraces: false
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract next segment suggestions from filtered hints
|
||||
* @param {string[]} filteredHints - Pre-filtered hints
|
||||
* @param {string} currentInput - Current user input
|
||||
* @returns {string[]} Array of suggestion segments
|
||||
*/
|
||||
const extractNextSegmentSuggestions = (filteredHints, currentInput) => {
|
||||
const suggestions = new Set();
|
||||
|
||||
filteredHints.forEach(hint => {
|
||||
if (!hint.toLowerCase().startsWith(currentInput.toLowerCase())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle exact match case
|
||||
if (hint.toLowerCase() === currentInput.toLowerCase()) {
|
||||
suggestions.add(hint.substring(hint.lastIndexOf('.') + 1));
|
||||
return;
|
||||
}
|
||||
|
||||
const inputLength = currentInput.length;
|
||||
|
||||
if (currentInput.endsWith('.')) {
|
||||
// Show next segment after the dot
|
||||
const afterDot = hint.substring(inputLength);
|
||||
const nextDot = afterDot.indexOf('.');
|
||||
const segment = nextDot === -1 ? afterDot : afterDot.substring(0, nextDot);
|
||||
suggestions.add(segment);
|
||||
} else {
|
||||
// Show complete current segment
|
||||
const lastDotInInput = currentInput.lastIndexOf('.');
|
||||
const currentSegmentStart = lastDotInInput + 1;
|
||||
const nextDotAfterInput = hint.indexOf('.', currentSegmentStart);
|
||||
const segment = nextDotAfterInput === -1
|
||||
? hint.substring(currentSegmentStart)
|
||||
: hint.substring(currentSegmentStart, nextDotAfterInput);
|
||||
suggestions.add(segment);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(suggestions).sort();
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract the relevant part of hints based on user input
|
||||
* @param {string[]} filteredHints - Pre-filtered hints
|
||||
* @param {string} currentInput - Current user input
|
||||
* @returns {string[]} Array of hint parts
|
||||
*/
|
||||
const getHintParts = (filteredHints, currentInput) => {
|
||||
if (!filteredHints || filteredHints.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return extractNextSegmentSuggestions(filteredHints, currentInput);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get allowed hints based on context and configuration
|
||||
* @param {Object} categorizedHints - All categorized hints
|
||||
* @param {string} context - Current context
|
||||
* @param {string[]} showHintsFor - Allowed hint types
|
||||
* @returns {string[]} Array of allowed hints
|
||||
*/
|
||||
const getAllowedHintsByContext = (categorizedHints, context, showHintsFor) => {
|
||||
let allowedHints = [];
|
||||
|
||||
if (context === 'variables' && showHintsFor.includes('variables')) {
|
||||
allowedHints = [...categorizedHints.variables];
|
||||
} else if (context === 'api') {
|
||||
const hasApiHints = showHintsFor.some(hint => ['req', 'res', 'bru'].includes(hint));
|
||||
if (hasApiHints) {
|
||||
allowedHints = [...categorizedHints.api];
|
||||
}
|
||||
} else if (context === 'anyword') {
|
||||
allowedHints = [...categorizedHints.anyword];
|
||||
}
|
||||
|
||||
return allowedHints;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter hints based on current word and allowed hint types
|
||||
* @param {Object} categorizedHints - All categorized hints
|
||||
* @param {string} currentWord - Current word being typed
|
||||
* @param {string} context - Current context
|
||||
* @param {string[]} showHintsFor - Allowed hint types
|
||||
* @returns {string[]} Filtered hints
|
||||
*/
|
||||
const filterHintsByContext = (categorizedHints, currentWord, context, showHintsFor = []) => {
|
||||
if (!currentWord) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allowedHints = getAllowedHintsByContext(categorizedHints, context, showHintsFor);
|
||||
|
||||
const filtered = allowedHints.filter(hint => {
|
||||
return hint.toLowerCase().startsWith(currentWord.toLowerCase());
|
||||
});
|
||||
|
||||
const hintParts = getHintParts(filtered, currentWord);
|
||||
|
||||
return hintParts.slice(0, 50);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create hint list for variables context
|
||||
* @param {string[]} filteredHints - Filtered hints
|
||||
* @param {Object} from - Start position
|
||||
* @param {Object} to - End position
|
||||
* @returns {Object} Hint object with list and positions
|
||||
*/
|
||||
const createVariableHintList = (filteredHints, from, to) => {
|
||||
const hintList = filteredHints.map(hint => ({
|
||||
text: hint,
|
||||
displayText: hint
|
||||
}));
|
||||
|
||||
return {
|
||||
list: hintList,
|
||||
from,
|
||||
to
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Create hint list for non-variable contexts
|
||||
* @param {string[]} filteredHints - Filtered hints
|
||||
* @param {Object} from - Start position
|
||||
* @param {Object} to - End position
|
||||
* @returns {Object} Hint object with list and positions
|
||||
*/
|
||||
const createStandardHintList = (filteredHints, from, to) => {
|
||||
return {
|
||||
list: filteredHints,
|
||||
from,
|
||||
to
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Bruno AutoComplete Helper - Main function with context awareness
|
||||
* @param {Object} cm - CodeMirror instance
|
||||
* @param {Object} allVariables - All available variables
|
||||
* @param {Object} options - Configuration options
|
||||
* @returns {Object|null} Hint object or null
|
||||
*/
|
||||
export const getAutoCompleteHints = (cm, allVariables = {}, options = {}) => {
|
||||
if (!allVariables) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const wordInfo = getCurrentWordWithContext(cm);
|
||||
if (!wordInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { word, from, to, context, requiresBraces } = wordInfo;
|
||||
const showHintsFor = options.showHintsFor || [];
|
||||
|
||||
// Check if this context requires braces but we're not in a brace context
|
||||
if (context === 'variables' && !requiresBraces) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const categorizedHints = buildCategorizedHintsList(allVariables, options);
|
||||
const filteredHints = filterHintsByContext(categorizedHints, word, context, showHintsFor);
|
||||
|
||||
if (filteredHints.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (context === 'variables') {
|
||||
return createVariableHintList(filteredHints, from, to);
|
||||
}
|
||||
|
||||
return createStandardHintList(filteredHints, from, to);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle keyup events for autocomplete
|
||||
* @param {Object} cm - CodeMirror instance
|
||||
* @param {Event} event - The keyup event
|
||||
* @param {Function} getAllVariablesFunc - Function to get all variables
|
||||
* @param {Object} options - Configuration options
|
||||
*/
|
||||
const handleKeyupForAutocomplete = (cm, event, getAllVariablesFunc, options) => {
|
||||
// Skip non-character keys
|
||||
if (!NON_CHARACTER_KEYS.test(event?.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allVariables = getAllVariablesFunc();
|
||||
const hints = getAutoCompleteHints(cm, allVariables, options);
|
||||
|
||||
if (!hints) {
|
||||
if (cm.state.completionActive) {
|
||||
cm.state.completionActive.close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
cm.showHint({
|
||||
hint: () => hints,
|
||||
completeSingle: false
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Setup Bruno AutoComplete Helper on a CodeMirror editor
|
||||
* @param {Object} editor - CodeMirror editor instance
|
||||
* @param {Function} getAllVariablesFunc - Function to get all variables
|
||||
* @param {Object} options - Configuration options
|
||||
* @returns {Function} Cleanup function
|
||||
*/
|
||||
export const setupAutoComplete = (editor, getAllVariablesFunc, options = {}) => {
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keyupHandler = (cm, event) => {
|
||||
handleKeyupForAutocomplete(cm, event, getAllVariablesFunc, options);
|
||||
};
|
||||
|
||||
editor.on('keyup', keyupHandler);
|
||||
|
||||
return () => {
|
||||
editor.off('keyup', keyupHandler);
|
||||
};
|
||||
};
|
||||
|
||||
// Initialize autocomplete command if not already present
|
||||
if (!CodeMirror.commands.autocomplete) {
|
||||
CodeMirror.commands.autocomplete = (cm, hint, options) => {
|
||||
cm.showHint({ hint, ...options });
|
||||
};
|
||||
}
|
||||
}
|
||||
540
packages/bruno-app/src/utils/codemirror/autocomplete.spec.js
Normal file
540
packages/bruno-app/src/utils/codemirror/autocomplete.spec.js
Normal file
@@ -0,0 +1,540 @@
|
||||
const { describe, it, expect, jest, beforeEach, afterEach } = require('@jest/globals');
|
||||
|
||||
const _mockedCodemirror = {
|
||||
commands: {},
|
||||
getCursor: jest.fn(),
|
||||
getLine: jest.fn(),
|
||||
getRange: jest.fn(),
|
||||
showHint: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
state: {}
|
||||
};
|
||||
|
||||
jest.mock('codemirror', () => {
|
||||
return _mockedCodemirror;
|
||||
});
|
||||
|
||||
// Import the functions to test
|
||||
import {
|
||||
getAutoCompleteHints,
|
||||
setupAutoComplete
|
||||
} from './autocomplete';
|
||||
|
||||
describe('Bruno Autocomplete', () => {
|
||||
let mockedCodemirror;
|
||||
|
||||
beforeEach(() => {
|
||||
mockedCodemirror = _mockedCodemirror;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getAutoCompleteHints', () => {
|
||||
describe('Variable autocomplete', () => {
|
||||
it('should provide variable hints when typing inside double curly braces', () => {
|
||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 9 });
|
||||
mockedCodemirror.getLine.mockReturnValue('{{envVar}}');
|
||||
mockedCodemirror.getRange.mockReturnValue('{{envVar');
|
||||
const allVariables = {
|
||||
envVar1: 'value1',
|
||||
envVar2: 'value2',
|
||||
};
|
||||
|
||||
const result = getAutoCompleteHints(mockedCodemirror, allVariables, {
|
||||
showHintsFor: ['variables']
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.list).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ text: 'envVar1', displayText: 'envVar1' }),
|
||||
expect.objectContaining({ text: 'envVar2', displayText: 'envVar2' })
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('should include mock data functions with $ prefix', () => {
|
||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 9 });
|
||||
mockedCodemirror.getRange.mockReturnValue('{{$randomI');
|
||||
|
||||
const result = getAutoCompleteHints(mockedCodemirror, {}, {
|
||||
showHintsFor: ['variables']
|
||||
});
|
||||
|
||||
expect(result.list).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ displayText: '$randomInt' })
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle process environment variables', () => {
|
||||
const allVariables = {
|
||||
process: {
|
||||
env: {
|
||||
NODE_ENV: 'development',
|
||||
API_URL: 'https://api.example.com'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 14 });
|
||||
mockedCodemirror.getRange.mockReturnValue('{{process.env.N');
|
||||
|
||||
const result = getAutoCompleteHints(mockedCodemirror, allVariables, {
|
||||
showHintsFor: ['variables']
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.list).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ displayText: 'NODE_ENV' })
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('should skip special internal keys', () => {
|
||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 10 });
|
||||
mockedCodemirror.getRange.mockReturnValue('{{path');
|
||||
|
||||
const allVariables = {
|
||||
pathParams: { id: '123' },
|
||||
maskedEnvVariables: { secret: '***' },
|
||||
path: 'value'
|
||||
};
|
||||
|
||||
const result = getAutoCompleteHints(mockedCodemirror, allVariables, {
|
||||
showHintsFor: ['variables']
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.list).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ displayText: 'path' })
|
||||
])
|
||||
);
|
||||
expect(result.list).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ displayText: 'pathParams' })
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle nested object variables', () => {
|
||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 12 });
|
||||
mockedCodemirror.getRange.mockReturnValue('{{config.api.');
|
||||
|
||||
const allVariables = {
|
||||
'config.api.url': 'https://echo.usebruno.com',
|
||||
'config.api.client_id': 'client_id',
|
||||
'config.api.client_secret': 'client_secret',
|
||||
'config.app.name': 'bruno'
|
||||
};
|
||||
|
||||
const result = getAutoCompleteHints(mockedCodemirror, allVariables, {
|
||||
showHintsFor: ['variables']
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.list).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ displayText: 'url' }),
|
||||
expect.objectContaining({ displayText: 'client_id' }),
|
||||
expect.objectContaining({ displayText: 'client_secret' })
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('API object context (req, res, bru)', () => {
|
||||
const testCases = [
|
||||
{
|
||||
name: 'req object',
|
||||
input: 'req.',
|
||||
expected: ['url', 'method', 'headers', 'body', 'timeout']
|
||||
},
|
||||
{
|
||||
name: 'res object',
|
||||
input: 'res.',
|
||||
expected: ['status', 'statusText', 'headers', 'body', 'responseTime']
|
||||
},
|
||||
{
|
||||
name: 'bru object',
|
||||
input: 'bru.',
|
||||
expected: ['cwd()', 'getEnvName()', 'getProcessEnv(key)', 'hasEnvVar(key)', 'getEnvVar(key)']
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(({ name, input, expected }) => {
|
||||
it(`should provide ${name} hints`, () => {
|
||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: input.length });
|
||||
mockedCodemirror.getLine.mockReturnValue(input);
|
||||
mockedCodemirror.getRange.mockReturnValue(input);
|
||||
|
||||
const result = getAutoCompleteHints(mockedCodemirror, {}, {
|
||||
showHintsFor: ['req', 'res', 'bru']
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.list).toEqual(expect.arrayContaining(expected));
|
||||
});
|
||||
});
|
||||
|
||||
it('should provide method hints for nested req objects', () => {
|
||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 7 });
|
||||
mockedCodemirror.getLine.mockReturnValue('req.get');
|
||||
mockedCodemirror.getRange.mockReturnValue('req.get');
|
||||
|
||||
const result = getAutoCompleteHints(mockedCodemirror, {}, {
|
||||
showHintsFor: ['req']
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.list).toEqual(
|
||||
expect.arrayContaining([
|
||||
'getUrl()',
|
||||
'getMethod()',
|
||||
'getAuthMode()',
|
||||
'getHeader(name)',
|
||||
'getHeaders()',
|
||||
'getBody()',
|
||||
'getTimeout()',
|
||||
'getExecutionMode()',
|
||||
'getName()'
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle bru.runner sub-object', () => {
|
||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 11 });
|
||||
mockedCodemirror.getLine.mockReturnValue('bru.runner.');
|
||||
mockedCodemirror.getRange.mockReturnValue('bru.runner.');
|
||||
|
||||
const result = getAutoCompleteHints(mockedCodemirror, {}, {
|
||||
showHintsFor: ['bru']
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.list).toEqual(
|
||||
expect.arrayContaining([
|
||||
'setNextRequest(requestName)',
|
||||
'skipRequest()',
|
||||
'stopExecution()'
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom hints and anyword context', () => {
|
||||
it('should provide custom anyword hints', () => {
|
||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 7 });
|
||||
mockedCodemirror.getLine.mockReturnValue('Content-');
|
||||
mockedCodemirror.getRange.mockReturnValue('Content-');
|
||||
|
||||
const options = {
|
||||
anywordAutocompleteHints: ['Content-Type', 'Content-Encoding', 'Content-Length']
|
||||
};
|
||||
|
||||
const result = getAutoCompleteHints(mockedCodemirror, {}, options, {
|
||||
showHintsFor: ['variables']
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.list).toEqual(
|
||||
expect.arrayContaining(['Content-Type', 'Content-Encoding', 'Content-Length'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle progressive hints for custom hints', () => {
|
||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 6 });
|
||||
mockedCodemirror.getLine.mockReturnValue('utils.');
|
||||
mockedCodemirror.getRange.mockReturnValue('utils.');
|
||||
|
||||
const options = {
|
||||
anywordAutocompleteHints: ['utils.string.trim', 'utils.string.capitalize', 'utils.array.map']
|
||||
};
|
||||
|
||||
const result = getAutoCompleteHints(mockedCodemirror, {}, options, {
|
||||
showHintsFor: ['variables']
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.list).toEqual(
|
||||
expect.arrayContaining(['string', 'array'])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filtering and options', () => {
|
||||
beforeEach(() => {
|
||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 4 });
|
||||
mockedCodemirror.getLine.mockReturnValue('req.');
|
||||
mockedCodemirror.getRange.mockReturnValue('req.');
|
||||
});
|
||||
|
||||
it('should respect showHintsFor option for excluding hints', () => {
|
||||
const options = { showHintsFor: ['res', 'bru'] };
|
||||
const result = getAutoCompleteHints(mockedCodemirror, {}, options, {
|
||||
showHintsFor: ['req']
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should show hints when included in showHintsFor', () => {
|
||||
const options = { showHintsFor: ['req'] };
|
||||
const result = getAutoCompleteHints(mockedCodemirror, {}, options, {
|
||||
showHintsFor: ['req']
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.list).toEqual(
|
||||
expect.arrayContaining(['url', 'method'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter variables based on showHintsFor', () => {
|
||||
mockedCodemirror.getLine.mockReturnValue('{{varNa}}');
|
||||
mockedCodemirror.getRange.mockReturnValue('{{varNa');
|
||||
|
||||
const allVariables = { envVar1: 'value1' };
|
||||
const options = { showHintsFor: ['req', 'res', 'bru'] };
|
||||
|
||||
const result = getAutoCompleteHints(mockedCodemirror, allVariables, options);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should limit results to 50 hints', () => {
|
||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 10 });
|
||||
mockedCodemirror.getLine.mockReturnValue('{{varName}}');
|
||||
mockedCodemirror.getRange.mockReturnValue('{{v');
|
||||
|
||||
const allVariables = {};
|
||||
for (let i = 0; i < 100; i++) {
|
||||
allVariables[`var${i}`] = `value${i}`;
|
||||
}
|
||||
|
||||
const result = getAutoCompleteHints(mockedCodemirror, allVariables, {
|
||||
showHintsFor: ['variables']
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.list.length).toBeLessThanOrEqual(50);
|
||||
});
|
||||
|
||||
it('should sort hints alphabetically', () => {
|
||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 10 });
|
||||
mockedCodemirror.getLine.mockReturnValue('{{v.');
|
||||
mockedCodemirror.getRange.mockReturnValue('{{v.');
|
||||
|
||||
const allVariables = {
|
||||
'v.zebra': 'value1',
|
||||
'v.apple': 'value2',
|
||||
'v.banana': 'value3'
|
||||
};
|
||||
|
||||
const result = getAutoCompleteHints(mockedCodemirror, allVariables, {
|
||||
showHintsFor: ['variables']
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
const displayTexts = result.list.map(item =>
|
||||
typeof item === 'object' ? item.displayText : item
|
||||
);
|
||||
|
||||
const userVars = displayTexts.filter(text => !text.startsWith('$'));
|
||||
expect(userVars).toEqual(['apple', 'banana', 'zebra']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should return null when no word is found at cursor', () => {
|
||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 0 });
|
||||
mockedCodemirror.getLine.mockReturnValue(' ');
|
||||
mockedCodemirror.getRange.mockReturnValue('');
|
||||
|
||||
const result = getAutoCompleteHints(mockedCodemirror, {});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle empty or null variables', () => {
|
||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 10 });
|
||||
mockedCodemirror.getLine.mockReturnValue('{{varName}}');
|
||||
mockedCodemirror.getRange.mockReturnValue('{{varName');
|
||||
|
||||
const emptyResult = getAutoCompleteHints(mockedCodemirror, {});
|
||||
const nullResult = getAutoCompleteHints(mockedCodemirror, null);
|
||||
|
||||
expect(emptyResult).toBeNull();
|
||||
expect(nullResult).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle cursor at end of line', () => {
|
||||
const line = 'req.getHea';
|
||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: line.length });
|
||||
mockedCodemirror.getLine.mockReturnValue(line);
|
||||
mockedCodemirror.getRange.mockReturnValue(line);
|
||||
|
||||
const result = getAutoCompleteHints(mockedCodemirror, {}, {
|
||||
showHintsFor: ['req']
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.list).toEqual(
|
||||
expect.arrayContaining(['getHeader(name)', 'getHeaders()'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle case-insensitive matching', () => {
|
||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 10 });
|
||||
mockedCodemirror.getLine.mockReturnValue('{{varName}}');
|
||||
mockedCodemirror.getRange.mockReturnValue('{{var');
|
||||
|
||||
const allVariables = {
|
||||
variable1: 'value1',
|
||||
Variable2: 'value2',
|
||||
VARIABLE3: 'value3'
|
||||
};
|
||||
|
||||
const result = getAutoCompleteHints(mockedCodemirror, allVariables, {
|
||||
showHintsFor: ['variables']
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.list.length).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setupAutoComplete', () => {
|
||||
let mockGetAllVariables;
|
||||
let cleanupFn;
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetAllVariables = jest.fn(() => ({ }));
|
||||
mockedCodemirror.state = {};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (cleanupFn) {
|
||||
cleanupFn();
|
||||
}
|
||||
});
|
||||
|
||||
describe('Setup and cleanup', () => {
|
||||
it('should setup keyup event listener and return cleanup function', () => {
|
||||
cleanupFn = setupAutoComplete(mockedCodemirror, mockGetAllVariables);
|
||||
|
||||
expect(mockedCodemirror.on).toHaveBeenCalledWith('keyup', expect.any(Function));
|
||||
expect(cleanupFn).toBeInstanceOf(Function);
|
||||
|
||||
cleanupFn();
|
||||
expect(mockedCodemirror.off).toHaveBeenCalledWith('keyup', expect.any(Function));
|
||||
});
|
||||
|
||||
it('should not setup if editor is null', () => {
|
||||
const result = setupAutoComplete(null, mockGetAllVariables);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(mockedCodemirror.on).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Event handling', () => {
|
||||
it('should trigger hints on character key press', () => {
|
||||
cleanupFn = setupAutoComplete(mockedCodemirror, mockGetAllVariables, {
|
||||
showHintsFor: ['req']
|
||||
});
|
||||
const keyupHandler = mockedCodemirror.on.mock.calls[0][1];
|
||||
|
||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 4 });
|
||||
mockedCodemirror.getLine.mockReturnValue('req.');
|
||||
mockedCodemirror.getRange.mockReturnValue('req.');
|
||||
|
||||
const mockEvent = { key: 'a' };
|
||||
keyupHandler(mockedCodemirror, mockEvent);
|
||||
|
||||
expect(mockGetAllVariables).toHaveBeenCalled();
|
||||
expect(mockedCodemirror.showHint).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not trigger hints on non-character keys', () => {
|
||||
cleanupFn = setupAutoComplete(mockedCodemirror, mockGetAllVariables);
|
||||
const keyupHandler = mockedCodemirror.on.mock.calls[0][1];
|
||||
|
||||
const nonCharacterKeys = ['Shift', 'Tab', 'Enter', 'Escape', 'ArrowUp', 'ArrowDown', 'Meta'];
|
||||
|
||||
nonCharacterKeys.forEach(key => {
|
||||
const mockEvent = { key };
|
||||
keyupHandler(mockedCodemirror, mockEvent);
|
||||
});
|
||||
|
||||
expect(mockedCodemirror.showHint).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should close existing completion when no hints available', () => {
|
||||
cleanupFn = setupAutoComplete(mockedCodemirror, mockGetAllVariables);
|
||||
const keyupHandler = mockedCodemirror.on.mock.calls[0][1];
|
||||
|
||||
const mockCompletion = { close: jest.fn() };
|
||||
mockedCodemirror.state.completionActive = mockCompletion;
|
||||
|
||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 0 });
|
||||
mockedCodemirror.getLine.mockReturnValue(' ');
|
||||
mockedCodemirror.getRange.mockReturnValue('');
|
||||
|
||||
const mockEvent = { key: 'a' };
|
||||
keyupHandler(mockedCodemirror, mockEvent);
|
||||
|
||||
expect(mockCompletion.close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should pass options to getAutoCompleteHints', () => {
|
||||
const options = { showHintsFor: ['req'] };
|
||||
cleanupFn = setupAutoComplete(mockedCodemirror, mockGetAllVariables, options);
|
||||
const keyupHandler = mockedCodemirror.on.mock.calls[0][1];
|
||||
|
||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 4 });
|
||||
mockedCodemirror.getLine.mockReturnValue('req.');
|
||||
mockedCodemirror.getRange.mockReturnValue('req.');
|
||||
|
||||
const mockEvent = { key: 'a' };
|
||||
keyupHandler(mockedCodemirror, mockEvent);
|
||||
|
||||
expect(mockedCodemirror.showHint).toHaveBeenCalledWith({
|
||||
hint: expect.any(Function),
|
||||
completeSingle: false
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('CodeMirror integration', () => {
|
||||
it('should define autocomplete command if not exists', () => {
|
||||
delete mockedCodemirror.commands.autocomplete;
|
||||
|
||||
jest.isolateModules(() => {
|
||||
require('./autocomplete');
|
||||
});
|
||||
|
||||
expect(mockedCodemirror.commands.autocomplete).toBeDefined();
|
||||
expect(typeof mockedCodemirror.commands.autocomplete).toBe('function');
|
||||
});
|
||||
|
||||
it('should not override existing autocomplete command', () => {
|
||||
const existingCommand = jest.fn();
|
||||
mockedCodemirror.commands.autocomplete = existingCommand;
|
||||
|
||||
jest.isolateModules(() => {
|
||||
require('./autocomplete');
|
||||
});
|
||||
|
||||
expect(mockedCodemirror.commands.autocomplete).toBe(existingCommand);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,7 @@
|
||||
import get from 'lodash/get';
|
||||
import { mockDataFunctions } from '@usebruno/common';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
}
|
||||
const CodeMirror = require('codemirror');
|
||||
|
||||
const pathFoundInVariables = (path, obj) => {
|
||||
const value = get(obj, path);
|
||||
|
||||
Reference in New Issue
Block a user