Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/bruno-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"jsonc-parser": "^3.2.1",
"jsonpath-plus": "^10.3.0",
"know-your-http-well": "^0.5.0",
"linkify-it": "^5.0.0",
"lodash": "^4.17.21",
"markdown-it": "^13.0.2",
"markdown-it-replace-link": "^1.2.0",
Expand Down
4 changes: 4 additions & 0 deletions packages/bruno-app/src/components/CodeEditor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ 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');
Expand Down Expand Up @@ -204,6 +205,8 @@ export default class CodeEditor extends React.Component {
editor,
autoCompleteOptions
);

setupLinkAware(editor);
}
}

Expand Down Expand Up @@ -266,6 +269,7 @@ export default class CodeEditor extends React.Component {

componentWillUnmount() {
if (this.editor) {
this.editor?._destroyLinkAware?.();
this.editor.off('change', this._onEdit);
this.editor.off('scroll', this.onScroll);
this.editor = null;
Expand Down
8 changes: 8 additions & 0 deletions packages/bruno-app/src/components/MultiLineEditor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
import { setupAutoComplete } from 'utils/codemirror/autocomplete';
import { MaskedEditor } from 'utils/common/masked-editor';
import StyledWrapper from './StyledWrapper';
import { setupLinkAware } from 'utils/codemirror/linkAware';
import { IconEye, IconEyeOff } from '@tabler/icons';

const CodeMirror = require('codemirror');
Expand All @@ -30,6 +31,8 @@ class MultiLineEditor extends Component {
const variables = getAllVariables(this.props.collection, this.props.item);

this.editor = CodeMirror(this.editorRef.current, {
lineWrapping: false,
lineNumbers: false,
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
placeholder: this.props.placeholder,
mode: 'brunovariables',
Expand Down Expand Up @@ -84,6 +87,8 @@ class MultiLineEditor extends Component {
this.editor,
autoCompleteOptions
);

setupLinkAware(this.editor);

this.editor.setValue(String(this.props.value) || '');
this.editor.on('change', this._onEdit);
Expand Down Expand Up @@ -168,6 +173,9 @@ class MultiLineEditor extends Component {
if (this.brunoAutoCompleteCleanup) {
this.brunoAutoCompleteCleanup();
}
if (this.editor?._destroyLinkAware) {
this.editor._destroyLinkAware();
}
if (this.maskedEditor) {
this.maskedEditor.destroy();
this.maskedEditor = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import StyledWrapper from './StyledWrapper';
import { IconWand } from '@tabler/icons';

import onHasCompletion from './onHasCompletion';
import { setupLinkAware } from 'utils/codemirror/linkAware';

const CodeMirror = require('codemirror');

Expand Down Expand Up @@ -138,6 +139,8 @@ export default class QueryEditor extends React.Component {
editor.on('beforeChange', this._onBeforeChange);
}
this.addOverlay();

setupLinkAware(editor);
}

componentDidUpdate(prevProps) {
Expand Down Expand Up @@ -170,6 +173,9 @@ export default class QueryEditor extends React.Component {

componentWillUnmount() {
if (this.editor) {
if (this.editor?._destroyLinkAware) {
this.editor._destroyLinkAware();
}
this.editor.off('change', this._onEdit);
this.editor.off('keyup', this._onKeyUp);
this.editor.off('hasCompletion', this._onHasCompletion);
Expand Down
9 changes: 7 additions & 2 deletions packages/bruno-app/src/components/SingleLineEditor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { MaskedEditor } from 'utils/common/masked-editor';
import { setupAutoComplete } from 'utils/codemirror/autocomplete';
import StyledWrapper from './StyledWrapper';
import { IconEye, IconEyeOff } from '@tabler/icons';
import { setupLinkAware } from 'utils/codemirror/linkAware';

const CodeMirror = require('codemirror');

Expand Down Expand Up @@ -40,7 +41,7 @@ class SingleLineEditor extends Component {
this.props.onSave();
}
};
const noopHandler = () => {};
const noopHandler = () => { };

this.editor = CodeMirror(this.editorRef.current, {
placeholder: this.props.placeholder ?? '',
Expand Down Expand Up @@ -94,7 +95,7 @@ class SingleLineEditor extends Component {
this.editor,
autoCompleteOptions
);

this.editor.setValue(String(this.props.value ?? ''));
this.editor.on('change', this._onEdit);
this.editor.on('paste', this._onPaste);
Expand All @@ -106,6 +107,7 @@ class SingleLineEditor extends Component {
if (this.props.showNewlineArrow) {
this._updateNewlineMarkers();
}
setupLinkAware(this.editor);
}

/** Enable or disable masking the rendered content of the editor */
Expand Down Expand Up @@ -189,6 +191,9 @@ class SingleLineEditor extends Component {

componentWillUnmount() {
if (this.editor) {
if (this.editor?._destroyLinkAware) {
this.editor._destroyLinkAware();
}
this.editor.off('change', this._onEdit);
this.editor.off('paste', this._onPaste);
this._clearNewlineMarkers();
Expand Down
8 changes: 8 additions & 0 deletions packages/bruno-app/src/globalStyles.js
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,14 @@ const GlobalStyle = createGlobalStyle`
background: #08f !important;
color: #fff !important;
}

.hovered-link.CodeMirror-link {
text-decoration: underline !important;
}
.cmd-ctrl-pressed .hovered-link.CodeMirror-link[data-url] {
cursor: pointer;
color: ${(props) => props.theme.textLink} !important;
}
`;

export default GlobalStyle;
183 changes: 183 additions & 0 deletions packages/bruno-app/src/utils/codemirror/linkAware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import LinkifyIt from 'linkify-it';
import { isMacOS } from 'utils/common/platform';
import { debounce } from 'lodash';
/**
* Marks URLs in the CodeMirror editor with clickable link styling
* @param {Object} editor - The CodeMirror editor instance
* @param {Object} linkify - The LinkifyIt instance for URL detection
* @param {string} linkClass - CSS class name for links
* @param {string} linkHint - Tooltip text for links
*/
function markUrls(editor, linkify, linkClass, linkHint) {
const doc = editor.getDoc();
const text = doc.getValue();

// Clear existing link marks
editor.getAllMarks().forEach((mark) => {
if (mark.className === linkClass) mark.clear();
});

// Find and mark new URLs
const matches = linkify.match(text);
matches?.forEach(({ index, lastIndex, url }) => {
const from = editor.posFromIndex(index);
const to = editor.posFromIndex(lastIndex);
editor.markText(from, to, {
className: linkClass,
attributes: {
'data-url': url,
'title': linkHint
}
});
});
}

/**
* Handles mouse enter events on links to show hover effects
* @param {Event} event - The mouse enter event
* @param {string} linkClass - CSS class name for links
* @param {string} linkHoverClass - CSS class name for hovered links
* @param {Function} updateCmdCtrlClass - Function to update Cmd/Ctrl state
*/
function handleMouseEnter(event, linkClass, linkHoverClass, updateCmdCtrlClass) {
const el = event.target;
if (!el.classList.contains(linkClass)) return;

updateCmdCtrlClass(event);

el.classList.add(linkHoverClass);

// Add hover effect to previous siblings that are also links
let sibling = el.previousElementSibling;
while (sibling && sibling.classList.contains(linkClass)) {
sibling.classList.add(linkHoverClass);
sibling = sibling.previousElementSibling;
}

// Add hover effect to next siblings that are also links
sibling = el.nextElementSibling;
while (sibling && sibling.classList.contains(linkClass)) {
sibling.classList.add(linkHoverClass);
sibling = sibling.nextElementSibling;
}
}

/**
* Handles mouse leave events on links to remove hover effects
* @param {Event} event - The mouse leave event
* @param {string} linkClass - CSS class name for links
* @param {string} linkHoverClass - CSS class name for hovered links
*/
function handleMouseLeave(event, linkClass, linkHoverClass) {
const el = event.target;
el.classList.remove(linkHoverClass);

// Remove hover effect from previous siblings that are also links
let sibling = el.previousElementSibling;
while (sibling && sibling.classList.contains(linkClass)) {
sibling.classList.remove(linkHoverClass);
sibling = sibling.previousElementSibling;
}

// Remove hover effect from next siblings that are also links
sibling = el.nextElementSibling;
while (sibling && sibling.classList.contains(linkClass)) {
sibling.classList.remove(linkHoverClass);
sibling = sibling.nextElementSibling;
}
}

/**
* Updates the CSS class on the editor wrapper based on Cmd/Ctrl key state
* @param {Event} event - The keyboard event
* @param {HTMLElement} editorWrapper - The editor wrapper element
* @param {string} cmdCtrlClass - CSS class name for Cmd/Ctrl pressed state
* @param {Function} isCmdOrCtrlPressed - Function to check if Cmd/Ctrl is pressed
*/
function updateCmdCtrlClass(event, editorWrapper, cmdCtrlClass, isCmdOrCtrlPressed) {
if (isCmdOrCtrlPressed(event)) {
editorWrapper.classList.add(cmdCtrlClass);
} else {
editorWrapper.classList.remove(cmdCtrlClass);
}
}

/**
* Handles click events on links to open them externally
* @param {Event} event - The click event
* @param {string} linkClass - CSS class name for links
* @param {Function} isCmdOrCtrlPressed - Function to check if Cmd/Ctrl is pressed
*/
function handleClick(event, linkClass, isCmdOrCtrlPressed) {
if (!isCmdOrCtrlPressed(event)) return;

if (event.target.classList.contains(linkClass)) {
event.preventDefault();
event.stopPropagation();
const url = event.target.getAttribute('data-url');
if (url) {
window?.ipcRenderer?.openExternal(url);
}
}
}

/**
* Sets up link awareness for a CodeMirror editor instance.
* This enables automatic URL detection, styling, and click-to-open functionality.
* @param {Object} editor - The CodeMirror editor instance
* @param {Object} options - Configuration options (currently unused but reserved for future use)
* @returns {void}
*/
function setupLinkAware(editor, options = {}) {
if (!editor) {
return;
}

// CSS class names and configuration
const cmdCtrlClass = 'cmd-ctrl-pressed';
const linkClass = 'CodeMirror-link';
const linkHoverClass = 'hovered-link';
const linkHint = isMacOS() ? 'Hold Cmd and click to open link' : 'Hold Ctrl and click to open link';

// Helper function to check if Cmd/Ctrl is pressed
const isCmdOrCtrlPressed = (event) => (isMacOS() ? event.metaKey : event.ctrlKey);

// Initialize LinkifyIt for URL detection
const linkify = new LinkifyIt();
const editorWrapper = editor.getWrapperElement();

// Create bound versions of event handlers with proper parameters
const boundMarkUrls = () => markUrls(editor, linkify, linkClass, linkHint);
const boundUpdateCmdCtrlClass = (event) => updateCmdCtrlClass(event, editorWrapper, cmdCtrlClass, isCmdOrCtrlPressed);
const boundHandleClick = (event) => handleClick(event, linkClass, isCmdOrCtrlPressed);
const boundHandleMouseEnter = (event) => handleMouseEnter(event, linkClass, linkHoverClass, boundUpdateCmdCtrlClass);
const boundHandleMouseLeave = (event) => handleMouseLeave(event, linkClass, linkHoverClass);

// Create debounced version of markUrls
const debouncedMarkUrls = debounce(() => {
requestAnimationFrame(boundMarkUrls);
}, 150);

// Initial URL marking
boundMarkUrls();

// Set up event listeners
editor.on('changes', debouncedMarkUrls);
window.addEventListener('keydown', boundUpdateCmdCtrlClass);
window.addEventListener('keyup', boundUpdateCmdCtrlClass);
editorWrapper.addEventListener('click', boundHandleClick);
editorWrapper.addEventListener('mouseover', boundHandleMouseEnter);
editorWrapper.addEventListener('mouseout', boundHandleMouseLeave);

// Cleanup function to remove all event listeners
editor._destroyLinkAware = () => {
editor.off('changes', debouncedMarkUrls);
window.removeEventListener('keydown', boundUpdateCmdCtrlClass);
window.removeEventListener('keyup', boundUpdateCmdCtrlClass);
editorWrapper.removeEventListener('click', boundHandleClick);
editorWrapper.removeEventListener('mouseover', boundHandleMouseEnter);
editorWrapper.removeEventListener('mouseout', boundHandleMouseLeave);
};
}

export { setupLinkAware };
Loading
Loading