TransWikia.com

Define a new ex command active only in specific filetype (C++)

Vi and Vim Asked on November 20, 2021

I would like to define a new command for C++ files/buffers (ft=cpp).

I shall explain the requirements through the example below. The newly defined command :stripdbg should strip the surrounding dbg() text and should accept an address range. e.g :10,20stripdbg, :.,$stripdbg, :%stripdbg etc should be possible and work as expected.

example.cpp

#include "dbg.h"
#include <vector>


int factorial(int n) {
  if (dbg(n <= 1)) {
    return dbg(1);
  } else {
    return dbg(n * factorial(n - 1));
  }
}

int main() {
  std::string message = "hello";
  dbg(message); // [example.cpp:15 (main)] message = "hello" (std::string)

  const int a = 2;
  const int b = dbg(3 * a) + 1; // [example.cpp:18 (main)] 3 * a = 6 (int)

  std::vector<int> numbers{b, 13, 42};
  dbg(numbers); // [example.cpp:21 (main)] numbers = {7, 13, 42} (size: 3)
                // (std::vector<int>)

  dbg("this line is executed"); // [example.cpp:23 (main)] this line is executed

  factorial(4);

  return 0;
}

How can we achieve this through .vimrc settings?

Update: Desired output after %StripDbg()

Background

The dbg.h project is extremely helpful for quick debugging without the need for setting up/configuring and stepping through a command-line gdb process for a C++ codebase. For instance, this proves to be extremely helpful for one-off debugging in the ‘coding test’ round of an interview for a software development position. Quite a few tech companies use Codility for timed coding tasks. For such a use, offline development and quick debugging using dbg works perfectly. When the code works as expected, then we just need to strip out the dbg() calls and copy it into the browser, so that the online compiler in codility does not error out during grading.

So, my expected output after running :%StripDbg shall be,

example_strippeddbg.cpp (just an illustrative filename for distinction. The proposed command does not affect the filename)

#include "dbg.h"
#include <vector>


int factorial(int n) {
  if (n <= 1) {           //  __stripped debug__
    return 1;             //  __stripped debug__
  } else {
    return n * factorial(n - 1);   //  __stripped debug__
  }
}

int main() {
  std::string message = "hello";
  message;  //  __stripped debug__

  const int a = 2;
  const int b = 3 * a + 1; //  __stripped debug__

  std::vector<int> numbers{b, 13, 42};
  numbers; //  __stripped debug__

  dbg("this line is executed");  // __Ideally, this line must be deleted__ This might be hard.

  factorial(4);

  return 0;
}

The only hard to tackle case might be dbg("this line is executed"); which should ideally be deleted , but if that is not possible, should be left untouched. I don’t wish to consider nested dbg() cases.

In summary, I am looking for the opposite of surround.vim for the specific dbg() regex sequence, but available as a standard ex command.

2 Answers

Unlike Maxin's answer, I'd recommend to use a ftplugin. They're really made to store the definition of filetype related mappings, commands, abbreviations, options, variables...

While we shall use <buffer> with mappings and abbreviations, it's -b with commands.

Now the problem is that you want to implement is quite complex. Vim main tool is regex. And regex don't support recursive patterns. Unfortunately, in your case, we need them to handle a variety of situations like dbg(f(x, g(y, z))) + dbg(u+v): we have parenthesis within calls to dbg, and several uses of dbg on a same line.

I'd say at best we could start from tricks like this one: https://old.reddit.com/r/vim/comments/5nf4pb/some_help_needed_about_a_vim_regex_for_a_plugin/dcbhavc/

If we simplify it (a little), it becomes.

   " ~/.vimrc
   filetype plugin on " at least!


    " ~/.vim/ftplugin/cpp/StripDebug.vim
    command! -bang -buffer -range StripDbg <line1>,<line2>call s:StripDbg("<bang>")
    
    let s:SINGLEQUOTED = '''%(\.|[^\'']+)*'''
    let s:DOUBLEQUOTED = '"%(\.|[^\"]+)*"'
    let s:NEUTRAL      = '[^''"[](){}]+'
    let s:TERMINAL     = s:NEUTRAL . '|' . s:SINGLEQUOTED . '|' . s:DOUBLEQUOTED
    let s:BALANCED     = '%(' . s:TERMINAL . ')*'
    
    for n in range(5)
      let s:PARENTHESIZED   = '(' . s:BALANCED . ')'
      let s:BALANCED        = '%(' . s:TERMINAL . '|' . s:PARENTHESIZED . ')*'
    endfor
    
    function! s:StripDbg(bang) range abort
      let confirm = a:bang == '!' ? '' : 'c'
      exe a:firstline.','.a:lastline.'s/<dbg(('.s:BALANCED.'))/1/g'.confirm
    endfunction

Unfortunately, it doesn't handle cases where you use gdb to log variables. I guess you'd need to write a refactoring plugin for clang-tidy & co in order to detect that the expression returned by gdb is used elsewhere.

I'm not sure whether the solution should not be to use macro at code level. Something like

template <typename T>
inline
decltype(auto) dbg(T && v) { // not sure about the best parameters and return type to use
#if ! defined(DEBUG)
    spdlog::debug("DGB: {}", v);
#endif
    return std::forward<T>(v);
}

Answered by Luc Hermitte on November 20, 2021

  1. You cannot have a command started with lowercase letter, use :Stripdbg or :StripDbg
  2. To define a command available only in specific filetype you can define it in autocommand event with -buffer parameter:
augroup cpp_group | au!
    au FileType cpp command! -buffer StripDbg call StripDbg()
augroup end
  1. You have to implement StripDbg() function to do actual stripping
  2. To add a range to your command add -range parameter and prepend <line1>,<line2> range to a function call:
augroup cpp_group | au!
    au FileType cpp command! -buffer -range StripDbg <line1>,<line2>call StripDbg()
augroup end
  1. You can use implicit a:firstline, a:lastline variables to go through the range and use help of vim-surround plugin (you have to install it).
func! StripDbg() range
    for lnr in range(a:firstline, a:lastline)
        if getline(lnr) =~ 'dbgs*('
            call search('dbgs*(', "W", line(lnr))
            " delete dbg leaving cursor on a (
            " here we use normal with ! to call default mappings
            normal! dw
            " use vim-surround mapping to remove ( )
            " note, here we use normal without !, to call mapping of vim-surround
            normal dsb
        endif
    endfor 
endfunc

The result of :%StripDbg is:

#include "dbg.h"
#include <vector>


int factorial(int n) {
  if (n <= 1) {           //  __stripped debug__
    return 1;             //  __stripped debug__
  } else {
    return n * factorial(n - 1);   //  __stripped debug__
  }
}

int main() {
  std::string message = "hello";
  message;  //  __stripped debug__

  const int a = 2;
  const int b = 3 * a + 1; //  __stripped debug__

  std::vector<int> numbers{b, 13, 42};
  numbers; //  __stripped debug__

  "this line is executed";  // __Ideally, this line must be deleted__ This might be hard.

  factorial(4);

  return 0;
}

NOTE, it doesn't take into account dbg("this line is executed") should be removed. But you can add this kind of checks to the function yourself. Also, if there are more then 1 dbg calls on the same line, only first one would be stripped.

UPDATE

Simpler solution using help of the :global command:

func! StripDbg()
    " place cursor on dbg
    call search('dbgs*(', "W")
    " delete dbg with normal vim dw
    " here we use normal with ! to call default mappings
    normal! dw
    " use vim-surround mapping to remove ( )
    " note, here we use normal without !, to call mapping of vim-surround
    normal dsb
endfunc

" Search every line that has dbg( or dbg (  and exectute StripDbg() function on that line
command! -buffer -range StripDbg <line1>,<line2>g/dbgs*(/call StripDbg()

Answered by Maxim Kim on November 20, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP