简化RunCode编码参数结构,新增ScriptEditor组件,优化CodeEditor代码提示

This commit is contained in:
fofolee 2025-01-25 13:57:14 +08:00
parent 680360ef2c
commit 1d9a675803
8 changed files with 472 additions and 97 deletions

View File

@ -12,8 +12,36 @@
<script>
import * as monaco from "monaco-editor";
import { toRaw } from "vue";
import importAll from "js/common/importAll.js";
import { defineComponent } from "vue";
export default {
//
let apis = importAll(
require.context("!raw-loader!plugins/monaco/types/", false, /\.ts$/)
);
//
let languageCompletions = importAll(
require.context("plugins/monaco/completions/", false, /\.js$/)
);
let monacoCompletionProviders = {};
//
const typeDefinitions = {
javascript: ["lib.es5.d.ts", "common.d.ts", "node.api.d.ts", "electron.d.ts"],
quickcommand: [
"lib.es5.d.ts",
"common.d.ts",
"node.api.d.ts",
"electron.d.ts",
"quickcommand.api.d.ts",
"utools.api.d.ts",
"shortcode.api.d.ts",
],
};
export default defineComponent({
name: "CodeEditor",
props: {
// v-model
@ -24,12 +52,12 @@ export default {
//
language: {
type: String,
default: "plaintext",
default: "javascript",
},
//
height: {
type: String,
default: "200px",
default: "300px",
},
//
theme: {
@ -128,6 +156,18 @@ export default {
monaco.editor.setTheme(newValue ? "vs-dark" : "vs");
},
},
language: {
immediate: true,
handler(newValue) {
if (this.editor) {
const language = ["webjavascript", "quickcommand"].includes(newValue)
? "javascript"
: newValue;
monaco.editor.setModelLanguage(this.rawEditor().getModel(), language);
this.loadTypes();
}
},
},
},
mounted() {
this.initEditor();
@ -141,16 +181,22 @@ export default {
methods: {
//
initEditor() {
const language = ["webjavascript", "quickcommand"].includes(this.language)
? "javascript"
: this.language;
const options = {
...this.defaultOptions,
...this.options,
value: this.value || "",
language: this.language,
language,
theme: this.theme,
};
this.editor = monaco.editor.create(this.$refs.editorContainer, options);
this.listenEditorValue();
this.loadTypes();
this.registerLanguage();
//
this.$nextTick(() => {
@ -211,13 +257,129 @@ export default {
this.editor.focus();
}
},
registerLanguage() {
let that = this;
const identifierPattern = "([a-zA-Z_]\\w*)";
let getTokens = (code) => {
let identifier = new RegExp(identifierPattern, "g");
let tokens = [];
let array1;
while ((array1 = identifier.exec(code)) !== null) {
tokens.push(array1[0]);
}
return Array.from(new Set(tokens));
};
let createDependencyProposals = (range, keyWords, editor, curWord) => {
let keys = [];
// fix getValue of undefined
let tokens = getTokens(toRaw(editor).getModel()?.getValue());
//
for (const item of tokens) {
if (item != curWord.word) {
keys.push({
label: item,
kind: monaco.languages.CompletionItemKind.Text,
documentation: "",
insertText: item,
range: range,
});
}
}
//
Object.keys(keyWords).forEach((ItemKind) => {
keyWords[ItemKind].forEach((item) => {
keys.push({
label: item,
kind: monaco.languages.CompletionItemKind[ItemKind],
documentation: "",
insertText: item,
range: range,
});
});
});
return keys;
};
// applescript
monaco.languages.register({
id: "applescript",
});
//
Object.keys(languageCompletions).forEach((language) => {
//
if (monacoCompletionProviders[language]) return;
monaco.languages.registerCompletionItemProvider(language, {
provideCompletionItems: function (model, position) {
var word = model.getWordUntilPosition(position);
var range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn,
};
return {
suggestions: createDependencyProposals(
range,
languageCompletions[language].default,
toRaw(that.editor),
word
),
};
},
});
monacoCompletionProviders[language] = true;
});
},
loadTypes() {
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
noSemanticValidation: true,
noSyntaxValidation: false,
});
const options = {
target: monaco.languages.typescript.ScriptTarget.ES6,
allowNonTsExtensions: true,
allowJs: true,
};
// webjavascript 使
if (this.language === "webjavascript") {
monaco.languages.typescript.javascriptDefaults.setCompilerOptions(
options
);
return;
}
//
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
...options,
lib: [],
});
const files = typeDefinitions[this.language] || [];
const declarations = files.map((file) => {
try {
return require(`!raw-loader!plugins/monaco/types/${file}`).default;
} catch (e) {
console.warn(`Failed to load type definition: ${file}`, e);
return "";
}
});
if (declarations.length > 0) {
//
monaco.languages.typescript.javascriptDefaults.addExtraLib(
declarations.join("\n"),
"api.d.ts"
);
}
},
},
computed: {
showPlaceholder() {
return this.placeholder && (!this.value || this.value.trim() === "");
},
},
};
});
</script>
<style scoped>
@ -246,7 +408,6 @@ export default {
.placeholder {
font-size: 14px;
font-family: sans-serif;
color: #535353;
user-select: none;
font-style: italic;
opacity: 0;
@ -254,10 +415,6 @@ export default {
}
.code-editor:focus-within .placeholder {
opacity: 1;
}
.body--dark .placeholder {
color: #666;
opacity: 0.3;
}
</style>

View File

@ -12,7 +12,7 @@
</template>
<script>
import { defineComponent } from "vue";
import { defineComponent, defineAsyncComponent } from "vue";
// OptionEditorOptionEditor使
import VariableInput from "components/composer/common/VariableInput.vue";
import NumberInput from "components/composer/common/NumberInput.vue";
@ -22,8 +22,10 @@ import ButtonGroup from "components/composer/common/ButtonGroup.vue";
import ControlInput from "components/composer/common/ControlInput.vue";
import CheckGroup from "components/composer/common/CheckGroup.vue";
import CheckButton from "components/composer/common/CheckButton.vue";
import CodeEditor from "components/composer/common/CodeEditor.vue";
import { QInput, QSelect, QToggle, QCheckbox } from "quasar";
const CodeEditor = defineAsyncComponent(() =>
import("components/composer/common/CodeEditor.vue")
);
export default defineComponent({
name: "ParamInput",
@ -36,11 +38,11 @@ export default defineComponent({
ControlInput,
CheckGroup,
CheckButton,
CodeEditor,
QToggle,
QInput,
QSelect,
QCheckbox,
CodeEditor,
},
props: {
config: {

View File

@ -0,0 +1,266 @@
<template>
<div class="script-editor">
<!-- 代码编辑器 -->
<CodeEditor
:model-value="argvs.code"
@update:modelValue="updateArgvs('code', $event)"
:language="argvs.language"
/>
<div class="row q-col-gutter-sm">
<!-- 语言选择 -->
<q-select
:model-value="argvs.language"
@update:modelValue="updateArgvs('language', $event)"
:options="Object.keys(programs).slice(2, -1)"
label="编程语言"
filled
dense
class="col-6"
>
<template v-slot:append>
<q-avatar size="sm" square>
<img :src="programs[argvs.language].icon" />
</q-avatar>
</template>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<img width="24" :src="programs[scope.opt].icon" />
</q-item-section>
<q-item-section>
<q-item-label v-html="scope.opt" />
</q-item-section>
</q-item>
</template>
</q-select>
<!-- 编码设置 -->
<q-select
class="col-3"
filled
dense
:model-value="argvs.scriptCode"
@update:modelValue="updateArgvs('scriptCode', $event)"
label="脚本文件编码"
:options="charsetOptions"
emit-value
map-options
/>
<q-select
class="col-3"
filled
dense
:model-value="argvs.outputCode"
@update:modelValue="updateArgvs('outputCode', $event)"
label="命令行输出编码"
:options="charsetOptions"
emit-value
map-options
/>
</div>
<div class="row q-col-gutter-sm">
<div class="col-6">
<ArrayEditor
topLabel="脚本参数"
:model-value="argvs.args"
@update:modelValue="updateArgvs('args', $event)"
/>
</div>
<!-- 终端运行设置 -->
<div class="col-6">
<BorderLabel label="终端运行设置">
<div class="row q-col-gutter-sm">
<CheckButton
:model-value="!!argvs.runInTerminal"
@update:modelValue="toggleTerminal"
label="在终端中运行"
/>
<template v-if="argvs.runInTerminal">
<VariableInput
:model-value="argvs.runInTerminal.dir"
@update:modelValue="updateTerminal('dir', $event)"
:options="{
dialog: {
type: 'open',
properties: ['openDirectory'],
},
}"
label="运行目录"
/>
<div class="col">
<div class="row q-col-gutter-sm">
<q-select
:model-value="argvs.runInTerminal.windows"
@update:modelValue="updateTerminal('windows', $event)"
:options="windowsTerminalOptions"
label="Windows终端"
filled
dense
emit-value
map-options
class="col-6"
/>
<q-select
:model-value="argvs.runInTerminal.macos"
@update:modelValue="updateTerminal('macos', $event)"
:options="macosTerminalOptions"
label="macOS终端"
filled
dense
emit-value
map-options
class="col-6"
/>
</div>
</div>
</template>
</div>
</BorderLabel>
</div>
</div>
</div>
</template>
<script>
import { defineComponent } from "vue";
import { newVarInputVal } from "js/composer/varInputValManager";
import CodeEditor from "components/composer/common/CodeEditor.vue";
import VariableInput from "components/composer/common/VariableInput.vue";
import ArrayEditor from "components/composer/common/ArrayEditor.vue";
import BorderLabel from "components/composer/common/BorderLabel.vue";
import CheckButton from "components/composer/common/CheckButton.vue";
import { parseFunction, stringifyArgv } from "js/composer/formatString";
import programs from "js/options/programs";
export default defineComponent({
name: "ScriptEditor",
components: {
CodeEditor,
VariableInput,
ArrayEditor,
BorderLabel,
CheckButton,
},
props: {
modelValue: Object,
},
emits: ["update:modelValue"],
data() {
return {
defaultArgvs: {
code: "",
language: "python",
args: [],
scriptCode: null,
outputCode: null,
runInTerminal: null,
},
programs: programs,
windowsTerminalOptions: ["wt", "cmd"],
macosTerminalOptions: ["warp", "iterm", "terminal"],
charsetOptions: [
{ label: "自动", value: null },
{ label: "UTF-8", value: "utf-8" },
{ label: "GBK", value: "gbk" },
],
};
},
computed: {
argvs() {
return (
this.modelValue.argvs ||
this.parseCodeToArgvs(this.modelValue.code) ||
this.defaultArgvs
);
},
},
methods: {
parseCodeToArgvs(code) {
if (!code) return this.defaultArgvs;
try {
const variableFormatPaths = ["arg1.args[*]"];
const result = parseFunction(code, { variableFormatPaths });
if (!result) return this.defaultArgvs;
const [scriptCode, options] = result.argvs;
return {
code: scriptCode,
...options,
};
} catch (e) {
console.error("解析参数失败:", e);
return this.defaultArgvs;
}
},
generateCode(argvs = this.argvs) {
const options = {
language: argvs.language,
};
if (argvs.scriptCode) {
options.scriptCode = argvs.scriptCode;
}
if (argvs.outputCode) {
options.outputCode = argvs.outputCode;
}
if (argvs.args?.length) {
options.args = argvs.args;
}
if (argvs.runInTerminal) {
options.runInTerminal = { ...argvs.runInTerminal };
}
return `${this.modelValue.value}(${stringifyArgv(
argvs.code
)}, ${stringifyArgv(options)})`;
},
getSummary(argvs) {
return `运行${argvs.language}代码`;
},
updateArgvs(key, value) {
const newArgvs = { ...this.argvs, [key]: value };
this.updateModelValue(newArgvs);
},
toggleTerminal(value) {
const newArgvs = { ...this.argvs };
newArgvs.runInTerminal = value
? { dir: newVarInputVal("str", "") }
: null;
this.updateModelValue(newArgvs);
},
updateTerminal(key, value) {
const newTerminal = { ...this.argvs.runInTerminal, [key]: value };
const newArgvs = { ...this.argvs, runInTerminal: newTerminal };
this.updateModelValue(newArgvs);
},
updateModelValue(argvs) {
this.$emit("update:modelValue", {
...this.modelValue,
summary: this.getSummary(argvs),
code: this.generateCode(argvs),
argvs,
});
},
},
mounted() {
const argvs = this.modelValue.argvs || this.defaultArgvs;
if (!this.modelValue.code) {
this.updateModelValue(argvs);
}
},
});
</script>
<style scoped>
.script-editor {
display: flex;
flex-direction: column;
gap: 8px;
}
</style>

View File

@ -273,9 +273,8 @@ export default {
* @param options 选项
* @param options.language 编程语言
* @param options.args 脚本参数
* @param options.charset 编码
* @param options.charset.scriptCode 脚本编码
* @param options.charset.outputCode 输出编码
* @param options.scriptCode 脚本文件编码
* @param options.outputCode 命令行输出编码
* @param options.runInTerminal 终端运行参数不传则不在终端运行
* @param options.runInTerminal.dir 运行目录
* @param options.runInTerminal.windows windows使用的终端默认wt
@ -286,11 +285,20 @@ export default {
const isWin = window.utools.isWindows();
const {
language = isWin ? "cmd" : "shell",
charset = {},
args = [],
runInTerminal,
} = options;
if (!options.scriptCode) {
options.scriptCode = ["cmd", "powershell"].includes(language)
? "gbk"
: "utf-8";
}
if (!options.outputCode) {
options.outputCode = isWin ? "gbk" : "utf-8";
}
// true使
const runInTerminalOptions =
runInTerminal === true ? {} : runInTerminal;
@ -306,23 +314,20 @@ export default {
}
const argsStr = args.map(unescapeAndQuote).join(" ");
const defaultCharset =
isWin && ["cmd", "powershell"].includes(language) ? "gbk" : "utf-8";
const { scriptCode = defaultCharset, outputCode = defaultCharset } =
charset;
window.runCodeFile(
code,
{
...programs[language],
charset: { scriptCode, outputCode },
charset: {
scriptCode: options.scriptCode,
outputCode: options.outputCode,
},
scptarg: argsStr,
},
runInTerminalOptions,
(result, err) => (err ? reject(err) : reslove(result))
(result, err) => (err ? reject(err) : reslove(result)),
false
);
false;
});
};

View File

@ -55,3 +55,7 @@ export const SelectListEditor = defineAsyncComponent(() =>
export const ReturnEditor = defineAsyncComponent(() =>
import("components/composer/script/ReturnEditor.vue")
);
export const ScriptEditor = defineAsyncComponent(() =>
import("components/composer/script/ScriptEditor.vue")
);

View File

@ -311,6 +311,7 @@ export const browserCommands = {
{
label: "脚本内容",
component: "CodeEditor",
language: "webjavascript",
icon: "code",
width: 12,
placeholder: "输入JavaScript代码使用return返回结果",
@ -382,6 +383,7 @@ export const browserCommands = {
config: [
{
component: "CodeEditor",
language: "css",
icon: "style",
width: 12,
placeholder: "输入CSS代码",

View File

@ -1,26 +1,10 @@
import programs from "js/options/programs";
export const scriptCommands = {
label: "编程相关",
icon: "integration_instructions",
commands: [
{
value: "",
label: "赋值",
icon: "script",
outputVariable: "value",
saveOutput: true,
config: [
{
label: "值或表达式",
component: "VariableInput",
width: 12,
},
],
},
{
value: "injectJs",
label: "注入JS脚本",
label: "注入JS代码",
icon: "script",
neverHasOutput: true,
isExpression: true,
@ -28,6 +12,7 @@ export const scriptCommands = {
{
label: "JS脚本",
component: "CodeEditor",
language: "quickcommand",
placeholder:
"共享当前上下文支持utoolsquickcommandquickcomposer等接口",
width: 12,
@ -36,53 +21,10 @@ export const scriptCommands = {
},
{
value: "quickcommand.runCode",
label: "执行代码",
icon: "script",
label: "运行脚本",
component: "ScriptEditor",
desc: "运行各种编程语言的代码",
isAsync: true,
outputVariable: "result",
saveOutput: true,
config: [
{
label: "脚本",
component: "CodeEditor",
placeholder: "需要本机安装了对应的解释器/编译器",
width: 12,
},
{
component: "OptionEditor",
width: 12,
options: {
language: {
label: "语言",
component: "QSelect",
icon: "language",
options: Object.keys(programs).slice(2, -1),
width: 8,
},
runInTerminal: {
label: "终端运行",
icon: "terminal",
component: "CheckButton",
width: 4,
},
args: {
topLabel: "参数",
icon: "data_array",
component: "ArrayEditor",
width: 12,
},
charset: {
label: "编码",
icon: "abc",
component: "DictEditor",
options: {
optionKeys: ["scriptCode", "outputCode"],
},
width: 12,
},
},
},
],
},
{
value: "return",

View File

@ -655,9 +655,8 @@ interface quickcommandApi {
* @param options
* @param options.language cmd或是shell
* @param options.args
* @param options.charset utf-8gbk
* @param options.charset.scriptCode
* @param options.charset.outputCode
* @param options.scriptCode
* @param options.outputCode
* @param options.runInTerminal
* @param options.runInTerminal.dir
* @param options.runInTerminal.windows windows使用的终端wt
@ -700,10 +699,8 @@ interface quickcommandApi {
| "csharp"
| "c";
args?: string[];
charset?: {
scriptCode?: string;
outputCode?: string;
};
scriptCode?: string;
outputCode?: string;
runInTerminal?: {
dir?: string;
windows?: "wt" | "cmd";