├── .eslintrc.json ├── .github └── workflows │ ├── main.yml │ └── publish.yaml ├── .gitignore ├── .vscode ├── JsonSchema.code-snippets ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── media └── rime.png ├── package-lock.json ├── package.json ├── resources ├── dark │ ├── actions │ │ ├── open.png │ │ ├── openFolder.png │ │ └── showAll.png │ ├── configKind │ │ ├── default-patch.png │ │ ├── default.png │ │ ├── folder.png │ │ ├── other.png │ │ ├── patch.png │ │ ├── program.png │ │ ├── schema-node-patch.png │ │ ├── schema-node.png │ │ └── schema.png │ └── refresh.png ├── documentation │ └── screencast │ │ ├── fileExplorer.gif │ │ ├── languageServer-syntaxValidation.gif │ │ ├── nodeExplorer-hiearchy.gif │ │ ├── nodeExplorer-navigation.gif │ │ └── nodeExplorer-schemaTooltip.gif ├── light │ ├── actions │ │ ├── open.png │ │ ├── openFolder.png │ │ └── showAll.png │ ├── configKind │ │ ├── default-patch.png │ │ ├── default.png │ │ ├── folder.png │ │ ├── other.png │ │ ├── patch.png │ │ ├── program.png │ │ ├── schema-node-patch.png │ │ ├── schema-node.png │ │ └── schema.png │ └── refresh.png └── schema │ ├── rime-default-yaml-schema.json │ └── rime-schema-yaml-schema.json ├── server ├── package-lock.json ├── package.json ├── src │ ├── RimeLanguageService.ts │ └── server.ts ├── tsconfig.json └── tsconfig.tsbuildinfo ├── src ├── RimeConfigurationTree.ts ├── RimeFileExplorerProvider.ts ├── RimeNodeExplorerProvider.ts ├── extension.ts └── test │ ├── runTest.ts │ └── suite │ ├── extension.test.ts │ └── index.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/class-name-casing": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build-test: 14 | strategy: 15 | matrix: 16 | os: [macos-latest, ubuntu-latest, windows-latest] 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | 22 | - name: Install Node.js 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: 10.x 26 | 27 | - name: Install Dependencies 28 | run: npm install && (cd server && npm install) 29 | 30 | - name: Language Server Build 31 | run: npm run server-compile 32 | 33 | - name: Build 34 | run: npm run esbuild 35 | 36 | - run: xvfb-run -a npm test 37 | if: runner.os == 'Linux' 38 | - run: npm test 39 | if: runner.os != 'Linux' -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build-test: 13 | strategy: 14 | matrix: 15 | os: [macos-latest, ubuntu-latest, windows-latest] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | 21 | - name: Install Node.js 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: 10.x 25 | 26 | - name: Install Dependencies 27 | run: npm install && (cd server && npm install) 28 | 29 | - name: Build 30 | run: npm run esbuild 31 | 32 | - run: xvfb-run -a npm test 33 | if: runner.os == 'Linux' 34 | - run: npm test 35 | if: runner.os != 'Linux' 36 | 37 | - name: Publish Extension 38 | if: success() && startsWith( github.ref, 'refs/tags/releases/') && matrix.os == 'ubuntu-latest' 39 | run: npm run deploy 40 | env: 41 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /.vscode/JsonSchema.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | // Place your rimebow-vscode workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and 3 | // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope 4 | // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is 5 | // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: 6 | // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. 7 | // Placeholders with the same ids are connected. 8 | // Example: 9 | // "Print to console": { 10 | // "scope": "javascript,typescript", 11 | // "prefix": "log", 12 | // "body": [ 13 | // "console.log('$1');", 14 | // "$2" 15 | // ], 16 | // "description": "Log output to console" 17 | // } 18 | "Insert schema for a string": { 19 | "scope": "json", 20 | "prefix": "ss", 21 | "description": "Insert schema definition for a string.", 22 | "body": [ 23 | "\"${1:name}\": {", 24 | " \"type\": \"string\",", 25 | " \"description\": \"${2:description}\"", 26 | "}" 27 | ] 28 | }, "Insert schema for an object": { 29 | "scope": "json", 30 | "prefix": "so", 31 | "description": "Insert schema definition for an object.", 32 | "body": [ 33 | "\"${1|name}\": {", 34 | " \"description\": \"${2:description}\",", 35 | " \"type\": \"object\",", 36 | " \"additionalProperties\": ${3:(true|false)},", 37 | " \"properties\": {", 38 | " }", 39 | "}" 40 | ] 41 | }, "Insert schema for an array": { 42 | "scope": "json", 43 | "prefix": "sa", 44 | "description": "Insert schema definition for an array.", 45 | "body": [ 46 | "\"${1|name}\": {", 47 | " \"description\": \"${2:description}\",", 48 | " \"type\": \"array\",", 49 | " \"additionalItems\": ${3:(true|false)},", 50 | " \"items\": {", 51 | " \"type\": \"${4:itemType}\"", 52 | " \"description\": \"${5:itemDescription}\"", 53 | " }", 54 | "}" 55 | ] 56 | } 57 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--disable-extensions", 15 | "--extensionDevelopmentPath=${workspaceFolder}" 16 | ], 17 | "outFiles": [ 18 | "${workspaceFolder}/out/**/*.js" 19 | ], 20 | }, 21 | { 22 | "name": "Extension Tests", 23 | "type": "extensionHost", 24 | "request": "launch", 25 | "sourceMaps": true, 26 | "runtimeExecutable": "${execPath}", 27 | "args": [ 28 | "--disable-extensions", 29 | "--extensionDevelopmentPath=${workspaceFolder}", 30 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 31 | ], 32 | "internalConsoleOptions": "openOnFirstSessionStart", 33 | "outFiles": [ 34 | "${workspaceFolder}/out/**/*.js", 35 | "${workspaceFolder}/out/test/**/*.js" 36 | ], 37 | "preLaunchTask": "npm: test-compile" 38 | }, 39 | { 40 | "type": "node", 41 | "request": "attach", 42 | "name": "Attach to Server", 43 | "port": 6009, 44 | "restart": true, 45 | "outFiles": ["${workspaceRoot}/server/out/**/*.js"] 46 | }, 47 | ], 48 | "compounds": [ 49 | { 50 | "name": "Client + Server", 51 | "configurations": ["Run Extension", "Attach to Server"] 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | "mochaExplorer.files": "out/test/suite/*.test.js", 10 | "mochaExplorer.ui": "tdd", 11 | "mochaExplorer.launcherScript": "node_modules/mocha-explorer-launcher-scripts/vscode-test", 12 | "mochaExplorer.autoload": false, 13 | "mochaExplorer.ipcRole": "server", 14 | "mochaExplorer.env": { 15 | "VSCODE_VERSION": "insiders", 16 | "ELECTRON_RUN_AS_NODE": null 17 | }, 18 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 19 | "typescript.tsc.autoDetect": "off", 20 | "workbench.colorCustomizations": { 21 | "activityBar.activeBackground": "#2f7c47", 22 | "activityBar.activeBorder": "#422c74", 23 | "activityBar.background": "#2f7c47", 24 | "activityBar.foreground": "#e7e7e7", 25 | "activityBar.inactiveForeground": "#e7e7e799", 26 | "activityBarBadge.background": "#422c74", 27 | "activityBarBadge.foreground": "#e7e7e7", 28 | "statusBar.background": "#215732", 29 | "statusBar.foreground": "#e7e7e7", 30 | "statusBarItem.hoverBackground": "#2f7c47", 31 | "titleBar.activeBackground": "#215732", 32 | "titleBar.activeForeground": "#e7e7e7", 33 | "titleBar.inactiveBackground": "#21573299", 34 | "titleBar.inactiveForeground": "#e7e7e799" 35 | }, 36 | "peacock.color": "#215732", 37 | "git.ignoreLimitWarning": true 38 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "esbuild-watch", 9 | "isBackground": true, 10 | "group": { 11 | "kind": "build", 12 | "isDefault": true 13 | }, 14 | "presentation": { 15 | "panel": "dedicated", 16 | "reveal": "never" 17 | }, 18 | "problemMatcher": [ 19 | "$tsc-watch" 20 | ] 21 | }, 22 | { 23 | "type": "npm", 24 | "script": "esbuild", 25 | "problemMatcher": [ 26 | "$eslint-compact" 27 | ], 28 | "group": "build", 29 | "label": "npm: esbuild" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | !out/main.js 4 | !server/out/*.js 5 | out/test/** 6 | src/** 7 | .gitignore 8 | vsc-extension-quickstart.md 9 | **/tsconfig.json 10 | **/.eslintrc.json 11 | **/*.map 12 | **/*.ts 13 | node_modules -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 更新日志 (Release Notes) 2 | ### 0.1.0 3 | * 配置文件浏览器 4 | * 配置结点浏览器 5 | 6 | ### 0.2.0 7 | * RIME language server -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 参与 2 | Rimebow 还不是一个完善的产品,如果你有兴趣,无论是提 issue、改 bug、加 feature、完善文档,我们都欢迎你来参与到这个项目中来,一起让 RIME 社区变得更好! 3 | 4 | 需要注意的是,在提 issue 之前,请先打开 Github issues 面板搜索一下,是否已经存在相关讨论,避免重复劳动。同样地,在捋起袖子准备大改一番代码前,也请看看是否已经有人发了相关的 pull request,或是在 issue 进行过讨论,尽量避免付出不必要的劳动。 5 | 6 | ## 调试和运行 7 | 如果你需要对本插件进行调试开发,或者有兴趣了解本插件的实现细节,可以 8 | 1. Fork 并 clone 本仓库 9 | 2. 在源代码根目录执行: 10 | ```shell 11 | npm install 12 | cd server/ && npm install && cd - 13 | npm watch 14 | ``` 15 | (你也可以直接在 VS Code 中 Ctrl/Command-Shift-P 调出 Command Pallete -> Tasks: Run Task 来执行这两个 npm 命令) 16 | 2. 通过 VS Code 打开源代码根目录 17 | 3. 修改代码或者添加断点 18 | 4. 调用 VS Code 的 Run -> Start Debugging 或 Run without Debugging 命令启动 Extension Development Host(另一个 VS Code 窗口) 19 | 5. 在新打开的 Extension Development Host 窗口中,你可以在左边的侧边栏中找到 Rimebow 插件 20 | 21 | 请阅读[官方文档](https://code.visualstudio.com/api)获取更多 VS Code 插件开发相关知识。 22 | 23 | ## 单元测试 24 | 提交代码前,请确保添加了相关的单元测试用例进行测试覆盖。 -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 元宝 Rimebow 2 | 元宝(Rimebow),你的一站式 RIME 配置管理助手。 3 | 4 | [![version](https://badgen.net/vs-marketplace/v/MengqiPei.rimebow?color=green)](https://marketplace.visualstudio.com/items?itemName=MengqiPei.rimebow) 5 | ![CI Status](https://github.com/mengqi92/Rimebow/actions/workflows/main.yml/badge.svg) 6 | 7 | ## 核心功能 8 | 9 | ### RIME 文件浏览器; 10 | - 一个界面浏览多处配置:无需来回切换程序配置目录和用户配置目录; 11 | ![文件浏览器](https://raw.githubusercontent.com/mengqi92/Rimebow/master/resources/documentation/screencast/fileExplorer.gif) 12 | ### RIME 结点浏览器; 13 | - 合并显示用户自定义配置与系统配置:哪些配置项打了补丁,打的补丁有没有生效,一目了然; 14 | ![结点浏览器](https://raw.githubusercontent.com/mengqi92/Rimebow/master/resources/documentation/screencast/nodeExplorer-hiearchy.gif) 15 | - 配置项来源标识:系统设置(default.yaml)、自定义系统设置(default.custom.yaml)、配置方案(foo.schema.yaml)、补丁(foo.custom.yaml)分别由不同图标显示,当前生效的配置项来自哪个文件,一看便知; 16 | - 配置项跳转:任意配置项结点,都可以一键跳转到其在源文件中的位置,方便后续编辑; 17 | ![配置结点定向](https://raw.githubusercontent.com/mengqi92/Rimebow/master/resources/documentation/screencast/nodeExplorer-navigation.gif) 18 | - 配置方案元信息显示:不用打开文件即可查看作者、版本等元信息; 19 | ![配置方案元信息](https://raw.githubusercontent.com/mengqi92/Rimebow/master/resources/documentation/screencast/nodeExplorer-schemaTooltip.gif) 20 | 21 | ### RIME Language server 22 | 借助 language server 和 Yaml schema,VS Code 可以对配置方案进行配置项的提示和语法检查。 23 | 24 | **注:该特性目前还在开发中,尚未完成,目前仅支持配置方案中的一部分配置项。** 25 | 26 | ![配置方案语法提示](https://raw.githubusercontent.com/mengqi92/Rimebow/master/resources/documentation/screencast/languageServer-syntaxValidation.gif) 27 | 28 | ## 插件的配置 29 | 本插件提供以下 VS Code 配置项,你可以在 VS Code Settings 中搜索以下配置项进行调整: 30 | ### `rimebow.userConfigDir`: RIME 用户配置文件所在目录 31 | * Windows 下(小狼毫)默认采用 `C:/Users/Foo/AppData/Roaming/Rime` 32 | * macOS 下(鼠须管)默认采用 `/Users/Library/Rime` 33 | * Linux 下 ibus-rime 默认采用 `~/.config/ibus/rime`,fcitx-rime 默认采用 `~/.config/fcitx/rime` 34 | ### `rimebow.defaultConfigDir`: RIME 程序配置文件所在目录 35 | * Windows 下(小狼毫)默认采用 `C:/Program Files (x86)/Rime/weasel-x.xx.x/data`,(当存在多个 weasel-x.xx.x 目录时,取最近修改过的目录作为当前程序目录) 36 | * macOS 下(鼠须管)默认采用 `/Library/Input Methods/Squirrel.app/Contents/SharedSupport/` 37 | * Linux 下(ibus-rime/fcitx-rime)默认采用 `/usr/share/rime-data` 38 | 39 | ## 鸣谢 40 | 本插件 Schema 文件的撰写翻译自 [LEO Yoon-Tsaw](https://github.com/LEOYoon-Tsaw) 关于 RIME 配置文件详尽的说明文档(https://github.com/LEOYoon-Tsaw/Rime_collections/blob/master/Rime_description.md),在此表示感谢! 41 | 42 | ## 开发规划 43 | - [ ] 支持 RIME 自定义编译命令 44 | - [ ] 支持 import_present 45 | - [ ] 繁体中文支持 46 | - [ ] 配置语法校验 47 | - [ ] Rime 配置语法 Language server 以及 schema 48 | - [ ] 在编辑器中显示配色方案颜色 -------------------------------------------------------------------------------- /media/rime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/media/rime.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rimebow", 3 | "displayName": "Rimebow", 4 | "publisher": "MengqiPei", 5 | "description": "An assistant along with your Rime configuration journey.", 6 | "repository": "https://github.com/mengqi92/Rimebow", 7 | "version": "0.2.6", 8 | "engines": { 9 | "vscode": "^1.45.0" 10 | }, 11 | "categories": [ 12 | "Other" 13 | ], 14 | "activationEvents": [ 15 | "onLanguage:yaml", 16 | "onView:rimeFileExplorer", 17 | "onView:rimeNodeExplorer" 18 | ], 19 | "main": "./out/main.js", 20 | "contributes": { 21 | "menus": { 22 | "view/title": [ 23 | { 24 | "command": "rimeFileExplorer.refreshEntry", 25 | "group": "navigation", 26 | "when": "view == rimeFileExplorer" 27 | }, 28 | { 29 | "command": "rimeNodeExplorer.refreshEntry", 30 | "group": "navigation", 31 | "when": "view == rimeNodeExplorer" 32 | }, 33 | { 34 | "command": "rimeNodeExplorer.showOnlySchemaNodes", 35 | "group": "navigation", 36 | "when": "view == rimeNodeExplorer" 37 | }, 38 | { 39 | "command": "rimeNodeExplorer.showAll", 40 | "group": "navigation", 41 | "when": "view == rimeNodeExplorer" 42 | } 43 | ], 44 | "view/item/context": [ 45 | { 46 | "command": "rimebow.openConfigFile", 47 | "group": "inline", 48 | "when": "viewItem == file || viewItem == node" 49 | }, 50 | { 51 | "command": "rimebow.openFolder", 52 | "group": "inline", 53 | "when": "viewItem == folder" 54 | } 55 | ] 56 | }, 57 | "commands": [ 58 | { 59 | "command": "rimeFileExplorer.refreshEntry", 60 | "title": "Refresh", 61 | "icon": { 62 | "light": "resources/light/refresh.png", 63 | "dark": "resources/dark/refresh.png" 64 | } 65 | }, 66 | { 67 | "command": "rimeNodeExplorer.refreshEntry", 68 | "title": "Refresh", 69 | "icon": { 70 | "light": "resources/light/refresh.png", 71 | "dark": "resources/dark/refresh.png" 72 | } 73 | }, 74 | { 75 | "command": "rimeNodeExplorer.showOnlySchemaNodes", 76 | "title": "Schema Only", 77 | "icon": { 78 | "light": "resources/light/configKind/schema.png", 79 | "dark": "resources/dark/configKind/schema.png" 80 | } 81 | }, 82 | { 83 | "command": "rimeNodeExplorer.showAll", 84 | "title": "Show All", 85 | "icon": { 86 | "light": "resources/light/actions/showAll.png", 87 | "dark": "resources/dark/actions/showAll.png" 88 | } 89 | }, 90 | { 91 | "command": "rimebow.openConfigFile", 92 | "title": "Open in Editor", 93 | "icon": { 94 | "light": "resources/light/actions/open.png", 95 | "dark": "resources/dark/actions/open.png" 96 | } 97 | }, 98 | { 99 | "command": "rimebow.openFolder", 100 | "title": "Open Folder in VS Code", 101 | "icon": { 102 | "light": "resources/light/actions/openFolder.png", 103 | "dark": "resources/dark/actions/openFolder.png" 104 | } 105 | } 106 | ], 107 | "views": { 108 | "rimebow": [ 109 | { 110 | "id": "rimeFileExplorer", 111 | "name": "RIME 配置文件" 112 | }, 113 | { 114 | "id": "rimeNodeExplorer", 115 | "name": "RIME 配置结点" 116 | } 117 | ] 118 | }, 119 | "viewsContainers": { 120 | "activitybar": [ 121 | { 122 | "id": "rimebow", 123 | "title": "Rimebow", 124 | "icon": "media/rime.png" 125 | } 126 | ] 127 | }, 128 | "configuration": { 129 | "type": "object", 130 | "title": "Rimebow configuration", 131 | "properties": { 132 | "rimebow.userConfigDir": { 133 | "type": "string", 134 | "default": false, 135 | "description": "The directory containing user defined config files." 136 | }, 137 | "rimebow.defaultConfigDir": { 138 | "type": "string", 139 | "default": false, 140 | "description": "The directory containing default RIME config files." 141 | } 142 | } 143 | } 144 | }, 145 | "scripts": { 146 | "vscode:prepublish": "npm run server-compile && npm run esbuild-base --minify", 147 | "esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=out/main.js --external:vscode --format=cjs --platform=node", 148 | "esbuild": "npm run esbuild-base -- --sourcemap", 149 | "esbuild-watch": "npm run esbuild-base -- --sourcemap --watch", 150 | "test-compile": "tsc -p ./", 151 | "test": "tsc -p ./ && node ./out/test/runTest.js --disable-extensions", 152 | "server-compile": "tsc -p server/", 153 | "deploy": "vsce publish" 154 | }, 155 | "devDependencies": { 156 | "@types/glob": "^7.1.1", 157 | "@types/lodash": "^4.14.158", 158 | "@types/mocha": "^7.0.2", 159 | "@types/node": "^13.11.0", 160 | "@types/vscode": "^1.45.0", 161 | "@typescript-eslint/eslint-plugin": "^2.30.0", 162 | "@typescript-eslint/parser": "^2.30.0", 163 | "esbuild": "^0.12.28", 164 | "eslint": "^6.8.0", 165 | "glob": "^7.1.6", 166 | "mocha": "^10.1.0", 167 | "mocha-explorer-launcher-scripts": "^0.3.0", 168 | "typescript": "^3.8.3", 169 | "vscode-test": "^1.3.0" 170 | }, 171 | "dependencies": { 172 | "lodash": "^4.17.21", 173 | "vscode-languageclient": "^6.1.3", 174 | "yaml-ast-parser": "0.0.43" 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /resources/dark/actions/open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/dark/actions/open.png -------------------------------------------------------------------------------- /resources/dark/actions/openFolder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/dark/actions/openFolder.png -------------------------------------------------------------------------------- /resources/dark/actions/showAll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/dark/actions/showAll.png -------------------------------------------------------------------------------- /resources/dark/configKind/default-patch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/dark/configKind/default-patch.png -------------------------------------------------------------------------------- /resources/dark/configKind/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/dark/configKind/default.png -------------------------------------------------------------------------------- /resources/dark/configKind/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/dark/configKind/folder.png -------------------------------------------------------------------------------- /resources/dark/configKind/other.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/dark/configKind/other.png -------------------------------------------------------------------------------- /resources/dark/configKind/patch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/dark/configKind/patch.png -------------------------------------------------------------------------------- /resources/dark/configKind/program.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/dark/configKind/program.png -------------------------------------------------------------------------------- /resources/dark/configKind/schema-node-patch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/dark/configKind/schema-node-patch.png -------------------------------------------------------------------------------- /resources/dark/configKind/schema-node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/dark/configKind/schema-node.png -------------------------------------------------------------------------------- /resources/dark/configKind/schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/dark/configKind/schema.png -------------------------------------------------------------------------------- /resources/dark/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/dark/refresh.png -------------------------------------------------------------------------------- /resources/documentation/screencast/fileExplorer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/documentation/screencast/fileExplorer.gif -------------------------------------------------------------------------------- /resources/documentation/screencast/languageServer-syntaxValidation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/documentation/screencast/languageServer-syntaxValidation.gif -------------------------------------------------------------------------------- /resources/documentation/screencast/nodeExplorer-hiearchy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/documentation/screencast/nodeExplorer-hiearchy.gif -------------------------------------------------------------------------------- /resources/documentation/screencast/nodeExplorer-navigation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/documentation/screencast/nodeExplorer-navigation.gif -------------------------------------------------------------------------------- /resources/documentation/screencast/nodeExplorer-schemaTooltip.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/documentation/screencast/nodeExplorer-schemaTooltip.gif -------------------------------------------------------------------------------- /resources/light/actions/open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/light/actions/open.png -------------------------------------------------------------------------------- /resources/light/actions/openFolder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/light/actions/openFolder.png -------------------------------------------------------------------------------- /resources/light/actions/showAll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/light/actions/showAll.png -------------------------------------------------------------------------------- /resources/light/configKind/default-patch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/light/configKind/default-patch.png -------------------------------------------------------------------------------- /resources/light/configKind/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/light/configKind/default.png -------------------------------------------------------------------------------- /resources/light/configKind/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/light/configKind/folder.png -------------------------------------------------------------------------------- /resources/light/configKind/other.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/light/configKind/other.png -------------------------------------------------------------------------------- /resources/light/configKind/patch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/light/configKind/patch.png -------------------------------------------------------------------------------- /resources/light/configKind/program.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/light/configKind/program.png -------------------------------------------------------------------------------- /resources/light/configKind/schema-node-patch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/light/configKind/schema-node-patch.png -------------------------------------------------------------------------------- /resources/light/configKind/schema-node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/light/configKind/schema-node.png -------------------------------------------------------------------------------- /resources/light/configKind/schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/light/configKind/schema.png -------------------------------------------------------------------------------- /resources/light/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengqi92/Rimebow/8ef42d50a8c46a061901409d047f4b945b1eaf5a/resources/light/refresh.png -------------------------------------------------------------------------------- /resources/schema/rime-default-yaml-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "$id": "https://d3gyfrtg0yt6hg.cloudfront.net/schema/rime-default-yaml-schema.json", 4 | "title": "Rime Default Configuration", 5 | "description": "A JSON Schema for RIME default.yaml configurations.", 6 | "type": "object", 7 | "definitions": { 8 | "regexString": { 9 | "type": "string", 10 | "format": "regex" 11 | }, 12 | "keyCombination": { 13 | "type": "string" 14 | }, 15 | "switchKeyActions": { 16 | "type": "string", 17 | "enum": [ 18 | "inline_ascii", 19 | "commit_code", 20 | "commit_text", 21 | "noop", 22 | "clear" 23 | ] 24 | }, "compilingCommands": { 25 | "__include": { 26 | "type": "object" 27 | }, "__patch": { 28 | "type": "object" 29 | }, "__append": { 30 | "type": "object" 31 | }, "__merge": { 32 | "type": "object" 33 | } 34 | } 35 | }, 36 | "properties": { 37 | "patch": { 38 | "type": "object", 39 | "properties": { 40 | "schema_list": { 41 | "type": "array", 42 | "items": { 43 | "type": "object", 44 | "properties": { 45 | "schema": { 46 | "type": "string" 47 | } 48 | } 49 | } 50 | }, 51 | "switcher": { 52 | "type": "object", 53 | "properties": { 54 | "caption": { 55 | "type": "string" 56 | }, 57 | "hotkeys": { 58 | "type": "array", 59 | "items": { 60 | "$ref": "#/definitions/keyCombination" 61 | } 62 | }, 63 | "save_options": { 64 | "type": "array", 65 | "items": { 66 | "type": "string" 67 | } 68 | }, 69 | "fold_options": { 70 | "type": "boolean" 71 | }, 72 | "abbreviate_options": { 73 | "type": "boolean" 74 | }, 75 | "option_list_separator": { 76 | "type": "string" 77 | } 78 | } 79 | }, 80 | "menu": { 81 | "type": "object", 82 | "properties": { 83 | "page_size": { 84 | "type": "number" 85 | } 86 | } 87 | }, 88 | "punctuator": { 89 | "type": "object", 90 | "additionalProperties": false, 91 | "properties": { 92 | "full_shape": { 93 | "type": "object" 94 | }, 95 | "half_shape": { 96 | "type": "object" 97 | }, 98 | "use_space": { 99 | "type": "boolean" 100 | }, 101 | "ascii_style": { 102 | "type": "object" 103 | } 104 | } 105 | }, 106 | "key_bindings": { 107 | "type": "array", 108 | "items": { 109 | "anyOf": [ 110 | { 111 | "type": "string", 112 | "enum": [ 113 | "emacs_editing", 114 | "move_by_word_with_tab", 115 | "paging_with_minus_equal", 116 | "paging_with_comma_period", 117 | "numbered_mode_switch" 118 | ] 119 | }, 120 | { 121 | "type": "object", 122 | "additionalProperties": false, 123 | "properties": { 124 | "when": { 125 | "type": "string", 126 | "enum": [ 127 | "paging", 128 | "has_menu", 129 | "composing", 130 | "always" 131 | ] 132 | }, 133 | "accept": { 134 | "$ref": "#/definitions/keyCombination" 135 | }, 136 | "send": { 137 | "$ref": "#/definitions/keyCombination" 138 | }, 139 | "toggle": { 140 | "type": "string", 141 | "enum": [ 142 | ".next", 143 | "ascii_mode", 144 | "ascii_punt", 145 | "full_shape", 146 | "simplification", 147 | "extended_charset" 148 | ] 149 | } 150 | } 151 | } 152 | ] 153 | } 154 | }, 155 | "recognizer": { 156 | "type": "object", 157 | "additionalProperties": false, 158 | "properties": { 159 | "patterns": { 160 | "type": "object", 161 | "properties": { 162 | "email": { 163 | "$ref": "#/definitions/regexString" 164 | }, 165 | "uppercase": { 166 | "$ref": "#/definitions/regexString" 167 | }, 168 | "url": { 169 | "$ref": "#/definitions/regexString" 170 | } 171 | } 172 | } 173 | } 174 | }, 175 | "ascii_composer": { 176 | "type": "object", 177 | "additionalProperties": false, 178 | "properties": { 179 | "good_old_caps_lock": { 180 | "type": "boolean" 181 | }, 182 | "switch_key": { 183 | "type": "object", 184 | "properties": { 185 | "Shift_L": { 186 | "$ref": "#/definitions/switchKeyActions" 187 | }, 188 | "Shift_R": { 189 | "$ref": "#/definitions/switchKeyActions" 190 | }, 191 | "Control_L": { 192 | "$ref": "#/definitions/switchKeyActions" 193 | }, 194 | "Control_R": { 195 | "$ref": "#/definitions/switchKeyActions" 196 | } 197 | } 198 | } 199 | } 200 | } 201 | } 202 | } 203 | } 204 | } -------------------------------------------------------------------------------- /resources/schema/rime-schema-yaml-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "$id": "https://d3gyfrtg0yt6hg.cloudfront.net/schema/rime-schema-yaml-schema.json", 4 | "title": "Rime Schema Configuration", 5 | "description": "A JSON Schema for RIME schema configurations.", 6 | "type": "object", 7 | "definitions": { 8 | "regexString": { 9 | "type": "string", 10 | "format": "regex" 11 | }, 12 | "keyCombination": { 13 | "type": "string" 14 | }, 15 | "switchKeyActions": { 16 | "type": "string", 17 | "enum": [ 18 | "inline_ascii", 19 | "commit_code", 20 | "commit_text", 21 | "noop", 22 | "clear" 23 | ] 24 | }, 25 | "compilingCommands": { 26 | "__include": { 27 | "type": "object" 28 | }, 29 | "__patch": { 30 | "type": "object" 31 | }, 32 | "__append": { 33 | "type": "object" 34 | }, 35 | "__merge": { 36 | "type": "object" 37 | } 38 | } 39 | }, 40 | "required": [ 41 | "schema", 42 | "engine" 43 | ], 44 | "properties": { 45 | "schema": { 46 | "description": "输入方案的元信息。", 47 | "type": "object", 48 | "required": [ 49 | "schema_id" 50 | ], 51 | "additionalProperties": false, 52 | "properties": { 53 | "schema_id": { 54 | "type": "string", 55 | "description": "方案的内部名称。" 56 | }, 57 | "name": { 58 | "type": "string", 59 | "description": "方案的显示名称。" 60 | }, 61 | "version": { 62 | "type": "string", 63 | "description": "方案的版本号。" 64 | }, 65 | "author": { 66 | "type": [ 67 | "string", 68 | "array" 69 | ], 70 | "description": "方案的发明人、撰写者。" 71 | }, 72 | "description": { 73 | "type": "string", 74 | "description": "方案历史、码表来源、方案规则等的简要描述。" 75 | }, 76 | "dependencies": { 77 | "type": [ 78 | "string", 79 | "array" 80 | ], 81 | "description": "所依赖的其它方案。" 82 | } 83 | } 84 | }, 85 | "switches": { 86 | "type": "array", 87 | "description": "输入方案定义的一系列开关。", 88 | "items": { 89 | "additionalItems": false, 90 | "oneOf": [ 91 | { 92 | "type": "object", 93 | "required": [ 94 | "name", 95 | "states" 96 | ], 97 | "additionalProperties": false, 98 | "properties": { 99 | "name": { 100 | "type": "string", 101 | "enum": [ 102 | "ascii_mode", 103 | "full_shape", 104 | "extended_charset", 105 | "ascii_punct", 106 | "simplification", 107 | "gbk" 108 | ], 109 | "description": "定义一个二值开关,其内部名称。" 110 | }, 111 | "states": { 112 | "type": "array", 113 | "items": { 114 | "type": "string" 115 | }, 116 | "description": "开关的各个状态描述。最终会显示在方案选单界面上。" 117 | }, 118 | "reset": { 119 | "type": "integer", 120 | "minimum": 0, 121 | "maximum": 1, 122 | "description": "开关的默认状态。" 123 | } 124 | } 125 | }, 126 | { 127 | "type": "object", 128 | "required": [ 129 | "options", 130 | "states" 131 | ], 132 | "additionalProperties": false, 133 | "properties": { 134 | "options": { 135 | "type": "array", 136 | "items": { 137 | "type": "string" 138 | }, 139 | "description": "定义一个多值开关,其各个选项的内部名称,如 [zh_trad, zh_cn, zh_mars] 将得到三值开关" 140 | }, 141 | "states": { 142 | "type": "array", 143 | "items": { 144 | "type": "string" 145 | }, 146 | "description": "开关的各个状态描述。最终会显示在方案选单界面上。" 147 | }, 148 | "reset": { 149 | "type": "integer", 150 | "minimum": 0, 151 | "maximum": 1, 152 | "description": "开关的默认状态。" 153 | } 154 | } 155 | } 156 | ] 157 | } 158 | }, 159 | "engine": { 160 | "description": "输入引擎设定。即挂接组件的「处方」。", 161 | "type": "object", 162 | "additionalProperties": false, 163 | "properties": { 164 | "processors": { 165 | "type": "array", 166 | "description": "负责处理各类按键消息的组件。", 167 | "items": { 168 | "additionalItems": false, 169 | "oneOf": [ 170 | { 171 | "type": "string", 172 | "enum": [ 173 | "ascii_composer" 174 | ], 175 | "description": "处理西文模式及中西文切换。" 176 | }, 177 | { 178 | "type": "string", 179 | "enum": [ 180 | "recognizer" 181 | ], 182 | "description": "与 segmentors 中的 matcher 搭配,处理符合特定规则的输入码,如网址反查等 tag。" 183 | }, 184 | { 185 | "type": "string", 186 | "enum": [ 187 | "key_binder" 188 | ], 189 | "description": "按键绑定。" 190 | }, 191 | { 192 | "type": "string", 193 | "enum": [ 194 | "speller" 195 | ], 196 | "description": "拼写处理器。" 197 | }, 198 | { 199 | "type": "string", 200 | "enum": [ 201 | "punctuator" 202 | ], 203 | "description": "标点处理器。将单个字符按键直接映射为标点符号或文字。" 204 | }, 205 | { 206 | "type": "string", 207 | "enum": [ 208 | "selector" 209 | ], 210 | "description": "选字处理器。处理数字选字键、上下候选定位、换页等。" 211 | }, 212 | { 213 | "type": "string", 214 | "enum": [ 215 | "navigator" 216 | ], 217 | "description": "处理输入栏内的光标移动。" 218 | }, 219 | { 220 | "type": "string", 221 | "enum": [ 222 | "express_editor" 223 | ], 224 | "description": "编辑器。处理空格、退格、回车上屏等。" 225 | }, 226 | { 227 | "type": "string", 228 | "enum": [ 229 | "fluency_editor" 230 | ], 231 | "description": "句式编辑器。用于以空格断词,回车上屏的输入方案,如「注音」、「语句流」。" 232 | }, 233 | { 234 | "type": "string", 235 | "enum": [ 236 | "chord_composer" 237 | ], 238 | "description": "并击处理器(和弦作曲家)。用于多键并击的输入方案,如「宫保拼音」。" 239 | }, 240 | { 241 | "type": "string", 242 | "pattern": "^lua_processor@\\w+", 243 | "description": "Lua 自定义脚本处理器。使用 lua 脚本定义的自定义按键。可定义多个。需添加@{函数名}后缀,如 lua_processor@foo。函数名定义于用户文件夹下的 rime.lua。", 244 | "example": "lua_processor@foo" 245 | } 246 | ] 247 | } 248 | }, 249 | "segmentors": { 250 | "type": "array", 251 | "description": "负责识别输入内容,分段并打标记的组件。", 252 | "items": { 253 | "additionalItems": false, 254 | "oneOf": [ 255 | { 256 | "type": "string", 257 | "enum": [ 258 | "ascii_segmentor" 259 | ], 260 | "description": "标识西文段落字母直接上屏。" 261 | }, 262 | { 263 | "type": "string", 264 | "enum": [ 265 | "matcher" 266 | ], 267 | "description": "与 processor 中的 recognizer 搭配,标识符合特定规则的段落,如网址、反查等。" 268 | }, 269 | { 270 | "type": "string", 271 | "enum": [ 272 | "abc_segmentor" 273 | ], 274 | "description": "将常规的文字段落标识为 `abc` tag。" 275 | }, 276 | { 277 | "type": "string", 278 | "enum": [ 279 | "punct_segmentor" 280 | ], 281 | "description": "将标点段落标识为 `punct` tag。" 282 | }, 283 | { 284 | "type": "string", 285 | "enum": [ 286 | "fallback_segmentor" 287 | ], 288 | "description": "标识其余未识别的段落。" 289 | }, 290 | { 291 | "type": "string", 292 | "pattern": "^affix_segmentor@\\w+", 293 | "description": "用户自定义标识器。可定义多个。需添加 @{tag} 后缀,如 affix_segmentor@foo。", 294 | "example": "affix_segmentor@foo" 295 | }, 296 | { 297 | "type": "string", 298 | "pattern": "^lua_segmentor@\\w+", 299 | "description": "自定义 Lua 函数标识器。可定义多个。需添加 @{函数名} 后缀,如 lua_segmentor@foo。", 300 | "example": "lua_segmentor@foo" 301 | } 302 | ] 303 | } 304 | }, 305 | "translators": { 306 | "type": "array", 307 | "description": "翻译特定输入码为一组候选文字的组件。", 308 | "items": { 309 | "additionalItems": false, 310 | "oneOf": [ 311 | { 312 | "type": "string", 313 | "enum": [ 314 | "echo_translator" 315 | ], 316 | "description": "回显输入码。输入码可以直接通过 Shift+Enter 上屏。" 317 | }, 318 | { 319 | "type": "string", 320 | "enum": [ 321 | "punct_translator" 322 | ], 323 | "description": "标点符号翻译器。配合 punct_segmentor 使用。" 324 | }, 325 | { 326 | "type": "string", 327 | "enum": [ 328 | "reverse_lookup_translator" 329 | ], 330 | "description": "反查翻译器。借助另一种编码方案查询编码。" 331 | }, 332 | { 333 | "type": "string", 334 | "pattern": "^table_translator(@\\w+)?", 335 | "description": "码表翻译器。可定义多个。需添加 @{码表名} 后缀,如 table_translator@cangjie。", 336 | "example": "table_translator@cangjie" 337 | }, 338 | { 339 | "type": "string", 340 | "pattern": "^script_translator(@\\w+)?", 341 | "description": "脚本翻译器。可定义多个。需添加 @{脚本名} 后缀,如 script_translator@pinyin。", 342 | "example": "script_translator@pinyin" 343 | }, 344 | { 345 | "type": "string", 346 | "pattern": "^lua_translator@\\w+", 347 | "description": "Lua 函数翻译器。可定义多个。需添加 @{lua 函数名} 后缀,如 lua_translator@date_translator。Lua 函数定义于用户文件夹下的 rime.lua,参数为 (input, seg, env)。", 348 | "example": "lua_translator@date_translator" 349 | } 350 | ] 351 | } 352 | }, 353 | "filters": { 354 | "type": "array", 355 | "description": "负责对翻译结果进行过滤的组件。", 356 | "items": { 357 | "additionalItems": false, 358 | "oneOf": [ 359 | { 360 | "type": "string", 361 | "enum": [ 362 | "uniquifier" 363 | ], 364 | "description": "过滤重复的候选字。有可能来自 simplifier。" 365 | }, 366 | { 367 | "type": "string", 368 | "enum": [ 369 | "cjk_minifier" 370 | ], 371 | "description": "字符集过滤。仅用于 script_translator,使其支持 extended_charset 开关。" 372 | }, 373 | { 374 | "type": "string", 375 | "enum": [ 376 | "single_char_filter" 377 | ], 378 | "description": "单字过滤器。如加载该组件,则屏蔽词典中的词组(仅 table_translator 有效)。" 379 | }, 380 | { 381 | "type": "string", 382 | "enum": [ 383 | "simplifier" 384 | ], 385 | "description": "用字转换。" 386 | }, 387 | { 388 | "type": "string", 389 | "pattern": "^reverse_lookup_filter@\\w+", 390 | "description": "反查过滤器。Rime 1.0 后替代 reverse_lookup_translator,以提供更灵活的反查方式。\n可定义多个。需添加@{过滤器名称}后缀,如 reverse_lookup_filter@pinyin_lookup。", 391 | "example": "reverse_lookup_filter@pinyin_lookup" 392 | }, 393 | { 394 | "type": "string", 395 | "pattern": "^charset_filter@\\w+", 396 | "description": "字符集过滤器。可定义多个。需添加@{字符集名称}后缀,如 charset_filter@utf-8。", 397 | "example": "charset_filter@utf-8" 398 | }, 399 | { 400 | "type": "string", 401 | "pattern": "^lua_filter@\\w+", 402 | "description": "使用 Lua 函数的自定义过滤器。可定义多个。需添加@{字符集名称}后缀,如 lua_filter@single_char_first。Lua 函数名定义于用户文件夹下的 rime.lua,参数为 (input, env)。", 403 | "example": "lua_filter@single_char_first" 404 | } 405 | ] 406 | } 407 | } 408 | } 409 | }, 410 | "speller": { 411 | "description": "拼写处理组件。接受字符按键,编辑输入码。", 412 | "type": "object", 413 | "additionalProperties": false, 414 | "properties": { 415 | "alphabet": { 416 | "type": "string", 417 | "description": "定义本方案输入键。" 418 | }, 419 | "initials": { 420 | "type": "string", 421 | "description": "定义仅作为始码的键。" 422 | }, 423 | "finals": { 424 | "type": "string", 425 | "description": "定义仅作为末码的键。" 426 | }, 427 | "delimiter": { 428 | "type": "string", 429 | "description": "上屏时的音节间分音符。", 430 | "examples": [ 431 | " ", 432 | "'", 433 | " '" 434 | ] 435 | }, 436 | "algebra": { 437 | "type": "array", 438 | "description": "拼写运算规则。由之算出的拼写汇入 prism 中。", 439 | "items": { 440 | "type": "string", 441 | "pattern": "((|derive|xform|fuzz|xlit|abbrev)/[^/]+/[^/]+/)|(erase/[^/]+/)", 442 | "examples": [ 443 | "erase/^xx$/", 444 | "derive/^([jqxy])u$/$1v/", 445 | "xform/^([aoe])(ng)?$/$1$1$2/", 446 | "xlit/QWRTYUIOPSDFGHJKLZXCVBNM/qwrtyuiopsdfghjklzxcvbnm/", 447 | "abbrev/^(.).+$/$1/" 448 | ] 449 | } 450 | }, 451 | "max_code_length": { 452 | "type": "integer", 453 | "description": "形码最大码长,如超出最大码长则顶字上屏。" 454 | }, 455 | "auto_select": { 456 | "type": "boolean", 457 | "pattern": "(true|false)", 458 | "description": "开关:自动上屏。" 459 | }, 460 | "auto_select_pattern": { 461 | "type": "string", 462 | "description": "自动上屏规则。以正则表达式描述,当输入串匹配时自动顶字上屏。" 463 | }, 464 | "use_space": { 465 | "type": "boolean", 466 | "pattern": "(true|false)", 467 | "description": "开关:以空格键作为输入码。" 468 | } 469 | } 470 | }, 471 | "segmentor": { 472 | "description": "输入码分段组件。识别不同内容类型,将输入码分段。", 473 | "type": "object", 474 | "additionalProperties": false, 475 | "properties": { 476 | "tag": { 477 | "type": "string", 478 | "description": "分段后打上的类型标记。" 479 | }, 480 | "prefix": { 481 | "type": "string", 482 | "description": "前缀标识。可留空。" 483 | }, 484 | "suffix": { 485 | "type": "string", 486 | "description": "后缀标识。可留空。" 487 | }, 488 | "tips": { 489 | "type": "string", 490 | "description": "输入前提示符。可留空。" 491 | }, 492 | "closing_tips": { 493 | "type": "string", 494 | "description": "结束输入提示符。可留空。" 495 | }, 496 | "extra_tags": { 497 | "type": "array", 498 | "description": "可定义的一组额外类型标记。", 499 | "items": { 500 | "type": "string", 501 | "description": "额外类型标记。" 502 | } 503 | } 504 | } 505 | }, 506 | "translator": { 507 | "description": "输入码翻译组件。将输入码翻译为一组候选文字。", 508 | "type": "object", 509 | "additionalProperties": false, 510 | "properties": { 511 | "enable_charset_filter": { 512 | "type": "boolean", 513 | "pattern": "(true|false)", 514 | "description": "开关:字符集过滤。仅对 table_translator 生效。" 515 | }, 516 | "enable_encoder": { 517 | "type": "boolean", 518 | "pattern": "(true|false)", 519 | "description": "开关:自动造词。仅对 table_translator 生效。" 520 | }, 521 | "encode_commit_history": { 522 | "type": "boolean", 523 | "pattern": "(true|false)", 524 | "description": "开关:对上屏词自动成词。仅对 table_translator 生效。" 525 | }, 526 | "max_phrase_length": { 527 | "type": "integer", 528 | "description": "最大自动成词词长。仅对 table_translator 生效。" 529 | }, 530 | "enable_completion": { 531 | "type": "boolean", 532 | "pattern": "(true|false)", 533 | "description": "开关:自动补全输入码。仅对 table_translator 生效。" 534 | }, 535 | "sentence_over_completion": { 536 | "type": "boolean", 537 | "pattern": "(true|false)", 538 | "description": "开关:开启智能组句,即使无全码对应字而仅有逐键提示时。仅对 table_translator 生效。" 539 | }, 540 | "strict_spelling": { 541 | "type": "boolean", 542 | "pattern": "(true|false)", 543 | "description": "开关:屏蔽符合 fuzz 拼写规则的单字候选,仅以其输入词组。仅对 table_translator 生效。" 544 | }, 545 | "disable_user_dict_for_patterns": { 546 | "description": "一组指定的匹配模式字符串,若输入串匹配任一指定模式,则在当前查询中禁用用户词典。", 547 | "type": "array", 548 | "additionalItems": false, 549 | "items": { 550 | "type": "string", 551 | "description": "编码的匹配模式字符串(pattern)。" 552 | } 553 | }, 554 | "enable_sentence": { 555 | "type": "boolean", 556 | "pattern": "(true|false)", 557 | "description": "开关:开启自动造句。" 558 | }, 559 | "enable_user_dict": { 560 | "type": "boolean", 561 | "pattern": "(true|false)", 562 | "description": "开关:开启用户词典(用于记录动态字词频、用户词)。" 563 | }, 564 | "dictionary": { 565 | "type": "string", 566 | "description": "提供给翻译器的字典文件。" 567 | }, 568 | "prism": { 569 | "type": "string", 570 | "description": "主翻译器生成的棱镜文件名,或副翻译器调用的棱镜文件名。" 571 | }, 572 | "user_dict": { 573 | "type": "string", 574 | "description": "指定的用户词典名。" 575 | }, 576 | "db_class": { 577 | "type": "string", 578 | "pattern": "(tabledb|userdb)", 579 | "description": "指定的用户词典类型。文本(tabledb)或二进制(userdb)。" 580 | }, 581 | "preedit_format": { 582 | "type": "array", 583 | "description": "自定义的一组上屏码规则。", 584 | "additionalItems": false, 585 | "items": { 586 | "type": "string", 587 | "description": "自定义上屏码规则" 588 | } 589 | }, 590 | "comment_format": { 591 | "type": "array", 592 | "description": "自定义的一组提示码规则。", 593 | "additionalItems": false, 594 | "items": { 595 | "type": "string", 596 | "description": "自定义提示码规则" 597 | } 598 | }, 599 | "spelling_hints": { 600 | "type": "integer", 601 | "description": "启用带调拼音标注的候选字数。仅对 script_translator 生效。" 602 | }, 603 | "initial_quality": { 604 | "type": "integer", 605 | "description": "当前翻译器翻译的优先级。" 606 | }, 607 | "tag": { 608 | "type": "string", 609 | "description": "设置此翻译器针对的 tag。可留空,留空则仅针对 abc。" 610 | }, 611 | "prefix": { 612 | "type": "string", 613 | "description": "设置此翻译器的前缀标识,可留空,留空则无前缀。" 614 | }, 615 | "suffix": { 616 | "type": "string", 617 | "description": "设置此翻译器的后缀标识,可留空,留空则无后缀。" 618 | }, 619 | "tips": { 620 | "type": "string", 621 | "description": "设置此翻译器的输入提示符,可留空,留空则无提示符。" 622 | }, 623 | "closing_tips": { 624 | "type": "string", 625 | "description": "设置此翻译器的结束输入提示符,可留空,留空则无提示符。" 626 | }, 627 | "contextual_suggestions": { 628 | "type": "boolean", 629 | "description": "是否使用语言模型优化输出结果(需要与 grammar 一起使用)。" 630 | }, 631 | "max_homophones": { 632 | "type": "integer", 633 | "description": "最大同音词群长度(需要与 grammar 一起使用)。" 634 | }, 635 | "max_homographs": { 636 | "type": "integer", 637 | "description": "最大同形词群长度(需要与 grammar 一起使用)。" 638 | } 639 | } 640 | }, 641 | "reverse_lookup_filter": { 642 | "description": "反查过滤器。将候选文字翻译为一组输入码。", 643 | "type": "object", 644 | "additionalProperties": false, 645 | "properties": { 646 | "tags": { 647 | "type": "string", 648 | "description": "设置其作用范围。" 649 | }, 650 | "overwrite_comment": { 651 | "type": "boolean", 652 | "description": "是否覆盖其他提示。" 653 | }, 654 | "dictionary": { 655 | "type": "string", 656 | "description": "反查所得提示码之码表。" 657 | }, 658 | "comment_format": { 659 | "type": "string", 660 | "description": "自定义提示码格式。" 661 | } 662 | } 663 | }, 664 | "simplifier": { 665 | "description": "简繁体转换器", 666 | "type": "object", 667 | "additionalProperties": false, 668 | "properties": { 669 | "option_name": { 670 | "type": "string", 671 | "description": "对应 switches 中设置的切换项名,即 key_binder/binding 中所用名。" 672 | }, 673 | "opencc_config": { 674 | "type": "string", 675 | "description": "用字转换的配置文件。位于 rime_dir/opencc/。自带之配置文件包含:繁转简(默认):t2s.json,繁转台湾:t2tw.json,繁转香港:t2hk.json,简转繁:s2t.json。", 676 | "enum": [ 677 | "t2s.json", 678 | "t2tw.json", 679 | "t2hk.json", 680 | "s2t.json" 681 | ] 682 | }, 683 | "tags": { 684 | "type": "string", 685 | "description": "设置转换范围。" 686 | }, 687 | "tips": { 688 | "type": "string", 689 | "description": "设置是否提示转换前的字。可填 none(或不填)、char(仅对单字有效)、all。", 690 | "enum": [ 691 | "none", 692 | "char", 693 | "all" 694 | ] 695 | }, 696 | "comment_format": { 697 | "type": "string", 698 | "description": "自定义提示码格式。" 699 | }, 700 | "allow_erase_comment": { 701 | "type": "boolean", 702 | "default": false, 703 | "description": "是否允许返回空提示码(默认为 false)。" 704 | }, 705 | "show_in_comment": { 706 | "type": "boolean", 707 | "description": "设置是否仅将转换结果显示在备忘中。" 708 | }, 709 | "excluded_types": { 710 | "type": "string", 711 | "description": "取消特定范围(一般为 reverse_lookup_translator)转换用字。" 712 | } 713 | } 714 | }, 715 | "chord_composer": { 716 | "description": "并击处理器(和弦作曲家)。用于多键并击的输入方案,如「宫保拼音」。", 717 | "type": "object", 718 | "additionalProperties": false, 719 | "properties": { 720 | "alphabet": { 721 | "type": "string", 722 | "description": "字母表,包含用于并击的按键。击键虽有先后,形成并击时,一律以字母表顺序排列。" 723 | }, 724 | "algebra": { 725 | "type": "string", 726 | "description": "拼写运算规则,将一组并击编码转换为拼音音节。" 727 | }, 728 | "output_format": { 729 | "type": "string", 730 | "description": "并击完成后采用的格式,追加隔音符号。" 731 | }, 732 | "prompt_format": { 733 | "type": "string", 734 | "description": "并击过程中采用的格式,如方括号。" 735 | } 736 | } 737 | } 738 | } 739 | } -------------------------------------------------------------------------------- /server/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rime-language-server", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "agent-base": { 8 | "version": "4.3.0", 9 | "resolved": "https://registry.npm.taobao.org/agent-base/download/agent-base-4.3.0.tgz?cache=0&sync_timestamp=1593732670803&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fagent-base%2Fdownload%2Fagent-base-4.3.0.tgz", 10 | "integrity": "sha1-gWXwHENgCbzK0LHRIvBe13Dvxu4=", 11 | "requires": { 12 | "es6-promisify": "^5.0.0" 13 | } 14 | }, 15 | "argparse": { 16 | "version": "1.0.10", 17 | "resolved": "https://registry.npm.taobao.org/argparse/download/argparse-1.0.10.tgz", 18 | "integrity": "sha1-vNZ5HqWuCXJeF+WtmIE0zUCz2RE=", 19 | "requires": { 20 | "sprintf-js": "~1.0.2" 21 | } 22 | }, 23 | "debug": { 24 | "version": "3.1.0", 25 | "resolved": "https://registry.npm.taobao.org/debug/download/debug-3.1.0.tgz", 26 | "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", 27 | "requires": { 28 | "ms": "2.0.0" 29 | } 30 | }, 31 | "es6-promise": { 32 | "version": "4.2.8", 33 | "resolved": "https://registry.npm.taobao.org/es6-promise/download/es6-promise-4.2.8.tgz", 34 | "integrity": "sha1-TrIVlMlyvEBVPSduUQU5FD21Pgo=" 35 | }, 36 | "es6-promisify": { 37 | "version": "5.0.0", 38 | "resolved": "https://registry.npm.taobao.org/es6-promisify/download/es6-promisify-5.0.0.tgz", 39 | "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", 40 | "requires": { 41 | "es6-promise": "^4.0.3" 42 | } 43 | }, 44 | "esprima": { 45 | "version": "4.0.1", 46 | "resolved": "https://registry.npm.taobao.org/esprima/download/esprima-4.0.1.tgz", 47 | "integrity": "sha1-E7BM2z5sXRnfkatph6hpVhmwqnE=" 48 | }, 49 | "http-proxy-agent": { 50 | "version": "2.1.0", 51 | "resolved": "https://registry.npm.taobao.org/http-proxy-agent/download/http-proxy-agent-2.1.0.tgz", 52 | "integrity": "sha1-5IIb7vWyFCogJr1zkm/lN2McVAU=", 53 | "requires": { 54 | "agent-base": "4", 55 | "debug": "3.1.0" 56 | } 57 | }, 58 | "https-proxy-agent": { 59 | "version": "2.2.4", 60 | "resolved": "https://registry.npm.taobao.org/https-proxy-agent/download/https-proxy-agent-2.2.4.tgz", 61 | "integrity": "sha1-TuenN6vZJniik9mzShr00NCMeHs=", 62 | "requires": { 63 | "agent-base": "^4.3.0", 64 | "debug": "^3.1.0" 65 | } 66 | }, 67 | "js-yaml": { 68 | "version": "3.14.0", 69 | "resolved": "https://registry.npm.taobao.org/js-yaml/download/js-yaml-3.14.0.tgz", 70 | "integrity": "sha1-p6NBcPJqIbsWJCTYray0ETpp5II=", 71 | "requires": { 72 | "argparse": "^1.0.7", 73 | "esprima": "^4.0.0" 74 | } 75 | }, 76 | "jsonc-parser": { 77 | "version": "2.3.0", 78 | "resolved": "https://registry.npm.taobao.org/jsonc-parser/download/jsonc-parser-2.3.0.tgz", 79 | "integrity": "sha1-fH/JiO4UhtNXNPqqqGb62wD6ke4=" 80 | }, 81 | "ms": { 82 | "version": "2.0.0", 83 | "resolved": "https://registry.npm.taobao.org/ms/download/ms-2.0.0.tgz", 84 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 85 | }, 86 | "prettier": { 87 | "version": "1.19.1", 88 | "resolved": "https://registry.npm.taobao.org/prettier/download/prettier-1.19.1.tgz", 89 | "integrity": "sha1-99f1/4qc2HKnvkyhQglZVqYHl8s=", 90 | "optional": true 91 | }, 92 | "request-light": { 93 | "version": "0.2.5", 94 | "resolved": "https://registry.npm.taobao.org/request-light/download/request-light-0.2.5.tgz", 95 | "integrity": "sha1-OKPaey5W96+Mu6V+ipSTDuI4B0Y=", 96 | "requires": { 97 | "http-proxy-agent": "^2.1.0", 98 | "https-proxy-agent": "^2.2.3", 99 | "vscode-nls": "^4.1.1" 100 | } 101 | }, 102 | "sprintf-js": { 103 | "version": "1.0.3", 104 | "resolved": "https://registry.npm.taobao.org/sprintf-js/download/sprintf-js-1.0.3.tgz", 105 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" 106 | }, 107 | "vscode-json-languageservice": { 108 | "version": "3.8.0", 109 | "resolved": "https://registry.npm.taobao.org/vscode-json-languageservice/download/vscode-json-languageservice-3.8.0.tgz", 110 | "integrity": "sha1-x+coP5k+PbOfpVAUB7Ajrab9OuM=", 111 | "requires": { 112 | "jsonc-parser": "^2.2.1", 113 | "vscode-languageserver-textdocument": "^1.0.1", 114 | "vscode-languageserver-types": "^3.15.1", 115 | "vscode-nls": "^4.1.2", 116 | "vscode-uri": "^2.1.2" 117 | } 118 | }, 119 | "vscode-jsonrpc": { 120 | "version": "5.0.1", 121 | "resolved": "https://registry.npm.taobao.org/vscode-jsonrpc/download/vscode-jsonrpc-5.0.1.tgz?cache=0&sync_timestamp=1596004930851&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvscode-jsonrpc%2Fdownload%2Fvscode-jsonrpc-5.0.1.tgz", 122 | "integrity": "sha1-m6ucMw2J9D/IwehwK1w24FigF5Q=" 123 | }, 124 | "vscode-languageserver": { 125 | "version": "6.1.1", 126 | "resolved": "https://registry.npm.taobao.org/vscode-languageserver/download/vscode-languageserver-6.1.1.tgz?cache=0&sync_timestamp=1596001274058&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvscode-languageserver%2Fdownload%2Fvscode-languageserver-6.1.1.tgz", 127 | "integrity": "sha1-12r8aBcsJ9QyfudDMrRo+8dA12I=", 128 | "requires": { 129 | "vscode-languageserver-protocol": "^3.15.3" 130 | } 131 | }, 132 | "vscode-languageserver-protocol": { 133 | "version": "3.15.3", 134 | "resolved": "https://registry.npm.taobao.org/vscode-languageserver-protocol/download/vscode-languageserver-protocol-3.15.3.tgz?cache=0&sync_timestamp=1596005051505&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvscode-languageserver-protocol%2Fdownload%2Fvscode-languageserver-protocol-3.15.3.tgz", 135 | "integrity": "sha1-P6mgcC10LPeIPLYYKmIS/NCh2Ls=", 136 | "requires": { 137 | "vscode-jsonrpc": "^5.0.1", 138 | "vscode-languageserver-types": "3.15.1" 139 | } 140 | }, 141 | "vscode-languageserver-textdocument": { 142 | "version": "1.0.1", 143 | "resolved": "https://registry.npm.taobao.org/vscode-languageserver-textdocument/download/vscode-languageserver-textdocument-1.0.1.tgz", 144 | "integrity": "sha1-F4Fo6H761hcbNyrdHeo09T5dMw8=" 145 | }, 146 | "vscode-languageserver-types": { 147 | "version": "3.15.1", 148 | "resolved": "https://registry.npm.taobao.org/vscode-languageserver-types/download/vscode-languageserver-types-3.15.1.tgz?cache=0&sync_timestamp=1596005129715&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvscode-languageserver-types%2Fdownload%2Fvscode-languageserver-types-3.15.1.tgz", 149 | "integrity": "sha1-F75x140vYjbUFPAAHOHvTSPmtt4=" 150 | }, 151 | "vscode-nls": { 152 | "version": "4.1.2", 153 | "resolved": "https://registry.npm.taobao.org/vscode-nls/download/vscode-nls-4.1.2.tgz", 154 | "integrity": "sha1-yov4u4KgmHsygB+f3f3S+5/TwWc=" 155 | }, 156 | "vscode-uri": { 157 | "version": "2.1.2", 158 | "resolved": "https://registry.npm.taobao.org/vscode-uri/download/vscode-uri-2.1.2.tgz", 159 | "integrity": "sha1-yNQN6T61evMfPHFd1lDiyiwJbxw=" 160 | }, 161 | "yaml-ast-parser-custom-tags": { 162 | "version": "0.0.43", 163 | "resolved": "https://registry.npm.taobao.org/yaml-ast-parser-custom-tags/download/yaml-ast-parser-custom-tags-0.0.43.tgz", 164 | "integrity": "sha1-RpaBRc5OJMsDwzEgV/DxQbk6fQI=" 165 | }, 166 | "yaml-language-server": { 167 | "version": "0.9.0", 168 | "resolved": "https://registry.npm.taobao.org/yaml-language-server/download/yaml-language-server-0.9.0.tgz", 169 | "integrity": "sha1-b3sAaN/RgliTINp9Nvet6Jf2xKA=", 170 | "requires": { 171 | "js-yaml": "^3.13.1", 172 | "jsonc-parser": "^2.2.1", 173 | "prettier": "^1.18.2", 174 | "request-light": "^0.2.4", 175 | "vscode-json-languageservice": "^3.6.0", 176 | "vscode-languageserver": "^5.2.1", 177 | "vscode-languageserver-types": "^3.15.1", 178 | "vscode-nls": "^4.1.2", 179 | "vscode-uri": "^2.1.1", 180 | "yaml-ast-parser-custom-tags": "0.0.43" 181 | }, 182 | "dependencies": { 183 | "vscode-jsonrpc": { 184 | "version": "4.0.0", 185 | "resolved": "https://registry.npm.taobao.org/vscode-jsonrpc/download/vscode-jsonrpc-4.0.0.tgz?cache=0&sync_timestamp=1596004930851&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvscode-jsonrpc%2Fdownload%2Fvscode-jsonrpc-4.0.0.tgz", 186 | "integrity": "sha1-p7907zJU0KDCcvqxXIISjjeLO+k=" 187 | }, 188 | "vscode-languageserver": { 189 | "version": "5.2.1", 190 | "resolved": "https://registry.npm.taobao.org/vscode-languageserver/download/vscode-languageserver-5.2.1.tgz?cache=0&sync_timestamp=1596001765060&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvscode-languageserver%2Fdownload%2Fvscode-languageserver-5.2.1.tgz", 191 | "integrity": "sha1-DS/t3TP5Kq312jJFDfSY1S9vFOs=", 192 | "requires": { 193 | "vscode-languageserver-protocol": "3.14.1", 194 | "vscode-uri": "^1.0.6" 195 | }, 196 | "dependencies": { 197 | "vscode-uri": { 198 | "version": "1.0.8", 199 | "resolved": "https://registry.npm.taobao.org/vscode-uri/download/vscode-uri-1.0.8.tgz", 200 | "integrity": "sha1-l2mq7OyuQCb7biI1nLOJRlgN7Vk=" 201 | } 202 | } 203 | }, 204 | "vscode-languageserver-protocol": { 205 | "version": "3.14.1", 206 | "resolved": "https://registry.npm.taobao.org/vscode-languageserver-protocol/download/vscode-languageserver-protocol-3.14.1.tgz?cache=0&sync_timestamp=1596005051505&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvscode-languageserver-protocol%2Fdownload%2Fvscode-languageserver-protocol-3.14.1.tgz", 207 | "integrity": "sha1-uKq2r64oSchKiYPTmhz3QkF6/i8=", 208 | "requires": { 209 | "vscode-jsonrpc": "^4.0.0", 210 | "vscode-languageserver-types": "3.14.0" 211 | }, 212 | "dependencies": { 213 | "vscode-languageserver-types": { 214 | "version": "3.14.0", 215 | "resolved": "https://registry.npm.taobao.org/vscode-languageserver-types/download/vscode-languageserver-types-3.14.0.tgz?cache=0&sync_timestamp=1596005129715&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvscode-languageserver-types%2Fdownload%2Fvscode-languageserver-types-3.14.0.tgz", 216 | "integrity": "sha1-07WVIkbTDlJBWStt3oKA4DlC50M=" 217 | } 218 | } 219 | } 220 | } 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rime-language-server", 3 | "description": "Rime configuration language server.", 4 | "version": "1.0.0", 5 | "author": "Mengqi Pei", 6 | "license": "MIT", 7 | "engines": { 8 | "node": "*" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/mengqi92/Rimebow" 13 | }, 14 | "dependencies": { 15 | "vscode-languageserver": "^6.1.1", 16 | "vscode-languageserver-textdocument": "^1.0.1", 17 | "yaml-language-server": "^0.9.0" 18 | }, 19 | "scripts": {} 20 | } 21 | -------------------------------------------------------------------------------- /server/src/RimeLanguageService.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { LanguageSettings, WorkspaceContextService } from 'yaml-language-server/out/server/src/languageservice/yamlLanguageService'; 4 | import { YAMLValidation } from 'yaml-language-server/out/server/src/languageservice/services/yamlValidation'; 5 | import { YAMLHover } from 'yaml-language-server/out/server/src/languageservice/services/yamlHover'; 6 | import { YAMLSchemaService } from 'yaml-language-server/out/server/src/languageservice/services/yamlSchemaService'; 7 | import { schemaContributions } from 'vscode-json-languageservice/lib/umd/services/configuration'; 8 | import { xhr, XHRResponse, configure as configureHttpRequests, getErrorStatusDescription } from 'request-light'; 9 | import { IConnection, createConnection, Diagnostic, Hover, Position } from 'vscode-languageserver'; 10 | import { TextDocument } from 'vscode-languageserver-textdocument'; 11 | import { URI } from 'vscode-uri'; 12 | import util = require('util'); 13 | import path = require('path'); 14 | import * as fs from 'fs'; 15 | 16 | const readFileAsync = util.promisify(fs.readFile); 17 | 18 | interface ClientSettings { 19 | enableHovering: boolean; 20 | enableValidation: boolean; 21 | } 22 | 23 | export const VSCODE_CONTENT_TYPE = 'vscode/content'; 24 | const CUSTOM_SCHEMA_CONTENT_TYPE = 'custom/schema/content'; 25 | export class RimeLanguageService { 26 | private _connection: IConnection; 27 | private _yamlValidator: YAMLValidation; 28 | private _yamlHover: YAMLHover; 29 | 30 | constructor(clientSettings: ClientSettings) { 31 | this._connection = createConnection(); 32 | 33 | const schemaUri = "https://d3gyfrtg0yt6hg.cloudfront.net/schema/rime-schema-yaml-schema.json"; 34 | let yamlSchemaService = new YAMLSchemaService(this._schemaRequestService, this._workspaceContext); 35 | yamlSchemaService.registerExternalSchema(schemaUri, ["/*.schema.yaml"]); 36 | const defaultSchemaUri = "https://d3gyfrtg0yt6hg.cloudfront.net/schema/rime-default-yaml-schema.json"; 37 | yamlSchemaService.registerExternalSchema(defaultSchemaUri, ["default.yaml"]); 38 | // if (settings.schemas) { 39 | // settings.schemas.forEach(settings => { 40 | // schemaService.registerExternalSchema(settings.uri, settings.fileMatch, settings.schema); 41 | // }); 42 | // } 43 | // yamlSchemaService.setSchemaContributions(schemaContributions); 44 | // yamlSchemaService.loadSchema(); 45 | 46 | this._yamlHover = new YAMLHover(yamlSchemaService, Promise); 47 | this._yamlValidator = new YAMLValidation(yamlSchemaService, Promise); 48 | let languagesettings: LanguageSettings = { 49 | validate: clientSettings.enableValidation 50 | }; 51 | this._yamlValidator.configure(languagesettings); 52 | } 53 | 54 | public validateTextDocument(document: TextDocument): Promise { 55 | return this._yamlValidator.doValidation(document); 56 | } 57 | 58 | public doHover(document: TextDocument, position: Position): Thenable { 59 | return this._yamlHover.doHover(document, position); 60 | } 61 | 62 | private async _schemaRequestService(uri: string): Promise { 63 | if (!uri) { 64 | return Promise.reject('No schema specified'); 65 | } 66 | 67 | const scheme = URI.parse(uri).scheme.toLowerCase(); 68 | console.log(`schema scheme: ${scheme}\nURI: ${uri}`); 69 | 70 | // If the requested schema is a local file, read and return the file contents 71 | switch (scheme) { 72 | case 'file': 73 | const fsPath = URI.parse(uri).fsPath; 74 | console.log(`fsPath: ${fsPath}`); 75 | try { 76 | return (await readFileAsync(fsPath, 'UTF-8')).toString(); 77 | } catch (err) { 78 | console.error(`error occurred when loading schema file: ${err}`); 79 | return ''; 80 | } 81 | // vscode schema content requests are forwarded to the client through LSP 82 | // This is a non-standard LSP extension introduced by the JSON language server 83 | // See https://github.com/microsoft/vscode/blob/master/extensions/json-language-features/server/README.md 84 | case 'vscode': 85 | return this._connection.sendRequest(VSCODE_CONTENT_TYPE, uri) 86 | .then(responseText => {return responseText;}, error => {return error.message;}); 87 | // HTTP(S) requests are sent and the response result is either the schema content or an error 88 | case 'http': 89 | case 'https': 90 | // Send the HTTP(S) schema content request and return the result 91 | const headers = { 'Accept-Encoding': 'gzip, deflate' }; 92 | return xhr({ url: uri, followRedirects: 5, headers }) 93 | .then(response => {return response.responseText;}, 94 | (error: XHRResponse) => {return Promise.reject(error.responseText || getErrorStatusDescription(error.status) || error.toString());}); 95 | default: 96 | // Neither local file nor vscode, nor HTTP(S) schema request, so send it off as a custom request 97 | return this._connection.sendRequest(CUSTOM_SCHEMA_CONTENT_TYPE, uri) as Thenable; 98 | } 99 | } 100 | 101 | private _workspaceContext: WorkspaceContextService = { 102 | resolveRelativePath: (relativePath: string, resource: string) => { 103 | return path.resolve(resource, relativePath); 104 | } 105 | }; 106 | } -------------------------------------------------------------------------------- /server/src/server.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createConnection, 3 | TextDocuments, 4 | Diagnostic, 5 | DiagnosticSeverity, 6 | ProposedFeatures, 7 | InitializeParams, 8 | DidChangeConfigurationNotification, 9 | CompletionItem, 10 | CompletionItemKind, 11 | TextDocumentPositionParams, 12 | TextDocumentSyncKind, 13 | InitializeResult, 14 | HoverParams 15 | } from 'vscode-languageserver'; 16 | 17 | import { RimeLanguageService } from './RimeLanguageService'; 18 | import { TextDocument } from 'vscode-languageserver-textdocument'; 19 | 20 | // Create a connection for the server. The connection uses Node's IPC as a transport. 21 | // Also include all preview / proposed LSP features. 22 | let connection = createConnection(ProposedFeatures.all); 23 | 24 | // Create a simple text document manager. The text document manager 25 | // supports full document sync only 26 | let documents: TextDocuments = new TextDocuments(TextDocument); 27 | 28 | let hasConfigurationCapability: boolean = false; 29 | let hasWorkspaceFolderCapability: boolean = false; 30 | let hasDiagnosticRelatedInformationCapability: boolean = false; 31 | 32 | connection.onInitialize((params: InitializeParams) => { 33 | let capabilities = params.capabilities; 34 | 35 | // Does the client support the `workspace/configuration` request? 36 | // If not, we will fall back using global settings 37 | hasConfigurationCapability = !!( 38 | capabilities.workspace && !!capabilities.workspace.configuration 39 | ); 40 | hasWorkspaceFolderCapability = !!( 41 | capabilities.workspace && !!capabilities.workspace.workspaceFolders 42 | ); 43 | hasDiagnosticRelatedInformationCapability = !!( 44 | capabilities.textDocument && 45 | capabilities.textDocument.publishDiagnostics && 46 | capabilities.textDocument.publishDiagnostics.relatedInformation 47 | ); 48 | 49 | const result: InitializeResult = { 50 | capabilities: { 51 | textDocumentSync: TextDocumentSyncKind.Incremental, 52 | // Tell the client that the server supports code completion 53 | completionProvider: { 54 | resolveProvider: true 55 | }, 56 | hoverProvider: true 57 | } 58 | }; 59 | if (hasWorkspaceFolderCapability) { 60 | result.capabilities.workspace = { 61 | workspaceFolders: { 62 | supported: true 63 | } 64 | }; 65 | } 66 | return result; 67 | }); 68 | 69 | connection.onInitialized(() => { 70 | if (hasConfigurationCapability) { 71 | // Register for all configuration changes. 72 | connection.client.register(DidChangeConfigurationNotification.type, undefined); 73 | } 74 | if (hasWorkspaceFolderCapability) { 75 | connection.workspace.onDidChangeWorkspaceFolders(_event => { 76 | connection.console.log('Workspace folder change event received.'); 77 | }); 78 | } 79 | }); 80 | 81 | const languageService: RimeLanguageService = new RimeLanguageService({ 82 | enableHovering: true, 83 | enableValidation: true 84 | }); 85 | 86 | // The example settings 87 | interface ExampleSettings { 88 | } 89 | 90 | // The global settings, used when the `workspace/configuration` request is not supported by the client. 91 | // Please note that this is not the case when using this server with the client provided in this example 92 | // but could happen with other clients. 93 | const defaultSettings: ExampleSettings = {}; 94 | let globalSettings: ExampleSettings = defaultSettings; 95 | 96 | // Cache the settings of all open documents 97 | let documentSettings: Map> = new Map(); 98 | 99 | connection.onDidChangeConfiguration(change => { 100 | if (hasConfigurationCapability) { 101 | // Reset all cached document settings 102 | documentSettings.clear(); 103 | } else { 104 | globalSettings = ( 105 | (change.settings.languageServerExample || defaultSettings) 106 | ); 107 | } 108 | 109 | // Revalidate all open text documents 110 | documents.all().forEach(validateTextDocument); 111 | }); 112 | 113 | function getDocumentSettings(resource: string): Thenable { 114 | if (!hasConfigurationCapability) { 115 | return Promise.resolve(globalSettings); 116 | } 117 | let result = documentSettings.get(resource); 118 | if (!result) { 119 | result = connection.workspace.getConfiguration({ 120 | scopeUri: resource, 121 | section: 'languageServerExample' 122 | }); 123 | documentSettings.set(resource, result); 124 | } 125 | return result; 126 | } 127 | 128 | // Only keep settings for open documents 129 | documents.onDidClose(e => { 130 | documentSettings.delete(e.document.uri); 131 | }); 132 | 133 | // The content of a text document has changed. This event is emitted 134 | // when the text document first opened or when its content has changed. 135 | documents.onDidChangeContent(change => { 136 | validateTextDocument(change.document); 137 | }); 138 | 139 | async function validateTextDocument(textDocument: TextDocument): Promise { 140 | const diagnostics = await languageService.validateTextDocument(textDocument); 141 | 142 | // Send the computed diagnostics to VSCode. 143 | connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); 144 | } 145 | 146 | connection.onDidChangeWatchedFiles(_change => { 147 | // Monitored files have change in VSCode 148 | connection.console.log('We received an file change event'); 149 | }); 150 | 151 | connection.onHover((params: HoverParams) => { 152 | let document = documents.get(params.textDocument.uri); 153 | return languageService.doHover(document, params.position); 154 | }); 155 | 156 | // This handler provides the initial list of the completion items. 157 | connection.onCompletion( 158 | (_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => { 159 | // The pass parameter contains the position of the text document in 160 | // which code complete got requested. For the example we ignore this 161 | // info and always provide the same completion items. 162 | return [ 163 | { 164 | label: 'TypeScript', 165 | kind: CompletionItemKind.Text, 166 | data: 1 167 | }, 168 | { 169 | label: 'JavaScript', 170 | kind: CompletionItemKind.Text, 171 | data: 2 172 | } 173 | ]; 174 | } 175 | ); 176 | 177 | // This handler resolves additional information for the item selected in 178 | // the completion list. 179 | connection.onCompletionResolve( 180 | (item: CompletionItem): CompletionItem => { 181 | if (item.data === 1) { 182 | item.detail = 'TypeScript details'; 183 | item.documentation = 'TypeScript documentation'; 184 | } else if (item.data === 2) { 185 | item.detail = 'JavaScript details'; 186 | item.documentation = 'JavaScript documentation'; 187 | } 188 | return item; 189 | } 190 | ); 191 | 192 | // Make the text document manager listen on the connection 193 | // for open, change and close text document events 194 | documents.listen(connection); 195 | 196 | // Listen on the connection 197 | connection.listen(); -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "composite": true, 11 | "rootDir": "src" 12 | }, 13 | "include": [ 14 | "src" 15 | ], 16 | "exclude": [ 17 | "node_modules", 18 | ".vscode-test" 19 | ] 20 | } -------------------------------------------------------------------------------- /src/RimeConfigurationTree.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import * as _ from 'lodash'; 5 | import util = require('util'); 6 | import * as Yaml from 'yaml-ast-parser'; 7 | import { determineScalarType, ScalarType } from 'yaml-ast-parser'; 8 | import { TreeItem, TreeItemCollapsibleState } from 'vscode'; 9 | import { YAMLNode, YAMLScalar } from 'yaml-ast-parser'; 10 | 11 | const readDirAsync = util.promisify(fs.readdir); 12 | const readFileAsync = util.promisify(fs.readFile); 13 | const statAsync = util.promisify(fs.stat); 14 | const existsAsync = util.promisify(fs.exists); 15 | 16 | export enum ItemKind { 17 | Root = 'ROOT', 18 | Folder = 'folder', 19 | File = 'file', 20 | Node = 'node' 21 | } 22 | 23 | export enum FileKind { 24 | Program, 25 | Default, 26 | DefaultCustom, 27 | Schema, 28 | Custom, 29 | Other 30 | } 31 | 32 | export interface ConfigTreeItemOptions { 33 | /** 34 | * The node key. 35 | */ 36 | readonly key: string; 37 | /** 38 | * Child nodes indexed by node key. 39 | */ 40 | readonly children: Map; 41 | /** 42 | * The kind of the config file containing this item. 43 | */ 44 | readonly kind: ItemKind; 45 | /** 46 | * Full path of the config file containing current node, used for navigation. 47 | */ 48 | readonly configFilePath: string; 49 | /** 50 | * Offset of the node in the original config file. 51 | */ 52 | readonly configOffset?: number; 53 | /** 54 | * Length of the node in the original config file. 55 | */ 56 | readonly configLength?: number; 57 | /** 58 | * The kind of the file when the node is a file or node (kind === ItemKind.File || kind === ItemKind.Node). 59 | */ 60 | readonly fileKind?: FileKind; 61 | /** 62 | * Whether current node is representing a sequential node. 63 | * Consider as false if no value provided. 64 | */ 65 | readonly isSequence?: boolean; 66 | /** 67 | * Whether current node is an element of a sequential node. 68 | * Consider as false if no value provided. 69 | */ 70 | readonly isSequenceElement?: boolean; 71 | /** 72 | * The value of the leaf node. 73 | * Consider the node as not a leaf node if no value provided. 74 | */ 75 | readonly value?: any; 76 | } 77 | 78 | export class RimeConfigNode extends TreeItem { 79 | /** 80 | * Node identifier. 81 | */ 82 | public key: string; 83 | /** 84 | * Children nodes indexed by node identifiers. 85 | */ 86 | public children: Map; 87 | /** 88 | * Value of the node, if any. 89 | */ 90 | public value?: any; 91 | /** 92 | * The value configured by default. 93 | * This field is only set for patched items. 94 | */ 95 | public defaultValue: any; 96 | /** 97 | * The kind of the file when the node is a config file. 98 | */ 99 | public fileKind?: FileKind; 100 | /** 101 | * Path to the configuration file that contains the current node. 102 | */ 103 | public configFilePath: string; 104 | /** 105 | * Offset of the node in the original config file. 106 | */ 107 | public configOffset: number = 0; 108 | /** 109 | * Length of the node in the original config file. 110 | */ 111 | public configLength: number = 0; 112 | /** 113 | * Whether current node is representing a sequential node. 114 | */ 115 | public isSequence: boolean = false; 116 | /** 117 | * Whether if current node is an element in a sequence. 118 | */ 119 | public isSequenceElement: boolean = false; 120 | /** 121 | * Whether if current node is a patched node. 122 | */ 123 | public isPatched: boolean = false; 124 | /** 125 | * Kind of the item. 126 | */ 127 | public readonly kind: ItemKind; 128 | constructor(options: ConfigTreeItemOptions) { 129 | super( 130 | options.value 131 | ? (options.isSequenceElement ? options.value : `${options.key}: ${options.value}`) 132 | : options.key, 133 | options.children.size > 0 ? TreeItemCollapsibleState.Collapsed : TreeItemCollapsibleState.None); 134 | this.key = options.key; 135 | this.children = options.children; 136 | this.value = options.value; 137 | this.configFilePath = options.configFilePath; 138 | this.configOffset = options.configOffset || 0; 139 | this.configLength = options.configLength || 0; 140 | this.kind = options.kind; 141 | this.fileKind = options.fileKind; 142 | this.isSequence = options.isSequence || false; 143 | this.isSequenceElement = options.isSequenceElement || false; 144 | 145 | this.contextValue = options.kind.toString(); 146 | this.tooltip = options.value ? `value: ${options.value}` : undefined; 147 | this.iconPath = this._getIconPath(options.kind, options.fileKind); 148 | } 149 | 150 | /** 151 | * Does current node has any child nodes. 152 | * @returns {boolean} Whether if current node has any child node. 153 | */ 154 | get hasChildren(): boolean { 155 | return this.children.size > 0; 156 | } 157 | 158 | get isCustomFile(): boolean { 159 | return this.fileKind === FileKind.Custom || this.fileKind === FileKind.DefaultCustom; 160 | } 161 | 162 | /** 163 | * Update current node. 164 | * @param {RimeConfigNode} newNode The new node to update to. 165 | */ 166 | public update(newNode: RimeConfigNode) { 167 | this.defaultValue = this.value; 168 | this.value = newNode.value; 169 | 170 | this.fileKind = newNode.fileKind; 171 | this.configFilePath = newNode.configFilePath; 172 | this.iconPath = newNode.iconPath; 173 | this.isSequence = newNode.isSequence || false; 174 | this.isSequenceElement = newNode.isSequenceElement || false; 175 | this.contextValue = newNode.kind.toString(); 176 | 177 | if (newNode.isCustomFile) { 178 | this.isPatched = true; 179 | } 180 | if (this.value) { 181 | this.label = this.isSequenceElement ? this.value : `${this.key}: ${this.value}`; 182 | } 183 | if (this.defaultValue) { 184 | this.tooltip = this.value ? `current: ${this.value}\ndefault: ${this.defaultValue}` : undefined; 185 | } 186 | } 187 | 188 | /** 189 | * Add a child node to current node. 190 | * @param childNode {RimeConfigNode} The child node to add. 191 | * @returns {RimeConfigNode} The child node added. Could be the existing node if there is a same child. 192 | */ 193 | public addChildNode(childNode: RimeConfigNode): RimeConfigNode { 194 | if (childNode.key === undefined) { 195 | throw new Error('No key found for given child node.'); 196 | } else if (this.children.has(childNode.key)) { 197 | return this.children.get(childNode.key)!; 198 | } 199 | 200 | this.children.set(childNode.key, childNode); 201 | if (!this.collapsibleState) { 202 | this.collapsibleState = TreeItemCollapsibleState.Collapsed; 203 | } 204 | return childNode; 205 | } 206 | 207 | private _getIconPath(itemKind: ItemKind, fileKind: FileKind | undefined): string | vscode.Uri | { light: string | vscode.Uri; dark: string | vscode.Uri; } | vscode.ThemeIcon | undefined { 208 | let iconFullName: string = ''; 209 | switch (itemKind) { 210 | case ItemKind.Folder: 211 | iconFullName = 'folder.png'; 212 | break; 213 | case ItemKind.File: 214 | switch (fileKind) { 215 | case FileKind.Program: 216 | iconFullName = 'program.png'; 217 | break; 218 | case FileKind.Default: 219 | iconFullName = 'default.png'; 220 | break; 221 | case FileKind.DefaultCustom: 222 | iconFullName = 'default-patch.png'; 223 | break; 224 | case FileKind.Schema: 225 | iconFullName = 'schema.png'; 226 | break; 227 | case FileKind.Custom: 228 | iconFullName = 'patch.png'; 229 | break; 230 | case FileKind.Other: 231 | iconFullName = 'other.png'; 232 | break; 233 | default: 234 | break; 235 | } 236 | break; 237 | case ItemKind.Node: 238 | switch (fileKind) { 239 | case FileKind.Program: 240 | iconFullName = 'program.png'; 241 | break; 242 | case FileKind.Default: 243 | iconFullName = 'default.png'; 244 | break; 245 | case FileKind.DefaultCustom: 246 | iconFullName = 'default-patch.png'; 247 | break; 248 | case FileKind.Schema: 249 | iconFullName = 'schema-node.png'; 250 | break; 251 | case FileKind.Custom: 252 | iconFullName = 'schema-node-patch.png'; 253 | break; 254 | case FileKind.Other: 255 | iconFullName = 'other.png'; 256 | break; 257 | default: 258 | break; 259 | } 260 | break; 261 | default: 262 | break; 263 | } 264 | if (iconFullName === '') { 265 | return undefined; 266 | } 267 | return { 268 | 'light': path.join(__filename, '..', '..', 'resources', 'light', 'configKind', iconFullName), 269 | 'dark': path.join(__filename, '..', '..', 'resources', 'dark', 'configKind', iconFullName), 270 | }; 271 | } 272 | } 273 | 274 | export class RimeConfigurationTree { 275 | public configTree: RimeConfigNode = new RimeConfigNode({ 276 | key: 'ROOT', 277 | children: new Map(), 278 | kind: ItemKind.Root, 279 | configFilePath: '' 280 | }); 281 | /** 282 | * Configuration tree, including config files, in the program config folder. 283 | */ 284 | public sharedConfigFolderNode: RimeConfigNode = new RimeConfigNode({ 285 | key: '程序配置', 286 | children: new Map(), 287 | configFilePath: '', 288 | kind: ItemKind.Folder, 289 | }); 290 | /** 291 | * Configuration tree, including config files, in the user config folder. 292 | */ 293 | public userConfigFolderNode: RimeConfigNode = new RimeConfigNode({ 294 | key: '用户配置', 295 | children: new Map(), 296 | configFilePath: '', 297 | kind: ItemKind.Folder 298 | }); 299 | 300 | private static readonly SHARED_CONFIG_LABEL: string = 'Shared Config'; 301 | private static readonly USER_CONFIG_LABEL: string = 'User Config'; 302 | private userConfigDir: string = ""; 303 | private sharedConfigDir: string = ""; 304 | 305 | constructor() { 306 | } 307 | 308 | public async build() { 309 | const sharedConfigDirConfigKey: string = "sharedConfigDir"; 310 | const userConfigDirConfigKey: string = "userConfigDir"; 311 | const rimeAssistantConfiguration: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration('rimebow'); 312 | if (rimeAssistantConfiguration.has(sharedConfigDirConfigKey) 313 | && await existsAsync(rimeAssistantConfiguration.get(sharedConfigDirConfigKey) as string)) { 314 | this.sharedConfigDir = rimeAssistantConfiguration.get(sharedConfigDirConfigKey) as string; 315 | } else { 316 | this.sharedConfigDir = await this._getDefaultSharedConfigDir(); 317 | } 318 | if (rimeAssistantConfiguration.has(userConfigDirConfigKey) 319 | && await existsAsync(rimeAssistantConfiguration.get(userConfigDirConfigKey) as string)) { 320 | this.userConfigDir = rimeAssistantConfiguration.get(userConfigDirConfigKey) as string; 321 | } else { 322 | this.userConfigDir = await this._getUserConfigDir(); 323 | } 324 | 325 | const sharedConfigFiles = await this._buildConfigTreeFromFiles( 326 | this.sharedConfigDir, RimeConfigurationTree.SHARED_CONFIG_LABEL); 327 | const userConfigFiles = await this._buildConfigTreeFromFiles( 328 | this.userConfigDir, RimeConfigurationTree.USER_CONFIG_LABEL); 329 | this.sharedConfigFolderNode.configFilePath = this.sharedConfigDir; 330 | this.userConfigFolderNode.configFilePath = this.userConfigDir; 331 | 332 | this._setupNodesForFileExplorer(sharedConfigFiles, this.sharedConfigFolderNode); 333 | this._setupNodesForFileExplorer(userConfigFiles, this.userConfigFolderNode); 334 | 335 | this.configTree.children = this._applyPatchesToSharedConfig(sharedConfigFiles, userConfigFiles); 336 | } 337 | 338 | /** 339 | * Build config tree for all the files in the given directory. 340 | * @param {string} configDir The directory path containing config files. 341 | * @param {string} label The label of the config directory. 342 | * @returns {Promise} A promise result containing a list of config nodes parsed. 343 | */ 344 | private async _buildConfigTreeFromFiles(configDir: string, label: string): Promise { 345 | const filesResult: Promise = readDirAsync(configDir); 346 | const fileNames = await filesResult; 347 | const promises: Promise[] = fileNames 348 | .filter((fileName: string) => fileName.endsWith('.yaml') && !fileName.endsWith('.dict.yaml')) 349 | .map(async (fileName: string): Promise => { 350 | return await this._buildConfigTreeFromFile(configDir, fileName); 351 | }); 352 | const fileItems: RimeConfigNode[] = await Promise.all(promises).catch(() => []); 353 | return fileItems; 354 | } 355 | 356 | protected async _buildConfigTreeFromFile(filePath: string, fileName: string): Promise { 357 | const fullName: string = path.join(filePath, fileName); 358 | const data = await readFileAsync(fullName); 359 | 360 | const doc: Yaml.YAMLNode = Yaml.load(data.toString()); 361 | 362 | const fileKind: FileKind = this._categoriseConfigFile(fileName); 363 | const fileLabel: string = fileName.replace('.yaml', '').replace('.custom', '').replace('.schema', ''); 364 | // The root node is representing the configuration file. 365 | let rootNode: RimeConfigNode = new RimeConfigNode({ 366 | key: fileLabel, 367 | children: new Map(), 368 | configFilePath: fullName, 369 | kind: ItemKind.File, 370 | fileKind: fileKind }); 371 | if (doc === null) { 372 | return rootNode; 373 | } 374 | // Build ConfigNode tree by traversing the nodeTree object. 375 | this._buildConfigTree(doc, rootNode, fullName, fileKind); 376 | if (fileKind === FileKind.Schema 377 | && rootNode.hasChildren 378 | && rootNode.children.has('schema')) { 379 | const schemaMetadata: RimeConfigNode = rootNode.children.get('schema')!; 380 | this._setSchemaNameAsLabel(schemaMetadata, rootNode); 381 | this._setMetadataAsTooltip(schemaMetadata, rootNode); 382 | } 383 | return rootNode; 384 | } 385 | 386 | /** 387 | * Build up a configuration tree based on the object tree parsed. 388 | * @param {YAMLNode} node The root node of the object tree parsed from yaml file. 389 | * @param {RimeConfigNode} rootNode The current traversed node in the configuration tree we are building. 390 | * @param {string} fullPath The full path of the configuration file. 391 | * @param {FileKind} fileKind Kind of the configuration file. 392 | */ 393 | protected _buildConfigTree(node: YAMLNode, rootNode: RimeConfigNode, fullPath: string, fileKind: FileKind) { 394 | if (node === undefined || node === null) { 395 | return; 396 | } 397 | switch (node.kind) { 398 | case Yaml.Kind.MAP: 399 | let mapNode: Yaml.YamlMap = node; 400 | mapNode.mappings.forEach((mapping: Yaml.YAMLMapping) => { 401 | this._buildASTNode(mapping, rootNode, fullPath, fileKind); 402 | }); 403 | break; 404 | case Yaml.Kind.SEQ: 405 | let sequenceNode: Yaml.YAMLSequence = node; 406 | sequenceNode.items.forEach((itemNode: YAMLNode) => { 407 | this._buildASTNode(itemNode, rootNode, fullPath, fileKind); 408 | }); 409 | break; 410 | case Yaml.Kind.SCALAR: 411 | rootNode.value = this._formatScalarValue(node); 412 | break; 413 | default: 414 | break; 415 | } 416 | } 417 | 418 | private _buildASTNode(mapping: YAMLNode, rootNode: RimeConfigNode, fullPath: string, fileKind: FileKind) { 419 | if (mapping.key === undefined || mapping.key === null) { 420 | return; 421 | } 422 | let current: RimeConfigNode = rootNode; 423 | let key: string = mapping.key!.value; 424 | let mappingStartPos: number = mapping.key!.startPosition; 425 | let mappingLength: number = mapping.key!.endPosition - mapping.key!.startPosition; 426 | let value: YAMLNode = mapping.value; 427 | // If the key has slash, create separate nodes for each part. 428 | // For instance, "foo/bar/baz: 1" should be created as a four-layer tree. 429 | if (key.indexOf("/") !== -1) { 430 | let leafNode: RimeConfigNode | undefined = this._buildSlashSeparatedNodes(key, current, mapping.key.startPosition, fileKind, fullPath); 431 | if (leafNode) { 432 | current = leafNode; 433 | const lastSlashInKeyIdx: number = key.lastIndexOf("/"); 434 | // Update current mapping to the leaf node (the part after the last "/"). 435 | key = key.substring(lastSlashInKeyIdx + 1); 436 | mappingStartPos = mappingStartPos + lastSlashInKeyIdx + 1; 437 | mappingLength = mappingLength - (lastSlashInKeyIdx + 1); 438 | } 439 | } 440 | switch (value.kind) { 441 | case Yaml.Kind.SCALAR: 442 | // Current node is a leaf node in the object tree. 443 | current.addChildNode(new RimeConfigNode({ 444 | key: key, 445 | children: new Map(), 446 | configFilePath: fullPath, 447 | configOffset: value.startPosition, 448 | configLength: value.endPosition - value.startPosition, 449 | value: this._formatScalarValue(value), 450 | kind: ItemKind.Node, 451 | fileKind: fileKind 452 | })); 453 | break; 454 | case Yaml.Kind.MAP: 455 | // Current node in the object tree has children. 456 | let childMapNode: RimeConfigNode = new RimeConfigNode({ 457 | key: key, 458 | children: new Map(), 459 | configFilePath: fullPath, 460 | configOffset: mappingStartPos, 461 | configLength: mappingLength, 462 | kind: ItemKind.Node, 463 | fileKind: fileKind 464 | }); 465 | current.addChildNode(childMapNode); 466 | this._buildConfigTree(value, childMapNode, fullPath, fileKind); 467 | break; 468 | case Yaml.Kind.SEQ: 469 | // Current node in the object tree has children and it's an array. 470 | let childSeqNode: RimeConfigNode = new RimeConfigNode({ 471 | key: key, 472 | children: new Map(), 473 | configFilePath: fullPath, 474 | configOffset: mappingStartPos, 475 | configLength: mappingLength, 476 | kind: ItemKind.Node, 477 | fileKind: fileKind, 478 | isSequence: true 479 | }); 480 | current.addChildNode(childSeqNode); 481 | let valueSeq: Yaml.YAMLSequence = value; 482 | valueSeq.items?.forEach((valueItem: YAMLNode, itemIndex: number) => { 483 | if (valueItem.kind === Yaml.Kind.SCALAR) { 484 | let grandChildNode: RimeConfigNode = new RimeConfigNode({ 485 | key: itemIndex.toString(), 486 | children: new Map(), 487 | configFilePath: fullPath, 488 | configOffset: valueItem.startPosition, 489 | configLength: valueItem.endPosition - valueItem.startPosition, 490 | kind: ItemKind.Node, 491 | fileKind: fileKind, 492 | value: this._formatScalarValue(valueItem), 493 | isSequenceElement: true 494 | }); 495 | childSeqNode.addChildNode(grandChildNode); 496 | } else { 497 | let grandChildNode: RimeConfigNode = new RimeConfigNode({ 498 | key: itemIndex.toString(), 499 | children: new Map(), 500 | configFilePath: fullPath, 501 | configOffset: valueItem.startPosition, 502 | configLength: valueItem.endPosition - valueItem.startPosition, 503 | kind: ItemKind.Node, 504 | fileKind: fileKind, 505 | isSequenceElement: true 506 | }); 507 | childSeqNode.addChildNode(grandChildNode); 508 | this._buildConfigTree(valueItem, grandChildNode, fullPath, fileKind); 509 | } 510 | }); 511 | break; 512 | default: 513 | break; 514 | } 515 | } 516 | 517 | /** 518 | * Recursively build multi-layer nodes according to the keys separated by slash. 519 | * For instance, given the key "foo/bar/baz", there would be 3 layers of nodes: foo -> bar -> baz. 520 | * @param {string} key The original key composing multi-layer keys by slashes, such as foo/bar/baz. 521 | * @param {RimeConfigNode} rootNode The root node to build from. 522 | * @param {number} keyStartPos The start position of the key in the original config file. 523 | * @param {FileKind} fileKind The kind of the config file. 524 | * @param {string} filePath Path to the config file. 525 | * @returns {RimeConfigNode} The leaf node built. 526 | */ 527 | protected _buildSlashSeparatedNodes( 528 | key: string, 529 | rootNode: RimeConfigNode, 530 | keyStartPos: number, 531 | fileKind: FileKind, 532 | filePath: string): RimeConfigNode | undefined { 533 | if (key === undefined || key === null) { 534 | return; 535 | } 536 | // Reached leaf. 537 | if (key.indexOf("/") === -1) { 538 | return rootNode; 539 | } 540 | 541 | const firstSlashInKeyIdx: number = key.indexOf("/"); 542 | const childNode: RimeConfigNode = new RimeConfigNode({ 543 | key: key.substring(0, key.indexOf("/")), 544 | children: new Map(), 545 | configFilePath: filePath, 546 | configOffset: keyStartPos, 547 | configLength: firstSlashInKeyIdx, 548 | kind: ItemKind.Node, 549 | fileKind: fileKind 550 | }); 551 | // add childNode as a child of rootNode, and then point to the childNode as current. 552 | rootNode = rootNode.addChildNode(childNode); 553 | return this._buildSlashSeparatedNodes(key.substring(firstSlashInKeyIdx + 1), rootNode, keyStartPos + firstSlashInKeyIdx + 1, fileKind, filePath); 554 | } 555 | 556 | /** 557 | * Apply patches from user config files to shared config files. 558 | * - When a default config file (the non-custom file) exists in both user config folder and shared config folder, use the user config one. 559 | * - When applying patches, patch nodes in user config will override the one in its default config. 560 | * For instance, patches in foo.custom.yaml overrides nodes in foo.yaml (the one in user config folder or the one in shared config folder). 561 | * - When applying schema patches, patch nodes in user config will first override the one in the default schema config (*.schema.yaml), 562 | * and then override the one in the default config (default.custom.yaml + default.yaml). 563 | * 564 | * Config priority: 565 | * - schema config: schema.custom.yaml > schema.yaml > default.custom.yaml > default.yaml 566 | * - other config: foo.custom > foo 567 | * @param {RimeConfigNode[]} sharedConfigFiles A list of shared config file nodes. 568 | * @param {RimeConfigNode[]} userConfigFiles A list of user config file nodes. 569 | * @returns {Map} The merged children map after applied patches. 570 | */ 571 | protected _applyPatchesToSharedConfig(sharedConfigFiles: RimeConfigNode[], userConfigFiles: RimeConfigNode[]): Map { 572 | // Collect and override non-custom files. 573 | let mergedResult: Map = new Map(); 574 | let nonCustomFiles: Map = new Map(); 575 | sharedConfigFiles.forEach((sharedConfigFile: RimeConfigNode) => { 576 | if (!nonCustomFiles.has(sharedConfigFile.key)) { 577 | nonCustomFiles.set(sharedConfigFile.key, sharedConfigFile); 578 | mergedResult.set(sharedConfigFile.key, sharedConfigFile); 579 | } 580 | }); 581 | userConfigFiles.filter((userConfigFile: RimeConfigNode) => !userConfigFile.isCustomFile) 582 | .forEach((userConfigFile: RimeConfigNode) => { 583 | nonCustomFiles.set(userConfigFile.key, userConfigFile); 584 | mergedResult.set(userConfigFile.key, userConfigFile); 585 | }); 586 | 587 | userConfigFiles.filter((userConfigFile: RimeConfigNode) => userConfigFile.isCustomFile) 588 | .forEach((userCustomFile: RimeConfigNode) => { 589 | if (!nonCustomFiles.has(userCustomFile.key)) { 590 | return; 591 | } 592 | const fileToPatch = nonCustomFiles.get(userCustomFile.key)!; 593 | // The file already exists in merged tree. Check if merge is needed. 594 | if (!userCustomFile.children.has('patch')) { 595 | return; 596 | } 597 | 598 | const patchNode: RimeConfigNode = userCustomFile.children.get('patch')!; 599 | mergedResult.set(userCustomFile.key, this._mergeTree(fileToPatch, patchNode)); 600 | }); 601 | 602 | // The default config file (default.yaml) now has been merged with the custom default config (default.custom.yaml). 603 | if (mergedResult.has('default')) { 604 | let defaultConfig: RimeConfigNode = mergedResult.get('default')!; 605 | // For each schema config, override default config. 606 | mergedResult.forEach((configFile: RimeConfigNode, fileKey: string) => { 607 | if (configFile.fileKind !== FileKind.Schema) { 608 | return; 609 | } 610 | let mergedTree: RimeConfigNode = this._cloneTree(configFile); 611 | mergedTree.children = this._mergeTree(defaultConfig, configFile).children; 612 | mergedResult.set(fileKey, mergedTree); 613 | }); 614 | } 615 | return mergedResult; 616 | } 617 | 618 | protected _cloneTree(tree: RimeConfigNode): RimeConfigNode { 619 | return _.cloneDeep(tree); 620 | } 621 | 622 | /** 623 | * Merge two trees. The merged result is updated based on a clone of tree A. 624 | * @param {RimeConfigNode} treeA The root node of the first tree to be merged. 625 | * @param {RimeConfigNode} treeB The root node of the second tree to be merged. 626 | * @returns {RimeConfigNode} The root node of the merged tree. 627 | */ 628 | protected _mergeTree(treeA: RimeConfigNode, treeB: RimeConfigNode): RimeConfigNode { 629 | if (treeB.key !== 'patch' && treeA.key !== 'default' && treeA.key !== treeB.key) { 630 | throw new Error('The trees to be merged have no common ancestor.'); 631 | } 632 | let mergedTree: RimeConfigNode = this._cloneTree(treeA); 633 | if (treeA.value && treeB.value && treeA.value !== treeB.value) { 634 | mergedTree.update(treeB); 635 | return mergedTree; 636 | } 637 | treeB.children.forEach((childB: RimeConfigNode, childBKey: string) => { 638 | if (treeA.children.has(childBKey)) { 639 | // The childB is also in tree A. 640 | const childA: RimeConfigNode = treeA.children.get(childBKey)!; 641 | if (childA.isSequence && childB.isSequence) { 642 | // Override child A when both are arrays. 643 | mergedTree.children.set(childBKey, childB); 644 | } else { 645 | const mergedChild: RimeConfigNode = this._mergeTree(childA, childB); 646 | mergedTree.children.set(childBKey, mergedChild); 647 | } 648 | } else { 649 | // The childB is a new node to tree A. 650 | mergedTree.addChildNode(childB); 651 | } 652 | }); 653 | return mergedTree; 654 | } 655 | 656 | private _categoriseConfigFile(fileNameWithExtensions: string): FileKind { 657 | const fileName: string = fileNameWithExtensions.replace('.yaml', ''); 658 | if (fileName === 'default.custom') { 659 | return FileKind.DefaultCustom; 660 | } else if (fileName === 'default') { 661 | return FileKind.Default; 662 | } else if (fileName.endsWith('schema')) { 663 | return FileKind.Schema; 664 | } else if (fileName.endsWith('custom')) { 665 | return FileKind.Custom; 666 | } else if (['weasel', 'squirrel', 'ibus_rime', 'installation', 'user'].indexOf(fileName) !== -1) { 667 | return FileKind.Program; 668 | } else { 669 | return FileKind.Other; 670 | } 671 | } 672 | 673 | private _setMetadataAsTooltip(schemaMetadata: RimeConfigNode, rootNode: RimeConfigNode) { 674 | if (!schemaMetadata.hasChildren) { 675 | return; 676 | } 677 | let tooltipLines: string[] = []; 678 | if (schemaMetadata.children.has('name') 679 | && schemaMetadata.children.get('name')!.value) { 680 | tooltipLines.push(`方案:${schemaMetadata.children.get('name')!.value}`); 681 | } 682 | if (schemaMetadata.children.has('schema_id')) { 683 | tooltipLines.push(`ID:${schemaMetadata.children.get('schema_id')!.value}`); 684 | } 685 | if (schemaMetadata.children.has('author')) { 686 | if (schemaMetadata.children.get('author')!.value) { 687 | tooltipLines.push(`作者:${schemaMetadata.children.get('author')!.value}`); 688 | } else if (schemaMetadata.children.get('author')!.hasChildren) { 689 | tooltipLines.push('作者:'); 690 | schemaMetadata.children.get('author')!.children.forEach((authorItem: RimeConfigNode) => { 691 | if (authorItem.isSequenceElement) { 692 | tooltipLines.push(`${authorItem.label}`); 693 | } 694 | }); 695 | } 696 | } 697 | if (schemaMetadata.children.has('version') 698 | && schemaMetadata.children.get('version')!.value) { 699 | tooltipLines.push(`版本:${schemaMetadata.children.get('version')!.value}`); 700 | } 701 | if (schemaMetadata.children.has('description') 702 | && schemaMetadata.children.get('description')!.value) { 703 | tooltipLines.push(`------\n${schemaMetadata.children.get('description')!.value}`); 704 | } 705 | rootNode.tooltip = tooltipLines.join('\n'); 706 | } 707 | 708 | private _setSchemaNameAsLabel(schemaMetadata: RimeConfigNode, fileNode: RimeConfigNode) { 709 | if (schemaMetadata.hasChildren 710 | && schemaMetadata.children.has('name') 711 | && schemaMetadata.children.get('name')!.value) { 712 | fileNode.label = schemaMetadata.children.get('name')!.value; 713 | } 714 | } 715 | 716 | private _formatScalarValue(valueNode: YAMLScalar): any { 717 | const base16 = /^0x[0-9a-fA-F]+$/; 718 | if (base16.test(valueNode.value)) { 719 | return valueNode.rawValue; 720 | } 721 | const scalarType = determineScalarType(valueNode); 722 | switch (scalarType) { 723 | case ScalarType.bool: 724 | return valueNode.valueObject; 725 | case ScalarType.int: 726 | return valueNode.valueObject; 727 | case ScalarType.string: 728 | return valueNode.value; 729 | case ScalarType.float: 730 | return valueNode.valueObject; 731 | default: 732 | return valueNode.rawValue; 733 | } 734 | } 735 | 736 | private async _getDefaultSharedConfigDir(): Promise { 737 | switch(process.platform) { 738 | case "win32": 739 | const programDir: string = path.join('C:', 'Program Files (x86)', 'Rime'); 740 | const entries: string[] = await readDirAsync(programDir); 741 | const weaselDirs: string[] = entries 742 | .filter((fileName: string) => fileName.startsWith('weasel')) 743 | .filter(async (entryName: string) => { 744 | const entryStat = await statAsync(path.join(programDir, entryName)); 745 | return entryStat.isDirectory(); 746 | }); 747 | if (weaselDirs.length === 1) { 748 | // Weasel: C:/Program Files (x86)/Rime/weasel-0.14.3/data 749 | return path.join('C:', 'Program Files (x86)', 'Rime', weaselDirs[0], 'data'); 750 | } else { 751 | // Return the one modified most recently. 752 | return await this._mostRecentModifiedDir(weaselDirs, programDir); 753 | } 754 | case "darwin": 755 | // Squirrel: /Library/Input Methods/Squirrel.app/Contents/SharedSupport/ 756 | return path.join('/Library', 'Input Methods', 'Squirrel.app', 'Contents', 'SharedSupport'); 757 | case "linux": 758 | // ibus-rime, fcitx-rime: /usr/share/rime-data 759 | return path.join('/usr', 'share', 'rime-data'); 760 | default: 761 | throw new Error(`Unsupported platform: ${process.platform}`); 762 | } 763 | } 764 | 765 | private async _mostRecentModifiedDir(weaselDirs: string[], programDir: string) { 766 | const stats: fs.Stats[] = await Promise.all(weaselDirs.map(async (dir: string): Promise => { 767 | return await statAsync(path.join(programDir, dir)); 768 | })); 769 | let maxMtime: Number = 0; 770 | let mostRecentDir = ""; 771 | stats.forEach((stat: fs.Stats, index: number) => { 772 | maxMtime = maxMtime > stat.mtimeMs ? maxMtime : stat.mtimeMs; 773 | mostRecentDir = weaselDirs[index]; 774 | }); 775 | return path.join(programDir, mostRecentDir); 776 | } 777 | 778 | private async _getUserConfigDir(): Promise { 779 | switch(process.platform) { 780 | case "win32": 781 | // 'C:\\Users\\mengq\\AppData\\Roaming\\Rime' 782 | // this.userConfigDir = path.join('C:', 'Users', 'mengq', 'AppData', 'Roaming', 'Rime'); 783 | return path.join(process.env.APPDATA!, 'Rime'); 784 | case "darwin": 785 | // Squirrel: /Users/Library/Rime 786 | return path.join(process.env.HOME!, 'Library', 'Rime'); 787 | case "linux": 788 | // ibus-rime: ~/.config/ibus/rime 789 | // fcitx-rime: ~/.config/fcitx/rime 790 | const ibusPath: string = path.join(process.env.HOME!, '.config', 'ibus', 'rime'); 791 | if (await existsAsync(ibusPath)) { 792 | return ibusPath; 793 | } else { 794 | return path.join(process.env.HOME!, '.config', 'fcitx', 'rime'); 795 | } 796 | default: 797 | throw new Error(`Unsupported platform: ${process.platform}`); 798 | } 799 | } 800 | 801 | private _setupNodesForFileExplorer(configFiles: RimeConfigNode[], folderNode: RimeConfigNode) { 802 | configFiles.filter((fileNode: RimeConfigNode) => !fileNode.isCustomFile) 803 | .forEach((fileNode: RimeConfigNode) => { 804 | const fileKey = `${fileNode.key}.yaml`; 805 | let clonedNode = this._cloneTree(fileNode); 806 | clonedNode.collapsibleState = undefined; 807 | folderNode.children.set(fileKey, clonedNode); 808 | folderNode.collapsibleState = TreeItemCollapsibleState.Collapsed; 809 | }); 810 | configFiles.filter((fileNode: RimeConfigNode) => fileNode.isCustomFile) 811 | .forEach((fileNode: RimeConfigNode) => { 812 | const customFileKey = `${fileNode.key}.custom.yaml`; 813 | let clonedNode = this._cloneTree(fileNode); 814 | clonedNode.collapsibleState = undefined; 815 | folderNode.children.set(customFileKey, clonedNode); 816 | folderNode.collapsibleState = TreeItemCollapsibleState.Collapsed; 817 | }); 818 | } 819 | } -------------------------------------------------------------------------------- /src/RimeFileExplorerProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { TreeItem } from 'vscode'; 3 | import { RimeConfigurationTree, RimeConfigNode } from './RimeConfigurationTree'; 4 | 5 | export class RimeFileExplorerProvider implements vscode.TreeDataProvider { 6 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); 7 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 8 | private readonly configurationTree: RimeConfigurationTree; 9 | 10 | constructor(configurationTree: RimeConfigurationTree) { 11 | this.configurationTree = configurationTree; 12 | } 13 | 14 | refresh(): void { 15 | // TODO: only rebuild file nodes. 16 | this.configurationTree.build(); 17 | this._onDidChangeTreeData.fire(undefined); 18 | } 19 | 20 | getTreeItem(element: TreeItem): TreeItem { 21 | return element; 22 | } 23 | 24 | getChildren(element?: TreeItem): vscode.ProviderResult { 25 | // Root node. 26 | if (!element) { 27 | let sharedConfigFolder: TreeItem = this.configurationTree.sharedConfigFolderNode; 28 | sharedConfigFolder.id = this.configurationTree.sharedConfigFolderNode.key; 29 | let userConfigFolder: TreeItem = this.configurationTree.userConfigFolderNode; 30 | userConfigFolder.id = this.configurationTree.userConfigFolderNode.key; 31 | return [sharedConfigFolder, userConfigFolder]; 32 | } else { 33 | if (element.id === this.configurationTree.sharedConfigFolderNode.id) { 34 | return Array.from(this.configurationTree.sharedConfigFolderNode.children.values()); 35 | } else if (element.id === this.configurationTree.userConfigFolderNode.id) { 36 | return Array.from(this.configurationTree.userConfigFolderNode.children.values()); 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/RimeNodeExplorerProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { RimeConfigurationTree, RimeConfigNode, FileKind } from './RimeConfigurationTree'; 3 | 4 | export class RimeNodeExplorerProvider implements vscode.TreeDataProvider { 5 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); 6 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 7 | private readonly configurationTree: RimeConfigurationTree; 8 | private _showOnlyFileKind: FileKind | undefined = undefined; 9 | 10 | constructor(configurationTree: RimeConfigurationTree) { 11 | this.configurationTree = configurationTree; 12 | } 13 | 14 | async refresh(): Promise { 15 | await this.configurationTree.build(); 16 | this._onDidChangeTreeData.fire(undefined); 17 | } 18 | 19 | getTreeItem(element: RimeConfigNode): RimeConfigNode { 20 | return element; 21 | } 22 | 23 | getChildren(element?: RimeConfigNode | undefined): vscode.ProviderResult { 24 | // Root node. 25 | if (!element) { 26 | return Array.from(this.configurationTree.configTree.children.values()) 27 | .filter((childNode: RimeConfigNode) => { 28 | if (this._showOnlyFileKind) { 29 | return childNode.fileKind === this._showOnlyFileKind; 30 | } else { 31 | return true; 32 | } 33 | }); 34 | } else { 35 | if (element.hasChildren) { 36 | return Array.from(element.children.values()); 37 | } else { 38 | return null; 39 | } 40 | } 41 | } 42 | 43 | /** 44 | * Show only given kind of files on the view. 45 | * @param {FileKind | undefined} fileKind The kind of files to show. Show all files when given undefined. 46 | */ 47 | public showOnly(fileKind: FileKind | undefined) { 48 | this._showOnlyFileKind = fileKind; 49 | this._onDidChangeTreeData.fire(undefined); 50 | } 51 | } -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as vscode from 'vscode'; 4 | import { RimeFileExplorerProvider } from './RimeFileExplorerProvider'; 5 | import { RimeNodeExplorerProvider } from './RimeNodeExplorerProvider'; 6 | import { RimeConfigurationTree, RimeConfigNode, FileKind } from './RimeConfigurationTree'; 7 | import path = require('path'); 8 | import { 9 | LanguageClient, 10 | LanguageClientOptions, 11 | ServerOptions, 12 | TransportKind 13 | } from 'vscode-languageclient'; 14 | 15 | let client: LanguageClient; 16 | 17 | export async function activate(context: vscode.ExtensionContext) { 18 | 19 | //================================== RIME LANGUAGE CLIENT START ==========================================// 20 | // The server is implemented in node 21 | let serverModule = context.asAbsolutePath(path.join('server', 'out', 'server.js')); 22 | // The debug options for the server 23 | // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging 24 | let debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] }; 25 | 26 | // If the extension is launched in debug mode then the debug server options are used 27 | // Otherwise the run options are used 28 | let serverOptions: ServerOptions = { 29 | run: { module: serverModule, transport: TransportKind.ipc }, 30 | debug: { 31 | module: serverModule, 32 | transport: TransportKind.ipc, 33 | options: debugOptions 34 | } 35 | }; 36 | 37 | // Options to control the language client 38 | let clientOptions: LanguageClientOptions = { 39 | // Register the server for plain text documents 40 | documentSelector: [{ scheme: 'file', language: 'yaml' }], 41 | synchronize: { 42 | // Notify the server about file changes to '.clientrc files contained in the workspace 43 | fileEvents: vscode.workspace.createFileSystemWatcher('**/*.?(e)y?(a)ml') 44 | } 45 | }; 46 | 47 | // Create the language client and start the client. 48 | const client = new LanguageClient( 49 | 'rimeLanguagerServer', 50 | 'Rime Language Server', 51 | serverOptions, 52 | clientOptions 53 | ); 54 | // Start the client. This will also launch the server 55 | client.start(); 56 | 57 | //==================================== RIME LANGUAGE CLIENT END -=========================================// 58 | 59 | 60 | //======================================= RIME EXPLORER START ============================================// 61 | 62 | const rimeConfigurationTree: RimeConfigurationTree = new RimeConfigurationTree(); 63 | await rimeConfigurationTree.build(); 64 | 65 | vscode.commands.registerCommand( 66 | 'rimebow.openConfigFile', 67 | (node: RimeConfigNode) => { 68 | vscode.window.showTextDocument(vscode.Uri.file(node.configFilePath)) 69 | .then((editor: vscode.TextEditor) => { 70 | const range: vscode.Range = new vscode.Range( 71 | editor.document.positionAt(node.configOffset), 72 | editor.document.positionAt(node.configOffset + node.configLength)); 73 | editor.revealRange(range, vscode.TextEditorRevealType.AtTop); 74 | }); 75 | }); 76 | vscode.commands.registerCommand('rimebow.openFolder', (node: RimeConfigNode) => { 77 | vscode.commands.executeCommand( 78 | 'vscode.openFolder', 79 | vscode.Uri.file(node.configFilePath), 80 | /* forceNewWindow */ true); 81 | }); 82 | const rimeFileExplorerProvider: RimeFileExplorerProvider = new RimeFileExplorerProvider(rimeConfigurationTree); 83 | vscode.commands.registerCommand('rimeFileExplorer.refreshEntry', () => { rimeFileExplorerProvider.refresh(); }); 84 | vscode.window.createTreeView('rimeFileExplorer', { treeDataProvider: rimeFileExplorerProvider }); 85 | 86 | const rimeNodeExplorerProvider: RimeNodeExplorerProvider = new RimeNodeExplorerProvider(rimeConfigurationTree); 87 | vscode.window.createTreeView('rimeNodeExplorer', { treeDataProvider: rimeNodeExplorerProvider }); 88 | vscode.commands.registerCommand('rimeNodeExplorer.refreshEntry', () => { rimeNodeExplorerProvider.refresh(); }); 89 | vscode.commands.registerCommand('rimeNodeExplorer.showOnlySchemaNodes', () => { rimeNodeExplorerProvider.showOnly(FileKind.Schema); }); 90 | vscode.commands.registerCommand('rimeNodeExplorer.showAll', () => { rimeNodeExplorerProvider.showOnly(undefined); }); 91 | 92 | //======================================= RIME EXPLORER END ============================================// 93 | } 94 | 95 | // this method is called when your extension is deactivated 96 | export function deactivate(): Thenable | undefined { 97 | if (!client) { 98 | return undefined; 99 | } 100 | return client.stop(); 101 | } -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from 'vscode-test'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ version: '1.45.0', extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error('Failed to run tests'); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as vscode from 'vscode'; 3 | import { RimeConfigurationTree, RimeConfigNode, ItemKind, FileKind } from '../../RimeConfigurationTree'; 4 | import { YAMLNode } from 'yaml-ast-parser'; 5 | import * as Yaml from 'yaml-ast-parser'; 6 | 7 | class RimeConfigurationTreeForTest extends RimeConfigurationTree { 8 | public async _buildConfigTreeFromFile(filePath: string, fileName: string): Promise { 9 | return super._buildConfigTreeFromFile(filePath, fileName); 10 | } 11 | 12 | public _buildConfigTree(doc: YAMLNode, rootNode: RimeConfigNode, fullPath: string, fileKind: FileKind) { 13 | return super._buildConfigTree(doc, rootNode, fullPath, fileKind); 14 | } 15 | 16 | public _applyPatchesToSharedConfig(sharedConfigTree: RimeConfigNode[], userConfigTree: RimeConfigNode[]) { 17 | return super._applyPatchesToSharedConfig(sharedConfigTree, userConfigTree); 18 | } 19 | 20 | public _mergeTree(treeA: RimeConfigNode, treeB: RimeConfigNode) { 21 | return super._mergeTree(treeA, treeB); 22 | } 23 | 24 | public _cloneTree(tree: RimeConfigNode) { 25 | return super._cloneTree(tree); 26 | } 27 | } 28 | 29 | suite('Extension Test Suite', () => { 30 | vscode.window.showInformationMessage('Start all tests.'); 31 | 32 | test('buildConfigTree_whenObjectTreeIsEmpty_expectNodeTreeBuiltIsNull', () => { 33 | // Arrange. 34 | const FILE_FULL_PATH: string = "C:/foo/bar/baz.yaml"; 35 | const FILE_NAME: string = "baz"; 36 | const FILE_KIND: FileKind = FileKind.Default; 37 | const rimeConfigurationTree: RimeConfigurationTreeForTest = new RimeConfigurationTreeForTest(); 38 | const rootNode: RimeConfigNode = new RimeConfigNode({ key: FILE_NAME, children: new Map(), kind: ItemKind.File, fileKind: FILE_KIND, configFilePath: FILE_FULL_PATH }); 39 | const doc: Yaml.YAMLNode = Yaml.load(''); 40 | let expectedRootNodeBuilt: RimeConfigNode = new RimeConfigNode({ key: FILE_NAME, children: new Map(), kind: ItemKind.Root, configFilePath: FILE_FULL_PATH }); 41 | expectedRootNodeBuilt.collapsibleState = vscode.TreeItemCollapsibleState.None; 42 | 43 | // Act. 44 | rimeConfigurationTree._buildConfigTree(doc, rootNode, FILE_FULL_PATH, FILE_KIND); 45 | 46 | // Assert. 47 | try { 48 | assert(rootNode); 49 | } catch (error) { 50 | assert.fail(`Error occurred during assertion: ${error.message}`); 51 | } 52 | }); 53 | 54 | test('buildConfigTree_whenObjectTreeIsOnlyOneLayerObject_expectNodeTreeBuilt', () => { 55 | // Arrange. 56 | const FILE_FULL_PATH: string = "C:/foo/bar/baz.yaml"; 57 | const FILE_NAME: string = "baz"; 58 | const FILE_KIND: FileKind = FileKind.Default; 59 | const rimeConfigurationTree: RimeConfigurationTreeForTest = new RimeConfigurationTreeForTest(); 60 | const rootNode: RimeConfigNode = new RimeConfigNode({ key: FILE_NAME, children: new Map(), kind: ItemKind.File, fileKind: FILE_KIND, configFilePath: FILE_FULL_PATH }); 61 | const oneLayerObject: string = "a: '1'\nb: 2"; 62 | const doc: Yaml.YAMLNode = Yaml.load(oneLayerObject); 63 | 64 | const expectedChildNodeA: RimeConfigNode = new RimeConfigNode({ 65 | key: 'a', 66 | children: new Map(), 67 | kind: ItemKind.Node, 68 | fileKind: FILE_KIND, 69 | configFilePath: FILE_FULL_PATH, 70 | configOffset: 3, 71 | configLength: 3, 72 | value: '1' }); 73 | const expectedChildNodeB: RimeConfigNode = new RimeConfigNode({ 74 | key: 'b', 75 | children: new Map(), 76 | kind: ItemKind.Node, 77 | fileKind: FILE_KIND, 78 | configFilePath: FILE_FULL_PATH, 79 | configOffset: 10, 80 | configLength: 1, 81 | value: 2 }); 82 | const expectedNodeBuilt: RimeConfigNode = new RimeConfigNode({ 83 | key: FILE_NAME, 84 | children: new Map([['a', expectedChildNodeA], ['b', expectedChildNodeB]]), 85 | configFilePath: FILE_FULL_PATH, 86 | kind: ItemKind.File, 87 | fileKind: FILE_KIND 88 | }); 89 | expectedNodeBuilt.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; 90 | 91 | // Act. 92 | rimeConfigurationTree._buildConfigTree(doc, rootNode, FILE_FULL_PATH, FILE_KIND); 93 | 94 | // Assert. 95 | try { 96 | assert.deepStrictEqual(rootNode, expectedNodeBuilt); 97 | } catch (error) { 98 | assert.fail(`Error occurred during assertion: ${error.message}`); 99 | } 100 | }); 101 | 102 | test('buildConfigTree_whenObjectTreeIsOneLayerObjectWithHexColorValue_expectColorValueStillInHexFormat', () => { 103 | // Arrange. 104 | const FILE_FULL_PATH: string = "C:/foo/bar/baz.yaml"; 105 | const FILE_NAME: string = "baz"; 106 | const FILE_KIND: FileKind = FileKind.Default; 107 | const rimeConfigurationTree: RimeConfigurationTreeForTest = new RimeConfigurationTreeForTest(); 108 | const rootNode: RimeConfigNode = new RimeConfigNode({ key: FILE_NAME, children: new Map(), kind: ItemKind.File, fileKind: FILE_KIND, configFilePath: FILE_FULL_PATH }); 109 | const oneLayerObject: string = "a: '1'\ncolor: 0xFFEE00"; 110 | const doc: Yaml.YAMLNode = Yaml.load(oneLayerObject); 111 | 112 | const expectedChildNodeA: RimeConfigNode = new RimeConfigNode({ 113 | key: 'a', 114 | children: new Map(), 115 | kind: ItemKind.Node, 116 | fileKind: FILE_KIND, 117 | configFilePath: FILE_FULL_PATH, 118 | configOffset: 3, 119 | configLength: 3, 120 | value: '1' 121 | }); 122 | const expectedChildNodeB: RimeConfigNode = new RimeConfigNode({ 123 | key: 'color', 124 | children: new Map(), 125 | kind: ItemKind.Node, 126 | fileKind: FILE_KIND, 127 | configFilePath: FILE_FULL_PATH, 128 | configOffset: 14, 129 | configLength: 8, 130 | value: '0xFFEE00' }); 131 | const expectedNodeBuilt: RimeConfigNode = new RimeConfigNode({ 132 | key: FILE_NAME, 133 | children: new Map([['a', expectedChildNodeA], ['color', expectedChildNodeB]]), 134 | configFilePath: FILE_FULL_PATH, 135 | kind: ItemKind.File, 136 | fileKind: FILE_KIND 137 | }); 138 | expectedNodeBuilt.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; 139 | 140 | // Act. 141 | rimeConfigurationTree._buildConfigTree(doc, rootNode, FILE_FULL_PATH, FILE_KIND); 142 | 143 | // Assert. 144 | try { 145 | assert.deepStrictEqual(rootNode, expectedNodeBuilt); 146 | } catch (error) { 147 | assert.fail(`Error occurred during assertion: ${error.message}`); 148 | } 149 | }); 150 | 151 | test('buildConfigTree_whenObjectTreeIsTwoLayerObject_expectNodeTreeBuilt', () => { 152 | // Arrange. 153 | const FILE_FULL_PATH: string = "C:/foo/bar/baz.yaml"; 154 | const FILE_NAME: string = "baz"; 155 | const FILE_KIND: FileKind = FileKind.Default; 156 | const rimeConfigurationTree: RimeConfigurationTreeForTest = new RimeConfigurationTreeForTest(); 157 | const rootNode: RimeConfigNode = new RimeConfigNode({ key: FILE_NAME, children: new Map(), kind: ItemKind.File, fileKind: FILE_KIND, configFilePath: FILE_FULL_PATH }); 158 | const twoLayerObject: string = "a: 1.5\nb: true\nc:\n c1: 31\n c2: '32'"; 159 | const doc: Yaml.YAMLNode = Yaml.load(twoLayerObject); 160 | 161 | const expectedChildNodeA: RimeConfigNode = new RimeConfigNode({ 162 | key: 'a', 163 | children: new Map(), 164 | kind: ItemKind.Node, 165 | fileKind: FILE_KIND, 166 | configFilePath: FILE_FULL_PATH, 167 | configOffset: 3, 168 | configLength: 3, 169 | value: 1.5 170 | }); 171 | const expectedChildNodeB: RimeConfigNode = new RimeConfigNode({ 172 | key: 'b', 173 | children: new Map(), 174 | kind: ItemKind.Node, 175 | fileKind: FILE_KIND, 176 | configFilePath: FILE_FULL_PATH, 177 | configOffset: 10, 178 | configLength: 4, 179 | value: true 180 | }); 181 | const expectedChildNodeC1: RimeConfigNode = new RimeConfigNode({ 182 | key: 'c1', 183 | children: new Map(), 184 | kind: ItemKind.Node, 185 | fileKind: FILE_KIND, 186 | configFilePath: FILE_FULL_PATH, 187 | configOffset: 24, 188 | configLength: 2, 189 | value: 31 190 | }); 191 | const expectedChildNodeC2: RimeConfigNode = new RimeConfigNode({ 192 | key: 'c2', 193 | children: new Map(), 194 | kind: ItemKind.Node, 195 | fileKind: FILE_KIND, 196 | configFilePath: FILE_FULL_PATH, 197 | configOffset: 33, 198 | configLength: 4, 199 | value: '32' 200 | }); 201 | const expectedChildNodeC: RimeConfigNode = new RimeConfigNode({ 202 | key: 'c', 203 | children: new Map([['c1', expectedChildNodeC1], ['c2', expectedChildNodeC2]]), 204 | configFilePath: FILE_FULL_PATH, 205 | kind: ItemKind.Node, 206 | configOffset: 15, 207 | configLength: 1, 208 | fileKind: FILE_KIND 209 | }); 210 | const expectedNodeBuilt: RimeConfigNode = new RimeConfigNode({ 211 | key: FILE_NAME, 212 | children: new Map([['a', expectedChildNodeA], ['b', expectedChildNodeB], ['c', expectedChildNodeC]]), 213 | configFilePath: FILE_FULL_PATH, 214 | kind: ItemKind.File, 215 | fileKind: FILE_KIND 216 | }); 217 | expectedNodeBuilt.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; 218 | 219 | // Act. 220 | rimeConfigurationTree._buildConfigTree(doc, rootNode, FILE_FULL_PATH, FILE_KIND); 221 | 222 | // Assert. 223 | try { 224 | assert.deepStrictEqual(rootNode, expectedNodeBuilt); 225 | } catch (error) { 226 | assert.fail(`Error occurred during assertion: ${error.message}`); 227 | } 228 | }); 229 | 230 | test('buildConfigTree_whenObjectTreeHasArray_expectNodeTreeBuilt', () => { 231 | // Arrange. 232 | const FILE_FULL_PATH: string = "C:/foo/bar/baz.yaml"; 233 | const FILE_NAME: string = "baz"; 234 | const FILE_KIND: FileKind = FileKind.Default; 235 | const rimeConfigurationTree: RimeConfigurationTreeForTest = new RimeConfigurationTreeForTest(); 236 | const rootNode: RimeConfigNode = new RimeConfigNode({ key: FILE_NAME, children: new Map(), kind: ItemKind.File, fileKind: FILE_KIND, configFilePath: FILE_FULL_PATH }); 237 | const twoLayerObject: string = "a: '1'\nb: 2\nc:\n - c1: 31\n c2: '32'\n - c3: 33"; 238 | const doc: Yaml.YAMLNode = Yaml.load(twoLayerObject); 239 | 240 | const expectedChildNodeA: RimeConfigNode = new RimeConfigNode({ 241 | key: 'a', 242 | children: new Map(), 243 | kind: ItemKind.Node, 244 | fileKind: FILE_KIND, 245 | configFilePath: FILE_FULL_PATH, 246 | configOffset: 3, 247 | configLength: 3, 248 | value: '1' 249 | }); 250 | const expectedChildNodeB: RimeConfigNode = new RimeConfigNode({ 251 | key: 'b', 252 | children: new Map(), 253 | kind: ItemKind.Node, 254 | fileKind: FILE_KIND, 255 | configFilePath: FILE_FULL_PATH, 256 | configOffset: 10, 257 | configLength: 1, 258 | value: 2 259 | }); 260 | const expectedChildNodeC1: RimeConfigNode = new RimeConfigNode({ 261 | key: 'c1', 262 | children: new Map(), 263 | kind: ItemKind.Node, 264 | fileKind: FILE_KIND, 265 | configFilePath: FILE_FULL_PATH, 266 | configOffset: 23, 267 | configLength: 2, 268 | value: 31 269 | }); 270 | const expectedChildNodeC2: RimeConfigNode = new RimeConfigNode({ 271 | key: 'c2', 272 | children: new Map(), 273 | kind: ItemKind.Node, 274 | fileKind: FILE_KIND, 275 | configFilePath: FILE_FULL_PATH, 276 | configOffset: 34, 277 | configLength: 4, 278 | value: '32' 279 | }); 280 | const expectedChildNodeC3: RimeConfigNode = new RimeConfigNode({ 281 | key: 'c3', 282 | children: new Map(), 283 | kind: ItemKind.Node, 284 | fileKind: FILE_KIND, 285 | configFilePath: FILE_FULL_PATH, 286 | configOffset: 47, 287 | configLength: 2, 288 | value: 33 289 | }); 290 | const expectedChildNodeCA0: RimeConfigNode = new RimeConfigNode({ 291 | key: '0', 292 | children: new Map([['c1', expectedChildNodeC1], ['c2', expectedChildNodeC2]]), 293 | configFilePath: FILE_FULL_PATH, 294 | configOffset: 19, 295 | configLength: 19, 296 | kind: ItemKind.Node, 297 | fileKind: FILE_KIND, 298 | isSequenceElement: true 299 | }); 300 | const expectedChildNodeCA1: RimeConfigNode = new RimeConfigNode({ 301 | key: '1', 302 | children: new Map([['c3', expectedChildNodeC3]]), 303 | configFilePath: FILE_FULL_PATH, 304 | configOffset: 43, 305 | configLength: 6, 306 | kind: ItemKind.Node, 307 | fileKind: FILE_KIND, 308 | isSequenceElement: true 309 | }); 310 | const expectedChildNodeC: RimeConfigNode = new RimeConfigNode({ 311 | key: 'c', 312 | children: new Map([['0', expectedChildNodeCA0], ['1', expectedChildNodeCA1]]), 313 | configFilePath: FILE_FULL_PATH, 314 | configOffset: 12, 315 | configLength: 1, 316 | kind: ItemKind.Node, 317 | fileKind: FILE_KIND, 318 | isSequence: true 319 | }); 320 | const expectedNodeBuilt: RimeConfigNode = new RimeConfigNode({ 321 | key: FILE_NAME, 322 | children: new Map([['a', expectedChildNodeA], ['b', expectedChildNodeB], ['c', expectedChildNodeC]]), 323 | configFilePath: FILE_FULL_PATH, 324 | kind: ItemKind.File, 325 | fileKind: FILE_KIND 326 | }); 327 | expectedNodeBuilt.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; 328 | 329 | // Act. 330 | rimeConfigurationTree._buildConfigTree(doc, rootNode, FILE_FULL_PATH, FILE_KIND); 331 | 332 | // Assert. 333 | try { 334 | assert.deepStrictEqual(rootNode, expectedNodeBuilt); 335 | } catch (error) { 336 | assert.fail(`Error occurred during assertion: ${error.message}`); 337 | } 338 | }); 339 | 340 | test('buildConfigTree_whenObjectTreeHasArrayOfLeaves_expectNodeTreeBuilt', () => { 341 | // Arrange. 342 | const FILE_FULL_PATH: string = "C:/foo/bar/baz.yaml"; 343 | const FILE_NAME: string = "baz"; 344 | const FILE_KIND: FileKind = FileKind.Default; 345 | const rimeConfigurationTree: RimeConfigurationTreeForTest = new RimeConfigurationTreeForTest(); 346 | const rootNode: RimeConfigNode = new RimeConfigNode({ key: FILE_NAME, children: new Map(), kind: ItemKind.File, fileKind: FILE_KIND, configFilePath: FILE_FULL_PATH }); 347 | const oneLayerObject: string = "a: '1'\nb: 2\nc:\n - 3\n - 4\n - '5'"; 348 | const doc: YAMLNode = Yaml.load(oneLayerObject); 349 | 350 | const expectedChildNodeA: RimeConfigNode = new RimeConfigNode({ 351 | key: 'a', 352 | children: new Map(), 353 | kind: ItemKind.Node, 354 | fileKind: FILE_KIND, 355 | configFilePath: FILE_FULL_PATH, 356 | configOffset: 3, 357 | configLength: 3, 358 | value: '1' 359 | }); 360 | const expectedChildNodeB: RimeConfigNode = new RimeConfigNode({ key: 'b', 361 | children: new Map(), 362 | kind: ItemKind.Node, 363 | fileKind: FILE_KIND, 364 | configFilePath: FILE_FULL_PATH, 365 | configOffset: 10, 366 | configLength: 1, 367 | value: 2 368 | }); 369 | const expectedChildNodeC1: RimeConfigNode = new RimeConfigNode({ key: '0', 370 | children: new Map(), 371 | kind: ItemKind.Node, 372 | fileKind: FILE_KIND, 373 | configFilePath: FILE_FULL_PATH, 374 | configOffset: 19, 375 | configLength: 1, 376 | value: 3, 377 | isSequenceElement: true 378 | }); 379 | const expectedChildNodeC2: RimeConfigNode = new RimeConfigNode({ key: '1', 380 | children: new Map(), 381 | kind: ItemKind.Node, 382 | fileKind: FILE_KIND, 383 | configFilePath: FILE_FULL_PATH, 384 | configOffset: 25, 385 | configLength: 1, 386 | value: 4, 387 | isSequenceElement: true 388 | }); 389 | const expectedChildNodeC3: RimeConfigNode = new RimeConfigNode({ key: '2', 390 | children: new Map(), 391 | kind: ItemKind.Node, 392 | fileKind: FILE_KIND, 393 | configFilePath: FILE_FULL_PATH, 394 | configOffset: 31, 395 | configLength: 3, 396 | value: '5', 397 | isSequenceElement: true 398 | }); 399 | const expectedChildNodeC: RimeConfigNode = new RimeConfigNode({ 400 | key: 'c', 401 | children: new Map([['0', expectedChildNodeC1], ['1', expectedChildNodeC2], ['2', expectedChildNodeC3]]), 402 | configFilePath: FILE_FULL_PATH, 403 | configOffset: 12, 404 | configLength: 1, 405 | kind: ItemKind.Node, 406 | fileKind: FILE_KIND, 407 | isSequence: true 408 | }); 409 | const expectedNodeBuilt: RimeConfigNode = new RimeConfigNode({ 410 | key: FILE_NAME, 411 | children: new Map([['a', expectedChildNodeA], ['b', expectedChildNodeB], ['c', expectedChildNodeC]]), 412 | configFilePath: FILE_FULL_PATH, 413 | kind: ItemKind.File, 414 | fileKind: FILE_KIND 415 | }); 416 | expectedNodeBuilt.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; 417 | 418 | // Act. 419 | rimeConfigurationTree._buildConfigTree(doc, rootNode, FILE_FULL_PATH, FILE_KIND); 420 | 421 | // Assert. 422 | try { 423 | assert.deepStrictEqual(rootNode, expectedNodeBuilt); 424 | } catch (error) { 425 | assert.fail(`Error occurred during assertion: ${error.message}`); 426 | } 427 | }); 428 | 429 | test('buildConfigTree_whenSlashInKeyWithScalarValue_expectNodeSeparatedBySlash', () => { 430 | // Arrange. 431 | const FILE_FULL_PATH: string = "C:/foo/bar/baz.yaml"; 432 | const FILE_NAME: string = "baz"; 433 | const FILE_KIND: FileKind = FileKind.Default; 434 | const rimeConfigurationTree: RimeConfigurationTreeForTest = new RimeConfigurationTreeForTest(); 435 | const rootNode: RimeConfigNode = new RimeConfigNode({ key: FILE_NAME, children: new Map(), kind: ItemKind.File, fileKind: FILE_KIND, configFilePath: FILE_FULL_PATH }); 436 | const objectWithSlashInKey: string = "a: '1'\nb: 2\nc/c1: 3"; 437 | const doc: YAMLNode = Yaml.load(objectWithSlashInKey); 438 | 439 | const expectedChildNodeA: RimeConfigNode = new RimeConfigNode({ key: 'a', 440 | children: new Map(), 441 | kind: ItemKind.Node, 442 | fileKind: FILE_KIND, 443 | configFilePath: FILE_FULL_PATH, 444 | configOffset: 3, 445 | configLength: 3, 446 | value: '1' 447 | }); 448 | const expectedChildNodeB: RimeConfigNode = new RimeConfigNode({ key: 'b', 449 | children: new Map(), 450 | kind: ItemKind.Node, 451 | fileKind: FILE_KIND, 452 | configFilePath: FILE_FULL_PATH, 453 | configOffset: 10, 454 | configLength: 1, 455 | value: 2 456 | }); 457 | const expectedChildNodeC1: RimeConfigNode = new RimeConfigNode({ key: 'c1', 458 | children: new Map(), 459 | kind: ItemKind.Node, 460 | fileKind: FILE_KIND, 461 | configFilePath: FILE_FULL_PATH, 462 | configOffset: 18, 463 | configLength: 1, 464 | value: 3 465 | }); 466 | const expectedChildNodeC: RimeConfigNode = new RimeConfigNode({ key: 'c', 467 | children: new Map([['c1', 468 | expectedChildNodeC1]]), 469 | fileKind: FILE_KIND, 470 | configFilePath: FILE_FULL_PATH, 471 | configOffset: 12, 472 | configLength: 1, 473 | kind: ItemKind.Node 474 | }); 475 | const expectedNodeBuilt: RimeConfigNode = new RimeConfigNode({ 476 | key: FILE_NAME, 477 | children: new Map([['a', expectedChildNodeA], ['b', expectedChildNodeB], ['c', expectedChildNodeC]]), 478 | configFilePath: FILE_FULL_PATH, 479 | kind: ItemKind.File, 480 | fileKind: FILE_KIND 481 | }); 482 | expectedNodeBuilt.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; 483 | 484 | // Act. 485 | rimeConfigurationTree._buildConfigTree(doc, rootNode, FILE_FULL_PATH, FILE_KIND); 486 | 487 | // Assert. 488 | try { 489 | assert.deepStrictEqual(rootNode, expectedNodeBuilt); 490 | } catch (error) { 491 | assert.fail(`Error occurred during assertion: ${error.message}`); 492 | } 493 | }); 494 | 495 | test('buildConfigTree_whenSlashInKey_expectNodeSeparatedBySlash', () => { 496 | // Arrange. 497 | const FILE_FULL_PATH: string = "C:/foo/bar/baz.yaml"; 498 | const FILE_NAME: string = "baz"; 499 | const FILE_KIND: FileKind = FileKind.Default; 500 | const rimeConfigurationTree: RimeConfigurationTreeForTest = new RimeConfigurationTreeForTest(); 501 | const rootNode: RimeConfigNode = new RimeConfigNode({ key: FILE_NAME, children: new Map(), kind: ItemKind.File, fileKind: FILE_KIND, configFilePath: FILE_FULL_PATH }); 502 | const twoLayerObjectWithSlashInKey: string = "a: '1'\nb: 2\nc/c1:\n c11: 31\n c12: '32'"; 503 | const doc: YAMLNode = Yaml.load(twoLayerObjectWithSlashInKey); 504 | 505 | const expectedChildNodeA: RimeConfigNode = new RimeConfigNode({ key: 'a', 506 | children: new Map(), 507 | kind: ItemKind.Node, 508 | fileKind: FILE_KIND, 509 | configFilePath: FILE_FULL_PATH, 510 | configOffset: 3, 511 | configLength: 3, 512 | value: '1' 513 | }); 514 | const expectedChildNodeB: RimeConfigNode = new RimeConfigNode({ key: 'b', 515 | children: new Map(), 516 | kind: ItemKind.Node, 517 | fileKind: FILE_KIND, 518 | configFilePath: FILE_FULL_PATH, 519 | configOffset: 10, 520 | configLength: 1, 521 | value: 2 522 | }); 523 | const expectedChildNodeC11: RimeConfigNode = new RimeConfigNode({ key: 'c11', 524 | children: new Map(), 525 | kind: ItemKind.Node, 526 | fileKind: FILE_KIND, 527 | configFilePath: FILE_FULL_PATH, 528 | configOffset: 25, 529 | configLength: 2, 530 | value: 31 531 | }); 532 | const expectedChildNodeC12: RimeConfigNode = new RimeConfigNode({ key: 'c12', 533 | children: new Map(), 534 | kind: ItemKind.Node, 535 | fileKind: FILE_KIND, 536 | configFilePath: FILE_FULL_PATH, 537 | configOffset: 35, 538 | configLength: 4, 539 | value: '32' 540 | }); 541 | const expectedChildNodeC1: RimeConfigNode = new RimeConfigNode({ 542 | key: 'c1', 543 | children: new Map([['c11', expectedChildNodeC11], ['c12', expectedChildNodeC12]]), 544 | configFilePath: FILE_FULL_PATH, 545 | configOffset: 14, 546 | configLength: 2, 547 | kind: ItemKind.Node, 548 | fileKind: FILE_KIND, 549 | }); 550 | const expectedChildNodeC: RimeConfigNode = new RimeConfigNode({ 551 | key: 'c', 552 | children: new Map([['c1', expectedChildNodeC1]]), 553 | configFilePath: FILE_FULL_PATH, 554 | configOffset: 12, 555 | configLength: 1, 556 | kind: ItemKind.Node, 557 | fileKind: FILE_KIND 558 | }); 559 | const expectedNodeBuilt: RimeConfigNode = new RimeConfigNode({ 560 | key: FILE_NAME, 561 | children: new Map([['a', expectedChildNodeA], ['b', expectedChildNodeB], ['c', expectedChildNodeC]]), 562 | configFilePath: FILE_FULL_PATH, 563 | kind: ItemKind.File, 564 | fileKind: FILE_KIND 565 | }); 566 | expectedNodeBuilt.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; 567 | 568 | // Act. 569 | rimeConfigurationTree._buildConfigTree(doc, rootNode, FILE_FULL_PATH, FILE_KIND); 570 | 571 | // Assert. 572 | try { 573 | assert.deepStrictEqual(rootNode, expectedNodeBuilt); 574 | } catch (error) { 575 | assert.fail(`Error occurred during assertion: ${error.message}`); 576 | } 577 | }); 578 | 579 | test('buildConfigTree_whenSlashInTwoKeys_expectNodeSeparatedBySlash', () => { 580 | // Arrange. 581 | const FILE_FULL_PATH: string = "C:/foo/bar/baz.yaml"; 582 | const FILE_NAME: string = "baz"; 583 | const FILE_KIND: FileKind = FileKind.Default; 584 | const rimeConfigurationTree: RimeConfigurationTreeForTest = new RimeConfigurationTreeForTest(); 585 | const rootNode: RimeConfigNode = new RimeConfigNode({ key: FILE_NAME, children: new Map(), kind: ItemKind.File, fileKind: FILE_KIND, configFilePath: FILE_FULL_PATH }); 586 | const twoLayerObject: string = "a: '1'\nb: 2\nc/c1:\n c11: 31\n c12: '32'\nd/d1: 4"; 587 | const doc: YAMLNode = Yaml.load(twoLayerObject); 588 | 589 | const expectedChildNodeA: RimeConfigNode = new RimeConfigNode({ key: 'a', 590 | children: new Map(), 591 | kind: ItemKind.Node, 592 | fileKind: FILE_KIND, 593 | configFilePath: FILE_FULL_PATH, 594 | configOffset: 3, 595 | configLength: 3, 596 | value: '1' 597 | }); 598 | const expectedChildNodeB: RimeConfigNode = new RimeConfigNode({ key: 'b', 599 | children: new Map(), 600 | kind: ItemKind.Node, 601 | fileKind: FILE_KIND, 602 | configFilePath: FILE_FULL_PATH, 603 | configOffset: 10, 604 | configLength: 1, 605 | value: 2 606 | }); 607 | const expectedChildNodeC11: RimeConfigNode = new RimeConfigNode({ key: 'c11', 608 | children: new Map(), 609 | kind: ItemKind.Node, 610 | fileKind: FILE_KIND, 611 | configFilePath: FILE_FULL_PATH, 612 | configOffset: 25, 613 | configLength: 2, 614 | value: 31 615 | }); 616 | const expectedChildNodeC12: RimeConfigNode = new RimeConfigNode({ key: 'c12', 617 | children: new Map(), 618 | kind: ItemKind.Node, 619 | fileKind: FILE_KIND, 620 | configFilePath: FILE_FULL_PATH, 621 | configOffset: 35, 622 | configLength: 4, 623 | value: '32' 624 | }); 625 | const expectedChildNodeC1: RimeConfigNode = new RimeConfigNode({ 626 | key: 'c1', 627 | children: new Map([['c11', expectedChildNodeC11], ['c12', expectedChildNodeC12]]), 628 | configFilePath: FILE_FULL_PATH, 629 | configOffset: 14, 630 | configLength: 2, 631 | kind: ItemKind.Node, 632 | fileKind: FILE_KIND 633 | }); 634 | const expectedChildNodeC: RimeConfigNode = new RimeConfigNode({ 635 | key: 'c', 636 | children: new Map([['c1', expectedChildNodeC1]]), 637 | configFilePath: FILE_FULL_PATH, 638 | configOffset: 12, 639 | configLength: 1, 640 | kind: ItemKind.Node, 641 | fileKind: FILE_KIND 642 | }); 643 | const expectedChildNodeD1: RimeConfigNode = new RimeConfigNode({ 644 | key: 'd1', 645 | children: new Map(), 646 | kind: ItemKind.Node, 647 | fileKind: FILE_KIND, 648 | configFilePath: FILE_FULL_PATH, 649 | configOffset: 46, 650 | configLength: 1, 651 | value: 4 652 | }); 653 | const expectedChildNodeD: RimeConfigNode = new RimeConfigNode({ 654 | key: 'd', 655 | children: new Map([['d1', expectedChildNodeD1]]), 656 | configFilePath: FILE_FULL_PATH, 657 | configOffset: 40, 658 | configLength: 1, 659 | kind: ItemKind.Node, 660 | fileKind: FILE_KIND 661 | }); 662 | const expectedNodeBuilt: RimeConfigNode = new RimeConfigNode({ 663 | key: FILE_NAME, 664 | children: new Map([['a', expectedChildNodeA], ['b', expectedChildNodeB], ['c', expectedChildNodeC], ['d', expectedChildNodeD]]), 665 | configFilePath: FILE_FULL_PATH, 666 | kind: ItemKind.File, 667 | fileKind: FILE_KIND 668 | }); 669 | expectedNodeBuilt.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; 670 | 671 | // Act. 672 | rimeConfigurationTree._buildConfigTree(doc, rootNode, FILE_FULL_PATH, FILE_KIND); 673 | 674 | // Assert. 675 | try { 676 | assert.deepStrictEqual(rootNode, expectedNodeBuilt); 677 | } catch (error) { 678 | assert.fail(`Error occurred during assertion: ${error.message}`); 679 | } 680 | }); 681 | 682 | test('buildConfigTree_whenSlashInTwoKeysWithDuplicatePart_expectNodeSeparatedBySlash', () => { 683 | // Arrange. 684 | const FILE_FULL_PATH: string = "C:/foo/bar/baz.yaml"; 685 | const FILE_NAME: string = "baz"; 686 | const FILE_KIND: FileKind = FileKind.Default; 687 | const rimeConfigurationTree: RimeConfigurationTreeForTest = new RimeConfigurationTreeForTest(); 688 | const rootNode: RimeConfigNode = new RimeConfigNode({ key: FILE_NAME, children: new Map(), kind: ItemKind.File, fileKind: FILE_KIND, configFilePath: FILE_FULL_PATH }); 689 | const twoLayerObject: string = "a: '1'\nb: 2\nc/c1:\n c11: 31\n c12: '32'\nc/c2: 4"; 690 | const doc: YAMLNode = Yaml.load(twoLayerObject); 691 | 692 | const expectedChildNodeA: RimeConfigNode = new RimeConfigNode({ key: 'a', 693 | children: new Map(), 694 | kind: ItemKind.Node, 695 | fileKind: FILE_KIND, 696 | configFilePath: FILE_FULL_PATH, 697 | configOffset: 3, 698 | configLength: 3, 699 | value: '1' 700 | }); 701 | const expectedChildNodeB: RimeConfigNode = new RimeConfigNode({ key: 'b', 702 | children: new Map(), 703 | kind: ItemKind.Node, 704 | fileKind: FILE_KIND, 705 | configFilePath: FILE_FULL_PATH, 706 | configOffset: 10, 707 | configLength: 1, 708 | value: 2 709 | }); 710 | const expectedChildNodeC11: RimeConfigNode = new RimeConfigNode({ key: 'c11', 711 | children: new Map(), 712 | kind: ItemKind.Node, 713 | fileKind: FILE_KIND, 714 | configFilePath: FILE_FULL_PATH, 715 | configOffset: 25, 716 | configLength: 2, 717 | value: 31 718 | }); 719 | const expectedChildNodeC12: RimeConfigNode = new RimeConfigNode({ key: 'c12', 720 | children: new Map(), 721 | kind: ItemKind.Node, 722 | fileKind: FILE_KIND, 723 | configFilePath: FILE_FULL_PATH, 724 | configOffset: 35, 725 | configLength: 4, 726 | value: '32' 727 | }); 728 | const expectedChildNodeC1: RimeConfigNode = new RimeConfigNode({ 729 | key: 'c1', 730 | children: new Map([['c11', expectedChildNodeC11], ['c12', expectedChildNodeC12]]), 731 | configFilePath: FILE_FULL_PATH, 732 | configOffset: 14, 733 | configLength: 2, 734 | kind: ItemKind.Node, 735 | fileKind: FILE_KIND 736 | }); 737 | const expectedChildNodeC2: RimeConfigNode = new RimeConfigNode({ key: 'c2', 738 | children: new Map(), 739 | kind: ItemKind.Node, 740 | fileKind: FILE_KIND, 741 | configFilePath: FILE_FULL_PATH, 742 | configOffset: 46, 743 | configLength: 1, 744 | value: 4 745 | }); 746 | const expectedChildNodeC: RimeConfigNode = new RimeConfigNode({ 747 | key: 'c', 748 | children: new Map([['c1', expectedChildNodeC1], ['c2', expectedChildNodeC2]]), 749 | configFilePath: FILE_FULL_PATH, 750 | configOffset: 12, 751 | configLength: 1, 752 | kind: ItemKind.Node, 753 | fileKind: FILE_KIND 754 | }); 755 | const expectedNodeBuilt: RimeConfigNode = new RimeConfigNode({ 756 | key: FILE_NAME, 757 | children: new Map([['a', expectedChildNodeA], ['b', expectedChildNodeB], ['c', expectedChildNodeC]]), 758 | configFilePath: FILE_FULL_PATH, 759 | kind: ItemKind.File, 760 | fileKind: FILE_KIND 761 | }); 762 | expectedNodeBuilt.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; 763 | 764 | // Act. 765 | rimeConfigurationTree._buildConfigTree(doc, rootNode, FILE_FULL_PATH, FILE_KIND); 766 | 767 | // Assert. 768 | try { 769 | assert.deepStrictEqual(rootNode, expectedNodeBuilt); 770 | } catch (error) { 771 | assert.fail(`Error occurred during assertion: ${error.message}`); 772 | } 773 | }); 774 | 775 | test('buildConfigTree_whenTwoSlashesInOneKey_expectNodeSeparatedBySlash', () => { 776 | // Arrange. 777 | const FILE_FULL_PATH: string = "C:/foo/bar/baz.yaml"; 778 | const FILE_NAME: string = "baz"; 779 | const FILE_KIND: FileKind = FileKind.Default; 780 | const rimeConfigurationTree: RimeConfigurationTreeForTest = new RimeConfigurationTreeForTest(); 781 | const rootNode: RimeConfigNode = new RimeConfigNode({ key: FILE_NAME, children: new Map(), kind: ItemKind.File, fileKind: FILE_KIND, configFilePath: FILE_FULL_PATH }); 782 | const twoLayerObject: string = "a: '1'\nb: 2\nc/c1/c11:\n c111: 31\n c112: '32'"; 783 | const doc: YAMLNode = Yaml.load(twoLayerObject); 784 | 785 | const expectedChildNodeA: RimeConfigNode = new RimeConfigNode({ key: 'a', 786 | children: new Map(), 787 | kind: ItemKind.Node, 788 | fileKind: FILE_KIND, 789 | configFilePath: FILE_FULL_PATH, 790 | configOffset: 3, 791 | configLength: 3, 792 | value: '1' 793 | }); 794 | const expectedChildNodeB: RimeConfigNode = new RimeConfigNode({ key: 'b', 795 | children: new Map(), 796 | kind: ItemKind.Node, 797 | fileKind: FILE_KIND, 798 | configFilePath: FILE_FULL_PATH, 799 | configOffset: 10, 800 | configLength: 1, 801 | value: 2 802 | }); 803 | const expectedChildNodeC111: RimeConfigNode = new RimeConfigNode({ key: 'c111', 804 | children: new Map(), 805 | kind: ItemKind.Node, 806 | fileKind: FILE_KIND, 807 | configFilePath: FILE_FULL_PATH, 808 | configOffset: 30, 809 | configLength: 2, 810 | value: 31 811 | }); 812 | const expectedChildNodeC112: RimeConfigNode = new RimeConfigNode({ key: 'c112', 813 | children: new Map(), 814 | kind: ItemKind.Node, 815 | fileKind: FILE_KIND, 816 | configFilePath: FILE_FULL_PATH, 817 | configOffset: 41, 818 | configLength: 4, 819 | value: '32' 820 | }); 821 | const expectedChildNodeC11: RimeConfigNode = new RimeConfigNode({ 822 | key: 'c11', 823 | children: new Map([['c111', expectedChildNodeC111], ['c112', expectedChildNodeC112]]), 824 | configFilePath: FILE_FULL_PATH, 825 | configOffset: 17, 826 | configLength: 3, 827 | kind: ItemKind.Node, 828 | fileKind: FILE_KIND 829 | }); 830 | const expectedChildNodeC1: RimeConfigNode = new RimeConfigNode({ 831 | key: 'c1', 832 | children: new Map([['c11', expectedChildNodeC11]]), 833 | configFilePath: FILE_FULL_PATH, 834 | configOffset: 14, 835 | configLength: 2, 836 | kind: ItemKind.Node, 837 | fileKind: FILE_KIND 838 | }); 839 | const expectedChildNodeC: RimeConfigNode = new RimeConfigNode({ 840 | key: 'c', 841 | children: new Map([['c1', expectedChildNodeC1]]), 842 | configFilePath: FILE_FULL_PATH, 843 | configOffset: 12, 844 | configLength: 1, 845 | kind: ItemKind.Node, 846 | fileKind: FILE_KIND 847 | }); 848 | const expectedNodeBuilt: RimeConfigNode = new RimeConfigNode({ 849 | key: FILE_NAME, 850 | children: new Map([['a', expectedChildNodeA], ['b', expectedChildNodeB], ['c', expectedChildNodeC]]), 851 | configFilePath: FILE_FULL_PATH, 852 | kind: ItemKind.File, 853 | fileKind: FILE_KIND 854 | }); 855 | expectedNodeBuilt.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; 856 | 857 | // Act. 858 | rimeConfigurationTree._buildConfigTree(doc, rootNode, FILE_FULL_PATH, FILE_KIND); 859 | 860 | // Assert. 861 | try { 862 | assert.deepStrictEqual(rootNode, expectedNodeBuilt); 863 | } catch (error) { 864 | assert.fail(`Error occurred during assertion: ${error.message}`); 865 | } 866 | }); 867 | 868 | 869 | test('mergeTree_whenNewNodeInB_expectNewNodeAddedToA', () => { 870 | // Arrange. 871 | const treeA: RimeConfigNode = new RimeConfigNode({key: 'a', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.Default, configFilePath: 'A_FILEPATH'}); 872 | const nodeB1: RimeConfigNode = new RimeConfigNode({key: 'b1', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.Custom, configFilePath: 'B_FILEPATH'}); 873 | const treeB: RimeConfigNode = new RimeConfigNode({key: 'a', children: new Map([['b1', nodeB1]]), kind: ItemKind.Node, fileKind: FileKind.Custom, configFilePath: 'B_FILEPATH'}); 874 | const rimeConfigurationTree: RimeConfigurationTreeForTest = new RimeConfigurationTreeForTest(); 875 | 876 | // Act. 877 | const mergedTree: RimeConfigNode = rimeConfigurationTree._mergeTree(treeA, treeB); 878 | 879 | // Assert. 880 | assert.equal(mergedTree.key, 'a'); 881 | assert.equal(mergedTree.children.size, 1); 882 | assert.ok(mergedTree.children.has('b1')); 883 | assert.equal(mergedTree.children.get('b1')!.key, 'b1'); 884 | assert.equal(mergedTree.children.get('b1')!.configFilePath, treeB.configFilePath); 885 | assert.equal(mergedTree.children.get('b1')!.fileKind, treeB.fileKind); 886 | }); 887 | 888 | test('mergeTree_whenUpdatedNodeInB_expectNodeOverrideInA', () => { 889 | // Arrange. 890 | // treeA: { 1: { 2: 'a' } } 891 | // treeB: { 1: { 2: 'b' } } 892 | const nodeA2: RimeConfigNode = new RimeConfigNode({key: '2', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.Default, configFilePath: 'A_FILEPATH', value: 'a' }); 893 | const treeA: RimeConfigNode = new RimeConfigNode({key: '1', children: new Map([['2', nodeA2]]), configFilePath: 'A_FILEPATH', kind: ItemKind.Node, fileKind: FileKind.Custom }); 894 | const nodeB2: RimeConfigNode = new RimeConfigNode({key: '2', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.Custom, configFilePath: 'B_FILEPATH', value: 'b' }); 895 | const treeB: RimeConfigNode = new RimeConfigNode({key: '1', children: new Map([['2', nodeB2]]), configFilePath: 'B_FILEPATH', kind: ItemKind.Node, fileKind: FileKind.Custom }); 896 | const rimeConfigurationTree: RimeConfigurationTreeForTest = new RimeConfigurationTreeForTest(); 897 | 898 | // Act. 899 | const mergedTree: RimeConfigNode = rimeConfigurationTree._mergeTree(treeA, treeB); 900 | 901 | // Assert. 902 | assert.equal(mergedTree.key, '1'); 903 | assert.equal(mergedTree.children.size, 1); 904 | assert.ok(mergedTree.children.has('2')); 905 | assert.equal(mergedTree.children.get('2')!.value, 'b'); 906 | assert.equal(mergedTree.children.get('2')!.label, '2: b'); 907 | assert.equal(mergedTree.children.get('2')!.configFilePath, nodeB2.configFilePath); 908 | assert.equal(mergedTree.children.get('2')!.fileKind, nodeB2.fileKind); 909 | }); 910 | 911 | test('mergeTree_whenUpdatedArrayInB_expectArrayOverrideInA', () => { 912 | // Arrange. 913 | // treeA: { a: [ a1, a2 ] } 914 | // treeB: { a: [ a2, a3, a4 ] } 915 | // expected: { a: [ a2, a3, a4 ] } 916 | const A_FILE_PATH: string = 'A_FILEPATH'; 917 | const B_FILE_PATH: string = 'B_FILEPBTH'; 918 | const nodeA1: RimeConfigNode = new RimeConfigNode({ key: '0', children: new Map(), configFilePath: A_FILE_PATH, kind: ItemKind.Node, fileKind: FileKind.Default, value: 'a1' }); 919 | const nodeA2: RimeConfigNode = new RimeConfigNode({ key: '1', children: new Map(), configFilePath: A_FILE_PATH, kind: ItemKind.Node, fileKind: FileKind.Default, value: 'a2' }); 920 | const treeA: RimeConfigNode = new RimeConfigNode({key: 'a', children: new Map([['0', nodeA1], ['1', nodeA2]]), kind: ItemKind.Node, fileKind: FileKind.Default, configFilePath: A_FILE_PATH}); 921 | const nodeB1: RimeConfigNode = new RimeConfigNode({ key: '0', children: new Map(), configFilePath: B_FILE_PATH, kind: ItemKind.Node, fileKind: FileKind.Custom, value: 'a2' }); 922 | const nodeB2: RimeConfigNode = new RimeConfigNode({ key: '1', children: new Map(), configFilePath: B_FILE_PATH, kind: ItemKind.Node, fileKind: FileKind.Custom, value: 'a3' }); 923 | const nodeB3: RimeConfigNode = new RimeConfigNode({ key: '2', children: new Map(), configFilePath: B_FILE_PATH, kind: ItemKind.Node, fileKind: FileKind.Custom, value: 'a4' }); 924 | const treeB: RimeConfigNode = new RimeConfigNode({key: 'a', children: new Map([['0', nodeB1], ['1', nodeB2], ['2', nodeB3]]), configFilePath: B_FILE_PATH, kind: ItemKind.Node, fileKind: FileKind.Custom }); 925 | const rimeConfigurationTree: RimeConfigurationTreeForTest = new RimeConfigurationTreeForTest(); 926 | 927 | // Act. 928 | const mergedTree: RimeConfigNode = rimeConfigurationTree._mergeTree(treeA, treeB); 929 | 930 | // Assert. 931 | assert.equal(mergedTree.key, 'a'); 932 | assert.equal(mergedTree.children.size, 3); 933 | assert.ok(mergedTree.children.has('0')); 934 | assert.equal(mergedTree.children.get('0')!.value, 'a2'); 935 | assert.equal(mergedTree.children.get('0')!.configFilePath, nodeB1.configFilePath); 936 | assert.equal(mergedTree.children.get('0')!.fileKind, nodeB1.fileKind); 937 | assert.ok(mergedTree.children.has('1')); 938 | assert.equal(mergedTree.children.get('1')!.value, 'a3'); 939 | assert.equal(mergedTree.children.get('1')!.configFilePath, nodeB2.configFilePath); 940 | assert.equal(mergedTree.children.get('1')!.fileKind, nodeB2.fileKind); 941 | assert.ok(mergedTree.children.has('2')); 942 | assert.equal(mergedTree.children.get('2')!.value, 'a4'); 943 | assert.equal(mergedTree.children.get('2')!.configFilePath, nodeB3.configFilePath); 944 | assert.equal(mergedTree.children.get('2')!.fileKind, nodeB3.fileKind); 945 | }); 946 | 947 | test('applyPatch_whenUserTreeHasPatch_expectNodeUpdatedInMergedTree', () => { 948 | // Arrange. 949 | // sharedConfigTree: { FileA: { 1: 'a' } } 950 | // userConfigTree: { FileA.custom: { 'patch': { 1: 'b' } } } 951 | // expectedMergedTree: { FileA: { 1: 'b' } } 952 | const nodeProgram1: RimeConfigNode = new RimeConfigNode({key: '1', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.Schema, configFilePath: 'ProgramPath/FileA.yaml', value: 'a'}); 953 | const nodeFileA: RimeConfigNode = new RimeConfigNode({key: 'FileA', children: new Map([['1', nodeProgram1]]), configFilePath: 'ProgramPath/FileA.yaml', kind: ItemKind.File, fileKind: FileKind.Schema}); 954 | const sharedConfigFiles: RimeConfigNode[] = [nodeFileA]; 955 | const nodeUser1: RimeConfigNode = new RimeConfigNode({key: '1', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.Custom, configFilePath: 'UserPath/FileA.custom.yaml', value: 'b'}); 956 | const nodeUserPatch: RimeConfigNode = new RimeConfigNode({key: 'patch', children: new Map([['1', nodeUser1]]), configFilePath: 'UserPath/FileA.custom.yaml', kind: ItemKind.Node, fileKind: FileKind.Custom}); 957 | const nodeFileACustom: RimeConfigNode = new RimeConfigNode({key: 'FileA', children: new Map([['patch', nodeUserPatch]]), configFilePath: 'UserPath/FileA.custom.yaml', kind: ItemKind.File, fileKind: FileKind.Custom}); 958 | const userConfigFiles: RimeConfigNode[] = [nodeFileACustom]; 959 | const rimeConfigurationTree: RimeConfigurationTreeForTest = new RimeConfigurationTreeForTest(); 960 | 961 | let expectedFileA1: RimeConfigNode = new RimeConfigNode({key: '1', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.Schema, configFilePath: 'ProgramPath/FileA.yaml', value: 'a'}); 962 | expectedFileA1.update(nodeUser1); 963 | const expectedFileA: RimeConfigNode = new RimeConfigNode({key: 'FileA', children: new Map([['1', expectedFileA1]]), configFilePath: 'ProgramPath/FileA.yaml', kind: ItemKind.File, fileKind: FileKind.Schema}); 964 | const expectedMergedTree: RimeConfigNode = new RimeConfigNode({key: 'PROGRAM', children: new Map([['FileA', expectedFileA]]), configFilePath: 'UserPath', kind: ItemKind.Folder}); 965 | 966 | // Act. 967 | let actualMergedChildren: Map = rimeConfigurationTree._applyPatchesToSharedConfig(sharedConfigFiles, userConfigFiles); 968 | 969 | // Assert. 970 | assert.deepStrictEqual(actualMergedChildren, expectedMergedTree.children); 971 | }); 972 | 973 | test('applyPatch_whenUserTreeHasBothNonCustomConfigAndCustomConfig_expectNodeUpdatedInMergedTree', () => { 974 | // Arrange. 975 | // sharedConfigTree: { FileA: { 1: 'a' } } 976 | // userConfigTree: { FileA: { 1: 'b', 2: '2a' }, FileA.custom: { 'patch': { 1: 'c' } } } 977 | // expectedMergedTree: { FileA: { 1: 'c', 2: '2a' } } 978 | const nodeShared1: RimeConfigNode = new RimeConfigNode({key: '1', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.Schema, configFilePath: 'ProgramPath/FileA.yaml', value: 'a'}); 979 | const nodeSharedFileA: RimeConfigNode = new RimeConfigNode({key: 'FileA', children: new Map([['1', nodeShared1]]), configFilePath: 'ProgramPath/FileA.yaml', kind: ItemKind.File, fileKind: FileKind.Schema}); 980 | const sharedConfigFiles: RimeConfigNode[] = [nodeSharedFileA]; 981 | const nodeUser1: RimeConfigNode = new RimeConfigNode({key: '1', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.Schema, configFilePath: 'UserPath/FileA.yaml', value: 'b'}); 982 | const nodeUser2: RimeConfigNode = new RimeConfigNode({key: '2', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.Schema, configFilePath: 'UserPath/FileA.yaml', value: '2a'}); 983 | const nodeUserFileA: RimeConfigNode = new RimeConfigNode({key: 'FileA', children: new Map([['1', nodeUser1], ['2', nodeUser2]]), configFilePath: 'UserPath/FileA.yaml', kind: ItemKind.File, fileKind: FileKind.Schema}); 984 | const nodeUserCustom1: RimeConfigNode = new RimeConfigNode({key: '1', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.Custom, configFilePath: 'UserPath/FileA.custom.yaml', value: 'c'}); 985 | const nodeUserCustomPatch: RimeConfigNode = new RimeConfigNode({key: 'patch', children: new Map([['1', nodeUserCustom1]]), configFilePath: 'UserPath/FileA.custom.yaml', kind: ItemKind.Node, fileKind: FileKind.Custom}); 986 | const nodeUserFileACustom: RimeConfigNode = new RimeConfigNode({key: 'FileA', children: new Map([['patch', nodeUserCustomPatch]]), configFilePath: 'UserPath/FileA.custom.yaml', kind: ItemKind.File, fileKind: FileKind.Custom}); 987 | const userConfigFiles: RimeConfigNode[] = [nodeUserFileA, nodeUserFileACustom]; 988 | const rimeConfigurationTree: RimeConfigurationTreeForTest = new RimeConfigurationTreeForTest(); 989 | 990 | let expectedFileA1: RimeConfigNode = new RimeConfigNode({key: '1', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.Schema, configFilePath: 'ProgramPath/FileA.yaml', value: 'b'}); 991 | expectedFileA1.update(nodeUserCustom1); 992 | const expectedFileA: RimeConfigNode = new RimeConfigNode({key: 'FileA', children: new Map([['1', expectedFileA1], ['2', nodeUser2]]), configFilePath: 'UserPath/FileA.yaml', kind: ItemKind.File, fileKind: FileKind.Schema}); 993 | const expectedMergedTree: RimeConfigNode = new RimeConfigNode({key: 'PROGRAM', children: new Map([['FileA', expectedFileA]]), configFilePath: 'UserPath', kind: ItemKind.Folder}); 994 | 995 | // Act. 996 | let actualMergedChildren: Map = rimeConfigurationTree._applyPatchesToSharedConfig(sharedConfigFiles, userConfigFiles); 997 | 998 | // Assert. 999 | assert.deepStrictEqual(actualMergedChildren, expectedMergedTree.children); 1000 | }); 1001 | 1002 | test('applyPatch_whenUserTreeHasDefaultConfig_expectSchemaConfigOverrideDefaultConfig', () => { 1003 | // Arrange. 1004 | // sharedConfigTree: { FileA: { 1: 'a' }, default: { 1: 'a' } } 1005 | // userConfigTree: { FileA: { 1: 'b', 2: '2a' }, FileA.custom: { 'patch': { 1: 'c' }, default: { 1: 'd' }, default.custom: { 'patch': { 1: 'e' } } } } 1006 | // expectedMergedTree: { FileA: { 1: 'c', 2: '2a' }, default: { 1: 'e' } } 1007 | const nodeShared1: RimeConfigNode = new RimeConfigNode({key: '1', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.Schema, configFilePath: 'ProgramPath/FileA.yaml', value: 'a'}); 1008 | const nodeSharedFileA: RimeConfigNode = new RimeConfigNode({key: 'FileA', children: new Map([['1', nodeShared1]]), configFilePath: 'ProgramPath/FileA.yaml', kind: ItemKind.File, fileKind: FileKind.Schema}); 1009 | const sharedConfigFiles: RimeConfigNode[] = [nodeSharedFileA]; 1010 | 1011 | const nodeUserDefault1: RimeConfigNode = new RimeConfigNode({key: '1', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.Default, configFilePath: 'UserPath/default.yaml', value: 'd'}); 1012 | const nodeUserDefaultFile: RimeConfigNode = new RimeConfigNode({key: 'default', children: new Map([['1', nodeUserDefault1]]), configFilePath: 'UserPath/default.yaml', kind: ItemKind.File, fileKind: FileKind.Default}); 1013 | 1014 | const nodeUserDefaultCustom1: RimeConfigNode = new RimeConfigNode({key: '1', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.DefaultCustom, configFilePath: 'UserPath/default.custom.yaml', value: 'e'}); 1015 | const nodeUserDefaultCustomPatch: RimeConfigNode = new RimeConfigNode({key: 'patch', children: new Map([['1', nodeUserDefaultCustom1]]), configFilePath: 'UserPath/default.custom.yaml', kind: ItemKind.Node, fileKind: FileKind.DefaultCustom}); 1016 | const nodeUserDefaultCustomFile: RimeConfigNode = new RimeConfigNode({key: 'default', children: new Map([['patch', nodeUserDefaultCustomPatch]]), configFilePath: 'UserPath/default.custom.yaml', kind: ItemKind.File, fileKind: FileKind.DefaultCustom}); 1017 | 1018 | const nodeUser1: RimeConfigNode = new RimeConfigNode({key: '1', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.Schema, configFilePath: 'UserPath/FileA.yaml', value: 'b'}); 1019 | const nodeUser2: RimeConfigNode = new RimeConfigNode({key: '2', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.Schema, configFilePath: 'UserPath/FileA.yaml', value: '2a'}); 1020 | const nodeUserFileA: RimeConfigNode = new RimeConfigNode({key: 'FileA', children: new Map([['1', nodeUser1], ['2', nodeUser2]]), configFilePath: 'UserPath/FileA.yaml', kind: ItemKind.File, fileKind: FileKind.Schema}); 1021 | 1022 | const nodeUserCustom1: RimeConfigNode = new RimeConfigNode({key: '1', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.Custom, configFilePath: 'UserPath/FileA.custom.yaml', value: 'c'}); 1023 | const nodeUserCustomPatch: RimeConfigNode = new RimeConfigNode({key: 'patch', children: new Map([['1', nodeUserCustom1]]), configFilePath: 'UserPath/FileA.custom.yaml', kind: ItemKind.Node, fileKind: FileKind.Custom}); 1024 | const nodeUserFileACustom: RimeConfigNode = new RimeConfigNode({key: 'FileA', children: new Map([['patch', nodeUserCustomPatch]]), configFilePath: 'UserPath/FileA.custom.yaml', kind: ItemKind.File, fileKind: FileKind.Custom}); 1025 | 1026 | const userConfigFiles: RimeConfigNode[] = [nodeUserFileA, nodeUserFileACustom, nodeUserDefaultFile, nodeUserDefaultCustomFile]; 1027 | 1028 | const rimeConfigurationTree: RimeConfigurationTreeForTest = new RimeConfigurationTreeForTest(); 1029 | 1030 | // In expected result, the node FileA is updated from default.custom (the merge result of default.yaml and default.custom.yaml). 1031 | let expectedFileA1: RimeConfigNode = new RimeConfigNode({key: '1', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.Default, configFilePath: 'UserPath/default.yaml', value: 'd'}); 1032 | expectedFileA1.update(nodeUserDefaultCustom1); 1033 | expectedFileA1.update(nodeUserCustom1); 1034 | const expectedDefault1: RimeConfigNode = new RimeConfigNode({key: '1', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.Default, configFilePath: 'UserPath/default.yaml', value: 'd'}); 1035 | expectedDefault1.update(nodeUserDefaultCustom1); 1036 | const expectedDefaultFile: RimeConfigNode = new RimeConfigNode({key: 'default', children: new Map([['1', expectedDefault1]]), configFilePath: 'UserPath/default.yaml', kind: ItemKind.File, fileKind: FileKind.Default}); 1037 | const expectedFileA: RimeConfigNode = new RimeConfigNode({key: 'FileA', children: new Map([['1', expectedFileA1], ['2', nodeUser2]]), configFilePath: 'UserPath/FileA.yaml', kind: ItemKind.File, fileKind: FileKind.Schema}); 1038 | const expectedMergedTree: RimeConfigNode = new RimeConfigNode({key: 'PROGRAM', children: new Map([['FileA', expectedFileA], ['default', expectedDefaultFile]]), configFilePath: 'UserPath', kind: ItemKind.Folder}); 1039 | 1040 | // Act. 1041 | let actualMergedChildren: Map = rimeConfigurationTree._applyPatchesToSharedConfig(sharedConfigFiles, userConfigFiles); 1042 | 1043 | // Assert. 1044 | assert.deepStrictEqual(actualMergedChildren, expectedMergedTree.children); 1045 | }); 1046 | 1047 | test('applyPatch_whenNewFileInUserTree_expectFileAddedInMergedTree', () => { 1048 | // Arrange. 1049 | // sharedConfigTree: { FileA: { 1: 'a' } } 1050 | // userConfigTree: { FileB: { 1: 'b' } } 1051 | // expectedMergedTree: { FileA: { 1: 'a' }, FileB: { 1: 'b' } } 1052 | const nodeProgram1: RimeConfigNode = new RimeConfigNode({key: '1', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.Schema, configFilePath: 'ProgramPath/FileA.yaml', value: 'a'}); 1053 | const nodeFileA: RimeConfigNode = new RimeConfigNode({key: 'FileA', children: new Map([['1', nodeProgram1]]), configFilePath: 'ProgramPath/FileA.yaml', kind: ItemKind.File, fileKind: FileKind.Schema}); 1054 | const sharedConfigFiles: RimeConfigNode[] = [nodeFileA]; 1055 | const nodeUser1: RimeConfigNode = new RimeConfigNode({key: '1', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.Schema, configFilePath: 'UserPath/FileB.yaml', value: 'b'}); 1056 | const nodeFileB: RimeConfigNode = new RimeConfigNode({key: 'FileB', children: new Map([['1', nodeUser1]]), configFilePath: 'UserPath/FileB.yaml', kind: ItemKind.File, fileKind: FileKind.Schema}); 1057 | const userConfigFiles: RimeConfigNode[] = [nodeFileB]; 1058 | const rimeConfigurationTree: RimeConfigurationTreeForTest = new RimeConfigurationTreeForTest(); 1059 | 1060 | const expectedFileA1: RimeConfigNode = new RimeConfigNode({key: '1', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.Schema, configFilePath: 'ProgramPath/FileA.yaml', value: 'a'}); 1061 | const expectedFileA: RimeConfigNode = new RimeConfigNode({key: 'FileA', children: new Map([['1', expectedFileA1]]), configFilePath: 'ProgramPath/FileA.yaml', kind: ItemKind.File, fileKind: FileKind.Schema}); 1062 | const expectedFileB1: RimeConfigNode = new RimeConfigNode({key: '1', children: new Map(), kind: ItemKind.Node, fileKind: FileKind.Schema, configFilePath: 'UserPath/FileB.yaml', value: 'b'}); 1063 | const expectedFileB: RimeConfigNode = new RimeConfigNode({key: 'FileB', children: new Map([['1', expectedFileB1]]), configFilePath: 'UserPath/FileB.yaml', kind: ItemKind.File, fileKind: FileKind.Schema}); 1064 | const expectedMergedTree: RimeConfigNode = new RimeConfigNode({key: 'PROGRAM', children: new Map([['FileA', expectedFileA], ['FileB', expectedFileB]]), configFilePath: 'UserPath', kind: ItemKind.Folder}); 1065 | 1066 | // Act. 1067 | let actualMergedChildren: Map = rimeConfigurationTree._applyPatchesToSharedConfig(sharedConfigFiles, userConfigFiles); 1068 | 1069 | // Assert. 1070 | assert.deepStrictEqual(actualMergedChildren, expectedMergedTree.children); 1071 | }); 1072 | 1073 | test('cloneTree_whenTreeHasChildren_expectTreeCloned', () => { 1074 | // Arrange. 1075 | const rimeConfigurationTree: RimeConfigurationTreeForTest = new RimeConfigurationTreeForTest(); 1076 | const FILE_PATH: string = 'FILE_PATH'; 1077 | // { a: '1' } 1078 | // { a: [ 1, 2 ], b: 3 } 1079 | let childNode1: RimeConfigNode = new RimeConfigNode({ key: '0', children: new Map(), configFilePath: FILE_PATH, value: 1, kind: ItemKind.Node, fileKind: FileKind.Default }); 1080 | let childNode2: RimeConfigNode = new RimeConfigNode({ key: '1', children: new Map(), configFilePath: FILE_PATH, value: 2, kind: ItemKind.Node, fileKind: FileKind.Default }); 1081 | let childNodeA: RimeConfigNode = new RimeConfigNode({ 1082 | key: 'a', 1083 | children: new Map([['0', childNode1], ['1', childNode2]]), 1084 | configFilePath: FILE_PATH, 1085 | kind: ItemKind.Node, 1086 | fileKind: FileKind.Default 1087 | }); 1088 | let childNodeB: RimeConfigNode = new RimeConfigNode({ key: 'b', children: new Map(), configFilePath: FILE_PATH, value: 3, kind: ItemKind.Node, fileKind: FileKind.Default }); 1089 | let tree: RimeConfigNode = new RimeConfigNode({ key: 'ROOT', children: new Map([['a', childNodeA], ['b', childNodeB]]), configFilePath: FILE_PATH, kind: ItemKind.Node, fileKind: FileKind.Default }); 1090 | 1091 | // Act. 1092 | let clonedTree: RimeConfigNode = rimeConfigurationTree._cloneTree(tree); 1093 | childNodeB.value = 4; 1094 | 1095 | // Assert. 1096 | assert.ok(clonedTree.hasChildren); 1097 | assert.equal(clonedTree.children.size, 2); 1098 | assert.ok(clonedTree.children.has('a')); 1099 | assert.equal(clonedTree.children.get('a')!.key, 'a'); 1100 | assert.ok(clonedTree.children.get('a')!.hasChildren); 1101 | assert.equal(clonedTree.children.get('a')!.children.size, 2); 1102 | assert.ok(clonedTree.children.get('a')!.children.has('0')); 1103 | assert.equal(clonedTree.children.get('a')!.children.get('0')!.value, 1); 1104 | assert.ok(clonedTree.children.get('a')!.children.has('1')); 1105 | assert.equal(clonedTree.children.get('a')!.children.get('1')!.value, 2); 1106 | assert.ok(clonedTree.children.has('b')); 1107 | assert.ok(!clonedTree.children.get('b')!.hasChildren); 1108 | // The cloned value should not be changed. 1109 | assert.equal(clonedTree.children.get('b')!.value, 3); 1110 | }); 1111 | }); 1112 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | color: true 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, '..'); 13 | 14 | return new Promise((c, e) => { 15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run(failures => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | e(err); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true /* enable all strict type-checking options */ 12 | /* Additional Checks */ 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | ".vscode-test", 20 | "server" 21 | ], 22 | "references": [ 23 | { 24 | "path": "./server/tsconfig.json" 25 | } 26 | ] 27 | } 28 | --------------------------------------------------------------------------------