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:
2008-06-24 15:25:25 +00:00
parent 05ce388f35
commit bdc030c664
11 changed files with 561 additions and 1 deletions

View 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()

View File

@@ -0,0 +1,77 @@
/**
* $Id: $
*
* ***** BEGIN GPL LICENSE BLOCK *****
*
* This program 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 2
* of the License, or (at your option) any later version.
*
* This program 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 this program; if not, write to the Free Software Foundation,
* Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
* The Original Code is Copyright (C) 2008, Blender Foundation
* All rights reserved.
*
* The Original Code is: all of this file.
*
* Contributor(s): none yet.
*
* ***** END GPL LICENSE BLOCK *****
*/
#ifndef BKE_SUGGESTIONS_H
#define BKE_SUGGESTIONS_H
#ifdef __cplusplus
extern "C" {
#endif
/* ****************************************************************************
Suggestions must be added in sorted order (no attempt is made to sort the list)
The list is then divided up based on the prefix provided by update_suggestions:
Example:
Prefix: ab
aaa <-- first
aab
aba <-- firstmatch
abb <-- lastmatch
baa
bab <-- last
**************************************************************************** */
struct Text;
typedef struct SuggItem {
struct SuggItem *prev, *next;
char *name;
char type;
} SuggItem;
typedef struct SuggList {
SuggItem *first, *last;
SuggItem *firstmatch, *lastmatch;
} SuggList;
void free_suggestions();
void add_suggestion(const char *name, char type);
void update_suggestions(const char *prefix);
SuggItem *suggest_first();
SuggItem *suggest_last();
void set_suggest_text(Text *text);
void clear_suggest_text();
short is_suggest_active(Text *text);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,125 @@
/**
* $Id: $
*
* ***** BEGIN GPL LICENSE BLOCK *****
*
* This program 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 2
* of the License, or (at your option) any later version.
*
* This program 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 this program; if not, write to the Free Software Foundation,
* Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
* The Original Code is Copyright (C) 2008, Blender Foundation
* All rights reserved.
*
* The Original Code is: all of this file.
*
* Contributor(s): none yet.
*
* ***** END GPL LICENSE BLOCK *****
*/
#include <stdlib.h>
#include <string.h>
#include "MEM_guardedalloc.h"
#include "BLI_blenlib.h"
#include "DNA_text_types.h"
#include "BKE_text.h"
#include "BKE_suggestions.h"
static SuggList suggestions= {NULL, NULL, NULL, NULL};
static Text *suggText = NULL;
void free_suggestions() {
SuggItem *item;
for (item = suggestions.last; item; item=item->prev)
MEM_freeN(item);
suggestions.first = suggestions.last = NULL;
suggestions.firstmatch = suggestions.lastmatch = NULL;
}
void add_suggestion(const char *name, char type) {
SuggItem *newitem;
newitem = MEM_mallocN(sizeof(SuggItem) + strlen(name) + 1, "SuggestionItem");
if (!newitem) {
printf("Failed to allocate memory for suggestion.\n");
return;
}
newitem->name = (char *) (newitem + 1);
strcpy(newitem->name, name);
newitem->type = type;
newitem->prev = newitem->next = NULL;
if (!suggestions.first) {
suggestions.first = suggestions.last = newitem;
} else {
newitem->prev = suggestions.last;
suggestions.last->next = newitem;
suggestions.last = newitem;
}
}
void update_suggestions(const char *prefix) {
SuggItem *match, *first, *last;
int cmp, len = strlen(prefix);
if (!suggestions.first) return;
if (len==0) {
suggestions.firstmatch = suggestions.first;
suggestions.lastmatch = suggestions.last;
return;
}
first = last = NULL;
for (match=suggestions.first; match; match=match->next) {
cmp = strncmp(prefix, match->name, len);
if (cmp==0) {
if (!first)
first = match;
} else if (cmp<0) {
if (!last) {
last = match->prev;
break;
}
}
}
if (first) {
if (!last) last = suggestions.last;
suggestions.firstmatch = first;
suggestions.lastmatch = last;
} else {
suggestions.firstmatch = suggestions.lastmatch = NULL;
}
}
SuggItem *suggest_first() {
return suggestions.firstmatch;
}
SuggItem *suggest_last() {
return suggestions.lastmatch;
}
void set_suggest_text(Text *text) {
suggText = text;
}
void clear_suggest_text() {
free_suggestions();
suggText = NULL;
}
short is_suggest_active(Text *text) {
return suggText==text ? 1 : 0;
}

View File

@@ -1066,6 +1066,7 @@ int BPY_menu_do_python( short menutype, int event )
case PYMENU_RENDER:
case PYMENU_WIZARDS:
case PYMENU_SCRIPTTEMPLATE:
case PYMENU_TEXTPLUGIN:
case PYMENU_MESHFACEKEY:
break;

View File

@@ -106,6 +106,8 @@ static int bpymenu_group_atoi( char *str )
return PYMENU_ARMATURE;
else if( !strcmp( str, "ScriptTemplate" ) )
return PYMENU_SCRIPTTEMPLATE;
else if( !strcmp( str, "TextPlugin" ) )
return PYMENU_TEXTPLUGIN;
else if( !strcmp( str, "MeshFaceKey" ) )
return PYMENU_MESHFACEKEY;
else if( !strcmp( str, "AddMesh" ) )
@@ -184,6 +186,9 @@ char *BPyMenu_group_itoa( short menugroup )
case PYMENU_SCRIPTTEMPLATE:
return "ScriptTemplate";
break;
case PYMENU_TEXTPLUGIN:
return "TextPlugin";
break;
case PYMENU_MESHFACEKEY:
return "MeshFaceKey";
break;

View File

@@ -99,6 +99,7 @@ typedef enum {
PYMENU_UVCALCULATION,
PYMENU_ARMATURE,
PYMENU_SCRIPTTEMPLATE,
PYMENU_TEXTPLUGIN,
PYMENU_HELP,/*Main Help menu items - prob best to leave for 'official' ones*/
PYMENU_HELPSYSTEM,/* Resources, troubleshooting, system tools */
PYMENU_HELPWEBSITES,/* Help -> Websites submenu */

View File

@@ -34,8 +34,11 @@
#include "BKE_global.h"
#include "BKE_main.h"
#include "BIF_drawtext.h"
#include "BIF_screen.h"
#include "BKE_text.h"
#include "BKE_suggestions.h"
#include "BLI_blenlib.h"
#include "DNA_screen_types.h"
#include "DNA_space_types.h"
#include "gen_utils.h"
#include "gen_library.h"
@@ -96,6 +99,7 @@ static PyObject *Text_set( BPy_Text * self, PyObject * args );
static PyObject *Text_asLines( BPy_Text * self );
static PyObject *Text_getCursorPos( BPy_Text * self );
static PyObject *Text_setCursorPos( BPy_Text * self, PyObject * args );
static PyObject *Text_suggest( BPy_Text * self, PyObject * args );
/*****************************************************************************/
/* Python BPy_Text methods table: */
@@ -124,6 +128,8 @@ static PyMethodDef BPy_Text_methods[] = {
"() - Return cursor position as (row, col) tuple"},
{"setCursorPos", ( PyCFunction ) Text_setCursorPos, METH_VARARGS,
"(row, col) - Set the cursor position to (row, col)"},
{"suggest", ( PyCFunction ) Text_suggest, METH_VARARGS,
"(list) - List of tuples of the form (name, type) where type is one of 'm', 'v', 'f' for module, variable and function respectively"},
{NULL, NULL, 0, NULL}
};
@@ -511,6 +517,57 @@ static PyObject *Text_setCursorPos( BPy_Text * self, PyObject * args )
Py_RETURN_NONE;
}
static PyObject *Text_suggest( BPy_Text * self, PyObject * args )
{
PyObject *item = NULL;
PyObject *list = NULL, *resl = NULL;
int list_len, i;
char *prefix, *name, type;
SpaceText *st;
if(!self->text)
return EXPP_ReturnPyObjError(PyExc_RuntimeError,
"This object isn't linked to a Blender Text Object");
/* Parse args for a list of tuples */
if(!PyArg_ParseTuple(args, "O!s", &PyList_Type, &list, &prefix))
return EXPP_ReturnPyObjError(PyExc_TypeError,
"expected list of tuples followed by a string");
if (curarea->spacetype != SPACE_TEXT)
return EXPP_ReturnPyObjError(PyExc_RuntimeError,
"Active space type is not text");
st = curarea->spacedata.first;
if (!st || !st->text)
return EXPP_ReturnPyObjError(PyExc_RuntimeError,
"Active text area has no Text object");
list_len = PyList_Size(list);
clear_suggest_text();
for (i = 0; i < list_len; i++) {
item = PyList_GetItem(list, i);
if (!PyTuple_Check(item) || PyTuple_GET_SIZE(item) != 2)
return EXPP_ReturnPyObjError(PyExc_AttributeError,
"list must contain only tuples of size 2" );
name = PyString_AsString(PyTuple_GetItem(item, 0));
type = PyString_AsString(PyTuple_GetItem(item, 1))[0];
if (!strlen(name) || (type!='m' && type!='v' && type!='f'))
return EXPP_ReturnPyObjError(PyExc_AttributeError,
"layer values must be in the range [1, 20]" );
add_suggestion(name, type);
}
update_suggestions(prefix);
set_suggest_text(st->text);
scrarea_queue_redraw(curarea);
Py_RETURN_NONE;
}
/*****************************************************************************/
/* Function: Text_compare */
/* Description: This is a callback function for the BPy_Text type. It */

View File

@@ -150,5 +150,15 @@ class Text:
cursor.
"""
def suggest(list):
"""
Set the suggestion list to the given list of tuples. This list *must* be
sorted by its first element, name.
@type list: list of tuples
@param list: List of pair-tuples of the form (name, type) where name is
the suggested name and type is one of 'm' (module or class), 'f'
(function or method), 'v' (variable).
"""
import id_generics
Text.__doc__ += id_generics.attributes

View File

@@ -60,6 +60,7 @@
#include "BKE_global.h"
#include "BKE_main.h"
#include "BKE_node.h"
#include "BKE_suggestions.h"
#include "BIF_gl.h"
#include "BIF_glutil.h"
@@ -999,6 +1000,19 @@ static void do_selection(SpaceText *st, int selecting)
txt_undo_add_toop(st->text, UNDO_STO, sell, selc, linep2, charp2);
}
void draw_suggestion_list(SpaceText *st) {
SuggItem *item, *last;
if (!is_suggest_active(st->text)) return;
for (item=suggest_first(), last=suggest_last(); item; item=item->next) {
/* Useful for testing but soon to be replaced by UI list */
printf("Suggest: %c %s\n", item->type, item->name);
if (item == last)
break;
}
}
void drawtextspace(ScrArea *sa, void *spacedata)
{
SpaceText *st= curarea->spacedata.first;
@@ -1072,6 +1086,7 @@ void drawtextspace(ScrArea *sa, void *spacedata)
}
draw_textscroll(st);
draw_suggestion_list(st);
curarea->win_swap= WIN_BACK_OK;
}

View File

@@ -240,6 +240,37 @@ static uiBlock *text_template_scriptsmenu (void *args_unused)
return block;
}
static void do_text_plugin_scriptsmenu(void *arg, int event)
{
BPY_menu_do_python(PYMENU_TEXTPLUGIN, event);
allqueue(REDRAWIMAGE, 0);
}
static uiBlock *text_plugin_scriptsmenu (void *args_unused)
{
uiBlock *block;
BPyMenu *pym;
int i= 0;
short yco = 20, menuwidth = 120;
block= uiNewBlock(&curarea->uiblocks, "text_plugin_scriptsmenu", UI_EMBOSSP, UI_HELV, G.curscreen->mainwin);
uiBlockSetButmFunc(block, do_text_plugin_scriptsmenu, NULL);
/* note that we acount for the N previous entries with i+20: */
for (pym = BPyMenuTable[PYMENU_TEXTPLUGIN]; pym; pym = pym->next, i++) {
uiDefIconTextBut(block, BUTM, 1, ICON_PYTHON, pym->name, 0, yco-=20, menuwidth, 19,
NULL, 0.0, 0.0, 1, i,
pym->tooltip?pym->tooltip:pym->filename);
}
uiBlockSetDirection(block, UI_RIGHT);
uiTextBoundsBlock(block, 60);
return block;
}
/* action executed after clicking in File menu */
static void do_text_filemenu(void *arg, int event)
{
@@ -726,6 +757,7 @@ static uiBlock *text_filemenu(void *arg_unused)
}
uiDefIconTextBlockBut(block, text_template_scriptsmenu, NULL, ICON_RIGHTARROW_THIN, "Script Templates", 0, yco-=20, 120, 19, "");
uiDefIconTextBlockBut(block, text_plugin_scriptsmenu, NULL, ICON_RIGHTARROW_THIN, "Text Plugins", 0, yco-=20, 120, 19, "");
if(curarea->headertype==HEADERTOP) {
uiBlockSetDirection(block, UI_DOWN);

View File

@@ -67,6 +67,7 @@
#include "DNA_sound_types.h"
#include "DNA_scene_types.h"
#include "DNA_screen_types.h"
#include "DNA_text_types.h"
#include "BKE_blender.h"
#include "BKE_curve.h"
@@ -79,6 +80,7 @@
#include "BKE_mball.h"
#include "BKE_node.h"
#include "BKE_packedFile.h"
#include "BKE_suggestions.h"
#include "BKE_texture.h"
#include "BKE_utildefines.h"
#include "BKE_pointcache.h"
@@ -1091,6 +1093,7 @@ void exit_usiblender(void)
free_actcopybuf();
free_vertexpaint();
free_imagepaint();
free_suggestions();
/* editnurb can remain to exist outside editmode */
freeNurblist(&editNurb);