├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── blank.yml ├── .gitignore ├── .npmignore ├── .vim └── coc-settings.json ├── README.md ├── package-lock.json ├── package.json ├── src ├── __vim_project_root ├── commands │ ├── dev │ │ └── index.ts │ ├── global.ts │ ├── index.ts │ ├── lsp.ts │ └── super.ts ├── index.ts ├── lib │ ├── notification │ │ ├── floatwindow.ts │ │ ├── index.ts │ │ └── message.ts │ ├── sdk.ts │ └── status.ts ├── provider │ ├── hotreload.ts │ ├── index.ts │ └── pub.ts ├── server │ ├── dev │ │ └── index.ts │ ├── devtools │ │ └── index.ts │ └── lsp │ │ ├── closingLabels.ts │ │ ├── codeActionProvider.ts │ │ ├── completionProvider.ts │ │ ├── extractProvider.ts │ │ ├── index.ts │ │ ├── outline.ts │ │ ├── resolveCompleteItem.ts │ │ └── signatureHelp.ts ├── sources │ ├── devices.ts │ ├── emulators.ts │ ├── index.ts │ └── sdks.ts └── util │ ├── constant.ts │ ├── dispose.ts │ ├── fs.ts │ ├── index.ts │ ├── logger.ts │ └── opener.ts ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 3 | extends: [ 4 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 5 | 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 6 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 7 | ], 8 | parserOptions: { 9 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 10 | sourceType: 'module', // Allows for the use of imports 11 | }, 12 | rules: { 13 | "@typescript-eslint/explicit-function-return-type": "off", 14 | "@typescript-eslint/no-explicit-any": "off", 15 | "@typescript-eslint/no-non-null-assertion": "off", 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | 13 | **Describe the bug** 14 | A clear and concise description of what the bug is. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Desktop (please complete the following information):** 30 | - OS: 31 | - Vim or Neovim: 32 | - (Neo)vim version: 33 | 34 | **Output channel:** 35 | 36 | 1. Set `"flutter.trace.server": "verbose"` 37 | 2. Reproduce the issue 38 | 3. `:CocList output` open output list and select `flutter` 39 | 40 | Log: 41 | 42 | ``` 43 | ``` 44 | -------------------------------------------------------------------------------- /.github/workflows/blank.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v1 18 | - name: Setup Node 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: '10.x' 22 | - name: Lint codes 23 | run: | 24 | npm install 25 | npm run lint 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # create by https://github.com/iamcco/coc-gitignore (Sun Oct 06 2019 19:53:02 GMT+0800 (GMT+08:00)) 2 | # Dart.gitignore: 3 | # See https://www.dartlang.org/guides/libraries/private-files 4 | 5 | # Files and directories created by pub 6 | .dart_tool/ 7 | .packages 8 | build/ 9 | # If you're building an application, you may want to check-in your pubspec.lock 10 | pubspec.lock 11 | 12 | # Directory created by dartdoc 13 | # If you don't generate documentation locally you can remove this line. 14 | doc/api/ 15 | 16 | # Avoid committing generated Javascript files: 17 | *.dart.js 18 | *.info.json # Produced by the --dump-info flag. 19 | *.js # When generated by dart2js. Don't specify *.js if your 20 | # project includes source files written in JavaScript. 21 | *.js_ 22 | *.js.deps 23 | *.js.map 24 | 25 | # create by https://github.com/iamcco/coc-gitignore (Sun Oct 06 2019 19:54:11 GMT+0800 (GMT+08:00)) 26 | # Node.gitignore: 27 | # Logs 28 | logs 29 | *.log 30 | npm-debug.log* 31 | yarn-debug.log* 32 | yarn-error.log* 33 | lerna-debug.log* 34 | 35 | # Diagnostic reports (https://nodejs.org/api/report.html) 36 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 37 | 38 | # Runtime data 39 | pids 40 | *.pid 41 | *.seed 42 | *.pid.lock 43 | 44 | # Directory for instrumented libs generated by jscoverage/JSCover 45 | lib-cov 46 | 47 | # Coverage directory used by tools like istanbul 48 | coverage 49 | *.lcov 50 | 51 | # nyc test coverage 52 | .nyc_output 53 | 54 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 55 | .grunt 56 | 57 | # Bower dependency directory (https://bower.io/) 58 | bower_components 59 | 60 | # node-waf configuration 61 | .lock-wscript 62 | 63 | # Compiled binary addons (https://nodejs.org/api/addons.html) 64 | build/Release 65 | 66 | # Dependency directories 67 | node_modules/ 68 | jspm_packages/ 69 | 70 | # TypeScript v1 declaration files 71 | typings/ 72 | 73 | # TypeScript cache 74 | *.tsbuildinfo 75 | 76 | # Optional npm cache directory 77 | .npm 78 | 79 | # Optional eslint cache 80 | .eslintcache 81 | 82 | # Optional REPL history 83 | .node_repl_history 84 | 85 | # Output of 'npm pack' 86 | *.tgz 87 | 88 | # Yarn Integrity file 89 | .yarn-integrity 90 | 91 | # dotenv environment variables file 92 | .env 93 | .env.test 94 | 95 | # parcel-bundler cache (https://parceljs.org/) 96 | .cache 97 | 98 | # next.js build output 99 | .next 100 | 101 | # nuxt.js build output 102 | .nuxt 103 | 104 | # vuepress build output 105 | .vuepress/dist 106 | 107 | # Serverless directories 108 | .serverless/ 109 | 110 | # FuseBox cache 111 | .fusebox/ 112 | 113 | # DynamoDB Local files 114 | .dynamodb/ 115 | 116 | # Node-patch: 117 | out/ 118 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | node_modules 3 | tsconfig.json 4 | *.map 5 | .tags 6 | .DS_Store 7 | webpack.config.js 8 | yarn.lock 9 | yarn-error.log 10 | .editorconfig 11 | -------------------------------------------------------------------------------- /.vim/coc-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Unimported", 4 | "getlog", 5 | "outchannel", 6 | "pubspec", 7 | "regist" 8 | ] 9 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![](https://flutter.dev/images/favicon.png) coc-flutter-tools 2 | 3 | Flutter support for (Neo)vim 4 | 5 | ![2019-10-07 23-31-40 2019-10-08 00_04_07](https://user-images.githubusercontent.com/5492542/66328510-58a6c480-e95f-11e9-95ca-0b4ed7c8e83f.gif) 6 | 7 | ## What is this? 8 | `coc-flutter-tools` is an active fork of [coc-flutter](https://github.com/iamcco/coc-flutter), that fixes bugs and adds new features. 9 | 10 | Big thanks to the author of `coc-flutter` [@iamcco](https://github.com/iamcco) 11 | 12 | Features of this fork: 13 | - DevTools support 14 | - Better DevLog Notifications (Notification Filtering) 15 | - UI Path Display 16 | - Flutter Outline Panel 17 | 18 | ![Flutter Outline](https://user-images.githubusercontent.com/8187501/90091803-eca0a400-dd59-11ea-9bee-2401c85ddff9.gif) 19 | 20 | 21 | ## Features 22 | 23 | > Make sure that you have the Flutter SDK path in the `PATH` environment variable 24 | - LSP features (power by [analysis_server](https://github.com/dart-lang/sdk/blob/master/pkg/analysis_server/tool/lsp_spec/README.md)) 25 | - Auto-completion 26 | - Diagnostics 27 | - Auto-formatting 28 | - Renaming elements 29 | - Hovering support 30 | - Signature help 31 | - Jumping to definitions/implementations/references 32 | - Highlighting 33 | - Widget trees (flutter outline) 34 | - Document symbols 35 | - Code actions 36 | - [More detail](https://github.com/dart-lang/sdk/blob/master/pkg/analysis_server/tool/lsp_spec/README.md) 37 | - Automatic hot reloading on save 38 | - Automatically run `flutter pub get` on `pubspec.yaml` changes 39 | - Flutter Dev Server support 40 | - Snippets (enable with `flutter.provider.enableSnippet`) 41 | - Device/Emulator List 42 | - SDK switching 43 | 44 | ## Installation 45 | 46 | `:CocInstall coc-flutter-tools` 47 | 48 | > **NOTE**: The [dart-vim-plugin](https://github.com/dart-lang/dart-vim-plugin) plugin is recommended (for filetype detection and syntax highlighting) 49 | 50 | Most likely the extension will find your SDK automatically as long as the `flutter` command maps to an SDK location on your system. 51 | 52 | If you are using a version manager like `asdf` that maps the `flutter` command to another binary instead of an SDK location or this extension cannot find your SDK for another reason you'll have to provide the extension with how to find your SDK. 53 | To do this there are a few options: 54 | 1. If your version manager supports a `which` command like then you can set the `flutter.sdk.flutter-lookup` config option. Eg. `"flutter.sdk.flutter-lookup": "asdf which flutter"`. 55 | 2. You can add the path where to find the SDK to the `flutter.sdk.searchPaths` config option. 56 | Either specify the exact folder the SDK is installed in or a folder that contains other folders which directly have an SDK in them. *Note that not all of these folders need to have an SDK, if they don't contain one they will simply be ignored* 57 | 3. Set the `flutter.sdk.path` config option to the exact path you want to use for your SDK. 58 | If you have also set the `flutter.sdk.searchPaths` then you can use the `FlutterSdks` list (see below) to see what versions you have installed and set the config option for you. **Note that this means that the `flutter.sdk.path` option will be overriden by this list** 59 | 60 | 61 | ## coc-list sources 62 | 63 | - FlutterSdks 64 | > `:CocList FlutterSdks` 65 | Shows all the sdks that can be found by using the `searchPaths` config and the `flutter-lookup` config options and allows you to switch between them, either only for your current workspace or globally. 66 | Besides those two ways to find sdks it also checks if you are using fvm and if so uses those directories to find your sdk. 67 | *You can disable this using the `flutter.fvm.enabled` config option.* 68 | 69 | You can also use this list to see what your current sdk is since it will have `(current)` behind it clearly. 70 | 71 | - Flutter Devices: `:CocList FlutterDevices` 72 | - Flutter Emulators: `:CocList FlutterEmulators` 73 | 74 | ## Settings 75 | 76 | - `flutter.enabled` Enables `coc-flutter-tools`, default: `true` 77 | - `flutter.lsp.initialization.onlyAnalyzeProjectsWithOpenFiles`: default: `true` 78 | > When set to true, analysis will only be performed for projects that have open files rather than the root workspace folder. 79 | - `flutter.lsp.initialization.suggestFromUnimportedLibraries`: default: `true` 80 | > When set to false, completion will not include synbols that are not already imported into the current file 81 | - [`flutter.lsp.initialization.closingLabels`](#closing-labels): default: `true` 82 | > When set to true, will display closing labels at end of closing, only neovim support. 83 | - [`flutter.UIPath`](#ui-path): default: `true` 84 | > When set to true, will display the path of the selected UI component on the status bar 85 | - `flutter.outlineWidth` controls the default width of the flutter outline panel. default: `30` 86 | - `flutter.outlineIconPadding` controls The number of spaces between the icon and the item text in the outline panel. default: `0` 87 | - `flutter.sdk.searchPaths` the paths to search for flutter sdks, either directories where flutter is installed or directories which contain directories where flutter versions have been installed 88 | eg. `/path/to/flutter` (command at `/path/to/flutter/bin/flutter`) or 89 | `~/flutter_versions` (command at `~/flutter_versions/version/bin/flutter`). 90 | - `flutter.sdk.dart-command` dart command, leave empty should just work, default: `''` 91 | - `flutter.sdk.dart-lookup` **only use this if you don't have a flutter installation but only dart** command to find dart executable location, used to infer dart-sdk location, default: `''` 92 | - `flutter.sdk.flutter-lookup` command to find flutter executable location, used to infer location of dart-sdk in flutter cache: `''` 93 | - `flutter.provider.hot-reload` Enable hot reload after save, default: `true` 94 | > only when there are no errors for the save file 95 | - `flutter.provider.enableSnippet` Enable completion item snippet, default: true 96 | - `import '';` => `import '${1}';${0}` 97 | - `someName(…)` => `someName(${1})${0}` 98 | - `setState(() {});` => `setState(() {\n\t${1}\n});${0}` 99 | - `flutter.openDevLogSplitCommand` Vim command to open dev log window, like: `botright 10split`, default: '' 100 | - `flutter.workspaceFolder.ignore` Path start within the list will not treat as workspaceFolder, default: [] 101 | - also flutter sdk will not treat as workspaceFolder, more detail issues [50](https://github.com/iamcco/coc-flutter/issues/50) 102 | - `flutter.runDevToolsAtStartup` Automatically open devtools debugger web page when a project is run, default: false 103 | - `flutter.autoOpenDevLog` Automatically open the dev log after calling flutter run, default: false 104 | - `flutter.autoHideDevLog` Automatically hide the dev log when the app stops running, default: false 105 | 106 | 107 | **Enable format on save**: 108 | 109 | If you have [dart-vim-plugin](https://github.com/dart-lang/dart-vim-plugin) install, put this in your vimrc: 110 | ```vim 111 | let g:dart_format_on_save = 1 112 | ``` 113 | 114 | Alternatively, you may use coc-settings. 115 | 116 | ```jsonc 117 | "coc.preferences.formatOnSaveFiletypes": [ 118 | "dart" 119 | ], 120 | ``` 121 | 122 | ## Code Actions 123 | 124 | `coc.nvim` provides code actions. To enable them, add the following configuration in your vimrc 125 | > this can also be found in `coc.nvim` README 126 | 127 | ``` vim 128 | xmap a (coc-codeaction-selected) 129 | nmap a (coc-codeaction-selected) 130 | ``` 131 | 132 | To show code actions on selected areas: 133 | 134 | - `aap` for current paragraph, `aw` for the current word 135 | 136 | Then you will see a list of code actions: 137 | 138 | - Wrap with Widget 139 | - Wrap with Center 140 | - etc 141 | 142 | ## Commands 143 | 144 | Get a list of flutter related commands: `CocList --input=flutter commands` 145 | 146 | **Global Commands**: 147 | 148 | - `flutter.run` Run flutter dev server 149 | - `flutter.attach` Attach running application 150 | - `flutter.create` Create flutter project using: `flutter create` 151 | - `flutter.doctor` Run: `flutter doctor` 152 | - `flutter.upgrade` Run: `flutter upgrade` 153 | - `flutter.pub.get` Run: `flutter pub get` 154 | - `flutter.devices` open devices list 155 | - `flutter.emulators` open emulators list 156 | - `flutter.outline` opens up an instance of the flutter outline side-panel 157 | - `flutter.toggleOutline` toggles the flutter outline side-panel 158 | 159 | **LSP Commands** 160 | 161 | - `flutter.gotoSuper` jump to the location of the super definition of the class or method 162 | 163 | **Dev Server Commands**: 164 | 165 | > These commands will only be available when the Flutter Dev Server is running (i.e after you run `flutter run`) 166 | 167 | - `flutter.dev.quit` Quit server 168 | - `flutter.dev.detach` Detach server 169 | - `flutter.dev.hotReload` Hot reload 170 | - `flutter.dev.hotRestart` Hot restart 171 | - `flutter.dev.screenshot` To save a screenshot to flutter.png 172 | - `flutter.dev.openDevLog` Open flutter dev server log 173 | - `flutter.dev.clearDevLog` Clear the flutter dev server log 174 | - `flutter.dev.debugDumpAPP` You can dump the widget hierarchy of the app (debugDumpApp) 175 | - `flutter.dev.elevationChecker` To toggle the elevation checker 176 | - `flutter.dev.debugDumpLayerTree` For layers (debugDumpLayerTree) 177 | - `flutter.dev.debugDumpRenderTree` To dump the rendering tree of the app (debugDumpRenderTree) 178 | - `flutter.dev.openDevToolsProfiler` Open the [DevTools](https://flutter.dev/docs/development/tools/devtools/overview) in the browser 179 | - `flutter.dev.debugPaintSizeEnabled` To toggle the display of construction lines (debugPaintSizeEnabled) 180 | - `flutter.dev.defaultTargetPlatform` To simulate different operating systems, (defaultTargetPlatform) 181 | - `flutter.dev.showPerformanceOverlay` To display the performance overlay (WidgetsApp.showPerformanceOverlay) 182 | - `flutter.dev.debugProfileWidgetBuilds` To enable timeline events for all widget build methods, (debugProfileWidgetBuilds) 183 | - `flutter.dev.showWidgetInspectorOverride` To toggle the widget inspector (WidgetsApp.showWidgetInspectorOverride) 184 | - `flutter.dev.debugDumpSemanticsHitTestOrder` Accessibility (debugDumpSemantics) for inverse hit test order 185 | - `flutter.dev.debugDumpSemanticsTraversalOrder` Accessibility (debugDumpSemantics) for traversal order 186 | 187 | ### Closing Labels 188 | 189 | when `flutter.lsp.initialization.closingLabels` is set to `true`, 190 | the closing labels will be display at end of closing. 191 | 192 | > this feature only support neovim since vim do not support virtual text 193 | 194 | | disabled | enabled | 195 | | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | 196 | | | | 197 | 198 | 199 | ### UI Path 200 | when `flutter.UIPath` is set to `true`, the path for the UI component under cursor will be shown on the status bar. 201 | ![Screen Shot 2020-08-09 at 6 25 10 PM](https://user-images.githubusercontent.com/8187501/89730211-575a9280-da6f-11ea-9ad1-73770c1840db.png) 202 | 203 | ### Flutter Outline 204 | In the Flutter Outline panel, press `enter` to goto the corresponding location in the source file. 205 | 206 |
Sponsored by Instaboard
207 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coc-flutter-tools", 3 | "version": "1.6.8", 4 | "description": "Rich Flutter development experience for (Neo)vim", 5 | "author": "David Chen ", 6 | "license": "MIT", 7 | "main": "out/index.js", 8 | "keywords": [ 9 | "coc.nvim", 10 | "neovim", 11 | "nvim", 12 | "flutter", 13 | "lsp", 14 | "dart" 15 | ], 16 | "engines": { 17 | "coc": "^0.0.77" 18 | }, 19 | "scripts": { 20 | "clean": "rm -rf ./out", 21 | "watch": "webpack --watch", 22 | "build": "webpack", 23 | "lint": "eslint --fix ./src/**/*.ts", 24 | "prepare": "npm run clean && npm run build" 25 | }, 26 | "prettier": { 27 | "singleQuote": true, 28 | "trailingComma": "all", 29 | "printWidth": 100, 30 | "semi": true, 31 | "useTabs": true 32 | }, 33 | "husky": { 34 | "hooks": { 35 | "pre-commit": "npm run lint", 36 | "pre-push": "npm run lint" 37 | } 38 | }, 39 | "activationEvents": [ 40 | "workspaceContains:pubspec.yaml", 41 | "onLanguage:dart", 42 | "onCommand:flutter.run", 43 | "onCommand:flutter.doctor", 44 | "onCommand:flutter.upgrade", 45 | "onCommand:flutter.create", 46 | "onCommand:flutter.pub.get", 47 | "onCommand:flutter.devices", 48 | "onCommand:flutter.emulators" 49 | ], 50 | "contributes": { 51 | "rootPatterns": [ 52 | { 53 | "filetype": "dart", 54 | "patterns": [ 55 | "pubspec.yaml" 56 | ] 57 | } 58 | ], 59 | "configuration": { 60 | "type": "object", 61 | "title": "flutter configuration", 62 | "properties": { 63 | "flutter.trace.server": { 64 | "type": "string", 65 | "default": "off", 66 | "enum": [ 67 | "off", 68 | "message", 69 | "verbose" 70 | ], 71 | "description": "Trace level of log" 72 | }, 73 | "flutter.enabled": { 74 | "type": "boolean", 75 | "default": true, 76 | "description": "Enable coc-flutter extension" 77 | }, 78 | "flutter.fvm.enabled": { 79 | "type": "boolean", 80 | "default": true, 81 | "description": "Enable checking of fvm directories to find flutter install" 82 | }, 83 | "flutter.sdk.path": { 84 | "type": "string", 85 | "default": [], 86 | "description": "The path of the flutter sdk to use. (When using the `FlutterSDKs` list to change sdk this value will be updated)" 87 | }, 88 | "flutter.sdk.searchPaths": { 89 | "type": "array", 90 | "default": [], 91 | "item": "string", 92 | "description": "The paths to search for flutter sdks, either directories where flutter is installed or directories which contain directories where flutter versions have been installed\neg. /path/to/flutter (command at /path/to/flutter/bin/flutter) \n~/flutter_versions (command at ~/flutter_versions/version/bin/flutter)." 93 | }, 94 | "flutter.UIPath": { 95 | "type": "boolean", 96 | "default": true, 97 | "description": "Whether if the path for the current UI component should be shown on the status bar" 98 | }, 99 | "flutter.outlineWidth": { 100 | "type": "number", 101 | "default": 30, 102 | "description": "The default width of the outline panel" 103 | }, 104 | "flutter.outlineIconPadding": { 105 | "type": "number", 106 | "default": 0, 107 | "description": "The number of spaces between the icon and the item text in the outline panel" 108 | }, 109 | "flutter.lsp.debug": { 110 | "type": "boolean", 111 | "default": false, 112 | "description": "Enable debug for language server" 113 | }, 114 | "flutter.lsp.initialization.onlyAnalyzeProjectsWithOpenFiles": { 115 | "type": "boolean", 116 | "default": false, 117 | "description": "When set to true, analysis will only be performed for projects that have open files rather than the root workspace folder." 118 | }, 119 | "flutter.lsp.initialization.suggestFromUnimportedLibraries": { 120 | "type": "boolean", 121 | "default": true, 122 | "description": "When set to false, completion will not include synbols that are not already imported into the current file" 123 | }, 124 | "flutter.lsp.initialization.closingLabels": { 125 | "type": "boolean", 126 | "default": true, 127 | "description": "When set to true, dart/textDocument/publishClosingLabels notifications will be sent with information to render editor closing labels." 128 | }, 129 | "flutter.lsp.initialization.outline": { 130 | "type": "boolean", 131 | "default": true, 132 | "description": "" 133 | }, 134 | "flutter.sdk.dart-command": { 135 | "type": "string", 136 | "default": "", 137 | "description": "dart command, leave empty should just work" 138 | }, 139 | "flutter.sdk.dart-lookup": { 140 | "type": "string", 141 | "default": "", 142 | "description": "command to find dart executable location, used to infer dart-sdk location" 143 | }, 144 | "flutter.sdk.flutter-lookup": { 145 | "type": "string", 146 | "default": "", 147 | "description": "command to find flutter executable location, used to infer location of dart-sdk in flutter cache" 148 | }, 149 | "flutter.provider.hot-reload": { 150 | "type": "boolean", 151 | "default": true, 152 | "description": "Enable hot reload after save" 153 | }, 154 | "flutter.provider.enableSnippet": { 155 | "type": "boolean", 156 | "default": true, 157 | "description": "Enable completion item snippet" 158 | }, 159 | "flutter.openDevLogSplitCommand": { 160 | "type": "string", 161 | "default": "", 162 | "description": "Vim command to open dev log window, like: `botright 10split`" 163 | }, 164 | "flutter.workspaceFolder.ignore": { 165 | "type": "array", 166 | "default": [], 167 | "item": "string", 168 | "description": "Path start within the list will not treat as workspaceFolder" 169 | }, 170 | "flutter.autoOpenDevLog": { 171 | "type": "boolean", 172 | "default": false, 173 | "description": "Automatically open the dev log after calling flutter run" 174 | }, 175 | "flutter.autoHideDevLog": { 176 | "type": "boolean", 177 | "default": false, 178 | "description": "Automatically hide the dev log when the app stops running" 179 | }, 180 | "flutter.runDevToolsAtStartup": { 181 | "type": "boolean", 182 | "default": false, 183 | "description": "Automatically run the DevTools debugger in a web browser when running a project" 184 | }, 185 | "flutter.commands.devicesTimeout": { 186 | "type": "integer", 187 | "default": 1, 188 | "description": "Sets the `--device-timout` flag when running `flutter devices`" 189 | }, 190 | "dart.analysisExcludedFolders": { 191 | "type": "array", 192 | "default": [], 193 | "item": "string", 194 | "description": "An array of paths (absolute or relative to each workspace folder) that should be excluded from analysis." 195 | }, 196 | "dart.enableSdkFormatter": { 197 | "type": "boolean", 198 | "default": true, 199 | "description": "When set to false, prevents registration (or unregisters) the SDK formatter. When set to true or not supplied, will register/reregister the SDK formatter." 200 | }, 201 | "dart.lineLength": { 202 | "type": "number", 203 | "default": 80, 204 | "description": "The number of characters the formatter should wrap code at. If unspecified, code will be wrapped at 80 characters." 205 | }, 206 | "dart.completeFunctionCalls": { 207 | "type": "boolean", 208 | "default": true, 209 | "description": "Completes functions/methods with their required parameters." 210 | }, 211 | "dart.showTodos": { 212 | "type": "boolean", 213 | "default": true, 214 | "description": "Whether to generate diagnostics for TODO comments. If unspecified, diagnostics will not be generated." 215 | } 216 | } 217 | }, 218 | "commands": [ 219 | { 220 | "command": "flutter.run", 221 | "title": "Run flutter server" 222 | }, 223 | { 224 | "command": "flutter.attach", 225 | "title": "Attach running application" 226 | }, 227 | { 228 | "command": "flutter.create", 229 | "title": "Create flutter project using: flutter create project-name" 230 | }, 231 | { 232 | "command": "flutter.doctor", 233 | "title": "flutter doctor" 234 | }, 235 | { 236 | "command": "flutter.upgrade", 237 | "title": "flutter upgrade" 238 | }, 239 | { 240 | "command": "flutter.pub.get", 241 | "title": "flutter pub get" 242 | }, 243 | { 244 | "command": "flutter.devices", 245 | "title": "open devices list" 246 | }, 247 | { 248 | "command": "flutter.emulators", 249 | "title": "open emulators list" 250 | }, 251 | { 252 | "command": "flutter.outline", 253 | "title": "Opens a side panel that shows real time the flutter outline" 254 | }, 255 | { 256 | "command": "flutter.outline", 257 | "title": "Toggles the flutter outline side panel" 258 | } 259 | ] 260 | }, 261 | "devDependencies": { 262 | "@types/node": "^14.14.31", 263 | "@typescript-eslint/eslint-plugin": "^3.9.1", 264 | "@typescript-eslint/parser": "^3.9.1", 265 | "coc.nvim": "^0.0.80", 266 | "colors": "^1.4.0", 267 | "eslint": "^7.7.0", 268 | "eslint-config-prettier": "^6.11.0", 269 | "eslint-plugin-prettier": "^3.1.4", 270 | "fast-glob": "^3.2.4", 271 | "husky": "^4.2.5", 272 | "prettier": "^2.0.5", 273 | "ts-loader": "^8.0.2", 274 | "typescript": "^4.0.2", 275 | "vscode-languageserver-protocol": "^3.15.3", 276 | "webpack": "^4.44.1", 277 | "webpack-cli": "^3.3.12", 278 | "which": "^2.0.2" 279 | }, 280 | "homepage": "https://github.com/theniceboy/coc-flutter-tools" 281 | } 282 | -------------------------------------------------------------------------------- /src/__vim_project_root: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theniceboy/coc-flutter-tools/65cf86be59e68ddc613a8adcc4d67ebfcca78238/src/__vim_project_root -------------------------------------------------------------------------------- /src/commands/dev/index.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcessWithoutNullStreams } from 'child_process'; 2 | import { commands, Disposable, workspace } from 'coc.nvim'; 3 | import { notification } from '../../lib/notification'; 4 | import { devServer } from '../../server/dev'; 5 | import { devToolsServer } from '../../server/devtools'; 6 | import { deleteCommandTitle, setCommandTitle } from '../../util'; 7 | import { cmdPrefix } from '../../util/constant'; 8 | import { Dispose } from '../../util/dispose'; 9 | import { logger } from '../../util/logger'; 10 | import { reduceSpace } from '../../util'; 11 | import { opener } from '../../util/opener'; 12 | 13 | const log = logger.getlog('dev-command'); 14 | 15 | interface DCmd { 16 | cmd?: string; 17 | desc: string; 18 | callback?: (...params: any[]) => any; 19 | } 20 | 21 | export const cmds: Record = { 22 | hotReload: { 23 | cmd: 'r', 24 | desc: 'Hot reload', 25 | }, 26 | hotRestart: { 27 | cmd: 'R', 28 | desc: 'Hot restart', 29 | }, 30 | debugDumpAPP: { 31 | cmd: 'w', 32 | desc: 'You can dump the widget hierarchy of the app (debugDumpApp)', 33 | callback: () => { 34 | devServer.openDevLog(); 35 | }, 36 | }, 37 | debugDumpRenderTree: { 38 | cmd: 't', 39 | desc: 'To dump the rendering tree of the app (debugDumpRenderTree)', 40 | callback: () => { 41 | devServer.openDevLog(); 42 | }, 43 | }, 44 | debugDumpLayerTree: { 45 | cmd: 'L', 46 | desc: 'For layers (debugDumpLayerTree)', 47 | callback: () => { 48 | devServer.openDevLog(); 49 | }, 50 | }, 51 | debugDumpSemanticsTraversalOrder: { 52 | cmd: 'S', 53 | desc: 'Accessibility (debugDumpSemantics) for traversal order', 54 | }, 55 | debugDumpSemanticsHitTestOrder: { 56 | cmd: 'U', 57 | desc: 'Accessibility (debugDumpSemantics) for inverse hit test order', 58 | }, 59 | showWidgetInspectorOverride: { 60 | cmd: 'i', 61 | desc: 'To toggle the widget inspector (WidgetsApp.showWidgetInspectorOverride)', 62 | }, 63 | debugPaintSizeEnabled: { 64 | cmd: 'p', 65 | desc: 'To toggle the display of construction lines (debugPaintSizeEnabled)', 66 | }, 67 | defaultTargetPlatform: { 68 | cmd: 'o', 69 | desc: 'To simulate different operating systems, (defaultTargetPlatform)', 70 | }, 71 | elevationChecker: { 72 | cmd: 'z', 73 | desc: 'To toggle the elevation checker', 74 | }, 75 | showPerformanceOverlay: { 76 | cmd: 'P', 77 | desc: 'To display the performance overlay (WidgetsApp.showPerformanceOverlay)', 78 | }, 79 | debugProfileWidgetBuilds: { 80 | cmd: 'a', 81 | desc: 'To enable timeline events for all widget build methods, (debugProfileWidgetBuilds)', 82 | }, 83 | screenshot: { 84 | cmd: 's', 85 | desc: 'To save a screenshot to flutter.png', 86 | }, 87 | detach: { 88 | cmd: 'd', 89 | desc: 'Detach server', 90 | }, 91 | quit: { 92 | cmd: 'q', 93 | desc: 'Quit server', 94 | }, 95 | copyProfilerUrl: { 96 | desc: 97 | 'Copy the observatory debugger and profiler web page to the system clipboard (register +)', 98 | callback: (run: Dev) => { 99 | run.copyProfilerUrl(); 100 | }, 101 | }, 102 | openProfiler: { 103 | desc: 'Observatory debugger and profiler web page', 104 | callback: (run: Dev) => { 105 | run.openProfiler(); 106 | }, 107 | }, 108 | openDevToolsProfiler: { 109 | desc: 'Load DevTools page in an external web browser', 110 | callback: (run: Dev) => { 111 | run.openDevToolsProfiler(); 112 | }, 113 | }, 114 | clearDevLog: { 115 | desc: 'Clear the flutter dev server log', 116 | callback: () => { 117 | if (devServer.state) { 118 | devServer.clearDevLog(); 119 | } 120 | }, 121 | }, 122 | openDevLog: { 123 | desc: 'Open flutter dev server log', 124 | callback: () => { 125 | if (devServer.state) { 126 | devServer.openDevLog(); 127 | } 128 | }, 129 | }, 130 | }; 131 | 132 | export class Dev extends Dispose { 133 | private profilerUrl: string | undefined; 134 | private cmds: Disposable[] = []; 135 | 136 | constructor() { 137 | super(); 138 | ['run', 'attach'].forEach((cmd) => { 139 | const cmdId = `${cmdPrefix}.${cmd}`; 140 | this.push(commands.registerCommand(cmdId, this[`${cmd}Server`], this)); 141 | this.push( 142 | (function () { 143 | setCommandTitle(cmdId, `${cmd} flutter server`); 144 | return { 145 | dispose() { 146 | deleteCommandTitle(cmdId); 147 | }, 148 | }; 149 | })(), 150 | ); 151 | }); 152 | this.push(devServer); 153 | log('register dev command'); 154 | this.push(devToolsServer); 155 | } 156 | 157 | runServer(...args: string[]) { 158 | this.execute('run', args); 159 | } 160 | 161 | attachServer(...args: string[]) { 162 | this.execute('attach', args); 163 | } 164 | 165 | private async execute(cmd: string, args: string[]) { 166 | log(`${cmd} dev server, devServer state: ${devServer.state}`); 167 | const state = await devServer.start([cmd].concat(args)); 168 | if (state) { 169 | devServer.onError(this.onError); 170 | devServer.onExit(this.onExit); 171 | devServer.onStdout(this.onStdout); 172 | devServer.onStderr(this.onStderr); 173 | this.registerCommands(); 174 | } 175 | } 176 | 177 | private registerCommands() { 178 | log('register commands'); 179 | this.cmds.push( 180 | ...Object.keys(cmds).map((key) => { 181 | const cmdId = `${cmdPrefix}.dev.${key}`; 182 | setCommandTitle(cmdId, cmds[key].desc); 183 | const subscription = commands.registerCommand(cmdId, this.execCmd(cmds[key])); 184 | return { 185 | dispose() { 186 | deleteCommandTitle(cmdId); 187 | subscription.dispose(); 188 | }, 189 | }; 190 | }), 191 | ); 192 | } 193 | 194 | private unRegisterCommands() { 195 | log('unregister commands'); 196 | if (this.cmds) { 197 | this.cmds.forEach((cmd) => { 198 | cmd.dispose(); 199 | }); 200 | } 201 | this.cmds = []; 202 | } 203 | 204 | private onError = (err: Error) => { 205 | log(`devServer error: ${err.message}\n${err.stack}`); 206 | this.unRegisterCommands(); 207 | notification.show(`${err.message}`); 208 | }; 209 | 210 | private onExit = (code: number) => { 211 | log(`devServer exit with: ${code}`); 212 | this.unRegisterCommands(); 213 | if (code !== 0 && code !== 1) { 214 | notification.show(`Flutter server exist with ${code}`); 215 | } 216 | }; 217 | 218 | private shouldLayoutOutputFilter = false; 219 | 220 | private filterInvalidLines(lines: string[]): string[] { 221 | return lines 222 | .map((line) => reduceSpace(line)) 223 | .filter((line) => { 224 | if (this.shouldLayoutOutputFilter) { 225 | if ( 226 | line.startsWith('flutter: ════════════════════════════════════════════════════════') 227 | ) { 228 | this.shouldLayoutOutputFilter = false; 229 | } 230 | return false; 231 | } 232 | if (line.startsWith('flutter: ══╡')) { 233 | this.shouldLayoutOutputFilter = true; 234 | return true; 235 | } 236 | 237 | return ( 238 | line !== '' && 239 | !/^[DIW]\//.test(line) && 240 | !line.startsWith('🔥 To hot reload') && 241 | !line.startsWith('An Observatory debugger and profiler') && 242 | !line.startsWith('For a more detailed help message, press "h"') && 243 | !line.startsWith('Initializing hot reload') && 244 | !line.startsWith('Performing hot reload') && 245 | !line.startsWith('Reloaded ') && 246 | !line.startsWith('Flutter run key commands.') && 247 | !line.startsWith('r Hot reload. 🔥🔥🔥') && 248 | !line.startsWith('R Hot restart.') && 249 | !line.startsWith('h Repeat this help message.') && 250 | !line.startsWith('d Detach (terminate "flutter run" but leave application running).') && 251 | !line.startsWith('c Clear the screen') && 252 | !line.startsWith('q Quit (terminate the application on the device).') && 253 | !line.startsWith('flutter: Another exception was thrown:') && 254 | !line.startsWith('[VERBOSE-2:profiler_metrics_ios.mm') && 255 | !line.startsWith('An Observatory debugger and profiler on') && 256 | !/^flutter: #\d+ +.+$/.test(line) 257 | ); 258 | }); 259 | } 260 | 261 | private onStdout = (lines: string[]) => { 262 | lines.forEach((line) => { 263 | const m = line.match( 264 | /^\s*An Observatory debugger and profiler on .* is available at:\s*(https?:\/\/127\.0\.0\.1:\d+\/.+\/)$/, 265 | ); 266 | if (m) { 267 | this.profilerUrl = m[1]; 268 | const config = workspace.getConfiguration('flutter'); 269 | const runDevToolsAtStartupEnabled = config.get('runDevToolsAtStartup', false); 270 | if (runDevToolsAtStartupEnabled) { 271 | this.openDevToolsProfiler(); 272 | } 273 | } 274 | }); 275 | notification.show(this.filterInvalidLines(lines)); 276 | }; 277 | 278 | private onStderr = (/* lines: string[] */) => { 279 | // TODO: stderr output 280 | }; 281 | 282 | execCmd(cmd: DCmd) { 283 | return () => { 284 | if (devServer.state) { 285 | if (cmd.cmd) { 286 | devServer.sendCommand(cmd.cmd); 287 | } 288 | if (cmd.callback) { 289 | cmd.callback(this); 290 | } 291 | } else { 292 | notification.show('Flutter server is not running!'); 293 | } 294 | }; 295 | } 296 | 297 | async copyProfilerUrl() { 298 | if (!this.profilerUrl) { 299 | return; 300 | } 301 | if (devServer.state) { 302 | workspace.nvim.command(`let @+='${this.profilerUrl}'`); 303 | return; 304 | } 305 | notification.show('Flutter server is not running!'); 306 | } 307 | 308 | openProfiler() { 309 | if (!this.profilerUrl) { 310 | return; 311 | } 312 | if (devServer.state) { 313 | try { 314 | return opener(this.profilerUrl); 315 | } catch (error) { 316 | log(`Open browser fail: ${error.message}\n${error.stack}`); 317 | notification.show(`Open browser fail: ${error.message || error}`); 318 | } 319 | } 320 | notification.show('Flutter server is not running!'); 321 | } 322 | 323 | openDevToolsProfiler(): void { 324 | if (!this.profilerUrl || !devServer.state) { 325 | return; 326 | } 327 | if (devToolsServer.state) { 328 | this.launchDevToolsInBrowser(); 329 | } else { 330 | devToolsServer.start(); 331 | devToolsServer.onStdout(() => { 332 | this.launchDevToolsInBrowser(); 333 | }); 334 | devToolsServer.onStderr(() => { 335 | this.openDevToolsProfiler(); 336 | }); 337 | } 338 | } 339 | 340 | private launchDevToolsInBrowser(): ChildProcessWithoutNullStreams | undefined { 341 | if (devToolsServer.state) { 342 | try { 343 | // assertion to fix encodeURIComponent not accepting undefined- we rule out undefined values before this is called 344 | const url = `http://${devToolsServer.devToolsUri}/#/?uri=ws${encodeURIComponent( 345 | this.profilerUrl as string, 346 | )}`; 347 | return opener(url); 348 | } catch (error) { 349 | log(`Open browser fail: ${error.message}\n${error.stack}`); 350 | notification.show(`Open browser fail: ${error.message || error}`); 351 | } 352 | } 353 | } 354 | 355 | dispose() { 356 | super.dispose(); 357 | this.unRegisterCommands(); 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /src/commands/global.ts: -------------------------------------------------------------------------------- 1 | import { commands, Disposable, window, workspace } from 'coc.nvim'; 2 | import { notification } from '../lib/notification'; 3 | import { flutterSDK } from '../lib/sdk'; 4 | import { deleteCommandTitle, formatMessage, setCommandTitle } from '../util'; 5 | import { cmdPrefix } from '../util/constant'; 6 | import { Dispose } from '../util/dispose'; 7 | import { getFlutterWorkspaceFolder } from '../util/fs'; 8 | import { logger } from '../util/logger'; 9 | 10 | const log = logger.getlog('global-commands'); 11 | 12 | interface GCmd { 13 | name?: string; 14 | cmd: string; 15 | desc: string; 16 | execute: (cmd: GCmd, ...args: string[]) => Promise; 17 | getArgs?: () => Promise; 18 | } 19 | 20 | const getCmd = () => { 21 | return async ({ cmd, getArgs }: GCmd, ...inputArgs: string[]): Promise => { 22 | let args: string[] = []; 23 | if (getArgs) { 24 | args = await getArgs(); 25 | } 26 | if (inputArgs.length) { 27 | args = args.concat(inputArgs); 28 | } 29 | const { err, stdout, stderr } = await flutterSDK.execFlutterCommand(`${cmd} ${args.join(' ')}`); 30 | const devLog = logger.devOutchannel; 31 | if (stdout) { 32 | devLog.append(`\n${stdout}\n`); 33 | } 34 | if (stderr) { 35 | devLog.append(`\n${stderr}\n`); 36 | } 37 | if (err) { 38 | devLog.append([err.message, err.stack].join('\n')); 39 | } 40 | devLog.show(); 41 | }; 42 | }; 43 | 44 | const cmds: GCmd[] = [ 45 | { 46 | cmd: 'upgrade', 47 | desc: 'flutter upgrade', 48 | execute: getCmd(), 49 | }, 50 | { 51 | cmd: 'doctor', 52 | desc: 'flutter doctor', 53 | execute: getCmd(), 54 | }, 55 | { 56 | cmd: 'create', 57 | desc: 'flutter create', 58 | execute: getCmd(), 59 | getArgs: async (): Promise => { 60 | const params = await window.requestInput('Input project name and other params: '); 61 | return params.split(' '); 62 | }, 63 | }, 64 | { 65 | cmd: 'pub get', 66 | name: 'pub.get', 67 | desc: 'flutter pub get', 68 | execute: async (): Promise => { 69 | const workspaceFolder = await getFlutterWorkspaceFolder(); 70 | log(`pub get command, workspace: ${workspaceFolder}`); 71 | if (!workspaceFolder) { 72 | notification.show('Flutter project workspaceFolder not found!'); 73 | return; 74 | } 75 | const { code, err, stdout, stderr } = await flutterSDK.execFlutterCommand(`pub get`, { 76 | cwd: workspaceFolder, 77 | }); 78 | notification.show(formatMessage(stdout)); 79 | if (err || code) { 80 | notification.show(formatMessage(stderr)); 81 | } 82 | }, 83 | }, 84 | { 85 | cmd: 'devices', 86 | desc: 'open devices list', 87 | execute: async (_, ...args: string[]): Promise => { 88 | workspace.nvim.command(`CocList FlutterDevices ${args.join(' ')}`); 89 | }, 90 | }, 91 | { 92 | cmd: 'emulators', 93 | desc: 'open emulators list', 94 | execute: async (): Promise => { 95 | workspace.nvim.command('CocList FlutterEmulators'); 96 | }, 97 | }, 98 | ]; 99 | 100 | export class Global extends Dispose { 101 | constructor() { 102 | super(); 103 | cmds.forEach((cmd) => { 104 | const { desc, execute, name } = cmd; 105 | const cmdId = `${cmdPrefix}.${name || cmd.cmd}`; 106 | this.push( 107 | commands.registerCommand(cmdId, async (...args: string[]) => { 108 | const statusBar = window.createStatusBarItem(0, { progress: true }); 109 | this.push(statusBar); 110 | statusBar.text = desc; 111 | statusBar.show(); 112 | await execute(cmd, ...args); 113 | this.remove(statusBar); 114 | }), 115 | Disposable.create(() => { 116 | deleteCommandTitle(cmdId); 117 | }), 118 | ); 119 | setCommandTitle(cmdId, desc); 120 | }); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | import { Dispose } from '../util/dispose'; 2 | import { Dev } from './dev'; 3 | import { Global } from './global'; 4 | import { LspServer } from '../server/lsp'; 5 | import { SuperCommand } from './super'; 6 | import { LspCommands } from './lsp'; 7 | 8 | export class Commands extends Dispose { 9 | constructor(lsp: LspServer) { 10 | super(); 11 | this.push(new Dev(), new Global(), new LspCommands(lsp), new SuperCommand(lsp)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/commands/lsp.ts: -------------------------------------------------------------------------------- 1 | import { commands } from 'coc.nvim'; 2 | import { notification } from '../lib/notification'; 3 | import { LspServer } from '../server/lsp'; 4 | import { deleteCommandTitle, setCommandTitle } from '../util'; 5 | import { cmdPrefix } from '../util/constant'; 6 | import { Dispose } from '../util/dispose'; 7 | 8 | export class LspCommands extends Dispose { 9 | private restartCmdId = `${cmdPrefix}.lsp.restart`; 10 | 11 | constructor(lsp: LspServer) { 12 | super(); 13 | this.push( 14 | commands.registerCommand(this.restartCmdId, async () => { 15 | if (!lsp.client) { 16 | return notification.show('analyzer LSP server is not running'); 17 | } 18 | await lsp.restart(); 19 | }), 20 | ); 21 | setCommandTitle(this.restartCmdId, 'restart the lsp server'); 22 | } 23 | 24 | dispose(): void { 25 | super.dispose(); 26 | deleteCommandTitle(this.restartCmdId); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/commands/super.ts: -------------------------------------------------------------------------------- 1 | import { commands, window, workspace } from 'coc.nvim'; 2 | import { Location, TextDocumentPositionParams } from 'vscode-languageserver-protocol'; 3 | import { notification } from '../lib/notification'; 4 | import { LspServer } from '../server/lsp'; 5 | import { deleteCommandTitle, setCommandTitle } from '../util'; 6 | import { cmdPrefix } from '../util/constant'; 7 | import { Dispose } from '../util/dispose'; 8 | 9 | export class SuperCommand extends Dispose { 10 | private requestType = 'dart/textDocument/super'; 11 | private cmdId = `${cmdPrefix}.gotoSuper`; 12 | 13 | constructor(lsp: LspServer) { 14 | super(); 15 | this.push( 16 | commands.registerCommand(this.cmdId, async () => { 17 | if (!lsp.client) { 18 | return notification.show('analyzer LSP server is not running'); 19 | } 20 | const doc = await workspace.document; 21 | if (!doc || !doc.textDocument || doc.textDocument.languageId !== 'dart') { 22 | return; 23 | } 24 | const position = await window.getCursorPosition(); 25 | const args: TextDocumentPositionParams = { 26 | textDocument: { 27 | uri: doc.uri, 28 | }, 29 | position, 30 | }; 31 | const params = await lsp.client.sendRequest(this.requestType, args); 32 | if (params) { 33 | workspace.jumpTo(params.uri, params.range.start); 34 | } 35 | }), 36 | ); 37 | setCommandTitle( 38 | this.cmdId, 39 | 'jump to the location of the super definition of the class or method', 40 | ); 41 | } 42 | 43 | dispose(): void { 44 | super.dispose(); 45 | deleteCommandTitle(this.cmdId); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext, workspace } from 'coc.nvim'; 2 | 3 | import { logger, logLevel } from './util/logger'; 4 | import { Commands } from './commands'; 5 | import { Providers } from './provider'; 6 | import { LspServer } from './server/lsp'; 7 | import { SourceList } from './sources'; 8 | 9 | export async function activate(context: ExtensionContext): Promise { 10 | const config = workspace.getConfiguration('flutter'); 11 | const isEnabled = config.get('enabled', true); 12 | 13 | // if not enabled then return 14 | if (!isEnabled) { 15 | return; 16 | } 17 | 18 | context.subscriptions.push(logger); 19 | // logger init 20 | logger.init(config.get('trace.server', 'off')); 21 | 22 | // register lsp server 23 | const lsp = new LspServer(); 24 | context.subscriptions.push(lsp); 25 | 26 | // register commands 27 | context.subscriptions.push(new Commands(lsp)); 28 | 29 | // register providers 30 | context.subscriptions.push(new Providers()); 31 | 32 | // register sources 33 | context.subscriptions.push(new SourceList(lsp)); 34 | } 35 | -------------------------------------------------------------------------------- /src/lib/notification/floatwindow.ts: -------------------------------------------------------------------------------- 1 | import { Buffer as NVIMBuffer, Window, workspace } from 'coc.nvim'; 2 | import { Dispose } from '../../util/dispose'; 3 | import { logger } from '../../util/logger'; 4 | import { Message } from './message'; 5 | 6 | const log = logger.getlog('floatWin'); 7 | 8 | export class FloatWindow extends Dispose { 9 | private buf: NVIMBuffer | undefined; 10 | private win: Window | undefined; 11 | 12 | constructor(public readonly message: Message) { 13 | super(); 14 | } 15 | 16 | public async show(top: number) { 17 | const { nvim } = workspace; 18 | const { message } = this; 19 | 20 | const buf = await nvim.createNewBuffer(false, true); 21 | await buf.setLines(message.lines, { start: 0, end: -1, strictIndexing: false }); 22 | const col = (await nvim.getOption('columns')) as number; 23 | const win = await nvim.openFloatWindow( 24 | buf, 25 | false, // do not enter 26 | { 27 | focusable: false, // can not be focusable 28 | relative: 'editor', 29 | anchor: 'NE', 30 | height: message.height, 31 | width: message.width + 2, 32 | row: top, 33 | col, 34 | }, 35 | ); 36 | this.buf = buf; 37 | this.win = win; 38 | 39 | nvim.pauseNotification(); 40 | await win.setOption('number', false); 41 | await win.setOption('wrap', true); 42 | await win.setOption('relativenumber', false); 43 | await win.setOption('cursorline', false); 44 | await win.setOption('cursorcolumn', false); 45 | await win.setOption('conceallevel', 2); 46 | await win.setOption('signcolumn', 'no'); 47 | await win.setOption('winhighlight', 'FoldColumn:NormalFloat'); 48 | await nvim.resumeNotification(); 49 | try { 50 | // vim is number and neovim is string 51 | await win.setOption('foldcolumn', workspace.isVim ? 1 : '1'); 52 | } catch (error) { 53 | log(`set foldcolumn error: ${error.message || error}`); 54 | } 55 | } 56 | 57 | public async dispose() { 58 | super.dispose(); 59 | if (this.buf) { 60 | this.buf = undefined; 61 | } 62 | if (this.win) { 63 | await this.win.close(true); 64 | } 65 | } 66 | 67 | public async update(top: number) { 68 | const { win } = this; 69 | if (win) { 70 | const config = await win.getConfig(); 71 | await win.setConfig({ 72 | ...config, 73 | row: top, 74 | }); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/lib/notification/index.ts: -------------------------------------------------------------------------------- 1 | import { window, workspace } from 'coc.nvim'; 2 | import { Dispose } from '../../util/dispose'; 3 | import { Message } from './message'; 4 | 5 | const messageMaxWidth = 60; 6 | const detectTimeGap = 1000 / 30; 7 | const messageDefaultShowTime = 3000; 8 | 9 | class Notification extends Dispose { 10 | private isSupportFloat = false; 11 | // top to the viewpoint 12 | private top = 1; 13 | private maxWidth: number = messageMaxWidth; 14 | private messages: Message[] = []; 15 | private display: Message[] = []; 16 | private timeGap: number = detectTimeGap; 17 | private timer: NodeJS.Timer | undefined; 18 | 19 | constructor() { 20 | super(); 21 | this.init(); 22 | } 23 | 24 | private async init() { 25 | const { nvim } = workspace; 26 | this.isSupportFloat = !!(await nvim.call('exists', '*nvim_open_win')); 27 | const screenWidth = (await nvim.getOption('columns')) as number; 28 | this.maxWidth = Math.min(this.maxWidth, screenWidth); 29 | } 30 | 31 | private async detect() { 32 | if (this.timer) { 33 | clearTimeout(this.timer); 34 | } 35 | const { timeGap, display } = this; 36 | let { top } = this; 37 | this.display = []; 38 | for (const message of display) { 39 | if (!message.isInvalid) { 40 | await message.update(timeGap, top); 41 | top += message.height; 42 | this.display.push(message); 43 | } else { 44 | message.dispose(); 45 | } 46 | } 47 | const { nvim } = workspace; 48 | const screenHeight = ((await nvim.getOption('lines')) as number) - 1; // 1 is statusline height 49 | while (this.messages.length) { 50 | const message = this.messages[this.messages.length - 1]; 51 | if (top + message.height > screenHeight) { 52 | break; 53 | } else { 54 | this.messages.pop(); 55 | await message.show(top); 56 | this.display.push(message); 57 | top += message.height; 58 | } 59 | } 60 | this.timer = setTimeout(() => { 61 | this.detect(); 62 | }, this.timeGap); 63 | } 64 | 65 | show(message: string | string[], showTime: number = messageDefaultShowTime) { 66 | const messages = ([] as string[]).concat(message); 67 | if (messages.length === 0) { 68 | return; 69 | } 70 | if (!this.isSupportFloat) { 71 | return window.showMessage(messages.join('\n')); 72 | } 73 | this.messages.push(new Message(messages, this.maxWidth, showTime)); 74 | this.detect(); 75 | } 76 | 77 | dispose() { 78 | if (this.timer) { 79 | clearTimeout(this.timer); 80 | } 81 | this.push(...this.display); 82 | this.messages = []; 83 | this.display = []; 84 | super.dispose(); 85 | } 86 | } 87 | 88 | export const notification = new Notification(); 89 | -------------------------------------------------------------------------------- /src/lib/notification/message.ts: -------------------------------------------------------------------------------- 1 | import { Dispose } from '../../util/dispose'; 2 | import { FloatWindow } from './floatwindow'; 3 | 4 | export class Message extends Dispose { 5 | private top = 0; 6 | public readonly height: number; 7 | public readonly width: number; 8 | public readonly floatWindow: FloatWindow; 9 | 10 | constructor(public readonly lines: string[], maxWidth: number, private _time: number = 1000) { 11 | super(); 12 | this.width = 0; 13 | this.height = 0; 14 | for (const line of lines) { 15 | const byteLength = Buffer.byteLength(line); 16 | this.width = Math.max(this.width, Math.min(byteLength, maxWidth)); 17 | this.height = this.height + Math.max(1, Math.ceil(byteLength / (maxWidth - 2))); 18 | } 19 | this.floatWindow = new FloatWindow(this); 20 | this.push(this.floatWindow); 21 | } 22 | 23 | public get time(): number { 24 | return this._time; 25 | } 26 | 27 | public get isInvalid(): boolean { 28 | return this._time <= 0; 29 | } 30 | 31 | public async show(top: number) { 32 | this.top = top; 33 | await this.floatWindow.show(top); 34 | } 35 | 36 | public async update(time: number, top: number) { 37 | this._time -= time; 38 | if (this.top !== top) { 39 | this.top = top; 40 | await this.floatWindow.update(top); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/sdk.ts: -------------------------------------------------------------------------------- 1 | import { ExecOptions } from 'child_process'; 2 | import { Uri, workspace, WorkspaceConfiguration } from 'coc.nvim'; 3 | import os, { homedir } from 'os'; 4 | import { dirname, join } from 'path'; 5 | import which from 'which'; 6 | import { execCommand, exists, getRealPath, readDir, readFile } from '../util/fs'; 7 | import { logger } from '../util/logger'; 8 | 9 | const log = logger.getlog('sdk'); 10 | 11 | const ANALYZER_SNAPSHOT_NAME = join('bin', 'snapshots', 'analysis_server.dart.snapshot'); 12 | const DART_COMMAND = join('bin', os.platform() === 'win32' ? 'dart.bat' : 'dart'); 13 | 14 | const _defaultSearchPaths: string[] = ['~/snap/flutter/common/flutter', '~/fvm/versions']; 15 | 16 | export interface FlutterSdk { 17 | location: string; 18 | version: string; 19 | fvmVersion?: string; 20 | isFvm: boolean; 21 | isCurrent: boolean; 22 | } 23 | 24 | class FlutterSDK { 25 | private _sdkHome = ''; 26 | private _state = false; 27 | private _dartHome = ''; 28 | private _analyzerSnapshotPath = ''; 29 | private _dartCommand = ''; 30 | private _flutterCommand?: string; 31 | private _fvmEnabled!: boolean; 32 | 33 | private config!: WorkspaceConfiguration; 34 | 35 | public get sdkHome(): string { 36 | return this._sdkHome; 37 | } 38 | 39 | public get state(): boolean { 40 | return this._state; 41 | } 42 | 43 | public get dartHome(): string { 44 | return this._dartHome; 45 | } 46 | 47 | public get analyzerSnapshotPath(): string { 48 | return this._analyzerSnapshotPath; 49 | } 50 | 51 | public get dartCommand(): string { 52 | return this._dartCommand; 53 | } 54 | 55 | public get flutterCommand(): string { 56 | return this._flutterCommand || 'flutter'; 57 | } 58 | 59 | private async _hasValidFlutterSdk(): Promise { 60 | return (await exists(this._sdkHome)) && (await exists(join(this._sdkHome, 'bin', 'flutter'))); 61 | } 62 | 63 | public async getVersion(): Promise<[number, number, number] | undefined> { 64 | if (this._dartCommand) { 65 | const { stderr, stdout } = await execCommand(`${this._dartCommand} --version`); 66 | if (stderr) { 67 | const m = stderr.match(/version:\s+(\d+)\.(\d+)\.(\d+)/); 68 | if (m) { 69 | log(`dart version: v${m[1]}.${m[2]}.${m[3]}`); 70 | return [parseFloat(m[1]), parseFloat(m[2]), parseFloat(m[3])]; 71 | } 72 | } else if (stdout) { 73 | const m = stdout.match(/version:\s+(\d+)\.(\d+)\.(\d+)/); 74 | if (m) { 75 | log(`dart version: v${m[1]}.${m[2]}.${m[3]}`); 76 | return [parseFloat(m[1]), parseFloat(m[2]), parseFloat(m[3])]; 77 | } 78 | } else { 79 | log('Failed to get dart version'); 80 | } 81 | } 82 | return undefined; 83 | } 84 | 85 | public async isVersionGreatOrEqualTo(version: [number, number, number]): Promise { 86 | const v = await this.getVersion(); 87 | if (!v) { 88 | return false; 89 | } 90 | for (let i = 0; i < 3; i++) { 91 | if (v[i] > version[i]) return true; 92 | if (v[i] != version[i]) return false; 93 | } 94 | return true; 95 | } 96 | 97 | async init(config: WorkspaceConfiguration): Promise { 98 | this.config = config; 99 | await this._init(); 100 | } 101 | 102 | private async _init(): Promise { 103 | this._dartCommand = this.config.get('sdk.dart-command', ''); 104 | const dartLookup = this.config.get('sdk.dart-lookup', ''); 105 | this._fvmEnabled = this.config.get('fvm.enabled', true); 106 | 107 | this._sdkHome = this.config.get('sdk.path', ''); 108 | let hasValidFlutterSdk = await this._hasValidFlutterSdk(); 109 | if (hasValidFlutterSdk) await this.initFlutterCommandsFromSdkHome(); 110 | 111 | try { 112 | if (this._fvmEnabled && !hasValidFlutterSdk) { 113 | await this.initDartSdkHomeFromLocalFvm(); 114 | hasValidFlutterSdk = await this._hasValidFlutterSdk(); 115 | } 116 | if (!hasValidFlutterSdk) { 117 | await this.initDarkSdkHomeFromSearchPaths(); 118 | hasValidFlutterSdk = await this._hasValidFlutterSdk(); 119 | } 120 | if (!hasValidFlutterSdk) { 121 | await this.initDarkSdkHome(dartLookup); 122 | hasValidFlutterSdk = await this._hasValidFlutterSdk(); 123 | } 124 | 125 | await this.initDartSdk(); 126 | if (!this._state) { 127 | log('Dart SDK not found!'); 128 | log( 129 | JSON.stringify( 130 | { 131 | dartHome: this._dartHome, 132 | analyzerSnapshotPath: this._analyzerSnapshotPath, 133 | }, 134 | null, 135 | 2, 136 | ), 137 | ); 138 | } 139 | } catch (error) { 140 | log(error.message || 'find dart sdk error!'); 141 | log(error.stack); 142 | } 143 | } 144 | 145 | private async initDarkSdkHomeFromSearchPaths() { 146 | try { 147 | const sdks = await this.findSdks(); 148 | if (sdks.length > 0) { 149 | this._sdkHome = sdks[0].location; 150 | await this.initFlutterCommandsFromSdkHome(); 151 | } 152 | } catch (error) { 153 | log(`Error configuring sdk from searchPaths: ${error.message}}`); 154 | } 155 | } 156 | 157 | private async initDartSdkHomeFromLocalFvm() { 158 | try { 159 | const workspaceFolder = workspace.workspaceFolder 160 | ? Uri.parse(workspace.workspaceFolder.uri).fsPath 161 | : workspace.cwd; 162 | const fvmLocation = join(workspaceFolder, '.fvm', 'flutter_sdk'); 163 | if (await exists(fvmLocation)) { 164 | log('Found local fvm sdk'); 165 | this._sdkHome = fvmLocation; 166 | await this.initFlutterCommandsFromSdkHome(); 167 | } else { 168 | log('No local fvm sdk'); 169 | } 170 | } catch (error) { 171 | log(`Error configuring local fvm sdk: ${error.message}}`); 172 | } 173 | } 174 | 175 | private async initFlutterCommandsFromSdkHome() { 176 | this._flutterCommand = join( 177 | this._sdkHome, 178 | 'bin', 179 | os.platform() === 'win32' ? 'flutter.bat' : 'flutter', 180 | ); 181 | log(`flutter command path => ${this.flutterCommand}`); 182 | if (!(await exists(this._flutterCommand))) { 183 | log('flutter command path does not exist'); 184 | } 185 | this._dartHome = join(this._sdkHome, 'bin', 'cache', 'dart-sdk'); 186 | log(`dart sdk home => ${this._dartHome}`); 187 | if (!(await exists(this._dartHome))) { 188 | log('dart sdk home path does not exist'); 189 | } 190 | } 191 | 192 | private async initDarkSdkHome(dartLookup: string) { 193 | try { 194 | let dartPath: string; 195 | 196 | if (dartLookup.length == 0) { 197 | dartPath = await which('dart'); 198 | } else { 199 | const { stdout } = await execCommand(dartLookup); 200 | dartPath = stdout; 201 | if (stdout.length == 0) { 202 | throw new Error('dart lookup returned empty string'); 203 | } 204 | } 205 | log(`which dart command => ${dartPath}`); 206 | 207 | if (dartPath) { 208 | dartPath = await getRealPath(dartPath); 209 | log(`dart command path => ${dartPath}`); 210 | this._dartHome = join(dirname(dartPath), '..'); 211 | log(`dart sdk home => ${this._dartHome}`); 212 | } 213 | } catch (error) { 214 | log(`dart command not found: ${error.message}`); 215 | } 216 | } 217 | 218 | private async initDartSdk() { 219 | this._analyzerSnapshotPath = join(this._dartHome, ANALYZER_SNAPSHOT_NAME); 220 | log(`analyzer path => ${this._analyzerSnapshotPath}`); 221 | this._state = await exists(this._analyzerSnapshotPath); 222 | if (!this._dartCommand) { 223 | this._dartCommand = join(this.dartHome, DART_COMMAND); 224 | if (os.platform() === 'win32') { 225 | const isCommandExists = await exists(this._dartCommand); 226 | if (!isCommandExists) { 227 | this._dartCommand = this._dartCommand.replace(/bat$/, 'exe'); 228 | } 229 | } 230 | } 231 | log(`dart command path => ${this._dartCommand}`); 232 | } 233 | 234 | execDartCommand( 235 | command: string, 236 | options: ExecOptions = {}, 237 | ): Promise<{ 238 | code: number; 239 | err: Error | null; 240 | stdout: string; 241 | stderr: string; 242 | }> { 243 | return execCommand(`${this.dartCommand} ${command}`, options); 244 | } 245 | 246 | execFlutterCommand( 247 | command: string, 248 | options: ExecOptions = {}, 249 | ): Promise<{ 250 | code: number; 251 | err: Error | null; 252 | stdout: string; 253 | stderr: string; 254 | }> { 255 | return execCommand(`${this.flutterCommand} ${command}`, options); 256 | } 257 | 258 | private async versionForSdk(location: string): Promise { 259 | const version = await execCommand(`cat ${join(location, 'version')}`); 260 | if (version.err) return; 261 | return version.stdout; 262 | } 263 | 264 | private async sdkWithLookup( 265 | flutterLookup: string, 266 | currentSdk: string, 267 | ): Promise { 268 | let flutterPath: string; 269 | if (flutterLookup.length == 0) { 270 | flutterPath = (await which('flutter')).trim(); 271 | } else { 272 | const { stdout } = await execCommand(flutterLookup); 273 | flutterPath = stdout.trim(); 274 | if (stdout.length == 0) { 275 | return; 276 | } 277 | } 278 | log(`which flutter command => ${flutterPath}`); 279 | 280 | if (flutterPath) { 281 | flutterPath = await getRealPath(flutterPath); 282 | flutterPath = flutterPath.trim(); 283 | if ( 284 | flutterPath.toLowerCase().endsWith(join('bin', 'flutter')) || 285 | flutterPath.toLowerCase().endsWith(join('bin', 'flutter.bat')) 286 | ) { 287 | flutterPath = join(flutterPath, '..', '..'); 288 | } 289 | const isFlutterDir = await exists(join(flutterPath, 'bin', 'flutter')); 290 | if (!isFlutterDir) return; 291 | const version = await this.versionForSdk(flutterPath); 292 | if (version) { 293 | return { 294 | location: flutterPath, 295 | version: version, 296 | isFvm: false, 297 | isCurrent: currentSdk == flutterPath, 298 | }; 299 | } 300 | } 301 | } 302 | 303 | async findSdks(): Promise { 304 | const sdks: FlutterSdk[] = []; 305 | 306 | const currentSdk = this.sdkHome.length == 0 ? '' : await getRealPath(this.sdkHome); 307 | 308 | const flutterLookup = this.config.get('sdk.flutter-lookup', ''); 309 | const fvmEnabled = this.config.get('fvm.enabled', true); 310 | const home = homedir(); 311 | 312 | const lookupSdk = await this.sdkWithLookup(flutterLookup, currentSdk); 313 | if (lookupSdk) { 314 | sdks.push(lookupSdk); 315 | } 316 | 317 | const paths = [...this.config.get('sdk.searchPaths', []), ..._defaultSearchPaths]; 318 | log(`searchPaths: ${paths}`); 319 | 320 | for (let path of paths) { 321 | path = path.replace(/^~/, home).trim(); 322 | const isFlutterDir = await exists(join(path, 'bin', 'flutter')); 323 | if (isFlutterDir) { 324 | const version = await this.versionForSdk(path); 325 | if (version && !sdks.some((sdk) => sdk.location == path)) { 326 | sdks.push({ 327 | location: path, 328 | isFvm: false, 329 | version: version, 330 | isCurrent: path == currentSdk, 331 | }); 332 | } 333 | continue; 334 | } 335 | 336 | const files = await readDir(path); 337 | for (const file of files) { 338 | const location = join(path, file).trim(); 339 | const isFlutterDir = await exists(join(location, 'bin', 'flutter')); 340 | if (!isFlutterDir) continue; 341 | if (sdks.some((sdk) => sdk.location == location)) continue; 342 | const version = await this.versionForSdk(location); 343 | sdks.push({ 344 | location: location, 345 | isFvm: false, 346 | version: version || 'unknown', 347 | isCurrent: location == currentSdk, 348 | }); 349 | } 350 | } 351 | 352 | if (fvmEnabled) { 353 | let fvmCachePath = join(home, 'fvm', 'versions'); 354 | const settingsPath = join(home, 'fvm', '.settings'); 355 | try { 356 | const fvmSettingsFileExists = await exists(settingsPath); 357 | if (fvmSettingsFileExists) { 358 | const settingsRaw = await readFile(settingsPath); 359 | const settings = JSON.parse(settingsRaw.toString()); 360 | if (typeof settings.cachePath == 'string' && settings.cachePath.trim() != '') { 361 | fvmCachePath = settings.cachePath; 362 | } 363 | } 364 | } catch (error) { 365 | log(`Failed to load fvm settings: ${error.message}`); 366 | } 367 | 368 | const fvmVersions = await readDir(fvmCachePath); 369 | for (const version of fvmVersions) { 370 | const location = join(fvmCachePath, version).trim(); 371 | const isFlutterDir = await exists(join(location, 'bin', 'flutter')); 372 | if (!isFlutterDir) continue; 373 | if (sdks.some((sdk) => sdk.location == location)) continue; 374 | const flutterVersion = await this.versionForSdk(location); 375 | sdks.push({ 376 | location: location, 377 | fvmVersion: version, 378 | isFvm: true, 379 | version: !flutterVersion ? version : `${version} (${flutterVersion})`, 380 | isCurrent: location == currentSdk, 381 | }); 382 | } 383 | } 384 | 385 | return sdks; 386 | } 387 | } 388 | 389 | export const flutterSDK = new FlutterSDK(); 390 | -------------------------------------------------------------------------------- /src/lib/status.ts: -------------------------------------------------------------------------------- 1 | import { StatusBarItem, window, workspace } from 'coc.nvim'; 2 | import { Dispose } from '../util/dispose'; 3 | 4 | class StatusBar extends Dispose { 5 | private isLSPReady = false; 6 | private statusBar: StatusBarItem | undefined = undefined; 7 | 8 | get isInitialized(): boolean { 9 | return this.statusBar != undefined; 10 | } 11 | 12 | ready() { 13 | this.isLSPReady = true; 14 | this.show('flutter', false); 15 | } 16 | 17 | init() { 18 | this.statusBar = window.createStatusBarItem(0, { progress: false }); 19 | this.push(this.statusBar); 20 | 21 | this.push( 22 | workspace.registerAutocmd({ 23 | event: 'BufEnter', 24 | request: false, 25 | callback: async () => { 26 | if (this.isLSPReady) { 27 | const doc = await workspace.document; 28 | if (doc.filetype === 'dart') { 29 | this.show('Flutter'); 30 | } else { 31 | this.hide(); 32 | } 33 | } 34 | }, 35 | }), 36 | ); 37 | } 38 | 39 | restartingLsp() { 40 | this.isLSPReady = false; 41 | this.show('restartingLsp', true); 42 | } 43 | 44 | show(message: string, isProgress?: boolean) { 45 | if (this.statusBar) { 46 | this.statusBar.text = message; 47 | if (isProgress !== undefined) { 48 | this.statusBar.isProgress = isProgress; 49 | } 50 | this.statusBar.show(); 51 | } 52 | } 53 | 54 | hide() { 55 | if (this.statusBar) { 56 | this.statusBar.hide(); 57 | } 58 | } 59 | 60 | progress(isProgress = true) { 61 | if (this.statusBar) { 62 | this.statusBar.isProgress = isProgress; 63 | } 64 | } 65 | 66 | dispose() { 67 | super.dispose(); 68 | this.statusBar = undefined; 69 | } 70 | } 71 | 72 | export const statusBar = new StatusBar(); 73 | -------------------------------------------------------------------------------- /src/provider/hotreload.ts: -------------------------------------------------------------------------------- 1 | import { workspace, ConfigurationChangeEvent, Disposable, diagnosticManager } from 'coc.nvim'; 2 | import { TextDocument, DiagnosticSeverity } from 'vscode-languageserver-protocol'; 3 | 4 | import { devServer } from '../server/dev'; 5 | 6 | export const registerHotReloadProvider = (): Disposable => { 7 | const enableHotReload = workspace 8 | .getConfiguration('flutter') 9 | .get('provider.hot-reload', true); 10 | let subscription: Disposable | undefined; 11 | if (enableHotReload) { 12 | subscription = workspace.onDidSaveTextDocument((doc: TextDocument) => { 13 | if (doc.languageId === 'dart' && devServer.state) { 14 | // do not reload if there ara errors for the save file 15 | const diagnostics = diagnosticManager.getDiagnostics(doc.uri); 16 | let hasErrors = diagnostics && true; 17 | try { 18 | hasErrors = 19 | hasErrors && 20 | diagnostics.find( 21 | (d) => d.source === 'dart' && d.severity === DiagnosticSeverity.Error, 22 | ) != null; 23 | } catch (e) { 24 | hasErrors = false; 25 | console.error(e); 26 | } 27 | 28 | if (!hasErrors) { 29 | devServer.sendCommand('r'); 30 | } 31 | } 32 | }); 33 | } 34 | 35 | const configChangeSubs = workspace.onDidChangeConfiguration((e: ConfigurationChangeEvent) => { 36 | if (e.affectsConfiguration('flutter')) { 37 | const isEnableHotReload = workspace 38 | .getConfiguration('flutter') 39 | .get('provider.hot-reload', true); 40 | // disable hot reload 41 | if (enableHotReload && !isEnableHotReload) { 42 | if (subscription) { 43 | subscription.dispose(); 44 | subscription = undefined; 45 | } 46 | // enable hot reload 47 | } else if (!enableHotReload && isEnableHotReload) { 48 | subscription = workspace.onDidSaveTextDocument((doc: TextDocument) => { 49 | if (doc.languageId === 'dart' && devServer.state) { 50 | devServer.sendCommand('r'); 51 | } 52 | }); 53 | } 54 | } 55 | }); 56 | 57 | return { 58 | dispose() { 59 | if (subscription) { 60 | subscription.dispose(); 61 | } 62 | configChangeSubs.dispose(); 63 | }, 64 | }; 65 | }; 66 | -------------------------------------------------------------------------------- /src/provider/index.ts: -------------------------------------------------------------------------------- 1 | import { registerHotReloadProvider } from './hotreload'; 2 | import { Dispose } from '../util/dispose'; 3 | import { pubUpdateProvider } from './pub'; 4 | 5 | export class Providers extends Dispose { 6 | constructor() { 7 | super(); 8 | this.push(registerHotReloadProvider(), pubUpdateProvider()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/provider/pub.ts: -------------------------------------------------------------------------------- 1 | import { Disposable, workspace, commands } from 'coc.nvim'; 2 | 3 | import { getFlutterWorkspaceFolder } from '../util/fs'; 4 | import { logger } from '../util/logger'; 5 | 6 | const log = logger.getlog('Pub provider'); 7 | 8 | export const pubUpdateProvider = (): Disposable => { 9 | return workspace.onDidSaveTextDocument(async (document) => { 10 | if (document.uri && document.uri.endsWith('pubspec.yaml')) { 11 | const workspaceFolder = await getFlutterWorkspaceFolder(); 12 | if (!workspaceFolder) { 13 | log('Flutter project not found!'); 14 | return; 15 | } 16 | commands.executeCommand('flutter.pub.get'); 17 | } 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/server/dev/index.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcessWithoutNullStreams, spawn } from 'child_process'; 2 | import { Disposable, OutputChannel, workspace } from 'coc.nvim'; 3 | import os from 'os'; 4 | import { notification } from '../../lib/notification'; 5 | import { flutterSDK } from '../../lib/sdk'; 6 | import { devLogName, lineBreak } from '../../util/constant'; 7 | import { Dispose } from '../../util/dispose'; 8 | import { getFlutterWorkspaceFolder } from '../../util/fs'; 9 | import { logger } from '../../util/logger'; 10 | 11 | const log = logger.getlog('server'); 12 | 13 | type callback = (...params: any[]) => void; 14 | 15 | class DevServer extends Dispose { 16 | private outputChannel: OutputChannel | undefined; 17 | private task: ChildProcessWithoutNullStreams | undefined; 18 | private onHandler: callback[] = []; 19 | private isAutoScroll = false; 20 | 21 | constructor() { 22 | super(); 23 | this.push({ 24 | dispose: () => { 25 | if (this.task) { 26 | try { 27 | this.task.kill(); 28 | this.task = undefined; 29 | } catch (error) { 30 | log(`dispose server error: ${error.message}`); 31 | } 32 | } 33 | }, 34 | }); 35 | } 36 | 37 | private _onError = (err: Error) => { 38 | this.task = undefined; 39 | log(`server error: ${err.message}`); 40 | }; 41 | 42 | private _onExit = (code: number) => { 43 | this.task = undefined; 44 | const config = workspace.getConfiguration('flutter'); 45 | if (config.get('autoHideDevLog', false) && this.outputChannel) { 46 | this.outputChannel.hide(); 47 | } 48 | log(`server exit with: ${code}`); 49 | }; 50 | 51 | private devLog(message: string) { 52 | if (this.outputChannel) { 53 | this.outputChannel.append(message); 54 | } 55 | } 56 | 57 | get state(): boolean { 58 | return !!this.task && this.task.stdin.writable; 59 | } 60 | 61 | async start(args: string[]): Promise { 62 | if (this.task && this.task.stdin.writable) { 63 | notification.show('Flutter dev server is running!'); 64 | return false; 65 | } 66 | const workspaceFolder = await getFlutterWorkspaceFolder(); 67 | if (!workspaceFolder) { 68 | notification.show('Flutter project workspaceFolder not found!'); 69 | return false; 70 | } 71 | 72 | log(`server start at: ${workspaceFolder}`); 73 | notification.show('Start flutter dev server...'); 74 | 75 | if (this.outputChannel) { 76 | this.outputChannel.clear(); 77 | } else { 78 | this.outputChannel = logger.devOutchannel; 79 | } 80 | 81 | this.task = spawn(flutterSDK.flutterCommand, args, { 82 | cwd: workspaceFolder, 83 | detached: false, 84 | shell: os.platform() === 'win32' ? true : undefined, 85 | }); 86 | this.task.on('exit', this._onExit); 87 | this.task.on('error', this._onError); 88 | 89 | const config = workspace.getConfiguration('flutter'); 90 | if (config.get('autoOpenDevLog', false)) { 91 | this.openDevLog(true); 92 | } 93 | 94 | if (this.onHandler.length) { 95 | this.onHandler.forEach((cb) => cb()); 96 | this.onHandler = []; 97 | } 98 | return true; 99 | } 100 | 101 | onExit(handler: (...params: any[]) => any) { 102 | const callback = () => { 103 | this.task!.on('exit', handler); 104 | }; 105 | if (this.task) { 106 | callback(); 107 | } else { 108 | this.onHandler.push(callback); 109 | } 110 | } 111 | 112 | onError(handler: (...params: any[]) => any) { 113 | if (this.task) { 114 | this.task.on('error', handler); 115 | } else { 116 | this.onHandler.push(() => { 117 | this.task!.on('error', handler); 118 | }); 119 | } 120 | } 121 | 122 | onData(channel: 'stdout' | 'stderr', handler: (lines: string[]) => void) { 123 | const callback = () => { 124 | this.task![channel].on('data', (chunk: Buffer) => { 125 | const text = chunk.toString(); 126 | this.devLog(text); 127 | handler(text.split(lineBreak)); 128 | }); 129 | }; 130 | if (this.task && this.task[channel]) { 131 | callback(); 132 | } else { 133 | this.onHandler.push(callback); 134 | } 135 | } 136 | 137 | onStdout(handler: (lines: string[]) => void) { 138 | this.onData('stdout', handler); 139 | } 140 | 141 | onStderr(handler: (lines: string[]) => void) { 142 | this.onData('stderr', handler); 143 | } 144 | 145 | sendCommand(cmd?: string) { 146 | if (!cmd) { 147 | return; 148 | } 149 | if (this.task && this.task.stdin.writable) { 150 | this.task.stdin.write(cmd); 151 | } else { 152 | notification.show('Flutter server is not running!'); 153 | } 154 | } 155 | 156 | clearDevLog() { 157 | if (this.outputChannel) { 158 | this.outputChannel.clear(); 159 | } 160 | } 161 | 162 | async openDevLog(preserveFocus?: boolean) { 163 | const config = workspace.getConfiguration('flutter'); 164 | const cmd = config.get('openDevLogSplitCommand', ''); 165 | if (this.outputChannel) { 166 | if (!cmd) { 167 | this.outputChannel.show(preserveFocus); 168 | } else { 169 | const win = await workspace.nvim.window; 170 | await workspace.nvim.command(`${cmd} output:///${devLogName}`); 171 | if (!preserveFocus) { 172 | workspace.nvim.call('win_gotoid', [win.id]); 173 | } 174 | } 175 | } 176 | setTimeout(() => { 177 | this.autoScrollLogWin(); 178 | }, 1000); 179 | } 180 | 181 | async autoScrollLogWin() { 182 | if (this.isAutoScroll) { 183 | return; 184 | } 185 | this.isAutoScroll = true; 186 | const buffers = await workspace.nvim.buffers; 187 | for (const buf of buffers) { 188 | const name = await buf.name; 189 | log(`bufName ${name}`); 190 | if (name === `output:///${devLogName}`) { 191 | // FIXME: coc.nvim version v0.80.0 do not export attach function 192 | const isAttach = await (buf as any).attach(false); 193 | if (!isAttach) { 194 | log(`Attach buf ${name} error`); 195 | this.isAutoScroll = false; 196 | return; 197 | } 198 | this.isAutoScroll = true; 199 | // FIXME: coc.nvim version v0.80.0 do not export listen function 200 | (buf as any).listen('lines', async () => { 201 | const wins = await workspace.nvim.windows; 202 | if (!wins || !wins.length) { 203 | return; 204 | } 205 | for (const win of wins) { 206 | try { 207 | const b = await win.buffer; 208 | const name = await b.name; 209 | if (name === `output:///${devLogName}`) { 210 | const lines = await buf.length; 211 | const curWin = await workspace.nvim.window; 212 | // do not scroll when log win get focus 213 | if (win.id === curWin.id) { 214 | return; 215 | } 216 | win.setCursor([lines, 0]); 217 | break; 218 | } 219 | } catch (e) {} 220 | } 221 | }); 222 | // FIXME: coc.nvim version v0.80.0 do not export listen function 223 | (buf as any).listen('detach', () => { 224 | if (this.isAutoScroll) { 225 | log(`Unexpected detach buf ${name}`); 226 | this.isAutoScroll = false; 227 | } 228 | }); 229 | this.push( 230 | Disposable.create(() => { 231 | if (this.isAutoScroll) { 232 | this.isAutoScroll = false; 233 | try { 234 | // FIXME: coc.nvim version v0.80.0 do not export these function 235 | (buf as any).removeAllListeners(); 236 | (buf as any).detach(); 237 | } catch (error) { 238 | log(`Detach error ${error.message || error}`); 239 | } 240 | } 241 | }), 242 | ); 243 | break; 244 | } 245 | } 246 | this.isAutoScroll = false; 247 | } 248 | } 249 | 250 | export const devServer = new DevServer(); 251 | -------------------------------------------------------------------------------- /src/server/devtools/index.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcessWithoutNullStreams, spawn } from 'child_process'; 2 | import { window } from 'coc.nvim'; 3 | import os from 'os'; 4 | import { notification } from '../../lib/notification'; 5 | import { flutterSDK } from '../../lib/sdk'; 6 | import { Dispose } from '../../util/dispose'; 7 | import { getFlutterWorkspaceFolder } from '../../util/fs'; 8 | import { logger } from '../../util/logger'; 9 | 10 | const log = logger.getlog('devtools-server'); 11 | 12 | type callback = (...params: any[]) => void; 13 | 14 | class DevToolsServer extends Dispose { 15 | private launchDevToolsTask: ChildProcessWithoutNullStreams | undefined; 16 | private activateDevToolsTask: ChildProcessWithoutNullStreams | undefined; 17 | private port: number | undefined; 18 | private PID: number | undefined; 19 | private host: string | undefined; 20 | private onHandler: callback[] = []; 21 | 22 | constructor() { 23 | super(); 24 | this.push({ 25 | dispose: () => { 26 | this.port = undefined; 27 | this.PID = undefined; 28 | this.host = undefined; 29 | if (this.launchDevToolsTask) { 30 | try { 31 | this.launchDevToolsTask.kill(); 32 | this.launchDevToolsTask = undefined; 33 | } catch (error) { 34 | log(`dispose server error: ${error.message}`); 35 | } 36 | } 37 | }, 38 | }); 39 | } 40 | 41 | get state(): boolean { 42 | return !!(this.launchDevToolsTask && this.port && this.host && this.PID); 43 | } 44 | 45 | get devToolsUri(): string | undefined { 46 | return this.state ? `${this.host}:${this.port}` : undefined; 47 | } 48 | 49 | private _onError = (err: Error) => { 50 | this.launchDevToolsTask = undefined; 51 | log(`devtools server error: ${err.message}`); 52 | }; 53 | 54 | private _onExit = (code: number) => { 55 | this.launchDevToolsTask = undefined; 56 | log(`devtools server exit with: ${code}`); 57 | }; 58 | 59 | async start(): Promise { 60 | if (this.state) { 61 | notification.show('Flutter devtools is running!'); 62 | return false; 63 | } 64 | 65 | const workspaceFolder: string | undefined = await getFlutterWorkspaceFolder(); 66 | if (!workspaceFolder) { 67 | notification.show('Flutter project workspaceFolder not found!'); 68 | return false; 69 | } 70 | 71 | notification.show('Launching flutter devtools...'); 72 | 73 | // run devtools server, look for an open port if default is unavailable, return output in JSON format 74 | this.launchDevToolsTask = spawn( 75 | flutterSDK.dartCommand, 76 | ['pub', 'global', 'run', 'devtools', '--machine', '--try-ports', '10'], 77 | { 78 | cwd: workspaceFolder, 79 | detached: false, 80 | shell: os.platform() === 'win32' ? true : undefined, 81 | }, 82 | ); 83 | this.launchDevToolsTask.on('exit', this._onExit); 84 | this.launchDevToolsTask.on('error', this._onError); 85 | 86 | if (this.onHandler.length) { 87 | this.onHandler.forEach((cb) => cb()); 88 | this.onHandler = []; 89 | } 90 | return true; 91 | } 92 | 93 | onStdout(handler: callback): void { 94 | const callback = () => { 95 | this.launchDevToolsTask!['stdout'].on('data', (chunk: Buffer) => { 96 | const text = chunk.toString(); 97 | // expecting JSON output because of --machine flag 98 | try { 99 | const json = JSON.parse(text); 100 | this.port = json['params']['port']; 101 | this.PID = json['params']['pid']; 102 | this.host = json['params']['host']; 103 | log(`Devtools running at ${this.host}:${this.port} with PID: ${this.PID}`); 104 | } catch (error) { 105 | log(`error while parsing ${text}:`); 106 | log(error); 107 | } 108 | handler(); 109 | }); 110 | }; 111 | if (this.launchDevToolsTask && this.launchDevToolsTask['stdout']) { 112 | callback(); 113 | } else { 114 | this.onHandler.push(callback); 115 | } 116 | } 117 | 118 | onStderr(handler: callback): void { 119 | const callback = () => { 120 | this.launchDevToolsTask!['stderr'].on('data', (chunk: Buffer) => { 121 | const text = chunk.toString(); 122 | 123 | // If devtools hasn't been activated, the process will write a message to stderr 124 | // Check for this message, prompt user to let us activate devtools, and continue if they accept 125 | const m = text.match(/No active package devtools/g); 126 | if (m) { 127 | const devToolsActivationPrompt = window.showPrompt( 128 | 'Flutter pub global devtools has not been activated. Activate now?', 129 | ); 130 | this.handleDevToolsActivationPrompt(devToolsActivationPrompt, handler); 131 | } 132 | // If devtools fails for some other unknown reason, log the output 133 | else { 134 | log(text); 135 | } 136 | }); 137 | }; 138 | if (this.launchDevToolsTask && this.launchDevToolsTask['stderr']) { 139 | callback(); 140 | } else { 141 | this.onHandler.push(callback); 142 | } 143 | } 144 | 145 | async handleDevToolsActivationPrompt(activationPrompt: Promise, handler: callback) { 146 | const value = await activationPrompt; 147 | if (value) { 148 | const workspaceFolder: string | undefined = await getFlutterWorkspaceFolder(); 149 | if (!workspaceFolder) { 150 | notification.show('Flutter project workspaceFolder not found!'); 151 | return false; 152 | } 153 | this.activateDevToolsTask = spawn( 154 | flutterSDK.dartCommand, 155 | ['pub', 'global', 'activate', 'devtools'], 156 | { 157 | cwd: workspaceFolder, 158 | detached: false, 159 | shell: os.platform() === 'win32' ? true : undefined, 160 | }, 161 | ); 162 | this.activateDevToolsTask.on('exit', () => { 163 | handler(); 164 | this.activateDevToolsTask = undefined; 165 | }); 166 | this.activateDevToolsTask.on('error', (err: Error) => { 167 | notification.show(`Error activating devtools: ${err.message}`); 168 | log(err.message); 169 | this.activateDevToolsTask = undefined; 170 | }); 171 | this.push({ 172 | dispose: () => { 173 | if (this.activateDevToolsTask) { 174 | try { 175 | this.activateDevToolsTask.kill(); 176 | this.activateDevToolsTask = undefined; 177 | } catch (err) { 178 | log(`Error disposing activateDevToolsTask: ${err.message}`); 179 | } 180 | } 181 | }, 182 | }); 183 | } else { 184 | notification.show( 185 | 'You must run "Flutter pub global activate devtools" to launch a devtools browser debugger.', 186 | ); 187 | } 188 | } 189 | } 190 | 191 | export const devToolsServer = new DevToolsServer(); 192 | -------------------------------------------------------------------------------- /src/server/lsp/closingLabels.ts: -------------------------------------------------------------------------------- 1 | import { LanguageClient, workspace } from 'coc.nvim'; 2 | import { Range } from 'vscode-languageserver-protocol'; 3 | 4 | import { Dispose } from '../../util/dispose'; 5 | import { logger } from '../../util/logger'; 6 | 7 | const log = logger.getlog('lsp-closing-labels'); 8 | 9 | // closing label namespace 10 | const virtualNamespace = 'flutter-closing-lablel'; 11 | // closing label highlight group 12 | const flutterClosingLabel = 'FlutterClosingLabel'; 13 | 14 | interface ClosingLabelsParams { 15 | uri: string; 16 | labels: { 17 | label: string; 18 | range: Range; 19 | }[]; 20 | } 21 | 22 | export class ClosingLabels extends Dispose { 23 | private nsIds: Record = {}; 24 | 25 | constructor(client: LanguageClient) { 26 | super(); 27 | this.init(client); 28 | log('register closing labels'); 29 | } 30 | 31 | async init(client: LanguageClient) { 32 | const { nvim } = workspace; 33 | // vim do not support virtual text 34 | if (!nvim.hasFunction('nvim_buf_set_virtual_text')) { 35 | return; 36 | } 37 | await nvim.command(`highlight default link ${flutterClosingLabel} Comment`); 38 | client.onNotification('dart/textDocument/publishClosingLabels', this.onClosingLabels); 39 | } 40 | 41 | onClosingLabels = async (params: ClosingLabelsParams) => { 42 | const { uri, labels } = params; 43 | if (!labels.length) { 44 | return; 45 | } 46 | const doc = workspace.getDocument(uri); 47 | // ensure the document is exists 48 | if (!doc) { 49 | return; 50 | } 51 | 52 | const { nvim } = workspace; 53 | const { buffer } = doc; 54 | 55 | // clear previous virtual text 56 | if (this.nsIds[uri] !== undefined) { 57 | buffer.clearNamespace(this.nsIds[uri], 1, -1); 58 | } 59 | 60 | this.nsIds[uri] = await nvim.createNamespace(virtualNamespace); 61 | nvim.pauseNotification(); 62 | for (const label of labels) { 63 | buffer.setVirtualText(this.nsIds[uri], label.range.end.line, [ 64 | [`// ${label.label}`, flutterClosingLabel], 65 | ]); 66 | } 67 | await nvim.resumeNotification(); 68 | }; 69 | 70 | dispose() { 71 | super.dispose(); 72 | // clear closing labels 73 | const uris = Object.keys(this.nsIds); 74 | if (uris.length) { 75 | uris.forEach((uri) => { 76 | const doc = workspace.getDocument(uri); 77 | if (!doc) { 78 | return; 79 | } 80 | doc.buffer.clearNamespace(this.nsIds[uri], 1, -1); 81 | }); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/server/lsp/codeActionProvider.ts: -------------------------------------------------------------------------------- 1 | import { ProvideCodeActionsSignature, CodeAction } from 'coc.nvim'; 2 | import { 3 | TextDocument, 4 | CodeActionContext, 5 | CancellationToken, 6 | Range, 7 | TextDocumentEdit, 8 | } from 'vscode-languageserver-protocol'; 9 | 10 | // add delete widget action 11 | 12 | export const codeActionProvider = async ( 13 | document: TextDocument, 14 | range: Range, 15 | context: CodeActionContext, 16 | token: CancellationToken, 17 | next: ProvideCodeActionsSignature, 18 | ) => { 19 | let res = await next(document, range, context, token); 20 | if (!res) { 21 | return res; 22 | } 23 | 24 | res = res.map((item) => { 25 | if ((item as CodeAction).kind) { 26 | if ((item as CodeAction).kind!.startsWith('quickfix.import')) { 27 | return { 28 | ...item, 29 | isPreferred: true, 30 | }; 31 | } 32 | } 33 | return item; 34 | }); 35 | 36 | const codeActions = res.slice(); 37 | res.some((item) => { 38 | if (item.title === 'Wrap with widget...' && (item as CodeAction).edit) { 39 | const edit = (item as CodeAction).edit; 40 | if (!edit || !edit.documentChanges) { 41 | return true; 42 | } 43 | const documentChanges = edit.documentChanges as TextDocumentEdit[]; 44 | if (!documentChanges || documentChanges.length === 0) { 45 | return true; 46 | } 47 | const codeAction = { 48 | ...item, 49 | title: 'Delete this widget', 50 | edit: { 51 | ...edit, 52 | documentChanges: [ 53 | { 54 | ...documentChanges[0], 55 | edits: [ 56 | { 57 | ...(documentChanges[0].edits[0] || {}), 58 | newText: '', 59 | }, 60 | ], 61 | }, 62 | ], 63 | }, 64 | }; 65 | codeActions.push(codeAction); 66 | return true; 67 | } 68 | return false; 69 | }); 70 | return codeActions; 71 | }; 72 | -------------------------------------------------------------------------------- /src/server/lsp/completionProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CancellationToken, 3 | CompletionContext, 4 | CompletionItem, 5 | CompletionList, 6 | Position, 7 | ProvideCompletionItemsSignature, 8 | Range, 9 | TextDocument, 10 | workspace, 11 | } from 'coc.nvim'; 12 | import { getResolveCompleteItemFunc } from './resolveCompleteItem'; 13 | 14 | export const completionProvider = async ( 15 | document: TextDocument, 16 | position: Position, 17 | context: CompletionContext, 18 | token: CancellationToken, 19 | next: ProvideCompletionItemsSignature, 20 | ): Promise => { 21 | let list: CompletionItem[] = []; 22 | try { 23 | const character = document.getText( 24 | Range.create(Position.create(position.line, position.character - 1), position), 25 | ); 26 | const res = await next(document, position, context, token); 27 | // CompletionItem[] or CompletionList 28 | if (res != null) { 29 | if ((res as CompletionList)?.isIncomplete != null) { 30 | list = (res as CompletionList).items; 31 | } else { 32 | list = res as CompletionItem[]; 33 | } 34 | } 35 | // reduce items since it's too many 36 | // ref: https://github.com/dart-lang/sdk/issues/42152 37 | if (list.length > 1000 && /[a-zA-Z]/i.test(character)) { 38 | list = list.filter((item) => new RegExp(character, 'i').test(item.label)); 39 | } 40 | const config = workspace.getConfiguration('dart'); 41 | const resolveCompleteItem = getResolveCompleteItemFunc({ 42 | completeFunctionCalls: config.get('completeFunctionCalls', true), 43 | }); 44 | // resolve complete item 45 | list = list.map(resolveCompleteItem); 46 | return (res as CompletionList)?.isIncomplete != null 47 | ? { 48 | items: list, 49 | isIncomplete: (res as CompletionList)?.isIncomplete ?? false, 50 | } 51 | : list; 52 | } catch (e) { 53 | return list; 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/server/lsp/extractProvider.ts: -------------------------------------------------------------------------------- 1 | import { ExecuteCommandSignature, window } from 'coc.nvim'; 2 | import { 3 | validClassNameRegex, 4 | validMethodNameRegex, 5 | validVariableNameRegex, 6 | } from '../../util/constant'; 7 | 8 | // code from https://github.com/Dart-Code/Dart-Code/commit/48b3f213fe1efb6f4aa00bebbf7ee8b0a58f21d6 9 | export const executeCommandProvider = async ( 10 | command: string, 11 | args: any[], 12 | next: ExecuteCommandSignature, 13 | ) => { 14 | if (command === 'refactor.perform') { 15 | const expectedCount = 6; 16 | if (args && args.length === expectedCount) { 17 | const refactorFailedErrorCode = -32011; 18 | const refactorKind = args[0]; 19 | const optionsIndex = 5; 20 | // Intercept EXTRACT_METHOD and EXTRACT_WIDGET to prompt the user for a name, since 21 | // LSP doesn't currently allow us to prompt during a code-action invocation. 22 | let name: string | undefined; 23 | switch (refactorKind) { 24 | case 'EXTRACT_METHOD': 25 | name = await window.requestInput('Enter a name for the method', 'NewMethod'); 26 | if (!name) return; 27 | if (!validMethodNameRegex.test(name)) { 28 | return window.showErrorMessage('Enter a valid method name'); 29 | } 30 | args[optionsIndex] = Object.assign({}, args[optionsIndex], { name }); 31 | break; 32 | case 'EXTRACT_WIDGET': 33 | name = await window.requestInput('Enter a name for the widget', 'NewWidget'); 34 | if (!name) return; 35 | if (!validClassNameRegex.test(name)) { 36 | return window.showErrorMessage('Enter a valid widget name'); 37 | } 38 | args[optionsIndex] = Object.assign({}, args[optionsIndex], { name }); 39 | break; 40 | case 'EXTRACT_LOCAL_VARIABLE': 41 | name = await window.requestInput('Enter a name for the variable', 'NewVariable'); 42 | if (!name) return; 43 | if (!validVariableNameRegex.test(name)) { 44 | return window.showErrorMessage('Enter a valid variable name'); 45 | } 46 | args[optionsIndex] = Object.assign({}, args[optionsIndex], { name }); 47 | break; 48 | } 49 | 50 | // The server may return errors for things like invalid names, so 51 | // capture the errors and present the error better if it's a refactor 52 | // error. 53 | try { 54 | return await next(command, args); 55 | } catch (e) { 56 | window.showErrorMessage(e.message); 57 | if (e?.code === refactorFailedErrorCode) { 58 | window.showErrorMessage(e.message); 59 | return; 60 | } else { 61 | throw e; 62 | } 63 | } 64 | } 65 | } 66 | return next(command, args); 67 | }; 68 | -------------------------------------------------------------------------------- /src/server/lsp/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Executable, 3 | ExecutableOptions, 4 | LanguageClient, 5 | LanguageClientOptions, 6 | OutputChannel, 7 | RevealOutputChannelOn, 8 | ServerOptions, 9 | services, 10 | Uri, 11 | window, 12 | workspace, 13 | } from 'coc.nvim'; 14 | import { homedir } from 'os'; 15 | import { flutterSDK } from '../../lib/sdk'; 16 | import { statusBar } from '../../lib/status'; 17 | import { Dispose } from '../../util/dispose'; 18 | import { logger } from '../../util/logger'; 19 | import { ClosingLabels } from './closingLabels'; 20 | import { Outline } from './outline'; 21 | import { codeActionProvider } from './codeActionProvider'; 22 | import { completionProvider } from './completionProvider'; 23 | import { executeCommandProvider } from './extractProvider'; 24 | import { SignatureHelpProvider } from './signatureHelp'; 25 | 26 | const log = logger.getlog('lsp-server'); 27 | 28 | class _ExecOptions implements Executable { 29 | get command(): string { 30 | return flutterSDK.dartCommand; 31 | } 32 | get args(): string[] { 33 | return [flutterSDK.analyzerSnapshotPath, '--lsp']; 34 | } 35 | options?: ExecutableOptions | undefined; 36 | } 37 | 38 | export class LspServer extends Dispose { 39 | private _client: LanguageClient | undefined; 40 | 41 | constructor() { 42 | super(); 43 | this.init(); 44 | } 45 | 46 | public get client(): LanguageClient | undefined { 47 | return this._client; 48 | } 49 | 50 | private execOptions = new _ExecOptions(); 51 | private outchannel?: OutputChannel; 52 | 53 | async init(): Promise { 54 | this.outchannel = window.createOutputChannel('flutter-lsp'); 55 | this.push(this.outchannel); 56 | const config = workspace.getConfiguration('flutter'); 57 | // is force lsp debug 58 | const isLspDebug = config.get('lsp.debug'); 59 | // dart sdk analysis snapshot path 60 | if (!flutterSDK.state) { 61 | await flutterSDK.init(config); 62 | } 63 | 64 | if (!flutterSDK.state) { 65 | log('flutter SDK not found!'); 66 | return; 67 | } 68 | 69 | // TODO: debug options 70 | // If the extension is launched in debug mode then the debug server options are used 71 | // Otherwise the run options are used 72 | const serverOptions: ServerOptions = { 73 | run: this.execOptions, 74 | debug: this.execOptions, 75 | }; 76 | 77 | // lsp initialization 78 | const initialization = config.get('lsp.initialization', { 79 | onlyAnalyzeProjectsWithOpenFiles: true, 80 | suggestFromUnimportedLibraries: true, 81 | closingLabels: true, 82 | outline: true, 83 | }); 84 | 85 | /** 86 | * disable disableDynamicRegister for version less then 2.6.0 87 | * issue: https://github.com/dart-lang/sdk/issues/38490 88 | */ 89 | const rightVersion = await flutterSDK.isVersionGreatOrEqualTo([2, 6, 0]); 90 | log(`rightVersion ${rightVersion}`); 91 | 92 | // Options to control the language client 93 | const clientOptions: LanguageClientOptions = { 94 | disableDynamicRegister: !rightVersion, 95 | // Register the server for dart document 96 | documentSelector: [ 97 | { 98 | scheme: 'file', 99 | language: 'dart', 100 | }, 101 | ], 102 | 103 | initializationOptions: initialization, 104 | 105 | outputChannel: this.outchannel, 106 | // do not automatically open outchannel 107 | revealOutputChannelOn: RevealOutputChannelOn.Never, 108 | 109 | middleware: { 110 | provideCompletionItem: config.get('provider.enableSnippet', true) 111 | ? completionProvider 112 | : undefined, 113 | provideCodeActions: codeActionProvider, 114 | executeCommand: executeCommandProvider, 115 | workspace: { 116 | didChangeWorkspaceFolders(data, next) { 117 | if (data.added.length && flutterSDK.sdkHome !== '') { 118 | const ignore = config 119 | .get('workspaceFolder.ignore', []) 120 | .concat(flutterSDK.sdkHome) 121 | .map((p) => { 122 | p = p.replace(/^(~|\$HOME)/, homedir()); 123 | return Uri.file(p).toString(); 124 | }); 125 | data.added = data.added.filter((fold) => !ignore.some((i) => fold.uri.startsWith(i))); 126 | } 127 | if (data.added.length || data.removed.length) { 128 | next(data); 129 | } 130 | }, 131 | }, 132 | }, 133 | }; 134 | 135 | // Create the language client and start the client. 136 | const client = new LanguageClient( 137 | `flutter`, 138 | 'flutter analysis server', 139 | serverOptions, 140 | clientOptions, 141 | isLspDebug, 142 | ); 143 | this._client = client; 144 | 145 | if (!statusBar.isInitialized) { 146 | statusBar.init(); 147 | this.push(statusBar); 148 | } 149 | 150 | client 151 | .onReady() 152 | .then(() => { 153 | log('analysis server ready!'); 154 | if (initialization.closingLabels) this.push(new ClosingLabels(client)); 155 | this.push(new Outline(client)); 156 | if (initialization.closingLabels) { 157 | // register closing label 158 | this.push(new ClosingLabels(client)); 159 | } 160 | // FIXME 161 | setTimeout(() => { 162 | // https://github.com/iamcco/coc-flutter/issues/8 163 | this.push(new SignatureHelpProvider(client)); 164 | }, 2000); 165 | statusBar.ready(); 166 | }) 167 | .catch((error: Error) => { 168 | statusBar.hide(); 169 | log(error.message || 'start analysis server fail!'); 170 | log(error.stack); 171 | }); 172 | 173 | // Push the disposable to the context's subscriptions so that the 174 | // client can be deactivated on extension deactivation 175 | this.push(services.registLanguageClient(client)); 176 | } 177 | 178 | async reloadSdk(): Promise { 179 | const config = workspace.getConfiguration('flutter'); 180 | await flutterSDK.init(config); 181 | } 182 | 183 | async restart(): Promise { 184 | statusBar.restartingLsp(); 185 | await this.reloadSdk(); 186 | await this._client?.stop(); 187 | this._client?.onReady().then(() => { 188 | statusBar.ready(); 189 | }); 190 | this._client?.start(); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/server/lsp/outline.ts: -------------------------------------------------------------------------------- 1 | import { commands, LanguageClient, workspace } from 'coc.nvim'; 2 | import { statusBar } from '../../lib/status'; 3 | import { cmdPrefix } from '../../util/constant'; 4 | import { Range } from 'vscode-languageserver-protocol'; 5 | 6 | import { Dispose } from '../../util/dispose'; 7 | 8 | const verticalLine = '│'; 9 | // const horizontalLine = '─'; 10 | const bottomCorner = '└'; 11 | const middleCorner = '├'; 12 | const icons = { 13 | TOP_LEVEL_VARIABLE: '\uf435 ', 14 | CLASS: '\uf0e8 ', 15 | FIELD: '\uf93d', 16 | CONSTRUCTOR: '\ue624 ', 17 | CONSTRUCTOR_INVOCATION: '\ufc2a ', 18 | FUNCTION: '\u0192 ', 19 | METHOD: '\uf6a6 ', 20 | GETTER: '\uf9fd', 21 | ENUM: '\uf779 ', 22 | ENUM_CONSTANT: '\uf02b ', 23 | }; 24 | const icon_default = '\ue612 '; 25 | const outlineBufferName = 'Flutter Outline'; 26 | 27 | function ucs2ToBinaryString(str: string) { 28 | const escstr = encodeURIComponent(str); 29 | const binstr = escstr.replace(/%([0-9A-F]{2})/gi, function (_, hex) { 30 | const i = parseInt(hex, 16); 31 | return String.fromCharCode(i); 32 | }); 33 | return binstr; 34 | } 35 | 36 | interface ClientParams_Outline { 37 | uri: string; 38 | outline: OutlineParams; 39 | } 40 | 41 | interface OutlineParams { 42 | element: ElementParams; 43 | range: Range; 44 | codeRange: Range; 45 | children: OutlineParams[]; 46 | folded: boolean; 47 | lineNumber: number | undefined; 48 | startCol: number | undefined; 49 | endCol: number | undefined; 50 | } 51 | 52 | interface ElementParams { 53 | name: string; 54 | range: Range; 55 | kind: string; 56 | parameters: string | undefined; 57 | typeParameters: string | undefined; 58 | returnType: string | undefined; 59 | } 60 | 61 | export class Outline extends Dispose { 62 | public outlines: Record = {}; 63 | public outlineStrings: Record = {}; 64 | // the corresponding outline item for each line number in the outline panel 65 | public outlinePanelData: Record = {}; 66 | public outlineVersions: Record = {}; 67 | public outlineVersions_Rendered: Record = {}; 68 | public renderedOutlineUri = ''; 69 | public outlineBuffer: any; 70 | public curOutlineItem: OutlineParams | undefined; 71 | public highlightIds: number[] = []; 72 | public showPath: boolean | undefined; 73 | public outlineWidth = 30; 74 | public curUri = ''; 75 | public iconSpacing = ''; 76 | 77 | constructor(client: LanguageClient) { 78 | super(); 79 | this.init(client); 80 | const config = workspace.getConfiguration('flutter'); 81 | this.showPath = config.get('UIPath', true); 82 | this.outlineWidth = config.get('outlineWidth', 30); 83 | this.iconSpacing = ' '.repeat(config.get('outlineIconPadding', 0)); 84 | } 85 | 86 | generateOutlineStrings = (uri: string) => { 87 | const root = this.outlines[uri]; 88 | const lines: string[] = []; 89 | const outlineItems: OutlineParams[] = []; 90 | const iconSpacing = this.iconSpacing; 91 | function genOutline(outline: OutlineParams, indentStr: string) { 92 | let indent = indentStr; 93 | let foldIndicator = ' '; 94 | let icon = icons[outline.element.kind]; 95 | if (icon === undefined) icon = icon_default; 96 | // icon += ' '; 97 | if (Array.isArray(outline.children) && outline.children.length > 0 && outline.folded === true) 98 | foldIndicator = '▸ '; 99 | const newLine = `${indent} ${icon}${iconSpacing}${outline.element.name}: ${ 100 | outline.codeRange.start.line + 1 101 | }`; 102 | outline.lineNumber = lines.length; 103 | outline.startCol = ucs2ToBinaryString(indent).length; 104 | outline.endCol = ucs2ToBinaryString(newLine).length; 105 | lines.push(newLine); 106 | outlineItems.push(outline); 107 | const len = indent.length; 108 | if (len > 0) { 109 | if (indent[len - 1] == middleCorner) { 110 | indent = indent.substr(0, len - 1) + verticalLine; 111 | } else if (indent[len - 1] == bottomCorner) { 112 | indent = indent.substr(0, len - 1) + ' '; 113 | } 114 | } 115 | if (Array.isArray(outline.children)) 116 | if (outline.children.length == 1) { 117 | genOutline(outline.children[0], `${indent} `); 118 | } else if (outline.children.length > 1) { 119 | for (let i = 0; i < outline.children.length; ++i) { 120 | if (i == outline.children.length - 1) { 121 | // indent = indent.substr(0, len - 2) + ' '; 122 | genOutline(outline.children[i], `${indent}${bottomCorner}`); 123 | } else { 124 | genOutline(outline.children[i], `${indent}${middleCorner}`); 125 | } 126 | } 127 | } 128 | } 129 | if (Array.isArray(root.children) && root.children.length > 0) 130 | for (const child of root.children) genOutline(child, ''); 131 | this.outlineStrings[uri] = lines; 132 | this.outlinePanelData[uri] = outlineItems; 133 | if (this.outlineVersions[uri] === undefined) { 134 | this.outlineVersions[uri] = 0; 135 | } else { 136 | this.outlineVersions[uri] += 1; 137 | } 138 | }; 139 | 140 | highlightCurrentOutlineItem = async () => { 141 | if ( 142 | this.curOutlineItem !== undefined && 143 | this.outlineBuffer !== undefined && 144 | this.curOutlineItem.lineNumber !== undefined && 145 | this.curOutlineItem.startCol !== undefined && 146 | this.curOutlineItem.endCol !== undefined 147 | ) { 148 | const windows = await workspace.nvim.windows; 149 | for (const win of windows) { 150 | try { 151 | const buf = await win.buffer; 152 | if (buf.id === this.outlineBuffer.id) { 153 | buf.clearHighlight(); 154 | win.setCursor([this.curOutlineItem.lineNumber + 1, 0]).catch(() => {}); 155 | buf 156 | .addHighlight({ 157 | hlGroup: 'HighlightedOutlineArea', 158 | line: this.curOutlineItem.lineNumber, 159 | colStart: this.curOutlineItem.startCol, 160 | colEnd: this.curOutlineItem.endCol, 161 | }) 162 | .catch(() => {}); 163 | } 164 | } catch (e) {} 165 | } 166 | } 167 | }; 168 | 169 | updateOutlineBuffer = async (uri: string, force = false) => { 170 | if ( 171 | ((this.outlineVersions[uri] === this.outlineVersions_Rendered[uri] && 172 | this.outlineVersions[uri] === undefined) || 173 | this.outlineVersions[uri] !== this.outlineVersions_Rendered[uri] || 174 | uri !== this.renderedOutlineUri || 175 | force) && 176 | this.outlineBuffer 177 | ) { 178 | this.renderedOutlineUri = uri; 179 | let content: string[] = []; 180 | if (this.outlineStrings[uri]) { 181 | this.outlineVersions_Rendered[uri] = this.outlineVersions[uri]; 182 | content = this.outlineStrings[uri]; 183 | } 184 | await this.outlineBuffer.length 185 | .then(async (len: number) => { 186 | if (Number.isInteger(len)) { 187 | await this.outlineBuffer.setOption('modifiable', true); 188 | if (len > content.length) { 189 | await this.outlineBuffer.setLines([], { 190 | start: 0, 191 | end: len - 1, 192 | strictIndexing: false, 193 | }); 194 | await this.outlineBuffer.setLines(content, { 195 | start: 0, 196 | end: 0, 197 | strictIndexing: false, 198 | }); 199 | } else { 200 | await this.outlineBuffer.setLines(content, { 201 | start: 0, 202 | end: len - 1, 203 | strictIndexing: false, 204 | }); 205 | } 206 | await this.outlineBuffer.setOption('modifiable', false); 207 | } 208 | }) 209 | .catch(() => {}); 210 | await this.outlineBuffer.length 211 | .then(async (len: number) => { 212 | await this.outlineBuffer.setOption('modifiable', true); 213 | if (len > content.length) { 214 | await this.outlineBuffer.setLines([], { 215 | start: 0, 216 | end: len - 1, 217 | strictIndexing: false, 218 | }); 219 | await this.outlineBuffer.setLines(content, { 220 | start: 0, 221 | end: 0, 222 | strictIndexing: false, 223 | }); 224 | } 225 | await this.outlineBuffer.setOption('modifiable', false); 226 | }) 227 | .catch(() => {}); 228 | } 229 | await this.highlightCurrentOutlineItem(); 230 | }; 231 | 232 | getUIPathFromCursor(outline: OutlineParams, cursor: number[]) { 233 | let elementPath = ''; 234 | let foundChild = true; 235 | while (foundChild) { 236 | foundChild = false; 237 | if (Array.isArray(outline.children) && outline.children.length > 0) { 238 | for (const child of outline.children) { 239 | const curLine = cursor[0] - 1, 240 | curCol = cursor[1] - 1; 241 | const startLine = child.codeRange.start.line, 242 | startCol = child.codeRange.start.character; 243 | const endLine = child.codeRange.end.line, 244 | endCol = child.codeRange.end.character; 245 | if ( 246 | (curLine > startLine || (curLine == startLine && curCol >= startCol)) && 247 | (curLine < endLine || (curLine == endLine && curCol < endCol)) 248 | ) { 249 | outline = child; 250 | foundChild = true; 251 | break; 252 | } 253 | } 254 | } 255 | if (foundChild) { 256 | elementPath += ` > ${outline.element.name}`; 257 | } else { 258 | break; 259 | } 260 | } 261 | this.curOutlineItem = outline; 262 | if (this.showPath) statusBar.show(elementPath, false); 263 | } 264 | 265 | async getCurrentUri() { 266 | const path = await workspace.nvim.commandOutput('echo expand("%:p")'); 267 | return `file://${path}`; 268 | } 269 | 270 | async init(client: LanguageClient) { 271 | const { nvim } = workspace; 272 | 273 | (nvim as any).on('notification', async (...args) => { 274 | if (args[0] === 'CocAutocmd') { 275 | if (args[1][0] === 'CursorMoved') { 276 | const bufId = args[1][1]; 277 | const cursor = args[1][2]; 278 | if (this.outlineBuffer && bufId === this.outlineBuffer.id) { 279 | } else { 280 | this.curUri = await this.getCurrentUri(); 281 | const outline = this.outlines[this.curUri]; 282 | if (outline) { 283 | this.getUIPathFromCursor(outline, cursor); 284 | this.updateOutlineBuffer(this.curUri); 285 | } 286 | } 287 | } else if (args[1][0] === 'BufEnter' && Number.isInteger(args[1][1])) { 288 | if (this.outlineBuffer && args[1][1] === this.outlineBuffer.id) { 289 | const wins = await nvim.windows; 290 | if (Array.isArray(wins)) { 291 | if (wins.length === 1) { 292 | nvim.command('q'); 293 | } else { 294 | const curWin = await nvim.window; 295 | const curTab = await curWin.tabpage; 296 | let winTabCount = 0; 297 | for (const win of wins) { 298 | const tab = await win.tabpage; 299 | if ((await tab.number) === (await curTab.number)) winTabCount += 1; 300 | } 301 | if (winTabCount === 1) curWin.close(true); 302 | } 303 | } 304 | } 305 | } 306 | } 307 | }); 308 | client.onNotification('dart/textDocument/publishOutline', this.onOutline); 309 | const openOutlinePanel = async () => { 310 | const curWin = await nvim.window; 311 | await nvim.command('set splitright'); 312 | await nvim.command(`${this.outlineWidth}vsplit ${outlineBufferName}`); 313 | const win = await nvim.window; 314 | await nvim.command('setlocal filetype=flutterOutline'); 315 | await nvim.command('set buftype=nofile'); 316 | await nvim.command('setlocal noswapfile'); 317 | await nvim.command('setlocal nomodifiable'); 318 | await nvim.command('setlocal winfixwidth'); 319 | await nvim.command('setlocal nocursorline'); 320 | await nvim.command('setlocal nobuflisted'); 321 | await nvim.command('setlocal bufhidden=wipe'); 322 | await nvim.command('setlocal nonumber'); 323 | await nvim.command('setlocal norelativenumber'); 324 | await nvim.command('setlocal nowrap'); 325 | await nvim.command( 326 | `syntax match OutlineLine /^\\(${verticalLine}\\| \\)*\\(${middleCorner}\\|${bottomCorner}\\)\\?/`, 327 | ); 328 | await nvim.command('highlight default link HighlightedOutlineArea IncSearch'); 329 | await nvim.command(`highlight default link OutlineLine Comment`); 330 | await nvim.command(`syntax match FlutterOutlineFunction /${icons.FUNCTION}/`); 331 | await nvim.command(`highlight default link FlutterOutlineFunction Function`); 332 | await nvim.command(`syntax match FlutterOutlineType /${icons.FIELD}/`); 333 | await nvim.command(`highlight default link FlutterOutlineType Identifier`); 334 | await nvim.command(`syntax match FlutterOutlineClass /${icons.CLASS}/`); 335 | await nvim.command(`highlight default link FlutterOutlineClass Type`); 336 | await nvim.command(`syntax match FlutterOutlineMethod /${icons.METHOD}/`); 337 | await nvim.command(`highlight default link FlutterOutlineMethod Function`); 338 | await nvim.command(`syntax match FlutterOutlineTopLevelVar /${icons.TOP_LEVEL_VARIABLE}/`); 339 | await nvim.command(`highlight default link FlutterOutlineTopLevelVar Identifier`); 340 | await nvim.command(`syntax match FlutterOutlineConstructor /${icons.CONSTRUCTOR}/`); 341 | await nvim.command(`highlight default link FlutterOutlineConstructor Identifier`); 342 | await nvim.command(`syntax match FlutterOutlineGetter /${icons.GETTER}/`); 343 | await nvim.command(`highlight default link FlutterOutlineGetter Function`); 344 | await nvim.command( 345 | `syntax match FlutterOutlineConstructorInvocation /${icons.CONSTRUCTOR_INVOCATION}/`, 346 | ); 347 | await nvim.command(`highlight default link FlutterOutlineConstructorInvocation Special`); 348 | await nvim.command(`syntax match FlutterOutlineEnum /${icons.ENUM}/`); 349 | await nvim.command(`highlight default link FlutterOutlineEnum Type`); 350 | await nvim.command(`syntax match FlutterOutlineEnumMember /${icons.ENUM_CONSTANT}/`); 351 | await nvim.command(`highlight default link FlutterOutlineEnumMember Identifier`); 352 | await nvim.command(`syntax match FlutterOutlineLineNumber /: \\d\\+$/`); 353 | await nvim.command(`highlight default link FlutterOutlineLineNumber Number`); 354 | this.outlineBuffer = await win.buffer; 355 | workspace.registerLocalKeymap('n', '', async () => { 356 | const curWin = await nvim.window; 357 | const cursor = await curWin.cursor; 358 | const outlineItems = this.outlinePanelData[this.curUri]; 359 | if (!Array.isArray(outlineItems)) return; 360 | const outline = outlineItems[cursor[0] - 1]; 361 | if (outline === undefined) return; 362 | const wins = await nvim.windows; 363 | if (Array.isArray(wins)) { 364 | const curWin = await nvim.window; 365 | const curTab = await curWin.tabpage; 366 | for (const win of wins) { 367 | const tab = await win.tabpage; 368 | if ( 369 | (await tab.number) === (await curTab.number) && 370 | `file://${await (await win.buffer).name}` === this.curUri 371 | ) { 372 | win 373 | .setCursor([outline.codeRange.start.line + 1, outline.codeRange.start.character]) 374 | .catch(() => {}); 375 | await nvim.call('win_gotoid', [win.id]); 376 | break; 377 | } 378 | } 379 | } 380 | }); 381 | await nvim.call('win_gotoid', [curWin.id]); 382 | const uri = await this.getCurrentUri(); 383 | this.updateOutlineBuffer(uri, true); 384 | // const buf = await win.buffer; 385 | }; 386 | commands.registerCommand(`${cmdPrefix}.outline`, async () => { 387 | await openOutlinePanel(); 388 | }); 389 | commands.registerCommand(`${cmdPrefix}.toggleOutline`, async () => { 390 | if (this.outlineBuffer === undefined) { 391 | await openOutlinePanel(); 392 | return; 393 | } 394 | const curWin = await nvim.window; 395 | const curTab = await curWin.tabpage; 396 | const wins = await nvim.windows; 397 | let shouldOpenOutlinePanel = true; 398 | for (const win of wins) { 399 | const tab = await win.tabpage; 400 | if ((await tab.number) === (await curTab.number)) { 401 | if ((await win.buffer).id === this.outlineBuffer.id) { 402 | shouldOpenOutlinePanel = false; 403 | win.close(true).catch(() => {}); 404 | } 405 | } 406 | } 407 | if (shouldOpenOutlinePanel) await openOutlinePanel(); 408 | }); 409 | } 410 | 411 | onOutline = async (params: ClientParams_Outline) => { 412 | const { uri, outline } = params; 413 | const doc = workspace.getDocument(uri); 414 | // ensure the document is exists 415 | if (!doc) { 416 | return; 417 | } 418 | 419 | this.outlines[uri] = outline; 420 | this.generateOutlineStrings(uri); 421 | this.updateOutlineBuffer(uri); 422 | }; 423 | } 424 | -------------------------------------------------------------------------------- /src/server/lsp/resolveCompleteItem.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItem, InsertTextFormat } from 'coc.nvim'; 2 | 3 | // functionName(…) 4 | const funcCallWithArgsRegex = /^(.*)\(…\)$/; 5 | // functionName() 6 | const funcCallRegex = /^(.*)\(\)$/; 7 | // keyName: , 8 | const propertyRegex = /^([^ ]+?:\s+),$/; 9 | 10 | export const getResolveCompleteItemFunc = (options: { completeFunctionCalls: boolean }) => ( 11 | item: CompletionItem, 12 | ) => { 13 | const { label, insertTextFormat } = item; 14 | 15 | // delete unnecessary filterText 16 | // ref: https://github.com/iamcco/coc-flutter/issues/70 17 | if (item.insertTextFormat === InsertTextFormat.Snippet && item.filterText) { 18 | delete item.filterText; 19 | } 20 | 21 | if (item.insertTextFormat === InsertTextFormat.Snippet && (item as any).textEditText) { 22 | item.insertText = (item as any).textEditText; 23 | } 24 | 25 | // delete unnecessary textEdit 26 | if (item.textEdit && item.insertText) { 27 | delete item.textEdit; 28 | } 29 | 30 | // remove unnecessary snippet 31 | // snippet xxxx${1:} === xxxx PlainText 32 | if ( 33 | item.insertTextFormat === InsertTextFormat.Snippet && 34 | item.insertText && 35 | item.insertText.endsWith('${1:}') 36 | ) { 37 | item.insertTextFormat = InsertTextFormat.PlainText; 38 | item.insertText = item.insertText.slice(0, -5); 39 | } 40 | 41 | // improve import 42 | if (label === "import '';" && insertTextFormat !== InsertTextFormat.Snippet) { 43 | item.insertText = "import '${1}';${0}"; 44 | item.insertTextFormat = InsertTextFormat.Snippet; 45 | return item; 46 | } 47 | 48 | // improve setState 49 | if (label === 'setState(() {});' && insertTextFormat !== InsertTextFormat.Snippet) { 50 | item.insertText = ['setState(() {', '\t${1}', '});${0}'].join('\n'); 51 | item.insertTextFormat = InsertTextFormat.Snippet; 52 | return item; 53 | } 54 | 55 | // improve `key: ,` 56 | let m = label.match(propertyRegex); 57 | if (m) { 58 | item.insertText = `${m[1]}\${1},\${0}`; 59 | item.insertTextFormat = InsertTextFormat.Snippet; 60 | return item; 61 | } 62 | 63 | // if dart.completeFunctionCalls: false 64 | // do not add `()` snippet 65 | if (options.completeFunctionCalls && item.insertTextFormat !== InsertTextFormat.Snippet) { 66 | // improve function() 67 | m = label.match(funcCallRegex); 68 | if (m) { 69 | item.insertText = `${m[1]}()\${0}`; 70 | item.insertTextFormat = InsertTextFormat.Snippet; 71 | return item; 72 | } 73 | // improve function(…?) 74 | m = label.match(funcCallWithArgsRegex); 75 | if (m) { 76 | item.insertText = `${m[1]}(\${1})\${0}`; 77 | item.insertTextFormat = InsertTextFormat.Snippet; 78 | return item; 79 | } 80 | } 81 | 82 | return item; 83 | }; 84 | -------------------------------------------------------------------------------- /src/server/lsp/signatureHelp.ts: -------------------------------------------------------------------------------- 1 | import { languages, LanguageClient } from 'coc.nvim'; 2 | import { 3 | TextDocument, 4 | Position, 5 | CancellationToken, 6 | SignatureHelp, 7 | } from 'vscode-languageserver-protocol'; 8 | 9 | import { Dispose } from '../../util/dispose'; 10 | 11 | export class SignatureHelpProvider extends Dispose { 12 | constructor(client: LanguageClient) { 13 | super(); 14 | this.push( 15 | languages.registerSignatureHelpProvider( 16 | [ 17 | { 18 | language: 'dart', 19 | scheme: 'file', 20 | }, 21 | ], 22 | { 23 | async provideSignatureHelp( 24 | document: TextDocument, 25 | position: Position, 26 | token: CancellationToken, 27 | ): Promise { 28 | return client.sendRequest( 29 | 'textDocument/signatureHelp', 30 | { 31 | textDocument: { 32 | uri: document.uri, 33 | }, 34 | position, 35 | }, 36 | token, 37 | ); 38 | }, 39 | }, 40 | ['(', ','], 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/sources/devices.ts: -------------------------------------------------------------------------------- 1 | import { IList, ListAction, ListItem, commands, workspace } from 'coc.nvim'; 2 | import colors from 'colors/safe'; 3 | 4 | import { lineBreak } from '../util/constant'; 5 | import { flutterSDK } from '../lib/sdk'; 6 | 7 | interface Device { 8 | name: string; 9 | deviceId: string; 10 | platform: string; 11 | system: string; 12 | } 13 | 14 | export default class DevicesList implements IList { 15 | public readonly name = 'FlutterDevices'; 16 | public readonly description = 'flutter devices list'; 17 | public readonly defaultAction = 'run'; 18 | public actions: ListAction[] = []; 19 | 20 | constructor() { 21 | this.actions.push({ 22 | name: 'run', 23 | multiple: false, 24 | execute: async (item, context) => { 25 | if (Array.isArray(item)) { 26 | return; 27 | } 28 | commands.executeCommand(`flutter.run`, '-d', item.data!.deviceId, ...context.args); 29 | }, 30 | }); 31 | } 32 | 33 | public async loadItems(): Promise { 34 | const config = workspace.getConfiguration('flutter'); 35 | const timeout = config.get('commands.devicesTimeout', 1); 36 | const { err, stdout } = await flutterSDK.execFlutterCommand( 37 | `devices --device-timeout=${timeout}`, 38 | ); 39 | let devices: Device[] = []; 40 | if (!err) { 41 | devices = stdout 42 | .split(lineBreak) 43 | .filter((line) => line.split('•').length === 4) 44 | .map((line) => { 45 | // MI 6 • 1ba39646 • android-arm64 • Android 9 (API 28) 46 | const items = line.split('•'); 47 | return { 48 | name: items[0].trim(), 49 | deviceId: items[1].trim(), 50 | platform: items[2].trim(), 51 | system: items[3].trim(), 52 | }; 53 | }); 54 | } 55 | return devices.map((device) => { 56 | return { 57 | label: `${colors.yellow(device.name)} • ${colors.gray( 58 | `${device.deviceId} • ${device.platform} • ${device.system}`, 59 | )}`, 60 | filterText: device.name, 61 | data: device, 62 | }; 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/sources/emulators.ts: -------------------------------------------------------------------------------- 1 | import { IList, ListAction, ListItem } from 'coc.nvim'; 2 | import colors from 'colors/safe'; 3 | 4 | import { lineBreak } from '../util/constant'; 5 | import { notification } from '../lib/notification'; 6 | import { flutterSDK } from '../lib/sdk'; 7 | 8 | interface Emulator { 9 | name: string; 10 | id: string; 11 | platform: string; 12 | system: string; 13 | } 14 | 15 | export default class EmulatorsList implements IList { 16 | public readonly name = 'FlutterEmulators'; 17 | public readonly description = 'flutter emulators list'; 18 | public readonly defaultAction = 'run'; 19 | public actions: ListAction[] = []; 20 | 21 | constructor() { 22 | this.actions.push({ 23 | name: 'run', 24 | multiple: false, 25 | execute: async (item) => { 26 | if (Array.isArray(item)) { 27 | return; 28 | } 29 | notification.show(`launch emulator ${item.data!.id}`); 30 | await flutterSDK.execFlutterCommand(`emulators --launch ${item.data!.id}`); 31 | }, 32 | }); 33 | } 34 | 35 | public async loadItems(): Promise { 36 | const { err, stdout } = await flutterSDK.execFlutterCommand('emulators'); 37 | let emulators: Emulator[] = []; 38 | if (!err) { 39 | emulators = stdout 40 | .split(lineBreak) 41 | .filter((line) => line.split('•').length === 4) 42 | .map((line) => { 43 | // apple_ios_simulator • iOS Simulator • Apple • ios 44 | const items = line.split('•'); 45 | return { 46 | name: items[1].trim(), 47 | id: items[0].trim(), 48 | platform: items[2].trim(), 49 | system: items[3].trim(), 50 | }; 51 | }); 52 | } 53 | return emulators.map((emulator) => { 54 | return { 55 | label: `${colors.yellow(emulator.id)} • ${colors.gray( 56 | `${emulator.name} • ${emulator.platform} • ${emulator.system}`, 57 | )}`, 58 | filterText: emulator.name, 59 | data: emulator, 60 | }; 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/sources/index.ts: -------------------------------------------------------------------------------- 1 | import { listManager } from 'coc.nvim'; 2 | import { LspServer } from '../server/lsp'; 3 | 4 | import { Dispose } from '../util/dispose'; 5 | import DevicesList from './devices'; 6 | import EmulatorsList from './emulators'; 7 | import SdksList from './sdks'; 8 | 9 | export class SourceList extends Dispose { 10 | constructor(lsp: LspServer) { 11 | super(); 12 | this.push( 13 | listManager.registerList(new DevicesList()), 14 | listManager.registerList(new EmulatorsList()), 15 | listManager.registerList(new SdksList(lsp)), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/sources/sdks.ts: -------------------------------------------------------------------------------- 1 | import { commands, IList, ListAction, ListItem, workspace } from 'coc.nvim'; 2 | import colors from 'colors/safe'; 3 | 4 | import { flutterSDK, FlutterSdk } from '../lib/sdk'; 5 | import { execCommand } from '../util/fs'; 6 | import { logger } from '../util/logger'; 7 | import { LspServer } from '../server/lsp'; 8 | 9 | const log = logger.getlog('SdksList'); 10 | 11 | export default class SdksList implements IList { 12 | public readonly name = 'FlutterSDKs'; 13 | public readonly description = 'list of local flutter sdks'; 14 | public readonly defaultAction = 'switch'; 15 | public actions: ListAction[] = []; 16 | 17 | constructor(lsp: LspServer) { 18 | this.actions.push({ 19 | name: 'switch', 20 | multiple: false, 21 | execute: async (item) => { 22 | if (Array.isArray(item)) { 23 | return; 24 | } 25 | const sdk: FlutterSdk = item.data; 26 | if (sdk.isCurrent) return; 27 | const config = workspace.getConfiguration('flutter'); 28 | if (sdk.isFvm) { 29 | await execCommand(`fvm use ${sdk.fvmVersion!}`); 30 | config.update('sdk.path', '.fvm/flutter_sdk'); 31 | log(`swithed to ${sdk.version} using fvm`); 32 | } else { 33 | config.update('sdk.path', sdk.location); 34 | } 35 | await lsp.reloadSdk(); 36 | await commands.executeCommand('flutter.pub.get'); 37 | await lsp.restart(); 38 | }, 39 | }); 40 | this.actions.push({ 41 | name: 'global switch', 42 | multiple: false, 43 | execute: async (item) => { 44 | if (Array.isArray(item)) { 45 | return; 46 | } 47 | const sdk: FlutterSdk = item.data; 48 | if (sdk.isCurrent) return; 49 | const config = workspace.getConfiguration('flutter'); 50 | config.update('sdk.path', sdk.location, true); 51 | await lsp.restart(); 52 | commands.executeCommand('flutter.pub.get'); 53 | }, 54 | }); 55 | } 56 | 57 | public async loadItems(): Promise { 58 | const sdks = await flutterSDK.findSdks(); 59 | return sdks.map((sdk) => { 60 | return { 61 | label: `${ 62 | sdk.isCurrent 63 | ? colors.yellow(sdk.version) + colors.bold(' (current)') 64 | : colors.yellow(sdk.version) 65 | } • ${colors.gray(`${sdk.location}`)}`, 66 | filterText: sdk.location, 67 | data: sdk, 68 | }; 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/util/constant.ts: -------------------------------------------------------------------------------- 1 | export const lineBreak = /\r?\n/; 2 | 3 | export const cmdPrefix = 'flutter'; 4 | 5 | export const devLogName = 'flutter-dev'; 6 | 7 | export const validMethodNameRegex = new RegExp('^[a-zA-Z_][a-zA-Z0-9_]*$'); 8 | 9 | export const validClassNameRegex = validMethodNameRegex; 10 | 11 | export const validVariableNameRegex = validMethodNameRegex; 12 | -------------------------------------------------------------------------------- /src/util/dispose.ts: -------------------------------------------------------------------------------- 1 | import { Disposable } from 'coc.nvim'; 2 | 3 | export class Dispose { 4 | public subscriptions: Disposable[] = []; 5 | 6 | push(...disposes: Disposable[]) { 7 | this.subscriptions.push(...disposes); 8 | } 9 | 10 | remove(subscription: Disposable) { 11 | this.subscriptions = this.subscriptions.filter((dispose) => { 12 | if (subscription === dispose) { 13 | dispose.dispose(); 14 | return false; 15 | } 16 | return true; 17 | }); 18 | } 19 | 20 | dispose() { 21 | if (this.subscriptions.length) { 22 | this.subscriptions.forEach((item) => { 23 | item.dispose(); 24 | }); 25 | this.subscriptions = []; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/util/fs.ts: -------------------------------------------------------------------------------- 1 | import { Stats } from '@nodelib/fs.scandir/out/types'; 2 | import { ErrnoException } from '@nodelib/fs.stat/out/types'; 3 | import { exec, ExecOptions } from 'child_process'; 4 | import { Uri, workspace } from 'coc.nvim'; 5 | import fastGlob from 'fast-glob'; 6 | import fs, { BaseEncodingOptions } from 'fs'; 7 | import { dirname, join, sep } from 'path'; 8 | import { logger } from './logger'; 9 | 10 | export const exists = async (path: string): Promise => { 11 | return new Promise((resolve) => { 12 | fs.exists(path, (exists) => { 13 | resolve(exists); 14 | }); 15 | }); 16 | }; 17 | 18 | export const findWorkspaceFolders = async (cwd: string, patterns: string[]): Promise => { 19 | const paths = await fastGlob(patterns, { 20 | onlyFiles: true, 21 | cwd, 22 | deep: 10, 23 | }); 24 | return paths.map((p) => join(cwd, dirname(p))); 25 | }; 26 | 27 | export const closestPath = (paths: string[]): string | undefined => { 28 | if (paths.length) { 29 | return paths.slice().sort((a, b) => { 30 | return a.split(sep).length - b.split(sep).length; 31 | })[0]; 32 | } 33 | return undefined; 34 | }; 35 | 36 | export const findWorkspaceFolder = async ( 37 | cwd: string, 38 | patterns: string[], 39 | ): Promise => { 40 | return closestPath(await findWorkspaceFolders(cwd, patterns)); 41 | }; 42 | 43 | export const getFlutterWorkspaceFolder = async (): Promise => { 44 | const workspaceFolder = workspace.workspaceFolder 45 | ? Uri.parse(workspace.workspaceFolder.uri).fsPath 46 | : workspace.cwd; 47 | return await findWorkspaceFolder(workspaceFolder, ['**/pubspec.yaml']); 48 | }; 49 | 50 | const log = logger.getlog('fs'); 51 | 52 | export const execCommand = ( 53 | command: string, 54 | options: ExecOptions = {}, 55 | ): Promise<{ 56 | code: number; 57 | err: Error | null; 58 | stdout: string; 59 | stderr: string; 60 | }> => { 61 | return new Promise((resolve) => { 62 | log(`executing command ${command}`); 63 | let code = 0; 64 | exec( 65 | command, 66 | { 67 | encoding: 'utf-8', 68 | ...options, 69 | }, 70 | (err: Error | null, stdout = '', stderr = '') => { 71 | resolve({ 72 | code, 73 | err, 74 | stdout, 75 | stderr, 76 | }); 77 | }, 78 | ).on('exit', (co: number) => co && (code = co)); 79 | }); 80 | }; 81 | 82 | export const isSymbolLink = async ( 83 | path: string, 84 | ): Promise<{ err: ErrnoException | null; stats: boolean }> => { 85 | return new Promise((resolve) => { 86 | fs.lstat(path, (err: ErrnoException | null, stats: Stats) => { 87 | resolve({ 88 | err, 89 | stats: stats && stats.isSymbolicLink(), 90 | }); 91 | }); 92 | }); 93 | }; 94 | 95 | export const getRealPath = async (path: string): Promise => { 96 | const { err, stats } = await isSymbolLink(path); 97 | if (!err && stats) { 98 | return new Promise((resolve) => { 99 | fs.realpath(path, (err: ErrnoException | null, realPath: string) => { 100 | if (err) { 101 | return resolve(path); 102 | } 103 | resolve(realPath); 104 | }); 105 | }); 106 | } 107 | return path; 108 | }; 109 | 110 | export const readDir = async ( 111 | path: string, 112 | options?: 113 | | { encoding: BufferEncoding | null; withFileTypes?: false } 114 | | BufferEncoding 115 | | undefined 116 | | null, 117 | ): Promise => { 118 | return new Promise((resolve) => { 119 | fs.readdir(path, options, (err, files) => { 120 | if (err) { 121 | return resolve([]); 122 | } else { 123 | return resolve(files); 124 | } 125 | }); 126 | }); 127 | }; 128 | 129 | export const readFile = async ( 130 | path: string, 131 | options?: (BaseEncodingOptions & { flag?: string }) | string | undefined | null, 132 | ): Promise => { 133 | return new Promise((resolve, reject) => { 134 | fs.readFile(path, options, (err, data) => { 135 | if (err) { 136 | return reject(err); 137 | } else { 138 | return resolve(data); 139 | } 140 | }); 141 | }); 142 | }; 143 | -------------------------------------------------------------------------------- /src/util/index.ts: -------------------------------------------------------------------------------- 1 | import { commands } from 'coc.nvim'; 2 | import { lineBreak } from './constant'; 3 | 4 | export const formatMessage = (text: string): string[] => 5 | text.trim().replace(/\s+/g, ' ').split(lineBreak); 6 | 7 | export const reduceSpace = (text: string): string => text.trim().replace(/\s+/g, ' '); 8 | 9 | export const setCommandTitle = (id: string, desc: string) => { 10 | // FIXME: coc.nvim version v0.80.0 do not export titles 11 | (commands as any).titles.set(id, desc); 12 | }; 13 | 14 | export const deleteCommandTitle = (id: string) => { 15 | // FIXME: coc.nvim version v0.80.0 do not export titles 16 | (commands as any).titles.delete(id); 17 | }; 18 | -------------------------------------------------------------------------------- /src/util/logger.ts: -------------------------------------------------------------------------------- 1 | import { OutputChannel, window } from 'coc.nvim'; 2 | import { devLogName } from './constant'; 3 | import { Dispose } from './dispose'; 4 | 5 | export type logLevel = 'off' | 'message' | 'verbose'; 6 | 7 | class Logger extends Dispose { 8 | private _outchannel: OutputChannel | undefined; 9 | private _devOutchannel: OutputChannel | undefined; 10 | private _traceServer: logLevel | undefined; 11 | 12 | init(level: logLevel) { 13 | this._traceServer = level; 14 | if (this._traceServer !== 'off') { 15 | this._outchannel = window.createOutputChannel('flutter'); 16 | this.push(this._outchannel); 17 | } 18 | } 19 | 20 | set outchannel(channel: OutputChannel | undefined) { 21 | this._outchannel = channel; 22 | } 23 | 24 | get outchannel(): OutputChannel | undefined { 25 | return this._outchannel; 26 | } 27 | 28 | get devOutchannel(): OutputChannel { 29 | if (!this._devOutchannel) { 30 | this._devOutchannel = window.createOutputChannel(devLogName); 31 | this.push(this._devOutchannel); 32 | } 33 | return this._devOutchannel; 34 | } 35 | 36 | get traceServer(): logLevel | undefined { 37 | return this._traceServer; 38 | } 39 | 40 | getlog(name: string): (message: string | undefined) => void { 41 | return (message: string | undefined) => { 42 | message && this._outchannel && this._outchannel.appendLine(`[${name}]: ${message}`); 43 | }; 44 | } 45 | 46 | dispose() { 47 | super.dispose(); 48 | this._outchannel = undefined; 49 | this._devOutchannel = undefined; 50 | this._traceServer = undefined; 51 | } 52 | } 53 | 54 | export const logger = new Logger(); 55 | -------------------------------------------------------------------------------- /src/util/opener.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * fork from https://github.com/domenic/opener 3 | */ 4 | import childProcess from 'child_process'; 5 | import os from 'os'; 6 | 7 | export function opener(args: string | string[], tool?: string) { 8 | args = ([] as string[]).concat(args); 9 | let platform = process.platform; 10 | 11 | // Attempt to detect Windows Subystem for Linux (WSL). 12 | // WSL itself as Linux (which works in most cases), but in 13 | // this specific case we need to treat it as actually being Windows. 14 | // The "Windows-way" of opening things through cmd.exe works just fine here, 15 | // whereas using xdg-open does not, since there is no X Windows in WSL. 16 | if (platform === 'linux' && os.release().indexOf('Microsoft') !== -1) { 17 | platform = 'win32'; 18 | } 19 | 20 | // http://stackoverflow.com/q/1480971/3191, but see below for Windows. 21 | let command: string; 22 | switch (platform) { 23 | case 'win32': { 24 | command = 'cmd.exe'; 25 | if (tool) { 26 | args.unshift(tool); 27 | } 28 | break; 29 | } 30 | case 'darwin': { 31 | command = 'open'; 32 | if (tool) { 33 | args.unshift(tool); 34 | args.unshift('-a'); 35 | } 36 | break; 37 | } 38 | default: { 39 | command = tool || 'xdg-open'; 40 | break; 41 | } 42 | } 43 | 44 | if (platform === 'win32') { 45 | // On Windows, we really want to use the "start" command. 46 | // But, the rules regarding arguments with spaces, and escaping them with quotes, 47 | // can get really arcane. So the easiest way to deal with this is to pass off the 48 | // responsibility to "cmd /c", which has that logic built in. 49 | // 50 | // Furthermore, if "cmd /c" double-quoted the first parameter, 51 | // then "start" will interpret it as a window title, 52 | // so we need to add a dummy empty-string window title: http://stackoverflow.com/a/154090/3191 53 | // 54 | // Additionally, on Windows ampersand needs to be escaped when passed to "start" 55 | args = args.map((value) => { 56 | return value.replace(/&/g, '^&'); 57 | }); 58 | args = ['/c', 'start', '""'].concat(args); 59 | } 60 | 61 | return childProcess.spawn(command, args, { 62 | detached: true, 63 | shell: os.platform() === 'win32' ? true : undefined, 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "lib": [ 5 | "es2015", 6 | "esnext" 7 | ], 8 | "module": "commonjs", 9 | "declaration": false, 10 | "sourceMap": false, 11 | "outDir": "out", 12 | "strict": true, 13 | "moduleResolution": "node", 14 | "noImplicitAny": false, 15 | "esModuleInterop": true 16 | }, 17 | "include": ["src"] 18 | } 19 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | /** @type {import('webpack').Configuration} */ 4 | module.exports = { 5 | entry: './src/index.ts', 6 | target: 'node', 7 | mode: 'production', 8 | resolve: { 9 | mainFields: ['module', 'main'], 10 | extensions: ['.js', '.ts'], 11 | }, 12 | externals: { 13 | 'coc.nvim': 'commonjs coc.nvim', 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.ts$/, 19 | exclude: /node_modules/, 20 | use: [ 21 | { 22 | loader: 'ts-loader', 23 | }, 24 | ], 25 | }, 26 | ], 27 | }, 28 | output: { 29 | path: path.join(__dirname, 'out'), 30 | filename: 'index.js', 31 | libraryTarget: 'commonjs', 32 | }, 33 | plugins: [], 34 | node: { 35 | __dirname: false, 36 | __filename: false, 37 | }, 38 | }; 39 | --------------------------------------------------------------------------------