Skip to main content
Glama
CodeViewer.vue6.39 kB
<template> <div ref="mainDivRef" :class=" clsx( 'flex flex-col w-full overflow-hidden relative', !disableScroll && 'max-h-full', ) " > <div v-if="showTitle" class="flex flex-row items-center justify-between p-xs text-base align-middle" :class="titleClasses" > <slot name="title"> <TruncateWithTooltip v-if="title">{{ title }}</TruncateWithTooltip> </slot> <div class="flex"> <IconButton v-if="allowCopy" :tooltip="copyTooltip" icon="clipboard-copy" iconTone="shade" tooltipPlacement="top" @click="copyCodeToClipboard" /> <slot name="actionButtons"></slot> </div> </div> <IconButton v-if="!showTitle && allowCopy" :tooltip="copyTooltip" icon="clipboard-copy" iconTone="shade" tooltipPlacement="top" :class=" clsx( 'absolute z-10 right-xs', mainDivTallEnoughForCopyIconPadding ? 'top-xs' : 'top-0', ) " @click="copyCodeToClipboard" /> <div :class=" clsx( 'w-full h-full overflow-auto scrollable', border && 'border', themeClasses('border-neutral-300', 'border-neutral-600'), ) " > <div ref="editorMountRef" class="w-full h-full overflow-auto scrollable" @keyup.stop @keydown.stop ></div> </div> </div> </template> <script setup lang="ts"> import * as _ from "lodash-es"; import { ref, computed, watch, PropType, onMounted, onBeforeMount, nextTick, } from "vue"; import { basicSetup, EditorView } from "codemirror"; import { StreamLanguage } from "@codemirror/language"; import { keymap } from "@codemirror/view"; import { indentWithTab } from "@codemirror/commands"; import { githubLight } from "@fsegurai/codemirror-theme-github-light"; import { githubDark } from "@fsegurai/codemirror-theme-github-dark"; import { EditorState, Compartment, Extension, StateEffect, } from "@codemirror/state"; import { properties as JsonModeParser } from "@codemirror/legacy-modes/mode/properties"; import { yaml as YamlModeParser } from "@codemirror/legacy-modes/mode/yaml"; import { diff as DiffModeParser } from "@codemirror/legacy-modes/mode/diff"; import clsx from "clsx"; import { IconButton, themeClasses, TruncateWithTooltip, useTheme, } from "@si/vue-lib/design-system"; import { javascript as CodemirrorJsLang } from "@codemirror/lang-javascript"; import { CodeLanguage } from "@/api/sdf/dal/code_view"; const props = defineProps({ code: { type: String }, codeLanguage: { type: String as PropType<CodeLanguage>, default: "unknown" }, // // could add validation fns? // // Format: "0.0px" fontSize: { type: String, default: "13px" }, // // Format: "0.0px" or "0%" height: { type: String }, showTitle: { type: Boolean }, allowCopy: { type: Boolean, default: true }, title: { type: String }, titleClasses: { type: String, default: "h-10 text-lg" }, border: { type: Boolean, default: false }, disableScroll: { type: Boolean }, copyTooltip: { type: String, default: "Copy code to clipboard" }, }); const numberOfLinesInCode = computed(() => { return (String(props.code).match(/\n/g) || "").length + 1; }); const mainDivRef = ref<HTMLElement>(); const mainDivTallEnoughForCopyIconPadding = computed( () => mainDivRef.value && mainDivRef.value?.getBoundingClientRect().height > 32, ); const { theme } = useTheme(); const editorMountRef = ref(); const readOnly = new Compartment(); let editorView: EditorView | undefined; // any new languages we want to support need to be added here const CODE_PARSER_LOOKUP = { diff: DiffModeParser, json: JsonModeParser, yaml: YamlModeParser, string: YamlModeParser, // TODO: what do we want to do here...? unknown: YamlModeParser, }; const editorThemeExtension = computed(() => { return { dark: githubDark, light: githubLight, }[theme.value]; }); const editorStyleExtension = computed(() => { const activeLineHighlight = theme.value === "dark" ? "#2d333b" : "#f6f8fa"; return EditorView.theme({ "&": { height: "100%", ..._.pick(props, "fontSize", "height"), }, ".cm-scroller": { overflow: "auto" }, ".cm-focused .cm-selectionBackground .cm-activeLine, .cm-selectionBackground, .cm-content .cm-activeLine ::selection": { backgroundColor: `${activeLineHighlight} !important` }, }); }); const javascriptLang = new Compartment(); const editorExtensionList = computed<Extension[]>(() => { const codeParser = props.codeLanguage === "javascript" ? javascriptLang.of(CodemirrorJsLang()) : StreamLanguage.define(CODE_PARSER_LOOKUP[props.codeLanguage]); return [ basicSetup, editorThemeExtension.value, editorStyleExtension.value, keymap.of([indentWithTab]), codeParser, readOnly.of(EditorState.readOnly.of(true)), EditorView.lineWrapping, ]; }); function initCodeMirrorEditor() { editorView = new EditorView({ state: EditorState.create({ // CodeMirror breaks with only one line of code doc: numberOfLinesInCode.value === 1 ? `${props.code}\n` : props.code, extensions: editorExtensionList.value, }), parent: editorMountRef.value, }); } function teardownCodeMirrorEditor() { editorView?.destroy(); } function syncEditorConfig() { editorView?.dispatch({ effects: StateEffect.reconfigure.of(editorExtensionList.value), }); } function syncEditorCode() { // this is what `CodeEditor` does, on any change it remounts the editor teardownCodeMirrorEditor(); initCodeMirrorEditor(); } onMounted(initCodeMirrorEditor); onBeforeMount(teardownCodeMirrorEditor); watch(editorExtensionList, syncEditorConfig, { immediate: true }); watch( () => props.code, () => { nextTick(() => { syncEditorCode(); }); }, ); // This doesn't work on IE, do we care? (is it polyfilled by our build system?) // RE ^^: https://www.youtube.com/watch?v=Ram7AKbtkGE function copyCodeToClipboard() { if (numberOfLinesInCode.value < 2) { navigator.clipboard.writeText(props.code as string); return; } if (!editorView) return; const code = editorView.state.doc.toString().trim(); navigator.clipboard.writeText(code); } </script>

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server