nadesiko3 3.3.45 → 3.3.49

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 (329) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +143 -132
  3. package/batch/browsers.template.md +3 -3
  4. package/batch/build_browsers.nako3 +72 -72
  5. package/batch/build_command.nako3 +44 -44
  6. package/batch/build_nako_version.nako3 +42 -42
  7. package/batch/calc_hash.nako3 +29 -29
  8. package/batch/cmd_txt2json.nako3 +74 -74
  9. package/batch/command.txt +270 -338
  10. package/batch/command_nakopad.txt +9 -69
  11. package/batch/download-extlib.nako3 +43 -43
  12. package/batch/gen_command_nakopad.nako3 +57 -57
  13. package/batch/jsplugin2text.nako3 +285 -285
  14. package/batch/pickup_command.nako3 +110 -110
  15. package/batch/pickup_reserved_words.nako3 +11 -11
  16. package/batch/publish_version.nako3 +46 -46
  17. package/batch/show_agents.js +14 -14
  18. package/batch/turtle2js.nako3 +21 -21
  19. package/bin/cnako3 +10 -10
  20. package/core/.editorconfig +6 -0
  21. package/core/.eslintrc.cjs +33 -0
  22. package/core/.github/dependabot.yml +7 -0
  23. package/core/.github/workflows/nodejs.yml +37 -0
  24. package/core/.github/workflows/super-linter.yml +61 -0
  25. package/core/.github/workflows/textlint.yml +199 -0
  26. package/core/LICENSE +21 -0
  27. package/core/README.md +66 -0
  28. package/core/batch/build_nako_version.nako3 +42 -0
  29. package/core/command/snako.mjs +105 -0
  30. package/core/command/snako.mts +116 -0
  31. package/core/index.mjs +21 -0
  32. package/core/index.mts +21 -0
  33. package/core/package.json +47 -0
  34. package/core/sample/hello.nako3 +7 -0
  35. package/core/sample/hoge.mjs +4 -0
  36. package/core/sample/hoge.mts +6 -0
  37. package/core/src/nako3.mjs +858 -0
  38. package/core/src/nako3.mts +967 -0
  39. package/core/src/nako_colors.mjs +78 -0
  40. package/core/src/nako_colors.mts +86 -0
  41. package/core/src/nako_core_version.mjs +8 -0
  42. package/core/src/nako_core_version.mts +19 -0
  43. package/core/src/nako_csv.mjs +185 -0
  44. package/core/src/nako_csv.mts +188 -0
  45. package/core/src/nako_errors.mjs +173 -0
  46. package/core/src/nako_errors.mts +197 -0
  47. package/core/src/nako_from_dncl.mjs +255 -0
  48. package/core/src/nako_from_dncl.mts +250 -0
  49. package/core/src/nako_gen.mjs +1648 -0
  50. package/core/src/nako_gen.mts +1719 -0
  51. package/core/src/nako_gen_async.mjs +1659 -0
  52. package/core/src/nako_gen_async.mts +1732 -0
  53. package/core/src/nako_global.mjs +107 -0
  54. package/core/src/nako_global.mts +138 -0
  55. package/core/src/nako_indent.mjs +445 -0
  56. package/core/src/nako_indent.mts +492 -0
  57. package/core/src/nako_josi_list.mjs +38 -0
  58. package/core/src/nako_josi_list.mts +45 -0
  59. package/core/src/nako_lex_rules.mjs +253 -0
  60. package/core/src/nako_lex_rules.mts +260 -0
  61. package/core/src/nako_lexer.mjs +609 -0
  62. package/core/src/nako_lexer.mts +612 -0
  63. package/core/src/nako_logger.mjs +199 -0
  64. package/core/src/nako_logger.mts +232 -0
  65. package/core/src/nako_parser3.mjs +2439 -0
  66. package/core/src/nako_parser3.mts +2195 -0
  67. package/core/src/nako_parser_base.mjs +370 -0
  68. package/core/src/nako_parser_base.mts +370 -0
  69. package/core/src/nako_parser_const.mjs +37 -0
  70. package/core/src/nako_parser_const.mts +37 -0
  71. package/core/src/nako_prepare.mjs +304 -0
  72. package/core/src/nako_prepare.mts +315 -0
  73. package/core/src/nako_reserved_words.mjs +38 -0
  74. package/core/src/nako_reserved_words.mts +38 -0
  75. package/core/src/nako_source_mapping.mjs +207 -0
  76. package/core/src/nako_source_mapping.mts +262 -0
  77. package/core/src/nako_test.mjs +37 -0
  78. package/core/src/nako_types.mjs +25 -0
  79. package/core/src/nako_types.mts +151 -0
  80. package/core/src/plugin_csv.mjs +49 -0
  81. package/core/src/plugin_csv.mts +50 -0
  82. package/core/src/plugin_math.mjs +328 -0
  83. package/core/src/plugin_math.mts +326 -0
  84. package/core/src/plugin_promise.mjs +91 -0
  85. package/core/src/plugin_promise.mts +91 -0
  86. package/core/src/plugin_system.mjs +2832 -0
  87. package/core/src/plugin_system.mts +2690 -0
  88. package/core/src/plugin_test.mjs +34 -0
  89. package/core/src/plugin_test.mts +34 -0
  90. package/core/test/array_test.mjs +34 -0
  91. package/core/test/basic_test.mjs +344 -0
  92. package/core/test/calc_test.mjs +140 -0
  93. package/core/test/core_module_test.mjs +23 -0
  94. package/core/test/debug_test.mjs +16 -0
  95. package/core/test/dncl_test.mjs +94 -0
  96. package/core/test/error_message_test.mjs +210 -0
  97. package/core/test/error_test.mjs +16 -0
  98. package/core/test/flow_test.mjs +373 -0
  99. package/core/test/func_call.mjs +160 -0
  100. package/core/test/func_test.mjs +149 -0
  101. package/core/test/indent_test.mjs +364 -0
  102. package/core/test/lex_test.mjs +168 -0
  103. package/core/test/literal_test.mjs +73 -0
  104. package/core/test/nako_lexer_test.mjs +35 -0
  105. package/core/test/nako_logger_test.mjs +76 -0
  106. package/core/test/nako_logger_test.mts +78 -0
  107. package/core/test/plugin_csv_test.mjs +38 -0
  108. package/core/test/plugin_promise_test.mjs +18 -0
  109. package/core/test/plugin_system_test.mjs +630 -0
  110. package/core/test/prepare_test.mjs +96 -0
  111. package/core/test/re_test.mjs +22 -0
  112. package/core/test/side_effects_test.mjs +92 -0
  113. package/core/test/variable_scope_test.mjs +149 -0
  114. package/core/tsconfig.json +101 -0
  115. package/demo/ace_editor.html +89 -89
  116. package/demo/ace_editor_tabs.html +161 -161
  117. package/demo/basic.html +71 -71
  118. package/demo/browsers.html +9 -10
  119. package/demo/css/basic.css +3 -3
  120. package/demo/css/common.css +157 -157
  121. package/demo/css/editor.css +8 -8
  122. package/demo/css/flow.css +3 -3
  123. package/demo/css/index.css +3 -3
  124. package/demo/flow.html +98 -98
  125. package/demo/graph.html +53 -53
  126. package/demo/image/nakopad-icon256.png +0 -0
  127. package/demo/index.html +133 -133
  128. package/demo/js/common.js +17 -17
  129. package/demo/js/turtle3d_test.js +44 -44
  130. package/demo/js/turtle_test.js +45 -45
  131. package/demo/nako3/calc.nako3 +4 -4
  132. package/demo/runscript.html +47 -47
  133. package/demo/runscript2.html +33 -33
  134. package/demo/runscript3.html +35 -35
  135. package/demo/runscript4.html +33 -33
  136. package/demo/turtle.html +58 -58
  137. package/demo/turtle2.html +141 -141
  138. package/demo/turtle3.html +279 -279
  139. package/demo/turtle3d.html +58 -58
  140. package/demo/turtle3d2.html +107 -107
  141. package/demo/version.html +24 -24
  142. package/doc/SETUP.md +157 -157
  143. package/doc/about.md +17 -17
  144. package/doc/browsers.md +26 -26
  145. package/doc/docgen.md +21 -21
  146. package/doc/editor.md +44 -44
  147. package/doc/files.md +39 -39
  148. package/doc/plugins.md +234 -234
  149. package/doc/release.md +79 -79
  150. package/doc/textlint.md +43 -43
  151. package/doc/win32.md +57 -57
  152. package/package.json +195 -192
  153. package/release/_hash.txt +28 -28
  154. package/release/_script-tags.txt +14 -14
  155. package/release/command.json +1 -1
  156. package/release/command.json.js +1 -1
  157. package/release/command_cnako3.json +1 -1
  158. package/release/command_list.json +1 -1
  159. package/release/editor.js +1 -1
  160. package/release/nako_gen_async.js +1 -1
  161. package/release/plugin_csv.js +1 -1
  162. package/release/stats.json +1 -1
  163. package/release/version.js +1 -1
  164. package/release/wnako3.js +1 -1
  165. package/release/wnako3webworker.js +1 -1
  166. package/src/browsers.txt +11 -12
  167. package/src/browsers_agents.json +2 -2
  168. package/src/browsers_agents.mjs +1 -1
  169. package/src/cnako3.mjs +17 -10
  170. package/src/cnako3.mts +18 -12
  171. package/src/cnako3mod.mjs +707 -687
  172. package/src/cnako3mod.mts +712 -696
  173. package/src/commander_ja.mjs +164 -164
  174. package/src/commander_ja.mts +161 -161
  175. package/src/enako3.mjs +68 -69
  176. package/src/era.mjs +22 -22
  177. package/src/image_turtle-elephant.mjs +2 -5
  178. package/src/image_turtle-panda.mjs +2 -5
  179. package/src/image_turtle64.mjs +2 -5
  180. package/src/index.mjs +9 -9
  181. package/src/index.mts +10 -11
  182. package/src/nako3editorfix.sfd +106 -106
  183. package/src/nako_version.mjs +8 -8
  184. package/src/nako_version.mts +2 -2
  185. package/src/plugin_browser.mjs +213 -212
  186. package/src/plugin_browser.mts +206 -205
  187. package/src/plugin_browser_ajax.mjs +399 -399
  188. package/src/plugin_browser_audio.mjs +109 -109
  189. package/src/plugin_browser_canvas.mjs +449 -449
  190. package/src/plugin_browser_chart.mjs +294 -294
  191. package/src/plugin_browser_color.mjs +49 -49
  192. package/src/plugin_browser_crypto.mjs +26 -26
  193. package/src/plugin_browser_dialog.mjs +53 -53
  194. package/src/plugin_browser_dom_basic.mjs +336 -336
  195. package/src/plugin_browser_dom_event.mjs +193 -193
  196. package/src/plugin_browser_dom_parts.mjs +396 -396
  197. package/src/plugin_browser_geolocation.mjs +51 -51
  198. package/src/plugin_browser_hotkey.mjs +25 -25
  199. package/src/plugin_browser_html.mjs +59 -59
  200. package/src/plugin_browser_in_worker.mjs +45 -45
  201. package/src/plugin_browser_location.mjs +21 -21
  202. package/src/plugin_browser_speech.mjs +111 -111
  203. package/src/plugin_browser_storage.mjs +121 -121
  204. package/src/plugin_browser_system.mjs +31 -31
  205. package/src/plugin_browser_websocket.mjs +73 -73
  206. package/src/plugin_caniuse.mjs +29 -29
  207. package/src/plugin_datetime.mjs +394 -394
  208. package/src/plugin_httpserver.mjs +277 -0
  209. package/src/plugin_httpserver.mts +286 -0
  210. package/src/plugin_kansuji.mjs +224 -224
  211. package/src/plugin_keigo.mjs +55 -55
  212. package/src/plugin_markup.mjs +32 -32
  213. package/src/plugin_node.mjs +1047 -1047
  214. package/src/plugin_node.mts +980 -980
  215. package/src/plugin_turtle.mjs +647 -647
  216. package/src/plugin_webworker.mjs +334 -334
  217. package/src/plugin_weykturtle3d.mjs +1214 -1214
  218. package/src/plugin_worker.mjs +95 -95
  219. package/src/repl.nako3 +63 -63
  220. package/src/wnako3.mjs +12 -12
  221. package/src/wnako3.mts +11 -11
  222. package/src/wnako3_editor.css +215 -215
  223. package/src/wnako3_editor.mjs +1542 -1542
  224. package/src/wnako3_editor.mts +1657 -1656
  225. package/src/wnako3mod.mjs +213 -213
  226. package/src/wnako3mod.mts +214 -214
  227. package/src/wnako3webworker.mjs +69 -68
  228. package/test/ace_editor/karma.config.js +94 -94
  229. package/test/ace_editor/test/.babelrc.json +3 -3
  230. package/test/ace_editor/test/ace_editor_test.js +178 -178
  231. package/test/ace_editor/test/html/custom_context.html +139 -139
  232. package/test/async/async_basic_test.mjs +122 -122
  233. package/test/browser/karma.config.js +221 -221
  234. package/test/browser/test/.babelrc.json +3 -3
  235. package/test/browser/test/compare_util.js +50 -50
  236. package/test/browser/test/html/div_basic.html +2 -2
  237. package/test/browser/test/html/event_dom_form.html +4 -4
  238. package/test/browser/test/html/event_dom_scrolldiv.html +5 -5
  239. package/test/browser/test/import_plugin_checker.js +24 -24
  240. package/test/browser/test/plugin_browser_test.js +51 -51
  241. package/test/browser/test/plugin_browser_test_ajax.js +123 -123
  242. package/test/browser/test/plugin_browser_test_color.js +18 -18
  243. package/test/browser/test/plugin_browser_test_dialog.js +72 -72
  244. package/test/browser/test/plugin_browser_test_dom_event.js +598 -598
  245. package/test/browser/test/plugin_browser_test_dom_parts.js +125 -125
  246. package/test/browser/test/plugin_browser_test_system.js +9 -9
  247. package/test/browser/test/plugin_turtle_test.js +817 -817
  248. package/test/browser/test/plugin_webworker_test.js +86 -86
  249. package/test/browser/test/require_test.js +68 -68
  250. package/test/bundled/karma.config.base.js +117 -117
  251. package/test/bundled/karma.config.js +86 -86
  252. package/test/bundled/test/.babelrc.json +3 -3
  253. package/test/bundled/test/bundled_test.js +69 -69
  254. package/test/bundled/test/html/custom_context.html +65 -65
  255. package/test/bundled/test/html/custom_debug.html +66 -66
  256. package/test/bundled/test4b.cmd +52 -52
  257. package/test/bundled/test_base/.babelrc.json +3 -3
  258. package/test/bundled/test_base/_checktool_test.js +25 -25
  259. package/test/bundled/test_base/basic_ajax_test.js +56 -56
  260. package/test/bundled/test_base/basic_async_test.js +18 -18
  261. package/test/bundled/test_base/basic_test.js +153 -153
  262. package/test/bundled/test_base/calc_test.js +132 -132
  263. package/test/bundled/test_base/css/browsers_box.css +114 -114
  264. package/test/bundled/test_base/html/custom_context.html +69 -69
  265. package/test/bundled/test_base/html/custom_debug.html +71 -71
  266. package/test/bundled/test_base/js/browsers_box.js +72 -72
  267. package/test/bundled/test_base/plugin_csv_test.js +37 -37
  268. package/test/bundled/test_base/plugin_datetime_test.js +115 -115
  269. package/test/bundled/test_base/plugin_kansuji_test.js +49 -49
  270. package/test/bundled/test_base/plugin_system_test.js +410 -410
  271. package/test/bundled/test_base/plugin_webworker_test.js +53 -53
  272. package/test/bundled/test_base/test_utils.js +191 -191
  273. package/test/common/plugin_browser_test.mjs +22 -24
  274. package/test/common/plugin_browser_ut_audio_test.mjs +108 -108
  275. package/test/common/plugin_browser_ut_color_test.mjs +21 -21
  276. package/test/common/plugin_browser_ut_dialog_test.mjs +100 -100
  277. package/test/common/plugin_browser_ut_html_test.mjs +13 -13
  278. package/test/common/plugin_browser_ut_system_test.mjs +10 -10
  279. package/test/common/plugin_markup_test.mjs +23 -23
  280. package/test/jsconfig.json +19 -19
  281. package/test/karma.config.js +91 -91
  282. package/test/node/async_test.mjs +96 -82
  283. package/test/node/commander_ja_test.mjs +89 -89
  284. package/test/node/error_message_test.mjs +243 -253
  285. package/test/node/kai_test.nako3 +6 -6
  286. package/test/node/node_test.mjs +60 -60
  287. package/test/node/plugin_broken.js.txt +3 -3
  288. package/test/node/plugin_browser_ut_ajax_test.mjs.todo +357 -357
  289. package/test/node/plugin_browser_ut_location_test.mjs +42 -42
  290. package/test/node/plugin_markup_test.mjs +47 -46
  291. package/test/node/plugin_math_test.mjs +45 -44
  292. package/test/node/plugin_node_test.mjs +98 -97
  293. package/test/node/plugin_test.mjs +44 -43
  294. package/test/node/relative_import_test_1.nako3 +1 -1
  295. package/test/node/relative_import_test_2.nako3 +2 -2
  296. package/test/node/require_nako3_test.mjs +67 -66
  297. package/test/node/requiretest.nako3 +4 -4
  298. package/test/node/requiretest_indirect.nako3 +1 -1
  299. package/test/node/requiretest_name.nako3 +5 -5
  300. package/test/node/runtime_error.nako3 +2 -2
  301. package/test/node/scope1.nako3 +10 -10
  302. package/test/node/scope2.nako3 +12 -12
  303. package/test/node/side_effects_test.mjs +39 -119
  304. package/test/node/sjis.txt +5 -5
  305. package/test/node/syntax_error.nako3 +1 -1
  306. package/test/node/wnako3_editor_test.mjs +384 -384
  307. package/tools/README.md +12 -7
  308. package/tools/check_new_version.nako3 +25 -25
  309. package/tools/nako3edit/html/daisyui/LICENSE +22 -22
  310. package/tools/nako3edit/html/daisyui/version_2.14.1 +1 -1
  311. package/tools/nako3edit/html/edit.html +170 -170
  312. package/tools/nako3edit/html/edit_plugin.js +6 -6
  313. package/tools/nako3edit/html/files.html +125 -125
  314. package/tools/nako3edit/html/nako3edit.css +65 -65
  315. package/tools/nako3edit/index.mjs +248 -244
  316. package/tools/nako3server/index.html +10 -0
  317. package/tools/nako3server/index.mjs +116 -116
  318. package/tools/nako3server/index.nako3 +34 -0
  319. package/release/nako_gen_async.js.LICENSE.txt +0 -35
  320. package/release/plugin_caniuse.js.LICENSE.txt +0 -11
  321. package/release/plugin_csv.js.LICENSE.txt +0 -15
  322. package/release/plugin_datetime.js.LICENSE.txt +0 -15
  323. package/release/plugin_kansuji.js.LICENSE.txt +0 -3
  324. package/release/plugin_markup.js.LICENSE.txt +0 -11
  325. package/release/plugin_turtle.js.LICENSE.txt +0 -15
  326. package/release/plugin_webworker.js.LICENSE.txt +0 -3
  327. package/release/plugin_weykturtle3d.js.LICENSE.txt +0 -3
  328. package/release/wnako3webworker.js.LICENSE.txt +0 -131
  329. package/tools/nako3edit/a.sqlite3 +0 -0
@@ -1,1656 +1,1657 @@
1
- // @ts-nocheck
2
- /* eslint-disable no-template-curly-in-string */
3
- /** なでしこのtokenのtypeをscope(CSSのクラス名)に変換する。 */
4
-
5
- import { OffsetToLineColumn } from 'nadesiko3core/src/nako_source_mapping.mjs'
6
- import { NakoError } from 'nadesiko3core/src/nako_errors.mjs'
7
- import NakoIndent from 'nadesiko3core/src/nako_indent.mjs'
8
- import { NakoPrepare } from 'nadesiko3core/src/nako_prepare.mjs'
9
- import { NakoLogger } from 'nadesiko3core/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
- }
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
+