Search code examples
htmlvimindentationvim-pluginneovim

Vim unexpectedly sources javascript indent filetype plugin on editing html files


TL;DR: vim seems to be sourcing both indent/javascript.vim and indent/html.vim on editing html files; is this intentional or a bug? How can I make html files only source html.vim?


Recently I found out that vim seems to be using indent filetype plugins for both javascript and html on editing html files, and I've done some testing based on this behaviour on minimal vim configurations.

Here is my one-line .vimrc:

filetype plugin indent on

Inside my .vim directory:

~ % tree .vim
.vim
└── indent
    ├── html.vim
    └── javascript.vim

1 directory, 2 files

Where:

~ % cat .vim/indent/javascript.vim
setlocal formatprg=js-beautify
let g:testvar_js="js testvar"
let g:testvar="testvar defined in javascript.vim"

and

~ % cat .vim/indent/html.vim      
setlocal formatprg=html-beautify
let g:testvar_html="html testvar"
let g:testvar="testvar defined in html.vim"

Then I open up a new, empty vim buffer with vim foo.html, and tested with some commands:

:set filetype?
  filetype=html
:set formatprg?
  formatprg=js-beautify
:echo g:testvar
testvar defined in javascript.vim
:echo g:testvar_html
html testvar
:echo g:testvar_js
js testvar

As if vim sources both indent filetype plugins, with indent/html.vim first and then indent/javascript.vim.

Therefore, my questions are:

  • Did I make any silly mistakes?
  • If no, then is this an intentional design, a bug, or is that vim has nothing to do with this at all?
  • Is there a way to make vim only source on html.vim when editing html files?

Some additional information that might be helpful:

  • I'm on vim 8.2, macOS arm64, using Terminal.app
  • Neovim exhibits the same behaviour; actually that's where I first note it
  • This behaviour does not occur for ftplugin/, only indent/
  • javascript files are not affected by indent/html.vim: variables defined in indent/html.vim are all undefined in a javascript buffer
  • formatprg of html files is always js-beautify on open, regardless of if there are any javascript code pieces or <script> tags inside that html file
  • An indent/css.vim will not be involved at all when editing html - I've tested
  • js-beautify and html-beautify are two separate executables (repository is here)
    bin % ls -n js-beautify
    lrwxr-xr-x  1 501  80  53 Apr 19 17:59 js-beautify -> ../lib/node_modules/js-beautify/js/bin/js-beautify.js
    bin % ls -n html-beautify
    lrwxr-xr-x  1 501  80  55 Apr 19 17:59 html-beautify -> ../lib/node_modules/js-beautify/js/bin/html-beautify.js
    

If you want me to do some additional tests or need more information, just shout.

Many thanks


Solution

  • Here is a perfectly valid HTML sample:

    <!DOCTYPE html>
    <html lang="en">
        <head>
            <title>Sample</title>
            <script>
                console.log('Hello, World!');
            </script>
            <style>
                body {
                    background: orange;
                }
            </style>
        </head>
        <body>
            <h1>Sample</h1>
        </body>
     </html>
    

    You will notice it has a tiny bit of embedded JavaScript in it, which is a good enough reason for $VIMRUNTIME/indent/html.vim to source $VIMRUNTIME/indent/javascript.vim. After all, the javascript indent script is supposed to know how to indent JavaScript, so why not use it in a html buffer that can contain embedded JavaScript?

    FWIW, here is the snippet responsible for that behaviour:

    if !exists('*GetJavascriptIndent')
      runtime! indent/javascript.vim
    endif
    

    Note that the maintainers of $VIMRUNTIME/indent/html.vim chose the external route for javascript and the internal one for css. Maybe because $VIMRUNTIME/indent/css.vim didn't fit the bill? I don't know and, frankly, I don't think it matters.

    Now, let's go through your mistakes…

    • Filetype-specific scripts (indent, syntax, ftplugins) are sourced in this order:

      1. ~/.vim/indent/<filetype>.vim,
      2. $VIMRUNTIME/indent/<filetype>.vim
      3. ~/.vim/after/indent/<filetype>.vim

      If you are not very careful, stuff you put in an earlier script might be overwritten when a later script is sourced. For that reason, it makes a lot more sense to put your own stuff in scripts under after/.

    • The following lines have nothing to do in indent scripts:

      setlocal formatprg=js-beautify
      setlocal formatprg=html-beautify
      

      They are supposed to be in ftplugins:

      " after/ftplugin/javascript.vim
      setlocal formatprg=js-beautify
      
      " after/ftplugin/html.vim
      setlocal formatprg=html-beautify
      

    So…

    Did I make any silly mistakes?

    Yes, see above.

    If no, then is this an intentional design, a bug, or is that vim has nothing to do with this at all?

    Well yes, this is an intentional design that works pretty well. It only caused problems because you misused it.

    Is there a way to make vim only source on html.vim when editing html files?

    • indent/html.vim? Yes, it certainly is possible but why would you want to do that?
    • ftplugin/html.vim? It already works the way you want and it is the right place for the things you mistakenly put in indent/html.vim to begin with.

    --- EDIT ---

    Just curious, indent/ files are supposed to set indentation options right, then why shouldn't I set the indentation program there?

    Filetype-specific scripts are typically sourced once, when a file of the corresponding filetype is loaded into a buffer. Because it is relatively common to have languages embedded in other languages (JavaScript in HTML) or languages that are supersets of other languages (C++ vs C), Vim makes it possible to source other filetype-specific scripts. That's pretty much a concrete example of code reuse and that's generally considered a good thing.

    Indent scripts can source other indent scripts, syntax scripts can source other syntax scripts, and ftplugins can source other ftplugins.

    So Vim gives us a useful low-level mechanism but it is up to us to decide what to put where, and that always depends on the context.

    In the case of HTML, it makes sense to use the existing JavaScript indent stuff, so $VIMRUNTIME/indent/html.vim sources $VIMRUNTIME/indent/javascript.vim early on and then proceeds with setting HTML-specific stuff. The end result is a html indent script that also supports embedded JavaScript. The html syntax script uses a similar mechanism in order to highlight embedded JavaScript. In some simple cases, you can even have one ftplugin sourcing another ftplugin but $VIMRUNTIME/ftplugin/html.vim doesn't.

    But it doesn't always makes sense: options may be overwritten, mappings may be overwritten or defined in contexts where they don't make sense, etc. In this specific case, what external tool to use for formatting is highly context-sensitive: you can't really expect js-beautify to format HTML properly or html-beautify to format JavaScript properly so formatprg must be set separately for the javascript and html filetypes.

    ANd this is where your first mistake kicks in.

    Here is once again the snippet that sources $VIMRUNTIME/indent/javascript.vim from $VIMRUNTIME/indent/html.vim:

    if !exists('*GetJavascriptIndent')
      runtime! indent/javascript.vim
    endif
    

    :help :runtime is a smart alternative to :help :source that looks for files in :help 'runtimepath'. Because your ~/.vim/indent/javascript.vim is in your runtimepath, it will be sourced. Because there is a !, every matching file is going to be sourced. Because it comes first in runtimepath, it might be overwritten by later scripts.

    In your case, $VIMRUNTIME/indent/html.vim automatically sources your ~/.vim/indent/javascript.vim, which contains stuff that shouldn't be set in a html buffer.

    The after directory allows you to have the last word on what is set for a given filetype because built-in scripts rarely, if ever, do runtime! after/indent/<filetype>.vim

    That explains why it is a bad idea to carelessly put your filetype-specific stuff in ~/.vim/{ftplugin,indent,syntax}/ and why you should put it in ~/.vim/after/{ftplugin,indent,syntax}/ instead.