jscheid / prettier.el

Prettier code formatting for Emacs.
GNU General Public License v3.0
167 stars 12 forks source link

Use different parsers for .md and .mdx files #136

Closed angrybacon closed 1 month ago

angrybacon commented 1 month ago

Describe the bug

I'm making assumptions with the current title, feel free to rename appropriately. Prettier.el doesn't seem to use the same parser that pnpx prettier does.

M-x prettier-info ```el (:emacs-version "GNU Emacs 30.0.60 (build 1, aarch64-apple-darwin23.5.0, NS appkit-2487.60 Version 14.5 (Build 23F79))\n of 2024-07-05" :prettier-el-version "1.3.0" :buffer-file-name "/path/to/project/File.mdx" :remote-id nil :major-mode markdown-mode :exec-path ("/Users/angrybacon/.nvm/versions/node/v20.12.2/bin" "/usr/local/sbin" "/opt/homebrew/bin" "/opt/homebrew/sbin" "/Users/angrybacon/Scripts" "/usr/local/bin" "/System/Cryptexes/App/usr/bin" "/Users/angrybacon/.cargo/bin" "/Users/angrybacon/Library/Android/sdk/emulator" "/Users/angrybacon/Library/Android/sdk/platform-tools" "/Users/angrybacon/.maestro/bin" "/usr/bin" "/bin" "/usr/sbin" "/sbin" "/opt/homebrew/Cellar/emacs-plus@30/30.0.60/libexec/emacs/30.0.60/aarch64-apple-darwin23.5.0") :env ("TERM=dumb" "LANG=en_US.UTF-8" "XPC_FLAGS=0x0" "XPC_SERVICE_NAME=application.org.gnu.Emacs.65335689.65335692" "__CF_USER_TEXT_ENCODING=0x1F6:0x0:0x0" "TMPDIR=/var/folders/c5/n35jrw5x4c57mbg_mzgtd6680000gp/T/" "SHELL=/bin/zsh" "HOME=/Users/angrybacon" "SSH_AUTH_SOCK=/private/tmp/com.apple.launchd.bNSJbZZf5r/Listeners" "LOGNAME=angrybacon" "PATH=/Users/angrybacon/.nvm/versions/node/v20.12.2/bin:/usr/local/sbin:/opt/homebrew/bin:/opt/homebrew/sbin:/Users/angrybacon/Scripts:/usr/local/bin:/System/Cryptexes/App/usr/bin:/Users/angrybacon/.cargo/bin:/Users/angrybacon/Library/Android/sdk/emulator:/Users/angrybacon/Library/Android/sdk/platform-tools:/Users/angrybacon/.maestro/bin:/usr/bin:/bin:/usr/sbin:/sbin" "__CFBundleIdentifier=org.gnu.Emacs" "COMMAND_MODE=unix2003" "USER=angrybacon") :prettier-options (:versions (:node "20.12.2" :acorn "8.11.3" :ada "2.7.6" :ares "1.27.0" :base64 "0.5.2" :brotli "1.1.0" :cjs_module_lexer "1.2.2" :cldr "44.1" :icu "74.2" :llhttp "8.1.2" :modules "115" :napi "9" :nghttp2 "1.60.0" :nghttp3 "0.7.0" :ngtcp2 "0.8.1" :openssl "3.0.13+quic" :simdutf "4.0.8" :tz "2024a" :undici "5.28.4" :unicode "15.1" :uv "1.46.0" :uvwasi "0.0.20" :v8 "11.3.244.8-node.19" :zlib "1.3.0.1-motley-40e35a7" :prettier "3.3.2") :options (:proseWrap "always" :singleQuote t :plugins [(:languages [(:linguistLanguageId 50 :name "CSS" :type "markup" :tmScope "source.css" :aceMode "css" :codemirrorMode "css" :codemirrorMimeType "text/css" :color "#563d7c" :extensions [".css" ".wxss"] :parsers ["css"] :vscodeLanguageIds ["css"]) (:linguistLanguageId 262764437 :name "PostCSS" :type "markup" :color "#dc3a0c" :tmScope "source.postcss" :group "CSS" :extensions [".pcss" ".postcss"] :aceMode "text" :parsers ["css"] :vscodeLanguageIds ["postcss"]) (:linguistLanguageId 198 :name "Less" :type "markup" :color "#1d365d" :aliases ["less-css"] :extensions [".less"] :tmScope "source.css.less" :aceMode "less" :codemirrorMode "css" :codemirrorMimeType "text/css" :parsers ["less"] :vscodeLanguageIds ["less"]) (:linguistLanguageId 329 :name "SCSS" :type "markup" :color "#c6538c" :tmScope "source.css.scss" :aceMode "scss" :codemirrorMode "css" :codemirrorMimeType "text/x-scss" :extensions [".scss"] :parsers ["scss"] :vscodeLanguageIds ["scss"]) (:linguistLanguageId 139 :name "GraphQL" :type "data" :color "#e10098" :extensions [".graphql" ".gql" ".graphqls"] :tmScope "source.graphql" :aceMode "text" :parsers ["graphql"] :vscodeLanguageIds ["graphql"]) (:linguistLanguageId 155 :name "Handlebars" :type "markup" :color "#f7931e" :aliases ["hbs" "htmlbars"] :extensions [".handlebars" ".hbs"] :tmScope "text.html.handlebars" :aceMode "handlebars" :parsers ["glimmer"] :vscodeLanguageIds ["handlebars"]) (:linguistLanguageId 146 :name "Angular" :type "markup" :tmScope "text.html.basic" :aceMode "html" :codemirrorMode "htmlmixed" :codemirrorMimeType "text/html" :color "#e34c26" :aliases ["xhtml"] :extensions [".component.html"] :parsers ["angular"] :vscodeLanguageIds ["html"] :filenames []) (:linguistLanguageId 146 :name "HTML" :type "markup" :tmScope "text.html.basic" :aceMode "html" :codemirrorMode "htmlmixed" :codemirrorMimeType "text/html" :color "#e34c26" :aliases ["xhtml"] :extensions [".html" ".hta" ".htm" ".html.hl" ".inc" ".xht" ".xhtml" ".mjml"] :parsers ["html"] :vscodeLanguageIds ["html"]) (:linguistLanguageId 146 :name "Lightning Web Components" :type "markup" :tmScope "text.html.basic" :aceMode "html" :codemirrorMode "htmlmixed" :codemirrorMimeType "text/html" :color "#e34c26" :aliases ["xhtml"] :extensions [] :parsers ["lwc"] :vscodeLanguageIds ["html"] :filenames []) (:linguistLanguageId 391 :name "Vue" :type "markup" :color "#41b883" :extensions [".vue"] :tmScope "text.html.vue" :aceMode "html" :parsers ["vue"] :vscodeLanguageIds ["vue"]) (:linguistLanguageId 183 :name "JavaScript" :type "programming" :tmScope "source.js" :aceMode "javascript" :codemirrorMode "javascript" :codemirrorMimeType "text/javascript" :color "#f1e05a" :aliases ["js" "node"] :extensions [".js" "._js" ".bones" ".cjs" ".es" ".es6" ".frag" ".gs" ".jake" ".javascript" ".jsb" ".jscad" ".jsfl" ".jslib" ".jsm" ".jspre" ".jss" ".mjs" ".njs" ".pac" ".sjs" ".ssjs" ".xsjs" ".xsjslib" ".wxs"] :filenames ["Jakefile"] :interpreters ["chakra" "d8" "gjs" "js" "node" "nodejs" "qjs" "rhino" "v8" "v8-shell" "zx"] :parsers ["babel" "acorn" "espree" "meriyah" "babel-flow" "babel-ts" "flow" "typescript"] :vscodeLanguageIds ["javascript" "mongo"]) (:linguistLanguageId 183 :name "Flow" :type "programming" :tmScope "source.js" :aceMode "javascript" :codemirrorMode "javascript" :codemirrorMimeType "text/javascript" :color "#f1e05a" :aliases [] :extensions [".js.flow"] :filenames [] :interpreters ["chakra" "d8" "gjs" "js" "node" "nodejs" "qjs" "rhino" "v8" "v8-shell"] :parsers ["flow" "babel-flow"] :vscodeLanguageIds ["javascript"]) (:linguistLanguageId 183 :name "JSX" :type "programming" :tmScope "source.js.jsx" :aceMode "javascript" :codemirrorMode "jsx" :codemirrorMimeType "text/jsx" :extensions [".jsx"] :parsers ["babel" "babel-flow" "babel-ts" "flow" "typescript" "espree" "meriyah"] :vscodeLanguageIds ["javascriptreact"] :group "JavaScript") (:linguistLanguageId 378 :name "TypeScript" :type "programming" :color "#3178c6" :aliases ["ts"] :interpreters ["deno" "ts-node"] :extensions [".ts" ".cts" ".mts"] :tmScope "source.ts" :aceMode "typescript" :codemirrorMode "javascript" :codemirrorMimeType "application/typescript" :parsers ["typescript" "babel-ts"] :vscodeLanguageIds ["typescript"]) (:linguistLanguageId 94901924 :name "TSX" :type "programming" :color "#3178c6" :group "TypeScript" :extensions [".tsx"] :tmScope "source.tsx" :aceMode "javascript" :codemirrorMode "jsx" :codemirrorMimeType "text/jsx" :parsers ["typescript" "babel-ts"] :vscodeLanguageIds ["typescriptreact"]) (:linguistLanguageId 174 :name "JSON.stringify" :type "data" :color "#292929" :tmScope "source.json" :aceMode "json" :codemirrorMode "javascript" :codemirrorMimeType "application/json" :aliases ["geojson" "jsonl" "topojson"] :extensions [".importmap"] :filenames ["package.json" "package-lock.json" "composer.json"] :parsers ["json-stringify"] :vscodeLanguageIds ["json"]) (:linguistLanguageId 174 :name "JSON" :type "data" :color "#292929" :tmScope "source.json" :aceMode "json" :codemirrorMode "javascript" :codemirrorMimeType "application/json" :aliases ["geojson" "jsonl" "topojson"] :extensions [".json" ".4DForm" ".4DProject" ".avsc" ".geojson" ".gltf" ".har" ".ice" ".JSON-tmLanguage" ".mcmeta" ".tfstate" ".tfstate.backup" ".topojson" ".webapp" ".webmanifest" ".yy" ".yyp"] :filenames [".all-contributorsrc" ".arcconfig" ".auto-changelog" ".c8rc" ".htmlhintrc" ".imgbotconfig" ".nycrc" ".tern-config" ".tern-project" ".watchmanconfig" "Pipfile.lock" "composer.lock" "flake.lock" "mcmod.info" ".babelrc" ".jscsrc" ".jshintrc" ".jslintrc" ".swcrc"] :parsers ["json"] :vscodeLanguageIds ["json"]) (:linguistLanguageId 423 :name "JSON with Comments" :type "data" :color "#292929" :group "JSON" :tmScope "source.js" :aceMode "javascript" :codemirrorMode "javascript" :codemirrorMimeType "text/javascript" :aliases ["jsonc"] :extensions [".jsonc" ".code-snippets" ".code-workspace" ".sublime-build" ".sublime-commands" ".sublime-completions" ".sublime-keymap" ".sublime-macro" ".sublime-menu" ".sublime-mousemap" ".sublime-project" ".sublime-settings" ".sublime-theme" ".sublime-workspace" ".sublime_metrics" ".sublime_session"] :filenames [] :parsers ["jsonc"] :vscodeLanguageIds ["jsonc"]) (:linguistLanguageId 175 :name "JSON5" :type "data" :color "#267CB9" :extensions [".json5"] :tmScope "source.js" :aceMode "javascript" :codemirrorMode "javascript" :codemirrorMimeType "application/json" :parsers ["json5"] :vscodeLanguageIds ["json5"]) (:linguistLanguageId 222 :name "Markdown" :type "prose" :color "#083fa1" :aliases ["md" "pandoc"] :aceMode "markdown" :codemirrorMode "gfm" :codemirrorMimeType "text/x-gfm" :wrap t :extensions [".md" ".livemd" ".markdown" ".mdown" ".mdwn" ".mkd" ".mkdn" ".mkdown" ".ronn" ".scd" ".workbook"] :filenames ["contents.lr" "README"] :tmScope "text.md" :parsers ["markdown"] :vscodeLanguageIds ["markdown"]) (:linguistLanguageId 222 :name "MDX" :type "prose" :color "#083fa1" :aliases ["md" "pandoc"] :aceMode "markdown" :codemirrorMode "gfm" :codemirrorMimeType "text/x-gfm" :wrap t :extensions [".mdx"] :filenames [] :tmScope "text.md" :parsers ["mdx"] :vscodeLanguageIds ["mdx"]) (:linguistLanguageId 407 :name "YAML" :type "data" :color "#cb171e" :tmScope "source.yaml" :aliases ["yml"] :extensions [".yml" ".mir" ".reek" ".rviz" ".sublime-syntax" ".syntax" ".yaml" ".yaml-tmlanguage" ".yaml.sed" ".yml.mysql"] :filenames [".clang-format" ".clang-tidy" ".gemrc" "CITATION.cff" "glide.lock" ".prettierrc" ".stylelintrc" ".lintstagedrc"] :aceMode "yaml" :codemirrorMode "yaml" :codemirrorMimeType "text/x-yaml" :parsers ["yaml"] :vscodeLanguageIds ["yaml" "ansible" "home-assistant"])] :options (:singleQuote (:category "Common" :type "boolean" :default nil :description "Use single quotes instead of double quotes.") :bracketSpacing (:category "Common" :type "boolean" :default t :description "Print spaces between brackets." :oppositeDescription "Do not print spaces between brackets.") :bracketSameLine (:category "Common" :type "boolean" :default nil :description "Put > of opening tags on the last line instead of on a new line.") :htmlWhitespaceSensitivity (:category "HTML" :type "choice" :default "css" :description "How to handle whitespaces in HTML." :choices [(:value "css" :description "Respect the default value of CSS display property.") (:value "strict" :description "Whitespaces are considered sensitive.") (:value "ignore" :description "Whitespaces are considered insensitive.")]) :singleAttributePerLine (:category "Common" :type "boolean" :default nil :description "Enforce single attribute per line in HTML, Vue and JSX.") :vueIndentScriptAndStyle (:category "HTML" :type "boolean" :default nil :description "Indent script and style tags in Vue files.") :arrowParens (:category "JavaScript" :type "choice" :default "always" :description "Include parentheses around a sole arrow function parameter." :choices [(:value "always" :description "Always include parens. Example: `(x) => x`") (:value "avoid" :description "Omit parens when possible. Example: `x => x`")]) :jsxBracketSameLine (:category "JavaScript" :type "boolean" :description "Put > on the last line instead of at a new line." :deprecated "2.4.0") :semi (:category "JavaScript" :type "boolean" :default t :description "Print semicolons." :oppositeDescription "Do not print semicolons, except at the beginning of lines which may need them.") :experimentalTernaries (:category "JavaScript" :type "boolean" :default nil :description "Use curious ternaries, with the question mark after the condition." :oppositeDescription "Default behavior of ternaries; keep question marks on the same line as the consequent.") :jsxSingleQuote (:category "JavaScript" :type "boolean" :default nil :description "Use single quotes in JSX.") :quoteProps (:category "JavaScript" :type "choice" :default "as-needed" :description "Change when properties in objects are quoted." :choices [(:value "as-needed" :description "Only add quotes around object properties where required.") (:value "consistent" :description "If at least one property in an object requires quotes, quote all properties.") (:value "preserve" :description "Respect the input use of quotes in object properties.")]) :trailingComma (:category "JavaScript" :type "choice" :default "all" :description "Print trailing commas wherever possible when multi-line." :choices [(:value "all" :description "Trailing commas wherever possible (including function arguments).") (:value "es5" :description "Trailing commas where valid in ES5 (objects, arrays, etc.)") (:value "none" :description "No trailing commas.")]) :proseWrap (:category "Common" :type "choice" :default "preserve" :description "How to wrap prose." :choices [(:value "always" :description "Wrap prose if it exceeds the print width.") (:value "never" :description "Do not wrap prose.") (:value "preserve" :description "Wrap prose as-is.")])) :parsers (:markdown (:astFormat "mdast") :mdx (:astFormat "mdast") :remark (:astFormat "mdast") :css (:astFormat "postcss") :less (:astFormat "postcss") :scss (:astFormat "postcss")) :printers (:estree (:experimentalFeatures (:avoidAstMutation t) :handleComments nil) :estree-json nil :mdast nil :postcss nil)) (:parsers (:prettier-el-null-parser (:astFormat "estree"))) (:name "/path/to/project/node_modules/@ianvs/prettier-plugin-sort-imports/lib/src/index.js" :options (:importOrder (:type "path" :category "Global" :array t :default [(:value ["" "" "^[.]"])] :description "Provide an order to sort imports. [node.js built-ins are always first]") :importOrderParserPlugins (:type "path" :category "Global" :array t :default [(:value ["typescript" "jsx"])] :description "Provide a list of plugins for special syntax") :importOrderTypeScriptVersion (:type "string" :category "Global" :default "1.0.0" :description "Version of TypeScript in use in the project. Determines some output syntax when using TypeScript.")) :parsers (:babel (:astFormat "estree") :babel-ts (:astFormat "estree") :flow (:astFormat "estree") :typescript (:astFormat "estree") :vue (:astFormat "html")))] :importOrder ["" "" "" "" "^@/(?!.+[.]s?css$)" "" "^[.](?!.+[.]s?css$)" "" "^@/" "^[.]"] :importOrderTypeScriptVersion "5.0.0" :cursorOffset -1 :bracketSpacing t :bracketSameLine nil :htmlWhitespaceSensitivity "css" :singleAttributePerLine nil :vueIndentScriptAndStyle nil :arrowParens "always" :semi t :experimentalTernaries nil :jsxSingleQuote nil :quoteProps "as-needed" :trailingComma "all" :importOrderParserPlugins ["typescript" "jsx"] :endOfLine "lf" :insertPragma nil :printWidth 80 :rangeEnd 1 :rangeStart 0 :requirePragma nil :tabWidth 2 :useTabs nil :embeddedLanguageFormatting "auto" :astFormat "estree" :locEnd nil :locStart nil :printer (:experimentalFeatures (:avoidAstMutation t) :handleComments nil) :originalText ".") :bestParser "markdown")) ```

To Reproduce

  1. Write a File.mdx file with

    import {
     Canvas,
     IconGallery,
     IconItem,
     Meta,
     Source,
     Story,
     Subtitle,
    } from '@storybook/addon-docs';
    
    import * as Something from './something';
  2. Run pnpx prettier -w File.mdx

  3. Confirm that File.mdx is already well-formatted

  4. Run prettier-prettify

  5. Confirm that File.mdx is formatted using different heuristics (wrong ones too)

    import { Canvas, IconGallery, IconItem, Meta, Source, Story, Subtitle, } from
    '@storybook/addon-docs';
    
    import \* as Something from './something';

    Note that the first import statement is split over 2 lines, like regular text from Markdown would.

Steps to reproduce the behavior.

Expected behavior

I'd expect pnpx prettier -w and prettier-prettify to format my file with the same heuristics.

Additional context

$ pnpx prettier --file-info File.mdx  
{ "ignored": false, "inferredParser": "mdx" }
prettier-enabled-parsers
(angular babel babel-flow babel-ts css elm espree flow graphql html java json
         json5 json-stringify less lua markdown mdx meriyah php postgresql pug
         python ruby scss sh solidity svelte swift toml typescript vue xml yaml)

The way I understand the issue, prettier.el infers the parser from the major mode, and markdown-mode.el doesn't have a different major mode for MDX. That means prettier.el doesn't have an easy to tell them apart. I'm surprised this was not mentioned in past issues though.

What would you say is the right way to fix this, if not a different major mode for MDX files?

jscheid commented 1 month ago

The way I understand the issue, prettier.el infers the parser from the major mode, and markdown-mode.el doesn't have a different major mode for MDX. That means prettier.el doesn't have an easy to tell them apart. I'm surprised this was not mentioned in past issues though.

Yes, I regret that decision. It was discussed previously but I don't think there's a point in digging it up now.

What would you say is the right way to fix this, if not a different major mode for MDX files?

Have a look at prettier-major-mode-parsers, specifically at the definitions for js*-mode and prettier--guess-js-ish. Basically the workaround here is to map markdown mode to a function such as prettier--markdown-parser which would check the file extension and return either markdown or mdx depending.

FWIW I'm working (on and off) on a successor to this package that will do what you'd expect here.

angrybacon commented 1 month ago

Thank you for the suggestion, posting here for posterity:

(defun me/prettier-markdown-parser ()
  "Return the Prettier parser for the current file.
Use the current buffer file extension if possible or fallback to the default
Markdown parser."
  (if-let* ((name (buffer-file-name))
            (extension (file-name-extension name))
            (_ (string-equal extension "mdx")))
      '(mdx)
    '(markdown)))

(use-package prettier
  :config
  (add-to-list
   'prettier-major-mode-parsers
   `(markdown-mode . ,#'me/prettier-markdown-parser))
  :init
  (add-to-list 'safe-local-eval-forms '(prettier-mode)))

FWIW I'm working (on and off) on a successor to this package that will do what you'd expect here.

Will it compete with radian-software/apheleia or did you mean something else?

jscheid commented 1 month ago

Ah, glad you worked it out, thanks for sharing your solution!

Nothing much to do with apheleia but, like apheleia, it will support other formatting tools besides Prettier. It'll also be even faster than this package, more debuggable and extensible, and fix a number of annoyances (mostly the one that made you open this ticket). I'm quite excited about it but realistically I won't be able to give it the final push before Sep/Oct. Follow me for updates or subscribe to this repo, I'll add a pointer somewhere here when it's ready.