├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ └── markdownlint-issue-template.md ├── dependabot.yml ├── workflows │ ├── ci.yml │ └── checkers.yml └── dictionary.txt ├── .npmrc ├── .gitignore ├── webworker ├── module-empty.js ├── unicorn-magic-stub.js ├── process-stub.js └── os-stub.js ├── images └── markdownlint-128.png ├── jsconfig.json ├── .vscodeignore ├── test-ui ├── first-line.cjs ├── index.cjs ├── run-tests.mjs └── tests.cjs ├── .vscode └── launch.json ├── LICENSE ├── stringify-error.mjs ├── .markdownlint.json ├── test ├── metadata-test.mjs └── stringify-error-test.mjs ├── CONTRIBUTING.md ├── eslint.config.mjs ├── webpack.config.js ├── snippets.json ├── CHANGELOG.md ├── markdownlint-cli2-config-schema.json ├── package.json ├── README.md └── extension.mjs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: DavidAnson 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode-test 2 | .vscode-test-web 3 | bundle.js 4 | bundle.web.js 5 | node_modules 6 | -------------------------------------------------------------------------------- /webworker/module-empty.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | "use strict"; 4 | 5 | module.exports = {}; 6 | -------------------------------------------------------------------------------- /images/markdownlint-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DavidAnson/vscode-markdownlint/HEAD/images/markdownlint-128.png -------------------------------------------------------------------------------- /webworker/unicorn-magic-stub.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | "use strict"; 4 | 5 | module.exports = { 6 | "toPath": (path) => path 7 | }; 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | target-branch: "next" 8 | -------------------------------------------------------------------------------- /webworker/process-stub.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | "use strict"; 4 | 5 | module.exports = { 6 | "cwd": () => "/", 7 | "env": {}, 8 | "versions": { 9 | "node": "0.0" 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "target": "es2017", 5 | "checkJs": true, 6 | "resolveJsonModule": true 7 | }, 8 | "exclude": [ 9 | "node_modules" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /webworker/os-stub.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | "use strict"; 4 | 5 | module.exports = { 6 | "cpus": () => [ {} ], 7 | "platform": () => null 8 | }; 9 | 10 | // Implement setImmediate via setTimeout 11 | globalThis.setImmediate = (func) => setTimeout(func, 0); 12 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .github 2 | .vscode 3 | node_modules 4 | test 5 | test-ui 6 | webworker 7 | .eslintrc.json 8 | .gitignore 9 | .markdownlint.json 10 | .npmrc 11 | .vscodeignore 12 | CONTRIBUTING.md 13 | eslint.config.mjs 14 | extension.mjs 15 | generate-config-schema.js 16 | jsconfig.json 17 | webpack.config.js 18 | -------------------------------------------------------------------------------- /test-ui/first-line.cjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | "use strict"; 4 | 5 | module.exports = { 6 | "names": [ "first-line" ], 7 | "description": "Rule that reports an error for the first line", 8 | "tags": [ "test" ], 9 | "function": function rule (parameters, onError) { 10 | // Unconditionally report an error for line 1 11 | onError({ 12 | "lineNumber": 1 13 | }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v6 13 | - uses: actions/setup-node@v6 14 | - run: npm install --no-package-lock 15 | - run: npm test 16 | - uses: coactions/setup-xvfb@v1 17 | with: 18 | run: npm run test-ui 19 | -------------------------------------------------------------------------------- /test-ui/index.cjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | "use strict"; 4 | 5 | // This must be CommonJS or the VS Code host fails with: 6 | // Error [ERR_REQUIRE_ESM]: require() of ES Module .../index.mjs not supported. 7 | 8 | const { tests } = require("./tests.cjs"); 9 | 10 | function run () { 11 | return tests.reduce((previous, current) => previous.then(() => { 12 | // eslint-disable-next-line no-console 13 | console.log(`- ${current.name}...`); 14 | return current(); 15 | }), Promise.resolve()); 16 | } 17 | 18 | module.exports = { run }; 19 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "args": [ 9 | "--extensionDevelopmentPath=${workspaceFolder}" 10 | ] 11 | }, 12 | { 13 | "name": "Launch Web Extension", 14 | "type": "extensionHost", 15 | "debugWebWorkerHost": true, 16 | "request": "launch", 17 | "args": [ 18 | "--extensionDevelopmentPath=${workspaceFolder}", 19 | "--extensionDevelopmentKind=web" 20 | ] 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.github/dictionary.txt: -------------------------------------------------------------------------------- 1 | .markdownlintignore 2 | atx 3 | blockquote 4 | CLI 5 | CommonMark 6 | config 7 | first-line-h1 8 | formatter 9 | IntelliSense 10 | JSON 11 | JSONC 12 | LLMs 13 | macOS 14 | markdownlint 15 | markdownlint-cli2 16 | markdownlint's 17 | matcher 18 | MD\d\d\d 19 | no-bare-urls 20 | no-blanks-blockquote 21 | no-inline-html 22 | no-missing-space-atx 23 | no-missing-space-closed-atx 24 | no-multiple-space-atx 25 | no-multiple-space-blockquote 26 | no-multiple-space-closed-atx 27 | npm 28 | ol-prefix 29 | Pandoc 30 | parsers 31 | reimplement 32 | single-h1 33 | suppressions 34 | TOML 35 | UI 36 | ul-indent 37 | ul-style 38 | untrusted 39 | workspaces 40 | YAML 41 | -------------------------------------------------------------------------------- /.github/workflows/checkers.yml: -------------------------------------------------------------------------------- 1 | name: Checkers 2 | 3 | on: 4 | pull_request: 5 | push: 6 | schedule: 7 | - cron: '30 12 * * *' 8 | workflow_dispatch: 9 | 10 | jobs: 11 | 12 | linkcheck: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v6 16 | - uses: JustinBeckwith/linkinator-action@v2 17 | with: 18 | linksToSkip: '^https://github.com/ ^https://www.npmjs.com/ ^https://thisdavej.com/' 19 | paths: '*.md' 20 | 21 | spellcheck: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v6 25 | - uses: tbroadley/spellchecker-cli-action@v1 26 | with: 27 | dictionaries: '.github/dictionary.txt' 28 | files: '*.md' 29 | -------------------------------------------------------------------------------- /test-ui/run-tests.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { runTests } from "@vscode/test-electron"; 4 | 5 | // @ts-ignore 6 | const extensionDevelopmentPath = new URL("..", import.meta.url).pathname; 7 | // @ts-ignore 8 | const extensionTestsPath = new URL("./index.cjs", import.meta.url).pathname; 9 | 10 | // @ts-ignore 11 | await runTests({ 12 | extensionDevelopmentPath, 13 | extensionTestsPath, 14 | "launchArgs": [ 15 | "--disable-extensions" 16 | // Including "--extensionDevelopmentKind=web" causes the VS Code host to fail with: 17 | // TypeError: Failed to fetch 18 | ] 19 | }); 20 | // @ts-ignore 21 | await runTests({ 22 | extensionDevelopmentPath, 23 | extensionTestsPath, 24 | "launchArgs": [ 25 | "--disable-extensions", 26 | "." 27 | ] 28 | }); 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) David Anson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /stringify-error.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Regular expression for matching line endings. 5 | */ 6 | const newlineRe = /\r\n?|\n/g; 7 | 8 | /** 9 | * Converts a string to an array of indented lines. 10 | * @param {String} string String to convert. 11 | * @returns {String[]} Array of indented lines. 12 | */ 13 | function toIndentedLines (string) { 14 | return string.split(newlineRe).map((line) => ` ${line}`); 15 | } 16 | 17 | /** 18 | * String-ifies an Error (or AggregateError) object. 19 | * @param {Error} error Error object to string-ify. 20 | * @returns {String} Error details. 21 | */ 22 | function stringifyError (error) { 23 | const name = error?.name || "[NO NAME]"; 24 | const message = error?.message || JSON.stringify(error); 25 | const stack = error?.stack || "[NO STACK]"; 26 | // @ts-ignore 27 | const cause = error?.cause; 28 | // @ts-ignore 29 | const errors = error?.errors || []; 30 | const result = [ `${name}: ${message}`, "stack:" ]; 31 | const frames = stack.split(newlineRe); 32 | const discardFrame = (frames[0] === result[0]); 33 | for (const frame of frames.slice(discardFrame ? 1 : 0)) { 34 | result.push(` ${frame.trim()}`); 35 | } 36 | if (cause) { 37 | result.push("cause:"); 38 | // eslint-disable-next-line unicorn/prefer-single-call 39 | result.push(...toIndentedLines(stringifyError(cause))); 40 | } 41 | if (errors.length > 0) { 42 | result.push("errors:"); 43 | for (const subError of errors) { 44 | result.push(...toIndentedLines(stringifyError(subError))); 45 | } 46 | } 47 | return result.join("\n"); 48 | } 49 | 50 | export default stringifyError; 51 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "code-block-style": { 3 | "style": "fenced" 4 | }, 5 | "code-fence-style": { 6 | "style": "backtick" 7 | }, 8 | "emphasis-style": { 9 | "style": "asterisk" 10 | }, 11 | "fenced-code-language": { 12 | "allowed_languages": [ 13 | "ini", 14 | "json", 15 | "jsonc", 16 | "markdown" 17 | ], 18 | "language_only": true 19 | }, 20 | "heading-style": { 21 | "style": "atx" 22 | }, 23 | "hr-style": { 24 | "style": "---" 25 | }, 26 | "line-length": false, 27 | "link-image-style": { 28 | "autolink": false, 29 | "collapsed": false, 30 | "shortcut": false, 31 | "url_inline": false 32 | }, 33 | "no-duplicate-heading": { 34 | "siblings_only": true 35 | }, 36 | "ol-prefix": { 37 | "style": "ordered" 38 | }, 39 | "proper-names": { 40 | "code_blocks": false, 41 | "names": [ 42 | "CommonMark", 43 | "Ctrl", 44 | "JavaScript", 45 | "Markdown", 46 | "markdown-it", 47 | "markdownlint", 48 | "Node.js", 49 | "Shift", 50 | "Visual Studio Code" 51 | ] 52 | }, 53 | "required-headings": { 54 | "headings": [ 55 | "# markdownlint", 56 | "## Introduction", 57 | "## Install", 58 | "## Use", 59 | "## Rules", 60 | "## Commands", 61 | "### Fix", 62 | "### Workspace", 63 | "### Disable", 64 | "## Configure", 65 | "### markdownlint.config", 66 | "### markdownlint.configFile", 67 | "### markdownlint.focusMode", 68 | "### markdownlint.run", 69 | "### markdownlint.severityForError", 70 | "### markdownlint.severityForWarning", 71 | "### markdownlint.customRules", 72 | "### markdownlint.lintWorkspaceGlobs", 73 | "## Suppress", 74 | "## Snippets", 75 | "## Security", 76 | "## History" 77 | ] 78 | }, 79 | "strong-style": { 80 | "style": "asterisk" 81 | }, 82 | "table-column-style": { 83 | "style": "aligned" 84 | }, 85 | "table-pipe-style": { 86 | "style": "leading_and_trailing" 87 | }, 88 | "ul-style": { 89 | "style": "asterisk" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test/metadata-test.mjs: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import path from "node:path"; 3 | import { describe, test } from "node:test"; 4 | import { fileURLToPath } from "node:url"; 5 | 6 | /** 7 | * Imports a file as JSON. 8 | * Avoids "ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time". 9 | * @param {ImportMeta} meta ESM import.meta object. 10 | * @param {string} file JSON file to import. 11 | * @returns {Promise} JSON object. 12 | */ 13 | const importWithTypeJson = async (meta, file) => ( 14 | // @ts-ignore 15 | JSON.parse(await fs.readFile(path.resolve(path.dirname(fileURLToPath(meta.url)), file))) 16 | ); 17 | 18 | const packageJson = await importWithTypeJson(import.meta, "../package.json"); 19 | const markdownlintPackageJson = await importWithTypeJson(import.meta, "../node_modules/markdownlint/package.json"); 20 | 21 | describe("metadata", () => { 22 | 23 | test("version numbers match", async (t) => { 24 | t.plan(184); 25 | const files = [ 26 | "./package.json", 27 | "./CHANGELOG.md", 28 | "./README.md", 29 | "./markdownlint-cli2-config-schema.json", 30 | "./markdownlint-config-schema.json" 31 | ]; 32 | const packages = [ 33 | // eslint-disable-next-line prefer-named-capture-group 34 | [ packageJson.dependencies["markdownlint-cli2"], /(?:DavidAnson\/markdownlint-cli2|markdownlint-cli2\/blob)\/v(\d+\.\d+\.\d+)/gu ], 35 | // eslint-disable-next-line prefer-named-capture-group 36 | [ markdownlintPackageJson.version, /(?:DavidAnson\/markdownlint|markdownlint\/blob)\/v(\d+\.\d+\.\d+)/gu ] 37 | ]; 38 | const contents = await Promise.all(files.map((file) => fs.readFile(file, "utf8"))); 39 | for (const content of contents) { 40 | // eslint-disable-next-line init-declarations 41 | let match; 42 | for (const [version, githubProjectOrFileRe] of packages) { 43 | while ((match = githubProjectOrFileRe.exec(content)) !== null) { 44 | t.assert.equal(match[1], version); 45 | } 46 | } 47 | // eslint-disable-next-line prefer-named-capture-group 48 | const firstChangelogRe = /\* (\d+\.\d+\.\d+) - /u; 49 | match = firstChangelogRe.exec(content); 50 | if (match) { 51 | const patchRe = /\.\d+$/u; 52 | t.assert.equal(match[1].replace(patchRe, ""), packageJson.version.replace(patchRe, "")); 53 | } 54 | } 55 | }); 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/markdownlint-issue-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: markdownlint issue template 3 | about: This template helps report issues with the markdownlint family of tools. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Interested in contributing? Great! Here are some suggestions to make it a good experience: 4 | 5 | Start by [opening an issue][issues], whether to identify a problem or outline a change. 6 | That issue should be used to discuss the situation and agree on a plan of action before writing code or sending a pull request. 7 | Maybe the problem isn't really a problem, or maybe there are more things to consider. 8 | If so, it's best to realize that before spending time and effort writing code that may not get used. 9 | 10 | Match the coding style of the files you edit. 11 | Although everyone has their own preferences and opinions, a pull request is not the right forum to debate them. 12 | 13 | Package versions for `dependencies` and `devDependencies` should be specified exactly (also known as "pinning"). 14 | The short explanation is that doing otherwise eventually leads to inconsistent behavior and broken functionality. 15 | (See [Why I pin dependency versions in Node.js packages][version-pinning] for a longer explanation.) 16 | 17 | Run basic tests via `npm test`. 18 | Run UI tests via `npm run test-ui`. 19 | Lint by running `npm run lint`. 20 | 21 | Pull requests should contain a single commit that addresses a single issue. 22 | If necessary, squash multiple commits before creating the pull request and when making changes. 23 | (See [Git Tools - Rewriting History][rewriting-history] for details.) 24 | 25 | Open pull requests against the `next` branch. 26 | That's where the latest changes are staged for the next release. 27 | Include the text "(fixes #??)" at the end of the commit message so the pull request will be associated with the relevant issue. 28 | End commit messages with a period (`.`). 29 | Once accepted, the tag `fixed in next` will be added to the issue. 30 | When the commit is merged to the main branch during the release process, the issue will be closed automatically. 31 | (See [Closing issues using keywords][closing-keywords] for details.) 32 | 33 | In order to maintain the permissive MIT license this project uses, all contributions must be your own and released under that license. 34 | Code you add should be an original work and should not be copied from elsewhere. 35 | Taking code from a different project, Stack Overflow, or the like is not allowed. 36 | The use of tools such as GitHub Copilot, ChatGPT, LLMs (large language models), etc. that incorporate code from other projects is not allowed. 37 | 38 | Thank you! 39 | 40 | [closing-keywords]: https://help.github.com/articles/closing-issues-using-keywords/ 41 | [issues]: https://github.com/DavidAnson/vscode-markdownlint/issues 42 | [rewriting-history]: https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History 43 | [version-pinning]: https://dlaa.me/blog/post/versionpinning 44 | 45 | 46 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable n/no-unpublished-import */ 2 | 3 | import js from "@eslint/js"; 4 | import eslintPluginN from "eslint-plugin-n"; 5 | import eslintPluginStylistic from "@stylistic/eslint-plugin"; 6 | import eslintPluginUnicorn from "eslint-plugin-unicorn"; 7 | 8 | export default [ 9 | js.configs.all, 10 | eslintPluginN.configs["flat/recommended"], 11 | eslintPluginStylistic.configs.customize({ 12 | "arrowParens": true, 13 | "braceStyle": "1tbs", 14 | "commaDangle": "never", 15 | "indent": "tab", 16 | "jsx": false, 17 | "quoteProps": "always", 18 | "quotes": "double", 19 | "semi": true 20 | }), 21 | eslintPluginUnicorn.configs["flat/all"], 22 | { 23 | "languageOptions": { 24 | "sourceType": "commonjs" 25 | }, 26 | "linterOptions": { 27 | "reportUnusedDisableDirectives": true 28 | }, 29 | "rules": { 30 | "array-bracket-spacing": [ "error", "always" ], 31 | "dot-location": [ "error", "property" ], 32 | "func-style": [ "error", "declaration" ], 33 | "function-call-argument-newline": [ "error", "consistent" ], 34 | "function-paren-newline": [ "error", "consistent" ], 35 | "global-require": "off", 36 | "indent": [ "error", "tab" ], 37 | "linebreak-style": "off", 38 | "max-classes-per-file": "off", 39 | "max-depth": [ "error", 6 ], 40 | "max-len": [ "error", 130 ], 41 | "max-lines": "off", 42 | "max-lines-per-function": "off", 43 | "max-statements": "off", 44 | "multiline-comment-style": [ "error", "separate-lines" ], 45 | "no-extra-parens": "off", 46 | "no-magic-numbers": "off", 47 | "no-plusplus": "off", 48 | "no-promise-executor-return": "off", 49 | "no-sync": "off", 50 | "no-tabs": "off", 51 | "no-ternary": "off", 52 | "no-undefined": "off", 53 | "no-use-before-define": [ "error", { "functions": false } ], 54 | "one-var": "off", 55 | "operator-linebreak": [ "error", "after" ], 56 | "padded-blocks": "off", 57 | "prefer-destructuring": "off", 58 | "prefer-named-capture-group": "off", 59 | "prefer-template": "off", 60 | "require-unicode-regexp": "off", 61 | "sort-imports": "off", 62 | "sort-keys": "off", 63 | 64 | "n/no-missing-require": [ "error", { "allowModules": [ "vscode" ] } ], 65 | 66 | "@stylistic/array-bracket-spacing": [ "error", "always" ], 67 | "@stylistic/indent": [ "error", "tab", { "ObjectExpression": "first" } ], 68 | "@stylistic/operator-linebreak": [ "error", "after" ], 69 | "@stylistic/space-before-function-paren": [ "error", "always" ], 70 | 71 | "unicorn/no-array-push-push": "off", 72 | "unicorn/no-array-reduce": "off", 73 | "unicorn/no-null": "off", 74 | "unicorn/prefer-module": "off", 75 | "unicorn/prefer-string-replace-all": "off" 76 | } 77 | }, 78 | { 79 | "files": [ 80 | "**/*.mjs" 81 | ], 82 | "languageOptions": { 83 | "sourceType": "module" 84 | } 85 | } 86 | ]; 87 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | "use strict"; 4 | 5 | /* eslint-disable n/no-unpublished-require */ 6 | 7 | const webpack = require("webpack"); 8 | const TerserPlugin = require("terser-webpack-plugin"); 9 | 10 | const nodeModulePrefixRe = /^node:/u; 11 | const baseConfig = { 12 | "target": "node", 13 | "entry": "./extension.mjs", 14 | "output": { 15 | "asyncChunks": false, 16 | "path": __dirname, 17 | "filename": "bundle.js", 18 | "libraryTarget": "commonjs2" 19 | }, 20 | "optimization": { 21 | // "minimize": false, 22 | // "moduleIds": "named", 23 | "minimizer": [ new TerserPlugin({ "extractComments": false }) ] 24 | }, 25 | "externals": { 26 | "vscode": "commonjs vscode" 27 | }, 28 | "plugins": [ 29 | // Rewrite requires to remove "node:" prefix 30 | new webpack.NormalModuleReplacementPlugin( 31 | nodeModulePrefixRe, 32 | (resource) => { 33 | const module = resource.request.replace(nodeModulePrefixRe, ""); 34 | resource.request = module; 35 | } 36 | ) 37 | ], 38 | "ignoreWarnings": [ 39 | { 40 | "message": /(asset|entrypoint) size limit/ 41 | }, 42 | { 43 | "message": /dependencies cannot be statically extracted/ 44 | }, 45 | { 46 | "message": /lazy load some parts of your application/ 47 | } 48 | ] 49 | }; 50 | const config = [ 51 | baseConfig, 52 | { 53 | ...baseConfig, 54 | "target": "webworker", 55 | "output": { 56 | ...baseConfig.output, 57 | "filename": "bundle.web.js", 58 | "libraryTarget": "commonjs" 59 | }, 60 | "plugins": [ 61 | ...baseConfig.plugins, 62 | // Intercept "markdown-it" to provide empty implementation 63 | new webpack.NormalModuleReplacementPlugin( 64 | /^markdown-it$/u, 65 | (resource) => { 66 | resource.request = require.resolve("./webworker/module-empty.js"); 67 | } 68 | ), 69 | // Intercept "node:stream/consumers" and "node:stream/promises" lacking a browserify entry 70 | new webpack.NormalModuleReplacementPlugin( 71 | /^stream\/(?:consumers|promises)$/u, 72 | (resource) => { 73 | resource.request = require.resolve("./webworker/module-empty.js"); 74 | } 75 | ), 76 | // Intercept existing "unicorn-magic" package to provide missing import 77 | new webpack.NormalModuleReplacementPlugin( 78 | /^unicorn-magic$/u, 79 | (resource) => { 80 | resource.request = require.resolve("./webworker/unicorn-magic-stub.js"); 81 | } 82 | ), 83 | // Intercept use of "process" to provide implementation 84 | new webpack.ProvidePlugin({ 85 | "process": "process-wrapper" 86 | }) 87 | ], 88 | "resolve": { 89 | "conditionNames": [ "markdownlint-imports-node", "..." ], 90 | "fallback": { 91 | "fs": false, 92 | "os": require.resolve("./webworker/os-stub.js"), 93 | "path": require.resolve("path-browserify"), 94 | "process": require.resolve("./webworker/process-stub.js"), 95 | "process-wrapper": require.resolve("./webworker/process-stub.js"), 96 | "stream": require.resolve("stream-browserify"), 97 | "url": require.resolve("./webworker/module-empty.js"), 98 | "util": require.resolve("util") 99 | } 100 | } 101 | } 102 | ]; 103 | module.exports = config; 104 | -------------------------------------------------------------------------------- /snippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "insertMarkdownLintDisableComment": { 3 | "prefix": "markdownlint-disable", 4 | "body": "", 5 | "description": "Disables one or more rules by name (MD013), alias (line-length), or tag (whitespace). Multiple rules are space-delimited (MD018 MD019). If no rules are specified, all rules are disabled. Takes effect starting with the line the comment is on." 6 | }, 7 | "insertMarkdownLintEnableComment": { 8 | "prefix": "markdownlint-enable", 9 | "body": "", 10 | "description": "Enables one or more rules by name (MD013), alias (line-length), or tag (whitespace). Multiple rules are space-delimited (MD018 MD019). If no rules are specified, all rules are enabled. Takes effect starting with the line the comment is on." 11 | }, 12 | 13 | "insertMarkdownLintDisableFileComment": { 14 | "prefix": "markdownlint-disable-file", 15 | "body": "", 16 | "description": "Disables one or more rules by name (MD013), alias (line-length), or tag (whitespace). Multiple rules are space-delimited (MD018 MD019). If no rules are specified, all rules are disabled. Applies to the entire file." 17 | }, 18 | "insertMarkdownLintEnableFileComment": { 19 | "prefix": "markdownlint-enable-file", 20 | "body": "", 21 | "description": "Enables one or more rules by name (MD013), alias (line-length), or tag (whitespace). Multiple rules are space-delimited (MD018 MD019). If no rules are specified, all rules are enabled. Applies to the entire file." 22 | }, 23 | 24 | "insertMarkdownLintDisableLineComment": { 25 | "prefix": "markdownlint-disable-line", 26 | "body": "", 27 | "description": "Disables one or more rules by name (MD013), alias (line-length), or tag (whitespace). Multiple rules are space-delimited (MD018 MD019). If no rules are specified, all rules are disabled. Applies to the current line only." 28 | }, 29 | "insertMarkdownLintDisableNextLineComment": { 30 | "prefix": "markdownlint-disable-next-line", 31 | "body": "", 32 | "description": "Disables one or more rules by name (MD013), alias (line-length), or tag (whitespace). Multiple rules are space-delimited (MD018 MD019). If no rules are specified, all rules are disabled. Applies to the next line only." 33 | }, 34 | 35 | "insertMarkdownLintCaptureComment": { 36 | "prefix": "markdownlint-capture", 37 | "body": "", 38 | "description": "Captures the current rule configuration. Takes effect starting with the line the comment is on." 39 | }, 40 | "insertMarkdownLintRestoreComment": { 41 | "prefix": "markdownlint-restore", 42 | "body": "", 43 | "description": "Restores the most recently captured rule configuration. Defaults to the document's initial configuration. Takes effect starting with the line the comment is on." 44 | }, 45 | 46 | "insertMarkdownLintConfigureFileComment": { 47 | "prefix": "markdownlint-configure-file", 48 | "body": "", 49 | "description": "Configures one or more rules by name (MD013), alias (line-length), or tag (whitespace) using the same JSON format as the \"markdownlint.config\" object. Applies to the entire file." 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | * 0.61.0 - Improved rules, add warnings, add `severityForError`/`Warning` 4 | * 0.60.0 - Improved rules 5 | * 0.59.0 - Add `configFile` setting 6 | * 0.58.0 - Convert to ECMAScript, use import (not require) 7 | * 0.57.0 - Improved rules 8 | * 0.56.0 - Improved rules, removed `markdownlint.ignore` 9 | * 0.55.0 - Improved rules, deprecated `markdownlint.ignore` 10 | * 0.54.0 - Improved rules, JSONC handling 11 | * 0.53.0 - Improved rules, removed deprecated rules/aliases 12 | * 0.52.0 - Improved rules, performance 13 | * 0.51.0 - Improved rules, better multi-root workspace handling 14 | * 0.50.0 - Improved rules 15 | * 0.49.0 - Improved rules and documentation 16 | * 0.48.0 - New rules, better linting in browser 17 | * 0.47.0 - Miscellaneous improvements 18 | * 0.46.0 - New rules, document formatting, better path handling 19 | * 0.45.0 - Add linting for workspace 20 | * 0.44.0 - Support virtual file systems 21 | * 0.43.0 - Web worker support 22 | * 0.42.0 - Add `focusMode` setting 23 | * 0.41.0 - Workspace trust, virtual workspaces 24 | * 0.40.0 - Switched to `markdownlint-cli2` (see below) 25 | * 0.39.0 - Improved rules, new fix 26 | * 0.38.0 - Improved rules, schemas, new snippet 27 | * 0.37.0 - Improved rules, extends, schema 28 | * 0.36.0 - Add command, improve diagnostics 29 | * 0.35.0 - Improved rules, .markdownlintignore 30 | * 0.34.0 - Improved rules, new fix, JSONC config 31 | * 0.33.0 - Improved rules, new fix, new snippets 32 | * 0.32.0 - Implement fix-on-save 33 | * 0.31.0 - Automatically fix more rule violations 34 | * 0.30.0 - Improved rules, startup performance 35 | * 0.29.0 - Add snippets 36 | * 0.28.0 - Improved rules, capture/restore 37 | * 0.27.0 - Improved rules and custom rule handling 38 | * 0.26.0 - Rule, code action, and performance improvements 39 | * 0.25.0 - Improved startup performance 40 | * 0.24.0 - Improved rules, custom links, math blocks 41 | * 0.23.0 - UI and custom rule improvements 42 | * 0.22.0 - Code action improvements 43 | * 0.21.0 - Custom rule improvements 44 | * 0.20.0 - Enable actions in Problems pane 45 | * 0.19.0 - Improved rules 46 | * 0.18.0 - Add support for YAML config files 47 | * 0.17.0 - Add support for ignoring files 48 | * 0.16.0 - Add support for custom rules 49 | * 0.15.0 - Add option to lint on type or save 50 | * 0.14.0 - Improved rules 51 | * 0.13.0 - New/improved rules 52 | * 0.12.0 - Add support for multi-root workspaces 53 | * 0.11.0 - Automatically fix some rule violations 54 | * 0.10.0 - Ignore comments/TOML, improved rules 55 | * 0.9.0 - Nested .markdownlint.json config 56 | * 0.8.0 - Improved rules, shareable config 57 | * 0.7.0 - New/improved rules, more details 58 | * 0.6.0 - Improved rules and underlining 59 | * 0.5.0 - New/improved rules, schema for settings 60 | * 0.4.0 - Throttle linting 61 | * 0.3.0 - Focused underlining 62 | * 0.2.0 - Custom configuration 63 | * 0.1.0 - Initial release 64 | 65 | ## About release 0.40.0 66 | 67 | Originally, this extension called into the [`markdownlint`](https://github.com/DavidAnson/markdownlint) library and needed to reimplement support for shared concepts like configuration files, etc.. With this release, the extension calls into [`markdownlint-cli2`](https://github.com/DavidAnson/markdownlint-cli2) to perform linting. This brings complete support for everything that tool supports, including the powerful `.markdownlint-cli2.{jsonc,yaml,js}` format that allows configuration inheritance along with different rules, custom rules, and [`markdown-it` plugins](https://www.npmjs.com/search?q=keywords:markdown-it-plugin) for each directory. This change means a few things behave differently or are no longer supported: 68 | 69 | * The default configuration (to disable `MD013`/`line-length`) is allowed to propagate into an open project even if that project has its own configuration. (This was partially true before.) To change behavior within a project (e.g., for complete consistency with CLI behavior) do so explicitly with any of the configuration mechanisms (for example, via `"line-length": true`). 70 | * The `.markdownlintrc` configuration file is no longer used. This format was supported for consistency with [`markdownlint-cli`](https://github.com/igorshubovych/markdownlint-cli) and was only partially implemented. Any of the configuration file formats used by `markdownlint-cli2` can be used instead. 71 | * The "Open this document's configuration" action is no longer present in VS Code. This feature attempted to determine the single configuration source for the current file, but now that configuration inheritance is supported, there may no longer be a single configuration file. 72 | * The `markdownlint.customRulesAlwaysAllow` setting is no longer used because custom rules (along with `markdown-it` plugins, `.markdownlint.cjs`, and `.markdownlint-cli2.cjs`) are automatically loaded by `markdownlint-cli2`. To prevent running external JavaScript like this, set the new `markdownlint.blockJavaScript` configuration to `true` in VS Code's user settings. 73 | * Custom rule paths specified by the `markdownlint.customRules` setting must begin with `./` when they are relative paths (this was previously optional). 74 | 75 | 76 | -------------------------------------------------------------------------------- /markdownlint-cli2-config-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://raw.githubusercontent.com/DavidAnson/markdownlint-cli2/v0.20.0/schema/markdownlint-cli2-config-schema.json", 4 | "title": "markdownlint-cli2 configuration schema", 5 | "type": "object", 6 | "properties": { 7 | "$schema": { 8 | "description": "JSON Schema URI (expected by some editors)", 9 | "type": "string", 10 | "default": "https://raw.githubusercontent.com/DavidAnson/markdownlint-cli2/v0.20.0/schema/markdownlint-cli2-config-schema.json" 11 | }, 12 | "config": { 13 | "description": "markdownlint configuration schema : https://github.com/DavidAnson/markdownlint/blob/v0.40.0/schema/.markdownlint.jsonc", 14 | "$ref": "https://raw.githubusercontent.com/DavidAnson/markdownlint/v0.40.0/schema/markdownlint-config-schema.json", 15 | "default": {} 16 | }, 17 | "customRules": { 18 | "description": "Module names or paths of custom rules to load and use when linting : https://github.com/DavidAnson/markdownlint-cli2/blob/v0.20.0/README.md#markdownlint-cli2jsonc", 19 | "type": "array", 20 | "default": [], 21 | "items": { 22 | "description": "Module name or path of a custom rule", 23 | "type": "string", 24 | "minLength": 1 25 | } 26 | }, 27 | "fix": { 28 | "description": "Whether to enable fixing of linting errors reported by rules that emit fix information : https://github.com/DavidAnson/markdownlint-cli2/blob/v0.20.0/README.md#markdownlint-cli2jsonc", 29 | "type": "boolean", 30 | "default": false 31 | }, 32 | "frontMatter": { 33 | "description": "Regular expression used to match and ignore any front matter at the beginning of a document : https://github.com/DavidAnson/markdownlint-cli2/blob/v0.20.0/README.md#markdownlint-cli2jsonc", 34 | "type": "string", 35 | "minLength": 1, 36 | "default": "" 37 | }, 38 | "gitignore": { 39 | "description": "Whether to ignore files referenced by .gitignore (or glob expression) (only valid at the root) : https://github.com/DavidAnson/markdownlint-cli2/blob/v0.20.0/README.md#markdownlint-cli2jsonc", 40 | "type": [ 41 | "boolean", 42 | "string" 43 | ], 44 | "default": false 45 | }, 46 | "globs": { 47 | "description": "Glob expressions to include when linting (only valid at the root) : https://github.com/DavidAnson/markdownlint-cli2/blob/v0.20.0/README.md#markdownlint-cli2jsonc", 48 | "type": "array", 49 | "default": [], 50 | "items": { 51 | "description": "Glob expression of files to lint", 52 | "type": "string", 53 | "minLength": 1 54 | } 55 | }, 56 | "ignores": { 57 | "description": "Glob expressions to ignore when linting : https://github.com/DavidAnson/markdownlint-cli2/blob/v0.20.0/README.md#markdownlint-cli2jsonc", 58 | "type": "array", 59 | "default": [], 60 | "items": { 61 | "description": "Glob expression of files to ignore", 62 | "type": "string", 63 | "minLength": 1 64 | } 65 | }, 66 | "markdownItPlugins": { 67 | "description": "markdown-it plugins to load and use when linting : https://github.com/DavidAnson/markdownlint-cli2/blob/v0.20.0/README.md#markdownlint-cli2jsonc", 68 | "type": "array", 69 | "default": [], 70 | "items": { 71 | "description": "Name or path of a markdown-it plugin followed by parameters", 72 | "type": "array", 73 | "items": [ 74 | { 75 | "description": "Name or path of a markdown-it plugin", 76 | "type": "string", 77 | "minLength": 1 78 | }, 79 | { 80 | "description": "Parameter(s) to pass to the markdown-it plugin" 81 | } 82 | ], 83 | "minItems": 1 84 | } 85 | }, 86 | "modulePaths": { 87 | "description": "Additional paths to resolve module locations from : https://github.com/DavidAnson/markdownlint-cli2/blob/v0.20.0/README.md#markdownlint-cli2jsonc", 88 | "type": "array", 89 | "default": [], 90 | "items": { 91 | "description": "Path to resolve module locations from", 92 | "type": "string", 93 | "minLength": 1 94 | } 95 | }, 96 | "noBanner": { 97 | "description": "Whether to disable the display of the banner message and version numbers on stdout (only valid at the root) : https://github.com/DavidAnson/markdownlint-cli2/blob/v0.20.0/README.md#markdownlint-cli2jsonc", 98 | "type": "boolean", 99 | "default": false 100 | }, 101 | "noInlineConfig": { 102 | "description": "Whether to disable support of HTML comments within Markdown content : https://github.com/DavidAnson/markdownlint-cli2/blob/v0.20.0/README.md#markdownlint-cli2jsonc", 103 | "type": "boolean", 104 | "default": false 105 | }, 106 | "noProgress": { 107 | "description": "Whether to disable the display of progress on stdout (only valid at the root) : https://github.com/DavidAnson/markdownlint-cli2/blob/v0.20.0/README.md#markdownlint-cli2jsonc", 108 | "type": "boolean", 109 | "default": false 110 | }, 111 | "outputFormatters": { 112 | "description": "Output formatters to load and use to customize markdownlint-cli2 output (only valid at the root) : https://github.com/DavidAnson/markdownlint-cli2/blob/v0.20.0/README.md#markdownlint-cli2jsonc", 113 | "type": "array", 114 | "default": [], 115 | "items": { 116 | "description": "Name or path of an output formatter followed by parameters", 117 | "type": "array", 118 | "items": [ 119 | { 120 | "description": "Name or path of an output formatter", 121 | "type": "string", 122 | "minLength": 1 123 | }, 124 | { 125 | "description": "Parameter(s) to pass to the output formatter" 126 | } 127 | ], 128 | "minItems": 1 129 | } 130 | }, 131 | "showFound": { 132 | "description": "Whether to show the list of found files on stdout (only valid at the root) : https://github.com/DavidAnson/markdownlint-cli2/blob/v0.20.0/README.md#markdownlint-cli2jsonc", 133 | "type": "boolean", 134 | "default": false 135 | } 136 | }, 137 | "additionalProperties": false 138 | } 139 | -------------------------------------------------------------------------------- /test/stringify-error-test.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { describe, test } from "node:test"; 4 | import { default as stringifyError } from "../stringify-error.mjs"; 5 | 6 | describe("stringify-error", () => { 7 | 8 | test("null", (t) => { 9 | t.plan(1); 10 | const error = null; 11 | const actual = stringifyError(error); 12 | const expected = "[NO NAME]: null\nstack:\n [NO STACK]"; 13 | t.assert.equal(actual, expected); 14 | }); 15 | 16 | test("number", (t) => { 17 | t.plan(1); 18 | const error = 10; 19 | // @ts-ignore 20 | const actual = stringifyError(error); 21 | const expected = `[NO NAME]: 10\nstack:\n [NO STACK]`; 22 | t.assert.equal(actual, expected); 23 | }); 24 | 25 | test("string", (t) => { 26 | t.plan(1); 27 | const error = "STRING"; 28 | // @ts-ignore 29 | const actual = stringifyError(error); 30 | const expected = `[NO NAME]: "STRING"\nstack:\n [NO STACK]`; 31 | t.assert.equal(actual, expected); 32 | }); 33 | 34 | test("object", (t) => { 35 | t.plan(1); 36 | const error = {}; 37 | // @ts-ignore 38 | const actual = stringifyError(error); 39 | const expected = `[NO NAME]: {}\nstack:\n [NO STACK]`; 40 | t.assert.equal(actual, expected); 41 | }); 42 | 43 | test("Error-like, name", (t) => { 44 | t.plan(1); 45 | const error = { 46 | "name": "NAME" 47 | }; 48 | // @ts-ignore 49 | const actual = stringifyError(error); 50 | const expected = `NAME: {"name":"NAME"}\nstack:\n [NO STACK]`; 51 | t.assert.equal(actual, expected); 52 | }); 53 | 54 | test("Error-like, name/message", (t) => { 55 | t.plan(1); 56 | const error = { 57 | "name": "NAME", 58 | "message": "MESSAGE" 59 | }; 60 | const actual = stringifyError(error); 61 | const expected = `NAME: MESSAGE\nstack:\n [NO STACK]`; 62 | t.assert.equal(actual, expected); 63 | }); 64 | 65 | test("Error-like, name/message/stack", (t) => { 66 | t.plan(1); 67 | const error = { 68 | "name": "NAME", 69 | "message": "MESSAGE", 70 | "stack": "STACK0\nSTACK1\nSTACK2" 71 | }; 72 | const actual = stringifyError(error); 73 | const expected = `NAME: MESSAGE\nstack:\n STACK0\n STACK1\n STACK2`; 74 | t.assert.equal(actual, expected); 75 | }); 76 | 77 | test("Error-like, name/message/stack-with-heading", (t) => { 78 | t.plan(1); 79 | const error = { 80 | "name": "NAME", 81 | "message": "MESSAGE", 82 | "stack": "NAME: MESSAGE\nSTACK0\nSTACK1\nSTACK2" 83 | }; 84 | const actual = stringifyError(error); 85 | const expected = `NAME: MESSAGE\nstack:\n STACK0\n STACK1\n STACK2`; 86 | t.assert.equal(actual, expected); 87 | }); 88 | 89 | test("Error-like, name/message/cause", (t) => { 90 | t.plan(1); 91 | const error = { 92 | "name": "NAME", 93 | "message": "MESSAGE", 94 | "cause": { 95 | "name": "name", 96 | "message": "message" 97 | } 98 | }; 99 | const actual = stringifyError(error); 100 | const expected = `NAME: MESSAGE\nstack:\n [NO STACK]\ncause:\n name: message\n stack:\n [NO STACK]`; 101 | t.assert.equal(actual, expected); 102 | }); 103 | 104 | test("Error-like, name/message/errors-none", (t) => { 105 | t.plan(1); 106 | const error = { 107 | "name": "NAME", 108 | "message": "MESSAGE", 109 | "errors": [] 110 | }; 111 | const actual = stringifyError(error); 112 | const expected = `NAME: MESSAGE\nstack:\n [NO STACK]`; 113 | t.assert.equal(actual, expected); 114 | }); 115 | 116 | test("Error-like, name/message/errors-one", (t) => { 117 | t.plan(1); 118 | const error = { 119 | "name": "NAME", 120 | "message": "MESSAGE", 121 | "errors": [ 122 | { 123 | "name": "name", 124 | "message": "message" 125 | } 126 | ] 127 | }; 128 | const actual = stringifyError(error); 129 | const expected = `NAME: MESSAGE\nstack:\n [NO STACK]\nerrors:\n name: message\n stack:\n [NO STACK]`; 130 | t.assert.equal(actual, expected); 131 | }); 132 | 133 | test("Error-like, name/message/errors-two", (t) => { 134 | t.plan(1); 135 | const error = { 136 | "name": "NAME", 137 | "message": "MESSAGE", 138 | "errors": [ 139 | { 140 | "name": "name", 141 | "message": "message" 142 | }, 143 | "string" 144 | ] 145 | }; 146 | const actual = stringifyError(error); 147 | const expected = `NAME: MESSAGE\nstack:\n [NO STACK]\nerrors:\n name: message\n stack:\n [NO STACK]\n [NO NAME]: "string"\n stack:\n [NO STACK]`; 148 | t.assert.equal(actual, expected); 149 | }); 150 | 151 | test("Error-like, name/message/errors-nested", (t) => { 152 | t.plan(1); 153 | const error = { 154 | "name": "NAME", 155 | "message": "MESSAGE", 156 | "errors": [ 157 | { 158 | "name": "name", 159 | "message": "message", 160 | "cause": { 161 | "name": "Name", 162 | "message": "Message", 163 | }, 164 | "errors": [ 165 | { 166 | "name": "NaMe", 167 | "message": "MeSsAgE", 168 | } 169 | ] 170 | } 171 | ] 172 | }; 173 | const actual = stringifyError(error); 174 | const expected = `NAME: MESSAGE\nstack:\n [NO STACK]\nerrors:\n name: message\n stack:\n [NO STACK]\n cause:\n Name: Message\n stack:\n [NO STACK]\n errors:\n NaMe: MeSsAgE\n stack:\n [NO STACK]`; 175 | t.assert.equal(actual, expected); 176 | }); 177 | 178 | function generalize (output) { 179 | return output. 180 | split("\n"). 181 | filter((s) => !s.includes("at ") || s.includes("stringify-error-test")). 182 | map((s) => s.replace(/^(\s*at ).*(stringify-error-test).*$/, "$1$2")). 183 | join("\n"); 184 | } 185 | 186 | test("Error, cause", (t) => { 187 | t.plan(1); 188 | // @ts-ignore 189 | const error = new Error("MESSAGE0", { "cause": new Error("MESSAGE1") } ); 190 | const actual = generalize(stringifyError(error)); 191 | const expected = "Error: MESSAGE0\nstack:\n at stringify-error-test\ncause:\n Error: MESSAGE1\n stack:\n at stringify-error-test"; 192 | t.assert.equal(actual, expected); 193 | }); 194 | 195 | test("AggregateError, nested", (t) => { 196 | t.plan(1); 197 | // @ts-ignore 198 | const error = new AggregateError([ new Error("MESSAGE1"), new AggregateError([ new Error("MESSAGE3") ], "MESSAGE2") ], "MESSAGE0"); 199 | const actual = generalize(stringifyError(error)); 200 | const expected = "AggregateError: MESSAGE0\nstack:\n at stringify-error-test\nerrors:\n Error: MESSAGE1\n stack:\n at stringify-error-test\n AggregateError: MESSAGE2\n stack:\n at stringify-error-test\n errors:\n Error: MESSAGE3\n stack:\n at stringify-error-test"; 201 | t.assert.equal(actual, expected); 202 | }); 203 | 204 | }); 205 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-markdownlint", 3 | "displayName": "markdownlint", 4 | "description": "Markdown linting and style checking for Visual Studio Code", 5 | "icon": "images/markdownlint-128.png", 6 | "version": "0.61.1", 7 | "author": "David Anson (https://dlaa.me/)", 8 | "publisher": "DavidAnson", 9 | "sponsor": { 10 | "url": "https://github.com/sponsors/DavidAnson" 11 | }, 12 | "license": "MIT", 13 | "homepage": "https://github.com/DavidAnson/vscode-markdownlint", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/DavidAnson/vscode-markdownlint.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/DavidAnson/vscode-markdownlint/issues" 20 | }, 21 | "funding": "https://github.com/sponsors/DavidAnson", 22 | "scripts": { 23 | "ci": "npm test && npm run test-ui", 24 | "compile": "webpack --mode production", 25 | "compile-debug": "webpack --mode none", 26 | "lint": "eslint --ignore-pattern bundle.js --ignore-pattern bundle.web.js --no-warn-ignored *.mjs *.js test-ui/*.cjs eslint.config.mjs && markdownlint-cli2 *.md", 27 | "schema": "cpy ./node_modules/markdownlint/schema/markdownlint-config-schema.json . --flat && cpy ./node_modules/markdownlint-cli2/schema/markdownlint-cli2-config-schema.json . --flat", 28 | "test": "node --test --experimental-test-coverage && npm run lint && npm run compile && npm run schema && git diff --exit-code", 29 | "test-ui": "node ./test-ui/run-tests.mjs", 30 | "test-web": "npm install --no-save @vscode/test-web && sed -i '.bak' -e 's/\\/{{uuid}}\\./\\//' node_modules/@vscode/test-web/out/server/workbench.js && vscode-test-web --browser=none --verbose --extensionDevelopmentPath=. .", 31 | "upgrade": "npx --yes npm-check-updates --upgrade" 32 | }, 33 | "categories": [ 34 | "Linters", 35 | "Formatters" 36 | ], 37 | "engines": { 38 | "vscode": "^1.97.0" 39 | }, 40 | "main": "./bundle.js", 41 | "browser": "./bundle.web.js", 42 | "dependencies": { 43 | "markdownlint-cli2": "0.20.0" 44 | }, 45 | "devDependencies": { 46 | "@eslint/js": "9.39.1", 47 | "@stylistic/eslint-plugin": "5.6.1", 48 | "@types/vscode": "1.97.0", 49 | "@vscode/test-electron": "2.5.2", 50 | "@vscode/vsce": "3.7.1", 51 | "cpy-cli": "6.0.0", 52 | "eslint": "9.39.1", 53 | "eslint-plugin-n": "17.23.1", 54 | "eslint-plugin-unicorn": "62.0.0", 55 | "path-browserify": "1.0.1", 56 | "stream-browserify": "3.0.0", 57 | "terser-webpack-plugin": "5.3.16", 58 | "util": "0.12.5", 59 | "webpack": "5.103.0", 60 | "webpack-cli": "6.0.1" 61 | }, 62 | "keywords": [ 63 | "markdown", 64 | "lint", 65 | "CommonMark", 66 | "md", 67 | "multi-root ready" 68 | ], 69 | "activationEvents": [ 70 | "onLanguage:markdown" 71 | ], 72 | "capabilities": { 73 | "untrustedWorkspaces": { 74 | "supported": "limited", 75 | "description": "In untrusted workspaces, JavaScript is blocked for custom rules, markdown-it plugins, and configuration files." 76 | }, 77 | "virtualWorkspaces": { 78 | "supported": "limited", 79 | "description": "In virtual workspaces, JavaScript is blocked for custom rules, markdown-it plugins, and configuration files." 80 | } 81 | }, 82 | "contributes": { 83 | "commands": [ 84 | { 85 | "command": "markdownlint.fixAll", 86 | "title": "Fix all supported markdownlint violations in the document" 87 | }, 88 | { 89 | "command": "markdownlint.lintWorkspace", 90 | "title": "Lint all Markdown files in the workspace with markdownlint" 91 | }, 92 | { 93 | "command": "markdownlint.openConfigFile", 94 | "title": "Create or open the markdownlint configuration file for the workspace" 95 | }, 96 | { 97 | "command": "markdownlint.toggleLinting", 98 | "title": "Toggle linting by markdownlint on/off (temporarily)" 99 | } 100 | ], 101 | "menus": { 102 | "commandPalette": [ 103 | { 104 | "command": "markdownlint.fixAll", 105 | "when": "editorLangId == markdown" 106 | }, 107 | { 108 | "command": "markdownlint.lintWorkspace", 109 | "when": "workbenchState != empty && terminalProcessSupported" 110 | }, 111 | { 112 | "command": "markdownlint.openConfigFile", 113 | "when": "workbenchState != empty" 114 | } 115 | ] 116 | }, 117 | "jsonValidation": [ 118 | { 119 | "fileMatch": [ 120 | ".markdownlint.json", 121 | ".markdownlint.jsonc" 122 | ], 123 | "url": "./markdownlint-config-schema.json" 124 | }, 125 | { 126 | "fileMatch": ".markdownlint-cli2.jsonc", 127 | "url": "./markdownlint-cli2-config-schema.json" 128 | } 129 | ], 130 | "yamlValidation": [ 131 | { 132 | "fileMatch": [ 133 | ".markdownlint.yaml", 134 | ".markdownlint.yml" 135 | ], 136 | "url": "./markdownlint-config-schema.json" 137 | }, 138 | { 139 | "fileMatch": ".markdownlint-cli2.yaml", 140 | "url": "./markdownlint-cli2-config-schema.json" 141 | } 142 | ], 143 | "taskDefinitions": [ 144 | { 145 | "type": "markdownlint", 146 | "when": "workbenchState != empty" 147 | } 148 | ], 149 | "problemMatchers": [ 150 | { 151 | "name": "markdownlint", 152 | "owner": "markdownlint", 153 | "fileLocation": "relative", 154 | "pattern": { 155 | "regexp": "^([^:]+):(\\d+)(?::(\\d+))?\\s(\\S+)\\s(\\S+)\\s(.+)$", 156 | "file": 1, 157 | "line": 2, 158 | "column": 3, 159 | "severity": 4, 160 | "code": 5, 161 | "message": 6 162 | } 163 | } 164 | ], 165 | "snippets": [ 166 | { 167 | "language": "markdown", 168 | "path": "./snippets.json" 169 | } 170 | ], 171 | "configuration": { 172 | "title": "markdownlint", 173 | "type": "object", 174 | "properties": { 175 | "markdownlint.config": { 176 | "description": "markdownlint configuration object", 177 | "scope": "resource", 178 | "type": "object", 179 | "$ref": "https://raw.githubusercontent.com/DavidAnson/markdownlint/v0.40.0/schema/markdownlint-config-schema.json", 180 | "default": {} 181 | }, 182 | "markdownlint.configFile": { 183 | "description": "Path to a configuration file that defines the base configuration", 184 | "scope": "resource", 185 | "type": "string", 186 | "default": null 187 | }, 188 | "markdownlint.customRules": { 189 | "description": "Array of paths for custom rules to include when linting", 190 | "scope": "resource", 191 | "type": "array", 192 | "items": { 193 | "type": "string" 194 | }, 195 | "default": [] 196 | }, 197 | "markdownlint.focusMode": { 198 | "description": "Makes it easier to focus while typing by hiding issues on or near the current line", 199 | "scope": "application", 200 | "type": [ 201 | "boolean", 202 | "integer" 203 | ], 204 | "default": false 205 | }, 206 | "markdownlint.lintWorkspaceGlobs": { 207 | "description": "Array of glob expressions to include or ignore when linting the workspace", 208 | "scope": "resource", 209 | "type": "array", 210 | "items": { 211 | "type": "string" 212 | }, 213 | "default": [ 214 | "**/*.{md,mkd,mdwn,mdown,markdown,markdn,mdtxt,mdtext,workbook}", 215 | "!**/*.code-search", 216 | "!**/bower_components", 217 | "!**/node_modules", 218 | "!**/.git", 219 | "!**/vendor" 220 | ] 221 | }, 222 | "markdownlint.run": { 223 | "description": "Run the linter on save (onSave) or on type (onType)", 224 | "scope": "resource", 225 | "type": "string", 226 | "enum": [ 227 | "onSave", 228 | "onType" 229 | ], 230 | "default": "onType" 231 | }, 232 | "markdownlint.severityForError": { 233 | "description": "VS Code diagnostic level to use for issues with severity \"error\"", 234 | "scope": "resource", 235 | "type": "string", 236 | "enum": [ 237 | "Error", 238 | "Warning", 239 | "Information", 240 | "Hint", 241 | "Ignore" 242 | ], 243 | "default": "Warning" 244 | }, 245 | "markdownlint.severityForWarning": { 246 | "description": "VS Code diagnostic level to use for issues with severity \"warning\"", 247 | "scope": "resource", 248 | "type": "string", 249 | "enum": [ 250 | "Error", 251 | "Warning", 252 | "Information", 253 | "Hint", 254 | "Ignore" 255 | ], 256 | "default": "Information" 257 | } 258 | } 259 | } 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /test-ui/tests.cjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | "use strict"; 4 | 5 | // This must be CommonJS or the VS Code host fails with: 6 | // Error: Cannot find package 'vscode' imported from .../tests.mjs 7 | 8 | /* eslint-disable func-style */ 9 | 10 | const assert = require("node:assert"); 11 | const fs = require("node:fs/promises"); 12 | const path = require("node:path"); 13 | const vscode = require("vscode"); 14 | 15 | // eslint-disable-next-line no-empty-function 16 | function noop () {} 17 | 18 | function testWrapper (test) { 19 | return new Promise((resolve, reject) => { 20 | const disposables = []; 21 | // eslint-disable-next-line no-use-before-define 22 | const timeout = setTimeout(() => rejectWrapper(new Error("TEST TIMEOUT")), 10_000); 23 | const cleanup = () => { 24 | clearTimeout(timeout); 25 | for (const disposable of disposables) { 26 | disposable.dispose(); 27 | } 28 | return vscode.commands.executeCommand("workbench.action.revertAndCloseActiveEditor") 29 | .then(() => { 30 | const workspaceSettingsJson = path.join(__dirname, "..", ".vscode", "settings.json"); 31 | return fs.access(workspaceSettingsJson).then(() => fs.rm(workspaceSettingsJson), noop); 32 | }); 33 | }; 34 | const resolveWrapper = (value) => { 35 | cleanup().then(() => resolve(value), reject); 36 | }; 37 | const rejectWrapper = (reason) => { 38 | cleanup().then(() => reject(reason?.stack || reason), reject); 39 | }; 40 | Promise.resolve().then(() => test(resolveWrapper, rejectWrapper, disposables)).catch(rejectWrapper); 41 | }); 42 | } 43 | 44 | function callbackWrapper (reject, callback) { 45 | Promise.resolve().then(callback).catch(reject); 46 | } 47 | 48 | function getDiagnostics (diagnosticChangeEvent, pathEndsWith) { 49 | const uris = diagnosticChangeEvent.uris.filter( 50 | (uri) => (uri.scheme === "file") && (uri.path.endsWith(pathEndsWith)) 51 | ); 52 | if (uris.length === 1) { 53 | const [ uri ] = uris; 54 | return vscode.languages.getDiagnostics(uri); 55 | } 56 | return []; 57 | } 58 | 59 | // Open README.md, create 2 violations, verify diagnostics, run fixAll command 60 | function openLintEditVerifyFixAll () { 61 | return testWrapper((resolve, reject, disposables) => { 62 | let fixedAll = false; 63 | disposables.push( 64 | vscode.window.onDidChangeActiveTextEditor((textEditor) => { 65 | callbackWrapper(reject, () => { 66 | assert.ok(textEditor.document.uri.path.endsWith("/README.md")); 67 | return textEditor.edit((editBuilder) => { 68 | // MD019 69 | editBuilder.insert(new vscode.Position(0, 1), " "); 70 | // MD012 71 | editBuilder.insert(new vscode.Position(1, 0), "\n"); 72 | }); 73 | }); 74 | }), 75 | vscode.languages.onDidChangeDiagnostics((diagnosticChangeEvent) => { 76 | callbackWrapper(reject, () => { 77 | const diagnostics = getDiagnostics(diagnosticChangeEvent, "/README.md"); 78 | if ((diagnostics.length > 0) && !fixedAll) { 79 | const [ md019, md012 ] = diagnostics; 80 | // @ts-ignore 81 | assert.equal(md019.code.value, "MD019"); 82 | assert.equal( 83 | // @ts-ignore 84 | md019.code.target.toString().replace(/v\d+\.\d+\.\d+/, "v0.0.0"), 85 | "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md019.md" 86 | ); 87 | assert.equal( 88 | md019.message, 89 | "MD019/no-multiple-space-atx: Multiple spaces after hash on atx style heading" 90 | ); 91 | assert.ok(md019.range.isEqual(new vscode.Range(0, 2, 0, 3))); 92 | assert.equal(md019.severity, vscode.DiagnosticSeverity.Warning); 93 | assert.equal(md019.source, "markdownlint"); 94 | // @ts-ignore 95 | assert.equal(md012.code.value, "MD012"); 96 | assert.equal( 97 | // @ts-ignore 98 | md012.code.target.toString().replace(/v\d+\.\d+\.\d+/, "v0.0.0"), 99 | "https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md012.md" 100 | ); 101 | assert.equal( 102 | md012.message, 103 | "MD012/no-multiple-blanks: Multiple consecutive blank lines [Expected: 1; Actual: 2]" 104 | ); 105 | assert.ok(md012.range.isEqual(new vscode.Range(2, 0, 2, 0))); 106 | assert.equal(md012.severity, vscode.DiagnosticSeverity.Warning); 107 | assert.equal(md012.source, "markdownlint"); 108 | vscode.commands.executeCommand("markdownlint.fixAll"); 109 | fixedAll = true; 110 | } else if ((diagnostics.length === 0) && fixedAll) { 111 | resolve(); 112 | } 113 | }); 114 | }) 115 | ); 116 | vscode.window.showTextDocument(vscode.Uri.file(path.join(__dirname, "..", "README.md"))) 117 | .then(noop, reject); 118 | }); 119 | } 120 | 121 | // Open README.md, create two violations, close file, verify no diagnostics 122 | function openLintEditCloseClean () { 123 | return testWrapper((resolve, reject, disposables) => { 124 | let closedActiveEditor = false; 125 | disposables.push( 126 | vscode.window.onDidChangeActiveTextEditor((textEditor) => { 127 | // eslint-disable-next-line consistent-return 128 | callbackWrapper(reject, () => { 129 | if (textEditor) { 130 | assert.ok(textEditor.document.uri.path.endsWith("/README.md")); 131 | return textEditor.edit((editBuilder) => { 132 | editBuilder.insert(new vscode.Position(0, 1), " "); 133 | editBuilder.insert(new vscode.Position(1, 0), "\n"); 134 | }); 135 | } 136 | }); 137 | }), 138 | vscode.languages.onDidChangeDiagnostics((diagnosticChangeEvent) => { 139 | callbackWrapper(reject, () => { 140 | const diagnostics = getDiagnostics(diagnosticChangeEvent, "/README.md"); 141 | if ((diagnostics.length > 0) && !closedActiveEditor) { 142 | vscode.commands.executeCommand("workbench.action.closeActiveEditor"); 143 | closedActiveEditor = true; 144 | } else if ((diagnostics.length === 0) && closedActiveEditor) { 145 | resolve(); 146 | } 147 | }); 148 | }) 149 | ); 150 | vscode.window.showTextDocument(vscode.Uri.file(path.join(__dirname, "..", "README.md"))) 151 | .then(noop, reject); 152 | }); 153 | } 154 | 155 | // Open README.md, add non-default violation (autolink), verify diagnostic 156 | function addNonDefaultViolation () { 157 | return testWrapper((resolve, reject, disposables) => { 158 | let validated = false; 159 | disposables.push( 160 | vscode.window.onDidChangeActiveTextEditor((textEditor) => { 161 | // eslint-disable-next-line consistent-return 162 | callbackWrapper(reject, () => { 163 | if (textEditor) { 164 | assert.ok(textEditor.document.uri.path.endsWith("/README.md")); 165 | return textEditor.edit((editBuilder) => { 166 | editBuilder.insert(new vscode.Position(2, 0), "\n\n"); 167 | }); 168 | } 169 | }); 170 | }), 171 | vscode.languages.onDidChangeDiagnostics((diagnosticChangeEvent) => { 172 | callbackWrapper(reject, () => { 173 | const diagnostics = getDiagnostics(diagnosticChangeEvent, "/README.md"); 174 | if ((diagnostics.length === 1) && !validated) { 175 | // @ts-ignore 176 | assert.equal(diagnostics[0].code.value, "MD054"); 177 | validated = true; 178 | vscode.commands.executeCommand("workbench.action.revertAndCloseActiveEditor") 179 | .then(noop, reject); 180 | } else if ((diagnostics.length === 0) && validated) { 181 | // Make sure diagonstics are clean for next test 182 | resolve(); 183 | } 184 | }); 185 | }) 186 | ); 187 | vscode.window.showTextDocument(vscode.Uri.file(path.join(__dirname, "..", "README.md"))) 188 | .then(noop, reject); 189 | }); 190 | } 191 | 192 | // Open README.md, create violations, save file, open diff view, undo edits 193 | function openEditDiffRevert () { 194 | return testWrapper((resolve, reject, disposables) => { 195 | let runOnce = false; 196 | let intervalId = null; 197 | disposables.push( 198 | vscode.window.onDidChangeActiveTextEditor((textEditor) => { 199 | // eslint-disable-next-line consistent-return 200 | callbackWrapper(reject, () => { 201 | if (!runOnce) { 202 | runOnce = true; 203 | assert.ok(textEditor.document.uri.path.endsWith("/CHANGELOG.md")); 204 | return textEditor.edit((editBuilder) => { 205 | editBuilder.insert(new vscode.Position(0, 1), " "); 206 | editBuilder.insert(new vscode.Position(1, 0), "\n"); 207 | }).then( 208 | () => textEditor.document.save() 209 | ); 210 | } 211 | }); 212 | }), 213 | vscode.workspace.onDidSaveTextDocument(() => { 214 | callbackWrapper(reject, () => { 215 | intervalId = setInterval(() => vscode.commands.executeCommand("git.openChange"), 50); 216 | }); 217 | }), 218 | vscode.window.onDidChangeVisibleTextEditors((textEditors) => { 219 | callbackWrapper(reject, () => { 220 | if (textEditors.length === 2) { 221 | clearInterval(intervalId); 222 | vscode.commands.executeCommand("undo").then( 223 | () => vscode.commands.executeCommand("workbench.action.files.save") 224 | ).then( 225 | () => vscode.commands.executeCommand("workbench.action.closeAllEditors") 226 | ).then(resolve); 227 | } 228 | }); 229 | }) 230 | ); 231 | vscode.window.showTextDocument(vscode.Uri.file(path.join(__dirname, "..", "CHANGELOG.md"))) 232 | .then(noop, reject); 233 | }); 234 | } 235 | 236 | // Open README.md, add custom rule, verify diagnostics, remove 237 | function dynamicWorkspaceSettingsChange () { 238 | return testWrapper((resolve, reject, disposables) => { 239 | const configuration = vscode.workspace.getConfiguration("markdownlint"); 240 | let editedSettings = false; 241 | let validated = false; 242 | disposables.push( 243 | vscode.languages.onDidChangeDiagnostics((diagnosticChangeEvent) => { 244 | callbackWrapper(reject, () => { 245 | const diagnostics = getDiagnostics(diagnosticChangeEvent, "/README.md"); 246 | if ((diagnostics.length === 0) && !editedSettings) { 247 | editedSettings = true; 248 | configuration.update("customRules", [ "./test-ui/first-line.cjs" ], vscode.ConfigurationTarget.Workspace); 249 | } else if ((diagnostics.length > 0) && editedSettings) { 250 | validated = true; 251 | configuration.update("customRules", undefined, vscode.ConfigurationTarget.Workspace); 252 | } else if ((diagnostics.length === 0) && validated) { 253 | resolve(); 254 | } 255 | }); 256 | }) 257 | ); 258 | vscode.window.showTextDocument(vscode.Uri.file(path.join(__dirname, "..", "README.md"))) 259 | .then(noop, reject); 260 | }); 261 | } 262 | 263 | // Run lintWorkspace command 264 | function lintWorkspace () { 265 | return testWrapper((resolve, reject, disposables) => { 266 | disposables.push( 267 | vscode.window.onDidOpenTerminal((terminal) => { 268 | callbackWrapper(reject, () => { 269 | assert.equal(terminal.name, "Lint all Markdown files in the workspace with markdownlint"); 270 | // Unable to examine contents of terminal using VS Code API 271 | resolve(); 272 | }); 273 | }) 274 | ); 275 | vscode.commands.executeCommand("markdownlint.lintWorkspace") 276 | .then(noop, reject); 277 | }); 278 | } 279 | 280 | const tests = [ 281 | openLintEditVerifyFixAll, 282 | openLintEditCloseClean, 283 | addNonDefaultViolation 284 | ]; 285 | if (vscode.workspace.workspaceFolders) { 286 | tests.push( 287 | openEditDiffRevert, 288 | dynamicWorkspaceSettingsChange, 289 | // Run this last because its diagnostics persist after test completion 290 | lintWorkspace 291 | ); 292 | } 293 | 294 | module.exports = { tests }; 295 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # markdownlint 2 | 3 | > Markdown/CommonMark linting and style checking for Visual Studio Code 4 | 5 | ## Introduction 6 | 7 | The [Markdown](https://en.wikipedia.org/wiki/Markdown) markup language is designed to be easy to read, write, and understand. It succeeds - and its flexibility is both a benefit and a drawback. Many styles are possible, so formatting can be inconsistent. Some constructs don't work well in all parsers and should be avoided. For example, [here are some common/troublesome Markdown constructs](https://gist.github.com/DavidAnson/006a6c2a2d9d7b21b025). 8 | 9 | [markdownlint](https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint) is an extension for the [Visual Studio Code editor](https://code.visualstudio.com) that includes a library of rules to encourage standards and consistency for Markdown files. It is powered by the [markdownlint library for Node.js](https://github.com/DavidAnson/markdownlint) (which was inspired by [markdownlint for Ruby](https://github.com/mivok/markdownlint)). Linting is performed by the [`markdownlint-cli2` engine](https://github.com/DavidAnson/markdownlint-cli2), which can be used in conjunction with this extension to provide command-line support for scripts and continuous integration scenarios. The [`markdownlint-cli2-action` GitHub Action](https://github.com/marketplace/actions/markdownlint-cli2-action) uses the same engine and can be integrated with project workflows. 10 | 11 | ## Install 12 | 13 | 1. Open [Visual Studio Code](https://code.visualstudio.com/) 14 | 2. Press `Ctrl+P`/`Ctrl+P`/`⌘P` to open the Quick Open dialog 15 | 3. Type `ext install markdownlint` to find the extension 16 | 4. Click the `Install` button, then the `Enable` button 17 | 18 | OR 19 | 20 | 1. Press `Ctrl+Shift+X`/`Ctrl+Shift+X`/`⇧⌘X` to open the Extensions tab 21 | 2. Type `markdownlint` to find the extension 22 | 3. Click the `Install` button, then the `Enable` button 23 | 24 | OR 25 | 26 | 1. Open a command-line prompt 27 | 2. Run `code --install-extension DavidAnson.vscode-markdownlint` 28 | 29 | ## Use 30 | 31 | When editing a Markdown file in VS Code with `markdownlint` installed, any lines that violate one of `markdownlint`'s rules (see below) will trigger a *Warning* in the editor. Warnings are indicated by a wavy green underline and can also be seen by pressing `Ctrl+Shift+M`/`Ctrl+Shift+M`/`⇧⌘M` to open the Errors and Warnings dialog. Hover the mouse pointer over a green line to see the warning or press `F8` and `Shift+F8`/`Shift+F8`/`⇧F8` to cycle through all the warnings (markdownlint warnings all begin with `MD###`). For more information about a `markdownlint` warning, place the cursor on a line and click the light bulb icon or press `Ctrl+.`/`Ctrl+.`/`⌘.` to open the quick fix dialog. Clicking one of the warnings in the dialog will display that rule's help entry in the default web browser. 32 | 33 | > For a tutorial, please see [Build an Amazing Markdown Editor Using Visual Studio Code and Pandoc](https://thisdavej.com/build-an-amazing-markdown-editor-using-visual-studio-code-and-pandoc/) by Dave Johnson. 34 | 35 | By default, `markdownlint` will scan and report issues for files that VS Code treats as Markdown. You can see what language mode the current file has in the Status Bar at the bottom of the window and you can [change the language mode for the current file](https://code.visualstudio.com/docs/languages/overview#_change-the-language-for-the-selected-file). If you have a custom file type that VS Code should always treat as Markdown, you can [associate that file extension with the `markdown` language identifier](https://code.visualstudio.com/docs/languages/overview#_add-a-file-extension-to-a-language). 36 | 37 | ## Rules 38 | 39 | * **[MD001](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md001.md)** *heading-increment* - Heading levels should only increment by one level at a time 40 | * **[MD003](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md003.md)** *heading-style* - Heading style 41 | * **[MD004](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md004.md)** *ul-style* - Unordered list style 42 | * **[MD005](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md005.md)** *list-indent* - Inconsistent indentation for list items at the same level 43 | * **[MD007](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md007.md)** *ul-indent* - Unordered list indentation 44 | * **[MD009](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md009.md)** *no-trailing-spaces* - Trailing spaces 45 | * **[MD010](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md010.md)** *no-hard-tabs* - Hard tabs 46 | * **[MD011](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md011.md)** *no-reversed-links* - Reversed link syntax 47 | * **[MD012](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md012.md)** *no-multiple-blanks* - Multiple consecutive blank lines 48 | * **[MD013](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md013.md)** *line-length* - Line length 49 | * **[MD014](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md014.md)** *commands-show-output* - Dollar signs used before commands without showing output 50 | * **[MD018](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md018.md)** *no-missing-space-atx* - No space after hash on atx style heading 51 | * **[MD019](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md019.md)** *no-multiple-space-atx* - Multiple spaces after hash on atx style heading 52 | * **[MD020](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md020.md)** *no-missing-space-closed-atx* - No space inside hashes on closed atx style heading 53 | * **[MD021](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md021.md)** *no-multiple-space-closed-atx* - Multiple spaces inside hashes on closed atx style heading 54 | * **[MD022](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md022.md)** *blanks-around-headings* - Headings should be surrounded by blank lines 55 | * **[MD023](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md023.md)** *heading-start-left* - Headings must start at the beginning of the line 56 | * **[MD024](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md024.md)** *no-duplicate-heading* - Multiple headings with the same content 57 | * **[MD025](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md025.md)** *single-title/single-h1* - Multiple top level headings in the same document 58 | * **[MD026](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md026.md)** *no-trailing-punctuation* - Trailing punctuation in heading 59 | * **[MD027](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md027.md)** *no-multiple-space-blockquote* - Multiple spaces after blockquote symbol 60 | * **[MD028](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md028.md)** *no-blanks-blockquote* - Blank line inside blockquote 61 | * **[MD029](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md029.md)** *ol-prefix* - Ordered list item prefix 62 | * **[MD030](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md030.md)** *list-marker-space* - Spaces after list markers 63 | * **[MD031](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md031.md)** *blanks-around-fences* - Fenced code blocks should be surrounded by blank lines 64 | * **[MD032](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md032.md)** *blanks-around-lists* - Lists should be surrounded by blank lines 65 | * **[MD033](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md033.md)** *no-inline-html* - Inline HTML 66 | * **[MD034](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md034.md)** *no-bare-urls* - Bare URL used 67 | * **[MD035](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md035.md)** *hr-style* - Horizontal rule style 68 | * **[MD036](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md036.md)** *no-emphasis-as-heading* - Emphasis used instead of a heading 69 | * **[MD037](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md037.md)** *no-space-in-emphasis* - Spaces inside emphasis markers 70 | * **[MD038](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md038.md)** *no-space-in-code* - Spaces inside code span elements 71 | * **[MD039](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md039.md)** *no-space-in-links* - Spaces inside link text 72 | * **[MD040](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md040.md)** *fenced-code-language* - Fenced code blocks should have a language specified 73 | * **[MD041](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md041.md)** *first-line-heading/first-line-h1* - First line in file should be a top level heading 74 | * **[MD042](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md042.md)** *no-empty-links* - No empty links 75 | * **[MD043](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md043.md)** *required-headings* - Required heading structure 76 | * **[MD044](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md044.md)** *proper-names* - Proper names should have the correct capitalization 77 | * **[MD045](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md045.md)** *no-alt-text* - Images should have alternate text (alt text) 78 | * **[MD046](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md046.md)** *code-block-style* - Code block style 79 | * **[MD047](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md047.md)** *single-trailing-newline* - Files should end with a single newline character 80 | * **[MD048](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md048.md)** *code-fence-style* - Code fence style 81 | * **[MD049](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md049.md)** *emphasis-style* - Emphasis style should be consistent 82 | * **[MD050](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md050.md)** *strong-style* - Strong style should be consistent 83 | * **[MD051](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md051.md)** *link-fragments* - Link fragments should be valid 84 | * **[MD052](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md052.md)** *reference-links-images* - Reference links and images should use a label that is defined 85 | * **[MD053](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md053.md)** *link-image-reference-definitions* - Link and image reference definitions should be needed 86 | * **[MD054](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md054.md)** *link-image-style* - Link and image style 87 | * **[MD055](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md055.md)** *table-pipe-style* - Table pipe style 88 | * **[MD056](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md056.md)** *table-column-count* - Table column count 89 | * **[MD058](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md058.md)** *blanks-around-tables* - Tables should be surrounded by blank lines 90 | * **[MD059](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md059.md)** *descriptive-link-text* - Link text should be descriptive 91 | * **[MD060](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md060.md)** *table-column-style* - Table column style 92 | 93 | See [markdownlint's Rules.md file](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/Rules.md) for more details. 94 | 95 | The following rules can be automatically fixed by moving the cursor to a rule violation (wavy underlined text) and typing `Ctrl+.`/`Ctrl+.`/`⌘.` or clicking the light bulb icon. 96 | 97 | * MD004 *ul-style* 98 | * MD005 *list-indent* 99 | * MD007 *ul-indent* 100 | * MD009 *no-trailing-spaces* 101 | * MD010 *no-hard-tabs* 102 | * MD011 *no-reversed-links* 103 | * MD012 *no-multiple-blanks* 104 | * MD014 *commands-show-output* 105 | * MD018 *no-missing-space-atx* 106 | * MD019 *no-multiple-space-atx* 107 | * MD020 *no-missing-space-closed-atx* 108 | * MD021 *no-multiple-space-closed-atx* 109 | * MD022 *blanks-around-headings* 110 | * MD023 *heading-start-left* 111 | * MD026 *no-trailing-punctuation* 112 | * MD027 *no-multiple-space-blockquote* 113 | * MD030 *list-marker-space* 114 | * MD031 *blanks-around-fences* 115 | * MD032 *blanks-around-lists* 116 | * MD034 *no-bare-urls* 117 | * MD037 *no-space-in-emphasis* 118 | * MD038 *no-space-in-code* 119 | * MD039 *no-space-in-links* 120 | * MD044 *proper-names* 121 | * MD047 *single-trailing-newline* 122 | * MD049 *emphasis-style* 123 | * MD050 *strong-style* 124 | * MD051 *link-fragments* 125 | * MD053 *link-image-reference-definitions* 126 | * MD054 *link-image-style* 127 | * MD058 *blanks-around-tables* 128 | 129 | ## Commands 130 | 131 | ### Fix 132 | 133 | All of a document's violations of the automatically-fixable rules above can be fixed for you. 134 | 135 | `markdownlint` registers itself as a [source code formatter](https://code.visualstudio.com/docs/editor/codebasics#_formatting) for Markdown files and can be invoked by the `Format Document`/`editor.action.formatDocument` and `Format Selection`/`editor.action.formatSelection` commands, either from the [Command Palette](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette) (via `View|Command Palette...` or `Ctrl+Shift+P`/`Ctrl+Shift+P`/`⇧⌘P`) or via the default key bindings of `Shift+Alt+F`/`Ctrl+Shift+I`/`⇧⌥F` (to format the document) and `Ctrl+K Ctrl+F`/`Ctrl+K Ctrl+F`/`⌘K ⌘F` (to format the selection). 136 | To automatically format when saving or pasting into a Markdown document, [configure Visual Studio Code's `editor.formatOnSave` or `editor.formatOnPaste` settings](https://code.visualstudio.com/docs/getstarted/settings#_language-specific-editor-settings) like so: 137 | 138 | ```json 139 | "[markdown]": { 140 | "editor.formatOnSave": true, 141 | "editor.formatOnPaste": true 142 | }, 143 | ``` 144 | 145 | `markdownlint` also contributes the `markdownlint.fixAll` command which fixes a document's violations in one step and can be run from the Command Palette or by [binding the command to a keyboard shortcut](https://code.visualstudio.com/Docs/editor/keybindings). 146 | To automatically fix violations when saving a Markdown document, configure Visual Studio Code's `editor.codeActionsOnSave` setting like so: 147 | 148 | ```json 149 | "editor.codeActionsOnSave": { 150 | "source.fixAll.markdownlint": true 151 | } 152 | ``` 153 | 154 | Automatically-applied fixes from either method can be reverted by `Edit|Undo` or `Ctrl+Z`/`Ctrl+Z`/`⌘Z`. 155 | 156 | ### Workspace 157 | 158 | To lint all Markdown files in the current workspace, run the `markdownlint.lintWorkspace` command (from the Command Palette or by binding it to a keyboard shortcut). 159 | This will use [`markdownlint-cli2`](https://github.com/DavidAnson/markdownlint-cli2), the same engine that powers the extension, to lint all files and output the results to a new terminal in the "Terminal" panel. 160 | Results will also appear in the "Problems" panel (`Ctrl+Shift+M`/`Ctrl+Shift+M`/`⇧⌘M`) because of the [problem matcher](https://code.visualstudio.com/docs/editor/tasks#_defining-a-problem-matcher) included with the extension. 161 | Entries in the "Problems" panel can be clicked to open the corresponding file in the editor. 162 | To customize the files that are included/excluded when linting a workspace, configure the `markdownlint.lintWorkspaceGlobs` setting (see below) at workspace or user scope. 163 | 164 | ### Disable 165 | 166 | To temporarily disable linting of Markdown documents, run the `markdownlint.toggleLinting` command (from the Command Palette or by binding it to a keyboard shortcut). To re-enable linting, run the `markdownlint.toggleLinting` command again. 167 | 168 | > **Note**: The effects of the `markdownlint.toggleLinting` command are reset when a new workspace is opened; linting defaults to enabled. 169 | 170 | ## Configure 171 | 172 | By default (i.e., without customizing anything), all rules are enabled *except* [`MD013`/`line-length`](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md013.md) because many files include lines longer than the conventional 80 character limit: 173 | 174 | ```json 175 | { 176 | "MD013": false 177 | } 178 | ``` 179 | 180 | Rules can be enabled, disabled, and customized by creating a [JSON](https://en.wikipedia.org/wiki/JSON) file named `.markdownlint.jsonc`/`.markdownlint.json` or a [YAML](https://en.wikipedia.org/wiki/YAML) file named `.markdownlint.yaml`/`.markdownlint.yml` or a [JavaScript](https://en.wikipedia.org/wiki/JavaScript) file named `.markdownlint.cjs` in any directory of a project. 181 | Additionally, options (which include rules and other settings) can be configured by creating a JSON file named `.markdownlint-cli2.jsonc` or a YAML file named `.markdownlint-cli2.yaml` or a JavaScript file named `.markdownlint-cli2.cjs` in any directory of a project. 182 | Rules can also be configured using VS Code's support for [user and workspace settings](https://code.visualstudio.com/docs/customization/userandworkspace). 183 | 184 | > For more information about configuration file precedence and complete examples, see the [Configuration section of the markdownlint-cli2 README.md](https://github.com/DavidAnson/markdownlint-cli2#configuration). 185 | 186 | A custom rule configuration is often defined by a `.markdownlint.json` file in the root of the project: 187 | 188 | ```json 189 | { 190 | "MD003": { "style": "atx_closed" }, 191 | "MD007": { "indent": 4 }, 192 | "no-hard-tabs": false 193 | } 194 | ``` 195 | 196 | To extend another configuration file, use the `extends` property to provide a relative path: 197 | 198 | ```json 199 | { 200 | "extends": "../.markdownlint.json", 201 | "no-hard-tabs": true 202 | } 203 | ``` 204 | 205 | Files referenced via `extends` do not need to be part of the current project (but usually are). 206 | 207 | Configuration sources have the following precedence (in decreasing order): 208 | 209 | * `.markdownlint-cli2.{jsonc,yaml,cjs}` file in the same or parent directory 210 | * `.markdownlint.{jsonc,json,yaml,yml,cjs}` file in the same or parent directory 211 | * Visual Studio Code user/workspace settings (see [markdownlint.config](#markdownlintconfig) and [markdownlint.configFile](#markdownlintconfigfile) below) 212 | * Default configuration (see above) 213 | 214 | Configuration changes saved to any location take effect immediately. 215 | Files referenced via `extends` are not monitored for changes. 216 | Inherited configuration can be explicitly disabled (or re-enabled) in any configuration file. 217 | 218 | When a workspace is open, running the `markdownlint.openConfigFile` command (from the Command Palette or by binding it to a keyboard shortcut) will open an editor for the `.markdownlint-cli2.{jsonc,yaml,cjs}` or `.markdownlint.{jsonc,json,yaml,yml,cjs}` configuration file in the root of the workspace. 219 | If none of these files exist, a new `.markdownlint.json` containing the default rule configuration will be opened in the editor in the "pending save" state. 220 | 221 | > **Note**: Because JavaScript is cached by VS Code after being loaded, edits to `.markdownlint.cjs`/`.markdownlint-cli2.cjs` require a restart of VS Code. 222 | 223 | ### markdownlint.config 224 | 225 | > **Note**: Using a project-local configuration file is preferred because doing so works with command-line tools and is easier for collaboration. 226 | 227 | The configuration above might look like the following in VS Code's user settings file: 228 | 229 | ```json 230 | { 231 | "editor.someSetting": true, 232 | "markdownlint.config": { 233 | "MD003": { "style": "atx_closed" }, 234 | "MD007": { "indent": 4 }, 235 | "no-hard-tabs": false 236 | } 237 | } 238 | ``` 239 | 240 | When using `extends` in this context: 241 | 242 | * File paths referenced by `extends` from configuration files within a workspace are resolved relative to that configuration file. 243 | * When running VS Code locally: 244 | * File paths referenced by `extends` from user settings are resolved relative to the user's home directory (e.g., `%USERPROFILE%` on Windows or `$HOME` on macOS/Linux). 245 | * File paths referenced by `extends` from workspace settings are resolved relative to the workspace folder. 246 | * VS Code's [predefined variables](https://code.visualstudio.com/docs/reference/variables-reference) `${userHome}` and `${workspaceFolder}` can be used within an `extends` path from user or workspace settings to override the default behavior. 247 | 248 | ### markdownlint.configFile 249 | 250 | The default behavior of storing configuration files in the root of a project works well most of the time. 251 | However, projects that need to store configuration files in a different location can set `configFile` to the project-relative path of that file. 252 | All [`markdownlint-cli2` configuration files used with `--config`](https://github.com/DavidAnson/markdownlint-cli2?tab=readme-ov-file#command-line) are supported. 253 | 254 | This looks like the following in VS Code's user settings: 255 | 256 | ```json 257 | { 258 | "editor.someSetting": true, 259 | "markdownlint.configFile": "./config/.markdownlint.jsonc" 260 | } 261 | ``` 262 | 263 | If [markdownlint.config](#markdownlintconfig) is also set, the settings from `configFile` take precedence. 264 | 265 | ### markdownlint.focusMode 266 | 267 | By default, all linting issues are logged and highlighted as you type or edit a document. This includes "transient" issues like `MD009`/`no-trailing-spaces` such as when typing at the end of a line. 268 | 269 | If you find this distracting, linting can be configured to ignore issues on the same line as the cursor. This looks like the following in VS Code's user settings: 270 | 271 | ```json 272 | { 273 | "editor.someSetting": true, 274 | "markdownlint.focusMode": true 275 | } 276 | ``` 277 | 278 | To ignore issues on the *N* lines above and below the cursor, set `focusMode` to a positive integer representing the number of lines to ignore in each direction: 279 | 280 | ```json 281 | { 282 | "editor.someSetting": true, 283 | "markdownlint.focusMode": 2 284 | } 285 | ``` 286 | 287 | The value of `2` in the example above will ignore issues on the line with the cursor, the 2 lines above it, and the 2 lines below it. 288 | 289 | > **Note**: This is an application-level setting and is only valid in user (not workspace) settings. 290 | 291 | ### markdownlint.run 292 | 293 | By default, linting is performed as you type or edit a document. Linting is fast and efficient and should not interfere with typical workflows. 294 | 295 | If you find this distracting, linting can be configured to run only when the document is saved. This looks like the following in VS Code's user settings: 296 | 297 | ```json 298 | { 299 | "editor.someSetting": true, 300 | "markdownlint.run": "onSave" 301 | } 302 | ``` 303 | 304 | > **Note**: When configured to run `onSave`, the list of reported issues will become outdated while the document is edited and will update when the document is saved. 305 | 306 | ### markdownlint.severityForError 307 | 308 | By default, the `markdownlint` library reports violations at severity level `error` and they are displayed in VS Code at Diagnostic Severity `Warning`. 309 | 310 | If you want to change the Diagnostic Severity shown by VS Code to something else, the `markdownlint.severityForError` configuration property can be set to any of the following values: 311 | 312 | | Value | VS Code Definition | 313 | | ------------- | --------------------------------------------------------------- | 314 | | `Error` | Something not allowed by the rules of a language or other means | 315 | | `Warning` | Something suspicious but allowed | 316 | | `Information` | Something to inform about but not a problem | 317 | | `Hint` | Something to hint to a better way of doing it | 318 | | `Ignore` | *Ignore the violation and do not report it* | 319 | 320 | For example: 321 | 322 | ```json 323 | { 324 | "editor.someSetting": true, 325 | "markdownlint.severityForError": "Information" 326 | } 327 | ``` 328 | 329 | ### markdownlint.severityForWarning 330 | 331 | This configuration property behaves the same as `markdownlint.severityForError` (see above) and is used for violations reported by the `markdownlint` library at severity level `warning`. 332 | 333 | By default, such violations are displayed in VS Code at Diagnostic Severity `Information`. 334 | 335 | The value of `severityForWarning` can be the same as the value of `severityForError`. 336 | 337 | ### markdownlint.customRules 338 | 339 | Custom rules can be specified in VS Code's user/workspace configuration to apply additional linting beyond the default set of rules. Custom rules are specified by the path to a JavaScript file or the name of or path to an [npm](https://www.npmjs.com/) package exporting one rule or an array of rules ([examples of custom rules](https://www.npmjs.com/search?q=keywords:markdownlint-rule)). 340 | 341 | Paths are typically relative to the root of the current workspace and should begin with `./` to [differentiate the relative path from a module identifier](https://nodejs.org/docs/latest-v18.x/api/modules.html#file-modules). Paths can be absolute and begin with `/`, though this is discouraged because it does not work reliably across different machines. If implementing custom rules in a workspace, consider committing the rule code under the `.vscode` directory where it will be separate from other workspace content and available to everyone who clones the repository. Paths of the form `{extension}/path` are relative to the base directory of the VS Code extension named `extension` (which must already be installed). This syntax allows custom rules to be included within another extension's package, though this is discouraged because it introduces a subtle dependency on the other extension. 342 | 343 | An example of VS Code's workspace settings for custom rules might look like the following: 344 | 345 | ```json 346 | { 347 | "editor.someSetting": true, 348 | "markdownlint.customRules": [ 349 | "./.vscode/my-custom-rule.js", 350 | "./.vscode/my-custom-rule-array.js", 351 | "./.vscode/npm-package-for-custom-rule", 352 | "/absolute/path/to/custom/rule.js", 353 | "{publisher.extension-name}/custom-rule.js", 354 | "{publisher.extension-name}/npm/rule/package" 355 | ] 356 | } 357 | ``` 358 | 359 | For information about authoring custom rules, see [the `markdownlint` documentation for custom rules](https://github.com/DavidAnson/markdownlint/blob/main/doc/CustomRules.md). 360 | 361 | > **Note**: Custom rules can also be specified (in a portable way other tools will recognize) via the [`customRules` property in `.markdownlint-cli2.{jsonc,yaml,cjs}`](https://github.com/DavidAnson/markdownlint-cli2#configuration). 362 | > In `markdownlint-cli2` configuration files, the `modulePaths` property can be used in conjunction to specify one or more additional paths for resolving module references. 363 | > This can be used to work around the VS Code limitation that globally-installed Node modules are unavailable by setting `modulePaths` to the location of the global module path (typically `/usr/local/lib` on macOS/Linux or `~/AppData/Roaming/npm` on Windows). 364 | 365 | ### markdownlint.lintWorkspaceGlobs 366 | 367 | The globs used when linting a workspace with the `markdownlint.lintWorkspace` command match VS Code's concept of "Markdown files that matter": 368 | 369 | ```jsonc 370 | [ 371 | // Source: https://github.com/microsoft/vscode/blob/main/extensions/markdown-basics/package.json 372 | "**/*.{md,mkd,mdwn,mdown,markdown,markdn,mdtxt,mdtext,workbook}", 373 | // Source: https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/search/browser/search.contribution.ts 374 | "!**/*.code-search", 375 | "!**/bower_components", 376 | "!**/node_modules", 377 | // Additional exclusions 378 | "!**/.git", 379 | "!**/vendor" 380 | ] 381 | ``` 382 | 383 | This list can be customized at workspace and user scope to include or exclude additional files and directories. 384 | For more information about syntax, see the ["Command Line" section of the markdownlint-cli2 documentation](https://github.com/DavidAnson/markdownlint-cli2#command-line). 385 | 386 | ## Suppress 387 | 388 | Individual warnings can be suppressed with comments in the Markdown file itself: 389 | 390 | ```markdown 391 | 392 | deliberate space * in * emphasis 393 | 394 | ``` 395 | 396 | More information about inline suppressions can be found in the [Configuration section of the `markdownlint` README.md](https://github.com/DavidAnson/markdownlint#configuration). 397 | 398 | ## Snippets 399 | 400 | The following snippets are available when editing a Markdown document (press `Ctrl+Space`/`Ctrl+Space`/`⌃Space` for IntelliSense suggestions): 401 | 402 | * `markdownlint-disable` 403 | * `markdownlint-enable` 404 | * `markdownlint-disable-line` 405 | * `markdownlint-disable-next-line` 406 | * `markdownlint-capture` 407 | * `markdownlint-restore` 408 | * `markdownlint-disable-file` 409 | * `markdownlint-enable-file` 410 | * `markdownlint-configure-file` 411 | 412 | ## Security 413 | 414 | Running JavaScript from custom rules, `markdown-it` plugins, or configuration files (such as `.markdownlint.cjs`/`.markdownlint-cli2.cjs`) could be a security risk, so VS Code's [Workspace Trust setting](https://code.visualstudio.com/docs/editor/workspace-trust) is honored to block JavaScript for untrusted workspaces. 415 | 416 | ## History 417 | 418 | See [CHANGELOG.md](CHANGELOG.md). 419 | -------------------------------------------------------------------------------- /extension.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | // Minimal imports (requires that may not be needed are inlined to reduce startup cost) 4 | // eslint-disable-next-line n/no-missing-import 5 | import vscode from "vscode"; 6 | import os from "node:os"; 7 | import path from "node:path"; 8 | import { promisify } from "node:util"; 9 | import { "main" as markdownlintCli2 } from "markdownlint-cli2"; 10 | import { applyFix, applyFixes } from "markdownlint-cli2/markdownlint"; 11 | import { readConfig } from "markdownlint-cli2/markdownlint/promise"; 12 | import helpers from "markdownlint-cli2/markdownlint/helpers"; 13 | // eslint-disable-next-line unicorn/no-keyword-prefix 14 | const { expandTildePath, newLineRe } = helpers; 15 | import parsers from "markdownlint-cli2/parsers"; 16 | 17 | // Constants 18 | const extensionDisplayName = "markdownlint"; 19 | const configFileGlob = ".markdownlint.{jsonc,json,yaml,yml,cjs}"; 20 | const optionsFileGlob = ".markdownlint-cli2.{jsonc,yaml,cjs}"; 21 | const markdownlintJson = ".markdownlint.json"; 22 | const configFileNames = [ 23 | ".markdownlint-cli2.jsonc", 24 | ".markdownlint-cli2.yaml", 25 | ".markdownlint-cli2.cjs", 26 | ".markdownlint.jsonc", 27 | markdownlintJson, 28 | ".markdownlint.yaml", 29 | ".markdownlint.yml", 30 | ".markdownlint.cjs" 31 | ]; 32 | const markdownLanguageId = "markdown"; 33 | // Untitled/unsaved document 34 | const schemeUntitled = "untitled"; 35 | // Standard file system workspace 36 | const schemeFile = "file"; 37 | // Used by extensions: 38 | // GitHub Repositories (github.remotehub) 39 | // Remote Repositories (ms-vscode.remote-repositories) 40 | const schemeVscodeVfs = "vscode-vfs"; 41 | // Used by @vscode/test-web when testing web extensions 42 | const schemeVscodeTestWeb = "vscode-test-web"; 43 | // Used by GistPad (vsls-contrib.gistfs) 44 | const schemeGist = "gist"; 45 | // Schemes that are okay to lint (as part of a workspace or independently) 46 | const schemeSupported = new Set([ 47 | schemeUntitled, 48 | schemeFile, 49 | schemeVscodeVfs, 50 | schemeVscodeTestWeb, 51 | schemeGist 52 | ]); 53 | // Schemes that are file system-like (support probing for configuration files) 54 | const schemeFileSystemLike = new Set([ 55 | schemeFile, 56 | schemeVscodeVfs, 57 | schemeVscodeTestWeb 58 | ]); 59 | const codeActionKindQuickFix = vscode.CodeActionKind.QuickFix; 60 | const codeActionKindSourceFixAll = vscode.CodeActionKind.SourceFixAll; 61 | const codeActionKindSourceFixAllExtension = codeActionKindSourceFixAll.append(extensionDisplayName); 62 | const defaultConfig = { 63 | "MD013": false 64 | }; 65 | const diagnosticSeverityError = "Error"; 66 | const diagnosticSeverityHint = "Hint"; 67 | const diagnosticSeverityIgnore = "Ignore"; 68 | const diagnosticSeverityInformation = "Information"; 69 | const diagnosticSeverityWarning = "Warning"; 70 | const diagnosticSeverities = new Set([ 71 | diagnosticSeverityError, 72 | diagnosticSeverityHint, 73 | diagnosticSeverityIgnore, 74 | diagnosticSeverityInformation, 75 | diagnosticSeverityWarning 76 | ]); 77 | 78 | const clickForInfo = "More information about "; 79 | const clickToFixThis = "Fix this violation of "; 80 | const clickToFixRulePrefix = "Fix all violations of "; 81 | const inTheDocument = " in the document"; 82 | const fixLineCommandName = "markdownlint.fixLine"; 83 | const fixAllCommandTitle = `Fix all supported ${extensionDisplayName} violations${inTheDocument}`; 84 | const fixAllCommandName = "markdownlint.fixAll"; 85 | const lintWorkspaceCommandName = "markdownlint.lintWorkspace"; 86 | const openConfigFileCommandName = "markdownlint.openConfigFile"; 87 | const toggleLintingCommandName = "markdownlint.toggleLinting"; 88 | const lintAllTaskName = `Lint all Markdown files in the workspace with ${extensionDisplayName}`; 89 | const problemMatcherName = `$${extensionDisplayName}`; 90 | const clickForConfigureInfo = `Details about configuring ${extensionDisplayName} rules`; 91 | const clickForConfigureUrl = "https://github.com/DavidAnson/vscode-markdownlint#configure"; 92 | const errorExceptionPrefix = "Exception while linting with markdownlint-cli2:\n"; 93 | const openCommand = "vscode.open"; 94 | const sectionConfig = "config"; 95 | const sectionConfigFile = "configFile"; 96 | const sectionCustomRules = "customRules"; 97 | const sectionFocusMode = "focusMode"; 98 | const sectionLintWorkspaceGlobs = "lintWorkspaceGlobs"; 99 | const sectionRun = "run"; 100 | const sectionSeverityForError = "severityForError"; 101 | const sectionSeverityForWarning = "severityForWarning"; 102 | const applicationConfigurationSections = [ sectionFocusMode ]; 103 | const throttleDuration = 500; 104 | const customRuleExtensionPrefixRe = /^\{([^}]+)\}\/(.*)$/iu; 105 | const driveLetterRe = /^[A-Za-z]:[/\\]/; 106 | const networkShareRe = /^\\\\[^\\]+\\/; 107 | const firstSegmentRe = /^\/{1,2}[^/]+\//; 108 | 109 | // Variables 110 | /** @type {Object.} */ 111 | const applicationConfiguration = {}; 112 | /** @type {Object.} */ 113 | const ruleNameToInformationUri = {}; 114 | /** @type {Map>} */ 115 | const workspaceFolderUriToDisposables = new Map(); 116 | let outputChannel = null; 117 | let outputChannelShown = false; 118 | let diagnosticCollection = null; 119 | let diagnosticGeneration = 0; 120 | /** @type {Object.} */ 121 | let runMap = {}; 122 | let lintingEnabled = true; 123 | const throttle = { 124 | "document": null, 125 | "timeout": null 126 | }; 127 | 128 | // Converts to a POSIX-style path 129 | // eslint-disable-next-line id-length 130 | function posixPath (p) { 131 | return p.split(path.sep).join(path.posix.sep); 132 | } 133 | 134 | // Gets the workspace folder Uri for the document Uri 135 | function getDocumentWorkspaceFolderUri (documentUri) { 136 | const workspaceFolder = vscode.workspace.getWorkspaceFolder(documentUri); 137 | return workspaceFolder ? 138 | workspaceFolder.uri : 139 | vscode.Uri.joinPath(documentUri, ".."); 140 | } 141 | 142 | // A Node-like fs object implemented using vscode.workspace.fs 143 | class FsWrapper { 144 | // Returns true 145 | static fwTrue () { 146 | return true; 147 | } 148 | 149 | // Returns false 150 | static fwFalse () { 151 | return false; 152 | } 153 | 154 | // Returns a Uri of fwFolderUri with the specified path segment 155 | fwFolderUriWithPathSegment (pathSegment) { 156 | // Fix drive letter issues on Windows 157 | let posixPathSegment = posixPath(pathSegment); 158 | if (driveLetterRe.test(posixPathSegment)) { 159 | // eslint-disable-next-line unicorn/prefer-ternary 160 | if ( 161 | this.fwFolderUri.path.startsWith("/") && 162 | driveLetterRe.test(this.fwFolderUri.path.slice(1)) 163 | ) { 164 | // Both paths begin with Windows drive letter, make it consistent 165 | posixPathSegment = `/${posixPathSegment}`; 166 | } else { 167 | // Folder path does not start with Windows drive letter, remove it 168 | posixPathSegment = posixPathSegment.replace(driveLetterRe, "/"); 169 | } 170 | } 171 | // Fix network share issues on Windows (possibly in addition to drive letter issues) 172 | if (networkShareRe.test(this.fwFolderUri.fsPath)) { 173 | // Path segment has the computer name prefixed, remove it 174 | posixPathSegment = posixPathSegment.replace(firstSegmentRe, "/"); 175 | } 176 | // Return consistently-formatted Uri with specified path 177 | return this.fwFolderUri.with({ "path": posixPathSegment }); 178 | } 179 | 180 | // Implements fs.access via vscode.workspace.fs 181 | fwAccess (pathSegment, mode, callback) { 182 | // eslint-disable-next-line no-param-reassign 183 | callback ||= mode; 184 | vscode.workspace.fs.stat( 185 | this.fwFolderUriWithPathSegment(pathSegment) 186 | ).then( 187 | () => callback(null), 188 | callback 189 | ); 190 | } 191 | 192 | // Implements fs.readdir via vscode.workspace.fs 193 | fwReaddir (pathSegment, options, callback) { 194 | // eslint-disable-next-line no-param-reassign 195 | callback ||= options; 196 | vscode.workspace.fs.readDirectory( 197 | this.fwFolderUriWithPathSegment(pathSegment) 198 | ).then( 199 | (namesAndTypes) => { 200 | const namesOrDirents = namesAndTypes.map( 201 | (nameAndType) => { 202 | const [ 203 | name, 204 | fileType 205 | ] = nameAndType; 206 | return options.withFileTypes ? 207 | { 208 | /* eslint-disable no-bitwise */ 209 | "isBlockDevice": FsWrapper.fwFalse, 210 | "isCharacterDevice": FsWrapper.fwFalse, 211 | "isDirectory": (fileType & vscode.FileType.Directory) ? FsWrapper.fwTrue : FsWrapper.fwFalse, 212 | "isFIFO": FsWrapper.fwFalse, 213 | "isFile": (fileType & vscode.FileType.File) ? FsWrapper.fwTrue : FsWrapper.fwFalse, 214 | "isSocket": FsWrapper.fwFalse, 215 | "isSymbolicLink": 216 | (fileType & vscode.FileType.SymbolicLink) ? FsWrapper.fwTrue : FsWrapper.fwFalse, 217 | /* eslint-enable no-bitwise */ 218 | name 219 | } : 220 | name; 221 | } 222 | ); 223 | callback(null, namesOrDirents); 224 | }, 225 | callback 226 | ); 227 | } 228 | 229 | // Implements fs.readFile via vscode.workspace.fs 230 | fwReadFile (pathSegment, options, callback) { 231 | // eslint-disable-next-line no-param-reassign 232 | callback ||= options; 233 | vscode.workspace.fs.readFile( 234 | this.fwFolderUriWithPathSegment(pathSegment) 235 | ).then( 236 | (bytes) => callback(null, new TextDecoder().decode(bytes)), 237 | callback 238 | ); 239 | } 240 | 241 | // Implements fs.stat via vscode.workspace.fs 242 | fwStat (pathSegment, options, callback) { 243 | // eslint-disable-next-line no-param-reassign 244 | callback ||= options; 245 | vscode.workspace.fs.stat( 246 | this.fwFolderUriWithPathSegment(pathSegment) 247 | ).then( 248 | (fileStat) => { 249 | // Stub required properties for fast-glob 250 | /* eslint-disable dot-notation, no-bitwise */ 251 | fileStat["isBlockDevice"] = FsWrapper.fwFalse; 252 | fileStat["isCharacterDevice"] = FsWrapper.fwFalse; 253 | fileStat["isDirectory"] = (fileStat.type & vscode.FileType.Directory) ? FsWrapper.fwTrue : FsWrapper.fwFalse; 254 | fileStat["isFIFO"] = FsWrapper.fwFalse; 255 | fileStat["isFile"] = (fileStat.type & vscode.FileType.File) ? FsWrapper.fwTrue : FsWrapper.fwFalse; 256 | fileStat["isSocket"] = FsWrapper.fwFalse; 257 | fileStat["isSymbolicLink"] = 258 | (fileStat.type & vscode.FileType.SymbolicLink) ? FsWrapper.fwTrue : FsWrapper.fwFalse; 259 | /* eslint-enable dot-notation, no-bitwise */ 260 | callback(null, fileStat); 261 | }, 262 | callback 263 | ); 264 | } 265 | 266 | // Constructs a new instance 267 | constructor (folderUri) { 268 | this.fwFolderUri = folderUri; 269 | this.access = this.fwAccess.bind(this); 270 | this.readdir = this.fwReaddir.bind(this); 271 | this.readFile = this.fwReadFile.bind(this); 272 | this.stat = this.fwStat.bind(this); 273 | this.lstat = this.stat; 274 | this.promises = {}; 275 | this.promises.access = promisify(this.fwAccess).bind(this); 276 | this.promises.readFile = promisify(this.fwReadFile).bind(this); 277 | this.promises.stat = promisify(this.fwStat).bind(this); 278 | } 279 | } 280 | 281 | // A Node-like fs object for a "null" file system 282 | class FsNull { 283 | // Implements fs.access/readdir/readFile/stat 284 | static fnError (pathSegment, modeOrOptions, callback) { 285 | // eslint-disable-next-line no-param-reassign 286 | callback ||= modeOrOptions; 287 | callback(new Error("FsNull.fnError")); 288 | } 289 | 290 | // Constructs a new instance 291 | constructor () { 292 | this.access = FsNull.fnError; 293 | this.readdir = this.access; 294 | this.readFile = this.access; 295 | this.stat = this.access; 296 | this.lstat = this.access; 297 | this.promises = {}; 298 | this.promises.access = promisify(FsNull.fnError); 299 | this.promises.readFile = this.promises.access; 300 | this.promises.stat = this.promises.access; 301 | } 302 | } 303 | 304 | // A VS Code Pseudoterminal for linting a workspace and emitting the results 305 | class LintWorkspacePseudoterminal { 306 | constructor () { 307 | this.writeEmitter = new vscode.EventEmitter(); 308 | this.closeEmitter = new vscode.EventEmitter(); 309 | } 310 | 311 | get onDidWrite () { 312 | return this.writeEmitter.event; 313 | } 314 | 315 | get onDidClose () { 316 | return this.closeEmitter.event; 317 | } 318 | 319 | open () { 320 | const logString = (message) => this.writeEmitter.fire( 321 | `${message.split(newLineRe).join("\r\n")}\r\n` 322 | ); 323 | lintWorkspace(logString) 324 | .finally(() => this.close()); 325 | } 326 | 327 | close () { 328 | this.writeEmitter.dispose(); 329 | this.closeEmitter.fire(); 330 | this.closeEmitter.dispose(); 331 | } 332 | } 333 | 334 | // Writes time, importance, and message to the output channel 335 | function outputLine (message, isError) { 336 | const time = (new Date()).toLocaleTimeString(); 337 | const importance = isError ? "ERROR" : "INFO"; 338 | outputChannel.appendLine(`[${time}] ${importance}: ${message}`); 339 | if (isError && !outputChannelShown) { 340 | outputChannelShown = true; 341 | outputChannel.show(true); 342 | } 343 | } 344 | 345 | function getDiagnosticSeverity (configuration, section, fallback) { 346 | let value = configuration.get(section); 347 | if (!diagnosticSeverities.has(value)) { 348 | value = fallback; 349 | } 350 | return vscode.DiagnosticSeverity[value] ?? null; 351 | } 352 | 353 | // Returns rule configuration from user/workspace configuration 354 | async function getConfig (fs, configuration, uri) { 355 | let userWorkspaceConfig = configuration.get(sectionConfig); 356 | // Bootstrap extend behavior into readConfig 357 | if (userWorkspaceConfig && userWorkspaceConfig.extends) { 358 | const userWorkspaceConfigMetadata = configuration.inspect(sectionConfig); 359 | const workspaceFolderUri = getDocumentWorkspaceFolderUri(uri); 360 | const useHomedir = 361 | // eslint-disable-next-line max-len 362 | (userWorkspaceConfigMetadata.globalValue && (userWorkspaceConfigMetadata.globalValue.extends === userWorkspaceConfig.extends)) || 363 | (workspaceFolderUri.scheme !== schemeFile); 364 | const homedir = os && os.homedir && os.homedir(); 365 | const workspaceFolderFsPath = posixPath(workspaceFolderUri.fsPath); 366 | const extendBase = ((useHomedir && homedir) ? homedir : workspaceFolderFsPath) || ""; 367 | let expanded = expandTildePath(userWorkspaceConfig.extends, os); 368 | if (homedir) { 369 | expanded = expanded.replace(/\${userHome}/g, homedir); 370 | } 371 | expanded = expanded.replace(/\${workspaceFolder}/g, workspaceFolderFsPath); 372 | const extendPath = path.resolve(extendBase, expanded); 373 | try { 374 | const extendConfig = await readConfig(extendPath, parsers, fs); 375 | userWorkspaceConfig = { 376 | ...extendConfig, 377 | ...userWorkspaceConfig 378 | }; 379 | } catch { 380 | // Ignore error 381 | } 382 | } 383 | return { 384 | ...defaultConfig, 385 | ...userWorkspaceConfig 386 | }; 387 | } 388 | 389 | // Returns an array of args entries for the config path for the user/workspace 390 | function getConfigFileArguments (configuration) { 391 | const configFile = configuration.get(sectionConfigFile); 392 | /** @type {string[]} */ 393 | const configFileArguments = (configFile?.length > 0) ? [ "--config", expandTildePath(configFile, os) ] : []; 394 | return configFileArguments; 395 | } 396 | 397 | // Returns custom rule configuration for user/workspace 398 | function getCustomRules (configuration) { 399 | const customRulesPaths = configuration.get(sectionCustomRules); 400 | const customRules = customRulesPaths.map((rulePath) => { 401 | const match = customRuleExtensionPrefixRe.exec(rulePath); 402 | if (match) { 403 | const [ 404 | , 405 | extensionName, 406 | relativePath 407 | ] = match; 408 | const extension = vscode.extensions.getExtension(extensionName); 409 | if (extension) { 410 | // eslint-disable-next-line no-param-reassign 411 | rulePath = posixPath( 412 | path.resolve(extension.extensionPath, relativePath) 413 | ); 414 | } 415 | } 416 | return rulePath; 417 | }); 418 | return customRules; 419 | } 420 | 421 | // Gets the value of the optionsDefault parameter to markdownlint-cli2 422 | async function getOptionsDefault (fs, workspaceConfiguration, config) { 423 | return { 424 | "config": config || await getConfig(fs, workspaceConfiguration), 425 | "customRules": getCustomRules(workspaceConfiguration) 426 | }; 427 | } 428 | 429 | // Gets the value of the optionsOverride parameter to markdownlint-cli2 430 | function getOptionsOverride () { 431 | return { 432 | "fix": false 433 | }; 434 | } 435 | 436 | // Gets the value of the noImport parameter to markdownlint-cli2 437 | function getNoImport (scheme) { 438 | const isTrusted = vscode.workspace.isTrusted; 439 | const isSchemeFile = (scheme === schemeFile); 440 | const isDesktop = Boolean(os && os.platform && os.platform()); 441 | return !isTrusted || !isSchemeFile || !isDesktop; 442 | } 443 | 444 | // Wraps getting options and calling into markdownlint-cli2 445 | async function markdownlintWrapper (document) { 446 | // Prepare markdownlint-cli2 parameters 447 | const scheme = document.uri.scheme; 448 | const independentDocument = !schemeFileSystemLike.has(scheme); 449 | const name = posixPath(document.uri.fsPath); 450 | const workspaceFolderUri = getDocumentWorkspaceFolderUri(document.uri); 451 | const fs = independentDocument ? 452 | new FsNull() : 453 | new FsWrapper(workspaceFolderUri); 454 | const configuration = vscode.workspace.getConfiguration(extensionDisplayName, document.uri); 455 | const errorSeverity = getDiagnosticSeverity(configuration, sectionSeverityForError, diagnosticSeverityWarning); 456 | const warningSeverity = getDiagnosticSeverity(configuration, sectionSeverityForWarning, diagnosticSeverityInformation); 457 | const config = await getConfig(fs, configuration, document.uri); 458 | const directory = independentDocument ? 459 | null : 460 | posixPath(workspaceFolderUri.fsPath); 461 | const argv = independentDocument ? 462 | [] : 463 | [ `:${name}`, ...getConfigFileArguments(configuration) ]; 464 | const contents = independentDocument ? 465 | "nonFileContents" : 466 | "fileContents"; 467 | let results = []; 468 | const parameters = { 469 | fs, 470 | directory, 471 | argv, 472 | [contents]: { 473 | [name]: document.getText() 474 | }, 475 | "noGlobs": true, 476 | "noImport": getNoImport(scheme), 477 | "optionsDefault": await getOptionsDefault(fs, configuration, config), 478 | "optionsOverride": { 479 | ...getOptionsOverride(), 480 | "outputFormatters": [] 481 | } 482 | }; 483 | // eslint-disable-next-line func-style 484 | const captureResultsFormatter = (options) => { 485 | results = options.results; 486 | }; 487 | parameters.optionsOverride.outputFormatters = [ [ captureResultsFormatter ] ]; 488 | // Invoke markdownlint-cli2 489 | return markdownlintCli2(parameters) 490 | .catch((error) => import("./stringify-error.mjs").then( 491 | (stringifyError) => outputLine(errorExceptionPrefix + stringifyError.default(error), true) 492 | )) 493 | .then(() => ({ 494 | results, 495 | errorSeverity, 496 | warningSeverity 497 | })); 498 | // If necessary some day to filter results by matching file name... 499 | // .then(() => results.filter((result) => isSchemeUntitled || (result.fileName === path.posix.relative(directory, name)))) 500 | } 501 | 502 | // Returns if the document is Markdown 503 | function isMarkdownDocument (document) { 504 | return ( 505 | // Markdown document with supported URI scheme 506 | // (Filters out problematic custom schemes like "comment" and "svn") 507 | (document.languageId === markdownLanguageId) && 508 | schemeSupported.has(document.uri.scheme) && 509 | ( 510 | // Non-virtual document or document authority matches an open workspace 511 | // (Filters out problematic scenarios like source control where vscode 512 | // .workspace.fs says documents of any name exist with content "") 513 | (document.uri.scheme !== schemeVscodeVfs) || 514 | vscode.workspace.workspaceFolders 515 | .filter((folder) => folder.uri.scheme === document.uri.scheme) 516 | .some((folder) => folder.uri.authority === document.uri.authority) 517 | ) 518 | ); 519 | } 520 | 521 | // Lints Markdown files in the workspace folder tree 522 | function lintWorkspace (logString) { 523 | const workspaceFolderUris = (vscode.workspace.workspaceFolders || []).map((folder) => folder.uri); 524 | return workspaceFolderUris.reduce( 525 | (previousPromise, workspaceFolderUri) => previousPromise.then(() => { 526 | logString(`Linting workspace folder "${workspaceFolderUri.toString()}"...`); 527 | const fs = new FsWrapper(workspaceFolderUri); 528 | const configuration = vscode.workspace.getConfiguration(extensionDisplayName, workspaceFolderUri); 529 | return getOptionsDefault(fs, configuration) 530 | .then((optionsDefault) => { 531 | const parameters = { 532 | fs, 533 | "argv": configuration.get(sectionLintWorkspaceGlobs), 534 | "directory": posixPath(workspaceFolderUri.fsPath), 535 | "logMessage": logString, 536 | "logError": logString, 537 | "noImport": getNoImport(workspaceFolderUri.scheme), 538 | optionsDefault, 539 | "optionsOverride": getOptionsOverride() 540 | }; 541 | return markdownlintCli2(parameters) 542 | .catch((error) => import("./stringify-error.mjs").then( 543 | (stringifyError) => logString(errorExceptionPrefix + stringifyError.default(error)) 544 | )) 545 | .then(() => logString("")); 546 | }); 547 | }), 548 | Promise.resolve() 549 | ); 550 | } 551 | 552 | // Runs the lintWorkspace task to lint all Markdown files in the workspace 553 | function lintWorkspaceViaTask () { 554 | return vscode.tasks.fetchTasks({ "type": extensionDisplayName }) 555 | .then((tasks) => { 556 | const lintWorkspaceTask = tasks.find((task) => task.name === lintAllTaskName); 557 | return lintWorkspaceTask ? 558 | vscode.tasks.executeTask(lintWorkspaceTask) : 559 | Promise.reject(new Error("Unable to fetch task.")); 560 | }); 561 | } 562 | 563 | // Lints a Markdown document 564 | function lint (document) { 565 | if (!lintingEnabled || !isMarkdownDocument(document)) { 566 | return; 567 | } 568 | const diagnostics = []; 569 | const targetGeneration = diagnosticGeneration; 570 | // Lint 571 | markdownlintWrapper(document) 572 | .then(({ results, errorSeverity, warningSeverity }) => { 573 | const { activeTextEditor } = vscode.window; 574 | for (const result of results) { 575 | // Create Diagnostics 576 | const lineNumber = result.lineNumber; 577 | const focusMode = applicationConfiguration[sectionFocusMode]; 578 | const focusModeRange = (!Number.isInteger(focusMode) || (focusMode < 0)) ? 579 | 0 : 580 | focusMode; 581 | const severity = (result.severity === "warning") ? warningSeverity : errorSeverity; 582 | if ( 583 | (severity !== null) && ( 584 | (applicationConfiguration[sectionFocusMode] === false) || 585 | !activeTextEditor || 586 | (activeTextEditor.document !== document) || 587 | (activeTextEditor.selection.active.line < (lineNumber - focusModeRange - 1)) || 588 | (activeTextEditor.selection.active.line > (lineNumber + focusModeRange - 1)) 589 | ) 590 | ) { 591 | const ruleName = result.ruleNames[0]; 592 | const ruleDescription = result.ruleDescription; 593 | const ruleInformationUri = result.ruleInformation && vscode.Uri.parse(result.ruleInformation); 594 | ruleNameToInformationUri[ruleName] = ruleInformationUri; 595 | let message = result.ruleNames.join("/") + ": " + ruleDescription; 596 | if (result.errorDetail) { 597 | message += " [" + result.errorDetail + "]"; 598 | } 599 | let range = document.lineAt(lineNumber - 1).range; 600 | if (result.errorRange) { 601 | const start = result.errorRange[0] - 1; 602 | const end = start + result.errorRange[1]; 603 | range = range.with(range.start.with(undefined, start), range.end.with(undefined, end)); 604 | } 605 | // @ts-ignore 606 | const diagnostic = new vscode.Diagnostic(range, message, severity); 607 | diagnostic.code = ruleInformationUri ? 608 | { 609 | "value": ruleName, 610 | "target": ruleInformationUri 611 | } : 612 | ruleName; 613 | diagnostic.source = extensionDisplayName; 614 | // @ts-ignore 615 | diagnostic.fixInfo = result.fixInfo; 616 | diagnostics.push(diagnostic); 617 | } 618 | } 619 | }) 620 | // Publish 621 | .then(() => { 622 | if (targetGeneration === diagnosticGeneration) { 623 | diagnosticCollection.set(document.uri, diagnostics); 624 | } 625 | }); 626 | } 627 | 628 | // Implements CodeActionsProvider.provideCodeActions to provide information and fix rule violations 629 | function provideCodeActions (document, range, codeActionContext) { 630 | const codeActions = []; 631 | // eslint-disable-next-line func-style 632 | const addToCodeActions = (action) => { 633 | if (!codeActionContext.only || codeActionContext.only.contains(action.kind)) { 634 | codeActions.push(action); 635 | } 636 | }; 637 | const diagnostics = codeActionContext.diagnostics || []; 638 | const extensionDiagnostics = diagnostics.filter((diagnostic) => diagnostic.source === extensionDisplayName); 639 | for (const diagnostic of extensionDiagnostics) { 640 | const ruleName = diagnostic.code.value || diagnostic.code; 641 | const ruleNameAlias = diagnostic.message.split(":")[0]; 642 | if (diagnostic.fixInfo) { 643 | // Provide code action to fix the violation 644 | const fixTitle = clickToFixThis + ruleNameAlias; 645 | const fixAction = new vscode.CodeAction(fixTitle, codeActionKindQuickFix); 646 | fixAction.command = { 647 | "title": fixTitle, 648 | "command": fixLineCommandName, 649 | "arguments": [ 650 | diagnostic.range.start.line, 651 | diagnostic.fixInfo 652 | ] 653 | }; 654 | fixAction.diagnostics = [ diagnostic ]; 655 | fixAction.isPreferred = true; 656 | addToCodeActions(fixAction); 657 | } 658 | // Provide code action for information about the violation 659 | const ruleInformationUri = ruleNameToInformationUri[ruleName]; 660 | if (ruleInformationUri) { 661 | const infoTitle = clickForInfo + ruleNameAlias; 662 | const infoAction = new vscode.CodeAction(infoTitle, codeActionKindQuickFix); 663 | infoAction.command = { 664 | "title": infoTitle, 665 | "command": openCommand, 666 | "arguments": [ ruleInformationUri ] 667 | }; 668 | addToCodeActions(infoAction); 669 | } 670 | if (diagnostic.fixInfo) { 671 | // Provide code action to fix all similar violations 672 | const fixTitle = clickToFixRulePrefix + ruleNameAlias + inTheDocument; 673 | const fixAction = new vscode.CodeAction(fixTitle, codeActionKindQuickFix); 674 | fixAction.command = { 675 | "title": fixTitle, 676 | "command": fixAllCommandName, 677 | "arguments": [ ruleName ] 678 | }; 679 | addToCodeActions(fixAction); 680 | } 681 | } 682 | if (extensionDiagnostics.length > 0) { 683 | // eslint-disable-next-line func-style 684 | const registerFixAllCodeAction = (codeActionKind) => { 685 | // Provide code action for fixing all violations 686 | const sourceFixAllAction = new vscode.CodeAction( 687 | fixAllCommandTitle, 688 | codeActionKind 689 | ); 690 | sourceFixAllAction.command = { 691 | "title": fixAllCommandTitle, 692 | "command": fixAllCommandName 693 | }; 694 | addToCodeActions(sourceFixAllAction); 695 | }; 696 | registerFixAllCodeAction(codeActionKindSourceFixAllExtension); 697 | registerFixAllCodeAction(codeActionKindQuickFix); 698 | // Provide code action for information about configuring rules 699 | const configureInfoAction = new vscode.CodeAction(clickForConfigureInfo, codeActionKindQuickFix); 700 | configureInfoAction.command = { 701 | "title": clickForConfigureInfo, 702 | "command": openCommand, 703 | "arguments": [ vscode.Uri.parse(clickForConfigureUrl) ] 704 | }; 705 | addToCodeActions(configureInfoAction); 706 | } 707 | return codeActions; 708 | } 709 | 710 | // Fixes violations of a rule on a line 711 | function fixLine (lineIndex, fixInfo) { 712 | return new Promise((resolve, reject) => { 713 | const editor = vscode.window.activeTextEditor; 714 | if (editor && fixInfo) { 715 | const document = editor.document; 716 | const lineNumber = fixInfo.lineNumber || (lineIndex + 1); 717 | const { text, range } = document.lineAt(lineNumber - 1); 718 | const fixedText = applyFix(text, fixInfo, "\n"); 719 | return editor.edit((editBuilder) => { 720 | if (typeof fixedText === "string") { 721 | editBuilder.replace(range, fixedText); 722 | } else { 723 | let deleteRange = range; 724 | if (lineNumber === 1) { 725 | if (document.lineCount > 1) { 726 | const nextLine = document.lineAt(range.end.line + 1); 727 | deleteRange = range.with({ "end": nextLine.range.start }); 728 | } 729 | } else { 730 | const previousLine = document.lineAt(range.start.line - 1); 731 | deleteRange = range.with({ "start": previousLine.range.end }); 732 | } 733 | editBuilder.delete(deleteRange); 734 | } 735 | }) 736 | .then(() => { 737 | // Remove inappropriate selection that may have been added by editBuilder.replace 738 | const cursorPosition = editor.selection.active; 739 | editor.selection = new vscode.Selection(cursorPosition, cursorPosition); 740 | }) 741 | .then(resolve, reject); 742 | } 743 | return resolve(); 744 | }); 745 | } 746 | 747 | // Fixes all violations in the active document 748 | function fixAll (ruleNameFilter) { 749 | return new Promise((resolve, reject) => { 750 | const editor = vscode.window.activeTextEditor; 751 | if (editor) { 752 | const document = editor.document; 753 | if (isMarkdownDocument(document)) { 754 | return markdownlintWrapper(document) 755 | .then(({ results }) => { 756 | const text = document.getText(); 757 | const errorsToFix = 758 | results.filter((error) => (!ruleNameFilter || (error.ruleNames[0] === ruleNameFilter))); 759 | const fixedText = applyFixes(text, errorsToFix); 760 | return (text === fixedText) ? 761 | null : 762 | editor.edit((editBuilder) => { 763 | const start = document.lineAt(0).range.start; 764 | const end = document.lineAt(document.lineCount - 1).range.end; 765 | editBuilder.replace(new vscode.Range(start, end), fixedText); 766 | }); 767 | }) 768 | .then(resolve, reject); 769 | } 770 | } 771 | return resolve(); 772 | }); 773 | } 774 | 775 | // Formats a range of the document (applying fixes) 776 | function formatDocument (document, range) { 777 | return new Promise((resolve, reject) => { 778 | if (isMarkdownDocument(document)) { 779 | return markdownlintWrapper(document) 780 | .then(({ results }) => { 781 | const rangeErrors = results.filter((error) => { 782 | const { fixInfo } = error; 783 | if (fixInfo) { 784 | // eslint-disable-next-line unicorn/consistent-destructuring 785 | const line = error.lineNumber - 1; 786 | return ((range.start.line <= line) && (line <= range.end.line)); 787 | } 788 | return false; 789 | }); 790 | const text = document.getText(); 791 | const fixedText = applyFixes(text, rangeErrors); 792 | const start = document.lineAt(0).range.start; 793 | const end = document.lineAt(document.lineCount - 1).range.end; 794 | return (text === fixedText) ? 795 | [] : 796 | [ vscode.TextEdit.replace(new vscode.Range(start, end), fixedText) ]; 797 | }) 798 | .then(resolve, reject); 799 | } 800 | return resolve(); 801 | }); 802 | } 803 | 804 | // Creates or opens the markdownlint configuration file for each workspace 805 | function openConfigFile () { 806 | const workspaceFolderUris = (vscode.workspace.workspaceFolders || []).map((folder) => folder.uri); 807 | for (const workspaceFolderUri of workspaceFolderUris) { 808 | Promise.all(configFileNames.map((configFileName) => { 809 | const fileUri = vscode.Uri.joinPath(workspaceFolderUri, configFileName); 810 | return vscode.workspace.fs.stat(fileUri).then( 811 | () => fileUri, 812 | () => null 813 | ); 814 | })).then((fileUris) => { 815 | const validFilePaths = fileUris.filter((filePath) => filePath !== null); 816 | if (validFilePaths.length > 0) { 817 | // File exists, open it 818 | vscode.window.showTextDocument(validFilePaths[0]); 819 | } else { 820 | // File does not exist, create one 821 | const fileUri = vscode.Uri.joinPath(workspaceFolderUri, markdownlintJson); 822 | const untitledFileUri = fileUri.with({ "scheme": schemeUntitled }); 823 | vscode.window.showTextDocument(untitledFileUri).then( 824 | (editor) => { 825 | editor.edit((editBuilder) => { 826 | editBuilder.insert( 827 | new vscode.Position(0, 0), 828 | JSON.stringify(defaultConfig, null, 2) 829 | ); 830 | }); 831 | } 832 | ); 833 | } 834 | }); 835 | } 836 | } 837 | 838 | // Toggles linting on/off 839 | function toggleLinting () { 840 | lintingEnabled = !lintingEnabled; 841 | clearDiagnosticsAndLintVisibleFiles(); 842 | } 843 | 844 | // Clears diagnostics and lints all visible files 845 | function clearDiagnosticsAndLintVisibleFiles (eventUri) { 846 | if (eventUri) { 847 | outputLine(`Re-linting due to "${eventUri.fsPath}" change.`); 848 | } 849 | diagnosticCollection.clear(); 850 | diagnosticGeneration++; 851 | outputChannelShown = false; 852 | lintVisibleFiles(); 853 | } 854 | 855 | // Lints all visible files 856 | function lintVisibleFiles () { 857 | didChangeVisibleTextEditors(vscode.window.visibleTextEditors); 858 | } 859 | 860 | // Returns the run setting for the document 861 | function getRun (document) { 862 | const name = document.uri.toString(); 863 | // Use cached configuration if present for file 864 | if (runMap[name]) { 865 | return runMap[name]; 866 | } 867 | // Read workspace configuration 868 | const configuration = vscode.workspace.getConfiguration(extensionDisplayName, document.uri); 869 | runMap[name] = configuration.get(sectionRun); 870 | outputLine("Linting for \"" + name + "\" will be run \"" + runMap[name] + "\"."); 871 | return runMap[name]; 872 | } 873 | 874 | // Clears the map of run settings 875 | function clearRunMap () { 876 | runMap = {}; 877 | } 878 | 879 | // Suppresses a pending lint for the specified document 880 | function suppressLint (document) { 881 | if (throttle.timeout && (document === throttle.document)) { 882 | clearTimeout(throttle.timeout); 883 | throttle.document = null; 884 | throttle.timeout = null; 885 | } 886 | } 887 | 888 | // Requests a lint of the specified document 889 | function requestLint (document) { 890 | suppressLint(document); 891 | throttle.document = document; 892 | throttle.timeout = setTimeout(() => { 893 | // Do not use throttle.document in this function; it may have changed 894 | lint(document); 895 | suppressLint(document); 896 | }, throttleDuration); 897 | } 898 | 899 | // Reads all application-scoped configuration settings 900 | function getApplicationConfiguration () { 901 | const configuration = vscode.workspace.getConfiguration(extensionDisplayName); 902 | for (const section of applicationConfigurationSections) { 903 | applicationConfiguration[section] = configuration.get(section); 904 | } 905 | } 906 | 907 | // Handles the onDidChangeActiveTextEditor event 908 | function didChangeActiveTextEditor () { 909 | if (applicationConfiguration[sectionFocusMode] !== false) { 910 | lintVisibleFiles(); 911 | } 912 | } 913 | 914 | // Handles the onDidChangeTextEditorSelection event 915 | function didChangeTextEditorSelection (change) { 916 | const document = change.textEditor.document; 917 | if ( 918 | isMarkdownDocument(document) && 919 | (applicationConfiguration[sectionFocusMode] !== false) 920 | ) { 921 | requestLint(document); 922 | } 923 | } 924 | 925 | // Handles the onDidChangeVisibleTextEditors event 926 | function didChangeVisibleTextEditors (textEditors) { 927 | for (const textEditor of textEditors) { 928 | lint(textEditor.document); 929 | } 930 | } 931 | 932 | // Handles the onDidOpenTextDocument event 933 | function didOpenTextDocument (document) { 934 | if (isMarkdownDocument(document)) { 935 | lint(document); 936 | suppressLint(document); 937 | } 938 | } 939 | 940 | // Handles the onDidChangeTextDocument event 941 | function didChangeTextDocument (change) { 942 | const document = change.document; 943 | if (isMarkdownDocument(document) && (getRun(document) === "onType")) { 944 | requestLint(document); 945 | } 946 | } 947 | 948 | // Handles the onDidSaveTextDocument event 949 | function didSaveTextDocument (document) { 950 | if (isMarkdownDocument(document) && (getRun(document) === "onSave")) { 951 | lint(document); 952 | suppressLint(document); 953 | } 954 | } 955 | 956 | // Handles the onDidCloseTextDocument event 957 | function didCloseTextDocument (document) { 958 | suppressLint(document); 959 | diagnosticCollection.delete(document.uri); 960 | } 961 | 962 | // Handles the onDidChangeConfiguration event 963 | function didChangeConfiguration (change) { 964 | if (!change || change.affectsConfiguration(extensionDisplayName)) { 965 | outputLine("Resetting configuration cache due to setting change."); 966 | getApplicationConfiguration(); 967 | clearRunMap(); 968 | clearDiagnosticsAndLintVisibleFiles(); 969 | } 970 | } 971 | 972 | // Handles the onDidGrantWorkspaceTrust event 973 | function didGrantWorkspaceTrust () { 974 | didChangeConfiguration(); 975 | } 976 | 977 | // Creates all file system watchers for the specified workspace folder Uri 978 | function createFileSystemWatchers (workspaceFolderUri) { 979 | disposeFileSystemWatchers(workspaceFolderUri); 980 | const relativeConfigFileGlob = new vscode.RelativePattern(workspaceFolderUri, "**/" + configFileGlob); 981 | const configWatcher = vscode.workspace.createFileSystemWatcher(relativeConfigFileGlob); 982 | const relativeOptionsFileGlob = new vscode.RelativePattern(workspaceFolderUri, "**/" + optionsFileGlob); 983 | const optionsWatcher = vscode.workspace.createFileSystemWatcher(relativeOptionsFileGlob); 984 | const workspaceFolderUriString = workspaceFolderUri.toString(); 985 | workspaceFolderUriToDisposables.set( 986 | workspaceFolderUriString, 987 | [ 988 | configWatcher, 989 | configWatcher.onDidCreate(clearDiagnosticsAndLintVisibleFiles), 990 | configWatcher.onDidChange(clearDiagnosticsAndLintVisibleFiles), 991 | configWatcher.onDidDelete(clearDiagnosticsAndLintVisibleFiles), 992 | optionsWatcher, 993 | optionsWatcher.onDidCreate(clearDiagnosticsAndLintVisibleFiles), 994 | optionsWatcher.onDidChange(clearDiagnosticsAndLintVisibleFiles), 995 | optionsWatcher.onDidDelete(clearDiagnosticsAndLintVisibleFiles) 996 | ] 997 | ); 998 | } 999 | 1000 | // Disposes of all file system watchers for the specified workspace folder Uri 1001 | function disposeFileSystemWatchers (workspaceFolderUri) { 1002 | const workspaceFolderUriString = workspaceFolderUri.toString(); 1003 | const disposables = workspaceFolderUriToDisposables.get(workspaceFolderUriString) || []; 1004 | for (const disposable of disposables) { 1005 | disposable.dispose(); 1006 | } 1007 | workspaceFolderUriToDisposables.delete(workspaceFolderUriString); 1008 | } 1009 | 1010 | // Handles the onDidChangeWorkspaceFolders event 1011 | function didChangeWorkspaceFolders (changes) { 1012 | for (const workspaceFolderUri of changes.removed.map((folder) => folder.uri)) { 1013 | disposeFileSystemWatchers(workspaceFolderUri); 1014 | } 1015 | for (const workspaceFolderUri of changes.added.map((folder) => folder.uri)) { 1016 | createFileSystemWatchers(workspaceFolderUri); 1017 | } 1018 | } 1019 | 1020 | export function activate (context) { 1021 | // Create OutputChannel 1022 | outputChannel = vscode.window.createOutputChannel(extensionDisplayName); 1023 | context.subscriptions.push(outputChannel); 1024 | 1025 | // Get application-level configuration 1026 | getApplicationConfiguration(); 1027 | 1028 | // Hook up to workspace events 1029 | context.subscriptions.push( 1030 | vscode.window.onDidChangeActiveTextEditor(didChangeActiveTextEditor), 1031 | vscode.window.onDidChangeTextEditorSelection(didChangeTextEditorSelection), 1032 | vscode.window.onDidChangeVisibleTextEditors(didChangeVisibleTextEditors), 1033 | vscode.workspace.onDidOpenTextDocument(didOpenTextDocument), 1034 | vscode.workspace.onDidChangeTextDocument(didChangeTextDocument), 1035 | vscode.workspace.onDidSaveTextDocument(didSaveTextDocument), 1036 | vscode.workspace.onDidCloseTextDocument(didCloseTextDocument), 1037 | vscode.workspace.onDidChangeConfiguration(didChangeConfiguration), 1038 | vscode.workspace.onDidGrantWorkspaceTrust(didGrantWorkspaceTrust), 1039 | vscode.workspace.onDidChangeWorkspaceFolders(didChangeWorkspaceFolders) 1040 | ); 1041 | 1042 | // Register CodeActionsProvider 1043 | const documentSelector = { "language": markdownLanguageId }; 1044 | const codeActionProvider = { 1045 | provideCodeActions 1046 | }; 1047 | const codeActionProviderMetadata = { 1048 | "providedCodeActionKinds": [ 1049 | codeActionKindQuickFix, 1050 | codeActionKindSourceFixAllExtension 1051 | ] 1052 | }; 1053 | context.subscriptions.push( 1054 | vscode.languages.registerCodeActionsProvider( 1055 | documentSelector, 1056 | codeActionProvider, 1057 | codeActionProviderMetadata 1058 | ) 1059 | ); 1060 | 1061 | // Register DocumentRangeFormattingEditProvider 1062 | const documentRangeFormattingEditProvider = { 1063 | "provideDocumentRangeFormattingEdits": formatDocument 1064 | }; 1065 | context.subscriptions.push( 1066 | vscode.languages.registerDocumentRangeFormattingEditProvider( 1067 | documentSelector, 1068 | documentRangeFormattingEditProvider 1069 | ) 1070 | ); 1071 | 1072 | // Register TaskProvider 1073 | const lintWorkspaceExecution = new vscode.CustomExecution( 1074 | () => Promise.resolve(new LintWorkspacePseudoterminal()) 1075 | ); 1076 | const lintWorkspaceTask = new vscode.Task( 1077 | { "type": extensionDisplayName }, 1078 | vscode.TaskScope.Workspace, 1079 | lintAllTaskName, 1080 | extensionDisplayName, 1081 | lintWorkspaceExecution, 1082 | problemMatcherName 1083 | ); 1084 | lintWorkspaceTask.presentationOptions = { 1085 | "showReuseMessage": false 1086 | }; 1087 | context.subscriptions.push( 1088 | vscode.tasks.registerTaskProvider( 1089 | extensionDisplayName, 1090 | { 1091 | "provideTasks": () => [ lintWorkspaceTask ], 1092 | // eslint-disable-next-line unicorn/no-useless-undefined 1093 | "resolveTask": () => undefined 1094 | } 1095 | ) 1096 | ); 1097 | 1098 | // Register Commands 1099 | // eslint-disable-next-line unicorn/prefer-single-call 1100 | context.subscriptions.push( 1101 | vscode.commands.registerCommand(fixAllCommandName, fixAll), 1102 | vscode.commands.registerCommand(fixLineCommandName, fixLine), 1103 | vscode.commands.registerCommand(lintWorkspaceCommandName, lintWorkspaceViaTask), 1104 | vscode.commands.registerCommand(openConfigFileCommandName, openConfigFile), 1105 | vscode.commands.registerCommand(toggleLintingCommandName, toggleLinting) 1106 | ); 1107 | 1108 | // Create DiagnosticCollection 1109 | diagnosticCollection = vscode.languages.createDiagnosticCollection(extensionDisplayName); 1110 | context.subscriptions.push(diagnosticCollection); 1111 | 1112 | // Hook up to file system changes for custom config file(s) 1113 | didChangeWorkspaceFolders({ 1114 | "added": vscode.workspace.workspaceFolders || [], 1115 | "removed": [] 1116 | }); 1117 | 1118 | // Cancel any pending operations and dispose of all file system watchers during deactivation 1119 | context.subscriptions.push({ 1120 | "dispose": () => { 1121 | suppressLint(throttle.document); 1122 | didChangeWorkspaceFolders({ 1123 | "added": [], 1124 | "removed": vscode.workspace.workspaceFolders || [] 1125 | }); 1126 | } 1127 | }); 1128 | 1129 | // Lint all visible documents 1130 | setTimeout(clearDiagnosticsAndLintVisibleFiles, throttleDuration); 1131 | } 1132 | --------------------------------------------------------------------------------