Skip to content

Commit

Permalink
Implement clang diagnostics/fixits service. Make it play with Vim Qui…
Browse files Browse the repository at this point in the history
…ckFix.

This is a second service now for which we are supposed to be providing feedback
as we are typing (the same is with semantic syntax highlighting).

Therefore, some refactoring had to take place in order to put these under a common
denominator: now there is a common, 'TextChangedI', event which we are now using to
hook all services on which are supposed to be running like this. This avoids code
duplication and makes it easier for future integrations.
  • Loading branch information
JBakamovic committed Feb 9, 2017
1 parent a00d5b6 commit 81a085c
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 74 deletions.
176 changes: 112 additions & 64 deletions core/.api.vimrc
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,88 @@ server_queue.put([0xFF, 0xFF, "shutdown_and_exit"])
EOF
endfunction

" --------------------------------------------------------------------------------------------------------------------------------------
"
" SOURCE CODE MODEL UTILITY FUNCTIONS
"
" --------------------------------------------------------------------------------------------------------------------------------------
" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Function: Y_SrcCodeModel_TextChangedIReset()
" Description: Resets variables to initial state.
" Dependency:
" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function! Y_SrcCodeModel_TextChangedIReset()
let s:y_prev_line = 0
let s:y_prev_col = 0
let s:y_prev_char = ''
endfunction

" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Function: Y_SrcCodeModel_TextChangedI()
" Description: A hook for services which are ought to be on 'TextChangedI' event (i.e. semantic highlight as you type).
" In order to minimize triggering the services after each and every character typed in, there is a
" Y_SrcCodeModel_TextChangedType() function which heuristicly gives us a hint if there was a big enough
" change for us to run the services or not.
" Dependency:
" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function! Y_SrcCodeModel_TextChangedI()
if Y_SrcCodeModel_TextChangedType()
call Y_SrcCodeHighlighter_Run()
call Y_SrcCodeDiagnostics_Run()
endif
endfunction

" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Function: Y_SrcCodeModel_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 'TextChangedI' 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_SrcCodeModel_TextChangedType()

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

" --------------------------------------------------------------------------------------------------------------------------------------
"
" SOURCE CODE MODEL API
Expand Down Expand Up @@ -876,16 +958,20 @@ endfunction
"
" --------------------------------------------------------------------------------------------------------------------------------------
" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Function: Y_SrcCodeHighlighter_Reset()
" Description: Resets variables to initial state.
" Function: Y_SrcCodeModel_Run(service_id, args)
" Description: Runs the specific service within the source code model (super)-service (i.e. syntax highlight, fixit, diagnostics, ...)
" Dependency:
" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function! Y_SrcCodeHighlighter_Reset()
let s:y_prev_line = 0
let s:y_prev_col = 0
let s:y_prev_char = ''
function! Y_SrcCodeModel_Run(service_id, args)
call insert(a:args, a:service_id)
call Y_ServerSendMsg(g:project_service_src_code_model['id'], a:args)
endfunction

" --------------------------------------------------------------------------------------------------------------------------------------
"
" SOURCE CODE HIGHLIGHT API
"
" --------------------------------------------------------------------------------------------------------------------------------------
" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Function: Y_SrcCodeHighlighter_Run()
" Description: Triggers the source code highlighting for current buffer.
Expand Down Expand Up @@ -924,19 +1010,6 @@ EOF
endif
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

" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Function: Y_SrcCodeHighlighter_Apply()
" Description: Apply the results of source code highlighting for given filename.
Expand All @@ -955,55 +1028,30 @@ function! Y_SrcCodeHighlighter_Apply(filename, syntax_file)
endif
endfunction

" --------------------------------------------------------------------------------------------------------------------------------------
"
" SOURCE CODE DIAGNOSTICS API
"
" --------------------------------------------------------------------------------------------------------------------------------------
" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" 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.
" Function: Y_SrcCodeDiagnostics_Run()
" Description: Triggers the source code diagnostics for current buffer.
" 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
function! Y_SrcCodeDiagnostics_Run()
if g:project_service_src_code_model['services']['diagnostics']['enabled']
let l:current_buffer = bufnr('%')
call Y_SrcCodeModel_Run(g:project_service_src_code_model['services']['diagnostics']['id'], [l:current_buffer])
endif
endfunction

" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Function: Y_SrcCodeDiagnostics_Apply()
" Description: Populates the quickfix window with source code diagnostics.
" Dependency:
" """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function! Y_SrcCodeDiagnostics_Apply(diagnostics)
call setqflist(a:diagnostics, 'r')
endfunction

" --------------------------------------------------------------------------------------------------------------------------------------
Expand Down
15 changes: 13 additions & 2 deletions core/.autocommands.vimrc
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,25 @@ augroup yavide_src_code_formatting_group
autocmd BufWritePost *.cpp,*.cc,*.c,*.h,*.hh,*.hpp call Y_SrcCodeFormatter_Run()
augroup END

augroup yavide_src_code_model_group
autocmd!
autocmd BufEnter *.cpp,*.cc,*.c,*.h,*.hh,*.hpp call Y_SrcCodeModel_TextChangedIReset()
autocmd TextChangedI *.cpp,*.cc,*.c,*.h,*.hh,*.hpp call Y_SrcCodeModel_TextChangedI()
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_src_code_diagnostics_group
autocmd!
autocmd BufEnter *.cpp,*.cc,*.c,*.h,*.hh,*.hpp call Y_SrcCodeDiagnostics_Run()
autocmd BufWritePost *.cpp,*.cc,*.c,*.h,*.hh,*.hpp call Y_SrcCodeDiagnostics_Run()
autocmd TextChanged *.cpp,*.cc,*.c,*.h,*.hh,*.hpp call Y_SrcCodeDiagnostics_Run()
augroup END

augroup yavide_layout_mgmt_group
Expand Down
3 changes: 2 additions & 1 deletion core/.globals.vimrc
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ let g:project_supported_types = {
" --------------------------------------------------------------------------------------------------------------------------------------
let g:project_service_src_code_model = { 'id' : 0, 'enabled' : 1, 'start' : function("Y_SrcCodeModel_Start"), 'stop' : function("Y_SrcCodeModel_Stop"),
\ 'services' : {
\ 'semantic_syntax_highlight' : { 'id' : 0, 'enabled' : 1 }
\ 'semantic_syntax_highlight' : { 'id' : 0, 'enabled' : 1 },
\ 'diagnostics' : { 'id' : 1, 'enabled' : 1 }
\ }
\ }
let g:project_service_project_builder = { 'id' : 1, 'enabled' : 1, 'start' : function("Y_ProjectBuilder_Start"), 'stop' : function("Y_ProjectBuilder_Stop") }
Expand Down
Empty file.
12 changes: 12 additions & 0 deletions core/services/diagnostics/diagnostics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import logging
import time

class Diagnostics():
def __init__(self, parser, callback = None):
self.parser = parser
self.callback = callback

def __call__(self, args):
diagnostics_iter = self.parser.get_diagnostics()
if self.callback:
self.callback(diagnostics_iter, args)
15 changes: 9 additions & 6 deletions core/services/parser/clang_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def __init__(self):
self.contents_filename = ''
self.original_filename = ''
self.ast_nodes_list = []
self.translation_unit = None
self.index = clang.cindex.Index.create()
self.default_args = ['-x', 'c++', '-std=c++14'] + get_system_includes()

Expand All @@ -79,21 +80,23 @@ def run(self, contents_filename, original_filename, compiler_args, project_root_
logging.info('User-provided compiler args = {0}'.format(compiler_args))
logging.info('Compiler working-directory = {0}'.format(project_root_directory))
try:
translation_unit = self.index.parse(
self.translation_unit = self.index.parse(
path = self.contents_filename,
args = self.default_args + compiler_args + ['-working-directory=' + project_root_directory],
options = clang.cindex.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD # TODO CXTranslationUnit_KeepGoing?
)

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

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

except:
logging.error(sys.exc_info()[0])

def get_diagnostics(self):
for diag in self.translation_unit.diagnostics:
logging.debug('Parsing diagnostics: ' + str(diag))
return self.translation_unit.diagnostics

def get_ast_node_list(self):
return self.ast_nodes_list

Expand Down
5 changes: 4 additions & 1 deletion core/services/source_code_model_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
from services.parser.clang_parser import ClangParser
from syntax_highlighter.syntax_highlighter import SyntaxHighlighter
from services.vim.syntax_generator import VimSyntaxGenerator
from diagnostics.diagnostics import Diagnostics
from services.vim.quickfix_diagnostics import VimQuickFixDiagnostics

class SourceCodeModel(YavideService):
def __init__(self, server_queue, yavide_instance):
YavideService.__init__(self, server_queue, yavide_instance)
self.parser = ClangParser()
self.service = {
0x0 : SyntaxHighlighter(self.parser, VimSyntaxGenerator(yavide_instance, "/tmp/yavideSyntaxFile.vim"))
0x0 : SyntaxHighlighter(self.parser, VimSyntaxGenerator(yavide_instance, "/tmp/yavideSyntaxFile.vim")),
0x1 : Diagnostics(self.parser, VimQuickFixDiagnostics(yavide_instance))
}

def unknown_service(self):
Expand Down
58 changes: 58 additions & 0 deletions core/services/vim/quickfix_diagnostics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import logging
from common.yavide_utils import YavideUtils

class VimQuickFixDiagnostics():
def __init__(self, yavide_instance):
self.yavide_instance = yavide_instance

def __call__(self, diagnostics_iter, args):
def clang_severity_to_quickfix_type(severity):
# Clang severity | Vim Quickfix type
# ----------------------------------
# Ignored = 0 0 ()
# Note = 1 I (info)
# Warning = 2 W (warning)
# Error = 3 E (error)
# Fatal = 4 other ()
# ----------------------------------
if severity == 0:
return '0'
elif severity == 1:
return 'I'
elif severity == 2:
return 'W'
elif severity == 3:
return 'E'
elif severity == 4:
return 'other'
return '0'

diagnostics = []
for d in diagnostics_iter:
diagnostics.append(
"{'bufnr': '" + str(args[0]) + "', " +
"'lnum': '" + str(d.location.line) + "', " +
"'col': '" + str(d.location.column) + "', " +
"'type': '" + clang_severity_to_quickfix_type(d.severity) + "', " +
"'text': '" + d.category_name + " | " + str(d.spelling).replace("'", r"") + "'}"
)

fixits = "Hint:"
for f in d.fixits:
fixits += \
" Try using '" + str(f.value) + "' instead. [col=" + \
str(f.range.start.column) + ":" + str(f.range.end.column) + "]"
# TODO How to handle multiline quickfix entries? It would be nice show each fixit in its own line.

if len(d.fixits):
diagnostics.append(
"{'bufnr': '" + str(args[0]) + "', " +
"'lnum': '" + str(d.location.line) + "', " +
"'col': '" + str(d.location.column) + "', " +
"'type': 'I', " +
"'text': '" + str(fixits).replace("'", r"") + "'}"
)

YavideUtils.call_vim_remote_function(self.yavide_instance, "Y_SrcCodeDiagnostics_Apply(" + str(diagnostics).replace('"', r"") + ")")
logging.debug("Diagnostics: " + str(diagnostics))

0 comments on commit 81a085c

Please sign in to comment.