mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-28 07:04:10 +00:00
754 lines
22 KiB
JavaScript
754 lines
22 KiB
JavaScript
import { mockDataFunctions } from '@usebruno/common';
|
|
|
|
const CodeMirror = require('codemirror');
|
|
|
|
// Static API hints - Bruno JavaScript API (subgrouped by category)
|
|
// TODO: Restore the commented-out APIs once the UI update fixes are live.
|
|
// Currently these APIs only work within the request lifecycle but fail to update the UI tables.
|
|
// e.g., setCollectionVar only sets the variable in the request lifecycle, fails to update the table in the UI.
|
|
const STATIC_API_HINTS = {
|
|
req: [
|
|
'req',
|
|
'req.url',
|
|
'req.method',
|
|
'req.headers',
|
|
'req.body',
|
|
'req.timeout',
|
|
'req.getUrl()',
|
|
'req.setUrl(url)',
|
|
'req.getHost()',
|
|
'req.getPath()',
|
|
'req.getQueryString()',
|
|
'req.getMethod()',
|
|
'req.getAuthMode()',
|
|
'req.setMethod(method)',
|
|
'req.getHeader(name)',
|
|
'req.getHeaders()',
|
|
'req.setHeader(name, value)',
|
|
'req.setHeaders(data)',
|
|
'req.deleteHeader(name)',
|
|
'req.deleteHeaders(data)',
|
|
'req.getBody()',
|
|
'req.setBody(data)',
|
|
'req.setMaxRedirects(maxRedirects)',
|
|
'req.getTimeout()',
|
|
'req.setTimeout(timeout)',
|
|
'req.getExecutionMode()',
|
|
'req.getName()',
|
|
'req.getPathParams()',
|
|
'req.getTags()',
|
|
'req.disableParsingResponseJson()',
|
|
'req.onFail(function(err) {})'
|
|
],
|
|
res: [
|
|
'res',
|
|
'res.status',
|
|
'res.statusText',
|
|
'res.headers',
|
|
'res.body',
|
|
'res.responseTime',
|
|
'res.url',
|
|
'res.getStatus()',
|
|
'res.getStatusText()',
|
|
'res.getHeader(name)',
|
|
'res.getHeaders()',
|
|
'res.getBody()',
|
|
'res.setBody(data)',
|
|
'res.getResponseTime()',
|
|
'res.getSize()',
|
|
'res.getSize().header',
|
|
'res.getSize().body',
|
|
'res.getSize().total',
|
|
'res.getUrl()'
|
|
],
|
|
bru: [
|
|
'bru',
|
|
'bru.cwd()',
|
|
'bru.getEnvName()',
|
|
'bru.getProcessEnv(key)',
|
|
'bru.hasEnvVar(key)',
|
|
'bru.getEnvVar(key)',
|
|
'bru.getFolderVar(key)',
|
|
'bru.getCollectionVar(key)',
|
|
// 'bru.setCollectionVar(key, value)',
|
|
'bru.hasCollectionVar(key)',
|
|
// 'bru.deleteCollectionVar(key)',
|
|
// 'bru.deleteAllCollectionVars()',
|
|
// 'bru.getAllCollectionVars()',
|
|
'bru.setEnvVar(key, value)',
|
|
'bru.setEnvVar(key, value, options)',
|
|
'bru.deleteEnvVar(key)',
|
|
'bru.getAllEnvVars()',
|
|
'bru.deleteAllEnvVars()',
|
|
'bru.hasVar(key)',
|
|
'bru.getVar(key)',
|
|
'bru.setVar(key,value)',
|
|
'bru.deleteVar(key)',
|
|
'bru.deleteAllVars()',
|
|
'bru.getAllVars()',
|
|
'bru.setNextRequest(requestName)',
|
|
'bru.getRequestVar(key)',
|
|
'bru.runRequest(requestPathName)',
|
|
'bru.sendRequest(requestConfig)',
|
|
'bru.sendRequest(requestConfig, callback)',
|
|
'bru.getAssertionResults()',
|
|
'bru.getTestResults()',
|
|
'bru.sleep(ms)',
|
|
'bru.getCollectionName()',
|
|
'bru.isSafeMode()',
|
|
'bru.getOauth2CredentialVar(key)',
|
|
'bru.getGlobalEnvVar(key)',
|
|
'bru.setGlobalEnvVar(key, value)',
|
|
// 'bru.deleteGlobalEnvVar(key)',
|
|
'bru.getAllGlobalEnvVars()',
|
|
// 'bru.deleteAllGlobalEnvVars()',
|
|
'bru.runner',
|
|
'bru.runner.setNextRequest(requestName)',
|
|
'bru.runner.skipRequest()',
|
|
'bru.runner.stopExecution()',
|
|
'bru.interpolate(str)',
|
|
'bru.cookies',
|
|
'bru.cookies.jar()',
|
|
'bru.cookies.jar().getCookie(url, name, callback)',
|
|
'bru.cookies.jar().getCookies(url, callback)',
|
|
'bru.cookies.jar().setCookie(url, name, value, callback)',
|
|
'bru.cookies.jar().setCookie(url, cookieObject, callback)',
|
|
'bru.cookies.jar().setCookies(url, cookiesArray, callback)',
|
|
'bru.cookies.jar().clear(callback)',
|
|
'bru.cookies.jar().deleteCookies(url, callback)',
|
|
'bru.cookies.jar().deleteCookie(url, name, callback)',
|
|
'bru.cookies.jar().hasCookie(url, name, callback)',
|
|
'bru.utils',
|
|
'bru.utils.minifyJson(json)',
|
|
'bru.utils.minifyXml(xml)',
|
|
'bru.resetOauth2Credential(credentialId)'
|
|
]
|
|
};
|
|
|
|
// 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);
|
|
}
|
|
});
|
|
|
|
// 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 {string[]} customHints - Array of custom hints
|
|
*/
|
|
const addCustomHintsToSet = (anywordHints, customHints) => {
|
|
if (customHints && Array.isArray(customHints)) {
|
|
customHints.forEach((hint) => {
|
|
generateProgressiveHints(hint).forEach((h) => anywordHints.add(h));
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Build categorized hints list from all sources
|
|
* @param {Object} allVariables - All available variables
|
|
* @param {string[]} anywordAutocompleteHints - Custom autocomplete hints
|
|
* @param {Object} options - Configuration options
|
|
* @returns {Object} Categorized hints object
|
|
*/
|
|
const buildCategorizedHintsList = (allVariables = {}, anywordAutocompleteHints = [], 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, anywordAutocompleteHints);
|
|
|
|
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) => {
|
|
const isApiHint = Object.keys(STATIC_API_HINTS).some(
|
|
(apiRoot) => apiRoot.toLowerCase().startsWith(word.toLowerCase()) || word.toLowerCase().startsWith(apiRoot.toLowerCase())
|
|
);
|
|
|
|
if (isApiHint) {
|
|
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 prefixMatches = new Set();
|
|
const substringMatches = new Set();
|
|
const lowerInput = currentInput.toLowerCase();
|
|
|
|
filteredHints.forEach((hint) => {
|
|
const lowerHint = hint.toLowerCase();
|
|
|
|
// For prefix matches, use the original progressive logic
|
|
if (lowerHint.startsWith(lowerInput)) {
|
|
// Handle exact match case
|
|
if (lowerHint === lowerInput) {
|
|
prefixMatches.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);
|
|
prefixMatches.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);
|
|
prefixMatches.add(segment);
|
|
}
|
|
} else if (lowerHint.includes(lowerInput)) {
|
|
// For substring matches (search within words), suggest the complete hint
|
|
substringMatches.add(hint);
|
|
}
|
|
});
|
|
|
|
// Return prefix matches first, then substring matches
|
|
return [...Array.from(prefixMatches).sort(), ...Array.from(substringMatches).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 lowerWord = currentWord.toLowerCase();
|
|
const filtered = allowedHints.filter((hint) => {
|
|
return hint.toLowerCase().includes(lowerWord);
|
|
});
|
|
|
|
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
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Show root-level API hints when the editor is empty
|
|
* @param {Object} cm - CodeMirror instance
|
|
* @param {string[]} showHintsFor - Array of hint types to show (e.g., ['req', 'res', 'bru'])
|
|
* @returns {boolean} True if hints were shown, false otherwise
|
|
*/
|
|
export const showRootHints = (cm, showHintsFor = []) => {
|
|
const wordInfo = getCurrentWordWithContext(cm);
|
|
// If user is currently typing a word, let handleKeyupForAutocomplete
|
|
// handle it instead of showing root hints.
|
|
if (wordInfo) {
|
|
return false;
|
|
}
|
|
|
|
const hints = Object.keys(STATIC_API_HINTS).filter((rootHint) => showHintsFor.includes(rootHint));
|
|
|
|
if (hints.length === 0) return false;
|
|
|
|
const cursor = cm.getCursor();
|
|
const hintList = createStandardHintList(hints, cursor, cursor);
|
|
|
|
cm.showHint({
|
|
hint: () => hintList,
|
|
completeSingle: false
|
|
});
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Bruno AutoComplete Helper - Main function with context awareness
|
|
* @param {Object} cm - CodeMirror instance
|
|
* @param {Object} allVariables - All available variables
|
|
* @param {string[]} anywordAutocompleteHints - Custom autocomplete hints
|
|
* @param {Object} options - Configuration options
|
|
* @returns {Object|null} Hint object or null
|
|
*/
|
|
export const getAutoCompleteHints = (cm, allVariables = {}, anywordAutocompleteHints = [], 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, anywordAutocompleteHints, 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 click events for autocomplete
|
|
* @param {Object} cm - CodeMirror instance
|
|
* @param {Object} options - Configuration options
|
|
*/
|
|
const handleClickForAutocomplete = (cm, options) => {
|
|
const allVariables = options.getAllVariables?.() || {};
|
|
const anywordAutocompleteHints = options.getAnywordAutocompleteHints?.() || [];
|
|
const showHintsFor = options.showHintsFor || [];
|
|
|
|
// Build all available hints
|
|
const categorizedHints = buildCategorizedHintsList(allVariables, anywordAutocompleteHints, options);
|
|
|
|
// Combine all hints based on showHintsFor configuration
|
|
let allHints = [];
|
|
|
|
// Add API hints if enabled
|
|
const hasApiHints = showHintsFor.some((hint) => ['req', 'res', 'bru'].includes(hint));
|
|
if (hasApiHints) {
|
|
allHints = [...allHints, ...categorizedHints.api];
|
|
}
|
|
|
|
// Add variable hints if enabled
|
|
if (showHintsFor.includes('variables')) {
|
|
allHints = [...allHints, ...categorizedHints.variables];
|
|
}
|
|
|
|
// Add anyword hints (always included)
|
|
allHints = [...allHints, ...categorizedHints.anyword];
|
|
|
|
// Remove duplicates and sort
|
|
allHints = [...new Set(allHints)].sort();
|
|
|
|
if (allHints.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const cursor = cm.getCursor();
|
|
|
|
if (cursor.ch > 0) return;
|
|
|
|
// Defer showHint to ensure editor is focused
|
|
setTimeout(() => {
|
|
cm.showHint({
|
|
hint: () => ({
|
|
list: allHints,
|
|
from: cursor,
|
|
to: cursor
|
|
}),
|
|
completeSingle: false
|
|
});
|
|
}, 0);
|
|
};
|
|
|
|
/**
|
|
* Handle keyup events for autocomplete
|
|
* @param {Object} cm - CodeMirror instance
|
|
* @param {Event} event - The keyup event
|
|
* @param {Object} options - Configuration options
|
|
*/
|
|
const handleKeyupForAutocomplete = (cm, event, options) => {
|
|
// Skip non-character keys
|
|
if (!NON_CHARACTER_KEYS.test(event?.key)) {
|
|
return;
|
|
}
|
|
|
|
const allVariables = options.getAllVariables?.() || {};
|
|
const anywordAutocompleteHints = options.getAnywordAutocompleteHints?.() || [];
|
|
const hints = getAutoCompleteHints(cm, allVariables, anywordAutocompleteHints, options);
|
|
|
|
if (!hints) {
|
|
const wordInfo = getCurrentWordWithContext(cm);
|
|
if (cm.state.completionActive && wordInfo) {
|
|
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 {Object} options - Configuration options
|
|
* @returns {Function} Cleanup function
|
|
*/
|
|
export const setupAutoComplete = (editor, options = {}) => {
|
|
if (!editor) {
|
|
return;
|
|
}
|
|
|
|
const keyupHandler = (cm, event) => {
|
|
handleKeyupForAutocomplete(cm, event, options);
|
|
};
|
|
|
|
editor.on('keyup', keyupHandler);
|
|
|
|
const clickHandler = (cm) => {
|
|
// Only show hints on click if the option is enabled and there's no active completion
|
|
if (options.showHintsOnClick) {
|
|
handleClickForAutocomplete(cm, options);
|
|
}
|
|
};
|
|
|
|
// Add click handler if showHintsOnClick is enabled
|
|
if (options.showHintsOnClick) {
|
|
editor.on('mousedown', clickHandler);
|
|
}
|
|
|
|
return () => {
|
|
editor.off('keyup', keyupHandler);
|
|
if (options.showHintsOnClick) {
|
|
editor.off('mousedown', clickHandler);
|
|
}
|
|
};
|
|
};
|
|
|
|
// Exported for testing
|
|
export { extractNextSegmentSuggestions };
|
|
|
|
// Initialize autocomplete command if not already present
|
|
if (!CodeMirror.commands.autocomplete) {
|
|
CodeMirror.commands.autocomplete = (cm, hint, options) => {
|
|
cm.showHint({ hint, ...options });
|
|
};
|
|
}
|