Text plugin basis with plugin for suggestions/completions. The suggest plugin works for imported global variables, methods, modules and module members. For example typing:
import Blender from Blender import * | <- cursor here suggests globals Blender.Draw.gl| <- cursor here suggests all Draw members starting gl Currently suggestions are listed in the console when the space is redrawn but will be presented as a menu-style list soon. Also to add are shortcut/activation keys to allow plugins to respond to certain key strokes.
This commit is contained in:
234
release/scripts/textplugin_suggest.py
Normal file
234
release/scripts/textplugin_suggest.py
Normal file
@@ -0,0 +1,234 @@
|
||||
#!BPY
|
||||
"""
|
||||
Name: 'Suggest'
|
||||
Blender: 243
|
||||
Group: 'TextPlugin'
|
||||
Tooltip: 'Suggests completions for the word at the cursor in a python script'
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from Blender import Text
|
||||
from StringIO import StringIO
|
||||
from inspect import *
|
||||
from tokenize import generate_tokens
|
||||
import token
|
||||
|
||||
TK_TYPE = 0
|
||||
TK_TOKEN = 1
|
||||
TK_START = 2 #(srow, scol)
|
||||
TK_END = 3 #(erow, ecol)
|
||||
TK_LINE = 4
|
||||
TK_ROW = 0
|
||||
TK_COL = 1
|
||||
|
||||
keywords = ['and', 'del', 'from', 'not', 'while', 'as', 'elif', 'global',
|
||||
'or', 'with', 'assert', 'else', 'if', 'pass', 'yield',
|
||||
'break', 'except', 'import', 'print', 'class', 'exec', 'in',
|
||||
'raise', 'continue', 'finally', 'is', 'return', 'def', 'for',
|
||||
'lambda', 'try' ]
|
||||
|
||||
execs = [] # Used to establish the same import context across defs (import is scope sensitive)
|
||||
|
||||
def getTokens(txt):
|
||||
global tokens_cached
|
||||
if tokens_cached==None:
|
||||
lines = txt.asLines()
|
||||
str = '\n'.join(lines)
|
||||
readline = StringIO(str).readline
|
||||
g = generate_tokens(readline)
|
||||
tokens = []
|
||||
for t in g: tokens.append(t)
|
||||
tokens_cached = tokens
|
||||
return tokens_cached
|
||||
tokens_cached = None
|
||||
|
||||
def isNameChar(s):
|
||||
return s.isalnum() or s in ['_']
|
||||
|
||||
# Returns words preceding the cursor that are separated by periods as a list in the
|
||||
# same order
|
||||
def getCompletionSymbols(txt):
|
||||
(l, c)= txt.getCursorPos()
|
||||
lines = txt.asLines()
|
||||
line = lines[l]
|
||||
a=0
|
||||
for a in range(1, c+1):
|
||||
if not isNameChar(line[c-a]) and line[c-a]!='.':
|
||||
a -= 1
|
||||
break
|
||||
return line[c-a:c].split('.')
|
||||
|
||||
|
||||
# Returns a list of tuples of symbol names and their types (name, type) where
|
||||
# type is one of:
|
||||
# m (module/class) Has its own members (includes classes)
|
||||
# v (variable) Has a type which may have its own members
|
||||
# f (function) Callable and may have a return type (with its own members)
|
||||
# It also updates the global import context (via execs)
|
||||
def getGlobals(txt):
|
||||
global execs
|
||||
|
||||
tokens = getTokens(txt)
|
||||
globals = dict()
|
||||
for i in range(len(tokens)):
|
||||
|
||||
# Handle all import statements
|
||||
if i>=1 and tokens[i-1][TK_TOKEN]=='import':
|
||||
|
||||
# Find 'from' if it exists
|
||||
fr= -1
|
||||
for a in range(1, i):
|
||||
if tokens[i-a][TK_TYPE]==token.NEWLINE: break
|
||||
if tokens[i-a][TK_TOKEN]=='from':
|
||||
fr=i-a
|
||||
break
|
||||
|
||||
# Handle: import ___[,___]
|
||||
if fr<0:
|
||||
|
||||
while True:
|
||||
if tokens[i][TK_TYPE]==token.NAME:
|
||||
# Add the import to the execs list
|
||||
x = tokens[i][TK_LINE].strip()
|
||||
k = tokens[i][TK_TOKEN]
|
||||
execs.append(x)
|
||||
|
||||
# Add the symbol name to the return list
|
||||
globals[k] = 'm'
|
||||
elif tokens[i][TK_TOKEN]!=',':
|
||||
break
|
||||
i += 1
|
||||
|
||||
# Handle statement: from ___[.___] import ___[,___]
|
||||
else: # fr>=0:
|
||||
|
||||
# Add the import to the execs list
|
||||
x = tokens[i][TK_LINE].strip()
|
||||
execs.append(x)
|
||||
|
||||
# Import parent module so we can process it for sub modules
|
||||
parent = ''.join([t[TK_TOKEN] for t in tokens[fr+1:i-1]])
|
||||
exec "import "+parent
|
||||
|
||||
# All submodules, functions, etc.
|
||||
if tokens[i][TK_TOKEN]=='*':
|
||||
|
||||
# Add each symbol name to the return list
|
||||
exec "d="+parent+".__dict__.items()"
|
||||
for k,v in d:
|
||||
if not globals.has_key(k) or not globals[k]:
|
||||
t='v'
|
||||
if ismodule(v): t='m'
|
||||
elif callable(v): t='f'
|
||||
globals[k] = t
|
||||
|
||||
# Specific function, submodule, etc.
|
||||
else:
|
||||
while True:
|
||||
if tokens[i][TK_TYPE]==token.NAME:
|
||||
k = tokens[i][TK_TOKEN]
|
||||
if not globals.has_key(k) or not globals[k]:
|
||||
t='v'
|
||||
try:
|
||||
exec 'v='+parent+'.'+k
|
||||
if ismodule(v): t='m'
|
||||
elif callable(v): t='f'
|
||||
except: pass
|
||||
globals[k] = t
|
||||
elif tokens[i][TK_TOKEN]!=',':
|
||||
break
|
||||
i += 1
|
||||
|
||||
elif tokens[i][TK_TYPE]==token.NAME and tokens[i][TK_TOKEN] not in keywords and (i==0 or tokens[i-1][TK_TOKEN]!='.'):
|
||||
k = tokens[i][TK_TOKEN]
|
||||
if not globals.has_key(k) or not globals[k]:
|
||||
t=None
|
||||
if (i>0 and tokens[i-1][TK_TOKEN]=='def'):
|
||||
t='f'
|
||||
else:
|
||||
t='v'
|
||||
globals[k] = t
|
||||
|
||||
return globals
|
||||
|
||||
def cmpi0(x, y):
|
||||
return cmp(x[0].lower(), y[0].lower())
|
||||
|
||||
def globalSuggest(txt, cs):
|
||||
global execs
|
||||
|
||||
suggestions = dict()
|
||||
(row, col) = txt.getCursorPos()
|
||||
globals = getGlobals(txt)
|
||||
|
||||
# Sometimes we have conditional includes which will fail if the module
|
||||
# cannot be found. So we protect outselves in a try block
|
||||
for x in execs:
|
||||
exec 'try: '+x+'\nexcept: pass'
|
||||
|
||||
if len(cs)==0:
|
||||
sub = ''
|
||||
else:
|
||||
sub = cs[0].lower()
|
||||
print 'Search:', sub
|
||||
|
||||
for k,t in globals.items():
|
||||
if k.lower().startswith(sub):
|
||||
suggestions[k] = t
|
||||
|
||||
l = list(suggestions.items())
|
||||
return sorted (l, cmp=cmpi0)
|
||||
|
||||
# Only works for 'static' members (eg. Text.Get)
|
||||
def memberSuggest(txt, cs):
|
||||
global execs
|
||||
|
||||
# Populate the execs for imports
|
||||
getGlobals(txt)
|
||||
|
||||
# Sometimes we have conditional includes which will fail if the module
|
||||
# cannot be found. So we protect outselves in a try block
|
||||
for x in execs:
|
||||
exec 'try: '+x+'\nexcept: pass'
|
||||
|
||||
suggestions = dict()
|
||||
(row, col) = txt.getCursorPos()
|
||||
|
||||
sub = cs[len(cs)-1].lower()
|
||||
print 'Search:', sub
|
||||
|
||||
t=None
|
||||
pre='.'.join(cs[:-1])
|
||||
try:
|
||||
exec "t="+pre
|
||||
except:
|
||||
print 'Failed to assign '+pre
|
||||
print execs
|
||||
print cs
|
||||
|
||||
if t!=None:
|
||||
for k,v in t.__dict__.items():
|
||||
if ismodule(v): t='m'
|
||||
elif callable(v): t='f'
|
||||
else: t='v'
|
||||
if k.lower().startswith(sub):
|
||||
suggestions[k] = t
|
||||
|
||||
l = list(suggestions.items())
|
||||
return sorted (l, cmp=cmpi0)
|
||||
|
||||
def main():
|
||||
txt = bpy.data.texts.active
|
||||
if txt==None: return
|
||||
|
||||
cs = getCompletionSymbols(txt)
|
||||
|
||||
if len(cs)<=1:
|
||||
l = globalSuggest(txt, cs)
|
||||
txt.suggest(l, cs[len(cs)-1])
|
||||
|
||||
else:
|
||||
l = memberSuggest(txt, cs)
|
||||
txt.suggest(l, cs[len(cs)-1])
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user