YCM中previewwindow显示函数类型信息如何实现

intro

在使用YCM的自动提示功能时,可以注意到选择complete提供的条目时,窗口的上面还有一个小窗口提示这个函数的声明信息,包括了函数的参数列表和类型信息。

这个对写代码非常有用,对于一段时间不看的函数,很容易记不得函数的参数列表和各自的类型信息,以至于在官方issue中希望提供一个可以显示指定函数原型的功能:Could Ycm display the function prototype on the cmdline

在具体的使用过程中,经常会发现因为函数参数是另一个函数返回值,可能就会导致preview中的函数原型信息丢失;或者代码写完之后,preview窗口不会自动关闭。

那么YCM的这个功能是如何实现的呢?preview window又有哪些特性呢?

preview window

特性

  • 唯一

previewwindow的帮助手册说明了previewwindow的一个重要特性:这种类型(具有这个属性)的window最多只能有一个(Only one window can have this option set)。

'previewwindow' 'pvw' boolean (default off)
local to window local-noglobal
{not available when compiled without the +quickfix
feature}
Identifies the preview window. Only one window can have this option
set. It's normally not set directly, but by using one of the commands
:ptag, :pedit, etc.

当有一个previewwindow已经打开的时候,在另一个窗口执行

E590: A preview window already exists: pvw

这也对应了通过pclose命令不需要参数即可关闭窗口。

/*
 * ":pclose": Close any preview window.
 */
    static void
ex_pclose(exarg_T *eap)
{
    win_T	*win;

    // First close any normal window.
    FOR_ALL_WINDOWS(win)
	if (win->w_p_pvw)
	{
	    ex_win_close(eap->forceit, win, NULL);
	    return;
	}
# ifdef FEAT_PROP_POPUP
    // Also when 'previewpopup' is empty, it might have been cleared.
    popup_close_preview();
# endif
}
  • 光标

当打开窗口时,光标不会自动转移到新打开的preview窗口。

:ped[it][!] [++opt] [+cmd] {file}
Edit {file} in the preview window. The preview window is
opened like with :ptag. The current window and cursor
position isn't changed. Useful example:
:pedit +/fputc /usr/include/stdio.h

关闭

SO上关于如何关闭preview的答案

The top window is called the preview window. So any of z, or :pc[lose][!] should work.

The below is the help for :help :pclose

CTRL-W z CTRL-W_z
CTRL-W CTRL-Z CTRL-W_CTRL-Z
:pc :pclose
:pc[lose][!] Close any "Preview" window currently open. When the 'hidden'
option is set, or when the buffer was changed and the [!] is
used, the buffer becomes hidden (unless there is another
window editing it). The command fails if any "Preview" buffer
cannot be closed. See also :close.
Another relevant help page would be :help preview-window

vim

doc

在vim的complete-items帮助中,描述了不同的约定的字典键值(key)对应的意义,其中提到了info对应的内容可能在previewwindow或者popupwindow中显示。

                                           complete-items  

Each list item can either be a string or a Dictionary. When it is a string it
is used as the completion. When it is a Dictionary it can contain these
items:
word the text that will be inserted, mandatory
abbr abbreviation of "word"; when not empty it is used in
the menu instead of "word"
menu extra text for the popup menu, displayed after "word"
or "abbr"
info more information about the item, can be displayed in a
preview or popup window
kind single letter indicating the type of completion
icase when non-zero case is to be ignored when comparing
items to be equal; when omitted zero is used, thus
items that only differ in case are added
equal when non-zero, always treat this item to be equal when
comparing. Which means, "equal=1" disables filtering
of this item.
dup when non-zero this match will be added even when an
item with the same word is already present.
empty when non-zero this match will be added even when it is
an empty string
user_data custom data which is associated with the item and
available in v:completed_item; it can be any type;
defaults to an empty string

source

将complete提供的信息转移到vim的C结构中。

///@file: insexpand.c
/*
 * Build a popup menu to show the completion matches.
 * Returns the popup menu entry that should be selected. Returns -1 if nothing
 * should be selected.
 */
    static int
ins_compl_build_pum(void)
{
///....
		compl_match_array[i].pum_text = compl->cp_str;
	    compl_match_array[i].pum_kind = compl->cp_text[CPT_KIND];
	    compl_match_array[i].pum_info = compl->cp_text[CPT_INFO];
	    compl_match_array[i].pum_score = compl->cp_score;
///....
}

当选择某个complete选项时,vim(的C代码)会自动在previewwindow中更新对应的info信息。

///@file:popupmenu.c
		    for (p = pum_array[pum_selected].pum_info; *p != NUL; )
		    {
			e = vim_strchr(p, '\n');
			if (e == NULL)
			{
			    ml_append(lnum++, p, 0, FALSE);
			    break;
			}
			*e = NUL;
			ml_append(lnum++, p, (int)(e - p + 1), FALSE);
			*e = '\n';
			p = e + 1;
		    }

ycm

plugin

将completion.completions信息提供给vim内置的complete函数。

"@file: YouCompleteMe/autoload/youcompleteme.vim
function! s:Complete()
  " It's possible for us to be called (by our timer) when we're not _strictly_
  " in insert mode. This can happen when mode is temporarily switched, e.g.
  " due to Ctrl-r or Ctrl-o or a timer or something. If we're not in insert
  " mode _now_ do nothing (FIXME: or should we queue a timer ?)
  if count( [ 'i', 'R' ], mode() ) == 0
    return
  endif

  if s:completion.line != line( '.' )
    " Given
    "   scb: column where the completion starts before auto-wrapping
    "   cb: cursor column before auto-wrapping
    "   sca: column where the completion starts after auto-wrapping
    "   ca: cursor column after auto-wrapping
    " we have
    "   ca - sca = cb - scb
    "   sca = scb + ca - cb
    let s:completion.completion_start_column +=
          \ col( '.' ) - s:completion.column
  endif
  if len( s:completion.completions )
    let old_completeopt = &completeopt
    set completeopt+=noselect
    call complete( s:completion.completion_start_column,
                 \ s:completion.completions )
    let &completeopt = old_completeopt
  elseif pumvisible()
    call s:CloseCompletionMenu()
  endif
endfunction

client

ycm客户端从ycmd的回包中获得detailed_info字段,填充到info字段中。

# @file:YouCompleteMe/python/ycm/client/completion_request.py
def _GetCompletionInfoField( completion_data ): 
  info = completion_data.get( 'detailed_info', '' )
      
  if 'extra_data' in completion_data:
    docstring = completion_data[ 'extra_data' ].get( 'doc_string', '' )
    if docstring:
      if info:
        info += '\n' + docstring
      else:
        info = docstring

  # This field may contain null characters e.g. \x00 in Python docstrings. Vim
  # cannot evaluate such characters so they are removed.
  return info.replace( '\x00', '' )
def ConvertCompletionDataToVimData( completion_data ):
  # See :h complete-items for a description of the dictionary fields.
  extra_menu_info = completion_data.get( 'extra_menu_info', '' )
  preview_info = _GetCompletionInfoField( completion_data )

  # When we are using a popup for the preview_info, it needs to fit on the
  # screen alongside the extra_menu_info. Let's use some heuristics.  If the
  # length of the extra_menu_info is more than, say, 1/3 of screen, truncate it
  # and stick it in the preview_info.
  if vimsupport.UsingPreviewPopup():
    max_width = max( int( vimsupport.DisplayWidth() / 3 ), 3 )
    extra_menu_info_width = vimsupport.DisplayWidthOfString( extra_menu_info )
    if extra_menu_info_width > max_width:
      if not preview_info.startswith( extra_menu_info ):
        preview_info = extra_menu_info + '\n\n' + preview_info
      extra_menu_info = extra_menu_info[ : ( max_width - 3 ) ] + '...'

  return {
    'word'     : completion_data[ 'insertion_text' ],
    'abbr'     : completion_data.get( 'menu_text', '' ),
    'menu'     : extra_menu_info,
    'info'     : preview_info,
    'kind'     : ToUnicode( completion_data.get( 'kind', '' ) )[ :1 ].lower(),
    # Disable Vim filtering.
    'equal'    : 1,
    'dup'      : 1,
    'empty'    : 1,
    # We store the completion item extra_data as a string in the completion
    # user_data. This allows us to identify the _exact_ item that was completed
    # in the CompleteDone handler, by inspecting this item from v:completed_item
    #
    # We convert to string because completion user data items must be strings.
    #
    # Note: Not all versions of Vim support this (added in 8.0.1483), but adding
    # the item to the dictionary is harmless in earlier Vims.
    # Note: Since 8.2.0084 we don't need to use json.dumps() here.
    'user_data': json.dumps( completion_data.get( 'extra_data', {} ) )
  }  

ycmd

ycmd的Python脚本从llvm中获得detailed_info信息,对应ycm_core中的DetailedInfoForPreviewWindow字段。

#@file: YouCompleteMe/third_party/ycmd/ycmd/completers/cpp/clang_completer.py
def ConvertCompletionData( completion_data ):
  return responses.BuildCompletionData(
    insertion_text = completion_data.TextToInsertInBuffer(),
    menu_text = completion_data.MainCompletionText(),
    extra_menu_info = completion_data.ExtraMenuInfo(),
    kind = completion_data.kind_.name,
    detailed_info = completion_data.DetailedInfoForPreviewWindow(),
    extra_data = BuildExtraData( completion_data ) )

在ycm_core实现中

//YouCompleteMe/third_party/ycmd/cpp/ycm/ClangCompleter/CompletionData.h
  // This is used to show extra information in vim's preview window. This is the
  // window that vim usually shows at the top of the buffer. This should be used
  // for extra information about the completion.
  std::string DetailedInfoForPreviewWindow() const {
    return detailed_info_;
  }
///@file: YouCompleteMe/third_party/ycmd/cpp/ycm/ycm_core.cpp
  py::class_< CompletionData >( mod, "CompletionData" )
    .def( py::init<>() )
    .def( "TextToInsertInBuffer", &CompletionData::TextToInsertInBuffer )
    .def( "MainCompletionText", &CompletionData::MainCompletionText )
    .def( "ExtraMenuInfo", &CompletionData::ExtraMenuInfo )
    .def( "DetailedInfoForPreviewWindow",
          &CompletionData::DetailedInfoForPreviewWindow )
    .def( "DocString", &CompletionData::DocString )
    .def_readonly( "kind_", &CompletionData::kind_ )
    .def_readonly( "fixit_", &CompletionData::fixit_ );

outro

对于开始提到的函数参数是函数返回值,如果不嫌麻烦的话,可以再次触发一次即可(因为前面YCM的issue中,作者也说到了不太可能会增加单独显示指定函数原型的功能);
当不再需要preview window时,通过快捷键或者cmd(:pc)关闭即可(当然把光标定位到窗口内部在关闭当前窗口也可以)。

看似简单直观的功能,中间使用大量脚本/代码/模块/协议及职责分工,这可能就是通常所说的“哪有岁月静好,只是有人负重前行"吧。

posted on 2024-10-16 21:09  tsecer  阅读(9)  评论(0编辑  收藏  举报

导航