Skip to content
This repository has been archived by the owner on Jan 5, 2023. It is now read-only.

Commit

Permalink
Implemented new, libclang-based, back-end for syntax highlighting s…
Browse files Browse the repository at this point in the history
…ervice.

This replaces previous `ctags`-based approach which was falling short in some cases and especially with modern C++
codebases. `libclang` compared to `ctags` is a full-featured C++ parser and, when configured properly, is able to
provide us with much more complete and correct results.

Not only that this new back-end is used for syntax highlighting, but it will be certainly reused to implement a whole
other set of features such as refactoring tools, fix-its, indexers, class browsers and alike.

Integration was somewhat more complex and has been changed, so right now syntax highlighting:
    * Is triggered on (almost) each text change (highlighting as you type).
        * Previously, it was only done after saving changes done on a file.
        * Triggering syntax highlighting after each and every character would be too excesive and
          for bigger and more complex files probably not even feasible. Hence, a simple heuristic method
          has been implemented which tries to identify situations when it is not absolutely necessary to
          trigger highlighting (i.e. still typing a word or inserting whitespaces in a way that it doesn't
          affect syntax highlighting). Although not perfect, this method certainly minimizes the unnecessary
          traffic.
    * Requires additional configuration in a form of project-specific compiler flags to get best results:
        * i.e. include paths, -Wall, -Werror and whatever else is used
        * Configuration is done via `g:project_compiler_args` found in `.yavide_proj` in project root directory.
        * Necessary system includes such as `/usr/bin/../lib64/clang/3.8.0/include` are detected automatically and
          are not required to be part of the configuration.
    * Requires an update of colorscheme as additional highlight groups have been introduced.
        * `yaflandia` colorscheme has been accordingly updated
        * Should one want to use some other colorscheme, a similar update will be needed in order to make a full
          use of syntax highlighting service.

Quirks:
    * Scrolling will become really really slow if `cursorline` is set. This is unfortunately very old, very known and
      very annoying Vim issue. One can circumvent it by turning it off (`set nocursorline`) for bigger files or turning
      it off permanently if one does not require such a feature.
  • Loading branch information
JBakamovic committed Dec 29, 2016
1 parent f36050d commit 9d44c4b
Show file tree
Hide file tree
Showing 14 changed files with 577 additions and 286 deletions.
5 changes: 4 additions & 1 deletion README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
* [FAQ](#faq)

# Changes
* 28th of December, 2016
* Implemented Clang-based source code [syntax highlighting](docs/services_framework.md#syntax-highlighting) service
(run `cd <yavide_install_dir>/colors/yaflandia && git pull` to get required colorscheme changes)
* 1st of July, 2016
* Implemented new generic client-server (async) [framework](docs/services_framework.md#framework) which enables dispatching any kind of operations to run in a separate
non-blocking background processes (so called [services](docs/services_framework.md#services)) and upon whose completion results can be reported back to the server ('Yavide').
Expand Down Expand Up @@ -44,7 +47,7 @@ See [some GIFs in action](docs/services_framework.md).
* Backed by real C/C++ compiler back-end to ensure total correctness
* Source code navigation
* Featuring a fully automated tag generation system which keeps the symbol database up-to-date
* Source code syntax highlighting
* Source code syntax highlighting based on `libclang`
* Providing more rich syntax highlighting support than the one provided originally by `Vim`
* Source code auto-formatting
* `clang-formatter` support
Expand Down
141 changes: 126 additions & 15 deletions core/.api.vimrc
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
" --------------------------------------------------------------------------------------------------------------------------------------
"
" SCRIPT LOCAL VARIABLES
"
" --------------------------------------------------------------------------------------------------------------------------------------
let s:y_prev_line = 0
let s:y_prev_col = 0
let s:y_prev_char = ''

" --------------------------------------------------------------------------------------------------------------------------------------
"
" YAVIDE VIMSCRIPT UTILS
"
" --------------------------------------------------------------------------------------------------------------------------------------
" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Function: Y_Utils_AppendToFile()
" Description: Writes 'lines' to 'file'
" Dependency:
" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function! s:Y_Utils_AppendToFile(file, lines)
call writefile(readfile(a:file) + a:lines, a:file)
endfunction
Expand Down Expand Up @@ -132,6 +146,7 @@ function! s:Y_Project_Create(bEmptyProject)
call add(l:project_settings, 'let g:' . 'project_name = ' . "\'" . l:project_name . "\'")
call add(l:project_settings, 'let g:' . 'project_category = ' . l:project_category)
call add(l:project_settings, 'let g:' . 'project_type = ' . l:project_type)
call add(l:project_settings, 'let g:' . 'project_compiler_args = ' . "\'\'")
call writefile(l:project_settings, g:project_configuration_filename)
return 0
endif
Expand Down Expand Up @@ -833,10 +848,7 @@ endfunction
" Dependency:
" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function! Y_SrcCodeHighlighter_Start()
"python import sys
"python import vim
"python sys.argv = ['', vim.eval('l:currentBuffer'), "/tmp", "-n", "-c", "-s", "-e", "-ev", "-u", "-cusm", "-lv", "-vd", "-fp", "-fd", "-t", "-m", "-efwd"]
call Y_ServerStartService(g:project_service_src_code_highlighter['id'], 'some param')
call Y_ServerStartService(g:project_service_src_code_highlighter['id'], 'dummy_param')
endfunction

" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
Expand All @@ -848,14 +860,65 @@ function! Y_SrcCodeHighlighter_Stop()
call Y_ServerStopService(g:project_service_src_code_highlighter['id'])
endfunction

" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Function: Y_SrcCodeHighlighter_Reset()
" Description: Resets variables to initial state.
" Dependency:
" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function! Y_SrcCodeHighlighter_Reset()
let s:y_prev_line = 0
let s:y_prev_col = 0
let s:y_prev_char = ''
endfunction

" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Function: Y_SrcCodeHighlighter_Run()
" Description: Triggers the source code highlighting for current buffer.
" Dependency:
" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function! Y_SrcCodeHighlighter_Run()
let l:currentBuffer = expand('%:p')
call Y_ServerSendMsg(g:project_service_src_code_highlighter['id'], l:currentBuffer)
let l:current_buffer = expand('%:p')
let l:contents_filename = l:current_buffer
let l:compiler_args = g:project_compiler_args

" If buffer contents are modified but not saved, we need to serialize contents of the current buffer into temporary file.
let l:bufferModified = getbufvar(bufnr('%'), '&modified')
if l:bufferModified == 1
let l:contents_filename = '/tmp/yavideTempBufferContents'

python << EOF
import vim
import os

# Serialize the contents
temp_file = open(vim.eval('l:contents_filename'), "w", 0)
temp_file.writelines(line + '\n' for line in vim.current.buffer)

# Append additional include path to the compiler args which points to the parent directory of current buffer.
# * This needs to be done because we will be doing analysis on tmp file which is outside the project directory.
# By doing this, we might invalidate header includes for that particular file and therefore trigger unnecessary
# Clang parsing errors.
# * An alternative would be to generate tmp files in original location but that would pollute project directory and
# potentially would not play well with other tools (indexer, version control, etc.).
vim.command("let l:compiler_args .= '" + " -I" + os.path.dirname(vim.eval("l:current_buffer")) + "'")
EOF

endif

call Y_ServerSendMsg(g:project_service_src_code_highlighter['id'], [l:contents_filename, l:current_buffer, l:compiler_args, g:project_root_directory])
endfunction

" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Function: Y_SrcCodeHighlighter_RunConditionally()
" Description: Conditionally runs the source code highlighter for current buffer.
" Tries to minimize triggering the syntax highlighter in some certain cases
" when it is not absolutely unnecessary (i.e. when typing letter after letter).
" Dependency:
" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function! Y_SrcCodeHighlighter_RunConditionally()
if Y_SrcCodeHighlighter_CheckTextChangedType()
call Y_SrcCodeHighlighter_Run()
endif
endfunction

" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
Expand All @@ -864,11 +927,8 @@ endfunction
" Dependency:
" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function! Y_SrcCodeHighlighter_Apply(filename, syntax_file)
let l:currentBuffer = expand('%:p')
if l:currentBuffer == a:filename
" Clear previously generated syntax rules
execute('syntax clear yavideCppNamespace yavideCppClass yavideCppStructure yavideCppEnum yavideCppEnumValue yavideCppUnion yavideCppClassStructUnionMember yavideCppLocalVariable yavideCppVariableDefinition yavideCppFunctionPrototype yavideCppFunctionDefinition yavideCppMacro yavideCppTypedef yavideCppExternForwardDeclaration')

let l:current_buffer = expand('%:p')
if l:current_buffer == a:filename
" Apply the syntax highlighting rules
execute('source '.a:syntax_file)

Expand All @@ -879,6 +939,57 @@ function! Y_SrcCodeHighlighter_Apply(filename, syntax_file)
endif
endfunction

" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Function: Y_SrcCodeHighlighter_CheckTextChangedType()
" Description: Implements simple heuristics to detect what kind of text change has taken place in current buffer.
" This is useful if one wants to install handler for 'TextChanged' events but not necessarily
" act on each of those because they are triggered rather frequently. This is by no means a perfect
" implementation but it tries to give good enough approximations. It probably can be improved and specialized further.
" Returns 0 for a non-interesting change. Otherwise, some value != 0.
" Dependency:
" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function! Y_SrcCodeHighlighter_CheckTextChangedType()

let l:textChangeType = 0 " no interesting change (i.e. typed in a letter after letter)

python << EOF
import vim

# Uncomment to enable debugging
#import logging
#logging.basicConfig(filename='/tmp/temp', filemode='w', level=logging.INFO)
#logging.info("y_prev_line = '{0}' y_prev_col = '{1}' y_prev_char = '{2}'. curr_line = '{3}' curr_col = '{4}' curr_char = '{5}'".format(vim.eval('s:y_prev_line'), vim.eval('s:y_prev_col'), vim.eval('s:y_prev_char'), curr_line, curr_col, curr_char))

curr_line = int(vim.eval("line('.')"))
curr_col = int(vim.eval("col('.')"))
curr_char = str(vim.eval("getline('.')[col('.')-2]"))

if curr_line > int(vim.eval('s:y_prev_line')):
vim.command("let l:textChangeType = 1") #logging.info("Switched to next line!")
elif curr_line < int(vim.eval('s:y_prev_line')):
vim.command("let l:textChangeType = 2") #logging.info("Switched to previous line!")
else:
if not curr_char.isalnum():
if str(vim.eval('s:y_prev_char')).isalnum():
vim.command("let l:textChangeType = 3") #logging.info("Delimiter!")
else:
if curr_col > int(vim.eval('s:y_prev_col')): #logging.info("---> '{0}'".format(vim.eval("getline('.')")[curr_col-1:]))
if len(vim.eval("getline('.')")[curr_col-1:]) > 0:
vim.command("let l:textChangeType = 3")
elif curr_col < int(vim.eval('s:y_prev_col')): #logging.info("<--- '{0}'".format(vim.eval("getline('.')")[:curr_col-1]))
if len(vim.eval("getline('.')")[curr_col-1:]) > 0:
vim.command("let l:textChangeType = 3")

vim.command('let s:y_prev_line = %s' % curr_line)
vim.command('let s:y_prev_col = %s' % curr_col)
vim.command('let s:y_prev_char = "%s"' % curr_char.replace('"', "\"").replace("\\", "\\\\"))

EOF

return l:textChangeType

endfunction

" --------------------------------------------------------------------------------------------------------------------------------------
"
" STATIC ANALYSIS API
Expand Down Expand Up @@ -1003,8 +1114,8 @@ endfunction
" Dependency:
" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function! Y_SrcCodeFormatter_Run()
let l:currentBuffer = expand('%:p')
call Y_ServerSendMsg(g:project_service_src_code_formatter['id'], l:currentBuffer)
let l:current_buffer = expand('%:p')
call Y_ServerSendMsg(g:project_service_src_code_formatter['id'], l:current_buffer)
endfunction

" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
Expand All @@ -1013,8 +1124,8 @@ endfunction
" Dependency:
" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function! Y_SrcCodeFormatter_Apply(filename)
let l:currentBuffer = expand('%:p')
if l:currentBuffer == a:filename
let l:current_buffer = expand('%:p')
if l:current_buffer == a:filename
execute('e')
endif
endfunction
Expand Down
4 changes: 4 additions & 0 deletions core/.autocommands.vimrc
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@ augroup END

augroup yavide_src_code_highlight_group
autocmd!
autocmd BufEnter * if index(['c', 'cpp'], &ft) < 0 | call clearmatches() | endif " We need to clear matches when entering non-Cxx buffers
autocmd BufEnter *.cpp,*.cc,*.c,*.h,*.hh,*.hpp call Y_SrcCodeHighlighter_Reset()
autocmd BufEnter *.cpp,*.cc,*.c,*.h,*.hh,*.hpp call Y_SrcCodeHighlighter_Run()
autocmd BufWritePost *.cpp,*.cc,*.c,*.h,*.hh,*.hpp call Y_SrcCodeHighlighter_Run()
autocmd TextChanged *.cpp,*.cc,*.c,*.h,*.hh,*.hpp call Y_SrcCodeHighlighter_Run()
autocmd TextChangedI *.cpp,*.cc,*.c,*.h,*.hh,*.hpp call Y_SrcCodeHighlighter_RunConditionally()
augroup END

augroup yavide_layout_mgmt_group
Expand Down
1 change: 1 addition & 0 deletions core/.globals.vimrc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ let g:project_java_tags_filename = '.java_tags'
let g:project_cxx_tags = ''
let g:project_cxx_tags_filename = '.cxx_tags'
let g:project_cscope_db_filename = 'cscope.out'
let g:project_compiler_args = ''
let g:project_env_build_preproces_command = ''
let g:project_env_build_command = ''
let g:project_env_src_code_format_config = '.clang-format'
Expand Down
118 changes: 118 additions & 0 deletions core/services/syntax_highlighter/clang_tokenizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import sys
import logging
import subprocess
import clang.cindex
from services.syntax_highlighter.token_identifier import TokenIdentifier

def get_system_includes():
output = subprocess.Popen(["g++", "-v", "-E", "-x", "c++", "-"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
pattern = ["#include <...> search starts here:", "End of search list."]
output = str(output)
return output[output.find(pattern[0]) + len(pattern[0]) : output.find(pattern[1])].replace(' ', '-I').split('\\n')

class ClangTokenizer():
def __init__(self):
self.filename = ''
self.token_list = []
self.index = clang.cindex.Index.create()
self.default_args = ['-x', 'c++', '-std=c++14'] + get_system_includes()

def run(self, filename, compiler_args, project_root_directory):
self.filename = filename
self.token_list = []
logging.info('Filename = {0}'.format(self.filename))
logging.info('Default args = {0}'.format(self.default_args))
logging.info('User-provided compiler args = {0}'.format(compiler_args))
logging.info('Compiler working-directory = {0}'.format(project_root_directory))
translation_unit = self.index.parse(
path = self.filename,
args = self.default_args + compiler_args + ['-working-directory=' + project_root_directory],
options = clang.cindex.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD
)

diag = translation_unit.diagnostics
for d in diag:
logging.info('Parsing error: ' + str(d))

logging.info('Translation unit: '.format(translation_unit.spelling))
self.__visit_all_nodes(translation_unit.cursor)

def get_token_list(self):
return self.token_list

def get_token_id(self, token):
if token.referenced:
return ClangTokenizer.to_token_id(token.referenced.kind)
return ClangTokenizer.to_token_id(token.kind)

def get_token_name(self, token):
if (token.referenced):
return token.referenced.spelling
else:
return token.spelling

def get_token_line(self, token):
return token.location.line

def get_token_column(self, token):
return token.location.column

def dump_token_list(self):
for idx, token in enumerate(self.token_list):
logging.debug(
'%-12s' % ('[' + str(token.location.line) + ', ' + str(token.location.column) + ']') +
'%-40s ' % str(token.spelling) +
'%-40s ' % str(token.kind) +
('%-40s ' % str(token.referenced.spelling) if (token.kind.is_reference()) else '') +
('%-40s ' % str(token.referenced.kind) if (token.kind.is_reference()) else ''))

def __visit_all_nodes(self, node):
for n in node.get_children():
if n.location.file and n.location.file.name == self.filename:
self.token_list.append(n)
self.__visit_all_nodes(n)

@staticmethod
def to_token_id(kind):
if (kind == clang.cindex.CursorKind.NAMESPACE):
return TokenIdentifier.getNamespaceId()
if (kind in [clang.cindex.CursorKind.CLASS_DECL, clang.cindex.CursorKind.CLASS_TEMPLATE, clang.cindex.CursorKind.CLASS_TEMPLATE_PARTIAL_SPECIALIZATION]):
return TokenIdentifier.getClassId()
if (kind == clang.cindex.CursorKind.STRUCT_DECL):
return TokenIdentifier.getStructId()
if (kind == clang.cindex.CursorKind.ENUM_DECL):
return TokenIdentifier.getEnumId()
if (kind == clang.cindex.CursorKind.ENUM_CONSTANT_DECL):
return TokenIdentifier.getEnumValueId()
if (kind == clang.cindex.CursorKind.UNION_DECL):
return TokenIdentifier.getUnionId()
if (kind == clang.cindex.CursorKind.FIELD_DECL):
return TokenIdentifier.getFieldId()
if (kind == clang.cindex.CursorKind.VAR_DECL):
return TokenIdentifier.getLocalVariableId()
if (kind in [clang.cindex.CursorKind.FUNCTION_DECL, clang.cindex.CursorKind.FUNCTION_TEMPLATE]):
return TokenIdentifier.getFunctionId()
if (kind in [clang.cindex.CursorKind.CXX_METHOD, clang.cindex.CursorKind.CONSTRUCTOR, clang.cindex.CursorKind.DESTRUCTOR]):
return TokenIdentifier.getMethodId()
if (kind == clang.cindex.CursorKind.PARM_DECL):
return TokenIdentifier.getFunctionParameterId()
if (kind == clang.cindex.CursorKind.TEMPLATE_TYPE_PARAMETER):
return TokenIdentifier.getTemplateTypeParameterId()
if (kind == clang.cindex.CursorKind.TEMPLATE_NON_TYPE_PARAMETER):
return TokenIdentifier.getTemplateNonTypeParameterId()
if (kind == clang.cindex.CursorKind.TEMPLATE_TEMPLATE_PARAMETER):
return TokenIdentifier.getTemplateTemplateParameterId()
if (kind == clang.cindex.CursorKind.MACRO_DEFINITION):
return TokenIdentifier.getMacroDefinitionId()
if (kind == clang.cindex.CursorKind.MACRO_INSTANTIATION):
return TokenIdentifier.getMacroInstantiationId()
if (kind in [clang.cindex.CursorKind.TYPEDEF_DECL, clang.cindex.CursorKind.TYPE_ALIAS_DECL]):
return TokenIdentifier.getTypedefId()
if (kind == clang.cindex.CursorKind.NAMESPACE_ALIAS):
return TokenIdentifier.getNamespaceAliasId()
if (kind == clang.cindex.CursorKind.USING_DIRECTIVE):
return TokenIdentifier.getUsingDirectiveId()
if (kind == clang.cindex.CursorKind.USING_DECLARATION):
return TokenIdentifier.getUsingDeclarationId()
return TokenIdentifier.getUnsupportedId()

Loading

0 comments on commit 9d44c4b

Please sign in to comment.