mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-07-29 12:51:49 +08:00
1116 lines
56 KiB
JavaScript
1116 lines
56 KiB
JavaScript
// ============================================================
|
|
// CodeMirror 6 Bundle — OpenClash
|
|
// Build: npx esbuild tools/codemirror/entry.js --bundle --format=iife --global-name=CM6 --minify --target=es2019 --outfile=root/www/luci-static/resources/openclash/js/cm6.min.js --legal-comments=none --loader:.css=text
|
|
// ============================================================
|
|
|
|
// ---- Core ----
|
|
import { EditorView, ViewPlugin, Decoration } from "@codemirror/view"
|
|
import { EditorState, Compartment, RangeSetBuilder } from "@codemirror/state"
|
|
|
|
// ---- View extensions ----
|
|
import {
|
|
lineNumbers, highlightActiveLine, highlightActiveLineGutter,
|
|
highlightSpecialChars, drawSelection, dropCursor,
|
|
rectangularSelection, crosshairCursor, keymap as cmKeymap, placeholder
|
|
} from "@codemirror/view"
|
|
|
|
// ---- Commands ----
|
|
import {
|
|
defaultKeymap, history, historyKeymap, indentWithTab, indentMore,
|
|
toggleComment
|
|
} from "@codemirror/commands"
|
|
|
|
// ---- Language core ----
|
|
import {
|
|
syntaxHighlighting, HighlightStyle, bracketMatching,
|
|
foldGutter, indentOnInput, StreamLanguage, foldKeymap, indentUnit,
|
|
getIndentUnit, foldable, ensureSyntaxTree
|
|
} from "@codemirror/language"
|
|
|
|
// ---- Tags ----
|
|
import { Tag } from "@lezer/highlight"
|
|
|
|
// ---- Custom log tags ----
|
|
const logTag = {
|
|
timestamp: Tag.define(),
|
|
bracket: Tag.define(),
|
|
category: Tag.define(),
|
|
logString: Tag.define(),
|
|
logLink: Tag.define(),
|
|
levelInfo: Tag.define(),
|
|
levelWarning: Tag.define(),
|
|
levelError: Tag.define(),
|
|
levelDebug: Tag.define(),
|
|
levelTip: Tag.define(),
|
|
levelFatal: Tag.define(),
|
|
levelWatchdog: Tag.define(),
|
|
}
|
|
|
|
// ---- Language packages ----
|
|
import { yaml } from "@codemirror/lang-yaml"
|
|
import { markdown } from "@codemirror/lang-markdown"
|
|
|
|
// ---- Legacy modes ----
|
|
import { shell } from "@codemirror/legacy-modes/mode/shell"
|
|
import { properties } from "@codemirror/legacy-modes/mode/properties"
|
|
|
|
// ---- Lint ----
|
|
import { linter, lintGutter } from "@codemirror/lint"
|
|
import { loadAll, YAML11_SCHEMA } from "js-yaml"
|
|
|
|
// ---- Search ----
|
|
import { search, searchKeymap, highlightSelectionMatches } from "@codemirror/search"
|
|
|
|
// ---- Autocomplete ----
|
|
import {
|
|
autocompletion, closeBrackets, closeBracketsKeymap, snippetCompletion,
|
|
acceptCompletion
|
|
} from "@codemirror/autocomplete"
|
|
|
|
// ---- Theme ----
|
|
import { githubDark, githubLight } from "@fsegurai/codemirror-theme-bundle"
|
|
|
|
// ---- Merge view ----
|
|
import { MergeView } from "@codemirror/merge"
|
|
|
|
// ---- Markdown rendering ----
|
|
import { marked } from "marked"
|
|
import hljs from "highlight.js/lib/core"
|
|
import yamlLang from "highlight.js/lib/languages/yaml"
|
|
import bashLang from "highlight.js/lib/languages/bash"
|
|
import jsonLang from "highlight.js/lib/languages/json"
|
|
import githubLightCSS from "highlight.js/styles/github.css"
|
|
import githubDarkCSS from "highlight.js/styles/github-dark-dimmed.css"
|
|
|
|
hljs.registerLanguage("yaml", yamlLang)
|
|
hljs.registerLanguage("yml", yamlLang)
|
|
hljs.registerLanguage("bash", bashLang)
|
|
hljs.registerLanguage("sh", bashLang)
|
|
hljs.registerLanguage("shell", bashLang)
|
|
hljs.registerLanguage("json", jsonLang)
|
|
|
|
// ============================================================
|
|
// Mihomo / Clash YAML keyword completion
|
|
// ============================================================
|
|
const mihomoKeywords = [
|
|
{ label: "port", type: "keyword", detail: "HTTP proxy port", info: "Invalid when mixed port is enabled" },
|
|
{ label: "socks-port", type: "keyword", detail: "SOCKS5 proxy port" },
|
|
{ label: "mixed-port", type: "keyword", detail: "HTTP/SOCKS mixed port", info: "Overrides port and socks-port when set" },
|
|
{ label: "redir-port", type: "keyword", detail: "Transparent proxy port" },
|
|
{ label: "tproxy-port", type: "keyword", detail: "TPROXY transparent proxy port" },
|
|
{ label: "mode", type: "keyword", detail: "Run mode: rule / global / direct" },
|
|
{ label: "log-level", type: "keyword", detail: "Log level: debug / info / warning / error / silent" },
|
|
{ label: "ipv6", type: "keyword", detail: "IPv6 traffic handling: true / false" },
|
|
{ label: "allow-lan", type: "keyword", detail: "Allow LAN connections: true / false" },
|
|
{ label: "bind-address", type: "keyword", detail: "Bind address, default *" },
|
|
{ label: "authentication", type: "keyword", detail: "HTTP/SOCKS authentication, array format" },
|
|
{ label: "external-controller", type: "keyword", detail: "RESTful API listen address" },
|
|
{ label: "external-ui", type: "keyword", detail: "Dashboard path" },
|
|
{ label: "secret", type: "keyword", detail: "API secret" },
|
|
{ label: "interface-name", type: "keyword", detail: "Outbound interface name" },
|
|
{ label: "routing-mark", type: "keyword", detail: "Routing mark" },
|
|
{ label: "hosts", type: "keyword", detail: "Custom hosts mapping" },
|
|
{ label: "profile", type: "keyword", detail: "Profiling configuration" },
|
|
{ label: "global-ua", type: "keyword", detail: "Global User-Agent" },
|
|
{ label: "unified-delay", type: "keyword", detail: "Unified delay: true / false" },
|
|
{ label: "tcp-concurrent", type: "keyword", detail: "TCP concurrent connections" },
|
|
{ label: "find-process-mode", type: "keyword", detail: "Find process mode: off / strict / always" },
|
|
{ label: "sniffing", type: "keyword", detail: "Domain sniffing: true / false" },
|
|
{ label: "keep-alive-interval", type: "keyword", detail: "TCP keep-alive interval (seconds)" },
|
|
{ label: "geodata-mode", type: "keyword", detail: "GEO data mode: true / false" },
|
|
{ label: "geodata-loader", type: "keyword", detail: "GEO loader: memconservative / standard" },
|
|
{ label: "geox-url", type: "keyword", detail: "GEOX custom URL template" },
|
|
{ label: "geosite", type: "keyword", detail: "GeoSite configuration" },
|
|
{ label: "geo-auto-update", type: "keyword", detail: "GEO auto update: true / false" },
|
|
{ label: "geo-update-interval", type: "keyword", detail: "GEO update interval (hours)" },
|
|
{ label: "tun", type: "keyword", detail: "TUN mode configuration" },
|
|
{ label: "enable", type: "property", detail: "Enable: true / false" },
|
|
{ label: "stack", type: "property", detail: "Protocol stack: system / gvisor / mixed" },
|
|
{ label: "device", type: "property", detail: "TUN device name" },
|
|
{ label: "dns-hijack", type: "property", detail: "DNS hijack address list" },
|
|
{ label: "auto-route", type: "property", detail: "Auto set route: true / false" },
|
|
{ label: "auto-detect-interface", type: "property", detail: "Auto detect interface: true / false" },
|
|
{ label: "mtu", type: "property", detail: "MTU value" },
|
|
{ label: "inet4-address", type: "property", detail: "IPv4 address range" },
|
|
{ label: "inet6-address", type: "property", detail: "IPv6 address range" },
|
|
{ label: "strict-route", type: "property", detail: "Strict route: true / false" },
|
|
{ label: "endpoint-independent-nat", type: "property", detail: "Endpoint-independent NAT: true / false" },
|
|
{ label: "udp-timeout", type: "property", detail: "UDP timeout" },
|
|
{ label: "dns", type: "keyword", detail: "DNS configuration" },
|
|
{ label: "nameserver", type: "property", detail: "DNS upstream servers" },
|
|
{ label: "fallback", type: "property", detail: "Fallback DNS servers" },
|
|
{ label: "fallback-filter", type: "property", detail: "Fallback filter configuration" },
|
|
{ label: "default-nameserver", type: "property", detail: "Default DNS servers" },
|
|
{ label: "nameserver-policy", type: "property", detail: "DNS by domain name" },
|
|
{ label: "proxy-server-nameserver", type: "property", detail: "Proxy DNS servers" },
|
|
{ label: "use-hosts", type: "property", detail: "Use hosts: true / false" },
|
|
{ label: "enhanced-mode", type: "property", detail: "DNS mode: normal / redir-host / fake-ip" },
|
|
{ label: "fake-ip-range", type: "property", detail: "Fake-IP address range" },
|
|
{ label: "fake-ip-filter", type: "property", detail: "Fake-IP filter domains" },
|
|
{ label: "type", type: "type", detail: "ss / ssr / vmess / trojan / snell / http / socks5 / hysteria / vless / tuic / wireguard" },
|
|
{ label: "server", type: "property", detail: "Server address" },
|
|
{ label: "name", type: "property", detail: "Proxy name" },
|
|
{ label: "password", type: "property", detail: "Password" },
|
|
{ label: "cipher", type: "property", detail: "Encryption method" },
|
|
{ label: "udp", type: "property", detail: "UDP relay: true / false" },
|
|
{ label: "xudp", type: "property", detail: "XUDP mode" },
|
|
{ label: "tfo", type: "property", detail: "TCP Fast Open: true / false" },
|
|
{ label: "mptcp", type: "property", detail: "Multipath TCP: true / false" },
|
|
{ label: "skip-cert-verify", type: "property", detail: "Skip certificate verification: true / false" },
|
|
{ label: "sni", type: "property", detail: "TLS SNI" },
|
|
{ label: "alpn", type: "property", detail: "TLS ALPN protocol list" },
|
|
{ label: "fingerprint", type: "property", detail: "TLS fingerprint" },
|
|
{ label: "client-fingerprint", type: "property", detail: "Client fingerprint" },
|
|
{ label: "servername", type: "property", detail: "TLS server name" },
|
|
{ label: "network", type: "property", detail: "Transport: tcp / ws / grpc / http" },
|
|
{ label: "plugin", type: "property", detail: "Plugin: obfs / v2ray-plugin etc." },
|
|
{ label: "plugin-opts", type: "property", detail: "Plugin options" },
|
|
{ label: "smux", type: "property", detail: "Connection multiplexing config" },
|
|
{ label: "proxy-groups", type: "class", detail: "Proxy group list" },
|
|
{ label: "proxies", type: "property", detail: "Sub-proxy/sub-group name list" },
|
|
{ label: "url", type: "property", detail: "Health check URL" },
|
|
{ label: "interval", type: "property", detail: "Check interval (seconds)" },
|
|
{ label: "timeout", type: "property", detail: "Check timeout (ms)" },
|
|
{ label: "lazy", type: "property", detail: "Lazy loading: true / false" },
|
|
{ label: "strategy", type: "property", detail: "Load balance strategy: consistent-hashing / round-robin" },
|
|
{ label: "max-failed-times", type: "property", detail: "Max failed times" },
|
|
{ label: "tolerance", type: "property", detail: "Latency tolerance (ms)" },
|
|
{ label: "disable-udp", type: "property", detail: "Disable UDP: true / false" },
|
|
{ label: "hidden", type: "property", detail: "Hidden: true / false" },
|
|
{ label: "icon", type: "property", detail: "Icon URL" },
|
|
{ label: "filter", type: "property", detail: "Node filter" },
|
|
{ label: "exclude-filter", type: "property", detail: "Exclude filter" },
|
|
{ label: "exclude-type", type: "property", detail: "Exclude type" },
|
|
{ label: "DOMAIN", type: "keyword", info: "Domain name" },
|
|
{ label: "DOMAIN-SUFFIX", type: "keyword", info: "Domain suffix" },
|
|
{ label: "DOMAIN-KEYWORD", type: "keyword", info: "Domain keyword" },
|
|
{ label: "DOMAIN-REGEX", type: "keyword", info: "Domain regex" },
|
|
{ label: "GEOSITE", type: "keyword", info: "GeoSite category" },
|
|
{ label: "GEOIP", type: "keyword", info: "Country/region code" },
|
|
{ label: "IP-CIDR", type: "keyword", info: "IP CIDR range" },
|
|
{ label: "IP-CIDR6", type: "keyword", info: "IPv6 CIDR range" },
|
|
{ label: "IP-ASN", type: "keyword", info: "ASN number" },
|
|
{ label: "SRC-IP-CIDR", type: "keyword", info: "Source IP CIDR" },
|
|
{ label: "SRC-PORT", type: "keyword", info: "Source port" },
|
|
{ label: "DST-PORT", type: "keyword", info: "Destination port" },
|
|
{ label: "PROCESS-NAME", type: "keyword", info: "Process name" },
|
|
{ label: "PROCESS-PATH", type: "keyword", info: "Process path" },
|
|
{ label: "RULE-SET", type: "keyword", info: "Rule set name" },
|
|
{ label: "AND", type: "keyword", info: "Logical AND" },
|
|
{ label: "OR", type: "keyword", info: "Logical OR" },
|
|
{ label: "NOT", type: "keyword", info: "Logical NOT" },
|
|
{ label: "MATCH", type: "keyword", info: "Match all (fallback)" },
|
|
{ label: "no-resolve", type: "keyword", info: "Rule modifier: skip DNS resolution" },
|
|
{ label: "proxy-providers", type: "class", detail: "Proxy provider configuration" },
|
|
{ label: "rule-providers", type: "class", detail: "Rule provider configuration" },
|
|
{ label: "behavior", type: "property", detail: "Behavior: classical / ipcidr / domain" },
|
|
{ label: "path", type: "property", detail: "Local path" },
|
|
{ label: "format", type: "property", detail: "Format: yaml / text / mrs" },
|
|
{ label: "health-check", type: "property", detail: "Health check configuration" },
|
|
]
|
|
|
|
// ============================================================
|
|
// Mihomo config snippet templates (based on default.yaml)
|
|
// ============================================================
|
|
const mihomoSnippets = [
|
|
// --- Basic configuration ---
|
|
snippetCompletion("mixed-port: #{1}\nmode: #{2}\nlog-level: #{3}\nallow-lan: #{4}\nbind-address: \"#{5}\"\nipv6: #{6}\nexternal-controller: 0.0.0.0:9093\n#{}", {
|
|
label: "config-base", type: "snippet", detail: "Basic configuration"
|
|
}),
|
|
|
|
// --- Node templates ---
|
|
snippetCompletion("- name: \"#{1}\"\n type: ss\n server: \"#{2}\"\n port: #{3}\n cipher: #{4}\n password: \"#{5}\"\n # udp: true\n # plugin: obfs\n # plugin-opts:\n # mode: tls\n # host: bing.com\n # client-fingerprint: chrome\n # smux:\n # enabled: false\n#{}", {
|
|
label: "ss-template", type: "snippet", detail: "Shadowsocks node"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: ssr\n server: \"#{2}\"\n port: #{3}\n cipher: #{4}\n password: \"#{5}\"\n obfs: #{6}\n protocol: #{7}\n # obfs-param: \"\"\n # protocol-param: \"\"\n # udp: true\n#{}", {
|
|
label: "ssr-template", type: "snippet", detail: "ShadowsocksR node"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: vmess\n server: \"#{2}\"\n port: #{3}\n uuid: \"#{4}\"\n alterId: #{5}\n cipher: auto\n # udp: true\n # network: ws\n # tls: true\n # servername: \"\"\n # skip-cert-verify: false\n # client-fingerprint: chrome\n # ws-opts:\n # path: /\n # headers:\n # Host: \"\"\n#{}", {
|
|
label: "vmess-template", type: "snippet", detail: "VMess node"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: vmess\n server: \"#{2}\"\n port: #{3}\n uuid: \"#{4}\"\n alterId: 0\n cipher: auto\n network: grpc\n tls: true\n servername: \"#{5}\"\n # skip-cert-verify: false\n # client-fingerprint: chrome\n grpc-opts:\n grpc-service-name: \"#{6}\"\n#{}", {
|
|
label: "vmess-grpc", type: "snippet", detail: "VMess gRPC node"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: trojan\n server: \"#{2}\"\n port: #{3}\n password: \"#{4}\"\n # udp: true\n # sni: \"\"\n # skip-cert-verify: false\n # client-fingerprint: chrome\n # network: ws\n # ws-opts:\n # path: /\n#{}", {
|
|
label: "trojan-template", type: "snippet", detail: "Trojan node"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: vless\n server: \"#{2}\"\n port: #{3}\n uuid: \"#{4}\"\n # udp: true\n # network: tcp\n # tls: true\n # servername: \"\"\n # flow: xtls-rprx-vision\n # client-fingerprint: chrome\n # reality-opts:\n # public-key: \"\"\n # short-id: \"\"\n#{}", {
|
|
label: "vless-template", type: "snippet", detail: "VLESS node"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: vless\n server: \"#{2}\"\n port: #{3}\n uuid: \"#{4}\"\n network: tcp\n tls: true\n udp: true\n flow: xtls-rprx-vision\n client-fingerprint: chrome\n servername: \"#{5}\"\n reality-opts:\n public-key: \"#{6}\"\n short-id: \"#{7}\"\n#{}", {
|
|
label: "vless-reality", type: "snippet", detail: "VLESS REALITY node"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: hysteria2\n server: \"#{2}\"\n port: #{3}\n password: \"#{4}\"\n # up: \"30 Mbps\"\n # down: \"200 Mbps\"\n # sni: \"\"\n # skip-cert-verify: false\n # obfs: salamander\n # obfs-password: \"\"\n#{}", {
|
|
label: "hysteria2-template", type: "snippet", detail: "Hysteria2 node"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: tuic\n server: \"#{2}\"\n port: #{3}\n uuid: \"#{4}\"\n password: \"#{5}\"\n # udp-relay-mode: native\n # sni: \"\"\n # skip-cert-verify: false\n # alpn:\n # - h3\n # congestion-controller: bbr\n#{}", {
|
|
label: "tuic-template", type: "snippet", detail: "TUIC node"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: wireguard\n server: \"#{2}\"\n port: #{3}\n ip: \"#{4}\"\n # ipv6: \"\"\n public-key: \"#{5}\"\n private-key: \"#{6}\"\n udp: true\n # reserved: \"\"\n # dialer-proxy: \"\"\n#{}", {
|
|
label: "wireguard-template", type: "snippet", detail: "WireGuard node"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: socks5\n server: \"#{2}\"\n port: #{3}\n # username: \"\"\n # password: \"\"\n # tls: true\n # skip-cert-verify: true\n # udp: true\n#{}", {
|
|
label: "socks5-template", type: "snippet", detail: "SOCKS5 node"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: http\n server: \"#{2}\"\n port: #{3}\n # username: \"\"\n # password: \"\"\n # tls: true\n # skip-cert-verify: true\n#{}", {
|
|
label: "http-template", type: "snippet", detail: "HTTP node"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: snell\n server: \"#{2}\"\n port: #{3}\n psk: \"#{4}\"\n version: #{5}\n # udp: true\n # obfs-opts:\n # mode: http\n # host: bing.com\n#{}", {
|
|
label: "snell-template", type: "snippet", detail: "Snell node"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: hysteria\n server: \"#{2}\"\n port: #{3}\n auth-str: \"#{4}\"\n protocol: udp\n up: \"#{5}\"\n down: \"#{6}\"\n # sni: \"\"\n # skip-cert-verify: false\n # alpn:\n # - h3\n#{}", {
|
|
label: "hysteria-template", type: "snippet", detail: "Hysteria (v1) node"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: mieru\n server: \"#{2}\"\n port: #{3}\n transport: TCP\n username: \"#{4}\"\n password: \"#{5}\"\n udp: true\n # multiplexing: MULTIPLEXING_LOW\n # handshake-mode: HANDSHAKE_STANDARD\n#{}", {
|
|
label: "mieru-template", type: "snippet", detail: "Mieru node"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: anytls\n server: \"#{2}\"\n port: #{3}\n password: \"#{4}\"\n # udp: true\n # sni: \"\"\n # skip-cert-verify: true\n # client-fingerprint: chrome\n # alpn:\n # - h2\n#{}", {
|
|
label: "anytls-template", type: "snippet", detail: "AnyTLS node"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: trusttunnel\n server: \"#{2}\"\n port: #{3}\n username: \"#{4}\"\n password: \"#{5}\"\n udp: true\n # sni: \"\"\n # skip-cert-verify: true\n # client-fingerprint: chrome\n # quic: true\n # congestion-controller: bbr\n#{}", {
|
|
label: "trusttunnel-template", type: "snippet", detail: "TrustTunnel node"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: sudoku\n server: \"#{2}\"\n port: #{3}\n key: \"#{4}\"\n aead-method: #{5}\n # padding-min: 2\n # padding-max: 7\n # table-type: prefer_ascii\n # httpmask:\n # disable: false\n # mode: legacy\n#{}", {
|
|
label: "sudoku-template", type: "snippet", detail: "Sudoku node"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: openvpn\n server: \"#{2}\"\n port: #{3}\n proto: udp\n # username: \"\"\n # password: \"\"\n ca: |\n -----BEGIN CERTIFICATE-----\n ...\n -----END CERTIFICATE-----\n cert: |\n -----BEGIN CERTIFICATE-----\n ...\n -----END CERTIFICATE-----\n key: |\n -----BEGIN PRIVATE KEY-----\n ...\n -----END PRIVATE KEY-----\n udp: true\n#{}", {
|
|
label: "openvpn-template", type: "snippet", detail: "OpenVPN node"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: ssh\n server: \"#{2}\"\n port: #{3}\n username: \"#{4}\"\n # password: \"\"\n # privateKey: path\n#{}", {
|
|
label: "ssh-template", type: "snippet", detail: "SSH node"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: dns\n#{}", {
|
|
label: "dns-out-template", type: "snippet", detail: "DNS outbound"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: direct\n # interface-name: en0\n # routing-mark: 6667\n#{}", {
|
|
label: "direct-template", type: "snippet", detail: "Direct outbound (custom interface)"
|
|
}),
|
|
|
|
// --- Proxy groups ---
|
|
snippetCompletion("- name: \"#{1}\"\n type: select\n proxies:\n - \"#{2}\"\n # disable-udp: false\n#{}", {
|
|
label: "group-select", type: "snippet", detail: "Select manual proxy group"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: url-test\n proxies:\n - \"#{2}\"\n url: \"#{3}\"\n interval: #{4}\n # tolerance: 150\n # lazy: true\n#{}", {
|
|
label: "group-url-test", type: "snippet", detail: "URL-Test auto speed test group"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: fallback\n proxies:\n - \"#{2}\"\n url: \"#{3}\"\n interval: #{4}\n#{}", {
|
|
label: "group-fallback", type: "snippet", detail: "Fallback failover group"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: load-balance\n proxies:\n - \"#{2}\"\n url: \"#{3}\"\n interval: #{4}\n # strategy: consistent-hashing\n#{}", {
|
|
label: "group-load-balance", type: "snippet", detail: "Load-Balance group"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: smart\n proxies:\n - \"#{2}\"\n url: \"#{3}\"\n interval: #{4}\n # timeout: 5000\n # max-failed-times: 3\n # lazy: true\n # disable-udp: false\n # filter: \"HK|SG\"\n # policy-priority: \"\"\n # uselightgbm: false\n # collectdata: false\n # sample-rate: 1.0\n # prefer-asn: false\n#{}", {
|
|
label: "group-smart", type: "snippet", detail: "Smart selection group"
|
|
}),
|
|
|
|
// --- Rules ---
|
|
snippetCompletion("- DOMAIN-SUFFIX,#{1},#{2}\n#{}", {
|
|
label: "rule-domain-suffix", type: "snippet", detail: "Domain suffix rule"
|
|
}),
|
|
snippetCompletion("- DOMAIN,#{1},#{2}\n#{}", {
|
|
label: "rule-domain", type: "snippet", detail: "Domain rule"
|
|
}),
|
|
snippetCompletion("- DOMAIN-KEYWORD,#{1},#{2}\n#{}", {
|
|
label: "rule-domain-keyword", type: "snippet", detail: "Domain keyword rule"
|
|
}),
|
|
snippetCompletion("- GEOSITE,#{1},#{2}\n#{}", {
|
|
label: "rule-geosite", type: "snippet", detail: "GeoSite rule"
|
|
}),
|
|
snippetCompletion("- GEOIP,#{1},#{2}\n#{}", {
|
|
label: "rule-geoip", type: "snippet", detail: "GEOIP rule"
|
|
}),
|
|
snippetCompletion("- IP-CIDR,#{1},#{2}\n#{}", {
|
|
label: "rule-ip-cidr", type: "snippet", detail: "IP-CIDR rule"
|
|
}),
|
|
snippetCompletion("- RULE-SET,#{1},#{2}\n#{}", {
|
|
label: "rule-set", type: "snippet", detail: "RULE-SET rule"
|
|
}),
|
|
snippetCompletion("- MATCH,#{1}\n#{}", {
|
|
label: "rule-match", type: "snippet", detail: "MATCH fallback rule"
|
|
}),
|
|
|
|
// --- DNS ---
|
|
snippetCompletion("dns:\n enable: #{1}\n listen: 0.0.0.0:53\n enhanced-mode: #{2}\n default-nameserver:\n - 114.114.114.114\n - 8.8.8.8\n nameserver:\n - \"#{3}\"\n # fallback:\n # - \"\"\n # fallback-filter:\n # geoip: true\n # geoip-code: CN\n#{}", {
|
|
label: "dns-template", type: "snippet", detail: "DNS configuration"
|
|
}),
|
|
snippetCompletion("nameserver-policy:\n \"#{1}\":\n - \"#{2}\"\n#{}", {
|
|
label: "dns-nameserver-policy", type: "snippet", detail: "DNS nameserver-policy"
|
|
}),
|
|
|
|
// --- TUN ---
|
|
snippetCompletion("tun:\n enable: #{1}\n stack: #{2}\n device: utun\n auto-route: #{3}\n auto-detect-interface: #{4}\n dns-hijack:\n - any:53\n # auto-redirect: false\n # mtu: 9000\n#{}", {
|
|
label: "tun-template", type: "snippet", detail: "TUN configuration"
|
|
}),
|
|
|
|
// --- proxy-provider ---
|
|
snippetCompletion("proxy-providers:\n #{1}:\n type: http\n url: \"#{2}\"\n interval: #{3}\n path: ./proxy_provider/#{1}.yaml\n # proxy: DIRECT\n # header:\n # User-Agent:\n # - \"mihomo/1.0\"\n health-check:\n enable: #{4}\n url: https://cp.cloudflare.com/generate_204\n interval: #{5}\n # override:\n # skip-cert-verify: true\n # udp: true\n # filter: \"HK|TW|SG\"\n # exclude-filter: \"Expired|Traffic\"\n#{}", {
|
|
label: "proxy-providers", type: "snippet", detail: "Proxy provider configuration"
|
|
}),
|
|
|
|
// --- rule-provider ---
|
|
snippetCompletion("rule-providers:\n #{1}:\n type: http\n behavior: #{2}\n url: \"#{3}\"\n interval: #{4}\n path: ./rule_provider/#{1}.#{5}\n format: #{6}\n#{}", {
|
|
label: "rule-providers", type: "snippet", detail: "Rule provider configuration"
|
|
}),
|
|
snippetCompletion("rule-providers:\n #{1}:\n type: file\n behavior: #{2}\n path: /path/to/#{1}.yaml\n format: #{3}\n#{}", {
|
|
label: "rule-providers-file", type: "snippet", detail: "Rule provider (local file)"
|
|
}),
|
|
snippetCompletion("rule-providers:\n #{1}:\n type: inline\n behavior: #{2}\n payload:\n - '#{3}'\n#{}", {
|
|
label: "rule-providers-inline", type: "snippet", detail: "Rule provider (inline)"
|
|
}),
|
|
|
|
// --- hosts ---
|
|
snippetCompletion("hosts:\n '#{1}': #{2}\n # '*.: 127.0.0.1\n # 'alpha..': '::1'\n # 'baidu.com': google.com\n#{}", {
|
|
label: "hosts", type: "snippet", detail: "Custom hosts mapping"
|
|
}),
|
|
|
|
// --- fake-ip-filter ---
|
|
snippetCompletion("fake-ip-filter:\n - '#{1}'\n - '+.#{2}'\n # - rule-set:fakeip-filter\n # - geosite:fakeip-filter\n # - '*.lan'\n # - localhost.ptlogin2.qq.com\n#{}", {
|
|
label: "fake-ip-filter", type: "snippet", detail: "Fake-IP filter list"
|
|
}),
|
|
|
|
// --- sniffer ---
|
|
snippetCompletion("sniffer:\n enable: #{1}\n override-destination: #{2}\n sniff:\n TLS:\n ports: [443]\n HTTP:\n ports: [80, 8080-8880]\n override-destination: true\n QUIC:\n ports: [443]\n # force-domain:\n # - +.v2ex.com\n # skip-domain:\n # - Mijia Cloud\n # parse-pure-ip: true\n # force-dns-mapping: true\n#{}", {
|
|
label: "sniffer", type: "snippet", detail: "Domain sniffing configuration"
|
|
}),
|
|
|
|
// --- dns (enhanced) ---
|
|
snippetCompletion("dns:\n enable: #{1}\n enhanced-mode: #{2}\n listen: 0.0.0.0:53\n ipv6: #{3}\n default-nameserver:\n - 114.114.114.114\n - 8.8.8.8\n nameserver:\n - 114.114.114.114\n - tls://223.5.5.5:853\n - https://doh.pub/dns-query\n # fallback:\n # - tls://1.1.1.1\n # fallback-filter:\n # geoip: true\n # geoip-code: CN\n # proxy-server-nameserver:\n # - https://doh.pub/dns-query\n # respect-rules: false\n#{}", {
|
|
label: "dns", type: "snippet", detail: "DNS configuration"
|
|
}),
|
|
|
|
// --- nameserver-policy (enhanced) ---
|
|
snippetCompletion("nameserver-policy:\n \"#{1}\":\n - \"#{2}\"\n # \"geosite:cn,private,apple\":\n # - https://doh.pub/dns-query\n # - https://dns.alidns.com/dns-query\n # \"geosite:category-ads-all\": rcode://success\n # \"rule-set:global,dns\": 8.8.8.8\n#{}", {
|
|
label: "nameserver-policy", type: "snippet", detail: "DNS by domain/geo/rule-set"
|
|
}),
|
|
|
|
// --- fallback-filter ---
|
|
snippetCompletion("fallback-filter:\n geoip: #{1}\n geoip-code: #{2}\n ipcidr:\n - 240.0.0.0/4\n - 0.0.0.0/32\n # domain:\n # - '+.google.com'\n # - '+.facebook.com'\n#{}", {
|
|
label: "fallback-filter", type: "snippet", detail: "DNS fallback filter criteria"
|
|
}),
|
|
|
|
// --- TUN ---
|
|
snippetCompletion("tun:\n enable: #{1}\n stack: #{2}\n device: utun\n auto-route: #{3}\n auto-detect-interface: #{4}\n dns-hijack:\n - any:53\n # auto-redirect: false\n # mtu: 9000\n # strict-route: true\n # endpoint-independent-nat: false\n#{}", {
|
|
label: "tun", type: "snippet", detail: "TUN mode configuration"
|
|
}),
|
|
|
|
// --- authentication ---
|
|
snippetCompletion("authentication:\n - \"#{1}:#{2}\"\n#{}", {
|
|
label: "authentication", type: "snippet", detail: "HTTP/SOCKS authentication"
|
|
}),
|
|
|
|
// --- geox-url ---
|
|
snippetCompletion("geox-url:\n geoip: \"#{1}\"\n geosite: \"#{2}\"\n mmdb: \"#{3}\"\n#{}", {
|
|
label: "geox-url", type: "snippet", detail: "Custom GEO data source URL"
|
|
}),
|
|
|
|
// --- profile ---
|
|
snippetCompletion("profile:\n store-selected: #{1}\n store-fake-ip: #{2}\n#{}", {
|
|
label: "profile", type: "snippet", detail: "Persistent storage configuration"
|
|
}),
|
|
|
|
// --- proxy-groups with use/filter ---
|
|
snippetCompletion("- name: \"#{1}\"\n type: select\n use:\n - #{2}\n # filter: \"HK|SG\"\n # exclude-filter: \"Expired|Traffic\"\n proxies:\n - DIRECT\n#{}", {
|
|
label: "proxy-group-use", type: "snippet", detail: "Proxy group (reference provider)"
|
|
}),
|
|
|
|
// --- listeners ---
|
|
snippetCompletion("listeners:\n - name: socks5-in\n type: socks\n port: #{1}\n # listen: 0.0.0.0\n # udp: true\n#{}", {
|
|
label: "listener-socks", type: "snippet", detail: "SOCKS5 inbound"
|
|
}),
|
|
snippetCompletion("listeners:\n - name: http-in\n type: http\n port: #{1}\n # listen: 0.0.0.0\n#{}", {
|
|
label: "listener-http", type: "snippet", detail: "HTTP inbound"
|
|
}),
|
|
snippetCompletion("listeners:\n - name: mixed-in\n type: mixed\n port: #{1}\n # listen: 0.0.0.0\n # udp: true\n # users:\n # - username: aaa\n # password: aaa\n#{}", {
|
|
label: "listener-mixed", type: "snippet", detail: "HTTP/SOCKS mixed inbound"
|
|
}),
|
|
snippetCompletion("listeners:\n - name: tun-in\n type: tun\n # stack: system\n # dns-hijack:\n # - 0.0.0.0:53\n # auto-route: true\n # mtu: 9000\n#{}", {
|
|
label: "listener-tun", type: "snippet", detail: "TUN inbound"
|
|
}),
|
|
|
|
// --- sub-rules ---
|
|
snippetCompletion("sub-rules:\n #{1}:\n - DOMAIN,#{2},#{3}\n - DOMAIN-SUFFIX,#{4},#{5}\n - IP-CIDR,#{6},#{7}\n - MATCH,#{8}\n#{}", {
|
|
label: "sub-rules", type: "snippet", detail: "Sub-rule set definition"
|
|
}),
|
|
|
|
// --- tunnels ---
|
|
snippetCompletion("tunnels:\n - tcp/udp,127.0.0.1:#{1},#{2}:#{3},#{4}\n#{}", {
|
|
label: "tunnels", type: "snippet", detail: "Tunnel forwarding (short format)"
|
|
}),
|
|
snippetCompletion("tunnels:\n - network: [tcp, udp]\n address: 127.0.0.1:#{1}\n target: #{2}:#{3}\n proxy: #{4}\n#{}", {
|
|
label: "tunnels-full", type: "snippet", detail: "Tunnel forwarding (full format)"
|
|
}),
|
|
|
|
// --- additional node types ---
|
|
snippetCompletion("- name: \"#{1}\"\n type: masque\n server: \"#{2}\"\n port: #{3}\n public-key: \"#{4}\"\n private-key: \"#{5}\"\n ip: \"#{6}\"\n udp: true\n # network: h3 # h3 / h2 / h3-l4proxy\n # dialer-proxy: \"\"\n#{}", {
|
|
label: "masque-template", type: "snippet", detail: "MASQUE node"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: rematch\n target-rematch-name: \"#{2}\"\n # target-sub-rule: \"\"\n#{}", {
|
|
label: "rematch-template", type: "snippet", detail: "Rematch node"
|
|
}),
|
|
snippetCompletion("- name: \"#{1}\"\n type: gost-relay\n server: \"#{2}\"\n port: #{3}\n udp: true\n tls: true\n # mux: true\n # username: \"\"\n # password: \"\"\n#{}", {
|
|
label: "gost-relay-template", type: "snippet", detail: "GOST Relay node"
|
|
}),
|
|
]
|
|
|
|
function mihomoCompletion(context) {
|
|
const word = context.matchBefore(/[\w-]+/)
|
|
if (!word || (word.from === word.to && !context.explicit)) return null
|
|
const filtered = mihomoKeywords.filter(k =>
|
|
k.label.toLowerCase().startsWith(word.text.toLowerCase())
|
|
)
|
|
const snippets = mihomoSnippets.filter(s => {
|
|
const label = (s.label || s.name || '')
|
|
return label.toLowerCase().startsWith(word.text.toLowerCase())
|
|
})
|
|
const options = [...filtered, ...snippets]
|
|
if (!options.length) return null
|
|
return { from: word.from, options, validFor: /^[\w-]*$/ }
|
|
}
|
|
|
|
function yamlLinter(delay = 750) {
|
|
return linter(view => {
|
|
const diagnostics = []
|
|
try { loadAll(view.state.doc.toString(), { schema: YAML11_SCHEMA }) } catch (e) {
|
|
const mark = e.mark
|
|
if (mark && mark.line !== undefined) {
|
|
const lineNo = mark.line + 1
|
|
if (lineNo <= view.state.doc.lines) {
|
|
const line = view.state.doc.line(lineNo)
|
|
const pos = Math.min(line.from + (mark.column || 0), line.to)
|
|
diagnostics.push({ from: pos, to: Math.min(pos + 1, line.to), severity: "error", message: e.reason || e.message })
|
|
}
|
|
} else {
|
|
diagnostics.push({ from: 0, to: 0, severity: "error", message: e.reason || e.message })
|
|
}
|
|
}
|
|
return diagnostics
|
|
}, typeof delay === 'number' ? { delay: delay } : undefined)
|
|
}
|
|
|
|
var _levelTagMap = null
|
|
var _levelRegex = null
|
|
function _buildLevelCache() {
|
|
if (!window.levelTranslations) { _levelTagMap = {}; _levelRegex = /(?!)/; return }
|
|
var map = {}, parts = []
|
|
for (var key in window.levelTranslations) {
|
|
if (window.levelTranslations.hasOwnProperty(key)) {
|
|
var text = window.levelTranslations[key]
|
|
map[text] = key
|
|
parts.push(text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
}
|
|
}
|
|
_levelTagMap = map
|
|
_levelRegex = new RegExp('^\\[(' + parts.join('|') + ')\\]')
|
|
}
|
|
|
|
const logLanguage = StreamLanguage.define({
|
|
tokenTable: {
|
|
timestamp: logTag.timestamp,
|
|
bracket: logTag.bracket,
|
|
category: logTag.category,
|
|
logString: logTag.logString,
|
|
logLink: logTag.logLink,
|
|
levelInfo: logTag.levelInfo,
|
|
levelWarning: logTag.levelWarning,
|
|
levelError: logTag.levelError,
|
|
levelDebug: logTag.levelDebug,
|
|
levelTip: logTag.levelTip,
|
|
levelFatal: logTag.levelFatal,
|
|
levelWatchdog: logTag.levelWatchdog,
|
|
},
|
|
startState() { return { tabDone: false } },
|
|
token(stream, state) {
|
|
if (stream.sol()) {
|
|
state.tabDone = false
|
|
if (stream.match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/)) return "timestamp"
|
|
}
|
|
if (stream.eatSpace()) return null
|
|
var ch = stream.peek()
|
|
if (!ch) { stream.skipToEnd(); return null }
|
|
if (ch === '\u3010' && stream.match(/\u3010[^\u3011]*\u3011/)) return "bracket"
|
|
if (state.tabDone) { stream.next(); return "logString" }
|
|
if (ch === '[') {
|
|
if (_levelTagMap === null) _buildLevelCache()
|
|
var levelMatch = stream.match(_levelRegex)
|
|
if (levelMatch) {
|
|
var levelKey = _levelTagMap[levelMatch[1]]
|
|
if (levelKey) {
|
|
var styleName = 'level' + levelKey.charAt(0).toUpperCase() + levelKey.slice(1)
|
|
if (logTag[styleName]) return styleName
|
|
}
|
|
return "category"
|
|
}
|
|
if (stream.match(/\[[A-Z][^\]]*\]/)) return "category"
|
|
}
|
|
if (ch !== '[') { state.tabDone = true }
|
|
stream.next()
|
|
return "logString"
|
|
}
|
|
})
|
|
|
|
const logHighlightStyle = HighlightStyle.define([
|
|
{ tag: logTag.timestamp, class: "cmt-log-timestamp" },
|
|
{ tag: logTag.bracket, class: "cmt-log-bracket" },
|
|
{ tag: logTag.category, class: "cmt-log-category" },
|
|
{ tag: logTag.logString, class: "cmt-log-string" },
|
|
{ tag: logTag.logLink, class: "cmt-log-link" },
|
|
{ tag: logTag.levelInfo, class: "cmt-log-level-info" },
|
|
{ tag: logTag.levelWarning, class: "cmt-log-level-warning" },
|
|
{ tag: logTag.levelError, class: "cmt-log-level-error" },
|
|
{ tag: logTag.levelDebug, class: "cmt-log-level-debug" },
|
|
{ tag: logTag.levelTip, class: "cmt-log-level-tip" },
|
|
{ tag: logTag.levelFatal, class: "cmt-log-level-fatal" },
|
|
{ tag: logTag.levelWatchdog, class: "cmt-log-level-watchdog" },
|
|
])
|
|
|
|
function syntaxPreload(buffer = 1000) {
|
|
return ViewPlugin.fromClass(class {
|
|
constructor(view) { this._preload(view) }
|
|
update(u) {
|
|
if (u.viewportChanged || u.docChanged) {
|
|
if (this._rafId) cancelAnimationFrame(this._rafId)
|
|
var self = this
|
|
this._rafId = requestAnimationFrame(function() {
|
|
self._rafId = null
|
|
self._preload(u.view)
|
|
})
|
|
}
|
|
}
|
|
_preload(view) {
|
|
var doc = view.state.doc
|
|
var ll = doc.lineAt(view.viewport.to).number
|
|
var target = Math.min(ll + buffer, doc.lines)
|
|
ensureSyntaxTree(view.state, doc.line(target).to, 50)
|
|
}
|
|
})
|
|
}
|
|
|
|
function baseExtensions(extra = []) {
|
|
return [
|
|
lineNumbers(), highlightActiveLine(), highlightActiveLineGutter(),
|
|
highlightSpecialChars(), drawSelection(), dropCursor(),
|
|
rectangularSelection(), crosshairCursor(),
|
|
bracketMatching(), foldGutter({ placeholderDOM: null }),
|
|
indentOnInput(), history(),
|
|
highlightSelectionMatches(), EditorView.lineWrapping,
|
|
closeBrackets(),
|
|
syntaxPreload(),
|
|
cmKeymap.of([...defaultKeymap, ...historyKeymap, ...searchKeymap, ...closeBracketsKeymap, ...foldKeymap, { key: 'Tab', run: function(v) { return acceptCompletion(v) || indentMore(v) } }]),
|
|
...extra
|
|
]
|
|
}
|
|
|
|
// ============================================================
|
|
// Custom indentation markers
|
|
// Fixes left:2px→6px: @codemirror/view >=~6.29 changed .cm-line
|
|
// padding from "0 2px" to "0 2px 0 6px".
|
|
// The ::before pseudo-element fills the full line height (no gaps)
|
|
// but is capped at one visual row (--oc-lh) so it never bleeds
|
|
// through wrapped text on long lines.
|
|
// ============================================================
|
|
|
|
const _ocImBaseTheme = EditorView.baseTheme({
|
|
'.cm-line': { position: 'relative' },
|
|
'.cm-oc-im::before': {
|
|
content: '""',
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: '6px',
|
|
right: 0,
|
|
height: 'var(--oc-lh, 100%)',
|
|
background: 'var(--oc-im)',
|
|
pointerEvents: 'none',
|
|
zIndex: '-1',
|
|
},
|
|
'&light': { '--oc-im-c': '#e1e4e8', '--oc-im-ca': '#d0d7de' },
|
|
'&dark': { '--oc-im-c': '#30363d', '--oc-im-ca': '#484f58' },
|
|
})
|
|
|
|
function _ocImIndent(text, ts) {
|
|
let n = 0
|
|
for (let i = 0; i < text.length; i++) {
|
|
if (text[i] === ' ') n++
|
|
else if (text[i] === '\t') n += ts
|
|
else break
|
|
}
|
|
return n
|
|
}
|
|
|
|
function _foldDepth(state, fl, ll, doc) {
|
|
var ranges = []
|
|
for (var n = fl; n <= ll; n++) {
|
|
var fr = foldable(state, doc.line(n).from, doc.line(n).to)
|
|
if (fr) ranges.push({ from: fr.from, to: fr.to })
|
|
}
|
|
|
|
var depths = new Int32Array(ll - fl + 1)
|
|
for (var n = fl; n <= ll; n++) {
|
|
var pos = doc.line(n).from, d = 0
|
|
for (var i = 0; i < ranges.length; i++)
|
|
if (ranges[i].from < pos && pos < ranges[i].to) d++
|
|
depths[n - fl] = d
|
|
}
|
|
return depths
|
|
}
|
|
|
|
function _foldDepth1(state, lineNo, fl, ll, doc) {
|
|
var pos = doc.line(lineNo).from, d = 0
|
|
for (var n = fl; n <= ll; n++) {
|
|
var fr = foldable(state, doc.line(n).from, doc.line(n).to)
|
|
if (fr && fr.from < pos && pos < fr.to) d++
|
|
}
|
|
return d
|
|
}
|
|
|
|
function indentMarkerExtension({ highlightActiveBlock = true, hideFirstIndent = false, thickness = 1, colors, deferMode = 'raf', buffer = 1000 } = {}) {
|
|
var extra = colors ? EditorView.baseTheme({
|
|
'&light': { '--oc-im-c': colors.light || '#e1e4e8', '--oc-im-ca': colors.activeLight || '#d0d7de' },
|
|
'&dark': { '--oc-im-c': colors.dark || '#30363d', '--oc-im-ca': colors.activeDark || '#484f58' },
|
|
}) : []
|
|
|
|
return [
|
|
_ocImBaseTheme,
|
|
extra,
|
|
ViewPlugin.fromClass(class {
|
|
constructor(view) {
|
|
this._view = view
|
|
this._stepPx = null
|
|
this._measurePending = false
|
|
this.decorations = this._build(view)
|
|
this._scheduleMeasure(view)
|
|
}
|
|
|
|
update(u) {
|
|
this._view = u.view
|
|
var needsRebuild = u.docChanged || u.viewportChanged ||
|
|
(highlightActiveBlock && u.selectionSet)
|
|
if (needsRebuild) {
|
|
if (deferMode === 'raf' && u.viewportChanged && !u.docChanged) {
|
|
if (this._rafId) cancelAnimationFrame(this._rafId)
|
|
var self = this
|
|
this._rafId = requestAnimationFrame(function() {
|
|
self._rafId = null
|
|
self._stepPx = null
|
|
self._measurePending = false
|
|
self.decorations = self._build(u.view)
|
|
self._scheduleMeasure(u.view)
|
|
})
|
|
return
|
|
}
|
|
if (this._rafId) { cancelAnimationFrame(this._rafId); this._rafId = null }
|
|
this._stepPx = null
|
|
this._measurePending = false
|
|
this.decorations = this._build(u.view)
|
|
this._scheduleMeasure(u.view)
|
|
} else if (this._measurePending) {
|
|
this._measurePending = false
|
|
this.decorations = this._build(u.view)
|
|
}
|
|
}
|
|
|
|
_scheduleMeasure(view) {
|
|
if (this._measureId !== undefined) view.cancelMeasure(this._measureId)
|
|
this._measureId = view.requestMeasure(this._mkSpec())
|
|
}
|
|
|
|
_mkSpec() {
|
|
var self = this
|
|
return {
|
|
read: function(view) {
|
|
var state = view.state, iw = getIndentUnit(state), ts = state.tabSize, doc = state.doc
|
|
var fl = doc.lineAt(view.viewport.from).number
|
|
var ll = doc.lineAt(view.viewport.to).number
|
|
|
|
var hasIndent = false
|
|
for (var n = fl; n <= ll; n++) {
|
|
if (_ocImIndent(doc.line(n).text, ts) >= iw) { hasIndent = true; break }
|
|
}
|
|
|
|
var stepPx = 0, lineHeight = 0
|
|
|
|
if (hasIndent) {
|
|
for (var n = fl; n <= ll; n++) {
|
|
var t = doc.line(n).text
|
|
if (t.trim() === '') continue
|
|
if (_ocImIndent(t, ts) >= iw) {
|
|
var c0 = view.coordsAtPos(doc.line(n).from)
|
|
var c1 = view.coordsAtPos(doc.line(n).from + iw)
|
|
if (c0 && c1) { stepPx = Math.round((c1.left - c0.left) * 100) / 100; break }
|
|
}
|
|
}
|
|
}
|
|
|
|
if (fl < doc.lines) {
|
|
var c0 = view.coordsAtPos(doc.line(fl).from)
|
|
var c1 = view.coordsAtPos(doc.line(fl + 1).from)
|
|
if (c0 && c1) lineHeight = Math.round(c1.top - c0.top)
|
|
}
|
|
|
|
return lineHeight > 0
|
|
? { stepPx: stepPx > 0 ? stepPx : -1, lineHeight: lineHeight }
|
|
: (stepPx > 0 ? stepPx : -1)
|
|
},
|
|
write: function(result) {
|
|
var stepPx, lineHeight
|
|
if (typeof result === 'number') {
|
|
stepPx = result; lineHeight = 0
|
|
} else if (result) {
|
|
stepPx = result.stepPx; lineHeight = result.lineHeight || 0
|
|
} else {
|
|
stepPx = -1; lineHeight = 0
|
|
}
|
|
if (typeof stepPx === 'number') {
|
|
self._stepPx = stepPx > 0 ? stepPx : -1
|
|
self._lineHeight = lineHeight
|
|
self._measurePending = true
|
|
var v = self._view
|
|
// Defer dispatch past the current update cycle
|
|
queueMicrotask(function() {
|
|
if (v && self._measurePending) v.dispatch({})
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_build(view) {
|
|
var sa = hideFirstIndent ? 1 : 0
|
|
var state = view.state
|
|
var iw = getIndentUnit(state)
|
|
var ts = state.tabSize
|
|
var doc = state.doc
|
|
var fl = doc.lineAt(view.viewport.from).number
|
|
var ll = doc.lineAt(view.viewport.to).number
|
|
var bl = Math.max(1, fl - buffer), el = Math.min(doc.lines, ll + buffer)
|
|
|
|
var lvls = new Int32Array(el - bl + 1)
|
|
var bk = new Uint8Array(el - bl + 1)
|
|
for (var n = bl; n <= el; n++) {
|
|
var t = doc.line(n).text
|
|
var b = t.trim() === ''
|
|
bk[n - bl] = b ? 1 : 0
|
|
lvls[n - bl] = b ? -1 : Math.floor(_ocImIndent(t, ts) / iw)
|
|
}
|
|
for (var n = bl; n <= el; n++) {
|
|
if (!bk[n - bl]) continue
|
|
var p = 0, nx = 0
|
|
for (var x = n - 1; x >= bl; x--) if (!bk[x - bl]) { p = lvls[x - bl]; break }
|
|
for (var x = n + 1; x <= el; x++) if (!bk[x - bl]) { nx = lvls[x - bl]; break }
|
|
lvls[n - bl] = Math.min(p, nx)
|
|
}
|
|
|
|
var maxLvl = 0
|
|
for (var n = fl; n <= ll; n++) {
|
|
var lv = lvls[n - bl]
|
|
if (lv > maxLvl) maxLvl = lv
|
|
}
|
|
if (maxLvl <= sa) return Decoration.none
|
|
|
|
var scopeOpen = new Uint8Array(maxLvl)
|
|
var showGuides = new Array(ll - fl + 1)
|
|
|
|
for (var n = bl; n <= el; n++) {
|
|
var lv = lvls[n - bl]
|
|
var isBlank = bk[n - bl] === 1
|
|
|
|
if (!isBlank) {
|
|
for (var k = lv; k < maxLvl; k++) scopeOpen[k] = 0
|
|
}
|
|
|
|
if (n >= fl && n <= ll) {
|
|
var guides = new Uint8Array(maxLvl)
|
|
showGuides[n - fl] = guides
|
|
if (lv > sa) {
|
|
for (var k = sa; k < lv && k < maxLvl; k++) {
|
|
if (scopeOpen[k]) guides[k] = 1
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!isBlank) {
|
|
var nextN = -1, nextLv = -1
|
|
for (var x = n + 1; x <= el; x++) {
|
|
if (!bk[x - bl]) { nextN = x; nextLv = lvls[x - bl]; break }
|
|
}
|
|
if (nextN >= 0 && nextLv > lv && lv < maxLvl) {
|
|
scopeOpen[lv] = 1
|
|
}
|
|
}
|
|
}
|
|
|
|
var activeLvl = undefined
|
|
if (highlightActiveBlock) {
|
|
var cn = doc.lineAt(state.selection.main.head).number
|
|
if (cn >= fl && cn <= ll) {
|
|
var cl = lvls[cn - bl]
|
|
var cGuides = showGuides[cn - fl]
|
|
if (cGuides && cl > sa) {
|
|
for (var k = cl - 1; k >= sa; k--) {
|
|
if (cGuides[k]) { activeLvl = k; break }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var stepPx = this._stepPx
|
|
if (stepPx === null || stepPx <= 0)
|
|
stepPx = iw * (view.defaultCharacterWidth || 8)
|
|
var lh = this._lineHeight
|
|
|
|
var builder = new RangeSetBuilder()
|
|
for (var n = fl; n <= ll; n++) {
|
|
var guides = showGuides[n - fl]
|
|
if (!guides) continue
|
|
var parts = []
|
|
for (var k = sa; k < maxLvl; k++) {
|
|
if (!guides[k]) continue
|
|
var pos = Math.round(k * stepPx)
|
|
var cv = (activeLvl !== undefined && k === activeLvl)
|
|
? 'var(--oc-im-ca)' : 'var(--oc-im-c)'
|
|
parts.push('linear-gradient(' + cv + ',' + cv + ') ' + pos + 'px 0/' + thickness + 'px 100% no-repeat')
|
|
}
|
|
if (!parts.length) continue
|
|
var line = doc.line(n)
|
|
var style = '--oc-im:' + parts.join(',')
|
|
if (lh > 0) style += ';--oc-lh:' + lh + 'px'
|
|
builder.add(line.from, line.from, Decoration.line({
|
|
class: 'cm-oc-im',
|
|
attributes: { style: style }
|
|
}))
|
|
}
|
|
return builder.finish()
|
|
}
|
|
}, { decorations: v => v.decorations })
|
|
]
|
|
}
|
|
|
|
function topSearchExtension() {
|
|
return search({ top: true })
|
|
}
|
|
|
|
function placeholderExtension(text) {
|
|
return placeholder(text)
|
|
}
|
|
|
|
const mergeDefaultConfig = {
|
|
diffConfig: { scanLimit: 5000 },
|
|
gutter: true,
|
|
revertControls: "b-to-a",
|
|
collapseUnchanged: false,
|
|
}
|
|
|
|
// ============================================================
|
|
// Merge scrollbar mirror — clones .cm-scroller scrollbar rules from
|
|
// the active CM6 theme onto .cm-mergeView and #debug-rendered so
|
|
// that all scrollbars share the same theme-aware appearance.
|
|
// Also injects .cm-merge-revert button colours.
|
|
// ============================================================
|
|
function mirrorThemeScrollbar() {
|
|
var styles = document.querySelectorAll('style');
|
|
var themeStyle = null;
|
|
var themeText = null;
|
|
|
|
for (var i = 0; i < styles.length; i++) {
|
|
var text = styles[i].textContent || '';
|
|
if (text.indexOf('.cm-scroller::-webkit-scrollbar') === -1) continue;
|
|
themeStyle = styles[i];
|
|
themeText = text;
|
|
break;
|
|
}
|
|
|
|
var editorEl = document.querySelector('.cm-editor');
|
|
var contentEl = editorEl ? editorEl.querySelector('.cm-content') : null;
|
|
var cs = contentEl ? getComputedStyle(contentEl) : null;
|
|
var txt = cs ? cs.color : null;
|
|
var rgb = txt ? txt.match(/[\d.]+/g) : null;
|
|
var r, g, b;
|
|
if (rgb && rgb.length >= 3) { r = rgb[0]; g = rgb[1]; b = rgb[2]; }
|
|
|
|
if (r) {
|
|
var thumbColor = 'rgba(' + r + ',' + g + ',' + b + ',0.60)';
|
|
var thumbHover = 'rgba(' + r + ',' + g + ',' + b + ',0.70)';
|
|
} else {
|
|
var thumbColor = _isDarkMode() ? 'rgba(255,255,255,0.60)' : 'rgba(0,0,0,0.60)';
|
|
var thumbHover = _isDarkMode() ? 'rgba(255,255,255,0.70)' : 'rgba(0,0,0,0.70)';
|
|
}
|
|
|
|
if (!themeText) return;
|
|
|
|
var parts = [], m;
|
|
var reWebkit = /([^{}]*)\.cm-scroller(::-webkit-scrollbar[^{]*)\{([^}]*)\}/gi;
|
|
while ((m = reWebkit.exec(themeText)) !== null) {
|
|
var body = m[3].replace(/;/g, ' !important;');
|
|
parts.push('.cm-mergeView' + m[2] + '{' + m[3] + '}');
|
|
parts.push('#debug-rendered' + m[2] + '{' + body + '}');
|
|
parts.push('#debug-rendered pre' + m[2] + '{' + body + '}');
|
|
}
|
|
// Clone Firefox rules
|
|
var reFF = /([^{}]*)\.cm-scroller\{([^}]*scrollbar-(?:width|color)[^}]*)\}/gi;
|
|
while ((m = reFF.exec(themeText)) !== null) {
|
|
var body = m[2];
|
|
var sw = body.match(/scrollbar-width\s*:\s*[^;]+;?/);
|
|
var sc = body.match(/scrollbar-color\s*:\s*[^;]+;?/);
|
|
if (sw || sc) {
|
|
var ffRule = (sw ? sw[0] : '') + (sc ? sc[0] : '');
|
|
parts.push('.cm-mergeView{' + ffRule + '}');
|
|
parts.push('#debug-rendered{' + ffRule + '}');
|
|
parts.push('#debug-rendered pre{' + ffRule + '}');
|
|
}
|
|
}
|
|
|
|
var gutterBg = r ? 'rgba(' + r + ',' + g + ',' + b + ',0.06)' : 'rgba(175,184,193,0.06)';
|
|
parts.push(
|
|
'.cm-merge-revert{background:' + gutterBg + '}' +
|
|
'.cm-merge-revert button{color:' + (r ? 'rgba(' + r + ',' + g + ',' + b + ',0.5)' : '#656d76') + '}' +
|
|
'.cm-merge-revert button:hover{color:' + (txt || '#1f2328') + ';background:' + (r ? 'rgba(' + r + ',' + g + ',' + b + ',0.12)' : 'rgba(175,184,193,0.2)') + '}'
|
|
);
|
|
|
|
var marker = '/*oc-merge-mirror*/';
|
|
var idx = themeStyle.textContent.indexOf(marker);
|
|
if (idx !== -1) {
|
|
themeStyle.textContent = themeStyle.textContent.substring(0, idx);
|
|
}
|
|
|
|
var thumbVal = thumbColor + ' !important';
|
|
var hoverVal = thumbHover + ' !important';
|
|
parts.push(
|
|
'.cm-editor .cm-scroller::-webkit-scrollbar-thumb,' +
|
|
'.cm-mergeView::-webkit-scrollbar-thumb,' +
|
|
'#debug-rendered::-webkit-scrollbar-thumb,' +
|
|
'#debug-rendered pre::-webkit-scrollbar-thumb{background:' + thumbVal + ';background-color:' + thumbVal + '}',
|
|
'.cm-editor .cm-scroller::-webkit-scrollbar-thumb:hover,' +
|
|
'.cm-mergeView::-webkit-scrollbar-thumb:hover,' +
|
|
'#debug-rendered::-webkit-scrollbar-thumb:hover,' +
|
|
'#debug-rendered pre::-webkit-scrollbar-thumb:hover{background:' + hoverVal + ';background-color:' + hoverVal + '}'
|
|
);
|
|
themeStyle.textContent += marker + parts.join('');
|
|
}
|
|
|
|
// ============================================================
|
|
// Theme compartment — allows dynamic light/dark switching
|
|
// ============================================================
|
|
const themeCompartment = new Compartment
|
|
|
|
function themeExtension(isDark) {
|
|
return themeCompartment.of(isDark ? githubDark : githubLight)
|
|
}
|
|
|
|
function dispatchTheme(view, isDark) {
|
|
view.dispatch({
|
|
effects: themeCompartment.reconfigure(isDark ? githubDark : githubLight)
|
|
})
|
|
// Mirror scrollbar rules into the new theme stylesheet
|
|
mirrorThemeScrollbar();
|
|
}
|
|
|
|
// ============================================================
|
|
// Fullscreen utility
|
|
// ============================================================
|
|
|
|
function getActiveEditor() {
|
|
var active = document.activeElement;
|
|
if (active && active.closest) {
|
|
var mw = active.closest('.cm-mergeView');
|
|
if (mw) return mw;
|
|
var ed = active.closest('.cm-editor');
|
|
if (ed) return ed;
|
|
}
|
|
return document.querySelector('.cm-editor') || null;
|
|
}
|
|
|
|
function toggleFullscreen(dom) {
|
|
if (!dom) return false;
|
|
if (dom.classList.contains('cm-fullscreen')) {
|
|
dom.classList.remove('cm-fullscreen');
|
|
dom.id = '';
|
|
return false;
|
|
} else {
|
|
dom.classList.add('cm-fullscreen');
|
|
dom.id = 'oc-fullscreen-active';
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Theme detection — reads localStorage then falls back to system preference
|
|
// ============================================================
|
|
|
|
function _isDarkMode() {
|
|
var theme = localStorage.getItem('oc-theme') || 'auto';
|
|
if (theme === 'dark') return true;
|
|
if (theme === 'light') return false;
|
|
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
}
|
|
|
|
// ============================================================
|
|
// Markdown rendering (for debug log preview)
|
|
// ============================================================
|
|
|
|
var _hljsCSSInjected = false
|
|
function injectHljsCSS() {
|
|
if (_hljsCSSInjected) return
|
|
_hljsCSSInjected = true
|
|
var isDark = _isDarkMode()
|
|
var style = document.createElement("style")
|
|
style.id = "hljs-theme"
|
|
style.textContent = isDark ? githubDarkCSS : githubLightCSS
|
|
document.head.appendChild(style)
|
|
}
|
|
|
|
function switchHljsTheme(isDark) {
|
|
if (!_hljsCSSInjected) { injectHljsCSS(); if (!_hljsCSSInjected) return }
|
|
var style = document.getElementById("hljs-theme")
|
|
if (style) style.textContent = isDark ? githubDarkCSS : githubLightCSS
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """)
|
|
}
|
|
|
|
marked.use({
|
|
breaks: true,
|
|
gfm: true,
|
|
silent: true,
|
|
renderer: {
|
|
code: function(token) {
|
|
var lang = token.lang || ""
|
|
if (lang && hljs.getLanguage(lang)) {
|
|
injectHljsCSS()
|
|
var result = hljs.highlight(token.text, { language: lang, ignoreIllegals: true })
|
|
return '<pre><code class="hljs language-' + lang + '"><span class="code-content">' + result.value + '</span></code></pre>'
|
|
}
|
|
return '<pre><code><span class="code-content">' + escapeHtml(token.text) + '</span></code></pre>'
|
|
}
|
|
}
|
|
})
|
|
|
|
function renderMarkdown(text) {
|
|
if (!text) return ''
|
|
try { return marked.parse(text) } catch(e) { return text }
|
|
}
|
|
|
|
export {
|
|
EditorView, EditorState, Compartment,
|
|
lineNumbers, highlightActiveLine, highlightActiveLineGutter,
|
|
highlightSpecialChars, drawSelection, dropCursor,
|
|
rectangularSelection, crosshairCursor, cmKeymap as keymap,
|
|
defaultKeymap, history, historyKeymap, indentWithTab,
|
|
toggleComment, foldKeymap,
|
|
syntaxHighlighting, HighlightStyle, bracketMatching,
|
|
foldGutter, indentOnInput, StreamLanguage, indentUnit,
|
|
yaml, markdown, shell, properties,
|
|
linter, lintGutter, yamlLinter,
|
|
search, searchKeymap, highlightSelectionMatches,
|
|
autocompletion, closeBrackets, closeBracketsKeymap,
|
|
snippetCompletion, mihomoCompletion,
|
|
githubDark, githubLight,
|
|
MergeView,
|
|
logLanguage, logHighlightStyle,
|
|
baseExtensions, syntaxPreload, placeholderExtension, indentMarkerExtension,
|
|
topSearchExtension, mergeDefaultConfig, mirrorThemeScrollbar,
|
|
themeExtension, dispatchTheme,
|
|
switchHljsTheme,
|
|
renderMarkdown,
|
|
getActiveEditor, toggleFullscreen
|
|
}
|