├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ └── CI.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .terserrc.js ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── babel.config.json ├── babel.unit.config.json ├── benchmark └── benchmark.js ├── dist ├── busy-signal.d.ts ├── commands.d.ts ├── editor │ ├── helpers.d.ts │ └── index.d.ts ├── editors.d.ts ├── helpers.d.ts ├── index.d.ts ├── index.js ├── index.js.map ├── intentions.d.ts ├── main.d.ts ├── marked.esm.ab6d9469.js ├── marked.esm.ab6d9469.js.map ├── panel │ ├── component.d.ts │ ├── delegate.d.ts │ ├── dock.d.ts │ └── index.d.ts ├── status-bar │ ├── element.d.ts │ ├── helpers.d.ts │ └── index.d.ts ├── tooltip │ ├── delegate.d.ts │ ├── index.d.ts │ └── message.d.ts ├── tree-view │ ├── helpers.d.ts │ └── index.d.ts ├── tsconfig.tsbuildinfo └── types │ ├── atom.d.ts │ ├── index.d.ts │ ├── intentions.d.ts │ └── linter.d.ts ├── lib ├── busy-signal.ts ├── commands.ts ├── editor │ ├── helpers.ts │ └── index.ts ├── editors.ts ├── electron.d.ts ├── helpers.ts ├── index.ts ├── intentions.ts ├── main.ts ├── panel │ ├── component.tsx │ ├── delegate.ts │ ├── dock.tsx │ └── index.ts ├── status-bar │ ├── element.ts │ ├── helpers.ts │ └── index.ts ├── tooltip │ ├── delegate.ts │ ├── index.tsx │ └── message.tsx ├── tree-view │ ├── helpers.ts │ └── index.ts ├── tsconfig.json └── types │ ├── atom.d.ts │ ├── index.d.ts │ ├── intentions.d.ts │ └── linter.d.ts ├── package.json ├── pnpm-lock.yaml ├── prettier.config.js ├── scripts └── get-linter-types.js ├── spec ├── activate.spec.js ├── busy-singal-spec.js ├── editor-spec.js ├── fixtures │ ├── .eslintrc.json │ ├── error.ts │ ├── expandable-description.rb │ ├── info.ts │ ├── long-text.ts │ ├── tsconfig.json │ ├── warning.ts │ └── with-fixes.ts ├── helpers.js └── very-large-file.zip └── styles ├── bottom-panel.less └── linter-ui.less /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-atomic/react", 3 | "ignorePatterns": ["dist/", "node_modules/", "spec/fixtures", "lib/types/linter/"], 4 | "rules": { 5 | "@typescript-eslint/no-inferrable-types": "off", 6 | "@typescript-eslint/no-non-null-assertion": "off", 7 | "react/react-in-jsx-scope": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | # don't diff machine generated files 4 | dist/ -diff 5 | package-lock.json -diff 6 | pnpm-lock.yaml -diff 7 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | Test: 10 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 11 | name: ${{ matrix.os }} - Atom ${{ matrix.atom_channel }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: 17 | - ubuntu-latest 18 | - macos-latest 19 | - windows-latest 20 | atom_channel: [stable, beta] 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Cache 24 | id: node_modules 25 | uses: actions/cache@v2 26 | with: 27 | path: | 28 | node_modules 29 | key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }} 30 | 31 | - uses: UziTech/action-setup-atom@v1 32 | with: 33 | channel: ${{ matrix.atom_channel }} 34 | - name: Versions 35 | run: apm -v 36 | 37 | - uses: pnpm/action-setup@v2.2.2 38 | with: 39 | version: 6 40 | 41 | - uses: actions/setup-node@v3 42 | with: 43 | node-version: 12.x 44 | 45 | - name: Install dependencies 46 | run: pnpm i 47 | 48 | - name: Run tests 👩🏾‍💻 49 | run: pnpm run test 50 | 51 | - name: Run benchmarks 52 | run: pnpm run benchmark 53 | 54 | Lint: 55 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 56 | runs-on: ubuntu-latest 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | steps: 60 | - uses: actions/checkout@v2 61 | with: 62 | fetch-depth: 0 63 | - if: "!contains(github.event.head_commit.message, 'Prepare')" 64 | name: Commit lint ✨ 65 | uses: wagoid/commitlint-github-action@v2 66 | 67 | - uses: pnpm/action-setup@v2 68 | with: 69 | version: 6 70 | 71 | - name: Install dependencies 72 | run: pnpm install 73 | 74 | - name: Format ✨ 75 | run: pnpm run test.format 76 | 77 | - name: Lint ✨ 78 | run: pnpm run test.lint 79 | 80 | # Release: 81 | # needs: [Test, Lint] 82 | # if: github.ref == 'refs/heads/master' && 83 | # github.event.repository.fork == false 84 | # runs-on: ubuntu-latest 85 | # steps: 86 | # - uses: actions/checkout@v2 87 | # - uses: UziTech/action-setup-atom@v1 88 | # - uses: actions/setup-node@v1 89 | # with: 90 | # node-version: '12.x' 91 | # - name: NPM install 92 | # run: npm install 93 | # - name: Build and Commit. 94 | # run: npm run build-commit 95 | # NOTE: uncomment when ready 96 | # - name: Release 🎉 97 | # uses: cycjimmy/semantic-release-action@v2 98 | # with: 99 | # extends: | 100 | # @semantic-release/apm-config 101 | # env: 102 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 103 | # ATOM_ACCESS_TOKEN: ${{ secrets.ATOM_ACCESS_TOKEN }} 104 | 105 | Skip: 106 | if: contains(github.event.head_commit.message, '[skip ci]') 107 | runs-on: ubuntu-latest 108 | steps: 109 | - name: Skip CI 🚫 110 | run: echo skip ci 111 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS metadata 2 | .DS_Store 3 | npm-debug.log 4 | Thumbs.db 5 | 6 | # Node 7 | node_modules 8 | .idea 9 | package-lock.json 10 | 11 | # TypeScript 12 | *.tsbuildinfo 13 | 14 | # Build directories 15 | dist 16 | .parcel-cache 17 | benchmarkTestFile.* 18 | lib/types/linter 19 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=* 2 | package-lock=false 3 | lockfile=true 4 | prefer-frozen-lockfile=true 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package.json 3 | package-lock.json 4 | pnpm-lock.yaml 5 | coverage 6 | build 7 | dist 8 | spec/fixtures 9 | lib/types/linter 10 | -------------------------------------------------------------------------------- /.terserrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('terser-config-atomic') 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 3.4.1 4 | 5 | - fix: skip loading package-deps if linter is already loaded 6 | - fix: do not destructure atom.inSpecMode 7 | 8 | ### 3.4.0 9 | 10 | - improve the size of the package. The package is only 52 KB! 11 | - initial support for asynchronous linter fixes 12 | - lazy load `marked` only when needed 13 | - drop support for Atom versions older than 1.52 (at least Electron 6 is required) 14 | - use `Solid`'s Show to conditionally render 15 | - use largeness from `atom-ide-base` to detect if inline linter markers should be skipped in large editors 16 | - use `terser-config-atomic` 17 | - avoid implicit conversions by accurate handling of types 18 | - `once`/`dobounce` the buttons 19 | - make `LinteUI.render` function async (affects the server) 20 | - await and handle all promises 21 | - use solid `createEffect` and `createSignal` for tooltip descriptions instead of `onMount` and `createState` 22 | - use `atom.workspace.getActiveTextEditor` instead of our custom function 23 | - destructure atom at the top of the modules to improve performance 24 | - remove `lodash` dependency 25 | - update dependencies (`Solid`, `Marked`, etc) 26 | - Many more optimizations and fixes 27 | 28 | https://github.com/steelbrain/linter-ui-default/compare/v3.3.1...v3.4.0 29 | 30 | ### 3.3.1 31 | 32 | - fix: update dependencies #639 33 | fix: the issue with atom-package-deps not installing the linter dependency #639 34 | - minor optimizations #639 35 | 36 | ### 3.3.0 37 | 38 | - feat: colorize the severity in the bottom panel 39 | - fix: sort based on severity (show errors first) 40 | 41 | ### 3.2.5 42 | 43 | - fix: bump atom-package-deps to support spaced paths on Windows 44 | 45 | ### 3.2.4 46 | 47 | - fix: fix copying text from tooltips on MacOS 48 | 49 | ### 3.2.3 50 | 51 | - fix: move the linter tooltip down when there is not enough space on top 52 | 53 | ### 3.2.2 54 | 55 | - Update deps 56 | 57 | ### 3.2.1 58 | 59 | - fix: use native copy handling in tooltips: 60 | - It is faster. 61 | - Supports MacOS (cmd+c) 62 | - In addition to performance benefits, it also fixes the jumps that happened in the linter when you wanted to copy the text 63 | - fix: make `toggleDescription` async 64 | 65 | ### 3.2.0 66 | 67 | - Render descriptions correctly (#625) 68 | - 69 | 70 | ### 3.1.1 71 | 72 | - Fix linter panel scroll (#622) 73 | 74 | ### 3.1.0 75 | 76 | - Make text in the tooltips selectable and copyable (#620) 77 | 78 | ### 3.0.3 79 | 80 | - Use the default button style for Fix button (#619) 81 | - Add `globalThis` workaround for old Atom (#618) (please update Atom!) 82 | 83 | ### 3.0.1 84 | 85 | - Bypass parcel temporarily 86 | 87 | ### 3.0.0 88 | 89 | - Redesigned tooltip :tada: (#616) 90 | 91 | ### 2.5.2 92 | 93 | - fix: use @font-family (#615) 94 | 95 | ### 2.5.1 96 | 97 | - clean up classes (merge subscriptions, emitter types, etc.) (#614) 98 | 99 | ### 2.5.0 100 | 101 | - Replaces React with Solid-js :tada: (#613) 102 | - Use solid-simple-table for Panel :tada: (#613) 103 | - Using solid makes linter more performant! (#613) 104 | - Better style for the panel (#613) 105 | - Update dependencies 106 | 107 | ### 2.4.1 108 | 109 | - fix: minor null bug fixes 110 | - fix: faster load time 111 | 112 | ### 2.4.0 113 | 114 | - feat: large/minified file optimizations (#612) 115 | 116 | ### 2.3.5 117 | 118 | - fix: do not move the linter tooltip out of the editor 119 | 120 | ### 2.3.4 121 | 122 | - fix: transform the whole overlay to make underneath of the tooltip clickable 123 | 124 | ### 2.3.3 125 | 126 | - fix: make event listeners passive 127 | 128 | ### 2.3.2 129 | 130 | - fix: linter tooltip messages not being visible in some themes 131 | 132 | ### 2.3.1 133 | 134 | - fix: remove extra space from the tooltip due to arrow pointer 135 | - fix: decrease the time required for showing the linter tooltip (faster response) 136 | 137 | ### 2.3.0 138 | 139 | - Fixes overlaps with datatips (linter messages now are positioned above the point, unless there is not enough space) (#607) 140 | - Changes the style of the tooltips so they match datatips (atom-ide-community style) (#607) 141 | 142 | info messages: 143 | 144 | ![image](https://user-images.githubusercontent.com/16418197/102887312-532a8280-441c-11eb-99ca-3e82306f3dfb.png) 145 | 146 | error messages: 147 | 148 | ![image](https://user-images.githubusercontent.com/16418197/102887315-54f44600-441c-11eb-8b3f-d89a463b5865.png) 149 | 150 | when the message is at the top of the page, it does not translate: 151 | 152 | ![image](https://user-images.githubusercontent.com/16418197/102887388-7bb27c80-441c-11eb-8961-6a13890ba86a.png) 153 | 154 | ### 2.2.4 155 | 156 | - Bump dependencies 157 | - Add benchmarks (#605) 158 | 159 | ### 2.2.3 160 | 161 | - if file path of the editor is undefined save the message in "" 162 | 163 | ### 2.2.2 164 | 165 | - Huge number of bug fixes (#604) 166 | - Maximize TypeScript strictness (#604) 167 | 168 | ### 2.2.1 169 | 170 | - Use parcel to build and optimize linter-ui-default (#603) 171 | - 172 | 173 | ### 2.2.0 174 | 175 | - feat: convert codebase to typescript. [This involved many bug fixes](https://github.com/steelbrain/linter-ui-default/pull/602) 176 | 177 | ## 2.1.5 178 | 179 | - fix: check if `intersectsWith` is a function 180 | 181 | ## 2.1.4 182 | 183 | - Update dependencies 184 | - Re-add package-lock.json 185 | 186 | ## 2.1.3 187 | 188 | - Remove package-lock.json 189 | 190 | ## 2.1.2 191 | 192 | - Add another null check for messages 193 | 194 | ## 2.1.1 195 | 196 | - null check guard for messages to prevent unforeseen errors 197 | 198 | ### 2.1.0 199 | 200 | - Bump deps #589 201 | - Use lodash.debounce instead of the deprecated sb-debounce.debounce 202 | 203 | ## 2.0.1 204 | 205 | - improve performance (#581) 206 | - rewrite rendering algorithm (#581) 207 | - fix memory leaks (#581) 208 | 209 | ## 1.8.1 210 | 211 | - Fix `showPanel` being set to `true` on Editor restart (Thanks @Osmose) 212 | 213 | ## 1.8.0 214 | 215 | - Remove support for Legacy Linter messages 216 | - Fix Panel hide/inactive detection (Thanks @willy2dg) 217 | 218 | ## 1.7.1 219 | 220 | - Restore old hiding behavior 221 | 222 | ## 1.7.0 223 | 224 | - Add a max-width to linter toolip 225 | - Re-add `alwaysTakeMinimumSpace` config which works with Atom Docks! 226 | - Attempt to fix `Cannot decorate a destroyed marker` errors (Fix by @sompylasar) 227 | 228 | ## 1.6.11 229 | 230 | - Upgrade `marked` version 231 | - Add `Fix` button to tooltips 232 | 233 | ## 1.6.10 234 | 235 | - Fix alignment of icons for Atom v1.20.0+ 236 | 237 | ## 1.6.9 238 | 239 | - Change inline highlighting style from `highlight` to `text` 240 | 241 | ## 1.6.7 242 | 243 | - Was missing check in another place 244 | 245 | ## 1.6.6 246 | 247 | - Possible fix for steelbrain/linter-ui-default#355 248 | 249 | ## 1.6.5 250 | 251 | - Fix inconsistency between panel height set by resizing and from settings 252 | 253 | ## 1.6.4 254 | 255 | - Fix a style issue on Atom v1.19.0 256 | - Fix incorrect times reported to Busy Signal 257 | - Fix behavior of file scope when Linter Panel is used inside a pane container 258 | 259 | ## 1.6.3 260 | 261 | - Hide tooltip when cursor is changed and `tooltipFollows` is set to `Both` 262 | 263 | ## 1.6.2 264 | 265 | - Flip default `hidePanelWhenEmpty` back to true 266 | - Fix a bug where clicking on other pane items (not even center) would hide status bar and panel 267 | 268 | ## 1.6.1 269 | 270 | - Flip default `hidePanelWhenEmpty` to false 271 | - Allow opening editors from markdown links from Message v2 description 272 | 273 | ## 1.6.0 274 | 275 | - Apply panelHeight changes live 276 | - Fix performance regression of v1.5.x 277 | - Use another color variable tooltip background 278 | - Only hide/show dock when Linter is the active item 279 | - Add `Jump to next issue` to `statusBarClickBehavior` config 280 | 281 | ## 1.5.4 282 | 283 | - Fix the last of known Linter Panel bugs 284 | 285 | ## 1.5.3 286 | 287 | - Hide panel if appropriate after active pane item change 288 | 289 | ## 1.5.2 290 | 291 | - F my life. 292 | 293 | ## 1.5.1 294 | 295 | - Flip a check that was making Panel behave weirdly 296 | 297 | ## 1.5.0 298 | 299 | - Remove `hidePanelUnlessTextEditor` config 300 | - Tweak Tooltip visuals (See #301 for Screenshots) 301 | - Do not focus Linter dock on open (less UI clutter) 302 | - Remove linter tooltip when Text Editor is unfocused 303 | - Readd `panelHeight` config and make it work on Docks 304 | - Replace Linter Status bar with icons instead of boxes 305 | - Change status bar to represent Entire Projecy by default 306 | - Add `Both` support to `tooltipFollows` config and make it default 307 | - Fix unnecessary jump to message when clicking links in description 308 | 309 | ## 1.4.0 310 | 311 | - Fix for Nuclide's file tree 312 | - Add `hidePanelWhenEmpty` config 313 | - Add `hidePanelUnlessTextEditor` config 314 | 315 | ## 1.3.0 (for Atom Beta) 316 | 317 | - Add docks API support 318 | - Remove tooltip if it exists on config change 319 | - Remove tooltip when cursor changes (only when `tooltipFollows` is set to `Mouse`) 320 | 321 | ## 1.2.4 322 | 323 | - Fix for Nuclide's Tree View 324 | - Remove tooltip if it exists on config change 325 | - Remove tooltip when cursor changes (only when `tooltipFollows` is set to `Mouse`) 326 | 327 | ## 1.2.3 328 | 329 | - Improve tooltip hiding logic 330 | - Fix two borders on the Bottom Table 331 | - Make description in Bottom Panel clickable 332 | - Use wavy underlines in tree view and editor 333 | - Use theme variable for Bottom Panel font size 334 | - Fix inconsistent border radius of Linter Status 335 | - Fix current line marker in gutter for soft wraps 336 | - Only show url icon on tooltip if specified by linter provider 337 | 338 | ## 1.2.2 339 | 340 | - Fix mouse tooltips for some users 341 | - Fix a crash in TreeView handling a file outside Project Path 342 | - Make busy signal installation and integration optional with `useBusySignal` config 343 | 344 | ## 1.2.1 345 | 346 | - Fix a deprecation warning caused by out-of-date Atom `TextEditor#markBufferRange.properties` 347 | 348 | ## 1.2.0 349 | 350 | - Add `showStatusBar` config 351 | - Fix a typo in Panel Component 352 | - Show line number styles in gutter container 353 | - Validate ranges for NaN to workaround Atom bug 354 | - Move the stauts bar to the left of line:col view 355 | - Fix a typo that would not let `showProviderName` work 356 | - Bump `sb-react-table` to include fix for [steelbrain/react-table#7](https://github.com/steelbrain/react-table/issues/7) 357 | - Fix `Current Line` selector for Panel to support Array-like objects 358 | - Hide Panel's `File` column when Panel is only representing current file 359 | 360 | ## 1.1.0 361 | 362 | - Add option to configure status bar position 363 | - Add support for `Current Line` in `panelRepresents` config 364 | - Fix the Bottom Panel taking 1px height when hidden ([#177](https://github.com/steelbrain/linter-ui-default/pull/177)) 365 | 366 | ## 1.0.0 367 | 368 | - Read the [release post](http://steelbrain.me/2017/03/13/linter-v2-released.html). 369 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `linter-ui-default` 2 | 3 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1: 4 | 5 | The following is a set of guidelines for contributing to linter-ui-default. 6 | These are just guidelines, not rules, use your best judgment and feel free to 7 | propose changes to this document in a pull request. 8 | 9 | This project adheres to the [Open Code of Conduct][code-of-conduct]. By participating, you are expected to uphold this code. 10 | 11 | ## Submitting Issues 12 | 13 | - You can create an issue [here](https://github.com/steelbrain/linter-ui-default/issues/new), but 14 | before doing that please read the notes below on collecting information and submitting issues. 15 | Include as many details as possible with your report. 16 | - Include the version of Atom you are using and the OS. 17 | - Include screenshots and animated GIFs whenever possible; they are immensely 18 | helpful. 19 | - Include the behavior you expected and other places you've seen that behavior 20 | such as Emacs, vi, Xcode, etc. 21 | - Check the dev tools (`alt-cmd-i`) for errors to include. If the dev tools 22 | are open _before_ the error is triggered, a full stack trace for the error 23 | will be logged. If you can reproduce the error, use this approach to get the 24 | full stack trace and include it in the issue. 25 | - On Mac, check Console.app for stack traces to include if reporting a crash. 26 | - Perform a [cursory search](https://github.com/steelbrain/linter-ui-default/search?q=&type=Issues&utf8=%E2%9C%93) 27 | to see if a similar issue has already been submitted. 28 | - Please setup a [profile picture](https://help.github.com/articles/how-do-i-set-up-my-profile-picture) 29 | to make yourself recognizable and so we can all get to know each other better. 30 | 31 | ## Pull Requests 32 | 33 | - We prefer small, focused, single-responsibility pull requests that include tests where possible. These can be contrasted with large pull requests, pull requests with multiple unrelated concerns, and pull requests which have no tests. 34 | - Include screenshots and animated GIFs in your pull request whenever possible. 35 | - **Please ensure that your pull request has no lint errors.** This is a project for linter-ui-defaults after all, 36 | so please ensure you have the [linter-ui-default-eslint](https://atom.io/packages/linter-ui-default-eslint) package installed in 37 | Atom. 38 | - Include thoughtfully-worded, well-structured 39 | [Jasmine](http://jasmine.github.io/) specs in the `./spec` folder. Run them using `apm test`. See 40 | the [Specs Styleguide](#specs-styleguide) below. 41 | - Document new code based on the 42 | [Documentation Styleguide](#documentation-styleguide) 43 | - End files with a newline. 44 | - Place requires in the following order: 45 | - Built in Node Modules (such as `path`) 46 | - Built in Atom and Atom Shell Modules (such as `atom`, `shell`) 47 | - Local Modules (using relative paths) 48 | - Place class properties in the following order: 49 | - Class methods and properties (methods starting with a `@`) 50 | - Instance methods and properties 51 | - Avoid platform-dependent code: 52 | - Use `require('fs-plus').getHomeDirectory()` to get the home directory. 53 | - Use `path.join()` to concatenate filenames. 54 | - Use `os.tmpdir()` rather than `/tmp` when you need to reference the 55 | temporary directory. 56 | - Using a plain `return` when returning explicitly at the end of a function. 57 | - Not `return null`, `return undefined`, `null`, or `undefined` 58 | 59 | ## Git Commit Messages 60 | 61 | - Use the present tense ("Add feature" not "Added feature") 62 | - Use the imperative mood ("Move cursor to..." not "Moves cursor to...") 63 | - Limit the first line to 72 characters or less 64 | - Reference issues and pull requests liberally 65 | - Consider starting the commit message with an applicable emoji: 66 | - :art: `:art:` when improving the format/structure of the code 67 | - :racehorse: `:racehorse:` when improving performance 68 | - :non-potable_water: `:non-potable_water:` when plugging memory leaks 69 | - :memo: `:memo:` when writing docs 70 | - :penguin: `:penguin:` when fixing something on Linux 71 | - :apple: `:apple:` when fixing something on Mac OS 72 | - :checkered_flag: `:checkered_flag:` when fixing something on Windows 73 | - :bug: `:bug:` when fixing a bug 74 | - :fire: `:fire:` when removing code or files 75 | - :green_heart: `:green_heart:` when fixing the CI build 76 | - :white_check_mark: `:white_check_mark:` when adding tests 77 | - :lock: `:lock:` when dealing with security 78 | - :arrow_up: `:arrow_up:` when upgrading dependencies 79 | - :arrow_down: `:arrow_down:` when downgrading dependencies 80 | - :shirt: `:shirt:` when removing linter-ui-default warnings 81 | 82 | ## Specs Styleguide 83 | 84 | - Include thoughtfully-worded, well-structured 85 | [Jasmine](http://jasmine.github.io/) specs in the `./spec` folder. 86 | - treat `describe` as a noun or situation. 87 | - treat `it` as a statement about state or how an operation changes state. 88 | 89 | ### Example 90 | 91 | ```coffee 92 | describe 'a dog', -> 93 | it 'barks', -> 94 | # spec here 95 | describe 'when the dog is happy', -> 96 | it 'wags its tail', -> 97 | # spec here 98 | ``` 99 | 100 | ### Commit Rights 101 | 102 | - Commit rights may be given to a contributor who has shown prior history of submitting high 103 | quality [pull requests](#specs-styleguide). 104 | - Commit rights may be taken away from a contributor 105 | that has repeatedly or willfully disregarded the [code of conduct][code-of-conduct]. 106 | 107 | Committers are expected to submit non-trivial changes via pull request, and receive :+1: / :-1: votes from two other contributors. Use your best judgement on what 108 | constitutes a "trivial" change. 109 | 110 | [code-of-conduct]: http://todogroup.org/opencodeofconduct/#Atom/opensource@github.com 111 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016 steelbrain 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Linter-UI-Default 2 | 3 | [![Greenkeeper badge](https://badges.greenkeeper.io/steelbrain/linter-ui-default.svg)](https://greenkeeper.io/) 4 | 5 | [![Slack Badge](https://img.shields.io/badge/chat-atom.io%20slack-blue.svg?style=flat-square)](http://atom-slack.herokuapp.com/) 6 | ![CI](https://github.com/steelbrain/linter-ui-default/workflows/CI/badge.svg) 7 | [![Plugin installs!](https://img.shields.io/apm/dm/linter-ui-default.svg?style=flat-square)](https://atom.io/packages/linter-ui-default) 8 | [![Package version!](https://img.shields.io/apm/v/linter-ui-default.svg?style=flat-square)](https://atom.io/packages/linter-ui-default) 9 | [![Dependencies!](https://img.shields.io/david/steelbrain/linter-ui-default.svg?style=flat-square)](https://david-dm.org/steelbrain/linter-ui-default) 10 | 11 | The default UI for linter. 12 | 13 | ### Installation 14 | 15 | You can install it from the CLI 16 | 17 | ``` 18 | apm install linter-ui-default 19 | ``` 20 | 21 | Or you can install from Settings view by searching for `linter-ui-default`. 22 | 23 | Linter UI require a recent version of Atom. Download the latest official version from: https://atom.io/ 24 | 25 | ### Screenshots 26 | 27 | ![tooltip](https://user-images.githubusercontent.com/16418197/106548395-8577d700-64d4-11eb-9eaa-1974f0903516.png) 28 | 29 | ![tooltip with multiple messages](https://user-images.githubusercontent.com/16418197/106654351-a89a9900-655d-11eb-8af2-f6a459720bf3.png) 30 | 31 | ![tooltip with fold button](https://user-images.githubusercontent.com/16418197/106548281-52354800-64d4-11eb-9d66-d26ee702cfa5.png) 32 | 33 | 34 | 35 | ![full-editor](https://user-images.githubusercontent.com/16418197/110440831-b375ab80-807e-11eb-8b34-4a85b135f180.png) 36 | 37 | ![panel gif](https://cloud.githubusercontent.com/assets/4278113/23879933/1ab17e2a-0872-11e7-803d-3fe0ccfc6790.gif) 38 | 39 | ### License 40 | 41 | This Project is licensed under the terms of MIT License, check the license file for more info. 42 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["babel-preset-solid"] 3 | } 4 | -------------------------------------------------------------------------------- /babel.unit.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "babel-preset-solid", 4 | [ 5 | "babel-preset-atomic", 6 | { 7 | "addModuleExports": false, 8 | "react": false, 9 | "typescript": true 10 | } 11 | ] 12 | ], 13 | "sourceMap": "inline" 14 | } 15 | -------------------------------------------------------------------------------- /benchmark/benchmark.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // eslint-disable-next-line import/no-unassigned-import 3 | import 'module-alias/register' 4 | import { Chance } from 'chance' 5 | const chance = new Chance() 6 | import type { Message } from '../lib/types' 7 | import { Range } from 'atom' 8 | import type { TextEditor } from 'atom' 9 | import Editor from '../dist/editor' 10 | import { writeFile as writeFileRaw } from 'fs' 11 | import { promisify } from 'util' 12 | const writeFile = promisify(writeFileRaw) 13 | 14 | /* ************************************************************************* */ 15 | 16 | function getRanomPoint(parLengths: number[]): [number, number] { 17 | const randomRow = chance.integer({ min: 0, max: parLengths.length }) 18 | const randomColumn = chance.integer({ min: 0, max: parLengths[randomRow] }) 19 | return [randomRow, randomColumn] 20 | } 21 | 22 | function getRandomRange(parLengths: number[]) { 23 | const pointsSorted = [getRanomPoint(parLengths), getRanomPoint(parLengths)].sort((p1, p2) => { 24 | return p1[0] - p2[0] 25 | }) 26 | return Range.fromObject(pointsSorted) 27 | } 28 | 29 | function generateRandomMessage( 30 | filePath: ?string, 31 | range: ?Range = getRandomRange(), 32 | severity: ?string = chance.pickone(['error', 'warning', 'info']), 33 | ): Message { 34 | return { 35 | key: chance.unique(chance.string, 1)[0], 36 | version: 2, 37 | severity, 38 | excerpt: String(chance.integer()), 39 | location: { file: filePath, position: range }, 40 | description: chance.sentence({ words: 20 }), 41 | } 42 | } 43 | 44 | async function getTestFile(filePath: string, numParagraphs: number = 30, numSentences: number = 10) { 45 | let str: string = '' 46 | const parLengths: number[] = new Array(numParagraphs) 47 | for (let i = 0; i < numParagraphs; i++) { 48 | const par = chance.paragraph({ sentences: numSentences }) 49 | str = str.concat(par, '\n') 50 | parLengths[i] = par.length 51 | } 52 | await writeFile(filePath, str) 53 | return { fileLegth: str.length, parLengths } 54 | } 55 | 56 | /* ************************************************************************* */ 57 | 58 | describe('Editor benchmark', function () { 59 | // parameters 60 | const numParagraphs = 300 61 | const numSentences = 10 62 | const filePath = './benchmark/benchmarkTestFile.txt' 63 | const messageNumlist = [5, 10, 20, 50, 70, 100, 200, 500, 800, 1000] // test for different number of messages 64 | 65 | let editor: Editor 66 | let textEditor: TextEditor 67 | let parLengths: number 68 | // let fileLength: number 69 | beforeEach(async function () { 70 | // make a test file 71 | const testFileProps = await getTestFile(filePath, numParagraphs, numSentences) 72 | parLengths = testFileProps.parLengths 73 | // fileLength = testFileProps.fileLength 74 | 75 | // open the test file 76 | await atom.workspace.open(filePath) 77 | 78 | // make a linter editor instance 79 | textEditor = atom.workspace.getActiveTextEditor() 80 | // Attache text editor 81 | jasmine.attachToDOM(textEditor.getElement()) 82 | 83 | // create linter Editor instance 84 | editor = new Editor(textEditor) 85 | 86 | // Activate linter-ui-default 87 | atom.packages.triggerDeferredActivationHooks() 88 | atom.packages.triggerActivationHook('core:loaded-shell-environment') 89 | 90 | atom.packages.loadPackage('linter-ui-default') 91 | }) 92 | 93 | it('applyChanges benchmark', function () { 94 | console.log('it adds the messages to the editor and then removes them') 95 | 96 | // test for different number of messages 97 | for (const messageNum of messageNumlist) { 98 | // get linter messages 99 | const messages = new Array(messageNum) 100 | for (let i = 0; i < messageNum; i++) { 101 | messages[i] = generateRandomMessage(filePath, getRandomRange(parLengths)) 102 | } 103 | 104 | console.log(`\n number of messages are ${messageNum} \n`) 105 | 106 | // Add 107 | expect(textEditor.getBuffer().getMarkerCount()).toBe(0) 108 | 109 | const ti_add = window.performance.now() 110 | 111 | editor.applyChanges(messages, []) 112 | 113 | const tf_add = window.performance.now() 114 | 115 | expect(textEditor.getBuffer().getMarkerCount()).toBe(messageNum) 116 | console.log( 117 | `Adding ${messageNum} linter messages took ${' '.repeat(50 - messageNum.toString().length)} ${( 118 | tf_add - ti_add 119 | ).toFixed(3)} ms`, 120 | ) 121 | 122 | // Remove 123 | const ti_remove = window.performance.now() 124 | 125 | editor.applyChanges([], messages) 126 | 127 | const tf_remove = window.performance.now() 128 | 129 | expect(textEditor.getBuffer().getMarkerCount()).toBe(0) 130 | console.log( 131 | `Removing ${messageNum} linter messages took ${' '.repeat(48 - messageNum.toString().length)} ${( 132 | tf_remove - ti_remove 133 | ).toFixed(3)} ms`, 134 | ) 135 | } 136 | }) 137 | 138 | afterEach(function () { 139 | editor.dispose() 140 | atom.workspace.destroyActivePaneItem() 141 | }) 142 | }) 143 | -------------------------------------------------------------------------------- /dist/busy-signal.d.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable } from 'atom'; 2 | import type { Linter } from './types'; 3 | import { BusySignalProvider, BusySignalRegistry } from 'atom-ide-base'; 4 | export default class BusySignal { 5 | provider: BusySignalProvider | null | undefined; 6 | executing: Set<{ 7 | linter: Linter; 8 | filePath: string | null | undefined; 9 | }>; 10 | providerTitles: Set; 11 | useBusySignal: boolean; 12 | subscriptions: CompositeDisposable; 13 | constructor(); 14 | attach(registry: BusySignalRegistry): void; 15 | update(): void; 16 | getExecuting(linter: Linter, filePath: string | null | undefined): { 17 | linter: Linter; 18 | filePath: string | null | undefined; 19 | } | null; 20 | didBeginLinting(linter: Linter, filePath: string | null | undefined): void; 21 | didFinishLinting(linter: Linter, filePath: string | null | undefined): void; 22 | dispose(): void; 23 | } 24 | -------------------------------------------------------------------------------- /dist/commands.d.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable } from 'atom'; 2 | import type { LinterMessage } from './types'; 3 | export default class Commands { 4 | messages: Array; 5 | subscriptions: CompositeDisposable; 6 | constructor(); 7 | applyAllSolutions(): void; 8 | move(forward: boolean, globally: boolean, severity?: string | null | undefined): Promise; 9 | update(messages: Array): void; 10 | dispose(): void; 11 | } 12 | -------------------------------------------------------------------------------- /dist/editor/helpers.d.ts: -------------------------------------------------------------------------------- 1 | import type { Point, TextEditor, TextEditorElement, PointLike } from 'atom'; 2 | import type Tooltip from '../tooltip/index'; 3 | export declare function getBufferPositionFromMouseEvent(event: MouseEvent, editor: TextEditor, editorElement: TextEditorElement): Point | null; 4 | export declare function mouseEventNearPosition({ event, editor, editorElement, tooltipElement, screenPosition, }: { 5 | event: { 6 | clientX: number; 7 | clientY: number; 8 | }; 9 | editor: TextEditor; 10 | editorElement: TextEditorElement; 11 | tooltipElement: Tooltip['element']; 12 | screenPosition: PointLike; 13 | }): boolean; 14 | export declare function hasParent(givenElement: HTMLElement | null, selector: string): boolean; 15 | -------------------------------------------------------------------------------- /dist/editor/index.d.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable, Disposable, Emitter, Range } from 'atom'; 2 | declare type CompositeDisposableType = CompositeDisposable & { 3 | disposed: boolean; 4 | }; 5 | import type { TextEditor, DisplayMarker, Marker, Gutter, Point, Cursor } from 'atom'; 6 | import Tooltip from '../tooltip'; 7 | import type { LinterMessage } from '../types'; 8 | export default class Editor { 9 | textEditor: TextEditor; 10 | gutter: Gutter | null; 11 | tooltip: Tooltip | null; 12 | emitter: Emitter<{ 13 | 'did-destroy': never; 14 | }, {}>; 15 | markers: Map; 16 | messages: Map; 17 | showTooltip: boolean; 18 | subscriptions: CompositeDisposableType; 19 | cursorPosition: Point | null; 20 | gutterPosition?: string; 21 | tooltipFollows: string; 22 | showDecorations?: boolean; 23 | showProviderName: boolean; 24 | ignoreTooltipInvocation: boolean; 25 | currentLineMarker: DisplayMarker | null; 26 | lastRange?: Range; 27 | lastIsEmpty?: boolean; 28 | lastCursorPositions: WeakMap; 29 | constructor(textEditor: TextEditor); 30 | listenForCurrentLine(): void; 31 | listenForMouseMovement(): import("event-kit").Disposable; 32 | listenForKeyboardMovement(): Disposable; 33 | updateGutter(): void; 34 | removeGutter(): void; 35 | updateTooltip(position: Point | null | undefined): void; 36 | removeTooltip(): void; 37 | applyChanges(added: Array, removed: Array): void; 38 | decorateMarker(message: LinterMessage, marker: DisplayMarker | Marker, paint?: 'gutter' | 'editor' | 'both'): void; 39 | saveMarker(key: string, marker: DisplayMarker | Marker): void; 40 | destroyMarker(key: string): void; 41 | onDidDestroy(callback: () => void): Disposable; 42 | dispose(): void; 43 | } 44 | export {}; 45 | -------------------------------------------------------------------------------- /dist/editors.d.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable } from 'atom'; 2 | import type { TextEditor } from 'atom'; 3 | import Editor from './editor'; 4 | import type { LinterMessage, MessagesPatch } from './types'; 5 | export declare type EditorsPatch = { 6 | added: Array; 7 | removed: Array; 8 | editors: Array; 9 | }; 10 | export declare type EditorsMap = Map; 11 | export default class Editors { 12 | editors: Set; 13 | messages: Array; 14 | firstRender: boolean; 15 | subscriptions: CompositeDisposable; 16 | constructor(); 17 | isFirstRender(): boolean; 18 | update({ messages, added, removed }: MessagesPatch): void; 19 | getEditor(textEditor: TextEditor): Editor | void; 20 | dispose(): void; 21 | } 22 | -------------------------------------------------------------------------------- /dist/helpers.d.ts: -------------------------------------------------------------------------------- 1 | import { Range } from 'atom'; 2 | import type { Point, PointLike, RangeCompatible, TextEditor } from 'atom'; 3 | import type { default as Editors, EditorsMap } from './editors'; 4 | import type { LinterMessage, MessageSolution } from './types'; 5 | export declare const severityScore: { 6 | error: number; 7 | warning: number; 8 | info: number; 9 | }; 10 | export declare const severityNames: { 11 | error: string; 12 | warning: string; 13 | info: string; 14 | }; 15 | export declare const WORKSPACE_URI = "atom://linter-ui-default"; 16 | export declare const DOCK_ALLOWED_LOCATIONS: string[]; 17 | export declare const DOCK_DEFAULT_LOCATION = "bottom"; 18 | export declare function $range(message: LinterMessage): Range | null | undefined; 19 | export declare function $file(message: LinterMessage): string | null | undefined; 20 | export declare function copySelection(): void; 21 | export declare function getPathOfMessage(message: LinterMessage): string; 22 | export declare function getEditorsMap(editors: Editors): { 23 | editorsMap: EditorsMap; 24 | filePaths: Array; 25 | }; 26 | export declare function filterMessages(messages: Array, filePath: string | null | undefined, severity?: string | null | undefined): Array; 27 | export declare function filterMessagesByRangeOrPoint(messages: Set | Array | Map, filePath: string | undefined, rangeOrPoint: Point | RangeCompatible): Array; 28 | export declare function openFile(file: string, position: PointLike | null | undefined): Promise; 29 | export declare function visitMessage(message: LinterMessage, reference?: boolean): Promise; 30 | export declare function openExternally(message: LinterMessage): void; 31 | export declare function sortMessages(rows: Array, sortDirection: [id: 'severity' | 'linterName' | 'file' | 'line', direction: 'asc' | 'desc']): Array; 32 | export declare function sortSolutions(solutions: MessageSolution[]): MessageSolution[]; 33 | export declare function applySolution(textEditor: TextEditor, solution: MessageSolution): boolean; 34 | /** 35 | * A function to get a value from the cache or calculate it if it is not available (and store it in the cache after calculation) 36 | * 37 | * @param map A reference to a Map of key to values that is used as the cache 38 | * @param key The current key to get calculate or get the cache for 39 | * @param calculate The function that is used to calculate the value if the cache is not hit 40 | */ 41 | export declare function get(map: Map, key: Key, calculate: () => Value | null): Value | null; 42 | /** A faster vresion of lodash.debounce */ 43 | export declare function debounce void>(func: T, wait?: number): T; 44 | /** A faster vresion of lodash.once */ 45 | export declare function once(func: T): T; 46 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import LinterUI from './main'; 2 | import type Intentions from './intentions'; 3 | import type { StatusBar as StatusBarRegistry } from 'atom/status-bar'; 4 | import type { BusySignalRegistry } from 'atom-ide-base'; 5 | export declare function activate(): void; 6 | export declare function deactivate(): void; 7 | export declare function provideUI(): LinterUI; 8 | export declare function provideIntentions(): Array; 9 | export declare function consumeSignal(signalService: BusySignalRegistry): void; 10 | export declare function consumeStatusBar(statusBarService: StatusBarRegistry): void; 11 | -------------------------------------------------------------------------------- /dist/intentions.d.ts: -------------------------------------------------------------------------------- 1 | import type { LinterMessage, ListItem } from './types'; 2 | import type { TextEditor, Point } from 'atom'; 3 | export default class Intentions { 4 | messages: Array; 5 | grammarScopes: Array; 6 | getIntentions({ textEditor, bufferPosition }: { 7 | textEditor: TextEditor; 8 | bufferPosition: Point; 9 | }): ListItem[]; 10 | update(messages: Array): void; 11 | } 12 | -------------------------------------------------------------------------------- /dist/main.d.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable } from 'atom'; 2 | import Panel from './panel'; 3 | import Commands from './commands'; 4 | import StatusBar from './status-bar'; 5 | import BusySignal from './busy-signal'; 6 | import Intentions from './intentions'; 7 | import type { Linter, LinterMessage, MessagesPatch } from './types'; 8 | import Editors from './editors'; 9 | import TreeView from './tree-view'; 10 | export default class LinterUI { 11 | name: string; 12 | panel?: Panel; 13 | signal: BusySignal; 14 | editors: Editors | null | undefined; 15 | treeview?: TreeView; 16 | commands: Commands; 17 | messages: Array; 18 | statusBar: StatusBar; 19 | intentions: Intentions; 20 | subscriptions: CompositeDisposable; 21 | idleCallbacks: Set; 22 | constructor(); 23 | render(difference: MessagesPatch): Promise; 24 | didBeginLinting(linter: Linter, filePath: string): void; 25 | didFinishLinting(linter: Linter, filePath: string): void; 26 | dispose(): void; 27 | } 28 | -------------------------------------------------------------------------------- /dist/marked.esm.ab6d9469.js: -------------------------------------------------------------------------------- 1 | ("undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{}).parcelRequiree95c.register("1o0DF",(function(e,t){function n(e){return e.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}function s(e,t){f[" "+e]||(f[" "+e]=k.test(e)?e+"/":r(e,"/",!0));const n=-1===(e=f[" "+e]).indexOf(":");return"//"===t.substring(0,2)?n?t:e.replace(/^([^:]+:)[\s\S]*$/,"$1")+t:"/"===t.charAt(0)?n?t:e.replace(/^([^:]+:\/*[^/]*)[\s\S]*$/,"$1")+t:e+t}function r(e,t,n){const s=e.length;if(0===s)return"";let r=0;for(;s>r;){const i=e.charAt(s-r-1);if(i!==t||n){if(i===t||!n)break;r++}else r++}return e.substr(0,s-r)}function i(e,t,n){const s=t.href,r=t.title?_(t.title):null,i=e[1].replace(/\\([\[\]])/g,"$1");return"!"!==e[0].charAt(0)?{type:"link",raw:n,href:s,title:r,text:i}:{type:"image",raw:n,href:s,title:r,text:_(i)}}function l(e){return e.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…")}function a(e){let t,n,s="";const r=e.length;for(t=0;r>t;t++)n=e.charCodeAt(t),Math.random()>.5&&(n="x"+n.toString(16)),s+="&#"+n+";";return s}function o(e,t,n){if(null==e)throw Error("marked(): input parameter is undefined or null");if("string"!=typeof e)throw Error("marked(): input parameter is of type "+{}.toString.call(e)+", string expected");if("function"==typeof t&&(n=t,t=null),t=W({},o.defaults,t||{}),Y(t),n){const s=t.highlight;let r;try{r=G.lex(e,t)}catch(e){return n(e)}const i=e=>{let i;if(!e)try{t.walkTokens&&o.walkTokens(r,t.walkTokens),i=V.parse(r,t)}catch(t){e=t}return t.highlight=s,e?n(e):n(null,i)};if(!s||3>s.length)return i();if(delete t.highlight,!r.length)return i();let l=0;return o.walkTokens(r,(e=>{"code"===e.type&&(l++,setTimeout((()=>{s(e.text,e.lang,((t,n)=>{if(t)return i(t);null!=n&&n!==e.text&&(e.text=n,e.escaped=!0),l--,0===l&&i()}))}),0))})),void(0===l&&i())}try{const n=G.lex(e,t);return t.walkTokens&&o.walkTokens(n,t.walkTokens),V.parse(n,t)}catch(e){if(e.message+="\nPlease report this to https://github.com/markedjs/marked.",t.silent)return"

An error occurred:

"+ee(e.message+"",!0)+"
";throw e}}Object.defineProperty(e.exports,"__esModule",{value:!0,configurable:!0}),Object.defineProperty(e.exports,"default",{get:()=>re,set:void 0,enumerable:!0,configurable:!0});var c={exports:{}};c.exports={defaults:{baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1},getDefaults:()=>({baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1}),changeDefaults(e){c.exports.defaults=e}};const h=/[&<>"']/,p=/[<>"']|&(?!#?\w+;)/,u={"&":"&","<":"<",">":">",'"':""","'":"'"},g=e=>u[e],d=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i,f={},k=/^[^:]+:\/*[^/]*$/;var x={escape(e,t){if(t){if(h.test(e))return e.replace(/[&<>"']/g,g)}else if(p.test(e))return e.replace(/[<>"']|&(?!#?\w+;)/g,g);return e},unescape:n,edit(e,t){e=e.source||e,t=t||"";const n={replace(t,s){return s=(s=s.source||s).replace(/(^|[^\[])\^/g,"$1"),e=e.replace(t,s),n},getRegex(){return RegExp(e,t)}};return n},cleanUrl(e,t,r){if(e){let e;try{e=decodeURIComponent(n(r)).replace(/[^\w:]/g,"").toLowerCase()}catch(e){return null}if(0===e.indexOf("javascript:")||0===e.indexOf("vbscript:")||0===e.indexOf("data:"))return null}t&&!d.test(r)&&(r=s(t,r));try{r=encodeURI(r).replace(/%25/g,"%")}catch(e){return null}return r},resolveUrl:s,noopTest:{exec(){}},merge(e){let t,n,s=1;for(;arguments.length>s;s++)for(n in t=arguments[s],t)({}).hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e},splitCells(e,t){const n=e.replace(/\|/g,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(n.length>t)n.splice(t);else for(;t>n.length;)n.push("");for(;n.length>s;s++)n[s]=n[s].trim().replace(/\\\|/g,"|");return n},rtrim:r,findClosingBracket(e,t){if(-1===e.indexOf(t[1]))return-1;const n=e.length;let s=0,r=0;for(;n>r;r++)if("\\"===e[r])r++;else if(e[r]===t[0])s++;else if(e[r]===t[1]&&(s--,0>s))return r;return-1},checkSanitizeDeprecation(e){e&&e.sanitize&&!e.silent&&console.warn("marked(): sanitize and sanitizer parameters are deprecated since version 0.7.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/#/USING_ADVANCED.md#options")},repeatString(e,t){if(1>t)return"";let n="";for(;t>1;)1&t&&(n+=e),t>>=1,e+=e;return n+e}};const{defaults:m}=c.exports,{rtrim:b,splitCells:w,escape:_,findClosingBracket:y}=x;var z=class{constructor(e){this.options=e||m}space(e){const t=this.rules.block.newline.exec(e);if(t)return t[0].length>1?{type:"space",raw:t[0]}:{raw:"\n"}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:b(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=((e,t)=>{const n=e.match(/^(\s+)(?:```)/);if(null===n)return t;const s=n[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[n]=t;return s.length>n.length?e:e.slice(s.length)})).join("\n")})(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim():t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=b(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e}}}nptable(e){const t=this.rules.block.nptable.exec(e);if(t){const e={type:"table",header:w(t[1].replace(/^ *| *\| *$/g,"")),align:t[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:t[3]?t[3].replace(/\n$/,"").split("\n"):[],raw:t[0]};if(e.header.length===e.align.length){let t,n=e.align.length;for(t=0;n>t;t++)e.align[t]=/^ *-+: *$/.test(e.align[t])?"right":/^ *:-+: *$/.test(e.align[t])?"center":/^ *:-+ *$/.test(e.align[t])?"left":null;for(n=e.cells.length,t=0;n>t;t++)e.cells[t]=w(e.cells[t],e.header.length);return e}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){const e=t[0].replace(/^ *> ?/gm,"");return{type:"blockquote",raw:t[0],text:e}}}list(e){const t=this.rules.block.list.exec(e);if(t){let e=t[0];const n=t[2],s=n.length>1,r={type:"list",raw:e,ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]},i=t[0].match(this.rules.block.item);let l,a,o,c,h,p,u,g,d,f=!1,k=i.length;o=this.rules.block.listItemStart.exec(i[0]);for(let t=0;k>t;t++){if(l=i[t],e=l,this.options.pedantic||(d=l.match(RegExp("\\n\\s*\\n {0,"+(o[0].length-1)+"}\\S")),d&&(h=l.length-d.index+i.slice(t+1).join("\n").length,r.raw=r.raw.substring(0,r.raw.length-h),l=l.substring(0,d.index),e=l,k=t+1)),t!==k-1){if(c=this.rules.block.listItemStart.exec(i[t+1]),this.options.pedantic?c[1].length>o[1].length:c[1].length>=o[0].length||c[1].length>3){i.splice(t,2,i[t]+(this.options.pedantic||c[1].length>=o[0].length||i[t].match(/\n$/)?"\n":"")+i[t+1]),t--,k--;continue}(!this.options.pedantic||this.options.smartLists?c[2][c[2].length-1]!==n[n.length-1]:s===(1===c[2].length))&&(h=i.slice(t+1).join("\n").length,r.raw=r.raw.substring(0,r.raw.length-h),t=k-1),o=c}a=l.length,l=l.replace(/^ *([*+-]|\d+[.)]) ?/,""),~l.indexOf("\n ")&&(a-=l.length,l=l.replace(this.options.pedantic?/^ {1,4}/gm:RegExp("^ {1,"+a+"}","gm"),"")),l=b(l,"\n"),t!==k-1&&(e+="\n"),p=f||/\n\n(?!\s*$)/.test(e),t!==k-1&&(f="\n\n"===e.slice(-2),p||(p=f)),p&&(r.loose=!0),this.options.gfm&&(u=/^\[[ xX]\] /.test(l),g=void 0,u&&(g=" "!==l[1],l=l.replace(/^\[[ xX]\] +/,""))),r.items.push({type:"list_item",raw:e,task:u,checked:g,loose:p,text:l})}return r}}html(e){const t=this.rules.block.html.exec(e);if(t)return{type:this.options.sanitize?"paragraph":"html",raw:t[0],pre:!this.options.sanitizer&&("pre"===t[1]||"script"===t[1]||"style"===t[1]),text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(t[0]):_(t[0]):t[0]}}def(e){const t=this.rules.block.def.exec(e);if(t)return t[3]&&(t[3]=t[3].substring(1,t[3].length-1)),{type:"def",tag:t[1].toLowerCase().replace(/\s+/g," "),raw:t[0],href:t[2],title:t[3]}}table(e){const t=this.rules.block.table.exec(e);if(t){const e={type:"table",header:w(t[1].replace(/^ *| *\| *$/g,"")),align:t[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:t[3]?t[3].replace(/\n$/,"").split("\n"):[]};if(e.header.length===e.align.length){e.raw=t[0];let n,s=e.align.length;for(n=0;s>n;n++)e.align[n]=/^ *-+: *$/.test(e.align[n])?"right":/^ *:-+: *$/.test(e.align[n])?"center":/^ *:-+ *$/.test(e.align[n])?"left":null;for(s=e.cells.length,n=0;s>n;n++)e.cells[n]=w(e.cells[n].replace(/^ *\| *| *\| *$/g,""),e.header.length);return e}}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:"="===t[2].charAt(0)?1:2,text:t[1]}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t)return{type:"paragraph",raw:t[0],text:"\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1]}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0]}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:_(t[1])}}tag(e,t,n){const s=this.rules.inline.tag.exec(e);if(s)return!t&&/^/i.test(s[0])&&(t=!1),!n&&/^<(pre|code|kbd|script)(\s|>)/i.test(s[0])?n=!0:n&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(s[0])&&(n=!1),{type:this.options.sanitize?"text":"html",raw:s[0],inLink:t,inRawBlock:n,text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(s[0]):_(s[0]):s[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;const t=b(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=y(t[2],"()");if(e>-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),/^$/.test(e)?n.slice(1):n.slice(1,-1)),i(t,{href:n?n.replace(this.rules.inline._escapes,"$1"):n,title:s?s.replace(this.rules.inline._escapes,"$1"):s},t[0])}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){let e=(n[2]||n[1]).replace(/\s+/g," ");if(e=t[e.toLowerCase()],!e||!e.href){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return i(n,e,n[0])}}emStrong(e,t,n=""){let s=this.rules.inline.emStrong.lDelim.exec(e);if(!s)return;if(s[3]&&n.match(/[\p{L}\p{N}]/u))return;const r=s[1]||s[2]||"";if(!r||r&&(""===n||this.rules.inline.punctuation.exec(n))){const n=s[0].length-1;let r,i,l=n,a=0;const o="*"===s[0][0]?this.rules.inline.emStrong.rDelimAst:this.rules.inline.emStrong.rDelimUnd;for(o.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=o.exec(t));)if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],r)if(i=r.length,s[3]||s[4])l+=i;else if(!((s[5]||s[6])&&n%3)||(n+i)%3){if(l-=i,0>=l)return i=Math.min(i,i+l+a),Math.min(n,i)%2?{type:"em",raw:e.slice(0,n+s.index+i+1),text:e.slice(1,n+s.index+i)}:{type:"strong",raw:e.slice(0,n+s.index+i+1),text:e.slice(2,n+s.index+i-1)}}else a+=i}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const n=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return n&&s&&(e=e.substring(1,e.length-1)),e=_(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2]}}autolink(e,t){const n=this.rules.inline.autolink.exec(e);if(n){let e,s;return"@"===n[2]?(e=_(this.options.mangle?t(n[1]):n[1]),s="mailto:"+e):(e=_(n[1]),s=e),{type:"link",raw:n[0],text:e,href:s,tokens:[{type:"text",raw:e,text:e}]}}}url(e,t){let n;if(n=this.rules.inline.url.exec(e)){let e,s;if("@"===n[2])e=_(this.options.mangle?t(n[0]):n[0]),s="mailto:"+e;else{let t;do{t=n[0],n[0]=this.rules.inline._backpedal.exec(n[0])[0]}while(t!==n[0]);e=_(n[0]),s="www."===n[1]?"http://"+e:e}return{type:"link",raw:n[0],text:e,href:s,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e,t,n){const s=this.rules.inline.text.exec(e);if(s){let e;return e=t?this.options.sanitize?this.options.sanitizer?this.options.sanitizer(s[0]):_(s[0]):s[0]:_(this.options.smartypants?n(s[0]):s[0]),{type:"text",raw:s[0],text:e}}}};const{noopTest:$,edit:S,merge:T}=x,A={newline:/^(?: *(?:\n|$))+/,code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,fences:/^ {0,3}(`{3,}(?=[^`\n]*\n)|~{3,})([^\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?:\n+|$)|$)/,hr:/^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3})(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?! {0,3}bull )\n*|\s*$)/,html:"^ {0,3}(?:<(script|pre|style)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))",def:/^ {0,3}\[(label)\]: *\n? *]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/,nptable:$,table:$,lheading:/^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html| +\n)[^\n]+)*)/,text:/^[^\n]+/,_label:/(?!\s*\])(?:\\[\[\]]|[^\[\]])+/,_title:/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/};A.def=S(A.def).replace("label",A._label).replace("title",A._title).getRegex(),A.bullet=/(?:[*+-]|\d{1,9}[.)])/,A.item=/^( *)(bull) ?[^\n]*(?:\n(?! *bull ?)[^\n]*)*/,A.item=S(A.item,"gm").replace(/bull/g,A.bullet).getRegex(),A.listItemStart=S(/^( *)(bull) */).replace("bull",A.bullet).getRegex(),A.list=S(A.list).replace(/bull/g,A.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+A.def.source+")").getRegex(),A._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",A._comment=/|$)/,A.html=S(A.html,"i").replace("comment",A._comment).replace("tag",A._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),A.paragraph=S(A._paragraph).replace("hr",A.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|!--)").replace("tag",A._tag).getRegex(),A.blockquote=S(A.blockquote).replace("paragraph",A.paragraph).getRegex(),A.normal=T({},A),A.gfm=T({},A.normal,{nptable:"^ *([^|\\n ].*\\|.*)\\n {0,3}([-:]+ *\\|[-| :]*)(?:\\n((?:(?!\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)",table:"^ *\\|(.+)\\n {0,3}\\|?( *[-:]+[-| :]*)(?:\\n *((?:(?!\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)"}),A.gfm.nptable=S(A.gfm.nptable).replace("hr",A.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|!--)").replace("tag",A._tag).getRegex(),A.gfm.table=S(A.gfm.table).replace("hr",A.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|!--)").replace("tag",A._tag).getRegex(),A.pedantic=T({},A.normal,{html:S("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",A._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:$,paragraph:S(A.normal._paragraph).replace("hr",A.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",A.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()});const I={escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:$,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/,nolink:/^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/,reflinkSearch:"reflink|nolink(?!\\()",emStrong:{lDelim:/^(?:\*+(?:([punct_])|[^\s*]))|^_+(?:([punct*])|([^\s_]))/,rDelimAst:/\_\_[^_*]*?\*[^_*]*?\_\_|[punct_](\*+)(?=[\s]|$)|[^punct*_\s](\*+)(?=[punct_\s]|$)|[punct_\s](\*+)(?=[^punct*_\s])|[\s](\*+)(?=[punct_])|[punct_](\*+)(?=[punct_])|[^punct*_\s](\*+)(?=[^punct*_\s])/,rDelimUnd:/\*\*[^_*]*?\_[^_*]*?\*\*|[punct*](\_+)(?=[\s]|$)|[^punct*_\s](\_+)(?=[punct*\s]|$)|[punct*\s](\_+)(?=[^punct*_\s])|[\s](\_+)(?=[punct*])|[punct*](\_+)(?=[punct*])/},code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:$,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\?@\\[\\]`^{|}~"};I.punctuation=S(I.punctuation).replace(/punctuation/g,I._punctuation).getRegex(),I.blockSkip=/\[[^\]]*?\]\([^\)]*?\)|`[^`]*?`|<[^>]*?>/g,I.escapedEmSt=/\\\*|\\_/g,I._comment=S(A._comment).replace("(?:--\x3e|$)","--\x3e").getRegex(),I.emStrong.lDelim=S(I.emStrong.lDelim).replace(/punct/g,I._punctuation).getRegex(),I.emStrong.rDelimAst=S(I.emStrong.rDelimAst,"g").replace(/punct/g,I._punctuation).getRegex(),I.emStrong.rDelimUnd=S(I.emStrong.rDelimUnd,"g").replace(/punct/g,I._punctuation).getRegex(),I._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,I._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,I._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,I.autolink=S(I.autolink).replace("scheme",I._scheme).replace("email",I._email).getRegex(),I._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,I.tag=S(I.tag).replace("comment",I._comment).replace("attribute",I._attribute).getRegex(),I._label=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,I._href=/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/,I._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,I.link=S(I.link).replace("label",I._label).replace("href",I._href).replace("title",I._title).getRegex(),I.reflink=S(I.reflink).replace("label",I._label).getRegex(),I.reflinkSearch=S(I.reflinkSearch,"g").replace("reflink",I.reflink).replace("nolink",I.nolink).getRegex(),I.normal=T({},I),I.pedantic=T({},I.normal,{strong:{start:/^__|\*\*/,middle:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,endAst:/\*\*(?!\*)/g,endUnd:/__(?!_)/g},em:{start:/^_|\*/,middle:/^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/,endAst:/\*(?!\*)/g,endUnd:/_(?!_)/g},link:S(/^!?\[(label)\]\((.*?)\)/).replace("label",I._label).getRegex(),reflink:S(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",I._label).getRegex()}),I.gfm=T({},I.normal,{escape:S(I.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\'+(n?e:U(e,!0))+"\n":"
"+(n?e:U(e,!0))+"
\n"}blockquote(e){return"
\n"+e+"
\n"}html(e){return e}heading(e,t,n,s){return this.options.headerIds?"'+e+"\n":""+e+"\n"}hr(){return this.options.xhtml?"
\n":"
\n"}list(e,t,n){const s=t?"ol":"ul";return"<"+s+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+"\n"}listitem(e){return"
  • "+e+"
  • \n"}checkbox(e){return" "}paragraph(e){return"

    "+e+"

    \n"}table(e,t){return t&&(t=""+t+""),"\n\n"+e+"\n"+t+"
    \n"}tablerow(e){return"\n"+e+"\n"}tablecell(e,t){const n=t.header?"th":"td";return(t.align?"<"+n+' align="'+t.align+'">':"<"+n+">")+e+"\n"}strong(e){return""+e+""}em(e){return""+e+""}codespan(e){return""+e+""}br(){return this.options.xhtml?"
    ":"
    "}del(e){return""+e+""}link(e,t,n){if(null===(e=O(this.options.sanitize,this.options.baseUrl,e)))return n;let s='
    ",s}image(e,t,n){if(null===(e=O(this.options.sanitize,this.options.baseUrl,e)))return n;let s=''+n+'":">",s}text(e){return e}},L=class{strong(e){return e}em(e){return e}codespan(e){return e}del(e){return e}html(e){return e}text(e){return e}link(e,t,n){return""+n}image(e,t,n){return""+n}br(){return""}},j=class{constructor(){this.seen={}}serialize(e){return e.toLowerCase().trim().replace(/<[!\/a-z].*?>/gi,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-")}getNextSafeSlug(e,t){let n=e,s=0;if(this.seen.hasOwnProperty(n)){s=this.seen[e];do{s++,n=e+"-"+s}while(this.seen.hasOwnProperty(n))}return t||(this.seen[e]=s,this.seen[n]=0),n}slug(e,t={}){const n=this.serialize(e);return this.getNextSafeSlug(n,t.dryrun)}};const B=P,M=L,N=j,{defaults:F}=c.exports,{unescape:X}=x,G=class e{constructor(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||q,this.options.tokenizer=this.options.tokenizer||new v,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options;const t={block:D.normal,inline:E.normal};this.options.pedantic?(t.block=D.pedantic,t.inline=E.pedantic):this.options.gfm&&(t.block=D.gfm,t.inline=this.options.breaks?E.breaks:E.gfm),this.tokenizer.rules=t}static get rules(){return{block:D,inline:E}}static lex(t,n){return new e(n).lex(t)}static lexInline(t,n){return new e(n).inlineTokens(t)}lex(e){return e=e.replace(/\r\n|\r/g,"\n").replace(/\t/g," "),this.blockTokens(e,this.tokens,!0),this.inline(this.tokens),this.tokens}blockTokens(e,t=[],n=!0){let s,r,i,l,a,o;for(this.options.pedantic&&(e=e.replace(/^ +$/gm,""));e;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some((n=>!!(s=n.call(this,e,t))&&(e=e.substring(s.raw.length),t.push(s),!0)))))if(s=this.tokenizer.space(e))e=e.substring(s.raw.length),s.type&&t.push(s);else if(s=this.tokenizer.code(e))e=e.substring(s.raw.length),l=t[t.length-1],l&&"paragraph"===l.type?(l.raw+="\n"+s.raw,l.text+="\n"+s.text):t.push(s);else if(s=this.tokenizer.fences(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.heading(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.nptable(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.hr(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.blockquote(e))e=e.substring(s.raw.length),s.tokens=this.blockTokens(s.text,[],n),t.push(s);else if(s=this.tokenizer.list(e)){for(e=e.substring(s.raw.length),i=s.items.length,r=0;i>r;r++)s.items[r].tokens=this.blockTokens(s.items[r].text,[],!1);t.push(s)}else if(s=this.tokenizer.html(e))e=e.substring(s.raw.length),t.push(s);else if(n&&(s=this.tokenizer.def(e)))e=e.substring(s.raw.length),this.tokens.links[s.tag]||(this.tokens.links[s.tag]={href:s.href,title:s.title});else if(s=this.tokenizer.table(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.lheading(e))e=e.substring(s.raw.length),t.push(s);else{if(a=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((function(e){s=e.call(this,n),"number"!=typeof s||0>s||(t=Math.min(t,s))})),1/0>t&&t>=0&&(a=e.substring(0,t+1))}if(n&&(s=this.tokenizer.paragraph(a)))l=t[t.length-1],o&&"paragraph"===l.type?(l.raw+="\n"+s.raw,l.text+="\n"+s.text):t.push(s),o=a.length!==e.length,e=e.substring(s.raw.length);else if(s=this.tokenizer.text(e))e=e.substring(s.raw.length),l=t[t.length-1],l&&"text"===l.type?(l.raw+="\n"+s.raw,l.text+="\n"+s.text):t.push(s);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw Error(t)}}return t}inline(e){let t,n,s,r,i,l;const a=e.length;for(t=0;a>t;t++)switch(l=e[t],l.type){case"paragraph":case"text":case"heading":l.tokens=[],this.inlineTokens(l.text,l.tokens);break;case"table":for(l.tokens={header:[],cells:[]},r=l.header.length,n=0;r>n;n++)l.tokens.header[n]=[],this.inlineTokens(l.header[n],l.tokens.header[n]);for(r=l.cells.length,n=0;r>n;n++)for(i=l.cells[n],l.tokens.cells[n]=[],s=0;i.length>s;s++)l.tokens.cells[n][s]=[],this.inlineTokens(i[s],l.tokens.cells[n][s]);break;case"blockquote":this.inline(l.tokens);break;case"list":for(r=l.items.length,n=0;r>n;n++)this.inline(l.items[n].tokens)}return e}inlineTokens(e,t=[],n=!1,s=!1){let r,i,o,c,h,p,u=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(c=this.tokenizer.rules.inline.reflinkSearch.exec(u));)e.includes(c[0].slice(c[0].lastIndexOf("[")+1,-1))&&(u=u.slice(0,c.index)+"["+Z("a",c[0].length-2)+"]"+u.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(c=this.tokenizer.rules.inline.blockSkip.exec(u));)u=u.slice(0,c.index)+"["+Z("a",c[0].length-2)+"]"+u.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(c=this.tokenizer.rules.inline.escapedEmSt.exec(u));)u=u.slice(0,c.index)+"++"+u.slice(this.tokenizer.rules.inline.escapedEmSt.lastIndex);for(;e;)if(h||(p=""),h=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((n=>!!(r=n.call(this,e,t))&&(e=e.substring(r.raw.length),t.push(r),!0)))))if(r=this.tokenizer.escape(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.tag(e,n,s))e=e.substring(r.raw.length),n=r.inLink,s=r.inRawBlock,i=t[t.length-1],i&&"text"===r.type&&"text"===i.type?(i.raw+=r.raw,i.text+=r.text):t.push(r);else if(r=this.tokenizer.link(e))e=e.substring(r.raw.length),"link"===r.type&&(r.tokens=this.inlineTokens(r.text,[],!0,s)),t.push(r);else if(r=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(r.raw.length),i=t[t.length-1],"link"===r.type?(r.tokens=this.inlineTokens(r.text,[],!0,s),t.push(r)):i&&"text"===r.type&&"text"===i.type?(i.raw+=r.raw,i.text+=r.text):t.push(r);else if(r=this.tokenizer.emStrong(e,u,p))e=e.substring(r.raw.length),r.tokens=this.inlineTokens(r.text,[],n,s),t.push(r);else if(r=this.tokenizer.codespan(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.br(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.del(e))e=e.substring(r.raw.length),r.tokens=this.inlineTokens(r.text,[],n,s),t.push(r);else if(r=this.tokenizer.autolink(e,a))e=e.substring(r.raw.length),t.push(r);else if(n||!(r=this.tokenizer.url(e,a))){if(o=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((function(e){s=e.call(this,n),"number"!=typeof s||0>s||(t=Math.min(t,s))})),1/0>t&&t>=0&&(o=e.substring(0,t+1))}if(r=this.tokenizer.inlineText(o,s,l))e=e.substring(r.raw.length),"_"!==r.raw.slice(-1)&&(p=r.raw.slice(-1)),h=!0,i=t[t.length-1],i&&"text"===i.type?(i.raw+=r.raw,i.text+=r.text):t.push(r);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw Error(t)}}else e=e.substring(r.raw.length),t.push(r);return t}},V=class e{constructor(e){this.options=e||F,this.options.renderer=this.options.renderer||new B,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new M,this.slugger=new N}static parse(t,n){return new e(n).parse(t)}static parseInline(t,n){return new e(n).parseInline(t)}parse(e,t=!0){let n,s,r,i,l,a,o,c,h,p,u,g,d,f,k,x,m,b,w,_="";const y=e.length;for(n=0;y>n;n++)if(p=e[n],this.options.extensions&&this.options.extensions.renderers&&this.options.extensions.renderers[p.type]&&(w=this.options.extensions.renderers[p.type].call(this,p),!1!==w||!["space","hr","heading","code","table","blockquote","list","html","paragraph","text"].includes(p.type)))_+=w||"";else switch(p.type){case"space":continue;case"hr":_+=this.renderer.hr();continue;case"heading":_+=this.renderer.heading(this.parseInline(p.tokens),p.depth,X(this.parseInline(p.tokens,this.textRenderer)),this.slugger);continue;case"code":_+=this.renderer.code(p.text,p.lang,p.escaped);continue;case"table":for(c="",o="",i=p.header.length,s=0;i>s;s++)o+=this.renderer.tablecell(this.parseInline(p.tokens.header[s]),{header:!0,align:p.align[s]});for(c+=this.renderer.tablerow(o),h="",i=p.cells.length,s=0;i>s;s++){for(a=p.tokens.cells[s],o="",l=a.length,r=0;l>r;r++)o+=this.renderer.tablecell(this.parseInline(a[r]),{header:!1,align:p.align[r]});h+=this.renderer.tablerow(o)}_+=this.renderer.table(c,h);continue;case"blockquote":h=this.parse(p.tokens),_+=this.renderer.blockquote(h);continue;case"list":for(u=p.ordered,g=p.start,d=p.loose,i=p.items.length,h="",s=0;i>s;s++)k=p.items[s],x=k.checked,m=k.task,f="",k.task&&(b=this.renderer.checkbox(x),d?k.tokens.length>0&&"text"===k.tokens[0].type?(k.tokens[0].text=b+" "+k.tokens[0].text,k.tokens[0].tokens&&k.tokens[0].tokens.length>0&&"text"===k.tokens[0].tokens[0].type&&(k.tokens[0].tokens[0].text=b+" "+k.tokens[0].tokens[0].text)):k.tokens.unshift({type:"text",text:b}):f+=b),f+=this.parse(k.tokens,d),h+=this.renderer.listitem(f,m,x);_+=this.renderer.list(h,u,g);continue;case"html":_+=this.renderer.html(p.text);continue;case"paragraph":_+=this.renderer.paragraph(this.parseInline(p.tokens));continue;case"text":for(h=p.tokens?this.parseInline(p.tokens):p.text;y>n+1&&"text"===e[n+1].type;)p=e[++n],h+="\n"+(p.tokens?this.parseInline(p.tokens):p.text);_+=t?this.renderer.paragraph(h):h;continue;default:{const e='Token with "'+p.type+'" type was not found.';if(this.options.silent)return void console.error(e);throw Error(e)}}return _}parseInline(e,t){t=t||this.renderer;let n,s,r,i="";const l=e.length;for(n=0;l>n;n++)if(s=e[n],this.options.extensions&&this.options.extensions.renderers&&this.options.extensions.renderers[s.type]&&(r=this.options.extensions.renderers[s.type].call(this,s),!1!==r||!["escape","html","link","image","strong","em","codespan","br","del","text"].includes(s.type)))i+=r||"";else switch(s.type){case"escape":i+=t.text(s.text);break;case"html":i+=t.html(s.text);break;case"link":i+=t.link(s.href,s.title,this.parseInline(s.tokens,t));break;case"image":i+=t.image(s.href,s.title,s.text);break;case"strong":i+=t.strong(this.parseInline(s.tokens,t));break;case"em":i+=t.em(this.parseInline(s.tokens,t));break;case"codespan":i+=t.codespan(s.text);break;case"br":i+=t.br();break;case"del":i+=t.del(this.parseInline(s.tokens,t));break;case"text":i+=t.text(s.text);break;default:{const e='Token with "'+s.type+'" type was not found.';if(this.options.silent)return void console.error(e);throw Error(e)}}return i}},H=z,J=P,K=L,Q=j,{merge:W,checkSanitizeDeprecation:Y,escape:ee}=x,{getDefaults:te,changeDefaults:ne,defaults:se}=c.exports;o.options=o.setOptions=e=>(W(o.defaults,e),ne(o.defaults),o),o.getDefaults=te,o.defaults=se,o.use=function(...e){const t=W({},...e),n=o.defaults.extensions||{renderers:{},childTokens:{}};let s;e.forEach((e=>{if(e.extensions&&(s=!0,e.extensions.forEach((e=>{if(!e.name)throw Error("extension name required");if(e.renderer){const t=n.renderers?n.renderers[e.name]:null;n.renderers[e.name]=t?function(...n){let s=e.renderer.apply(this,n);return!1===s&&(s=t.apply(this,n)),s}:e.renderer}if(e.tokenizer){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw Error("extension level must be 'block' or 'inline'");n[e.level]?n[e.level].unshift(e.tokenizer):n[e.level]=[e.tokenizer],e.start&&("block"===e.level?n.startBlock?n.startBlock.push(e.start):n.startBlock=[e.start]:"inline"===e.level&&(n.startInline?n.startInline.push(e.start):n.startInline=[e.start]))}e.childTokens&&(n.childTokens[e.name]=e.childTokens)}))),e.renderer){const n=o.defaults.renderer||new J;for(const t in e.renderer){const s=n[t];n[t]=(...r)=>{let i=e.renderer[t].apply(n,r);return!1===i&&(i=s.apply(n,r)),i}}t.renderer=n}if(e.tokenizer){const n=o.defaults.tokenizer||new H;for(const t in e.tokenizer){const s=n[t];n[t]=(...r)=>{let i=e.tokenizer[t].apply(n,r);return!1===i&&(i=s.apply(n,r)),i}}t.tokenizer=n}if(e.walkTokens){const n=o.defaults.walkTokens;t.walkTokens=t=>{e.walkTokens.call(this,t),n&&n(t)}}s&&(t.extensions=n),o.setOptions(t)}))},o.walkTokens=(e,t)=>{for(const n of e)switch(t(n),n.type){case"table":for(const e of n.tokens.header)o.walkTokens(e,t);for(const e of n.tokens.cells)for(const n of e)o.walkTokens(n,t);break;case"list":o.walkTokens(n.items,t);break;default:o.defaults.extensions&&o.defaults.extensions.childTokens&&o.defaults.extensions.childTokens[n.type]?o.defaults.extensions.childTokens[n.type].forEach((e=>{o.walkTokens(n[e],t)})):n.tokens&&o.walkTokens(n.tokens,t)}},o.parseInline=(e,t)=>{if(null==e)throw Error("marked.parseInline(): input parameter is undefined or null");if("string"!=typeof e)throw Error("marked.parseInline(): input parameter is of type "+{}.toString.call(e)+", string expected");t=W({},o.defaults,t||{}),Y(t);try{const n=G.lexInline(e,t);return t.walkTokens&&o.walkTokens(n,t.walkTokens),V.parseInline(n,t)}catch(e){if(e.message+="\nPlease report this to https://github.com/markedjs/marked.",t.silent)return"

    An error occurred:

    "+ee(e.message+"",!0)+"
    ";throw e}},o.Parser=V,o.parser=V.parse,o.Renderer=J,o.TextRenderer=K,o.Lexer=G,o.lexer=G.lex,o.Tokenizer=H,o.Slugger=Q,o.parse=o;var re=o})); 2 | //# sourceMappingURL=marked.esm.ab6d9469.js.map 3 | -------------------------------------------------------------------------------- /dist/panel/component.d.ts: -------------------------------------------------------------------------------- 1 | import type Delegate from './delegate'; 2 | declare type Props = { 3 | delegate: Delegate; 4 | }; 5 | export declare function PanelComponent(props: Props): import("solid-js").JSX.Element; 6 | export {}; 7 | -------------------------------------------------------------------------------- /dist/panel/delegate.d.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable, Disposable, Emitter } from 'atom'; 2 | import type { LinterMessage } from '../types'; 3 | export default class PanelDelegate { 4 | emitter: Emitter<{}, { 5 | 'observe-messages': Array; 6 | }>; 7 | messages: Array; 8 | filteredMessages: Array; 9 | subscriptions: CompositeDisposable; 10 | panelRepresents?: 'Entire Project' | 'Current File' | 'Current Line'; 11 | constructor(); 12 | getFilteredMessages(): Array; 13 | update(messages?: Array | null | undefined): void; 14 | onDidChangeMessages(callback: (messages: Array) => void): Disposable; 15 | dispose(): void; 16 | } 17 | -------------------------------------------------------------------------------- /dist/panel/dock.d.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable, Dock, WorkspaceCenter } from 'atom'; 2 | import type Delegate from './delegate'; 3 | export declare type PaneContainer = Dock & { 4 | state: { 5 | size: number; 6 | }; 7 | render: Function; 8 | paneForItem: WorkspaceCenter['paneForItem']; 9 | location: string; 10 | }; 11 | export default class PanelDock { 12 | element: HTMLElement; 13 | subscriptions: CompositeDisposable; 14 | panelHeight: number; 15 | alwaysTakeMinimumSpace: boolean; 16 | lastSetPaneHeight?: number; 17 | constructor(delegate: Delegate); 18 | doPanelResize(forConfigHeight?: boolean): void; 19 | getURI(): string; 20 | getTitle(): string; 21 | getDefaultLocation(): string; 22 | getAllowedLocations(): string[]; 23 | getPreferredHeight(): any; 24 | dispose(): void; 25 | } 26 | -------------------------------------------------------------------------------- /dist/panel/index.d.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable } from 'atom'; 2 | import Delegate from './delegate'; 3 | import PanelDock from './dock'; 4 | import type { LinterMessage } from '../types'; 5 | export default class Panel { 6 | panel: PanelDock | null; 7 | element: HTMLElement; 8 | delegate: Delegate; 9 | messages: Array; 10 | deactivating: boolean; 11 | subscriptions: CompositeDisposable; 12 | showPanelConfig: boolean; 13 | hidePanelWhenEmpty: boolean; 14 | showPanelStateMessages: boolean; 15 | activationTimer: number; 16 | constructor(); 17 | private getPanelLocation; 18 | activate(): Promise; 19 | update(newMessages?: Array | null | undefined): Promise; 20 | refresh(): Promise; 21 | dispose(): void; 22 | } 23 | -------------------------------------------------------------------------------- /dist/status-bar/element.d.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable, Emitter } from 'atom'; 2 | import type { Disposable } from 'atom'; 3 | export default class Element { 4 | item: HTMLElement; 5 | itemErrors: HTMLElement; 6 | itemWarnings: HTMLElement; 7 | itemInfos: HTMLElement; 8 | emitter: Emitter<{}, { 9 | click: 'error' | 'warning' | 'info'; 10 | }>; 11 | subscriptions: CompositeDisposable; 12 | constructor(); 13 | setVisibility(prefix: string, visibility: boolean): void; 14 | update(countErrors: number, countWarnings: number, countInfos: number): void; 15 | onDidClick(callback: (type: 'error' | 'warning' | 'info') => void): Disposable; 16 | dispose(): void; 17 | } 18 | -------------------------------------------------------------------------------- /dist/status-bar/helpers.d.ts: -------------------------------------------------------------------------------- 1 | export declare function getElement(icon: string): HTMLElement; 2 | -------------------------------------------------------------------------------- /dist/status-bar/index.d.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable } from 'atom'; 2 | import type { StatusBar as StatusBarRegistry } from 'atom/status-bar'; 3 | import Element from './element'; 4 | import type { LinterMessage } from '../types'; 5 | export default class StatusBar { 6 | element: Element; 7 | messages: Array; 8 | subscriptions: CompositeDisposable; 9 | statusBarRepresents?: 'Entire Project' | 'Current File'; 10 | statusBarClickBehavior?: 'Toggle Panel' | 'Jump to next issue' | 'Toggle Status Bar Scope'; 11 | constructor(); 12 | update(messages?: Array): void; 13 | attach(statusBarRegistry: StatusBarRegistry): void; 14 | dispose(): void; 15 | } 16 | -------------------------------------------------------------------------------- /dist/tooltip/delegate.d.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable, Emitter } from 'atom'; 2 | import type { Disposable } from 'atom'; 3 | export default class TooltipDelegate { 4 | emitter: Emitter; 5 | expanded: boolean; 6 | subscriptions: CompositeDisposable; 7 | showProviderName?: boolean; 8 | constructor(); 9 | onShouldUpdate(callback: () => void): Disposable; 10 | onShouldExpand(callback: () => void): Disposable; 11 | onShouldCollapse(callback: () => void): Disposable; 12 | dispose(): void; 13 | } 14 | -------------------------------------------------------------------------------- /dist/tooltip/index.d.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable, Emitter } from 'atom'; 2 | import type { Disposable, Point, TextEditor, DisplayMarker } from 'atom'; 3 | import type { LinterMessage } from '../types'; 4 | export default class Tooltip { 5 | marker: DisplayMarker; 6 | element: HTMLElement; 7 | emitter: Emitter<{ 8 | 'did-destroy': never; 9 | }, {}>; 10 | messages: Array; 11 | subscriptions: CompositeDisposable; 12 | constructor(messages: Array, position: Point, textEditor: TextEditor); 13 | isValid(position: Point, messages: Map): boolean; 14 | onDidDestroy(callback: () => void): Disposable; 15 | dispose(): void; 16 | } 17 | -------------------------------------------------------------------------------- /dist/tooltip/message.d.ts: -------------------------------------------------------------------------------- 1 | import type TooltipDelegate from './delegate'; 2 | import type { Message } from '../types'; 3 | declare type Props = { 4 | key: string; 5 | message: Message; 6 | delegate: TooltipDelegate; 7 | }; 8 | export default function MessageElement(props: Props): import("solid-js").JSX.Element; 9 | export {}; 10 | -------------------------------------------------------------------------------- /dist/tree-view/helpers.d.ts: -------------------------------------------------------------------------------- 1 | import type { LinterMessage } from '../types'; 2 | import type { TreeViewHighlight } from './index'; 3 | export declare function calculateDecorations(decorateOnTreeView: 'Files and Directories' | 'Files' | undefined, messages: Array): Record; 4 | -------------------------------------------------------------------------------- /dist/tree-view/index.d.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable } from 'atom'; 2 | import type { LinterMessage } from '../types'; 3 | export declare type TreeViewHighlight = { 4 | info: boolean; 5 | error: boolean; 6 | warning: boolean; 7 | }; 8 | export default class TreeView { 9 | messages: Array; 10 | decorations: Record; 11 | subscriptions: CompositeDisposable; 12 | decorateOnTreeView?: 'Files and Directories' | 'Files' | 'None'; 13 | constructor(); 14 | update(givenMessages?: Array | null | undefined): void; 15 | applyDecorations(decorations: Record): void; 16 | dispose(): void; 17 | static getElement(): HTMLElement | null; 18 | static getElementByPath(parent: HTMLElement, filePath: string): HTMLElement | null; 19 | } 20 | -------------------------------------------------------------------------------- /dist/types/atom.d.ts: -------------------------------------------------------------------------------- 1 | import { TextEditor, Package, CommandEvent } from 'atom' 2 | 3 | // TODO: uses internal API 4 | export type TextEditorExtra = TextEditor & { 5 | getURI?: () => string 6 | isAlive?: () => boolean 7 | } 8 | 9 | // TODO: uses internal API 10 | interface PackageDepsList { 11 | [key: string]: string[] 12 | } 13 | 14 | export type PackageExtra = Package & { 15 | metadata: PackageDepsList 16 | } 17 | 18 | export interface CommandEventExtra extends CommandEvent { 19 | // TODO add to @types/atom 20 | // TODO will it be undefined? 21 | originalEvent?: T 22 | } 23 | -------------------------------------------------------------------------------- /dist/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './atom' 2 | export * from './intentions' 3 | export * from './linter' 4 | -------------------------------------------------------------------------------- /dist/types/intentions.d.ts: -------------------------------------------------------------------------------- 1 | import { TextEditor, Point } from 'atom' 2 | 3 | // from intentions package: 4 | // https://github.com/steelbrain/intentions/blob/master/lib/types.js 5 | export type ListItem = { 6 | // // Automatically added 7 | readonly __$sb_intentions_class?: string 8 | 9 | // From providers 10 | icon?: string 11 | class?: string 12 | title: string 13 | priority: number 14 | selected(): void 15 | } 16 | 17 | export type IntentionsListProvider = { 18 | grammarScopes: Array 19 | getIntentions(parameters: { textEditor: TextEditor; bufferPosition: Point }): Array | Promise> 20 | } 21 | -------------------------------------------------------------------------------- /dist/types/linter.d.ts: -------------------------------------------------------------------------------- 1 | export * from './linter/types/linter' 2 | -------------------------------------------------------------------------------- /lib/busy-signal.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable } from 'atom' 2 | const { config, project } = atom 3 | import type { Linter } from './types' 4 | import { BusySignalProvider, BusySignalRegistry } from 'atom-ide-base' 5 | 6 | export default class BusySignal { 7 | provider: BusySignalProvider | null | undefined 8 | executing: Set<{ 9 | linter: Linter 10 | filePath: string | null | undefined 11 | }> = new Set() 12 | providerTitles: Set = new Set() 13 | useBusySignal: boolean = true 14 | subscriptions: CompositeDisposable = new CompositeDisposable() 15 | 16 | constructor() { 17 | this.subscriptions.add( 18 | config.observe('linter-ui-default.useBusySignal', (useBusySignal: boolean) => { 19 | this.useBusySignal = useBusySignal 20 | }), 21 | ) 22 | } 23 | attach(registry: BusySignalRegistry) { 24 | this.provider = registry.create() 25 | this.update() 26 | } 27 | update() { 28 | const provider = this.provider 29 | if (!provider) { 30 | return 31 | } 32 | if (!this.useBusySignal) { 33 | return 34 | } 35 | const fileMap: Map> = new Map() 36 | const currentTitles = new Set() 37 | 38 | for (const { filePath, linter } of this.executing) { 39 | let names = fileMap.get(filePath) 40 | if (!names) { 41 | fileMap.set(filePath, (names = [])) 42 | } 43 | names.push(linter.name) 44 | } 45 | 46 | for (const [filePath, names] of fileMap) { 47 | const path = typeof filePath === 'string' ? ` on ${project.relativizePath(filePath)[1]}` : '' 48 | names.forEach(name => { 49 | const title = `${name}${path}` 50 | currentTitles.add(title) 51 | if (!this.providerTitles.has(title)) { 52 | // Add the title since it hasn't been seen before 53 | this.providerTitles.add(title) 54 | provider.add(title) 55 | } 56 | }) 57 | } 58 | 59 | // Remove any titles no longer active 60 | this.providerTitles.forEach(title => { 61 | if (!currentTitles.has(title)) { 62 | provider.remove(title) 63 | this.providerTitles.delete(title) 64 | } 65 | }) 66 | 67 | fileMap.clear() 68 | } 69 | getExecuting(linter: Linter, filePath: string | null | undefined) { 70 | for (const entry of this.executing) { 71 | if (entry.linter === linter && entry.filePath === filePath) { 72 | return entry 73 | } 74 | } 75 | return null 76 | } 77 | didBeginLinting(linter: Linter, filePath: string | null | undefined) { 78 | if (this.getExecuting(linter, filePath)) { 79 | return 80 | } 81 | this.executing.add({ linter, filePath }) 82 | this.update() 83 | } 84 | didFinishLinting(linter: Linter, filePath: string | null | undefined) { 85 | const entry = this.getExecuting(linter, filePath) 86 | if (entry) { 87 | this.executing.delete(entry) 88 | this.update() 89 | } 90 | } 91 | dispose() { 92 | if (this.provider) { 93 | this.provider.clear() 94 | } 95 | this.providerTitles.clear() 96 | this.executing.clear() 97 | this.subscriptions.dispose() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/commands.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'assert' 2 | import { CompositeDisposable } from 'atom' 3 | const { config, workspace, commands, clipboard } = atom 4 | import { $file, $range, visitMessage, sortMessages, sortSolutions, filterMessages, applySolution } from './helpers' 5 | import type { LinterMessage, Message } from './types' 6 | 7 | export default class Commands { 8 | messages: Array = [] 9 | subscriptions: CompositeDisposable = new CompositeDisposable() 10 | 11 | constructor() { 12 | this.subscriptions.add( 13 | commands.add('atom-workspace', { 14 | 'linter-ui-default:next': () => this.move(true, true), 15 | 'linter-ui-default:previous': () => this.move(false, true), 16 | 'linter-ui-default:next-error': () => this.move(true, true, 'error'), 17 | 'linter-ui-default:previous-error': () => this.move(false, true, 'error'), 18 | 'linter-ui-default:next-warning': () => this.move(true, true, 'warning'), 19 | 'linter-ui-default:previous-warning': () => this.move(false, true, 'warning'), 20 | 'linter-ui-default:next-info': () => this.move(true, true, 'info'), 21 | 'linter-ui-default:previous-info': () => this.move(false, true, 'info'), 22 | 23 | 'linter-ui-default:next-in-current-file': () => this.move(true, false), 24 | 'linter-ui-default:previous-in-current-file': () => this.move(false, false), 25 | 'linter-ui-default:next-error-in-current-file': () => this.move(true, false, 'error'), 26 | 'linter-ui-default:previous-error-in-current-file': () => this.move(false, false, 'error'), 27 | 'linter-ui-default:next-warning-in-current-file': () => this.move(true, false, 'warning'), 28 | 'linter-ui-default:previous-warning-in-current-file': () => this.move(false, false, 'warning'), 29 | 'linter-ui-default:next-info-in-current-file': () => this.move(true, false, 'info'), 30 | 'linter-ui-default:previous-info-in-current-file': () => this.move(false, false, 'info'), 31 | 32 | 'linter-ui-default:toggle-panel': () => togglePanel(), 33 | 34 | // NOTE: Add no-ops here so they are recognized by commands registry 35 | // Real commands are registered when tooltip is shown inside tooltip's delegate 36 | 'linter-ui-default:expand-tooltip'() { 37 | /* no operation */ 38 | }, 39 | 'linter-ui-default:collapse-tooltip'() { 40 | /* no operation */ 41 | }, 42 | }), 43 | commands.add('atom-text-editor:not([mini])', { 44 | 'linter-ui-default:apply-all-solutions': () => this.applyAllSolutions(), 45 | }), 46 | commands.add('#linter-panel', { 47 | 'core:copy': () => { 48 | const selection = document.getSelection() 49 | if (selection) { 50 | clipboard.write(selection.toString()) 51 | } 52 | }, 53 | }), 54 | ) 55 | } 56 | 57 | // NOTE: Apply solutions from bottom to top, so they don't invalidate each other 58 | // NOTE: This only apply the solutions that are not async 59 | applyAllSolutions(): void { 60 | const textEditor = workspace.getActiveTextEditor() 61 | invariant(textEditor !== undefined, 'textEditor was null on a command supposed to run on text-editors only') 62 | const messages = sortMessages(filterMessages(this.messages, textEditor.getPath()), ['line', 'desc']) 63 | messages.forEach(function (message) { 64 | if (message.version === 2 && Array.isArray(message.solutions) && message.solutions.length > 0) { 65 | applySolution(textEditor, sortSolutions(message.solutions)[0]) 66 | } 67 | }) 68 | } 69 | async move(forward: boolean, globally: boolean, severity: string | null | undefined = null) { 70 | const currentEditor = workspace.getActiveTextEditor() 71 | const currentFile: any = currentEditor?.getPath() ?? NaN 72 | // NOTE: ^ Setting default to NaN so it won't match empty file paths in messages 73 | const messages = sortMessages(filterMessages(this.messages, globally ? null : currentFile, severity), ['file', 'asc']) 74 | const expectedValue = forward ? -1 : 1 75 | 76 | if (!currentEditor) { 77 | const message = forward ? messages[0] : messages[messages.length - 1] 78 | if (message) { 79 | await visitMessage(message) 80 | } 81 | return 82 | } 83 | const currentPosition = currentEditor.getCursorBufferPosition() 84 | 85 | // NOTE: Iterate bottom to top to find the previous message 86 | // Because if we search top to bottom when sorted, first item will always 87 | // be the smallest 88 | if (!forward) { 89 | messages.reverse() 90 | } 91 | 92 | let found: Message | null = null 93 | let currentFileEncountered = false 94 | for (let i = 0, length = messages.length; i < length; i++) { 95 | const message = messages[i] 96 | const messageFile = $file(message) 97 | const messageRange = $range(message) 98 | 99 | if (!currentFileEncountered && messageFile === currentFile) { 100 | currentFileEncountered = true 101 | } 102 | if (typeof messageFile === 'string' && messageRange) { 103 | if (currentFileEncountered && messageFile !== currentFile) { 104 | found = message 105 | break 106 | } else if (messageFile === currentFile && currentPosition.compare(messageRange.start) === expectedValue) { 107 | found = message 108 | break 109 | } 110 | } 111 | } 112 | 113 | if (!found && messages.length) { 114 | // Reset back to first or last depending on direction 115 | found = messages[0] 116 | } 117 | 118 | if (found) { 119 | await visitMessage(found) 120 | } 121 | } 122 | update(messages: Array) { 123 | this.messages = messages 124 | } 125 | dispose(): void { 126 | this.subscriptions.dispose() 127 | } 128 | } 129 | 130 | function togglePanel(): void { 131 | config.set('linter-ui-default.showPanel', !(config.get('linter-ui-default.showPanel') as boolean)) 132 | } 133 | -------------------------------------------------------------------------------- /lib/editor/helpers.ts: -------------------------------------------------------------------------------- 1 | import type { Point, TextEditor, TextEditorElement, PointLike } from 'atom' 2 | import type Tooltip from '../tooltip/index' 3 | 4 | const TOOLTIP_WIDTH_HIDE_OFFSET = 30 5 | 6 | export function getBufferPositionFromMouseEvent( 7 | event: MouseEvent, 8 | editor: TextEditor, 9 | editorElement: TextEditorElement, 10 | ): Point | null { 11 | const editorComponent = editorElement.getComponent() 12 | const pixelPosition = editorComponent.pixelPositionForMouseEvent(event) 13 | const screenPosition = editorComponent.screenPositionForPixelPosition(pixelPosition) 14 | if (Number.isNaN(screenPosition.row) || Number.isNaN(screenPosition.column)) { 15 | return null 16 | } 17 | // ^ Workaround for NaN bug steelbrain/linter-ui-default#191 18 | const expectedPixelPosition = editorElement.pixelPositionForScreenPosition(screenPosition) 19 | const differenceTop = pixelPosition.top - expectedPixelPosition.top 20 | const differenceLeft = pixelPosition.left - expectedPixelPosition.left 21 | // Only allow offset of 20px - Fixes steelbrain/linter-ui-default#63 22 | if ( 23 | (differenceTop === 0 || (differenceTop > 0 && differenceTop < 20) || (differenceTop < 0 && differenceTop > -20)) && 24 | (differenceLeft === 0 || (differenceLeft > 0 && differenceLeft < 20) || (differenceLeft < 0 && differenceLeft > -20)) 25 | ) { 26 | return editor.bufferPositionForScreenPosition(screenPosition) 27 | } 28 | return null 29 | } 30 | 31 | export function mouseEventNearPosition({ 32 | event, 33 | editor, 34 | editorElement, 35 | tooltipElement, 36 | screenPosition, 37 | }: { 38 | event: { clientX: number; clientY: number } 39 | editor: TextEditor 40 | editorElement: TextEditorElement 41 | tooltipElement: Tooltip['element'] 42 | screenPosition: PointLike 43 | }): boolean { 44 | const pixelPosition = editorElement.getComponent().pixelPositionForMouseEvent(event) 45 | const expectedPixelPosition = editorElement.pixelPositionForScreenPosition(screenPosition) 46 | const differenceTop = pixelPosition.top - expectedPixelPosition.top 47 | const differenceLeft = pixelPosition.left - expectedPixelPosition.left 48 | 49 | const editorLineHeight = editor.getLineHeightInPixels() 50 | const elementHeight = tooltipElement.offsetHeight + editorLineHeight 51 | const elementWidth = tooltipElement.offsetWidth 52 | 53 | if ( 54 | /* Cursor is below the line*/ (differenceTop > 0 && differenceTop > elementHeight + 1.5 * editorLineHeight) || 55 | /* Cursor is above the line */ (differenceTop < 0 && differenceTop < -1.5 * editorLineHeight) || 56 | /* Right of the start of highlight */ (differenceLeft > 0 && 57 | differenceLeft > elementWidth + TOOLTIP_WIDTH_HIDE_OFFSET) || 58 | /* Left of start of highlight */ (differenceTop < 0 && differenceLeft < -1 * TOOLTIP_WIDTH_HIDE_OFFSET) 59 | ) { 60 | return false 61 | } 62 | return true 63 | } 64 | 65 | export function hasParent(givenElement: HTMLElement | null, selector: string): boolean { 66 | let element: HTMLElement | null = givenElement 67 | if (element === null) { 68 | return false 69 | } 70 | do { 71 | if (element.matches(selector)) { 72 | return true 73 | } 74 | element = element.parentElement 75 | } while (element !== null && element.nodeName !== 'HTML') 76 | return false 77 | } 78 | -------------------------------------------------------------------------------- /lib/editor/index.ts: -------------------------------------------------------------------------------- 1 | import { debounce, $range, filterMessagesByRangeOrPoint } from '../helpers' 2 | import disposableEvent from 'disposable-event' 3 | import { TargetWithAddEventListener } from 'disposable-event/src/target' 4 | import { CompositeDisposable, Disposable, Emitter, Range, CursorPositionChangedEvent } from 'atom' 5 | const { config, views } = atom 6 | type CompositeDisposableType = CompositeDisposable & { disposed: boolean } 7 | 8 | // $FlowIgnore: Cursor is a type 9 | import type { TextEditor, DisplayMarker, Marker, Gutter, Point, Cursor } from 'atom' 10 | 11 | import Tooltip from '../tooltip' 12 | 13 | import { hasParent, mouseEventNearPosition, getBufferPositionFromMouseEvent } from './helpers' 14 | import type { LinterMessage } from '../types' 15 | 16 | export default class Editor { 17 | textEditor: TextEditor 18 | gutter: Gutter | null = null 19 | tooltip: Tooltip | null = null 20 | emitter = new Emitter<{ 'did-destroy': never }>() 21 | markers = new Map>() 22 | messages = new Map() 23 | showTooltip: boolean = true 24 | subscriptions = new CompositeDisposable() as CompositeDisposableType 25 | cursorPosition: Point | null = null 26 | gutterPosition?: string 27 | tooltipFollows: string = 'Both' 28 | showDecorations?: boolean 29 | showProviderName: boolean = true 30 | ignoreTooltipInvocation: boolean = false 31 | currentLineMarker: DisplayMarker | null = null 32 | lastRange?: Range 33 | lastIsEmpty?: boolean 34 | lastCursorPositions = new WeakMap() 35 | 36 | constructor(textEditor: TextEditor) { 37 | this.textEditor = textEditor 38 | 39 | let tooltipSubscription: CompositeDisposable | null = null 40 | 41 | this.subscriptions.add( 42 | this.emitter, 43 | textEditor.onDidDestroy(() => { 44 | this.dispose() 45 | }), 46 | new Disposable(function () { 47 | tooltipSubscription?.dispose() 48 | }), 49 | // configs 50 | config.observe('linter-ui-default.showProviderName', (showProviderName: boolean) => { 51 | this.showProviderName = showProviderName 52 | }), 53 | config.observe('linter-ui-default.showDecorations', (showDecorations: boolean) => { 54 | const notInitial = typeof this.showDecorations !== 'undefined' 55 | this.showDecorations = showDecorations 56 | if (notInitial) { 57 | this.updateGutter() 58 | } 59 | }), 60 | // gutter config 61 | config.observe('linter-ui-default.gutterPosition', (gutterPosition: string | undefined) => { 62 | const notInitial = typeof this.gutterPosition !== 'undefined' 63 | this.gutterPosition = gutterPosition 64 | if (notInitial) { 65 | this.updateGutter() 66 | } 67 | }), 68 | // tooltip config 69 | config.observe('linter-ui-default.showTooltip', (showTooltip: boolean) => { 70 | this.showTooltip = showTooltip 71 | if (!showTooltip && this.tooltip !== null) { 72 | this.removeTooltip() 73 | } 74 | }), 75 | config.observe('linter-ui-default.tooltipFollows', (tooltipFollows: string) => { 76 | this.tooltipFollows = tooltipFollows 77 | tooltipSubscription?.dispose() 78 | tooltipSubscription = new CompositeDisposable() 79 | if (tooltipFollows === 'Mouse' || tooltipFollows === 'Both') { 80 | tooltipSubscription.add(this.listenForMouseMovement()) 81 | } 82 | if (tooltipFollows === 'Keyboard' || tooltipFollows === 'Both') { 83 | tooltipSubscription.add(this.listenForKeyboardMovement()) 84 | } 85 | this.removeTooltip() 86 | }), 87 | // cursor position change 88 | textEditor.onDidChangeCursorPosition(({ cursor, newBufferPosition }) => { 89 | const lastBufferPosition = this.lastCursorPositions.get(cursor) 90 | if (lastBufferPosition === undefined || !lastBufferPosition.isEqual(newBufferPosition)) { 91 | this.lastCursorPositions.set(cursor, newBufferPosition) 92 | this.ignoreTooltipInvocation = false 93 | } 94 | if (this.tooltipFollows === 'Mouse') { 95 | this.removeTooltip() 96 | } 97 | }), 98 | // text change 99 | textEditor.getBuffer().onDidChangeText(() => { 100 | const cursors = textEditor.getCursors() 101 | for (const cursor of cursors) { 102 | this.lastCursorPositions.set(cursor, cursor.getBufferPosition()) 103 | } 104 | if (this.tooltipFollows !== 'Mouse') { 105 | this.ignoreTooltipInvocation = true 106 | this.removeTooltip() 107 | } 108 | }), 109 | ) 110 | this.updateGutter() 111 | this.listenForCurrentLine() 112 | } 113 | listenForCurrentLine() { 114 | this.subscriptions.add( 115 | this.textEditor.observeCursors(cursor => { 116 | const handlePositionChange = ({ start, end }: { start: Point; end: Point }) => { 117 | const gutter = this.gutter 118 | if (gutter === null || this.subscriptions.disposed) { 119 | return 120 | } 121 | // We need that Range.fromObject hack below because when we focus index 0 on multi-line selection 122 | // end.column is the column of the last line but making a range out of two and then accesing 123 | // the end seems to fix it (black magic?) 124 | const currentRange = Range.fromObject([start, end]) 125 | const linesRange = Range.fromObject([ 126 | [start.row, 0], 127 | [end.row, Infinity], 128 | ]) 129 | const currentIsEmpty = currentRange.isEmpty() 130 | 131 | // NOTE: Atom does not paint gutter if multi-line and last line has zero index 132 | if (start.row !== end.row && currentRange.end.column === 0) { 133 | linesRange.end.row-- 134 | } 135 | if (this.lastRange?.isEqual(linesRange) === true && currentIsEmpty === this.lastIsEmpty) { 136 | return 137 | } 138 | if (this.currentLineMarker) { 139 | this.currentLineMarker.destroy() 140 | this.currentLineMarker = null 141 | } 142 | this.lastRange = linesRange 143 | this.lastIsEmpty = currentIsEmpty 144 | 145 | this.currentLineMarker = this.textEditor.markScreenRange(linesRange, { 146 | invalidate: 'never', 147 | }) 148 | const item = document.createElement('span') 149 | item.className = `line-number cursor-line linter-cursor-line ${currentIsEmpty ? 'cursor-line-no-selection' : ''}` 150 | gutter.decorateMarker(this.currentLineMarker, { 151 | item, 152 | class: 'linter-row', 153 | }) 154 | } 155 | 156 | const cursorMarker = cursor.getMarker() 157 | const subscriptions = new CompositeDisposable() 158 | subscriptions.add( 159 | cursorMarker.onDidChange(({ newHeadScreenPosition, newTailScreenPosition }) => { 160 | handlePositionChange({ 161 | start: newHeadScreenPosition, 162 | end: newTailScreenPosition, 163 | }) 164 | }), 165 | cursor.onDidDestroy(() => { 166 | this.subscriptions.remove(subscriptions) 167 | subscriptions.dispose() 168 | }), 169 | new Disposable(() => { 170 | if (this.currentLineMarker) { 171 | this.currentLineMarker.destroy() 172 | this.currentLineMarker = null 173 | } 174 | }), 175 | ) 176 | this.subscriptions.add(subscriptions) 177 | handlePositionChange(cursorMarker.getScreenRange()) 178 | }), 179 | ) 180 | } 181 | listenForMouseMovement() { 182 | const editorElement = views.getView(this.textEditor) 183 | 184 | return disposableEvent( 185 | (editorElement as unknown) as TargetWithAddEventListener, 186 | 'mousemove', 187 | debounce((event: MouseEvent) => { 188 | if (this.subscriptions.disposed || !hasParent(event.target as HTMLElement | null, 'div.scroll-view')) { 189 | return 190 | } 191 | const tooltip = this.tooltip 192 | if ( 193 | tooltip !== null && 194 | mouseEventNearPosition({ 195 | event, 196 | editor: this.textEditor, 197 | editorElement, 198 | tooltipElement: tooltip.element, 199 | screenPosition: tooltip.marker.getStartScreenPosition(), 200 | }) 201 | ) { 202 | return 203 | } 204 | 205 | this.cursorPosition = getBufferPositionFromMouseEvent(event, this.textEditor, editorElement) 206 | this.ignoreTooltipInvocation = false 207 | if (this.cursorPosition) { 208 | this.updateTooltip(this.cursorPosition) 209 | } else { 210 | this.removeTooltip() 211 | } 212 | }, 100), 213 | { passive: true }, 214 | ) 215 | } 216 | listenForKeyboardMovement() { 217 | return this.textEditor.onDidChangeCursorPosition( 218 | debounce(({ newBufferPosition }: CursorPositionChangedEvent) => { 219 | this.cursorPosition = newBufferPosition 220 | this.updateTooltip(newBufferPosition) 221 | }, 16), 222 | ) 223 | } 224 | updateGutter() { 225 | this.removeGutter() 226 | if (this.showDecorations !== true) { 227 | this.gutter = null 228 | return 229 | } 230 | const priority = this.gutterPosition === 'Left' ? -100 : 100 231 | this.gutter = this.textEditor.addGutter({ 232 | name: 'linter-ui-default', 233 | priority, 234 | }) 235 | for (const [key, markers] of this.markers) { 236 | const message = this.messages.get(key) 237 | if (message) { 238 | for (const marker of markers) { 239 | this.decorateMarker(message, marker, 'gutter') 240 | } 241 | } 242 | } 243 | } 244 | removeGutter() { 245 | if (this.gutter) { 246 | try { 247 | this.gutter.destroy() 248 | } catch (_) { 249 | /* This throws when the text editor is disposed */ 250 | } 251 | } 252 | } 253 | updateTooltip(position: Point | null | undefined) { 254 | if (!position || this.tooltip?.isValid(position, this.messages) === true) { 255 | return 256 | } 257 | this.removeTooltip() 258 | if (!this.showTooltip) { 259 | return 260 | } 261 | if (this.ignoreTooltipInvocation) { 262 | return 263 | } 264 | 265 | const messages = filterMessagesByRangeOrPoint(this.messages, this.textEditor.getPath(), position) 266 | if (messages.length === 0) { 267 | return 268 | } 269 | 270 | this.tooltip = new Tooltip(messages, position, this.textEditor) 271 | const tooltipMarker = this.tooltip.marker 272 | // save markers of the tooltip (for destorying them in this.applyChanges) 273 | for (const message of messages) { 274 | this.saveMarker(message.key, tooltipMarker) 275 | } 276 | 277 | // $FlowIgnore: this.tooltip is not null 278 | this.tooltip.onDidDestroy(() => { 279 | this.tooltip = null 280 | }) 281 | } 282 | removeTooltip() { 283 | this.tooltip?.marker.destroy() 284 | } 285 | applyChanges(added: Array, removed: Array) { 286 | const textBuffer = this.textEditor.getBuffer() 287 | 288 | for (let i = 0, length = removed.length; i < length; i++) { 289 | const message = removed[i] 290 | this.destroyMarker(message.key) 291 | } 292 | 293 | for (let i = 0, length = added.length; i < length; i++) { 294 | const message = added[i] 295 | const markerRange = $range(message) 296 | if (!markerRange) { 297 | // Only for backward compatibility 298 | continue 299 | } 300 | // TODO this marker is Marker no DisplayMarker!! 301 | const marker: Marker = textBuffer.markRange(markerRange, { 302 | invalidate: 'never', 303 | }) 304 | this.decorateMarker(message, marker) 305 | marker.onDidChange(({ oldHeadPosition, newHeadPosition, isValid }) => { 306 | if (!isValid || (newHeadPosition.row === 0 && oldHeadPosition.row !== 0)) { 307 | return 308 | } 309 | if (message.version === 2) { 310 | message.location.position = marker.previousEventState.range 311 | } 312 | }) 313 | } 314 | 315 | this.updateTooltip(this.cursorPosition) 316 | } 317 | decorateMarker(message: LinterMessage, marker: DisplayMarker | Marker, paint: 'gutter' | 'editor' | 'both' = 'both') { 318 | this.saveMarker(message.key, marker) 319 | this.messages.set(message.key, message) 320 | 321 | if (paint === 'both' || paint === 'editor') { 322 | this.textEditor.decorateMarker(marker, { 323 | type: 'text', 324 | class: `linter-highlight linter-${message.severity}`, 325 | }) 326 | } 327 | 328 | const gutter = this.gutter 329 | if (gutter && (paint === 'both' || paint === 'gutter')) { 330 | const element = document.createElement('span') 331 | element.className = `linter-gutter linter-gutter-${message.severity} icon icon-${message.icon ?? 'primitive-dot'}` 332 | gutter.decorateMarker(marker, { 333 | class: 'linter-row', 334 | item: element, 335 | }) 336 | } 337 | } 338 | 339 | // add marker to the message => marker map 340 | saveMarker(key: string, marker: DisplayMarker | Marker) { 341 | const allMarkers = this.markers.get(key) ?? [] 342 | allMarkers.push(marker) 343 | this.markers.set(key, allMarkers) 344 | } 345 | 346 | // destroy markers of a key 347 | destroyMarker(key: string) { 348 | const markers = this.markers.get(key) 349 | if (markers) { 350 | for (const marker of markers) { 351 | marker?.destroy() 352 | } 353 | } 354 | this.markers.delete(key) 355 | this.messages.delete(key) 356 | } 357 | 358 | onDidDestroy(callback: () => void) { 359 | return this.emitter.on('did-destroy', callback) 360 | } 361 | dispose() { 362 | this.emitter.emit('did-destroy') 363 | this.subscriptions.dispose() 364 | this.removeGutter() 365 | this.removeTooltip() 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /lib/editors.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable } from 'atom' 2 | const { config, workspace, notifications } = atom 3 | import type { TextEditor } from 'atom' 4 | import Editor from './editor' 5 | import { $file, getEditorsMap, filterMessages } from './helpers' 6 | import { largeness } from 'atom-ide-base/commons-atom/editor-largeness' 7 | import type { LinterMessage, MessagesPatch } from './types' 8 | 9 | export type EditorsPatch = { 10 | added: Array 11 | removed: Array 12 | editors: Array 13 | } 14 | 15 | export type EditorsMap = Map 16 | 17 | export default class Editors { 18 | editors: Set = new Set() 19 | messages: Array = [] 20 | firstRender: boolean = true 21 | subscriptions: CompositeDisposable = new CompositeDisposable() 22 | 23 | constructor() { 24 | // TODO move the config to a separate package 25 | const largeLineCount = config.get('linter-ui-default.largeFileLineCount') as number 26 | const longLineLength = config.get('linter-ui-default.longLineLength') as number 27 | 28 | this.subscriptions.add( 29 | workspace.observeTextEditors(textEditor => { 30 | // TODO we do this check only at the begining. Probably we should do this later too? 31 | if (largeness(textEditor, largeLineCount, longLineLength)) { 32 | const notif = notifications.addWarning('Linter: Large/Minified file detected', { 33 | detail: 34 | 'Adding inline linter markers are skipped for this file for performance reasons (linter pane is still active)', 35 | dismissable: true, 36 | buttons: [ 37 | { 38 | text: 'Force enable', 39 | onDidClick: () => { 40 | this.getEditor(textEditor) 41 | notif.dismiss() 42 | }, 43 | }, 44 | { 45 | text: 'Change threshold', 46 | onDidClick: async () => { 47 | await workspace.open('atom://config/packages/linter-ui-default') 48 | // it is the 16th setting :D 49 | document.querySelectorAll('.control-group')[16].scrollIntoView() 50 | notif.dismiss() 51 | }, 52 | }, 53 | ], 54 | }) 55 | setTimeout(() => { 56 | notif.dismiss() 57 | }, 5000) 58 | return 59 | } 60 | this.getEditor(textEditor) 61 | }), 62 | workspace.getCenter().observeActivePaneItem(paneItem => { 63 | this.editors.forEach(editor => { 64 | if (editor.textEditor !== paneItem) { 65 | editor.removeTooltip() 66 | } 67 | }) 68 | }), 69 | ) 70 | } 71 | isFirstRender(): boolean { 72 | return this.firstRender 73 | } 74 | update({ messages, added, removed }: MessagesPatch) { 75 | this.messages = messages 76 | this.firstRender = false 77 | 78 | const { editorsMap, filePaths } = getEditorsMap(this) 79 | added.forEach(function (message) { 80 | if (!message || !message.location) { 81 | return 82 | } 83 | const filePath = $file(message) 84 | if (typeof filePath === 'string' && editorsMap.has(filePath)) { 85 | editorsMap.get(filePath)!.added.push(message) 86 | } 87 | }) 88 | removed.forEach(function (message) { 89 | if (!message || !message.location) { 90 | return 91 | } 92 | const filePath = $file(message) 93 | if (typeof filePath === 'string' && editorsMap.has(filePath)) { 94 | editorsMap.get(filePath)!.removed.push(message) 95 | } 96 | }) 97 | 98 | filePaths.forEach(function (filePath) { 99 | if (editorsMap.has(filePath)) { 100 | const { added, removed, editors } = editorsMap.get(filePath) as EditorsPatch 101 | if (added.length || removed.length) { 102 | editors.forEach(editor => editor.applyChanges(added, removed)) 103 | } 104 | } 105 | }) 106 | } 107 | getEditor(textEditor: TextEditor): Editor | void { 108 | for (const entry of this.editors) { 109 | if (entry.textEditor === textEditor) { 110 | return entry 111 | } 112 | } 113 | const editor = new Editor(textEditor) 114 | this.editors.add(editor) 115 | editor.onDidDestroy(() => { 116 | this.editors.delete(editor) 117 | }) 118 | editor.subscriptions.add( 119 | textEditor.onDidChangePath(() => { 120 | editor.dispose() 121 | this.getEditor(textEditor) 122 | }), 123 | ) 124 | editor.subscriptions.add( 125 | textEditor.onDidChangeGrammar(() => { 126 | editor.dispose() 127 | this.getEditor(textEditor) 128 | }), 129 | ) 130 | editor.applyChanges(filterMessages(this.messages, textEditor.getPath()), []) 131 | return editor 132 | } 133 | dispose() { 134 | for (const entry of this.editors) { 135 | entry.dispose() 136 | } 137 | this.subscriptions.dispose() 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /lib/electron.d.ts: -------------------------------------------------------------------------------- 1 | // Electron types 2 | declare module 'electron' { 3 | export const shell: { 4 | openExternal(url: string): void 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Range } from 'atom' 2 | const { workspace, project, clipboard } = atom 3 | import type { Point, PointLike, RangeCompatible, TextEditor, WorkspaceOpenOptions } from 'atom' 4 | import { shell } from 'electron' 5 | import type { default as Editors, EditorsMap } from './editors' 6 | import type { LinterMessage, MessageSolution } from './types' 7 | 8 | export const severityScore = { 9 | error: 3, 10 | warning: 2, 11 | info: 1, 12 | } 13 | 14 | export const severityNames = { 15 | error: 'Error', 16 | warning: 'Warning', 17 | info: 'Info', 18 | } 19 | export const WORKSPACE_URI = 'atom://linter-ui-default' 20 | export const DOCK_ALLOWED_LOCATIONS = ['center', 'bottom'] 21 | export const DOCK_DEFAULT_LOCATION = 'bottom' 22 | 23 | export function $range(message: LinterMessage): Range | null | undefined { 24 | return message.location.position 25 | } 26 | export function $file(message: LinterMessage): string | null | undefined { 27 | return message.location.file 28 | } 29 | export function copySelection() { 30 | const selection = getSelection() 31 | if (selection) { 32 | clipboard.write(selection.toString()) 33 | } 34 | } 35 | export function getPathOfMessage(message: LinterMessage): string { 36 | return project.relativizePath($file(message) ?? '')[1] 37 | } 38 | 39 | export function getEditorsMap(editors: Editors): { editorsMap: EditorsMap; filePaths: Array } { 40 | // TODO types 41 | const editorsMap: EditorsMap = new Map() 42 | const filePaths: string[] = [] 43 | for (const entry of editors.editors) { 44 | const filePath = entry.textEditor.getPath() ?? '' // if undefined save it as "" 45 | if (editorsMap.has(filePath)) { 46 | editorsMap.get(filePath)!.editors.push(entry) 47 | } else { 48 | editorsMap.set(filePath, { 49 | added: [], 50 | removed: [], 51 | editors: [entry], 52 | }) 53 | filePaths.push(filePath) 54 | } 55 | } 56 | return { editorsMap, filePaths } 57 | } 58 | 59 | export function filterMessages( 60 | messages: Array, 61 | filePath: string | null | undefined, 62 | severity: string | null | undefined = null, 63 | ): Array { 64 | const filtered: Array = [] 65 | messages.forEach(function (message) { 66 | if (!message || !message.location) { 67 | return 68 | } 69 | if (($file(message) === filePath) && (!severity || message.severity === severity)) { 70 | filtered.push(message) 71 | } 72 | }) 73 | return filtered 74 | } 75 | 76 | export function filterMessagesByRangeOrPoint( 77 | messages: Set | Array | Map, 78 | filePath: string | undefined, 79 | rangeOrPoint: Point | RangeCompatible, 80 | ): Array { 81 | const filtered: Array = [] 82 | const expectedRange = 83 | rangeOrPoint.constructor.name === 'Point' 84 | ? new Range(rangeOrPoint as Point, rangeOrPoint as Point) 85 | : Range.fromObject(rangeOrPoint as RangeCompatible) 86 | messages.forEach(function (message: LinterMessage) { 87 | const file = $file(message) 88 | const range = $range(message) 89 | if ( 90 | typeof file === 'string' && 91 | range && 92 | file === filePath && 93 | typeof range.intersectsWith === 'function' && 94 | range.intersectsWith(expectedRange) 95 | ) { 96 | filtered.push(message) 97 | } 98 | }) 99 | return filtered 100 | } 101 | 102 | export async function openFile(file: string, position: PointLike | null | undefined) { 103 | const options: WorkspaceOpenOptions = { searchAllPanes: true } 104 | if (position) { 105 | options.initialLine = position.row 106 | options.initialColumn = position.column 107 | } 108 | await workspace.open(file, options) 109 | } 110 | 111 | export async function visitMessage(message: LinterMessage, reference = false) { 112 | let messageFile: string | undefined | null 113 | let messagePosition: Point | undefined 114 | if (reference) { 115 | if (!message.reference || !message.reference.file) { 116 | console.warn('[Linter-UI-Default] Message does not have a valid reference. Ignoring') 117 | return 118 | } 119 | messageFile = message.reference.file 120 | messagePosition = message.reference.position 121 | } else { 122 | const messageRange = $range(message) 123 | messageFile = $file(message) 124 | if (messageRange) { 125 | messagePosition = messageRange.start 126 | } 127 | } 128 | if (typeof messageFile === 'string') { 129 | await openFile(messageFile, messagePosition) 130 | } 131 | } 132 | 133 | export function openExternally(message: LinterMessage) { 134 | if (message.version === 2 && message.url !== undefined) { 135 | shell.openExternal(message.url) 136 | } 137 | } 138 | 139 | export function sortMessages( 140 | rows: Array, 141 | sortDirection: [id: 'severity' | 'linterName' | 'file' | 'line', direction: 'asc' | 'desc'], 142 | ): Array { 143 | const sortDirectionID = sortDirection[0] 144 | const sortDirectionDirection = sortDirection[1] 145 | const multiplyWith = sortDirectionDirection === 'asc' ? 1 : -1 146 | 147 | return rows.sort(function (a, b) { 148 | if (sortDirectionID === 'severity') { 149 | const severityA = severityScore[a.severity] 150 | const severityB = severityScore[b.severity] 151 | if (severityA !== severityB) { 152 | return multiplyWith * (severityA > severityB ? 1 : -1) 153 | } 154 | } 155 | if (sortDirectionID === 'linterName') { 156 | const sortValue = a.severity.localeCompare(b.severity) 157 | if (sortValue !== 0) { 158 | return multiplyWith * sortValue 159 | } 160 | } 161 | if (sortDirectionID === 'file') { 162 | const fileA = getPathOfMessage(a) 163 | const fileALength = fileA.length 164 | const fileB = getPathOfMessage(b) 165 | const fileBLength = fileB.length 166 | if (fileALength !== fileBLength) { 167 | return multiplyWith * (fileALength > fileBLength ? 1 : -1) 168 | } else if (fileA !== fileB) { 169 | return multiplyWith * fileA.localeCompare(fileB) 170 | } 171 | } 172 | if (sortDirectionID === 'line') { 173 | const rangeA = $range(a) 174 | const rangeB = $range(b) 175 | if (rangeA && !rangeB) { 176 | return 1 177 | } else if (rangeB && !rangeA) { 178 | return -1 179 | } else if (rangeA && rangeB) { 180 | if (rangeA.start.row !== rangeB.start.row) { 181 | return multiplyWith * (rangeA.start.row > rangeB.start.row ? 1 : -1) 182 | } 183 | if (rangeA.start.column !== rangeB.start.column) { 184 | return multiplyWith * (rangeA.start.column > rangeB.start.column ? 1 : -1) 185 | } 186 | } 187 | } 188 | 189 | return 0 190 | }) 191 | } 192 | 193 | export function sortSolutions(solutions: MessageSolution[]) { 194 | return solutions.sort(function (a, b) { 195 | if (a.priority === undefined || b.priority === undefined) { 196 | return 0 197 | } 198 | return b.priority - a.priority 199 | }) 200 | } 201 | 202 | export function applySolution(textEditor: TextEditor, solution: MessageSolution): boolean { 203 | if ('apply' in solution) { 204 | solution.apply() 205 | return true 206 | } 207 | const range = solution.position 208 | const replaceWith = solution.replaceWith 209 | if ('currentText' in solution) { 210 | const currentText = solution.currentText 211 | const textInRange = textEditor.getTextInBufferRange(range) 212 | if (currentText !== textInRange) { 213 | console.warn( 214 | '[linter-ui-default] Not applying fix because text did not match the expected one', 215 | 'expected', 216 | currentText, 217 | 'but got', 218 | textInRange, 219 | ) 220 | return false 221 | } 222 | } 223 | textEditor.setTextInBufferRange(range, replaceWith) 224 | return true 225 | } 226 | 227 | /** 228 | * A function to get a value from the cache or calculate it if it is not available (and store it in the cache after calculation) 229 | * 230 | * @param map A reference to a Map of key to values that is used as the cache 231 | * @param key The current key to get calculate or get the cache for 232 | * @param calculate The function that is used to calculate the value if the cache is not hit 233 | */ 234 | export function get(map: Map, key: Key, calculate: () => Value | null): Value | null { 235 | // get cache 236 | const cachedValue = map.get(key) 237 | if (cachedValue !== undefined) { 238 | // cache hit 239 | return cachedValue 240 | } else { 241 | // calculate 242 | const calculatedValue = calculate() 243 | if (calculatedValue !== null) { 244 | // calculation successful 245 | map.set(key, calculatedValue) 246 | } 247 | return calculatedValue 248 | } 249 | } 250 | 251 | /** A faster vresion of lodash.debounce */ 252 | /* eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any */ 253 | export function debounce void>(func: T, wait?: number): T { 254 | let timeoutId: NodeJS.Timeout | undefined 255 | // @ts-ignore 256 | return (...args: Parameters) => { 257 | if (timeoutId !== undefined) { 258 | clearTimeout(timeoutId) 259 | } 260 | timeoutId = setTimeout(() => { 261 | func(...args) 262 | }, wait) 263 | } 264 | } 265 | 266 | /** A faster vresion of lodash.once */ 267 | /* eslint-disable-next-line @typescript-eslint/ban-types */ 268 | export function once(func: T): T { 269 | let result: any 270 | let called = false 271 | // @ts-ignore 272 | return (...args: Parameters) => { 273 | if (!called) { 274 | result = func(...args) 275 | called = true 276 | } 277 | return result 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | const { config, packages } = atom 2 | import LinterUI from './main' 3 | import type Intentions from './intentions' 4 | import type { /* IntentionsListProvider, */ PackageExtra } from './types' 5 | import type { StatusBar as StatusBarRegistry } from 'atom/status-bar' 6 | import type { BusySignalRegistry } from 'atom-ide-base' 7 | 8 | const idleCallbacks: Set = new Set() 9 | 10 | const instances: Set = new Set() 11 | let signalRegistry: BusySignalRegistry | undefined 12 | let statusBarRegistry: StatusBarRegistry | undefined 13 | 14 | export function activate() { 15 | if (config.get('linter-ui-default.useBusySignal') as boolean) { 16 | // This is a necessary evil, see steelbrain/linter#1355 17 | ;(packages.getLoadedPackage('linter-ui-default') as PackageExtra).metadata['package-deps'].push('busy-signal') 18 | } 19 | 20 | const callbackID = window.requestIdleCallback(async () => { 21 | idleCallbacks.delete(callbackID) 22 | if (!atom.inSpecMode()) { 23 | await package_deps() 24 | } 25 | }) 26 | idleCallbacks.add(callbackID) 27 | } 28 | 29 | /** Install Atom package dependencies if not already loaded */ 30 | async function package_deps() { 31 | // (to prevent loading atom-package-deps and package.json when the deps are already loaded) 32 | if (!atom.packages.isPackageLoaded('linter') || !atom.packages.isPackageLoaded('intentions')) { 33 | // install if not installed 34 | await (await import('atom-package-deps')).install('linter-ui-default', true) 35 | // enable if disabled 36 | atom.notifications.addInfo(`Enabling package linter and intentions that are needed for "linter-ui-default"`) 37 | atom.packages.enablePackage('linter') 38 | atom.packages.enablePackage('intentions') 39 | } 40 | } 41 | 42 | export function deactivate() { 43 | idleCallbacks.forEach(callbackID => window.cancelIdleCallback(callbackID)) 44 | idleCallbacks.clear() 45 | for (const entry of instances) { 46 | entry.dispose() 47 | } 48 | instances.clear() 49 | } 50 | 51 | export function provideUI(): LinterUI { 52 | const instance = new LinterUI() 53 | instances.add(instance) 54 | if (signalRegistry) { 55 | instance.signal.attach(signalRegistry) 56 | } 57 | return instance 58 | } 59 | 60 | // TODO: use IntentionsListProvider as the return type 61 | export function provideIntentions(): Array { 62 | return Array.from(instances).map(entry => entry.intentions) 63 | } 64 | 65 | export function consumeSignal(signalService: BusySignalRegistry) { 66 | signalRegistry = signalService 67 | instances.forEach(function (instance) { 68 | instance.signal.attach(signalRegistry as BusySignalRegistry) 69 | }) 70 | } 71 | 72 | export function consumeStatusBar(statusBarService: StatusBarRegistry) { 73 | statusBarRegistry = statusBarService 74 | instances.forEach(function (instance) { 75 | instance.statusBar.attach(statusBarRegistry as StatusBarRegistry) 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /lib/intentions.ts: -------------------------------------------------------------------------------- 1 | import { $range, applySolution, filterMessages } from './helpers' 2 | import type { LinterMessage, ListItem, MessageSolution } from './types' 3 | import type { TextEditor, Point } from 'atom' 4 | 5 | export default class Intentions { 6 | messages: Array = [] 7 | grammarScopes: Array = ['*'] 8 | 9 | getIntentions({ textEditor, bufferPosition }: { textEditor: TextEditor; bufferPosition: Point }) { 10 | let intentions: ListItem[] = [] 11 | const messages = filterMessages(this.messages, textEditor.getPath()) 12 | 13 | for (const message of messages) { 14 | const messageSolutions = message.solutions 15 | const hasArrayFixes = Array.isArray(messageSolutions) && messageSolutions.length > 0 16 | if (!hasArrayFixes && typeof messageSolutions !== 'function') { 17 | // if it doesn't have solutions then continue 18 | continue 19 | } 20 | const range = $range(message) 21 | if (range?.containsPoint(bufferPosition) !== true) { 22 | // if not in range then continue 23 | continue 24 | } 25 | 26 | const linterName = message.linterName || 'Linter' 27 | 28 | if (message.version === 2) { 29 | if (hasArrayFixes) { 30 | intentions = intentions.concat( 31 | (messageSolutions as MessageSolution[]).map(solution => ({ 32 | priority: typeof solution.priority === 'number' ? solution.priority + 200 : 200, 33 | icon: 'tools', 34 | title: solution.title ?? `Fix ${linterName} issue`, 35 | selected() { 36 | applySolution(textEditor, solution) 37 | }, 38 | })), 39 | ) 40 | } 41 | } 42 | } 43 | return intentions 44 | } 45 | update(messages: Array) { 46 | this.messages = messages 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/main.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable } from 'atom' 2 | const { config } = atom 3 | import Panel from './panel' 4 | import Commands from './commands' 5 | import StatusBar from './status-bar' 6 | import BusySignal from './busy-signal' 7 | import Intentions from './intentions' 8 | import type { Linter, LinterMessage, MessagesPatch } from './types' 9 | 10 | import Editors from './editors' 11 | import TreeView from './tree-view' 12 | 13 | export default class LinterUI { 14 | name: string = 'Linter' 15 | panel?: Panel 16 | signal: BusySignal = new BusySignal() 17 | editors: Editors | null | undefined 18 | treeview?: TreeView 19 | commands: Commands = new Commands() 20 | messages: Array = [] 21 | statusBar: StatusBar = new StatusBar() 22 | intentions: Intentions = new Intentions() 23 | subscriptions: CompositeDisposable = new CompositeDisposable() 24 | idleCallbacks: Set = new Set() 25 | 26 | constructor() { 27 | this.subscriptions.add(this.signal, this.commands, this.statusBar) 28 | 29 | const obsShowPanelCB = window.requestIdleCallback( 30 | /* observeShowPanel */ async () => { 31 | this.idleCallbacks.delete(obsShowPanelCB) 32 | this.panel = new Panel() 33 | await this.panel.update(this.messages) 34 | }, 35 | ) 36 | this.idleCallbacks.add(obsShowPanelCB) 37 | 38 | const obsShowDecorationsCB = window.requestIdleCallback( 39 | /* observeShowDecorations */ () => { 40 | this.idleCallbacks.delete(obsShowDecorationsCB) 41 | this.subscriptions.add( 42 | config.observe('linter-ui-default.showDecorations', (showDecorations: boolean) => { 43 | if (showDecorations && !this.editors) { 44 | this.editors = new Editors() 45 | this.editors.update({ 46 | added: this.messages, 47 | removed: [], 48 | messages: this.messages, 49 | }) 50 | } else if (!showDecorations && this.editors) { 51 | this.editors.dispose() 52 | this.editors = null 53 | } 54 | }), 55 | ) 56 | }, 57 | ) 58 | this.idleCallbacks.add(obsShowDecorationsCB) 59 | } 60 | 61 | async render(difference: MessagesPatch) { 62 | const editors = this.editors 63 | 64 | this.messages = difference.messages 65 | if (editors) { 66 | if (editors.isFirstRender()) { 67 | editors.update({ 68 | added: difference.messages, 69 | removed: [], 70 | messages: difference.messages, 71 | }) 72 | } else { 73 | editors.update(difference) 74 | } 75 | } 76 | // Initialize the TreeView subscription if necessary 77 | if (!this.treeview) { 78 | this.treeview = new TreeView() 79 | this.subscriptions.add(this.treeview) 80 | } 81 | this.treeview.update(difference.messages) 82 | 83 | if (this.panel) { 84 | await this.panel.update(difference.messages) 85 | } 86 | this.commands.update(difference.messages) 87 | this.intentions.update(difference.messages) 88 | this.statusBar.update(difference.messages) 89 | } 90 | 91 | didBeginLinting(linter: Linter, filePath: string) { 92 | this.signal.didBeginLinting(linter, filePath) 93 | } 94 | didFinishLinting(linter: Linter, filePath: string) { 95 | this.signal.didFinishLinting(linter, filePath) 96 | } 97 | dispose() { 98 | this.idleCallbacks.forEach(callbackID => window.cancelIdleCallback(callbackID)) 99 | this.idleCallbacks.clear() 100 | this.subscriptions.dispose() 101 | if (this.panel) { 102 | this.panel.dispose() 103 | } 104 | if (this.editors) { 105 | this.editors.dispose() 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/panel/component.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal, onMount } from 'solid-js' 2 | import { SimpleTable } from 'solid-simple-table' 3 | import { $range, severityNames, sortMessages, visitMessage, openExternally, getPathOfMessage } from '../helpers' 4 | import type Delegate from './delegate' 5 | import type { LinterMessage } from '../types' 6 | 7 | type Props = { 8 | delegate: Delegate 9 | } 10 | 11 | export function PanelComponent(props: Props) { 12 | const [getMessages, setMessages] = createSignal(props.delegate.filteredMessages, false) 13 | 14 | onMount(() => { 15 | props.delegate.onDidChangeMessages(messages => { 16 | setMessages(messages) 17 | }) 18 | }) 19 | 20 | const columns = [ 21 | { id: 'severity', label: 'Severity' }, 22 | { id: 'linterName', label: 'Provider' }, 23 | { id: 'excerpt', label: 'Description', onClick, sortable: false }, 24 | { id: 'line', label: 'Line', onClick }, 25 | ] 26 | if (props.delegate.panelRepresents === 'Entire Project') { 27 | columns.push({ 28 | id: 'file', 29 | label: 'File', 30 | onClick, 31 | }) 32 | } 33 | 34 | return ( 35 |
    36 | i.key} 43 | bodyRenderer={bodyRenderer} 44 | style={{ width: '100%' }} 45 | className="linter dark" 46 | /> 47 |
    48 | ) 49 | } 50 | 51 | function bodyRenderer(row: LinterMessage, column: string): string | HTMLElement { 52 | const range = $range(row) 53 | 54 | switch (column) { 55 | case 'file': 56 | return getPathOfMessage(row) 57 | case 'line': 58 | return range ? `${range.start.row + 1}:${range.start.column + 1}` : '' 59 | case 'excerpt': 60 | return row.excerpt 61 | case 'severity': 62 | return ( 63 |
    {severityNames[row.severity]}
    64 | ) as HTMLElement 65 | 66 | default: 67 | return row[column] 68 | } 69 | } 70 | 71 | async function onClick(e: MouseEvent, row: LinterMessage) { 72 | if ((e.target as HTMLElement).tagName === 'A') { 73 | return 74 | } 75 | if (process.platform === 'darwin' ? e.metaKey : e.ctrlKey) { 76 | if (e.shiftKey) { 77 | openExternally(row) 78 | } else { 79 | await visitMessage(row, true) 80 | } 81 | } else { 82 | await visitMessage(row) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/panel/delegate.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable, Disposable, Emitter, Range } from 'atom' 2 | const { config, workspace } = atom 3 | import { filterMessages, filterMessagesByRangeOrPoint } from '../helpers' 4 | import type { LinterMessage } from '../types' 5 | 6 | export default class PanelDelegate { 7 | emitter = new Emitter<{}, { 'observe-messages': Array }>() // eslint-disable-line @typescript-eslint/ban-types 8 | messages: Array = [] 9 | filteredMessages: Array = [] 10 | subscriptions: CompositeDisposable = new CompositeDisposable() 11 | panelRepresents?: 'Entire Project' | 'Current File' | 'Current Line' 12 | 13 | constructor() { 14 | let changeSubscription: Disposable | null = null 15 | this.subscriptions.add( 16 | config.observe('linter-ui-default.panelRepresents', (panelRepresents: PanelDelegate['panelRepresents']) => { 17 | const notInitial = typeof this.panelRepresents !== 'undefined' 18 | this.panelRepresents = panelRepresents 19 | if (notInitial) { 20 | this.update() 21 | } 22 | }), 23 | workspace.getCenter().observeActivePaneItem(() => { 24 | if (changeSubscription) { 25 | changeSubscription.dispose() 26 | changeSubscription = null 27 | } 28 | const textEditor = workspace.getActiveTextEditor() 29 | if (textEditor !== undefined) { 30 | if (this.panelRepresents !== 'Entire Project') { 31 | this.update() 32 | } 33 | let oldRow = -1 34 | changeSubscription = textEditor.onDidChangeCursorPosition(({ newBufferPosition }) => { 35 | if (oldRow !== newBufferPosition.row && this.panelRepresents === 'Current Line') { 36 | oldRow = newBufferPosition.row 37 | this.update() 38 | } 39 | }) 40 | } 41 | 42 | if (this.panelRepresents !== 'Entire Project' || textEditor !== undefined) { 43 | this.update() 44 | } 45 | }), 46 | new Disposable(() => { 47 | changeSubscription?.dispose() 48 | }), 49 | ) 50 | } 51 | getFilteredMessages(): Array { 52 | let filteredMessages: Array = [] 53 | if (this.panelRepresents === 'Entire Project') { 54 | filteredMessages = this.messages 55 | } else if (this.panelRepresents === 'Current File') { 56 | const activeEditor = workspace.getActiveTextEditor() 57 | if (!activeEditor) { 58 | return [] 59 | } 60 | filteredMessages = filterMessages(this.messages, activeEditor.getPath()) 61 | } else if (this.panelRepresents === 'Current Line') { 62 | const activeEditor = workspace.getActiveTextEditor() 63 | if (!activeEditor) { 64 | return [] 65 | } 66 | const activeLine = activeEditor.getCursors()[0].getBufferRow() 67 | filteredMessages = filterMessagesByRangeOrPoint( 68 | this.messages, 69 | activeEditor.getPath(), 70 | Range.fromObject([ 71 | [activeLine, 0], 72 | [activeLine, Infinity], 73 | ]), 74 | ) 75 | } 76 | return filteredMessages 77 | } 78 | update(messages: Array | null | undefined = null): void { 79 | if (Array.isArray(messages)) { 80 | this.messages = messages 81 | } 82 | this.filteredMessages = this.getFilteredMessages() 83 | this.emitter.emit('observe-messages', this.filteredMessages) 84 | } 85 | onDidChangeMessages(callback: (messages: Array) => void): Disposable { 86 | return this.emitter.on('observe-messages', callback) 87 | } 88 | dispose() { 89 | this.subscriptions.dispose() 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/panel/dock.tsx: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable, Dock, WorkspaceCenter } from 'atom' 2 | const { config, workspace, views } = atom 3 | import { WORKSPACE_URI, DOCK_ALLOWED_LOCATIONS, DOCK_DEFAULT_LOCATION } from '../helpers' 4 | import type Delegate from './delegate' 5 | import { render } from 'solid-js/web' 6 | 7 | // NOTE: these were lazy 8 | import { PanelComponent } from './component' 9 | 10 | // TODO Make these API public 11 | export type PaneContainer = Dock & { 12 | state: { size: number } 13 | render: Function 14 | paneForItem: WorkspaceCenter['paneForItem'] 15 | location: string 16 | } 17 | 18 | // eslint-disable-next-line no-use-before-define 19 | function getPaneContainer(item: PanelDock): PaneContainer | null { 20 | const paneContainer = workspace.paneContainerForItem(item) 21 | // NOTE: This is an internal API access 22 | // It's necessary because there's no Public API for it yet 23 | if ( 24 | paneContainer && 25 | // @ts-ignore internal API 26 | typeof paneContainer.state === 'object' && 27 | // @ts-ignore internal API 28 | typeof paneContainer.state.size === 'number' && 29 | // @ts-ignore internal API 30 | typeof paneContainer.render === 'function' 31 | ) { 32 | // @ts-ignore internal API 33 | return paneContainer as PaneContainer 34 | } 35 | return null 36 | } 37 | 38 | export default class PanelDock { 39 | element: HTMLElement = document.createElement('div') 40 | subscriptions: CompositeDisposable = new CompositeDisposable() 41 | panelHeight: number = 100 42 | alwaysTakeMinimumSpace: boolean = true 43 | lastSetPaneHeight?: number 44 | 45 | constructor(delegate: Delegate) { 46 | this.subscriptions.add( 47 | config.observe('linter-ui-default.panelHeight', panelHeight => { 48 | const changed = typeof this.panelHeight === 'number' 49 | this.panelHeight = panelHeight 50 | if (changed) { 51 | this.doPanelResize(true) 52 | } 53 | }), 54 | config.observe('linter-ui-default.alwaysTakeMinimumSpace', alwaysTakeMinimumSpace => { 55 | this.alwaysTakeMinimumSpace = alwaysTakeMinimumSpace 56 | }), 57 | ) 58 | this.doPanelResize() 59 | render(() => , this.element) 60 | } 61 | // NOTE: Chose a name that won't conflict with Dock APIs 62 | doPanelResize(forConfigHeight = false) { 63 | const paneContainer = getPaneContainer(this) 64 | if (paneContainer === null) { 65 | return 66 | } 67 | let minimumHeight: number | null = null 68 | 69 | const paneContainerView = views.getView(paneContainer) 70 | if (paneContainerView && this.alwaysTakeMinimumSpace) { 71 | // NOTE: Super horrible hack but the only possible way I could find :(( 72 | const dockNamesElement = paneContainerView.querySelector('.list-inline.tab-bar.inset-panel') 73 | const dockNamesRects = dockNamesElement ? dockNamesElement.getClientRects()[0] : null 74 | const tableElement = this.element.querySelector('table') 75 | const panelRects = tableElement ? tableElement.getClientRects()[0] : null 76 | if (dockNamesRects && panelRects) { 77 | minimumHeight = dockNamesRects.height + panelRects.height + 1 78 | } 79 | } 80 | 81 | let updateConfigHeight: number | null = null 82 | const heightSet = 83 | minimumHeight !== null && !forConfigHeight ? Math.min(minimumHeight, this.panelHeight) : this.panelHeight 84 | 85 | // Person resized the panel, save new resized value to config 86 | if (this.lastSetPaneHeight !== undefined && paneContainer.state.size !== this.lastSetPaneHeight && !forConfigHeight) { 87 | updateConfigHeight = paneContainer.state.size 88 | } 89 | 90 | this.lastSetPaneHeight = heightSet 91 | paneContainer.state.size = heightSet 92 | paneContainer.render(paneContainer.state) 93 | 94 | if (updateConfigHeight !== null) { 95 | config.set('linter-ui-default.panelHeight', updateConfigHeight) 96 | } 97 | } 98 | 99 | /* eslint-disable class-methods-use-this */ 100 | // atom API requires these methods 101 | getURI() { 102 | return WORKSPACE_URI 103 | } 104 | getTitle() { 105 | return 'Linter' 106 | } 107 | getDefaultLocation() { 108 | return DOCK_DEFAULT_LOCATION 109 | } 110 | getAllowedLocations() { 111 | return DOCK_ALLOWED_LOCATIONS 112 | } 113 | getPreferredHeight() { 114 | return config.get('linter-ui-default.panelHeight') 115 | } 116 | /* eslint-enable class-methods-use-this */ 117 | 118 | dispose() { 119 | this.subscriptions.dispose() 120 | const paneContainer = getPaneContainer(this) 121 | if (paneContainer !== null && !this.alwaysTakeMinimumSpace && paneContainer.state.size !== this.panelHeight) { 122 | config.set('linter-ui-default.panelHeight', paneContainer.state.size) 123 | paneContainer.paneForItem(this)?.destroyItem(this, true) 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /lib/panel/index.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable } from 'atom' 2 | const { config, workspace } = atom 3 | import Delegate from './delegate' 4 | import PanelDock from './dock' 5 | import type { LinterMessage } from '../types' 6 | import type { PaneContainer } from './dock' 7 | 8 | export default class Panel { 9 | panel: PanelDock | null = null 10 | element: HTMLElement = document.createElement('div') 11 | delegate: Delegate = new Delegate() 12 | messages: Array = [] 13 | deactivating: boolean = false 14 | subscriptions: CompositeDisposable = new CompositeDisposable() 15 | showPanelConfig: boolean = true 16 | hidePanelWhenEmpty: boolean = true 17 | showPanelStateMessages: boolean = false 18 | activationTimer: number 19 | constructor() { 20 | this.subscriptions.add( 21 | this.delegate, 22 | config.observe('linter-ui-default.hidePanelWhenEmpty', async (hidePanelWhenEmpty: boolean) => { 23 | this.hidePanelWhenEmpty = hidePanelWhenEmpty 24 | await this.refresh() 25 | }), 26 | workspace.onDidDestroyPane(({ pane: destroyedPane }) => { 27 | const isPaneItemDestroyed = this.panel !== null ? destroyedPane.getItems().includes(this.panel) : true 28 | if (isPaneItemDestroyed && !this.deactivating) { 29 | this.panel = null 30 | config.set('linter-ui-default.showPanel', false) 31 | } 32 | }), 33 | workspace.onDidDestroyPaneItem(({ item: paneItem }) => { 34 | if (paneItem instanceof PanelDock && !this.deactivating) { 35 | this.panel = null 36 | config.set('linter-ui-default.showPanel', false) 37 | } 38 | }), 39 | config.observe('linter-ui-default.showPanel', async (showPanel: boolean) => { 40 | this.showPanelConfig = showPanel 41 | await this.refresh() 42 | }), 43 | workspace.getCenter().observeActivePaneItem(async () => { 44 | this.showPanelStateMessages = Boolean(this.delegate.filteredMessages.length) 45 | await this.refresh() 46 | }), 47 | ) 48 | this.activationTimer = window.requestIdleCallback(async () => { 49 | let firstTime = true 50 | const dock = workspace.getBottomDock() 51 | this.subscriptions.add( 52 | dock.onDidChangeActivePaneItem(paneItem => { 53 | if (!this.panel || this.getPanelLocation() !== 'bottom') { 54 | return 55 | } 56 | if (firstTime) { 57 | firstTime = false 58 | return 59 | } 60 | const isFocusIn = paneItem === this.panel 61 | const externallyToggled = isFocusIn !== this.showPanelConfig 62 | if (externallyToggled) { 63 | config.set('linter-ui-default.showPanel', !this.showPanelConfig) 64 | } 65 | }), 66 | dock.onDidChangeVisible(visible => { 67 | if (!this.panel || this.getPanelLocation() !== 'bottom') { 68 | return 69 | } 70 | if (!visible) { 71 | // ^ When it's time to tell config to hide 72 | if (this.showPanelConfig && this.hidePanelWhenEmpty && !this.showPanelStateMessages) { 73 | // Ignore because we just don't have any messages to show, everything else is fine 74 | return 75 | } 76 | } 77 | if (dock.getActivePaneItem() !== this.panel) { 78 | // Ignore since the visibility of this panel is not changing 79 | return 80 | } 81 | const externallyToggled = visible !== this.showPanelConfig 82 | if (externallyToggled) { 83 | config.set('linter-ui-default.showPanel', !this.showPanelConfig) 84 | } 85 | }), 86 | ) 87 | 88 | await this.activate() 89 | }) 90 | } 91 | 92 | private getPanelLocation() { 93 | if (!this.panel) { 94 | return null 95 | } 96 | // @ts-ignore internal API 97 | const paneContainer: PaneContainer | undefined = workspace.paneContainerForItem(this.panel) 98 | return paneContainer?.location 99 | } 100 | 101 | async activate() { 102 | if (this.panel) { 103 | return 104 | } 105 | this.panel = new PanelDock(this.delegate) 106 | await workspace.open(this.panel, { 107 | activatePane: false, 108 | activateItem: false, 109 | searchAllPanes: true, 110 | }) 111 | await this.update() 112 | await this.refresh() 113 | } 114 | 115 | async update(newMessages: Array | null | undefined = null) { 116 | if (newMessages) { 117 | this.messages = newMessages 118 | } 119 | this.delegate.update(this.messages) 120 | this.showPanelStateMessages = Boolean(this.delegate.filteredMessages.length) 121 | await this.refresh() 122 | } 123 | 124 | async refresh() { 125 | const panel = this.panel 126 | if (panel === null) { 127 | if (this.showPanelConfig) { 128 | await this.activate() 129 | } 130 | return 131 | } 132 | // @ts-ignore internal API 133 | const paneContainer: PaneContainer | undefined = workspace.paneContainerForItem(panel) 134 | if (paneContainer?.location !== 'bottom') { 135 | return 136 | } 137 | const isActivePanel = paneContainer.getActivePaneItem() === panel 138 | const visibilityAllowed1 = this.showPanelConfig 139 | const visibilityAllowed2 = this.hidePanelWhenEmpty ? this.showPanelStateMessages : true 140 | if (visibilityAllowed1 && visibilityAllowed2) { 141 | if (!isActivePanel) { 142 | paneContainer.paneForItem(panel)?.activateItem(panel) 143 | } 144 | paneContainer.show() 145 | panel.doPanelResize() 146 | } else if (isActivePanel) { 147 | paneContainer.hide() 148 | } 149 | } 150 | 151 | dispose() { 152 | this.deactivating = true 153 | if (this.panel) { 154 | this.panel.dispose() 155 | } 156 | this.subscriptions.dispose() 157 | window.cancelIdleCallback(this.activationTimer) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /lib/status-bar/element.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable, Emitter } from 'atom' 2 | const { tooltips } = atom 3 | import type { Disposable } from 'atom' 4 | 5 | import * as Helpers from './helpers' 6 | 7 | export default class Element { 8 | item: HTMLElement = document.createElement('div') 9 | itemErrors: HTMLElement = Helpers.getElement('stop') 10 | itemWarnings: HTMLElement = Helpers.getElement('alert') 11 | itemInfos: HTMLElement = Helpers.getElement('info') 12 | 13 | emitter = new Emitter<{}, { click: 'error' | 'warning' | 'info' }>() // eslint-disable-line @typescript-eslint/ban-types 14 | subscriptions: CompositeDisposable = new CompositeDisposable() 15 | 16 | constructor() { 17 | this.item.appendChild(this.itemErrors) 18 | this.item.appendChild(this.itemWarnings) 19 | this.item.appendChild(this.itemInfos) 20 | this.item.classList.add('inline-block') 21 | this.item.classList.add('linter-status-count') 22 | 23 | this.subscriptions.add( 24 | this.emitter, 25 | tooltips.add(this.itemErrors, { title: 'Linter Errors' }), 26 | tooltips.add(this.itemWarnings, { title: 'Linter Warnings' }), 27 | tooltips.add(this.itemInfos, { title: 'Linter Infos' }), 28 | ) 29 | 30 | this.itemErrors.onclick = () => this.emitter.emit('click', 'error') 31 | this.itemWarnings.onclick = () => this.emitter.emit('click', 'warning') 32 | this.itemInfos.onclick = () => this.emitter.emit('click', 'info') 33 | 34 | this.update(0, 0, 0) 35 | } 36 | setVisibility(prefix: string, visibility: boolean) { 37 | if (visibility) { 38 | this.item.classList.remove(`hide-${prefix}`) 39 | } else { 40 | this.item.classList.add(`hide-${prefix}`) 41 | } 42 | } 43 | update(countErrors: number, countWarnings: number, countInfos: number): void { 44 | this.itemErrors.childNodes[0].textContent = String(countErrors) 45 | this.itemWarnings.childNodes[0].textContent = String(countWarnings) 46 | this.itemInfos.childNodes[0].textContent = String(countInfos) 47 | 48 | if (countErrors) { 49 | this.itemErrors.classList.add('text-error') 50 | } else { 51 | this.itemErrors.classList.remove('text-error') 52 | } 53 | 54 | if (countWarnings) { 55 | this.itemWarnings.classList.add('text-warning') 56 | } else { 57 | this.itemWarnings.classList.remove('text-warning') 58 | } 59 | 60 | if (countInfos) { 61 | this.itemInfos.classList.add('text-info') 62 | } else { 63 | this.itemInfos.classList.remove('text-info') 64 | } 65 | } 66 | onDidClick(callback: (type: 'error' | 'warning' | 'info') => void): Disposable { 67 | return this.emitter.on('click', callback) 68 | } 69 | dispose() { 70 | this.subscriptions.dispose() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/status-bar/helpers.ts: -------------------------------------------------------------------------------- 1 | export function getElement(icon: string): HTMLElement { 2 | const element = document.createElement('a') 3 | 4 | element.classList.add(`icon-${icon}`) 5 | 6 | element.appendChild(document.createTextNode('')) 7 | 8 | return element 9 | } 10 | -------------------------------------------------------------------------------- /lib/status-bar/index.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable, Disposable } from 'atom' 2 | const { config, workspace, views, commands } = atom 3 | import type { StatusBar as StatusBarRegistry, Tile as StatusBarTile } from 'atom/status-bar' 4 | import Element from './element' 5 | import { $file } from '../helpers' 6 | import type { LinterMessage } from '../types' 7 | 8 | export default class StatusBar { 9 | element: Element = new Element() 10 | messages: Array = [] 11 | subscriptions: CompositeDisposable = new CompositeDisposable() 12 | statusBarRepresents?: 'Entire Project' | 'Current File' 13 | statusBarClickBehavior?: 'Toggle Panel' | 'Jump to next issue' | 'Toggle Status Bar Scope' 14 | 15 | constructor() { 16 | this.subscriptions.add( 17 | this.element, 18 | config.observe('linter-ui-default.statusBarRepresents', (statusBarRepresents: StatusBar['statusBarRepresents']) => { 19 | const notInitial = typeof this.statusBarRepresents !== 'undefined' 20 | this.statusBarRepresents = statusBarRepresents 21 | if (notInitial) { 22 | this.update() 23 | } 24 | }), 25 | config.observe( 26 | 'linter-ui-default.statusBarClickBehavior', 27 | (statusBarClickBehavior: StatusBar['statusBarClickBehavior']) => { 28 | const notInitial = typeof this.statusBarClickBehavior !== 'undefined' 29 | this.statusBarClickBehavior = statusBarClickBehavior 30 | if (notInitial) { 31 | this.update() 32 | } 33 | }, 34 | ), 35 | config.observe('linter-ui-default.showStatusBar', (showStatusBar: boolean) => { 36 | this.element.setVisibility('config', showStatusBar) 37 | }), 38 | workspace.getCenter().observeActivePaneItem(paneItem => { 39 | const isTextEditor = workspace.isTextEditor(paneItem) 40 | this.element.setVisibility('pane', isTextEditor) 41 | if (isTextEditor && this.statusBarRepresents === 'Current File') { 42 | this.update() 43 | } 44 | }), 45 | ) 46 | 47 | this.element.onDidClick(async type => { 48 | const workspaceView = views.getView(workspace) 49 | if (this.statusBarClickBehavior === 'Toggle Panel') { 50 | await commands.dispatch(workspaceView, 'linter-ui-default:toggle-panel') 51 | } else if (this.statusBarClickBehavior === 'Toggle Status Bar Scope') { 52 | config.set( 53 | 'linter-ui-default.statusBarRepresents', 54 | this.statusBarRepresents === 'Entire Project' ? 'Current File' : 'Entire Project', 55 | ) 56 | } else { 57 | const postfix = this.statusBarRepresents === 'Current File' ? '-in-current-file' : '' 58 | await commands.dispatch(workspaceView, `linter-ui-default:next-${type}${postfix}`) 59 | } 60 | }) 61 | } 62 | update(messages?: Array): void { 63 | if (messages !== undefined) { 64 | this.messages = messages 65 | } else { 66 | messages = this.messages 67 | } 68 | 69 | const count = { error: 0, warning: 0, info: 0 } 70 | const currentTextEditor = workspace.getActiveTextEditor() 71 | const currentPath = currentTextEditor?.getPath() ?? NaN 72 | // NOTE: ^ Setting default to NaN so it won't match empty file paths in messages 73 | 74 | messages.forEach(message => { 75 | if (this.statusBarRepresents === 'Entire Project' || $file(message) === currentPath) { 76 | if (message.severity === 'error') { 77 | count.error++ 78 | } else if (message.severity === 'warning') { 79 | count.warning++ 80 | } else { 81 | count.info++ 82 | } 83 | } 84 | }) 85 | this.element.update(count.error, count.warning, count.info) 86 | } 87 | attach(statusBarRegistry: StatusBarRegistry) { 88 | let statusBar: StatusBarTile | null = null 89 | 90 | this.subscriptions.add( 91 | config.observe('linter-ui-default.statusBarPosition', statusBarPosition => { 92 | if (statusBar) { 93 | statusBar.destroy() 94 | } 95 | statusBar = statusBarRegistry[`add${statusBarPosition}Tile`]({ 96 | item: this.element.item, 97 | priority: statusBarPosition === 'Left' ? 0 : 1000, 98 | }) 99 | }), 100 | ) 101 | this.subscriptions.add( 102 | new Disposable(function () { 103 | if (statusBar) { 104 | statusBar.destroy() 105 | } 106 | }), 107 | ) 108 | } 109 | dispose() { 110 | this.subscriptions.dispose() 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /lib/tooltip/delegate.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable, Emitter } from 'atom' 2 | const { config, workspace, views, commands } = atom 3 | import type { Disposable } from 'atom' 4 | import { CommandEventExtra } from '../types' 5 | 6 | export default class TooltipDelegate { 7 | emitter: Emitter = new Emitter<{ 8 | 'should-update': never 9 | 'should-expand': never 10 | 'should-collapse': never 11 | }>() 12 | expanded: boolean = false 13 | subscriptions: CompositeDisposable = new CompositeDisposable() 14 | showProviderName?: boolean 15 | 16 | constructor() { 17 | this.subscriptions.add( 18 | this.emitter, 19 | config.observe('linter-ui-default.showProviderName', (showProviderName: boolean) => { 20 | const shouldUpdate = typeof this.showProviderName !== 'undefined' 21 | this.showProviderName = showProviderName 22 | if (shouldUpdate) { 23 | this.emitter.emit('should-update') 24 | } 25 | }), 26 | commands.add('atom-workspace', { 27 | 'linter-ui-default:expand-tooltip': (event: CommandEventExtra) => { 28 | if (this.expanded) { 29 | return 30 | } 31 | this.expanded = true 32 | this.emitter.emit('should-expand') 33 | 34 | // If bound to a key, collapse when that key is released, just like old times 35 | if (event.originalEvent?.isTrusted === true) { 36 | // $FlowIgnore: document.body is never null 37 | document.body.addEventListener( 38 | 'keyup', 39 | async function eventListener() { 40 | // $FlowIgnore: document.body is never null 41 | document.body.removeEventListener('keyup', eventListener) 42 | await commands.dispatch(views.getView(workspace), 'linter-ui-default:collapse-tooltip') 43 | }, 44 | { passive: true }, 45 | ) 46 | } 47 | }, 48 | 'linter-ui-default:collapse-tooltip': () => { 49 | this.expanded = false 50 | this.emitter.emit('should-collapse') 51 | }, 52 | }), 53 | ) 54 | } 55 | onShouldUpdate(callback: () => void): Disposable { 56 | return this.emitter.on('should-update', callback) 57 | } 58 | onShouldExpand(callback: () => void): Disposable { 59 | return this.emitter.on('should-expand', callback) 60 | } 61 | onShouldCollapse(callback: () => void): Disposable { 62 | return this.emitter.on('should-collapse', callback) 63 | } 64 | dispose() { 65 | this.emitter.dispose() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/tooltip/index.tsx: -------------------------------------------------------------------------------- 1 | import { For, Show, render } from 'solid-js/web' 2 | import { CompositeDisposable, Emitter, TextEditorElement } from 'atom' 3 | import type { Disposable, Point, TextEditor, DisplayMarker } from 'atom' 4 | import Delegate from './delegate' 5 | import MessageElement from './message' 6 | import { $range } from '../helpers' 7 | import type { LinterMessage } from '../types' 8 | import { makeOverlaySelectable } from 'atom-ide-base/commons-ui/float-pane/selectable-overlay' 9 | 10 | export default class Tooltip { 11 | marker: DisplayMarker 12 | element: HTMLElement = document.createElement('div') 13 | emitter = new Emitter<{ 'did-destroy': never }>() 14 | messages: Array 15 | subscriptions: CompositeDisposable = new CompositeDisposable() 16 | 17 | constructor(messages: Array, position: Point, textEditor: TextEditor) { 18 | this.messages = messages 19 | this.marker = textEditor.markBufferRange([position, position]) 20 | this.marker.onDidDestroy(() => this.emitter.emit('did-destroy')) 21 | 22 | const delegate = new Delegate() 23 | 24 | // make tooltips copyable and selectable 25 | makeOverlaySelectable(textEditor, this.element) 26 | 27 | this.element.id = 'linter-tooltip' 28 | 29 | textEditor.decorateMarker(this.marker, { 30 | type: 'overlay', 31 | item: this.element, 32 | }) 33 | 34 | this.subscriptions.add(this.emitter, delegate) 35 | 36 | render(() => , this.element) 37 | moveElement(this.element, position, textEditor) 38 | } 39 | 40 | isValid(position: Point, messages: Map): boolean { 41 | if (this.messages.length !== 1 || !messages.has(this.messages[0].key)) { 42 | return false 43 | } 44 | const range = $range(this.messages[0]) 45 | return range?.containsPoint(position) === true 46 | } 47 | onDidDestroy(callback: () => void): Disposable { 48 | return this.emitter.on('did-destroy', callback) 49 | } 50 | dispose() { 51 | this.emitter.emit('did-destroy') 52 | this.subscriptions.dispose() 53 | } 54 | } 55 | 56 | interface TooltipElementProps { 57 | messages: LinterMessage[] 58 | delegate: Delegate 59 | } 60 | 61 | function TooltipElement(props: TooltipElementProps) { 62 | return ( 63 |
    64 | 65 | {message => ( 66 | 67 | 68 | 69 | )} 70 | 71 |
    72 | ) 73 | } 74 | 75 | /** Move box above the current editing line */ 76 | // HACK: patch the decoration's style so it is shown above the current line 77 | function moveElement(element: HTMLElement, position: Point, textEditor: TextEditor) { 78 | setTimeout(() => { 79 | const hight = element.getBoundingClientRect().height 80 | const lineHight = textEditor.getLineHeightInPixels() 81 | // @ts-ignore: internal API 82 | const availableHight = (position.row - textEditor.getFirstVisibleScreenRow()) * lineHight 83 | if (hight < availableHight) { 84 | const overlay = element.parentElement 85 | if (overlay !== null) { 86 | overlay.style.transform = `translateY(-${2 + lineHight + hight}px)` 87 | } 88 | } else { 89 | // move down so it does not overlap with datatip-overlay 90 | // @ts-ignore 91 | const dataTip = (textEditor.getElement() as TextEditorElement).querySelector('.datatip-overlay') 92 | if (dataTip !== null) { 93 | const overlay = element.parentElement 94 | if (overlay !== null) { 95 | overlay.style.transform = `translateY(${dataTip.clientHeight}px)` 96 | } 97 | } 98 | } 99 | element.style.visibility = 'visible' 100 | }, 50) 101 | } 102 | -------------------------------------------------------------------------------- /lib/tooltip/message.tsx: -------------------------------------------------------------------------------- 1 | const { workspace } = atom 2 | import { createSignal, onMount, createEffect, Show } from 'solid-js' 3 | import * as url from 'url' 4 | import { debounce, once, visitMessage, openExternally, openFile, applySolution, sortSolutions } from '../helpers' 5 | let marked: typeof import('marked') | undefined 6 | 7 | import type TooltipDelegate from './delegate' 8 | import type { Message, LinterMessage } from '../types' 9 | // TODO why do we need to debounce/once these buttons? They shouldn't be called multiple times 10 | 11 | type Props = { 12 | key: string 13 | message: Message 14 | delegate: TooltipDelegate 15 | } 16 | 17 | export default function MessageElement(props: Props) { 18 | const [getDescription, setDescription] = createSignal('Loading ...') 19 | const [getShowDescription, setShowDescription] = createSignal(false) 20 | 21 | createEffect(async () => { 22 | if (getShowDescription()) { 23 | const description = props.message.description 24 | console.log(description) 25 | if (typeof description === 'string') { 26 | setDescription(await renderMarkdown(description)) 27 | } else if (typeof description === 'function') { 28 | const response = await description() 29 | if (typeof response !== 'string') { 30 | throw new Error(`Expected result to be string, got: ${typeof response}`) 31 | } 32 | setDescription(response) 33 | } else { 34 | console.error('[Linter] Invalid description detected, expected string or function but got:', typeof description) 35 | } 36 | } 37 | }) 38 | 39 | onMount(() => { 40 | props.delegate.onShouldUpdate(() => { 41 | setShowDescription(false) 42 | setDescription('Loading ...') 43 | }) 44 | props.delegate.onShouldExpand(() => { 45 | setShowDescription(true) 46 | }) 47 | props.delegate.onShouldCollapse(() => { 48 | setShowDescription(false) 49 | }) 50 | }) 51 | 52 | // These props are static (non-reactive) 53 | const { message, delegate } = props 54 | 55 | return ( 56 |
    57 |
    58 | {/* fold button if has message description */} 59 | 60 | setShowDescription(!getShowDescription())}> 61 | 62 | 63 | 64 | {/* fix button */} 65 | 66 | 69 | 70 |
    71 |
    72 | {/* provider name */} 73 | {`${message.linterName}: `} 74 |
    75 | { 76 | // main message text 77 | message.excerpt 78 | } 79 |
    80 |
    81 | {/* message reference */} 82 | 83 | visitMessage(message, true))}> 84 | 85 | 86 | 87 | {/* message url */} 88 | 89 | openExternally(message))}> 90 | 91 | 92 | 93 |
    94 |
    95 | {/* message description */} 96 | 97 |
    98 |
    99 |
    100 | ) 101 | } 102 | 103 | function onFixClick(message: Message): void { 104 | const messageSolutions = message.solutions 105 | const textEditor = workspace.getActiveTextEditor() 106 | if (textEditor !== undefined) { 107 | if (Array.isArray(messageSolutions) && messageSolutions.length > 0) { 108 | applySolution(textEditor, sortSolutions(messageSolutions)[0]) 109 | } 110 | } 111 | } 112 | 113 | function canBeFixed(message: LinterMessage): boolean { 114 | const messageSolutions = message.solutions 115 | if (Array.isArray(messageSolutions) && messageSolutions.length > 0) { 116 | return true 117 | } 118 | return false 119 | } 120 | 121 | async function thisOpenFile(ev: MouseEvent) { 122 | if (!(ev.target instanceof HTMLElement)) { 123 | return 124 | } 125 | const href = findHref(ev.target) 126 | if (href === null) { 127 | return 128 | } 129 | // parse the link. e.g. atom://linter?file=&row=&column= 130 | const { protocol, hostname, query } = url.parse(href, true) 131 | if (protocol !== 'atom:' || hostname !== 'linter') { 132 | return 133 | } 134 | // TODO: based on the types query is never null 135 | if (query?.file === undefined) { 136 | return 137 | } else { 138 | const { file, row, column } = query 139 | // TODO: will these be an array? 140 | await openFile( 141 | /* file */ Array.isArray(file) ? file[0] : file, 142 | /* position */ { 143 | row: row !== undefined ? parseInt(Array.isArray(row) ? row[0] : row, 10) : 0, 144 | column: column !== undefined ? parseInt(Array.isArray(column) ? column[0] : column, 10) : 0, 145 | }, 146 | ) 147 | } 148 | } 149 | 150 | function findHref(elementGiven: HTMLElement): string | null { 151 | let el: HTMLElement | null = elementGiven 152 | while (el !== null && !el.classList.contains('linter-line')) { 153 | if (el instanceof HTMLAnchorElement) { 154 | return el.href 155 | } 156 | el = el.parentElement 157 | } 158 | return null 159 | } 160 | 161 | async function renderMarkdown(description: string) { 162 | if (marked === undefined) { 163 | // eslint-disable-next-line require-atomic-updates 164 | marked = (await import('marked')).default 165 | } 166 | return marked(description) 167 | } 168 | -------------------------------------------------------------------------------- /lib/tree-view/helpers.ts: -------------------------------------------------------------------------------- 1 | const { project } = atom 2 | import Path from 'path' 3 | import { $file } from '../helpers' 4 | import type { LinterMessage } from '../types' 5 | import type { TreeViewHighlight } from './index' 6 | 7 | function getChunks(filePath: string, projectPath: string): Array { 8 | const toReturn: Array = [] 9 | const chunks = filePath.split(Path.sep) 10 | while (chunks.length) { 11 | const currentPath = chunks.join(Path.sep) 12 | if (currentPath) { 13 | // This is required for when you open files outside of project window 14 | // and the last entry is '' because unix paths start with / 15 | toReturn.push(currentPath) 16 | if (currentPath === projectPath) { 17 | break 18 | } 19 | } 20 | chunks.pop() 21 | } 22 | return toReturn 23 | } 24 | 25 | function getChunksByProjects(filePath: string, projectPaths: Array): Array { 26 | const matchingProjectPath = projectPaths.find(p => filePath.startsWith(p)) 27 | if (matchingProjectPath === undefined) { 28 | return [filePath] 29 | } 30 | return getChunks(filePath, matchingProjectPath) 31 | } 32 | 33 | function mergeChange(change: Record, filePath: string, severity: string): void { 34 | if (change[filePath] === undefined) { 35 | change[filePath] = { 36 | info: false, 37 | error: false, 38 | warning: false, 39 | } 40 | } 41 | change[filePath]![severity] = true 42 | } 43 | 44 | export function calculateDecorations( 45 | decorateOnTreeView: 'Files and Directories' | 'Files' | undefined, 46 | messages: Array, 47 | ): Record { 48 | const toReturn: Record = {} 49 | const projectPaths: Array = project.getPaths() 50 | messages.forEach(function (message) { 51 | const filePath = $file(message) 52 | if (typeof filePath === 'string') { 53 | const chunks = decorateOnTreeView === 'Files' ? [filePath] : getChunksByProjects(filePath, projectPaths) 54 | chunks.forEach(chunk => mergeChange(toReturn, chunk, message.severity)) 55 | } 56 | }) 57 | return toReturn 58 | } 59 | -------------------------------------------------------------------------------- /lib/tree-view/index.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable } from 'atom' 2 | const { config } = atom 3 | import disposableEvent from 'disposable-event' 4 | import type { TargetWithAddEventListener } from 'disposable-event/src/target' 5 | 6 | import { debounce, get } from '../helpers' 7 | import { calculateDecorations } from './helpers' 8 | import type { LinterMessage } from '../types' 9 | 10 | export type TreeViewHighlight = { 11 | info: boolean 12 | error: boolean 13 | warning: boolean 14 | } 15 | 16 | export default class TreeView { 17 | messages: Array = [] 18 | decorations: Record = {} 19 | subscriptions: CompositeDisposable = new CompositeDisposable() 20 | decorateOnTreeView?: 'Files and Directories' | 'Files' | 'None' 21 | 22 | constructor() { 23 | this.subscriptions.add( 24 | config.observe('linter-ui-default.decorateOnTreeView', (decorateOnTreeView: TreeView['decorateOnTreeView']) => { 25 | if (typeof this.decorateOnTreeView === 'undefined') { 26 | this.decorateOnTreeView = decorateOnTreeView 27 | } else if (decorateOnTreeView === 'None') { 28 | this.update([]) 29 | this.decorateOnTreeView = decorateOnTreeView 30 | } else { 31 | const messages = this.messages 32 | this.decorateOnTreeView = decorateOnTreeView 33 | this.update(messages) 34 | } 35 | }), 36 | ) 37 | 38 | setTimeout(() => { 39 | const element = TreeView.getElement() 40 | if (!element) { 41 | return 42 | } 43 | // Subscription is only added if the CompositeDisposable hasn't been disposed 44 | this.subscriptions.add( 45 | disposableEvent( 46 | (element as unknown) as TargetWithAddEventListener, 47 | 'click', 48 | debounce(() => { 49 | this.update() 50 | }), 51 | { passive: true }, 52 | ), 53 | ) 54 | }, 100) 55 | } 56 | 57 | update(givenMessages: Array | null | undefined = null) { 58 | if (Array.isArray(givenMessages)) { 59 | this.messages = givenMessages 60 | } 61 | const messages = this.messages 62 | 63 | const element = TreeView.getElement() 64 | const decorateOnTreeView = this.decorateOnTreeView 65 | if (!element || decorateOnTreeView === 'None') { 66 | return 67 | } 68 | 69 | this.applyDecorations(calculateDecorations(decorateOnTreeView, messages)) 70 | } 71 | 72 | applyDecorations(decorations: Record) { 73 | const treeViewElement = TreeView.getElement() 74 | if (!treeViewElement) { 75 | return 76 | } 77 | 78 | const elementCache = new Map() 79 | const appliedDecorations: Record = {} 80 | 81 | const filePaths = Object.keys(this.decorations) 82 | for (const filePath of filePaths) { 83 | if (!(filePath in decorations)) { 84 | // Removed 85 | const element = get(elementCache, filePath, () => TreeView.getElementByPath(treeViewElement, filePath)) 86 | if (element !== null) { 87 | removeDecoration(element) 88 | } 89 | } 90 | } 91 | 92 | const filePathsNew = Object.keys(decorations) 93 | for (const filePath of filePathsNew) { 94 | const element = get(elementCache, filePath, () => TreeView.getElementByPath(treeViewElement, filePath)) 95 | if (element !== null) { 96 | // decorations[filePath] is not undefined because we are looping over the existing keys 97 | const decoration = decorations[filePath] as TreeViewHighlight 98 | handleDecoration(element, decoration, Boolean(this.decorations[filePath])) 99 | appliedDecorations[filePath] = decoration 100 | } 101 | } 102 | 103 | this.decorations = appliedDecorations 104 | } 105 | 106 | dispose() { 107 | this.subscriptions.dispose() 108 | } 109 | 110 | static getElement(): HTMLElement | null { 111 | return document.querySelector('.tree-view') 112 | } 113 | 114 | static getElementByPath(parent: HTMLElement, filePath: string): HTMLElement | null { 115 | return parent.querySelector(`[data-path=${CSS.escape(filePath)}]`) 116 | } 117 | } 118 | 119 | function handleDecoration(element: HTMLElement, highlights: TreeViewHighlight, update: boolean = false) { 120 | let decoration: HTMLElement | null = null 121 | if (update) { 122 | decoration = element.querySelector('linter-decoration') 123 | } 124 | if (decoration !== null) { 125 | decoration.className = '' 126 | } else { 127 | decoration = document.createElement('linter-decoration') 128 | element.appendChild(decoration) 129 | } 130 | if (highlights.error) { 131 | decoration.classList.add('linter-error') 132 | } else if (highlights.warning) { 133 | decoration.classList.add('linter-warning') 134 | } else if (highlights.info) { 135 | decoration.classList.add('linter-info') 136 | } 137 | } 138 | 139 | function removeDecoration(element: HTMLElement) { 140 | element.querySelector('linter-decoration')?.remove() 141 | } 142 | -------------------------------------------------------------------------------- /lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "strictNullChecks": true, 5 | "noUnusedLocals": true, 6 | "noUnusedParameters": true, 7 | "noImplicitReturns": true, 8 | "noImplicitAny": false, 9 | "noImplicitThis": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "declaration": true, 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "incremental": true, 15 | "sourceMap": true, 16 | "inlineSourceMap": false, 17 | "inlineSources": true, 18 | "preserveSymlinks": true, 19 | "removeComments": false, 20 | "jsx": "preserve", 21 | "jsxImportSource": "solid-js", 22 | "lib": ["ES2018", "dom"], 23 | "target": "ES2018", 24 | "allowJs": true, 25 | "esModuleInterop": true, 26 | "module": "commonjs", 27 | "moduleResolution": "node", 28 | "resolveJsonModule": true, 29 | "importHelpers": false, 30 | "outDir": "../dist", 31 | "paths": { 32 | "solid-js": ["../node_modules/solid-js/types/index.d.ts"], 33 | "solid-js/web": ["../node_modules/solid-js/web/types/index.d.ts"] 34 | } 35 | }, 36 | "compileOnSave": false, 37 | "exclude": ["types/linter/**/*"] 38 | } 39 | -------------------------------------------------------------------------------- /lib/types/atom.d.ts: -------------------------------------------------------------------------------- 1 | import { TextEditor, Package, CommandEvent } from 'atom' 2 | 3 | // TODO: uses internal API 4 | export type TextEditorExtra = TextEditor & { 5 | getURI?: () => string 6 | isAlive?: () => boolean 7 | } 8 | 9 | // TODO: uses internal API 10 | interface PackageDepsList { 11 | [key: string]: string[] 12 | } 13 | 14 | export type PackageExtra = Package & { 15 | metadata: PackageDepsList 16 | } 17 | 18 | export interface CommandEventExtra extends CommandEvent { 19 | // TODO add to @types/atom 20 | // TODO will it be undefined? 21 | originalEvent?: T 22 | } 23 | -------------------------------------------------------------------------------- /lib/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './atom' 2 | export * from './intentions' 3 | export * from './linter' 4 | -------------------------------------------------------------------------------- /lib/types/intentions.d.ts: -------------------------------------------------------------------------------- 1 | import { TextEditor, Point } from 'atom' 2 | 3 | // from intentions package: 4 | // https://github.com/steelbrain/intentions/blob/master/lib/types.js 5 | export type ListItem = { 6 | // // Automatically added 7 | readonly __$sb_intentions_class?: string 8 | 9 | // From providers 10 | icon?: string 11 | class?: string 12 | title: string 13 | priority: number 14 | selected(): void 15 | } 16 | 17 | export type IntentionsListProvider = { 18 | grammarScopes: Array 19 | getIntentions(parameters: { textEditor: TextEditor; bufferPosition: Point }): Array | Promise> 20 | } 21 | -------------------------------------------------------------------------------- /lib/types/linter.d.ts: -------------------------------------------------------------------------------- 1 | export * from './linter/types/linter' 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linter-ui-default", 3 | "main": "./dist/index.js", 4 | "version": "3.4.1", 5 | "description": "Default UI for the Linter package", 6 | "keywords": [ 7 | "linter-ui", 8 | "linter-ui-default" 9 | ], 10 | "repository": "https://github.com/steelbrain/linter-ui-default", 11 | "license": "MIT", 12 | "engines": { 13 | "atom": ">=1.52.0", 14 | "electron": ">=6.x" 15 | }, 16 | "scripts": { 17 | "format": "prettier --write .", 18 | "test.format": "prettier . --check", 19 | "lint": "eslint . --fix", 20 | "test.lint": "eslint .", 21 | "clean": "shx rm -rf dist", 22 | "types": "(tsc -p ./lib/tsconfig.json --emitDeclarationOnly || echo done) && (shx cp -r ./lib/types/ dist/types && shx rm -rf ./dist/types/linter/)", 23 | "build.unit": "npm run types && babel ./lib --out-dir ./dist --config-file ./babel.unit.config.json --extensions .tsx,.ts", 24 | "dev": "cross-env NODE_ENV=development parcel watch --target main ./lib/index.ts", 25 | "build": "cross-env NODE_ENV=production parcel build --target main ./lib/index.ts --detailed-report 50", 26 | "test.only": "atom --test spec", 27 | "test": "npm run build.unit && npm run build && npm run test.only", 28 | "get.linter-types": "node ./scripts/get-linter-types.js", 29 | "benchmark": "cross-env NODE_ENV=production npm run build.unit && atom --test ./benchmark/benchmark.js", 30 | "prepare": "npm run get.linter-types && npm run build", 31 | "build-commit": "npm run clean && npm run types && build-commit -o dist", 32 | "prepublishOnly": "npm run build-commit" 33 | }, 34 | "dependencies": { 35 | "atom-ide-base": "3.1.1", 36 | "atom-package-deps": "^7.2.3", 37 | "disposable-event": "^2.0.0", 38 | "marked": "^2.1.2", 39 | "solid-js": "~0.26.5", 40 | "solid-simple-table": "0.2.6" 41 | }, 42 | "devDependencies": { 43 | "@babel/cli": "^7.14.5", 44 | "@types/atom": "^1.40.10", 45 | "@types/chance": "^1.1.2", 46 | "@types/jasmine": "^3.7.7", 47 | "@types/lodash": "^4.14.170", 48 | "@types/marked": "^2.0.3", 49 | "@types/node": "^15.12.4", 50 | "@types/react": "^17.0.11", 51 | "@types/react-dom": "^17.0.8", 52 | "@types/requestidlecallback": "^0.3.1", 53 | "babel-preset-atomic": "^4.1.0", 54 | "babel-preset-solid": "~0.26.5", 55 | "build-commit": "0.1.4", 56 | "chance": "^1.1.7", 57 | "cross-env": "^7.0.3", 58 | "eslint": "^7.29.0", 59 | "eslint-config-atomic": "1.16.1", 60 | "gitly": "^2.1.1", 61 | "jasmine-fix": "^1.3.1", 62 | "module-alias": "^2.2.2", 63 | "parcel": "2.0.0-nightly.729", 64 | "prettier-config-atomic": "^2.0.5", 65 | "shx": "^0.3.3", 66 | "terser-config-atomic": "^0.1.1", 67 | "typescript": "^4" 68 | }, 69 | "targets": { 70 | "main": { 71 | "context": "electron-renderer", 72 | "includeNodeModules": { 73 | "atom": false, 74 | "electron": false, 75 | "atom-package-deps": false, 76 | "disposable-event": false 77 | }, 78 | "outputFormat": "commonjs", 79 | "isLibrary": true 80 | } 81 | }, 82 | "alias": { 83 | "solid-js": "solid-js/dist/solid.js", 84 | "solid-js/web": "solid-js/web/dist/web.js", 85 | "solid-simple-table": "solid-simple-table/dist/SimpleTable.module.js", 86 | "marked": "marked/lib/marked.esm.js" 87 | }, 88 | "_moduleAliases": { 89 | "solid-js": "./node_modules/solid-js/dist/solid.cjs", 90 | "solid-js/web": "./node_modules/solid-js/web/dist/web.cjs" 91 | }, 92 | "providedServices": { 93 | "linter-ui": { 94 | "versions": { 95 | "1.0.0": "provideUI" 96 | } 97 | }, 98 | "intentions:list": { 99 | "versions": { 100 | "1.0.0": "provideIntentions" 101 | } 102 | } 103 | }, 104 | "consumedServices": { 105 | "busy-signal": { 106 | "versions": { 107 | "1.0.0": "consumeSignal" 108 | } 109 | }, 110 | "status-bar": { 111 | "versions": { 112 | "^1.0.0": "consumeStatusBar" 113 | } 114 | } 115 | }, 116 | "activationHooks": [ 117 | "core:loaded-shell-environment" 118 | ], 119 | "package-deps": [ 120 | { 121 | "name": "intentions" 122 | }, 123 | { 124 | "name": "linter", 125 | "minimumVersion": "3.0.0" 126 | } 127 | ], 128 | "configSchema": { 129 | "showPanel": { 130 | "type": "boolean", 131 | "description": "Show panel at the bottom of screen", 132 | "default": false, 133 | "order": 1 134 | }, 135 | "showTooltip": { 136 | "description": "Show inline issue tooltips", 137 | "type": "boolean", 138 | "default": true, 139 | "order": 1 140 | }, 141 | "showStatusBar": { 142 | "description": "Show status bar with error / warning / info count at the bottom", 143 | "type": "boolean", 144 | "default": true, 145 | "order": 1 146 | }, 147 | "showDecorations": { 148 | "description": "Underline editor text and highlight gutter of issues", 149 | "type": "boolean", 150 | "default": true, 151 | "order": 1 152 | }, 153 | "showProviderName": { 154 | "type": "boolean", 155 | "description": "Show provider name in tooltip (Hint: It's always shown in the panel)", 156 | "default": false, 157 | "order": 1 158 | }, 159 | "useBusySignal": { 160 | "description": "Whether to integrate with the `busy-signal` package", 161 | "type": "boolean", 162 | "default": true, 163 | "order": 1 164 | }, 165 | "hidePanelWhenEmpty": { 166 | "description": "Hide panel when there are no issues to display", 167 | "type": "boolean", 168 | "default": true, 169 | "order": 1 170 | }, 171 | "alwaysTakeMinimumSpace": { 172 | "description": "Auto resizes Linter panel when there are less issues to show", 173 | "type": "boolean", 174 | "default": false, 175 | "order": 1 176 | }, 177 | "decorateOnTreeView": { 178 | "type": "string", 179 | "description": "Underline the selected type in TreeView to indicate issues", 180 | "default": "Files", 181 | "enum": [ 182 | "Files and Directories", 183 | "Files", 184 | "None" 185 | ], 186 | "order": 2 187 | }, 188 | "panelRepresents": { 189 | "type": "string", 190 | "enum": [ 191 | "Entire Project", 192 | "Current File", 193 | "Current Line" 194 | ], 195 | "default": "Current File", 196 | "order": 2 197 | }, 198 | "statusBarPosition": { 199 | "title": "Statusbar Position", 200 | "type": "string", 201 | "enum": [ 202 | "Left", 203 | "Right" 204 | ], 205 | "default": "Left", 206 | "order": 2 207 | }, 208 | "statusBarRepresents": { 209 | "title": "Statusbar Represents", 210 | "type": "string", 211 | "enum": [ 212 | "Entire Project", 213 | "Current File" 214 | ], 215 | "default": "Entire Project", 216 | "order": 2 217 | }, 218 | "statusBarClickBehavior": { 219 | "title": "Statusbar Click Behavior", 220 | "type": "string", 221 | "enum": [ 222 | "Toggle Panel", 223 | "Toggle Status Bar Scope", 224 | "Jump to next issue" 225 | ], 226 | "default": "Toggle Panel", 227 | "order": 2 228 | }, 229 | "tooltipFollows": { 230 | "type": "string", 231 | "description": "Choose whether tooltips show on mouseover, or follow the keyboard cursor", 232 | "enum": [ 233 | "Both", 234 | "Mouse", 235 | "Keyboard" 236 | ], 237 | "default": "Both", 238 | "order": 2 239 | }, 240 | "gutterPosition": { 241 | "title": "Gutter Highlights Position", 242 | "description": "Relative to the line numbers", 243 | "type": "string", 244 | "enum": [ 245 | "Left", 246 | "Right" 247 | ], 248 | "default": "Right", 249 | "order": 2 250 | }, 251 | "panelHeight": { 252 | "title": "Panel Height", 253 | "description": "in px", 254 | "type": "number", 255 | "default": 100, 256 | "order": 3 257 | }, 258 | "longLineLength": { 259 | "title": "Long Line Length", 260 | "description": "If an editor has a line with a length more than this number, linter will skip adding inline linter markers.", 261 | "type": "number", 262 | "default": 4000, 263 | "order": 10 264 | }, 265 | "largeFileLineCount": { 266 | "title": "Large File Line Count", 267 | "description": "If an editor more line numbers than this number, linter will skip adding inline linter markers.", 268 | "type": "number", 269 | "default": 20000, 270 | "order": 11 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('prettier-config-atomic'), 3 | // this just to minimize the changes for linter-ui-default 4 | printWidth: 125, 5 | singleQuote: true, 6 | arrowParens: 'avoid', 7 | trailingComma: 'all', 8 | } 9 | -------------------------------------------------------------------------------- /scripts/get-linter-types.js: -------------------------------------------------------------------------------- 1 | const { download, extract } = require('gitly') 2 | const { dirname, join } = require('path') 3 | const { shx } = require('shx/lib/shx') 4 | const { tmpdir } = require('os') 5 | 6 | ;(async function main() { 7 | const source = await download('steelbrain/linter') 8 | const root = dirname(__dirname) 9 | const distFolder = join(root, 'lib', 'types', 'linter') 10 | shx(['', '', 'rm', '-rf', distFolder]) 11 | // shx([, , "mkdir", "-p", distFolder]) 12 | 13 | const extractFolder = join(tmpdir(), 'linter') 14 | shx(['', '', 'mkdir', '-p', extractFolder]) 15 | await extract(source, extractFolder) 16 | 17 | shx(['', '', 'mv', join(extractFolder, 'dist'), distFolder]) 18 | shx(['', '', 'rm', '-rf', extractFolder]) 19 | 20 | // avoid circular types 21 | shx(['', '', 'rm', '-rf', join(distFolder, 'types', 'linter-ui-default')]) 22 | shx([ 23 | '', 24 | '', 25 | 'sed', 26 | '-i', 27 | './linter-ui-default/main', 28 | '../../../main', 29 | join(distFolder, 'types', 'linter-ui-default.d.ts'), 30 | ]) 31 | })() 32 | -------------------------------------------------------------------------------- /spec/activate.spec.js: -------------------------------------------------------------------------------- 1 | describe('activation and installation', () => { 2 | const deps = ['intentions', 'linter'] 3 | 4 | beforeEach(async () => { 5 | jasmine.attachToDOM(atom.views.getView(atom.workspace)) 6 | 7 | /* Activation */ 8 | // Trigger deferred activation 9 | atom.packages.triggerDeferredActivationHooks() 10 | // Activate activation hook 11 | atom.packages.triggerActivationHook('core:loaded-shell-environment') 12 | 13 | // Activate the package 14 | await atom.packages.activatePackage('linter-ui-default') 15 | }) 16 | 17 | it('Installation', function () { 18 | expect(atom.packages.isPackageLoaded('linter-ui-default')).toBeTruthy() 19 | const allDeps = atom.packages.getAvailablePackageNames() 20 | deps.forEach(dep => { 21 | expect(allDeps.includes(dep)).toBeTruthy() 22 | }) 23 | }) 24 | 25 | it('Activation', function () { 26 | expect(atom.packages.isPackageLoaded('linter-ui-default')).toBeTruthy() 27 | deps.forEach(async dep => { 28 | await atom.packages.activatePackage(dep) 29 | expect(atom.packages.isPackageLoaded(dep)).toBeTruthy() 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /spec/busy-singal-spec.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // eslint-disable-next-line import/no-unassigned-import 3 | import 'module-alias/register' 4 | import { beforeEach } from 'jasmine-fix' 5 | import BusySignal from '../dist/busy-signal' 6 | import { getLinter } from './helpers' 7 | 8 | class SignalRegistry { 9 | texts: Array 10 | constructor() { 11 | this.texts = [] 12 | } 13 | clear() { 14 | this.texts.splice(0) 15 | } 16 | add(text) { 17 | if (this.texts.includes(text)) { 18 | throw new TypeError(`'${text}' already added`) 19 | } 20 | this.texts.push(text) 21 | } 22 | remove(text) { 23 | const index = this.texts.indexOf(text) 24 | if (index !== -1) { 25 | this.texts.splice(index, 1) 26 | } 27 | } 28 | static create() { 29 | const registry = new SignalRegistry() 30 | spyOn(registry, 'add').andCallThrough() 31 | spyOn(registry, 'remove').andCallThrough() 32 | spyOn(registry, 'clear').andCallThrough() 33 | return registry 34 | } 35 | } 36 | 37 | describe('BusySignal', function () { 38 | let busySignal 39 | 40 | beforeEach(async function () { 41 | // Activate activation hook 42 | atom.packages.triggerDeferredActivationHooks() 43 | atom.packages.triggerActivationHook('core:loaded-shell-environment') 44 | 45 | await atom.packages.loadPackage('linter-ui-default') 46 | busySignal = new BusySignal() 47 | busySignal.attach(SignalRegistry) 48 | }) 49 | afterEach(function () { 50 | busySignal.dispose() 51 | }) 52 | 53 | it('tells the registry when linting is in progress without adding duplicates', function () { 54 | const linterA = getLinter() 55 | const texts = busySignal.provider && busySignal.provider.texts 56 | expect(texts).toEqual([]) 57 | busySignal.didBeginLinting(linterA, '/') 58 | expect(texts).toEqual(['some on /']) 59 | busySignal.didFinishLinting(linterA, '/') 60 | busySignal.didFinishLinting(linterA, '/') 61 | expect(texts).toEqual([]) 62 | busySignal.didBeginLinting(linterA, '/') 63 | busySignal.didBeginLinting(linterA, '/') 64 | expect(texts).toEqual(['some on /']) 65 | busySignal.didFinishLinting(linterA, '/') 66 | expect(texts).toEqual([]) 67 | }) 68 | it('shows one line per file and one for all project scoped ones', function () { 69 | const linterA = getLinter('A') 70 | const linterB = getLinter('B') 71 | const linterC = getLinter('C') 72 | const linterD = getLinter('D') 73 | const linterE = getLinter('E') 74 | busySignal.didBeginLinting(linterA, '/a') 75 | busySignal.didBeginLinting(linterA, '/aa') 76 | busySignal.didBeginLinting(linterB, '/b') 77 | busySignal.didBeginLinting(linterC, '/b') 78 | busySignal.didBeginLinting(linterD) 79 | busySignal.didBeginLinting(linterE) 80 | const texts = busySignal.provider && busySignal.provider.texts 81 | // Test initial state 82 | expect(texts).toEqual(['A on /a', 'A on /aa', 'B on /b', 'C on /b', 'D', 'E']) 83 | // Test finish event for no file for a linter 84 | busySignal.didFinishLinting(linterA) 85 | expect(texts).toEqual(['A on /a', 'A on /aa', 'B on /b', 'C on /b', 'D', 'E']) 86 | // Test finish of a single file of a linter with two files running 87 | busySignal.didFinishLinting(linterA, '/a') 88 | expect(texts).toEqual(['A on /aa', 'B on /b', 'C on /b', 'D', 'E']) 89 | // Test finish of the last remaining file for linterA 90 | busySignal.didFinishLinting(linterA, '/aa') 91 | expect(texts).toEqual(['B on /b', 'C on /b', 'D', 'E']) 92 | // Test finish of first linter of two running on '/b' 93 | busySignal.didFinishLinting(linterB, '/b') 94 | expect(texts).toEqual(['C on /b', 'D', 'E']) 95 | // Test finish of second (last) linter running on '/b' 96 | busySignal.didFinishLinting(linterC, '/b') 97 | expect(texts).toEqual(['D', 'E']) 98 | // Test finish even for an unkown file for a linter 99 | busySignal.didFinishLinting(linterD, '/b') 100 | expect(texts).toEqual(['D', 'E']) 101 | // Test finishing a project linter (no file) 102 | busySignal.didFinishLinting(linterD) 103 | expect(texts).toEqual(['E']) 104 | // Test finishing the last linter 105 | busySignal.didFinishLinting(linterE) 106 | expect(texts).toEqual([]) 107 | }) 108 | it('clears everything on dispose', function () { 109 | const linterA = getLinter() 110 | busySignal.didBeginLinting(linterA, '/a') 111 | const texts = busySignal.provider && busySignal.provider.texts 112 | expect(texts).toEqual(['some on /a']) 113 | busySignal.dispose() 114 | expect(texts).toEqual([]) 115 | }) 116 | }) 117 | -------------------------------------------------------------------------------- /spec/editor-spec.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // eslint-disable-next-line import/no-unassigned-import 3 | import 'module-alias/register' 4 | import { Range } from 'atom' 5 | // eslint-disable-next-line no-unused-vars 6 | import { it, beforeEach, afterEach } from 'jasmine-fix' 7 | import Editor from '../dist/editor' 8 | import { getMessage } from './helpers' 9 | 10 | describe('Editor', function () { 11 | let editor 12 | let message 13 | let textEditor 14 | 15 | beforeEach(async function () { 16 | message = getMessage() 17 | message.location.position = [ 18 | [2, 0], 19 | [2, 1], 20 | ] 21 | message.location.file = __filename 22 | await atom.workspace.open(__filename) 23 | textEditor = atom.workspace.getActiveTextEditor() 24 | editor = new Editor(textEditor) 25 | 26 | // Activate activation hook 27 | atom.packages.triggerDeferredActivationHooks() 28 | atom.packages.triggerActivationHook('core:loaded-shell-environment') 29 | 30 | atom.packages.loadPackage('linter-ui-default') 31 | }) 32 | afterEach(function () { 33 | editor.dispose() 34 | atom.workspace.destroyActivePaneItem() 35 | }) 36 | 37 | describe('applyChanges', function () { 38 | it('applies the messages to the editor', function () { 39 | expect(textEditor.getBuffer().getMarkerCount()).toBe(0) 40 | editor.applyChanges([message], []) 41 | expect(textEditor.getBuffer().getMarkerCount()).toBe(1) 42 | editor.applyChanges([], [message]) 43 | expect(textEditor.getBuffer().getMarkerCount()).toBe(0) 44 | }) 45 | it('makes sure that the message is updated if text is manipulated', function () { 46 | expect(textEditor.getBuffer().getMarkerCount()).toBe(0) 47 | editor.applyChanges([message], []) 48 | expect(textEditor.getBuffer().getMarkerCount()).toBe(1) 49 | expect(Range.fromObject(message.location.position)).toEqual({ 50 | start: { row: 2, column: 0 }, 51 | end: { row: 2, column: 1 }, 52 | }) 53 | textEditor.getBuffer().insert([2, 0], 'Hello') 54 | expect(Range.fromObject(message.location.position)).toEqual({ 55 | start: { row: 2, column: 0 }, 56 | end: { row: 2, column: 6 }, 57 | }) 58 | editor.applyChanges([], [message]) 59 | expect(Range.fromObject(message.location.position)).toEqual({ 60 | start: { row: 2, column: 0 }, 61 | end: { row: 2, column: 6 }, 62 | }) 63 | expect(textEditor.getBuffer().getMarkerCount()).toBe(0) 64 | }) 65 | }) 66 | describe('Response to config', function () { 67 | it('responds to `gutterPosition`', function () { 68 | atom.config.set('linter-ui-default.gutterPosition', 'Left') 69 | expect(editor.gutter && editor.gutter.priority).toBe(-100) 70 | atom.config.set('linter-ui-default.gutterPosition', 'Right') 71 | expect(editor.gutter && editor.gutter.priority).toBe(100) 72 | }) 73 | it('responds to `showDecorations`', function () { 74 | atom.config.set('linter-ui-default.showDecorations', false) 75 | expect(editor.gutter).toBe(null) 76 | atom.config.set('linter-ui-default.showDecorations', true) 77 | expect(editor.gutter).not.toBe(null) 78 | }) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /spec/fixtures/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-atomic", 3 | "rules": { 4 | "@typescript-eslint/no-inferrable-types": "off", 5 | "@typescript-eslint/no-non-null-assertion": "off", 6 | "react/react-in-jsx-scope": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /spec/fixtures/error.ts: -------------------------------------------------------------------------------- 1 | 2 | // error and warning together 3 | console. 4 | -------------------------------------------------------------------------------- /spec/fixtures/expandable-description.rb: -------------------------------------------------------------------------------- 1 | # the descriptions are expandable (RuboCop) 2 | def Myfun() 3 | return 1 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/info.ts: -------------------------------------------------------------------------------- 1 | // info 2 | import "@babel/preset-typescript" 3 | -------------------------------------------------------------------------------- /spec/fixtures/long-text.ts: -------------------------------------------------------------------------------- 1 | // both datatip and linter tooltip below 2 | function myfun(x: Function) { 3 | return x 4 | } 5 | 6 | myfun(() => console.log("hello world")) 7 | 8 | 9 | 10 | 11 | // datatip below and linter tooltip above 12 | function myfun2(x: Function) { 13 | return x 14 | } 15 | -------------------------------------------------------------------------------- /spec/fixtures/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../lib/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /spec/fixtures/warning.ts: -------------------------------------------------------------------------------- 1 | 2 | // warning 3 | () => {} 4 | -------------------------------------------------------------------------------- /spec/fixtures/with-fixes.ts: -------------------------------------------------------------------------------- 1 | // fix button 2 | let x = 1 3 | -------------------------------------------------------------------------------- /spec/helpers.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export function getMessage(type: ?string = 'Error', filePath: ?string, range: ?Object): Object { 4 | const message: Object = { 5 | version: 2, 6 | severity: type.toLowerCase(), 7 | excerpt: String(Math.random()), 8 | location: { file: filePath, position: range }, 9 | } 10 | 11 | return message 12 | } 13 | 14 | export function getLinter(name: ?string = 'some'): Object { 15 | return { 16 | name, 17 | grammarScopes: [], 18 | lint() { 19 | /* no operation */ 20 | }, 21 | } 22 | } 23 | 24 | export function dispatchCommand(target: Object, commandName: string) { 25 | atom.commands.dispatch(atom.views.getView(target), commandName) 26 | } 27 | -------------------------------------------------------------------------------- /spec/very-large-file.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steelbrain/linter-ui-default/136d151c99918eb3a9040d7dd8f21b8da368ab19/spec/very-large-file.zip -------------------------------------------------------------------------------- /styles/bottom-panel.less: -------------------------------------------------------------------------------- 1 | @import (inline) '../node_modules/solid-simple-table/dist/SimpleTable.css'; 2 | @import 'ui-variables'; 3 | 4 | .solid-simple-table.linter { 5 | color: @text-color; 6 | font-size: @font-size; 7 | } 8 | 9 | .solid-simple-table.linter thead { 10 | background-color: darken(@base-background-color, 3%); 11 | user-select: none; 12 | } 13 | 14 | .solid-simple-table.linter tbody { 15 | background-color: @base-background-color; 16 | } 17 | 18 | .solid-simple-table.linter td, 19 | .solid-simple-table.linter th { 20 | padding: 3px 10px; 21 | } 22 | 23 | .solid-simple-table.linter td { 24 | min-width: 100px; 25 | border: (@font-size / 7) solid darken(@base-background-color, 3%); 26 | } 27 | 28 | .solid-simple-table.linter td:nth-child(4), 29 | .solid-simple-table.linter td:nth-child(5) { 30 | cursor: pointer; 31 | } 32 | 33 | .solid-simple-table.linter { 34 | &.error { 35 | .message-with-severity(@text-color-error); 36 | } 37 | &.warning { 38 | .message-with-severity(@text-color-warning); 39 | } 40 | &.info { 41 | .message-with-severity(@text-color-info); 42 | } 43 | 44 | // message severity function 45 | .message-with-severity(@color) { 46 | border-left: 4px solid @color; 47 | padding-left: 8px; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /styles/linter-ui.less: -------------------------------------------------------------------------------- 1 | @import 'ui-variables'; 2 | 3 | .wavy_border(@color) { 4 | background-image: linear-gradient(45deg, transparent 65%, @color 80%, transparent 90%), 5 | linear-gradient(135deg, transparent 5%, @color 15%, transparent 25%), 6 | linear-gradient(135deg, transparent 45%, @color 55%, transparent 65%), 7 | linear-gradient(45deg, transparent 25%, @color 35%, transparent 50%); 8 | background-size: 8px 2px; 9 | background-repeat: repeat-x; 10 | background-position: left bottom; 11 | } 12 | 13 | #linter-panel { 14 | overflow: auto; 15 | } 16 | 17 | #linter-tooltip { 18 | margin-top: 3px; 19 | max-width: 64em; 20 | visibility: hidden; // controlled by JavaScript 21 | padding: 5px 5px 2px 5px; 22 | font-family: @font-family; 23 | 24 | user-select: text; 25 | pointer-events: all; 26 | 27 | .linter-message { 28 | padding: 10px; 29 | 30 | color: #fff - @inset-panel-background-color; // invert; 31 | background: @app-background-color; 32 | 33 | .linter-excerpt { 34 | padding: 5px; 35 | display: flex; 36 | align-items: center; 37 | 38 | // border between messages 39 | &:not(:last-child) { 40 | border-bottom: 1px solid fade(contrast(@inset-panel-background-color), 5%); 41 | } 42 | 43 | &.error { 44 | .message-with-severity(@text-color-error); 45 | } 46 | &.warning { 47 | .message-with-severity(@text-color-warning); 48 | } 49 | &.info { 50 | .message-with-severity(@text-color-info); 51 | } 52 | 53 | // message severity function 54 | .message-with-severity(@color) { 55 | border-left: 4px solid @color; 56 | 57 | &:last-child::before { 58 | border-color: transparent transparent @color @color; 59 | } 60 | } 61 | 62 | .linter-text { 63 | margin-left: 0.5em; 64 | } 65 | 66 | .provider-name { 67 | display: inline; 68 | font-weight: bolder; 69 | } 70 | 71 | // align button with message 72 | .linter-buttons-right { 73 | margin-left: 10px; 74 | 75 | // url button 76 | a { 77 | color: @text-color; 78 | } 79 | } 80 | 81 | .fix-btn { 82 | margin-left: 5px; 83 | } 84 | } 85 | 86 | // TODO: Arrow pointer does not fit in the new design 87 | // // Arrow pointer 88 | // &:last-child::after { 89 | // content: ''; 90 | // // so the arrow does not take a row in the tooltip 91 | // display: block; 92 | // width: 0; 93 | // // position it below the tooltip 94 | // position: relative; 95 | // left: -20px; 96 | // top: 3px; 97 | // // triangle 98 | // border-top: 0 solid transparent; 99 | // border-right: 0 solid transparent; 100 | // border-bottom: 8px solid; 101 | // border-left: 8px solid transparent; 102 | // } 103 | // TODO arrow pointer for before 104 | 105 | a { 106 | font-weight: bold; 107 | } 108 | p { 109 | margin-bottom: 0; 110 | } 111 | .linter-line { 112 | display: block; 113 | } 114 | .badge { 115 | color: @text-color-highlight; 116 | } 117 | } 118 | } 119 | 120 | .linter-message { 121 | display: block; 122 | padding: 2px 5px; 123 | } 124 | 125 | .icon.linter-icon::before { 126 | font-size: 14px; 127 | } 128 | 129 | atom-text-editor.editor .linter-row { 130 | /* Take up the full allowed width */ 131 | left: 0; 132 | right: 0; 133 | /* Align the linter dot in the middle */ 134 | display: flex; 135 | align-items: center; 136 | justify-content: center; 137 | } 138 | 139 | .linter-gutter { 140 | .error_type(@color, @priority) { 141 | color: @color; 142 | z-index: @priority; 143 | } 144 | &.linter-gutter-info { 145 | .error_type(@background-color-info, -1); 146 | } 147 | &.linter-gutter-warning { 148 | .error_type(@background-color-warning, 0); 149 | } 150 | &.linter-gutter-error { 151 | .error_type(@background-color-error, 1); 152 | } 153 | } 154 | 155 | atom-text-editor.editor .linter-highlight, 156 | .linter-highlight { 157 | &.linter-info { 158 | .wavy_border(@background-color-info); 159 | } 160 | &.linter-warning { 161 | .wavy_border(@background-color-warning); 162 | } 163 | &.linter-error { 164 | .wavy_border(@background-color-error); 165 | } 166 | } 167 | 168 | .tree-view { 169 | span.name { 170 | position: relative; 171 | } 172 | .error_type(@color) { 173 | top: 0; 174 | left: 1.8rem; 175 | width: calc(~'100% - 1.8rem'); 176 | height: 100%; 177 | position: absolute; 178 | .wavy_border(@color); 179 | } 180 | .linter-info { 181 | .error_type(@background-color-info); 182 | } 183 | .linter-warning { 184 | .error_type(@background-color-warning); 185 | } 186 | .linter-error { 187 | .error_type(@background-color-error); 188 | } 189 | } 190 | .linter-gutter.icon::before { 191 | width: 100%; 192 | margin-right: 0px; 193 | font-size: 1em; 194 | } 195 | 196 | .linter-status-count { 197 | a { 198 | padding: 2px; 199 | } 200 | a::before { 201 | font-size: 12px; 202 | } 203 | &.hide-config, 204 | &.hide-pane { 205 | display: none; 206 | } 207 | } 208 | .linter-cursor-line { 209 | width: 100%; 210 | padding-left: 0; 211 | } 212 | 213 | linter-decoration { 214 | pointer-events: none; 215 | } 216 | --------------------------------------------------------------------------------