Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIan Thompson <quornian@googlemail.com>2008-09-01 18:04:22 +0400
committerIan Thompson <quornian@googlemail.com>2008-09-01 18:04:22 +0400
commitaa4e4da8c31a09c8afe0e60d26a1e68dd949e6b8 (patch)
tree32996747e2a8fae58f158d1a322f4a3aca5668b3 /release
parent7b9ee57c0bff50f812c00bc16801e8e2da12e253 (diff)
parent062bf735e7eb6ae4922127efe027ee9354dc4cf6 (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/blenderbuttonsbin69599 -> 68088 bytes
-rw-r--r--release/scripts/bpymodules/BPyTextPlugin.py814
-rw-r--r--release/scripts/scripttemplate_text_plugin.py69
-rw-r--r--release/scripts/textplugin_functiondocs.py64
-rw-r--r--release/scripts/textplugin_imports.py91
-rw-r--r--release/scripts/textplugin_membersuggest.py90
-rw-r--r--release/scripts/textplugin_outliner.py142
-rw-r--r--release/scripts/textplugin_suggest.py94
-rw-r--r--release/scripts/textplugin_templates.py123
9 files changed, 1487 insertions, 0 deletions
diff --git a/release/datafiles/blenderbuttons b/release/datafiles/blenderbuttons
index a4834091692..8ae492bcd1f 100644
--- a/release/datafiles/blenderbuttons
+++ b/release/datafiles/blenderbuttons
Binary files differ
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()