# -*- coding: utf-8 -*-
#
# This file is part of NINJA-IDE (http://ninja-ide.org).
#
# NINJA-IDE is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# any later version.
#
# NINJA-IDE is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with NINJA-IDE; If not, see <http://www.gnu.org/licenses/>.
# DISCLAIMER ABOUT READING THIS CODE:
# We are not responsible for any kind of mental or emotional
# damage that may arise from reading this code.
import re
import token as tkn
from tokenize import generate_tokens, TokenError
try:
from StringIO import StringIO
except:
from io import StringIO # lint:ok
from ninja_ide.core import settings
from ninja_ide.gui.editor import helpers
from ninja_ide.tools.completion import model
from ninja_ide.tools.completion import analyzer
from ninja_ide.tools.completion import completer
from ninja_ide.tools.completion import completion_daemon
#Because my python doesn't have it, and is not in the web docs either
#but trust me, it exists!
_TOKEN_NL = 54
class CodeCompletion(object):
def __init__(self):
self.analyzer = analyzer.Analyzer()
self.cdaemon = completion_daemon.CompletionDaemon()
# Set modules reference to model
model.MODULES = self.cdaemon.modules
self.module_id = None
self.patIndent = re.compile('^\s+')
self.patClass = re.compile("class (\w+?)\(")
self.patFunction = re.compile("(\w+?)\(")
self.patWords = re.compile('\W+')
self._valid_op = (')', '}', ']')
self._invalid_op = ('(', '{', '[')
self._invalid_words = ('if', 'elif', 'for', 'while', 'in', 'return',
'and', 'or', 'del', 'except', 'from', 'import', 'is', 'print',
'super', 'yield')
self.keywords = settings.SYNTAX['python']['keywords']
def unload_module(self):
self.cdaemon.unload_module(self.module_id)
def analyze_file(self, path, source=None, indent=settings.INDENT,
useTabs=settings.USE_TABS):
if source is None:
with open(path) as f:
source = f.read()
split_last_lines = source.rsplit('\n', 2)
if len(split_last_lines) > 1 and \
split_last_lines[-2].endswith(':') and split_last_lines[-1] == '':
indent = helpers.get_indentation(split_last_lines[-2], indent,
useTabs)
source += '%spass;' % indent
self.module_id = path
if not self.cdaemon.daemon.is_alive():
completion_daemon.shutdown_daemon()
del self.cdaemon
self.cdaemon = completion_daemon.CompletionDaemon()
# Set modules reference to model
model.MODULES = self.cdaemon.modules
module = self.cdaemon.get_module(self.module_id)
module = self.analyzer.analyze(source, module)
self.cdaemon.inspect_module(self.module_id, module)
def _tokenize_text(self, code):
# TODO Optimization, only iterate until the previous line of a class??
token_code = []
try:
for tkn_type, tkn_str, pos, _, line, \
in generate_tokens(StringIO(code).readline):
token_code.append((tkn_type, tkn_str, pos, line))
except TokenError:
#This is an expected situation, where i don't want to do anything
#possible an unbalanced brace like: func(os.p| (| = cursor-end)
pass
except IndentationError:
return []
while token_code[-1][0] in (tkn.ENDMARKER, tkn.DEDENT, tkn.NEWLINE):
token_code.pop()
return token_code
def _search_for_scope(self, token_code):
if not token_code or not token_code[-1][3].startswith(' '):
return None
scopes = []
indent = self.patIndent.match(token_code[-1][3])
if indent is not None:
indent = len(indent.group())
else:
indent = 0
previous_line = ('', '')
keep_exploring = True
iterate = reversed(token_code)
line = iterate.next()
while keep_exploring:
is_indented = line[3].startswith(' ')
is_definition = line[1] in ('def', 'class')
#Skip lines that are not def or class
if is_indented and is_definition:
new_indent = self.patIndent.match(line[3])
if new_indent is not None:
new_indent = len(new_indent.group())
#We only care about the function where we are
if new_indent < indent:
indent = new_indent
#We need the "previous_line" because we are exploring
#the tokens backwards, and when we reach to def or class
#the actual name was in the line before.
scopes.insert(0, previous_line)
elif not is_indented and is_definition:
scopes.insert(0, previous_line)
keep_exploring = False
previous_line = line[1]
try:
line = iterate.next()
except StopIteration:
keep_exploring = False
return scopes
def _search_for_completion_segment(self, token_code):
tokens = []
keep_iter = True
iterate = reversed(token_code)
while keep_iter:
try:
value = iterate.next()
if value[0] in (tkn.NEWLINE, tkn.INDENT, tkn.DEDENT):
keep_iter = False
tokens.append(value)
except:
keep_iter = False
segment = ''
brace_stack = 0
first_element = True
for t in tokens:
token_str = t[1]
if token_str in self._invalid_words and not first_element:
break
elif token_str in self._valid_op:
if brace_stack == 0:
segment = token_str + segment
brace_stack += 1
elif token_str in self._invalid_op:
brace_stack -= 1
if brace_stack == 0:
segment = token_str + segment
continue
first_element = False
if brace_stack != 0:
continue
op = t[0]
if (op == tkn.NAME) or token_str == '.':
segment = token_str + segment
elif op == tkn.OP:
break
return segment
def get_prefix(self, code, offset):
"""Return the prefix of the word under the cursor and a boolean
saying if it is a valid completion area."""
token_code = self._tokenize_text(code[:offset])
var_segment = self._search_for_completion_segment(token_code)
words_final = var_segment.rsplit('.', 1)
final_word = ''
if not var_segment.endswith('.') and len(words_final) > 1:
final_word = words_final[1].strip()
elif (var_segment != "") and len(words_final) == 1:
final_word = words_final[0].strip()
return final_word, (var_segment != "")
def get_completion(self, code, offset):
token_code = self._tokenize_text(code[:offset])
scopes = self._search_for_scope(token_code)
# Find section to attempt for code completion (var_segment)
var_segment = self._search_for_completion_segment(token_code)
words = var_segment.split('.', 1)
words_final = var_segment.rsplit('.', 1)
# Main_attribute is the stem off of which to search for completions
# i.e. in exec(ninja_ide.gui. ..., "ninja_ide.gui.main_panel"
# would be main_attribute.
main_attribute = words[0].strip().split('(', 1)
attr_name = main_attribute[0]
word = ''
final_word = ''
if var_segment.count(".") > 0:
word = words[1].strip()
if not var_segment.endswith('.') and len(words_final) > 1:
final_word = words_final[1].strip()
word = word.rsplit('.', 1)[0].strip()
if final_word == word:
word = ''
self.cdaemon.lock.acquire()
module = self.cdaemon.get_module(self.module_id)
if module:
imports = module.get_imports()
result = module.get_type(attr_name, word, scopes)
else:
result = {'found': False, 'type': None}
self.cdaemon.lock.release()
if result['found'] and result['type'] is not None:
prefix = attr_name
if result['type'] != attr_name:
prefix = result['type']
word = final_word
to_complete = "%s.%s" % (prefix, word)
if result.get('main_attr_replace', False):
to_complete = var_segment.replace(attr_name, result['type'], 1)
imports = [imp.split('.')[0] for imp in imports]
data = completer.get_all_completions(to_complete, imports)
# Move system attributes beginning in '__' (built_in_attribs)
# to the end of the list.
built_in_attribs = [d for d in data.get('attributes', [])
if d[:2] == '__']
if built_in_attribs:
for i in built_in_attribs:
data['attributes'].remove(i)
data['attributes'] += built_in_attribs
if data:
return data
else:
result = {'found': None, 'type': None}
if result['type'] is not None and len(result['type']) > 0:
data = {'attributes': result['type']['attributes'],
'functions': result['type']['functions']}
else:
clazzes = sorted(set(self.patClass.findall(code)))
funcs = sorted(set(self.patFunction.findall(code)))
attrs = sorted(set(self.patWords.split(code)))
if final_word in attrs:
attrs.remove(final_word)
if attr_name in attrs:
attrs.remove(attr_name)
filter_attrs = lambda x: (x not in funcs) and \
not x.isdigit() and (x not in self.keywords)
attrs = list(filter(filter_attrs, attrs))
funcs = [x for x in funcs if x not in clazzes]
data = {'attributes': attrs,
'functions': funcs,
'classes': clazzes}
return data