Переглянути джерело

Editor: add rule completion

bdbai 3 роки тому
батько
коміт
c5a6065ab5

+ 113 - 1
Maple.App/MonacoEditor/src/completion.ts

@@ -263,6 +263,7 @@ function completeProxy(
 
     return []
 }
+
 function completeProxyGroup(
     model: monaco.editor.ITextModel,
     position: monaco.Position,
@@ -316,7 +317,7 @@ function completeProxyGroup(
     if (currentKv !== undefined && position.column >= currentKv.valueStartCol) {
         // Want a property value
         const range = new monaco.Range(lineId, currentKv.valueStartCol, lineId, currentKv.valueStartCol + currentKv.value.length)
-        switch (currentKv.value) {
+        switch (currentKv.key) {
             case facts.GROUP_PROPERTY_KEY_METHOD:
                 return facts.KNOWN_GROUP_METHODS.map(m => ({
                     label: m,
@@ -375,6 +376,114 @@ function completeProxyGroup(
     return propertyKeySuggestions.concat(proxyOrGroupNameSuggestions)
 }
 
+function completeRule(model: monaco.editor.ITextModel,
+    position: monaco.Position,
+    struct: ILeafConfStruct,
+): monaco.languages.CompletionItem[] {
+    const lineId = position.lineNumber
+    const line = trimComment(model.getLineContent(lineId))
+    const args = splitByComma(line, 1)
+    const currentArgId = line.slice(0, position.column - 1).split(',').length - 1
+
+    if (currentArgId === 0) {
+        if (args.length > 1) {
+            const range = new monaco.Range(
+                lineId,
+                Math.min(position.column, args[0].startCol),
+                lineId,
+                Math.max(position.column, args[0].startCol + args[0].text.length),
+            )
+            return facts.RULE_TYPES.map(p => ({
+                label: p.name,
+                kind: monaco.languages.CompletionItemKind.Function,
+                insertText: p.name,
+                documentation: p.desc,
+                range,
+            }))
+        } else {
+            const range = new monaco.Range(
+                lineId,
+                Math.min(args[0].startCol),
+                lineId,
+                args[0].startCol + args[0].text.length,
+            )
+            return facts.RULE_TYPES.map(p => ({
+                label: p.name,
+                kind: monaco.languages.CompletionItemKind.Function,
+                insertText: p.snippet,
+                insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
+                documentation: p.desc,
+                range,
+            }))
+        }
+    }
+
+    const ruleType = args[0].text.trim()
+    if (ruleType === facts.RULE_TYPE_FINAL && currentArgId === 1 || currentArgId === 2) {
+        const range = new monaco.Range(lineId, args[currentArgId].startCol, lineId, args[currentArgId].startCol + args[currentArgId].text.length)
+        return collectProxyOrGroupSuggestions(model, struct, range)
+    }
+
+    if (ruleType === facts.RULE_TYPE_FINAL && currentArgId > 1 || currentArgId > 2) {
+        return []
+    }
+
+    if (ruleType === facts.RULE_TYPE_EXTERNAL) {
+        let colonPos = args[currentArgId].text.indexOf(':')
+        if (colonPos === -1) {
+            const range = new monaco.Range(
+                lineId,
+                Math.min(position.column, args[currentArgId].startCol),
+                lineId,
+                Math.max(position.column, args[currentArgId].startCol + args[currentArgId].text.length),
+            )
+            return [facts.RULE_EXTERNAL_SOURCE_SITE, facts.RULE_EXTERNAL_SOURCE_MMDB].map(p => ({
+                label: p,
+                kind: monaco.languages.CompletionItemKind.Field,
+                insertText: p + ':',
+                documentation: 'TODO: doc',
+                range,
+            }))
+        }
+        const colonCol = args[currentArgId].startCol + colonPos
+        if (position.column > colonCol) {
+            return []
+        }
+
+        const range = new monaco.Range(
+            lineId,
+            Math.min(position.column, args[currentArgId].startCol),
+            lineId,
+            Math.max(colonCol),
+        )
+        return [facts.RULE_EXTERNAL_SOURCE_SITE, facts.RULE_EXTERNAL_SOURCE_MMDB].map(p => ({
+            label: p,
+            kind: monaco.languages.CompletionItemKind.Field,
+            insertText: p,
+            documentation: 'TODO: doc',
+            range,
+        }))
+    }
+
+    if (ruleType === facts.RULE_TYPE_NETWORK) {
+        const range = new monaco.Range(
+            lineId,
+            Math.min(position.column, args[currentArgId].startCol),
+            lineId,
+            Math.max(position.column, args[currentArgId].startCol + args[currentArgId].text.length),
+        )
+        return [facts.RULE_NETWORK_TCP, facts.RULE_NETWORK_UDP].map(p => ({
+            label: p,
+            kind: monaco.languages.CompletionItemKind.EnumMember,
+            insertText: p,
+            documentation: 'TODO: doc',
+            range,
+        }))
+    }
+
+    return []
+}
+
 export const completionProvider: monaco.languages.CompletionItemProvider = {
     triggerCharacters: [' ', '[', ',', '='],
     provideCompletionItems(model, position) {
@@ -410,6 +519,9 @@ export const completionProvider: monaco.languages.CompletionItemProvider = {
             case facts.SECTION_PROXY_GROUP:
                 suggestions = completeProxyGroup(model, position, struct)
                 break
+            case facts.SECTION_RULE:
+                suggestions = completeRule(model, position, struct)
+                break
         }
         return { suggestions, incomplete: true }
     },

+ 17 - 7
Maple.App/MonacoEditor/src/facts.ts

@@ -270,6 +270,7 @@ export const PROXY_GROUP_PROPERTY_KEY_MAP: Record<string, IProxyPropertyKeyDef>
 export interface IRuleTypeDef {
     name: string,
     desc: string,
+    snippet: string,
 }
 
 export const RULE_TYPE_IP_CIDR = 'IP-CIDR'
@@ -278,17 +279,26 @@ export const RULE_TYPE_DOMAIN_SUFFIX = 'DOMAIN-SUFFIX'
 export const RULE_TYPE_DOMAIN_KEYWORD = 'DOMAIN-KEYWORD'
 export const RULE_TYPE_GEOIP = 'GEOIP'
 export const RULE_TYPE_EXTERNAL = 'EXTERNAL'
+export const RULE_TYPE_PORT_RANGE = 'PORT-RANGE'
+export const RULE_TYPE_NETWORK = 'NETWORK'
+export const RULE_TYPE_INBOUND_TAG = 'INBOUND-TAG'
 export const RULE_TYPE_FINAL = 'FINAL'
 
 export const RULE_TYPES: IRuleTypeDef[] = [
-    { name: RULE_TYPE_IP_CIDR, desc: '' },
-    { name: RULE_TYPE_DOMAIN, desc: '' },
-    { name: RULE_TYPE_DOMAIN_SUFFIX, desc: '' },
-    { name: RULE_TYPE_DOMAIN_KEYWORD, desc: '' },
-    { name: RULE_TYPE_GEOIP, desc: '' },
-    { name: RULE_TYPE_EXTERNAL, desc: '' },
-    { name: RULE_TYPE_FINAL, desc: '' },
+    { name: RULE_TYPE_IP_CIDR, desc: '', snippet: 'IP-CIDR, ${1:8.8.8.8/24}, ${2:proxy}' },
+    { name: RULE_TYPE_DOMAIN, desc: '', snippet: 'DOMAIN, ${1:www.google.com}, ${2:proxy}' },
+    { name: RULE_TYPE_DOMAIN_SUFFIX, desc: '', snippet: 'DOMAIN-SUFFIX, ${1:example.com}, ${2:proxy}' },
+    { name: RULE_TYPE_DOMAIN_KEYWORD, desc: '', snippet: 'DOMAIN-KEYWORD, ${1:keyword}, ${2:proxy}' },
+    { name: RULE_TYPE_GEOIP, desc: '', snippet: 'GEOIP, ${1:us}, ${2:proxy}' },
+    { name: RULE_TYPE_EXTERNAL, desc: '', snippet: 'EXTERNAL, ${1|site:geolocation-cn,site:geosite.dat:category-ads-all,mmdb:cn|}, ${2:proxy}' },
+    { name: RULE_TYPE_PORT_RANGE, desc: '', snippet: 'PORT-RANGE, ${1:8000-9000}, ${2:proxy}' },
+    { name: RULE_TYPE_NETWORK, desc: '', snippet: 'NETWORK, ${1|TCP,UDP|}, ${2:proxy}' },
+    { name: RULE_TYPE_INBOUND_TAG, desc: '', snippet: 'INBOUND-TAG, ${1:inbound-tag}, ${2:proxy}' },
+    { name: RULE_TYPE_FINAL, desc: '', snippet: 'FINAL, ${1:proxy}' },
 ]
 
+export const RULE_NETWORK_TCP = 'TCP'
+export const RULE_NETWORK_UDP = 'UDP'
+
 export const RULE_EXTERNAL_SOURCE_MMDB = 'mmdb'
 export const RULE_EXTERNAL_SOURCE_SITE = 'site'

+ 43 - 0
Maple.App/MonacoEditor/src/validate.ts

@@ -969,6 +969,49 @@ function validateRules(
                             }
                         }
                         break
+                    case facts.RULE_TYPE_PORT_RANGE:
+                        {
+                            const hypenPos = ruleItem.text.indexOf('-')
+                            if (hypenPos === -1) {
+                                errors.push({
+                                    severity: monaco.MarkerSeverity.Error,
+                                    startLineNumber: currLineId,
+                                    startColumn: ruleItem.startCol,
+                                    endLineNumber: currLineId,
+                                    endColumn: ruleItem.startCol + ruleItem.text.length,
+                                    message: `A port range must have two components, separated by a hyphen ("-").`,
+                                })
+                            } else {
+                                validatePortNumber(
+                                    ruleItem.text.substring(0, hypenPos),
+                                    currLineId,
+                                    ruleItem.startCol,
+                                    errors,
+                                )
+                                validatePortNumber(
+                                    ruleItem.text.substring(hypenPos + 1),
+                                    currLineId,
+                                    ruleItem.startCol + hypenPos + 1,
+                                    errors,
+                                )
+                            }
+                        }
+                        break
+                    case facts.RULE_TYPE_NETWORK:
+                        if (ruleItem.text !== facts.RULE_NETWORK_TCP && ruleItem.text !== facts.RULE_NETWORK_UDP) {
+                            errors.push({
+                                severity: monaco.MarkerSeverity.Error,
+                                startLineNumber: currLineId,
+                                startColumn: ruleItem.startCol,
+                                endLineNumber: currLineId,
+                                endColumn: ruleItem.startCol + ruleItem.text.length,
+                                message: `Unknown network type "${ruleItem.text}". Allowed values are "TCP" and "UDP".`,
+                            })
+                        }
+                        break
+                    case facts.RULE_TYPE_INBOUND_TAG:
+                        // ???
+                        break
                     default:
                         break
                 }