├── .changeset ├── README.md ├── config.json ├── few-candles-care.md ├── good-planes-explode.md ├── quick-mails-hope.md └── smooth-numbers-grin.md ├── .github ├── FUNDING.yml └── workflows │ ├── ci.yaml │ └── netlify.yaml ├── .gitignore ├── .npmrc ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── LICENSE ├── README.md ├── extension ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── assets │ ├── logo.png │ ├── screenshot-config.png │ ├── screenshot-md.png │ ├── screenshot-output-panel.png │ ├── screenshot-txt.png │ ├── staturbar.png │ ├── status-checking.png │ ├── status-connected.png │ ├── status-connecting.png │ ├── status-done.png │ ├── status-error.png │ └── status-paused.png ├── package.json ├── privacy-policy.md ├── src │ ├── GrammarlyClient.ts │ ├── StatusBarController.ts │ ├── constants.ts │ ├── index.ts │ ├── interfaces.ts │ └── server.ts └── tsconfig.json ├── fixtures ├── .gitignore ├── .vscode │ └── settings.json ├── readme.md ├── sample.md └── sample.txt ├── jest.config.js ├── package.json ├── packages ├── grammarly-languageclient │ ├── CHANGELOG.md │ ├── LICENSE │ ├── package.json │ ├── readme.md │ ├── src │ │ ├── GrammarlyLanguageClientOptions.ts │ │ ├── index.browser.ts │ │ ├── index.node.ts │ │ ├── index.ts │ │ └── protocol.ts │ └── tsconfig.json ├── grammarly-languageserver │ ├── CHANGELOG.md │ ├── bin │ │ └── server.js │ ├── package-lock.json │ ├── package.json │ ├── readme.md │ ├── src │ │ ├── DOMParser.ts │ │ ├── FileStorage.ts │ │ ├── VirtualStorage.ts │ │ ├── constants.ts │ │ ├── createLanguageServer.ts │ │ ├── global.d.ts │ │ ├── index.browser.ts │ │ ├── index.node.ts │ │ ├── index.ts │ │ ├── interfaces │ │ │ ├── InitializationOptions.ts │ │ │ ├── LanguageName.ts │ │ │ └── Registerable.ts │ │ ├── is.ts │ │ ├── polyfill-fetch.ts │ │ ├── services │ │ │ ├── CodeActionService.ts │ │ │ ├── ConfigurationService.ts │ │ │ ├── DiagnosticsService.ts │ │ │ ├── DocumentService.ts │ │ │ ├── HoverService.ts │ │ │ ├── toArray.ts │ │ │ └── toMarkdown.ts │ │ └── string.ts │ └── tsconfig.json └── grammarly-richtext-encoder │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── Language.ts │ ├── LanguageHTML.ts │ ├── LanguageMarkdown.ts │ └── index.ts │ ├── test │ ├── __snapshots__ │ │ └── markdown.test.ts.snap │ ├── markdown.md │ └── markdown.test.ts │ ├── tsconfig.json │ └── tsconfig.test.json ├── parsers ├── tree-sitter-html.wasm └── tree-sitter-markdown.wasm ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── polyfills ├── empty.js ├── fetch.js └── minimatch-path.js ├── redirect ├── functions │ └── redirect.js ├── index.html └── netlify.toml ├── rollup.config.js ├── scripts ├── build-extension.mjs ├── build-wasm.mjs ├── build-web-extension.mjs └── publish-extension.mjs └── start.cmd /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/master/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.4.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "linked": [ 6 | [ 7 | "grammarly-languageclient", 8 | "grammarly-languageserver" 9 | ] 10 | ], 11 | "access": "public", 12 | "baseBranch": "main", 13 | "updateInternalDependencies": "patch" 14 | } 15 | -------------------------------------------------------------------------------- /.changeset/few-candles-care.md: -------------------------------------------------------------------------------- 1 | --- 2 | 'grammarly-richtext-encoder': patch 3 | --- 4 | 5 | Fix markdown encoding when heading starts with a link 6 | -------------------------------------------------------------------------------- /.changeset/good-planes-explode.md: -------------------------------------------------------------------------------- 1 | --- 2 | 'grammarly': minor 3 | --- 4 | 5 | Extension settings `grammarly.config.suggestions.*` renamed to `grammarly.config.suggestionCategories.*` to align with Grammarly SDK config options. 6 | 7 | The old settings are still supported, but will be removed in the next major release. 8 | -------------------------------------------------------------------------------- /.changeset/quick-mails-hope.md: -------------------------------------------------------------------------------- 1 | --- 2 | 'grammarly-languageclient': patch 3 | 'grammarly-languageserver': patch 4 | 'grammarly-richtext-encoder': patch 5 | --- 6 | 7 | Update Grammarly SDK to v2.3.17 8 | -------------------------------------------------------------------------------- /.changeset/smooth-numbers-grin.md: -------------------------------------------------------------------------------- 1 | --- 2 | 'grammarly-languageserver': patch 3 | --- 4 | 5 | Fix, add missing `grammarly.dismiss` command in language server 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: znck 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | 'on': 3 | pull_request: 4 | branches: 5 | - main 6 | push: 7 | branches: 8 | - main 9 | tags: 10 | - v* 11 | concurrency: 12 | group: build-${{ github.ref }} 13 | cancel-in-progress: true 14 | jobs: 15 | build: 16 | name: Build 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | - name: Cache node packages 22 | uses: actions/cache@v3 23 | env: 24 | cache-name: pnpm-modules 25 | with: 26 | key: >- 27 | ${{ runner.os }}-build-${{ env.cache-name }}-${{ 28 | hashFiles('**/pnpm-lock.yaml') }} 29 | restore-keys: | 30 | ${{ runner.os }}-build-${{ env.cache-name }}- 31 | ${{ runner.os }}-build- 32 | ${{ runner.os }}- 33 | path: | 34 | ~/.pnpm-store 35 | ${{ github.workspace }}/.pnpm 36 | - name: Setup Node 37 | uses: actions/setup-node@v3 38 | with: 39 | node-version: 18 40 | - name: Setup PNPM 41 | uses: pnpm/action-setup@v2 42 | with: 43 | run_install: | 44 | - recursive: true 45 | args: [--frozen-lockfile] 46 | - name: Build 47 | run: pnpm run build 48 | - name: Upload build artefact 49 | uses: actions/upload-artifact@v3 50 | with: 51 | name: build-artefact 52 | retention-days: 30 53 | path: | 54 | ./packages/*/dist 55 | ./extension/dist 56 | build-windows: 57 | name: Build on Windows 58 | runs-on: windows-2022 59 | steps: 60 | - name: Checkout 61 | uses: actions/checkout@v3 62 | - name: Setup Node 63 | uses: actions/setup-node@v3 64 | with: 65 | node-version: 18 66 | - name: Setup PNPM 67 | uses: pnpm/action-setup@v2 68 | with: 69 | run_install: | 70 | - recursive: true 71 | args: [--frozen-lockfile] 72 | - name: Build 73 | run: pnpm run build 74 | 75 | test: 76 | name: Unit Tests 77 | runs-on: ubuntu-latest 78 | needs: build 79 | steps: 80 | - name: Checkout 81 | uses: actions/checkout@v3 82 | - name: Cache node packages 83 | uses: actions/cache@v3 84 | env: 85 | cache-name: pnpm-modules 86 | with: 87 | key: >- 88 | ${{ runner.os }}-build-${{ env.cache-name }}-${{ 89 | hashFiles('**/pnpm-lock.yaml') }} 90 | restore-keys: | 91 | ${{ runner.os }}-build-${{ env.cache-name }}- 92 | ${{ runner.os }}-build- 93 | ${{ runner.os }}- 94 | path: | 95 | ~/.pnpm-store 96 | ${{ github.workspace }}/.pnpm 97 | - name: Setup Node 98 | uses: actions/setup-node@v3 99 | with: 100 | node-version: 18 101 | - name: Setup PNPM 102 | uses: pnpm/action-setup@v2 103 | with: 104 | run_install: | 105 | - recursive: true 106 | args: [--frozen-lockfile] 107 | - name: Download build artefact 108 | uses: actions/download-artifact@v2 109 | with: 110 | name: build-artefact 111 | path: . 112 | - run: pnpm test 113 | pre-release: 114 | name: Pre-release 115 | runs-on: ubuntu-latest 116 | needs: test 117 | concurrency: 118 | group: pre-release 119 | cancel-in-progress: true 120 | environment: Pre Release 121 | if: github.ref == 'refs/heads/main' 122 | steps: 123 | - name: Checkout 124 | uses: actions/checkout@v3 125 | - name: Cache node packages 126 | uses: actions/cache@v3 127 | env: 128 | cache-name: pnpm-modules 129 | with: 130 | key: >- 131 | ${{ runner.os }}-build-${{ env.cache-name }}-${{ 132 | hashFiles('**/pnpm-lock.yaml') }} 133 | restore-keys: | 134 | ${{ runner.os }}-build-${{ env.cache-name }}- 135 | ${{ runner.os }}-build- 136 | ${{ runner.os }}- 137 | path: | 138 | ~/.pnpm-store 139 | ${{ github.workspace }}/.pnpm 140 | - name: Setup Node 141 | uses: actions/setup-node@v3 142 | with: 143 | node-version: 18 144 | - name: Setup PNPM 145 | uses: pnpm/action-setup@v2 146 | with: 147 | run_install: | 148 | - recursive: true 149 | args: [--frozen-lockfile] 150 | - name: Download build artefact 151 | uses: actions/download-artifact@v2 152 | with: 153 | name: build-artefact 154 | path: . 155 | - name: Publish Pre-release Extension 156 | run: > 157 | pnpm recursive --filter ./extension run build 158 | 159 | pnpm recursive --filter ./extension run release 160 | env: 161 | RELEASE_CHANNEL: pre-release 162 | VSCODE_MARKETPLACE_TOKEN: ${{ secrets.VSCE_TOKEN }} 163 | OVSX_REGISTRY_TOKEN: ${{ secrets.OVSX_REGISTRY_TOKEN }} 164 | - name: Publish Pre-release Packages 165 | run: > 166 | pnpm recursive exec -- pnpm version prerelease --preid=next-$(date 167 | +%s) --no-commit-hooks --no-git-tag-version 168 | 169 | echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > .npmrc 170 | 171 | pnpm recursive publish --tag next --access public --no-git-checks 172 | env: 173 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 174 | - uses: marvinpinto/action-automatic-releases@latest 175 | with: 176 | title: Development Build 177 | repo_token: ${{ secrets.GITHUB_TOKEN }} 178 | automatic_release_tag: latest 179 | prerelease: true 180 | files: | 181 | extension/*.vsix 182 | release: 183 | name: Release 184 | runs-on: ubuntu-latest 185 | needs: test 186 | if: startsWith(github.event.ref, 'refs/tags/v') 187 | concurrency: 188 | group: release 189 | cancel-in-progress: false 190 | steps: 191 | - name: Checkout 192 | uses: actions/checkout@v3 193 | - name: Cache node packages 194 | uses: actions/cache@v3 195 | env: 196 | cache-name: pnpm-modules 197 | with: 198 | key: >- 199 | ${{ runner.os }}-build-${{ env.cache-name }}-${{ 200 | hashFiles('**/pnpm-lock.yaml') }} 201 | restore-keys: | 202 | ${{ runner.os }}-build-${{ env.cache-name }}- 203 | ${{ runner.os }}-build- 204 | ${{ runner.os }}- 205 | path: | 206 | ~/.pnpm-store 207 | ${{ github.workspace }}/.pnpm 208 | - name: Setup Node 209 | uses: actions/setup-node@v3 210 | with: 211 | node-version: 18 212 | - name: Setup PNPM 213 | uses: pnpm/action-setup@v2 214 | with: 215 | run_install: | 216 | - recursive: true 217 | args: [--frozen-lockfile] 218 | - name: Download build artefact 219 | uses: actions/download-artifact@v2 220 | with: 221 | name: build-artefact 222 | path: . 223 | - name: Publish Extension 224 | run: > 225 | pnpm recursive --filter ./extension run build 226 | 227 | pnpm recursive --filter ./extension run release 228 | 229 | env: 230 | VSCODE_MARKETPLACE_TOKEN: ${{ secrets.VSCE_TOKEN }} 231 | OVSX_REGISTRY_TOKEN: ${{ secrets.OVSX_REGISTRY_TOKEN }} 232 | continue-on-error: true 233 | - name: Publish Packages 234 | run: | 235 | echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > .npmrc 236 | pnpm recursive publish --tag latest --access public --no-git-checks 237 | env: 238 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 239 | - uses: marvinpinto/action-automatic-releases@latest 240 | with: 241 | repo_token: ${{ secrets.GITHUB_TOKEN }} 242 | prerelease: false 243 | files: | 244 | extension/*.vsix 245 | -------------------------------------------------------------------------------- /.github/workflows/netlify.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy Redirect App 2 | "on": 3 | workflow_dispatch: 4 | concurrency: 5 | group: deploy-${{ github.ref }} 6 | cancel-in-progress: true 7 | jobs: 8 | build: 9 | name: Deploy to Netlify 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Deploy 15 | env: 16 | NETLIFY_SITE_ID: 3a6212c2-e042-41b8-b282-f43f7b05a197 17 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_TOKEN }} 18 | run: npx -y netlify-cli deploy --prod 19 | working-directory: redirect 20 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.vsix 3 | dist/ 4 | .tmp/ 5 | 6 | .log 7 | *.log 8 | 9 | .vscode-test-web/ 10 | 11 | # Local Netlify folder 12 | .netlify 13 | 14 | # OS generated 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | root=true 2 | publish-branch=main 3 | link-workspace-packages = true 4 | strict-engines = true 5 | auto-install-peers = false 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "semi": false, 5 | "printWidth": 120 6 | } 7 | -------------------------------------------------------------------------------- /.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 | 6 | ] 7 | } -------------------------------------------------------------------------------- /.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 | "env": { 14 | "DEBUG": "grammarly:*" 15 | }, 16 | "args": [ 17 | "--disable-extensions", 18 | "--extensionDevelopmentPath=${workspaceFolder}/extension", 19 | "${workspaceFolder}/fixtures" 20 | ], 21 | "outFiles": ["${workspaceFolder}/extension/dist/*.js"], 22 | "sourceMaps": false 23 | }, 24 | { 25 | "name": "Run Web Extension", 26 | "type": "pwa-extensionHost", 27 | "debugWebWorkerHost": true, 28 | "request": "launch", 29 | "runtimeExecutable": "${execPath}", 30 | "env": { 31 | "DEBUG": "grammarly:*" 32 | }, 33 | "args": [ 34 | "--disable-extensions", 35 | "--extensionDevelopmentPath=${workspaceFolder}/extension", 36 | "--extensionDevelopmentKind=web", 37 | "${workspaceFolder}/fixtures" 38 | ], 39 | "outFiles": ["${workspaceFolder}/extension/dist/*.js"] 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "grammarly.selectors": [ 3 | { 4 | "language": "markdown", 5 | "pattern": "**/*.md" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.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": "watch", 9 | "isBackground": true, 10 | 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | src/** 5 | .gitignore 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/tslint.json 9 | **/*.map 10 | **/*.ts 11 | node_modules/** 12 | !node_module/vscode-languageserver -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2022 Rahul Kadyan 4 | Copyright (c) 2021-2022 Jen-Chieh Shen Rahul Kadyan 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) 2 | [![Release](https://img.shields.io/github/release/emacs-grammarly/grammarly-language-server.svg?logo=github)](https://github.com/emacs-grammarly/grammarly-language-server/releases/latest) 3 | [![npm](https://img.shields.io/npm/v/@emacs-grammarly/grammarly-languageserver?logo=npm&color=green)](https://www.npmjs.com/package/@emacs-grammarly/grammarly-languageserver) 4 | [![npm-dt](https://img.shields.io/npm/dt/@emacs-grammarly/grammarly-languageserver.svg)](https://npmcharts.com/compare/@emacs-grammarly/grammarly-languageserver?minimal=true) 5 | [![npm-dm](https://img.shields.io/npm/dm/@emacs-grammarly/grammarly-languageserver.svg)](https://npmcharts.com/compare/@emacs-grammarly/grammarly-languageserver?minimal=true) 6 | 7 | # Grammarly for VS Code 8 | 9 | [![CI/CD](https://github.com/emacs-grammarly/grammarly-language-server/actions/workflows/ci.yaml/badge.svg)](https://github.com/emacs-grammarly/grammarly-language-server/actions/workflows/ci.yaml) 10 | 11 | A language server implementation on top of Grammarly's SDK. 12 | 13 | ## Development Setup 14 | 15 | This project uses [pnpm](https://pnpm.io). 16 | 17 | ```sh 18 | pnpm install 19 | pnpm run build 20 | ``` 21 | 22 | ## Adding support for new language 23 | 24 | 1. Add `"onLanguage:"` to `activationEvents` in [extension/package.json](./extension/package.json) 25 | 2. Add [tree-sitter](https://tree-sitter.github.io/tree-sitter/) grammar 26 | 1. Install tree-sitter grammar package (generally package are named as `tree-sitter-`) 27 | 2. Add the package to the wasm build script: [scripts/build-wasm.mjs](./scripts/build-wasm.mjs) 28 | 3. Add language transformer in the directory 29 | 1. Create `Language.ts` 30 | 2. For reference, check [`LanguageHTML.ts`](./packages/grammarly-richtext-encoder/src/LanguageHTML.ts) 31 | 32 | ## How to get help 33 | 34 | Have a question, or want to provide feedback? Use [repository discussions](https://github.com/znck/grammarly/discussions) to ask questions, share bugs or feedback, or chat with other users. 35 | 36 | ## Older Packages 37 | 38 | `unofficial-grammarly-api`, `unofficial-grammarly-language-client` and `unofficial-grammarly-language-server` are deprecated and archived: https://github.com/znck/grammarly/tree/v0 39 | 40 | ## Support 41 | 42 | This extension is maintained by [Rahul Kadyan](https://github.com/znck). You can [💖 sponsor him](https://github.com/sponsors/znck) for the continued development of this extension. 43 | 44 |

45 | 46 | 47 | 48 |

49 | 50 |
51 |
52 |
53 | -------------------------------------------------------------------------------- /extension/.vscodeignore: -------------------------------------------------------------------------------- 1 | ** 2 | !dist/ 3 | !assets/ 4 | !package.json 5 | !README.md 6 | !CHANGELOG.md 7 | !LICENSE.txt 8 | -------------------------------------------------------------------------------- /extension/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.22.1 4 | 5 | - c2a3108: Update dependencies 6 | 7 | ## 0.22.0 8 | 9 | - ce4c6cb: Add **Files > Include** and **Files > Exclude** setting 10 | - Deprecate setting `grammarly.patterns` in favor of `grammarly.files.include` 11 | - Add `grammarly.files.include` and `grammarly.files.exclude` settings for selecting documents 12 | 13 | ## 0.20.0 14 | 15 | - 2de7e79: Support for connected Grammarly account in web extension (https://github.dev and https://vscode.dev) 16 | - 75fce63: Pause text checking session 17 | - Commands: 18 | - `Grammarly: Pause text check` — Available when the active editor has an active Grammarly session 19 | - `Grammarly: Resume text check` — Available when the active editor has a paused Grammarly session 20 | - `Grammarly: Restart language server` 21 | - Configuration: 22 | - `grammarly.startTextCheckInPausedState` — When enabled, new text checking session is paused initially 23 | 24 | ## 0.18.1 25 | 26 | - c735bc8: Use config from workspace configuration (correctly) 27 | 28 | ## 0.18.0 29 | 30 | - a30aa93: Support for connected Grammarly account 31 | 32 | ## 0.16.0 33 | 34 | - 1b8a750: Use Grammarly SDK 35 | 36 | ## 0.14.0 37 | 38 | - 1ed857d: Show diagnostics in the correct position after accepting fixes 39 | 40 | ## 0.13.0 41 | 42 | - OAuth Support 43 | -------------------------------------------------------------------------------- /extension/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Rahul Kadyan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /extension/README.md: -------------------------------------------------------------------------------- 1 | # Grammarly for VS Code 2 | 3 | This extension brings [Grammarly](https://grammarly.com) to VS Code. 4 | 5 | ## Getting Started 6 | 7 | You need to configure which files should be checked with Grammarly. 8 | 9 | - Set `grammarly.files.include` or **Grammarly > Files > Include** to the allowed list of files. 10 | - Run `grammarly.check` or **Grammarly: Check text** command 11 | 12 | Default configuration: 13 | 14 | ```json 15 | { 16 | "grammarly.files.include": ["**/README.md", "**/readme.md", "**/*.txt"] 17 | } 18 | ``` 19 | 20 | You may use `grammarly.files.exclude` to ignore specific files. 21 | 22 | ## Grammarly account or premium 23 | 24 | Run `grammarly.login` or **Grammarly: Login / Connect your account** command to connect your Grammarly account. 25 | Run `grammarly.logout` or **Grammarly: Log out** to disconnect your account. 26 | 27 | ## Configuration 28 | 29 | Configure dialect, document domain, and which check to include in settings. 30 | 31 | ![](./assets/screenshot-config.png) 32 | 33 | ## Supported Languages 34 | 35 | - plaintext 36 | - markdown (work in progress) — [CommonMark](https://commonmark.org/) 37 | - html (work in progress) 38 | - Many more you can enable by opening Grammarly > Files > Include in settings: 39 | 40 | ![image](https://user-images.githubusercontent.com/120114860/208191020-702c5053-7a97-469e-bde1-bdada021ed90.png) 41 | 42 | 43 | ## Troubleshooting 44 | 45 | The status of the Grammarly text-checking session is displayed on the status bar (bottom right). Clicking on the status bar icon would pause/resume text checking session. 46 | 47 | ![](./assets/staturbar.png) 48 | 49 | | Session | Connecting | Checking | Done | Paused | Error | 50 | | ----------------- | ----------------------------------- | --------------------------------- | ---------------------------------- | ------------------------------- | ------------------------------ | 51 | | Anonymous | ![](./assets/status-connecting.png) | ![](./assets/status-checking.png) | ![](./assets/status-done.png) | ![](./assets/status-paused.png) | ![](./assets/status-error.png) | 52 | | Grammarly Account | ![](./assets/status-connecting.png) | ![](./assets/status-checking.png) | ![](./assets/status-connected.png) | ![](./assets/status-paused.png) | ![](./assets/status-error.png) | 53 | 54 | Check output panel for logs. 55 | 56 | ![](./assets/screenshot-output-panel.png) 57 | 58 | Run `grammarly.restart` or **Grammarly: Restart language server** to restart the text checking service. 59 | 60 | ## How to get help 61 | 62 | Have a question, or want to provide feedback? Use [repository discussions](https://github.com/znck/grammarly/discussions) to ask questions, share bugs or feedback, or chat with other users. 63 | 64 | ## Support 65 | 66 | This extension is maintained by [Rahul Kadyan](https://github.com/znck). You can [💖 sponsor him](https://github.com/sponsors/znck) for the continued development of this extension. 67 | 68 |

69 | 70 | 71 | 72 |

73 | 74 |
75 |
76 |
77 | -------------------------------------------------------------------------------- /extension/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-grammarly/grammarly-language-server/f923374e8da9a62cca50b568876b4b391c408b8f/extension/assets/logo.png -------------------------------------------------------------------------------- /extension/assets/screenshot-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-grammarly/grammarly-language-server/f923374e8da9a62cca50b568876b4b391c408b8f/extension/assets/screenshot-config.png -------------------------------------------------------------------------------- /extension/assets/screenshot-md.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-grammarly/grammarly-language-server/f923374e8da9a62cca50b568876b4b391c408b8f/extension/assets/screenshot-md.png -------------------------------------------------------------------------------- /extension/assets/screenshot-output-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-grammarly/grammarly-language-server/f923374e8da9a62cca50b568876b4b391c408b8f/extension/assets/screenshot-output-panel.png -------------------------------------------------------------------------------- /extension/assets/screenshot-txt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-grammarly/grammarly-language-server/f923374e8da9a62cca50b568876b4b391c408b8f/extension/assets/screenshot-txt.png -------------------------------------------------------------------------------- /extension/assets/staturbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-grammarly/grammarly-language-server/f923374e8da9a62cca50b568876b4b391c408b8f/extension/assets/staturbar.png -------------------------------------------------------------------------------- /extension/assets/status-checking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-grammarly/grammarly-language-server/f923374e8da9a62cca50b568876b4b391c408b8f/extension/assets/status-checking.png -------------------------------------------------------------------------------- /extension/assets/status-connected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-grammarly/grammarly-language-server/f923374e8da9a62cca50b568876b4b391c408b8f/extension/assets/status-connected.png -------------------------------------------------------------------------------- /extension/assets/status-connecting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-grammarly/grammarly-language-server/f923374e8da9a62cca50b568876b4b391c408b8f/extension/assets/status-connecting.png -------------------------------------------------------------------------------- /extension/assets/status-done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-grammarly/grammarly-language-server/f923374e8da9a62cca50b568876b4b391c408b8f/extension/assets/status-done.png -------------------------------------------------------------------------------- /extension/assets/status-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-grammarly/grammarly-language-server/f923374e8da9a62cca50b568876b4b391c408b8f/extension/assets/status-error.png -------------------------------------------------------------------------------- /extension/assets/status-paused.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-grammarly/grammarly-language-server/f923374e8da9a62cca50b568876b4b391c408b8f/extension/assets/status-paused.png -------------------------------------------------------------------------------- /extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "grammarly", 4 | "publisher": "znck", 5 | "displayName": "Grammarly", 6 | "description": "A grammar checking for Visual Studio Code using Grammarly.", 7 | "version": "0.24.0", 8 | "icon": "assets/logo.png", 9 | "preview": true, 10 | "engines": { 11 | "vscode": "^1.63.0" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/znck/grammarly", 16 | "directory": "extension" 17 | }, 18 | "categories": [ 19 | "Other" 20 | ], 21 | "keywords": [ 22 | "Grammarly", 23 | "grammar", 24 | "spellcheck", 25 | "writing assistant", 26 | "writing" 27 | ], 28 | "activationEvents": [ 29 | "onCommand:grammarly.check", 30 | "onCommand:grammarly.login", 31 | "onCommand:grammarly.logout", 32 | "onLanguage:plaintext", 33 | "onLanguage:markdown", 34 | "onLanguage:html" 35 | ], 36 | "contributes": { 37 | "configuration": { 38 | "title": "Grammarly", 39 | "properties": { 40 | "grammarly.patterns": { 41 | "type": "array", 42 | "description": "A glob pattern, like `*.{md,txt}` for file scheme.", 43 | "items": { 44 | "type": "string" 45 | }, 46 | "default": [ 47 | "**/readme.md", 48 | "**/README.md", 49 | "**/*.txt" 50 | ], 51 | "required": true, 52 | "scope": "window", 53 | "order": 0, 54 | "markdownDeprecationMessage": "Use [Files: Include](#grammarly.files.include#)" 55 | }, 56 | "grammarly.files.include": { 57 | "type": "array", 58 | "markdownDescription": "Configure [glob patterns](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options) for including files and folders.", 59 | "items": { 60 | "type": "string" 61 | }, 62 | "default": [ 63 | "**/readme.md", 64 | "**/README.md", 65 | "**/*.txt" 66 | ], 67 | "required": true, 68 | "scope": "window", 69 | "order": 1 70 | }, 71 | "grammarly.files.exclude": { 72 | "type": "array", 73 | "markdownDescription": "Configure [glob patterns](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options) for excluding files and folders.", 74 | "items": { 75 | "type": "string" 76 | }, 77 | "default": [], 78 | "required": true, 79 | "scope": "window", 80 | "order": 2 81 | }, 82 | "grammarly.selectors": { 83 | "type": "array", 84 | "description": "Filter documents to be checked with Grammarly.", 85 | "items": { 86 | "type": "object", 87 | "properties": { 88 | "scheme": { 89 | "type": "string", 90 | "description": "A Uri scheme, like `file` or `untitled`." 91 | }, 92 | "language": { 93 | "type": "string", 94 | "description": "A language id, like `typescript`." 95 | }, 96 | "pattern": { 97 | "type": "string", 98 | "description": "A glob pattern, like `*.{md,txt}`." 99 | } 100 | } 101 | }, 102 | "default": [], 103 | "required": true, 104 | "scope": "window", 105 | "order": 99 106 | }, 107 | "grammarly.startTextCheckInPausedState": { 108 | "type": "boolean", 109 | "description": "Start text checking session in paused state", 110 | "default": false 111 | }, 112 | "grammarly.config.documentDialect": { 113 | "markdownDescription": "Specific variety of English being written. See [this article](https://support.grammarly.com/hc/en-us/articles/115000089992-Select-between-British-English-American-English-Canadian-English-and-Australian-English) for differences.", 114 | "enum": [ 115 | "american", 116 | "australian", 117 | "british", 118 | "canadian", 119 | "auto-text" 120 | ], 121 | "enumDescriptions": [ 122 | "", 123 | "", 124 | "", 125 | "", 126 | "An appropriate value based on the text." 127 | ], 128 | "default": "auto-text", 129 | "scope": "language-overridable" 130 | }, 131 | "grammarly.config.documentDomain": { 132 | "markdownDescription": "The style or type of writing to be checked. See [What is domain/document type](https://support.grammarly.com/hc/en-us/articles/115000091472-What-is-domain-document-type-)?", 133 | "enum": [ 134 | "academic", 135 | "business", 136 | "general", 137 | "mail", 138 | "casual", 139 | "creative" 140 | ], 141 | "enumDescriptions": [ 142 | "Academic is the strictest style of writing. On top of catching grammar and punctuation issues, Grammarly will make suggestions around passive voice, contractions, and informal pronouns (I, you), and will point out unclear antecedents (e.g., sentences starting with “This is…”).", 143 | "The business style setting checks the text against formal writing criteria. However, unlike the Academic domain, it allows the use of some informal expressions, informal pronouns, and unclear antecedents.", 144 | "This is the default style and uses a medium level of strictness.", 145 | "The email genre is similar to the General domain and helps ensure that your email communication is engaging. In addition to catching grammar, spelling, and punctuation mistakes, Grammarly also points out the use of overly direct language that may sound harsh to a reader.", 146 | "Casual is designed for informal types of writing and ignores most style issues. It does not flag contractions, passive voice, informal pronouns, who-versus-whom usage, split infinitives, or run-on sentences. This style is suitable for personal communication.", 147 | "This is the most permissive style. It catches grammar, punctuation, and spelling mistakes but allows some leeway for those who want to intentionally bend grammar rules to achieve certain effects. Creative doesn’t flag sentence fragments (missing subjects or verbs), wordy sentences, colloquialisms, informal pronouns, passive voice, incomplete comparisons, or run-on sentences." 148 | ], 149 | "default": "general", 150 | "scope": "language-overridable" 151 | }, 152 | "grammarly.config.suggestionCategories.conjugationAtStartOfSentence": { 153 | "description": "Flags use of conjunctions such as \"but\" and \"and\" at the beginning of sentences.", 154 | "enum": [ 155 | "on", 156 | "off" 157 | ], 158 | "default": "off", 159 | "scope": "language-overridable" 160 | }, 161 | "grammarly.config.suggestionCategories.fluency": { 162 | "description": "Suggests ways to sound more natural and fluent.", 163 | "enum": [ 164 | "on", 165 | "off" 166 | ], 167 | "default": "on", 168 | "scope": "language-overridable" 169 | }, 170 | "grammarly.config.suggestionCategories.informalPronounsAcademic": { 171 | "description": "Flags use of personal pronouns such as \"I\" and \"you\" in academic writing.", 172 | "enum": [ 173 | "on", 174 | "off" 175 | ], 176 | "default": "off", 177 | "scope": "language-overridable" 178 | }, 179 | "grammarly.config.suggestionCategories.missingSpaces": { 180 | "description": "Suggests adding missing spacing after a numeral when writing times.", 181 | "enum": [ 182 | "on", 183 | "off" 184 | ], 185 | "default": "on", 186 | "scope": "language-overridable" 187 | }, 188 | "grammarly.config.suggestionCategories.nounStrings": { 189 | "description": "Flags a series of nouns that modify a final noun.", 190 | "enum": [ 191 | "on", 192 | "off" 193 | ], 194 | "default": "on", 195 | "scope": "language-overridable" 196 | }, 197 | "grammarly.config.suggestionCategories.numbersBeginningSentences": { 198 | "description": "Suggests spelling out numbers at the beginning of sentences.", 199 | "enum": [ 200 | "on", 201 | "off" 202 | ], 203 | "default": "on", 204 | "scope": "language-overridable" 205 | }, 206 | "grammarly.config.suggestionCategories.numbersZeroThroughTen": { 207 | "description": "Suggests spelling out numbers zero through ten.", 208 | "enum": [ 209 | "on", 210 | "off" 211 | ], 212 | "default": "on", 213 | "scope": "language-overridable" 214 | }, 215 | "grammarly.config.suggestionCategories.oxfordComma": { 216 | "description": "Suggests adding the Oxford comma after the second-to-last item in a list of things.", 217 | "enum": [ 218 | "on", 219 | "off" 220 | ], 221 | "default": "off", 222 | "scope": "language-overridable" 223 | }, 224 | "grammarly.config.suggestionCategories.passiveVoice": { 225 | "description": "Flags use of passive voice.", 226 | "enum": [ 227 | "on", 228 | "off" 229 | ], 230 | "default": "off", 231 | "scope": "language-overridable" 232 | }, 233 | "grammarly.config.suggestionCategories.personFirstLanguage": { 234 | "description": "Suggests using person-first language to refer respectfully to an individual with a disability.", 235 | "enum": [ 236 | "on", 237 | "off" 238 | ], 239 | "default": "on", 240 | "scope": "language-overridable" 241 | }, 242 | "grammarly.config.suggestionCategories.possiblyBiasedLanguageAgeRelated": { 243 | "description": "Suggests alternatives to potentially biased language related to older adults.", 244 | "enum": [ 245 | "on", 246 | "off" 247 | ], 248 | "default": "on", 249 | "scope": "language-overridable" 250 | }, 251 | "grammarly.config.suggestionCategories.possiblyBiasedLanguageDisabilityRelated": { 252 | "description": "Suggests alternatives to potentially ableist language.", 253 | "enum": [ 254 | "on", 255 | "off" 256 | ], 257 | "default": "on", 258 | "scope": "language-overridable" 259 | }, 260 | "grammarly.config.suggestionCategories.possiblyBiasedLanguageFamilyRelated": { 261 | "description": "Suggests alternatives to potentially biased language related to parenting and family systems.", 262 | "enum": [ 263 | "on", 264 | "off" 265 | ], 266 | "default": "on", 267 | "scope": "language-overridable" 268 | }, 269 | "grammarly.config.suggestionCategories.possiblyBiasedLanguageGenderRelated": { 270 | "description": "Suggests alternatives to potentially gender-biased and non-inclusive phrasing.", 271 | "enum": [ 272 | "on", 273 | "off" 274 | ], 275 | "default": "on", 276 | "scope": "language-overridable" 277 | }, 278 | "grammarly.config.suggestionCategories.possiblyBiasedLanguageHumanRights": { 279 | "description": "Suggests alternatives to language related to human slavery.", 280 | "enum": [ 281 | "on", 282 | "off" 283 | ], 284 | "default": "on", 285 | "scope": "language-overridable" 286 | }, 287 | "grammarly.config.suggestionCategories.possiblyBiasedLanguageHumanRightsRelated": { 288 | "description": "Suggests alternatives to terms with origins in the institution of slavery.", 289 | "enum": [ 290 | "on", 291 | "off" 292 | ], 293 | "default": "on", 294 | "scope": "language-overridable" 295 | }, 296 | "grammarly.config.suggestionCategories.possiblyBiasedLanguageLGBTQIARelated": { 297 | "description": "Flags LGBTQIA+-related terms that may be seen as biased, outdated, or disrespectful in some contexts.", 298 | "enum": [ 299 | "on", 300 | "off" 301 | ], 302 | "default": "on", 303 | "scope": "language-overridable" 304 | }, 305 | "grammarly.config.suggestionCategories.possiblyBiasedLanguageRaceEthnicityRelated": { 306 | "description": "Suggests alternatives to potentially biased language related to race and ethnicity.", 307 | "enum": [ 308 | "on", 309 | "off" 310 | ], 311 | "default": "on", 312 | "scope": "language-overridable" 313 | }, 314 | "grammarly.config.suggestionCategories.possiblyPoliticallyIncorrectLanguage": { 315 | "description": "Suggests alternatives to language that may be considered politically incorrect.", 316 | "enum": [ 317 | "on", 318 | "off" 319 | ], 320 | "default": "on", 321 | "scope": "language-overridable" 322 | }, 323 | "grammarly.config.suggestionCategories.prepositionAtTheEndOfSentence": { 324 | "description": "Flags use of prepositions such as \"with\" and \"in\" at the end of sentences.", 325 | "enum": [ 326 | "on", 327 | "off" 328 | ], 329 | "default": "off", 330 | "scope": "language-overridable" 331 | }, 332 | "grammarly.config.suggestionCategories.punctuationWithQuotation": { 333 | "description": "Suggests placing punctuation before closing quotation marks.", 334 | "enum": [ 335 | "on", 336 | "off" 337 | ], 338 | "default": "on", 339 | "scope": "language-overridable" 340 | }, 341 | "grammarly.config.suggestionCategories.readabilityFillerWords": { 342 | "description": "Flags long, complicated sentences that could potentially confuse your reader.", 343 | "enum": [ 344 | "on", 345 | "off" 346 | ], 347 | "default": "on", 348 | "scope": "language-overridable" 349 | }, 350 | "grammarly.config.suggestionCategories.readabilityTransforms": { 351 | "description": "Suggests splitting long, complicated sentences that could potentially confuse your reader.", 352 | "enum": [ 353 | "on", 354 | "off" 355 | ], 356 | "default": "on", 357 | "scope": "language-overridable" 358 | }, 359 | "grammarly.config.suggestionCategories.sentenceVariety": { 360 | "description": "Flags series of sentences that follow the same pattern.", 361 | "enum": [ 362 | "on", 363 | "off" 364 | ], 365 | "default": "on", 366 | "scope": "language-overridable" 367 | }, 368 | "grammarly.config.suggestionCategories.spacesSurroundingSlash": { 369 | "description": "Suggests removing extra spaces surrounding a slash.", 370 | "enum": [ 371 | "on", 372 | "off" 373 | ], 374 | "default": "on", 375 | "scope": "language-overridable" 376 | }, 377 | "grammarly.config.suggestionCategories.splitInfinitive": { 378 | "description": "Suggests rewriting split infinitives so that an adverb doesn't come between \"to\" and the verb.", 379 | "enum": [ 380 | "on", 381 | "off" 382 | ], 383 | "default": "on", 384 | "scope": "language-overridable" 385 | }, 386 | "grammarly.config.suggestionCategories.stylisticFragments": { 387 | "description": "Suggests completing all incomplete sentences, including stylistic sentence fragments that may be intentional.", 388 | "enum": [ 389 | "on", 390 | "off" 391 | ], 392 | "default": "off", 393 | "scope": "language-overridable" 394 | }, 395 | "grammarly.config.suggestionCategories.unnecessaryEllipses": { 396 | "description": "Flags unnecessary use of ellipses (...).", 397 | "enum": [ 398 | "on", 399 | "off" 400 | ], 401 | "default": "off", 402 | "scope": "language-overridable" 403 | }, 404 | "grammarly.config.suggestionCategories.variety": { 405 | "description": "Suggests alternatives to words that occur frequently in the same paragraph.", 406 | "enum": [ 407 | "on", 408 | "off" 409 | ], 410 | "default": "on", 411 | "scope": "language-overridable" 412 | }, 413 | "grammarly.config.suggestionCategories.vocabulary": { 414 | "description": "Suggests alternatives to bland and overused words such as \"good\" and \"nice\".", 415 | "enum": [ 416 | "on", 417 | "off" 418 | ], 419 | "default": "on", 420 | "scope": "language-overridable" 421 | }, 422 | "grammarly.config.suggestions.ConjunctionAtStartOfSentence": { 423 | "description": "Flags use of conjunctions such as 'but' and 'and' at the beginning of sentences.", 424 | "enum": [ 425 | true, 426 | false, 427 | null 428 | ], 429 | "default": null, 430 | "scope": "language-overridable", 431 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.conjunctionAtStartOfSentence` instead." 432 | }, 433 | "grammarly.config.suggestions.Fluency": { 434 | "description": "Suggests ways to sound more natural and fluent.", 435 | "enum": [ 436 | true, 437 | false, 438 | null 439 | ], 440 | "default": null, 441 | "scope": "language-overridable", 442 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.fluency` instead." 443 | }, 444 | "grammarly.config.suggestions.InformalPronounsAcademic": { 445 | "description": "Flags use of personal pronouns such as 'I' and 'you' in academic writing.", 446 | "enum": [ 447 | true, 448 | false, 449 | null 450 | ], 451 | "default": null, 452 | "scope": "language-overridable", 453 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.informalPronounsAcademic` instead." 454 | }, 455 | "grammarly.config.suggestions.MissingSpaces": { 456 | "description": "Suggests adding missing spacing after a numeral when writing times.", 457 | "enum": [ 458 | true, 459 | false, 460 | null 461 | ], 462 | "default": null, 463 | "scope": "language-overridable", 464 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.missingSpaces` instead." 465 | }, 466 | "grammarly.config.suggestions.NounStrings": { 467 | "description": "Flags a series of nouns that modify a final noun.", 468 | "enum": [ 469 | true, 470 | false, 471 | null 472 | ], 473 | "default": null, 474 | "scope": "language-overridable", 475 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.nounStrings` instead." 476 | }, 477 | "grammarly.config.suggestions.NumbersBeginningSentences": { 478 | "description": "Suggests spelling out numbers at the beginning of sentences.", 479 | "enum": [ 480 | true, 481 | false, 482 | null 483 | ], 484 | "default": null, 485 | "scope": "language-overridable", 486 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.numbersBeginningSentences` instead." 487 | }, 488 | "grammarly.config.suggestions.NumbersZeroThroughTen": { 489 | "description": "Suggests spelling out numbers zero through ten.", 490 | "enum": [ 491 | true, 492 | false, 493 | null 494 | ], 495 | "default": null, 496 | "scope": "language-overridable", 497 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.numbersZeroThroughTen` instead." 498 | }, 499 | "grammarly.config.suggestions.OxfordComma": { 500 | "description": "Suggests adding the Oxford comma after the second-to-last item in a list of things.", 501 | "enum": [ 502 | true, 503 | false, 504 | null 505 | ], 506 | "default": null, 507 | "scope": "language-overridable", 508 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.oxfordComma` instead." 509 | }, 510 | "grammarly.config.suggestions.PassiveVoice": { 511 | "description": "Flags use of passive voice.", 512 | "enum": [ 513 | true, 514 | false, 515 | null 516 | ], 517 | "default": null, 518 | "scope": "language-overridable", 519 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.passiveVoice` instead." 520 | }, 521 | "grammarly.config.suggestions.PersonFirstLanguage": { 522 | "description": "Suggests using person-first language to refer respectfully to an individual with a disability.", 523 | "enum": [ 524 | true, 525 | false, 526 | null 527 | ], 528 | "default": null, 529 | "scope": "language-overridable", 530 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.personFirstLanguage` instead." 531 | }, 532 | "grammarly.config.suggestions.PossiblyBiasedLanguageAgeRelated": { 533 | "description": "Suggests alternatives to potentially biased language related to older adults.", 534 | "enum": [ 535 | true, 536 | false, 537 | null 538 | ], 539 | "default": null, 540 | "scope": "language-overridable", 541 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.possiblyBiasedLanguageAgeRelated` instead." 542 | }, 543 | "grammarly.config.suggestions.PossiblyBiasedLanguageDisabilityRelated": { 544 | "description": "Suggests alternatives to potentially ableist language.", 545 | "enum": [ 546 | true, 547 | false, 548 | null 549 | ], 550 | "default": null, 551 | "scope": "language-overridable", 552 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.possiblyBiasedLanguageDisabilityRelated` instead." 553 | }, 554 | "grammarly.config.suggestions.PossiblyBiasedLanguageFamilyRelated": { 555 | "description": "Suggests alternatives to potentially biased language related to parenting and family systems.", 556 | "enum": [ 557 | true, 558 | false, 559 | null 560 | ], 561 | "default": null, 562 | "scope": "language-overridable", 563 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.possiblyBiasedLanguageFamilyRelated` instead." 564 | }, 565 | "grammarly.config.suggestions.PossiblyBiasedLanguageGenderRelated": { 566 | "description": "Suggests alternatives to potentially gender-biased and non-inclusive phrasing.", 567 | "enum": [ 568 | true, 569 | false, 570 | null 571 | ], 572 | "default": null, 573 | "scope": "language-overridable", 574 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.possiblyBiasedLanguageGenderRelated` instead." 575 | }, 576 | "grammarly.config.suggestions.PossiblyBiasedLanguageHumanRights": { 577 | "description": "Suggests alternatives to language related to human slavery.", 578 | "enum": [ 579 | true, 580 | false, 581 | null 582 | ], 583 | "default": null, 584 | "scope": "language-overridable", 585 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.possiblyBiasedLanguageHumanRights` instead." 586 | }, 587 | "grammarly.config.suggestions.PossiblyBiasedLanguageHumanRightsRelated": { 588 | "description": "Suggests alternatives to terms with origins in the institution of slavery.", 589 | "enum": [ 590 | true, 591 | false, 592 | null 593 | ], 594 | "default": null, 595 | "scope": "language-overridable", 596 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.possiblyBiasedLanguageHumanRightsRelated` instead." 597 | }, 598 | "grammarly.config.suggestions.PossiblyBiasedLanguageLgbtqiaRelated": { 599 | "description": "Flags LGBTQIA+-related terms that may be seen as biased, outdated, or disrespectful in some contexts.", 600 | "enum": [ 601 | true, 602 | false, 603 | null 604 | ], 605 | "default": null, 606 | "scope": "language-overridable", 607 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.possiblyBiasedLanguageLgbtqiaRelated` instead." 608 | }, 609 | "grammarly.config.suggestions.PossiblyBiasedLanguageRaceEthnicityRelated": { 610 | "description": "Suggests alternatives to potentially biased language related to race and ethnicity.", 611 | "enum": [ 612 | true, 613 | false, 614 | null 615 | ], 616 | "default": null, 617 | "scope": "language-overridable", 618 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.possiblyBiasedLanguageRaceEthnicityRelated` instead." 619 | }, 620 | "grammarly.config.suggestions.PossiblyPoliticallyIncorrectLanguage": { 621 | "description": "Suggests alternatives to language that may be considered politically incorrect.", 622 | "enum": [ 623 | true, 624 | false, 625 | null 626 | ], 627 | "default": null, 628 | "scope": "language-overridable", 629 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.possiblyPoliticallyIncorrectLanguage` instead." 630 | }, 631 | "grammarly.config.suggestions.PrepositionAtTheEndOfSentence": { 632 | "description": "Flags use of prepositions such as 'with' and 'in' at the end of sentences.", 633 | "enum": [ 634 | true, 635 | false, 636 | null 637 | ], 638 | "default": null, 639 | "scope": "language-overridable", 640 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.prepositionAtTheEndOfSentence` instead." 641 | }, 642 | "grammarly.config.suggestions.PunctuationWithQuotation": { 643 | "description": "Suggests placing punctuation before closing quotation marks.", 644 | "enum": [ 645 | true, 646 | false, 647 | null 648 | ], 649 | "default": null, 650 | "scope": "language-overridable", 651 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.punctuationWithQuotation` instead." 652 | }, 653 | "grammarly.config.suggestions.ReadabilityFillerwords": { 654 | "description": "Flags long, complicated sentences that could potentially confuse your reader.", 655 | "enum": [ 656 | true, 657 | false, 658 | null 659 | ], 660 | "default": null, 661 | "scope": "language-overridable", 662 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.readabilityFillerwords` instead." 663 | }, 664 | "grammarly.config.suggestions.ReadabilityTransforms": { 665 | "description": "Suggests splitting long, complicated sentences that could potentially confuse your reader.", 666 | "enum": [ 667 | true, 668 | false, 669 | null 670 | ], 671 | "default": null, 672 | "scope": "language-overridable", 673 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.readabilityTransforms` instead." 674 | }, 675 | "grammarly.config.suggestions.SentenceVariety": { 676 | "description": "Flags series of sentences that follow the same pattern.", 677 | "enum": [ 678 | true, 679 | false, 680 | null 681 | ], 682 | "default": null, 683 | "scope": "language-overridable", 684 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.sentenceVariety` instead." 685 | }, 686 | "grammarly.config.suggestions.SpacesSurroundingSlash": { 687 | "description": "Suggests removing extra spaces surrounding a slash.", 688 | "enum": [ 689 | true, 690 | false, 691 | null 692 | ], 693 | "default": null, 694 | "scope": "language-overridable", 695 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.spacesSurroundingSlash` instead." 696 | }, 697 | "grammarly.config.suggestions.SplitInfinitive": { 698 | "description": "Suggests rewriting split infinitives so that an adverb doesn't come between 'to' and the verb.", 699 | "enum": [ 700 | true, 701 | false, 702 | null 703 | ], 704 | "default": null, 705 | "scope": "language-overridable", 706 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.splitInfinitive` instead." 707 | }, 708 | "grammarly.config.suggestions.StylisticFragments": { 709 | "description": "Suggests completing all incomplete sentences, including stylistic sentence fragments that may be intentional.", 710 | "enum": [ 711 | true, 712 | false, 713 | null 714 | ], 715 | "default": null, 716 | "scope": "language-overridable", 717 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.stylisticFragments` instead." 718 | }, 719 | "grammarly.config.suggestions.UnnecessaryEllipses": { 720 | "description": "Flags unnecessary use of ellipses (...).", 721 | "enum": [ 722 | true, 723 | false, 724 | null 725 | ], 726 | "default": null, 727 | "scope": "language-overridable", 728 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.unnecessaryEllipses` instead." 729 | }, 730 | "grammarly.config.suggestions.Variety": { 731 | "description": "Suggests alternatives to words that occur frequently in the same paragraph.", 732 | "enum": [ 733 | true, 734 | false, 735 | null 736 | ], 737 | "default": null, 738 | "scope": "language-overridable", 739 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.variety` instead." 740 | }, 741 | "grammarly.config.suggestions.Vocabulary": { 742 | "description": "Suggests alternatives to bland and overused words such as 'good' and 'nice'.", 743 | "enum": [ 744 | true, 745 | false, 746 | null 747 | ], 748 | "default": null, 749 | "scope": "language-overridable", 750 | "deprecationMessage": "Use `grammarly.config.suggestionCategories.vocabulary` instead." 751 | } 752 | } 753 | }, 754 | "commands": [ 755 | { 756 | "title": "Check text", 757 | "category": "Grammarly", 758 | "command": "grammarly.check", 759 | "icon": "$(pass-filled)", 760 | "enablement": "!grammarly.isActive" 761 | }, 762 | { 763 | "title": "Login / Connect your account", 764 | "category": "Grammarly", 765 | "command": "grammarly.login", 766 | "icon": "$(log-in)", 767 | "enablement": "!grammarly.isRunning || !grammarly.isUserAccountConnected" 768 | }, 769 | { 770 | "title": "Log out", 771 | "category": "Grammarly", 772 | "command": "grammarly.logout", 773 | "icon": "$(log-out)", 774 | "enablement": "grammarly.isUserAccountConnected" 775 | }, 776 | { 777 | "title": "Restart language server", 778 | "category": "Grammarly", 779 | "command": "grammarly.restartServer", 780 | "icon": "$(debug-restart)" 781 | }, 782 | { 783 | "title": "Pause text check", 784 | "category": "Grammarly", 785 | "command": "grammarly.pauseCheck", 786 | "icon": "$(debug-pause)", 787 | "enablement": "grammarly.isActive && !grammarly.isPaused" 788 | }, 789 | { 790 | "title": "Resume text check", 791 | "category": "Grammarly", 792 | "command": "grammarly.resumeCheck", 793 | "icon": "$(debug-start)", 794 | "enablement": "grammarly.isActive && grammarly.isPaused" 795 | } 796 | ] 797 | }, 798 | "license": "MIT", 799 | "main": "./dist/extension/index.node.js", 800 | "browser": "./dist/extension/index.browser.js", 801 | "buildConfig": { 802 | "external": [ 803 | "vscode" 804 | ], 805 | "useMain": false, 806 | "sources": { 807 | "src/index.ts": [ 808 | { 809 | "format": "esm", 810 | "file": "dist/extension/index.mjs" 811 | }, 812 | { 813 | "format": "cjs", 814 | "file": "dist/extension/index.node.js", 815 | "bundle": { 816 | "platform": "node", 817 | "external": [ 818 | "vscode" 819 | ] 820 | } 821 | } 822 | ], 823 | "src/server.ts": [ 824 | { 825 | "format": "esm", 826 | "file": "dist/server/index.mjs" 827 | }, 828 | { 829 | "format": "cjs", 830 | "file": "dist/server/index.node.js", 831 | "bundle": { 832 | "platform": "node", 833 | "conditions": [ 834 | "node", 835 | "import" 836 | ], 837 | "external": [ 838 | "buffer", 839 | "bufferutil", 840 | "encoding", 841 | "node:buffer", 842 | "node:crypto", 843 | "node:events", 844 | "node:fs", 845 | "node:http", 846 | "node:https", 847 | "node:net", 848 | "node:os", 849 | "node:path", 850 | "node:perf_hooks", 851 | "node:process", 852 | "node:stream", 853 | "node:stream/web", 854 | "node:tls", 855 | "node:url", 856 | "node:util", 857 | "node:vm", 858 | "node:zlib", 859 | "worker_threads", 860 | "utf-8-validate" 861 | ] 862 | } 863 | } 864 | ] 865 | } 866 | }, 867 | "files": [ 868 | "./dist", 869 | "./assets" 870 | ], 871 | "scripts": { 872 | "build": "node ../scripts/build-extension.mjs", 873 | "release": "node ../scripts/publish-extension.mjs" 874 | }, 875 | "dependencies": { 876 | "@emacs-grammarly/grammarly-languageclient": "workspace:*", 877 | "@emacs-grammarly/grammarly-languageserver": "workspace:*" 878 | }, 879 | "devDependencies": { 880 | "@types/vscode": "^1.63.0", 881 | "@types/node": "^16.0.0" 882 | } 883 | } -------------------------------------------------------------------------------- /extension/privacy-policy.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | We do not collect or store your data. 4 | -------------------------------------------------------------------------------- /extension/src/GrammarlyClient.ts: -------------------------------------------------------------------------------- 1 | import { GrammarlyLanguageClient } from '@emacs-grammarly/grammarly-languageclient' 2 | import { 3 | commands, 4 | Disposable, 5 | DocumentFilter, 6 | env, 7 | ExtensionContext, 8 | languages, 9 | RelativePattern, 10 | StatusBarAlignment, 11 | TextDocument, 12 | Uri, 13 | window, 14 | workspace, 15 | } from 'vscode' 16 | import { Registerable } from './interfaces' 17 | 18 | export class GrammarlyClient implements Registerable { 19 | public client!: GrammarlyLanguageClient 20 | private session?: Disposable 21 | private callbacks = new Set<() => unknown>() 22 | private isReady = false 23 | private selectors: DocumentFilter[] = [] 24 | 25 | constructor(private readonly context: ExtensionContext) {} 26 | 27 | public onReady(fn: () => unknown): Disposable { 28 | this.callbacks.add(fn) 29 | if (this.isReady) fn() 30 | return new Disposable(() => this.callbacks.delete(fn)) 31 | } 32 | 33 | public matchesDocumentSelector(document: TextDocument): boolean { 34 | const selector: DocumentFilter[] = workspace 35 | .getConfiguration('grammarly') 36 | .get('files.exclude', []) 37 | .map((pattern) => ({ pattern })) 38 | 39 | return languages.match(this.selectors, document) > 0 && languages.match(selector, document) <= 0 40 | } 41 | 42 | private createClient(): GrammarlyLanguageClient { 43 | const config = workspace.getConfiguration('grammarly') 44 | const folder = workspace.workspaceFolders?.[0] 45 | this.selectors = [] 46 | config.get('patterns', []).forEach((pattern) => { 47 | this.selectors.push({ 48 | scheme: 'file', 49 | pattern: folder != null ? new RelativePattern(folder, pattern) : pattern, 50 | }) 51 | }) 52 | config.get('files.include', []).forEach((pattern) => { 53 | this.selectors.push({ pattern }) 54 | }) 55 | config.get('selectors', []).forEach((selector) => { 56 | if (folder != null && selector.pattern != null) { 57 | this.selectors.push({ 58 | ...selector, 59 | pattern: new RelativePattern(folder, String(selector.pattern)), 60 | }) 61 | } else { 62 | this.selectors.push(selector) 63 | } 64 | }) 65 | 66 | const client = new GrammarlyLanguageClient( 67 | isNode() 68 | ? this.context.asAbsolutePath(`dist/server/index.node.js`) 69 | : Uri.joinPath(this.context.extensionUri, `dist/server/index.browser.js`).toString(), 70 | { 71 | id: 'client_BaDkMgx4X19X9UxxYRCXZo', 72 | name: 'Grammarly', 73 | outputChannel: window.createOutputChannel('Grammarly'), 74 | documentSelector: this.selectors 75 | .map((selector) => 76 | selector.language != null || selector.pattern != null || selector.scheme != null ? (selector as any) : null, 77 | ) 78 | .filter((value: T | null): value is T => value != null), 79 | initializationOptions: { 80 | startTextCheckInPausedState: config.get('startTextCheckInPausedState'), 81 | }, 82 | revealOutputChannelOn: 3, 83 | progressOnInitialization: true, 84 | errorHandler: { 85 | error(error) { 86 | window.showErrorMessage(error.message) 87 | return 2 88 | }, 89 | closed() { 90 | return 1 91 | }, 92 | }, 93 | markdown: { 94 | isTrusted: true, 95 | // @ts-ignore 96 | supportHtml: true, 97 | }, 98 | middleware: { 99 | didOpen: (document, next) => { 100 | if (this.matchesDocumentSelector(document)) next(document) 101 | }, 102 | didChange: (event, next) => { 103 | if (this.matchesDocumentSelector(event.document)) next(event) 104 | }, 105 | didSave: (document, next) => { 106 | if (this.matchesDocumentSelector(document)) next(document) 107 | }, 108 | }, 109 | }, 110 | ) 111 | 112 | return client 113 | } 114 | 115 | register() { 116 | return Disposable.from( 117 | workspace.onDidChangeConfiguration(async (event) => { 118 | if ( 119 | event.affectsConfiguration('grammarly.patterns') || 120 | event.affectsConfiguration('grammarly.files') || 121 | event.affectsConfiguration('grammarly.selectors') 122 | ) { 123 | await this.start() 124 | } 125 | }), 126 | window.registerUriHandler({ 127 | handleUri: async (uri) => { 128 | const url = new URL(uri.toString(true)) 129 | if (url.pathname === '/auth/callback') { 130 | try { 131 | url.searchParams.delete('state') // added by github.dev 132 | await this.client.protocol.handleOAuthCallbackUri(url.toString()) 133 | } catch (error) { 134 | await window.showErrorMessage((error as Error).message) 135 | return 136 | } 137 | 138 | if (await this.client.protocol.isUserAccountConnected()) { 139 | await window.showInformationMessage('Account connected.') 140 | } 141 | } else { 142 | throw new Error(`Unexpected URI: ${url.toString()}`) 143 | } 144 | }, 145 | }), 146 | commands.registerCommand('grammarly.check', async () => { 147 | const document = window.activeTextEditor?.document 148 | if (document == null) return console.log('No active document') 149 | const status = await this.client.protocol.getDocumentStatus(document.uri.toString()) 150 | const excluded: DocumentFilter[] = workspace 151 | .getConfiguration('grammarly') 152 | .get('files.exclude', []) 153 | .map((pattern) => ({ pattern })) 154 | if (this.matchesDocumentSelector(document) && status != null) { 155 | await window.showInformationMessage(`Grammarly is already enabled for this file.`) 156 | } else if (languages.match(excluded, document) > 0) { 157 | await window.showInformationMessage( 158 | `This file is explicitly excluded using Grammarly > Files > Exclude setting.`, 159 | ) 160 | } else { 161 | const action = await window.showInformationMessage( 162 | `Grammarly is not enabled for this file. Enable now?`, 163 | { 164 | modal: true, 165 | detail: [ 166 | `- Scheme: ${document.uri.scheme}`, 167 | `- Language: ${document.languageId}`, 168 | `- Path: ${workspace.asRelativePath(document.uri)}`, 169 | ].join('\n'), 170 | }, 171 | 172 | 'Current file', 173 | `All ${document.languageId} files`, 174 | ) 175 | 176 | if (action != null) { 177 | const workspaceConfig = workspace.getConfiguration('grammarly') 178 | const workspaceSelectors = workspaceConfig.get('selectors', []) 179 | const selector: DocumentFilter = { 180 | language: document.languageId, 181 | scheme: document.uri.scheme, 182 | pattern: action === 'Current file' ? workspace.asRelativePath(document.uri) : undefined, 183 | } 184 | const selectors = [...workspaceSelectors, selector] 185 | await workspaceConfig.update('selectors', selectors, false) 186 | await this.start() 187 | } 188 | } 189 | }), 190 | commands.registerCommand('grammarly.login', async () => { 191 | const internalRedirectUri = Uri.parse(`${env.uriScheme}://znck.grammarly/auth/callback`, true) 192 | const externalRedirectUri = await env.asExternalUri(internalRedirectUri) 193 | 194 | const isExternalURLDifferent = internalRedirectUri.toString(true) === externalRedirectUri.toString(true) 195 | const redirectUri = isExternalURLDifferent 196 | ? internalRedirectUri.toString(true) 197 | : 'https://vscode-extension-grammarly.netlify.app/.netlify/functions/redirect' 198 | const url = new URL(await this.client.protocol.getOAuthUrl(redirectUri)) 199 | url.searchParams.set('state', toBase64URL(externalRedirectUri.toString(true))) 200 | 201 | if (!(await env.openExternal(Uri.parse(url.toString(), true)))) { 202 | await window.showErrorMessage('Failed to open login page.') 203 | } 204 | }), 205 | commands.registerCommand('grammarly.logout', async () => { 206 | await this.client.protocol.logout() 207 | await window.showInformationMessage('Logged out.') 208 | }), 209 | { dispose: () => this.session?.dispose() }, 210 | ) 211 | } 212 | 213 | public async start(): Promise { 214 | const statusbar = window.createStatusBarItem(StatusBarAlignment.Left, Number.MIN_SAFE_INTEGER) 215 | statusbar.text = `$(sync~spin) ${this.session == null ? 'Starting' : 'Restarting'} Grammarly language server` 216 | statusbar.show() 217 | try { 218 | this.session?.dispose() 219 | this.client = this.createClient() 220 | this.session = this.client.start() 221 | await this.client.onReady() 222 | await commands.executeCommand('setContext', 'grammarly.isRunning', true) 223 | this.isReady = true 224 | this.callbacks.forEach((fn) => { 225 | try { 226 | fn() 227 | } catch (error) { 228 | console.error(error) 229 | } 230 | }) 231 | } catch (error) { 232 | await commands.executeCommand('setContext', 'grammarly.isRunning', false) 233 | await window.showErrorMessage(`The extension couldn't be started. See the output channel for details.`) 234 | } finally { 235 | statusbar.dispose() 236 | } 237 | } 238 | } 239 | 240 | function isNode(): boolean { 241 | return typeof process !== 'undefined' && process.versions?.node != null 242 | } 243 | 244 | function toBase64URL(text: string): string { 245 | if (typeof Buffer !== 'undefined') return Buffer.from(text, 'utf-8').toString('base64url') 246 | return btoa(text).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_') 247 | } 248 | -------------------------------------------------------------------------------- /extension/src/StatusBarController.ts: -------------------------------------------------------------------------------- 1 | import { commands, Disposable, StatusBarAlignment, TextDocument, Uri, window, workspace } from 'vscode' 2 | import { GrammarlyClient } from './GrammarlyClient' 3 | 4 | type Status = 'idle' | 'connecting' | 'checking' | 'error' | 'paused' 5 | 6 | export class StatusBarController { 7 | #current: { uri: string; status: Status | null } | null = null 8 | #statusbar = window.createStatusBarItem(StatusBarAlignment.Right, Number.MIN_SAFE_INTEGER) 9 | 10 | constructor(private readonly grammarly: GrammarlyClient) { 11 | grammarly.onReady(() => { 12 | grammarly.client.protocol.onDocumentStatus(({ uri, status }) => { 13 | if (uri === this.#current?.uri) { 14 | this.#current.status = status 15 | this.update() 16 | } 17 | }) 18 | grammarly.client.protocol.onUserAccountConnectedChange(() => this.update()) 19 | }) 20 | } 21 | 22 | public register() { 23 | this.grammarly.onReady(() => this.update()) 24 | 25 | let isRestarting = false 26 | return Disposable.from( 27 | this.#statusbar, 28 | workspace.onDidCloseTextDocument(() => this.update()), 29 | window.onDidChangeActiveTextEditor(() => this.update()), 30 | commands.registerCommand('grammarly.restartServer', async () => { 31 | if (isRestarting) return 32 | try { 33 | isRestarting = true 34 | await this.grammarly.start() 35 | } finally { 36 | isRestarting = false 37 | } 38 | }), 39 | commands.registerCommand('grammarly.pauseCheck', async (uri?: Uri) => { 40 | const id = uri ?? window.activeTextEditor?.document.uri 41 | if (id == null) return 42 | 43 | await this.grammarly.client.protocol.pause(id.toString()) 44 | await this.update() 45 | }), 46 | commands.registerCommand('grammarly.resumeCheck', async (uri?: Uri) => { 47 | const id = uri ?? window.activeTextEditor?.document.uri 48 | if (id == null) return 49 | await this.grammarly.client.protocol.resume(id.toString()) 50 | await this.update() 51 | }), 52 | ) 53 | } 54 | 55 | private async getStatus(document: TextDocument): Promise { 56 | const uri = document.uri.toString() 57 | return await this.grammarly.client.protocol.getDocumentStatus(uri) 58 | } 59 | 60 | public async update(): Promise { 61 | await Promise.resolve() 62 | const document = window.activeTextEditor?.document 63 | const isUser = await this.grammarly.client.protocol.isUserAccountConnected() 64 | await commands.executeCommand('setContext', 'grammarly.isUserAccountConnected', isUser) 65 | if (document == null) return this.hide() 66 | const status = await this.getStatus(document) 67 | this.#current = { uri: document.uri.toString(), status } 68 | if (status == null && !this.grammarly.matchesDocumentSelector(document)) return this.hide() 69 | const accountIcon = isUser ? '$(account)' : '' 70 | const statusIcon = 71 | status == null 72 | ? '$(sync)' 73 | : status === 'connecting' 74 | ? '$(sync~spin)' 75 | : status === 'error' 76 | ? accountIcon + '$(warning)' 77 | : status === 'idle' 78 | ? accountIcon + '$(pass-filled)' 79 | : status === 'paused' 80 | ? '$(debug-start)' 81 | : '$(loading~spin)' 82 | this.#statusbar.text = statusIcon 83 | this.#statusbar.color = status === 'error' ? 'red' : '' 84 | this.#statusbar.accessibilityInformation = { 85 | label: status ?? '', 86 | role: 'button', 87 | } 88 | 89 | this.#statusbar.tooltip = [ 90 | `Your Grammarly account is ${isUser ? '' : 'not '}used for this file.`, 91 | `Connection status: ${status}`, 92 | status === 'error' ? `Restart now?` : null, 93 | status === 'paused' ? `Resume text checking?` : null, 94 | ] 95 | .filter(Boolean) 96 | .join('\n') 97 | this.#statusbar.command = 98 | status === 'error' 99 | ? { title: 'Restart', command: 'grammarly.restartServer' } 100 | : status === 'paused' 101 | ? { title: 'Resume', command: 'grammarly.resumeCheck', arguments: [document.uri] } 102 | : { title: 'Pause', command: 'grammarly.pauseCheck', arguments: [document.uri] } 103 | this.#statusbar.show() 104 | await commands.executeCommand('setContext', 'grammarly.isActive', true) 105 | await commands.executeCommand('setContext', 'grammarly.isPaused', status === 'paused') 106 | } 107 | 108 | private async hide() { 109 | this.#statusbar.hide() 110 | await commands.executeCommand('setContext', 'grammarly.isActive', false) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /extension/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const EXTENSION = Symbol('ExtensionContext'); 2 | -------------------------------------------------------------------------------- /extension/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Disposable, ExtensionContext } from 'vscode' 2 | import { GrammarlyClient } from './GrammarlyClient' 3 | import { StatusBarController } from './StatusBarController' 4 | 5 | export async function activate(context: ExtensionContext) { 6 | const grammarly = new GrammarlyClient(context) 7 | await grammarly.start() 8 | return Disposable.from(grammarly.register(), new StatusBarController(grammarly).register()) 9 | } 10 | -------------------------------------------------------------------------------- /extension/src/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { Disposable } from 'vscode'; 2 | 3 | export interface Registerable { 4 | register(): Disposable; 5 | } 6 | 7 | export interface AuthParams { 8 | username: string; 9 | password: string; 10 | } 11 | -------------------------------------------------------------------------------- /extension/src/server.ts: -------------------------------------------------------------------------------- 1 | import { startLanguageServer } from '@emacs-grammarly/grammarly-languageserver' 2 | 3 | startLanguageServer() 4 | -------------------------------------------------------------------------------- /extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ES2019", 5 | "moduleResolution": "node", 6 | 7 | "lib": ["ES2019"], 8 | "sourceMap": true, 9 | "strict": true, 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noUnusedParameters": true, 15 | "skipLibCheck": true, 16 | "esModuleInterop": true 17 | }, 18 | "include": ["src/index.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /fixtures/.gitignore: -------------------------------------------------------------------------------- 1 | /user/* -------------------------------------------------------------------------------- /fixtures/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "grammarly.selectors": [ 3 | { 4 | "language": "markdown", 5 | "scheme": "file", 6 | "pattern": "sample.md" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /fixtures/readme.md: -------------------------------------------------------------------------------- 1 | ## The basics 2 | 3 | Misspellings and grammatical errors can effect your credibility. 4 | The same goes for misused commas, and other types of punctuation . 5 | Not only will Grammarly underline these issues in red, but it will 6 | also showed you how to correctly write the sentence. 7 | 8 | _Typical highlights works_ well when writing but they are a bit lmpty . 9 | Underlines **that are** blue indicate that Grammarly has spotted an unnecessarily wordy sentence. You’ll find suggestions that 10 | can possibly help you revise a wordy **sentence** in 11 | an [effortless](https://example.com) manner. 12 | But wait...there’s more? 13 | 14 | Grammarly Premium can give you very helpful feedback on your writing. Passive voice can be fixed by Grammarly, and it can handle classical word-choice mistakes. It can also help with inconsistencies such as switching between Email, e-mail, and email or the U.S.A. and the USA. 15 | 16 | It can even help when _you wanna_ refine ur slang or formality level. That’s especially useful when writing for a broad audience ranging from businessmen to friends and family, don’t you think? It’ll inspect your vocabulary carefully and suggest the best word to make sure you don’t have to analyze your writing too much. 17 | -------------------------------------------------------------------------------- /fixtures/sample.md: -------------------------------------------------------------------------------- 1 | ## The basics 2 | 3 | Misspellings and grammatical errors can effect your credibility. 4 | The same goes for misused commas, and other types of punctuation . 5 | Not only will Grammarly underline these issues in red, but it will 6 | also showed you how to correctly write the sentence. 7 | 8 | _Typical highlights works_ well when writing but they are a bit lmpty . 9 | Underlines **that are** blue indicate that Grammarly has spotted an unnecessarily wordy sentence. You’ll find suggestions that 10 | can possibly help you revise a wordy **sentence** in 11 | an effortless manner. 12 | But wait...there’s more? 13 | 14 | Grammarly Premium can give you very helpful feedback on your writing. Passive voice can be fixed by Grammarly, and it can handle classical word-choice mistakes. It can also help with inconsistencies such as switching between Email, e-mail, and email or the U.S.A. and the USA. 15 | 16 | It can even help when you wanna refine ur slang or formality level. That’s especially useful when writing for a broad audience ranging from businessmen to friends and family, don’t you think? It’ll inspect your vocabulary carefully and suggest the best word to make sure you don’t have to analyze your writing too much. 17 | -------------------------------------------------------------------------------- /fixtures/sample.txt: -------------------------------------------------------------------------------- 1 | The basics 2 | 3 | Misspellings and grammatical errors can effect your credibility. The same goes for misused commas, and other types of punctuation . Not only will Grammarly underline these issues in red, but it will also showed you how to correctly write the sentence. 4 | Blue underlines indicate that Grammarly has spotted a sentence that is unnecessarily wordy. You’ll find suggestions that can possibly help you revise a wordy sentence in an effortless manner. 5 | But wait...there’s more? 6 | 7 | Grammarly Premium can give you very helpful feedback on your writing. Passive voice can be fixed by Grammarly, and it can handle classical word-choice mistakes. It can also help with inconsistencies such as switching between Email, e-mail, and email or the U.S.A. and the USA. 8 | 9 | It can even help when you wanna refine ur slang or formality level. That’s especially useful when writing for a broad audience ranging from businessmen to friends and family, don’t you think? It’ll inspect your vocabulary carefully and suggest the best word to make sure you don’t have to analyze your writing too much. 10 | 11 | This is a mitsake. 12 | 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** @type {import('@jest/types').Config.InitialOptions} */ 3 | const config = { 4 | projects: ['packages/*/jest.config.js'], 5 | } 6 | 7 | module.exports = config 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "packageManager": "pnpm@8.10.2", 4 | "engines": { 5 | "node": "^18.18.2" 6 | }, 7 | "scripts": { 8 | "build": "rollup -c && node scripts/build-web-extension.mjs", 9 | "build:wasm": "node scripts/build-wasm.mjs", 10 | "watch": "rollup --watch -c", 11 | "test": "jest", 12 | "release": "changeset version", 13 | "open-in-browser": "vscode-test-web --host 127.0.0.1 --browser firefox --extensionDevelopmentPath=./extension ./fixtures" 14 | }, 15 | "devDependencies": { 16 | "@changesets/cli": "^2.22.0", 17 | "@netlify/functions": "^1.0.0", 18 | "@rollup/plugin-alias": "^3.1.9", 19 | "@rollup/plugin-commonjs": "^22.0.0", 20 | "@rollup/plugin-node-resolve": "^13.2.1", 21 | "@rollup/plugin-replace": "^4.0.0", 22 | "@rollup/plugin-typescript": "^8.3.2", 23 | "@types/jest": "^27.5.0", 24 | "@vscode/test-web": "^0.0.24", 25 | "@vuedx/monorepo-tools": "^0.2.2-next-1651055813.0", 26 | "esbuild": "^0.14.38", 27 | "husky": "^7.0.4", 28 | "jest": "^27.0.0", 29 | "lint-staged": "^12.4.1", 30 | "node-fetch": "^3.3.1", 31 | "prettier": "^2.6.2", 32 | "rollup": "^2.71.1", 33 | "rollup-plugin-copy": "^3.4.0", 34 | "semver": "^7.3.7", 35 | "tree-sitter-cli": "^0.20.8", 36 | "tree-sitter-html": "^0.20.0", 37 | "tree-sitter-markdown": "^0.7.1", 38 | "tslib": "^2.4.0", 39 | "typescript": "^4.6.4", 40 | "vsce": "^2.7.0" 41 | }, 42 | "gitHooks": { 43 | "pre-commit": "lint-staged" 44 | }, 45 | "lint-staged": { 46 | "*.{js,ts,json,yml}": "prettier --write" 47 | }, 48 | "pnpm": { 49 | "neverBuiltDependencies": [ 50 | "keytar", 51 | "tree-sitter-cli", 52 | "tree-sitter-html", 53 | "tree-sitter-markdown" 54 | ] 55 | } 56 | } -------------------------------------------------------------------------------- /packages/grammarly-languageclient/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # grammarly-languageclient 2 | 3 | ## 0.0.3 4 | 5 | ### Patch Changes 6 | 7 | - 75fce63: Pause text checking session 8 | 9 | - Commands: 10 | - `Grammarly: Pause text check` — Available when active editor has an active Grammarly session 11 | - `Grammarly: Resume text check` — Available when active editor has a paused Grammarly session 12 | - `Grammarly: Restart language server` 13 | - Configuration: 14 | - `grammarly.startTextCheckInPausedState` — When enabled, new text checking session is paused initially 15 | 16 | ## 0.0.1 17 | 18 | ### Patch Changes 19 | 20 | - a30aa93: Support for connected Grammarly account 21 | -------------------------------------------------------------------------------- /packages/grammarly-languageclient/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Rahul Kadyan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/grammarly-languageclient/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@emacs-grammarly/grammarly-languageclient", 3 | "version": "0.0.3", 4 | "description": "LSP client implementation for Grammarly", 5 | "author": "Rahul Kadyan ", 6 | "main": "./dist/index.node.cjs", 7 | "module": "./dist/index.node.mjs", 8 | "browser": "./dist/index.browser.mjs", 9 | "types": "./dist/index.d.ts", 10 | "exports": { 11 | ".": { 12 | "node": { 13 | "require": "./dist/index.node.cjs", 14 | "import": "./dist/index.node.mjs", 15 | "default": "./dist/index.node.mjs" 16 | }, 17 | "default": { 18 | "import": "./dist/index.browser.mjs", 19 | "default": "./dist/index.browser.mjs" 20 | } 21 | }, 22 | "./package.json": "./package.json" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/znck/grammarly", 27 | "directory": "packages/grammarly-languageclient" 28 | }, 29 | "buildConfig": { 30 | "useMain": false, 31 | "external": [ 32 | "vscode-languageclient/browser", 33 | "vscode-languageclient/node" 34 | ], 35 | "sources": { 36 | "src/index.ts": [ 37 | { 38 | "format": "dts", 39 | "file": "dist/index.d.ts" 40 | } 41 | ], 42 | "src/index.browser.ts": [ 43 | { 44 | "format": "esm", 45 | "file": "dist/index.browser.mjs" 46 | } 47 | ], 48 | "src/index.node.ts": [ 49 | { 50 | "format": "esm", 51 | "file": "dist/index.node.mjs" 52 | }, 53 | { 54 | "format": "cjs", 55 | "file": "dist/index.node.cjs" 56 | } 57 | ] 58 | } 59 | }, 60 | "license": "MIT", 61 | "files": [ 62 | "dist", 63 | "bin" 64 | ], 65 | "dependencies": { 66 | "vscode-languageclient": "^7.0.0" 67 | }, 68 | "devDependencies": { 69 | "@grammarly/sdk": "^2.3.17" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/grammarly-languageclient/readme.md: -------------------------------------------------------------------------------- 1 | # LanguageClient for Grammarly SDK 2 | 3 | A client for `grammarly-languageserver`. 4 | 5 | ## Support 6 | 7 | This extension is maintained by [Rahul Kadyan](https://github.com/znck). You can [💖 sponsor him](https://github.com/sponsors/znck) for the continued development of this extension. 8 | 9 |

10 | 11 | 12 | 13 |

14 | 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /packages/grammarly-languageclient/src/GrammarlyLanguageClientOptions.ts: -------------------------------------------------------------------------------- 1 | import { LanguageClientOptions } from 'vscode-languageclient/browser' 2 | 3 | export interface GrammarlyLanguageClientOptions extends LanguageClientOptions { 4 | id: string 5 | name: string 6 | } 7 | -------------------------------------------------------------------------------- /packages/grammarly-languageclient/src/index.browser.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { LanguageClient } from 'vscode-languageclient/browser' 4 | import type { GrammarlyLanguageClientOptions } from './GrammarlyLanguageClientOptions' 5 | import { createProtocol, Protocol } from './protocol' 6 | 7 | export class GrammarlyLanguageClient extends LanguageClient { 8 | public readonly protocol: Protocol 9 | 10 | public constructor(serverPath: string, options: GrammarlyLanguageClientOptions) { 11 | super( 12 | options.id, 13 | options.name, 14 | { 15 | ...options, 16 | initializationOptions: { 17 | clientId: options.id, 18 | ...options.initializationOptions, 19 | }, 20 | }, 21 | new Worker(serverPath, {}), 22 | ) 23 | this.protocol = createProtocol(this) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/grammarly-languageclient/src/index.node.ts: -------------------------------------------------------------------------------- 1 | import { LanguageClient, ServerOptions, TransportKind } from 'vscode-languageclient/node' 2 | import type { GrammarlyLanguageClientOptions } from './GrammarlyLanguageClientOptions' 3 | import { createProtocol, Protocol } from './protocol' 4 | 5 | export class GrammarlyLanguageClient extends LanguageClient { 6 | public readonly protocol: Protocol 7 | 8 | public constructor(serverPath: string, options: GrammarlyLanguageClientOptions) { 9 | const config = { 10 | ...options, 11 | initializationOptions: { 12 | clientId: options.id, 13 | ...options.initializationOptions, 14 | }, 15 | } 16 | 17 | super(options.id, options.name, getLanguageServerOptions(serverPath), config) 18 | this.protocol = createProtocol(this) 19 | } 20 | } 21 | 22 | function getLanguageServerOptions(module: string): ServerOptions { 23 | return { 24 | run: { module, transport: TransportKind.ipc }, 25 | debug: { 26 | module, 27 | transport: TransportKind.ipc, 28 | options: { 29 | execArgv: ['--nolazy', '--inspect=5512'], 30 | }, 31 | }, 32 | } 33 | } 34 | 35 | function isNode(): boolean { 36 | return typeof process !== 'undefined' && process.versions?.node != null 37 | } 38 | -------------------------------------------------------------------------------- /packages/grammarly-languageclient/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { BaseLanguageClient, MessageTransports } from 'vscode-languageclient' 2 | import type { GrammarlyLanguageClientOptions } from './GrammarlyLanguageClientOptions' 3 | import type { Protocol } from './protocol' 4 | export type { GrammarlyLanguageClientOptions } from './GrammarlyLanguageClientOptions' 5 | export declare class GrammarlyLanguageClient extends BaseLanguageClient { 6 | protected getLocale(): string 7 | protected createMessageTransports(encoding: string): Promise 8 | constructor(serverPath: string, options: GrammarlyLanguageClientOptions) 9 | public readonly protocol: Protocol 10 | } 11 | -------------------------------------------------------------------------------- /packages/grammarly-languageclient/src/protocol.ts: -------------------------------------------------------------------------------- 1 | import type { BaseLanguageClient } from 'vscode-languageclient' 2 | import type { SessionStatus, SuggestionId } from '@grammarly/sdk' 3 | 4 | export interface Protocol { 5 | pause(uri: string): Promise 6 | resume(uri: string): Promise 7 | getDocumentStatus(uri: string): Promise 8 | isUserAccountConnected(): Promise 9 | getOAuthUrl(oauthRedirectUri: string): Promise 10 | logout(): Promise 11 | dismissSuggestion(params: { uri: string; suggestionId: SuggestionId }): Promise 12 | handleOAuthCallbackUri(uri: string): void 13 | onDocumentStatus(fn: (params: { uri: string; status: SessionStatus }) => unknown): void 14 | onUserAccountConnectedChange(fn: (params: { isUserAccountConnected: boolean }) => unknown): void 15 | } 16 | 17 | export function createProtocol(client: BaseLanguageClient): Protocol { 18 | return new Proxy({} as Protocol, { 19 | get(_, property) { 20 | if (typeof property !== 'string') return 21 | if (property.startsWith('on')) { 22 | return (fn: (...args: unknown[]) => unknown) => client.onNotification(`$/${property}`, fn) 23 | } else { 24 | return async (...args: unknown[]): Promise => { 25 | try { 26 | await client.onReady() 27 | const result = await client.sendRequest(`$/${property}`, args) 28 | return result 29 | } catch (error) { 30 | throw error 31 | } 32 | } 33 | } 34 | }, 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /packages/grammarly-languageclient/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ES2019", 5 | "moduleResolution": "node", 6 | "lib": ["ES2019"], 7 | 8 | "strict": true, 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "noUnusedParameters": true, 16 | "skipLibCheck": true, 17 | "esModuleInterop": true 18 | }, 19 | "include": ["src/"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # grammarly-languageserver 2 | 3 | ## 0.0.4 4 | 5 | ### Patch Changes 6 | 7 | - c2a3108: Fix the grammarly-languageserver executable's shebang 8 | 9 | ## 0.0.3 10 | 11 | ### Patch Changes 12 | 13 | - 2de7e79: Support for connected Grammarly account in web extension (https://github.dev and https://vscode.dev) 14 | - bdbee32: Fix import path in grammarly-languageserver bin 15 | - 75fce63: Pause text checking session 16 | 17 | - Commands: 18 | - `Grammarly: Pause text check` — Available when active editor has an active Grammarly session 19 | - `Grammarly: Resume text check` — Available when active editor has a paused Grammarly session 20 | - `Grammarly: Restart language server` 21 | - Configuration: 22 | - `grammarly.startTextCheckInPausedState` — When enabled, new text checking session is paused initially 23 | 24 | ## 0.0.2 25 | 26 | ### Patch Changes 27 | 28 | - c735bc8: Use config from workspace configuration in Grammarly SDK 29 | 30 | ## 0.0.1 31 | 32 | ### Patch Changes 33 | 34 | - a30aa93: Support for connected Grammarly account 35 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/bin/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const server = require('../dist/index.node.cjs') 4 | 5 | server.startLanguageServer() 6 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@emacs-grammarly/grammarly-languageserver", 3 | "version": "0.1.2", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@emacs-grammarly/grammarly-languageserver", 9 | "version": "0.1.2", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@emacs-grammarly/grammarly-richtext-encoder": "^0.0.1", 13 | "@grammarly/sdk": "^1.7.4", 14 | "htmlparser2": "^8.0.1", 15 | "idb-keyval": "^6.1.0", 16 | "inversify": "^6.0.1", 17 | "node-fetch": "^2.6.0", 18 | "reflect-metadata": "^0.1.13", 19 | "vscode-languageserver": "^7.0.0", 20 | "vscode-languageserver-textdocument": "^1.0.4", 21 | "web-tree-sitter": "0.20.5" 22 | }, 23 | "bin": { 24 | "grammarly-languageserver": "bin/server.js" 25 | }, 26 | "devDependencies": { 27 | "domhandler": "^5.0.3" 28 | } 29 | }, 30 | "../../node_modules/.pnpm/@grammarly+sdk@1.7.3/node_modules/@grammarly/sdk": { 31 | "version": "1.7.3", 32 | "extraneous": true, 33 | "license": "Apache-2.0", 34 | "devDependencies": { 35 | "@grammarly/plugin-core": "1.7.3", 36 | "@grammarly/plugin-reactivity": "1.7.3", 37 | "@grammarly/plugin-sdk": "1.7.3", 38 | "@microsoft/api-extractor": "^7.15.1", 39 | "@types/jest": "^26.0.22", 40 | "@types/node": "^17.0.23", 41 | "@types/node-fetch": "^2.6.1", 42 | "@types/web": "^0.0.61", 43 | "domhandler": "^5.0.1", 44 | "htmlparser2": "^7.2.0", 45 | "isomorphic-fetch": "^3.0.0", 46 | "jest": "^26.6.3", 47 | "ts-jest": "^26.5.4", 48 | "ws": "^8.5.0" 49 | } 50 | }, 51 | "../../node_modules/.pnpm/inversify@6.0.1/node_modules/inversify": { 52 | "version": "6.0.1", 53 | "license": "MIT", 54 | "devDependencies": { 55 | "@types/chai": "4.2.22", 56 | "@types/mocha": "9.0.0", 57 | "@types/sinon": "9.0.11", 58 | "browserify": "17.0.0", 59 | "chai": "4.3.4", 60 | "istanbul": "0.4.5", 61 | "karma": "6.3.4", 62 | "karma-chai": "0.1.0", 63 | "karma-chrome-launcher": "3.1.0", 64 | "karma-commonjs": "1.0.0", 65 | "karma-es6-shim": "1.0.0", 66 | "karma-firefox-launcher": "2.1.1", 67 | "karma-ie-launcher": "1.0.0", 68 | "karma-mocha": "2.0.1", 69 | "karma-mocha-reporter": "2.2.5", 70 | "karma-phantomjs-launcher": "1.0.4", 71 | "karma-sinon": "1.0.5", 72 | "mocha": "9.1.2", 73 | "nyc": "15.1.0", 74 | "publish-please": "5.5.2", 75 | "reflect-metadata": "0.1.13", 76 | "sinon": "9.2.4", 77 | "ts-node": "10.3.0", 78 | "tsify": "5.0.4", 79 | "tslint": "6.1.3", 80 | "typescript": "4.4.4", 81 | "updates": "12.2.2" 82 | } 83 | }, 84 | "../../node_modules/.pnpm/node-fetch@2.6.7/node_modules/node-fetch": { 85 | "version": "2.6.7", 86 | "license": "MIT", 87 | "dependencies": { 88 | "whatwg-url": "^5.0.0" 89 | }, 90 | "devDependencies": { 91 | "@ungap/url-search-params": "^0.1.2", 92 | "abort-controller": "^1.1.0", 93 | "abortcontroller-polyfill": "^1.3.0", 94 | "babel-core": "^6.26.3", 95 | "babel-plugin-istanbul": "^4.1.6", 96 | "babel-preset-env": "^1.6.1", 97 | "babel-register": "^6.16.3", 98 | "chai": "^3.5.0", 99 | "chai-as-promised": "^7.1.1", 100 | "chai-iterator": "^1.1.1", 101 | "chai-string": "~1.3.0", 102 | "codecov": "3.3.0", 103 | "cross-env": "^5.2.0", 104 | "form-data": "^2.3.3", 105 | "is-builtin-module": "^1.0.0", 106 | "mocha": "^5.0.0", 107 | "nyc": "11.9.0", 108 | "parted": "^0.1.1", 109 | "promise": "^8.0.3", 110 | "resumer": "0.0.0", 111 | "rollup": "^0.63.4", 112 | "rollup-plugin-babel": "^3.0.7", 113 | "string-to-arraybuffer": "^1.0.2", 114 | "teeny-request": "3.7.0" 115 | }, 116 | "engines": { 117 | "node": "4.x || >=6.0.0" 118 | }, 119 | "peerDependencies": { 120 | "encoding": "^0.1.0" 121 | }, 122 | "peerDependenciesMeta": { 123 | "encoding": { 124 | "optional": true 125 | } 126 | } 127 | }, 128 | "../../node_modules/.pnpm/reflect-metadata@0.1.13/node_modules/reflect-metadata": { 129 | "version": "0.1.13", 130 | "license": "Apache-2.0", 131 | "devDependencies": { 132 | "@types/chai": "^3.4.34", 133 | "@types/mocha": "^2.2.34", 134 | "@types/node": "^6.0.52", 135 | "chai": "^3.5.0", 136 | "del": "^2.2.2", 137 | "ecmarkup": "^3.9.3", 138 | "gulp": "^3.9.1", 139 | "gulp-emu": "^1.1.0", 140 | "gulp-live-server": "0.0.30", 141 | "gulp-mocha": "^3.0.1", 142 | "gulp-rename": "^1.2.2", 143 | "gulp-sequence": "^0.4.6", 144 | "gulp-tsb": "^2.0.3", 145 | "mocha": "^3.2.0", 146 | "typescript": "^2.1.4" 147 | } 148 | }, 149 | "../../node_modules/.pnpm/vscode-languageserver-textdocument@1.0.4/node_modules/vscode-languageserver-textdocument": { 150 | "version": "1.0.4", 151 | "license": "MIT" 152 | }, 153 | "../../node_modules/.pnpm/vscode-languageserver@7.0.0/node_modules/vscode-languageserver": { 154 | "version": "7.0.0", 155 | "license": "MIT", 156 | "dependencies": { 157 | "vscode-languageserver-protocol": "3.16.0" 158 | }, 159 | "bin": { 160 | "installServerIntoExtension": "bin/installServerIntoExtension" 161 | } 162 | }, 163 | "../../node_modules/.pnpm/web-tree-sitter@0.20.5/node_modules/web-tree-sitter": { 164 | "version": "0.20.5", 165 | "license": "MIT", 166 | "devDependencies": { 167 | "chai": "^4.2.0", 168 | "mocha": "^6.1.4", 169 | "terser": "^3.17.0" 170 | } 171 | }, 172 | "node_modules/@emacs-grammarly/grammarly-richtext-encoder": { 173 | "version": "0.0.1", 174 | "resolved": "https://registry.npmjs.org/@emacs-grammarly/grammarly-richtext-encoder/-/grammarly-richtext-encoder-0.0.1.tgz", 175 | "integrity": "sha512-RBdamsxvoCGS04KhPigm0XdktuSIbEzxO3bUchZPKs+MWMvN8n35XwmY6pR4N9jEupwZ9N8pe6aQBgh0OaPsBg==", 176 | "dependencies": { 177 | "web-tree-sitter": "0.20.5" 178 | } 179 | }, 180 | "node_modules/@grammarly/sdk": { 181 | "version": "1.7.4", 182 | "resolved": "https://registry.npmjs.org/@grammarly/sdk/-/sdk-1.7.4.tgz", 183 | "integrity": "sha512-sgz5jkTNTKS9ST4svOswTuA5H09h3il1Xp2Yin63BFX9rUFOPDEiCnrenHu9cAxx5FpF0wVKcoNjW09WNZpwJw==" 184 | }, 185 | "node_modules/dom-serializer": { 186 | "version": "2.0.0", 187 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", 188 | "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", 189 | "dependencies": { 190 | "domelementtype": "^2.3.0", 191 | "domhandler": "^5.0.2", 192 | "entities": "^4.2.0" 193 | }, 194 | "funding": { 195 | "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" 196 | } 197 | }, 198 | "node_modules/domelementtype": { 199 | "version": "2.3.0", 200 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", 201 | "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", 202 | "funding": [ 203 | { 204 | "type": "github", 205 | "url": "https://github.com/sponsors/fb55" 206 | } 207 | ] 208 | }, 209 | "node_modules/domhandler": { 210 | "version": "5.0.3", 211 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", 212 | "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", 213 | "dependencies": { 214 | "domelementtype": "^2.3.0" 215 | }, 216 | "engines": { 217 | "node": ">= 4" 218 | }, 219 | "funding": { 220 | "url": "https://github.com/fb55/domhandler?sponsor=1" 221 | } 222 | }, 223 | "node_modules/domutils": { 224 | "version": "3.0.1", 225 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", 226 | "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", 227 | "dependencies": { 228 | "dom-serializer": "^2.0.0", 229 | "domelementtype": "^2.3.0", 230 | "domhandler": "^5.0.1" 231 | }, 232 | "funding": { 233 | "url": "https://github.com/fb55/domutils?sponsor=1" 234 | } 235 | }, 236 | "node_modules/entities": { 237 | "version": "4.3.0", 238 | "resolved": "https://registry.npmjs.org/entities/-/entities-4.3.0.tgz", 239 | "integrity": "sha512-/iP1rZrSEJ0DTlPiX+jbzlA3eVkY/e8L8SozroF395fIqE3TYF/Nz7YOMAawta+vLmyJ/hkGNNPcSbMADCCXbg==", 240 | "engines": { 241 | "node": ">=0.12" 242 | }, 243 | "funding": { 244 | "url": "https://github.com/fb55/entities?sponsor=1" 245 | } 246 | }, 247 | "node_modules/htmlparser2": { 248 | "version": "8.0.1", 249 | "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", 250 | "integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==", 251 | "funding": [ 252 | "https://github.com/fb55/htmlparser2?sponsor=1", 253 | { 254 | "type": "github", 255 | "url": "https://github.com/sponsors/fb55" 256 | } 257 | ], 258 | "dependencies": { 259 | "domelementtype": "^2.3.0", 260 | "domhandler": "^5.0.2", 261 | "domutils": "^3.0.1", 262 | "entities": "^4.3.0" 263 | } 264 | }, 265 | "node_modules/idb-keyval": { 266 | "version": "6.1.0", 267 | "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.1.0.tgz", 268 | "integrity": "sha512-u/qHZ75rlD3gH+Zah8dAJVJcGW/RfCnfNrFkElC5RpRCnpsCXXhqjVk+6MoVKJ3WhmNbRYdI6IIVP88e+5sxGw==", 269 | "dependencies": { 270 | "safari-14-idb-fix": "^3.0.0" 271 | } 272 | }, 273 | "node_modules/inversify": { 274 | "resolved": "../../node_modules/.pnpm/inversify@6.0.1/node_modules/inversify", 275 | "link": true 276 | }, 277 | "node_modules/node-fetch": { 278 | "resolved": "../../node_modules/.pnpm/node-fetch@2.6.7/node_modules/node-fetch", 279 | "link": true 280 | }, 281 | "node_modules/reflect-metadata": { 282 | "resolved": "../../node_modules/.pnpm/reflect-metadata@0.1.13/node_modules/reflect-metadata", 283 | "link": true 284 | }, 285 | "node_modules/safari-14-idb-fix": { 286 | "version": "3.0.0", 287 | "resolved": "https://registry.npmjs.org/safari-14-idb-fix/-/safari-14-idb-fix-3.0.0.tgz", 288 | "integrity": "sha512-eBNFLob4PMq8JA1dGyFn6G97q3/WzNtFK4RnzT1fnLq+9RyrGknzYiM/9B12MnKAxuj1IXr7UKYtTNtjyKMBog==" 289 | }, 290 | "node_modules/vscode-languageserver": { 291 | "resolved": "../../node_modules/.pnpm/vscode-languageserver@7.0.0/node_modules/vscode-languageserver", 292 | "link": true 293 | }, 294 | "node_modules/vscode-languageserver-textdocument": { 295 | "resolved": "../../node_modules/.pnpm/vscode-languageserver-textdocument@1.0.4/node_modules/vscode-languageserver-textdocument", 296 | "link": true 297 | }, 298 | "node_modules/web-tree-sitter": { 299 | "resolved": "../../node_modules/.pnpm/web-tree-sitter@0.20.5/node_modules/web-tree-sitter", 300 | "link": true 301 | } 302 | }, 303 | "dependencies": { 304 | "@emacs-grammarly/grammarly-richtext-encoder": { 305 | "version": "0.0.1", 306 | "resolved": "https://registry.npmjs.org/@emacs-grammarly/grammarly-richtext-encoder/-/grammarly-richtext-encoder-0.0.1.tgz", 307 | "integrity": "sha512-RBdamsxvoCGS04KhPigm0XdktuSIbEzxO3bUchZPKs+MWMvN8n35XwmY6pR4N9jEupwZ9N8pe6aQBgh0OaPsBg==", 308 | "requires": { 309 | "web-tree-sitter": "0.20.5" 310 | } 311 | }, 312 | "@grammarly/sdk": { 313 | "version": "1.7.4", 314 | "resolved": "https://registry.npmjs.org/@grammarly/sdk/-/sdk-1.7.4.tgz", 315 | "integrity": "sha512-sgz5jkTNTKS9ST4svOswTuA5H09h3il1Xp2Yin63BFX9rUFOPDEiCnrenHu9cAxx5FpF0wVKcoNjW09WNZpwJw==" 316 | }, 317 | "dom-serializer": { 318 | "version": "2.0.0", 319 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", 320 | "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", 321 | "requires": { 322 | "domelementtype": "^2.3.0", 323 | "domhandler": "^5.0.2", 324 | "entities": "^4.2.0" 325 | } 326 | }, 327 | "domelementtype": { 328 | "version": "2.3.0", 329 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", 330 | "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" 331 | }, 332 | "domhandler": { 333 | "version": "5.0.3", 334 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", 335 | "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", 336 | "requires": { 337 | "domelementtype": "^2.3.0" 338 | } 339 | }, 340 | "domutils": { 341 | "version": "3.0.1", 342 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", 343 | "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", 344 | "requires": { 345 | "dom-serializer": "^2.0.0", 346 | "domelementtype": "^2.3.0", 347 | "domhandler": "^5.0.1" 348 | } 349 | }, 350 | "entities": { 351 | "version": "4.3.0", 352 | "resolved": "https://registry.npmjs.org/entities/-/entities-4.3.0.tgz", 353 | "integrity": "sha512-/iP1rZrSEJ0DTlPiX+jbzlA3eVkY/e8L8SozroF395fIqE3TYF/Nz7YOMAawta+vLmyJ/hkGNNPcSbMADCCXbg==" 354 | }, 355 | "htmlparser2": { 356 | "version": "8.0.1", 357 | "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", 358 | "integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==", 359 | "requires": { 360 | "domelementtype": "^2.3.0", 361 | "domhandler": "^5.0.2", 362 | "domutils": "^3.0.1", 363 | "entities": "^4.3.0" 364 | } 365 | }, 366 | "idb-keyval": { 367 | "version": "6.1.0", 368 | "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.1.0.tgz", 369 | "integrity": "sha512-u/qHZ75rlD3gH+Zah8dAJVJcGW/RfCnfNrFkElC5RpRCnpsCXXhqjVk+6MoVKJ3WhmNbRYdI6IIVP88e+5sxGw==", 370 | "requires": { 371 | "safari-14-idb-fix": "^3.0.0" 372 | } 373 | }, 374 | "inversify": { 375 | "version": "file:../../node_modules/.pnpm/inversify@6.0.1/node_modules/inversify", 376 | "requires": { 377 | "@types/chai": "4.2.22", 378 | "@types/mocha": "9.0.0", 379 | "@types/sinon": "9.0.11", 380 | "browserify": "17.0.0", 381 | "chai": "4.3.4", 382 | "istanbul": "0.4.5", 383 | "karma": "6.3.4", 384 | "karma-chai": "0.1.0", 385 | "karma-chrome-launcher": "3.1.0", 386 | "karma-commonjs": "1.0.0", 387 | "karma-es6-shim": "1.0.0", 388 | "karma-firefox-launcher": "2.1.1", 389 | "karma-ie-launcher": "1.0.0", 390 | "karma-mocha": "2.0.1", 391 | "karma-mocha-reporter": "2.2.5", 392 | "karma-phantomjs-launcher": "1.0.4", 393 | "karma-sinon": "1.0.5", 394 | "mocha": "9.1.2", 395 | "nyc": "15.1.0", 396 | "publish-please": "5.5.2", 397 | "reflect-metadata": "0.1.13", 398 | "sinon": "9.2.4", 399 | "ts-node": "10.3.0", 400 | "tsify": "5.0.4", 401 | "tslint": "6.1.3", 402 | "typescript": "4.4.4", 403 | "updates": "12.2.2" 404 | } 405 | }, 406 | "node-fetch": { 407 | "version": "file:../../node_modules/.pnpm/node-fetch@2.6.7/node_modules/node-fetch", 408 | "requires": { 409 | "@ungap/url-search-params": "^0.1.2", 410 | "abort-controller": "^1.1.0", 411 | "abortcontroller-polyfill": "^1.3.0", 412 | "babel-core": "^6.26.3", 413 | "babel-plugin-istanbul": "^4.1.6", 414 | "babel-preset-env": "^1.6.1", 415 | "babel-register": "^6.16.3", 416 | "chai": "^3.5.0", 417 | "chai-as-promised": "^7.1.1", 418 | "chai-iterator": "^1.1.1", 419 | "chai-string": "~1.3.0", 420 | "codecov": "3.3.0", 421 | "cross-env": "^5.2.0", 422 | "form-data": "^2.3.3", 423 | "is-builtin-module": "^1.0.0", 424 | "mocha": "^5.0.0", 425 | "nyc": "11.9.0", 426 | "parted": "^0.1.1", 427 | "promise": "^8.0.3", 428 | "resumer": "0.0.0", 429 | "rollup": "^0.63.4", 430 | "rollup-plugin-babel": "^3.0.7", 431 | "string-to-arraybuffer": "^1.0.2", 432 | "teeny-request": "3.7.0", 433 | "whatwg-url": "^5.0.0" 434 | } 435 | }, 436 | "reflect-metadata": { 437 | "version": "file:../../node_modules/.pnpm/reflect-metadata@0.1.13/node_modules/reflect-metadata", 438 | "requires": { 439 | "@types/chai": "^3.4.34", 440 | "@types/mocha": "^2.2.34", 441 | "@types/node": "^6.0.52", 442 | "chai": "^3.5.0", 443 | "del": "^2.2.2", 444 | "ecmarkup": "^3.9.3", 445 | "gulp": "^3.9.1", 446 | "gulp-emu": "^1.1.0", 447 | "gulp-live-server": "0.0.30", 448 | "gulp-mocha": "^3.0.1", 449 | "gulp-rename": "^1.2.2", 450 | "gulp-sequence": "^0.4.6", 451 | "gulp-tsb": "^2.0.3", 452 | "mocha": "^3.2.0", 453 | "typescript": "^2.1.4" 454 | } 455 | }, 456 | "safari-14-idb-fix": { 457 | "version": "3.0.0", 458 | "resolved": "https://registry.npmjs.org/safari-14-idb-fix/-/safari-14-idb-fix-3.0.0.tgz", 459 | "integrity": "sha512-eBNFLob4PMq8JA1dGyFn6G97q3/WzNtFK4RnzT1fnLq+9RyrGknzYiM/9B12MnKAxuj1IXr7UKYtTNtjyKMBog==" 460 | }, 461 | "vscode-languageserver": { 462 | "version": "file:../../node_modules/.pnpm/vscode-languageserver@7.0.0/node_modules/vscode-languageserver", 463 | "requires": { 464 | "vscode-languageserver-protocol": "3.16.0" 465 | } 466 | }, 467 | "vscode-languageserver-textdocument": { 468 | "version": "file:../../node_modules/.pnpm/vscode-languageserver-textdocument@1.0.4/node_modules/vscode-languageserver-textdocument" 469 | }, 470 | "web-tree-sitter": { 471 | "version": "file:../../node_modules/.pnpm/web-tree-sitter@0.20.5/node_modules/web-tree-sitter", 472 | "requires": { 473 | "chai": "^4.2.0", 474 | "mocha": "^6.1.4", 475 | "terser": "^3.17.0" 476 | } 477 | } 478 | } 479 | } 480 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@emacs-grammarly/grammarly-languageserver", 3 | "version": "0.2.3", 4 | "description": "LSP server implementation for Grammarly", 5 | "author": "Rahul Kadyan ", 6 | "bin": "./bin/server.js", 7 | "main": "./dist/index.node.cjs", 8 | "module": "./dist/index.node.mjs", 9 | "browser": "./dist/index.browser.mjs", 10 | "types": "./dist/index.d.ts", 11 | "exports": { 12 | ".": { 13 | "node": { 14 | "require": "./dist/index.node.cjs", 15 | "import": "./dist/index.node.mjs", 16 | "default": "./dist/index.node.mjs" 17 | }, 18 | "default": { 19 | "import": "./dist/index.browser.mjs", 20 | "default": "./dist/index.browser.mjs" 21 | } 22 | }, 23 | "./package.json": "./package.json" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/znck/grammarly", 28 | "directory": "packages/grammarly-languageserver" 29 | }, 30 | "buildConfig": { 31 | "useMain": false, 32 | "external": [ 33 | "vscode-languageserver/browser", 34 | "vscode-languageserver/node", 35 | "node:os", 36 | "node:fs", 37 | "node:path" 38 | ], 39 | "sources": { 40 | "src/index.ts": [ 41 | { 42 | "format": "dts", 43 | "file": "dist/index.d.ts" 44 | } 45 | ], 46 | "src/index.browser.ts": [ 47 | { 48 | "format": "esm", 49 | "file": "dist/index.browser.mjs" 50 | } 51 | ], 52 | "src/index.node.ts": [ 53 | { 54 | "format": "esm", 55 | "file": "dist/index.node.mjs" 56 | }, 57 | { 58 | "format": "cjs", 59 | "file": "dist/index.node.cjs" 60 | } 61 | ] 62 | } 63 | }, 64 | "license": "MIT", 65 | "files": [ 66 | "dist", 67 | "bin" 68 | ], 69 | "dependencies": { 70 | "@grammarly/sdk": "2.3.17", 71 | "@emacs-grammarly/grammarly-richtext-encoder": "^0.0.5", 72 | "htmlparser2": "^8.0.1", 73 | "idb-keyval": "^6.1.0", 74 | "inversify": "^6.0.1", 75 | "node-fetch": "^2.6.0", 76 | "reflect-metadata": "^0.1.13", 77 | "vscode-languageserver": "^7.0.0", 78 | "vscode-languageserver-textdocument": "^1.0.4", 79 | "web-tree-sitter": "^0.20.8" 80 | }, 81 | "devDependencies": { 82 | "domhandler": "^5.0.3" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/readme.md: -------------------------------------------------------------------------------- 1 | # LanguageServer for Grammarly SDK 2 | 3 | A language server implementation on top of Grammarly's SDK. 4 | 5 | ## Support 6 | 7 | This extension is maintained by [Rahul Kadyan](https://github.com/znck). You can [💖 sponsor him](https://github.com/sponsors/znck) for the continued development of this extension. 8 | 9 |

10 | 11 | 12 | 13 |

14 | 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/src/DOMParser.ts: -------------------------------------------------------------------------------- 1 | import { parseDocument } from 'htmlparser2' 2 | import { ChildNode } from 'domhandler' 3 | 4 | export class DOMParser { 5 | parseFromString(code: string): any { 6 | const doc = parseDocument(code) 7 | 8 | const body = { 9 | childNodes: doc.children.map((node) => this._createDomNode(node as any)), 10 | } 11 | 12 | return { body } 13 | } 14 | 15 | private _createDomNode(node: ChildNode): any { 16 | if ('children' in node) { 17 | if (node.type !== 'tag') return 18 | return { 19 | nodeType: node.nodeType, 20 | nodeName: node.tagName.toUpperCase(), 21 | childNodes: node.children.map((node) => this._createDomNode(node)), 22 | } 23 | } else if ('data' in node) { 24 | return { 25 | nodeType: node.nodeType, 26 | nodeName: '#text', 27 | textContent: node.data, 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/src/FileStorage.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, rmSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs' 2 | import { resolve } from 'node:path' 3 | 4 | export class FileStorage { 5 | constructor(private readonly directory: string) { 6 | mkdirSync(directory, { recursive: true }) 7 | } 8 | 9 | getItem(key: string): string | null { 10 | try { 11 | return readFileSync(resolve(this.directory, key), 'utf-8') 12 | } catch { 13 | return null 14 | } 15 | } 16 | 17 | setItem(key: string, value: string): void { 18 | writeFileSync(resolve(this.directory, key), value) 19 | } 20 | 21 | removeItem(key: string): void { 22 | rmSync(resolve(this.directory, key), { force: true }) 23 | } 24 | 25 | clear() { 26 | rmSync(this.directory, { force: true, recursive: true }) 27 | mkdirSync(this.directory, { recursive: true }) 28 | } 29 | 30 | key(index: number): string | undefined { 31 | return readdirSync(this.directory)[index] 32 | } 33 | 34 | get length(): number { 35 | return readdirSync(this.directory).length 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/src/VirtualStorage.ts: -------------------------------------------------------------------------------- 1 | import { createStore, get, set, keys, del, delMany } from 'idb-keyval' 2 | 3 | export class VirtualStorage { 4 | public items = new Map() 5 | 6 | get length(): number { 7 | return this.items.size 8 | } 9 | 10 | clear(): void { 11 | this.items.clear() 12 | } 13 | 14 | getItem(key: string): string | null { 15 | return this.items.get(key) ?? null 16 | } 17 | 18 | key(index: number): string | null { 19 | return Array.from(this.items.keys())[index] ?? null 20 | } 21 | 22 | removeItem(key: string): void { 23 | this.items.delete(key) 24 | } 25 | 26 | setItem(key: string, value: string): void { 27 | this.items.set(key, value) 28 | } 29 | } 30 | 31 | export class IDBStorage extends VirtualStorage { 32 | private store = createStore('grammarly-languageserver', 'localStorage') 33 | 34 | async load(): Promise { 35 | for (const key of await keys(this.store)) { 36 | const value = await get(key, this.store) 37 | if (value != null) { 38 | this.items.set(key, value) 39 | } 40 | } 41 | } 42 | 43 | clear(): void { 44 | delMany(Array.from(this.items.keys()), this.store) 45 | super.clear() 46 | } 47 | 48 | getItem(key: string): string | null { 49 | get(key, this.store).then((value) => { 50 | if (value != null) { 51 | this.items.set(key, value) 52 | } else { 53 | this.items.delete(key) 54 | } 55 | }) 56 | 57 | return super.getItem(key) 58 | } 59 | 60 | removeItem(key: string): void { 61 | del(key, this.store) 62 | this.items.delete(key) 63 | } 64 | 65 | setItem(key: string, value: string): void { 66 | set(key, value, this.store) 67 | this.items.set(key, value) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const GRAMMARLY_SDK = Symbol('GrammarlySDK') 2 | export const TEXT_DOCUMENTS_FACTORY = Symbol('TextDocuments') 3 | export const CLIENT = Symbol('ClientCapabilities') 4 | export const CLIENT_INFO = Symbol('ClientInfo') 5 | export const CLIENT_INITIALIZATION_OPTIONS = Symbol('ClientInitializationOptions') 6 | export const SERVER = Symbol('ServerCapabilities') 7 | export const CONNECTION = Symbol('Connection') 8 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/src/createLanguageServer.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | import { Container } from 'inversify' 3 | import type { 4 | createConnection, 5 | ServerCapabilities, 6 | TextDocuments, 7 | TextDocumentsConfiguration, 8 | } from 'vscode-languageserver' 9 | import { 10 | CLIENT, 11 | CLIENT_INFO, 12 | CLIENT_INITIALIZATION_OPTIONS, 13 | CONNECTION, 14 | GRAMMARLY_SDK, 15 | SERVER, 16 | TEXT_DOCUMENTS_FACTORY, 17 | } from './constants' 18 | import { CodeActionService } from './services/CodeActionService' 19 | import { ConfigurationService } from './services/ConfigurationService' 20 | import { DiagnosticsService } from './services/DiagnosticsService' 21 | import { DocumentService } from './services/DocumentService' 22 | import { HoverService } from './services/HoverService' 23 | import type { SDK } from '@grammarly/sdk' 24 | import { InitializationOptions } from './interfaces/InitializationOptions' 25 | 26 | interface Disposable { 27 | dispose(): void 28 | } 29 | 30 | export interface Options { 31 | getConnection(): ReturnType 32 | createTextDocuments(config: TextDocumentsConfiguration): TextDocuments 33 | init(clientId: string): Promise 34 | pathEnvironmentForSDK(clientId: string): void | Promise 35 | } 36 | 37 | export function createLanguageServer({ 38 | getConnection, 39 | createTextDocuments, 40 | init, 41 | pathEnvironmentForSDK, 42 | }: Options): () => void { 43 | return () => { 44 | const disposables: Disposable[] = [] 45 | const capabilities: ServerCapabilities = {} 46 | const container = new Container({ 47 | autoBindInjectable: true, 48 | defaultScope: 'Singleton', 49 | }) 50 | const connection = getConnection() 51 | 52 | container.bind(CONNECTION).toConstantValue(connection) 53 | container.bind(SERVER).toConstantValue(capabilities) 54 | 55 | connection.onInitialize(async (params) => { 56 | connection.console.log('Initializing...') 57 | const options = params.initializationOptions as InitializationOptions | undefined 58 | if (options?.clientId == null) { 59 | connection.console.error('Error: clientId is required') 60 | throw new Error('clientId is required') 61 | } 62 | await pathEnvironmentForSDK(options.clientId) 63 | const sdk = await init(options.clientId) 64 | 65 | container.bind(CLIENT).toConstantValue(params.capabilities) 66 | container.bind(CLIENT_INFO).toConstantValue({ ...params.clientInfo, id: options.clientId }) 67 | container.bind(CLIENT_INITIALIZATION_OPTIONS).toConstantValue(options) 68 | container.bind(GRAMMARLY_SDK).toConstantValue(sdk) 69 | container.bind(TEXT_DOCUMENTS_FACTORY).toConstantValue(createTextDocuments) 70 | 71 | disposables.push( 72 | container.get(ConfigurationService).register(), 73 | container.get(DocumentService).register(), 74 | container.get(DiagnosticsService).register(), 75 | container.get(HoverService).register(), 76 | container.get(CodeActionService).register(), 77 | ) 78 | 79 | connection.onRequest('$/handleOAuthCallbackUri', async (url: string) => { 80 | await sdk.handleOAuthCallback(url) 81 | }) 82 | 83 | connection.onRequest('$/isUserAccountConnected', async () => { 84 | return sdk.isUserAccountConnected 85 | }) 86 | 87 | connection.onRequest('$/getOAuthUrl', async (oauthRedirectUri: string) => { 88 | try { 89 | return await sdk.getOAuthUrl(oauthRedirectUri) 90 | } catch (error) { 91 | console.error(error) 92 | throw error 93 | } 94 | }) 95 | 96 | connection.onRequest('$/logout', async () => { 97 | await sdk.logout() 98 | }) 99 | 100 | sdk.addEventListener('isUserAccountConnected', () => { 101 | connection.sendNotification('$/onUserAccountConnectedChange', { 102 | isUserAccountConnected: sdk.isUserAccountConnected, 103 | }) 104 | }) 105 | 106 | return { 107 | serverInfo: { 108 | name: 'Grammarly', 109 | }, 110 | capabilities, 111 | } 112 | }) 113 | 114 | connection.onInitialized(() => { 115 | connection.console.log('Initialized!') 116 | }) 117 | 118 | connection.onExit(() => { 119 | disposables.forEach((disposable) => disposable.dispose()) 120 | }) 121 | 122 | connection.listen() 123 | connection.console.log('Ready!') 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare var __DEV__: boolean 2 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/src/index.browser.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BrowserMessageReader, 3 | BrowserMessageWriter, 4 | createConnection, 5 | ProposedFeatures, 6 | TextDocuments, 7 | TextDocumentsConfiguration, 8 | } from 'vscode-languageserver/browser' 9 | import { createLanguageServer } from './createLanguageServer' 10 | import { DOMParser } from './DOMParser' 11 | import { IDBStorage, VirtualStorage } from './VirtualStorage' 12 | function getConnection() { 13 | const messageReader = new BrowserMessageReader(self as unknown as Worker) 14 | const messageWriter = new BrowserMessageWriter(self as unknown as Worker) 15 | return createConnection(ProposedFeatures.all, messageReader, messageWriter) 16 | } 17 | 18 | function createTextDocuments(config: TextDocumentsConfiguration): TextDocuments { 19 | return new TextDocuments(config) 20 | } 21 | 22 | // Polyfill DOMParser as it is not available in worker. 23 | if (!('DOMParser' in globalThis)) (globalThis as any).DOMParser = DOMParser 24 | const localStorage = new IDBStorage() 25 | if (!('localStorage' in globalThis)) (globalThis as any).localStorage = localStorage 26 | const sessionStorage = new VirtualStorage() 27 | if (!('sessionStorage' in globalThis)) (globalThis as any).sessionStorage = sessionStorage 28 | 29 | export const startLanguageServer = createLanguageServer({ 30 | getConnection, 31 | createTextDocuments, 32 | init(clientId) { 33 | // @ts-ignore 34 | return new globalThis.Grammarly.SDK(clientId) 35 | }, 36 | async pathEnvironmentForSDK() { 37 | if ((globalThis as any).localStorage === localStorage) { 38 | await localStorage.load() 39 | } 40 | }, 41 | }) 42 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/src/index.node.ts: -------------------------------------------------------------------------------- 1 | import './polyfill-fetch' 2 | 3 | import { 4 | createConnection, 5 | ProposedFeatures, 6 | TextDocuments, 7 | TextDocumentsConfiguration, 8 | } from 'vscode-languageserver/node' 9 | import { createLanguageServer } from './createLanguageServer' 10 | import { init } from '@grammarly/sdk' 11 | import { FileStorage } from './FileStorage' 12 | import { homedir } from 'node:os' 13 | import { resolve } from 'node:path' 14 | 15 | function getConnection() { 16 | return createConnection(ProposedFeatures.all) 17 | } 18 | 19 | function createTextDocuments(config: TextDocumentsConfiguration): TextDocuments { 20 | return new TextDocuments(config) 21 | } 22 | 23 | function pathEnvironmentForSDK(clientId: string): void { 24 | ;(globalThis as any).localStorage = new FileStorage( 25 | resolve(homedir(), '.config', 'grammarly-languageserver', clientId), 26 | ) 27 | } 28 | 29 | export const startLanguageServer = createLanguageServer({ 30 | getConnection, 31 | createTextDocuments, 32 | init, 33 | pathEnvironmentForSDK, 34 | }) 35 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/src/index.ts: -------------------------------------------------------------------------------- 1 | export declare function startLanguageServer(): void 2 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/src/interfaces/InitializationOptions.ts: -------------------------------------------------------------------------------- 1 | export interface InitializationOptions { 2 | clientId: string 3 | startTextCheckInPausedState?: boolean 4 | } 5 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/src/interfaces/LanguageName.ts: -------------------------------------------------------------------------------- 1 | export type LanguageName = 'html' | 'markdown' 2 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/src/interfaces/Registerable.ts: -------------------------------------------------------------------------------- 1 | export interface Registerable { 2 | register(): { 3 | dispose(): void 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/src/is.ts: -------------------------------------------------------------------------------- 1 | export function isNumber(value: unknown): value is number { 2 | return typeof value === 'number'; 3 | } 4 | 5 | export function isString(value: unknown): value is string { 6 | return typeof value === 'string'; 7 | } 8 | 9 | export function isError(value: unknown): value is Error { 10 | return !!value && value instanceof Error; 11 | } 12 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/src/polyfill-fetch.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | if (typeof global !== 'undefined' && typeof global.fetch === 'undefined') { 3 | ;(async () => { 4 | const { default: fetch, Request, Response, Headers } = await getNodeFetch() 5 | global.fetch = fetch 6 | global.Request = Request 7 | global.Response = Response 8 | global.Headers = Headers 9 | })() 10 | 11 | async function getNodeFetch(): any { 12 | try { 13 | return await import('node-fetch') 14 | } catch { 15 | return require('node-fetch') 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/src/services/CodeActionService.ts: -------------------------------------------------------------------------------- 1 | import { SuggestionId, SuggestionReplacementId } from '@grammarly/sdk' 2 | import { inject, injectable } from 'inversify' 3 | import { CodeAction, Connection, Disposable, ServerCapabilities } from 'vscode-languageserver' 4 | import { CONNECTION, SERVER } from '../constants' 5 | import { Registerable } from '../interfaces/Registerable' 6 | import { DiagnosticsService, SuggestionDiagnostic } from './DiagnosticsService' 7 | import { DocumentService } from './DocumentService' 8 | 9 | @injectable() 10 | export class CodeActionService implements Registerable { 11 | #connection: Connection 12 | #capabilities: ServerCapabilities 13 | #documents: DocumentService 14 | #diagnostics: DiagnosticsService 15 | 16 | public constructor( 17 | @inject(CONNECTION) connection: Connection, 18 | @inject(SERVER) capabilities: ServerCapabilities, 19 | diagnostics: DiagnosticsService, 20 | documents: DocumentService, 21 | ) { 22 | this.#connection = connection 23 | this.#capabilities = capabilities 24 | this.#diagnostics = diagnostics 25 | this.#documents = documents 26 | } 27 | 28 | register(): Disposable { 29 | this.#capabilities.codeActionProvider = { 30 | codeActionKinds: ['quickfix'], 31 | resolveProvider: true, 32 | } 33 | 34 | this.#connection.onCodeAction(async ({ textDocument, context }): Promise => { 35 | const document = this.#documents.get(textDocument.uri) 36 | if (document == null) return [] 37 | return await Promise.all( 38 | context.diagnostics 39 | .map((diagnostic) => 40 | typeof diagnostic.data === 'string' 41 | ? this.#diagnostics.getSuggestionDiagnostic(document, diagnostic.data) 42 | : null, 43 | ) 44 | .filter((item): item is SuggestionDiagnostic => item != null) 45 | .flatMap(({ suggestion, diagnostic }) => { 46 | const actions = suggestion.replacements.map((replacement): CodeAction => { 47 | return { 48 | title: suggestion.title + (replacement.label != null ? ` — ${replacement.label}` : ''), 49 | kind: 'quickfix', 50 | diagnostics: [diagnostic], 51 | data: { 52 | uri: document.original.uri, 53 | suggestionId: suggestion.id, 54 | replacementId: replacement.id, 55 | }, 56 | } 57 | }) 58 | 59 | const dismiss: CodeAction = { 60 | title: `Dismiss — ${suggestion.title}`, 61 | kind: 'quickfix', 62 | diagnostics: [diagnostic], 63 | command: { 64 | title: 'Dismiss suggestion', 65 | command: 'grammarly.dismiss', 66 | arguments: [ 67 | { 68 | uri: document.original.uri, 69 | suggestionId: suggestion.id, 70 | }, 71 | ], 72 | }, 73 | } 74 | 75 | actions.push(dismiss) 76 | 77 | return actions 78 | }), 79 | ) 80 | }) 81 | 82 | this.#connection.onCodeActionResolve(async (codeAction) => { 83 | if (codeAction.data == null) return codeAction 84 | const { uri, suggestionId, replacementId } = codeAction.data as { 85 | uri: string 86 | suggestionId: SuggestionId 87 | replacementId: SuggestionReplacementId 88 | } 89 | const document = this.#documents.get(uri) 90 | if (document == null) return codeAction 91 | 92 | const edit = await document.session.applySuggestion({ 93 | suggestionId, 94 | replacementId, 95 | }) 96 | this.#connection.console.log(JSON.stringify(edit, null, 2)) 97 | const range = document.findOriginalRange(edit.range.start, edit.range.end) 98 | const newText = document.toText(edit.content) 99 | 100 | codeAction.edit = { 101 | changes: { 102 | [uri]: [{ range, newText }], 103 | }, 104 | } 105 | 106 | return codeAction 107 | }) 108 | 109 | return { dispose() {} } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/src/services/ConfigurationService.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify' 2 | import type { Connection, Disposable } from 'vscode-languageserver' 3 | import { CONNECTION } from '../constants' 4 | import { Registerable } from '../interfaces/Registerable' 5 | import { EditorConfig } from '@grammarly/sdk' 6 | 7 | type DocumentConfig = Pick 8 | 9 | @injectable() 10 | export class ConfigurationService implements Registerable { 11 | readonly #connection: Connection 12 | 13 | public constructor(@inject(CONNECTION) connection: Connection) { 14 | this.#connection = connection 15 | } 16 | 17 | public register(): Disposable { 18 | return { dispose() {} } 19 | } 20 | 21 | public async getSettings(): Promise { 22 | const result: { config?: DocumentConfig } | undefined = await this.#connection.workspace.getConfiguration( 23 | 'grammarly', 24 | ) 25 | 26 | return result?.config ?? {} 27 | } 28 | 29 | public async getDocumentSettings(uri: string): Promise { 30 | const result: { config?: DocumentConfig } | undefined = await Promise.race([ 31 | this.#connection.workspace.getConfiguration({ scopeUri: uri, section: 'grammarly' }), 32 | new Promise((resolve) => setTimeout(resolve, 1000, {})), 33 | ]) 34 | 35 | const options: DocumentConfig = { 36 | documentDialect: 'american', 37 | ...result?.config, 38 | } 39 | 40 | if ((options as any).suggestions != null) { 41 | options.suggestionCategories = { ...options.suggestionCategories } 42 | 43 | for (const [key, value] of Object.entries((options as any).suggestions)) { 44 | if (value == null) continue // ignore default values. 45 | 46 | const property = (key.slice(0, 1).toLocaleLowerCase() + 47 | key.slice(1)) as unknown as keyof Required['suggestionCategories'] 48 | options.suggestionCategories[property] ??= value ? 'on' : 'off' 49 | } 50 | 51 | delete (options as any).suggestions 52 | } 53 | 54 | return options 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/src/services/DiagnosticsService.ts: -------------------------------------------------------------------------------- 1 | import { Suggestion, SuggestionId } from '@grammarly/sdk' 2 | import { inject, injectable } from 'inversify' 3 | import type { Connection, Diagnostic, DiagnosticSeverity, Disposable, Range } from 'vscode-languageserver' 4 | import { CONNECTION } from '../constants' 5 | import { Registerable } from '../interfaces/Registerable' 6 | import { DocumentService, GrammarlyDocument } from './DocumentService' 7 | 8 | export type SuggestionDiagnostic = { 9 | diagnostic: Diagnostic 10 | suggestion: Suggestion 11 | } 12 | 13 | @injectable() 14 | export class DiagnosticsService implements Registerable { 15 | #connection: Connection 16 | #documents: DocumentService 17 | #diagnostics: Map> 18 | 19 | public constructor(@inject(CONNECTION) connection: Connection, documents: DocumentService) { 20 | this.#connection = connection 21 | this.#documents = documents 22 | this.#diagnostics = new Map() 23 | } 24 | 25 | public register(): Disposable { 26 | this.#documents.onDidOpen((document) => this.#setupDiagnostics(document)) 27 | this.#documents.onDidClose((document) => this.#clearDiagnostics(document)) 28 | this.#connection.onRequest('$/pause', ([uri]: [uri: string]) => { 29 | const document = this.#documents.get(uri) 30 | if (document == null) return 31 | document.pause() 32 | this.#sendDiagnostics(document) 33 | }) 34 | this.#connection.onRequest('$/resume', ([uri]: [uri: string]) => { 35 | const document = this.#documents.get(uri) 36 | if (document == null) return 37 | document.resume() 38 | this.#sendDiagnostics(document) 39 | }) 40 | return { dispose() {} } 41 | } 42 | 43 | public findSuggestionDiagnostics(document: GrammarlyDocument, range: Range): SuggestionDiagnostic[] { 44 | const diagnostics: SuggestionDiagnostic[] = [] 45 | const s = document.original.offsetAt(range.start) 46 | const e = document.original.offsetAt(range.end) 47 | this.#diagnostics.get(document.original.uri)?.forEach((item) => { 48 | const start = document.original.offsetAt(item.diagnostic.range.start) 49 | const end = document.original.offsetAt(item.diagnostic.range.end) 50 | if (start <= e && s <= end) diagnostics.push(item) 51 | }) 52 | 53 | return diagnostics 54 | } 55 | 56 | public getSuggestionDiagnostic(document: GrammarlyDocument, code: string): SuggestionDiagnostic | undefined { 57 | return this.#diagnostics.get(document.original.uri)?.get(code) 58 | } 59 | 60 | #setupDiagnostics(document: GrammarlyDocument) { 61 | this.#connection.console.log(`${document.session.status} ${document.original.uri}`) 62 | const diagnostics = new Map() 63 | const sendDiagnostics = (): void => this.#sendDiagnostics(document) 64 | 65 | this.#diagnostics.set(document.original.uri, diagnostics) 66 | document.session.addEventListener('suggestions', (event) => { 67 | event.detail.added.forEach((suggestion) => { 68 | diagnostics.set(suggestion.id, { suggestion, diagnostic: this.#toDiagnostic(document, suggestion) }) 69 | }) 70 | event.detail.updated.forEach((suggestion) => { 71 | diagnostics.set(suggestion.id, { suggestion, diagnostic: this.#toDiagnostic(document, suggestion) }) 72 | }) 73 | event.detail.removed.forEach((suggestion) => { 74 | diagnostics.delete(suggestion.id) 75 | }) 76 | sendDiagnostics() 77 | }) 78 | document.session.addEventListener('status', (event) => { 79 | this.#connection.console.log(`${event.detail} ${document.original.uri}`) 80 | this.#connection.sendNotification('$/onDocumentStatus', { 81 | uri: document.original.uri, 82 | status: event.detail, 83 | }) 84 | 85 | switch (event.detail) { 86 | case 'idle': 87 | diagnostics.clear() 88 | document.session.suggestions.forEach((suggestion) => { 89 | diagnostics.set(suggestion.id, { suggestion, diagnostic: this.#toDiagnostic(document, suggestion) }) 90 | }) 91 | sendDiagnostics() 92 | break 93 | } 94 | }) 95 | } 96 | 97 | #sendDiagnostics(document: GrammarlyDocument) { 98 | const diagnostics = this.#diagnostics.get(document.original.uri) ?? new Map() 99 | 100 | this.#connection.sendDiagnostics({ 101 | uri: document.original.uri, 102 | diagnostics: document.isPaused ? [] : Array.from(diagnostics.values()).map((item) => item.diagnostic), 103 | }) 104 | } 105 | 106 | #clearDiagnostics(document: GrammarlyDocument): void { 107 | this.#connection.sendDiagnostics({ 108 | uri: document.original.uri, 109 | version: document.original.version, 110 | diagnostics: [], 111 | }) 112 | } 113 | 114 | #toDiagnostic(document: GrammarlyDocument, suggestion: Suggestion): Diagnostic { 115 | const highlight = suggestion.highlights[0] 116 | 117 | return { 118 | data: suggestion.id, 119 | message: suggestion.title, 120 | range: document.findOriginalRange(highlight.start, highlight.end), 121 | source: 'Grammarly', 122 | severity: suggestion.type === 'corrective' ? 1 : 3, 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/src/services/DocumentService.ts: -------------------------------------------------------------------------------- 1 | import type { RichText, SDK, Session, SuggestionId } from '@grammarly/sdk' 2 | import { inject, injectable } from 'inversify' 3 | import type { 4 | Connection, 5 | Disposable, 6 | ServerCapabilities, 7 | TextDocuments, 8 | TextDocumentsConfiguration, 9 | } from 'vscode-languageserver' 10 | import type { Range, TextDocumentContentChangeEvent } from 'vscode-languageserver-textdocument' 11 | import { TextDocument } from 'vscode-languageserver-textdocument' 12 | import Parser from 'web-tree-sitter' 13 | import { CLIENT_INITIALIZATION_OPTIONS, CONNECTION, GRAMMARLY_SDK, SERVER, TEXT_DOCUMENTS_FACTORY } from '../constants' 14 | import { InitializationOptions } from '../interfaces/InitializationOptions' 15 | import { Registerable } from '../interfaces/Registerable' 16 | import { createParser, transformers, SourceMap, Transformer } from '@emacs-grammarly/grammarly-richtext-encoder' 17 | import { ConfigurationService } from './ConfigurationService' 18 | 19 | @injectable() 20 | export class DocumentService implements Registerable { 21 | #config: ConfigurationService 22 | #connection: Connection 23 | #capabilities: ServerCapabilities 24 | #documents: TextDocuments 25 | #onDocumentOpenCbs: Array<(document: GrammarlyDocument) => void | Promise> = [] 26 | #onDocumentCloseCbs: Array<(document: GrammarlyDocument) => void | Promise> = [] 27 | 28 | public constructor( 29 | @inject(CONNECTION) connection: Connection, 30 | @inject(SERVER) capabilities: ServerCapabilities, 31 | @inject(GRAMMARLY_SDK) sdk: SDK, 32 | @inject(TEXT_DOCUMENTS_FACTORY) createTextDocuments: (config: TextDocumentsConfiguration) => TextDocuments, 33 | @inject(CLIENT_INITIALIZATION_OPTIONS) options: InitializationOptions, 34 | config: ConfigurationService, 35 | ) { 36 | this.#connection = connection 37 | this.#capabilities = capabilities 38 | this.#config = config 39 | this.#documents = createTextDocuments({ 40 | create(uri, languageId, version, content) { 41 | const document = new GrammarlyDocument(TextDocument.create(uri, languageId, version, content), async () => { 42 | const options = await config.getDocumentSettings(uri) 43 | if (options.documentDialect === 'auto-text') options.documentDialect = 'american' 44 | connection.console.log(`create text checking session for "${uri}" with ${JSON.stringify(options, null, 2)} `) 45 | 46 | try { 47 | const session = sdk.withText({ ops: [] }, options) 48 | session.setConfig(options) 49 | session.addEventListener('error', (error) => { 50 | connection.console.error('[Error in Grammarly SDK]: ' + error.detail.message) 51 | }) 52 | 53 | connection.console.log(`text checking session for "${uri}" is ready`) 54 | 55 | return session 56 | } catch (error) { 57 | connection.console.error( 58 | '[Error in Grammarly SDK]: ' + (error as Error).message + ' ' + (error as Error).stack, 59 | ) 60 | throw error 61 | } 62 | }) 63 | 64 | if (options.startTextCheckInPausedState === true) document.pause() 65 | 66 | return document 67 | }, 68 | update(document, changes, version) { 69 | document.update(changes, version) 70 | return document 71 | }, 72 | }) 73 | } 74 | 75 | public register(): Disposable { 76 | this.#capabilities.textDocumentSync = { 77 | openClose: true, 78 | change: 2, 79 | } 80 | this.#capabilities.executeCommandProvider = { 81 | commands: ['grammarly.dismiss'], 82 | } 83 | 84 | this.#documents.listen(this.#connection) 85 | 86 | this.#connection.onRequest('$/getDocumentStatus', async ([uri]: [uri: string]) => { 87 | const document = this.#documents.get(uri) 88 | if (document == null) return null 89 | if (document.isPaused) return 'paused' 90 | await document.isReady() 91 | return document.session.status 92 | }) 93 | 94 | this.#connection.onRequest( 95 | '$/dismissSuggestion', 96 | async ([options]: [{ uri: string; suggestionId: SuggestionId }]) => { 97 | const document = this.#documents.get(options.uri) 98 | if (document == null) return 99 | await document.session.dismissSuggestion({ suggestionId: options.suggestionId }) 100 | }, 101 | ) 102 | 103 | this.#connection.onExecuteCommand(async (event) => { 104 | if (event.command === 'grammarly.dismiss' && event.arguments != null) { 105 | const [options] = event.arguments as [{ uri: string; suggestionId: SuggestionId }] 106 | const document = this.#documents.get(options.uri) 107 | if (document == null) return 108 | await document.session.dismissSuggestion({ suggestionId: options.suggestionId }) 109 | } 110 | }) 111 | 112 | this.#connection.onDidChangeConfiguration(async () => { 113 | await Promise.all( 114 | this.#documents.all().map(async (document) => { 115 | await document.isReady() 116 | document.session.setConfig(await this.#config.getDocumentSettings(document.original.uri)) 117 | }), 118 | ) 119 | }) 120 | 121 | const disposables = [ 122 | this.#documents.onDidOpen(async ({ document }) => { 123 | this.#connection.console.log('open ' + document.original.uri) 124 | await document.isReady() 125 | this.#connection.console.log('ready ' + document.original.uri) 126 | this.#connection.sendNotification('$/grammarlyCheckingStatus', { 127 | uri: document.original.uri, 128 | status: document.session.status, 129 | }) 130 | this.#onDocumentOpenCbs.forEach((cb) => cb(document)) 131 | }), 132 | this.#documents.onDidClose(({ document }) => { 133 | this.#connection.console.log('close ' + document.original.uri) 134 | this.#onDocumentCloseCbs.forEach((cb) => cb(document)) 135 | document.session.disconnect() 136 | }), 137 | { 138 | dispose: () => { 139 | this.#documents.all().forEach((document) => document.session.disconnect()) 140 | this.#onDocumentOpenCbs.length = 0 141 | this.#onDocumentCloseCbs.length = 0 142 | }, 143 | }, 144 | ] 145 | 146 | return { 147 | dispose() { 148 | disposables.forEach((disposable) => disposable.dispose()) 149 | }, 150 | } 151 | } 152 | 153 | public get(uri: string): GrammarlyDocument | undefined { 154 | return this.#documents.get(uri) 155 | } 156 | 157 | public onDidOpen(fn: (document: GrammarlyDocument) => void | Promise): void { 158 | this.#onDocumentOpenCbs.push(fn) 159 | } 160 | 161 | public onDidClose(fn: (document: GrammarlyDocument) => void | Promise): void { 162 | this.#onDocumentCloseCbs.push(fn) 163 | } 164 | } 165 | 166 | export class GrammarlyDocument { 167 | public original: TextDocument 168 | public session!: Session 169 | private readonly createSession: () => Promise> 170 | 171 | #context: { 172 | parser: Parser 173 | tree: Parser.Tree 174 | transformer: Transformer 175 | sourcemap: SourceMap 176 | } | null = null 177 | 178 | constructor(original: TextDocument, createSession: () => Promise>) { 179 | this.original = original 180 | this.createSession = createSession 181 | } 182 | 183 | private _isReady: Promise | null = null 184 | private _isPaused = false 185 | 186 | get isPaused(): boolean { 187 | return this._isPaused 188 | } 189 | 190 | public pause(): void { 191 | this._isPaused = true 192 | } 193 | 194 | public resume(): void { 195 | this._isPaused = false 196 | this.#sync() 197 | } 198 | 199 | public async isReady(): Promise { 200 | if (this._isReady != null) await this._isReady 201 | if (this.session != null) return 202 | this._isReady = (async () => { 203 | this.session = await this.createSession() 204 | await this.#createTree() 205 | this.#sync() 206 | this._isReady = null 207 | })() 208 | 209 | await this._isReady 210 | } 211 | 212 | public findOriginalOffset(offset: number): number { 213 | if (this.#context == null) return offset 214 | const map = this.#context.sourcemap 215 | const index = binarySearchLowerBound(0, map.length - 1, (index) => map[index][1] < offset) 216 | const node = map[index] 217 | if (node == null) return 0 218 | return node[0] + Math.max(0, offset - node[1]) 219 | } 220 | 221 | public findOriginalRange(start: number, end: number): Range { 222 | return { 223 | start: this.original.positionAt(this.findOriginalOffset(start)), 224 | end: this.original.positionAt(this.findOriginalOffset(end)), 225 | } 226 | } 227 | 228 | public toText(text: RichText): string { 229 | return this.#context?.transformer.decode(text) ?? text.ops.map((op) => op.insert).join('') 230 | } 231 | 232 | public update(changes: TextDocumentContentChangeEvent[], version: number): void { 233 | const context = this.#context 234 | if (context == null) { 235 | TextDocument.update(this.original, changes, version) 236 | } else if (changes.every((change) => 'range' in change)) { 237 | const _changes = changes as Array<{ range: Range; text: string }> 238 | const offsets = _changes.map((change) => ({ 239 | start: this.original.offsetAt(change.range.start), 240 | end: this.original.offsetAt(change.range.end), 241 | })) 242 | TextDocument.update(this.original, changes, version) 243 | _changes.forEach((change, index) => { 244 | const newEndIndex = offsets[index].start + change.text.length 245 | const newEndPosition = this.original.positionAt(newEndIndex) 246 | context.tree.edit({ 247 | startIndex: offsets[index].start, 248 | oldEndIndex: offsets[index].end, 249 | newEndIndex: offsets[index].start + change.text.length, 250 | startPosition: { row: change.range.start.line, column: change.range.start.character }, 251 | oldEndPosition: { row: change.range.end.line, column: change.range.end.character }, 252 | newEndPosition: { row: newEndPosition.line, column: newEndPosition.character }, 253 | }) 254 | }) 255 | context.tree = context.parser.parse(this.original.getText(), context.tree) 256 | } else { 257 | TextDocument.update(this.original, changes, version) 258 | context.tree = context.parser.parse(this.original.getText()) 259 | } 260 | 261 | this.#sync() 262 | } 263 | 264 | async #createTree() { 265 | const language = this.original.languageId 266 | 267 | switch (language) { 268 | case 'html': 269 | case 'markdown': 270 | const parser = await createParser(language) 271 | const transformer = transformers[language] 272 | const tree = parser.parse(this.original.getText()) 273 | this.#context = { parser, tree, transformer, sourcemap: [] } 274 | break 275 | } 276 | } 277 | 278 | #sync(): void { 279 | if (this._isPaused) return 280 | if (this.#context != null) { 281 | const [text, map] = this.#context.transformer.encode(this.#context.tree) 282 | this.session.setText(text) 283 | this.#context.sourcemap = map 284 | } else { 285 | this.session.setText({ ops: [{ insert: this.original.getText() }] }) 286 | } 287 | } 288 | } 289 | 290 | function binarySearchLowerBound(lo: number, hi: number, isValid: (mid: number) => boolean): number { 291 | while (lo < hi) { 292 | const mid = Math.ceil((hi + lo) / 2) 293 | if (isValid(mid)) { 294 | lo = mid 295 | } else { 296 | hi = mid - 1 297 | } 298 | } 299 | 300 | return hi 301 | } 302 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/src/services/HoverService.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify' 2 | import type { Connection, Disposable, ServerCapabilities } from 'vscode-languageserver' 3 | import { CONNECTION, SERVER } from '../constants' 4 | import { Registerable } from '../interfaces/Registerable' 5 | import { DiagnosticsService } from './DiagnosticsService' 6 | import { DocumentService } from './DocumentService' 7 | import { toMarkdown } from './toMarkdown' 8 | 9 | @injectable() 10 | export class HoverService implements Registerable { 11 | #connection: Connection 12 | #capabilities: ServerCapabilities 13 | #documents: DocumentService 14 | #diagnostics: DiagnosticsService 15 | 16 | public constructor( 17 | @inject(CONNECTION) connection: Connection, 18 | @inject(SERVER) capabilities: ServerCapabilities, 19 | diagnostics: DiagnosticsService, 20 | documents: DocumentService, 21 | ) { 22 | this.#connection = connection 23 | this.#capabilities = capabilities 24 | this.#diagnostics = diagnostics 25 | this.#documents = documents 26 | } 27 | 28 | register(): Disposable { 29 | this.#capabilities.hoverProvider = true 30 | 31 | this.#connection.onHover(async ({ textDocument, position }) => { 32 | const document = this.#documents.get(textDocument.uri) 33 | if (document == null) return null 34 | const diagnostics = this.#diagnostics.findSuggestionDiagnostics(document, { start: position, end: position }) 35 | diagnostics.sort((a, b) => b.suggestion.highlights[0].start - a.suggestion.highlights[0].start) 36 | const diagnostic = diagnostics[0] 37 | if (diagnostic == null) return null 38 | 39 | const contents = `**${diagnostic.suggestion.title.trim()}**\n\n${toMarkdown( 40 | diagnostic.suggestion.description, 41 | ).trim()}\n\n\n${ 42 | diagnostic.suggestion.replacements.length === 1 43 | ? `… ${toMarkdown(diagnostic.suggestion.replacements[0].preview).trim()} …` 44 | : diagnostic.suggestion.replacements 45 | .map((replacement) => `1. … ${toMarkdown(replacement.preview).trim()} …\n`) 46 | .join('') 47 | }` 48 | 49 | return { 50 | range: diagnostic.diagnostic.range, 51 | contents: { 52 | kind: 'markdown', 53 | value: contents, 54 | }, 55 | } 56 | }) 57 | 58 | return { dispose() {} } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/src/services/toArray.ts: -------------------------------------------------------------------------------- 1 | export function toArray(item?: T | T[]): T[] { 2 | if (!item) 3 | return []; 4 | else if (Array.isArray(item)) 5 | return item; 6 | else 7 | return [item]; 8 | } 9 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/src/services/toMarkdown.ts: -------------------------------------------------------------------------------- 1 | import { Markup, MarkupChild } from '@grammarly/sdk' 2 | 3 | function encodeLeadingAndTrailingSpace(text: string): string { 4 | return text.replace(/^\n+|\n+$/, '').replace(/^[ ]+|[ ]+$/g, (m) => ' '.repeat(m.length)) 5 | } 6 | 7 | export function toMarkdown(markup: Markup): string { 8 | let indent = 0 9 | function stringify(node: MarkupChild): string { 10 | if (typeof node === 'string') return node + '\n' 11 | 12 | const children: MarkupChild[] = [] 13 | node.children.forEach((child) => { 14 | if (typeof child !== 'string' && ['del', 'em', 'strong'].includes(child.type) && children.length > 0) { 15 | const last = children[children.length - 1] 16 | if (typeof last !== 'string' && last.type === child.type) { 17 | last.children.push(...child.children) 18 | } 19 | } 20 | 21 | if (typeof child === 'string') { 22 | children.push(child) 23 | } else { 24 | children.push({ type: child.type, children: child.children.slice() }) 25 | } 26 | }) 27 | 28 | switch (node.type) { 29 | case 'ul': 30 | try { 31 | indent += 2 32 | return `${processChildren(node.children)}\n` 33 | } finally { 34 | indent -= 2 35 | } 36 | case 'li': 37 | return ' '.repeat(indent - 2) + `- ${processChildren(node.children)}\n` 38 | case 'del': 39 | return `~~${encodeLeadingAndTrailingSpace(processChildren(node.children))}~~\n` 40 | case 'em': 41 | return `_${encodeLeadingAndTrailingSpace(processChildren(node.children))}_\n` 42 | case 'strong': 43 | return `**${encodeLeadingAndTrailingSpace(processChildren(node.children))}**\n` 44 | case 'ins': 45 | return `${processChildren(node.children)}\n` 46 | default: 47 | return processChildren(node.children) 48 | } 49 | } 50 | 51 | return processChildren(markup) 52 | 53 | function processChildren(nodes: MarkupChild[]): string { 54 | return nodes.map((node) => stringify(node)).join('') 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/src/string.ts: -------------------------------------------------------------------------------- 1 | export function capitalize(str: string) { 2 | return str[0].toUpperCase() + str.substring(1); 3 | } 4 | 5 | export function calculateTime(words: number, wordsPerMinute: number) { 6 | const wordsPerSecond = wordsPerMinute / 60; 7 | const time = secondsToHumanReadable(words / wordsPerSecond); 8 | 9 | return time; 10 | } 11 | 12 | export function secondsToHumanReadable(sec: number) { 13 | const hours = Math.floor(sec / 3600); 14 | const minutes = Math.floor((sec - hours * 3600) / 60); 15 | let seconds = sec - hours * 3600 - minutes * 60; 16 | 17 | seconds = hours > 0 || minutes > 10 ? 0 : Math.floor(seconds); 18 | 19 | return [ 20 | hours ? `${hours} ${choose(hours, 'hr', 'hrs')}` : '', 21 | minutes ? `${minutes} ${choose(minutes, 'min', 'mins')}` : '', 22 | seconds ? `${seconds} ${choose(seconds, 'sec', 'secs')}` : '', 23 | ] 24 | .filter(Boolean) 25 | .join(' '); 26 | } 27 | 28 | export function choose(count: number, singular: string, plural: string) { 29 | return count === 1 ? singular : plural; 30 | } 31 | 32 | export function asText(strings: string[], glue: string = ' ') { 33 | return strings.filter(Boolean).join(glue); 34 | } 35 | 36 | export function formatLines(str: string, maxLen: number) { 37 | let targetString = ''; 38 | 39 | while (str) { 40 | if (str.length <= maxLen) { 41 | targetString += '\n' + str; 42 | str = ''; 43 | } else { 44 | const index = str.substr(0, maxLen).lastIndexOf(' '); 45 | targetString += '\n' + str.substr(0, Math.max(1, index)); 46 | str = str.substr(Math.max(1, index)); 47 | } 48 | } 49 | 50 | return targetString.trim(); 51 | } 52 | -------------------------------------------------------------------------------- /packages/grammarly-languageserver/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ES2019", 5 | "moduleResolution": "node", 6 | "lib": ["ES2019", "WebWorker"], 7 | 8 | "strict": true, 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "noUnusedParameters": true, 16 | "skipLibCheck": true, 17 | "esModuleInterop": true 18 | }, 19 | "include": ["src/"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/grammarly-richtext-encoder/jest.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** @type {import('@jest/types').Config.InitialOptions} */ 3 | const config = { 4 | name: require('./package.json').name, 5 | preset: 'ts-jest', 6 | testEnvironment: 'node', 7 | globals: { 8 | 'ts-jest': { 9 | tsconfig: '/tsconfig.test.json', 10 | }, 11 | }, 12 | } 13 | 14 | module.exports = config 15 | -------------------------------------------------------------------------------- /packages/grammarly-richtext-encoder/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@emacs-grammarly/grammarly-richtext-encoder", 3 | "version": "0.0.5", 4 | "description": "Transform progamming languages to Grammarly's richtext format", 5 | "author": "Rahul Kadyan ", 6 | "main": "./dist/index.cjs", 7 | "module": "./dist/index.mjs", 8 | "types": "./dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "require": "./dist/index.cjs", 12 | "import": "./dist/index.mjs", 13 | "default": "./dist/index.mjs" 14 | }, 15 | "./package.json": "./package.json" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/znck/grammarly", 20 | "directory": "packages/grammarly-richtext-encoder" 21 | }, 22 | "license": "MIT", 23 | "files": [ 24 | "dist" 25 | ], 26 | "dependencies": { 27 | "web-tree-sitter": "^0.20.8" 28 | }, 29 | "devDependencies": { 30 | "@grammarly/sdk": "^2.3.17", 31 | "@types/jest": "^27.5.0", 32 | "@types/node": "^16.11.6", 33 | "jest": "^28.1.0", 34 | "ts-jest": "^28.0.2" 35 | }, 36 | "scripts": { 37 | "test": "jest" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/grammarly-richtext-encoder/src/Language.ts: -------------------------------------------------------------------------------- 1 | import { InsertOperation, RichText, RichTextAttributes } from '@grammarly/sdk' 2 | import Parser from 'web-tree-sitter' 3 | 4 | export type SourceMap = Array<[original: number, generated: number, length: number]> 5 | export interface Transformer { 6 | encode(tree: Parser.Tree): [RichText, SourceMap] 7 | decode(text: RichText): string 8 | } 9 | 10 | export function createTransformer(options: { 11 | isBlockNode(node: Parser.SyntaxNode): boolean 12 | shouldIgnoreSubtree(node: Parser.SyntaxNode): boolean 13 | getAttributesFor(node: Parser.SyntaxNode, parentAttrs: RichTextAttributes): RichTextAttributes 14 | processNode(node: Parser.SyntaxNode, insert: (text: string, node?: Parser.SyntaxNode, skip?: number) => void): void 15 | stringify(node: Decoder.Node, content: string): string 16 | }): Transformer { 17 | return { encode, decode } 18 | 19 | function encode(tree: Parser.Tree): [RichText, SourceMap] { 20 | let offset = 0 21 | let attributes: RichTextAttributes = {} 22 | const richtext: RichText = { ops: [] } 23 | const sourcemap: SourceMap = [] 24 | processNode(tree.rootNode) 25 | return [richtext, sourcemap] 26 | 27 | function processNode(node: Parser.SyntaxNode): void { 28 | if (options.shouldIgnoreSubtree(node)) return // stop processing sub-tree 29 | const previousAttributes = attributes 30 | attributes = { ...previousAttributes, ...options.getAttributesFor(node, { ...attributes }) } 31 | options.processNode(node, insert) 32 | node.children.forEach(processNode) 33 | if (options.isBlockNode(node) && !hasTrailingNewline()) insert('\n') 34 | attributes = previousAttributes 35 | } 36 | 37 | function insert(text: string, node?: Parser.SyntaxNode, skip: number = 0): void { 38 | if (text === '') return 39 | richtext.ops.push({ 40 | insert: text, 41 | attributes: text === '\n' ? pickBlockAttributes(attributes) : pickInlineAttributes(attributes), 42 | }) 43 | if (node != null) sourcemap.push([node.startIndex + skip, offset, text.length]) 44 | else if (sourcemap.length > 0) { 45 | const last = sourcemap[sourcemap.length - 1] 46 | sourcemap.push([last[0] + last[2], offset, 0]) 47 | } else { 48 | sourcemap.push([0, offset, 0]) 49 | } 50 | offset += text.length 51 | } 52 | 53 | function hasTrailingNewline(): boolean { 54 | return richtext.ops.length > 0 && String(richtext.ops[richtext.ops.length - 1].insert).endsWith('\n') 55 | } 56 | } 57 | 58 | function decode(text: RichText): string { 59 | const ops: InsertOperation[] = text.ops.reduce((ops, op) => { 60 | if (ops.length === 0 || op.insert === '\n') { 61 | ops.push(op) 62 | } else { 63 | const last = ops[ops.length - 1] 64 | if ( 65 | typeof last.insert === 'string' && 66 | typeof op.insert === 'string' && 67 | last.insert !== '\n' && 68 | sameAttributes(last.attributes, op.attributes) 69 | ) { 70 | last.insert += op.insert 71 | } else { 72 | ops.push(op) 73 | } 74 | } 75 | 76 | return ops 77 | }, [] as InsertOperation[]) 78 | const root: Decoder.Element = { type: 'block', childNodes: [], attributes: {}, value: {} } 79 | 80 | function findParent(node: Decoder.Element, attributes: RichTextAttributes): Decoder.Element { 81 | if (node === root) { 82 | const parent: Decoder.Element = { type: 'inline', attributes, value: attributes, childNodes: [], parent: node } 83 | root.childNodes.push(parent) 84 | return parent 85 | } 86 | 87 | const diff = diffAttributes(node.attributes, attributes) 88 | if (diff.removed.length == 0 && diff.added.length === 0) return node 89 | if (diff.removed.length === 0) { 90 | const value: RichTextAttributes = {} 91 | diff.added.forEach((key) => { 92 | value[key] = attributes[key] as any 93 | }) 94 | const parent: Decoder.Element = { type: 'inline', attributes, value, childNodes: [], parent: node } 95 | node.childNodes.push(parent) 96 | return parent 97 | } 98 | 99 | if (node.parent == null) throw new Error('Unexpected') 100 | 101 | return findParent(node.parent, attributes) 102 | } 103 | 104 | let current: Decoder.Element = root 105 | const leaves: Decoder.Text[] = [] 106 | for (let i = 0; i < ops.length; i += 1) { 107 | const op = ops[i] 108 | if (op.insert === '\n' && i > 0) { 109 | let j = i 110 | for (; j >= 0; --j) { 111 | if (ops[j - 1]?.insert === '\n') break 112 | } 113 | const target = commonAncestor(leaves.find((leave) => leave.op === ops[j])!, leaves[leaves.length - 1]) 114 | const parent = target.parent! 115 | const node: Decoder.Element = { 116 | type: 'block', 117 | attributes: op.attributes ?? {}, 118 | value: op.attributes ?? {}, 119 | childNodes: [target], 120 | parent: parent, 121 | } 122 | const index = parent.childNodes.indexOf(target) 123 | parent.childNodes.splice(index, 1) 124 | parent.childNodes.push(node) 125 | target.parent = node 126 | } else { 127 | const node: Decoder.Text = { type: '#text', op } 128 | leaves.push(node) 129 | current = findParent(current, op.attributes ?? {}) 130 | current.childNodes.push(node) 131 | } 132 | } 133 | 134 | return processNode(root) 135 | 136 | function processNode(node: Decoder.Node): string { 137 | if (node.type === '#text') return options.stringify(node, '') 138 | return options.stringify(node, node.childNodes.map((node) => processNode(node)).join('')) 139 | } 140 | } 141 | } 142 | 143 | function commonAncestor(a: Decoder.Node, b: Decoder.Node): Decoder.Element { 144 | const pa = pathFromRoot(a) 145 | const pb = pathFromRoot(b) 146 | const n = Math.min(pa.length, pb.length) 147 | for (let i = 0; i < n; ++i) { 148 | if (pa[i] !== pb[i]) return getEl(pa[i - 1]) 149 | } 150 | 151 | throw new Error('No commont ancestor') 152 | 153 | function getEl(node: Decoder.Node): Decoder.Element { 154 | if (node.type === '#text') return node.parent! 155 | return node 156 | } 157 | } 158 | 159 | function pathFromRoot(node: Decoder.Node): Decoder.Node[] { 160 | const path: Decoder.Node[] = [] 161 | let current: Decoder.Node | undefined = node 162 | while (current != null) { 163 | path.push(current) 164 | current = current.parent 165 | } 166 | 167 | return path.reverse() 168 | } 169 | 170 | export namespace Decoder { 171 | interface BaseNode { 172 | type: string 173 | parent?: Element 174 | } 175 | 176 | export interface Text extends BaseNode { 177 | type: '#text' 178 | op: InsertOperation 179 | } 180 | 181 | export interface Element extends BaseNode { 182 | type: 'block' | 'inline' 183 | childNodes: Node[] 184 | attributes: RichTextAttributes 185 | value: RichTextAttributes 186 | } 187 | 188 | export type Node = Text | Element 189 | } 190 | 191 | function sameAttributes(a?: RichTextAttributes, b?: RichTextAttributes): boolean { 192 | if (a === b) return true 193 | if (a == null || b == null) return false 194 | if (Object.keys(a).length !== Object.keys(b).length) return false 195 | 196 | return (Object.keys(a) as Array).every((key) => a[key] === b[key]) 197 | } 198 | 199 | function diffAttributes( 200 | target?: RichTextAttributes, 201 | source?: RichTextAttributes, 202 | ): { added: Array; removed: Array } { 203 | if (target == null && source == null) return { added: [], removed: [] } 204 | else if (target == null && source != null) return { added: Object.keys(source) as any, removed: [] } 205 | else if (target != null && source == null) return { added: [], removed: Object.keys(target) as any } 206 | else if (target != null && source != null) { 207 | const added: Array = [] 208 | const removed: Array = [] 209 | 210 | for (const key of Object.keys(target) as Array) { 211 | if (!(key in source)) { 212 | removed.push(key) 213 | } else if (source[key] !== target[key]) { 214 | added.push(key) 215 | removed.push(key) 216 | } 217 | } 218 | 219 | for (const key of Object.keys(source) as Array) { 220 | if (!(key in target)) { 221 | added.push(key) 222 | } 223 | } 224 | 225 | return { added, removed } 226 | } 227 | 228 | return { added: [], removed: [] } 229 | } 230 | 231 | function pickBlockAttributes(attributes: RichTextAttributes): RichTextAttributes { 232 | const picked: RichTextAttributes = {} 233 | 234 | for (const key of ['header', 'list', 'indent'] as const) { 235 | if (key in attributes) { 236 | // @ts-expect-error 237 | picked[key] = attributes[key] 238 | } 239 | } 240 | 241 | return picked 242 | } 243 | 244 | function pickInlineAttributes(attributes: RichTextAttributes): RichTextAttributes { 245 | const picked: RichTextAttributes = {} 246 | 247 | for (const key of ['bold', 'italic', 'underline', 'code', 'link'] as const) { 248 | if (key in attributes) { 249 | // @ts-expect-error 250 | picked[key] = attributes[key] 251 | } 252 | } 253 | 254 | return picked 255 | } 256 | -------------------------------------------------------------------------------- /packages/grammarly-richtext-encoder/src/LanguageHTML.ts: -------------------------------------------------------------------------------- 1 | import { RichTextAttributes } from '@grammarly/sdk' 2 | import { createTransformer } from './Language' 3 | 4 | const IGNORED_NODES = new Set([ 5 | "'", 6 | '/>', 7 | '"', 8 | '<', 9 | '', 13 | 'attribute', 14 | 'attribute_name', 15 | 'attribute_value', 16 | 'comment', 17 | 'doctype', 18 | 'end_tag', 19 | 'erroneous_end_tag', 20 | 'erroneous_end_tag_name', 21 | 'fragment', 22 | 'quoted_attribute_value', 23 | 'raw_text', 24 | 'script_element', 25 | 'self_closing_tag', 26 | 'start_tag', 27 | 'style_element', 28 | 'tag_name', 29 | ]) 30 | const OTHER_NODES = new Set(['text']) 31 | const BLOCK_NODES = new Set(['element']) 32 | 33 | export const html = createTransformer({ 34 | isBlockNode(node) { 35 | return BLOCK_NODES.has(node.type) 36 | }, 37 | shouldIgnoreSubtree(node) { 38 | return IGNORED_NODES.has(node.type) 39 | }, 40 | getAttributesFor(node) { 41 | switch (node.type) { 42 | case 'strong_emphasis': 43 | return { bold: true } 44 | case 'emphasis': 45 | return { italic: true } 46 | case 'code_span': 47 | return { code: true } 48 | case 'atx_heading': 49 | if (node.firstChild != null) { 50 | // atx_h[1-6]_marker 51 | return { header: parseInt(node.firstChild.type.substring(5, 6), 10) as 1 | 2 | 3 | 4 | 5 | 6 } 52 | } 53 | return {} 54 | 55 | default: 56 | return {} 57 | } 58 | }, 59 | stringify(node, content) { 60 | if (node.type === '#text') { 61 | if (typeof node.op === 'string') return node.op 62 | return '\n' 63 | } 64 | 65 | return toHTML(content, node.value) 66 | 67 | function toHTML(text: string, attributes: RichTextAttributes): string { 68 | if (attributes.bold) return `${text}` 69 | if (attributes.italic) return `${text}` 70 | if (attributes.code) return `${text}` 71 | if (attributes.linebreak) return `
` 72 | if (attributes.link) return `${text}` 73 | if (attributes.header) return `${content}` 74 | 75 | return text 76 | } 77 | }, 78 | processNode(node, insert) { 79 | if (node.type === 'text') { 80 | insert(node.text, node) 81 | } 82 | }, 83 | }) 84 | -------------------------------------------------------------------------------- /packages/grammarly-richtext-encoder/src/LanguageMarkdown.ts: -------------------------------------------------------------------------------- 1 | import type { RichTextAttributes } from '@grammarly/sdk' 2 | import { createTransformer } from './Language' 3 | 4 | const IGNORED_NODES = new Set([ 5 | 'atx_h1_marker', 6 | 'atx_h2_marker', 7 | 'atx_h3_marker', 8 | 'atx_h4_marker', 9 | 'atx_h5_marker', 10 | 'atx_h6_marker', 11 | 'block_quote', 12 | 'code_fence_content', 13 | 'fenced_code_block', 14 | 'html_atrribute', 15 | 'html_attribute_key', 16 | 'html_attribute_value', 17 | 'html_cdata_section', 18 | 'html_close_tag', 19 | 'html_comment', 20 | 'html_declaration_name', 21 | 'html_declaration', 22 | 'html_open_tag', 23 | 'html_processing_instruction', 24 | 'html_self_closing_tag', 25 | 'html_tag_name', 26 | 'link_destination', 27 | 'link_reference_definition', 28 | 'list_marker', 29 | 'setext_h1_underline', 30 | 'setext_h2_underline', 31 | 32 | 'table_cell', 33 | 'table_column_alignment', 34 | 'table_data_row', 35 | 'table_delimiter_row', 36 | 'table_header_row', 37 | 'table', 38 | 'task_list_item_marker', 39 | ]) 40 | const OTHER_NODES = new Set([ 41 | 'backslash_escape', 42 | 'character_reference', 43 | 'code_span', 44 | 'email_autolink', 45 | 'emphasis', 46 | 'hard_line_break', 47 | 'heading_content', 48 | 'image', 49 | 'info_string', 50 | 'line_break', 51 | 'link_label', 52 | 'link_title', 53 | 'link', 54 | 'link_text', 55 | 'loose_list', 56 | 'soft_line_break', 57 | 'strikethrough', 58 | 'strong_emphasis', 59 | 'text', 60 | 'uri_autolink', 61 | 'virtual_space', 62 | 'www_autolink', 63 | ]) 64 | const BLOCK_NODES = new Set([ 65 | 'document', 66 | 'atx_heading', 67 | 'setext_heading', 68 | 'task_list_item', 69 | 'html_block', 70 | 'image_description', 71 | 'indented_code_block', 72 | 'list_item', 73 | 'paragraph', 74 | 'thematic_break', 75 | 'tight_list', 76 | ]) 77 | 78 | export const markdown = createTransformer({ 79 | isBlockNode(node) { 80 | return BLOCK_NODES.has(node.type) 81 | }, 82 | shouldIgnoreSubtree(node) { 83 | return IGNORED_NODES.has(node.type) 84 | }, 85 | getAttributesFor(node, attributes) { 86 | switch (node.type) { 87 | case 'strong_emphasis': 88 | return { bold: true } 89 | case 'emphasis': 90 | return { italic: true } 91 | case 'code_span': 92 | return { code: true } 93 | case 'atx_heading': 94 | if (node.firstChild != null) { 95 | // atx_h[1-6]_marker 96 | return { header: parseInt(node.firstChild.type.substring(5, 6), 10) as 1 | 2 | 3 | 4 | 5 | 6 } 97 | } 98 | return {} 99 | case 'setext_heading': 100 | if (node.lastNamedChild != null) { 101 | // setext_h[1-2]_underline 102 | return { 103 | header: parseInt(node.lastNamedChild.type.substring('setext_h'.length, 'setext_h'.length + 1), 10) as 1 | 2, 104 | } 105 | } 106 | return {} 107 | case 'link': 108 | if (node.lastNamedChild?.type === 'link_destination') { 109 | return { 110 | link: node.lastNamedChild.firstNamedChild?.text ?? '', 111 | } 112 | } 113 | return { link: '' } 114 | 115 | case 'tight_list': 116 | return { 117 | list: /[0-9]/.test(node.firstNamedChild?.firstNamedChild?.text ?? '') ? 'number' : 'bullet', 118 | indent: attributes.indent != null ? attributes.indent + 1 : 1, 119 | } 120 | 121 | default: 122 | return {} 123 | } 124 | }, 125 | stringify(node, content) { 126 | if (node.type === '#text') { 127 | if (typeof node.op.insert === 'string') return node.op.insert 128 | return '\n' 129 | } 130 | 131 | return toMarkdown(content, node.value) 132 | }, 133 | processNode(node, insert) { 134 | if (node.type === 'text') { 135 | if (node.parent?.parent?.type === 'atx_heading' && node.parent.firstChild?.equals(node)) { 136 | insert(node.text.slice(1), node, 1) 137 | } else if ( 138 | node.parent?.type === 'paragraph' && 139 | node.text.startsWith(': ') && 140 | node.parent.firstChild?.equals(node) 141 | ) { 142 | insert(node.text.slice(2), node, 2) // #255 - Definition Lists 143 | } else insert(node.text, node) 144 | } else if (node.type === 'line_break' || node.type === 'hard_line_break') { 145 | insert('\n', node) 146 | } else if (node.type === 'soft_line_break') { 147 | insert(' ', node) 148 | } 149 | }, 150 | }) 151 | 152 | function toMarkdown(text: string, attributes?: RichTextAttributes): string { 153 | if (attributes == null) return text 154 | if (attributes.bold) return `**${text}**` 155 | if (attributes.italic) return `_${text}_` 156 | if (attributes.code) return '`' + text + '`' 157 | if (attributes.link) return `[${text}](${attributes.link})` 158 | if (attributes.list && attributes.indent != null) 159 | return `${' '.repeat(attributes.indent - 1)}${attributes.list === 'number' ? '1. ' : '- '}${text}\n` 160 | if (attributes.header) return '#'.repeat(attributes.header) + ' ' + text + '\n' 161 | if (attributes['code-block']) return '```\n' + text + '\n```\n' 162 | return text 163 | } 164 | -------------------------------------------------------------------------------- /packages/grammarly-richtext-encoder/src/index.ts: -------------------------------------------------------------------------------- 1 | import Parser from 'web-tree-sitter' 2 | import { html } from './LanguageHTML' 3 | import { markdown } from './LanguageMarkdown' 4 | 5 | export type { SourceMap, Transformer } from './Language' 6 | 7 | const parsers = new Map() 8 | const parsersPending = new Map>() 9 | 10 | export async function createParser(language: string): Promise { 11 | const previous = parsers.get(language) 12 | if (previous != null) return previous 13 | const parser = createParserInner() 14 | parsersPending.set(language, parser) 15 | 16 | return await parser 17 | 18 | async function createParserInner() { 19 | if (isNodeJS()) { 20 | const fetch = globalThis.fetch 21 | try { 22 | // @ts-ignore 23 | globalThis.fetch = null 24 | await Parser.init() 25 | } catch (e) { 26 | console.log('Error in TreeSitter parser:', e) 27 | throw e 28 | } finally { 29 | globalThis.fetch = fetch 30 | } 31 | } else { 32 | await Parser.init() 33 | } 34 | 35 | try { 36 | const parser = new Parser() 37 | parser.setLanguage(await Parser.Language.load(getLanguageFile())) 38 | parsers.set(language, parser) 39 | parsersPending.delete(language) 40 | return parser 41 | } catch (e) { 42 | console.log(`Error in TreeSitter ${language} parser:`, e) 43 | throw e 44 | } 45 | } 46 | 47 | function getLanguageFile(): string | Uint8Array { 48 | if (isNodeJS()) { 49 | // @ts-ignore 50 | if (process.env.NODE_ENV === 'test') { 51 | // @ts-ignore 52 | return require.resolve(`../dist/tree-sitter-${language}.wasm`) 53 | } 54 | // @ts-ignore 55 | return require.resolve(`./tree-sitter-${language}.wasm`) 56 | } 57 | 58 | return `tree-sitter-${language}.wasm` 59 | } 60 | } 61 | 62 | function isNodeJS(): boolean { 63 | // @ts-ignore - Ignore if process does not exist. 64 | return typeof process !== 'undefined' && process.versions?.node != null 65 | } 66 | 67 | export const transformers = { html, markdown } as const 68 | -------------------------------------------------------------------------------- /packages/grammarly-richtext-encoder/test/__snapshots__/markdown.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`markdown encode 1`] = ` 4 | Heading 1 5 | \\n {"header":1} 6 | Heading 2 7 | \\n {"header":2} 8 | Heading 5 9 | \\n {"header":3} 10 | Heading 4 11 | \\n {"header":4} 12 | Heading 5 13 | \\n {"header":5} 14 | Heading 6 15 | \\n {"header":5} 16 | Heading 1 17 | \\n {"header":1} 18 | Heading 2 19 | \\n {"header":2} 20 | Inline text can be 21 | bold {"bold":true} 22 | , 23 | italic {"italic":true} 24 | , 25 | strikethrough 26 | , or 27 | code {"code":true} 28 | . 29 | \\n 30 | Links: 31 | link1 {"link":""} 32 | 33 | link2 {"link":"#href"} 34 | \\n 35 | Unordered List 36 | \\n {"list":"bullet","indent":1} 37 | A 38 | \\n {"list":"bullet","indent":1} 39 | A.1 40 | \\n {"list":"bullet","indent":2} 41 | A.2 42 | \\n {"list":"bullet","indent":2} 43 | B 44 | \\n {"list":"bullet","indent":1} 45 | B.1 46 | \\n {"list":"bullet","indent":2} 47 | B.2 48 | \\n {"list":"bullet","indent":2} 49 | Some 50 | inline 51 | html 52 | \\n 53 | First Term 54 | 55 | : This is the definition of the first term. 56 | \\n 57 | Second Term 58 | 59 | : This is one definition of the second term. 60 | 61 | : This is another definition of the second term. 62 | \\n 63 | 1 {"link":""} 64 | header 65 | \\n {"header":1} 66 | `; 67 | -------------------------------------------------------------------------------- /packages/grammarly-richtext-encoder/test/markdown.md: -------------------------------------------------------------------------------- 1 | # Heading 1 2 | 3 | ## Heading 2 4 | 5 | ### Heading 5 6 | 7 | #### Heading 4 8 | 9 | ##### Heading 5 10 | 11 | ##### Heading 6 12 | 13 | # Heading 1 14 | 15 | ## Heading 2 16 | 17 | Inline text can be **bold**, _italic_, ~~strikethrough~~, or `code`. 18 | 19 | Links: [link1] [link2](#href) 20 | 21 | - Unordered List 22 | - A 23 | - A.1 24 | - A.2 25 | - B 26 | - B.1 27 | - B.2 28 | 29 | ```js 30 | foo = bar 31 | ``` 32 | 33 | Some inline html 34 | 35 | First Term 36 | : This is the definition of the first term. 37 | 38 | Second Term 39 | : This is one definition of the second term. 40 | : This is another definition of the second term. 41 | 42 | # [1] header 43 | -------------------------------------------------------------------------------- /packages/grammarly-richtext-encoder/test/markdown.test.ts: -------------------------------------------------------------------------------- 1 | import { createParser, transformers } from '../src' 2 | import { readFile } from 'node:fs/promises' 3 | import { RichText } from '@grammarly/sdk' 4 | 5 | expect.addSnapshotSerializer({ 6 | test(value) { 7 | return typeof value !== 'string' 8 | }, 9 | print(richtext, _print, indent) { 10 | return (richtext as RichText).ops 11 | .map((op) => 12 | indent( 13 | JSON.stringify(op.insert).slice(1, -1) + 14 | (op.attributes == null || Object.keys(op.attributes).length === 0 15 | ? '' 16 | : ' ' + JSON.stringify(op.attributes)), 17 | ), 18 | ) 19 | .join('\n') 20 | }, 21 | }) 22 | 23 | describe('markdown', () => { 24 | test('encode', async () => { 25 | const parser = await createParser('markdown') 26 | const contents = await readFile(`${__dirname}/markdown.md`, 'utf-8') 27 | const [richtext] = transformers.markdown.encode(parser.parse(contents)) 28 | expect(richtext).toMatchSnapshot() 29 | }) 30 | 31 | test('decode', async () => { 32 | expect( 33 | transformers.markdown.decode({ 34 | ops: [ 35 | // 36 | { insert: 'This is ' }, 37 | { insert: 'bold text', attributes: { bold: true } }, 38 | { insert: '.' }, 39 | ], 40 | }), 41 | ).toMatchInlineSnapshot(`"This is **bold text**."`) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /packages/grammarly-richtext-encoder/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ES2019", 5 | "moduleResolution": "node", 6 | "lib": ["ES2019", "WebWorker"], 7 | 8 | "strict": true, 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "noUnusedParameters": true, 16 | "skipLibCheck": true, 17 | "esModuleInterop": true 18 | }, 19 | "include": ["src/"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/grammarly-richtext-encoder/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["./test/"] 4 | } 5 | -------------------------------------------------------------------------------- /parsers/tree-sitter-html.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-grammarly/grammarly-language-server/f923374e8da9a62cca50b568876b4b391c408b8f/parsers/tree-sitter-html.wasm -------------------------------------------------------------------------------- /parsers/tree-sitter-markdown.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-grammarly/grammarly-language-server/f923374e8da9a62cca50b568876b4b391c408b8f/parsers/tree-sitter-markdown.wasm -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - './extension/' 3 | - './packages/*' 4 | -------------------------------------------------------------------------------- /polyfills/empty.js: -------------------------------------------------------------------------------- 1 | throw new Error('Not available') 2 | -------------------------------------------------------------------------------- /polyfills/fetch.js: -------------------------------------------------------------------------------- 1 | export default function fetch() {} 2 | export const Request = null 3 | export const Response = null 4 | export const Headers = null 5 | -------------------------------------------------------------------------------- /polyfills/minimatch-path.js: -------------------------------------------------------------------------------- 1 | export const sep = '/' 2 | -------------------------------------------------------------------------------- /redirect/functions/redirect.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | let schemes = new Set([ 3 | 'vscode', 4 | 'vscode-insiders', 5 | 'vscodium', 6 | 'gitpod-code', 7 | 'code-oss', 8 | ]) 9 | let validQueryParams = new Set([ 10 | 'vscode-reqid', 11 | 'vscode-scheme', 12 | 'vscode-authority', 13 | 'vscode-path', 14 | 'windowId', 15 | ]) 16 | 17 | /** 18 | * @param {import('@netlify/functions').HandlerEvent} event 19 | * @param {import('@netlify/functions').HandlerContext} _context 20 | * @returns {Promise} 21 | */ 22 | exports.handler = async function (event, _context) { 23 | const { state, code } = event.queryStringParameters 24 | if (state == null) throw new Error(`Missing "state" query parameter.`) 25 | const url = new URL(Buffer.from(state, 'base64url').toString()) 26 | const scheme = url.protocol.slice(0, -1) 27 | if ( 28 | url.origin === 'https://github.dev' && 29 | url.pathname === '/extension-auth-callback' 30 | ) { 31 | validQueryParams = new Set(['state']) 32 | } else if (scheme === 'http' || scheme === 'https') { 33 | validate( 34 | url.searchParams.get('vscode-scheme'), 35 | url.searchParams.get('vscode-authority'), 36 | url.searchParams.get('vscode-path'), 37 | ) 38 | } else { 39 | validate(scheme, url.host, url.pathname) 40 | } 41 | 42 | url.searchParams.forEach((_, key) => { 43 | if (!validQueryParams.has(key)) url.searchParams.delete(key) 44 | }) 45 | url.searchParams.set('code', code) 46 | 47 | return getResponse(url.toString()) 48 | } 49 | 50 | function validate(scheme, hostname, pathname) { 51 | if (!schemes.has(scheme)) throw new Error(`Invalid scheme: ${scheme}`) 52 | if (hostname !== 'znck.grammarly') 53 | throw new Error(`Invalid authority: ${hostname}`) 54 | if (pathname !== '/auth/callback') 55 | throw new Error(`Invalid path: ${pathname}`) 56 | } 57 | 58 | /** 59 | * @param {string} url 60 | * @returns {import('@netlify/functions').HandlerResponse} 61 | */ 62 | function getResponse(url) { 63 | return { 64 | statusCode: 302, 65 | headers: { 66 | Location: url, 67 | 'Content-Type': 'text/html', 68 | }, 69 | body: `Redirecting to ${url}`, 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /redirect/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Grammarly for VS Code 6 | 7 | 8 |

9 | This website provides Grammarly account connection to Grammarly for VS Code extension when used as a web 10 | extension. See source of ./functions/redirect.js. 11 |

12 | 13 |

14 | Created by Rahul Kadyan. Source at 15 | https://github.com/znck/grammarly/tree/main/redirect. 18 |

19 | 20 | 21 | -------------------------------------------------------------------------------- /redirect/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "." 3 | 4 | [functions] 5 | directory = "functions/" 6 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript' 2 | import copy from 'rollup-plugin-copy' 3 | import { generateRollupOptions } from '@vuedx/monorepo-tools' 4 | import Path from 'path' 5 | 6 | export default [ 7 | ...generateRollupOptions({ 8 | extend(kind, info) { 9 | if (kind === 'dts') return info.rollupOptions 10 | const options = info.rollupOptions 11 | options.plugins.push(typescript({ tsconfig: info.tsconfig.configFile })) 12 | 13 | if (info.packageJson.name === 'grammarly' && options.input.endsWith('server.ts')) { 14 | const file = options.output[0].file 15 | options.plugins.push( 16 | wasm(file), 17 | copy({ 18 | targets: [ 19 | { 20 | src: Path.resolve( 21 | __dirname, 22 | 'packages/grammarly-languageserver/node_modules/web-tree-sitter/tree-sitter.wasm', 23 | ), 24 | dest: Path.dirname(file), 25 | }, 26 | ], 27 | }), 28 | ) 29 | } 30 | if ( 31 | info.packageJson.name === 'grammarly-languageserver' || 32 | info.packageJson.name === 'grammarly-richtext-encoder' 33 | ) { 34 | options.plugins.push(wasm(options.output[0].file)) 35 | } 36 | 37 | return options 38 | }, 39 | }), 40 | ] 41 | 42 | function wasm(file) { 43 | return copy({ 44 | targets: [ 45 | { 46 | src: Path.resolve(__dirname, 'parsers/tree-sitter-{html,markdown}.wasm'), 47 | dest: Path.dirname(file), 48 | }, 49 | ], 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /scripts/build-extension.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import fetch, { Headers } from 'node-fetch' 3 | import { execSync } from 'node:child_process' 4 | import FS from 'node:fs' 5 | import Path from 'node:path' 6 | import { fileURLToPath } from 'node:url' 7 | import semver from 'semver' 8 | 9 | /** 10 | * @param {string} itemName 11 | */ 12 | async function findLatestVersion(itemName) { 13 | const response = await fetch('https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery', { 14 | method: 'POST', 15 | headers: new Headers({ 16 | Accept: 'application/json;api-version=7.1-preview.1;excludeUrls=true', 17 | 'Content-Type': 'application/json', 18 | }), 19 | body: JSON.stringify({ 20 | assetTypes: null, 21 | filters: [ 22 | { 23 | criteria: [{ filterType: 7, value: itemName }], 24 | direction: 2, 25 | pageSize: 100, 26 | pageNumber: 1, 27 | sortBy: 0, 28 | sortOrder: 0, 29 | pagingToken: null, 30 | }, 31 | ], 32 | flags: 2151, 33 | }), 34 | }) 35 | 36 | const body = /** @type {{results: Array<{extensions: Array<{versions: Array<{version: string}>}>}>}} */ ( 37 | await response.json() 38 | ) 39 | 40 | const extension = body.results.flatMap((result) => result.extensions).find((extension) => extension != null) 41 | 42 | if (extension != null) return extension.versions[0]?.version 43 | 44 | return undefined 45 | } 46 | 47 | const fileNames = ['package.json'] 48 | export function getDir() { 49 | const arg = process.argv[2] ?? process.cwd() 50 | const dir = Path.isAbsolute(arg) ? arg : Path.resolve(process.cwd(), arg) 51 | return dir 52 | } 53 | 54 | /** 55 | * @param {string} dir 56 | */ 57 | function backup(dir) { 58 | for (const fileName of fileNames) { 59 | const file = Path.resolve(dir, fileName) 60 | const fileBak = Path.resolve(dir, `${fileName}.bak`) 61 | 62 | FS.writeFileSync(fileBak, FS.readFileSync(file, 'utf-8')) 63 | } 64 | } 65 | 66 | /** 67 | * @param {string} dir 68 | */ 69 | function revert(dir) { 70 | for (const fileName of fileNames) { 71 | const file = Path.resolve(dir, fileName) 72 | const fileBak = Path.resolve(dir, `${fileName}.bak`) 73 | 74 | if (FS.existsSync(fileBak)) { 75 | FS.writeFileSync(file, FS.readFileSync(fileBak, 'utf-8')) 76 | FS.unlinkSync(fileBak) 77 | } 78 | } 79 | } 80 | 81 | /** 82 | * @param {string} current 83 | * @param {string} [latest] 84 | */ 85 | function getNextPreReleaseVersion(current, latest) { 86 | let target = current 87 | const v1 = semver.parse(current) 88 | const v2 = semver.parse(latest) 89 | 90 | if (v1 == null) return semver.inc(target, 'minor') 91 | if (v2 == null || latest == null) { 92 | return semver.inc(target, v1.minor % 2 === 0 ? 'minor' : 'patch') 93 | } 94 | 95 | if (v2.compare(v1) > 0) target = latest 96 | 97 | return semver.inc(target, semver.minor(target) % 2 === 0 ? 'minor' : 'patch') 98 | } 99 | 100 | /** 101 | * 102 | * @param {string} dir 103 | */ 104 | async function transform(dir) { 105 | const pkg = JSON.parse(FS.readFileSync(`${dir}/package.json`, 'utf-8')) 106 | const RELEASE_CHANNEL = /** @type {'release'|'pre-release'} */ (process.env['RELEASE_CHANNEL'] ?? 'release') 107 | 108 | delete pkg.dependencies 109 | delete pkg.devDependencies 110 | 111 | if (RELEASE_CHANNEL === 'pre-release') { 112 | pkg['pre-release'] = true 113 | pkg.version = getNextPreReleaseVersion(pkg.version, await findLatestVersion(`${pkg.publisher}.${pkg.name}`)) 114 | console.debug('Setting version to', pkg.version) 115 | } 116 | 117 | const packageFile = Path.resolve(dir, 'package.json') 118 | 119 | FS.writeFileSync(packageFile, JSON.stringify(pkg, null, 2)) 120 | } 121 | 122 | /** 123 | * @param {string} dir 124 | * @param {() => void} fn 125 | */ 126 | async function prepareExtensionForPackaging(dir, fn) { 127 | try { 128 | backup(dir) 129 | await transform(dir) 130 | fn() 131 | } finally { 132 | revert(dir) 133 | } 134 | } 135 | 136 | const rootDir = Path.resolve(Path.dirname(fileURLToPath(import.meta.url)), '..') 137 | const extensionDir = Path.resolve(rootDir, 'extension') 138 | const bin = Path.resolve(rootDir, 'node_modules/.bin/vsce') 139 | prepareExtensionForPackaging(extensionDir, () => { 140 | const execArgs = { stdio: [0, 1, 2], cwd: extensionDir } 141 | const RELEASE_CHANNEL = /** @type {'release'|'pre-release'} */ (process.env['RELEASE_CHANNEL'] ?? 'release') 142 | const args = RELEASE_CHANNEL === 'pre-release' ? '--pre-release' : '' 143 | execSync( 144 | `${bin} package --no-dependencies --baseImagesUrl "https://github.com/znck/grammarly/raw/HEAD/extension" --baseContentUrl "https://github.com/znck/grammarly/raw/HEAD/extension" ${args} --out grammarly.vsix`, 145 | execArgs, 146 | ) 147 | }) 148 | -------------------------------------------------------------------------------- /scripts/build-wasm.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { execSync } from 'node:child_process' 3 | import { dirname, resolve } from 'node:path' 4 | import { fileURLToPath } from 'node:url' 5 | 6 | const packageNameByLanguage = { 7 | html: 'tree-sitter-html', 8 | markdown: 'tree-sitter-markdown', 9 | } 10 | 11 | const __filename = fileURLToPath(import.meta.url) 12 | const __dirname = dirname(__filename) 13 | const rootDir = resolve(__dirname, '..') 14 | for (const language in packageNameByLanguage) { 15 | const packageName = packageNameByLanguage[language] 16 | console.log(`* Building ${language}`) 17 | execSync(`$(pnpm bin)/tree-sitter build-wasm ${resolve(rootDir, `node_modules/${packageName}`)}`, { 18 | stdio: 'inherit', 19 | }) 20 | execSync(`mv ${packageName}.wasm parsers/tree-sitter-${language}.wasm`, { stdio: 'inherit' }) 21 | } 22 | -------------------------------------------------------------------------------- /scripts/build-web-extension.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import esbuild from 'esbuild' 3 | import fetch from 'node-fetch' 4 | import FS from 'node:fs' 5 | import Path from 'node:path' 6 | import { fileURLToPath } from 'node:url' 7 | 8 | const __dirname = Path.dirname(fileURLToPath(import.meta.url)) 9 | const { 10 | dependencies: { '@grammarly/sdk': sdkTargetVersion }, 11 | } = JSON.parse(FS.readFileSync(Path.resolve(__dirname, '../packages/grammarly-languageserver/package.json'), 'utf-8')) 12 | 13 | const controller = new AbortController() 14 | const timeout = setTimeout(() => controller.abort(), 5000) 15 | const version = String(sdkTargetVersion).replace(/^.*?(\d+\.\d+).*?$/, (_, match) => match) 16 | 17 | const response = await fetch('https://js.grammarly.com/grammarly-sdk@' + version, { 18 | signal: controller.signal, 19 | redirect: 'follow', 20 | }) 21 | clearTimeout(timeout) 22 | const contents = await response.text() 23 | 24 | await esbuild.build({ 25 | entryPoints: [Path.resolve(__dirname, '../extension/dist/extension/index.mjs')], 26 | bundle: true, 27 | external: ['vscode'], 28 | platform: 'browser', 29 | format: 'cjs', 30 | outfile: Path.resolve(__dirname, '../extension/dist/extension/index.browser.js'), 31 | write: true, 32 | }) 33 | 34 | await esbuild.build({ 35 | entryPoints: [Path.resolve(__dirname, '../extension/dist/server/index.mjs')], 36 | bundle: true, 37 | external: ['vscode'], 38 | platform: 'browser', 39 | format: 'iife', 40 | outfile: Path.resolve(__dirname, '../extension/dist/server/index.browser.js'), 41 | footer: { js: ';globalThis.window=globalThis;' + contents + '\n' }, 42 | write: true, 43 | plugins: [ 44 | { 45 | name: 'polyfills', 46 | setup(build) { 47 | build.onResolve({ filter: /.*/ }, (args) => { 48 | if ((args.path === 'path' || args.path === 'fs') && Path.basename(args.importer) === 'tree-sitter.js') { 49 | return { path: Path.resolve(__dirname, '../polyfills/empty.js') } 50 | } 51 | }) 52 | }, 53 | }, 54 | ], 55 | }) 56 | -------------------------------------------------------------------------------- /scripts/publish-extension.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { execSync } from 'node:child_process' 3 | import * as Path from 'node:path' 4 | import { fileURLToPath } from 'node:url' 5 | 6 | const rootDir = Path.resolve(Path.dirname(fileURLToPath(import.meta.url)), '..') 7 | const extensionDir = Path.resolve(rootDir, 'extension') 8 | const bin = Path.resolve(rootDir, 'node_modules/.bin/vsce') 9 | const execArgs = { stdio: [0, 1, 2], cwd: extensionDir } 10 | const RELEASE_CHANNEL = /** @type {'release'|'pre-release'} */ (process.env['RELEASE_CHANNEL'] ?? 'release') 11 | const VSCODE_MARKETPLACE_TOKEN = process.env.VSCODE_MARKETPLACE_TOKEN 12 | const OVSX_REGISTRY_TOKEN = process.env.OVSX_REGISTRY_TOKEN 13 | const args = RELEASE_CHANNEL === 'pre-release' ? '--pre-release' : '' 14 | execSync(`${bin} publish -p "${VSCODE_MARKETPLACE_TOKEN}" ${args} --packagePath grammarly.vsix`, execArgs) 15 | execSync(`pnpx ovsx publish -p "${OVSX_REGISTRY_TOKEN}" --packagePath grammarly.vsix`, execArgs) // Does not support pre-release arg yet. 16 | -------------------------------------------------------------------------------- /start.cmd: -------------------------------------------------------------------------------- 1 | @ECHO off 2 | SETLOCAL 3 | CALL :find_dp0 4 | 5 | IF EXIST "%dp0%\node.exe" ( 6 | SET "_prog=%dp0%\node.exe" 7 | ) ELSE ( 8 | SET "_prog=node" 9 | SET PATHEXT=%PATHEXT:;.JS;=;% 10 | ) 11 | 12 | "%_prog%" "%dp0%\packages\grammarly-languageserver\bin\server.js" %* 13 | ENDLOCAL 14 | EXIT /b %errorlevel% 15 | :find_dp0 16 | SET dp0=%~dp0 17 | EXIT /b 18 | --------------------------------------------------------------------------------