add codemirror

This commit is contained in:
锦恢 2025-06-01 15:26:26 +08:00
parent 97f89b0833
commit 34bc18085a
6 changed files with 401 additions and 142 deletions

View File

@ -7,6 +7,7 @@
- 解决 issue#21 vscode插件界面bug在高度有限情况下无法通过滚动完全显示连接按钮。
- 解决 issue#21 最后一个标签页关闭并恢复默认页面。
- 解决 issue#22 工具模块UI异常现在 openmcp 支持解析 pydantic 进行 typing 的 python mcp 了。
- 优化对象输入框,现在对象输入框具有语法高亮和受限度的自动补全了。
## [main] 0.1.1
- 修复 SSH 连接 Ubuntu 的情况下的部分 bug

185
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "openmcp",
"version": "0.1.1",
"version": "0.1.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openmcp",
"version": "0.1.1",
"version": "0.1.2",
"workspaces": [
"service",
"renderer",
@ -510,6 +510,109 @@
"node": ">=6.9.0"
}
},
"node_modules/@codemirror/autocomplete": {
"version": "6.18.6",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz",
"integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz",
"integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.27.0",
"@lezer/common": "^1.1.0"
}
},
"node_modules/@codemirror/lang-json": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
"integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/json": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.0.tgz",
"integrity": "sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.1.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/lint": {
"version": "6.8.5",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz",
"integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.35.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/search": {
"version": "6.5.11",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz",
"integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/state": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/theme-one-dark": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz",
"integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/highlight": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
"version": "6.37.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.37.1.tgz",
"integrity": "sha512-Qy4CAUwngy/VQkEz0XzMKVRcckQuqLYWKqVpDDDghBe5FSXSqfVrJn49nw3ePZHxRUz4nRmb05Lgi+9csWo4eg==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.5.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@ -1460,6 +1563,41 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@lezer/common": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
"license": "MIT"
},
"node_modules/@lezer/highlight": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/json": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/lr": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@malept/cross-spawn-promise": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz",
@ -1515,6 +1653,12 @@
"node": ">=10"
}
},
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"license": "MIT"
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.1.tgz",
@ -4209,6 +4353,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/codemirror": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
"integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -4549,6 +4708,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -9996,6 +10161,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/style-mod": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
"license": "MIT"
},
"node_modules/sumchecker": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
@ -10883,6 +11054,12 @@
"typescript": ">=5.0.0"
}
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
},
"node_modules/wasm-feature-detect": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz",
@ -11367,6 +11544,10 @@
"name": "@openmcp/renderer",
"version": "0.1.0",
"dependencies": {
"@codemirror/autocomplete": "^6.18.6",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/theme-one-dark": "^6.1.2",
"codemirror": "^6.0.1",
"core-js": "^3.8.3",
"element-plus": "^2.9.9",
"katex": "^0.16.21",

View File

@ -16,6 +16,10 @@
"type-check": "vue-tsc --build"
},
"dependencies": {
"@codemirror/autocomplete": "^6.18.6",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/theme-one-dark": "^6.1.2",
"codemirror": "^6.0.1",
"core-js": "^3.8.3",
"element-plus": "^2.9.9",
"katex": "^0.16.21",

View File

@ -221,4 +221,10 @@ a {
.el-dropdown-menu__item:hover {
background-color: var(--background) !important;
}
/* codemirror */
.ͼo,
.ͼo .cm-gutters {
background-color: transparent !important;
}

View File

@ -1,147 +1,199 @@
<template>
<div class="k-input-object">
<textarea ref="textareaRef" v-model="inputValue" class="k-input-object__textarea"
:class="{ 'is-invalid': isInvalid }" @input="handleInput" @blur="handleBlur"
@keydown="handleKeydown"
:placeholder="props.placeholder"
></textarea>
</div>
<div v-if="errorMessage" class="k-input-object__error">
{{ errorMessage }}
<div :ref="el => editorContainer = el" class="k-input-object__editor"></div>
<div v-if="errorMessage" class="k-input-object__error">
{{ errorMessage }}
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watch, nextTick } from 'vue';
import { debounce } from 'lodash';
<script lang="ts" setup>
import { ref, onMounted, watch, type PropType } from 'vue'
import { EditorView, basicSetup } from 'codemirror'
import type { Completion, CompletionContext } from "@codemirror/autocomplete"
import { jsonLanguage } from "@codemirror/lang-json"
export default defineComponent({
name: 'KInputObject',
props: {
modelValue: {
type: Object,
default: () => ({})
},
placeholder: {
type: String,
default: '请输入 JSON 对象'
},
debounceTime: {
type: Number,
default: 500
}
import { json } from '@codemirror/lang-json'
import { oneDark } from '@codemirror/theme-one-dark'
import { debounce } from 'lodash'
const props = defineProps({
modelValue: {
type: Object,
default: () => ({})
},
emits: ['update:modelValue', 'parse-error'],
setup(props, { emit }) {
const textareaRef = ref<HTMLTextAreaElement | null>(null)
const inputValue = ref<string>(JSON.stringify(props.modelValue, null, 2))
const isInvalid = ref<boolean>(false)
const errorMessage = ref<string>('')
//
const debouncedParse = debounce((value: string) => {
if (value.trim() === '') {
errorMessage.value = '';
isInvalid.value = false;
emit('update:modelValue', undefined);
return;
}
try {
const parsed = JSON.parse(value);
isInvalid.value = false;
errorMessage.value = '';
emit('update:modelValue', parsed);
} catch (error) {
isInvalid.value = true;
errorMessage.value = 'JSON 解析错误: ' + (error as Error).message;
emit('parse-error', error);
}
}, props.debounceTime)
const handleInput = () => {
debouncedParse(inputValue.value)
}
const handleBlur = () => {
//
debouncedParse.flush()
}
// modelValue
watch(
() => props.modelValue,
(newVal) => {
const currentParsed = tryParse(inputValue.value)
if (!isDeepEqual(currentParsed, newVal)) {
inputValue.value = JSON.stringify(newVal, null, 2)
}
},
{ deep: true }
)
// JSON
const tryParse = (value: string): any => {
try {
return JSON.parse(value)
} catch {
return undefined
}
}
//
const isDeepEqual = (obj1: any, obj2: any): boolean => {
return JSON.stringify(obj1) === JSON.stringify(obj2)
}
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === '{') {
event.preventDefault();
const start = textareaRef.value!.selectionStart;
const end = textareaRef.value!.selectionEnd;
const value = inputValue.value;
const newValue = value.substring(0, start) + '{\n \n}' + value.substring(end);
inputValue.value = newValue;
nextTick(() => {
textareaRef.value!.setSelectionRange(start + 2, start + 2);
});
} else if (event.key === '"') {
event.preventDefault();
const start = textareaRef.value!.selectionStart;
const end = textareaRef.value!.selectionEnd;
const value = inputValue.value;
const newValue = value.substring(0, start) + '""' + value.substring(end);
inputValue.value = newValue;
nextTick(() => {
textareaRef.value!.setSelectionRange(start + 1, start + 1);
});
} else if (event.key === 'Tab') {
event.preventDefault();
const start = textareaRef.value!.selectionStart;
const end = textareaRef.value!.selectionEnd;
const value = inputValue.value;
const newValue = value.substring(0, start) + ' ' + value.substring(end);
inputValue.value = newValue;
nextTick(() => {
textareaRef.value!.setSelectionRange(start + 1, start + 1);
});
} else if (event.key === 'Enter' && inputValue.value.trim() === '') {
event.preventDefault();
inputValue.value = '{}';
}
};
return {
textareaRef,
inputValue,
isInvalid,
errorMessage,
handleInput,
handleBlur,
handleKeydown,
props
}
placeholder: {
type: String,
default: '请输入 JSON 对象'
},
debounceTime: {
type: Number,
default: 500
},
schema: {
type: Object as PropType<{
type?: string;
properties?: Record<string, {
type: string;
description?: string;
default?: any;
enum?: any[];
}>;
required?: string[];
}>,
default: () => ({})
}
})
const emit = defineEmits(['update:modelValue', 'parse-error'])
const editorContainer = ref<any>(null);
const editorView = ref<EditorView | null>(null);
const isInvalid = ref(false);
const errorMessage = ref('');
const inputValue = ref<string>(JSON.stringify(props.modelValue, null, 2));
//
const debouncedParse = debounce((value: string) => {
if (value.trim() === '') {
errorMessage.value = '';
isInvalid.value = false;
emit('update:modelValue', undefined);
return;
}
try {
const parsed = JSON.parse(value);
isInvalid.value = false;
errorMessage.value = '';
emit('update:modelValue', parsed);
} catch (error) {
isInvalid.value = true;
errorMessage.value = 'JSON 解析错误: ' + (error as Error).message;
emit('parse-error', error);
}
}, props.debounceTime);
onMounted(() => {
if (editorContainer.value) {
const extensions = [
basicSetup,
json(),
oneDark,
EditorView.updateListener.of(update => {
if (update.docChanged) {
const value = update.state.doc.toString()
debouncedParse(value)
}
})
]
// schema
if (Object.keys(props.schema).length > 0) {
extensions.push(
jsonLanguage.data.of({
autocomplete: getJsonCompletion(props.schema)
})
)
}
editorView.value = new EditorView({
doc: JSON.stringify(props.modelValue, null, 2),
extensions,
parent: editorContainer.value
})
}
})
//
function getJsonCompletion(schema: any) {
return (context: CompletionContext) => {
//
const charBefore = context.state.sliceDoc(context.pos - 1, context.pos)
if (/[,.{}[\]:]/.test(charBefore)) return null
const word = context.matchBefore(/\w*/)
if (!word) return null
//
const state = context.state
const pos = context.pos
const line = state.doc.lineAt(pos)
const textBefore = line.text.slice(0, pos - line.from)
//
const quoteCount = (textBefore.match(/"/g) || []).length
if (quoteCount % 2 !== 0) return null
const completions: Completion[] = []
//
if (schema.properties) {
Object.entries(schema.properties).forEach(([key, value]) => {
completions.push({
label: key,
type: "property",
apply: `"${key}": ${getDefaultValue(value as any)}`
})
})
}
return {
from: word.from,
options: completions,
validFor: /^\w*$/
}
}
}
//
function getDefaultValue(property: any): string {
if (property.default !== undefined) {
return JSON.stringify(property.default)
}
switch (property.type) {
case 'string': return '""'
case 'number': return '0'
case 'boolean': return 'false'
case 'object': return '{}'
case 'array': return '[]'
default: return 'null'
}
}
// watch
// watch(
// () => props.modelValue,
// (newVal) => {
// const currentParsed = tryParse(inputValue.value)
// if (!isDeepEqual(currentParsed, newVal)) {
// const newContent = JSON.stringify(newVal, null, 2)
// editorView.value?.dispatch({
// changes: {
// from: 0,
// to: editorView.value.state.doc.length,
// insert: newContent
// }
// })
// }
// },
// { deep: true }
// )
// JSON
const tryParse = (value: string): any => {
try {
return JSON.parse(value)
} catch {
return undefined
}
}
//
const isDeepEqual = (obj1: any, obj2: any): boolean => {
return JSON.stringify(obj1) === JSON.stringify(obj2)
}
</script>
<style scoped>
@ -151,6 +203,7 @@ export default defineComponent({
border-radius: .5em;
margin-bottom: 15px;
display: flex;
flex-direction: column;
}
.k-input-object__textarea {
@ -174,6 +227,24 @@ export default defineComponent({
border-color: var(--el-color-error);
}
.k-input-object__error {
color: var(--el-color-error);
font-size: 12px;
margin-top: 4px;
}
.k-input-object__editor {
width: 100%;
border: 1px solid var(--el-border-color-light);
border-radius: 4px;
overflow: hidden;
background-color: var(--el-bg-color-overlay);
}
.k-input-object__editor.is-invalid {
border-color: var(--el-color-error);
}
.k-input-object__error {
color: var(--el-color-error);
font-size: 12px;

View File

@ -38,6 +38,7 @@
<k-input-object
v-else-if="property.type === 'object'"
v-model="tabStorage.formData[name]"
:schema="property"
:placeholder="property.description || t('enter') + ' ' + (property.title || name)"
/>
</el-form-item>
@ -84,14 +85,9 @@ if (!tabStorage.formData) {
tabStorage.formData = {};
}
console.log(tabStorage.formData);
const formRef = ref<FormInstance>();
const loading = ref(false);
const currentTool = computed(() => {
for (const client of mcpClientAdapter.clients) {
const tool = client.tools?.get(tabStorage.currentToolName);