completion.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. import * as monaco from 'monaco-editor'
  2. import * as facts from './facts'
  3. import { findIndexOfSections, ILeafConfKvItem, ILeafConfStruct, parseKvLine, parseSectionGeneral, parseStruct, splitByComma, trimComment, trimWithPos } from './parse'
  4. function collectProxyOrGroupSuggestions(
  5. model: monaco.editor.ITextModel,
  6. struct: ILeafConfStruct,
  7. range: monaco.Range,
  8. ) {
  9. return struct.sections.filter(s => s.sectionName === facts.SECTION_PROXY)
  10. .flatMap(s => Array.from({ length: s.endLine - s.startLine + 1 }, (_, i) => s.startLine + i))
  11. .map(lineId => parseKvLine(model.getLineContent(lineId), lineId, 1))
  12. .filter((kv): kv is ILeafConfKvItem => kv !== undefined)
  13. .map(kv => ({
  14. label: kv.key,
  15. kind: monaco.languages.CompletionItemKind.Variable,
  16. insertText: kv.key,
  17. detail: 'proxy',
  18. range,
  19. })).concat(struct.sections.filter(s => s.sectionName === facts.SECTION_PROXY_GROUP)
  20. .flatMap(s => Array.from({ length: s.endLine - s.startLine + 1 }, (_, i) => s.startLine + i))
  21. .map(lineId => parseKvLine(model.getLineContent(lineId), lineId, 1))
  22. .filter((kv): kv is ILeafConfKvItem => kv !== undefined)
  23. .map(kv => ({
  24. label: kv.key,
  25. kind: monaco.languages.CompletionItemKind.Variable,
  26. insertText: kv.key,
  27. detail: 'proxy group',
  28. range,
  29. })))
  30. }
  31. function generateBoolCandidates(range: monaco.Range): monaco.languages.CompletionItem[] {
  32. return ['true', 'false'].map(l => ({
  33. label: l,
  34. kind: monaco.languages.CompletionItemKind.Keyword,
  35. insertText: l,
  36. range,
  37. }))
  38. }
  39. function completeSectionHeader(
  40. model: monaco.editor.ITextModel,
  41. position: monaco.Position,
  42. struct: ILeafConfStruct,
  43. ): monaco.languages.CompletionItem[] {
  44. const appearedSections = new Set(struct.sections.map(s => s.sectionName))
  45. const suggestedSections = facts.KNOWN_SECTION_NAMES.filter(s => !appearedSections.has(s))
  46. const { trimmed: line, startCol } = trimWithPos(trimComment(model.getLineContent(position.lineNumber)), 1)
  47. if (!line.startsWith('[')) {
  48. return []
  49. }
  50. return suggestedSections.map(sectionName => ({
  51. label: `[${sectionName}]`,
  52. kind: monaco.languages.CompletionItemKind.Module,
  53. insertText: sectionName + ']',
  54. range: {
  55. startLineNumber: position.lineNumber,
  56. startColumn: startCol + 1,
  57. endLineNumber: position.lineNumber,
  58. endColumn: startCol + line.length,
  59. }
  60. }))
  61. }
  62. function completeGeneralSection(
  63. model: monaco.editor.ITextModel,
  64. position: monaco.Position,
  65. struct: ILeafConfStruct,
  66. ): monaco.languages.CompletionItem[] {
  67. const lineId = position.lineNumber
  68. const line = trimComment(model.getLineContent(lineId))
  69. const { startCol } = trimWithPos(line, 1)
  70. const eqPos = line.indexOf('=')
  71. let eqCol = eqPos + 1
  72. if (eqPos === -1) {
  73. eqCol = line.length + 1
  74. }
  75. if (position.column <= eqCol) {
  76. const filledKeys = new Set(parseSectionGeneral(model, struct).map(i => i.key))
  77. const range = new monaco.Range(lineId, startCol, lineId, eqCol)
  78. return facts.GENERAL_SETTING_KEYS.filter(k => !filledKeys.has(k.name)).map(k => {
  79. switch (k.name) {
  80. case facts.SETTING_TUN:
  81. return {
  82. label: k.name,
  83. kind: monaco.languages.CompletionItemKind.Keyword,
  84. range,
  85. insertText: eqPos === -1 ? `${k.name} = auto` : k.name + ' ',
  86. documentation: k.desc,
  87. }
  88. case facts.SETTING_LOGLEVEL:
  89. return {
  90. label: k.name,
  91. kind: monaco.languages.CompletionItemKind.Keyword,
  92. range,
  93. insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
  94. insertText: eqPos === -1 ? `${k.name} = \${1|${facts.LOG_LEVELS.join(',')}|}` : k.name + ' ',
  95. documentation: k.desc,
  96. }
  97. case facts.SETTING_ROUTING_DOMAIN_RESOLVE:
  98. return {
  99. label: k.name,
  100. kind: monaco.languages.CompletionItemKind.Keyword,
  101. range,
  102. insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
  103. insertText: eqPos === -1 ? `${k.name} = \${1|true,false|}` : k.name + ' ',
  104. documentation: k.desc,
  105. }
  106. }
  107. return {
  108. label: k.name,
  109. kind: monaco.languages.CompletionItemKind.Keyword,
  110. range,
  111. insertText: eqPos === -1 ? k.name + ' = ' : k.name + ' ',
  112. documentation: k.desc,
  113. }
  114. })
  115. }
  116. const kv = parseKvLine(line, lineId, 1)!
  117. const range = new monaco.Range(lineId, kv.valueStartCol, lineId, kv.valueStartCol + kv.value.length)
  118. switch (kv.key) {
  119. case facts.SETTING_TUN:
  120. return [{
  121. label: 'auto',
  122. kind: monaco.languages.CompletionItemKind.Keyword,
  123. range,
  124. insertText: 'auto',
  125. }]
  126. case facts.SETTING_LOGLEVEL:
  127. return facts.LOG_LEVELS.map(l => ({
  128. label: l,
  129. kind: monaco.languages.CompletionItemKind.Keyword,
  130. range,
  131. insertText: l,
  132. }))
  133. case facts.SETTING_ROUTING_DOMAIN_RESOLVE:
  134. return generateBoolCandidates(range)
  135. }
  136. return []
  137. }
  138. function completeProxy(
  139. model: monaco.editor.ITextModel,
  140. position: monaco.Position,
  141. struct: ILeafConfStruct,
  142. ): monaco.languages.CompletionItem[] {
  143. const lineId = position.lineNumber
  144. const line = trimComment(model.getLineContent(lineId))
  145. const eqPos = line.indexOf('=')
  146. let eqCol = eqPos + 1
  147. if (eqPos === -1) {
  148. return []
  149. }
  150. if (position.column <= eqCol) {
  151. return []
  152. }
  153. const argsText = line.slice(eqPos + 1)
  154. const args = splitByComma(argsText, eqCol + 1)
  155. const protocolItem = args[0]
  156. if (protocolItem === undefined || args.length === 1) {
  157. const range = protocolItem === undefined
  158. ? new monaco.Range(lineId, position.column, lineId, position.column)
  159. : new monaco.Range(
  160. lineId,
  161. Math.min(position.column, protocolItem.startCol),
  162. lineId,
  163. Math.max(position.column, protocolItem.startCol + protocolItem.text.length))
  164. return facts.PROXY_PROTOCOLS.map(p => ({
  165. label: p.name,
  166. kind: monaco.languages.CompletionItemKind.Class,
  167. insertText: p.snippet,
  168. insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
  169. documentation: p.desc,
  170. range,
  171. }))
  172. } else if (position.column <= protocolItem.startCol + protocolItem.text.length) {
  173. const range = new monaco.Range(lineId, protocolItem.startCol, lineId, protocolItem.startCol + protocolItem.text.length)
  174. return facts.PROXY_PROTOCOLS.map(p => ({
  175. label: p.name,
  176. kind: monaco.languages.CompletionItemKind.Class,
  177. insertText: p.name,
  178. documentation: p.desc,
  179. range,
  180. }))
  181. }
  182. let expectedNonKvArgs = 3
  183. if (
  184. protocolItem.text === facts.PROTOCOL_DIRECT
  185. || protocolItem.text === facts.PROTOCOL_REJECT
  186. || protocolItem.text === facts.PROTOCOL_REJECT_DROP
  187. ) {
  188. expectedNonKvArgs = 1
  189. }
  190. const currentArgId = argsText.slice(0, position.column - eqCol - 1).split(',').length - 1
  191. if (currentArgId < expectedNonKvArgs) {
  192. return []
  193. }
  194. const currentArg = args[currentArgId]
  195. const currentKv = parseKvLine(currentArg.text, lineId, currentArg.startCol)
  196. if (currentKv === undefined || position.column <= currentKv.keyStartCol + currentKv.key.length) {
  197. let protocolKeyMap = facts.PROXY_PROTOCOL_PROPERTY_KEY_MAP[protocolItem.text]
  198. if (protocolKeyMap === undefined) {
  199. return []
  200. }
  201. protocolKeyMap = {
  202. required: new Set(protocolKeyMap.required),
  203. allowed: new Set(protocolKeyMap.allowed),
  204. }
  205. let firstKvArgId = args.findIndex(i => i.text.indexOf('=') !== -1)
  206. if (firstKvArgId === -1) {
  207. firstKvArgId = args.length
  208. }
  209. for (const kvArg of args.slice(firstKvArgId)) {
  210. const kv = parseKvLine(kvArg.text, lineId, kvArg.startCol)
  211. if (kv === undefined) {
  212. continue
  213. }
  214. protocolKeyMap.allowed.delete(kv.key)
  215. protocolKeyMap.required.delete(kv.key)
  216. }
  217. return [...protocolKeyMap.required, ...protocolKeyMap.allowed].map(k => ({
  218. label: k,
  219. kind: monaco.languages.CompletionItemKind.Property,
  220. insertText: k,
  221. documentation: facts.PROXY_PROPERTY_KEYS_DESC_MAP.get(k),
  222. range: currentKv === undefined
  223. ? new monaco.Range(lineId, currentArg.startCol, lineId, currentArg.startCol + currentArg.text.length)
  224. : new monaco.Range(lineId, currentKv.keyStartCol, lineId, currentKv.keyStartCol + currentKv.key.length),
  225. }))
  226. }
  227. const range = new monaco.Range(lineId, currentKv.valueStartCol, lineId, currentKv.valueStartCol + currentKv.value.length)
  228. switch (currentKv.key) {
  229. case facts.PROXY_PROPERTY_KEY_METHOD:
  230. return facts.KNOWN_AEAD_CIPHERS.map(c => ({
  231. label: c,
  232. kind: monaco.languages.CompletionItemKind.EnumMember,
  233. insertText: c,
  234. range,
  235. }))
  236. case facts.PROXY_PROPERTY_KEY_WS:
  237. case facts.PROXY_PROPERTY_KEY_TLS:
  238. case facts.PROXY_PROPERTY_KEY_AMUX:
  239. case facts.PROXY_PROPERTY_KEY_QUIC:
  240. return generateBoolCandidates(range)
  241. case facts.PROXY_PROPERTY_KEY_TLS_CERT:
  242. // TODO: complete cert files
  243. break
  244. case facts.PROXY_PROPERTY_KEY_INTERFACE:
  245. // TODO: complete interfaces
  246. break
  247. }
  248. return []
  249. }
  250. function completeProxyGroup(
  251. model: monaco.editor.ITextModel,
  252. position: monaco.Position,
  253. struct: ILeafConfStruct,
  254. ): monaco.languages.CompletionItem[] {
  255. const lineId = position.lineNumber
  256. const line = trimComment(model.getLineContent(lineId))
  257. const eqPos = line.indexOf('=')
  258. let eqCol = eqPos + 1
  259. if (eqPos === -1) {
  260. return []
  261. }
  262. if (position.column <= eqCol) {
  263. return []
  264. }
  265. const argsText = line.slice(eqPos + 1)
  266. const args = splitByComma(argsText, eqCol + 1)
  267. const groupTypeItem = args[0]
  268. if (groupTypeItem === undefined || args.length === 1) {
  269. const range = groupTypeItem === undefined
  270. ? new monaco.Range(lineId, position.column, lineId, position.column)
  271. : new monaco.Range(
  272. lineId,
  273. Math.min(position.column, groupTypeItem.startCol),
  274. lineId,
  275. Math.max(position.column, groupTypeItem.startCol + groupTypeItem.text.length))
  276. return facts.GROUP_TYPES.map(p => ({
  277. label: p.name,
  278. kind: monaco.languages.CompletionItemKind.Constructor,
  279. insertText: p.snippet,
  280. insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
  281. documentation: p.desc,
  282. range,
  283. }))
  284. } else if (position.column <= groupTypeItem.startCol + groupTypeItem.text.length) {
  285. const range = new monaco.Range(lineId, groupTypeItem.startCol, lineId, groupTypeItem.startCol + groupTypeItem.text.length)
  286. return facts.GROUP_TYPES.map(p => ({
  287. label: p.name,
  288. kind: monaco.languages.CompletionItemKind.Constructor,
  289. insertText: p.name,
  290. documentation: p.desc,
  291. range,
  292. }))
  293. }
  294. const currentArgId = argsText.slice(0, position.column - eqCol - 1).split(',').length - 1
  295. const currentArg = args[currentArgId]
  296. const currentKv = parseKvLine(currentArg.text, lineId, currentArg.startCol)
  297. if (currentKv !== undefined && position.column >= currentKv.valueStartCol) {
  298. // Want a property value
  299. const range = new monaco.Range(lineId, currentKv.valueStartCol, lineId, currentKv.valueStartCol + currentKv.value.length)
  300. switch (currentKv.key) {
  301. case facts.GROUP_PROPERTY_KEY_METHOD:
  302. return facts.KNOWN_GROUP_METHODS.map(m => ({
  303. label: m,
  304. kind: monaco.languages.CompletionItemKind.EnumMember,
  305. insertText: m,
  306. range,
  307. documentation: 'TODO: doc',
  308. }))
  309. case facts.GROUP_PROPERTY_KEY_FAILOVER:
  310. case facts.GROUP_PROPERTY_KEY_FALLBACK_CACHE:
  311. case facts.GROUP_PROPERTY_KEY_HEALTH_CHECK:
  312. return generateBoolCandidates(range)
  313. case facts.GROUP_PROPERTY_KEY_LAST_RESORT:
  314. return collectProxyOrGroupSuggestions(model, struct, range)
  315. }
  316. return []
  317. }
  318. let firstKvArgId = args.findIndex(i => i.text.indexOf('=') !== -1)
  319. if (firstKvArgId === -1) {
  320. firstKvArgId = args.length
  321. }
  322. let groupTypeKeyMap = facts.PROXY_GROUP_PROPERTY_KEY_MAP[groupTypeItem.text]
  323. if (groupTypeKeyMap === undefined) {
  324. return []
  325. }
  326. let proxyOrGroupNameSuggestions: monaco.languages.CompletionItem[] = []
  327. if (currentKv === undefined && currentArgId < firstKvArgId) {
  328. const range = new monaco.Range(lineId, currentArg.startCol, lineId, currentArg.startCol + currentArg.text.length)
  329. proxyOrGroupNameSuggestions = collectProxyOrGroupSuggestions(model, struct, range)
  330. if (currentArgId < firstKvArgId - 1) {
  331. return proxyOrGroupNameSuggestions
  332. }
  333. }
  334. groupTypeKeyMap = {
  335. required: new Set(groupTypeKeyMap.required),
  336. allowed: new Set(groupTypeKeyMap.allowed),
  337. }
  338. for (const kvArg of args.slice(firstKvArgId)) {
  339. const kv = parseKvLine(kvArg.text, lineId, kvArg.startCol)
  340. if (kv === undefined) {
  341. continue
  342. }
  343. groupTypeKeyMap.allowed.delete(kv.key)
  344. groupTypeKeyMap.required.delete(kv.key)
  345. }
  346. const propertyKeySuggestions: monaco.languages.CompletionItem[] = [...groupTypeKeyMap.required, ...groupTypeKeyMap.allowed].map(k => ({
  347. label: k,
  348. kind: monaco.languages.CompletionItemKind.Property,
  349. insertText: k,
  350. documentation: facts.GROUP_PROPERTY_KEYS_DESC_MAP.get(k),
  351. range: currentKv === undefined
  352. ? new monaco.Range(lineId, currentArg.startCol, lineId, currentArg.startCol + currentArg.text.length)
  353. : new monaco.Range(lineId, currentKv.keyStartCol, lineId, currentKv.keyStartCol + currentKv.key.length),
  354. }))
  355. return propertyKeySuggestions.concat(proxyOrGroupNameSuggestions)
  356. }
  357. function completeRule(model: monaco.editor.ITextModel,
  358. position: monaco.Position,
  359. struct: ILeafConfStruct,
  360. ): monaco.languages.CompletionItem[] {
  361. const lineId = position.lineNumber
  362. const line = trimComment(model.getLineContent(lineId))
  363. const args = splitByComma(line, 1)
  364. const currentArgId = line.slice(0, position.column - 1).split(',').length - 1
  365. if (currentArgId === 0) {
  366. if (args.length > 1) {
  367. const range = new monaco.Range(
  368. lineId,
  369. Math.min(position.column, args[0].startCol),
  370. lineId,
  371. Math.max(position.column, args[0].startCol + args[0].text.length),
  372. )
  373. return facts.RULE_TYPES.map(p => ({
  374. label: p.name,
  375. kind: monaco.languages.CompletionItemKind.Function,
  376. insertText: p.name,
  377. documentation: p.desc,
  378. range,
  379. }))
  380. } else {
  381. const range = new monaco.Range(
  382. lineId,
  383. Math.min(args[0].startCol),
  384. lineId,
  385. args[0].startCol + args[0].text.length,
  386. )
  387. return facts.RULE_TYPES.map(p => ({
  388. label: p.name,
  389. kind: monaco.languages.CompletionItemKind.Function,
  390. insertText: p.snippet,
  391. insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
  392. documentation: p.desc,
  393. range,
  394. }))
  395. }
  396. }
  397. const ruleType = args[0].text.trim()
  398. if (ruleType === facts.RULE_TYPE_FINAL && currentArgId === 1 || currentArgId === 2) {
  399. const range = new monaco.Range(lineId, args[currentArgId].startCol, lineId, args[currentArgId].startCol + args[currentArgId].text.length)
  400. return collectProxyOrGroupSuggestions(model, struct, range)
  401. }
  402. if (ruleType === facts.RULE_TYPE_FINAL && currentArgId > 1 || currentArgId > 2) {
  403. return []
  404. }
  405. if (ruleType === facts.RULE_TYPE_EXTERNAL) {
  406. let colonPos = args[currentArgId].text.indexOf(':')
  407. if (colonPos === -1) {
  408. const range = new monaco.Range(
  409. lineId,
  410. Math.min(position.column, args[currentArgId].startCol),
  411. lineId,
  412. Math.max(position.column, args[currentArgId].startCol + args[currentArgId].text.length),
  413. )
  414. return [facts.RULE_EXTERNAL_SOURCE_SITE, facts.RULE_EXTERNAL_SOURCE_MMDB].map(p => ({
  415. label: p,
  416. kind: monaco.languages.CompletionItemKind.Field,
  417. insertText: p + ':',
  418. documentation: 'TODO: doc',
  419. range,
  420. }))
  421. }
  422. const colonCol = args[currentArgId].startCol + colonPos
  423. if (position.column > colonCol) {
  424. return []
  425. }
  426. const range = new monaco.Range(
  427. lineId,
  428. Math.min(position.column, args[currentArgId].startCol),
  429. lineId,
  430. Math.max(colonCol),
  431. )
  432. return [facts.RULE_EXTERNAL_SOURCE_SITE, facts.RULE_EXTERNAL_SOURCE_MMDB].map(p => ({
  433. label: p,
  434. kind: monaco.languages.CompletionItemKind.Field,
  435. insertText: p,
  436. documentation: 'TODO: doc',
  437. range,
  438. }))
  439. }
  440. if (ruleType === facts.RULE_TYPE_NETWORK) {
  441. const range = new monaco.Range(
  442. lineId,
  443. Math.min(position.column, args[currentArgId].startCol),
  444. lineId,
  445. Math.max(position.column, args[currentArgId].startCol + args[currentArgId].text.length),
  446. )
  447. return [facts.RULE_NETWORK_TCP, facts.RULE_NETWORK_UDP].map(p => ({
  448. label: p,
  449. kind: monaco.languages.CompletionItemKind.EnumMember,
  450. insertText: p,
  451. documentation: 'TODO: doc',
  452. range,
  453. }))
  454. }
  455. return []
  456. }
  457. export const completionProvider: monaco.languages.CompletionItemProvider = {
  458. triggerCharacters: [' ', '[', ',', '='],
  459. provideCompletionItems(model, position) {
  460. const textUntilPosition = model.getValueInRange({
  461. startLineNumber: position.lineNumber,
  462. startColumn: 1,
  463. endLineNumber: position.lineNumber,
  464. endColumn: position.column,
  465. })
  466. if (textUntilPosition.includes('#')) {
  467. // Inside a comment
  468. return { suggestions: [] }
  469. }
  470. const struct = parseStruct(model)
  471. const sectionHeaderOpenMatch = textUntilPosition.match(/^(\s*)\[(\s*)/)
  472. if (sectionHeaderOpenMatch) {
  473. return { suggestions: completeSectionHeader(model, position, struct) }
  474. }
  475. const sectionIndex = findIndexOfSections(struct.sections, position.lineNumber)
  476. if (sectionIndex === -1) {
  477. return { suggestions: [] }
  478. }
  479. let suggestions: monaco.languages.CompletionItem[] = []
  480. switch (struct.sections[sectionIndex].sectionName) {
  481. case facts.SECTION_GENERAL:
  482. suggestions = completeGeneralSection(model, position, struct)
  483. break
  484. case facts.SECTION_PROXY:
  485. suggestions = completeProxy(model, position, struct)
  486. break
  487. case facts.SECTION_PROXY_GROUP:
  488. suggestions = completeProxyGroup(model, position, struct)
  489. break
  490. case facts.SECTION_RULE:
  491. suggestions = completeRule(model, position, struct)
  492. break
  493. }
  494. return { suggestions, incomplete: true }
  495. },
  496. }