nadesiko3 3.3.49 → 3.3.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +143 -143
  3. package/batch/.DS_Store +0 -0
  4. package/batch/browsers.template.md +3 -3
  5. package/batch/build_browsers.nako3 +72 -72
  6. package/batch/build_command.nako3 +44 -44
  7. package/batch/build_nako_version.nako3 +42 -42
  8. package/batch/calc_hash.nako3 +29 -29
  9. package/batch/cmd_txt2json.nako3 +74 -74
  10. package/batch/command.txt +79 -0
  11. package/batch/command_nakopad.txt +69 -0
  12. package/batch/download-extlib.nako3 +43 -43
  13. package/batch/gen_command_nakopad.nako3 +57 -57
  14. package/batch/jsplugin2text.nako3 +285 -285
  15. package/batch/pickup_command.nako3 +110 -110
  16. package/batch/pickup_reserved_words.nako3 +11 -11
  17. package/batch/publish_version.nako3 +46 -46
  18. package/batch/show_agents.js +14 -14
  19. package/batch/turtle2js.nako3 +21 -21
  20. package/bin/cnako3 +10 -10
  21. package/core/LICENSE +21 -21
  22. package/core/README.md +66 -66
  23. package/core/batch/build_nako_version.nako3 +42 -42
  24. package/core/command/snako.mjs +105 -105
  25. package/core/command/snako.mts +116 -116
  26. package/core/index.mjs +21 -21
  27. package/core/package.json +47 -47
  28. package/core/sample/hello.nako3 +7 -7
  29. package/core/sample/hoge.mjs +4 -4
  30. package/core/sample/hoge.mts +6 -6
  31. package/core/src/nako3.mjs +864 -858
  32. package/core/src/nako3.mts +976 -967
  33. package/core/src/nako_colors.mjs +78 -78
  34. package/core/src/nako_colors.mts +86 -86
  35. package/core/src/nako_core_version.mjs +8 -8
  36. package/core/src/nako_core_version.mts +19 -19
  37. package/core/src/nako_csv.mjs +185 -185
  38. package/core/src/nako_csv.mts +188 -188
  39. package/core/src/nako_errors.mjs +173 -173
  40. package/core/src/nako_errors.mts +197 -197
  41. package/core/src/nako_from_dncl.mjs +255 -255
  42. package/core/src/nako_from_dncl.mts +250 -250
  43. package/core/src/nako_gen.mjs +1647 -1648
  44. package/core/src/nako_gen.mts +1718 -1719
  45. package/core/src/nako_gen_async.mjs +1659 -1659
  46. package/core/src/nako_gen_async.mts +1732 -1732
  47. package/core/src/nako_global.mjs +107 -107
  48. package/core/src/nako_global.mts +138 -138
  49. package/core/src/nako_indent.mjs +445 -445
  50. package/core/src/nako_indent.mts +492 -492
  51. package/core/src/nako_josi_list.mjs +38 -38
  52. package/core/src/nako_josi_list.mts +45 -45
  53. package/core/src/nako_lex_rules.mjs +253 -253
  54. package/core/src/nako_lex_rules.mts +260 -260
  55. package/core/src/nako_lexer.mjs +609 -609
  56. package/core/src/nako_lexer.mts +612 -612
  57. package/core/src/nako_logger.mjs +199 -199
  58. package/core/src/nako_logger.mts +232 -232
  59. package/core/src/nako_parser3.mjs +2439 -2439
  60. package/core/src/nako_parser3.mts +2195 -2195
  61. package/core/src/nako_parser_base.mjs +370 -370
  62. package/core/src/nako_parser_base.mts +370 -370
  63. package/core/src/nako_parser_const.mjs +37 -37
  64. package/core/src/nako_parser_const.mts +37 -37
  65. package/core/src/nako_prepare.mjs +304 -304
  66. package/core/src/nako_prepare.mts +315 -315
  67. package/core/src/nako_reserved_words.mjs +38 -38
  68. package/core/src/nako_reserved_words.mts +38 -38
  69. package/core/src/nako_source_mapping.mjs +207 -207
  70. package/core/src/nako_source_mapping.mts +262 -262
  71. package/core/src/nako_test.mjs +37 -37
  72. package/core/src/nako_types.mjs +25 -25
  73. package/core/src/nako_types.mts +151 -151
  74. package/core/src/plugin_csv.mjs +49 -49
  75. package/core/src/plugin_csv.mts +50 -50
  76. package/core/src/plugin_math.mjs +328 -328
  77. package/core/src/plugin_math.mts +326 -326
  78. package/core/src/plugin_promise.mjs +91 -91
  79. package/core/src/plugin_promise.mts +91 -91
  80. package/core/src/plugin_system.mjs +2832 -2832
  81. package/core/src/plugin_system.mts +2690 -2690
  82. package/core/src/plugin_test.mjs +34 -34
  83. package/core/src/plugin_test.mts +34 -34
  84. package/demo/.DS_Store +0 -0
  85. package/demo/ace_editor.html +89 -89
  86. package/demo/ace_editor_tabs.html +161 -161
  87. package/demo/basic.html +71 -71
  88. package/demo/browsers.html +10 -9
  89. package/demo/css/basic.css +3 -3
  90. package/demo/css/common.css +157 -157
  91. package/demo/css/editor.css +8 -8
  92. package/demo/css/flow.css +3 -3
  93. package/demo/css/index.css +3 -3
  94. package/demo/extlib/.DS_Store +0 -0
  95. package/demo/flow.html +98 -98
  96. package/demo/graph.html +53 -53
  97. package/demo/image/nakopad-icon256.png +0 -0
  98. package/demo/index.html +133 -133
  99. package/demo/js/common.js +17 -17
  100. package/demo/js/turtle3d_test.js +44 -44
  101. package/demo/js/turtle_test.js +45 -45
  102. package/demo/nako3/calc.nako3 +4 -4
  103. package/demo/runscript.html +47 -47
  104. package/demo/runscript2.html +33 -33
  105. package/demo/runscript3.html +35 -35
  106. package/demo/runscript4.html +33 -33
  107. package/demo/turtle.html +58 -58
  108. package/demo/turtle2.html +141 -141
  109. package/demo/turtle3.html +279 -279
  110. package/demo/turtle3d.html +58 -58
  111. package/demo/turtle3d2.html +107 -107
  112. package/demo/version.html +24 -24
  113. package/doc/SETUP.md +157 -157
  114. package/doc/about.md +17 -17
  115. package/doc/browsers.md +26 -26
  116. package/doc/docgen.md +21 -21
  117. package/doc/editor.md +44 -44
  118. package/doc/files.md +39 -39
  119. package/doc/plugins.md +234 -234
  120. package/doc/release.md +79 -79
  121. package/doc/textlint.md +43 -43
  122. package/doc/win32.md +57 -57
  123. package/package.json +195 -195
  124. package/release/_hash.txt +28 -28
  125. package/release/_script-tags.txt +14 -14
  126. package/release/command.json +1 -1
  127. package/release/command.json.js +1 -1
  128. package/release/command_cnako3.json +1 -1
  129. package/release/command_list.json +1 -1
  130. package/release/editor.js +1 -1
  131. package/release/nako_gen_async.js +1 -1
  132. package/release/nako_gen_async.js.LICENSE.txt +35 -0
  133. package/release/plugin_caniuse.js.LICENSE.txt +11 -0
  134. package/release/plugin_csv.js +1 -1
  135. package/release/plugin_csv.js.LICENSE.txt +15 -0
  136. package/release/plugin_datetime.js.LICENSE.txt +15 -0
  137. package/release/plugin_kansuji.js.LICENSE.txt +3 -0
  138. package/release/plugin_markup.js.LICENSE.txt +11 -0
  139. package/release/plugin_turtle.js.LICENSE.txt +15 -0
  140. package/release/plugin_webworker.js.LICENSE.txt +3 -0
  141. package/release/plugin_weykturtle3d.js.LICENSE.txt +3 -0
  142. package/release/stats.json +1 -1
  143. package/release/version.js +1 -1
  144. package/release/wnako3.js +1 -1
  145. package/release/wnako3webworker.js +1 -1
  146. package/release/wnako3webworker.js.LICENSE.txt +131 -0
  147. package/src/.DS_Store +0 -0
  148. package/src/browsers.txt +12 -11
  149. package/src/browsers_agents.json +2 -2
  150. package/src/browsers_agents.mjs +1 -1
  151. package/src/cnako3.mjs +17 -17
  152. package/src/cnako3.mts +18 -18
  153. package/src/cnako3mod.mjs +707 -707
  154. package/src/cnako3mod.mts +712 -712
  155. package/src/commander_ja.mjs +164 -164
  156. package/src/commander_ja.mts +161 -161
  157. package/src/enako3.mjs +68 -68
  158. package/src/era.mjs +22 -22
  159. package/src/image_turtle-elephant.mjs +2 -2
  160. package/src/image_turtle-panda.mjs +2 -2
  161. package/src/image_turtle64.mjs +2 -2
  162. package/src/index.mjs +9 -9
  163. package/src/index.mts +10 -10
  164. package/src/nako3editorfix.sfd +106 -106
  165. package/src/nako_version.mjs +8 -8
  166. package/src/nako_version.mts +2 -2
  167. package/src/plugin_browser.mjs +213 -213
  168. package/src/plugin_browser.mts +206 -206
  169. package/src/plugin_browser_ajax.mjs +399 -399
  170. package/src/plugin_browser_audio.mjs +109 -109
  171. package/src/plugin_browser_canvas.mjs +449 -449
  172. package/src/plugin_browser_chart.mjs +294 -294
  173. package/src/plugin_browser_color.mjs +49 -49
  174. package/src/plugin_browser_crypto.mjs +26 -26
  175. package/src/plugin_browser_dialog.mjs +53 -53
  176. package/src/plugin_browser_dom_basic.mjs +336 -336
  177. package/src/plugin_browser_dom_event.mjs +193 -193
  178. package/src/plugin_browser_dom_parts.mjs +396 -396
  179. package/src/plugin_browser_geolocation.mjs +51 -51
  180. package/src/plugin_browser_hotkey.mjs +25 -25
  181. package/src/plugin_browser_html.mjs +59 -59
  182. package/src/plugin_browser_in_worker.mjs +45 -45
  183. package/src/plugin_browser_location.mjs +21 -21
  184. package/src/plugin_browser_speech.mjs +111 -111
  185. package/src/plugin_browser_storage.mjs +121 -121
  186. package/src/plugin_browser_system.mjs +31 -31
  187. package/src/plugin_browser_websocket.mjs +73 -73
  188. package/src/plugin_caniuse.mjs +29 -29
  189. package/src/plugin_datetime.mjs +394 -394
  190. package/src/plugin_httpserver.mjs +277 -277
  191. package/src/plugin_httpserver.mts +286 -286
  192. package/src/plugin_kansuji.mjs +224 -224
  193. package/src/plugin_keigo.mjs +55 -55
  194. package/src/plugin_markup.mjs +32 -32
  195. package/src/plugin_node.mjs +1047 -1047
  196. package/src/plugin_node.mts +980 -980
  197. package/src/plugin_turtle.mjs +647 -647
  198. package/src/plugin_webworker.mjs +334 -334
  199. package/src/plugin_weykturtle3d.mjs +1214 -1214
  200. package/src/plugin_worker.mjs +95 -95
  201. package/src/repl.nako3 +63 -63
  202. package/src/wnako3.mjs +12 -12
  203. package/src/wnako3.mts +11 -11
  204. package/src/wnako3_editor.css +215 -215
  205. package/src/wnako3_editor.mjs +1542 -1542
  206. package/src/wnako3_editor.mts +1658 -1657
  207. package/src/wnako3mod.mjs +213 -213
  208. package/src/wnako3mod.mts +214 -214
  209. package/src/wnako3webworker.mjs +69 -69
  210. package/test/.DS_Store +0 -0
  211. package/test/ace_editor/karma.config.js +94 -94
  212. package/test/ace_editor/test/.babelrc.json +3 -3
  213. package/test/ace_editor/test/ace_editor_test.js +178 -178
  214. package/test/ace_editor/test/html/custom_context.html +139 -139
  215. package/test/async/async_basic_test.mjs +122 -122
  216. package/test/browser/.DS_Store +0 -0
  217. package/test/browser/karma.config.js +221 -221
  218. package/test/browser/test/.babelrc.json +3 -3
  219. package/test/browser/test/compare_util.js +50 -50
  220. package/test/browser/test/html/div_basic.html +2 -2
  221. package/test/browser/test/html/event_dom_form.html +4 -4
  222. package/test/browser/test/html/event_dom_scrolldiv.html +5 -5
  223. package/test/browser/test/import_plugin_checker.js +24 -24
  224. package/test/browser/test/plugin_browser_test.js +51 -51
  225. package/test/browser/test/plugin_browser_test_ajax.js +123 -123
  226. package/test/browser/test/plugin_browser_test_color.js +18 -18
  227. package/test/browser/test/plugin_browser_test_dialog.js +72 -72
  228. package/test/browser/test/plugin_browser_test_dom_event.js +598 -598
  229. package/test/browser/test/plugin_browser_test_dom_parts.js +125 -125
  230. package/test/browser/test/plugin_browser_test_system.js +9 -9
  231. package/test/browser/test/plugin_turtle_test.js +817 -817
  232. package/test/browser/test/plugin_webworker_test.js +86 -86
  233. package/test/browser/test/require_test.js +68 -68
  234. package/test/bundled/.DS_Store +0 -0
  235. package/test/bundled/karma.config.base.js +117 -117
  236. package/test/bundled/karma.config.js +86 -86
  237. package/test/bundled/test/.babelrc.json +3 -3
  238. package/test/bundled/test/bundled_test.js +69 -69
  239. package/test/bundled/test/html/custom_context.html +65 -65
  240. package/test/bundled/test/html/custom_debug.html +66 -66
  241. package/test/bundled/test4b.cmd +52 -52
  242. package/test/bundled/test_base/.DS_Store +0 -0
  243. package/test/bundled/test_base/.babelrc.json +3 -3
  244. package/test/bundled/test_base/_checktool_test.js +25 -25
  245. package/test/bundled/test_base/basic_ajax_test.js +56 -56
  246. package/test/bundled/test_base/basic_async_test.js +18 -18
  247. package/test/bundled/test_base/basic_test.js +153 -153
  248. package/test/bundled/test_base/calc_test.js +132 -132
  249. package/test/bundled/test_base/css/browsers_box.css +114 -114
  250. package/test/bundled/test_base/html/custom_context.html +69 -69
  251. package/test/bundled/test_base/html/custom_debug.html +71 -71
  252. package/test/bundled/test_base/js/browsers_box.js +72 -72
  253. package/test/bundled/test_base/plugin_csv_test.js +37 -37
  254. package/test/bundled/test_base/plugin_datetime_test.js +115 -115
  255. package/test/bundled/test_base/plugin_kansuji_test.js +49 -49
  256. package/test/bundled/test_base/plugin_system_test.js +410 -410
  257. package/test/bundled/test_base/plugin_webworker_test.js +53 -53
  258. package/test/bundled/test_base/test_utils.js +191 -191
  259. package/test/common/.DS_Store +0 -0
  260. package/test/common/plugin_browser_test.mjs +22 -22
  261. package/test/common/plugin_browser_ut_audio_test.mjs +108 -108
  262. package/test/common/plugin_browser_ut_color_test.mjs +21 -21
  263. package/test/common/plugin_browser_ut_dialog_test.mjs +100 -100
  264. package/test/common/plugin_browser_ut_html_test.mjs +13 -13
  265. package/test/common/plugin_browser_ut_system_test.mjs +10 -10
  266. package/test/common/plugin_markup_test.mjs +23 -23
  267. package/test/jsconfig.json +19 -19
  268. package/test/karma.config.js +91 -91
  269. package/test/node/.DS_Store +0 -0
  270. package/test/node/async_test.mjs +96 -96
  271. package/test/node/commander_ja_test.mjs +89 -89
  272. package/test/node/error_message_test.mjs +243 -243
  273. package/test/node/kai_test.nako3 +6 -6
  274. package/test/node/node_test.mjs +60 -60
  275. package/test/node/plugin_broken.js.txt +3 -3
  276. package/test/node/plugin_browser_ut_ajax_test.mjs.todo +357 -357
  277. package/test/node/plugin_browser_ut_location_test.mjs +42 -42
  278. package/test/node/plugin_markup_test.mjs +47 -47
  279. package/test/node/plugin_math_test.mjs +45 -45
  280. package/test/node/plugin_node_test.mjs +98 -98
  281. package/test/node/plugin_test.mjs +44 -44
  282. package/test/node/relative_import_test_1.nako3 +1 -1
  283. package/test/node/relative_import_test_2.nako3 +2 -2
  284. package/test/node/require_nako3_test.mjs +67 -67
  285. package/test/node/requiretest.nako3 +4 -4
  286. package/test/node/requiretest_indirect.nako3 +1 -1
  287. package/test/node/requiretest_name.nako3 +5 -5
  288. package/test/node/runtime_error.nako3 +2 -2
  289. package/test/node/scope1.nako3 +10 -10
  290. package/test/node/scope2.nako3 +12 -12
  291. package/test/node/side_effects_test.mjs +39 -39
  292. package/test/node/sjis.txt +5 -5
  293. package/test/node/syntax_error.nako3 +1 -1
  294. package/test/node/wnako3_editor_test.mjs +384 -384
  295. package/tools/.DS_Store +0 -0
  296. package/tools/README.md +12 -12
  297. package/tools/check_new_version.nako3 +25 -25
  298. package/tools/nako3edit/.DS_Store +0 -0
  299. package/tools/nako3edit/a.sqlite3 +0 -0
  300. package/tools/nako3edit/html/.DS_Store +0 -0
  301. package/tools/nako3edit/html/daisyui/LICENSE +22 -22
  302. package/tools/nako3edit/html/daisyui/version_2.14.1 +1 -1
  303. package/tools/nako3edit/html/edit.html +170 -170
  304. package/tools/nako3edit/html/edit_plugin.js +6 -6
  305. package/tools/nako3edit/html/files.html +125 -125
  306. package/tools/nako3edit/html/nako3edit.css +65 -65
  307. package/tools/nako3edit/index.mjs +248 -248
  308. package/tools/nako3server/index.html +10 -10
  309. package/tools/nako3server/index.mjs +116 -116
  310. package/tools/nako3server/index.nako3 +34 -34
  311. package/core/.editorconfig +0 -6
  312. package/core/.eslintrc.cjs +0 -33
  313. package/core/.github/dependabot.yml +0 -7
  314. package/core/.github/workflows/nodejs.yml +0 -37
  315. package/core/.github/workflows/super-linter.yml +0 -61
  316. package/core/.github/workflows/textlint.yml +0 -199
  317. package/core/index.mts +0 -21
  318. package/core/test/array_test.mjs +0 -34
  319. package/core/test/basic_test.mjs +0 -344
  320. package/core/test/calc_test.mjs +0 -140
  321. package/core/test/core_module_test.mjs +0 -23
  322. package/core/test/debug_test.mjs +0 -16
  323. package/core/test/dncl_test.mjs +0 -94
  324. package/core/test/error_message_test.mjs +0 -210
  325. package/core/test/error_test.mjs +0 -16
  326. package/core/test/flow_test.mjs +0 -373
  327. package/core/test/func_call.mjs +0 -160
  328. package/core/test/func_test.mjs +0 -149
  329. package/core/test/indent_test.mjs +0 -364
  330. package/core/test/lex_test.mjs +0 -168
  331. package/core/test/literal_test.mjs +0 -73
  332. package/core/test/nako_lexer_test.mjs +0 -35
  333. package/core/test/nako_logger_test.mjs +0 -76
  334. package/core/test/nako_logger_test.mts +0 -78
  335. package/core/test/plugin_csv_test.mjs +0 -38
  336. package/core/test/plugin_promise_test.mjs +0 -18
  337. package/core/test/plugin_system_test.mjs +0 -630
  338. package/core/test/prepare_test.mjs +0 -96
  339. package/core/test/re_test.mjs +0 -22
  340. package/core/test/side_effects_test.mjs +0 -92
  341. package/core/test/variable_scope_test.mjs +0 -149
  342. package/core/tsconfig.json +0 -101
@@ -1,1657 +1,1658 @@
1
- // @ts-nocheck
2
- /* eslint-disable no-template-curly-in-string */
3
- /** なでしこのtokenのtypeをscope(CSSのクラス名)に変換する。 */
4
-
5
- import { OffsetToLineColumn } from '../core/src/nako_source_mapping.mjs'
6
- import { NakoError } from '../core/src/nako_errors.mjs'
7
- import NakoIndent from '../core/src/nako_indent.mjs'
8
- import { NakoPrepare } from '../core/src/nako_prepare.mjs'
9
- import { NakoLogger } from '../core/src/nako_logger.mjs'
10
-
11
- // alias
12
- const getBlockStructure = NakoIndent.getBlockStructure
13
- const getIndent = NakoIndent.getIndent
14
- const countIndent = NakoIndent.countIndent
15
- const isIndentSyntaxEnabled = NakoIndent.isIndentSyntaxEnabled
16
-
17
- /**
18
- * @typedef {import('./nako3')} NakoCompiler
19
- *
20
- * @typedef {{
21
- * getValue(): string
22
- * setValue(text: string): void
23
- * session: Session
24
- * execCommand(command: string): void
25
- * setReadOnly(value: boolean): void
26
- * setOption(key: string, value: unknown): void
27
- * getOption(key: string): unknown
28
- * setOptions(entries: Record<string, unknown>): void
29
- * setFontSize(px: number): void
30
- * setKeyboardHandler(name: string): void
31
- * setTheme(name: string): void
32
- * container: HTMLElement
33
- * wnako3EditorId?: number
34
- * getCursorPosition(): { row: number, column: number }
35
- * commands: { addCommand(data: { name: string, exec: (editor: AceEditor, args: any[]) => void }): void }
36
- * }} AceEditor
37
- *
38
- * @typedef {import("./nako_lexer").TokenWithSourceMap} TokenWithSourceMap
39
- *
40
- * @typedef {{
41
- * getLine(row: number): string
42
- * getAllLines(): string[]
43
- * getLength(): number
44
- * insertInLine(position: { row: number, column: number }, text: string): void
45
- * removeInLine(row: number, columnStart: number, columnEnd: number): void
46
- * replace(range: AceRange, text: string): void
47
- * }} AceDocument
48
- *
49
- * @typedef {{
50
- * doc: AceDocument
51
- * bgTokenizer: BackgroundTokenizer
52
- * getScrollTop(): number
53
- * setScrollTop(x: number): void
54
- * getScrollLeft(): number
55
- * setScrollLeft(x: number): void
56
- * getUndoManager(): any
57
- * setUndoManager(x: any): void
58
- * selection: { getRange(): AceRange, isBackwards(): boolean, setRange(range: AceRange, reversed: boolean): void, clearSelection(): void }
59
- * setMode(mode: string | object): void
60
- * }} Session
61
- *
62
- * @typedef {{}} AceRange
63
- *
64
- * @typedef {new (startLine: number, startColumn: number, endLine: number, endColumn: number) => AceRange} TypeofAceRange
65
- *
66
- * @typedef {string} TokenType
67
- * @typedef {{ type: TokenType, value: string, docHTML: string | null }} EditorToken
68
- *
69
- * @typedef {{ start: { row: number }, command: { id: string, title: string, arguments: string[] } }} CodeLens
70
- */
71
-
72
- /**
73
- * シンタックスハイライトでは一般にテキストの各部分に 'comment.line' のようなラベルを付け、各エディタテーマがそのそれぞれの色を設定する。
74
- * ace editor では例えば 'comment.line' が付いた部分はクラス .ace_comment.ace_line が付いたHTMLタグで囲まれ、各テーマはそれに対応するCSSを実装する。
75
- * @param {TokenWithSourceMap} token
76
- * @returns {TokenType}
77
- */
78
- export function getScope (token) {
79
- switch (token.type) {
80
- case 'line_comment': return 'comment.line'
81
- case 'range_comment': return 'comment.block'
82
- case 'def_test': return 'keyword.control'
83
- case 'def_func': return 'keyword.control'
84
- case 'func': return 'entity.name.function'
85
- case 'number': return 'constant.numeric'
86
- // 独立した助詞
87
- case 'とは':
88
- case 'ならば':
89
- case 'でなければ':
90
- return 'keyword.control'
91
- // 制御構文
92
- case 'ここから':
93
- case 'ここまで':
94
- case 'もし':
95
- case '違えば':
96
- case 'require':
97
- return 'keyword.control'
98
- // 予約語
99
- case '回':
100
- case '':
101
- case '繰り返す':
102
- case '反復':
103
- case '抜ける':
104
- case '続ける':
105
- case '戻る':
106
- case '先に':
107
- case '次に':
108
- case '代入':
109
- case '逐次実行':
110
- case '条件分岐':
111
- case '取込':
112
- case 'エラー監視':
113
- case 'エラー':
114
- case '変数':
115
- case '実行速度優先':
116
- return 'keyword.control'
117
- case '定める':
118
- case '定数':
119
- return 'support.constant'
120
- // 演算子
121
- case 'shift_r0':
122
- case 'shift_r':
123
- case 'shift_l':
124
- case 'gteq':
125
- case 'lteq':
126
- case 'noteq':
127
- case 'eq':
128
- case 'not':
129
- case 'gt':
130
- case 'lt':
131
- case 'and':
132
- case 'or':
133
- case '@':
134
- case '+':
135
- case '-':
136
- case '*':
137
- case '/':
138
- case '%':
139
- case '^':
140
- case '&':
141
- return 'keyword.operator'
142
- case 'string':
143
- case 'string_ex':
144
- return 'string.other'
145
- case 'word':
146
- if (['そう', 'それ', '回数', '対象キー', '対象'].includes(token.value)) {
147
- return 'variable.language'
148
- } else {
149
- return 'variable.other'
150
- }
151
- default:
152
- return 'markup.other'
153
- }
154
- }
155
-
156
- /**
157
- * @param {TokenWithSourceMap} compilerToken
158
- * @param {NakoCompiler} nako3
159
- * @param {string} value
160
- * @param {boolean} includesLastCharacter
161
- * @param {boolean} underlineJosi
162
- */
163
- export function getEditorTokens (compilerToken, nako3, value, includesLastCharacter, underlineJosi) {
164
- const type = getScope(compilerToken)
165
- const docHTML = getDocumentationHTML(compilerToken, nako3)
166
-
167
- // 助詞があれば助詞の部分を分割する。
168
- // 最後の文字が現在の行に含まれないときは助詞を表示しない。そうしないと例えば `「文字列\n」を表示` の「列」の部分に下線が引かれてしまう。
169
- if (compilerToken.rawJosi && value.length >= compilerToken.rawJosi.length && includesLastCharacter && underlineJosi) {
170
- return [
171
- { type, docHTML, value: value.slice(0, -compilerToken.rawJosi.length) },
172
- { type: type + '.markup.underline', docHTML, value: value.slice(-compilerToken.josi.length) }
173
- ]
174
- }
175
- return [
176
- { type, docHTML, value }
177
- ]
178
- }
179
-
180
- /**
181
- * `name` が定義されたプラグインの名前を返す。
182
- * @param {string} name
183
- * @param {NakoCompiler} nako3
184
- * @returns {string | null}
185
- */
186
- export function findPluginName (name, nako3) {
187
- for (const pluginName of Object.keys(nako3.__module)) {
188
- if (Object.keys(nako3.__module[pluginName]).includes(name)) {
189
- return pluginName
190
- }
191
- }
192
- return null
193
- }
194
-
195
- /**
196
- * i = 0, 1, 2, ... に対して 'A', 'B', 'C', ... 'Z', 'AA', 'AB', ... を返す。
197
- * @param {number} i
198
- * @returns {string}
199
- */
200
- export function createParameterName (i) {
201
- const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
202
- return i.toString(26).split('').map((v) => alphabet[parseInt(v, 26)]).join('')
203
- }
204
-
205
- /**
206
- * パラメータの定義を表す文字列を生成する。例えば `[['と', 'の'], ['を']]` に対して `'(Aと|Aの、Bを)'` を返す、パラメータが無い場合、空文字列を返す。
207
- * @param {string[][]} josi
208
- * @retunrs {string}
209
- */
210
- export function createParameterDeclaration (josi) {
211
- const args = josi.map((union, i) => union.map((v) => `${createParameterName(i)}${v}`).join('|')).join('、')
212
- if (args !== '') {
213
- return `(${args})`
214
- } else {
215
- return ''
216
- }
217
- }
218
-
219
- // https://stackoverflow.com/a/6234804
220
- /** @param {string} t */
221
- export function escapeHTML (t) {
222
- return t
223
- .replace(/&/g, '&amp;')
224
- .replace(/</g, '&lt;')
225
- .replace(/>/g, '&gt;')
226
- .replace(/"/g, '&quot;')
227
- .replace(/'/g, '&#039;')
228
- }
229
-
230
- /**
231
- * 関数のドキュメントを返す。
232
- * @param {TokenWithSourceMap} token
233
- * @param {NakoCompiler} nako3
234
- * @returns {string | null}
235
- */
236
- export function getDocumentationHTML (token, nako3) {
237
- /** @param {string} text */
238
- const meta = (text) => `<span class="tooltip-plugin-name">${escapeHTML(text)}</span>`
239
- if (token.type === 'func') {
240
- /** @type {string | null} */
241
- const pluginName = findPluginName(token.value + '', nako3) || (token.meta && token.meta.file ? token.meta.file : null)
242
- const josi = (token.meta && token.meta.josi) ? createParameterDeclaration(token.meta.josi) : '' // {関数} のとき token.meta.josi が存在しない
243
- if (pluginName) {
244
- return escapeHTML(josi + token.value) + meta(pluginName)
245
- }
246
- return escapeHTML(josi + token.value)
247
- } else if (token.type === 'word') {
248
- /** @type {string | null} */
249
- const pluginName = findPluginName(token.value + '', nako3) || (token.meta && token.meta.file ? token.meta.file : null)
250
- if (pluginName) {
251
- return escapeHTML(token.value + '') + meta(pluginName)
252
- }
253
- }
254
- return null
255
- }
256
-
257
- /**
258
- * ace editor ではエディタの文字列の全ての部分に何らかの `type` を付けなければならない。
259
- * なでしこのエディタでは 'markup.other' をデフォルト値として使うことにした。
260
- * @param {number} row
261
- * @param {AceDocument} doc
262
- * @returns {EditorToken[]}
263
- */
264
- const getDefaultTokens = (row, doc) => [{ type: 'markup.other', value: doc.getLine(row), docHTML: null }]
265
-
266
- /**
267
- * 一時的にloggerを無効化する。そうしないとシンタックスハイライトの更新のたびにloggerへコンパイルエラーや警告が送られて、結果のボックスに行が追加されてしまう。
268
- * @type {<T>(nako3: NakoCompiler, f: () => T) => T}
269
- */
270
- const withoutLogger = (nako3, f) => {
271
- const logger = nako3.logger
272
- try {
273
- nako3.replaceLogger()
274
- return f()
275
- } finally {
276
- nako3.logger = logger
277
- }
278
- }
279
-
280
- /**
281
- * プログラムをlexerでtokenizeした後、ace editor 用のトークン列に変換する。
282
- * @param {string[]} lines
283
- * @param {NakoCompiler} nako3
284
- * @param {boolean} underlineJosi
285
- */
286
- export function tokenize (lines, nako3, underlineJosi) {
287
- const code = lines.join('\n')
288
-
289
- // 取り込み文を含めてしまうと依存ファイルが大きい時に時間がかかってしまうため、
290
- // 取り込み文を無視してトークン化してから、依存ファイルで定義された関数名と一致するトークンを関数のトークンへ変換する。
291
- nako3.reset()
292
- const lexerOutput = withoutLogger(nako3, () => nako3.lex(code, 'main.nako3', undefined, true))
293
- lexerOutput.commentTokens = lexerOutput.commentTokens.filter((t) => t.file === 'main.nako3')
294
- lexerOutput.requireTokens = lexerOutput.requireTokens.filter((t) => t.file === 'main.nako3')
295
- lexerOutput.tokens = lexerOutput.tokens.filter((t) => t.file === 'main.nako3')
296
-
297
- // 外部ファイルで定義された関数名に一致するトークンのtypeをfuncに変更する。
298
- // 取り込んでいないファイルも参照される問題や、関数名の重複がある場合に正しくない情報を表示する問題がある。
299
- // eslint-disable-next-line no-lone-blocks
300
- {
301
- /** @type {Record<string, object>} */
302
- for (const [file, { funclist }] of Object.entries(nako3.dependencies)) {
303
- for (const token of lexerOutput.tokens) {
304
- if (token.type === 'word' && token.value !== 'それ' && funclist[token.value]) {
305
- token.type = 'func'
306
- // meta.file に定義元のファイル名を持たせる。
307
- token.meta = { ...funclist[token.value + ''], file }
308
- }
309
- }
310
- }
311
- }
312
-
313
- // eol、eof、長さが1未満のトークン、位置を特定できないトークンを消す
314
- /** @type {(TokenWithSourceMap & { startOffset: number, endOffset: number })[]} */
315
- // @ts-ignore
316
- const tokens = [...lexerOutput.tokens, ...lexerOutput.commentTokens, ...lexerOutput.requireTokens].filter((t) =>
317
- t.type !== 'eol' && t.type !== 'eof' &&
318
- typeof t.startOffset === 'number' && typeof t.endOffset === 'number' &&
319
- t.startOffset < t.endOffset)
320
-
321
- // startOffsetでソートする
322
- tokens.sort((a, b) => (a.startOffset || 0) - (b.startOffset || 0))
323
-
324
- // 各行について、余る文字の無いようにエディタのトークンに変換する。
325
- // 複数のトークンが重なることはないと仮定する。
326
- let lineStartOffset = 0
327
- let tokenIndex = 0
328
- // 実際に必要なプロパティはtype, valueだけで、docは独自に追加した。
329
- /** @type {EditorToken[][]} */
330
- const editorTokens = [] // 各行のエディタのトークン
331
- for (let i = 0; i < lines.length; i++) {
332
- editorTokens.push([])
333
- const lineEndOffset = lineStartOffset + lines[i].length
334
- let offset = lineStartOffset
335
-
336
- // 現在の行にかかっているトークンまで飛ばす
337
- while (tokenIndex < tokens.length &&
338
- tokens[tokenIndex].endOffset <= lineStartOffset) {
339
- tokenIndex++
340
- }
341
-
342
- // 行全体を完全にまたがっているトークンが存在する場合
343
- if (tokenIndex < tokens.length &&
344
- tokens[tokenIndex].startOffset <= lineStartOffset &&
345
- tokens[tokenIndex].endOffset >= lineEndOffset) {
346
- editorTokens[i].push(...getEditorTokens(tokens[tokenIndex], nako3, lines[i], tokens[tokenIndex].endOffset <= lineEndOffset, underlineJosi))
347
- } else {
348
- // 行頭をまたがっているトークンが存在する場合
349
- if (tokenIndex < tokens.length &&
350
- tokens[tokenIndex].startOffset <= lineStartOffset) {
351
- editorTokens[i].push(...getEditorTokens(tokens[tokenIndex], nako3, code.slice(offset, tokens[tokenIndex].endOffset), true, underlineJosi))
352
- offset = tokens[tokenIndex].endOffset
353
- tokenIndex++
354
- }
355
-
356
- // 行頭も行末もまたがっていないトークンを処理する
357
- while (tokenIndex < tokens.length &&
358
- tokens[tokenIndex].endOffset < lineEndOffset) {
359
- // このトークンと直前のトークンの間に隙間があるなら、埋める
360
- if (offset < tokens[tokenIndex].startOffset) {
361
- editorTokens[i].push({
362
- type: 'markup.other',
363
- docHTML: null,
364
- value: code.slice(offset, tokens[tokenIndex].startOffset)
365
- })
366
- offset = tokens[tokenIndex].startOffset
367
- }
368
-
369
- // 現在のトークンを使う
370
- editorTokens[i].push(...getEditorTokens(tokens[tokenIndex], nako3, code.slice(offset, tokens[tokenIndex].endOffset), true, underlineJosi))
371
- offset = tokens[tokenIndex].endOffset
372
- tokenIndex++
373
- }
374
-
375
- // 行末をまたがっているトークンが存在する場合
376
- if (tokenIndex < tokens.length &&
377
- tokens[tokenIndex].startOffset < lineEndOffset) {
378
- // トークンの前の隙間
379
- if (offset < tokens[tokenIndex].startOffset) {
380
- editorTokens[i].push({
381
- type: 'markup.other',
382
- docHTML: null,
383
- value: code.slice(offset, tokens[tokenIndex].startOffset)
384
- })
385
- offset = tokens[tokenIndex].startOffset
386
- }
387
-
388
- // トークンを使う
389
- editorTokens[i].push(...getEditorTokens(tokens[tokenIndex], nako3, code.slice(tokens[tokenIndex].startOffset, lineEndOffset), tokens[tokenIndex].endOffset <= lineEndOffset, underlineJosi))
390
- } else {
391
- editorTokens[i].push({
392
- type: 'markup.other',
393
- docHTML: null,
394
- value: code.slice(offset, lineEndOffset)
395
- })
396
- }
397
- }
398
-
399
- lineStartOffset += lines[i].length + 1
400
- }
401
-
402
- return { editorTokens, lexerOutput }
403
- }
404
-
405
- /**
406
- * エディタ上にエラーメッセージの波線とgutterの赤いマークとエラーメッセージのポップアップを設定するためのクラス。
407
- */
408
- export class EditorMarkers {
409
- /**
410
- * @param {any} session
411
- * @param {AceDocument} doc
412
- * @param {TypeofAceRange} AceRange
413
- * @param {boolean} disable
414
- */
415
- constructor (session, doc, AceRange, disable) {
416
- this.session = session
417
- this.doc = doc
418
- this.AceRange = AceRange
419
- /** @type {any[]} */
420
- this.markers = []
421
- this.hasAnnotations = false
422
- this.disable = disable
423
- }
424
-
425
- /**
426
- * @param {number} startLine
427
- * @param {number | null} startColumn
428
- * @param {number | null} endLine
429
- * @param {number | null} endColumn
430
- * @param {(row: number) => string} getLine
431
- * @returns {[number, number, number, number]}
432
- */
433
- static fromNullable (startLine, startColumn, endLine, endColumn, getLine) {
434
- if (startColumn === null) {
435
- startColumn = 0
436
- }
437
- if (endLine === null) {
438
- endLine = startLine
439
- }
440
- if (endColumn === null) {
441
- endColumn = getLine(endLine).length
442
- }
443
-
444
- // 最低でも1文字分の長さをとる
445
- if (startLine === endLine && startColumn === endColumn) {
446
- endColumn++
447
- }
448
-
449
- return [startLine, startColumn, endLine, endColumn]
450
- }
451
-
452
- /**
453
- * @param {string} code @param {number} startOffset @param {number} endOffset
454
- * @returns {[number, number, number, number]}
455
- */
456
- static fromOffset (code, startOffset, endOffset) {
457
- const offsetToLineColumn = new OffsetToLineColumn(code)
458
- const start = offsetToLineColumn.map(startOffset, false)
459
- const end = offsetToLineColumn.map(endOffset, false)
460
- return [start.line, start.column, end.line, end.column]
461
- }
462
-
463
- /**
464
- * @param {string} code
465
- * @param {{ line?: number, startOffset?: number | null, endOffset?: number | null, message: string }} error
466
- * @param {(row: number) => string} getLine
467
- * @returns {[number, number, number, number]}
468
- */
469
- static fromError (code, error, getLine) {
470
- if (typeof error.startOffset === 'number' && typeof error.endOffset === 'number') {
471
- // 完全な位置を取得できる場合
472
- return this.fromOffset(code, error.startOffset, error.endOffset)
473
- } else if (typeof error.line === 'number') {
474
- // 行全体の場合
475
- return this.fromNullable(error.line, null, null, null, getLine)
476
- } else {
477
- // 位置が不明な場合
478
- return this.fromNullable(0, null, null, null, getLine)
479
- }
480
- }
481
-
482
- /**
483
- * @param {number} startLine
484
- * @param {number | null} startColumn
485
- * @param {number | null} endLine
486
- * @param {number | null} endColumn
487
- * @param {string} message
488
- * @param {'warn' | 'error'} type
489
- */
490
- add (startLine, startColumn, endLine, endColumn, message, type) {
491
- if (this.disable) {
492
- return
493
- }
494
- const range = new this.AceRange(...EditorMarkers.fromNullable(startLine, startColumn, endLine, endColumn, (row) => this.doc.getLine(row)))
495
- this.markers.push(this.session.addMarker(range, 'marker-' + (type === 'warn' ? 'yellow' : 'red'), 'text', false))
496
- // type 'error' | 'warning' | 'info'
497
- this.session.setAnnotations([{ row: startLine, column: startColumn, text: message, type: type === 'warn' ? 'warning' : 'error' }])
498
- this.hasAnnotations = true
499
- }
500
-
501
- /**
502
- * @param {string} code
503
- * @param {{ line?: number, startOffset?: number | null, endOffset?: number | null, message: string }} error
504
- * @param {'warn' | 'error'} type
505
- */
506
- addByError (code, error, type) {
507
- this.add(...EditorMarkers.fromError(code, error, (row) => this.doc.getLine(row)), error.message, type)
508
- }
509
-
510
- /**
511
- * 全てのエラーメッセージを削除する。
512
- */
513
- clear () {
514
- for (const marker of this.markers) {
515
- this.session.removeMarker(marker)
516
- }
517
- this.markers.length = 0
518
- if (this.hasAnnotations) {
519
- this.session.clearAnnotations()
520
- this.hasAnnotations = false
521
- }
522
- }
523
- }
524
-
525
- /**
526
- * ace editor のBackgroundTokenizerを上書きして、シンタックスハイライトを自由に表示するためのクラス。
527
- * ace editor ではシンタックスハイライトのために正規表現ベースのBackgroundTokenizerクラスを用意し定期的にトークン化を
528
- * 行っているが、正規表現ではなくなでしこのコンパイラの出力を使うためにはそれを上書きする必要がある。
529
- */
530
- export class BackgroundTokenizer {
531
- /**
532
- * @param {AceDocument} doc
533
- * @param {NakoCompiler} nako3
534
- * @param {(firstRow: number, lastRow: number, ms: number) => void} onTokenUpdate
535
- * @param {(code: string, err: Error) => void} onCompileError
536
- * @param {boolean} underlineJosi
537
- */
538
- constructor (doc, nako3, onTokenUpdate, onCompileError, underlineJosi) {
539
- this.onUpdate = onTokenUpdate
540
- this.doc = doc
541
- this.dirty = true
542
- this.nako3 = nako3
543
- this.onCompileError = onCompileError
544
- this.underlineJosi = underlineJosi
545
-
546
- // オートコンプリートで使うために、直近のtokenizeの結果を保存しておく
547
- /** @type {ReturnType<NakoCompiler['lex']> | null} */
548
- this.lastLexerOutput = null
549
-
550
- // 各行のパース結果。
551
- // typeはscopeのこと。配列の全要素のvalueを結合した文字列がその行の文字列と等しくなる必要がある。
552
- /** @type {EditorToken[][]} */
553
- this.lines = this.doc.getAllLines().map((line) => [{ type: 'markup.other', value: line, docHTML: null }])
554
-
555
- // this.lines は外部から勝手に編集されてしまうため、コピーを持つ
556
- /** @type {{ code: string, lines: string } | null} */
557
- this.cache = null
558
-
559
- this.deleted = false
560
-
561
- /** @public */
562
- this.enabled = true
563
-
564
- const update = () => {
565
- if (this.deleted) {
566
- return
567
- }
568
- if (this.dirty && this.enabled) {
569
- const startTime = Date.now()
570
- this.dirty = false
571
- const code = this.doc.getAllLines().join('\n')
572
- try {
573
- const startTime = Date.now()
574
- const out = tokenize(this.doc.getAllLines(), nako3, this.underlineJosi)
575
- this.lastLexerOutput = out.lexerOutput
576
- this.lines = out.editorTokens
577
- this.cache = { code, lines: JSON.stringify(this.lines) }
578
-
579
- // ファイル全体の更新を通知する。
580
- onTokenUpdate(0, this.doc.getLength() - 1, Date.now() - startTime)
581
- } catch (e) {
582
- onCompileError(code, e)
583
- }
584
- // tokenizeに時間がかかる場合、文字を入力できるように次回の実行を遅くする。
585
- setTimeout(update, Math.max(100, Math.min(5000, (Date.now() - startTime) * 5)))
586
- } else {
587
- setTimeout(update, 100)
588
- }
589
- }
590
-
591
- // コンストラクタが返る前にコールバックを呼ぶのはバグの元になるため一瞬待つ。
592
- // たとえば `const a = new BackgroundTokenizer(..., () => { /* aを使った処理 */ }, ...)` がReferenceErrorになる。
593
- setTimeout(() => { update() }, 0)
594
- }
595
-
596
- dispose () {
597
- this.deleted = true
598
- }
599
-
600
- /**
601
- * テキストに変更があったときに呼ばれる。IME入力中には呼ばれない。
602
- * @param {{ action: string, start: { row: number, column: number }, end: { row: number, column: number }, lines: string[] }} delta
603
- */
604
- $updateOnChange (delta) {
605
- this.dirty = true
606
- const startRow = delta.start.row
607
- const endRow = delta.end.row
608
- if (startRow === endRow) { // 1行の編集
609
- if (delta.action === 'insert' && this.lines[startRow]) { // 行内に文字列を挿入
610
- const columnStart = delta.start.column
611
- // updateOnChangeはIME入力中には呼ばれない。composition_placeholder を消さないとIME確定後の表示がずれる。
612
- const oldTokens = this.lines[startRow]
613
- .filter((v) => v.type !== 'composition_placeholder')
614
- /** @type {EditorToken[]} */
615
- const newTokens = []
616
- let i = 0
617
- let offset = 0
618
-
619
- // columnStartより左のトークンはそのまま保持する
620
- while (i < oldTokens.length && offset + oldTokens[i].value.length <= columnStart) {
621
- newTokens.push(oldTokens[i])
622
- offset += oldTokens[i].value.length
623
- i++
624
- }
625
-
626
- // columnStartに重なっているトークンがあれば、2つに分割する
627
- if (i < oldTokens.length && offset < columnStart) {
628
- newTokens.push({ type: oldTokens[i].type, value: oldTokens[i].value.slice(0, columnStart - offset), docHTML: null })
629
- newTokens.push({ type: 'markup.other', value: delta.lines[0], docHTML: null })
630
- newTokens.push({ type: oldTokens[i].type, value: oldTokens[i].value.slice(columnStart - offset), docHTML: null })
631
- i++
632
- } else {
633
- newTokens.push({ type: 'markup.other', value: delta.lines[0], docHTML: null })
634
- }
635
-
636
- // columnStartより右のトークンもそのまま保持する
637
- while (i < oldTokens.length) {
638
- newTokens.push(oldTokens[i])
639
- i++
640
- }
641
-
642
- this.lines[startRow] = newTokens
643
- } else {
644
- this.lines[startRow] = getDefaultTokens(startRow, this.doc)
645
- }
646
- } else if (delta.action === 'remove') { // 範囲削除
647
- this.lines.splice(startRow, endRow - startRow + 1, getDefaultTokens(startRow, this.doc))
648
- } else { // 行の挿入
649
- this.lines.splice(startRow, 1, ...Array(endRow - startRow + 1).fill(null).map((_, i) => getDefaultTokens(i + startRow, this.doc)))
650
- }
651
- }
652
-
653
- /**
654
- * tokenizerの出力を返す。文字入力したときに呼ばれる。
655
- * @param {number} row
656
- */
657
- getTokens (row) {
658
- // IME入力中はthis.lines[row]に自動的にnullが設定される。その場合新しく行のトークン列を生成して返さなければならない。
659
- // 返した配列には自動的にIMEの入力用のテキストボックスであるcomposition_placeholderが挿入される。
660
- if (!this.lines[row]) {
661
- let ok = false
662
-
663
- if (this.enabled) {
664
- // tokenizeは非常に遅いため、キャッシュを使えるならそれを使う。
665
- const code = this.doc.getAllLines().join('\n')
666
- if (this.cache !== null && this.cache.code === code) {
667
- ok = true
668
- } else {
669
- try {
670
- const lines = tokenize(this.doc.getAllLines(), this.nako3, this.underlineJosi)
671
- this.cache = { code, lines: JSON.stringify(lines.editorTokens) }
672
- ok = true
673
- } catch (e) {
674
- if (!(e instanceof NakoError)) {
675
- console.error(e)
676
- }
677
- }
678
- }
679
- }
680
-
681
- if (ok && this.cache !== null) {
682
- this.lines[row] = JSON.parse(this.cache.lines)[row]
683
- } else {
684
- this.lines[row] = getDefaultTokens(row, this.doc)
685
- }
686
- }
687
- return this.lines[row]
688
- }
689
-
690
- // ace側から呼ばれるが無視するメソッド
691
- // @ts-ignore
692
- start (startRow) { /* pass */ }
693
- // @ts-ignore
694
- fireUpdateEvent (firstRow, lastRow) { /* pass */ }
695
- // @ts-ignore
696
- setDocument (doc) { /* pass */ }
697
- scheduleStart () { /* pass */ }
698
- // @ts-ignore
699
- setTokenizer (tokenizer) { /* pass */ }
700
- stop () { /* pass */ }
701
- // @ts-ignore
702
- getState (row) { return 'start' }
703
- }
704
-
705
- /**
706
- * シンタックスハイライト以外のエディタの挙動の定義。
707
- */
708
- export class LanguageFeatures {
709
- /**
710
- * @param {TypeofAceRange} AceRange
711
- * @param {NakoCompiler} nako3
712
- */
713
- constructor (AceRange, nako3) {
714
- this.AceRange = AceRange
715
- this.nako3 = nako3
716
- }
717
-
718
- /**
719
- * Ctrl + / の動作の定義。
720
- * @param {string} state
721
- * @param {Session} session
722
- * @param {number} startRow
723
- * @param {number} endRow
724
- */
725
- static toggleCommentLines (state, { doc }, startRow, endRow) {
726
- const prepare = NakoPrepare.getInstance()
727
- /**
728
- * @param {string} line
729
- * @returns {{ type: 'blank' | 'code' } | { type: 'comment', start: number, len: number }}
730
- */
731
- const parseLine = (line) => {
732
- // 先頭の空白を消す
733
- const indent = getIndent(line)
734
- if (indent === line) {
735
- return { type: 'blank' }
736
- }
737
- line = line.substring(indent.length)
738
-
739
- // 先頭がコメントの開始文字かどうか確認する
740
- const ch2 = line.substring(0, 2).split('').map((c) => prepare.convert1ch(c)).join('')
741
- if (ch2.substring(0, 1) === '#') {
742
- return { type: 'comment', start: indent.length, len: 1 + (line.charAt(1) === ' ' ? 1 : 0) }
743
- }
744
- if (ch2 === '//') {
745
- return { type: 'comment', start: indent.length, len: 2 + (line.charAt(2) === ' ' ? 1 : 0) }
746
- }
747
-
748
- return { type: 'code' }
749
- }
750
-
751
- /** @type {number[]} */
752
- const rows = []
753
- for (let i = startRow; i <= endRow; i++) {
754
- rows.push(i)
755
- }
756
-
757
- // 全ての行が空白行ならコメントアウト、全ての行が行コメントで始まるか空白行ならアンコメント、そうでなければコメントアウト。
758
- if (!rows.every((row) => parseLine(doc.getLine(row)).type === 'blank') &&
759
- rows.every((row) => parseLine(doc.getLine(row)).type !== 'code')) {
760
- // アンコメント
761
- for (const row of rows) {
762
- // 行コメントで始まる行ならアンコメントする。
763
- // 行コメントの直後にスペースがあるなら、それも1文字だけ削除する。
764
- const line = parseLine(doc.getLine(row))
765
- if (line.type === 'comment') {
766
- doc.removeInLine(row, line.start, line.start + line.len)
767
- }
768
- }
769
- } else {
770
- // 最もインデントの低い行のインデント数を数える
771
- const minIndent = Math.min(...rows.map((row) => countIndent(doc.getLine(row))))
772
-
773
- // コメントアウトする
774
- for (const row of rows) {
775
- const line = doc.getLine(row)
776
- let column = line.length
777
- for (let i = 0; i < line.length; i++) {
778
- if (countIndent(line.slice(0, i)) >= minIndent) {
779
- column = i
780
- break
781
- }
782
- }
783
- doc.insertInLine({ row, column }, '// ')
784
- }
785
- }
786
- }
787
-
788
- /**
789
- * 文字を入力するたびに呼ばれる。trueを返すとautoOutdentが呼ばれる。
790
- * @param {string} state
791
- * @param {string} line
792
- * @param {string} input
793
- * @returns {boolean}
794
- */
795
- static checkOutdent (state, line, input) {
796
- // 特定のキーワードの入力が終わったタイミングでインデントを自動修正する。
797
- // '違えば'のautoOutdentは「もし」と「条件分岐」のどちらのものか見分けが付かないため諦める。
798
- // 「ここ|ま」(縦線がカーソル)の状態で「で」を打つとtrueになってしまう問題があるが、修正するには引数が足りない。
799
- // eslint-disable-next-line no-irregular-whitespace
800
- return /^[  ・\t]*ここまで$/.test(line + input)
801
- }
802
-
803
- /**
804
- * checkOutdentがtrueを返したときに呼ばれる。
805
- * @param {string} state
806
- * @param {Session} session
807
- * @param {number} row
808
- * @returns {void}
809
- */
810
- autoOutdent (state, { doc }, row) {
811
- // 1行目なら何もしない
812
- if (row === 0) {
813
- return
814
- }
815
- const prevLine = doc.getLine(row - 1)
816
- let indent
817
- if (LanguageFeatures.isBlockStart(prevLine)) {
818
- // 1つ前の行が「〜ならば」などのブロック開始行なら、その行に合わせる。
819
- indent = getIndent(prevLine)
820
- } else {
821
- // そうでなければ、1つ前の行のインデントから1段階outdentした位置に合わせる。
822
- const s = this.getBlockStructure(doc.getAllLines().join('\n'))
823
- const parent = s.parents[row]
824
- indent = parent !== null ? s.spaces[parent] : ''
825
- }
826
-
827
- // 置換する
828
- const oldIndent = getIndent(doc.getLine(row))
829
- doc.replace(new this.AceRange(row, 0, row, oldIndent.length), indent)
830
- }
831
-
832
- /**
833
- * エンターキーを押して行が追加されたときに挿入する文字列を指定する。
834
- * @param {string} state
835
- * @param {string} line 改行前にカーソルがあった行の文字列
836
- * @param {string} tab タブ文字(デフォルトでは " ")
837
- */
838
- static getNextLineIndent (state, line, tab) {
839
- // ●で始まるか、特定のキーワードで終わる場合にマッチする。
840
- if (this.isBlockStart(line)) {
841
- return getIndent(line) + tab
842
- }
843
- return getIndent(line)
844
- }
845
-
846
- /** @param {string} line */
847
- static isBlockStart (line) {
848
- // eslint-disable-next-line no-irregular-whitespace
849
- return /^[  ・\t]*●|(ならば|なければ|ここから|条件分岐|違えば|回|繰り返(す|し)|の間|反復|とは|には|エラー監視|エラーならば|実行速度優先)、?\s*$/.test(line)
850
- }
851
-
852
- /**
853
- * オートコンプリート
854
- * @param {number} row
855
- * @param {string} prefix getCompletionPrefixの出力
856
- * @param {NakoCompiler} nako3
857
- * @param {BackgroundTokenizer} backgroundTokenizer
858
- */
859
- static getCompletionItems (row, prefix, nako3, backgroundTokenizer) {
860
- /**
861
- * keyはcaption。metaは候補の横に薄く表示されるテキスト。
862
- * @type {Map<string, { value: string, meta: Set<string>, score: number }>}
863
- */
864
- const result = new Map()
865
- /** 引数のリストを含まない、関数名だけのリスト @type {Set<string>} */
866
- const values = new Set()
867
-
868
- /**
869
- * オートコンプリートの項目を追加する。すでに存在するならマージする。
870
- * @param {string} caption @param {string} value @param {string} meta
871
- */
872
- const addItem = (caption, value, meta) => {
873
- const item = result.get(caption)
874
- if (item) {
875
- item.meta.add(meta)
876
- } else {
877
- // 日本語の文字数は英語よりずっと多いため、ただ一致する文字数を数えるだけで十分。
878
- const score = prefix.split('').filter((c) => value.includes(c)).length
879
- result.set(caption, { value, meta: new Set([meta]), score })
880
- values.add(value)
881
- }
882
- }
883
-
884
- // プラグイン関数
885
- for (const name of Object.keys(nako3.__varslist[0])) {
886
- if (name.startsWith('!')) { // 「!PluginBrowser:初期化」などを除外
887
- continue
888
- }
889
- const f = nako3.funclist[name]
890
- if (typeof f !== 'object' || f === null) {
891
- continue
892
- }
893
-
894
- const pluginName = findPluginName(name, nako3) || 'プラグイン'
895
- if (f.type === 'func') {
896
- addItem(createParameterDeclaration(f.josi) + name, name, pluginName)
897
- } else {
898
- addItem(name, name, pluginName)
899
- }
900
- }
901
-
902
- // 依存ファイルが定義した関数名
903
- for (const [file, { funclist }] of Object.entries(nako3.dependencies)) {
904
- for (const [name, f] of Object.entries(funclist)) {
905
- const josi = (f && f.type === 'func') ? createParameterDeclaration(f.josi) : ''
906
- addItem(josi + name, name, file)
907
- }
908
- }
909
-
910
- // 現在のファイル内に存在する名前
911
- if (backgroundTokenizer.lastLexerOutput !== null) {
912
- for (const token of backgroundTokenizer.lastLexerOutput.tokens) {
913
- const name = token.value + ''
914
- // 同じ行のトークンの場合、自分自身にマッチしている可能性が高いため除外する。
915
- // すでに定義されている場合も、定義ではなく参照の可能性が高いため除外する。
916
- if (token.line === row || values.has(name)) {
917
- continue
918
- }
919
- if (token.type === 'word') {
920
- addItem(name, name, '変数')
921
- } else if (token.type === 'func') {
922
- const f = nako3.funclist[name]
923
- const josi = (f && f.type === 'func') ? createParameterDeclaration(f.josi) : ''
924
- addItem(josi + name, name, '関数')
925
- }
926
- }
927
- }
928
-
929
- return Array.from(result.entries()).map(([caption, data]) => ({ caption, ...data, meta: Array.from(data.meta).join(', ') }))
930
- }
931
-
932
- /**
933
- * スニペット
934
- */
935
- /** @param {string} text */
936
- static getSnippets (text) {
937
- // インデント構文が有効化されているなら「ここまで」を消す
938
- const indentSyntax = isIndentSyntaxEnabled(text)
939
-
940
- /** @param {string} en @param {string} jp @param {string} snippet */
941
- const item = (en, jp, snippet) => indentSyntax
942
- ? { caption: en, meta: `\u21E5 ${jp}`, score: 1, snippet: snippet.replace(/\t*ここまで(\n|$)/g, '').replace(/\t/g, ' ') }
943
- : { caption: en, meta: `\u21E5 ${jp}`, score: 1, snippet: snippet.replace(/\t/g, ' ') }
944
-
945
- return [
946
- item('if', 'もし〜ならば', 'もし${1:1=1}ならば\n\t${2:1を表示}\n違えば\n\t${3:2を表示}\nここまで\n'),
947
- item('times', '〜回', '${1:3}回\n\t${2:1を表示}\nここまで\n'),
948
- item('for', '繰り返す', '${1:N}で${2:1}から${3:3}まで繰り返す\n\t${4:Nを表示}\nここまで\n'),
949
- item('while', '〜の間', '${1:N<2の間}\n\tN=N+1\nここまで\n'),
950
- item('foreach', '〜を反復', '${1:[1,2,3]}を反復\n\t${2:対象を表示}\nここまで\n'),
951
- item('switch', '〜で条件分岐', '${1:N}で条件分岐\n\t${2:1}ならば\n\t\t${3:1を表示}\n\tここまで\n\t${4:2}ならば\n\t\t${5:2を表示}\n\tここまで\n\t違えば\n\t\t${6:3を表示}\n\tここまで\nここまで\n'),
952
- item('function', '●〜とは', '●(${1:AとBを})${2:足す}とは\n\t${3:A+Bを戻す}\nここまで\n'),
953
- item('test', '●テスト:〜とは', '●テスト:${2:足す}とは\n\t1と2を足す\n\tそれと3がASSERT等しい\nここまで\n'),
954
- item('try', 'エラー監視', 'エラー監視\n\t${1:1のエラー発生}\nエラーならば\n\t${2:2を表示}\nここまで\n')
955
- ]
956
- }
957
-
958
- /**
959
- * @param {string} line
960
- * @param {NakoCompiler} nako3
961
- */
962
- static getCompletionPrefix (line, nako3) {
963
- /** @type {ReturnType<NakoCompiler['lex']>["tokens"] | null} */
964
- let tokens = null
965
-
966
- // ひらがなとアルファベットとカタカナと漢字のみオートコンプリートする。
967
- if (line.length === 0 || !/[ぁ-んa-zA-Zァ-ヶー\u3005\u4E00-\u9FCF]/.test(line[line.length - 1])) {
968
- return ''
969
- }
970
-
971
- // 現在の行のカーソルより前の部分をlexerにかける。速度を優先して1行だけ処理する。
972
- try {
973
- nako3.reset()
974
- tokens = withoutLogger(nako3, () => nako3.lex(line, 'completion.nako3', undefined, true)).tokens
975
- .filter((t) => t.type !== 'eol' && t.type !== 'eof')
976
- } catch (e) {
977
- if (!(e instanceof NakoError)) {
978
- console.error(e)
979
- }
980
- }
981
- if (tokens === null || tokens.length === 0 || !tokens[tokens.length - 1].value) {
982
- return ''
983
- }
984
- const prefix = tokens[tokens.length - 1].value + ''
985
-
986
- // 単語の先頭がひらがなではなく末尾がひらがなのとき、助詞を打っている可能性が高いためオートコンプリートしない。
987
- if (/[ぁ-ん]/.test(prefix[prefix.length - 1]) && !/[ぁ-ん]/.test(prefix[0])) {
988
- return ''
989
- }
990
-
991
- // 最後のトークンの値を、オートコンプリートで既に入力した部分とする。
992
- return prefix
993
- }
994
-
995
- /**
996
- * 文字を打つたびに各行についてこの関数が呼ばれる。'start'を返した行はfold可能な範囲の先頭の行になる。
997
- * @param {Session} session
998
- * @param {string} foldStyle
999
- * @param {number} row
1000
- * @returns {'start' | ''}
1001
- */
1002
- getFoldWidget ({ doc }, foldStyle, row) {
1003
- // 速度が重要なため正規表現でマッチする。
1004
- return LanguageFeatures.isBlockStart(doc.getLine(row)) ? 'start' : ''
1005
- }
1006
-
1007
- /**
1008
- * getFoldWidgetが'start'を返した行に設置されるfold用のボタンが押されたときに呼ばれる。
1009
- * @param {Session} session
1010
- * @param {string} foldStyle
1011
- * @param {number} row
1012
- * @returns {AceRange | null} foldする範囲
1013
- */
1014
- getFoldWidgetRange ({ doc }, foldStyle, row) {
1015
- const pair = this.getBlockStructure(doc.getAllLines().join('\n')).pairs.find((v) => v[0] === row)
1016
- if (pair !== undefined) {
1017
- return new this.AceRange(pair[0], doc.getLine(pair[0]).length, pair[1] - 1, doc.getLine(pair[1] - 1).length)
1018
- }
1019
- return null
1020
- }
1021
-
1022
- /**
1023
- * @param {AceDocument} doc
1024
- * @returns {CodeLens[]}
1025
- */
1026
- static getCodeLens (doc) {
1027
- const results = []
1028
- for (const [row, line] of Array.from(doc.getAllLines().entries())) {
1029
- // eslint-disable-next-line no-irregular-whitespace
1030
- const matches = /^[  ・\t]*●テスト:(.+?)(?:とは|$)/.exec(line)
1031
- if (matches !== null) {
1032
- results.push({
1033
- start: { row },
1034
- command: { title: 'テストを実行', id: 'runTest', arguments: [matches[1]] }
1035
- })
1036
- }
1037
- }
1038
- return results
1039
- }
1040
-
1041
- /**
1042
- * @param {string} code
1043
- * @returns {ReturnType<getBlockStructure>}
1044
- * @private
1045
- */
1046
- getBlockStructure (code) {
1047
- // キャッシュ
1048
- if (!this.blockStructure || this.blockStructure.code !== code) {
1049
- // @ts-ignore
1050
- this.blockStructure = { code, data: getBlockStructure(code) }
1051
- }
1052
- return this.blockStructure.data
1053
- }
1054
- }
1055
-
1056
- /**
1057
- * 複数ファイルを表示するための最低限のAPIを提供する。
1058
- * @typedef {{ content: string, cursor: { range: AceRange, reversed: boolean }, scroll: { top: number, left: number }, undoManger: any }} EditorTabState
1059
- */
1060
- class EditorTabs {
1061
- /**
1062
- * @param {AceEditor} editor
1063
- * @param {TypeofAceRange} AceRange
1064
- * @param {any} UndoManager
1065
- */
1066
- constructor (editor, AceRange, UndoManager) {
1067
- this.editor = editor
1068
- this.AceRange = AceRange
1069
- this.UndoManager = UndoManager
1070
- }
1071
-
1072
- /** @param {string} content @returns {EditorTabState} */
1073
- newTab (content) {
1074
- return {
1075
- content,
1076
- cursor: { range: new this.AceRange(0, 0, 0, 0), reversed: false },
1077
- scroll: { left: 0, top: 0 },
1078
- undoManger: new this.UndoManager()
1079
- }
1080
- }
1081
-
1082
- /** @returns {EditorTabState} */
1083
- getTab () {
1084
- return {
1085
- content: this.editor.getValue(),
1086
- cursor: { range: this.editor.session.selection.getRange(), reversed: this.editor.session.selection.isBackwards() },
1087
- scroll: { left: this.editor.session.getScrollLeft(), top: this.editor.session.getScrollTop() },
1088
- undoManger: this.editor.session.getUndoManager()
1089
- }
1090
- }
1091
-
1092
- /** @param {EditorTabState} state */
1093
- setTab (state) {
1094
- this.editor.setValue(state.content)
1095
- this.editor.session.selection.setRange(state.cursor.range, state.cursor.reversed)
1096
- this.editor.session.setScrollLeft(state.scroll.left)
1097
- this.editor.session.setScrollTop(state.scroll.top)
1098
- this.editor.session.setUndoManager(state.undoManger)
1099
- }
1100
- }
1101
-
1102
- class Options {
1103
- /** @param {AceEditor} editor */
1104
- static save (editor) {
1105
- try {
1106
- /** @type {any} */
1107
- const obj = {}
1108
- for (const key of ['syntaxHighlighting', 'keyboardHandler', 'theme', 'fontSize', 'wrap', 'useSoftTabs', 'tabSize', 'showInvisibles', 'enableLiveAutocompletion', 'indentedSoftWrap', 'underlineJosi']) {
1109
- obj[key] = editor.getOption(key)
1110
- }
1111
- localStorage.setItem('nako3EditorOptions', JSON.stringify(obj))
1112
- } catch (e) {
1113
- // JSON.stringify のエラー、localStorageのエラーなど
1114
- console.error(e)
1115
- return null
1116
- }
1117
- }
1118
-
1119
- /** @param {AceEditor} editor */
1120
- static load (editor) {
1121
- try {
1122
- if (!window.localStorage) {
1123
- return null
1124
- }
1125
- const text = window.localStorage.getItem('nako3EditorOptions')
1126
- if (text === null) {
1127
- return null
1128
- }
1129
- const json = JSON.parse(text)
1130
- if (['ace/keyboard/vscode', 'ace/keyboard/emacs', 'ace/keyboard/sublime', 'ace/keyboard/vim'].includes(json.keyboardHandler)) {
1131
- editor.setOption('keyboardHandler', json.keyboardHandler)
1132
- }
1133
- if (['ace/theme/xcode', 'ace/theme/monokai'].includes(json.theme)) {
1134
- editor.setOption('theme', json.theme)
1135
- }
1136
- if (typeof json.fontSize === 'number') {
1137
- editor.setOption('fontSize', Math.min(48, Math.max(6, json.fontSize)))
1138
- }
1139
- for (const key of ['syntaxHighlighting', 'wrap', 'useSoftTabs', 'showInvisibles', 'enableLiveAutocompletion', 'indentedSoftWrap', 'underlineJosi']) {
1140
- if (typeof json[key] === 'boolean') {
1141
- editor.setOption(key, json[key])
1142
- }
1143
- }
1144
- if (typeof json.tabSize === 'number') {
1145
- editor.setOption('tabSize', Math.min(16, Math.max(0, json.tabSize)))
1146
- }
1147
- } catch (e) {
1148
- // JSONのパースエラー、localStorageのエラーなど
1149
- console.error(e)
1150
- return null
1151
- }
1152
- }
1153
-
1154
- /**
1155
- * OptionPanelクラスをなでしこ用に書き換える。
1156
- * @param {any} OptionPanel
1157
- * @param {AceEditor} editor
1158
- */
1159
- static initPanel (OptionPanel, editor) {
1160
- const panel = new OptionPanel(editor) // editorはエラーが飛ばなければ何でも良い
1161
-
1162
- // ページ内で一度だけ呼ぶ
1163
- if (this.done) {
1164
- return
1165
- }
1166
- this.done = true
1167
-
1168
- // renderメソッドを呼ぶとrenderOptionGroupにoptionGroups.Main、optionGroups.More が順に渡されることを利用して、optionGroupsを書き換える。
1169
- let isMain = true
1170
- panel.renderOptionGroup = (group) => {
1171
- if (isMain) { // Main
1172
- for (const key of Object.keys(group)) {
1173
- delete group[key]
1174
- }
1175
-
1176
- // スマートフォンでも見れるように、文字数は最小限にする
1177
- group['シンタックスハイライト'] = {
1178
- path: 'syntaxHighlighting'
1179
- }
1180
- group['キーバインド'] = {
1181
- path: 'keyboardHandler',
1182
- type: 'select',
1183
- items: [
1184
- { caption: 'VSCode', value: 'ace/keyboard/vscode' },
1185
- { caption: 'Emacs', value: 'ace/keyboard/emacs' },
1186
- { caption: 'Sublime', value: 'ace/keyboard/sublime' },
1187
- { caption: 'Vim', value: 'ace/keyboard/vim' }
1188
- ]
1189
- }
1190
- group['カラーテーマ'] = {
1191
- path: 'theme',
1192
- type: 'select',
1193
- items: [
1194
- { caption: 'ライト', value: 'ace/theme/xcode' },
1195
- { caption: 'ダーク', value: 'ace/theme/monokai' }
1196
- ]
1197
- }
1198
- group['文字サイズ'] = {
1199
- path: 'fontSize',
1200
- type: 'number',
1201
- defaultValue: 16
1202
- }
1203
- group['行の折り返し'] = {
1204
- path: 'wrap',
1205
- type: 'select',
1206
- items: [
1207
- { caption: 'なし', value: 'off' },
1208
- { caption: 'あり', value: 'free' }
1209
- ]
1210
- }
1211
- group['ソフトタブ'] = [{
1212
- path: 'useSoftTabs'
1213
- }, {
1214
- ariaLabel: 'Tab Size',
1215
- path: 'tabSize',
1216
- type: 'number',
1217
- values: [2, 3, 4, 8, 16]
1218
- }]
1219
- group['空白文字を表示'] = {
1220
- path: 'showInvisibles'
1221
- }
1222
- group['常に自動補完'] = {
1223
- path: 'enableLiveAutocompletion'
1224
- }
1225
- group['折り返した行をインデント'] = {
1226
- path: 'indentedSoftWrap'
1227
- }
1228
- group['助詞に下線を引く'] = {
1229
- path: 'underlineJosi'
1230
- }
1231
- isMain = false
1232
- } else { // More
1233
- for (const key of Object.keys(group)) {
1234
- delete group[key]
1235
- }
1236
- }
1237
- }
1238
- panel.render()
1239
-
1240
- // 設定メニューは ace/ext/settings_menu.js の showSettingsMenu 関数によって開かれる。
1241
- // showSettingsMenu 関数は new OptionPanel(editor).render() で新しい設定パネルのインスタンスを生成するため、
1242
- // renderメソッドを上書きすることで、生成されたインスタンスにアクセスできる。
1243
- const render = OptionPanel.prototype.render
1244
- const self: any = globalThis
1245
- OptionPanel.prototype.render = function (...args) {
1246
- render.apply(this, ...args) // 元の処理
1247
- // OptionPanel.setOption() で発火される setOption イベントをキャッチする
1248
- this.on('setOption', () => {
1249
- console.log('設定を保存しました。')
1250
- self.save(this.editor)
1251
- })
1252
- }
1253
- }
1254
- }
1255
-
1256
- /**
1257
- * ace/ext/language_tools の設定がグローバル変数で保持されているため、こちら側でもグローバル変数で管理しないと、エディタが複数あるときに正しく動かない。
1258
- * - captionはオートコンプリートの候補として表示されるテキスト
1259
- * - metaはcaptionのテキストの右に薄く表示されるテキスト
1260
- * - docHTMLはその更に右に独立したウィンドウで表示されるHTMLによる説明
1261
- * - valueは決定したときに実際に挿入される文字列。プレースホルダーを配置するなら代わりにsnippetに値を設定する。
1262
- *
1263
- * @typedef {{
1264
- * getCompletions(
1265
- * editor: any,
1266
- * session: Session,
1267
- * pos: { row: number, column: number },
1268
- * prefix: any,
1269
- * callback: (
1270
- * a: null,
1271
- * b: { meta: string, caption: string, value?: string, score: number, docHTML?: string, snippet?: string }[]
1272
- * ) => void
1273
- * ): void
1274
- * getDocTooltip?(item: any): void
1275
- * }} Completer
1276
- * @type {Completer[]}
1277
- */
1278
- const completers = []
1279
-
1280
- let editorIdCounter = 0
1281
- /**
1282
- * 指定したidのHTML要素をなでしこ言語のエディタにする。
1283
- *
1284
- * - ace editor がグローバルに読み込まれている必要がある。
1285
- * - wnako3_editor.css を読み込む必要がある。
1286
- * - readonly にするには data-nako3-readonly="true" を設定する。
1287
- * - エラー位置の表示を無効化するには data-nako3-disable-marker="true" を設定する。
1288
- * - 縦方向にリサイズ可能にするには nako3-resizable="true" を設定する。
1289
- * - デバイスが遅いときにシンタックスハイライトを無効化する機能を切るには nako3-force-syntax-highlighting="true" を設定する。
1290
- *
1291
- * @param {string | Element} idOrElement HTML要素
1292
- * @param {import('./wnako3')} nako3
1293
- * @param {any} ace
1294
- */
1295
- export function setupEditor (idOrElement, nako3, ace) {
1296
- /** @type {AceEditor} */
1297
- const editor = ace.edit(idOrElement)
1298
- const element = typeof idOrElement === 'string' ? document.getElementById(idOrElement) : idOrElement
1299
- if (element === null) {
1300
- throw new Error(`idが ${idOrElement} のHTML要素は存在しません。`)
1301
- }
1302
-
1303
- /** @type {TypeofAceRange} */
1304
- const AceRange = ace.require('ace/range').Range
1305
- const editorMarkers = new EditorMarkers(
1306
- editor.session,
1307
- editor.session.bgTokenizer.doc,
1308
- AceRange,
1309
- !!element.dataset.nako3DisableMarker
1310
- )
1311
-
1312
- if (element.classList.contains('nako3_ace_mounted')) {
1313
- // 同じエディタを誤って複数回初期化すると、ace editor の挙動を書き換えているせいで
1314
- // 意図しない動作をしたため、すでにエディタとして使われていないことを確認する。
1315
- throw new Error('なでしこ言語のエディタの初期化処理を同一のHTML要素に対して複数回適用しました。')
1316
- }
1317
- // lang="ja" があると表示がずれる問題の修正 #839
1318
- element.setAttribute('lang', 'en')
1319
- // 以前のバージョンではnako3_editorをhtmlに直接付けていたため、互換性のためnako3_editorとは別のクラス名を使用する。
1320
- element.classList.add('nako3_ace_mounted')
1321
- element.classList.add('nako3_editor') // CSSのため
1322
- const readonly = element.dataset.nako3Readonly
1323
- // eslint-disable-next-line no-extra-boolean-cast
1324
- if (!!readonly) {
1325
- element.classList.add('readonly')
1326
- editor.setReadOnly(true)
1327
- }
1328
-
1329
- editor.setFontSize(16)
1330
-
1331
- /** @param {Session} session */
1332
- const resetEditorTokens = (session) => {
1333
- // 一旦テキスト全体を消してから、元に戻す
1334
- /** @type {AceDocument} */
1335
- const doc = session.doc
1336
- const lines = doc.getAllLines()
1337
- const range = session.selection.getRange()
1338
- doc.removeFullLines(0, doc.getLength())
1339
- doc.insert({ row: 0, column: 0 }, lines.join('\n'))
1340
- session.selection.setRange(range, false)
1341
- }
1342
-
1343
- ace.require('ace/config').defineOptions(editor.constructor.prototype, 'editor', {
1344
- syntaxHighlighting: {
1345
- /** @type {(this: AceEditor, value: boolean) => void} */
1346
- set: function (value) {
1347
- this.session.bgTokenizer.enabled = value
1348
- resetEditorTokens(this.session)
1349
- },
1350
- initialValue: true
1351
- },
1352
- underlineJosi: {
1353
- /** @type {(this: AceEditor, value: boolean) => void} */
1354
- set: function (value) {
1355
- this.session.bgTokenizer.underlineJosi = value
1356
- resetEditorTokens(this.session)
1357
- },
1358
- initialValue: true
1359
- }
1360
- })
1361
-
1362
- editor.setOptions({
1363
- wrap: 'free',
1364
- indentedSoftWrap: false,
1365
- showPrintMargin: false
1366
- })
1367
- ace.require('ace/keybindings/vscode')
1368
- editor.setKeyboardHandler('ace/keyboard/vscode')
1369
-
1370
- // ドキュメントのホバー
1371
- const Tooltip = ace.require('ace/tooltip').Tooltip
1372
- const tooltip = new Tooltip(editor.container)
1373
- const event = ace.require('ace/lib/event')
1374
- event.addListener(editor.renderer.content, 'mouseout', () => {
1375
- // マウスカーソルがエディタの外に出たら、tooltipを隠す
1376
- tooltip.hide()
1377
- })
1378
- editor.on('mousemove', (e) => {
1379
- // マウスカーソルがトークンに重なったときにtooltipを表示する。モバイル端末の場合はトークンにカーソルが当たったときに表示される。
1380
- const pos = e.getDocumentPosition()
1381
- // getTokenAtはcolumnが行末より大きいとき行末のトークンを返してしまう。
1382
- if (pos.column >= e.editor.session.getLine(pos.row).length) {
1383
- tooltip.hide()
1384
- return
1385
- }
1386
- // getTokenAtは実際よりも1文字右のトークンを取得してしまうため、columnに1を足している。
1387
- /** @type {EditorToken} */
1388
- const token = e.editor.session.getTokenAt(pos.row, pos.column + 1)
1389
- if (token === null || !token.docHTML) {
1390
- // ドキュメントが存在しないトークンならtooltipを表示しない
1391
- tooltip.hide()
1392
- return
1393
- }
1394
-
1395
- tooltip.setHtml(token.docHTML)
1396
- tooltip.show(null, e.clientX, e.clientY)
1397
- })
1398
- editor.session.on('change', () => {
1399
- // モバイル端末でドキュメントが存在するトークンを編集するときにツールチップが消えない問題を解消するために、文字を打ったらtooltipを隠す。
1400
- tooltip.hide()
1401
-
1402
- // 文字入力したらマーカーを消す
1403
- editorMarkers.clear()
1404
- })
1405
-
1406
- const forceSyntaxHighlighting = !!element.dataset.nako3ForceSyntaxHighlighting
1407
-
1408
- let isFirstTime = true
1409
- const oldBgTokenizer = editor.session.bgTokenizer
1410
- const backgroundTokenizer = new BackgroundTokenizer(
1411
- editor.session.bgTokenizer.doc,
1412
- nako3,
1413
- (firstRow, lastRow, ms) => {
1414
- oldBgTokenizer._signal('update', { data: { first: firstRow, last: lastRow } })
1415
-
1416
- // 処理が遅い場合シンタックスハイライトを無効化する。
1417
- if (ms > 220 && editor.getOption('syntaxHighlighting') && !readonly && !forceSyntaxHighlighting && isFirstTime) {
1418
- isFirstTime = false
1419
- slowSpeedMessage.classList.add('visible')
1420
- editor.setOption('syntaxHighlighting', false)
1421
- setTimeout(() => {
1422
- slowSpeedMessage.classList.remove('visible')
1423
- }, 13000)
1424
- }
1425
- },
1426
- (code, err) => { editorMarkers.addByError(code, err, 'error') },
1427
- /** @type {boolean} */(editor.getOption('underlineJosi'))
1428
- )
1429
-
1430
- // オートコンプリートを有効化する
1431
- editor.setOptions({
1432
- enableBasicAutocompletion: true,
1433
- enableSnippets: true,
1434
- enableLiveAutocompletion: true
1435
- })
1436
-
1437
- const editorId = editorIdCounter++
1438
- editor.wnako3EditorId = editorId
1439
-
1440
- // オートコンプリートのcompleterを設定する
1441
- completers.push(
1442
- {
1443
- getCompletions (editor, session, pos, prefix, callback) {
1444
- if (editor.wnako3EditorId !== editorId) {
1445
- callback(null, [])
1446
- } else {
1447
- const items = LanguageFeatures.getCompletionItems(pos.row, prefix, nako3, backgroundTokenizer)
1448
- // 完全に一致する候補があればオートコンプリートしない。(Aceエディタでの挙動が微妙なため。)
1449
- if (items.some((v) => v.value === prefix)) {
1450
- callback(null, [])
1451
- return
1452
- }
1453
- callback(null, items)
1454
- }
1455
- }
1456
- },
1457
- { getCompletions (editor, session, pos, prefix, callback) { callback(null, (editor.wnako3EditorId !== editorId) ? [] : LanguageFeatures.getSnippets(editor.session.doc.getAllLines().join('\n'))) } }
1458
- )
1459
- ace.require('ace/ext/language_tools').setCompleters(completers)
1460
-
1461
- // オートコンプリートの単語の区切りが日本語に対応していないため、メソッドを上書きして対応させる。
1462
- // 文字を入力するたびに呼ばれ、''以外を返すとその文字列をもとにしてautocompletionが始まる。
1463
- ace.require('ace/autocomplete/util').getCompletionPrefix = (/** @type {AceEditor} */ editor) => {
1464
- const pos = editor.getCursorPosition()
1465
- return LanguageFeatures.getCompletionPrefix(editor.session.doc.getLine(pos.row).slice(0, pos.column), nako3)
1466
- }
1467
-
1468
- // エディタの挙動の設定
1469
- const languageFeatures = new LanguageFeatures(AceRange, nako3)
1470
- const oop = ace.require('ace/lib/oop')
1471
- const TextMode = ace.require('ace/mode/text').Mode
1472
- const Mode = function () {
1473
- this.HighlightRules = new TextMode().HighlightRules
1474
- this.foldingRules = {
1475
- getFoldWidget: languageFeatures.getFoldWidget.bind(languageFeatures),
1476
- getFoldWidgetRange: languageFeatures.getFoldWidgetRange.bind(languageFeatures)
1477
- }
1478
- }
1479
- oop.inherits(Mode, TextMode)
1480
- Mode.prototype.toggleCommentLines = LanguageFeatures.toggleCommentLines.bind(LanguageFeatures)
1481
- Mode.prototype.getNextLineIndent = LanguageFeatures.getNextLineIndent.bind(LanguageFeatures)
1482
- Mode.prototype.checkOutdent = LanguageFeatures.checkOutdent.bind(LanguageFeatures)
1483
- Mode.prototype.autoOutdent = languageFeatures.autoOutdent.bind(languageFeatures)
1484
- editor.session.setMode(new Mode())
1485
-
1486
- // tokenizer (シンタックスハイライト)の上書き
1487
- editor.session.bgTokenizer.stop()
1488
- editor.session.bgTokenizer = backgroundTokenizer
1489
-
1490
- editor.setTheme('ace/theme/xcode')
1491
-
1492
- // 設定メニューの上書き
1493
- // なでしこ用に上書きした設定の削除やテキストの和訳をする。
1494
- Options.load(editor)
1495
- const OptionPanel = ace.require('ace/ext/options').OptionPanel
1496
- Options.initPanel(OptionPanel, editor)
1497
-
1498
- // 右下のボタン全体を囲むdiv
1499
- const buttonContainer = document.createElement('div')
1500
- buttonContainer.classList.add('button-container')
1501
- editor.container.appendChild(buttonContainer)
1502
-
1503
- // 遅い端末へのメッセージのボタン
1504
- const slowSpeedMessage = document.createElement('span')
1505
- slowSpeedMessage.classList.add('slow-speed-message')
1506
- slowSpeedMessage.innerHTML = '<span>エディタの|応答速度が|低下したため|シンタックス|ハイライトを|無効化|しました。</span>'.replace(/\|/g, '</span><span>')
1507
- buttonContainer.appendChild(slowSpeedMessage)
1508
-
1509
- // テストの定義の上に「テストを実行」ボタンを表示する
1510
- /** @type {{ name: 'test', callback: (testName: string | undefined) => void }[]} */
1511
- const codeLensListeners = []
1512
- try {
1513
- const CodeLens = ace.require('ace/ext/code_lens')
1514
- editor.setOption('enableCodeLens', true)
1515
- editor.commands.addCommand({
1516
- name: 'runTest',
1517
- exec: (/** @type {AceEditor} */editor, /** @type {any[]} */args) => {
1518
- codeLensListeners
1519
- .filter((v) => v.name === 'test')
1520
- .forEach((f) => f.callback(args[0]))
1521
- }
1522
- })
1523
- CodeLens.registerCodeLensProvider(editor, {
1524
- provideCodeLenses: (/** @type {Session} */session, /** @type {(_: null, arr: CodeLens[]) => void} */callback) => {
1525
- callback(null, codeLensListeners.some((v) => v.name === 'test') ? LanguageFeatures.getCodeLens(session.doc) : [])
1526
- }
1527
- })
1528
- } catch (e) {
1529
- console.error(e) // ext/code_lens のscriptタグが読み込まれていない場合など。
1530
- }
1531
-
1532
- // 「全画面表示」ボタン
1533
- const exitFullscreen = () => {
1534
- editor.container.classList.remove('fullscreen')
1535
- editor.renderer.setScrollMargin(0, 0, 0, 0) // marginを元に戻す
1536
- }
1537
- const fullscreenButton = document.createElement('span')
1538
- fullscreenButton.classList.add('editor-button')
1539
- fullscreenButton.innerText = '全画面表示'
1540
- fullscreenButton.addEventListener('click', (e) => {
1541
- if (editor.container.classList.contains('fullscreen')) {
1542
- exitFullscreen()
1543
- } else {
1544
- editor.container.classList.add('fullscreen')
1545
- editor.renderer.setScrollMargin(20, 20, 0, 0) // 上下に少し隙間を開ける
1546
- }
1547
- e.preventDefault()
1548
- })
1549
- buttonContainer.appendChild(fullscreenButton)
1550
-
1551
- // 「設定を開く」ボタン
1552
- const settingsButton = document.createElement('span')
1553
- settingsButton.classList.add('editor-button')
1554
- settingsButton.innerText = '設定を開く'
1555
- settingsButton.addEventListener('click', (e) => {
1556
- exitFullscreen()
1557
- editor.execCommand('showSettingsMenu')
1558
- e.preventDefault()
1559
- })
1560
- buttonContainer.appendChild(settingsButton)
1561
-
1562
- // 複数ファイルの切り替え
1563
- const UndoManager = ace.require('ace/undomanager').UndoManager
1564
- const editorTabs = new EditorTabs(editor, AceRange, UndoManager)
1565
-
1566
- // リサイズ可能にする
1567
- const resizable = element.dataset.nako3Resizable
1568
- if (resizable) {
1569
- new MutationObserver(() => { editor.resize() }).observe(editor.container, { attributes: true })
1570
- editor.renderer.setScrollMargin(4, 0, 4, 0)
1571
- editor.container.classList.add('resizable')
1572
- }
1573
-
1574
- const retokenize = () => { backgroundTokenizer.dirty = true }
1575
-
1576
- /**
1577
- * プログラムを実行して、エラーがあればエディタ上に波線を表示する。出力はoutputContainerに表示する。
1578
- * methodが'test'のとき、testNameを指定すると1つのテストだけ実行できる。
1579
- * @param {{
1580
- * outputContainer?: HTMLElement
1581
- * file?: string
1582
- * preCode?: string
1583
- * localFiles?: Record<string, string>
1584
- * method?: 'run' | 'test' | 'compile'
1585
- * testName?: string
1586
- * }} opts
1587
- */
1588
- const run = (opts) => {
1589
- const code = editor.getValue()
1590
- const preCode = opts.preCode || '' // プログラムの前に自動的に挿入されるコード
1591
-
1592
- // loggerを新しいインスタンスに置き換える。そうしないとどのエディタで起きたエラー(や警告や出力)なのかが分からない。
1593
- const logger = nako3.replaceLogger()
1594
- if (opts.outputContainer) {
1595
- const c = opts.outputContainer
1596
- logger.addListener('info', ({ html }) => {
1597
- if (!c) { console.log(html) }
1598
- c.style.display = 'block'
1599
- c.innerHTML += html
1600
- })
1601
- opts.outputContainer.classList.add('nako3-output-container')
1602
- }
1603
- let filename = opts.file || 'main.nako3'
1604
-
1605
- // 警告とエラーをエディタ上に表示する。
1606
- logger.addListener('info', ({ position, noColor, level }) => {
1607
- if (position && (position.file === filename && (level === 'warn' || level === 'error'))) {
1608
- editorMarkers.addByError(code, { ...position, message: noColor }, level)
1609
- }
1610
- })
1611
-
1612
- // 依存ファイルを読み込む。
1613
- const promise = nako3.loadDependencies(preCode + code, filename, preCode, opts.localFiles || {})
1614
- .then(() => {
1615
- // プログラムを実行する。
1616
- if (!filename) { filename = 'main.nako3' }
1617
- if (opts.method === 'test') {
1618
- return nako3.test(preCode + code, filename, preCode, opts.testName)
1619
- } else if (opts.method === 'compile') {
1620
- return nako3.compile(preCode + code, filename, false, preCode)
1621
- } else {
1622
- const opt = { resetEnv: true, resetAll: true }
1623
- return nako3.runEx(preCode + code, filename, opt, preCode)
1624
- }
1625
- })
1626
- .catch((err) => {
1627
- // エラーはloggerに送られるため何もしなくて良い
1628
- // しかし念のため console.error で出力
1629
- console.error('[wnako3_editor]', err)
1630
- })
1631
- .then(async (res) => {
1632
- // 読み込んだ依存ファイルの情報を使って再度シンタックスハイライトする。
1633
- retokenize()
1634
- // シンタックスハイライトが終わるのを待つ
1635
- while (backgroundTokenizer.dirty) {
1636
- await new Promise((resolve) => setTimeout(resolve, 0))
1637
- }
1638
- return res
1639
- })
1640
- .catch((err) => {
1641
- console.error('[wnako3_editor::run::promise::catch]', err)
1642
- })
1643
-
1644
- return { promise, logger, code }
1645
- }
1646
-
1647
- return { editor, editorMarkers, editorTabs, retokenize, run, codeLensListeners }
1648
- }
1649
-
1650
- export default {
1651
- tokenize,
1652
- setupEditor,
1653
- LanguageFeatures,
1654
- EditorMarkers,
1655
- BackgroundTokenizer
1656
- }
1657
-
1
+ // @ts-nocheck
2
+ /* eslint-disable no-template-curly-in-string */
3
+ /** なでしこのtokenのtypeをscope(CSSのクラス名)に変換する。 */
4
+
5
+ import { OffsetToLineColumn } from '../core/src/nako_source_mapping.mjs'
6
+ import { NakoError } from '../core/src/nako_errors.mjs'
7
+ import NakoIndent from '../core/src/nako_indent.mjs'
8
+ import { NakoPrepare } from '../core/src/nako_prepare.mjs'
9
+ import { NakoLogger } from '../core/src/nako_logger.mjs'
10
+ import { NakoCompiler } from '../core/src/nako3.mjs'
11
+
12
+ // alias
13
+ const getBlockStructure = NakoIndent.getBlockStructure
14
+ const getIndent = NakoIndent.getIndent
15
+ const countIndent = NakoIndent.countIndent
16
+ const isIndentSyntaxEnabled = NakoIndent.isIndentSyntaxEnabled
17
+
18
+ /**
19
+ * @typedef {import('./nako3')} NakoCompiler
20
+ *
21
+ * @typedef {{
22
+ * getValue(): string
23
+ * setValue(text: string): void
24
+ * session: Session
25
+ * execCommand(command: string): void
26
+ * setReadOnly(value: boolean): void
27
+ * setOption(key: string, value: unknown): void
28
+ * getOption(key: string): unknown
29
+ * setOptions(entries: Record<string, unknown>): void
30
+ * setFontSize(px: number): void
31
+ * setKeyboardHandler(name: string): void
32
+ * setTheme(name: string): void
33
+ * container: HTMLElement
34
+ * wnako3EditorId?: number
35
+ * getCursorPosition(): { row: number, column: number }
36
+ * commands: { addCommand(data: { name: string, exec: (editor: AceEditor, args: any[]) => void }): void }
37
+ * }} AceEditor
38
+ *
39
+ * @typedef {import("./nako_lexer").TokenWithSourceMap} TokenWithSourceMap
40
+ *
41
+ * @typedef {{
42
+ * getLine(row: number): string
43
+ * getAllLines(): string[]
44
+ * getLength(): number
45
+ * insertInLine(position: { row: number, column: number }, text: string): void
46
+ * removeInLine(row: number, columnStart: number, columnEnd: number): void
47
+ * replace(range: AceRange, text: string): void
48
+ * }} AceDocument
49
+ *
50
+ * @typedef {{
51
+ * doc: AceDocument
52
+ * bgTokenizer: BackgroundTokenizer
53
+ * getScrollTop(): number
54
+ * setScrollTop(x: number): void
55
+ * getScrollLeft(): number
56
+ * setScrollLeft(x: number): void
57
+ * getUndoManager(): any
58
+ * setUndoManager(x: any): void
59
+ * selection: { getRange(): AceRange, isBackwards(): boolean, setRange(range: AceRange, reversed: boolean): void, clearSelection(): void }
60
+ * setMode(mode: string | object): void
61
+ * }} Session
62
+ *
63
+ * @typedef {{}} AceRange
64
+ *
65
+ * @typedef {new (startLine: number, startColumn: number, endLine: number, endColumn: number) => AceRange} TypeofAceRange
66
+ *
67
+ * @typedef {string} TokenType
68
+ * @typedef {{ type: TokenType, value: string, docHTML: string | null }} EditorToken
69
+ *
70
+ * @typedef {{ start: { row: number }, command: { id: string, title: string, arguments: string[] } }} CodeLens
71
+ */
72
+
73
+ /**
74
+ * シンタックスハイライトでは一般にテキストの各部分に 'comment.line' のようなラベルを付け、各エディタテーマがそのそれぞれの色を設定する。
75
+ * ace editor では例えば 'comment.line' が付いた部分はクラス .ace_comment.ace_line が付いたHTMLタグで囲まれ、各テーマはそれに対応するCSSを実装する。
76
+ * @param {TokenWithSourceMap} token
77
+ * @returns {TokenType}
78
+ */
79
+ export function getScope (token) {
80
+ switch (token.type) {
81
+ case 'line_comment': return 'comment.line'
82
+ case 'range_comment': return 'comment.block'
83
+ case 'def_test': return 'keyword.control'
84
+ case 'def_func': return 'keyword.control'
85
+ case 'func': return 'entity.name.function'
86
+ case 'number': return 'constant.numeric'
87
+ // 独立した助詞
88
+ case 'とは':
89
+ case 'ならば':
90
+ case 'でなければ':
91
+ return 'keyword.control'
92
+ // 制御構文
93
+ case 'ここから':
94
+ case 'ここまで':
95
+ case 'もし':
96
+ case '違えば':
97
+ case 'require':
98
+ return 'keyword.control'
99
+ // 予約語
100
+ case '':
101
+ case '':
102
+ case '繰り返す':
103
+ case '反復':
104
+ case '抜ける':
105
+ case '続ける':
106
+ case '戻る':
107
+ case '先に':
108
+ case '次に':
109
+ case '代入':
110
+ case '逐次実行':
111
+ case '条件分岐':
112
+ case '取込':
113
+ case 'エラー監視':
114
+ case 'エラー':
115
+ case '変数':
116
+ case '実行速度優先':
117
+ return 'keyword.control'
118
+ case '定める':
119
+ case '定数':
120
+ return 'support.constant'
121
+ // 演算子
122
+ case 'shift_r0':
123
+ case 'shift_r':
124
+ case 'shift_l':
125
+ case 'gteq':
126
+ case 'lteq':
127
+ case 'noteq':
128
+ case 'eq':
129
+ case 'not':
130
+ case 'gt':
131
+ case 'lt':
132
+ case 'and':
133
+ case 'or':
134
+ case '@':
135
+ case '+':
136
+ case '-':
137
+ case '*':
138
+ case '/':
139
+ case '%':
140
+ case '^':
141
+ case '&':
142
+ return 'keyword.operator'
143
+ case 'string':
144
+ case 'string_ex':
145
+ return 'string.other'
146
+ case 'word':
147
+ if (['そう', 'それ', '回数', '対象キー', '対象'].includes(token.value)) {
148
+ return 'variable.language'
149
+ } else {
150
+ return 'variable.other'
151
+ }
152
+ default:
153
+ return 'markup.other'
154
+ }
155
+ }
156
+
157
+ /**
158
+ * @param {TokenWithSourceMap} compilerToken
159
+ * @param {NakoCompiler} nako3
160
+ * @param {string} value
161
+ * @param {boolean} includesLastCharacter
162
+ * @param {boolean} underlineJosi
163
+ */
164
+ export function getEditorTokens (compilerToken, nako3, value, includesLastCharacter, underlineJosi) {
165
+ const type = getScope(compilerToken)
166
+ const docHTML = getDocumentationHTML(compilerToken, nako3)
167
+
168
+ // 助詞があれば助詞の部分を分割する。
169
+ // 最後の文字が現在の行に含まれないときは助詞を表示しない。そうしないと例えば `「文字列\n」を表示` の「列」の部分に下線が引かれてしまう。
170
+ if (compilerToken.rawJosi && value.length >= compilerToken.rawJosi.length && includesLastCharacter && underlineJosi) {
171
+ return [
172
+ { type, docHTML, value: value.slice(0, -compilerToken.rawJosi.length) },
173
+ { type: type + '.markup.underline', docHTML, value: value.slice(-compilerToken.josi.length) }
174
+ ]
175
+ }
176
+ return [
177
+ { type, docHTML, value }
178
+ ]
179
+ }
180
+
181
+ /**
182
+ * `name` が定義されたプラグインの名前を返す。
183
+ * @param {string} name
184
+ * @param {NakoCompiler} nako3
185
+ * @returns {string | null}
186
+ */
187
+ export function findPluginName (name, nako3) {
188
+ for (const pluginName of Object.keys(nako3.__module)) {
189
+ if (Object.keys(nako3.__module[pluginName]).includes(name)) {
190
+ return pluginName
191
+ }
192
+ }
193
+ return null
194
+ }
195
+
196
+ /**
197
+ * i = 0, 1, 2, ... に対して 'A', 'B', 'C', ... 'Z', 'AA', 'AB', ... を返す。
198
+ * @param {number} i
199
+ * @returns {string}
200
+ */
201
+ export function createParameterName (i) {
202
+ const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
203
+ return i.toString(26).split('').map((v) => alphabet[parseInt(v, 26)]).join('')
204
+ }
205
+
206
+ /**
207
+ * パラメータの定義を表す文字列を生成する。例えば `[['と', 'の'], ['を']]` に対して `'(Aと|Aの、Bを)'` を返す、パラメータが無い場合、空文字列を返す。
208
+ * @param {string[][]} josi
209
+ * @retunrs {string}
210
+ */
211
+ export function createParameterDeclaration (josi) {
212
+ const args = josi.map((union, i) => union.map((v) => `${createParameterName(i)}${v}`).join('|')).join('、')
213
+ if (args !== '') {
214
+ return `(${args})`
215
+ } else {
216
+ return ''
217
+ }
218
+ }
219
+
220
+ // https://stackoverflow.com/a/6234804
221
+ /** @param {string} t */
222
+ export function escapeHTML (t) {
223
+ return t
224
+ .replace(/&/g, '&amp;')
225
+ .replace(/</g, '&lt;')
226
+ .replace(/>/g, '&gt;')
227
+ .replace(/"/g, '&quot;')
228
+ .replace(/'/g, '&#039;')
229
+ }
230
+
231
+ /**
232
+ * 関数のドキュメントを返す。
233
+ * @param {TokenWithSourceMap} token
234
+ * @param {NakoCompiler} nako3
235
+ * @returns {string | null}
236
+ */
237
+ export function getDocumentationHTML (token, nako3) {
238
+ /** @param {string} text */
239
+ const meta = (text) => `<span class="tooltip-plugin-name">${escapeHTML(text)}</span>`
240
+ if (token.type === 'func') {
241
+ /** @type {string | null} */
242
+ const pluginName = findPluginName(token.value + '', nako3) || (token.meta && token.meta.file ? token.meta.file : null)
243
+ const josi = (token.meta && token.meta.josi) ? createParameterDeclaration(token.meta.josi) : '' // {関数} のとき token.meta.josi が存在しない
244
+ if (pluginName) {
245
+ return escapeHTML(josi + token.value) + meta(pluginName)
246
+ }
247
+ return escapeHTML(josi + token.value)
248
+ } else if (token.type === 'word') {
249
+ /** @type {string | null} */
250
+ const pluginName = findPluginName(token.value + '', nako3) || (token.meta && token.meta.file ? token.meta.file : null)
251
+ if (pluginName) {
252
+ return escapeHTML(token.value + '') + meta(pluginName)
253
+ }
254
+ }
255
+ return null
256
+ }
257
+
258
+ /**
259
+ * ace editor ではエディタの文字列の全ての部分に何らかの `type` を付けなければならない。
260
+ * なでしこのエディタでは 'markup.other' をデフォルト値として使うことにした。
261
+ * @param {number} row
262
+ * @param {AceDocument} doc
263
+ * @returns {EditorToken[]}
264
+ */
265
+ const getDefaultTokens = (row, doc) => [{ type: 'markup.other', value: doc.getLine(row), docHTML: null }]
266
+
267
+ /**
268
+ * 一時的にloggerを無効化する。そうしないとシンタックスハイライトの更新のたびにloggerへコンパイルエラーや警告が送られて、結果のボックスに行が追加されてしまう。
269
+ * @type {<T>(nako3: NakoCompiler, f: () => T) => T}
270
+ */
271
+ const withoutLogger = (nako3, f) => {
272
+ const logger = nako3.logger
273
+ try {
274
+ nako3.replaceLogger()
275
+ return f()
276
+ } finally {
277
+ nako3.logger = logger
278
+ }
279
+ }
280
+
281
+ /**
282
+ * プログラムをlexerでtokenizeした後、ace editor 用のトークン列に変換する。
283
+ * @param lines
284
+ * @param nako3
285
+ * @param underlineJosi
286
+ */
287
+ export function tokenize (lines: string[], nako3: NakoCompiler, underlineJosi: boolean) {
288
+ const code = lines.join('\n')
289
+
290
+ // 取り込み文を含めてしまうと依存ファイルが大きい時に時間がかかってしまうため、
291
+ // 取り込み文を無視してトークン化してから、依存ファイルで定義された関数名と一致するトークンを関数のトークンへ変換する。
292
+ nako3.reset({needToClearPlugin: false})
293
+ const lexerOutput = withoutLogger(nako3, () => nako3.lex(code, 'main.nako3', undefined, true))
294
+ lexerOutput.commentTokens = lexerOutput.commentTokens.filter((t) => t.file === 'main.nako3')
295
+ lexerOutput.requireTokens = lexerOutput.requireTokens.filter((t) => t.file === 'main.nako3')
296
+ lexerOutput.tokens = lexerOutput.tokens.filter((t) => t.file === 'main.nako3')
297
+
298
+ // 外部ファイルで定義された関数名に一致するトークンのtypeをfuncに変更する。
299
+ // 取り込んでいないファイルも参照される問題や、関数名の重複がある場合に正しくない情報を表示する問題がある。
300
+ // eslint-disable-next-line no-lone-blocks
301
+ {
302
+ /** @type {Record<string, object>} */
303
+ for (const [file, { funclist }] of Object.entries(nako3.dependencies)) {
304
+ for (const token of lexerOutput.tokens) {
305
+ if (token.type === 'word' && token.value !== 'それ' && funclist[token.value]) {
306
+ token.type = 'func'
307
+ // meta.file に定義元のファイル名を持たせる。
308
+ token.meta = { ...funclist[token.value + ''], file }
309
+ }
310
+ }
311
+ }
312
+ }
313
+
314
+ // eol、eof、長さが1未満のトークン、位置を特定できないトークンを消す
315
+ /** @type {(TokenWithSourceMap & { startOffset: number, endOffset: number })[]} */
316
+ // @ts-ignore
317
+ const tokens = [...lexerOutput.tokens, ...lexerOutput.commentTokens, ...lexerOutput.requireTokens].filter((t) =>
318
+ t.type !== 'eol' && t.type !== 'eof' &&
319
+ typeof t.startOffset === 'number' && typeof t.endOffset === 'number' &&
320
+ t.startOffset < t.endOffset)
321
+
322
+ // startOffsetでソートする
323
+ tokens.sort((a, b) => (a.startOffset || 0) - (b.startOffset || 0))
324
+
325
+ // 各行について、余る文字の無いようにエディタのトークンに変換する。
326
+ // 複数のトークンが重なることはないと仮定する。
327
+ let lineStartOffset = 0
328
+ let tokenIndex = 0
329
+ // 実際に必要なプロパティはtype, valueだけで、docは独自に追加した。
330
+ /** @type {EditorToken[][]} */
331
+ const editorTokens = [] // 各行のエディタのトークン
332
+ for (let i = 0; i < lines.length; i++) {
333
+ editorTokens.push([])
334
+ const lineEndOffset = lineStartOffset + lines[i].length
335
+ let offset = lineStartOffset
336
+
337
+ // 現在の行にかかっているトークンまで飛ばす
338
+ while (tokenIndex < tokens.length &&
339
+ tokens[tokenIndex].endOffset <= lineStartOffset) {
340
+ tokenIndex++
341
+ }
342
+
343
+ // 行全体を完全にまたがっているトークンが存在する場合
344
+ if (tokenIndex < tokens.length &&
345
+ tokens[tokenIndex].startOffset <= lineStartOffset &&
346
+ tokens[tokenIndex].endOffset >= lineEndOffset) {
347
+ editorTokens[i].push(...getEditorTokens(tokens[tokenIndex], nako3, lines[i], tokens[tokenIndex].endOffset <= lineEndOffset, underlineJosi))
348
+ } else {
349
+ // 行頭をまたがっているトークンが存在する場合
350
+ if (tokenIndex < tokens.length &&
351
+ tokens[tokenIndex].startOffset <= lineStartOffset) {
352
+ editorTokens[i].push(...getEditorTokens(tokens[tokenIndex], nako3, code.slice(offset, tokens[tokenIndex].endOffset), true, underlineJosi))
353
+ offset = tokens[tokenIndex].endOffset
354
+ tokenIndex++
355
+ }
356
+
357
+ // 行頭も行末もまたがっていないトークンを処理する
358
+ while (tokenIndex < tokens.length &&
359
+ tokens[tokenIndex].endOffset < lineEndOffset) {
360
+ // このトークンと直前のトークンの間に隙間があるなら、埋める
361
+ if (offset < tokens[tokenIndex].startOffset) {
362
+ editorTokens[i].push({
363
+ type: 'markup.other',
364
+ docHTML: null,
365
+ value: code.slice(offset, tokens[tokenIndex].startOffset)
366
+ })
367
+ offset = tokens[tokenIndex].startOffset
368
+ }
369
+
370
+ // 現在のトークンを使う
371
+ editorTokens[i].push(...getEditorTokens(tokens[tokenIndex], nako3, code.slice(offset, tokens[tokenIndex].endOffset), true, underlineJosi))
372
+ offset = tokens[tokenIndex].endOffset
373
+ tokenIndex++
374
+ }
375
+
376
+ // 行末をまたがっているトークンが存在する場合
377
+ if (tokenIndex < tokens.length &&
378
+ tokens[tokenIndex].startOffset < lineEndOffset) {
379
+ // トークンの前の隙間
380
+ if (offset < tokens[tokenIndex].startOffset) {
381
+ editorTokens[i].push({
382
+ type: 'markup.other',
383
+ docHTML: null,
384
+ value: code.slice(offset, tokens[tokenIndex].startOffset)
385
+ })
386
+ offset = tokens[tokenIndex].startOffset
387
+ }
388
+
389
+ // トークンを使う
390
+ editorTokens[i].push(...getEditorTokens(tokens[tokenIndex], nako3, code.slice(tokens[tokenIndex].startOffset, lineEndOffset), tokens[tokenIndex].endOffset <= lineEndOffset, underlineJosi))
391
+ } else {
392
+ editorTokens[i].push({
393
+ type: 'markup.other',
394
+ docHTML: null,
395
+ value: code.slice(offset, lineEndOffset)
396
+ })
397
+ }
398
+ }
399
+
400
+ lineStartOffset += lines[i].length + 1
401
+ }
402
+
403
+ return { editorTokens, lexerOutput }
404
+ }
405
+
406
+ /**
407
+ * エディタ上にエラーメッセージの波線とgutterの赤いマークとエラーメッセージのポップアップを設定するためのクラス。
408
+ */
409
+ export class EditorMarkers {
410
+ /**
411
+ * @param {any} session
412
+ * @param {AceDocument} doc
413
+ * @param {TypeofAceRange} AceRange
414
+ * @param {boolean} disable
415
+ */
416
+ constructor (session, doc, AceRange, disable) {
417
+ this.session = session
418
+ this.doc = doc
419
+ this.AceRange = AceRange
420
+ /** @type {any[]} */
421
+ this.markers = []
422
+ this.hasAnnotations = false
423
+ this.disable = disable
424
+ }
425
+
426
+ /**
427
+ * @param {number} startLine
428
+ * @param {number | null} startColumn
429
+ * @param {number | null} endLine
430
+ * @param {number | null} endColumn
431
+ * @param {(row: number) => string} getLine
432
+ * @returns {[number, number, number, number]}
433
+ */
434
+ static fromNullable (startLine, startColumn, endLine, endColumn, getLine) {
435
+ if (startColumn === null) {
436
+ startColumn = 0
437
+ }
438
+ if (endLine === null) {
439
+ endLine = startLine
440
+ }
441
+ if (endColumn === null) {
442
+ endColumn = getLine(endLine).length
443
+ }
444
+
445
+ // 最低でも1文字分の長さをとる
446
+ if (startLine === endLine && startColumn === endColumn) {
447
+ endColumn++
448
+ }
449
+
450
+ return [startLine, startColumn, endLine, endColumn]
451
+ }
452
+
453
+ /**
454
+ * @param {string} code @param {number} startOffset @param {number} endOffset
455
+ * @returns {[number, number, number, number]}
456
+ */
457
+ static fromOffset (code, startOffset, endOffset) {
458
+ const offsetToLineColumn = new OffsetToLineColumn(code)
459
+ const start = offsetToLineColumn.map(startOffset, false)
460
+ const end = offsetToLineColumn.map(endOffset, false)
461
+ return [start.line, start.column, end.line, end.column]
462
+ }
463
+
464
+ /**
465
+ * @param {string} code
466
+ * @param {{ line?: number, startOffset?: number | null, endOffset?: number | null, message: string }} error
467
+ * @param {(row: number) => string} getLine
468
+ * @returns {[number, number, number, number]}
469
+ */
470
+ static fromError (code, error, getLine) {
471
+ if (typeof error.startOffset === 'number' && typeof error.endOffset === 'number') {
472
+ // 完全な位置を取得できる場合
473
+ return this.fromOffset(code, error.startOffset, error.endOffset)
474
+ } else if (typeof error.line === 'number') {
475
+ // 行全体の場合
476
+ return this.fromNullable(error.line, null, null, null, getLine)
477
+ } else {
478
+ // 位置が不明な場合
479
+ return this.fromNullable(0, null, null, null, getLine)
480
+ }
481
+ }
482
+
483
+ /**
484
+ * @param {number} startLine
485
+ * @param {number | null} startColumn
486
+ * @param {number | null} endLine
487
+ * @param {number | null} endColumn
488
+ * @param {string} message
489
+ * @param {'warn' | 'error'} type
490
+ */
491
+ add (startLine, startColumn, endLine, endColumn, message, type) {
492
+ if (this.disable) {
493
+ return
494
+ }
495
+ const range = new this.AceRange(...EditorMarkers.fromNullable(startLine, startColumn, endLine, endColumn, (row) => this.doc.getLine(row)))
496
+ this.markers.push(this.session.addMarker(range, 'marker-' + (type === 'warn' ? 'yellow' : 'red'), 'text', false))
497
+ // type 'error' | 'warning' | 'info'
498
+ this.session.setAnnotations([{ row: startLine, column: startColumn, text: message, type: type === 'warn' ? 'warning' : 'error' }])
499
+ this.hasAnnotations = true
500
+ }
501
+
502
+ /**
503
+ * @param {string} code
504
+ * @param {{ line?: number, startOffset?: number | null, endOffset?: number | null, message: string }} error
505
+ * @param {'warn' | 'error'} type
506
+ */
507
+ addByError (code, error, type) {
508
+ this.add(...EditorMarkers.fromError(code, error, (row) => this.doc.getLine(row)), error.message, type)
509
+ }
510
+
511
+ /**
512
+ * 全てのエラーメッセージを削除する。
513
+ */
514
+ clear () {
515
+ for (const marker of this.markers) {
516
+ this.session.removeMarker(marker)
517
+ }
518
+ this.markers.length = 0
519
+ if (this.hasAnnotations) {
520
+ this.session.clearAnnotations()
521
+ this.hasAnnotations = false
522
+ }
523
+ }
524
+ }
525
+
526
+ /**
527
+ * ace editor BackgroundTokenizerを上書きして、シンタックスハイライトを自由に表示するためのクラス。
528
+ * ace editor ではシンタックスハイライトのために正規表現ベースのBackgroundTokenizerクラスを用意し定期的にトークン化を
529
+ * 行っているが、正規表現ではなくなでしこのコンパイラの出力を使うためにはそれを上書きする必要がある。
530
+ */
531
+ export class BackgroundTokenizer {
532
+ /**
533
+ * @param {AceDocument} doc
534
+ * @param {NakoCompiler} nako3
535
+ * @param {(firstRow: number, lastRow: number, ms: number) => void} onTokenUpdate
536
+ * @param {(code: string, err: Error) => void} onCompileError
537
+ * @param {boolean} underlineJosi
538
+ */
539
+ constructor (doc, nako3, onTokenUpdate, onCompileError, underlineJosi) {
540
+ this.onUpdate = onTokenUpdate
541
+ this.doc = doc
542
+ this.dirty = true
543
+ this.nako3 = nako3
544
+ this.onCompileError = onCompileError
545
+ this.underlineJosi = underlineJosi
546
+
547
+ // オートコンプリートで使うために、直近のtokenizeの結果を保存しておく
548
+ /** @type {ReturnType<NakoCompiler['lex']> | null} */
549
+ this.lastLexerOutput = null
550
+
551
+ // 各行のパース結果。
552
+ // typeはscopeのこと。配列の全要素のvalueを結合した文字列がその行の文字列と等しくなる必要がある。
553
+ /** @type {EditorToken[][]} */
554
+ this.lines = this.doc.getAllLines().map((line) => [{ type: 'markup.other', value: line, docHTML: null }])
555
+
556
+ // this.lines は外部から勝手に編集されてしまうため、コピーを持つ
557
+ /** @type {{ code: string, lines: string } | null} */
558
+ this.cache = null
559
+
560
+ this.deleted = false
561
+
562
+ /** @public */
563
+ this.enabled = true
564
+
565
+ const update = () => {
566
+ if (this.deleted) {
567
+ return
568
+ }
569
+ if (this.dirty && this.enabled) {
570
+ const startTime = Date.now()
571
+ this.dirty = false
572
+ const code = this.doc.getAllLines().join('\n')
573
+ try {
574
+ const startTime = Date.now()
575
+ const out = tokenize(this.doc.getAllLines(), nako3, this.underlineJosi)
576
+ this.lastLexerOutput = out.lexerOutput
577
+ this.lines = out.editorTokens
578
+ this.cache = { code, lines: JSON.stringify(this.lines) }
579
+
580
+ // ファイル全体の更新を通知する。
581
+ onTokenUpdate(0, this.doc.getLength() - 1, Date.now() - startTime)
582
+ } catch (e) {
583
+ onCompileError(code, e)
584
+ }
585
+ // tokenizeに時間がかかる場合、文字を入力できるように次回の実行を遅くする。
586
+ setTimeout(update, Math.max(100, Math.min(5000, (Date.now() - startTime) * 5)))
587
+ } else {
588
+ setTimeout(update, 100)
589
+ }
590
+ }
591
+
592
+ // コンストラクタが返る前にコールバックを呼ぶのはバグの元になるため一瞬待つ。
593
+ // たとえば `const a = new BackgroundTokenizer(..., () => { /* aを使った処理 */ }, ...)` がReferenceErrorになる。
594
+ setTimeout(() => { update() }, 0)
595
+ }
596
+
597
+ dispose () {
598
+ this.deleted = true
599
+ }
600
+
601
+ /**
602
+ * テキストに変更があったときに呼ばれる。IME入力中には呼ばれない。
603
+ * @param {{ action: string, start: { row: number, column: number }, end: { row: number, column: number }, lines: string[] }} delta
604
+ */
605
+ $updateOnChange (delta) {
606
+ this.dirty = true
607
+ const startRow = delta.start.row
608
+ const endRow = delta.end.row
609
+ if (startRow === endRow) { // 1行の編集
610
+ if (delta.action === 'insert' && this.lines[startRow]) { // 行内に文字列を挿入
611
+ const columnStart = delta.start.column
612
+ // updateOnChangeはIME入力中には呼ばれない。composition_placeholder を消さないとIME確定後の表示がずれる。
613
+ const oldTokens = this.lines[startRow]
614
+ .filter((v) => v.type !== 'composition_placeholder')
615
+ /** @type {EditorToken[]} */
616
+ const newTokens = []
617
+ let i = 0
618
+ let offset = 0
619
+
620
+ // columnStartより左のトークンはそのまま保持する
621
+ while (i < oldTokens.length && offset + oldTokens[i].value.length <= columnStart) {
622
+ newTokens.push(oldTokens[i])
623
+ offset += oldTokens[i].value.length
624
+ i++
625
+ }
626
+
627
+ // columnStartに重なっているトークンがあれば、2つに分割する
628
+ if (i < oldTokens.length && offset < columnStart) {
629
+ newTokens.push({ type: oldTokens[i].type, value: oldTokens[i].value.slice(0, columnStart - offset), docHTML: null })
630
+ newTokens.push({ type: 'markup.other', value: delta.lines[0], docHTML: null })
631
+ newTokens.push({ type: oldTokens[i].type, value: oldTokens[i].value.slice(columnStart - offset), docHTML: null })
632
+ i++
633
+ } else {
634
+ newTokens.push({ type: 'markup.other', value: delta.lines[0], docHTML: null })
635
+ }
636
+
637
+ // columnStartより右のトークンもそのまま保持する
638
+ while (i < oldTokens.length) {
639
+ newTokens.push(oldTokens[i])
640
+ i++
641
+ }
642
+
643
+ this.lines[startRow] = newTokens
644
+ } else {
645
+ this.lines[startRow] = getDefaultTokens(startRow, this.doc)
646
+ }
647
+ } else if (delta.action === 'remove') { // 範囲削除
648
+ this.lines.splice(startRow, endRow - startRow + 1, getDefaultTokens(startRow, this.doc))
649
+ } else { // 行の挿入
650
+ this.lines.splice(startRow, 1, ...Array(endRow - startRow + 1).fill(null).map((_, i) => getDefaultTokens(i + startRow, this.doc)))
651
+ }
652
+ }
653
+
654
+ /**
655
+ * tokenizerの出力を返す。文字入力したときに呼ばれる。
656
+ * @param {number} row
657
+ */
658
+ getTokens (row) {
659
+ // IME入力中はthis.lines[row]に自動的にnullが設定される。その場合新しく行のトークン列を生成して返さなければならない。
660
+ // 返した配列には自動的にIMEの入力用のテキストボックスであるcomposition_placeholderが挿入される。
661
+ if (!this.lines[row]) {
662
+ let ok = false
663
+
664
+ if (this.enabled) {
665
+ // tokenizeは非常に遅いため、キャッシュを使えるならそれを使う。
666
+ const code = this.doc.getAllLines().join('\n')
667
+ if (this.cache !== null && this.cache.code === code) {
668
+ ok = true
669
+ } else {
670
+ try {
671
+ const lines = tokenize(this.doc.getAllLines(), this.nako3, this.underlineJosi)
672
+ this.cache = { code, lines: JSON.stringify(lines.editorTokens) }
673
+ ok = true
674
+ } catch (e) {
675
+ if (!(e instanceof NakoError)) {
676
+ console.error(e)
677
+ }
678
+ }
679
+ }
680
+ }
681
+
682
+ if (ok && this.cache !== null) {
683
+ this.lines[row] = JSON.parse(this.cache.lines)[row]
684
+ } else {
685
+ this.lines[row] = getDefaultTokens(row, this.doc)
686
+ }
687
+ }
688
+ return this.lines[row]
689
+ }
690
+
691
+ // ace側から呼ばれるが無視するメソッド
692
+ // @ts-ignore
693
+ start (startRow) { /* pass */ }
694
+ // @ts-ignore
695
+ fireUpdateEvent (firstRow, lastRow) { /* pass */ }
696
+ // @ts-ignore
697
+ setDocument (doc) { /* pass */ }
698
+ scheduleStart () { /* pass */ }
699
+ // @ts-ignore
700
+ setTokenizer (tokenizer) { /* pass */ }
701
+ stop () { /* pass */ }
702
+ // @ts-ignore
703
+ getState (row) { return 'start' }
704
+ }
705
+
706
+ /**
707
+ * シンタックスハイライト以外のエディタの挙動の定義。
708
+ */
709
+ export class LanguageFeatures {
710
+ /**
711
+ * @param {TypeofAceRange} AceRange
712
+ * @param {NakoCompiler} nako3
713
+ */
714
+ constructor (AceRange, nako3) {
715
+ this.AceRange = AceRange
716
+ this.nako3 = nako3
717
+ }
718
+
719
+ /**
720
+ * Ctrl + / の動作の定義。
721
+ * @param {string} state
722
+ * @param {Session} session
723
+ * @param {number} startRow
724
+ * @param {number} endRow
725
+ */
726
+ static toggleCommentLines (state, { doc }, startRow, endRow) {
727
+ const prepare = NakoPrepare.getInstance()
728
+ /**
729
+ * @param {string} line
730
+ * @returns {{ type: 'blank' | 'code' } | { type: 'comment', start: number, len: number }}
731
+ */
732
+ const parseLine = (line) => {
733
+ // 先頭の空白を消す
734
+ const indent = getIndent(line)
735
+ if (indent === line) {
736
+ return { type: 'blank' }
737
+ }
738
+ line = line.substring(indent.length)
739
+
740
+ // 先頭がコメントの開始文字かどうか確認する
741
+ const ch2 = line.substring(0, 2).split('').map((c) => prepare.convert1ch(c)).join('')
742
+ if (ch2.substring(0, 1) === '#') {
743
+ return { type: 'comment', start: indent.length, len: 1 + (line.charAt(1) === ' ' ? 1 : 0) }
744
+ }
745
+ if (ch2 === '//') {
746
+ return { type: 'comment', start: indent.length, len: 2 + (line.charAt(2) === ' ' ? 1 : 0) }
747
+ }
748
+
749
+ return { type: 'code' }
750
+ }
751
+
752
+ /** @type {number[]} */
753
+ const rows = []
754
+ for (let i = startRow; i <= endRow; i++) {
755
+ rows.push(i)
756
+ }
757
+
758
+ // 全ての行が空白行ならコメントアウト、全ての行が行コメントで始まるか空白行ならアンコメント、そうでなければコメントアウト。
759
+ if (!rows.every((row) => parseLine(doc.getLine(row)).type === 'blank') &&
760
+ rows.every((row) => parseLine(doc.getLine(row)).type !== 'code')) {
761
+ // アンコメント
762
+ for (const row of rows) {
763
+ // 行コメントで始まる行ならアンコメントする。
764
+ // 行コメントの直後にスペースがあるなら、それも1文字だけ削除する。
765
+ const line = parseLine(doc.getLine(row))
766
+ if (line.type === 'comment') {
767
+ doc.removeInLine(row, line.start, line.start + line.len)
768
+ }
769
+ }
770
+ } else {
771
+ // 最もインデントの低い行のインデント数を数える
772
+ const minIndent = Math.min(...rows.map((row) => countIndent(doc.getLine(row))))
773
+
774
+ // コメントアウトする
775
+ for (const row of rows) {
776
+ const line = doc.getLine(row)
777
+ let column = line.length
778
+ for (let i = 0; i < line.length; i++) {
779
+ if (countIndent(line.slice(0, i)) >= minIndent) {
780
+ column = i
781
+ break
782
+ }
783
+ }
784
+ doc.insertInLine({ row, column }, '// ')
785
+ }
786
+ }
787
+ }
788
+
789
+ /**
790
+ * 文字を入力するたびに呼ばれる。trueを返すとautoOutdentが呼ばれる。
791
+ * @param {string} state
792
+ * @param {string} line
793
+ * @param {string} input
794
+ * @returns {boolean}
795
+ */
796
+ static checkOutdent (state, line, input) {
797
+ // 特定のキーワードの入力が終わったタイミングでインデントを自動修正する。
798
+ // '違えば'のautoOutdentは「もし」と「条件分岐」のどちらのものか見分けが付かないため諦める。
799
+ // 「ここ|ま」(縦線がカーソル)の状態で「で」を打つとtrueになってしまう問題があるが、修正するには引数が足りない。
800
+ // eslint-disable-next-line no-irregular-whitespace
801
+ return /^[  ・\t]*ここまで$/.test(line + input)
802
+ }
803
+
804
+ /**
805
+ * checkOutdentがtrueを返したときに呼ばれる。
806
+ * @param {string} state
807
+ * @param {Session} session
808
+ * @param {number} row
809
+ * @returns {void}
810
+ */
811
+ autoOutdent (state, { doc }, row) {
812
+ // 1行目なら何もしない
813
+ if (row === 0) {
814
+ return
815
+ }
816
+ const prevLine = doc.getLine(row - 1)
817
+ let indent
818
+ if (LanguageFeatures.isBlockStart(prevLine)) {
819
+ // 1つ前の行が「〜ならば」などのブロック開始行なら、その行に合わせる。
820
+ indent = getIndent(prevLine)
821
+ } else {
822
+ // そうでなければ、1つ前の行のインデントから1段階outdentした位置に合わせる。
823
+ const s = this.getBlockStructure(doc.getAllLines().join('\n'))
824
+ const parent = s.parents[row]
825
+ indent = parent !== null ? s.spaces[parent] : ''
826
+ }
827
+
828
+ // 置換する
829
+ const oldIndent = getIndent(doc.getLine(row))
830
+ doc.replace(new this.AceRange(row, 0, row, oldIndent.length), indent)
831
+ }
832
+
833
+ /**
834
+ * エンターキーを押して行が追加されたときに挿入する文字列を指定する。
835
+ * @param {string} state
836
+ * @param {string} line 改行前にカーソルがあった行の文字列
837
+ * @param {string} tab タブ文字(デフォルトでは " ")
838
+ */
839
+ static getNextLineIndent (state, line, tab) {
840
+ // ●で始まるか、特定のキーワードで終わる場合にマッチする。
841
+ if (this.isBlockStart(line)) {
842
+ return getIndent(line) + tab
843
+ }
844
+ return getIndent(line)
845
+ }
846
+
847
+ /** @param {string} line */
848
+ static isBlockStart (line) {
849
+ // eslint-disable-next-line no-irregular-whitespace
850
+ return /^[  ・\t]*●|(ならば|なければ|ここから|条件分岐|違えば|回|繰り返(す|し)|の間|反復|とは|には|エラー監視|エラーならば|実行速度優先)、?\s*$/.test(line)
851
+ }
852
+
853
+ /**
854
+ * オートコンプリート
855
+ * @param {number} row
856
+ * @param {string} prefix getCompletionPrefixの出力
857
+ * @param {NakoCompiler} nako3
858
+ * @param {BackgroundTokenizer} backgroundTokenizer
859
+ */
860
+ static getCompletionItems (row, prefix, nako3, backgroundTokenizer) {
861
+ /**
862
+ * keyはcaption。metaは候補の横に薄く表示されるテキスト。
863
+ * @type {Map<string, { value: string, meta: Set<string>, score: number }>}
864
+ */
865
+ const result = new Map()
866
+ /** 引数のリストを含まない、関数名だけのリスト @type {Set<string>} */
867
+ const values = new Set()
868
+
869
+ /**
870
+ * オートコンプリートの項目を追加する。すでに存在するならマージする。
871
+ * @param {string} caption @param {string} value @param {string} meta
872
+ */
873
+ const addItem = (caption, value, meta) => {
874
+ const item = result.get(caption)
875
+ if (item) {
876
+ item.meta.add(meta)
877
+ } else {
878
+ // 日本語の文字数は英語よりずっと多いため、ただ一致する文字数を数えるだけで十分。
879
+ const score = prefix.split('').filter((c) => value.includes(c)).length
880
+ result.set(caption, { value, meta: new Set([meta]), score })
881
+ values.add(value)
882
+ }
883
+ }
884
+
885
+ // プラグイン関数
886
+ for (const name of Object.keys(nako3.__varslist[0])) {
887
+ if (name.startsWith('!')) { // 「!PluginBrowser:初期化」などを除外
888
+ continue
889
+ }
890
+ const f = nako3.funclist[name]
891
+ if (typeof f !== 'object' || f === null) {
892
+ continue
893
+ }
894
+
895
+ const pluginName = findPluginName(name, nako3) || 'プラグイン'
896
+ if (f.type === 'func') {
897
+ addItem(createParameterDeclaration(f.josi) + name, name, pluginName)
898
+ } else {
899
+ addItem(name, name, pluginName)
900
+ }
901
+ }
902
+
903
+ // 依存ファイルが定義した関数名
904
+ for (const [file, { funclist }] of Object.entries(nako3.dependencies)) {
905
+ for (const [name, f] of Object.entries(funclist)) {
906
+ const josi = (f && f.type === 'func') ? createParameterDeclaration(f.josi) : ''
907
+ addItem(josi + name, name, file)
908
+ }
909
+ }
910
+
911
+ // 現在のファイル内に存在する名前
912
+ if (backgroundTokenizer.lastLexerOutput !== null) {
913
+ for (const token of backgroundTokenizer.lastLexerOutput.tokens) {
914
+ const name = token.value + ''
915
+ // 同じ行のトークンの場合、自分自身にマッチしている可能性が高いため除外する。
916
+ // すでに定義されている場合も、定義ではなく参照の可能性が高いため除外する。
917
+ if (token.line === row || values.has(name)) {
918
+ continue
919
+ }
920
+ if (token.type === 'word') {
921
+ addItem(name, name, '変数')
922
+ } else if (token.type === 'func') {
923
+ const f = nako3.funclist[name]
924
+ const josi = (f && f.type === 'func') ? createParameterDeclaration(f.josi) : ''
925
+ addItem(josi + name, name, '関数')
926
+ }
927
+ }
928
+ }
929
+
930
+ return Array.from(result.entries()).map(([caption, data]) => ({ caption, ...data, meta: Array.from(data.meta).join(', ') }))
931
+ }
932
+
933
+ /**
934
+ * スニペット
935
+ */
936
+ /** @param {string} text */
937
+ static getSnippets (text) {
938
+ // インデント構文が有効化されているなら「ここまで」を消す
939
+ const indentSyntax = isIndentSyntaxEnabled(text)
940
+
941
+ /** @param {string} en @param {string} jp @param {string} snippet */
942
+ const item = (en, jp, snippet) => indentSyntax
943
+ ? { caption: en, meta: `\u21E5 ${jp}`, score: 1, snippet: snippet.replace(/\t*ここまで(\n|$)/g, '').replace(/\t/g, ' ') }
944
+ : { caption: en, meta: `\u21E5 ${jp}`, score: 1, snippet: snippet.replace(/\t/g, ' ') }
945
+
946
+ return [
947
+ item('if', 'もし〜ならば', 'もし${1:1=1}ならば\n\t${2:1を表示}\n違えば\n\t${3:2を表示}\nここまで\n'),
948
+ item('times', '〜回', '${1:3}回\n\t${2:1を表示}\nここまで\n'),
949
+ item('for', '繰り返す', '${1:N}で${2:1}から${3:3}まで繰り返す\n\t${4:Nを表示}\nここまで\n'),
950
+ item('while', '〜の間', '${1:N<2の間}\n\tN=N+1\nここまで\n'),
951
+ item('foreach', '〜を反復', '${1:[1,2,3]}を反復\n\t${2:対象を表示}\nここまで\n'),
952
+ item('switch', '〜で条件分岐', '${1:N}で条件分岐\n\t${2:1}ならば\n\t\t${3:1を表示}\n\tここまで\n\t${4:2}ならば\n\t\t${5:2を表示}\n\tここまで\n\t違えば\n\t\t${6:3を表示}\n\tここまで\nここまで\n'),
953
+ item('function', '●〜とは', '●(${1:AとBを})${2:足す}とは\n\t${3:A+Bを戻す}\nここまで\n'),
954
+ item('test', '●テスト:〜とは', '●テスト:${2:足す}とは\n\t1と2を足す\n\tそれと3がASSERT等しい\nここまで\n'),
955
+ item('try', 'エラー監視', 'エラー監視\n\t${1:1のエラー発生}\nエラーならば\n\t${2:2を表示}\nここまで\n')
956
+ ]
957
+ }
958
+
959
+ /**
960
+ * @param {string} line
961
+ * @param {NakoCompiler} nako3
962
+ */
963
+ static getCompletionPrefix (line, nako3) {
964
+ /** @type {ReturnType<NakoCompiler['lex']>["tokens"] | null} */
965
+ let tokens = null
966
+
967
+ // ひらがなとアルファベットとカタカナと漢字のみオートコンプリートする。
968
+ if (line.length === 0 || !/[ぁ-んa-zA-Zァ-ヶー\u3005\u4E00-\u9FCF]/.test(line[line.length - 1])) {
969
+ return ''
970
+ }
971
+
972
+ // 現在の行のカーソルより前の部分をlexerにかける。速度を優先して1行だけ処理する。
973
+ try {
974
+ nako3.reset()
975
+ tokens = withoutLogger(nako3, () => nako3.lex(line, 'completion.nako3', undefined, true)).tokens
976
+ .filter((t) => t.type !== 'eol' && t.type !== 'eof')
977
+ } catch (e) {
978
+ if (!(e instanceof NakoError)) {
979
+ console.error(e)
980
+ }
981
+ }
982
+ if (tokens === null || tokens.length === 0 || !tokens[tokens.length - 1].value) {
983
+ return ''
984
+ }
985
+ const prefix = tokens[tokens.length - 1].value + ''
986
+
987
+ // 単語の先頭がひらがなではなく末尾がひらがなのとき、助詞を打っている可能性が高いためオートコンプリートしない。
988
+ if (/[ぁ-ん]/.test(prefix[prefix.length - 1]) && !/[ぁ-ん]/.test(prefix[0])) {
989
+ return ''
990
+ }
991
+
992
+ // 最後のトークンの値を、オートコンプリートで既に入力した部分とする。
993
+ return prefix
994
+ }
995
+
996
+ /**
997
+ * 文字を打つたびに各行についてこの関数が呼ばれる。'start'を返した行はfold可能な範囲の先頭の行になる。
998
+ * @param {Session} session
999
+ * @param {string} foldStyle
1000
+ * @param {number} row
1001
+ * @returns {'start' | ''}
1002
+ */
1003
+ getFoldWidget ({ doc }, foldStyle, row) {
1004
+ // 速度が重要なため正規表現でマッチする。
1005
+ return LanguageFeatures.isBlockStart(doc.getLine(row)) ? 'start' : ''
1006
+ }
1007
+
1008
+ /**
1009
+ * getFoldWidgetが'start'を返した行に設置されるfold用のボタンが押されたときに呼ばれる。
1010
+ * @param {Session} session
1011
+ * @param {string} foldStyle
1012
+ * @param {number} row
1013
+ * @returns {AceRange | null} foldする範囲
1014
+ */
1015
+ getFoldWidgetRange ({ doc }, foldStyle, row) {
1016
+ const pair = this.getBlockStructure(doc.getAllLines().join('\n')).pairs.find((v) => v[0] === row)
1017
+ if (pair !== undefined) {
1018
+ return new this.AceRange(pair[0], doc.getLine(pair[0]).length, pair[1] - 1, doc.getLine(pair[1] - 1).length)
1019
+ }
1020
+ return null
1021
+ }
1022
+
1023
+ /**
1024
+ * @param {AceDocument} doc
1025
+ * @returns {CodeLens[]}
1026
+ */
1027
+ static getCodeLens (doc) {
1028
+ const results = []
1029
+ for (const [row, line] of Array.from(doc.getAllLines().entries())) {
1030
+ // eslint-disable-next-line no-irregular-whitespace
1031
+ const matches = /^[  ・\t]*●テスト:(.+?)(?:とは|$)/.exec(line)
1032
+ if (matches !== null) {
1033
+ results.push({
1034
+ start: { row },
1035
+ command: { title: 'テストを実行', id: 'runTest', arguments: [matches[1]] }
1036
+ })
1037
+ }
1038
+ }
1039
+ return results
1040
+ }
1041
+
1042
+ /**
1043
+ * @param {string} code
1044
+ * @returns {ReturnType<getBlockStructure>}
1045
+ * @private
1046
+ */
1047
+ getBlockStructure (code) {
1048
+ // キャッシュ
1049
+ if (!this.blockStructure || this.blockStructure.code !== code) {
1050
+ // @ts-ignore
1051
+ this.blockStructure = { code, data: getBlockStructure(code) }
1052
+ }
1053
+ return this.blockStructure.data
1054
+ }
1055
+ }
1056
+
1057
+ /**
1058
+ * 複数ファイルを表示するための最低限のAPIを提供する。
1059
+ * @typedef {{ content: string, cursor: { range: AceRange, reversed: boolean }, scroll: { top: number, left: number }, undoManger: any }} EditorTabState
1060
+ */
1061
+ class EditorTabs {
1062
+ /**
1063
+ * @param {AceEditor} editor
1064
+ * @param {TypeofAceRange} AceRange
1065
+ * @param {any} UndoManager
1066
+ */
1067
+ constructor (editor, AceRange, UndoManager) {
1068
+ this.editor = editor
1069
+ this.AceRange = AceRange
1070
+ this.UndoManager = UndoManager
1071
+ }
1072
+
1073
+ /** @param {string} content @returns {EditorTabState} */
1074
+ newTab (content) {
1075
+ return {
1076
+ content,
1077
+ cursor: { range: new this.AceRange(0, 0, 0, 0), reversed: false },
1078
+ scroll: { left: 0, top: 0 },
1079
+ undoManger: new this.UndoManager()
1080
+ }
1081
+ }
1082
+
1083
+ /** @returns {EditorTabState} */
1084
+ getTab () {
1085
+ return {
1086
+ content: this.editor.getValue(),
1087
+ cursor: { range: this.editor.session.selection.getRange(), reversed: this.editor.session.selection.isBackwards() },
1088
+ scroll: { left: this.editor.session.getScrollLeft(), top: this.editor.session.getScrollTop() },
1089
+ undoManger: this.editor.session.getUndoManager()
1090
+ }
1091
+ }
1092
+
1093
+ /** @param {EditorTabState} state */
1094
+ setTab (state) {
1095
+ this.editor.setValue(state.content)
1096
+ this.editor.session.selection.setRange(state.cursor.range, state.cursor.reversed)
1097
+ this.editor.session.setScrollLeft(state.scroll.left)
1098
+ this.editor.session.setScrollTop(state.scroll.top)
1099
+ this.editor.session.setUndoManager(state.undoManger)
1100
+ }
1101
+ }
1102
+
1103
+ class Options {
1104
+ /** @param {AceEditor} editor */
1105
+ static save (editor) {
1106
+ try {
1107
+ /** @type {any} */
1108
+ const obj = {}
1109
+ for (const key of ['syntaxHighlighting', 'keyboardHandler', 'theme', 'fontSize', 'wrap', 'useSoftTabs', 'tabSize', 'showInvisibles', 'enableLiveAutocompletion', 'indentedSoftWrap', 'underlineJosi']) {
1110
+ obj[key] = editor.getOption(key)
1111
+ }
1112
+ localStorage.setItem('nako3EditorOptions', JSON.stringify(obj))
1113
+ } catch (e) {
1114
+ // JSON.stringify のエラー、localStorageのエラーなど
1115
+ console.error(e)
1116
+ return null
1117
+ }
1118
+ }
1119
+
1120
+ /** @param {AceEditor} editor */
1121
+ static load (editor) {
1122
+ try {
1123
+ if (!window.localStorage) {
1124
+ return null
1125
+ }
1126
+ const text = window.localStorage.getItem('nako3EditorOptions')
1127
+ if (text === null) {
1128
+ return null
1129
+ }
1130
+ const json = JSON.parse(text)
1131
+ if (['ace/keyboard/vscode', 'ace/keyboard/emacs', 'ace/keyboard/sublime', 'ace/keyboard/vim'].includes(json.keyboardHandler)) {
1132
+ editor.setOption('keyboardHandler', json.keyboardHandler)
1133
+ }
1134
+ if (['ace/theme/xcode', 'ace/theme/monokai'].includes(json.theme)) {
1135
+ editor.setOption('theme', json.theme)
1136
+ }
1137
+ if (typeof json.fontSize === 'number') {
1138
+ editor.setOption('fontSize', Math.min(48, Math.max(6, json.fontSize)))
1139
+ }
1140
+ for (const key of ['syntaxHighlighting', 'wrap', 'useSoftTabs', 'showInvisibles', 'enableLiveAutocompletion', 'indentedSoftWrap', 'underlineJosi']) {
1141
+ if (typeof json[key] === 'boolean') {
1142
+ editor.setOption(key, json[key])
1143
+ }
1144
+ }
1145
+ if (typeof json.tabSize === 'number') {
1146
+ editor.setOption('tabSize', Math.min(16, Math.max(0, json.tabSize)))
1147
+ }
1148
+ } catch (e) {
1149
+ // JSONのパースエラー、localStorageのエラーなど
1150
+ console.error(e)
1151
+ return null
1152
+ }
1153
+ }
1154
+
1155
+ /**
1156
+ * OptionPanelクラスをなでしこ用に書き換える。
1157
+ * @param {any} OptionPanel
1158
+ * @param {AceEditor} editor
1159
+ */
1160
+ static initPanel (OptionPanel, editor) {
1161
+ const panel = new OptionPanel(editor) // editorはエラーが飛ばなければ何でも良い
1162
+
1163
+ // ページ内で一度だけ呼ぶ
1164
+ if (this.done) {
1165
+ return
1166
+ }
1167
+ this.done = true
1168
+
1169
+ // renderメソッドを呼ぶとrenderOptionGroupにoptionGroups.Main、optionGroups.More が順に渡されることを利用して、optionGroupsを書き換える。
1170
+ let isMain = true
1171
+ panel.renderOptionGroup = (group) => {
1172
+ if (isMain) { // Main
1173
+ for (const key of Object.keys(group)) {
1174
+ delete group[key]
1175
+ }
1176
+
1177
+ // スマートフォンでも見れるように、文字数は最小限にする
1178
+ group['シンタックスハイライト'] = {
1179
+ path: 'syntaxHighlighting'
1180
+ }
1181
+ group['キーバインド'] = {
1182
+ path: 'keyboardHandler',
1183
+ type: 'select',
1184
+ items: [
1185
+ { caption: 'VSCode', value: 'ace/keyboard/vscode' },
1186
+ { caption: 'Emacs', value: 'ace/keyboard/emacs' },
1187
+ { caption: 'Sublime', value: 'ace/keyboard/sublime' },
1188
+ { caption: 'Vim', value: 'ace/keyboard/vim' }
1189
+ ]
1190
+ }
1191
+ group['カラーテーマ'] = {
1192
+ path: 'theme',
1193
+ type: 'select',
1194
+ items: [
1195
+ { caption: 'ライト', value: 'ace/theme/xcode' },
1196
+ { caption: 'ダーク', value: 'ace/theme/monokai' }
1197
+ ]
1198
+ }
1199
+ group['文字サイズ'] = {
1200
+ path: 'fontSize',
1201
+ type: 'number',
1202
+ defaultValue: 16
1203
+ }
1204
+ group['行の折り返し'] = {
1205
+ path: 'wrap',
1206
+ type: 'select',
1207
+ items: [
1208
+ { caption: 'なし', value: 'off' },
1209
+ { caption: 'あり', value: 'free' }
1210
+ ]
1211
+ }
1212
+ group['ソフトタブ'] = [{
1213
+ path: 'useSoftTabs'
1214
+ }, {
1215
+ ariaLabel: 'Tab Size',
1216
+ path: 'tabSize',
1217
+ type: 'number',
1218
+ values: [2, 3, 4, 8, 16]
1219
+ }]
1220
+ group['空白文字を表示'] = {
1221
+ path: 'showInvisibles'
1222
+ }
1223
+ group['常に自動補完'] = {
1224
+ path: 'enableLiveAutocompletion'
1225
+ }
1226
+ group['折り返した行をインデント'] = {
1227
+ path: 'indentedSoftWrap'
1228
+ }
1229
+ group['助詞に下線を引く'] = {
1230
+ path: 'underlineJosi'
1231
+ }
1232
+ isMain = false
1233
+ } else { // More
1234
+ for (const key of Object.keys(group)) {
1235
+ delete group[key]
1236
+ }
1237
+ }
1238
+ }
1239
+ panel.render()
1240
+
1241
+ // 設定メニューは ace/ext/settings_menu.js showSettingsMenu 関数によって開かれる。
1242
+ // showSettingsMenu 関数は new OptionPanel(editor).render() で新しい設定パネルのインスタンスを生成するため、
1243
+ // renderメソッドを上書きすることで、生成されたインスタンスにアクセスできる。
1244
+ const render = OptionPanel.prototype.render
1245
+ const self: any = globalThis
1246
+ OptionPanel.prototype.render = function (...args) {
1247
+ render.apply(this, ...args) // 元の処理
1248
+ // OptionPanel.setOption() で発火される setOption イベントをキャッチする
1249
+ this.on('setOption', () => {
1250
+ console.log('設定を保存しました。')
1251
+ self.save(this.editor)
1252
+ })
1253
+ }
1254
+ }
1255
+ }
1256
+
1257
+ /**
1258
+ * ace/ext/language_tools の設定がグローバル変数で保持されているため、こちら側でもグローバル変数で管理しないと、エディタが複数あるときに正しく動かない。
1259
+ * - captionはオートコンプリートの候補として表示されるテキスト
1260
+ * - metaはcaptionのテキストの右に薄く表示されるテキスト
1261
+ * - docHTMLはその更に右に独立したウィンドウで表示されるHTMLによる説明
1262
+ * - valueは決定したときに実際に挿入される文字列。プレースホルダーを配置するなら代わりにsnippetに値を設定する。
1263
+ *
1264
+ * @typedef {{
1265
+ * getCompletions(
1266
+ * editor: any,
1267
+ * session: Session,
1268
+ * pos: { row: number, column: number },
1269
+ * prefix: any,
1270
+ * callback: (
1271
+ * a: null,
1272
+ * b: { meta: string, caption: string, value?: string, score: number, docHTML?: string, snippet?: string }[]
1273
+ * ) => void
1274
+ * ): void
1275
+ * getDocTooltip?(item: any): void
1276
+ * }} Completer
1277
+ * @type {Completer[]}
1278
+ */
1279
+ const completers = []
1280
+
1281
+ let editorIdCounter = 0
1282
+ /**
1283
+ * 指定したidのHTML要素をなでしこ言語のエディタにする。
1284
+ *
1285
+ * - ace editor がグローバルに読み込まれている必要がある。
1286
+ * - wnako3_editor.css を読み込む必要がある。
1287
+ * - readonly にするには data-nako3-readonly="true" を設定する。
1288
+ * - エラー位置の表示を無効化するには data-nako3-disable-marker="true" を設定する。
1289
+ * - 縦方向にリサイズ可能にするには nako3-resizable="true" を設定する。
1290
+ * - デバイスが遅いときにシンタックスハイライトを無効化する機能を切るには nako3-force-syntax-highlighting="true" を設定する。
1291
+ *
1292
+ * @param {string | Element} idOrElement HTML要素
1293
+ * @param {import('./wnako3')} nako3
1294
+ * @param {any} ace
1295
+ */
1296
+ export function setupEditor (idOrElement, nako3, ace) {
1297
+ /** @type {AceEditor} */
1298
+ const editor = ace.edit(idOrElement)
1299
+ const element = typeof idOrElement === 'string' ? document.getElementById(idOrElement) : idOrElement
1300
+ if (element === null) {
1301
+ throw new Error(`idが ${idOrElement} のHTML要素は存在しません。`)
1302
+ }
1303
+
1304
+ /** @type {TypeofAceRange} */
1305
+ const AceRange = ace.require('ace/range').Range
1306
+ const editorMarkers = new EditorMarkers(
1307
+ editor.session,
1308
+ editor.session.bgTokenizer.doc,
1309
+ AceRange,
1310
+ !!element.dataset.nako3DisableMarker
1311
+ )
1312
+
1313
+ if (element.classList.contains('nako3_ace_mounted')) {
1314
+ // 同じエディタを誤って複数回初期化すると、ace editor の挙動を書き換えているせいで
1315
+ // 意図しない動作をしたため、すでにエディタとして使われていないことを確認する。
1316
+ throw new Error('なでしこ言語のエディタの初期化処理を同一のHTML要素に対して複数回適用しました。')
1317
+ }
1318
+ // lang="ja" があると表示がずれる問題の修正 #839
1319
+ element.setAttribute('lang', 'en')
1320
+ // 以前のバージョンではnako3_editorをhtmlに直接付けていたため、互換性のためnako3_editorとは別のクラス名を使用する。
1321
+ element.classList.add('nako3_ace_mounted')
1322
+ element.classList.add('nako3_editor') // CSSのため
1323
+ const readonly = element.dataset.nako3Readonly
1324
+ // eslint-disable-next-line no-extra-boolean-cast
1325
+ if (!!readonly) {
1326
+ element.classList.add('readonly')
1327
+ editor.setReadOnly(true)
1328
+ }
1329
+
1330
+ editor.setFontSize(16)
1331
+
1332
+ /** @param {Session} session */
1333
+ const resetEditorTokens = (session) => {
1334
+ // 一旦テキスト全体を消してから、元に戻す
1335
+ /** @type {AceDocument} */
1336
+ const doc = session.doc
1337
+ const lines = doc.getAllLines()
1338
+ const range = session.selection.getRange()
1339
+ doc.removeFullLines(0, doc.getLength())
1340
+ doc.insert({ row: 0, column: 0 }, lines.join('\n'))
1341
+ session.selection.setRange(range, false)
1342
+ }
1343
+
1344
+ ace.require('ace/config').defineOptions(editor.constructor.prototype, 'editor', {
1345
+ syntaxHighlighting: {
1346
+ /** @type {(this: AceEditor, value: boolean) => void} */
1347
+ set: function (value) {
1348
+ this.session.bgTokenizer.enabled = value
1349
+ resetEditorTokens(this.session)
1350
+ },
1351
+ initialValue: true
1352
+ },
1353
+ underlineJosi: {
1354
+ /** @type {(this: AceEditor, value: boolean) => void} */
1355
+ set: function (value) {
1356
+ this.session.bgTokenizer.underlineJosi = value
1357
+ resetEditorTokens(this.session)
1358
+ },
1359
+ initialValue: true
1360
+ }
1361
+ })
1362
+
1363
+ editor.setOptions({
1364
+ wrap: 'free',
1365
+ indentedSoftWrap: false,
1366
+ showPrintMargin: false
1367
+ })
1368
+ ace.require('ace/keybindings/vscode')
1369
+ editor.setKeyboardHandler('ace/keyboard/vscode')
1370
+
1371
+ // ドキュメントのホバー
1372
+ const Tooltip = ace.require('ace/tooltip').Tooltip
1373
+ const tooltip = new Tooltip(editor.container)
1374
+ const event = ace.require('ace/lib/event')
1375
+ event.addListener(editor.renderer.content, 'mouseout', () => {
1376
+ // マウスカーソルがエディタの外に出たら、tooltipを隠す
1377
+ tooltip.hide()
1378
+ })
1379
+ editor.on('mousemove', (e) => {
1380
+ // マウスカーソルがトークンに重なったときにtooltipを表示する。モバイル端末の場合はトークンにカーソルが当たったときに表示される。
1381
+ const pos = e.getDocumentPosition()
1382
+ // getTokenAtはcolumnが行末より大きいとき行末のトークンを返してしまう。
1383
+ if (pos.column >= e.editor.session.getLine(pos.row).length) {
1384
+ tooltip.hide()
1385
+ return
1386
+ }
1387
+ // getTokenAtは実際よりも1文字右のトークンを取得してしまうため、columnに1を足している。
1388
+ /** @type {EditorToken} */
1389
+ const token = e.editor.session.getTokenAt(pos.row, pos.column + 1)
1390
+ if (token === null || !token.docHTML) {
1391
+ // ドキュメントが存在しないトークンならtooltipを表示しない
1392
+ tooltip.hide()
1393
+ return
1394
+ }
1395
+
1396
+ tooltip.setHtml(token.docHTML)
1397
+ tooltip.show(null, e.clientX, e.clientY)
1398
+ })
1399
+ editor.session.on('change', () => {
1400
+ // モバイル端末でドキュメントが存在するトークンを編集するときにツールチップが消えない問題を解消するために、文字を打ったらtooltipを隠す。
1401
+ tooltip.hide()
1402
+
1403
+ // 文字入力したらマーカーを消す
1404
+ editorMarkers.clear()
1405
+ })
1406
+
1407
+ const forceSyntaxHighlighting = !!element.dataset.nako3ForceSyntaxHighlighting
1408
+
1409
+ let isFirstTime = true
1410
+ const oldBgTokenizer = editor.session.bgTokenizer
1411
+ const backgroundTokenizer = new BackgroundTokenizer(
1412
+ editor.session.bgTokenizer.doc,
1413
+ nako3,
1414
+ (firstRow, lastRow, ms) => {
1415
+ oldBgTokenizer._signal('update', { data: { first: firstRow, last: lastRow } })
1416
+
1417
+ // 処理が遅い場合シンタックスハイライトを無効化する。
1418
+ if (ms > 220 && editor.getOption('syntaxHighlighting') && !readonly && !forceSyntaxHighlighting && isFirstTime) {
1419
+ isFirstTime = false
1420
+ slowSpeedMessage.classList.add('visible')
1421
+ editor.setOption('syntaxHighlighting', false)
1422
+ setTimeout(() => {
1423
+ slowSpeedMessage.classList.remove('visible')
1424
+ }, 13000)
1425
+ }
1426
+ },
1427
+ (code, err) => { editorMarkers.addByError(code, err, 'error') },
1428
+ /** @type {boolean} */(editor.getOption('underlineJosi'))
1429
+ )
1430
+
1431
+ // オートコンプリートを有効化する
1432
+ editor.setOptions({
1433
+ enableBasicAutocompletion: true,
1434
+ enableSnippets: true,
1435
+ enableLiveAutocompletion: true
1436
+ })
1437
+
1438
+ const editorId = editorIdCounter++
1439
+ editor.wnako3EditorId = editorId
1440
+
1441
+ // オートコンプリートのcompleterを設定する
1442
+ completers.push(
1443
+ {
1444
+ getCompletions (editor, session, pos, prefix, callback) {
1445
+ if (editor.wnako3EditorId !== editorId) {
1446
+ callback(null, [])
1447
+ } else {
1448
+ const items = LanguageFeatures.getCompletionItems(pos.row, prefix, nako3, backgroundTokenizer)
1449
+ // 完全に一致する候補があればオートコンプリートしない。(Aceエディタでの挙動が微妙なため。)
1450
+ if (items.some((v) => v.value === prefix)) {
1451
+ callback(null, [])
1452
+ return
1453
+ }
1454
+ callback(null, items)
1455
+ }
1456
+ }
1457
+ },
1458
+ { getCompletions (editor, session, pos, prefix, callback) { callback(null, (editor.wnako3EditorId !== editorId) ? [] : LanguageFeatures.getSnippets(editor.session.doc.getAllLines().join('\n'))) } }
1459
+ )
1460
+ ace.require('ace/ext/language_tools').setCompleters(completers)
1461
+
1462
+ // オートコンプリートの単語の区切りが日本語に対応していないため、メソッドを上書きして対応させる。
1463
+ // 文字を入力するたびに呼ばれ、''以外を返すとその文字列をもとにしてautocompletionが始まる。
1464
+ ace.require('ace/autocomplete/util').getCompletionPrefix = (/** @type {AceEditor} */ editor) => {
1465
+ const pos = editor.getCursorPosition()
1466
+ return LanguageFeatures.getCompletionPrefix(editor.session.doc.getLine(pos.row).slice(0, pos.column), nako3)
1467
+ }
1468
+
1469
+ // エディタの挙動の設定
1470
+ const languageFeatures = new LanguageFeatures(AceRange, nako3)
1471
+ const oop = ace.require('ace/lib/oop')
1472
+ const TextMode = ace.require('ace/mode/text').Mode
1473
+ const Mode = function () {
1474
+ this.HighlightRules = new TextMode().HighlightRules
1475
+ this.foldingRules = {
1476
+ getFoldWidget: languageFeatures.getFoldWidget.bind(languageFeatures),
1477
+ getFoldWidgetRange: languageFeatures.getFoldWidgetRange.bind(languageFeatures)
1478
+ }
1479
+ }
1480
+ oop.inherits(Mode, TextMode)
1481
+ Mode.prototype.toggleCommentLines = LanguageFeatures.toggleCommentLines.bind(LanguageFeatures)
1482
+ Mode.prototype.getNextLineIndent = LanguageFeatures.getNextLineIndent.bind(LanguageFeatures)
1483
+ Mode.prototype.checkOutdent = LanguageFeatures.checkOutdent.bind(LanguageFeatures)
1484
+ Mode.prototype.autoOutdent = languageFeatures.autoOutdent.bind(languageFeatures)
1485
+ editor.session.setMode(new Mode())
1486
+
1487
+ // tokenizer (シンタックスハイライト)の上書き
1488
+ editor.session.bgTokenizer.stop()
1489
+ editor.session.bgTokenizer = backgroundTokenizer
1490
+
1491
+ editor.setTheme('ace/theme/xcode')
1492
+
1493
+ // 設定メニューの上書き
1494
+ // なでしこ用に上書きした設定の削除やテキストの和訳をする。
1495
+ Options.load(editor)
1496
+ const OptionPanel = ace.require('ace/ext/options').OptionPanel
1497
+ Options.initPanel(OptionPanel, editor)
1498
+
1499
+ // 右下のボタン全体を囲むdiv
1500
+ const buttonContainer = document.createElement('div')
1501
+ buttonContainer.classList.add('button-container')
1502
+ editor.container.appendChild(buttonContainer)
1503
+
1504
+ // 遅い端末へのメッセージのボタン
1505
+ const slowSpeedMessage = document.createElement('span')
1506
+ slowSpeedMessage.classList.add('slow-speed-message')
1507
+ slowSpeedMessage.innerHTML = '<span>エディタの|応答速度が|低下したため|シンタックス|ハイライトを|無効化|しました。</span>'.replace(/\|/g, '</span><span>')
1508
+ buttonContainer.appendChild(slowSpeedMessage)
1509
+
1510
+ // テストの定義の上に「テストを実行」ボタンを表示する
1511
+ /** @type {{ name: 'test', callback: (testName: string | undefined) => void }[]} */
1512
+ const codeLensListeners = []
1513
+ try {
1514
+ const CodeLens = ace.require('ace/ext/code_lens')
1515
+ editor.setOption('enableCodeLens', true)
1516
+ editor.commands.addCommand({
1517
+ name: 'runTest',
1518
+ exec: (/** @type {AceEditor} */editor, /** @type {any[]} */args) => {
1519
+ codeLensListeners
1520
+ .filter((v) => v.name === 'test')
1521
+ .forEach((f) => f.callback(args[0]))
1522
+ }
1523
+ })
1524
+ CodeLens.registerCodeLensProvider(editor, {
1525
+ provideCodeLenses: (/** @type {Session} */session, /** @type {(_: null, arr: CodeLens[]) => void} */callback) => {
1526
+ callback(null, codeLensListeners.some((v) => v.name === 'test') ? LanguageFeatures.getCodeLens(session.doc) : [])
1527
+ }
1528
+ })
1529
+ } catch (e) {
1530
+ console.error(e) // ext/code_lens のscriptタグが読み込まれていない場合など。
1531
+ }
1532
+
1533
+ // 「全画面表示」ボタン
1534
+ const exitFullscreen = () => {
1535
+ editor.container.classList.remove('fullscreen')
1536
+ editor.renderer.setScrollMargin(0, 0, 0, 0) // marginを元に戻す
1537
+ }
1538
+ const fullscreenButton = document.createElement('span')
1539
+ fullscreenButton.classList.add('editor-button')
1540
+ fullscreenButton.innerText = '全画面表示'
1541
+ fullscreenButton.addEventListener('click', (e) => {
1542
+ if (editor.container.classList.contains('fullscreen')) {
1543
+ exitFullscreen()
1544
+ } else {
1545
+ editor.container.classList.add('fullscreen')
1546
+ editor.renderer.setScrollMargin(20, 20, 0, 0) // 上下に少し隙間を開ける
1547
+ }
1548
+ e.preventDefault()
1549
+ })
1550
+ buttonContainer.appendChild(fullscreenButton)
1551
+
1552
+ // 「設定を開く」ボタン
1553
+ const settingsButton = document.createElement('span')
1554
+ settingsButton.classList.add('editor-button')
1555
+ settingsButton.innerText = '設定を開く'
1556
+ settingsButton.addEventListener('click', (e) => {
1557
+ exitFullscreen()
1558
+ editor.execCommand('showSettingsMenu')
1559
+ e.preventDefault()
1560
+ })
1561
+ buttonContainer.appendChild(settingsButton)
1562
+
1563
+ // 複数ファイルの切り替え
1564
+ const UndoManager = ace.require('ace/undomanager').UndoManager
1565
+ const editorTabs = new EditorTabs(editor, AceRange, UndoManager)
1566
+
1567
+ // リサイズ可能にする
1568
+ const resizable = element.dataset.nako3Resizable
1569
+ if (resizable) {
1570
+ new MutationObserver(() => { editor.resize() }).observe(editor.container, { attributes: true })
1571
+ editor.renderer.setScrollMargin(4, 0, 4, 0)
1572
+ editor.container.classList.add('resizable')
1573
+ }
1574
+
1575
+ const retokenize = () => { backgroundTokenizer.dirty = true }
1576
+
1577
+ /**
1578
+ * プログラムを実行して、エラーがあればエディタ上に波線を表示する。出力はoutputContainerに表示する。
1579
+ * methodが'test'のとき、testNameを指定すると1つのテストだけ実行できる。
1580
+ * @param {{
1581
+ * outputContainer?: HTMLElement
1582
+ * file?: string
1583
+ * preCode?: string
1584
+ * localFiles?: Record<string, string>
1585
+ * method?: 'run' | 'test' | 'compile'
1586
+ * testName?: string
1587
+ * }} opts
1588
+ */
1589
+ const run = (opts) => {
1590
+ const code = editor.getValue()
1591
+ const preCode = opts.preCode || '' // プログラムの前に自動的に挿入されるコード
1592
+
1593
+ // loggerを新しいインスタンスに置き換える。そうしないとどのエディタで起きたエラー(や警告や出力)なのかが分からない。
1594
+ const logger = nako3.replaceLogger()
1595
+ if (opts.outputContainer) {
1596
+ const c = opts.outputContainer
1597
+ logger.addListener('info', ({ html }) => {
1598
+ if (!c) { console.log(html) }
1599
+ c.style.display = 'block'
1600
+ c.innerHTML += html
1601
+ })
1602
+ opts.outputContainer.classList.add('nako3-output-container')
1603
+ }
1604
+ let filename = opts.file || 'main.nako3'
1605
+
1606
+ // 警告とエラーをエディタ上に表示する。
1607
+ logger.addListener('info', ({ position, noColor, level }) => {
1608
+ if (position && (position.file === filename && (level === 'warn' || level === 'error'))) {
1609
+ editorMarkers.addByError(code, { ...position, message: noColor }, level)
1610
+ }
1611
+ })
1612
+
1613
+ // 依存ファイルを読み込む。
1614
+ const promise = nako3.loadDependencies(preCode + code, filename, preCode, opts.localFiles || {})
1615
+ .then(() => {
1616
+ // プログラムを実行する。
1617
+ if (!filename) { filename = 'main.nako3' }
1618
+ if (opts.method === 'test') {
1619
+ return nako3.test(preCode + code, filename, preCode, opts.testName)
1620
+ } else if (opts.method === 'compile') {
1621
+ return nako3.compile(preCode + code, filename, false, preCode)
1622
+ } else {
1623
+ const opt = { resetEnv: true, resetAll: true }
1624
+ return nako3.runEx(preCode + code, filename, opt, preCode)
1625
+ }
1626
+ })
1627
+ .catch((err) => {
1628
+ // エラーはloggerに送られるため何もしなくて良い
1629
+ // しかし念のため console.error で出力
1630
+ console.error('[wnako3_editor]', err)
1631
+ })
1632
+ .then(async (res) => {
1633
+ // 読み込んだ依存ファイルの情報を使って再度シンタックスハイライトする。
1634
+ retokenize()
1635
+ // シンタックスハイライトが終わるのを待つ
1636
+ while (backgroundTokenizer.dirty) {
1637
+ await new Promise((resolve) => setTimeout(resolve, 0))
1638
+ }
1639
+ return res
1640
+ })
1641
+ .catch((err) => {
1642
+ console.error('[wnako3_editor::run::promise::catch]', err)
1643
+ })
1644
+
1645
+ return { promise, logger, code }
1646
+ }
1647
+
1648
+ return { editor, editorMarkers, editorTabs, retokenize, run, codeLensListeners }
1649
+ }
1650
+
1651
+ export default {
1652
+ tokenize,
1653
+ setupEditor,
1654
+ LanguageFeatures,
1655
+ EditorMarkers,
1656
+ BackgroundTokenizer
1657
+ }
1658
+