diff options
author | Ian Thompson <quornian@googlemail.com> | 2008-09-01 18:04:22 +0400 |
---|---|---|
committer | Ian Thompson <quornian@googlemail.com> | 2008-09-01 18:04:22 +0400 |
commit | aa4e4da8c31a09c8afe0e60d26a1e68dd949e6b8 (patch) | |
tree | 32996747e2a8fae58f158d1a322f4a3aca5668b3 /release | |
parent | 7b9ee57c0bff50f812c00bc16801e8e2da12e253 (diff) | |
parent | 062bf735e7eb6ae4922127efe027ee9354dc4cf6 (diff) |
Text Editor (GSOC 2008)
=======================
Merge of branch soc-2008-quorn to trunk:
Merged 14970:16308 to trunk@16307, updated to HEAD.
Merged 16318
Main features from this branch:
- Python text plugins
- Suggestions and documentation elements
- Improved syntax highlighting
- Word wrap
- Additional editing tools
- Various undo and clipboard fixes
- File header info and modification checks
Diffstat (limited to 'release')
-rw-r--r-- | release/datafiles/blenderbuttons | bin | 69599 -> 68088 bytes | |||
-rw-r--r-- | release/scripts/bpymodules/BPyTextPlugin.py | 814 | ||||
-rw-r--r-- | release/scripts/scripttemplate_text_plugin.py | 69 | ||||
-rw-r--r-- | release/scripts/textplugin_functiondocs.py | 64 | ||||
-rw-r--r-- | release/scripts/textplugin_imports.py | 91 | ||||
-rw-r--r-- | release/scripts/textplugin_membersuggest.py | 90 | ||||
-rw-r--r-- | release/scripts/textplugin_outliner.py | 142 | ||||
-rw-r--r-- | release/scripts/textplugin_suggest.py | 94 | ||||
-rw-r--r-- | release/scripts/textplugin_templates.py | 123 |
9 files changed, 1487 insertions, 0 deletions
diff --git a/release/datafiles/blenderbuttons b/release/datafiles/blenderbuttons Binary files differindex a4834091692..8ae492bcd1f 100644 --- a/release/datafiles/blenderbuttons +++ b/release/datafiles/blenderbuttons diff --git a/release/scripts/bpymodules/BPyTextPlugin.py b/release/scripts/bpymodules/BPyTextPlugin.py new file mode 100644 index 00000000000..5e5c9f55e53 --- /dev/null +++ b/release/scripts/bpymodules/BPyTextPlugin.py @@ -0,0 +1,814 @@ +"""The BPyTextPlugin Module + +Use get_cached_descriptor(txt) to retrieve information about the script held in +the txt Text object. + +Use print_cache_for(txt) to print the information to the console. + +Use line, cursor = current_line(txt) to get the logical line and cursor position + +Use get_targets(line, cursor) to find out what precedes the cursor: + aaa.bbb.cc|c.ddd -> ['aaa', 'bbb', 'cc'] + +Use resolve_targets(txt, targets) to turn a target list into a usable object if +one is found to match. +""" + +import bpy, sys, os +import __builtin__, tokenize +from Blender.sys import time +from tokenize import generate_tokens, TokenError, \ + COMMENT, DEDENT, INDENT, NAME, NEWLINE, NL, STRING, NUMBER + +class Definition(): + """Describes a definition or defined object through its name, line number + and docstring. This is the base class for definition based descriptors. + """ + + def __init__(self, name, lineno, doc=''): + self.name = name + self.lineno = lineno + self.doc = doc + +class ScriptDesc(): + """Describes a script through lists of further descriptor objects (classes, + defs, vars) and dictionaries to built-in types (imports). If a script has + not been fully parsed, its incomplete flag will be set. The time of the last + parse is held by the time field and the name of the text object from which + it was parsed, the name field. + """ + + def __init__(self, name, imports, classes, defs, vars, incomplete=False): + self.name = name + self.imports = imports + self.classes = classes + self.defs = defs + self.vars = vars + self.incomplete = incomplete + self.parse_due = 0 + + def set_delay(self, delay): + self.parse_due = time() + delay + +class ClassDesc(Definition): + """Describes a class through lists of further descriptor objects (defs and + vars). The name of the class is held by the name field and the line on + which it is defined is held in lineno. + """ + + def __init__(self, name, parents, defs, vars, lineno, doc=''): + Definition.__init__(self, name, lineno, doc) + self.parents = parents + self.defs = defs + self.vars = vars + +class FunctionDesc(Definition): + """Describes a function through its name and list of parameters (name, + params) and the line on which it is defined (lineno). + """ + + def __init__(self, name, params, lineno, doc=''): + Definition.__init__(self, name, lineno, doc) + self.params = params + +class VarDesc(Definition): + """Describes a variable through its name and type (if ascertainable) and the + line on which it is defined (lineno). If no type can be determined, type + will equal None. + """ + + def __init__(self, name, type, lineno): + Definition.__init__(self, name, lineno) + self.type = type # None for unknown (supports: dict/list/str) + +# Context types +CTX_UNSET = -1 +CTX_NORMAL = 0 +CTX_SINGLE_QUOTE = 1 +CTX_DOUBLE_QUOTE = 2 +CTX_COMMENT = 3 + +# Python keywords +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' ] + +# Module file extensions +MODULE_EXTS = ['.py', '.pyc', '.pyo', '.pyw', '.pyd'] + +ModuleType = type(__builtin__) +NoneScriptDesc = ScriptDesc('', dict(), dict(), dict(), dict(), True) + +_modules = {} +_modules_updated = 0 +_parse_cache = dict() + +def _load_module_names(): + """Searches the sys.path for module files and lists them, along with + sys.builtin_module_names, in the global dict _modules. + """ + + global _modules + + for n in sys.builtin_module_names: + _modules[n] = None + for p in sys.path: + if p == '': p = os.curdir + if not os.path.isdir(p): continue + for f in os.listdir(p): + for ext in MODULE_EXTS: + if f.endswith(ext): + _modules[f[:-len(ext)]] = None + break + +_load_module_names() + +def _trim_doc(doc): + """Trims the quotes from a quoted STRING token (eg. "'''text'''" -> "text") + """ + + l = len(doc) + i = 0 + while i < l/2 and (doc[i] == "'" or doc[i] == '"'): + i += 1 + return doc[i:-i] + +def resolve_targets(txt, targets): + """Attempts to return a useful object for the locally or externally defined + entity described by targets. If the object is local (defined in txt), a + Definition instance is returned. If the object is external (imported or + built in), the object itself is returned. If no object can be found, None is + returned. + """ + + count = len(targets) + if count==0: return None + + obj = None + local = None + i = 1 + + desc = get_cached_descriptor(txt) + b = targets[0].find('(') + if b==-1: b = None # Trick to let us use [:b] and get the whole string + + if desc.classes.has_key(targets[0][:b]): + local = desc.classes[targets[0][:b]] + elif desc.defs.has_key(targets[0]): + local = desc.defs[targets[0]] + elif desc.vars.has_key(targets[0]): + obj = desc.vars[targets[0]].type + + if local: + while i < count: + b = targets[i].find('(') + if b==-1: b = None + if hasattr(local, 'classes') and local.classes.has_key(targets[i][:b]): + local = local.classes[targets[i][:b]] + elif hasattr(local, 'defs') and local.defs.has_key(targets[i]): + local = local.defs[targets[i]] + elif hasattr(local, 'vars') and local.vars.has_key(targets[i]): + obj = local.vars[targets[i]].type + local = None + i += 1 + break + else: + local = None + break + i += 1 + + if local: return local + + if not obj: + if desc.imports.has_key(targets[0]): + obj = desc.imports[targets[0]] + else: + builtins = get_builtins() + if builtins.has_key(targets[0]): + obj = builtins[targets[0]] + + while obj and i < count: + if hasattr(obj, targets[i]): + obj = getattr(obj, targets[i]) + else: + obj = None + break + i += 1 + + return obj + +def get_cached_descriptor(txt, force_parse=0): + """Returns the cached ScriptDesc for the specified Text object 'txt'. If the + script has not been parsed in the last 'period' seconds it will be reparsed + to obtain this descriptor. + + Specifying TP_AUTO for the period (default) will choose a period based on the + size of the Text object. Larger texts are parsed less often. + """ + + global _parse_cache + + parse = True + key = hash(txt) + if not force_parse and _parse_cache.has_key(key): + desc = _parse_cache[key] + if desc.parse_due > time(): + parse = desc.incomplete + + if parse: + desc = parse_text(txt) + + return desc + +def parse_text(txt): + """Parses an entire script's text and returns a ScriptDesc instance + containing information about the script. + + If the text is not a valid Python script (for example if brackets are left + open), parsing may fail to complete. However, if this occurs, no exception + is thrown. Instead the returned ScriptDesc instance will have its incomplete + flag set and information processed up to this point will still be accessible. + """ + + start_time = time() + txt.reset() + tokens = generate_tokens(txt.readline) # Throws TokenError + + curl, cursor = txt.getCursorPos() + linen = curl + 1 # Token line numbers are one-based + + imports = dict() + imp_step = 0 + + classes = dict() + cls_step = 0 + + defs = dict() + def_step = 0 + + vars = dict() + var1_step = 0 + var2_step = 0 + var3_step = 0 + var_accum = dict() + var_forflag = False + + indent = 0 + prev_type = -1 + prev_text = '' + incomplete = False + + while True: + try: + type, text, start, end, line = tokens.next() + except StopIteration: + break + except (TokenError, IndentationError): + incomplete = True + break + + # Skip all comments and line joining characters + if type == COMMENT or type == NL: + continue + + ################# + ## Indentation ## + ################# + + if type == INDENT: + indent += 1 + elif type == DEDENT: + indent -= 1 + + ######################### + ## Module importing... ## + ######################### + + imp_store = False + + # Default, look for 'from' or 'import' to start + if imp_step == 0: + if text == 'from': + imp_tmp = [] + imp_step = 1 + elif text == 'import': + imp_from = None + imp_tmp = [] + imp_step = 2 + + # Found a 'from', create imp_from in form '???.???...' + elif imp_step == 1: + if text == 'import': + imp_from = '.'.join(imp_tmp) + imp_tmp = [] + imp_step = 2 + elif type == NAME: + imp_tmp.append(text) + elif text != '.': + imp_step = 0 # Invalid syntax + + # Found 'import', imp_from is populated or None, create imp_name + elif imp_step == 2: + if text == 'as': + imp_name = '.'.join(imp_tmp) + imp_step = 3 + elif type == NAME or text == '*': + imp_tmp.append(text) + elif text != '.': + imp_name = '.'.join(imp_tmp) + imp_symb = imp_name + imp_store = True + + # Found 'as', change imp_symb to this value and go back to step 2 + elif imp_step == 3: + if type == NAME: + imp_symb = text + else: + imp_store = True + + # Both imp_name and imp_symb have now been populated so we can import + if imp_store: + + # Handle special case of 'import *' + if imp_name == '*': + parent = get_module(imp_from) + imports.update(parent.__dict__) + + else: + # Try importing the name as a module + try: + if imp_from: + module = get_module(imp_from +'.'+ imp_name) + else: + module = get_module(imp_name) + except (ImportError, ValueError, AttributeError, TypeError): + # Try importing name as an attribute of the parent + try: + module = __import__(imp_from, globals(), locals(), [imp_name]) + imports[imp_symb] = getattr(module, imp_name) + except (ImportError, ValueError, AttributeError, TypeError): + pass + else: + imports[imp_symb] = module + + # More to import from the same module? + if text == ',': + imp_tmp = [] + imp_step = 2 + else: + imp_step = 0 + + ################### + ## Class parsing ## + ################### + + # If we are inside a class then def and variable parsing should be done + # for the class. Otherwise the definitions are considered global + + # Look for 'class' + if cls_step == 0: + if text == 'class': + cls_name = None + cls_lineno = start[0] + cls_indent = indent + cls_step = 1 + + # Found 'class', look for cls_name followed by '(' parents ')' + elif cls_step == 1: + if not cls_name: + if type == NAME: + cls_name = text + cls_sline = False + cls_parents = dict() + cls_defs = dict() + cls_vars = dict() + elif type == NAME: + if classes.has_key(text): + parent = classes[text] + cls_parents[text] = parent + cls_defs.update(parent.defs) + cls_vars.update(parent.vars) + elif text == ':': + cls_step = 2 + + # Found 'class' name ... ':', now check if it's a single line statement + elif cls_step == 2: + if type == NEWLINE: + cls_sline = False + else: + cls_sline = True + cls_doc = '' + cls_step = 3 + + elif cls_step == 3: + if not cls_doc and type == STRING: + cls_doc = _trim_doc(text) + if cls_sline: + if type == NEWLINE: + classes[cls_name] = ClassDesc(cls_name, cls_parents, cls_defs, cls_vars, cls_lineno, cls_doc) + cls_step = 0 + else: + if type == DEDENT and indent <= cls_indent: + classes[cls_name] = ClassDesc(cls_name, cls_parents, cls_defs, cls_vars, cls_lineno, cls_doc) + cls_step = 0 + + ################# + ## Def parsing ## + ################# + + # Look for 'def' + if def_step == 0: + if text == 'def': + def_name = None + def_lineno = start[0] + def_step = 1 + + # Found 'def', look for def_name followed by '(' + elif def_step == 1: + if type == NAME: + def_name = text + def_params = [] + elif def_name and text == '(': + def_step = 2 + + # Found 'def' name '(', now identify the parameters upto ')' + # TODO: Handle ellipsis '...' + elif def_step == 2: + if type == NAME: + def_params.append(text) + elif text == ':': + def_step = 3 + + # Found 'def' ... ':', now check if it's a single line statement + elif def_step == 3: + if type == NEWLINE: + def_sline = False + else: + def_sline = True + def_doc = '' + def_step = 4 + + elif def_step == 4: + if type == STRING: + def_doc = _trim_doc(text) + newdef = None + if def_sline: + if type == NEWLINE: + newdef = FunctionDesc(def_name, def_params, def_lineno, def_doc) + else: + if type == NAME: + newdef = FunctionDesc(def_name, def_params, def_lineno, def_doc) + if newdef: + if cls_step > 0: # Parsing a class + cls_defs[def_name] = newdef + else: + defs[def_name] = newdef + def_step = 0 + + ########################## + ## Variable assignation ## + ########################## + + if cls_step > 0: # Parsing a class + # Look for 'self.???' + if var1_step == 0: + if text == 'self': + var1_step = 1 + elif var1_step == 1: + if text == '.': + var_name = None + var1_step = 2 + else: + var1_step = 0 + elif var1_step == 2: + if type == NAME: + var_name = text + if cls_vars.has_key(var_name): + var_step = 0 + else: + var1_step = 3 + elif var1_step == 3: + if text == '=': + var1_step = 4 + elif text != ',': + var1_step = 0 + elif var1_step == 4: + var_type = None + if type == NUMBER: + close = end[1] + if text.find('.') != -1: var_type = float + else: var_type = int + elif type == STRING: + close = end[1] + var_type = str + elif text == '[': + close = line.find(']', end[1]) + var_type = list + elif text == '(': + close = line.find(')', end[1]) + var_type = tuple + elif text == '{': + close = line.find('}', end[1]) + var_type = dict + elif text == 'dict': + close = line.find(')', end[1]) + var_type = dict + if var_type and close+1 < len(line): + if line[close+1] != ' ' and line[close+1] != '\t': + var_type = None + cls_vars[var_name] = VarDesc(var_name, var_type, start[0]) + var1_step = 0 + + elif def_step > 0: # Parsing a def + # Look for 'global ???[,???]' + if var2_step == 0: + if text == 'global': + var2_step = 1 + elif var2_step == 1: + if type == NAME: + if not vars.has_key(text): + vars[text] = VarDesc(text, None, start[0]) + elif text != ',' and type != NL: + var2_step == 0 + + else: # In global scope + if var3_step == 0: + # Look for names + if text == 'for': + var_accum = dict() + var_forflag = True + elif text == '=' or (var_forflag and text == 'in'): + var_forflag = False + var3_step = 1 + elif type == NAME: + if prev_text != '.' and not vars.has_key(text): + var_accum[text] = VarDesc(text, None, start[0]) + elif not text in [',', '(', ')', '[', ']']: + var_accum = dict() + var_forflag = False + elif var3_step == 1: + if len(var_accum) != 1: + var_type = None + vars.update(var_accum) + else: + var_name = var_accum.keys()[0] + var_type = None + if type == NUMBER: + if text.find('.') != -1: var_type = float + else: var_type = int + elif type == STRING: var_type = str + elif text == '[': var_type = list + elif text == '(': var_type = tuple + elif text == '{': var_type = dict + vars[var_name] = VarDesc(var_name, var_type, start[0]) + var3_step = 0 + + ####################### + ## General utilities ## + ####################### + + prev_type = type + prev_text = text + + desc = ScriptDesc(txt.name, imports, classes, defs, vars, incomplete) + desc.set_delay(10 * (time()-start_time) + 0.05) + + global _parse_cache + _parse_cache[hash(txt)] = desc + return desc + +def get_modules(since=1): + """Returns the set of built-in modules and any modules that have been + imported into the system upto 'since' seconds ago. + """ + + global _modules, _modules_updated + + t = time() + if _modules_updated < t - since: + _modules.update(sys.modules) + _modules_updated = t + return _modules.keys() + +def suggest_cmp(x, y): + """Use this method when sorting a list of suggestions. + """ + + return cmp(x[0].upper(), y[0].upper()) + +def get_module(name): + """Returns the module specified by its name. The module itself is imported + by this method and, as such, any initialization code will be executed. + """ + + mod = __import__(name) + components = name.split('.') + for comp in components[1:]: + mod = getattr(mod, comp) + return mod + +def type_char(v): + """Returns the character used to signify the type of a variable. Use this + method to identify the type character for an item in a suggestion list. + + The following values are returned: + 'm' if the parameter is a module + 'f' if the parameter is callable + 'v' if the parameter is variable or otherwise indeterminable + + """ + + if isinstance(v, ModuleType): + return 'm' + elif callable(v): + return 'f' + else: + return 'v' + +def get_context(txt): + """Establishes the context of the cursor in the given Blender Text object + + Returns one of: + CTX_NORMAL - Cursor is in a normal context + CTX_SINGLE_QUOTE - Cursor is inside a single quoted string + CTX_DOUBLE_QUOTE - Cursor is inside a double quoted string + CTX_COMMENT - Cursor is inside a comment + + """ + + l, cursor = txt.getCursorPos() + lines = txt.asLines(0, l+1) + + # FIXME: This method is too slow in large files for it to be called as often + # as it is. So for lines below the 1000th line we do this... (quorn) + if l > 1000: return CTX_NORMAL + + # Detect context (in string or comment) + in_str = CTX_NORMAL + for line in lines: + if l == 0: + end = cursor + else: + end = len(line) + l -= 1 + + # Comments end at new lines + if in_str == CTX_COMMENT: + in_str = CTX_NORMAL + + for i in range(end): + if in_str == 0: + if line[i] == "'": in_str = CTX_SINGLE_QUOTE + elif line[i] == '"': in_str = CTX_DOUBLE_QUOTE + elif line[i] == '#': in_str = CTX_COMMENT + else: + if in_str == CTX_SINGLE_QUOTE: + if line[i] == "'": + in_str = CTX_NORMAL + # In again if ' escaped, out again if \ escaped, and so on + for a in range(i-1, -1, -1): + if line[a] == '\\': in_str = 1-in_str + else: break + elif in_str == CTX_DOUBLE_QUOTE: + if line[i] == '"': + in_str = CTX_NORMAL + # In again if " escaped, out again if \ escaped, and so on + for a in range(i-1, -1, -1): + if line[i-a] == '\\': in_str = 2-in_str + else: break + + return in_str + +def current_line(txt): + """Extracts the Python script line at the cursor in the Blender Text object + provided and cursor position within this line as the tuple pair (line, + cursor). + """ + + lineindex, cursor = txt.getCursorPos() + lines = txt.asLines() + line = lines[lineindex] + + # Join previous lines to this line if spanning + i = lineindex - 1 + while i > 0: + earlier = lines[i].rstrip() + if earlier.endswith('\\'): + line = earlier[:-1] + ' ' + line + cursor += len(earlier) + i -= 1 + + # Join later lines while there is an explicit joining character + i = lineindex + while i < len(lines)-1 and lines[i].rstrip().endswith('\\'): + later = lines[i+1].strip() + line = line + ' ' + later[:-1] + i += 1 + + return line, cursor + +def get_targets(line, cursor): + """Parses a period separated string of valid names preceding the cursor and + returns them as a list in the same order. + """ + + brk = 0 + targets = [] + j = cursor + i = j-1 + while i >= 0: + if line[i] == ')': brk += 1 + elif brk: + if line[i] == '(': brk -= 1 + else: + if line[i] == '.': + targets.insert(0, line[i+1:j]); j=i + elif not (line[i].isalnum() or line[i] == '_' or line[i] == '.'): + break + i -= 1 + targets.insert(0, line[i+1:j]) + return targets + +def get_defs(txt): + """Returns a dictionary which maps definition names in the source code to + a list of their parameter names. + + The line 'def doit(one, two, three): print one' for example, results in the + mapping 'doit' : [ 'one', 'two', 'three' ] + """ + + return get_cached_descriptor(txt).defs + +def get_vars(txt): + """Returns a dictionary of variable names found in the specified Text + object. This method locates all names followed directly by an equal sign: + 'a = ???' or indirectly as part of a tuple/list assignment or inside a + 'for ??? in ???:' block. + """ + + return get_cached_descriptor(txt).vars + +def get_imports(txt): + """Returns a dictionary which maps symbol names in the source code to their + respective modules. + + The line 'from Blender import Text as BText' for example, results in the + mapping 'BText' : <module 'Blender.Text' (built-in)> + + Note that this method imports the modules to provide this mapping as as such + will execute any initilization code found within. + """ + + return get_cached_descriptor(txt).imports + +def get_builtins(): + """Returns a dictionary of built-in modules, functions and variables.""" + + return __builtin__.__dict__ + + +################################# +## Debugging utility functions ## +################################# + +def print_cache_for(txt, period=sys.maxint): + """Prints out the data cached for a given Text object. If no period is + given the text will not be reparsed and the cached version will be returned. + Otherwise if the period has expired the text will be reparsed. + """ + + desc = get_cached_descriptor(txt, period) + print '================================================' + print 'Name:', desc.name, '('+str(hash(txt))+')' + print '------------------------------------------------' + print 'Defs:' + for name, ddesc in desc.defs.items(): + print ' ', name, ddesc.params, ddesc.lineno + print ' ', ddesc.doc + print '------------------------------------------------' + print 'Vars:' + for name, vdesc in desc.vars.items(): + print ' ', name, vdesc.type, vdesc.lineno + print '------------------------------------------------' + print 'Imports:' + for name, item in desc.imports.items(): + print ' ', name.ljust(15), item + print '------------------------------------------------' + print 'Classes:' + for clsnme, clsdsc in desc.classes.items(): + print ' *********************************' + print ' Name:', clsnme + print ' ', clsdsc.doc + print ' ---------------------------------' + print ' Defs:' + for name, ddesc in clsdsc.defs.items(): + print ' ', name, ddesc.params, ddesc.lineno + print ' ', ddesc.doc + print ' ---------------------------------' + print ' Vars:' + for name, vdesc in clsdsc.vars.items(): + print ' ', name, vdesc.type, vdesc.lineno + print ' *********************************' + print '================================================' diff --git a/release/scripts/scripttemplate_text_plugin.py b/release/scripts/scripttemplate_text_plugin.py new file mode 100644 index 00000000000..4ae562736d3 --- /dev/null +++ b/release/scripts/scripttemplate_text_plugin.py @@ -0,0 +1,69 @@ +#!BPY +""" +Name: 'Text Plugin' +Blender: 246 +Group: 'ScriptTemplate' +Tooltip: 'Add a new text for writing a text plugin' +""" + +from Blender import Window +import bpy + +script_data = \ +'''#!BPY +""" +Name: 'My Plugin Script' +Blender: 246 +Group: 'TextPlugin' +Shortcut: 'Ctrl+Alt+U' +Tooltip: 'Put some useful info here' +""" + +# Add a licence here if you wish to re-distribute, we recommend the GPL + +from Blender import Window, sys +import BPyTextPlugin, bpy + +def my_script_util(txt): + # This function prints out statistical information about a script + + desc = BPyTextPlugin.get_cached_descriptor(txt) + print '---------------------------------------' + print 'Script Name:', desc.name + print 'Classes:', len(desc.classes) + print ' ', desc.classes.keys() + print 'Functions:', len(desc.defs) + print ' ', desc.defs.keys() + print 'Variables:', len(desc.vars) + print ' ', desc.vars.keys() + +def main(): + + # Gets the active text object, there can be many in one blend file. + txt = bpy.data.texts.active + + # Silently return if the script has been run with no active text + if not txt: + return + + # Text plug-ins should run quickly so we time it here + Window.WaitCursor(1) + t = sys.time() + + # Run our utility function + my_script_util(txt) + + # Timing the script is a good way to be aware on any speed hits when scripting + print 'Plugin script finished in %.2f seconds' % (sys.time()-t) + Window.WaitCursor(0) + + +# This lets you import the script without running it +if __name__ == '__main__': + main() +''' + +new_text = bpy.data.texts.new('textplugin_template.py') +new_text.write(script_data) +bpy.data.texts.active = new_text +Window.RedrawAll() diff --git a/release/scripts/textplugin_functiondocs.py b/release/scripts/textplugin_functiondocs.py new file mode 100644 index 00000000000..41c8d4842a0 --- /dev/null +++ b/release/scripts/textplugin_functiondocs.py @@ -0,0 +1,64 @@ +#!BPY +""" +Name: 'Function Documentation | Ctrl I' +Blender: 246 +Group: 'TextPlugin' +Shortcut: 'Ctrl+I' +Tooltip: 'Attempts to display documentation about the function preceding the cursor.' +""" + +# Only run if we have the required modules +try: + import bpy + from BPyTextPlugin import * +except ImportError: + OK = False +else: + OK = True + +def main(): + txt = bpy.data.texts.active + if not txt: + return + + (line, c) = current_line(txt) + + # Check we are in a normal context + if get_context(txt) != CTX_NORMAL: + return + + # Identify the name under the cursor + llen = len(line) + while c<llen and (line[c].isalnum() or line[c]=='_'): + c += 1 + + targets = get_targets(line, c) + + # If no name under cursor, look backward to see if we're in function parens + if len(targets) == 0 or targets[0] == '': + # Look backwards for first '(' without ')' + b = 0 + found = False + for i in range(c-1, -1, -1): + if line[i] == ')': b += 1 + elif line[i] == '(': + b -= 1 + if b < 0: + found = True + c = i + break + if found: targets = get_targets(line, c) + if len(targets) == 0 or targets[0] == '': + return + + obj = resolve_targets(txt, targets) + if not obj: return + + if isinstance(obj, Definition): # Local definition + txt.showDocs(obj.doc) + elif hasattr(obj, '__doc__') and obj.__doc__: + txt.showDocs(obj.__doc__) + +# Check we are running as a script and not imported as a module +if __name__ == "__main__" and OK: + main() diff --git a/release/scripts/textplugin_imports.py b/release/scripts/textplugin_imports.py new file mode 100644 index 00000000000..ec608243c2b --- /dev/null +++ b/release/scripts/textplugin_imports.py @@ -0,0 +1,91 @@ +#!BPY +""" +Name: 'Import Complete|Space' +Blender: 246 +Group: 'TextPlugin' +Shortcut: 'Space' +Tooltip: 'Lists modules when import or from is typed' +""" + +# Only run if we have the required modules +try: + import bpy, sys + from BPyTextPlugin import * +except ImportError: + OK = False +else: + OK = True + +def main(): + txt = bpy.data.texts.active + if not txt: + return + + line, c = current_line(txt) + + # Check we are in a normal context + if get_context(txt) != CTX_NORMAL: + return + + pos = line.rfind('from ', 0, c) + + # No 'from' found + if pos == -1: + # Check instead for straight 'import xxxx' + pos2 = line.rfind('import ', 0, c) + if pos2 != -1: + pos2 += 7 + for i in range(pos2, c): + if line[i]==',' or (line[i]==' ' and line[i-1]==','): + pos2 = i+1 + elif not line[i].isalnum() and line[i] != '_': + return + items = [(m, 'm') for m in get_modules()] + items.sort(cmp = suggest_cmp) + txt.suggest(items, line[pos2:c].strip()) + return + + # Found 'from xxxxx' before cursor + immediate = True + pos += 5 + for i in range(pos, c): + if not line[i].isalnum() and line[i] != '_' and line[i] != '.': + immediate = False + break + + # Immediate 'from' followed by at most a module name + if immediate: + items = [(m, 'm') for m in get_modules()] + items.sort(cmp = suggest_cmp) + txt.suggest(items, line[pos:c]) + return + + # Found 'from' earlier, suggest import if not already there + pos2 = line.rfind('import ', pos, c) + + # No 'import' found after 'from' so suggest it + if pos2 == -1: + txt.suggest([('import', 'k')], '') + return + + # Immediate 'import' before cursor and after 'from...' + for i in range(pos2+7, c): + if line[i]==',' or (line[i]==' ' and line[i-1]==','): + pass + elif not line[i].isalnum() and line[i] != '_': + return + between = line[pos:pos2-1].strip() + try: + mod = get_module(between) + except ImportError: + return + + items = [('*', 'k')] + for (k,v) in mod.__dict__.items(): + items.append((k, type_char(v))) + items.sort(cmp = suggest_cmp) + txt.suggest(items, '') + +# Check we are running as a script and not imported as a module +if __name__ == "__main__" and OK: + main() diff --git a/release/scripts/textplugin_membersuggest.py b/release/scripts/textplugin_membersuggest.py new file mode 100644 index 00000000000..7c0de78b704 --- /dev/null +++ b/release/scripts/textplugin_membersuggest.py @@ -0,0 +1,90 @@ +#!BPY +""" +Name: 'Member Suggest | .' +Blender: 246 +Group: 'TextPlugin' +Shortcut: 'Period' +Tooltip: 'Lists members of the object preceding the cursor in the current text space' +""" + +# Only run if we have the required modules +try: + import bpy + from BPyTextPlugin import * +except ImportError: + OK = False +else: + OK = True + +def main(): + txt = bpy.data.texts.active + if not txt: + return + + (line, c) = current_line(txt) + + # Check we are in a normal context + if get_context(txt) != CTX_NORMAL: + return + + targets = get_targets(line, c) + + if targets[0] == '': # Check if we are looking at a constant [] {} '' etc. + i = c - len('.'.join(targets)) - 1 + if i >= 0: + if line[i] == '"' or line[i] == "'": + targets[0] = 'str' + elif line[i] == '}': + targets[0] = 'dict' + elif line[i] == ']': # Could be array elem x[y] or list [y] + i = line.rfind('[', 0, i) - 1 + while i >= 0: + if line[i].isalnum() or line[i] == '_': + break + elif line[i] != ' ' and line[i] != '\t': + i = -1 + break + i -= 1 + if i < 0: + targets[0] = 'list' + + obj = resolve_targets(txt, targets[:-1]) + if not obj: + return + + items = [] + + if isinstance(obj, VarDesc): + obj = obj.type + + if isinstance(obj, Definition): # Locally defined + if hasattr(obj, 'classes'): + items.extend([(s, 'f') for s in obj.classes.keys()]) + if hasattr(obj, 'defs'): + items.extend([(s, 'f') for s in obj.defs.keys()]) + if hasattr(obj, 'vars'): + items.extend([(s, 'v') for s in obj.vars.keys()]) + + else: # Otherwise we have an imported or builtin object + try: + attr = obj.__dict__.keys() + except AttributeError: + attr = dir(obj) + else: + if not attr: attr = dir(obj) + + for k in attr: + try: + v = getattr(obj, k) + except (AttributeError, TypeError): # Some attributes are not readable + pass + else: + items.append((k, type_char(v))) + + if items != []: + items.sort(cmp = suggest_cmp) + txt.suggest(items, targets[-1]) + +# Check we are running as a script and not imported as a module +if __name__ == "__main__" and OK: + main() diff --git a/release/scripts/textplugin_outliner.py b/release/scripts/textplugin_outliner.py new file mode 100644 index 00000000000..3879a2819a5 --- /dev/null +++ b/release/scripts/textplugin_outliner.py @@ -0,0 +1,142 @@ +#!BPY +""" +Name: 'Code Outline | Ctrl T' +Blender: 246 +Group: 'TextPlugin' +Shortcut: 'Ctrl+T' +Tooltip: 'Provides a menu for jumping to class and functions definitions.' +""" + +# Only run if we have the required modules +try: + import bpy + from BPyTextPlugin import * + from Blender import Draw +except ImportError: + OK = False +else: + OK = True + +def make_menu(items, eventoffs): + n = len(items) + if n < 20: + return [(items[i], i+1+eventoffs) for i in range(len(items))] + + letters = [] + check = 'abcdefghijklmnopqrstuvwxyz_' # Names cannot start 0-9 + for c in check: + for item in items: + if item[0].lower() == c: + letters.append(c) + break + + entries = {} + i = 0 + for item in items: + i += 1 + c = item[0].lower() + entries.setdefault(c, []).append((item, i+eventoffs)) + + subs = [] + for c in letters: + subs.append((c, entries[c])) + + return subs + +def find_word(txt, word): + i = 0 + txt.reset() + while True: + try: + line = txt.readline() + except StopIteration: + break + c = line.find(word) + if c != -1: + txt.setCursorPos(i, c) + break + i += 1 + +def main(): + txt = bpy.data.texts.active + if not txt: + return + + # Identify word under cursor + if get_context(txt) == CTX_NORMAL: + line, c = current_line(txt) + start = c-1 + end = c + while start >= 0: + if not line[start].lower() in 'abcdefghijklmnopqrstuvwxyz0123456789_': + break + start -= 1 + while end < len(line): + if not line[end].lower() in 'abcdefghijklmnopqrstuvwxyz0123456789_': + break + end += 1 + word = line[start+1:end] + if word in KEYWORDS: + word = None + else: + word = None + + script = get_cached_descriptor(txt) + items = [] + desc = None + + tmp = script.classes.keys() + tmp.sort(cmp = suggest_cmp) + class_menu = make_menu(tmp, len(items)) + class_menu_length = len(tmp) + items.extend(tmp) + + tmp = script.defs.keys() + tmp.sort(cmp = suggest_cmp) + defs_menu = make_menu(tmp, len(items)) + defs_menu_length = len(tmp) + items.extend(tmp) + + tmp = script.vars.keys() + tmp.sort(cmp = suggest_cmp) + vars_menu = make_menu(tmp, len(items)) + vars_menu_length = len(tmp) + items.extend(tmp) + + menu = [('Script %t', 0), + ('Classes', class_menu), + ('Functions', defs_menu), + ('Variables', vars_menu)] + if word: + menu.extend([None, ('Locate', [(word, -10)])]) + + i = Draw.PupTreeMenu(menu) + if i == -1: + return + + # Chosen to search for word under cursor + if i == -10: + if script.classes.has_key(word): + desc = script.classes[word] + elif script.defs.has_key(word): + desc = script.defs[word] + elif script.vars.has_key(word): + desc = script.vars[word] + else: + find_word(txt, word) + return + else: + i -= 1 + if i < class_menu_length: + desc = script.classes[items[i]] + elif i < class_menu_length + defs_menu_length: + desc = script.defs[items[i]] + elif i < class_menu_length + defs_menu_length + vars_menu_length: + desc = script.vars[items[i]] + + if desc: + txt.setCursorPos(desc.lineno-1, 0) + +# Check we are running as a script and not imported as a module +if __name__ == "__main__" and OK: + main() diff --git a/release/scripts/textplugin_suggest.py b/release/scripts/textplugin_suggest.py new file mode 100644 index 00000000000..d8122212d3b --- /dev/null +++ b/release/scripts/textplugin_suggest.py @@ -0,0 +1,94 @@ +#!BPY +""" +Name: 'Suggest All | Ctrl Space' +Blender: 246 +Group: 'TextPlugin' +Shortcut: 'Ctrl+Space' +Tooltip: 'Performs suggestions based on the context of the cursor' +""" + +# Only run if we have the required modules +try: + import bpy + from BPyTextPlugin import * +except ImportError: + OK = False +else: + OK = True + +def check_membersuggest(line, c): + pos = line.rfind('.', 0, c) + if pos == -1: + return False + for s in line[pos+1:c]: + if not s.isalnum() and s != '_': + return False + return True + +def check_imports(line, c): + pos = line.rfind('import ', 0, c) + if pos > -1: + for s in line[pos+7:c]: + if not s.isalnum() and s != '_': + return False + return True + pos = line.rfind('from ', 0, c) + if pos > -1: + for s in line[pos+5:c]: + if not s.isalnum() and s != '_': + return False + return True + return False + +def main(): + txt = bpy.data.texts.active + if not txt: + return + + line, c = current_line(txt) + + # Check we are in a normal context + if get_context(txt) != CTX_NORMAL: + return + + # Check the character preceding the cursor and execute the corresponding script + + if check_membersuggest(line, c): + import textplugin_membersuggest + textplugin_membersuggest.main() + return + + elif check_imports(line, c): + import textplugin_imports + textplugin_imports.main() + return + + # Otherwise we suggest globals, keywords, etc. + list = [] + targets = get_targets(line, c) + desc = get_cached_descriptor(txt) + + for k in KEYWORDS: + list.append((k, 'k')) + + for k, v in get_builtins().items(): + list.append((k, type_char(v))) + + for k, v in desc.imports.items(): + list.append((k, type_char(v))) + + for k, v in desc.classes.items(): + list.append((k, 'f')) + + for k, v in desc.defs.items(): + list.append((k, 'f')) + + for k, v in desc.vars.items(): + list.append((k, 'v')) + + list.sort(cmp = suggest_cmp) + txt.suggest(list, targets[-1]) + +# Check we are running as a script and not imported as a module +if __name__ == "__main__" and OK: + main() diff --git a/release/scripts/textplugin_templates.py b/release/scripts/textplugin_templates.py new file mode 100644 index 00000000000..8f949563ac0 --- /dev/null +++ b/release/scripts/textplugin_templates.py @@ -0,0 +1,123 @@ +#!BPY +""" +Name: 'Template Completion | Tab' +Blender: 246 +Group: 'TextPlugin' +Shortcut: 'Tab' +Tooltip: 'Completes templates based on the text preceding the cursor' +""" + +# Only run if we have the required modules +try: + import bpy + from BPyTextPlugin import * + from Blender import Text +except ImportError: + OK = False +else: + OK = True + +templates = { + 'ie': + 'if ${1:cond}:\n' + '\t${2}\n' + 'else:\n' + '\t${3}\n', + 'iei': + 'if ${1:cond}:\n' + '\t${2}\n' + 'elif:\n' + '\t${3}\n' + 'else:\n' + '\t${4}\n', + 'def': + 'def ${1:name}(${2:params}):\n' + '\t"""(${2}) - ${3:comment}"""\n' + '\t${4}', + 'cls': + 'class ${1:name}(${2:parent}):\n' + '\t"""${3:docs}"""\n' + '\t\n' + '\tdef __init__(self, ${4:params}):\n' + '\t\t"""Creates a new ${1}"""\n' + '\t\t${5}', + 'class': + 'class ${1:name}(${2:parent}):\n' + '\t"""${3:docs}"""\n' + '\t\n' + '\tdef __init__(self, ${4:params}):\n' + '\t\t"""Creates a new ${1}"""\n' + '\t\t${5}' +} + +def main(): + txt = bpy.data.texts.active + if not txt: + return + + row, c = txt.getCursorPos() + line = txt.asLines(row, row+1)[0] + indent=0 + while indent<c and (line[indent]==' ' or line[indent]=='\t'): + indent += 1 + + # Check we are in a normal context + if get_context(txt) != CTX_NORMAL: + return + + targets = get_targets(line, c-1); + if len(targets) != 1: return + + color = (0, 192, 32) + + for trigger, template in templates.items(): + if trigger != targets[0]: continue + inserts = {} + txt.delete(-len(trigger)-1) + y, x = txt.getCursorPos() + first = None + + # Insert template text and parse for insertion points + count = len(template); i = 0 + while i < count: + if i<count-1 and template[i]=='$' and template[i+1]=='{': + i += 2 + e = template.find('}', i) + item = template[i:e].split(':') + if len(item)<2: item.append('') + if not inserts.has_key(item[0]): + inserts[item[0]] = (item[1], [(x, y)]) + else: + inserts[item[0]][1].append((x, y)) + item[1] = inserts[item[0]][0] + if not first: first = (item[1], x, y) + txt.insert(item[1]) + x += len(item[1]) + i = e + else: + txt.insert(template[i]) + if template[i] == '\n': + txt.insert(line[:indent]) + y += 1 + x = indent + else: + x += 1 + i += 1 + + # Insert markers at insertion points + for id, (text, points) in inserts.items(): + for x, y in points: + txt.setCursorPos(y, x) + txt.setSelectPos(y, x+len(text)) + txt.markSelection((hash(text)+int(id)) & 0xFFFF, color, + Text.TMARK_TEMP | Text.TMARK_EDITALL) + if first: + text, x, y = first + txt.setCursorPos(y, x) + txt.setSelectPos(y, x+len(text)) + break + + +# Check we are running as a script and not imported as a module +if __name__ == "__main__" and OK: + main() |