瀏覽代碼

feat: code editor autocomplete

Ahmad Kholid 2 年之前
父節點
當前提交
20476c063f

+ 5 - 0
src/components/newtab/shared/SharedCodemirror.vue

@@ -124,4 +124,9 @@ onBeforeUnmount(() => {
   font-family: 'Source Code Pro', Fira code, Fira Mono, Consolas, Menlo, Courier,
     monospace !important;
 }
+
+.cm-tooltip-autocomplete {
+  margin-left: -385px;
+  margin-top: -22px;
+}
 </style>

+ 16 - 0
src/components/newtab/shared/SharedConditionBuilder/ConditionBuilderInputs.vue

@@ -37,6 +37,7 @@
           <shared-codemirror
             v-if="name === 'code'"
             v-model="inputsData[index].data[name]"
+            :extensions="codemirrorExts"
             class="code-condition mt-2"
             style="margin-left: 0"
           />
@@ -72,7 +73,13 @@
 import { ref, watch, defineAsyncComponent } from 'vue';
 import { nanoid } from 'nanoid';
 import { useI18n } from 'vue-i18n';
+import { autocompletion } from '@codemirror/autocomplete';
 import cloneDeep from 'lodash.clonedeep';
+import {
+  automaFuncsSnippets,
+  automaFuncsCompletion,
+  completeFromGlobalScope,
+} from '@/utils/codeEditorAutocomplete';
 import { conditionBuilder } from '@/utils/shared';
 import EditAutocomplete from '../../workflow/edit/EditAutocomplete.vue';
 
@@ -92,6 +99,15 @@ const props = defineProps({
 });
 const emit = defineEmits(['update']);
 
+const autocompleteList = [automaFuncsSnippets.automaRefData];
+const codemirrorExts = [
+  autocompletion({
+    override: [
+      automaFuncsCompletion(autocompleteList),
+      completeFromGlobalScope,
+    ],
+  }),
+];
 const conditionOperators = conditionBuilder.compareTypes.reduce((acc, type) => {
   if (!acc[type.category]) acc[type.category] = [];
 

+ 20 - 0
src/components/newtab/workflow/edit/EditCreateElement.vue

@@ -77,6 +77,7 @@
           </div>
           <shared-codemirror
             v-model="blockData.javascript"
+            :extensions="codemirrorExts"
             lang="javascript"
             class="h-full"
           />
@@ -117,7 +118,13 @@
 </template>
 <script setup>
 import { reactive, watch, defineAsyncComponent } from 'vue';
+import { autocompletion } from '@codemirror/autocomplete';
 import cloneDeep from 'lodash.clonedeep';
+import {
+  automaFuncsSnippets,
+  automaFuncsCompletion,
+  completeFromGlobalScope,
+} from '@/utils/codeEditorAutocomplete';
 import EditInteractionBase from './EditInteractionBase.vue';
 
 const SharedCodemirror = defineAsyncComponent(() =>
@@ -153,6 +160,19 @@ const tabs = [
   { id: 'javascript', name: 'JavaScript' },
 ];
 
+const autocompleteList = [
+  automaFuncsSnippets.automaExecWorkflow,
+  automaFuncsSnippets.automaRefData,
+];
+const codemirrorExts = [
+  autocompletion({
+    override: [
+      automaFuncsCompletion(autocompleteList),
+      completeFromGlobalScope,
+    ],
+  }),
+];
+
 const blockData = reactive(cloneDeep(props.data));
 const state = reactive({
   showModal: false,

+ 14 - 90
src/components/newtab/workflow/edit/EditJavascriptCode.vue

@@ -120,8 +120,12 @@
 <script setup>
 import { watch, reactive, defineAsyncComponent } from 'vue';
 import { useI18n } from 'vue-i18n';
-import { syntaxTree } from '@codemirror/language';
-import { autocompletion, snippet } from '@codemirror/autocomplete';
+import { autocompletion } from '@codemirror/autocomplete';
+import {
+  automaFuncsSnippets,
+  automaFuncsCompletion,
+  completeFromGlobalScope,
+} from '@/utils/codeEditorAutocomplete';
 import { store } from '../../settings/jsBlockWrap';
 
 function modifyWhiteSpace() {
@@ -155,6 +159,7 @@ const availableFuncs = [
   },
   { name: 'automaResetTimeout()', id: 'automaresettimeout' },
 ];
+const autocompleteList = Object.values(automaFuncsSnippets).slice(0, 4);
 
 const state = reactive({
   activeTab: 'code',
@@ -169,96 +174,15 @@ function updateData(value) {
 function addScript() {
   state.preloadScripts.push({ src: '', removeAfterExec: true });
 }
-const dontCompleteIn = [
-  'String',
-  'TemplateString',
-  'LineComment',
-  'BlockComment',
-  'VariableDefinition',
-  'PropertyDefinition',
-];
-/* eslint-disable no-template-curly-in-string */
-function automaFuncsCompletion(context) {
-  const word = context.matchBefore(/\w*/);
-  const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);
-
-  if (
-    (word.from === word.to && !context.explicit) ||
-    dontCompleteIn.includes(nodeBefore.name)
-  )
-    return null;
-
-  return {
-    from: word.from,
-    options: [
-      {
-        label: 'automaNextBlock',
-        type: 'function',
-        apply: snippet('automaNextBlock(${data})'),
-        info: () => {
-          const container = document.createElement('div');
-
-          container.innerHTML = `
-            <code>automaNextBlock(<i>data</i>, <i>insert?</i>)</code>
-            <p class="mt-2">
-              Execute the next block
-              <a href="https://docs.automa.site/blocks/javascript-code.html#automanextblock-data" target="_blank" class="underline">
-                Read more
-              </a>
-            </p>
-          `;
-
-          return container;
-        },
-      },
-      {
-        label: 'automaSetVariable',
-        type: 'function',
-        apply: snippet("automaSetVariable('${name}', ${value})"),
-        info: () => {
-          const container = document.createElement('div');
-
-          container.innerHTML = `
-            <code>automaRefData(<i>name</i>, <i>value</i>)</code>
-            <p class="mt-2">
-              Set the value of a variable
-            </p>
-          `;
-
-          return container;
-        },
-      },
-      {
-        label: 'automaRefData',
-        type: 'function',
-        apply: snippet("automaRefData('${keyword}', '${path}')"),
-        info: () => {
-          const container = document.createElement('div');
-
-          container.innerHTML = `
-            <code>automaRefData(<i>keyword</i>, <i>path</i>)</code>
-            <p class="mt-2">
-              Use this function to
-              <a href="https://docs.automa.site/api-reference/reference-data.html" target="_blank" class="underline">
-                reference data
-              </a>
-            </p>
-          `;
 
-          return container;
-        },
-      },
-      {
-        label: 'automaResetTimeout',
-        type: 'function',
-        info: 'Reset javascript execution timeout',
-        apply: 'automaResetTimeout()',
-      },
+const codemirrorExts = [
+  autocompletion({
+    override: [
+      automaFuncsCompletion(autocompleteList),
+      completeFromGlobalScope,
     ],
-  };
-}
-
-const codemirrorExts = [autocompletion({ override: [automaFuncsCompletion] })];
+  }),
+];
 
 watch(
   () => state.code,

+ 158 - 0
src/utils/codeEditorAutocomplete.js

@@ -0,0 +1,158 @@
+/* eslint-disable no-template-curly-in-string */
+import { snippet } from '@codemirror/autocomplete';
+import { syntaxTree } from '@codemirror/language';
+
+const completePropertyAfter = ['PropertyName', '.', '?.'];
+const excludeProps = ['chrome', 'Mousetrap'];
+
+function completeProperties(from, object) {
+  const options = [];
+  /* eslint-disable-next-line */
+  for (const name in object) {
+    if (
+      !name.startsWith('__') &&
+      !name.startsWith('webpack') &&
+      !excludeProps.includes(name)
+    )
+      options.push({
+        label: name,
+        type: typeof object[name] === 'function' ? 'function' : 'variable',
+      });
+  }
+  return {
+    from,
+    options,
+    validFor: /^[\w$]*$/,
+  };
+}
+
+export const dontCompleteIn = [
+  'String',
+  'TemplateString',
+  'LineComment',
+  'BlockComment',
+  'VariableDefinition',
+  'PropertyDefinition',
+];
+export function completeFromGlobalScope(context) {
+  const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);
+
+  if (
+    completePropertyAfter.includes(nodeBefore.name) &&
+    nodeBefore.parent?.name === 'MemberExpression'
+  ) {
+    const object = nodeBefore.parent.getChild('Expression');
+    if (object?.name === 'VariableName') {
+      const from = /\./.test(nodeBefore.name) ? nodeBefore.to : nodeBefore.from;
+      const variableName = context.state.sliceDoc(object.from, object.to);
+      if (typeof window[variableName] === 'object')
+        return completeProperties(from, window[variableName]);
+    }
+  } else if (nodeBefore.name === 'VariableName') {
+    return completeProperties(nodeBefore.from, window);
+  } else if (context.explicit && !dontCompleteIn.includes(nodeBefore.name)) {
+    return completeProperties(context.pos, window);
+  }
+  return null;
+}
+
+export function automaFuncsCompletion(snippets) {
+  return function (context) {
+    const word = context.matchBefore(/\w*/);
+    const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);
+
+    if (
+      (word.from === word.to && !context.explicit) ||
+      dontCompleteIn.includes(nodeBefore.name)
+    )
+      return null;
+
+    return {
+      from: word.from,
+      options: snippets,
+    };
+  };
+}
+
+export const automaFuncsSnippets = {
+  automaNextBlock: {
+    label: 'automaNextBlock',
+    type: 'function',
+    apply: snippet('automaNextBlock(${data})'),
+    info: () => {
+      const container = document.createElement('div');
+
+      container.innerHTML = `
+        <code>automaNextBlock(<i>data</i>, <i>insert?</i>)</code>
+        <p class="mt-2">
+          Execute the next block
+          <a href="https://docs.automa.site/blocks/javascript-code.html#automanextblock-data" target="_blank" class="underline">
+            Read more
+          </a>
+        </p>
+      `;
+
+      return container;
+    },
+  },
+  automaSetVariable: {
+    label: 'automaSetVariable',
+    type: 'function',
+    apply: snippet("automaSetVariable('${name}', ${value})"),
+    info: () => {
+      const container = document.createElement('div');
+
+      container.innerHTML = `
+        <code>automaRefData(<i>name</i>, <i>value</i>)</code>
+        <p class="mt-2">
+          Set the value of a variable
+        </p>
+      `;
+
+      return container;
+    },
+  },
+  automaRefData: {
+    label: 'automaRefData',
+    type: 'function',
+    apply: snippet("automaRefData('${keyword}', '${path}')"),
+    info: () => {
+      const container = document.createElement('div');
+
+      container.innerHTML = `
+        <code>automaRefData(<i>keyword</i>, <i>path</i>)</code>
+        <p class="mt-2">
+          Use this function to
+          <a href="https://docs.automa.site/api-reference/reference-data.html" target="_blank" class="underline">
+            reference data
+          </a>
+        </p>
+      `;
+
+      return container;
+    },
+  },
+  automaResetTimeout: {
+    label: 'automaResetTimeout',
+    type: 'function',
+    info: 'Reset javascript execution timeout',
+    apply: 'automaResetTimeout()',
+  },
+  automaExecWorkflow: {
+    label: 'automaExecWorkflow',
+    type: 'function',
+    apply: snippet("automaExecWorkflow({ id: '${workflowId}' })"),
+    info: () => {
+      const container = document.createElement('div');
+
+      container.innerHTML = `
+        <code>automaRefData(<i>options</i>)</code>
+        <p class="mt-2">
+          Execute a workflow
+        </p>
+      `;
+
+      return container;
+    },
+  },
+};