Skip to content

Commit

Permalink
feat(ui): Request Panel > Code Editors > Highlight valid and invalid …
Browse files Browse the repository at this point in the history
…environment variables
  • Loading branch information
flawiddsouza committed Mar 4, 2024
1 parent e9bcba0 commit 4382878
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 82 deletions.
122 changes: 88 additions & 34 deletions packages/ui/src/components/CodeMirrorEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<script>
import { EditorView, highlightActiveLine, keymap, highlightSpecialChars, lineNumbers, highlightActiveLineGutter } from '@codemirror/view'
import { EditorState } from '@codemirror/state'
import { EditorState, StateEffect } from '@codemirror/state'
import { json } from '@codemirror/lang-json'
import { javascript } from '@codemirror/lang-javascript'
import { graphqlLanguage } from 'altair-codemirror-graphql'
Expand All @@ -13,6 +13,7 @@ import { indentOnInput, indentUnit, bracketMatching, foldGutter, syntaxHighlight
import { defaultKeymap, indentWithTab, history, historyKeymap, selectLine, selectLineBoundaryForward } from '@codemirror/commands'
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search'
import { codeMirrorSyntaxHighlighting } from '@/helpers'
import { envVarDecoration } from '@/utils/codemirror-extensions'
const styleOverrides = EditorView.theme({
'.cm-panel.cm-search input, .cm-panel.cm-search button, .cm-panel.cm-search label': {
Expand All @@ -39,7 +40,7 @@ document.onkeydown = function(evt) {
}
}
function createState(language, documentText, vueInstance) {
function getLanguageFuncAndHighlightStyle(language) {
let languageFunc = null
let highlightStyle = defaultHighlightStyle
Expand All @@ -57,39 +58,56 @@ function createState(language, documentText, vueInstance) {
highlightStyle = codeMirrorSyntaxHighlighting()
}
return { languageFunc, highlightStyle }
}
function getExtensions(vueInstance, language) {
const { languageFunc, highlightStyle } = getLanguageFuncAndHighlightStyle(language)
// languageFunc will be null for plain text & unsupported languages,
// so we filter it out, to prevent errors
const languageArray = [
languageFunc
].filter(Boolean)
return [
...languageArray,
syntaxHighlighting(highlightStyle, { fallback: true }),
lineNumbers(),
highlightActiveLineGutter(),
foldGutter({ openText: '', closedText: '' }),
closeBrackets(),
bracketMatching(),
indentOnInput(),
highlightActiveLine(),
history(),
highlightSpecialChars(),
highlightSelectionMatches(),
indentUnit.of(' '), // 4 spaces
EditorView.lineWrapping,
EditorView.updateListener.of(v => {
if(v.docChanged) {
vueInstance.emitted = true
vueInstance.$emit('update:modelValue', v.state.doc.toString())
}
}),
styleOverrides,
keymap.of([
...defaultKeymap,
...historyKeymap,
indentWithTab,
...searchKeymap,
selectLineKeyMap
]),
vueInstance.readonly ? EditorState.readOnly.of(true) : EditorState.readOnly.of(false),
envVarDecoration(vueInstance.envVariables),
]
}
function createState(language, documentText, vueInstance) {
return EditorState.create({
doc: documentText,
extensions: [
languageFunc,
syntaxHighlighting(highlightStyle, { fallback: true }),
lineNumbers(),
highlightActiveLineGutter(),
foldGutter({ openText: '', closedText: '' }),
closeBrackets(),
bracketMatching(),
indentOnInput(),
highlightActiveLine(),
history(),
highlightSpecialChars(),
highlightSelectionMatches(),
indentUnit.of(' '), // 4 spaces
EditorView.lineWrapping,
EditorView.updateListener.of(v => {
if(v.docChanged) {
vueInstance.emitted = true
vueInstance.$emit('update:modelValue', v.state.doc.toString())
}
}),
styleOverrides,
keymap.of([
...defaultKeymap,
...historyKeymap,
indentWithTab,
...searchKeymap,
selectLineKeyMap
]),
vueInstance.readonly ? EditorState.readOnly.of(true) : EditorState.readOnly.of(false),
]
extensions: getExtensions(vueInstance, language)
})
}
Expand All @@ -107,6 +125,10 @@ export default {
type: Boolean,
default: false
},
envVariables: {
type: Object,
default: () => ({})
},
},
data() {
return {
Expand All @@ -121,7 +143,17 @@ export default {
} else {
this.emitted = false
}
}
},
envVariables: {
deep: true,
handler() {
if (this.editor) {
this.editor.dispatch({
effects: StateEffect.reconfigure.of(getExtensions(this, this.lang))
})
}
}
},
},
methods: {
setValue(value) {
Expand Down Expand Up @@ -172,4 +204,26 @@ export default {
.code-mirror-editor .cm-editor {
height: 100%;
}
.code-mirror-editor .valid-env-var {
background-color: var(--valid-env-highlight-background-color);
border-radius: 3px;
padding: 2px 0;
color: var(--valid-env-highlight-color);
}
.code-mirror-editor .valid-env-var * {
color: var(--valid-env-highlight-color) !important;
}
.code-mirror-editor .invalid-env-var {
background-color: var(--invalid-env-highlight-background-color);
border-radius: 3px;
padding: 2px 0;
color: var(--invalid-env-highlight-color);
}
.code-mirror-editor .invalid-env-var * {
color: var(--invalid-env-highlight-color) !important;
}
</style>
46 changes: 3 additions & 43 deletions packages/ui/src/components/CodeMirrorSingleLine.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,10 @@
</template>

<script>
import { EditorView, keymap, placeholder, ViewPlugin, Decoration } from '@codemirror/view'
import { EditorState, RangeSetBuilder, StateEffect } from '@codemirror/state'
import { EditorView, keymap, placeholder } from '@codemirror/view'
import { EditorState, StateEffect } from '@codemirror/state'
import { history, historyKeymap } from '@codemirror/commands'
const envVarDecoration = envVariables => {
return ViewPlugin.fromClass(class {
decorations
constructor(view) {
this.decorations = this.highlightEnvVariables(view, envVariables)
}
update(update) {
if (update.docChanged || update.viewportChanged) {
this.decorations = this.highlightEnvVariables(update.view, envVariables)
}
}
highlightEnvVariables(view, envVariables) {
const builder = new RangeSetBuilder()
const re = /{{\s*(\w+)\s*}}/g
for (let { from, to } of view.visibleRanges) {
const range = view.state.doc.sliceString(from, to)
let match
while ((match = re.exec(range))) {
const start = from + match.index
const end = start + match[0].length
const varName = match[1]
const isInEnv = varName in envVariables
const className = isInEnv ? 'valid-env-var' : 'invalid-env-var'
const titleText = isInEnv ? envVariables[varName] : 'Environment variable not found'
const decoration = Decoration.mark({
class: className,
attributes: {title: titleText}
})
builder.add(start, end, decoration)
}
}
return builder.finish()
}
}, {
decorations: v => v.decorations
})
}
import { envVarDecoration } from '@/utils/codemirror-extensions'
function getExtensions(vueInstance) {
return [
Expand Down
37 changes: 32 additions & 5 deletions packages/ui/src/components/RequestPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -101,23 +101,50 @@
</tr>
</table>
</div>
<div v-if="activeTab.body.mimeType === 'text/plain'">
<textarea v-model="activeTab.body.text" style="width: 100%; padding: 0.5rem;" spellcheck="false"></textarea>
<div v-if="activeTab.body.mimeType === 'text/plain'" class="oy-a">
<CodeMirrorEditor
v-model="activeTab.body.text"
lang="text"
:env-variables="activeTabEnvironmentResolved"
class="code-editor"
:key="'code-mirror-editor-' + activeTab._id + '-' + refreshCodeMirrorEditors"
></CodeMirrorEditor>
</div>
<div v-if="activeTab.body.mimeType === 'application/json'" class="oy-a">
<CodeMirrorEditor v-model="activeTab.body.text" lang="json" class="code-editor" :key="'code-mirror-editor-' + activeTab._id + '-' + refreshCodeMirrorEditors" ref="jsonEditor"></CodeMirrorEditor>
<CodeMirrorEditor
v-model="activeTab.body.text"
lang="json"
:env-variables="activeTabEnvironmentResolved"
class="code-editor"
:key="'code-mirror-editor-' + activeTab._id + '-' + refreshCodeMirrorEditors"
ref="jsonEditor"
></CodeMirrorEditor>
</div>
<div class="request-panel-body-footer" v-if="activeTab.body.mimeType === 'application/json'">
<button class="button" @click="beautifyJSON">Beautify JSON</button>
</div>
<div style="display: grid; grid-template-rows: 1fr 130px auto; height: 100%; overflow: auto;" v-if="activeTab.body.mimeType === 'application/graphql'">
<div class="oy-a" style="min-height: 130px;">
<CodeMirrorEditor v-model="graphql.query" lang="graphql" class="code-editor" :key="'code-mirror-editor1-' + activeTab._id + '-' + refreshCodeMirrorEditors" ref="graphqlEditor"></CodeMirrorEditor>
<CodeMirrorEditor
v-model="graphql.query"
lang="graphql"
:env-variables="activeTabEnvironmentResolved"
class="code-editor"
:key="'code-mirror-editor1-' + activeTab._id + '-' + refreshCodeMirrorEditors"
ref="graphqlEditor"
></CodeMirrorEditor>
</div>
<div style="margin-top: 0.5rem;display: grid; grid-template-rows: auto 1fr;">
<div style="margin-bottom: 0.3rem; user-select: none;">Query Variables</div>
<div class="oy-a">
<CodeMirrorEditor v-model="graphql.variables" lang="json" class="code-editor" :key="'code-mirror-editor2-' + activeTab._id + '-' + refreshCodeMirrorEditors" ref="jsonEditor"></CodeMirrorEditor>
<CodeMirrorEditor
v-model="graphql.variables"
lang="json"
:env-variables="activeTabEnvironmentResolved"
class="code-editor"
:key="'code-mirror-editor2-' + activeTab._id + '-' + refreshCodeMirrorEditors"
ref="jsonEditor"
></CodeMirrorEditor>
</div>
</div>
<div class="request-panel-body-footer">
Expand Down
43 changes: 43 additions & 0 deletions packages/ui/src/utils/codemirror-extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ViewPlugin, Decoration } from '@codemirror/view'
import { RangeSetBuilder } from '@codemirror/state'

export function envVarDecoration(envVariables) {
return ViewPlugin.fromClass(class {
decorations

constructor(view) {
this.decorations = this.highlightEnvVariables(view, envVariables)
}

update(update) {
if (update.docChanged || update.viewportChanged) {
this.decorations = this.highlightEnvVariables(update.view, envVariables)
}
}

highlightEnvVariables(view, envVariables) {
const builder = new RangeSetBuilder()
const re = /{{\s*(\w+)\s*}}/g
for (const { from, to } of view.visibleRanges) {
const range = view.state.doc.sliceString(from, to)
let match
while ((match = re.exec(range))) {
const start = from + match.index
const end = start + match[0].length
const varName = match[1]
const isInEnv = varName in envVariables
const className = isInEnv ? 'valid-env-var' : 'invalid-env-var'
const titleText = isInEnv ? envVariables[varName] : 'Environment variable not found'
const decoration = Decoration.mark({
class: className,
attributes: {title: titleText}
})
builder.add(start, end, decoration)
}
}
return builder.finish()
}
}, {
decorations: v => v.decorations
})
}

0 comments on commit 4382878

Please sign in to comment.