17

Is there a way to have vim not flag HTTP URLs and email addresses as spelling errors? Or, more generally, a way to list regexes of valid spellings?

3 Answers 3

20

You can add custom syntax rules, and give them the @nospell keyword, this will tell Vim not to apply spell checking to this syntax match. For example:

:syn match UrlNoSpell "\w\+:\/\/[^[:space:]]\+" contains=@NoSpell

The above will work for text files, and some filetypes (such as markdown), but not for all filetypes.

Note that I used a fairly simple regex here; see How do I parse URL's from the text for some alternatives.


For other filetypes, you need to do a bit more work. For example, for python files, comments are contained in a pythonComment group, from /usr/share/vim/vim74/syntax/python.vim:

syn match   pythonComment   "#.*$" contains=pythonTodo,@Spell

To override this, we need to do:

:syn match UrlNoSpellComment "\w\+:\/\/[^[:space:]]\+" contains=@NoSpell containedin=pythonComment
:highlight def link UrlNoSpellComment Comment

The trick is to add a list of previously matched syntax matches in which our custom syntax match may be contained with containedin= This tells Vim to look for the UrlNoSpell regex within the pythonComment matches.

We also need to use highlight to set the correct colours, since these aren't inherited.

You need to do this in a number of places, for example for Python strings:

:syn match UrlNoSpellString "\w\+:\/\/[^[:space:]]\+" contains=@NoSpell containedin=pythonString
:highlight def link UrlNoSpellString String

We need 2 different syntax match groups so we can apply the correct syntax highlighting.

Of course, for other filestypes you need to use other containedin= syntax matches... There is, AFAIK, no "universal" solution, but looking up the correct ones in /usr/share/vim/vim74/syntax/*.vim should not be too difficult.


Note that all of the above commands must be executed after the syntax files; there are 2 ways to do this:

  • From a command or key mapping, this must be invoked manually every time. e.g.

    fun! NoUrlSpell()
        if &filetype == 'python'
            :syn match UrlNoSpellComment "\w\+:\/\/[^[:space:]]\+" contains=@NoSpell containedin=pythonComment
            :highlight def link UrlNoSpellComment Comment
            :syn match UrlNoSpellString "\w\+:\/\/[^[:space:]]\+" contains=@NoSpell containedin=pythonString
            :highlight def link UrlNoSpellString String
        elseif &filetype == 'ruby'
            " ...
        else
            syn match   pythonComment   "#.*$" contains=pythonTodo,@Spell
        endif
    endfun
    command NoUrlSpell :call NoUrlSpell()
    
  • Put the commands in in the ~/.vim/after/syntax/[filetype].vim. Vim will pick up these files and execute them after the default syntax files (see: :help after-directory).

3
  • “We also need to use highlight to set the correct colours, since these aren't inherited. […] We need 2 different syntax match groups so we can apply the correct syntax highlighting.” You can also mark the matchgroup as being transparent. This way, the matched bits keep the style of their containing group and you don’t need a different rule for each containing group. However a transparent matchgroup apparently isn’t matched at toplevel, so you’d still need two rules (one for toplevel, one for contained), but that is still simpler.
    – Maëlan
    Jun 8, 2022 at 14:13
  • It looks like Cecil Curry's newer answer already covers that @Maëlan. I don't think I knew about transparent back in 2015. Jun 8, 2022 at 16:55
  • Indeed I saw that afterwards.
    – Maëlan
    Jun 8, 2022 at 17:10
5
+150

Martin Tournoij's otherwise excellent answer fails to behave as expected for me – probably due to my leveraging diraol's phenomenal python-mode plugin rather than Vim's default syntax file for Python.

To avoid highlighting URIs in Python comments, strings, or docstrings under python-mode, add the following concise one-liner to your user-specific ~/.vim/after/syntax/python.vim file:

syntax match NoSpellUriPython '\w\+:\/\/[^[:space:]]\+' transparent contained containedin=pythonComment,python.*String contains=@NoSpell

That's it. Crucially, note that this compresses what would have been twelve separate lines in Martin's answer into a single line. How? Elementary, my dear Vim-based Watson. We add:

  • The transparent keyword, instructing Vim to inherit the highlight properties for this child syntax from its parent syntax (e.g., comments, strings). This enables us to avoid explicitly restating highlight def link for each child syntax group.
  • The contained keyword, preventing this child syntax from extending past the boundaries of its parent syntax (e.g., EOL for comments, string delimiters for strings).
  • All parent syntax groups delimited by commas to the containedin keyword. The .* regex operator enables us to cleverly match all Python string syntax groups (i.e., pythonString, pythonUniString, pythonRawString, pythonUniRawString, pythonDocstring) with minimal pain and maximal forward compatibility.

Although technically valid, the vimscript embedded in Martin's answer violates the DRY (Don't Repeat Yourself) principle. See also this similar answer for further details.

But wait... there's more.

Unasked-for Improvements for Great Glory

I'm sufficiently annoyed by both Vim's overly naïve default spell checking and third-party plugins (e.g., Spelunker, which unconditionally spell checks the entire buffer rather than merely code comments and strings) that I've decided... to actually do something about that. </gasp>

The well-tested Vim snippet below intelligently avoids spell checking all of the following within Python comments and strings:

  • URIs (as above).
  • CamelCase identifiers.
  • snake_case identifiers.
  • UPPERCASE identifiers.
  • @-prefixed identifiers (e.g., @muhdecorator).
  • "-delimited filetyped filenames (e.g., "muh_module.py").
  • :-delimited substrings (e.g., the :func: in :func:`re.sub`).
  • `-delimited substrings (e.g., the re.sub in :func:`re.sub`).

Add some or all of the following to your user-specific ~/.vim/after/syntax/python.vim file and gasp in delight as Vim actually spell checks the RightStuff™ for once:

" Avoid spell checking URIs.
syntax match NoSpellPythonUri /\v\w+:\/\/[^[:space:]]+/ transparent contained containedin=pythonComment,python.*String contains=@NoSpell

" Avoid spell checking both CamelCase-formatted identifiers and uppercase
" identifiers. Since most languages (excluding Raku) prohibit Unicode in
" identifiers, these matches are intentionally confined to ASCII codepoints
" (e.g., "[A-Z]" rather than "[[:upper:]]").
syntax match NoSpellPythonCaps /\v<[A-Z]([A-Z0-9]{-1,}|[a-z0-9]+[A-Z0-9].{-})>/ transparent contained containedin=pythonComment,python.*String contains=@NoSpell

"FIXME: For unknown reasons, enabling this and *ONLY* this "syntax" statement
"causes subtle (but horrible) failures across "python-mode" indentation and
"syntax highlighting. While lamentable, we need "python-mode" more than we need
"to avoid spell checking snake_case-formatted identifiers. See also this
"currently unresolved upstream issue:
"    https://github.com/python-mode/python-mode/issues/1083
"
" " Avoid spell checking snake_case-formatted identifiers.
" syntax match NoSpellPythonSnake
"   \ /\v<\w+_.{-1,}>/ transparent
"   \ contained containedin=pythonComment,python.*String contains=@NoSpell

" Avoid spell checking "@"-prefixed identifiers.
syntax match NoSpellPythonDecorator /\v\@[a-zA-Z].{-}>/ transparent contained containedin=pythonComment,python.*String contains=@NoSpell

" Avoid spell checking ":"-delimited substrings.
syntax match NoSpellPythonColons /\v:[^:]+:/ transparent contained containedin=pythonComment,python.*String contains=@NoSpell

" Avoid spell checking "`"-delimited substrings.
syntax match NoSpellPythonTicks /\v`[^`]+`/ transparent contained containedin=pythonComment,python.*String contains=@NoSpell

" Avoid spell checking '"'-delimited filetyped filenames matched as a
" double-quoted substring containing a filename prefix, a period, and one to
" four characters comprising a filetype.
syntax match NoSpellPythonPath /\v"[^"]+.[^"]{1,4}"/ transparent contained containedin=pythonComment,python.*String contains=@NoSpell

Of course, all of the above could (and possibly should) be reduced to a single one-liner matching one Godzilla-like regular expression that no one including myself would ever be able to maintain or even read. For everyone's sanity, I didn't do that.

If someone who is not me would like to make a GitHub-hosted Vim plugin expanding the above to other popular languages, that'd be just great. Vim's default spell checking implementation is almost there, folks; it just needs a helping hand from the open-source community.

Until then, may the StackOverflow be with you always!

2
  • 3
    I kinda figured out this whole Vim syntax highlighting thing while writing answers here. I didn't always know what I was doing to be honest, so especially some of the older answers may be less-than-optimal 😅 Apr 11, 2020 at 9:13
  • You're too modest, Martin. Without your "breaking the ice" answer, I and many others wouldn't have even known where to begin. Thanks for getting this editor party started! Rome may not have been built in a day, but at least we can spell check it in one now. Apr 14, 2020 at 3:43
0

Elaborating on Martin and Cecil’s excellent answers, I came up with this.

All the code I give is to be put in your vimrc, after :syntax enable (so that you can plug onto the syntax linter by doing :au Syntax * ...).

So, the annoying part in the problem at hand is that you need to specify for all filetypes you care about, all the matchgroups in which you want your spelling exception to apply. There is no way around that, but you can relieve the task a bit by defining a cluster. I use this one.

au Syntax * syn cluster AllSpell contains=
\cComment,cCommentL,cString,cCppString,
\javaComment,javaLineComment,javaCommentTitle,javaString,
\javascriptComment,javascriptLineComment,jsonString,
\scalaMultilineComment,scalaTrailingComment,
\hsLineComment,hsBlockComment,
\ocamlComment,ocamlString,opamComment,opamString,
\lispComment,lispCommentRegion,lispString,
\makeComment,
\shComment,shSingleQuote,shDoubleQuote,zshComment,
\vimComment,vim9Comment,vimLineComment,vim9LineComment,vimMtchComment,vimString,
\pythonComment,pythonString,pythonRawString,
\luaComment,luaString,luaString2,
\perlComment,perlPOD,
\prologComment,prologCComment,prologString,
\texComment,bibField,bibComment,bibComment2,bibComment3,bibQuote,bibBrace,bibParen,
\xmlComment,xmlString,htmlComment,cssComment,

To figure out the matchgroups you want to insert into, you need to look into the code of the linters for the filetypes you care about (e.g. in /usr/share/vim/vimXX/yourfiletype.vim). Grepping for Spell helps. Typically that will be the matchgroups for comments and string literals.

According to Cecil’s answer you can use wildcards in matchgroup names, so for instance I could have shortened vimComment,vim9Comment,vimLineComment,vim9LineComment,vimMtchComment to just vim.*Comment. Perhaps catching all of .*Comment,.*String would be a good idea, if brutal? Still, not all matchgroup names follow this pattern so you still need to do that work of looking into the linting code. Edited 2022-06-16: Wildcards are expanded early so this will likely not work, because the matchgroups are not defined yet.

Then, you can do:

au Syntax * syn match Url
  \ "\w\+:\/\/[:/?#[\]@!$&'()*+,;=0-9[:lower:][:upper:]_\-.~]\+"
  \ contains=@NoSpell containedin=@AllSpell transparent
au Syntax * syn match Url
  \ "\w\+:\/\/[:/?#[\]@!$&'()*+,;=0-9[:lower:][:upper:]_\-.~]\+"
  \ contains=@NoSpell

This uses a more satisfying regex for URLs (it matches a protocol:// followed by any succession of valid URL characters—as per RFC 3986—and Unicode letters).

The first rule matches contained URLs, thanks to the cluster just defined. It has the transparent option so that it lets the containing matchgroup style the URL. Because of that option, the rule apparently doesn’t match URLs at toplevel, so a second rule is needed. (Of course, if you wanted to, you could also style the URL differently, e.g. :hi def link Url Underlined; then you must remove the transparent option and only one rule is needed.)

We can avoid repeating ourselves with a function:

fun! MatchNoSpell(group, regex)
  let l:cmd = 'au Syntax * syn match ' . a:group . ' /'.a:regex.'/ contains=@NoSpell'
  exe l:cmd . ' containedin=@AllSpell transparent'
  exe l:cmd
endfun

call MatchNoSpell('Url',
  \ '\w\+:\/\/[:/?#[\]@!$&''()*+,;=0-9[:lower:][:upper:]_\-.~]\+' )

This function is useful because it can be used for more spelling exceptions.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.