From d165a04377d68c1503dcffc2be10a0bd8204ed05 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Sat, 21 Jan 2023 18:12:34 +0530 Subject: [PATCH] feat: environment variable syntax highlighting --- .../SingleLineEditor/StyledWrapper.js | 13 +- .../src/components/SingleLineEditor/index.js | 7 +- .../src/pageComponents/Index/index.js | 2 + packages/bruno-app/src/pages/_app.js | 1 + .../src/utils/codemirror/brunoVarInfo.css | 28 +++ .../src/utils/codemirror/brunoVarInfo.js | 198 ++++++++++++++++++ 6 files changed, 246 insertions(+), 3 deletions(-) create mode 100644 packages/bruno-app/src/utils/codemirror/brunoVarInfo.css create mode 100644 packages/bruno-app/src/utils/codemirror/brunoVarInfo.js diff --git a/packages/bruno-app/src/components/SingleLineEditor/StyledWrapper.js b/packages/bruno-app/src/components/SingleLineEditor/StyledWrapper.js index 6e456820b..5ca8c019e 100644 --- a/packages/bruno-app/src/components/SingleLineEditor/StyledWrapper.js +++ b/packages/bruno-app/src/components/SingleLineEditor/StyledWrapper.js @@ -24,7 +24,18 @@ const StyledWrapper = styled.div` font-family: Inter, sans-serif !important; font-weight: 400; } - } + } + + .tooltip { + position: absolute; + top: 0; + left: 0; + background: #fff; + border: 1px solid #ccc; + border-radius: 3px; + padding: 5px; + font-size: 12px; + z-index: 100; } .cm-variable-valid{color: green} diff --git a/packages/bruno-app/src/components/SingleLineEditor/index.js b/packages/bruno-app/src/components/SingleLineEditor/index.js index fbeb8bb69..946dc1f32 100644 --- a/packages/bruno-app/src/components/SingleLineEditor/index.js +++ b/packages/bruno-app/src/components/SingleLineEditor/index.js @@ -24,6 +24,9 @@ class SingleLineEditor extends Component { lineNumbers: false, autofocus: true, mode: "brunovariables", + brunoVarInfo: { + variables: getEnvironmentVariables(this.props.collection), + }, extraKeys: { "Enter": () => { if (this.props.onRun) { @@ -65,8 +68,7 @@ class SingleLineEditor extends Component { 'Tab': () => {} }, }); - this.editor.setValue(this.props.value) - + this.editor.setValue(this.props.value); this.editor.on('change', (cm) => { this.props.onChange(cm.getValue()); }); @@ -76,6 +78,7 @@ class SingleLineEditor extends Component { componentDidUpdate(prevProps) { let variables = getEnvironmentVariables(this.props.collection); if (!isEqual(variables, this.variables)) { + this.editor.options.brunoVarInfo.variables = variables; this.addOverlay(); } } diff --git a/packages/bruno-app/src/pageComponents/Index/index.js b/packages/bruno-app/src/pageComponents/Index/index.js index d7e5d1016..c74d60c11 100644 --- a/packages/bruno-app/src/pageComponents/Index/index.js +++ b/packages/bruno-app/src/pageComponents/Index/index.js @@ -31,6 +31,8 @@ if (!SERVER_RENDERED) { require('codemirror-graphql/info'); require('codemirror-graphql/jump'); require('codemirror-graphql/mode'); + + require('utils/codemirror/brunoVarInfo'); } export default function Main() { diff --git a/packages/bruno-app/src/pages/_app.js b/packages/bruno-app/src/pages/_app.js index f0b5bbce5..7682c2c84 100644 --- a/packages/bruno-app/src/pages/_app.js +++ b/packages/bruno-app/src/pages/_app.js @@ -12,6 +12,7 @@ import '../styles/globals.css'; import 'tailwindcss/dist/tailwind.min.css'; import 'codemirror/lib/codemirror.css'; import 'graphiql/graphiql.min.css'; +import 'utils/codemirror/brunoVarInfo.css'; function SafeHydrate({ children }) { return
{typeof window === 'undefined' ? null : children}
; diff --git a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.css b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.css new file mode 100644 index 000000000..ce116defc --- /dev/null +++ b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.css @@ -0,0 +1,28 @@ +.CodeMirror-brunoVarInfo { + background: white; + border-radius: 2px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); + box-sizing: border-box; + font-size: 13px; + line-height: 16px; + margin: 8px -8px; + max-width: 800px; + opacity: 0; + overflow: hidden; + padding: 8px 8px; + position: fixed; + transition: opacity 0.15s; + z-index: 50; +} + +.CodeMirror-brunoVarInfo :first-child { + margin-top: 0; +} + +.CodeMirror-brunoVarInfo :last-child { + margin-bottom: 0; +} + +.CodeMirror-brunoVarInfo p { + margin: 1em 0; +} diff --git a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js new file mode 100644 index 000000000..dc0cb64ed --- /dev/null +++ b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js @@ -0,0 +1,198 @@ +/** + * Copyright (c) 2017, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file at https://github.com/graphql/codemirror-graphql/tree/v0.8.3 + */ + +let CodeMirror; +const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true; + +if (!SERVER_RENDERED) { + CodeMirror = require('codemirror'); + + const renderVarInfo = (token, options, cm, pos) => { + const str = token.string || ''; + + // str is of format {{variableName}}, extract variableName + const variableName = str.substring(2, str.length - 2); + + // get the value of the variable + const variableValue = options.variables[variableName] || ''; + + const into = document.createElement('div'); + const descriptionDiv = document.createElement('div'); + descriptionDiv.className = 'info-description'; + + descriptionDiv.appendChild(document.createTextNode(variableValue)); + into.appendChild(descriptionDiv); + + return into; + }; + + CodeMirror.defineOption('brunoVarInfo', false, function(cm, options, old) { + + if (old && old !== CodeMirror.Init) { + const oldOnMouseOver = cm.state.brunoVarInfo.onMouseOver; + CodeMirror.off(cm.getWrapperElement(), 'mouseover', oldOnMouseOver); + clearTimeout(cm.state.brunoVarInfo.hoverTimeout); + delete cm.state.brunoVarInfo; + } + + if (options) { + const state = (cm.state.brunoVarInfo = createState(options)); + state.onMouseOver = onMouseOver.bind(null, cm); + CodeMirror.on(cm.getWrapperElement(), 'mouseover', state.onMouseOver); + } + }); + + function createState(options) { + return { + options: + options instanceof Function + ? {render: options} + : options === true ? {} : options, + }; + } + + function getHoverTime(cm) { + const options = cm.state.brunoVarInfo.options; + return (options && options.hoverTime) || 50; + } + + function onMouseOver(cm, e) { + const state = cm.state.brunoVarInfo; + const target = e.target || e.srcElement; + + if (target.nodeName !== 'SPAN' || state.hoverTimeout !== undefined) { + return; + } + + if(target.className !== 'cm-variable-valid') { + return; + } + + const box = target.getBoundingClientRect(); + + const hoverTime = getHoverTime(cm); + state.hoverTimeout = setTimeout(onHover, hoverTime); + + const onMouseMove = function() { + clearTimeout(state.hoverTimeout); + state.hoverTimeout = setTimeout(onHover, hoverTime); + }; + + const onMouseOut = function() { + CodeMirror.off(document, 'mousemove', onMouseMove); + CodeMirror.off(cm.getWrapperElement(), 'mouseout', onMouseOut); + clearTimeout(state.hoverTimeout); + state.hoverTimeout = undefined; + }; + + const onHover = function() { + CodeMirror.off(document, 'mousemove', onMouseMove); + CodeMirror.off(cm.getWrapperElement(), 'mouseout', onMouseOut); + state.hoverTimeout = undefined; + onMouseHover(cm, box); + }; + + CodeMirror.on(document, 'mousemove', onMouseMove); + CodeMirror.on(cm.getWrapperElement(), 'mouseout', onMouseOut); + } + + function onMouseHover(cm, box) { + const pos = cm.coordsChar({ + left: (box.left + box.right) / 2, + top: (box.top + box.bottom) / 2, + }); + + const state = cm.state.brunoVarInfo; + const options = state.options; + const token = cm.getTokenAt(pos, true); + if (token) { + const brunoVarInfo = renderVarInfo(token, options, cm, pos); + if (brunoVarInfo) { + showPopup(cm, box, brunoVarInfo); + } + } + } + + function showPopup(cm, box, brunoVarInfo) { + const popup = document.createElement('div'); + popup.className = 'CodeMirror-brunoVarInfo'; + popup.appendChild(brunoVarInfo); + document.body.appendChild(popup); + + const popupBox = popup.getBoundingClientRect(); + const popupStyle = popup.currentStyle || window.getComputedStyle(popup); + const popupWidth = + popupBox.right - + popupBox.left + + parseFloat(popupStyle.marginLeft) + + parseFloat(popupStyle.marginRight); + const popupHeight = + popupBox.bottom - + popupBox.top + + parseFloat(popupStyle.marginTop) + + parseFloat(popupStyle.marginBottom); + + let topPos = box.bottom; + if ( + popupHeight > window.innerHeight - box.bottom - 15 && + box.top > window.innerHeight - box.bottom + ) { + topPos = box.top - popupHeight; + } + + if (topPos < 0) { + topPos = box.bottom; + } + + // make popup appear on top of cursor + if (topPos > 70) { + topPos = topPos - 70; + } + + let leftPos = Math.max(0, window.innerWidth - popupWidth - 15); + if (leftPos > box.left) { + leftPos = box.left; + } + + popup.style.opacity = 1; + popup.style.top = topPos + 'px'; + popup.style.left = leftPos + 'px'; + + let popupTimeout; + + const onMouseOverPopup = function() { + clearTimeout(popupTimeout); + }; + + const onMouseOut = function() { + clearTimeout(popupTimeout); + popupTimeout = setTimeout(hidePopup, 200); + }; + + const hidePopup = function() { + CodeMirror.off(popup, 'mouseover', onMouseOverPopup); + CodeMirror.off(popup, 'mouseout', onMouseOut); + CodeMirror.off(cm.getWrapperElement(), 'mouseout', onMouseOut); + + if (popup.style.opacity) { + popup.style.opacity = 0; + setTimeout(function() { + if (popup.parentNode) { + popup.parentNode.removeChild(popup); + } + }, 600); + } else if (popup.parentNode) { + popup.parentNode.removeChild(popup); + } + }; + + CodeMirror.on(popup, 'mouseover', onMouseOverPopup); + CodeMirror.on(popup, 'mouseout', onMouseOut); + CodeMirror.on(cm.getWrapperElement(), 'mouseout', onMouseOut); + } +}