├── .changeset ├── README.md └── config.json ├── .github ├── FUNDING.yml └── workflows │ └── ci.yaml ├── .gitignore ├── .npmrc ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── extension ├── .vscodeignore ├── CHANGELOG.md ├── README.md ├── assets │ ├── category.png │ ├── logo.png │ ├── screenshot1.png │ ├── screenshot2.png │ └── set-credentials-screencast.gif ├── package.json ├── src │ ├── client │ │ └── index.ts │ ├── commands │ │ ├── AddWordCommand.ts │ │ ├── CheckGrammarCommand.ts │ │ ├── ClearCredentialsCommand.ts │ │ ├── IgnoreIssueCommand.ts │ │ ├── ServerCallbackCommand.ts │ │ ├── SetGoalsCommand.ts │ │ └── StatsCommand.ts │ ├── constants.ts │ ├── controllers │ │ └── StatusBarController.ts │ ├── extension.ts │ ├── form.ts │ ├── interfaces.ts │ ├── keytar.ts │ ├── server.ts │ ├── services │ │ └── AuthenticationService.ts │ ├── settings.ts │ └── utils │ │ ├── Logger.ts │ │ ├── is.ts │ │ ├── string.ts │ │ ├── toArray.ts │ │ └── watch.ts ├── test │ ├── autoActivate.spec.ts │ ├── command.spec.ts │ ├── grammarly.spec.ts │ ├── runner.ts │ ├── tsconfig.json │ └── utils.ts ├── tsconfig.json └── yarn.lock ├── fixtures ├── .gitignore ├── .vscode │ └── settings.json ├── folder │ ├── .vscode │ │ └── settings.json │ ├── add-word.md │ ├── autoActivate.off.md │ └── autoActivate.on.md ├── readme.md └── workspace │ └── workspace.code-workspace ├── jest.config.js ├── package-lock.json ├── package.json ├── packages ├── grammarly-api │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── DevLogger.ts │ │ ├── GrammarlyClient.ts │ │ ├── SocketClient.ts │ │ ├── SocketError.ts │ │ ├── auth.ts │ │ ├── checks │ │ │ ├── check.ts │ │ │ ├── checkGrammar.ts │ │ │ └── checkPlagiarism.ts │ │ ├── global.d.ts │ │ ├── index.ts │ │ └── transport │ │ │ ├── Request.ts │ │ │ ├── RequestKind.ts │ │ │ ├── Response.ts │ │ │ ├── ResponseKind.ts │ │ │ ├── enums │ │ │ ├── AlertFeedbackType.ts │ │ │ ├── AlertImpactType.ts │ │ │ ├── AlertMutedByType.ts │ │ │ ├── AlertViewType.ts │ │ │ ├── AsyncChecksTypes.ts │ │ │ ├── AutoCorrectFeedbackType.ts │ │ │ ├── AutocompleteFeedbackType.ts │ │ │ ├── DialectType.ts │ │ │ ├── DocumentAudienceType.ts │ │ │ ├── DocumentDomainType.ts │ │ │ ├── DocumentGoalType.ts │ │ │ ├── EmotionFeedbackType.ts │ │ │ ├── ErrorCodeType.ts │ │ │ ├── ErrorSeverityType.ts │ │ │ ├── FeatureType.ts │ │ │ ├── LensFeedbackType.ts │ │ │ ├── MutedFeedbackType.ts │ │ │ ├── OptionType.ts │ │ │ ├── PredictionType.ts │ │ │ ├── SuggestionRejectionReasonType.ts │ │ │ ├── SynonymFeedbackType.ts │ │ │ ├── SystemFeedbackType.ts │ │ │ ├── TakeawayFeedbackType.ts │ │ │ ├── UserMutedScopeType.ts │ │ │ ├── WritingEmotionType.ts │ │ │ ├── WritingStyleType.ts │ │ │ └── WritingToneType.ts │ │ │ ├── events │ │ │ ├── AlertEvent.ts │ │ │ ├── AlertsChangedEvent.ts │ │ │ ├── AsyncCheckFinishedEvent.ts │ │ │ ├── CompleteEvent.ts │ │ │ ├── EmotionsEvent.ts │ │ │ ├── ErrorEvent.ts │ │ │ ├── FinishedEvent.ts │ │ │ ├── HeatmapEvent.ts │ │ │ ├── PlagiarismEvent.ts │ │ │ ├── RemoveEvent.ts │ │ │ ├── TakeawaysEvent.ts │ │ │ ├── TextInfoEvent.ts │ │ │ └── TextMapsEvent.ts │ │ │ ├── interfaces │ │ │ ├── AlertCardLayout.ts │ │ │ ├── AlertExtraProperties.ts │ │ │ ├── DocumentContext.ts │ │ │ ├── DocumentStatistics.ts │ │ │ ├── EmogenieExtraProperties.ts │ │ │ ├── Emotion.ts │ │ │ ├── FeedbackType.ts │ │ │ ├── FluencyExtraProperties.ts │ │ │ ├── HeatmapRange.ts │ │ │ ├── Id.ts │ │ │ ├── IdAlert.ts │ │ │ ├── IdHeatmap.ts │ │ │ ├── IdRevision.ts │ │ │ ├── IdTakeaway.ts │ │ │ ├── OutcomeScores.ts │ │ │ ├── OutcomeScoresWithPlagiarism.ts │ │ │ ├── PlagiarismExtraProperties.ts │ │ │ ├── Synonym.ts │ │ │ ├── SynonymsGroup.ts │ │ │ └── VoxExtraProperties.ts │ │ │ ├── messages │ │ │ ├── AlertFeedbackRequest.ts │ │ │ ├── AlertFeedbackResponse.ts │ │ │ ├── BaseAckResponse.ts │ │ │ ├── BaseFeedbackAckResponse.ts │ │ │ ├── BaseFeedbackRequest.ts │ │ │ ├── BaseRequest.ts │ │ │ ├── BaseResponse.ts │ │ │ ├── DebugInfoRequest.ts │ │ │ ├── DebugInfoResponse.ts │ │ │ ├── EmotionFeedbackRequest.ts │ │ │ ├── EmotionFeedbackResponse.ts │ │ │ ├── LensFeedbackRequest.ts │ │ │ ├── LensFeedbackResponse.ts │ │ │ ├── MutedFeedbackRequest.ts │ │ │ ├── MutedFeedbackResponse.ts │ │ │ ├── OptionRequest.ts │ │ │ ├── OptionResponse.ts │ │ │ ├── PingRequest.ts │ │ │ ├── PingResponse.ts │ │ │ ├── SetContextRequest.ts │ │ │ ├── SetContextResponse.ts │ │ │ ├── StartRequest.ts │ │ │ ├── StartResponse.ts │ │ │ ├── SubmitOTChunkRequest.ts │ │ │ ├── SubmitOTChunkResponse.ts │ │ │ ├── SubmitOTRequest.ts │ │ │ ├── SubmitOTResponse.ts │ │ │ ├── SynonymsRequest.ts │ │ │ ├── SynonymsResponse.ts │ │ │ ├── SystemFeedbackRequest.ts │ │ │ ├── SystemFeedbackResponse.ts │ │ │ ├── TextStatsRequest.ts │ │ │ ├── TextStatsResponse.ts │ │ │ ├── ToggleChecksRequest.ts │ │ │ └── ToggleChecksResponse.ts │ │ │ └── ot │ │ │ ├── ChangeSet.ts │ │ │ ├── Delta.ts │ │ │ ├── Op.ts │ │ │ ├── OpDelete.ts │ │ │ ├── OpInsert.ts │ │ │ ├── OpRetain.ts │ │ │ ├── Range.ts │ │ │ └── Transform.ts │ ├── tsconfig.json │ └── yarn.lock ├── grammarly-language-client │ ├── CHANGELOG.md │ ├── LICENSE │ ├── package.json │ ├── src │ │ ├── GrammarlyLanguageClient.ts │ │ ├── GrammarlyLanguageClientOptions.ts │ │ ├── global.d.ts │ │ ├── index.ts │ │ └── options.ts │ ├── tsconfig.json │ └── yarn.lock └── grammarly-language-server │ ├── CHANGELOG.md │ ├── bin │ └── server.js │ ├── package.json │ ├── src │ ├── DevLogger.ts │ ├── GrammarlyDocument.ts │ ├── GrammarlyHostFactory.ts │ ├── constants.ts │ ├── global.d.ts │ ├── helpers.ts │ ├── hosts │ │ ├── CheckHostStatus.ts │ │ └── TextGrammarCheckHost.ts │ ├── index.ts │ ├── interfaces.ts │ ├── is.ts │ ├── parsers │ │ ├── index.ts │ │ └── markdown.ts │ ├── protocol.ts │ ├── services │ │ ├── ConfigurationService.ts │ │ ├── DictionaryService.ts │ │ ├── DocumentService.ts │ │ ├── GrammarlyDiagnosticsService.ts │ │ └── toArray.ts │ ├── settings.ts │ ├── string.ts │ └── watch.ts │ ├── tsconfig.json │ └── yarn.lock ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── rollup.config.js ├── scripts └── runTests.js ├── start.cmd ├── tsconfig.test.tsbuildinfo └── yarn.lock /.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 | "unofficial-grammarly-language-client", 8 | "unofficial-grammarly-language-server" 9 | ] 10 | ], 11 | "access": "public", 12 | "baseBranch": "main", 13 | "updateInternalDependencies": "patch" 14 | } 15 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: znck 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release: 7 | description: Should publish packages and extensions? 8 | default: 'no' 9 | pull_request: 10 | release: 11 | types: 12 | - released 13 | push: 14 | paths-ignore: 15 | - 'docs/**' 16 | - 'samples/**' 17 | - '.github/**' 18 | - '!.github/workflows/ci.yaml' 19 | branches: 20 | - main 21 | 22 | defaults: 23 | run: 24 | shell: bash 25 | 26 | jobs: 27 | debug: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - run: | 31 | echo "ref: ${{ github.ref }}, event: ${{ github.event_name }}, action: ${{ github.event.action }}" 32 | 33 | release: 34 | name: Release 35 | runs-on: ubuntu-latest 36 | environment: Production 37 | if: | 38 | ( 39 | github.event_name == 'release' && 40 | github.event.action == 'released' 41 | ) || ( 42 | github.event.inputs.release == 'yes' 43 | ) 44 | steps: 45 | - name: Checkout 46 | uses: actions/checkout@v2 47 | 48 | - name: Setup Node 49 | uses: actions/setup-node@v2-beta 50 | with: 51 | node-version: '14.14.0' 52 | 53 | - name: Setup PNPM 54 | uses: pnpm/action-setup@v1.2.1 55 | with: 56 | version: 5.5.4 57 | run_install: | 58 | - args: [--frozen-lockfile, --silent] 59 | 60 | - name: Build Packages 61 | run: | 62 | pnpm build 63 | 64 | - name: Publish Extension 65 | run: | 66 | pnpm recursive --filter ./extension run release 67 | env: 68 | VSCODE_MARKETPLACE_TOKEN: ${{ secrets.VSCE_TOKEN }} 69 | 70 | - name: Publish Packages 71 | run: | 72 | echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' >> .npmrc 73 | pnpm recursive --filter ./packages publish --tag latest --access public --no-git-checks 74 | env: 75 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.vsix 3 | dist/ 4 | /.log 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | root=true 2 | publish-branch=main 3 | link-workspace-packages = true 4 | -------------------------------------------------------------------------------- /.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 | "compounds": [ 8 | { 9 | "name": "Debug Extension", 10 | "configurations": ["Run Extension", "Attach to TS Server"] 11 | } 12 | ], 13 | "configurations": [ 14 | { 15 | "name": "Attach to TS Server", 16 | "type": "node", 17 | "request": "attach", 18 | "protocol": "inspector", 19 | "port": 6009, 20 | "sourceMaps": true, 21 | "outFiles": ["${workspaceFolder}/out/*.js"] 22 | }, 23 | 24 | { 25 | "name": "Run Extension", 26 | "type": "extensionHost", 27 | "request": "launch", 28 | "runtimeExecutable": "${execPath}", 29 | "env": { 30 | "DEBUG": "grammarly:*" 31 | }, 32 | "args": [ 33 | "--disable-extensions", 34 | "--extensionDevelopmentPath=${workspaceFolder}/extension", 35 | "${workspaceFolder}/fixtures" 36 | ], 37 | "outFiles": ["${workspaceFolder}/extension/dist/*.js"] 38 | }, 39 | { 40 | "name": "Extension Tests", 41 | "type": "extensionHost", 42 | "request": "launch", 43 | "runtimeExecutable": "${execPath}", 44 | "args": [ 45 | "--disable-extensions", 46 | "--extensionDevelopmentPath=${workspaceFolder}", 47 | "--extensionTestsPath=${workspaceFolder}/out-test/runner", 48 | "--user-data-dir='${workspaceFolder}/fixtures/user'", 49 | "${workspaceFolder}/fixtures/folder" 50 | ], 51 | "outFiles": ["${workspaceFolder}/out-test/**/*.js", "${workspaceFolder}/out/**/*.js"], 52 | "preLaunchTask": "npm: build:test" 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "out": false 4 | }, 5 | "search.exclude": { 6 | "out": true 7 | }, 8 | "typescript.tsc.autoDetect": "off", 9 | "editor.formatOnSave": true, 10 | "cSpell.words": [ 11 | "AUTOCORRECT", 12 | "Emogenie", 13 | "Grammarly", 14 | "capi", 15 | "docid", 16 | "errored", 17 | "freews", 18 | "gnar", 19 | "heatmap", 20 | "inversify", 21 | "keytar", 22 | "languageclient", 23 | "minicard", 24 | "subalerts", 25 | "textdocument" 26 | ], 27 | "typescript.preferences.quoteStyle": "single", 28 | "typescript.preferences.importModuleSpecifierEnding": "minimal", 29 | "cSpell.enabled": true 30 | } 31 | -------------------------------------------------------------------------------- /.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 Rahul Kadyan 4 | Copyright (c) 2021-2022 Jen-Chieh Shen 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 | > ⚠️ **Notice:** This language server is deprecated; we now move to the newer one, 2 | [grammarly-language-server](https://github.com/emacs-grammarly/grammarly-language-server). 3 | 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) 5 | [![Release](https://img.shields.io/github/release/emacs-grammarly/unofficial-grammarly-language-server.svg?logo=github)](https://github.com/emacs-grammarly/unofficial-grammarly-language-server/releases/latest) 6 | [![npm](https://img.shields.io/npm/v/@emacs-grammarly/unofficial-grammarly-language-server?logo=npm&color=green)](https://www.npmjs.com/package/@emacs-grammarly/unofficial-grammarly-language-server) 7 | [![npm-dt](https://img.shields.io/npm/dt/@emacs-grammarly/unofficial-grammarly-language-server.svg)](https://npmcharts.com/compare/@emacs-grammarly/unofficial-grammarly-language-server?minimal=true) 8 | [![npm-dm](https://img.shields.io/npm/dm/@emacs-grammarly/unofficial-grammarly-language-server.svg)](https://npmcharts.com/compare/@emacs-grammarly/unofficial-grammarly-language-server?minimal=true) 9 | 10 | # Grammarly 11 | 12 | [![CI/CD](https://github.com/emacs-grammarly/unofficial-grammarly-language-server/actions/workflows/ci.yaml/badge.svg)](https://github.com/emacs-grammarly/unofficial-grammarly-language-server/actions/workflows/ci.yaml) 13 | [![dependencies Status](https://status.david-dm.org/gh/emacs-grammarly/unofficial-grammarly-language-server.svg)](https://david-dm.org/emacs-grammarly/unofficial-grammarly-language-server) 14 | 15 | > Update: Grammarly API is released, so this project will switch to official API. See https://github.com/znck/grammarly/issues/206 16 | 17 | Unofficial Grammarly extension. 18 | 19 | ![Preview of Grammarly diagnostics](./extension/assets/screenshot1.png) 20 | 21 | ## Using a Paid Grammarly Account 22 | 23 | Use the `Grammarly: Login to grammarly.com` command to enter your account credentials. The command will prompt you first for username and then for a password. 24 | 25 | ![set credentials screencast](./extension/assets/set-credentials-screencast.gif) 26 | 27 | ## Configuring alert severity 28 | 29 | You can assign severity levels to the Grammarly diagnostics category. To find diagnostics category name, hover on a problem in the editor. (see category name highlighted in the following example) 30 | 31 | ![Grammarly diagnostic category](./extension/assets/category.png) 32 | 33 | ```json 34 | { 35 | "grammarly.severity": { 36 | "Fragment": 2 // Sets Fragment category to Warning. 37 | } 38 | } 39 | ``` 40 | 41 | ### Severity Levels 42 | 43 | | Name | Value | 44 | | ----------- | ----- | 45 | | Error | 1 | 46 | | Warning | 2 | 47 | | Information | 3 | 48 | | Hint | 4 | 49 | 50 | ## Extension Settings 51 | 52 | This extension contributes to the following settings: 53 | 54 | - `grammarly.autoActivate`: Configures Grammarly activation behavior. When set to `false`, you need to run `Grammarly: Check grammar errors` to start Grammarly service. 55 | - `grammarly.audience`: Sets the default audience for every document. 56 | - `grammarly.dialect`: Sets the default dialect for every document. 57 | - `grammarly.domain`: Sets the default domain for every document. 58 | - `grammarly.emotions`: Sets the default list of emotions for every document. 59 | - `grammarly.goals`: Sets the default list of goals for every document. 60 | - `grammarly.userWords`: Custom word in the user dictionary. 61 | - `grammarly.overrides`: Customize `audience`, `dialect`, `domain`, `emotions` and `goals` for specific documents. 62 | - `grammarly.diagnostics`: Sets language-specific rules to ignore unnecessary diagnostics. 63 | - `grammarly.severity`: Remap the severity of Grammarly alerts. 64 | 65 | ## Release Notes 66 | 67 | ### Version 0.12.0 68 | 69 | - Adds a command to clear credentials. 70 | 71 | ### Version 0.11.0 72 | 73 | - New languages: asciidoc and json. 74 | - Improved status bar to show extension activity and document status. 75 | 76 | ### Version 0.10.0 77 | 78 | - Opt-out automatic activation by setting `grammarly.autoActivate` to `false`. 79 | 80 | ### Version 0.9.0 81 | 82 | - Set document goals interactively using `Grammarly: Set document goals` command. 83 | 84 | ### Version 0.8.0 85 | 86 | - All file schemes (except `git://`) are supported. 87 | - Diagnostics severity is now configurable. 88 | - A detailed explanation for grammar issues is provided on hover. 89 | - Diagnostics positions are updated on text change. 90 | 91 | ### Version 0.7.0 92 | 93 | Using the keytar package to store user credentials in the system keychain. 94 | 95 | ### Version 0.6.0 96 | 97 | Ignore diagnostics in regions of markdown. By default, fenced code blocks are ignored. 98 | 99 | To ignore inline code snippets, set `grammarly.diagnostics` to: 100 | 101 | ```json 102 | { 103 | "[markdown]": { 104 | "ignore": ["inlineCode", "code"] 105 | } 106 | } 107 | ``` 108 | 109 | The `ignore` option uses node types from remark AST, you can find supported type in [this example on ASTExplorer](https://astexplorer.net/#/gist/6f869d3c43eed83a533b8146ac0f470b/latest). 110 | 111 | ### Version 0.5.0 112 | 113 | Custom Grammarly goals per document. 114 | 115 | ### Version 0.4.0 116 | 117 | Dismiss alerts. 118 | 119 | ### Version 0.3.0 120 | 121 | Save words to local or Grammarly dictionary. 122 | 123 | ![Add to dictionary example](./extension/assets/screenshot2.png) 124 | 125 | ### Version 0.1.0 126 | 127 | Uses incremental document sync to send operational transformation messages to Grammarly API which 128 | gives near real-time feedback/diagnostics. 129 | 130 | ### Version 0.0.0 131 | 132 | The initial release of unofficial Grammarly extension. 133 | 134 | **Enjoy!** 135 | -------------------------------------------------------------------------------- /extension/.vscodeignore: -------------------------------------------------------------------------------- 1 | ** 2 | !dist/ 3 | !assets/ 4 | !package.json 5 | !readme.md 6 | !CHANGELOG.md 7 | -------------------------------------------------------------------------------- /extension/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # grammarly 2 | 3 | ## 0.14.0 4 | 5 | ### Minor Changes 6 | 7 | - 1ed857d: Show diagnostics in the correct position after accepting fixes 8 | 9 | ## 0.13.0 10 | 11 | ### Minor Changes 12 | 13 | - OAuth Support 14 | -------------------------------------------------------------------------------- /extension/README.md: -------------------------------------------------------------------------------- 1 | # Grammarly for VS Code 2 | 3 | Unofficial Grammarly extension. [💖 Sponsor](https://github.com/sponsors/znck) 4 | 5 | ![Preview of Grammarly diagnostics](./assets/screenshot1.png) 6 | 7 | ## Using a Paid Grammarly Account 8 | 9 | Use the `Grammarly: Login to grammarly.com` command to enter your account credentials. The command will prompt you first for username and then for a password. 10 | 11 | ![set credentials screencast](./assets/set-credentials-screencast.gif) 12 | 13 | ## Configuring alert severity 14 | 15 | You can assign severity levels to the Grammarly diagnostics category. To find diagnostics category name, hover on a problem in the editor. (see category name highlighted in the following example) 16 | 17 | ![Grammarly diagnostic category](./assets/category.png) 18 | 19 | ```json 20 | { 21 | "grammarly.severity": { 22 | "Fragment": 2 // Sets Fragment category to Warning. 23 | } 24 | } 25 | ``` 26 | 27 | ### Severity Levels 28 | 29 | | Name | Value | 30 | | ----------- | ----- | 31 | | Error | 1 | 32 | | Warning | 2 | 33 | | Information | 3 | 34 | | Hint | 4 | 35 | 36 | ## Extension Settings 37 | 38 | This extension contributes to the following settings: 39 | 40 | - `grammarly.autoActivate`: Configures Grammarly activation behavior. When set to `false`, you need to run `Grammarly: Check grammar errors` to start Grammarly service. 41 | - `grammarly.audience`: Sets the default audience for every document. 42 | - `grammarly.dialect`: Sets the default dialect for every document. 43 | - `grammarly.domain`: Sets the default domain for every document. 44 | - `grammarly.emotions`: Sets the default list of emotions for every document. 45 | - `grammarly.goals`: Sets the default list of goals for every document. 46 | - `grammarly.userWords`: Custom word in the user dictionary. 47 | - `grammarly.overrides`: Customize `audience`, `dialect`, `domain`, `emotions` and `goals` for specific documents. 48 | - `grammarly.diagnostics`: Sets language-specific rules to ignore unnecessary diagnostics. 49 | - `grammarly.severity`: Remap the severity of Grammarly alerts. 50 | 51 | ## Release Notes 52 | 53 | ### Version 0.12.0 54 | 55 | - Adds a command to clear credentials. 56 | 57 | ### Version 0.11.0 58 | 59 | - New languages: asciidoc and json. 60 | - Improved status bar to show extension activity and document status. 61 | 62 | ### Version 0.10.0 63 | 64 | - Opt-out automatic activation by setting `grammarly.autoActivate` to `false`. 65 | 66 | ### Version 0.9.0 67 | 68 | - Set document goals interactively using `Grammarly: Set document goals` command. 69 | 70 | ### Version 0.8.0 71 | 72 | - All file schemes (except `git://`) are supported. 73 | - Diagnostics severity is now configurable. 74 | - A detailed explanation for grammar issues is provided on hover. 75 | - Diagnostics positions are updated on text change. 76 | 77 | ### Version 0.7.0 78 | 79 | Using the keytar package to store user credentials in the system keychain. 80 | 81 | ### Version 0.6.0 82 | 83 | Ignore diagnostics in regions of markdown. By default, fenced code blocks are ignored. 84 | 85 | To ignore inline code snippets, set `grammarly.diagnostics` to: 86 | 87 | ```json 88 | { 89 | "[markdown]": { 90 | "ignore": ["inlineCode", "code"] 91 | } 92 | } 93 | ``` 94 | 95 | The `ignore` option uses node types from remark AST, you can find supported type in [this example on ASTExplorer](https://astexplorer.net/#/gist/6f869d3c43eed83a533b8146ac0f470b/latest). 96 | 97 | ### Version 0.5.0 98 | 99 | Custom Grammarly goals per document. 100 | 101 | ### Version 0.4.0 102 | 103 | Dismiss alerts. 104 | 105 | ### Version 0.3.0 106 | 107 | Save words to local or Grammarly dictionary. 108 | 109 | ![Add to dictionary example](./assets/screenshot2.png) 110 | 111 | ### Version 0.1.0 112 | 113 | Uses incremental document sync to send operational transformation messages to Grammarly API which 114 | gives near real-time feedback/diagnostics. 115 | 116 | ### Version 0.0.0 117 | 118 | The initial release of unofficial Grammarly extension. 119 | 120 | **Enjoy!** 121 | -------------------------------------------------------------------------------- /extension/assets/category.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-grammarly/unofficial-grammarly-language-server/26c1899bce4913be1e25306375e2a522981a6706/extension/assets/category.png -------------------------------------------------------------------------------- /extension/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-grammarly/unofficial-grammarly-language-server/26c1899bce4913be1e25306375e2a522981a6706/extension/assets/logo.png -------------------------------------------------------------------------------- /extension/assets/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-grammarly/unofficial-grammarly-language-server/26c1899bce4913be1e25306375e2a522981a6706/extension/assets/screenshot1.png -------------------------------------------------------------------------------- /extension/assets/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-grammarly/unofficial-grammarly-language-server/26c1899bce4913be1e25306375e2a522981a6706/extension/assets/screenshot2.png -------------------------------------------------------------------------------- /extension/assets/set-credentials-screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacs-grammarly/unofficial-grammarly-language-server/26c1899bce4913be1e25306375e2a522981a6706/extension/assets/set-credentials-screencast.gif -------------------------------------------------------------------------------- /extension/src/client/index.ts: -------------------------------------------------------------------------------- 1 | import { GrammarlyLanguageClient } from '@emacs-grammarly/unofficial-grammarly-language-client' 2 | import { Disposable, ExtensionContext, Uri, workspace } from 'vscode' 3 | import { AuthenticationService } from '../services/AuthenticationService' 4 | import { Registerable } from '../interfaces' 5 | 6 | export class GrammarlyClient extends GrammarlyLanguageClient implements Registerable { 7 | constructor(context: ExtensionContext, private readonly auth: AuthenticationService) { 8 | 9 | super(context.asAbsolutePath('dist/server/index.js'), { 10 | info: { 11 | name: 'Grammarly' 12 | }, 13 | getCredentials: async () => { 14 | if (process.env.EXTENSION_TEST_MODE) return null 15 | 16 | return null 17 | }, 18 | loadToken: async () => { 19 | if (process.env.EXTENSION_TEST_MODE) return null 20 | 21 | return await this.auth.getCookie() 22 | }, 23 | saveToken: async (cookie) => { 24 | await this.auth.setCookie(cookie) 25 | }, 26 | getIgnoredDocuments: (uri) => 27 | workspace.getConfiguration('grammarly', Uri.parse(uri)).get('ignore') ?? [], 28 | }) 29 | } 30 | 31 | register() { 32 | return Disposable.from(this.start()) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /extension/src/commands/AddWordCommand.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify' 2 | import vscode, { commands, ConfigurationTarget, Uri, workspace } from 'vscode' 3 | import { GrammarlyClient } from '../client' 4 | import { Registerable } from '../interfaces' 5 | 6 | @injectable() 7 | export class AddWordCommand implements Registerable { 8 | constructor(private readonly client: GrammarlyClient) {} 9 | 10 | register() { 11 | return commands.registerCommand('grammarly.addWord', this.execute.bind(this)) 12 | } 13 | 14 | private async execute( 15 | target: 'grammarly' | 'workspace' | 'folder' | 'user', 16 | documentURI: string, 17 | code: number, 18 | word: string, 19 | ) { 20 | switch (target) { 21 | case 'folder': 22 | await addToFolderDictionary(documentURI, word) 23 | break 24 | 25 | case 'workspace': 26 | await addToWorkspaceDictionary(documentURI, word) 27 | break 28 | 29 | case 'user': 30 | await addToUserDictionary(word) 31 | break 32 | 33 | case 'grammarly': 34 | if (this.client.isReady()) { 35 | vscode.window.showInformationMessage( 36 | `Grammarly service is not ready for adding the word "${word}" to the dictionary.`, 37 | ) 38 | 39 | return 40 | } 41 | await this.client.addToDictionary(documentURI, code) 42 | await this.client.dismissAlert(documentURI, code) 43 | break 44 | } 45 | } 46 | } 47 | 48 | async function addToUserDictionary(word: string) { 49 | const config = workspace.getConfiguration('grammarly') 50 | const words = config.get('userWords') || [] 51 | 52 | if (!words.includes(word)) { 53 | words.push(word) 54 | words.sort() 55 | 56 | await config.update('userWords', words, ConfigurationTarget.Global) 57 | } 58 | } 59 | 60 | async function addToFolderDictionary(uri: string, word: string) { 61 | const config = workspace.getConfiguration('grammarly', Uri.parse(uri)) 62 | const words = config.get('userWords') || [] 63 | 64 | if (!words.includes(word)) { 65 | words.push(word) 66 | words.sort() 67 | 68 | await config.update('userWords', words, ConfigurationTarget.WorkspaceFolder) 69 | } 70 | } 71 | 72 | async function addToWorkspaceDictionary(uri: string, word: string) { 73 | const config = workspace.getConfiguration('grammarly', Uri.parse(uri)) 74 | const words = config.get('userWords') || [] 75 | 76 | if (!words.includes(word)) { 77 | words.push(word) 78 | words.sort() 79 | 80 | await config.update('userWords', words, ConfigurationTarget.Workspace) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /extension/src/commands/CheckGrammarCommand.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify' 2 | import { commands, Disposable, window } from 'vscode' 3 | import { GrammarlyClient } from '../client' 4 | import { Registerable } from '../interfaces' 5 | 6 | @injectable() 7 | export class CheckCommand implements Registerable { 8 | constructor (private readonly client: GrammarlyClient) { } 9 | 10 | register() { 11 | return Disposable.from( 12 | commands.registerCommand('grammarly.check', this.execute.bind(this)), 13 | commands.registerCommand('grammarly.stop', this.execute.bind(this, true)), 14 | ) 15 | } 16 | 17 | private async execute(stop: boolean = false) { 18 | if (!this.client.isReady()) return 19 | 20 | if (!window.activeTextEditor) { 21 | window.showInformationMessage('No active text document found.') 22 | 23 | return 24 | } 25 | 26 | const document = window.activeTextEditor.document 27 | 28 | if (this.client.isIgnoredDocument(document.uri.toString(), document.languageId)) { 29 | const ext = document.fileName.substr(document.fileName.lastIndexOf('.')) 30 | window.showInformationMessage(`The ${ext} filetype is not supported.`) 31 | // TODO: Add a button to create github issue. 32 | return 33 | } 34 | 35 | if (stop) { 36 | await this.client.stopCheck(document.uri.toString()) 37 | } else { 38 | await this.client.check(document.uri.toString()) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /extension/src/commands/ClearCredentialsCommand.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify' 2 | 3 | import { commands, window } from 'vscode' 4 | import { getKeyTar } from '../keytar' 5 | import { Registerable } from '../interfaces' 6 | 7 | @injectable() 8 | export class ClearCredentialsCommand implements Registerable { 9 | register() { 10 | return commands.registerCommand('grammarly.clearCredentials', this.execute.bind(this)) 11 | } 12 | 13 | private async execute() { 14 | for (const credentials of await getKeyTar().findCredentials('vscode-grammarly')) { 15 | getKeyTar().deletePassword('vscode-grammarly', credentials.account) 16 | } 17 | for (const credentials of await getKeyTar().findCredentials('vscode-grammarly-cookie')) { 18 | getKeyTar().deletePassword('vscode-grammarly-cookie', credentials.account) 19 | } 20 | window.showInformationMessage(`Logged out of grammarly.com.`) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /extension/src/commands/IgnoreIssueCommand.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify' 2 | import { commands } from 'vscode' 3 | import { GrammarlyClient } from '../client' 4 | import { Registerable } from '../interfaces' 5 | 6 | @injectable() 7 | export class IgnoreIssueCommand implements Registerable { 8 | constructor(private readonly client: GrammarlyClient) {} 9 | 10 | register() { 11 | return commands.registerCommand('grammarly.ignoreIssue', this.execute.bind(this)) 12 | } 13 | 14 | private async execute(uri: string, alertId: number) { 15 | if (!this.client.isReady()) return 16 | 17 | await this.client.dismissAlert(uri, alertId) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /extension/src/commands/ServerCallbackCommand.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify' 2 | import { commands } from 'vscode' 3 | import { GrammarlyClient } from '../client' 4 | import { Registerable } from '../interfaces' 5 | import { Logger } from '../utils/Logger' 6 | 7 | @injectable() 8 | export class ServerCallbackCommand implements Registerable { 9 | private LOGGER = new Logger(ServerCallbackCommand.name) 10 | 11 | constructor (private readonly client: GrammarlyClient) { } 12 | 13 | register() { 14 | this.LOGGER.trace('Registering grammarly.callback command') 15 | 16 | return commands.registerCommand('grammarly.callback', this.execute.bind(this)) 17 | } 18 | 19 | private async execute(options: { method: string; params: any }) { 20 | if (!this.client.isReady()) { 21 | await this.client.onReady() 22 | } 23 | 24 | this.client.sendFeedback(options.method, options.params) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /extension/src/commands/SetGoalsCommand.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify' 2 | import minimatch from 'minimatch' 3 | import { DocumentContext } from '@emacs-grammarly/unofficial-grammarly-api' 4 | import { commands, ConfigurationTarget, window, workspace } from 'vscode' 5 | import { GrammarlyClient } from '../client' 6 | import { form, select } from '../form' 7 | import { Registerable } from '../interfaces' 8 | import { GrammarlySettings } from '../settings' 9 | import { toArray } from '../utils/toArray' 10 | 11 | @injectable() 12 | export class SetGoalsCommand implements Registerable { 13 | constructor(private readonly client: GrammarlyClient) { } 14 | 15 | register() { 16 | return commands.registerCommand('grammarly.setGoals', this.execute.bind(this)) 17 | } 18 | 19 | private async execute() { 20 | if (!this.client.isReady()) return 21 | if (!window.activeTextEditor) { 22 | window.showInformationMessage('No active text document found.') 23 | 24 | return 25 | } 26 | 27 | const document = window.activeTextEditor.document 28 | 29 | if (this.client.isIgnoredDocument(document.uri.toString(), document.languageId)) { 30 | const ext = document.fileName.substr(document.fileName.lastIndexOf('.')) 31 | window.showInformationMessage(`The ${ext} filetype is not supported.`) 32 | return 33 | } 34 | 35 | if (!workspace.getWorkspaceFolder(document.uri)) { 36 | window.showInformationMessage(`The file does not belong to current workspace.`) 37 | return 38 | } 39 | 40 | const uri = document.uri.toString() 41 | const config = workspace.getConfiguration().get('grammarly')! 42 | const override = config.overrides.find((override) => 43 | toArray(override.files).some((pattern) => minimatch(uri, pattern)), 44 | ) 45 | const settings: DocumentContext = { 46 | audience: config.audience, 47 | dialect: config.dialect, 48 | domain: config.domain, 49 | emotions: config.emotions, 50 | goals: config.goals, 51 | style: config.style, 52 | ...override?.config, 53 | } 54 | 55 | const result = await form('Set Goals', [ 56 | select('audience', 'Audience', [ 57 | { 58 | label: 'general', 59 | description: 'Easy for anyone to read with minimal effort.', 60 | picked: settings.audience === 'general', 61 | }, 62 | { 63 | label: 'knowledgeable', 64 | description: 'Requires focus to read and understand.', 65 | picked: settings.audience === 'knowledgeable', 66 | }, 67 | { 68 | label: 'expert', 69 | description: 'May require rereading to understand.', 70 | picked: settings.audience === 'expert', 71 | }, 72 | ]), 73 | select('dialect', 'Dialect', [ 74 | { 75 | label: 'american', 76 | description: '🇺🇸 American', 77 | picked: settings.dialect === 'american', 78 | }, 79 | { 80 | label: 'australian', 81 | description: '🇦🇺 Australian', 82 | picked: settings.dialect === 'australian', 83 | }, 84 | { 85 | label: 'british', 86 | description: '🇬🇧 British', 87 | picked: settings.dialect === 'british', 88 | }, 89 | { 90 | label: 'canadian', 91 | description: '🇨🇦 Canadian', 92 | picked: settings.dialect === 'canadian', 93 | }, 94 | ]), 95 | select('domain', 'Domain', [ 96 | { 97 | label: 'academic', 98 | picked: settings.domain === 'academic', 99 | description: 'Strictly applies all rules and formal writing conventions.', 100 | }, 101 | { 102 | label: 'business', 103 | picked: settings.domain === 'business', 104 | description: 'Applies almost all rules, but allow some informal expressions.', 105 | }, 106 | { 107 | label: 'general', 108 | picked: settings.domain === 'general', 109 | description: 'Applies most rules and conventions with medium strictness.', 110 | }, 111 | { 112 | label: 'technical', 113 | picked: settings.domain === 'technical', 114 | description: 'Applies almost all rules, plus technical writing conventions.', 115 | }, 116 | { 117 | label: 'casual', 118 | picked: settings.domain === 'casual', 119 | description: 'Applies most rules, but allow stylistic flexibility.', 120 | }, 121 | { 122 | label: 'creative', 123 | picked: settings.domain === 'creative', 124 | description: 'Allows some intentional bending of rules and conventions.', 125 | }, 126 | ]), 127 | select( 128 | 'emotions', 129 | 'Emotions: How do you want to sound?', 130 | [ 131 | { 132 | label: 'neutral', 133 | picked: settings.emotions.includes('neutral'), 134 | description: '😐 Neutral', 135 | }, 136 | { 137 | label: 'confident', 138 | picked: settings.emotions.includes('confident'), 139 | description: '🤝 Confident', 140 | }, 141 | { 142 | label: 'joyful', 143 | picked: settings.emotions.includes('joyful'), 144 | description: '🙂 Joyful', 145 | }, 146 | { 147 | label: 'optimistic', 148 | picked: settings.emotions.includes('optimistic'), 149 | description: '✌️ Optimistic', 150 | }, 151 | { 152 | label: 'respectful', 153 | picked: settings.emotions.includes('respectful'), 154 | description: '🙌 Respectful', 155 | }, 156 | { 157 | label: 'urgent', 158 | picked: settings.emotions.includes('urgent'), 159 | description: '⏰ Urgent', 160 | }, 161 | { 162 | label: 'friendly', 163 | picked: settings.emotions.includes('friendly'), 164 | description: '🤗 Friendly', 165 | }, 166 | { 167 | label: 'analytical', 168 | picked: settings.emotions.includes('analytical'), 169 | description: '📊 Analytical', 170 | }, 171 | ], 172 | { canSelectMany: true }, 173 | ), 174 | select( 175 | 'goals', 176 | 'Goals: What are you trying to do?', 177 | [ 178 | { 179 | label: 'inform', 180 | picked: settings.goals.includes('inform'), 181 | description: 'Inform', 182 | }, 183 | { 184 | label: 'describe', 185 | picked: settings.goals.includes('describe'), 186 | description: 'Describe', 187 | }, 188 | { 189 | label: 'convince', 190 | picked: settings.goals.includes('convince'), 191 | description: 'Convince', 192 | }, 193 | { 194 | label: 'tellStory', 195 | picked: settings.goals.includes('tellStory'), 196 | description: 'Tell A Story', 197 | }, 198 | ], 199 | { canSelectMany: true }, 200 | ), 201 | ]).run() 202 | 203 | if (result) { 204 | const config = workspace.getConfiguration('grammarly', document.uri) 205 | const overrides = config.get('overrides') || [] 206 | const file = workspace.asRelativePath(document.uri) 207 | const pattern = `**/${file}` 208 | const index = overrides.findIndex((override) => override.files.includes(pattern)) 209 | 210 | if (index >= 0) { 211 | if (overrides[index].files.length === 1) { 212 | overrides[index].config = result 213 | } else { 214 | overrides[index].files.splice(overrides[index].files.indexOf(pattern), 1) 215 | overrides.push({ 216 | files: [pattern], 217 | config: result, 218 | }) 219 | } 220 | } else { 221 | overrides.push({ 222 | files: [pattern], 223 | config: result, 224 | }) 225 | } 226 | 227 | await config.update('overrides', overrides, ConfigurationTarget.WorkspaceFolder) 228 | 229 | this.client.check(document.uri.toString()) 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /extension/src/commands/StatsCommand.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify' 2 | import { commands, window } from 'vscode' 3 | import { GrammarlyClient } from '../client' 4 | import { Registerable } from '../interfaces' 5 | import { capitalize } from '../utils/string' 6 | 7 | @injectable() 8 | export class StatsCommand implements Registerable { 9 | constructor(private readonly client: GrammarlyClient) {} 10 | 11 | register() { 12 | return commands.registerCommand('grammarly.stats', this.execute.bind(this)) 13 | } 14 | 15 | private async execute() { 16 | if (!this.client.isReady()) return 17 | 18 | if (!window.activeTextEditor) { 19 | window.showInformationMessage('No active text document found.') 20 | 21 | return 22 | } 23 | 24 | const document = window.activeTextEditor.document 25 | 26 | if (this.client.isIgnoredDocument(document.uri.toString(), document.languageId)) { 27 | const ext = document.fileName.substr(document.fileName.lastIndexOf('.')) 28 | window.showInformationMessage(`The ${ext} filetype is not supported.`) 29 | // TODO: Add a button to create github issue. 30 | return 31 | } 32 | 33 | try { 34 | const uri = document.uri.toString() 35 | const state = await this.client.getDocumentState(uri) 36 | 37 | if (state == null || !('status' in state)) return 38 | 39 | const { score, textInfo, emotions, scores } = state 40 | 41 | const scoresMessages = Array.from(Object.entries(scores)).map(([key, value]) => `${key} ${~~(value! * 100)}`) 42 | 43 | await window.showInformationMessage( 44 | ` 45 | Text Score: ${score} out of 100. 46 | This score represents the quality of writing in this document. ${ 47 | score < 100 ? `You can increase it by addressing Grammarly's suggestions.` : '' 48 | } 49 | 50 | ${ 51 | textInfo != null 52 | ? `${textInfo.wordsCount} words 53 | ${textInfo.charsCount} characters 54 | ${calculateTime(textInfo.wordsCount, 250)} reading time 55 | ${calculateTime(textInfo.wordsCount, 130)} speaking time 56 | ${textInfo.readabilityScore} readability score` 57 | : '' 58 | } 59 | 60 | 61 | ${scoresMessages.length ? scoresMessages.join('\n') : ''} 62 | 63 | ${ 64 | emotions.length 65 | ? [ 66 | `Here’s how your text sounds:\n`, 67 | emotions 68 | .map((emotion) => `${emotion.emoji} ${capitalize(emotion.name)} ${~~(emotion.confidence * 100)}%\n`) 69 | .join('\n'), 70 | ].join('\n') 71 | : '' 72 | } 73 | 74 | `.replace(/^[ \t]+/gm, ''), 75 | { modal: true }, 76 | ) 77 | } catch (error) { 78 | window.showErrorMessage(`Grammarly: ${error.message}`) 79 | // TODO: Add report url. 80 | } 81 | } 82 | } 83 | 84 | function calculateTime(words: number, wordsPerMinute: number) { 85 | const wordsPerSecond = wordsPerMinute / 60 86 | const time = secondsToTime(words / wordsPerSecond) 87 | 88 | return time 89 | } 90 | 91 | function secondsToTime(sec: number) { 92 | const hours = Math.floor(sec / 3600) 93 | const minutes = Math.floor((sec - hours * 3600) / 60) 94 | let seconds = sec - hours * 3600 - minutes * 60 95 | 96 | seconds = hours > 0 || minutes > 10 ? 0 : Math.floor(seconds) 97 | 98 | return [ 99 | hours ? `${hours} ${choose(hours, 'hr', 'hrs')}` : '', 100 | minutes ? `${minutes} ${choose(minutes, 'min', 'mins')}` : '', 101 | seconds ? `${seconds} ${choose(seconds, 'sec', 'secs')}` : '', 102 | ] 103 | .filter(Boolean) 104 | .join(' ') 105 | } 106 | 107 | function choose(count: number, singular: string, plural: string) { 108 | return count === 1 ? singular : plural 109 | } 110 | -------------------------------------------------------------------------------- /extension/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const EXTENSION = Symbol('ExtensionContext'); 2 | -------------------------------------------------------------------------------- /extension/src/extension.ts: -------------------------------------------------------------------------------- 1 | import { Container } from 'inversify' 2 | import 'reflect-metadata' 3 | import { Disposable, ExtensionContext } from 'vscode' 4 | import { GrammarlyClient } from './client' 5 | import { AddWordCommand } from './commands/AddWordCommand' 6 | import { CheckCommand } from './commands/CheckGrammarCommand' 7 | import { ClearCredentialsCommand } from './commands/ClearCredentialsCommand' 8 | import { IgnoreIssueCommand } from './commands/IgnoreIssueCommand' 9 | import { ServerCallbackCommand } from './commands/ServerCallbackCommand' 10 | import { AuthenticationService } from './services/AuthenticationService' 11 | import { SetGoalsCommand } from './commands/SetGoalsCommand' 12 | import { StatsCommand } from './commands/StatsCommand' 13 | import { EXTENSION } from './constants' 14 | import { StatusBarController } from './controllers/StatusBarController' 15 | 16 | export async function activate(context: ExtensionContext) { 17 | const container = new Container({ 18 | autoBindInjectable: true, 19 | defaultScope: 'Singleton', 20 | }) 21 | 22 | container.bind(EXTENSION).toConstantValue(context) 23 | container.bind(GrammarlyClient).toConstantValue(new GrammarlyClient(context, container.get(AuthenticationService))) 24 | 25 | context.subscriptions.push( 26 | container.get(GrammarlyClient).register(), 27 | container.get(StatusBarController).register(), 28 | container.get(AddWordCommand).register(), 29 | container.get(CheckCommand).register(), 30 | container.get(IgnoreIssueCommand).register(), 31 | container.get(StatsCommand).register(), 32 | container.get(AuthenticationService).register(), 33 | container.get(ClearCredentialsCommand).register(), 34 | container.get(ServerCallbackCommand).register(), 35 | container.get(SetGoalsCommand).register(), 36 | new Disposable(() => container.unbindAll()), 37 | ) 38 | 39 | await container.get(GrammarlyClient).onReady() 40 | } 41 | 42 | export function deactivate() { } 43 | -------------------------------------------------------------------------------- /extension/src/form.ts: -------------------------------------------------------------------------------- 1 | import { Disposable, InputBox, QuickInputButtons, window, QuickPick, QuickPickItem } from 'vscode'; 2 | export interface InputRenderOptions { 3 | value?: string; 4 | } 5 | export interface InputOptions { 6 | placeholder: string; 7 | value: string; 8 | validate(value: string, data: T): Promise; 9 | } 10 | 11 | export interface SelectOption { 12 | placeholder: string; 13 | value: string; 14 | canSelectMany: boolean; 15 | } 16 | 17 | export interface FormField { 18 | name: string; 19 | render: (props: InputRenderOptions) => F; 20 | onAccept?(field: F, data: T, next: (error?: Error) => void): Promise; 21 | } 22 | 23 | export function input(name: string, label: string, options: Partial> = {}): FormField { 24 | return { 25 | name, 26 | onAccept: options.validate 27 | ? async (field, data, next) => { 28 | const prompt = field.prompt; 29 | try { 30 | field.prompt = 'Validating ...'; 31 | await options.validate!(field.value, data); 32 | field.validationMessage = undefined; 33 | next(); 34 | } catch (error) { 35 | field.validationMessage = error.message; 36 | next(error); 37 | } finally { 38 | field.prompt = prompt; 39 | } 40 | } 41 | : undefined, 42 | render: (props: InputRenderOptions) => { 43 | const field = window.createInputBox(); 44 | field.prompt = label; 45 | field.value = props.value || options.value || ''; 46 | field.placeholder = options.placeholder; 47 | return field; 48 | }, 49 | }; 50 | } 51 | 52 | export function select( 53 | name: string, 54 | label: string, 55 | items: R[], 56 | options: Partial = {} 57 | ): FormField> { 58 | return { 59 | name, 60 | render: (props: InputRenderOptions) => { 61 | const field = window.createQuickPick(); 62 | field.items = items; 63 | field.matchOnDescription = true; 64 | field.matchOnDetail = true; 65 | field.value = props.value || options.value || ''; 66 | field.placeholder = label; 67 | field.canSelectMany = 'canSelectMany' in options ? options.canSelectMany! : false; 68 | 69 | if (!field.canSelectMany) { 70 | const item = items.find((item) => item.picked); 71 | 72 | if (item) { 73 | field.selectedItems = [item]; 74 | field.activeItems = [item]; 75 | } 76 | } else { 77 | field.selectedItems = items.filter((item) => item.picked); 78 | field.activeItems = items.filter((item) => item.picked); 79 | } 80 | 81 | return field; 82 | }, 83 | }; 84 | } 85 | 86 | export function form(label: string, fields: FormField>[]) { 87 | const run = async () => { 88 | const data: any = {}; 89 | const disposables: Disposable[] = []; 90 | try { 91 | for (let index = 1; index <= fields.length; ++index) { 92 | const field = fields[index - 1]; 93 | const input = field.render({ value: data[field.name] }); 94 | input.title = label; 95 | input.step = index; 96 | input.totalSteps = fields.length; 97 | disposables.push(input); 98 | const exec = () => 99 | new Promise((resolve, reject) => { 100 | if (index > 1) { 101 | input.buttons = [QuickInputButtons.Back]; 102 | } 103 | disposables.push( 104 | input.onDidAccept(() => { 105 | if (field.onAccept) { 106 | input.busy = true; 107 | input.enabled = false; 108 | input.ignoreFocusOut = true; 109 | field.onAccept(input, data, async (error) => { 110 | input.busy = false; 111 | input.enabled = true; 112 | input.ignoreFocusOut = false; 113 | if (error) resolve(await exec()); 114 | else resolve(); 115 | }); 116 | } else { 117 | resolve(); 118 | } 119 | }), 120 | input.onDidChangeValue(() => { 121 | if ('validationMessage' in input) { 122 | input.validationMessage = undefined; 123 | } 124 | }), 125 | input.onDidTriggerButton((button) => { 126 | if (button === QuickInputButtons.Back) { 127 | --index; 128 | resolve(); 129 | } else { 130 | reject(new Error('Unexpected extra button.')); 131 | } 132 | }) 133 | ); 134 | input.show(); 135 | }); 136 | await exec(); 137 | data[field.name] = 138 | 'selectedItems' in input 139 | ? !input.canSelectMany 140 | ? input.selectedItems[0].label 141 | : input.selectedItems.map((item) => item.label) 142 | : input.value; 143 | disposables.forEach((disposable) => disposable.dispose()); 144 | } 145 | return data as T; 146 | } catch (error) { 147 | disposables.forEach((disposable) => disposable.dispose()); 148 | return null; 149 | } 150 | }; 151 | return { run }; 152 | } 153 | -------------------------------------------------------------------------------- /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/keytar.ts: -------------------------------------------------------------------------------- 1 | export function getKeyTar(): { 2 | getPassword(service: string, account: string): Promise; 3 | setPassword(service: string, account: string, password: string): Promise; 4 | deletePassword(service: string, account: string): Promise; 5 | findPassword(service: string): Promise; 6 | findCredentials(service: string): Promise>; 7 | } { 8 | return eval('require')('keytar'); 9 | } 10 | -------------------------------------------------------------------------------- /extension/src/server.ts: -------------------------------------------------------------------------------- 1 | import { startLanguageServer } from '@emacs-grammarly/unofficial-grammarly-language-server' 2 | 3 | startLanguageServer() 4 | -------------------------------------------------------------------------------- /extension/src/services/AuthenticationService.ts: -------------------------------------------------------------------------------- 1 | import base64 from 'base64url' 2 | import crypto from 'crypto' 3 | import { injectable } from 'inversify' 4 | import fetch from 'node-fetch' 5 | import qs from 'querystring' 6 | import { commands, Disposable, env, Uri, UriHandler, window } from 'vscode' 7 | import { Registerable } from '../interfaces' 8 | import { getKeyTar } from '../keytar' 9 | import { GrammarlyAuthContext } from '@emacs-grammarly/unofficial-grammarly-api' 10 | 11 | const COOKIE_KEY = 'vscode-grammarly-cookie' 12 | const CLIENTS: Record = { 13 | vscode: 'extensionVSCode', 14 | 'vscode-insiders': 'extensionVSCodeInsiders', 15 | } 16 | 17 | function stringifyCookie(cookie: Record): string { 18 | return Object.entries(cookie) 19 | .map(([key, value]) => key + '=' + value + ';') 20 | .join(' ') 21 | } 22 | function parseSetCookieHeaders(cookies: string[]): Record { 23 | return cookies 24 | .map((x) => x.split('=')) 25 | .reduce((obj, [key, val]) => { 26 | obj[key] = val.split(';')[0] 27 | 28 | return obj 29 | }, {} as Record) 30 | } 31 | 32 | @injectable() 33 | export class AuthenticationService implements Registerable, UriHandler { 34 | private challenges = new Map any }>() 35 | 36 | register() { 37 | void this.getCookie().then(value => { 38 | commands.executeCommand('setContext', 'grammarly:isAuthenticated', value != null) 39 | }) 40 | 41 | return Disposable.from( 42 | commands.registerCommand('grammarly.login', this.execute.bind(this)), 43 | commands.registerCommand('grammarly.logout', () => { 44 | this.setCookie(null) 45 | void window.showInformationMessage('Logged out of grammarly.com.') 46 | }), 47 | window.registerUriHandler(this), 48 | ) 49 | } 50 | 51 | handleUri(uri: Uri) { 52 | if (uri.path === '/auth/callback') { 53 | const args = qs.parse(uri.query) as { code: string; code_challenge: string } 54 | const challenge = Array.from(this.challenges.keys()) 55 | const handler = this.challenges.get(args.code_challenge) ?? this.challenges.get(challenge[0]) 56 | 57 | if (handler != null) { 58 | handler.callback(null, args.code) 59 | } else { 60 | void window.showErrorMessage(JSON.stringify({ 61 | challenge, 62 | url: uri.query 63 | })) 64 | } 65 | } 66 | } 67 | 68 | async execute() { 69 | this.setCookie(await this.login()) 70 | } 71 | 72 | async getCookie(): Promise { 73 | return await getKeyTar().findPassword(COOKIE_KEY) 74 | } 75 | 76 | async setCookie(cookie: string | null) { 77 | if (cookie != null) { 78 | await getKeyTar().setPassword(COOKIE_KEY, 'default', cookie) 79 | commands.executeCommand('setContext', 'grammarly:isAuthenticated', true) 80 | } else { 81 | await getKeyTar().deletePassword(COOKIE_KEY, 'default') 82 | commands.executeCommand('setContext', 'grammarly:isAuthenticated', false) 83 | } 84 | } 85 | 86 | async login(): Promise { 87 | const clientId = CLIENTS[env.uriScheme] 88 | if (clientId == null) throw new Error(`Unsupported URI scheme "${env.uriScheme}://"`) 89 | 90 | const codeVerifier = base64.encode(crypto.randomBytes(96)) 91 | const challenge = base64.encode(crypto.createHash('sha256').update(codeVerifier).digest()) 92 | 93 | await env.openExternal( 94 | Uri.parse( 95 | `https://grammarly.com/signin/app?client_id=${clientId}&code_challenge=${challenge}`, 96 | ), 97 | ) 98 | 99 | return new Promise((resolve, reject) => { 100 | const id = setTimeout(reject, 5 * 60 * 1000, new Error('Timeout')) 101 | this.challenges.set(challenge, { 102 | secret: codeVerifier, 103 | callback: async (error, code) => { 104 | clearTimeout(id) 105 | if (error != null) return reject(error) 106 | 107 | try { 108 | const auth = await fetch(`https://auth.grammarly.com/v3/user/oranonymous?app=${clientId}`, { 109 | method: 'GET', 110 | headers: { 111 | 'x-client-type': clientId, 112 | 'x-client-version': '0.0.0', 113 | } 114 | }) 115 | const anonymousCookie = parseSetCookieHeaders(auth.headers.raw()['set-cookie']) 116 | const response = await fetch('https://auth.grammarly.com/v3/api/unified-login/code/exchange', { 117 | method: 'POST', 118 | headers: { 119 | 'Accept': 'application/json', 120 | 'Content-Type': 'application/json', 121 | 'x-client-type': clientId, 122 | 'x-client-version': '0.0.0', 123 | 'x-csrf-token': anonymousCookie['csrf-token'], 124 | 'x-container-id': anonymousCookie['gnar_containerId'], 125 | 'cookie': `grauth=${anonymousCookie['grauth']}; csrf-token=${anonymousCookie['csrf-token']}`, 126 | }, 127 | body: JSON.stringify({ 128 | client_id: clientId, 129 | code, 130 | code_verifier: codeVerifier, 131 | }), 132 | }) 133 | 134 | 135 | const cookie = parseSetCookieHeaders(response.headers.raw()['set-cookie'] ?? []) 136 | if (cookie.grauth == null) throw new Error('Cannot find "grauth" cookie') 137 | const { user } = await response.json() 138 | 139 | void window.showInformationMessage(`Logged in as ${user.name}.`) 140 | const authInfo: GrammarlyAuthContext = { 141 | isAnonymous: false, 142 | isPremium: user.type === 'Premium' || user.free === false, 143 | token: stringifyCookie(cookie), 144 | container: cookie['gnar_containerId'], 145 | username: user.email 146 | } 147 | 148 | resolve(JSON.stringify(authInfo)) 149 | } catch (error) { 150 | reject(error) 151 | } 152 | }, 153 | }) 154 | }) 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /extension/src/settings.ts: -------------------------------------------------------------------------------- 1 | import { DocumentContext } from '@emacs-grammarly/unofficial-grammarly-api' 2 | import { DiagnosticSeverity } from 'vscode' 3 | 4 | export interface GrammarlySettings extends DocumentContext { 5 | /** Extension Config */ 6 | autoActivate: boolean 7 | ignore: string[] 8 | userWords: string[] 9 | diagnostics: Record< 10 | string, 11 | { 12 | ignore: string[] 13 | } 14 | > 15 | severity: Record 16 | 17 | /** Grammarly Document Config */ 18 | overrides: Array<{ 19 | files: string[] 20 | config: Partial 21 | }> 22 | 23 | debug: boolean 24 | showUsernameInStatusBar: boolean 25 | showDeletedTextInQuickFix: boolean 26 | showExplanation: boolean 27 | showExamples: boolean 28 | hideUnavailablePremiumAlerts: boolean 29 | } 30 | 31 | export const DEFAULT: GrammarlySettings = { 32 | /** Extension Config */ 33 | autoActivate: true, 34 | ignore: [], 35 | severity: { 36 | Determiners: DiagnosticSeverity.Error, 37 | Misspelled: DiagnosticSeverity.Error, 38 | Unknown: DiagnosticSeverity.Error, 39 | ClosingPunct: DiagnosticSeverity.Error, 40 | Nouns: DiagnosticSeverity.Error, 41 | 42 | OddWords: DiagnosticSeverity.Warning, 43 | CompPunct: DiagnosticSeverity.Warning, 44 | Clarity: DiagnosticSeverity.Warning, 45 | Dialects: DiagnosticSeverity.Warning, 46 | 47 | WordChoice: DiagnosticSeverity.Information, 48 | Readability: DiagnosticSeverity.Information, 49 | 50 | PassiveVoice: DiagnosticSeverity.Hint, 51 | 52 | _default: DiagnosticSeverity.Hint, 53 | }, 54 | userWords: [], 55 | diagnostics: { 56 | '[markdown]': { 57 | ignore: ['code'], 58 | }, 59 | '[mdx]': { 60 | ignore: ['code'], 61 | }, 62 | }, 63 | 64 | /** Grammarly Config */ 65 | audience: 'knowledgeable', 66 | dialect: 'american', 67 | domain: 'general', 68 | emotions: [], 69 | goals: [], 70 | style: 'neutral', 71 | 72 | /** Grammarly Document Config */ 73 | overrides: [], 74 | 75 | /** Internal */ 76 | debug: false, 77 | showUsernameInStatusBar: true, 78 | showDeletedTextInQuickFix: true, 79 | showExplanation: true, 80 | showExamples: false, 81 | hideUnavailablePremiumAlerts: false, 82 | } 83 | -------------------------------------------------------------------------------- /extension/src/utils/Logger.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'util' 2 | 3 | export const enum LoggerLevel { 4 | TRACE, 5 | DEBUG, 6 | INFO, 7 | WARN, 8 | ERROR, 9 | NONE, 10 | } 11 | 12 | const displayLevel = { 13 | [LoggerLevel.TRACE]: 'TRACE', 14 | [LoggerLevel.DEBUG]: 'DEBUG', 15 | [LoggerLevel.INFO]: 'INFO', 16 | [LoggerLevel.WARN]: 'WARN', 17 | [LoggerLevel.ERROR]: 'ERROR', 18 | [LoggerLevel.NONE]: 'NONE', 19 | } 20 | 21 | function isString(value: any): value is string { 22 | return typeof value === 'string' 23 | } 24 | 25 | function isError(value: any): value is Error { 26 | return value instanceof Error 27 | } 28 | 29 | export class Logger { 30 | static options = { 31 | enabled: new Set(['*']), 32 | level: LoggerLevel.DEBUG, 33 | } 34 | 35 | constructor (public readonly name: string, public readonly defaultContext: string = '') { } 36 | 37 | trace(msg: string, ...args: any[]): void 38 | trace(context: string, msg: string, ...args: any[]): void 39 | trace(...args: any[]) { 40 | this.write(LoggerLevel.TRACE, args) 41 | } 42 | 43 | debug(msg: string, ...args: any[]): void 44 | debug(context: string, msg: string, ...args: any[]): void 45 | debug(...args: any[]) { 46 | this.write(LoggerLevel.DEBUG, args) 47 | } 48 | 49 | info(msg: string, ...args: any[]): void 50 | info(context: string, msg: string, ...args: any[]): void 51 | info(...args: any[]) { 52 | this.write(LoggerLevel.INFO, args) 53 | } 54 | 55 | warn(msg: string, ...args: any[]): void 56 | warn(context: string, msg: string, ...args: any[]): void 57 | warn(...args: any[]) { 58 | this.write(LoggerLevel.WARN, args) 59 | } 60 | 61 | error(msg: string, ...args: any[]): void 62 | error(msg: Error, ...args: any[]): void 63 | error(context: string, msg: string, ...args: any[]): void 64 | error(context: string, msg: Error, ...args: any[]): void 65 | error(...args: any[]) { 66 | this.write(LoggerLevel.ERROR, args) 67 | } 68 | 69 | private write(level: LoggerLevel, args: any[]) { 70 | if ( 71 | level >= Logger.options.level && 72 | (Logger.options.enabled.has('*') || Logger.options.enabled.has(this.name)) 73 | ) { 74 | const context = 75 | args.length >= 2 && isString(args[0]) && (isString(args[1]) || isError(args[1])) 76 | ? args.shift() 77 | : this.defaultContext 78 | 79 | const message = `${Date.now()} ${displayLevel[level]} [${this.name}]${context ? ' (' + context + ')' : ''} ${this.inspect( 80 | args, 81 | )}` 82 | 83 | switch (level) { 84 | case LoggerLevel.ERROR: 85 | console.error(message) 86 | break 87 | case LoggerLevel.WARN: 88 | console.warn(message) 89 | break 90 | default: 91 | console.log(message) 92 | break 93 | } 94 | } 95 | } 96 | 97 | private inspect(args: any[]) { 98 | return args.map((arg) => (typeof arg === 'object' && arg ? inspect(arg, true, null) : arg)).join(' ') 99 | } 100 | } 101 | 102 | export class DevLogger extends Logger { } 103 | -------------------------------------------------------------------------------- /extension/src/utils/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 | -------------------------------------------------------------------------------- /extension/src/utils/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 | -------------------------------------------------------------------------------- /extension/src/utils/toArray.ts: -------------------------------------------------------------------------------- 1 | export function toArray(item?: T | T[]): T[] { 2 | if (!item) return []; 3 | else if (Array.isArray(item)) return item; 4 | else return [item]; 5 | } 6 | -------------------------------------------------------------------------------- /extension/src/utils/watch.ts: -------------------------------------------------------------------------------- 1 | import { Ref, effect, stop, isRef } from '@vue/reactivity'; 2 | 3 | const INITIAL_WATCHER_VALUE = {}; 4 | 5 | export function watch(ref: Ref, cb: (newValue: T, oldValue: T | undefined) => void) { 6 | const getter = () => traverse(ref.value) as T; 7 | 8 | let oldValue: T = INITIAL_WATCHER_VALUE as any; 9 | const job = () => { 10 | const newValue = runner(); 11 | try { 12 | cb(newValue, oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue); 13 | } catch (error) { 14 | console.error(error); 15 | } 16 | 17 | oldValue = newValue; 18 | }; 19 | const runner = effect(getter, { 20 | lazy: true, 21 | scheduler: job, 22 | }); 23 | 24 | job(); 25 | 26 | return () => { 27 | stop(runner); 28 | }; 29 | } 30 | 31 | export function watchEffect(cb: () => void) { 32 | const runner = effect(cb, { lazy: true }); 33 | 34 | runner(); 35 | 36 | return () => { 37 | stop(runner); 38 | }; 39 | } 40 | 41 | function traverse(value: unknown, seen: Set = new Set()) { 42 | if (!isObject(value) || seen.has(value)) { 43 | return value; 44 | } 45 | seen.add(value); 46 | if (isRef(value)) { 47 | traverse(value.value, seen); 48 | } else if (Array.isArray(value)) { 49 | for (let i = 0; i < value.length; i++) { 50 | traverse(value[i], seen); 51 | } 52 | } else if (value instanceof Map) { 53 | value.forEach((_, key) => { 54 | // to register mutation dep for existing keys 55 | traverse(value.get(key), seen); 56 | }); 57 | } else if (value instanceof Set) { 58 | value.forEach((v) => { 59 | traverse(v, seen); 60 | }); 61 | } else { 62 | for (const key in value) { 63 | traverse((value as any)[key], seen); 64 | } 65 | } 66 | return value; 67 | } 68 | 69 | function isObject(value: unknown): value is object { 70 | return typeof value === 'object' && value !== null; 71 | } 72 | -------------------------------------------------------------------------------- /extension/test/autoActivate.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { before, suite, teardown, test } from 'mocha'; 3 | import vscode from 'vscode'; 4 | import { 5 | findMisspelledWord, 6 | getDiagnostics, 7 | getFile, 8 | openFile, 9 | resetVSCodeFolder, 10 | sleep, 11 | } from './utils'; 12 | 13 | suite('AutoEnable', function () { 14 | before(async () => { 15 | await vscode.extensions.getExtension('znck.grammarly').activate(); 16 | }); 17 | 18 | teardown(async () => { 19 | resetVSCodeFolder(); 20 | await vscode.commands.executeCommand('workbench.action.closeAllEditors'); 21 | }); 22 | 23 | test('on', async () => { 24 | const document = (await openFile(getFile('folder/autoActivate.on.md'))) 25 | .document; 26 | const diagnostics = await getDiagnostics( 27 | document.uri, 28 | (diagnostics) => !!findMisspelledWord(diagnostics, 'inversifyjs') 29 | ); 30 | 31 | expect(diagnostics).to.have.length.greaterThan(0); 32 | }); 33 | 34 | test('off', async () => { 35 | await vscode.workspace 36 | .getConfiguration('grammarly') 37 | .update('autoActivate', false); 38 | 39 | await sleep(); 40 | expect(vscode.window.activeTextEditor).to.be.undefined; 41 | const document = (await openFile(getFile('folder/autoActivate.off.md'))) 42 | .document; 43 | 44 | const fn = async () => { 45 | try { 46 | return await getDiagnostics( 47 | document.uri, 48 | (diagnostics) => !!findMisspelledWord(diagnostics, 'inversifyjs') 49 | ); 50 | } catch (error) { 51 | expect(error.message).to.equal( 52 | 'Did not recevies any diagnostics after 10000ms.' 53 | ); 54 | return []; 55 | } 56 | }; 57 | 58 | const diagnostics = await fn(); 59 | expect(diagnostics).to.have.length(0); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /extension/test/command.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { before, suite, teardown, test } from 'mocha'; 3 | import vscode from 'vscode'; 4 | import { 5 | getFile, 6 | getUserWords, 7 | getUserWordsForDocument, 8 | resetVSCodeFolder, 9 | } from './utils'; 10 | 11 | suite('Commands', function () { 12 | before(async () => { 13 | await vscode.extensions.getExtension('znck.grammarly').activate(); 14 | }); 15 | 16 | teardown(async () => { 17 | resetVSCodeFolder(); 18 | }); 19 | 20 | test('grammarly.addWord: user dictionary', async () => { 21 | const word = 'word' + Date.now(); 22 | expect(getUserWords()).to.not.include(word); 23 | await vscode.commands.executeCommand( 24 | 'grammarly.addWord', 25 | 'user', 26 | '', 27 | 0, 28 | word 29 | ); 30 | expect(getUserWords()).to.include(word); 31 | }); 32 | 33 | test('grammarly.addWord: folder dictionary', async () => { 34 | const word = 'word' + Date.now(); 35 | const uri = vscode.Uri.file(getFile('folder/add-word.md')); 36 | expect(getUserWordsForDocument(uri)).to.not.include(word); 37 | await vscode.commands.executeCommand( 38 | 'grammarly.addWord', 39 | 'folder', 40 | uri.toString(), 41 | 0, 42 | word 43 | ); 44 | expect(getUserWordsForDocument(uri)).to.include(word); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /extension/test/grammarly.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { before, suite, teardown, test } from 'mocha'; 3 | import vscode from 'vscode'; 4 | import { 5 | executeCodeActionProvider, 6 | findMisspelledWord, 7 | getDiagnostics, 8 | getFile, 9 | openFile, 10 | } from './utils'; 11 | 12 | suite('Language Server', function () { 13 | before(async () => { 14 | await vscode.extensions.getExtension('znck.grammarly').activate(); 15 | }); 16 | 17 | teardown(async () => { 18 | await vscode.commands.executeCommand('workbench.action.files.revert'); 19 | await vscode.commands.executeCommand('workbench.action.closeAllEditors'); 20 | }); 21 | 22 | test('CodeAction: add to dictionary', async function () { 23 | const document = (await openFile(getFile('folder/add-word.md'))).document; 24 | expect(vscode.window.activeTextEditor?.document).to.equal(document); 25 | const diagnostics = await getDiagnostics( 26 | document.uri, 27 | (diagnostics) => !!findMisspelledWord(diagnostics, 'inversifyjs') 28 | ); 29 | const diagnostic = findMisspelledWord(diagnostics, 'inversifyjs')!; 30 | const actions = await executeCodeActionProvider( 31 | document.uri, 32 | diagnostic.range 33 | ); 34 | 35 | { 36 | const action = actions!.find((action) => 37 | action.title.includes('Grammarly: add "inversifyjs" to user dictionary') 38 | ); 39 | expect(action.command.command).to.be.equal('grammarly.addWord'); 40 | expect(action.command.arguments[0]).to.be.equal('user'); 41 | expect(action.command.arguments[3]).to.be.equal('inversifyjs'); 42 | } 43 | { 44 | const action = actions!.find((action) => 45 | action.title.includes( 46 | 'Grammarly: add "inversifyjs" to folder dictionary' 47 | ) 48 | ); 49 | expect(action.command.command).to.be.equal('grammarly.addWord'); 50 | expect(action.command.arguments[0]).to.be.equal('folder'); 51 | expect(action.command.arguments[3]).to.be.equal('inversifyjs'); 52 | } 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /extension/test/runner.ts: -------------------------------------------------------------------------------- 1 | import glob from 'fast-glob'; 2 | import Mocha from 'mocha'; 3 | 4 | export async function run(): Promise { 5 | const directory = __dirname; 6 | const mocha = new Mocha({ 7 | ui: 'tdd', 8 | timeout: 100000, 9 | useColors: true, 10 | }); 11 | const files = await glob('**/*.spec.js', { cwd: directory, absolute: true }); 12 | files.forEach((file) => mocha.addFile(file)); 13 | 14 | return new Promise((resolve, reject) => { 15 | mocha.run((failures) => { 16 | if (failures > 0) { 17 | reject(new Error(`${failures} test(s) failed.`)); 18 | } else { 19 | resolve(); 20 | } 21 | }); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /extension/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "outDir": "../out-test", 5 | "rootDir": ".", 6 | "declaration": false, 7 | "esModuleInterop": true, 8 | "sourceMap": true, 9 | "skipLibCheck": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /extension/test/utils.ts: -------------------------------------------------------------------------------- 1 | import Fs from 'fs'; 2 | import Path from 'path'; 3 | import vscode from 'vscode'; 4 | export async function executeCodeActionProvider( 5 | uri: vscode.Uri, 6 | range: vscode.Range 7 | ) { 8 | return vscode.commands.executeCommand( 9 | 'vscode.executeCodeActionProvider', 10 | uri, 11 | range 12 | ); 13 | } 14 | export async function openFile(fileName: string) { 15 | const document = await vscode.workspace.openTextDocument( 16 | vscode.Uri.file(fileName) 17 | ); 18 | return vscode.window.showTextDocument(document); 19 | } 20 | export function range(start: [number, number], end: [number, number] = start) { 21 | return new vscode.Range(position(...start), position(...end)); 22 | } 23 | export function position(line: number, character: number) { 24 | return new vscode.Position(line, character); 25 | } 26 | export function findMisspelledWord( 27 | diagnostics: vscode.Diagnostic[], 28 | word: string 29 | ) { 30 | return diagnostics.find((diagnostic) => 31 | diagnostic.message.includes('Misspelled word: ' + word) 32 | ); 33 | } 34 | export function getUserWords() { 35 | return vscode.workspace 36 | .getConfiguration('grammarly') 37 | .get('userWords'); 38 | } 39 | export function getUserWordsForDocument(uri: vscode.Uri) { 40 | return vscode.workspace 41 | .getConfiguration('grammarly', uri) 42 | .get('userWords'); 43 | } 44 | export function getFile(fileName: string) { 45 | return Path.resolve(__dirname, '../fixtures/', fileName); 46 | } 47 | export async function sleep(timeInMs = 200) { 48 | return new Promise((resolve) => setTimeout(resolve, timeInMs)); 49 | } 50 | export async function getDiagnostics( 51 | uri: vscode.Uri, 52 | check = (diagnostics: vscode.Diagnostic[]) => diagnostics.length > 0, 53 | timeout = 10000 54 | ) { 55 | const startedAt = Date.now(); 56 | let time = 200; 57 | do { 58 | await sleep(time); 59 | try { 60 | const result = vscode.languages.getDiagnostics(uri); 61 | if (check(result)) { 62 | return result; 63 | } 64 | } catch (error) { 65 | console.error(error); 66 | } 67 | 68 | time = Math.min(timeout, time * 2); 69 | } while (Date.now() - startedAt < timeout); 70 | throw new Error(`Did not recevies any diagnostics after ${timeout}ms.`); 71 | } 72 | 73 | export function resetVSCodeFolder() { 74 | Fs.writeFileSync(getFile('folder/.vscode/settings.json'), '{}'); 75 | } 76 | -------------------------------------------------------------------------------- /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/"] 19 | } 20 | -------------------------------------------------------------------------------- /fixtures/.gitignore: -------------------------------------------------------------------------------- 1 | /user/* -------------------------------------------------------------------------------- /fixtures/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "grammarly.overrides": [ 3 | { 4 | "files": [ 5 | "**/readme.md" 6 | ], 7 | "config": { 8 | "audience": "knowledgeable", 9 | "dialect": "canadian", 10 | "domain": "technical", 11 | "emotions": [ 12 | "confident", 13 | "optimistic", 14 | "friendly", 15 | "analytical" 16 | ], 17 | "goals": [ 18 | "convince" 19 | ] 20 | } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /fixtures/folder/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /fixtures/folder/add-word.md: -------------------------------------------------------------------------------- 1 | # Fixture: Add Word 2 | 3 | An unknown word like inversifyjs can be added to the dictionary. 4 | -------------------------------------------------------------------------------- /fixtures/folder/autoActivate.off.md: -------------------------------------------------------------------------------- 1 | # Fixture: Add Word 2 | 3 | An unknown word like inversifyjs can be added to the dictionary. 4 | -------------------------------------------------------------------------------- /fixtures/folder/autoActivate.on.md: -------------------------------------------------------------------------------- 1 | # Fixture: Add Word 2 | 3 | An unknown word like inversifyjs can be added to the dictionary. 4 | -------------------------------------------------------------------------------- /fixtures/readme.md: -------------------------------------------------------------------------------- 1 | ## The basics 2 | 3 | Mispellings 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 | Underlines that are blue 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 | -------------------------------------------------------------------------------- /fixtures/workspace/workspace.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "name": "Folder 1", 5 | "path": "folder1" 6 | }, 7 | { 8 | "name": "Folder 2", 9 | "path": "folder2" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "run-s -l build:*", 5 | "build:packages": "rollup -c", 6 | "build:extension": "ncc build -o extension/dist/extension extension/src/extension.ts", 7 | "build:server": "ncc build -o extension/dist/server extension/src/server.ts", 8 | "watch": "run-p -l watch:*", 9 | "watch:packages": "rollup -c --watch", 10 | "watch:extension": "ncc build -s --watch -o extension/dist/extension extension/src/extension.ts", 11 | "watch:server": "ncc build --watch -o extension/dist/server extension/src/server.ts", 12 | "test": "jest" 13 | }, 14 | "devDependencies": { 15 | "@changesets/cli": "^2.13.0", 16 | "@rollup/plugin-json": "^4.1.0", 17 | "@rollup/plugin-node-resolve": "^10.0.0", 18 | "@rollup/plugin-replace": "^2.3.4", 19 | "@rollup/plugin-typescript": "^6.1.0", 20 | "@types/minimatch": "^3.0.3", 21 | "@types/node-fetch": "^2.5.8", 22 | "@types/ws": "^7.4.0", 23 | "@vercel/ncc": "^0.25.0", 24 | "husky": "^4.3.0", 25 | "inversify": "^5.0.5", 26 | "jest": "^26.6.3", 27 | "lint-staged": "^10.1.2", 28 | "node-fetch": "^2.6.1", 29 | "npm-run-all": "^4.1.5", 30 | "prettier": "^2.0.4", 31 | "quill-delta": "^4.2.2", 32 | "rollup": "^2.3.4", 33 | "rollup-plugin-dts": "^1.4.13", 34 | "rollup-plugin-filesize": "^9.0.2", 35 | "ts-jest": "^26.4.3", 36 | "tslib": "^2.0.3", 37 | "typescript": "^4.0.5", 38 | "@emacs-grammarly/unofficial-grammarly-api": "^0.2.2", 39 | "vscode-languageclient": "^7.0.0", 40 | "vscode-languageserver": "^6.1.1", 41 | "vscode-languageserver-textdocument": "^1.0.1" 42 | }, 43 | "gitHooks": { 44 | "pre-commit": "lint-staged" 45 | }, 46 | "lint-staged": { 47 | "*.{js,ts,json,yml}": "prettier --write" 48 | } 49 | } -------------------------------------------------------------------------------- /packages/grammarly-api/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # unofficial-grammarly-api-2 2 | 3 | ## 0.2.0 4 | 5 | ### Minor Changes 6 | 7 | - 1ed857d: Add text transformation helper functions 8 | 9 | ## 0.1.0 10 | 11 | ### Minor Changes 12 | 13 | - OAuth Support 14 | -------------------------------------------------------------------------------- /packages/grammarly-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@emacs-grammarly/unofficial-grammarly-api", 3 | "version": "0.2.2", 4 | "description": "Grammarly API client", 5 | "author": "jcs090218@gmail.com", 6 | "main": "dist/index.cjs.js", 7 | "module": "dist/index.esm.js", 8 | "types": "dist/index.d.ts", 9 | "files": [ 10 | "dist", 11 | "bin" 12 | ], 13 | "dependencies": { 14 | "node-fetch": "^2.6.1", 15 | "quill-delta": "^4.2.2", 16 | "ws": "^7.3.1" 17 | }, 18 | "devDependencies": { 19 | "@types/node-fetch": "^2.5.7", 20 | "@types/ws": "^7.2.9" 21 | }, 22 | "publishConfig": { 23 | "access": "public" 24 | } 25 | } -------------------------------------------------------------------------------- /packages/grammarly-api/src/DevLogger.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'util' 2 | 3 | export const enum LoggerLevel { 4 | TRACE, 5 | DEBUG, 6 | INFO, 7 | WARN, 8 | ERROR, 9 | NONE, 10 | } 11 | 12 | const displayLevel = { 13 | [LoggerLevel.TRACE]: 'TRACE', 14 | [LoggerLevel.DEBUG]: 'DEBUG', 15 | [LoggerLevel.INFO]: 'INFO', 16 | [LoggerLevel.WARN]: 'WARN', 17 | [LoggerLevel.ERROR]: 'ERROR', 18 | [LoggerLevel.NONE]: 'NONE', 19 | } 20 | 21 | function isString(value: any): value is string { 22 | return typeof value === 'string' 23 | } 24 | 25 | function isError(value: any): value is Error { 26 | return value instanceof Error 27 | } 28 | 29 | export class Logger { 30 | static options = { 31 | enabled: new Set(['*']), 32 | level: LoggerLevel.DEBUG, 33 | } 34 | 35 | constructor (public readonly name: string, public readonly defaultContext: string = '') { } 36 | 37 | trace(msg: string, ...args: any[]): void 38 | trace(context: string, msg: string, ...args: any[]): void 39 | trace(...args: any[]) { 40 | this.write(LoggerLevel.TRACE, args) 41 | } 42 | 43 | debug(msg: string, ...args: any[]): void 44 | debug(context: string, msg: string, ...args: any[]): void 45 | debug(...args: any[]) { 46 | this.write(LoggerLevel.DEBUG, args) 47 | } 48 | 49 | info(msg: string, ...args: any[]): void 50 | info(context: string, msg: string, ...args: any[]): void 51 | info(...args: any[]) { 52 | this.write(LoggerLevel.INFO, args) 53 | } 54 | 55 | warn(msg: string, ...args: any[]): void 56 | warn(context: string, msg: string, ...args: any[]): void 57 | warn(...args: any[]) { 58 | this.write(LoggerLevel.WARN, args) 59 | } 60 | 61 | error(msg: string, ...args: any[]): void 62 | error(msg: Error, ...args: any[]): void 63 | error(context: string, msg: string, ...args: any[]): void 64 | error(context: string, msg: Error, ...args: any[]): void 65 | error(...args: any[]) { 66 | this.write(LoggerLevel.ERROR, args) 67 | } 68 | 69 | private write(level: LoggerLevel, args: any[]) { 70 | if ( 71 | level >= Logger.options.level && 72 | (Logger.options.enabled.has('*') || Logger.options.enabled.has(this.name)) 73 | ) { 74 | const context = 75 | args.length >= 2 && isString(args[0]) && (isString(args[1]) || isError(args[1])) 76 | ? args.shift() 77 | : this.defaultContext 78 | 79 | const message = `${Date.now()} ${displayLevel[level]} [${this.name}]${context ? ' (' + context + ')' : ''} ${this.inspect( 80 | args, 81 | )}` 82 | 83 | switch (level) { 84 | case LoggerLevel.ERROR: 85 | console.error(message) 86 | break 87 | case LoggerLevel.WARN: 88 | console.warn(message) 89 | break 90 | default: 91 | console.log(message) 92 | break 93 | } 94 | } 95 | } 96 | 97 | private inspect(args: any[]) { 98 | return args.map((arg) => (typeof arg === 'object' && arg ? inspect(arg, true, null) : arg)).join(' ') 99 | } 100 | } 101 | 102 | export class DevLogger extends Logger { } 103 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/SocketError.ts: -------------------------------------------------------------------------------- 1 | export class SocketError extends Error { 2 | constructor(public readonly code: number, message: string) { 3 | super(message) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/auth.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch' 2 | import { DevLogger } from './DevLogger' 3 | 4 | const LOGGER = __DEV__ ? new DevLogger('GrammarlyAuth') : null 5 | 6 | function toCookie(params: Record) { 7 | return Object.entries(params) 8 | .map(([key, value]) => key + '=' + value + ';') 9 | .join(' ') 10 | } 11 | 12 | const UA = 13 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36' 14 | 15 | const BROWSER_HEADERS = { 16 | 'User-Agent': UA, 17 | Accept: 18 | 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3', 19 | 'Accept-Language': 'en-GB,en-US;q=0.9,en;q=0.8', 20 | 'Cache-Control': 'no-cache', 21 | Pragma: 'no-cache', 22 | } 23 | 24 | function cookieToObject(cookies: string[]) { 25 | return cookies 26 | .map((x) => x.split('=')) 27 | .reduce((obj, [key, val]) => { 28 | obj[key as keyof AuthCookie] = val.split(';')[0] 29 | 30 | return obj 31 | }, {} as AuthCookie) 32 | } 33 | 34 | export interface AuthCookie { 35 | gnar_containerId: string 36 | grauth: string 37 | 'csrf-token': string 38 | funnelType: string 39 | browser_info: string 40 | redirect_location: string 41 | } 42 | 43 | export interface RawAuthCookie { 44 | raw: string 45 | headers: string[] 46 | parsed: AuthCookie 47 | } 48 | 49 | async function getInitialCookie(): Promise { 50 | const response = await fetch('https://www.grammarly.com/signin', { 51 | headers: { 52 | ...BROWSER_HEADERS, 53 | 'Sec-Fetch-Mode': 'navigate', 54 | 'Sec-Fetch-Site': 'same-origin', 55 | 'Sec-Fetch-User': '?1', 56 | 'Upgrade-Insecure-Requests': '1', 57 | Referer: 'https://www.grammarly.com/', 58 | }, 59 | method: 'GET', 60 | }) 61 | 62 | if (response.status < 300) { 63 | const cookies = response.headers.raw()['set-cookie'] 64 | const result = { 65 | raw: response.headers.get('Set-Cookie')!, 66 | headers: cookies, 67 | parsed: cookieToObject(cookies), 68 | } 69 | 70 | if (__DEV__) LOGGER?.trace('Received container ID', result.parsed.gnar_containerId) 71 | 72 | return result 73 | } 74 | 75 | try { 76 | if (__DEV__) 77 | LOGGER?.trace(`Cannot find container ID: ${response.status} - ${response.statusText}`, await response.text()) 78 | } catch { 79 | if (__DEV__) LOGGER?.trace(`Cannot find container ID: ${response.status} - ${response.statusText}`) 80 | } 81 | 82 | return null 83 | } 84 | 85 | function generateRedirectLocation(): string { 86 | return Buffer.from( 87 | JSON.stringify({ 88 | type: '', 89 | location: `https://www.grammarly.com/`, 90 | }), 91 | ).toString('base64') 92 | } 93 | 94 | export interface GrammarlyAuthContext { 95 | isAnonymous: boolean 96 | isPremium?: boolean 97 | token: string 98 | container: string 99 | username: string 100 | } 101 | 102 | export async function anonymous(client?: string, version?: string): Promise { 103 | if (__DEV__) LOGGER?.trace('Connecting anonymously') 104 | const cookie = await getInitialCookie() 105 | if (!cookie) { 106 | if (__DEV__) LOGGER?.error('Failed to get container ID') 107 | throw new Error('Authentication cannot be started.') 108 | } 109 | 110 | const response = await fetch( 111 | 'https://auth.grammarly.com/v3/user/oranonymous?app=chromeExt&containerId=' + cookie.parsed.gnar_containerId, 112 | { 113 | method: 'GET', 114 | headers: { 115 | ...BROWSER_HEADERS, 116 | Accept: 'application/json', 117 | 'x-client-type': client ?? 'extension-chrome', 118 | 'x-client-version': version ?? '1.2.390-SNAPSHOT', 119 | 'x-container-id': cookie.parsed.gnar_containerId, 120 | 'x-csrf-token': cookie.parsed['csrf-token'], 121 | 'Sec-Fetch-Mode': 'cors', 122 | 'Sec-Fetch-Site': 'same-site', 123 | Referer: 'https://www.grammarly.com/signin', 124 | Origin: 'https://www.grammarly.com', 125 | cookie: toCookie({ 126 | gnar_containerId: cookie.parsed.gnar_containerId, 127 | redirect_location: generateRedirectLocation(), 128 | firefox_freemium: 'true', 129 | funnelType: 'free', 130 | browser_info: cookie.parsed.browser_info, 131 | }), 132 | }, 133 | }, 134 | ) 135 | 136 | if (response.ok) { 137 | const cookies = response.headers.raw()['set-cookie'] 138 | 139 | try { 140 | const data = await response.json() 141 | if (__DEV__) LOGGER?.info('Authentication successful: ' + data.id) 142 | 143 | return { 144 | isAnonymous: true, 145 | token: toCookie({ 146 | ...cookie.parsed, 147 | ...cookieToObject(cookies), 148 | }), 149 | container: cookie.parsed.gnar_containerId, 150 | username: 'anonymous', 151 | } 152 | } catch { } 153 | } 154 | 155 | try { 156 | if (__DEV__) 157 | LOGGER?.error(`anonymous connection failed: ${response.status} - ${response.statusText}`, await response.text()) 158 | } catch { 159 | if (__DEV__) LOGGER?.error(`anonymous connection failed: ${response.status} - ${response.statusText}`) 160 | } 161 | 162 | throw new Error(response.statusText) 163 | } 164 | 165 | export async function authenticate(username: string, password: string): Promise { 166 | if (__DEV__) LOGGER?.trace('Connecting as ' + username) 167 | 168 | const cookie = await getInitialCookie() 169 | 170 | if (!cookie) { 171 | if (__DEV__) LOGGER?.error('Failed to get container ID') 172 | throw new Error('Authentication cannot be started.') 173 | } 174 | 175 | const headers = { 176 | accept: 'application/json', 177 | 'accept-language': BROWSER_HEADERS['Accept-Language'], 178 | 'content-type': 'application/json', 179 | 'user-agent': BROWSER_HEADERS['User-Agent'], 180 | 'x-client-type': 'funnel', 181 | 'x-client-version': '1.2.2026', 182 | 'x-container-id': cookie.parsed.gnar_containerId, 183 | 'x-csrf-token': cookie.parsed['csrf-token'], 184 | 'sec-fetch-site': 'same-site', 185 | 'sec-fetch-mode': 'cors', 186 | cookie: `gnar_containrId=${cookie.parsed.gnar_containerId}; grauth=${cookie.parsed.grauth}; csrf-token=${cookie.parsed['csrf-token']}`, 187 | } 188 | 189 | const response = await fetch('https://auth.grammarly.com/v3/api/login', { 190 | follow: 0, 191 | compress: true, 192 | method: 'POST', 193 | body: JSON.stringify({ 194 | email_login: { email: username, password, secureLogin: false }, 195 | }), 196 | headers, 197 | }) 198 | 199 | if (response.ok) { 200 | const cookies = response.headers.raw()['set-cookie'] 201 | 202 | try { 203 | const data = await response.json() 204 | if (__DEV__) LOGGER?.info('Authentication successful:', data) 205 | } catch { } 206 | 207 | return { 208 | isAnonymous: false, 209 | token: toCookie({ 210 | ...cookie.parsed, 211 | ...cookieToObject(cookies), 212 | }), 213 | container: cookie.parsed.gnar_containerId, 214 | username, 215 | } 216 | } 217 | 218 | try { 219 | const contents = await response.text() 220 | if (__DEV__) LOGGER?.error(`anonymous connection failed: ${response.status} - ${response.statusText}`, contents) 221 | 222 | const result = JSON.parse(contents) 223 | 224 | if (result.error === 'SHOW_CAPTCHA') { 225 | const error = new Error('Authentication requires captcha input.') 226 | 227 | // @ts-ignore 228 | error.code = result.error 229 | 230 | throw error 231 | } 232 | } catch { 233 | if (__DEV__) LOGGER?.error(`anonymous connection failed: ${response.status} - ${response.statusText}`) 234 | } 235 | 236 | const error = new Error(response.statusText) 237 | 238 | // @ts-ignore 239 | error.code = result.error 240 | 241 | throw error 242 | } 243 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/checks/check.ts: -------------------------------------------------------------------------------- 1 | import { version } from '../../package.json' 2 | import { anonymous, authenticate } from '../auth' 3 | import { GrammarlyClient } from '../GrammarlyClient' 4 | import { DocumentContext } from '../transport/interfaces/DocumentContext' 5 | import { getIdRevision } from '../transport/interfaces/IdRevision' 6 | 7 | export interface CheckOptions { 8 | clientName?: string 9 | clientVersion?: string 10 | credentials: { username: string; password: string } 11 | context?: Partial 12 | } 13 | export async function createCheckClient(text: string, options: Partial): Promise { 14 | return new Promise(async (resolve) => { 15 | const client = new GrammarlyClient({ 16 | documentId: Buffer.from(text).toString('hex').substr(0, 64), 17 | clientName: options?.clientName ?? 'generic-check', 18 | clientType: 'general', 19 | clientVersion: options?.clientVersion ?? version, 20 | getToken: async () => { 21 | const result = await (options?.credentials != null 22 | ? authenticate(options.credentials.username, options.credentials.password) 23 | : anonymous()) 24 | 25 | return result.token 26 | }, 27 | onConnection: async () => { 28 | await client.start({ dialect: options?.context?.dialect ?? 'american' }) 29 | const { rev } = await client.submitOT({ 30 | rev: getIdRevision(0), 31 | doc_len: 0, 32 | deltas: [{ ops: [{ insert: text }] }], 33 | chunked: false, 34 | }) 35 | 36 | if (options?.context != null) { 37 | await client.setContext({ 38 | rev, 39 | documentContext: { 40 | audience: 'knowledgeable', 41 | dialect: 'american', 42 | domain: 'general', 43 | emotions: [], 44 | goals: [], 45 | style: 'neutral', 46 | ...options.context, 47 | }, 48 | }) 49 | } 50 | 51 | resolve(client) 52 | }, 53 | }) 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/checks/checkGrammar.ts: -------------------------------------------------------------------------------- 1 | import { AlertEvent } from '../transport/events/AlertEvent' 2 | import { CheckOptions, createCheckClient } from './check' 3 | 4 | export async function checkGrammar(text: string, options?: CheckOptions): Promise { 5 | const grammarly = await createCheckClient(text, options ?? {}) 6 | 7 | const alerts: AlertEvent[] = [] 8 | 9 | grammarly.onAlert((alert) => alerts.push(alert)) 10 | 11 | return new Promise((resolve) => { 12 | grammarly.onFinished(() => { 13 | grammarly.dispose() 14 | resolve(alerts) 15 | }) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/checks/checkPlagiarism.ts: -------------------------------------------------------------------------------- 1 | import { PlagiarismEvent } from '../transport/events/PlagiarismEvent' 2 | import { CheckOptions, createCheckClient } from './check' 3 | 4 | export async function checkPlagiarism(text: string, options: CheckOptions): Promise { 5 | const alerts: PlagiarismEvent[] = [] 6 | const grammarly = await createCheckClient(text, options) 7 | 8 | await grammarly.toggleChecks({ 9 | checks: { 10 | plagiarism: true, 11 | }, 12 | }) 13 | 14 | return new Promise((resolve) => { 15 | grammarly.onPlagiarism((alert) => alerts.push(alert)) 16 | grammarly.onFinished(() => { 17 | grammarly.dispose() 18 | resolve(alerts) 19 | }) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare var __DEV__: boolean 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth' 2 | 3 | export * from './checks/check' 4 | export * from './checks/checkGrammar' 5 | export * from './checks/checkPlagiarism' 6 | 7 | export * from './transport/enums/AlertFeedbackType' 8 | export * from './transport/enums/AlertImpactType' 9 | export * from './transport/enums/AlertViewType' 10 | export * from './transport/enums/AsyncChecksTypes' 11 | export * from './transport/enums/AutoCorrectFeedbackType' 12 | export * from './transport/enums/AutocompleteFeedbackType' 13 | export * from './transport/enums/DialectType' 14 | export * from './transport/enums/DocumentAudienceType' 15 | export * from './transport/enums/DocumentDomainType' 16 | export * from './transport/enums/DocumentGoalType' 17 | export * from './transport/enums/EmotionFeedbackType' 18 | export * from './transport/enums/ErrorCodeType' 19 | export * from './transport/enums/ErrorSeverityType' 20 | export * from './transport/enums/FeatureType' 21 | export * from './transport/enums/LensFeedbackType' 22 | export * from './transport/enums/MutedFeedbackType' 23 | export * from './transport/enums/OptionType' 24 | export * from './transport/enums/PredictionType' 25 | export * from './transport/enums/SuggestionRejectionReasonType' 26 | export * from './transport/enums/SynonymFeedbackType' 27 | export * from './transport/enums/SystemFeedbackType' 28 | export * from './transport/enums/TakeawayFeedbackType' 29 | export * from './transport/enums/UserMutedScopeType' 30 | export * from './transport/enums/WritingEmotionType' 31 | export * from './transport/enums/WritingStyleType' 32 | export * from './transport/enums/WritingToneType' 33 | 34 | export * from './transport/events/AlertEvent' 35 | export * from './transport/events/AlertsChangedEvent' 36 | export * from './transport/events/AsyncCheckFinishedEvent' 37 | export * from './transport/events/CompleteEvent' 38 | export * from './transport/events/EmotionsEvent' 39 | export * from './transport/events/ErrorEvent' 40 | export * from './transport/events/FinishedEvent' 41 | export * from './transport/events/HeatmapEvent' 42 | export * from './transport/events/PlagiarismEvent' 43 | export * from './transport/events/RemoveEvent' 44 | export * from './transport/events/TakeawaysEvent' 45 | export * from './transport/events/TextInfoEvent' 46 | export * from './transport/events/TextMapsEvent' 47 | 48 | export * from './transport/interfaces/AlertCardLayout' 49 | export * from './transport/interfaces/AlertExtraProperties' 50 | export * from './transport/interfaces/DocumentContext' 51 | export * from './transport/interfaces/DocumentStatistics' 52 | export * from './transport/interfaces/Emotion' 53 | export * from './transport/interfaces/FeedbackType' 54 | export * from './transport/interfaces/FluencyExtraProperties' 55 | export * from './transport/interfaces/HeatmapRange' 56 | export * from './transport/interfaces/IdAlert' 57 | export * from './transport/interfaces/IdHeatmap' 58 | export * from './transport/interfaces/IdRevision' 59 | export * from './transport/interfaces/IdTakeaway' 60 | export * from './transport/interfaces/OutcomeScores' 61 | export * from './transport/interfaces/OutcomeScoresWithPlagiarism' 62 | export * from './transport/interfaces/PlagiarismExtraProperties' 63 | export * from './transport/interfaces/Synonym' 64 | export * from './transport/interfaces/SynonymsGroup' 65 | export * from './transport/interfaces/VoxExtraProperties' 66 | 67 | export * from './transport/messages/AlertFeedbackRequest' 68 | export * from './transport/messages/AlertFeedbackResponse' 69 | export * from './transport/messages/DebugInfoRequest' 70 | export * from './transport/messages/DebugInfoResponse' 71 | export * from './transport/messages/EmotionFeedbackRequest' 72 | export * from './transport/messages/EmotionFeedbackResponse' 73 | export * from './transport/messages/LensFeedbackRequest' 74 | export * from './transport/messages/LensFeedbackResponse' 75 | export * from './transport/messages/MutedFeedbackRequest' 76 | export * from './transport/messages/MutedFeedbackResponse' 77 | export * from './transport/messages/OptionRequest' 78 | export * from './transport/messages/OptionResponse' 79 | export * from './transport/messages/PingRequest' 80 | export * from './transport/messages/PingResponse' 81 | export * from './transport/messages/SetContextRequest' 82 | export * from './transport/messages/SetContextResponse' 83 | export * from './transport/messages/StartRequest' 84 | export * from './transport/messages/StartResponse' 85 | export * from './transport/messages/SubmitOTChunkRequest' 86 | export * from './transport/messages/SubmitOTChunkResponse' 87 | export * from './transport/messages/SubmitOTRequest' 88 | export * from './transport/messages/SubmitOTResponse' 89 | export * from './transport/messages/SynonymsRequest' 90 | export * from './transport/messages/SynonymsResponse' 91 | export * from './transport/messages/SystemFeedbackRequest' 92 | export * from './transport/messages/SystemFeedbackResponse' 93 | export * from './transport/messages/TextStatsRequest' 94 | export * from './transport/messages/TextStatsResponse' 95 | export * from './transport/messages/ToggleChecksRequest' 96 | export * from './transport/messages/ToggleChecksResponse' 97 | 98 | export * from './transport/ot/ChangeSet' 99 | export * from './transport/ot/Delta' 100 | export * from './transport/ot/Op' 101 | export * from './transport/ot/OpDelete' 102 | export * from './transport/ot/OpInsert' 103 | export * from './transport/ot/OpRetain' 104 | export * from './transport/ot/Range' 105 | export * from './transport/ot/Transform' 106 | 107 | export * from './transport/Request' 108 | export * from './transport/RequestKind' 109 | export * from './transport/Response' 110 | export * from './transport/ResponseKind' 111 | 112 | export * from './GrammarlyClient' 113 | export * from './SocketClient' 114 | export * from './SocketError' 115 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/Request.ts: -------------------------------------------------------------------------------- 1 | import { type } from 'os' 2 | import { AlertFeedbackRequest } from './messages/AlertFeedbackRequest' 3 | import { DebugInfoRequest } from './messages/DebugInfoRequest' 4 | import { DebugInfoResponse } from './messages/DebugInfoResponse' 5 | import { EmotionFeedbackRequest } from './messages/EmotionFeedbackRequest' 6 | import { LensFeedbackRequest } from './messages/LensFeedbackRequest' 7 | import { MutedFeedbackRequest } from './messages/MutedFeedbackRequest' 8 | import { OptionRequest } from './messages/OptionRequest' 9 | import { PingRequest } from './messages/PingRequest' 10 | import { SetContextRequest } from './messages/SetContextRequest' 11 | import { StartRequest } from './messages/StartRequest' 12 | import { SubmitOTChunkRequest } from './messages/SubmitOTChunkRequest' 13 | import { SubmitOTRequest } from './messages/SubmitOTRequest' 14 | import { SynonymsRequest } from './messages/SynonymsRequest' 15 | import { SystemFeedbackRequest } from './messages/SystemFeedbackRequest' 16 | import { TextStatsRequest } from './messages/TextStatsRequest' 17 | import { ToggleChecksRequest } from './messages/ToggleChecksRequest' 18 | import { RequestKind } from './RequestKind' 19 | 20 | export type FeedbackRequest = 21 | | AlertFeedbackRequest 22 | | EmotionFeedbackRequest 23 | | LensFeedbackRequest 24 | | MutedFeedbackRequest 25 | | SystemFeedbackRequest 26 | 27 | export type Request = 28 | | DebugInfoRequest 29 | | FeedbackRequest 30 | | OptionRequest 31 | | PingRequest 32 | | SetContextRequest 33 | | StartRequest 34 | | SubmitOTRequest 35 | | SubmitOTChunkRequest 36 | | SynonymsRequest 37 | | TextStatsRequest 38 | | ToggleChecksRequest 39 | 40 | export interface RequestTypeToRequestMapping { 41 | [RequestKind.DEBUG_INFO]: DebugInfoRequest 42 | [RequestKind.FEEDBACK]: FeedbackRequest 43 | [RequestKind.OPTION]: OptionRequest 44 | [RequestKind.PING]: PingRequest 45 | [RequestKind.SET_CONTEXT]: SetContextRequest 46 | [RequestKind.START]: StartRequest 47 | [RequestKind.SUBMIT_OT]: SubmitOTRequest 48 | [RequestKind.SUBMIT_OT_CHUNK]: SubmitOTChunkRequest 49 | [RequestKind.SYNONYMS]: SynonymsRequest 50 | [RequestKind.TEXT_STATS]: TextStatsRequest 51 | [RequestKind.TOGGLE_CHECKS]: ToggleChecksRequest 52 | } 53 | 54 | export function isRequestType( 55 | request: any, 56 | kind: K, 57 | ): request is RequestTypeToRequestMapping[K] { 58 | return request.action === kind 59 | } 60 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/RequestKind.ts: -------------------------------------------------------------------------------- 1 | export const RequestKind = { 2 | DEBUG_INFO: 'get_debug_info', 3 | FEEDBACK: 'feedback', 4 | OPTION: 'option', 5 | PING: 'ping', 6 | SET_CONTEXT: 'set_context', 7 | START: 'start', 8 | SUBMIT_OT: 'submit_ot', 9 | SUBMIT_OT_CHUNK: 'submit_ot_chunk', 10 | SYNONYMS: 'synonyms', 11 | TEXT_STATS: 'get_text_stats', 12 | TOGGLE_CHECKS: 'toggle_checks', 13 | } as const 14 | export type RequestKindType = typeof RequestKind[keyof typeof RequestKind] 15 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/Response.ts: -------------------------------------------------------------------------------- 1 | import { AlertEvent } from './events/AlertEvent' 2 | import { AlertsChangedEvent } from './events/AlertsChangedEvent' 3 | import { AsyncCheckFinishedEvent } from './events/AsyncCheckFinishedEvent' 4 | import { CompleteEvent } from './events/CompleteEvent' 5 | import { EmotionsEvent } from './events/EmotionsEvent' 6 | import { ErrorEvent } from './events/ErrorEvent' 7 | import { FinishedEvent } from './events/FinishedEvent' 8 | import { HeatmapEvent } from './events/HeatmapEvent' 9 | import { PlagiarismEvent } from './events/PlagiarismEvent' 10 | import { RemoveEvent } from './events/RemoveEvent' 11 | import { TakeawaysEvent } from './events/TakeawaysEvent' 12 | import { TextInfoEvent } from './events/TextInfoEvent' 13 | import { TextMapsEvent } from './events/TextMapsEvent' 14 | import { AlertFeedbackResponse } from './messages/AlertFeedbackResponse' 15 | import { DebugInfoResponse } from './messages/DebugInfoResponse' 16 | import { EmotionFeedbackResponse } from './messages/EmotionFeedbackResponse' 17 | import { LensFeedbackResponse } from './messages/LensFeedbackResponse' 18 | import { MutedFeedbackResponse } from './messages/MutedFeedbackResponse' 19 | import { OptionResponse } from './messages/OptionResponse' 20 | import { PingResponse } from './messages/PingResponse' 21 | import { SetContextResponse } from './messages/SetContextResponse' 22 | import { StartResponse } from './messages/StartResponse' 23 | import { SubmitOTChunkResponse } from './messages/SubmitOTChunkResponse' 24 | import { SubmitOTResponse } from './messages/SubmitOTResponse' 25 | import { SynonymsResponse } from './messages/SynonymsResponse' 26 | import { SystemFeedbackResponse } from './messages/SystemFeedbackResponse' 27 | import { TextStatsResponse } from './messages/TextStatsResponse' 28 | import { ToggleChecksResponse } from './messages/ToggleChecksResponse' 29 | import { Request } from './Request' 30 | import { RequestKind } from './RequestKind' 31 | import { ResponseKind, ResponseKindType } from './ResponseKind' 32 | 33 | export type Event = 34 | | AlertEvent 35 | | AlertsChangedEvent 36 | | AsyncCheckFinishedEvent 37 | | CompleteEvent 38 | | EmotionsEvent 39 | | ErrorEvent 40 | | FinishedEvent 41 | | HeatmapEvent 42 | | PlagiarismEvent 43 | | RemoveEvent 44 | | TakeawaysEvent 45 | | TextInfoEvent 46 | | TextMapsEvent 47 | 48 | export type FeedbackResponse = 49 | | AlertFeedbackResponse 50 | | EmotionFeedbackResponse 51 | | LensFeedbackResponse 52 | | MutedFeedbackResponse 53 | | SystemFeedbackResponse 54 | 55 | export type Response = 56 | | AlertEvent 57 | | AlertsChangedEvent 58 | | AsyncCheckFinishedEvent 59 | | CompleteEvent 60 | | DebugInfoResponse 61 | | EmotionsEvent 62 | | ErrorEvent 63 | | FeedbackResponse 64 | | FinishedEvent 65 | | HeatmapEvent 66 | | OptionResponse 67 | | PlagiarismEvent 68 | | PingResponse 69 | | RemoveEvent 70 | | SetContextResponse 71 | | StartResponse 72 | | SubmitOTResponse 73 | | SubmitOTChunkResponse 74 | | SynonymsResponse 75 | | TakeawaysEvent 76 | | TextInfoEvent 77 | | TextMapsEvent 78 | | TextStatsResponse 79 | | ToggleChecksResponse 80 | 81 | export interface ResponseTypeToResponseMapping { 82 | [ResponseKind.ALERT]: AlertEvent 83 | [ResponseKind.ALERT_CHANGES]: AlertsChangedEvent 84 | [ResponseKind.ASYNC_CHECK_FINISHED]: AsyncCheckFinishedEvent 85 | [ResponseKind.COMPLETE]: CompleteEvent 86 | [ResponseKind.DEBUG_INFO]: DebugInfoResponse 87 | [ResponseKind.EMOTIONS]: EmotionsEvent 88 | [ResponseKind.ERROR]: ErrorEvent 89 | [ResponseKind.FEEDBACK]: FeedbackResponse 90 | [ResponseKind.FINISHED]: FinishedEvent 91 | [ResponseKind.HEATMAP]: HeatmapEvent 92 | [ResponseKind.OPTION]: OptionResponse 93 | [ResponseKind.PING]: PlagiarismEvent 94 | [ResponseKind.PLAGIARISM]: PingResponse 95 | [ResponseKind.REMOVE]: RemoveEvent 96 | [ResponseKind.SET_CONTEXT]: SetContextResponse 97 | [ResponseKind.START]: StartResponse 98 | [ResponseKind.SUBMIT_OT]: SubmitOTResponse 99 | [ResponseKind.SUBMIT_OT_CHUNK]: SubmitOTChunkResponse 100 | [ResponseKind.SYNONYMS]: SynonymsResponse 101 | [ResponseKind.TAKEAWAYS]: TakeawaysEvent 102 | [ResponseKind.TEXT_INFO]: TextInfoEvent 103 | [ResponseKind.TEXT_MAPS]: TextMapsEvent 104 | [ResponseKind.TEXT_STATS]: TextStatsResponse 105 | [ResponseKind.TOGGLE_CHECKS]: ToggleChecksResponse 106 | } 107 | 108 | export interface RequestTypeToResponseMapping { 109 | [RequestKind.DEBUG_INFO]: DebugInfoResponse 110 | [RequestKind.FEEDBACK]: FeedbackResponse 111 | [RequestKind.OPTION]: OptionResponse 112 | [RequestKind.PING]: PingResponse 113 | [RequestKind.SET_CONTEXT]: SetContextResponse 114 | [RequestKind.START]: StartResponse 115 | [RequestKind.SUBMIT_OT]: SubmitOTResponse 116 | [RequestKind.SUBMIT_OT_CHUNK]: SubmitOTChunkResponse 117 | [RequestKind.SYNONYMS]: SynonymsResponse 118 | [RequestKind.TEXT_STATS]: TextStatsResponse 119 | [RequestKind.TOGGLE_CHECKS]: ToggleChecksResponse 120 | } 121 | 122 | export type ResponseOf = RequestTypeToResponseMapping[T['action']] 123 | 124 | export function isResponseType( 125 | request: any, 126 | kind: K, 127 | ): request is ResponseTypeToResponseMapping[K] { 128 | return request.action === kind 129 | } 130 | 131 | export function isEvent(message: Response): message is Event { 132 | switch (message.action) { 133 | case ResponseKind.START: 134 | case ResponseKind.SUBMIT_OT: 135 | case ResponseKind.SUBMIT_OT_CHUNK: 136 | case ResponseKind.FEEDBACK: 137 | case ResponseKind.PING: 138 | case ResponseKind.OPTION: 139 | case ResponseKind.TEXT_STATS: 140 | case ResponseKind.DEBUG_INFO: 141 | case ResponseKind.SYNONYMS: 142 | case ResponseKind.SET_CONTEXT: 143 | case ResponseKind.TOGGLE_CHECKS: 144 | return false 145 | 146 | default: 147 | return true 148 | } 149 | } 150 | 151 | export function isAckResponse(message: Response): message is Exclude { 152 | return !isEvent(message) 153 | } 154 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/ResponseKind.ts: -------------------------------------------------------------------------------- 1 | export const ResponseKind = { 2 | ALERT: 'alert', 3 | ALERT_CHANGES: 'alert_changes', 4 | ASYNC_CHECK_FINISHED: 'async_check_finished', 5 | COMPLETE: 'complete', 6 | DEBUG_INFO: 'debug_info', 7 | EMOTIONS: 'emotions', 8 | ERROR: 'error', 9 | FEEDBACK: 'feedback', 10 | FINISHED: 'finished', 11 | HEATMAP: 'heatmap', 12 | OPTION: 'option', 13 | PING: 'pong', 14 | PLAGIARISM: 'plagiarism', 15 | REMOVE: 'remove', 16 | SET_CONTEXT: 'set_context', 17 | START: 'start', 18 | SUBMIT_OT: 'submit_ot', 19 | SUBMIT_OT_CHUNK: 'submit_ot_chunk', 20 | SYNONYMS: 'synonyms', 21 | TAKEAWAYS: 'takeaways', 22 | TEXT_INFO: 'text_info', 23 | TEXT_MAPS: 'text_maps', 24 | TEXT_STATS: 'text_stats', 25 | TOGGLE_CHECKS: 'toggle_checks', 26 | } as const 27 | export type ResponseKindType = typeof ResponseKind[keyof typeof ResponseKind] 28 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/AlertFeedbackType.ts: -------------------------------------------------------------------------------- 1 | export type AlertFeedbackType = 2 | | 'IGNORE' 3 | | 'ADD_TO_DICTIONARY' 4 | | 'LOOKED' 5 | | 'ACCEPTED' 6 | | 'CLOSED' 7 | | 'LIKE' 8 | | 'DISLIKE' 9 | | 'WRONG_SUGGESTION' 10 | | 'OFFENSIVE_CONTENT' 11 | | 'EXPANDED' 12 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/AlertImpactType.ts: -------------------------------------------------------------------------------- 1 | export type AlertImpactType = 'critical' | 'advanced' 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/AlertMutedByType.ts: -------------------------------------------------------------------------------- 1 | export type AlertMutedByType = 'MUTED_BY_USER' | 'NOT_ELIGIBLE_FOR_INLINE' | 'NOT_MUTED' 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/AlertViewType.ts: -------------------------------------------------------------------------------- 1 | export type AlertViewType = 'all' | 'priority' 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/AsyncChecksTypes.ts: -------------------------------------------------------------------------------- 1 | export type AsyncChecksTypes = 'plagiarism' 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/AutoCorrectFeedbackType.ts: -------------------------------------------------------------------------------- 1 | export type AutoCorrectFeedbackType = 'AUTOCORRECT_ACCEPT' | 'AUTOCORRECT_DISMISS' | 'AUTOCORRECT_REPLACE' 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/AutocompleteFeedbackType.ts: -------------------------------------------------------------------------------- 1 | export type AutocompleteFeedbackType = 2 | | 'COMPLETION_SHOWN' 3 | | 'COMPLETION_IGNORED' 4 | | 'COMPLETION_ACCEPTED' 5 | | 'COMPLETION_REJECTED' 6 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/DialectType.ts: -------------------------------------------------------------------------------- 1 | export type DialectType = 'american' | 'australian' | 'british' | 'canadian' 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/DocumentAudienceType.ts: -------------------------------------------------------------------------------- 1 | export type DocumentAudienceType = 'general' | 'knowledgeable' | 'expert' 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/DocumentDomainType.ts: -------------------------------------------------------------------------------- 1 | export type DocumentDomainType = 'academic' | 'business' | 'general' | 'technical' | 'casual' | 'creative' 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/DocumentGoalType.ts: -------------------------------------------------------------------------------- 1 | export type DocumentGoalType = 'inform' | 'describe' | 'convince' | 'tellStory' 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/EmotionFeedbackType.ts: -------------------------------------------------------------------------------- 1 | export type EmotionFeedbackType = 'EMOTION_LIKE' | 'EMOTION_DISLIKE' 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/ErrorCodeType.ts: -------------------------------------------------------------------------------- 1 | export type ErrorCodeType = 2 | | 'not_authorized' 3 | | 'session_not_initialized' 4 | | 'bad_request' 5 | | 'backend_error' 6 | | 'auth_error' 7 | | 'runtime_error' 8 | | 'illegal_dict_word' 9 | | 'timeout' 10 | | 'cannot_find_synonym' 11 | | 'cannot_get_text_stats' 12 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/ErrorSeverityType.ts: -------------------------------------------------------------------------------- 1 | export type ErrorSeverityType = 'INFO' | 'WARN' | 'ERROR' 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/FeatureType.ts: -------------------------------------------------------------------------------- 1 | export type FeatureType = 2 | | 'alerts_changes' 3 | | 'alerts_update' 4 | | 'alternative_deletes_card' 5 | | 'attention_heatmap' 6 | | 'completions' 7 | | 'consistency_check' 8 | | 'demo_text_free_premium_alerts' 9 | | 'emogenie_check' 10 | | 'filler_words_check' 11 | | 'free_clarity_alerts' 12 | | 'free_inline_advanced_alerts' 13 | | 'full_sentence_rewrite_card' 14 | | 'key_takeaways' 15 | | 'mute_quoted_alerts' 16 | | 'plagiarism_alerts_update' 17 | | 'readability_check' 18 | | 'sentence_variety_check' 19 | | 'set_goals_link' 20 | | 'super_alerts' 21 | | 'text_info' 22 | | 'tone_cards' 23 | | 'turn_to_list_card' 24 | | 'user_mutes' 25 | | 'vox_check' 26 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/LensFeedbackType.ts: -------------------------------------------------------------------------------- 1 | export type LensFeedbackType = 'LENS_CLOSE' | 'LENS_OPEN' | 'DISMISS_BY_LENS' 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/MutedFeedbackType.ts: -------------------------------------------------------------------------------- 1 | export type MutedFeedbackType = 'MUTE' | 'UNMUTE' 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/OptionType.ts: -------------------------------------------------------------------------------- 1 | export type OptionType = 'gnar_containerId' 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/PredictionType.ts: -------------------------------------------------------------------------------- 1 | export type PredictionType = 'emogenie' | 'clarity' 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/SuggestionRejectionReasonType.ts: -------------------------------------------------------------------------------- 1 | export type SuggestionRejectionReasonType = 2 | | 'NOT_RELEVANT' 3 | | 'WRONG_TONE' 4 | | 'INCORRECT' 5 | | 'WRONG_GRAMMAR' 6 | | 'OFFENSIVE' 7 | | 'OTHER' 8 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/SynonymFeedbackType.ts: -------------------------------------------------------------------------------- 1 | export type SynonymFeedbackType = 'SYNONYM_ACCEPTED' 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/SystemFeedbackType.ts: -------------------------------------------------------------------------------- 1 | export type SystemFeedbackType = 'RECHECK_SHOWN' 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/TakeawayFeedbackType.ts: -------------------------------------------------------------------------------- 1 | export type TakeawayFeedbackType = 'TAKEAWAY_LIKE' | 'TAKEAWAY_DISLIKE' | 'TAKEAWAY_LOOKED' 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/UserMutedScopeType.ts: -------------------------------------------------------------------------------- 1 | export type UserMutedScopeType = 'GLOBAL' | 'SESSION' | 'DOCUMENT' 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/WritingEmotionType.ts: -------------------------------------------------------------------------------- 1 | export type WritingEmotionType = 2 | | 'neutral' 3 | | 'confident' 4 | | 'joyful' 5 | | 'optimistic' 6 | | 'friendly' 7 | | 'urgent' 8 | | 'analytical' 9 | | 'respectful' 10 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/WritingStyleType.ts: -------------------------------------------------------------------------------- 1 | export type WritingStyleType = 'informal' | 'neutral' | 'formal' 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/enums/WritingToneType.ts: -------------------------------------------------------------------------------- 1 | export type WritingToneType = 'mild' 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/events/AlertEvent.ts: -------------------------------------------------------------------------------- 1 | import { AlertImpactType } from '../enums/AlertImpactType' 2 | import { AlertMutedByType } from '../enums/AlertMutedByType' 3 | import { AlertViewType } from '../enums/AlertViewType' 4 | import { AlertCardLayout } from '../interfaces/AlertCardLayout' 5 | import { AlertExtraProperties } from '../interfaces/AlertExtraProperties' 6 | import { IdAlert } from '../interfaces/IdAlert' 7 | import { IdRevision } from '../interfaces/IdRevision' 8 | import { Transform } from '../ot/Transform' 9 | import { ResponseKind } from '../ResponseKind' 10 | import { BaseResponse } from '../messages/BaseResponse' 11 | 12 | export interface AlertEvent extends BaseResponse { 13 | id: IdAlert 14 | action: typeof ResponseKind.ALERT 15 | rev: IdRevision 16 | begin: number 17 | end: number 18 | highlightBegin: number 19 | highlightEnd: number 20 | text: string 21 | pname: string 22 | point: string 23 | highlightText: string 24 | category: string 25 | categoryHuman: string 26 | group: string 27 | title: string 28 | details: string 29 | examples: string 30 | explanation: string 31 | transforms: string[] 32 | replacements: string[] 33 | free: boolean 34 | extra_properties: AlertExtraProperties 35 | hidden: boolean 36 | impact: AlertImpactType 37 | cardLayout: AlertCardLayout 38 | sentence_no: number 39 | todo: string 40 | minicardTitle: string 41 | cost?: number 42 | updatable?: boolean 43 | transformJson?: Transform 44 | labels?: string[] 45 | subalerts?: Array<{ 46 | transformJson: Transform 47 | highlightText: string 48 | label: string 49 | }> 50 | muted?: AlertMutedByType 51 | view?: AlertViewType 52 | } 53 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/events/AlertsChangedEvent.ts: -------------------------------------------------------------------------------- 1 | import { ResponseKind } from '../ResponseKind' 2 | import { BaseResponse } from '../messages/BaseResponse' 3 | import { AlertExtraProperties } from '../interfaces/AlertExtraProperties' 4 | import { Transform } from '../ot/Transform' 5 | import { AlertMutedByType } from '../enums/AlertMutedByType' 6 | import { IdRevision } from '../interfaces/IdRevision' 7 | 8 | export interface AlertsChangedEvent extends BaseResponse { 9 | action: typeof ResponseKind.ALERT_CHANGES 10 | extra_properties?: AlertExtraProperties 11 | rev: IdRevision 12 | transformJson?: Transform 13 | muted?: AlertMutedByType 14 | } 15 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/events/AsyncCheckFinishedEvent.ts: -------------------------------------------------------------------------------- 1 | import { ResponseKind } from '../ResponseKind' 2 | import { BaseResponse } from '../messages/BaseResponse' 3 | import { OutcomeScores } from '../interfaces/OutcomeScores' 4 | import { IdRevision } from '../interfaces/IdRevision' 5 | 6 | export interface AsyncCheckFinishedEvent extends BaseResponse { 7 | action: typeof ResponseKind.ASYNC_CHECK_FINISHED 8 | rev: IdRevision 9 | check: 0 10 | outcomeScores: OutcomeScores 11 | } 12 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/events/CompleteEvent.ts: -------------------------------------------------------------------------------- 1 | import { IdRevision } from '../interfaces/IdRevision' 2 | import { ResponseKind } from '../ResponseKind' 3 | import { BaseResponse } from '../messages/BaseResponse' 4 | 5 | export interface CompleteEvent extends BaseResponse { 6 | action: typeof ResponseKind.COMPLETE 7 | completions: Array<{ 8 | text: string 9 | patternName: string 10 | prefixBegin: number 11 | prefixEnd: number 12 | textBegin: number 13 | textEnd: number 14 | confidence: number 15 | confidenceCurve: Readonly> 16 | }> 17 | threshold: number 18 | rev: IdRevision 19 | } 20 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/events/EmotionsEvent.ts: -------------------------------------------------------------------------------- 1 | import { Emotion } from '../interfaces/Emotion' 2 | import { ResponseKind } from '../ResponseKind' 3 | import { BaseResponse } from '../messages/BaseResponse' 4 | 5 | export interface EmotionsEvent extends BaseResponse { 6 | action: typeof ResponseKind.EMOTIONS 7 | emotions: Emotion[] 8 | } 9 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/events/ErrorEvent.ts: -------------------------------------------------------------------------------- 1 | import { ErrorCodeType } from '../enums/ErrorCodeType' 2 | import { ErrorSeverityType } from '../enums/ErrorSeverityType' 3 | import { BaseResponse } from '../messages/BaseResponse' 4 | import { ResponseKind } from '../ResponseKind' 5 | 6 | export interface ErrorEvent extends BaseResponse { 7 | action: typeof ResponseKind.ERROR 8 | error: ErrorCodeType 9 | severity: ErrorSeverityType 10 | } 11 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/events/FinishedEvent.ts: -------------------------------------------------------------------------------- 1 | import { DialectType } from '../enums/DialectType' 2 | import { IdAlert } from '../interfaces/IdAlert' 3 | import { IdRevision } from '../interfaces/IdRevision' 4 | import { OutcomeScores } from '../interfaces/OutcomeScores' 5 | import { BaseResponse } from '../messages/BaseResponse' 6 | import { ResponseKind } from '../ResponseKind' 7 | 8 | export interface FinishedEvent extends BaseResponse { 9 | action: typeof ResponseKind.FINISHED 10 | rev: IdRevision 11 | score: number 12 | dialect: DialectType 13 | outcomeScores?: Partial 14 | generalScore?: number 15 | removed?: IdAlert[] 16 | } 17 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/events/HeatmapEvent.ts: -------------------------------------------------------------------------------- 1 | import { HeatmapRange } from '../interfaces/HeatmapRange' 2 | import { IdHeatmap } from '../interfaces/IdHeatmap' 3 | import { IdRevision } from '../interfaces/IdRevision' 4 | import { ResponseKind } from '../ResponseKind' 5 | import { BaseResponse } from '../messages/BaseResponse' 6 | 7 | export interface HeatmapEvent extends BaseResponse { 8 | action: typeof ResponseKind.HEATMAP 9 | add: HeatmapRange[] 10 | update: HeatmapRange[] 11 | remove: IdHeatmap[] 12 | rev: IdRevision 13 | originalRev: IdRevision 14 | version: number 15 | } 16 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/events/PlagiarismEvent.ts: -------------------------------------------------------------------------------- 1 | import { ResponseKind } from '../ResponseKind' 2 | import { BaseResponse } from '../messages/BaseResponse' 3 | 4 | export interface PlagiarismEvent extends BaseResponse { 5 | action: typeof ResponseKind.PLAGIARISM 6 | } 7 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/events/RemoveEvent.ts: -------------------------------------------------------------------------------- 1 | import { IdAlert } from '../interfaces/IdAlert' 2 | import { BaseResponse } from '../messages/BaseResponse' 3 | import { ResponseKind } from '../ResponseKind' 4 | 5 | export interface RemoveEvent extends BaseResponse { 6 | id: IdAlert 7 | action: typeof ResponseKind.REMOVE 8 | hint?: 'NOT_FIXED' 9 | mergedIn?: IdAlert 10 | } 11 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/events/TakeawaysEvent.ts: -------------------------------------------------------------------------------- 1 | import { IdRevision } from '../interfaces/IdRevision' 2 | import { IdTakeaway } from '../interfaces/IdTakeaway' 3 | import { BaseResponse } from '../messages/BaseResponse' 4 | import { ResponseKind } from '../ResponseKind' 5 | 6 | type Takeaway = any 7 | 8 | export interface TakeawaysEvent extends BaseResponse { 9 | action: typeof ResponseKind.TAKEAWAYS 10 | add: Takeaway[] 11 | update: Takeaway[] 12 | remove: IdTakeaway[] 13 | rev: IdRevision 14 | } 15 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/events/TextInfoEvent.ts: -------------------------------------------------------------------------------- 1 | import { ResponseKind } from '../ResponseKind' 2 | import { BaseResponse } from '../messages/BaseResponse' 3 | 4 | export interface TextInfoEvent extends BaseResponse { 5 | action: typeof ResponseKind.TEXT_INFO 6 | wordsCount: number 7 | charsCount: number 8 | readabilityScore: number 9 | messages?: { 10 | assistantHeader: string 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/events/TextMapsEvent.ts: -------------------------------------------------------------------------------- 1 | import { ResponseKind } from '../ResponseKind' 2 | import { BaseResponse } from '../messages/BaseResponse' 3 | 4 | export interface TextMapsEvent extends BaseResponse { 5 | action: typeof ResponseKind.TEXT_MAPS 6 | score: number 7 | generalScore: number 8 | } 9 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/interfaces/AlertCardLayout.ts: -------------------------------------------------------------------------------- 1 | import { PredictionType } from '../enums/PredictionType' 2 | 3 | export interface AlertCardLayout { 4 | category: string 5 | group: string 6 | groupDescription: string 7 | rank: number 8 | outcome: string 9 | outcomeDescription: string 10 | prediction?: PredictionType 11 | userMuteCategory?: string 12 | userMuteCategoryDescription?: string 13 | } 14 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/interfaces/AlertExtraProperties.ts: -------------------------------------------------------------------------------- 1 | import { EmogenieExtraProperties } from './EmogenieExtraProperties'; 2 | import { FluencyExtraProperties } from './FluencyExtraProperties'; 3 | import { PlagiarismExtraProperties } from './PlagiarismExtraProperties'; 4 | import { VoxExtraProperties } from './VoxExtraProperties'; 5 | 6 | export type AlertExtraProperties = Partial< 7 | { 8 | add_to_dict: string; 9 | did_you_mean: string; 10 | show_title: string; 11 | enhancement: string; 12 | url: string; 13 | sentence: string; 14 | priority: string; 15 | 16 | // C+E checks 17 | progress: number; 18 | } & PlagiarismExtraProperties & 19 | VoxExtraProperties & 20 | FluencyExtraProperties & 21 | EmogenieExtraProperties 22 | >; 23 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/interfaces/DocumentContext.ts: -------------------------------------------------------------------------------- 1 | import { DialectType } from '../enums/DialectType' 2 | import { DocumentAudienceType } from '../enums/DocumentAudienceType' 3 | import { DocumentDomainType } from '../enums/DocumentDomainType' 4 | import { DocumentGoalType } from '../enums/DocumentGoalType' 5 | import { WritingEmotionType } from '../enums/WritingEmotionType' 6 | import { WritingStyleType } from '../enums/WritingStyleType' 7 | 8 | export interface DocumentContext { 9 | dialect: DialectType 10 | domain: DocumentDomainType 11 | goals: DocumentGoalType[] 12 | audience?: DocumentAudienceType 13 | style?: WritingStyleType 14 | emotions: WritingEmotionType[] 15 | } 16 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/interfaces/DocumentStatistics.ts: -------------------------------------------------------------------------------- 1 | export interface DocumentStatistics { 2 | words: number; 3 | chars: number; 4 | sentences: number; 5 | uniqueWords: number; 6 | uniqueWordsIndex: number; 7 | rareWords: number; 8 | rareWordsIndex: number; 9 | wordLength: number; 10 | wordLengthIndex: number; 11 | sentenceLength: number; 12 | sentenceLengthIndex: number; 13 | readabilityScore: number; 14 | readabilityDescription: string; 15 | } 16 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/interfaces/EmogenieExtraProperties.ts: -------------------------------------------------------------------------------- 1 | export interface EmogenieExtraProperties { 2 | tone: string; 3 | emoji: string; 4 | full_sentence_rewrite: string; 5 | } 6 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/interfaces/Emotion.ts: -------------------------------------------------------------------------------- 1 | export interface Emotion { 2 | emoji: string; 3 | name: string; 4 | confidence: number; 5 | } 6 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/interfaces/FeedbackType.ts: -------------------------------------------------------------------------------- 1 | import { AlertFeedbackType } from '../enums/AlertFeedbackType'; 2 | import { AutocompleteFeedbackType } from '../enums/AutocompleteFeedbackType'; 3 | import { AutoCorrectFeedbackType } from '../enums/AutoCorrectFeedbackType'; 4 | import { EmotionFeedbackType } from '../enums/EmotionFeedbackType'; 5 | import { LensFeedbackType } from '../enums/LensFeedbackType'; 6 | import { MutedFeedbackType } from '../enums/MutedFeedbackType'; 7 | import { SystemFeedbackType } from '../enums/SystemFeedbackType'; 8 | import { TakeawayFeedbackType } from '../enums/TakeawayFeedbackType'; 9 | 10 | export type FeedbackType = 11 | | AlertFeedbackType 12 | | LensFeedbackType 13 | | EmotionFeedbackType 14 | | SystemFeedbackType 15 | | MutedFeedbackType 16 | | AutoCorrectFeedbackType 17 | | AutocompleteFeedbackType 18 | | TakeawayFeedbackType; 19 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/interfaces/FluencyExtraProperties.ts: -------------------------------------------------------------------------------- 1 | export interface FluencyExtraProperties { 2 | fluency_message: string; 3 | } 4 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/interfaces/HeatmapRange.ts: -------------------------------------------------------------------------------- 1 | import { IdHeatmap } from './IdHeatmap'; 2 | 3 | export interface HeatmapRange { 4 | id: IdHeatmap; 5 | begin: number; 6 | end: number; 7 | text: string; 8 | intensities: [number, number]; 9 | } 10 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/interfaces/Id.ts: -------------------------------------------------------------------------------- 1 | export type Id = number & { __type: T }; 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/interfaces/IdAlert.ts: -------------------------------------------------------------------------------- 1 | import { Id } from './Id'; 2 | 3 | export type IdAlert = Id<'Alert'>; 4 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/interfaces/IdHeatmap.ts: -------------------------------------------------------------------------------- 1 | import { Id } from './Id'; 2 | 3 | export type IdHeatmap = Id<'Heatmap'>; 4 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/interfaces/IdRevision.ts: -------------------------------------------------------------------------------- 1 | import { Id } from './Id' 2 | 3 | export type IdRevision = Id<'Revision'> 4 | export function getIdRevision(rev: number): IdRevision { 5 | return rev as IdRevision 6 | } 7 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/interfaces/IdTakeaway.ts: -------------------------------------------------------------------------------- 1 | import { Id } from './Id'; 2 | 3 | export type IdTakeaway = Id<'Takeaway'>; 4 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/interfaces/OutcomeScores.ts: -------------------------------------------------------------------------------- 1 | export interface OutcomeScores { 2 | Clarity: number; 3 | Correctness: number; 4 | Engagement: number; 5 | Tone: number; 6 | 'Style guide': number; 7 | GeneralScore: number; 8 | } 9 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/interfaces/OutcomeScoresWithPlagiarism.ts: -------------------------------------------------------------------------------- 1 | import { OutcomeScores } from './OutcomeScores'; 2 | 3 | export interface OutcomeScoresWithPlagiarism extends OutcomeScores { 4 | Originality: number; 5 | } 6 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/interfaces/PlagiarismExtraProperties.ts: -------------------------------------------------------------------------------- 1 | export interface PlagiarismExtraProperties { 2 | source: 'WEB_PAGE' | 'PUBLICATION'; 3 | percent: string; 4 | title: string; 5 | authors: string; 6 | reference_apa: string; 7 | reference_chicago: string; 8 | reference_mla: string; 9 | } 10 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/interfaces/Synonym.ts: -------------------------------------------------------------------------------- 1 | export interface Synonym { 2 | base: string; 3 | derived: string; 4 | } 5 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/interfaces/SynonymsGroup.ts: -------------------------------------------------------------------------------- 1 | import { Synonym } from './Synonym'; 2 | 3 | export interface SynonymsGroup { 4 | synonyms: Synonym[]; 5 | meaning?: string; 6 | } 7 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/interfaces/VoxExtraProperties.ts: -------------------------------------------------------------------------------- 1 | export interface VoxExtraProperties { 2 | voxCompanyName: string; 3 | voxLogoUrl: string; 4 | } 5 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/AlertFeedbackRequest.ts: -------------------------------------------------------------------------------- 1 | import { AlertFeedbackType } from '../enums/AlertFeedbackType'; 2 | import { IdAlert } from '../interfaces/IdAlert'; 3 | import { BaseFeedbackRequest } from './BaseFeedbackRequest'; 4 | 5 | export interface AlertFeedbackRequest extends BaseFeedbackRequest { 6 | type: AlertFeedbackType; 7 | alertId: IdAlert; 8 | text?: string; 9 | } 10 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/AlertFeedbackResponse.ts: -------------------------------------------------------------------------------- 1 | import { AlertFeedbackType } from '../enums/AlertFeedbackType' 2 | import { BaseFeedbackAckResponse } from './BaseFeedbackAckResponse' 3 | 4 | export interface AlertFeedbackResponse extends BaseFeedbackAckResponse { 5 | type: AlertFeedbackType 6 | } 7 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/BaseAckResponse.ts: -------------------------------------------------------------------------------- 1 | import { BaseResponse } from './BaseResponse'; 2 | 3 | export interface BaseAckResponse extends BaseResponse { 4 | id: number; 5 | } 6 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/BaseFeedbackAckResponse.ts: -------------------------------------------------------------------------------- 1 | import { FeedbackType } from '../interfaces/FeedbackType'; 2 | import { OutcomeScores } from '../interfaces/OutcomeScores'; 3 | import { BaseAckResponse } from './BaseAckResponse'; 4 | 5 | export interface BaseFeedbackAckResponse extends BaseAckResponse { 6 | type: FeedbackType; 7 | scores?: OutcomeScores; 8 | } 9 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/BaseFeedbackRequest.ts: -------------------------------------------------------------------------------- 1 | import { FeedbackType } from '../interfaces/FeedbackType'; 2 | import { BaseRequest } from './BaseRequest'; 3 | 4 | export interface BaseFeedbackRequest extends BaseRequest { 5 | type: FeedbackType; 6 | } 7 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/BaseRequest.ts: -------------------------------------------------------------------------------- 1 | import { RequestKindType } from '../RequestKind' 2 | 3 | export interface BaseRequest { 4 | id: number 5 | action: RequestKindType 6 | } 7 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/BaseResponse.ts: -------------------------------------------------------------------------------- 1 | import { ResponseKindType } from '../ResponseKind'; 2 | 3 | export interface BaseResponse { 4 | action: ResponseKindType; 5 | } 6 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/DebugInfoRequest.ts: -------------------------------------------------------------------------------- 1 | import { RequestKind } from '../RequestKind' 2 | import { BaseRequest } from './BaseRequest' 3 | 4 | export interface DebugInfoRequest extends BaseRequest { 5 | action: typeof RequestKind.DEBUG_INFO 6 | } 7 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/DebugInfoResponse.ts: -------------------------------------------------------------------------------- 1 | import { IdRevision } from '../interfaces/IdRevision' 2 | import { ResponseKind } from '../ResponseKind' 3 | import { BaseAckResponse } from './BaseAckResponse' 4 | 5 | export interface DebugInfoResponse extends BaseAckResponse { 6 | action: typeof ResponseKind['DEBUG_INFO'] 7 | rev: IdRevision 8 | sid: number 9 | text: string 10 | } 11 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/EmotionFeedbackRequest.ts: -------------------------------------------------------------------------------- 1 | import { EmotionFeedbackType } from '../enums/EmotionFeedbackType'; 2 | import { BaseFeedbackRequest } from './BaseFeedbackRequest'; 3 | 4 | export interface EmotionFeedbackRequest extends BaseFeedbackRequest { 5 | type: EmotionFeedbackType; 6 | emotion: string; 7 | } 8 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/EmotionFeedbackResponse.ts: -------------------------------------------------------------------------------- 1 | import { EmotionFeedbackType } from '../enums/EmotionFeedbackType'; 2 | import { BaseFeedbackAckResponse } from './BaseFeedbackAckResponse'; 3 | 4 | export interface EmotionFeedbackResponse extends BaseFeedbackAckResponse { 5 | type: EmotionFeedbackType; 6 | } 7 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/LensFeedbackRequest.ts: -------------------------------------------------------------------------------- 1 | import { LensFeedbackType } from '../enums/LensFeedbackType'; 2 | import { BaseFeedbackRequest } from './BaseFeedbackRequest'; 3 | 4 | export interface LensFeedbackRequest extends BaseFeedbackRequest { 5 | type: LensFeedbackType; 6 | lens: string; 7 | } 8 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/LensFeedbackResponse.ts: -------------------------------------------------------------------------------- 1 | import { LensFeedbackType } from '../enums/LensFeedbackType' 2 | import { BaseFeedbackAckResponse } from './BaseFeedbackAckResponse' 3 | 4 | export interface LensFeedbackResponse extends BaseFeedbackAckResponse { 5 | type: LensFeedbackType 6 | } 7 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/MutedFeedbackRequest.ts: -------------------------------------------------------------------------------- 1 | import { MutedFeedbackType } from '../enums/MutedFeedbackType'; 2 | import { BaseFeedbackRequest } from './BaseFeedbackRequest'; 3 | import { UserMutedScopeType } from '../enums/UserMutedScopeType'; 4 | 5 | export interface MutedFeedbackRequest extends BaseFeedbackRequest { 6 | type: MutedFeedbackType; 7 | userMuteScope: UserMutedScopeType; 8 | userMuteCategories: string[]; 9 | } 10 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/MutedFeedbackResponse.ts: -------------------------------------------------------------------------------- 1 | import { MutedFeedbackType } from '../enums/MutedFeedbackType'; 2 | import { BaseFeedbackAckResponse } from './BaseFeedbackAckResponse'; 3 | 4 | export interface MutedFeedbackResponse extends BaseFeedbackAckResponse { 5 | type: MutedFeedbackType; 6 | } 7 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/OptionRequest.ts: -------------------------------------------------------------------------------- 1 | import { OptionType } from '../enums/OptionType' 2 | import { RequestKind } from '../RequestKind' 3 | import { BaseRequest } from './BaseRequest' 4 | 5 | export interface OptionRequest extends BaseRequest { 6 | action: typeof RequestKind.OPTION 7 | name: OptionType 8 | value: string 9 | } 10 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/OptionResponse.ts: -------------------------------------------------------------------------------- 1 | import { ResponseKind } from '../ResponseKind' 2 | import { BaseAckResponse } from './BaseAckResponse' 3 | 4 | export interface OptionResponse extends BaseAckResponse { 5 | action: typeof ResponseKind.OPTION 6 | } 7 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/PingRequest.ts: -------------------------------------------------------------------------------- 1 | import { RequestKind } from '../RequestKind' 2 | import { BaseRequest } from './BaseRequest' 3 | 4 | export interface PingRequest extends BaseRequest { 5 | action: typeof RequestKind.PING 6 | } 7 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/PingResponse.ts: -------------------------------------------------------------------------------- 1 | import { ResponseKind } from '../ResponseKind' 2 | import { BaseAckResponse } from './BaseAckResponse' 3 | 4 | export interface PingResponse extends BaseAckResponse { 5 | action: typeof ResponseKind.PING 6 | } 7 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/SetContextRequest.ts: -------------------------------------------------------------------------------- 1 | import { DocumentContext } from '../interfaces/DocumentContext' 2 | import { IdRevision } from '../interfaces/IdRevision' 3 | import { RequestKind } from '../RequestKind' 4 | import { BaseRequest } from './BaseRequest' 5 | 6 | export interface SetContextRequest extends BaseRequest { 7 | action: typeof RequestKind.SET_CONTEXT 8 | rev: IdRevision 9 | documentContext: DocumentContext 10 | } 11 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/SetContextResponse.ts: -------------------------------------------------------------------------------- 1 | import { IdRevision } from '../interfaces/IdRevision' 2 | import { ResponseKind } from '../ResponseKind' 3 | import { BaseAckResponse } from './BaseAckResponse' 4 | 5 | export interface SetContextResponse extends BaseAckResponse { 6 | action: typeof ResponseKind.SET_CONTEXT 7 | rev: IdRevision 8 | } 9 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/StartRequest.ts: -------------------------------------------------------------------------------- 1 | import { DialectType } from '../enums/DialectType' 2 | import { FeatureType } from '../enums/FeatureType' 3 | import { DocumentContext } from '../interfaces/DocumentContext' 4 | import { RequestKind } from '../RequestKind' 5 | import { BaseRequest } from './BaseRequest' 6 | 7 | export interface StartRequest extends BaseRequest { 8 | action: typeof RequestKind.START 9 | client: string 10 | clientSubtype: string 11 | clientVersion: string 12 | dialect: DialectType 13 | docid: string 14 | documentContext?: DocumentContext 15 | clientSupports?: FeatureType[] 16 | } 17 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/StartResponse.ts: -------------------------------------------------------------------------------- 1 | import { ResponseKind } from '../ResponseKind' 2 | import { BaseAckResponse } from './BaseAckResponse' 3 | 4 | export interface StartResponse extends BaseAckResponse { 5 | action: typeof ResponseKind.START 6 | sid: number 7 | } 8 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/SubmitOTChunkRequest.ts: -------------------------------------------------------------------------------- 1 | import { IdRevision } from '../interfaces/IdRevision' 2 | import { Delta } from '../ot/Delta' 3 | import { RequestKind } from '../RequestKind' 4 | import { BaseRequest } from './BaseRequest' 5 | 6 | export interface SubmitOTChunkRequest extends BaseRequest { 7 | action: typeof RequestKind.SUBMIT_OT_CHUNK 8 | rev: IdRevision 9 | doc_len: number 10 | deltas: [Delta] 11 | chunked: false 12 | } 13 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/SubmitOTChunkResponse.ts: -------------------------------------------------------------------------------- 1 | import { IdRevision } from '../interfaces/IdRevision' 2 | import { ResponseKind } from '../ResponseKind' 3 | import { BaseAckResponse } from './BaseAckResponse' 4 | 5 | export interface SubmitOTChunkResponse extends BaseAckResponse { 6 | action: typeof ResponseKind.SUBMIT_OT_CHUNK 7 | rev: IdRevision 8 | } 9 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/SubmitOTRequest.ts: -------------------------------------------------------------------------------- 1 | import { IdRevision } from '../interfaces/IdRevision' 2 | import { Delta } from '../ot/Delta' 3 | import { RequestKind } from '../RequestKind' 4 | import { BaseRequest } from './BaseRequest' 5 | 6 | export interface SubmitOTRequest extends BaseRequest { 7 | action: typeof RequestKind.SUBMIT_OT 8 | rev: IdRevision 9 | doc_len: number 10 | deltas: Delta[] 11 | chunked: false 12 | } 13 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/SubmitOTResponse.ts: -------------------------------------------------------------------------------- 1 | import { IdRevision } from '../interfaces/IdRevision' 2 | import { ResponseKind } from '../ResponseKind' 3 | import { BaseAckResponse } from './BaseAckResponse' 4 | 5 | export interface SubmitOTResponse extends BaseAckResponse { 6 | action: typeof ResponseKind.SUBMIT_OT 7 | rev: IdRevision 8 | } 9 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/SynonymsRequest.ts: -------------------------------------------------------------------------------- 1 | import { RequestKind } from '../RequestKind' 2 | import { BaseRequest } from './BaseRequest' 3 | 4 | export interface SynonymsRequest extends BaseRequest { 5 | action: typeof RequestKind.SYNONYMS 6 | begin: number 7 | token: string 8 | } 9 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/SynonymsResponse.ts: -------------------------------------------------------------------------------- 1 | import { SynonymsGroup } from '../interfaces/SynonymsGroup' 2 | import { ResponseKind } from '../ResponseKind' 3 | import { BaseAckResponse } from './BaseAckResponse' 4 | 5 | export interface SynonymsResponse extends BaseAckResponse { 6 | action: typeof ResponseKind.SYNONYMS 7 | token: string 8 | synonyms: { pos: number; meanings: SynonymsGroup[] } 9 | } 10 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/SystemFeedbackRequest.ts: -------------------------------------------------------------------------------- 1 | import { SystemFeedbackType } from '../enums/SystemFeedbackType'; 2 | import { BaseFeedbackRequest } from './BaseFeedbackRequest'; 3 | 4 | export interface SystemFeedbackRequest extends BaseFeedbackRequest { 5 | type: SystemFeedbackType; 6 | } 7 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/SystemFeedbackResponse.ts: -------------------------------------------------------------------------------- 1 | import { SystemFeedbackType } from '../enums/SystemFeedbackType' 2 | import { BaseFeedbackAckResponse } from './BaseFeedbackAckResponse' 3 | 4 | export interface SystemFeedbackResponse extends BaseFeedbackAckResponse { 5 | type: SystemFeedbackType 6 | } 7 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/TextStatsRequest.ts: -------------------------------------------------------------------------------- 1 | import { RequestKind } from '../RequestKind' 2 | import { BaseRequest } from './BaseRequest' 3 | 4 | export interface TextStatsRequest extends BaseRequest { 5 | action: typeof RequestKind.TEXT_STATS 6 | } 7 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/TextStatsResponse.ts: -------------------------------------------------------------------------------- 1 | import { DocumentStatistics } from '../interfaces/DocumentStatistics' 2 | import { ResponseKind } from '../ResponseKind' 3 | import { BaseAckResponse } from './BaseAckResponse' 4 | 5 | export interface TextStatsResponse extends BaseAckResponse, DocumentStatistics { 6 | action: typeof ResponseKind.TEXT_STATS 7 | } 8 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/ToggleChecksRequest.ts: -------------------------------------------------------------------------------- 1 | import { AsyncChecksTypes } from '../enums/AsyncChecksTypes' 2 | import { RequestKind } from '../RequestKind' 3 | import { BaseRequest } from './BaseRequest' 4 | 5 | export interface ToggleChecksRequest extends BaseRequest { 6 | action: typeof RequestKind.TOGGLE_CHECKS 7 | checks: Record 8 | } 9 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/messages/ToggleChecksResponse.ts: -------------------------------------------------------------------------------- 1 | import { ResponseKind } from '../ResponseKind' 2 | import { BaseAckResponse } from './BaseAckResponse' 3 | 4 | export interface ToggleChecksResponse extends BaseAckResponse { 5 | action: typeof ResponseKind.TOGGLE_CHECKS 6 | } 7 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/ot/ChangeSet.ts: -------------------------------------------------------------------------------- 1 | import QuillDelta from 'quill-delta' 2 | import { Delta } from './Delta' 3 | import { Op } from './Op' 4 | 5 | export class ChangeSet { 6 | private prev = new QuillDelta() 7 | private next = new QuillDelta() 8 | private delta: QuillDelta 9 | 10 | constructor (prevText: string, nextText: string) { 11 | this.prev.insert(prevText) 12 | this.next.insert(nextText) 13 | this.delta = this.prev.diff(this.next) 14 | } 15 | 16 | diff(): Delta[] { 17 | return [{ ops: this.delta.ops as Op[] }] 18 | } 19 | 20 | reposition(offset: number): number { 21 | return this.delta.transformPosition(offset) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/ot/Delta.ts: -------------------------------------------------------------------------------- 1 | import { Op } from './Op'; 2 | 3 | export interface Delta { 4 | ops: Op[]; 5 | } 6 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/ot/Op.ts: -------------------------------------------------------------------------------- 1 | import { OpDelete } from './OpDelete'; 2 | import { OpInsert } from './OpInsert'; 3 | import { OpRetain } from './OpRetain'; 4 | 5 | export type Op = OpRetain | OpInsert | OpDelete; 6 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/ot/OpDelete.ts: -------------------------------------------------------------------------------- 1 | export type OpDelete = { delete: number }; 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/ot/OpInsert.ts: -------------------------------------------------------------------------------- 1 | export type OpInsert = { insert: string }; 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/ot/OpRetain.ts: -------------------------------------------------------------------------------- 1 | export type OpRetain = { retain: number }; 2 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/ot/Range.ts: -------------------------------------------------------------------------------- 1 | export interface Range { 2 | s: number; 3 | e: number; 4 | type?: 'main' | 'focus'; 5 | } 6 | -------------------------------------------------------------------------------- /packages/grammarly-api/src/transport/ot/Transform.ts: -------------------------------------------------------------------------------- 1 | import { Delta } from './Delta'; 2 | import { Range } from './Range'; 3 | 4 | export interface Transform { 5 | highlights: Range[]; 6 | context: Range; 7 | alternatives: Delta[]; 8 | } 9 | 10 | export function applyDelta(text: string, change: Delta): string { 11 | let newText = '' 12 | 13 | change.ops.forEach(op => { 14 | if ('insert' in op) { 15 | newText += op.insert 16 | } else if ('delete' in op) { 17 | text = text.substr(op.delete) 18 | } else { 19 | newText += text.substr(0, op.retain) 20 | text = text.substr(op.retain) 21 | } 22 | }) 23 | 24 | return newText + text 25 | } 26 | -------------------------------------------------------------------------------- /packages/grammarly-api/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-api/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/node-fetch@^2.5.7": 6 | version "2.5.8" 7 | resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.8.tgz#e199c835d234c7eb0846f6618012e558544ee2fb" 8 | integrity sha512-fbjI6ja0N5ZA8TV53RUqzsKNkl9fv8Oj3T7zxW7FGv1GSH7gwJaNF8dzCjrqKaxKeUpTz4yT1DaJFq/omNpGfw== 9 | dependencies: 10 | "@types/node" "*" 11 | form-data "^3.0.0" 12 | 13 | "@types/node@*": 14 | version "14.14.32" 15 | resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.32.tgz#90c5c4a8d72bbbfe53033f122341343249183448" 16 | integrity sha512-/Ctrftx/zp4m8JOujM5ZhwzlWLx22nbQJiVqz8/zE15gOeEW+uly3FSX4fGFpcfEvFzXcMCJwq9lGVWgyARXhg== 17 | 18 | "@types/ws@^7.2.9": 19 | version "7.4.0" 20 | resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.0.tgz#499690ea08736e05a8186113dac37769ab251a0e" 21 | integrity sha512-Y29uQ3Uy+58bZrFLhX36hcI3Np37nqWE7ky5tjiDoy1GDZnIwVxS0CgF+s+1bXMzjKBFy+fqaRfb708iNzdinw== 22 | dependencies: 23 | "@types/node" "*" 24 | 25 | asynckit@^0.4.0: 26 | version "0.4.0" 27 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 28 | integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= 29 | 30 | combined-stream@^1.0.8: 31 | version "1.0.8" 32 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" 33 | integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== 34 | dependencies: 35 | delayed-stream "~1.0.0" 36 | 37 | delayed-stream@~1.0.0: 38 | version "1.0.0" 39 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 40 | integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= 41 | 42 | fast-diff@1.2.0: 43 | version "1.2.0" 44 | resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" 45 | integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== 46 | 47 | form-data@^3.0.0: 48 | version "3.0.1" 49 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" 50 | integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== 51 | dependencies: 52 | asynckit "^0.4.0" 53 | combined-stream "^1.0.8" 54 | mime-types "^2.1.12" 55 | 56 | lodash.clonedeep@^4.5.0: 57 | version "4.5.0" 58 | resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" 59 | integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= 60 | 61 | lodash.isequal@^4.5.0: 62 | version "4.5.0" 63 | resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" 64 | integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= 65 | 66 | mime-db@1.46.0: 67 | version "1.46.0" 68 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.46.0.tgz#6267748a7f799594de3cbc8cde91def349661cee" 69 | integrity sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ== 70 | 71 | mime-types@^2.1.12: 72 | version "2.1.29" 73 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.29.tgz#1d4ab77da64b91f5f72489df29236563754bb1b2" 74 | integrity sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ== 75 | dependencies: 76 | mime-db "1.46.0" 77 | 78 | node-fetch@^2.6.1: 79 | version "2.6.1" 80 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" 81 | integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== 82 | 83 | quill-delta@^4.2.2: 84 | version "4.2.2" 85 | resolved "https://registry.yarnpkg.com/quill-delta/-/quill-delta-4.2.2.tgz#015397d046e0a3bed087cd8a51f98c11a1b8f351" 86 | integrity sha512-qjbn82b/yJzOjstBgkhtBjN2TNK+ZHP/BgUQO+j6bRhWQQdmj2lH6hXG7+nwwLF41Xgn//7/83lxs9n2BkTtTg== 87 | dependencies: 88 | fast-diff "1.2.0" 89 | lodash.clonedeep "^4.5.0" 90 | lodash.isequal "^4.5.0" 91 | 92 | ws@^7.3.1: 93 | version "7.4.4" 94 | resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59" 95 | integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw== 96 | -------------------------------------------------------------------------------- /packages/grammarly-language-client/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # unofficial-grammarly-language-client 2 | 3 | ## 0.2.0 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [1ed857d] 8 | - unofficial-grammarly-api-2@0.2.0 9 | 10 | ## 0.1.0 11 | 12 | ### Minor Changes 13 | 14 | - OAuth Support 15 | 16 | ### Patch Changes 17 | 18 | - Updated dependencies [undefined] 19 | - unofficial-grammarly-api-2@0.1.0 20 | -------------------------------------------------------------------------------- /packages/grammarly-language-client/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-language-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@emacs-grammarly/unofficial-grammarly-language-client", 3 | "version": "0.2.2", 4 | "description": "LSP client implementation for Grammarly", 5 | "author": "Rahul Kadyan ", 6 | "main": "dist/index.cjs.js", 7 | "module": "dist/index.esm.js", 8 | "types": "dist/index.d.ts", 9 | "license": "MIT", 10 | "files": [ 11 | "dist", 12 | "bin" 13 | ], 14 | "dependencies": { 15 | "minimatch": "^3.0.4", 16 | "@emacs-grammarly/unofficial-grammarly-api": "0.2.2", 17 | "vscode-jsonrpc": "^5.0.1", 18 | "vscode-languageclient": "^6.1.3", 19 | "vscode-languageserver-textdocument": "^1.0.1" 20 | }, 21 | "devDependencies": { 22 | "@types/minimatch": "^3.0.3" 23 | }, 24 | "publishConfig": { 25 | "access": "public" 26 | } 27 | } -------------------------------------------------------------------------------- /packages/grammarly-language-client/src/GrammarlyLanguageClient.ts: -------------------------------------------------------------------------------- 1 | import minimatch from 'minimatch' 2 | // @ts-ignore 3 | import { Disposable, LanguageClient } from 'vscode-languageclient' 4 | import { GrammarlyLanguageServer } from '../../grammarly-language-server/src/protocol' 5 | import { GrammarlyLanguageClientOptions } from './GrammarlyLanguageClientOptions' 6 | import { getLanguageClientOptions, getLanguageServerOptions, LANGUAGES } from './options' 7 | 8 | export class GrammarlyLanguageClient { 9 | public readonly grammarly: LanguageClient 10 | 11 | constructor (private readonly serverPath: string, private readonly options: GrammarlyLanguageClientOptions) { 12 | this.grammarly = new LanguageClient( 13 | options.info?.name ?? 'unknown', 14 | options.info?.name ?? 'unknown', 15 | getLanguageServerOptions(this.serverPath), 16 | getLanguageClientOptions(), 17 | ) 18 | } 19 | 20 | private _isReady = false 21 | 22 | start(): Disposable { 23 | const disposable = this.grammarly.start() 24 | 25 | this.grammarly.onReady().then(() => { 26 | this._isReady = true 27 | this.grammarly.onRequest(GrammarlyLanguageServer.Client.Feature.getCredentials, this.options.getCredentials) 28 | this.grammarly.onRequest(GrammarlyLanguageServer.Client.Feature.getToken, async () => { 29 | const content = this.options.loadToken != null ? await this.options.loadToken() : null 30 | 31 | if (content != null) return JSON.parse(content) 32 | }) 33 | 34 | this.grammarly.onRequest(GrammarlyLanguageServer.Client.Feature.storeToken, async (cookie: any) => { 35 | if (this.options.saveToken != null) await this.options.saveToken(cookie != null ? JSON.stringify(cookie) : null) 36 | }) 37 | 38 | this.grammarly.onRequest(GrammarlyLanguageServer.Client.Feature.showError, ({ message, buttons }: { message: string, buttons: string[] }) => { 39 | const actions = Array.from(buttons).filter(Boolean).map(String) 40 | if (this.options.onError != null) { 41 | return this.options.onError(message, actions) 42 | } else { 43 | console.error(message) 44 | return null 45 | } 46 | }) 47 | }) 48 | 49 | return disposable 50 | } 51 | 52 | get onReady(): () => Promise { 53 | return this.grammarly.onReady.bind(this.grammarly) 54 | } 55 | 56 | isReady(): boolean { 57 | if (!this._isReady) { 58 | return false 59 | } 60 | 61 | return true 62 | } 63 | 64 | isIgnoredDocument(uri: string, languageId: string): boolean { 65 | const ignore = this.options.getIgnoredDocuments != null ? this.options.getIgnoredDocuments(uri) : [] 66 | const isIgnored = !LANGUAGES.includes(languageId) || ignore.some((pattern) => minimatch(uri, pattern)) 67 | 68 | return isIgnored 69 | } 70 | 71 | async getDocumentState(uri: string): Promise { 72 | return this.grammarly.sendRequest(GrammarlyLanguageServer.Feature.getDocumentState, { uri }) 73 | } 74 | 75 | async sendFeedback(method: string, params: any): Promise { 76 | await this.grammarly.sendRequest(method, params) 77 | } 78 | 79 | async check(uri: string): Promise { 80 | await this.grammarly.sendRequest(GrammarlyLanguageServer.Feature.checkGrammar, { uri }) 81 | } 82 | 83 | async stopCheck(uri: string): Promise { 84 | await this.grammarly.sendRequest(GrammarlyLanguageServer.Feature.stop, { uri }) 85 | } 86 | 87 | async dismissAlert(uri: string, alertId: number): Promise { 88 | await this.grammarly.sendRequest(GrammarlyLanguageServer.Feature.dismissAlert, { uri, id: alertId }) 89 | } 90 | 91 | async addToDictionary(uri: string, alertId: number): Promise { 92 | await this.grammarly.sendRequest(GrammarlyLanguageServer.Feature.addToDictionary, { uri, id: alertId }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /packages/grammarly-language-client/src/GrammarlyLanguageClientOptions.ts: -------------------------------------------------------------------------------- 1 | import { TextDocument } from 'vscode-languageserver-textdocument' 2 | 3 | export interface GrammarlyLanguageClientOptions { 4 | info?: { 5 | name: string 6 | version?: string 7 | } 8 | getCredentials: () => Promise<{ username: string; password: string } | string | null> 9 | getIgnoredDocuments?: (uri: string) => string[] 10 | onError?: (error: string, actions: string[]) => Promise 11 | saveToken?: (token: string | null) => Promise 12 | loadToken?: () => Promise 13 | } 14 | -------------------------------------------------------------------------------- /packages/grammarly-language-client/src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare var __DEV__: boolean 2 | -------------------------------------------------------------------------------- /packages/grammarly-language-client/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './GrammarlyLanguageClient' 2 | export * from './GrammarlyLanguageClientOptions' 3 | -------------------------------------------------------------------------------- /packages/grammarly-language-client/src/options.ts: -------------------------------------------------------------------------------- 1 | import { LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient' 2 | 3 | const supportedSchemes = ['file', 'untitled', 'vue', 'gist'] 4 | 5 | export function generateDocumentSelectors(languages: string[]): LanguageClientOptions['documentSelector'] { 6 | return languages.map((language) => supportedSchemes.map((scheme) => ({ language, scheme }))).flat(3) 7 | } 8 | 9 | export function getLanguageServerOptions(module: string): ServerOptions { 10 | return { 11 | run: { 12 | module, 13 | transport: TransportKind.ipc, 14 | }, 15 | debug: { 16 | module, 17 | transport: TransportKind.ipc, 18 | options: { 19 | execArgv: ['--nolazy', '--inspect=6009'], 20 | }, 21 | }, 22 | } 23 | } 24 | 25 | export const LANGUAGES = [ 26 | 'asciidoc', 27 | 'git-commit', 28 | 'git-rebase', 29 | 'json', 30 | 'latex', 31 | 'markdown', 32 | 'mdx', 33 | 'plaintext', 34 | 'restructuredtext', 35 | ] 36 | 37 | export function getLanguageClientOptions(): LanguageClientOptions { 38 | return { 39 | documentSelector: generateDocumentSelectors(LANGUAGES), 40 | synchronize: { 41 | configurationSection: 'grammarly', 42 | }, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/grammarly-language-client/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", "../grammarly-language-server/src/protocol.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/grammarly-language-client/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@emacs-grammarly/unofficial-grammarly-api@0.2.2": 6 | version "0.2.2" 7 | resolved "https://registry.yarnpkg.com/@emacs-grammarly/unofficial-grammarly-api/-/unofficial-grammarly-api-0.2.2.tgz#79c74c1fe3c97d087b2c059eed671fff367cf8fc" 8 | integrity sha512-GKv/FrBSpHcAwLbuNV97uUgFBA8UiUxnYD2HkgCLDdFf+s7NadDI9OGwi5mMWaEjlim8WD/AOmx/gH39SbnI9g== 9 | dependencies: 10 | node-fetch "^2.6.1" 11 | quill-delta "^4.2.2" 12 | ws "^7.3.1" 13 | 14 | "@types/minimatch@^3.0.3": 15 | version "3.0.3" 16 | resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" 17 | integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== 18 | 19 | balanced-match@^1.0.0: 20 | version "1.0.0" 21 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 22 | integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= 23 | 24 | brace-expansion@^1.1.7: 25 | version "1.1.11" 26 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 27 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 28 | dependencies: 29 | balanced-match "^1.0.0" 30 | concat-map "0.0.1" 31 | 32 | concat-map@0.0.1: 33 | version "0.0.1" 34 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 35 | integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= 36 | 37 | fast-diff@1.2.0: 38 | version "1.2.0" 39 | resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" 40 | integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== 41 | 42 | lodash.clonedeep@^4.5.0: 43 | version "4.5.0" 44 | resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" 45 | integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= 46 | 47 | lodash.isequal@^4.5.0: 48 | version "4.5.0" 49 | resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" 50 | integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= 51 | 52 | minimatch@^3.0.4: 53 | version "3.0.4" 54 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 55 | integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== 56 | dependencies: 57 | brace-expansion "^1.1.7" 58 | 59 | node-fetch@^2.6.1: 60 | version "2.6.1" 61 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" 62 | integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== 63 | 64 | quill-delta@^4.2.2: 65 | version "4.2.2" 66 | resolved "https://registry.yarnpkg.com/quill-delta/-/quill-delta-4.2.2.tgz#015397d046e0a3bed087cd8a51f98c11a1b8f351" 67 | integrity sha512-qjbn82b/yJzOjstBgkhtBjN2TNK+ZHP/BgUQO+j6bRhWQQdmj2lH6hXG7+nwwLF41Xgn//7/83lxs9n2BkTtTg== 68 | dependencies: 69 | fast-diff "1.2.0" 70 | lodash.clonedeep "^4.5.0" 71 | lodash.isequal "^4.5.0" 72 | 73 | semver@^6.3.0: 74 | version "6.3.0" 75 | resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" 76 | integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== 77 | 78 | vscode-jsonrpc@^5.0.1: 79 | version "5.0.1" 80 | resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz#9bab9c330d89f43fc8c1e8702b5c36e058a01794" 81 | integrity sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A== 82 | 83 | vscode-languageclient@^6.1.3: 84 | version "6.1.4" 85 | resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-6.1.4.tgz#54aa8b1559ae2e0499cb6ab746cc2662fb6ecc0f" 86 | integrity sha512-EUOU+bJu6axmt0RFNo3nrglQLPXMfanbYViJee3Fbn2VuQoX0ZOI4uTYhSRvYLP2vfwTP/juV62P/mksCdTZMA== 87 | dependencies: 88 | semver "^6.3.0" 89 | vscode-languageserver-protocol "3.15.3" 90 | 91 | vscode-languageserver-protocol@3.15.3: 92 | version "3.15.3" 93 | resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz#3fa9a0702d742cf7883cb6182a6212fcd0a1d8bb" 94 | integrity sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw== 95 | dependencies: 96 | vscode-jsonrpc "^5.0.1" 97 | vscode-languageserver-types "3.15.1" 98 | 99 | vscode-languageserver-textdocument@^1.0.1: 100 | version "1.0.1" 101 | resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz#178168e87efad6171b372add1dea34f53e5d330f" 102 | integrity sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA== 103 | 104 | vscode-languageserver-types@3.15.1: 105 | version "3.15.1" 106 | resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz#17be71d78d2f6236d414f0001ce1ef4d23e6b6de" 107 | integrity sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ== 108 | 109 | ws@^7.3.1: 110 | version "7.4.4" 111 | resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59" 112 | integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw== 113 | -------------------------------------------------------------------------------- /packages/grammarly-language-server/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # unofficial-grammarly-language-server 2 | 3 | ## 0.2.0 4 | 5 | ### Minor Changes 6 | 7 | - 1ed857d: Support dismiss alert feedback 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies [1ed857d] 12 | - unofficial-grammarly-api-2@0.2.0 13 | 14 | ## 0.1.0 15 | 16 | ### Minor Changes 17 | 18 | - OAuth Support 19 | 20 | ### Patch Changes 21 | 22 | - Updated dependencies [undefined] 23 | - unofficial-grammarly-api-2@0.1.0 24 | -------------------------------------------------------------------------------- /packages/grammarly-language-server/bin/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const server = require('../dist/index.cjs') 4 | 5 | server.startLanguageServer() 6 | -------------------------------------------------------------------------------- /packages/grammarly-language-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@emacs-grammarly/unofficial-grammarly-language-server", 3 | "version": "0.2.2", 4 | "description": "LSP server implementation for Grammarly", 5 | "author": "Jen-Chieh Shen ", 6 | "main": "dist/index.cjs.js", 7 | "module": "dist/index.esm.js", 8 | "types": "dist/index.d.ts", 9 | "bin": "bin/server.js", 10 | "license": "MIT", 11 | "files": [ 12 | "dist", 13 | "bin" 14 | ], 15 | "dependencies": { 16 | "@flatten-js/interval-tree": "^1.0.13", 17 | "@vue/reactivity": "^3.0.2", 18 | "inversify": "^5.0.1", 19 | "minimatch": "^3.0.4", 20 | "reflect-metadata": "^0.1.13", 21 | "remark": "^13.0.0", 22 | "@emacs-grammarly/unofficial-grammarly-api": "^0.2.2", 23 | "vscode-jsonrpc": "^5.0.1", 24 | "vscode-languageserver": "^6.1.1", 25 | "vscode-languageserver-textdocument": "^1.0.1" 26 | }, 27 | "publishConfig": { 28 | "access": "public" 29 | } 30 | } -------------------------------------------------------------------------------- /packages/grammarly-language-server/src/DevLogger.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'util' 2 | 3 | export const enum LoggerLevel { 4 | TRACE, 5 | DEBUG, 6 | INFO, 7 | WARN, 8 | ERROR, 9 | NONE, 10 | } 11 | 12 | const displayLevel = { 13 | [LoggerLevel.TRACE]: 'TRACE', 14 | [LoggerLevel.DEBUG]: 'DEBUG', 15 | [LoggerLevel.INFO]: 'INFO', 16 | [LoggerLevel.WARN]: 'WARN', 17 | [LoggerLevel.ERROR]: 'ERROR', 18 | [LoggerLevel.NONE]: 'NONE', 19 | } 20 | 21 | function isString(value: any): value is string { 22 | return typeof value === 'string' 23 | } 24 | 25 | function isError(value: any): value is Error { 26 | return value instanceof Error 27 | } 28 | 29 | export class Logger { 30 | static options = { 31 | enabled: new Set(['*']), 32 | level: LoggerLevel.DEBUG, 33 | } 34 | 35 | constructor (public readonly name: string, public readonly defaultContext: string = '') { } 36 | 37 | trace(msg: string, ...args: any[]): void 38 | trace(context: string, msg: string, ...args: any[]): void 39 | trace(...args: any[]) { 40 | this.write(LoggerLevel.TRACE, args) 41 | } 42 | 43 | debug(msg: string, ...args: any[]): void 44 | debug(context: string, msg: string, ...args: any[]): void 45 | debug(...args: any[]) { 46 | this.write(LoggerLevel.DEBUG, args) 47 | } 48 | 49 | info(msg: string, ...args: any[]): void 50 | info(context: string, msg: string, ...args: any[]): void 51 | info(...args: any[]) { 52 | this.write(LoggerLevel.INFO, args) 53 | } 54 | 55 | warn(msg: string, ...args: any[]): void 56 | warn(context: string, msg: string, ...args: any[]): void 57 | warn(...args: any[]) { 58 | this.write(LoggerLevel.WARN, args) 59 | } 60 | 61 | error(msg: string, ...args: any[]): void 62 | error(msg: Error, ...args: any[]): void 63 | error(context: string, msg: string, ...args: any[]): void 64 | error(context: string, msg: Error, ...args: any[]): void 65 | error(...args: any[]) { 66 | this.write(LoggerLevel.ERROR, args) 67 | } 68 | 69 | private write(level: LoggerLevel, args: any[]) { 70 | if ( 71 | level >= Logger.options.level && 72 | (Logger.options.enabled.has('*') || Logger.options.enabled.has(this.name)) 73 | ) { 74 | const context = 75 | args.length >= 2 && isString(args[0]) && (isString(args[1]) || isError(args[1])) 76 | ? args.shift() 77 | : this.defaultContext 78 | 79 | const message = `${Date.now()} ${displayLevel[level]} [${this.name}]${context ? ' (' + context + ')' : ''} ${this.inspect( 80 | args, 81 | )}` 82 | 83 | switch (level) { 84 | case LoggerLevel.ERROR: 85 | console.error(message) 86 | break 87 | case LoggerLevel.WARN: 88 | console.warn(message) 89 | break 90 | default: 91 | console.log(message) 92 | break 93 | } 94 | } 95 | } 96 | 97 | private inspect(args: any[]) { 98 | return args.map((arg) => (typeof arg === 'object' && arg ? inspect(arg, true, null) : arg)).join(' ') 99 | } 100 | } 101 | 102 | export class DevLogger extends Logger { } 103 | -------------------------------------------------------------------------------- /packages/grammarly-language-server/src/GrammarlyDocument.ts: -------------------------------------------------------------------------------- 1 | import { Position, Range, TextDocument, TextDocumentContentChangeEvent } from 'vscode-languageserver-textdocument' 2 | import { GrammarlyHostFactory } from './GrammarlyHostFactory' 3 | import { TextGrammarCheckHost } from './hosts/TextGrammarCheckHost' 4 | import { parsers } from './parsers' 5 | 6 | export class GrammarlyDocument implements TextDocument { 7 | private _host: TextGrammarCheckHost | null = null 8 | private isDirty = true 9 | private rangeToIdentifierFn?: (interval: [number, number]) => string[] 10 | 11 | private constructor (private internal: TextDocument) { } 12 | 13 | attachHost(factory: GrammarlyHostFactory, clientInfo: { name: string; version?: string }) { 14 | this.detachHost() 15 | 16 | this._host = factory.create(this, clientInfo) 17 | } 18 | 19 | detachHost() { 20 | if (this._host) { 21 | this._host.dispose() 22 | this._host = null 23 | } 24 | } 25 | 26 | inIgnoredRange(interval: [number, number], tags: string[]): boolean { 27 | if (this.isDirty) { 28 | this.isDirty = false 29 | const parser = parsers[this.languageId] 30 | 31 | try { 32 | if (parser) this.rangeToIdentifierFn = parser.parse(this.getText()) 33 | } catch { } 34 | } 35 | 36 | if (this.rangeToIdentifierFn) { 37 | const matched = new Set(this.rangeToIdentifierFn(interval)) 38 | 39 | return tags.some((tag) => matched.has(tag)) 40 | } 41 | 42 | return false 43 | } 44 | 45 | get host() { 46 | return this._host 47 | } 48 | 49 | get uri() { 50 | return this.internal.uri 51 | } 52 | 53 | get languageId() { 54 | return this.internal.languageId 55 | } 56 | 57 | get version() { 58 | return this.internal.version 59 | } 60 | 61 | getText(range?: Range): string { 62 | return this.internal.getText(range) 63 | } 64 | 65 | positionAt(offset: number): Position { 66 | return this.internal.positionAt(offset) 67 | } 68 | 69 | rangeAt(start: number, end: number): Range { 70 | return { start: this.positionAt(start), end: this.positionAt(end) } 71 | } 72 | 73 | offsetAt(position: Position): number { 74 | return this.internal.offsetAt(position) 75 | } 76 | 77 | get lineCount() { 78 | return this.internal.lineCount 79 | } 80 | 81 | static create(uri: string, languageId: string, version: number, content: string): GrammarlyDocument { 82 | return new GrammarlyDocument(TextDocument.create(uri, languageId, version, content)) 83 | } 84 | 85 | static update( 86 | document: GrammarlyDocument, 87 | changes: TextDocumentContentChangeEvent[], 88 | version: number, 89 | ): GrammarlyDocument { 90 | document.isDirty = true 91 | 92 | document.internal = TextDocument.update(document.internal, changes, version) 93 | if (document._host) { 94 | document._host.setText(document.getText()) 95 | } 96 | 97 | return document 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /packages/grammarly-language-server/src/GrammarlyHostFactory.ts: -------------------------------------------------------------------------------- 1 | import { 2 | anonymous, 3 | authenticate, 4 | DocumentContext, 5 | GrammarlyAuthContext, 6 | SocketError, 7 | SocketErrorCode, 8 | } from '@emacs-grammarly/unofficial-grammarly-api' 9 | import { DevLogger } from './DevLogger' 10 | import { GrammarlyDocument } from './GrammarlyDocument' 11 | import { TextGrammarCheckHost } from './hosts/TextGrammarCheckHost' 12 | import { version } from '../package.json' 13 | 14 | const knownClients: Record = { 15 | 'vscode': { 16 | name: 'extension_vscode', 17 | type: 'general', 18 | version: version 19 | }, 20 | 'vscode-insiders': { 21 | name: 'extension_vscode', 22 | type: 'insiders', 23 | version: version 24 | }, 25 | } 26 | 27 | export class GrammarlyHostFactory { 28 | private LOGGER = new DevLogger(GrammarlyHostFactory.name) 29 | private auth: GrammarlyAuthContext | null = null 30 | 31 | constructor( 32 | private getDocumentContext: (document: GrammarlyDocument) => Promise, 33 | private getCredentials: () => Promise<{ username: string; password: string } | string>, 34 | private storeToken: (token: string | null) => void, 35 | ) { } 36 | 37 | public create(document: GrammarlyDocument, clientInfo: { name: string; version?: string }) { 38 | const host = new TextGrammarCheckHost( 39 | knownClients[clientInfo.name] ?? clientInfo, 40 | document, 41 | () => this.getDocumentContext(document), 42 | () => this.getAuth(), 43 | (error) => { 44 | if (error instanceof SocketError) { 45 | if (error.code === SocketErrorCode.UNAUTHORIZED) { 46 | this.auth = null 47 | // @ts-ignore - accessing private property. 48 | host.api.reconnect() 49 | } 50 | } 51 | }, 52 | ) 53 | 54 | return host 55 | } 56 | 57 | private async getAuth(): Promise { 58 | if (!this.auth) { 59 | try { 60 | this.auth = await this.asUser() 61 | } catch (error) { 62 | console.error(error) 63 | } 64 | } 65 | 66 | return this.auth || this.asAnonymous() 67 | } 68 | 69 | private async asAnonymous() { 70 | return (this.auth = await anonymous()) 71 | } 72 | 73 | private async asUser() { 74 | const credentials = await this.getCredentials() 75 | 76 | if (typeof credentials === 'string') { 77 | return JSON.parse(credentials) 78 | } else if (credentials) { 79 | this.auth = await authenticate(credentials.username, credentials.password) 80 | if (this.auth) this.storeToken(JSON.stringify(this.auth)) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/grammarly-language-server/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const CLIENT = Symbol('ClientCapabilities') 2 | export const CLIENT_INFO = Symbol('ClientInfo') 3 | export const SERVER = Symbol('ServerCapabilities') 4 | export const CONNECTION = Symbol('Connection') 5 | -------------------------------------------------------------------------------- /packages/grammarly-language-server/src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare var __DEV__: boolean 2 | -------------------------------------------------------------------------------- /packages/grammarly-language-server/src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { AlertEvent } from '@emacs-grammarly/unofficial-grammarly-api' 2 | import { CodeAction, CodeActionKind, Diagnostic, DiagnosticSeverity, DiagnosticTag, Range } from 'vscode-languageserver' 3 | import { TextDocument } from 'vscode-languageserver-textdocument' 4 | 5 | export function createGrammarlyFix( 6 | document: TextDocument, 7 | alert: AlertEvent, 8 | newText: string, 9 | diagnostics: Diagnostic[] = [], 10 | ): CodeAction { 11 | const range = getRangeInDocument(document, alert.begin, alert.end) 12 | 13 | return { 14 | title: `${alert.todo} -> ${newText}`.replace(/^[a-z]/, (m) => m.toLocaleUpperCase()), 15 | kind: CodeActionKind.QuickFix, 16 | diagnostics, 17 | isPreferred: true, 18 | command: { 19 | command: 'grammarly.postQuickFix', 20 | title: '', 21 | arguments: [document.uri, alert.id, { range, newText }], 22 | }, 23 | edit: { 24 | changes: { 25 | [document.uri]: [ 26 | { 27 | range, 28 | newText, 29 | }, 30 | ], 31 | }, 32 | }, 33 | } 34 | } 35 | 36 | export function createGrammarlySynonymFix( 37 | document: TextDocument, 38 | word: string, 39 | replacement: { base: string; derived: string }, 40 | range: Range, 41 | ): CodeAction { 42 | return { 43 | title: `Synonym: ${word} -> ${replacement.derived}`, 44 | kind: CodeActionKind.QuickFix, 45 | edit: { 46 | changes: { 47 | [document.uri]: [ 48 | { 49 | range, 50 | newText: replacement.derived, 51 | }, 52 | ], 53 | }, 54 | }, 55 | } 56 | } 57 | 58 | export function createAddToDictionaryFix(document: TextDocument, alert: AlertEvent, target: string): CodeAction { 59 | return { 60 | title: `Grammarly: add "${alert.text}" to ${target} dictionary`, 61 | kind: CodeActionKind.QuickFix, 62 | command: { 63 | command: 'grammarly.addWord', 64 | title: `Grammarly: save to ${target} dictionary`, 65 | arguments: [target, document.uri, alert.id, alert.text], 66 | }, 67 | } 68 | } 69 | 70 | export function createIgnoreFix(document: TextDocument, alert: AlertEvent): CodeAction { 71 | return { 72 | title: `Grammarly: ignore issue`, 73 | kind: CodeActionKind.QuickFix, 74 | command: { 75 | command: 'grammarly.ignoreIssue', 76 | title: `Grammarly: ignore`, 77 | arguments: [document.uri, alert.id], 78 | }, 79 | } 80 | } 81 | export function toMarkdown(source: string) { 82 | return source 83 | .replace(/

/gi, (_) => `\n\n`) 84 | .replace(//gi, (_) => ` \n`) 85 | .replace(/Incorrect:/gi, (_) => `- **Incorrect:** `) 86 | .replace(/Correct:/gi, (_) => `- **Correct:** `) 87 | .replace(/\s?(.*?)<\/b>\s?/gi, (_, content) => ` **${content}** `) 88 | .replace(/\s?(.*?)<\/i>\s?/gi, (_, content) => ` _${content}_ `) 89 | .replace(/<\/?[^>]+(>|$)/g, '') 90 | .replace(/\n\n(\n|\s)+/, '\n\n') 91 | .trim() 92 | } 93 | 94 | export function getMarkdownDescription(alert: AlertEvent) { 95 | return alert.explanation || alert.details || alert.examples 96 | ? toMarkdown( 97 | `### ${alert.title}${alert.explanation ? `\n\n${alert.explanation}` : ''}${alert.details ? `\n\n${alert.details}` : '' 98 | }${alert.examples ? `\n\n### Examples\n\n${alert.examples}` : ''}`, 99 | ) 100 | : '' 101 | } 102 | export function isSpellingAlert(alert: AlertEvent) { 103 | return alert.group === 'Spelling' 104 | } 105 | 106 | export function capturePromiseErrors(fn: T, fallback?: unknown): T { 107 | return (async (...args: unknown[]) => { 108 | try { 109 | return await fn(...args) 110 | } catch (error) { 111 | console.error(error) 112 | return fallback 113 | } 114 | }) as any 115 | } 116 | 117 | export function createDiagnostic( 118 | document: TextDocument, 119 | alert: AlertEvent, 120 | severityMap: Record, 121 | ) { 122 | const severity = severityMap[alert.category] || severityMap['_default'] 123 | const diagnostic: Diagnostic = { 124 | severity, 125 | message: (alert.title || '').replace(/<\/?[^>]+(>|$)/g, ''), 126 | source: 'Grammarly: ' + alert.category, 127 | code: alert.id, 128 | range: getRangeInDocument(document, alert.begin, alert.end), 129 | tags: severity === DiagnosticSeverity.Hint ? [DiagnosticTag.Unnecessary] : [], 130 | } 131 | 132 | return diagnostic 133 | } 134 | 135 | export function getRangeInDocument(document: TextDocument, start: number, end: number): Range { 136 | return { 137 | start: document.positionAt(start), 138 | end: document.positionAt(end), 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /packages/grammarly-language-server/src/hosts/CheckHostStatus.ts: -------------------------------------------------------------------------------- 1 | export type CheckHostStatus = 'CHECKING' | 'IDLE' 2 | -------------------------------------------------------------------------------- /packages/grammarly-language-server/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | import { Container } from 'inversify' 3 | import { createConnection, ProposedFeatures } from 'vscode-languageserver' 4 | import { CLIENT, CLIENT_INFO, CONNECTION, SERVER } from './constants' 5 | import { ConfigurationService } from './services/ConfigurationService' 6 | import { DictionaryService } from './services/DictionaryService' 7 | import { DocumentService } from './services/DocumentService' 8 | import { GrammarlyDiagnosticsService } from './services/GrammarlyDiagnosticsService' 9 | 10 | export * from './protocol' 11 | 12 | interface Disposable { 13 | dispose(): void 14 | } 15 | 16 | export function startLanguageServer(): void { 17 | const disposables: Disposable[] = [] 18 | const capabilities: any = {} 19 | const container = new Container({ 20 | autoBindInjectable: true, 21 | defaultScope: 'Singleton', 22 | }) 23 | const connection = createConnection(ProposedFeatures.all) 24 | 25 | container.bind(CONNECTION).toConstantValue(connection) 26 | container.bind(SERVER).toConstantValue(capabilities) 27 | 28 | connection.onInitialize((params) => { 29 | container.bind(CLIENT).toConstantValue(params.capabilities) 30 | container.bind(CLIENT_INFO).toConstantValue(params.clientInfo ?? { name: '' }) 31 | disposables.push( 32 | container.get(ConfigurationService).register(), 33 | container.get(DocumentService).register(), 34 | container.get(DictionaryService).register(), 35 | container.get(GrammarlyDiagnosticsService).register(), 36 | ) 37 | 38 | return { 39 | serverInfo: { 40 | name: 'Grammarly', 41 | }, 42 | capabilities, 43 | } 44 | }) 45 | 46 | connection.onExit(() => { 47 | disposables.forEach((disposable) => disposable.dispose()) 48 | }) 49 | 50 | connection.listen() 51 | } 52 | -------------------------------------------------------------------------------- /packages/grammarly-language-server/src/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface Registerable { 2 | register(): { 3 | dispose(): void 4 | } 5 | } 6 | 7 | export interface AuthParams { 8 | username: string 9 | password: string 10 | } 11 | -------------------------------------------------------------------------------- /packages/grammarly-language-server/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-language-server/src/parsers/index.ts: -------------------------------------------------------------------------------- 1 | import * as markdown from './markdown'; 2 | 3 | interface Parser { 4 | parse(content: string): (interval: [number, number]) => string[]; 5 | } 6 | 7 | export const parsers: Record = { 8 | markdown, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/grammarly-language-server/src/parsers/markdown.ts: -------------------------------------------------------------------------------- 1 | import remark from 'remark'; 2 | import { Node, Parent } from 'unist'; 3 | import IntervalTree from '@flatten-js/interval-tree'; 4 | 5 | const parser = remark(); 6 | 7 | export function parse(content: string) { 8 | const ast = parser.parse(content); 9 | const tree = new IntervalTree(); 10 | 11 | iterate(ast as Parent, node => { 12 | const { type, position } = node; 13 | if (position) { 14 | tree.insert([position.start.offset!, position.end.offset!], type); 15 | } 16 | }); 17 | 18 | return (interval: [number, number]) => tree.search(interval) as string[]; 19 | } 20 | 21 | function iterate(node: Parent, fn: (node: Node) => void) { 22 | const queue: Parent[] = [node]; 23 | 24 | fn(node); 25 | 26 | while (queue.length) { 27 | const node = queue.shift()!; 28 | 29 | node.children.forEach(node => { 30 | fn(node); 31 | 32 | if (Array.isArray(node.children)) { 33 | queue.push(node as Parent); 34 | } 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/grammarly-language-server/src/protocol.ts: -------------------------------------------------------------------------------- 1 | import type { Emotion, Event, IdAlert, OutcomeScoresWithPlagiarism, TextInfoEvent } from '@emacs-grammarly/unofficial-grammarly-api' 2 | import type { CheckHostStatus } from './hosts/CheckHostStatus' 3 | import type { AuthParams } from './interfaces' 4 | 5 | export namespace GrammarlyLanguageServer { 6 | export type DocumentState = 7 | | { 8 | uri: string 9 | status: CheckHostStatus 10 | score: number 11 | scores: Partial 12 | emotions: Emotion[] 13 | textInfo: Omit | null 14 | totalAlertsCount: number 15 | additionalFixableAlertsCount: number 16 | premiumAlertsCount: number 17 | user: { isAnonymous: boolean; isPremium: boolean, username: string } 18 | } 19 | | { 20 | uri: string 21 | } 22 | 23 | export interface DocumentRef { 24 | uri: string 25 | } 26 | 27 | export interface FeedbackAcceptAlert extends DocumentRef { 28 | id: IdAlert 29 | text: string 30 | } 31 | 32 | export interface FeedbackDismissAlert extends DocumentRef { 33 | id: IdAlert 34 | } 35 | export interface FeedbackAddToDictionary extends DocumentRef { 36 | id: IdAlert 37 | } 38 | 39 | export const Feature = { 40 | stop: ('$/stop' as unknown) as import('vscode-jsonrpc').RequestType, 41 | checkGrammar: ('$/checkGrammar' as unknown) as import('vscode-jsonrpc').RequestType, 42 | acceptAlert: ('$/feedbackAcceptAlert' as unknown) as import('vscode-jsonrpc').RequestType< 43 | FeedbackAcceptAlert, 44 | void, 45 | Error 46 | >, 47 | dismissAlert: ('$/feedbackDismissAlert' as unknown) as import('vscode-jsonrpc').RequestType< 48 | FeedbackDismissAlert, 49 | void, 50 | Error 51 | >, 52 | addToDictionary: ('$/feedbackAddToDictionary' as unknown) as import('vscode-jsonrpc').RequestType< 53 | FeedbackAddToDictionary, 54 | void, 55 | Error 56 | >, 57 | getDocumentState: ('$/getDocumentState' as unknown) as import('vscode-jsonrpc').RequestType< 58 | DocumentRef, 59 | DocumentState | null, 60 | Error 61 | >, 62 | } 63 | 64 | export namespace Client { 65 | export const Feature = { 66 | getCredentials: ('$/getCredentials' as unknown) as import('vscode-jsonrpc').RequestType0, 67 | getToken: ('$/getToken' as unknown) as import('vscode-jsonrpc').RequestType0<{ token: string } | null, Error>, 68 | storeToken: ('$/storeToken' as unknown) as import('vscode-jsonrpc').RequestType<{ token: string }, void, Error>, 69 | showError: ('$/showError' as unknown) as import('vscode-jsonrpc').RequestType<{ message: string, buttons: string[] }, string | null, Error>, 70 | updateDocumentState: ('$/updateDocumentState' as unknown) as import('vscode-jsonrpc').RequestType< 71 | DocumentState, 72 | void, 73 | Error 74 | >, 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/grammarly-language-server/src/services/ConfigurationService.ts: -------------------------------------------------------------------------------- 1 | import minimatch from 'minimatch' 2 | import { inject, injectable } from 'inversify' 3 | import { DocumentContext } from '@emacs-grammarly/unofficial-grammarly-api' 4 | import { Connection, DiagnosticSeverity, Disposable } from 'vscode-languageserver' 5 | import { CONNECTION } from '../constants' 6 | import { Registerable } from '../interfaces' 7 | import { DEFAULT, GrammarlySettings } from '../settings' 8 | import { toArray } from './toArray' 9 | 10 | @injectable() 11 | export class ConfigurationService implements Registerable { 12 | private default: GrammarlySettings = DEFAULT 13 | private user = this.default 14 | 15 | private perDocumentSettings = new Map() 16 | private wip = new Map>() 17 | 18 | constructor(@inject(CONNECTION) private readonly connection: Connection) { } 19 | 20 | public get settings(): Readonly { 21 | return this.user 22 | } 23 | 24 | register() { 25 | this.connection.onDidChangeConfiguration(({ settings }) => { 26 | if ('grammarly' in settings) { 27 | this.user = { 28 | ...this.default, 29 | ...settings.grammarly, 30 | } 31 | this.perDocumentSettings.clear() 32 | this.wip.clear() 33 | } 34 | }) 35 | 36 | setTimeout(() => { 37 | this.connection.workspace.getConfiguration('grammarly').then((settings) => { 38 | this.user = { 39 | ...this.default, 40 | ...settings, 41 | } 42 | }) 43 | }, 0) 44 | 45 | return Disposable.create(() => { 46 | this.wip.clear() 47 | this.perDocumentSettings.clear() 48 | }) 49 | } 50 | 51 | async getAlertSeverity(uri: string): Promise> { 52 | const config = await this.connection.workspace.getConfiguration({ 53 | scopeUri: uri, 54 | section: 'grammarly', 55 | }) 56 | 57 | return { 58 | ...this.default.severity, 59 | ...config?.severity, 60 | } 61 | } 62 | 63 | async getIgnoredTags(uri: string, languageId: string): Promise { 64 | const config: GrammarlySettings = await this.connection.workspace.getConfiguration({ 65 | scopeUri: uri, 66 | section: 'grammarly', 67 | }) 68 | 69 | return config.diagnostics[`[${languageId}]`]?.ignore || [] 70 | } 71 | 72 | getDocumentSettings(uri: string, fresh = false) { 73 | if (this.perDocumentSettings.has(uri) && fresh === false) { 74 | return this.perDocumentSettings.get(uri)! 75 | } 76 | 77 | if (this.wip.has(uri)) { 78 | return this.wip.get(uri)! 79 | } 80 | 81 | const get = async () => { 82 | const config: GrammarlySettings = { 83 | ...this.user, 84 | ...(await this.connection.workspace.getConfiguration({ 85 | scopeUri: uri, 86 | section: 'grammarly', 87 | })), 88 | } 89 | 90 | const override = config.overrides.find((override) => 91 | toArray(override.files).some((pattern) => minimatch(uri, pattern)), 92 | ) 93 | 94 | const settings: DocumentContext = { 95 | audience: config.audience, 96 | dialect: config.dialect, 97 | domain: config.domain, 98 | emotions: config.emotions, 99 | goals: config.goals, 100 | style: config.style, 101 | ...override?.config, 102 | } 103 | 104 | this.perDocumentSettings.set(uri, settings) 105 | 106 | this.wip.delete(uri) 107 | 108 | return settings 109 | } 110 | 111 | const promise = get() 112 | 113 | this.wip.set(uri, promise) 114 | 115 | return promise 116 | } 117 | 118 | isIgnoredDocument(uri: string) { 119 | return toArray(this.user.ignore).some((pattern) => minimatch(uri, pattern)) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /packages/grammarly-language-server/src/services/DictionaryService.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | import { Disposable } from 'vscode-languageserver'; 3 | import { Registerable } from '../interfaces'; 4 | import { ConfigurationService } from './ConfigurationService'; 5 | 6 | @injectable() 7 | export class DictionaryService implements Registerable { 8 | constructor(private readonly configuration: ConfigurationService) {} 9 | 10 | register() { 11 | return Disposable.create(() => {}); 12 | } 13 | 14 | isKnownWord(word: string) { 15 | const words = this.configuration.settings.userWords; 16 | 17 | return words.includes(word) || words.includes(word.toLocaleLowerCase()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/grammarly-language-server/src/services/DocumentService.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from 'inversify' 2 | import { 3 | ClientCapabilities, 4 | Connection, 5 | Disposable, 6 | ServerCapabilities, 7 | TextDocuments, 8 | TextDocumentSyncKind, 9 | } from 'vscode-languageserver' 10 | import { CLIENT_INFO, CONNECTION, SERVER } from '../constants' 11 | import { GrammarlyDocument } from '../GrammarlyDocument' 12 | import { GrammarlyHostFactory } from '../GrammarlyHostFactory' 13 | import { Registerable } from '../interfaces' 14 | import { ConfigurationService } from './ConfigurationService' 15 | import { GrammarlyLanguageServer } from '../protocol' 16 | import { DevLogger } from '../DevLogger' 17 | 18 | @injectable() 19 | export class DocumentService implements Registerable { 20 | private LOGGER = new DevLogger(DocumentService.name) 21 | private documents = new TextDocuments(GrammarlyDocument) 22 | private hostFactory = new GrammarlyHostFactory( 23 | async (document) => this.configuration.getDocumentSettings(document.uri), 24 | async () => this.getCredentials(), 25 | async (token) => this.connection.sendRequest(GrammarlyLanguageServer.Client.Feature.storeToken, { token }), 26 | ) 27 | 28 | private onDocumentOpenCbs: Array<(document: GrammarlyDocument) => any> = [] 29 | private onDocumentCloseCbs: Array<(document: GrammarlyDocument) => any> = [] 30 | 31 | constructor( 32 | @inject(CONNECTION) private readonly connection: Connection, 33 | @inject(SERVER) private readonly capabilities: ServerCapabilities, 34 | @inject(CLIENT_INFO) private readonly client: { name: string; version?: string }, 35 | private readonly configuration: ConfigurationService, 36 | ) { } 37 | 38 | register() { 39 | this.capabilities.textDocumentSync = { 40 | openClose: true, 41 | change: TextDocumentSyncKind.Incremental, 42 | } 43 | 44 | this.documents.listen(this.connection) 45 | 46 | const disposables = [ 47 | this.documents.onDidOpen(({ document }) => this.attachHost(document)), 48 | this.documents.onDidClose(({ document }) => this.handleClose(document)), 49 | Disposable.create(() => this.documents.all().forEach((document) => document.detachHost())), 50 | ] 51 | 52 | return Disposable.create(() => disposables.forEach((disposable) => disposable.dispose())) 53 | } 54 | 55 | get(uri: string) { 56 | return this.documents.get(uri) 57 | } 58 | 59 | onDidOpen(fn: (document: GrammarlyDocument) => any) { 60 | this.onDocumentOpenCbs.push(fn) 61 | } 62 | 63 | onDidClose(fn: (document: GrammarlyDocument) => any) { 64 | this.onDocumentCloseCbs.push(fn) 65 | } 66 | 67 | async attachHost(document: GrammarlyDocument, force = false) { 68 | if (!this.configuration.settings.autoActivate && !force) return 69 | 70 | document.attachHost(this.hostFactory, this.client) 71 | this.onDocumentOpenCbs.forEach((cb) => cb(document)) 72 | } 73 | 74 | private async getCredentials() { 75 | try { 76 | const result = await this.connection.sendRequest(GrammarlyLanguageServer.Client.Feature.getToken) 77 | if (result) return JSON.stringify(result) 78 | } catch (error) { 79 | console.error(error) 80 | } 81 | 82 | return this.connection.sendRequest(GrammarlyLanguageServer.Client.Feature.getCredentials) 83 | } 84 | 85 | private async handleClose(document: GrammarlyDocument) { 86 | this.onDocumentCloseCbs.forEach((cb) => cb(document)) 87 | document.detachHost() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/grammarly-language-server/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-language-server/src/settings.ts: -------------------------------------------------------------------------------- 1 | import { DocumentContext } from '@emacs-grammarly/unofficial-grammarly-api' 2 | import { DiagnosticSeverity } from 'vscode-languageserver' 3 | 4 | export interface GrammarlySettings extends DocumentContext { 5 | /** Extension Config */ 6 | autoActivate: boolean 7 | ignore: string[] 8 | userWords: string[] 9 | diagnostics: Record< 10 | string, 11 | { 12 | ignore: string[] 13 | } 14 | > 15 | severity: Record 16 | 17 | /** Grammarly Document Config */ 18 | overrides: Array<{ 19 | files: string[] 20 | config: Partial 21 | }> 22 | 23 | debug: boolean 24 | showUsernameInStatusBar: boolean 25 | showDeletedTextInQuickFix: boolean 26 | showExplanation: boolean 27 | showExamples: boolean 28 | hideUnavailablePremiumAlerts: boolean 29 | } 30 | 31 | export const DEFAULT: GrammarlySettings = { 32 | /** Extension Config */ 33 | autoActivate: true, 34 | ignore: [], 35 | severity: { 36 | Determiners: DiagnosticSeverity.Error, 37 | Misspelled: DiagnosticSeverity.Error, 38 | Unknown: DiagnosticSeverity.Error, 39 | ClosingPunct: DiagnosticSeverity.Error, 40 | Nouns: DiagnosticSeverity.Error, 41 | 42 | OddWords: DiagnosticSeverity.Warning, 43 | CompPunct: DiagnosticSeverity.Warning, 44 | Clarity: DiagnosticSeverity.Warning, 45 | Dialects: DiagnosticSeverity.Warning, 46 | 47 | WordChoice: DiagnosticSeverity.Information, 48 | Readability: DiagnosticSeverity.Information, 49 | 50 | PassiveVoice: DiagnosticSeverity.Hint, 51 | 52 | _default: DiagnosticSeverity.Hint, 53 | }, 54 | userWords: [], 55 | diagnostics: { 56 | '[markdown]': { 57 | ignore: ['code'], 58 | }, 59 | '[mdx]': { 60 | ignore: ['code'], 61 | }, 62 | }, 63 | 64 | /** Grammarly Config */ 65 | audience: 'knowledgeable', 66 | dialect: 'american', 67 | domain: 'general', 68 | emotions: [], 69 | goals: [], 70 | style: 'neutral', 71 | 72 | /** Grammarly Document Config */ 73 | overrides: [], 74 | 75 | /** Internal */ 76 | debug: false, 77 | showUsernameInStatusBar: true, 78 | showDeletedTextInQuickFix: true, 79 | showExplanation: true, 80 | showExamples: false, 81 | hideUnavailablePremiumAlerts: false, 82 | } 83 | -------------------------------------------------------------------------------- /packages/grammarly-language-server/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-language-server/src/watch.ts: -------------------------------------------------------------------------------- 1 | import { Ref, effect, stop, isRef } from '@vue/reactivity'; 2 | 3 | const INITIAL_WATCHER_VALUE = {}; 4 | 5 | export function watch(ref: Ref, cb: (newValue: T, oldValue: T | undefined) => void) { 6 | const getter = () => traverse(ref.value) as T; 7 | 8 | let oldValue: T = INITIAL_WATCHER_VALUE as any; 9 | const job = () => { 10 | const newValue = runner(); 11 | try { 12 | cb(newValue, oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue); 13 | } catch (error) { 14 | console.error(error); 15 | } 16 | 17 | oldValue = newValue; 18 | }; 19 | const runner = effect(getter, { 20 | lazy: true, 21 | scheduler: job, 22 | }); 23 | 24 | job(); 25 | 26 | return () => { 27 | stop(runner); 28 | }; 29 | } 30 | 31 | export function watchEffect(cb: () => void) { 32 | const runner = effect(cb, { lazy: true }); 33 | 34 | runner(); 35 | 36 | return () => { 37 | stop(runner); 38 | }; 39 | } 40 | 41 | function traverse(value: unknown, seen: Set = new Set()) { 42 | if (!isObject(value) || seen.has(value)) { 43 | return value; 44 | } 45 | seen.add(value); 46 | if (isRef(value)) { 47 | traverse(value.value, seen); 48 | } else if (Array.isArray(value)) { 49 | for (let i = 0; i < value.length; i++) { 50 | traverse(value[i], seen); 51 | } 52 | } else if (value instanceof Map) { 53 | value.forEach((_, key) => { 54 | // to register mutation dep for existing keys 55 | traverse(value.get(key), seen); 56 | }); 57 | } else if (value instanceof Set) { 58 | value.forEach((v) => { 59 | traverse(v, seen); 60 | }); 61 | } else { 62 | for (const key in value) { 63 | traverse((value as any)[key], seen); 64 | } 65 | } 66 | return value; 67 | } 68 | 69 | function isObject(value: unknown): value is object { 70 | return typeof value === 'object' && value !== null; 71 | } 72 | -------------------------------------------------------------------------------- /packages/grammarly-language-server/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 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - './extension/' 3 | - './packages/*' 4 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import json from '@rollup/plugin-json' 2 | import nodeResolve from '@rollup/plugin-node-resolve' 3 | import replace from '@rollup/plugin-replace' 4 | import typescript from '@rollup/plugin-typescript' 5 | import size from 'rollup-plugin-filesize' 6 | import dts from 'rollup-plugin-dts' 7 | import Path from 'path' 8 | 9 | function deps(fileName) { 10 | return Array.from(Object.keys(require(fileName).dependencies || {})) 11 | } 12 | 13 | function abs(fileName) { 14 | return Path.resolve(__dirname, fileName) 15 | } 16 | 17 | /** @type {import('rollup').RollupOptions[]} */ 18 | const configs = [ 19 | { 20 | input: './packages/grammarly-api/src/index.ts', 21 | output: { 22 | format: 'esm', 23 | file: './packages/grammarly-api/dist/index.d.ts', 24 | }, 25 | plugins: [dts()], 26 | }, 27 | { 28 | input: './packages/grammarly-language-server/src/index.ts', 29 | output: { 30 | format: 'esm', 31 | file: './packages/grammarly-language-server/dist/index.d.ts', 32 | }, 33 | plugins: [dts()], 34 | }, 35 | { 36 | input: './packages/grammarly-language-client/src/index.ts', 37 | output: { 38 | format: 'esm', 39 | file: './packages/grammarly-language-client/dist/index.d.ts', 40 | }, 41 | plugins: [dts()], 42 | }, 43 | { 44 | input: './packages/grammarly-api/src/index.ts', 45 | output: [ 46 | { 47 | format: 'cjs', 48 | file: './packages/grammarly-api/dist/index.cjs.js', 49 | sourcemap: true, 50 | }, 51 | { 52 | format: 'esm', 53 | file: './packages/grammarly-api/dist/index.esm.js', 54 | sourcemap: true, 55 | }, 56 | ], 57 | plugins: [ 58 | replace({ 59 | values: { 60 | __DEV__: `process.env.NODE_ENV !== 'production'`, 61 | }, 62 | }), 63 | nodeResolve(), 64 | json(), 65 | typescript({ tsconfig: abs('./packages/grammarly-api/tsconfig.json') }), 66 | size(), 67 | ], 68 | external: [...deps('./packages/grammarly-api/package.json'), 'ws', 'util'], 69 | }, 70 | { 71 | input: './packages/grammarly-language-server/src/index.ts', 72 | output: [ 73 | { 74 | format: 'cjs', 75 | file: './packages/grammarly-language-server/dist/index.cjs.js', 76 | sourcemap: true, 77 | }, 78 | { 79 | format: 'esm', 80 | file: './packages/grammarly-language-server/dist/index.esm.js', 81 | sourcemap: true, 82 | }, 83 | ], 84 | plugins: [ 85 | replace({ 86 | values: { 87 | __DEV__: `process.env.NODE_ENV !== 'production'`, 88 | }, 89 | }), 90 | // nodeResolve(), 91 | json(), 92 | typescript({ tsconfig: abs('./packages/grammarly-language-server/tsconfig.json') }), 93 | size(), 94 | ], 95 | external: [...deps('./packages/grammarly-language-server/package.json'), 'crypto', 'util', 'events'], 96 | }, 97 | { 98 | input: './packages/grammarly-language-client/src/index.ts', 99 | output: [ 100 | { 101 | format: 'cjs', 102 | file: './packages/grammarly-language-client/dist/index.cjs.js', 103 | sourcemap: true, 104 | }, 105 | { 106 | format: 'esm', 107 | file: './packages/grammarly-language-client/dist/index.esm.js', 108 | sourcemap: true, 109 | }, 110 | ], 111 | plugins: [ 112 | replace({ 113 | values: { 114 | __DEV__: `process.env.NODE_ENV !== 'production'`, 115 | }, 116 | }), 117 | // nodeResolve(), 118 | json(), 119 | typescript({ tsconfig: abs('./packages/grammarly-language-client/tsconfig.json') }), 120 | size(), 121 | ], 122 | external: [...deps('./packages/grammarly-language-client/package.json')], 123 | }, 124 | ] 125 | 126 | export default configs 127 | -------------------------------------------------------------------------------- /scripts/runTests.js: -------------------------------------------------------------------------------- 1 | const Path = require('path'); 2 | const { runTests } = require('vscode-test'); 3 | 4 | process.env.VSCODE_CLI = '1'; 5 | 6 | async function main() { 7 | try { 8 | // The folder containing the Extension Manifest package.json 9 | // Passed to `--extensionDevelopmentPath` 10 | const extensionDevelopmentPath = Path.resolve(__dirname, '..'); 11 | 12 | // The path to the extension test runner script 13 | // Passed to --extensionTestsPath 14 | const extensionTestsPath = Path.resolve(__dirname, '../out-test/runner'); 15 | 16 | // Download VS Code, unzip it and run the integration test 17 | await runTests({ 18 | version: '1.41.0', 19 | extensionDevelopmentPath, 20 | extensionTestsPath, 21 | launchArgs: [ 22 | '--disable-extensions', 23 | '--user-data-dir=' + Path.resolve(__dirname, '../fixtures/user'), 24 | Path.resolve(__dirname, '../fixtures/folder'), 25 | ], 26 | extensionTestsEnv: { 27 | EXTENSION_TEST_MODE: true, 28 | }, 29 | }); 30 | } catch { 31 | console.error('Failed to run tests'); 32 | process.exit(1); 33 | } 34 | } 35 | 36 | main(); 37 | -------------------------------------------------------------------------------- /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-language-server\bin\server.js" %* 13 | ENDLOCAL 14 | EXIT /b %errorlevel% 15 | :find_dp0 16 | SET dp0=%~dp0 17 | EXIT /b 18 | --------------------------------------------------------------------------------