Improve new text editor support to be production quality.

This commit is contained in:
Tom Rothamel
2012-03-10 17:45:52 -05:00
parent 71ccf249f2
commit 5ef40f8c3e
16 changed files with 229 additions and 227 deletions
+4
View File
@@ -0,0 +1,4 @@
import renpy
# Do nothing when the editor is invoked.
Editor = renpy.editor.Editor
-6
View File
@@ -1,6 +0,0 @@
# Name: None
# Version: 1
# Description: Disables the automatic launching of an editor.
config.editor = None
config.editor_transient = None
+4
View File
@@ -0,0 +1,4 @@
import renpy
# Pass the file off to the system editor (as determined by file associations).
Editor = renpy.editor.SystemEditor
+39 -82
View File
@@ -13,75 +13,54 @@ init python:
if persistent.editor is None:
persistent.editor = "jEdit"
# A map from editor name to the file containing information about
# that editor.
# Should we set up the editor?
set_editor = "RENPY_EDIT_PY" not in os.environ
# A map from editor name to EditorInfo object.
editors = { }
# A map from editor to the version of that editor.
editor_versions = { }
# A map from editor to a description of that editor.
editor_descriptions = { }
class EditorInfo(object):
def __init__(self, filename):
# The path to the editor info file.
self.filename = filename
# The name of the editor.
self.name = os.path.basename(filename)[:-len(".edit.py")]
# The time the editor file was last modified. We use this
# to decide if we should update the editors mat when we
# have multiple versions of an editor in contention.
self.mtime = os.path.getmtime(filename)
# Should we set up the editor? How about the transient editor?
set_editor = "RENPY_EDITOR" not in os.environ
set_editor_transient = "RENPY_EDITOR_TRANSIENT" not in os.environ
if set_editor and not set_editor_transient:
config.editor_transient = config.editor
os.environ['RENPY_EDITOR_TRANSIENT'] = config.editor
set_editor_transient = False
def scan_editor(ef):
def scan_editor(filename):
"""
Inserts an editor into editors if there isn't a newer
editor there already.
"""
Scans a single editor file to get the meta-information. If it
checks out, adds it to editors. Uses editor_versions as a cache
so we only add the newest version of each editor.
"""
info = { }
ei = EditorInfo(filename)
if ei.name in editors:
if editors[ei.name].mtime >= ei.mtime:
return
editors[ei.name] = ei
f = file(ef, "r")
for l in f:
m = re.match("#\s*(\w+):\s*(.*?)\s*$", l)
if not m:
break
info[m.group(1)] = m.group(2)
f.close()
try:
name = info["Name"]
version = int(info["Version"])
description = info.get("Description", "")
except:
traceback.print_exc()
print >>sys.stderr, ef
if version > editor_versions.get(name, -1):
editors[name] = ef
editor_versions[name] = version
editor_descriptions[name] = description
def scan_editors():
"""
Finds all *.editor.py files, and uses them to populate the list
of editors.
"""
Finds all *.edit.py files, and uses them to populate the list
of editors.
"""
editors.clear()
editor_versions.clear()
editor_descriptions.clear()
for d in [ config.renpy_base, persistent.projects_directory ]:
if d is None:
continue
for ef in glob.glob(d + "/*/*.editor.py"):
scan_editor(ef)
for filename in glob.glob(d + "/*/*.edit.py"):
scan_editor(filename)
def setup_editor():
@@ -93,44 +72,24 @@ init python:
if not set_editor:
return
ef = None
ei = None
for i in [ persistent.editor, "jEdit", "None" ]:
if i in editors:
ef = editors[i]
ei = editors[i]
break
else:
return
ctx = {
"renpy" : renpy,
"config" : config,
"persistent" : persistent,
"base" : os.path.dirname(renpy.fsencode(ef)),
}
execfile(renpy.fsencode(ef), ctx, ctx)
if set_editor:
if config.editor:
os.environ['RENPY_EDITOR'] = config.editor
else:
if 'RENPY_EDITOR' in os.environ:
del os.environ['RENPY_EDITOR']
if set_editor_transient:
if config.editor_transient:
os.environ['RENPY_EDITOR_TRANSIENT'] = config.editor_transient
else:
if 'RENPY_EDITOR_TRANSIENT' in os.environ:
del os.environ['RENPY_EDITOR_TRANSIENT']
os.environ["RENPY_EDIT_PY"] = renpy.fsencode(os.path.abspath(ei.filename))
renpy.editor.init()
label editor:
python hide:
if not set_editor:
error(_(u"The editor has been set from the RENPY_EDITOR environment variable, and cannot be changed."), "options")
error(_(u"The editor has been set from the RENPY_EDIT_PY environment variable, and cannot be changed."), "options")
set_tooltip("")
@@ -147,9 +106,7 @@ label editor:
ui.vbox()
for i in sorted(editors, key=lambda a : a.lower()):
button(i,
ui.returns(i),
editor_descriptions[i])
button(i, ui.returns(i))
ui.close() # Vbox
ui.close() # Scrolled
+41
View File
@@ -0,0 +1,41 @@
import os
import renpy
import subprocess
class Editor(renpy.editor.Editor):
def begin(self, new_window=False, **kwargs):
args = [ ]
mydir = os.path.dirname(__file__)
jar = os.path.join(mydir, "../jedit/jedit.jar")
# My Java does not like having non-ASCII characters in jar paths.
# Using relpath won't guarantee those characters won't exist - but it
# makes them less likely in common use cases.
jar = os.path.relpath(jar)
if renpy.windows:
args = [ "javaw.exe", "-jar", jar ]
else:
args = [ "java", "-jar", jar ]
if new_window:
args.append("-newplainview")
else:
args.append("-reuseview")
self.arguments = args
def open(self, filename, line=None, **kwargs):
filename = renpy.exports.fsencode(filename)
self.arguments.append(filename)
if line is not None:
self.arguments.append("+line:{0}".format(line))
def end(self, **kwargs):
print self.arguments
subprocess.Popen(self.arguments)
-19
View File
@@ -1,19 +0,0 @@
# Name: jEdit
# Version: 1
# Description: jEdit requires Java be installed on your computer.
import os
import os.path
import sys
if sys.platform == 'win32':
editor = os.path.normpath(base + "/../jedit/jedit.exe")
editor = renpy.shell_escape(editor)
config.editor = '"' + editor + '" -reuseview "%(filename)s" +line:%(line)d "%(otherfiles)s"'
config.editor_transient = '"' + editor + '" -newplainview "%(filename)s" +line:%(line)d "%(otherfiles)s"'
else:
editor = os.path.normpath(base + "/../jedit/jedit.jar")
editor = renpy.shell_escape(editor)
config.editor = 'java -jar "' + editor + '" -reuseview "%(filename)s" +line:%(line)d "%(otherfiles)s"'
config.editor_transient = 'java -jar "' + editor + '" -newplainview "%(filename)s" +line:%(line)d "%(otherfiles)s"'
+1 -1
View File
@@ -11,7 +11,7 @@ label options:
editor = persistent.editor
if not set_editor:
editor = _(u"Using RENPY_EDITOR")
editor = _(u"Using RENPY_EDIT_PY")
text_variable(_("Text Editor"), editor, "editor",
_(u"Change the default text editor."))
+1 -1
View File
@@ -254,7 +254,7 @@ label edit_script:
python hide:
if not config.editor:
if not "RENPY_EDIT_PY" in os.environ:
error(_(u"No editor has been selected."))
files = [ project.gamedir + "/" + i for i in os.listdir(project.gamedir) if i.endswith(".rpy") if not i[0] == "."]
+6 -6
View File
@@ -284,16 +284,16 @@ framerate = 100
# The number of frames that Ren'Py has shown.
frames = 0
# A text editor that is launched at the location of the current
# NOT USED: A text editor that is launched at the location of the current
# statement.
editor = os.environ.get('RENPY_EDITOR', None)
editor = None # os.environ.get('RENPY_EDITOR', None)
# Text editor, with arguments to reload or clobber the file - used,
# NOT USED: Text editor, with arguments to reload or clobber the file - used,
# for example, to display traceback.txt.
editor_transient = os.environ.get('RENPY_EDITOR_TRANSIENT', editor)
editor_transient = None # os.environ.get('RENPY_EDITOR_TRANSIENT', editor)
# The separator used between files in the text editor.
editor_file_separator = os.environ.get('RENPY_EDITOR_FILE_SEPARATOR', '" "')
# NOT USED: The separator used between files in the text editor.
editor_file_separator = None # os.environ.get('RENPY_EDITOR_FILE_SEPARATOR', '" "')
# Enable developer mode?
developer = False
+39 -56
View File
@@ -45,44 +45,43 @@ class Editor(object):
Each operation takes a path to operate on. If the editor has a buffer
corresponding to that path, that buffer is used. Otherwise, the editor
is implicitly opened.
We reserve the right to add new keyword arguments to methods of this class,
so please ensure that subclasses accept and ignore unknown keyword
arguments.
"""
def open(self, path): #@ReservedAssignment
"""
Ensures `path` is open in the editor.
"""
def reopen(self, path):
"""
Causes the editor to reopen the file at `path`.
"""
def line(self, path, number):
"""
Moves the cursor for `path` to `line`. Lines in a file are numbered
starting with 1.
"""
def focus(self, path):
"""
Focuses `path`, which means that the buffer containing it should be
presented to the user. Ideally, the window containing it will also
pop to the top of the OS's window stack.
"""
def begin(self):
def begin(self, new_window=False, **kwargs):
"""
Begins an editor transaction.
`new_window`
If True, a new editor window will be created and presented to the
user. Otherwise, and existing editor window will be used.
"""
def end(self):
def end(self, **kwargs):
"""
Ends an editor transaction.
"""
def open(self, filename, line=None, **kwargs): #@ReservedAssignment
"""
Ensures `path` is open in the editor. This may be called multiple
times per transaction.
`line`
If not None, this should be a line number to open in the
editor.
The first open call in a transaction is somewhat special - that file
should be given focus in a tabbed editor environment.
"""
class SystemEditor(Editor):
def open(self, filename): #@ReservedAssignment
def open(self, filename, line=None, **kwargs): #@ReservedAssignment
filename = renpy.exports.fsencode(filename)
@@ -104,66 +103,50 @@ editor = None
def init():
"""
Creates the editor object, based on the contents of the RENPY_EDITOR_PY
Creates the editor object, based on the contents of the RENPY_EDIT_PY
file.
"""
global editor
editor = SystemEditor()
path = os.environ.get("RENPY_EDITOR_PY", None)
path = os.environ.get("RENPY_EDIT_PY", None)
if path is None:
return
try:
f = file(path, "rU")
code = f.read()
f.close()
except:
raise Exception("{0} could not be opened.".format(path))
scope = { }
exec code in scope
scope = { "__file__" : path }
execfile(path, scope, scope)
if "editor" in scope:
editor = scope["editor"]
if "Editor" in scope:
editor = scope["Editor"]()
return
raise Exception("{0} did not create an editor variable.".format(path))
raise Exception("{0} did not define an Editor class.".format(path))
def launch_editor(filenames, line=1, transient=False):
"""
Causes the editor to be launched.
"""
if editor is None:
init()
if editor is None:
return False
filenames = [ renpy.parser.unelide_filename(i) for i in filenames ]
try:
editor.begin()
editor.begin(new_window=transient)
for i in filenames:
if transient:
editor.open(i)
else:
editor.reopen(i)
editor.line(filenames[0], line)
editor.focus(filenames[0])
editor.open(i, line)
line = None # The line number only applies to the first filename.
editor.end()
return True
except:
if renpy.config.debug:
raise
traceback.print_exc()
return False
init()
+5
View File
@@ -1,3 +1,4 @@
# Copyright 2004-2012 Tom Rothamel <pytom@bishoujo.us>
#
# Permission is hereby granted, free of charge, to any person
@@ -1676,6 +1677,8 @@ def set_physical_size(size):
def fsencode(s):
"""
:doc: other
Converts s from unicode to the filesystem encoding.
"""
@@ -1687,6 +1690,8 @@ def fsencode(s):
def fsdecode(s):
"""
:doc: other
Converts s from filesystem encoding to unicode.
"""
+4
View File
@@ -9,6 +9,10 @@ The new RAPT tool makes it far easier to package a Ren'Py game for Android.
It can semi-automatically set up an Android build environment on your
system, build a package, and install that package on your Android device.
To fix some editor-related problems, backported the 6.14 editor system. This
changes how editors are configured. Please see :ref:`text-editor-integration`
for a description of the new system.
The new :var:`config.save_dump` variable causes Ren'Py to write out
save_dump.txt each time it saves. This file describes the contents of the
save, making it possible to figure out what's causing an overly large save
-53
View File
@@ -565,59 +565,6 @@ Rarely or Internally Used
A list of layers that are cleared when entering a new context.
.. var:: config.editor = None
If not None, this is expected to be a command line for an editor
that is invoked when the launch_editor (normally shift-E) key is
pressed. The following substitutions make sense here:
%(filename)s
The filename of the most interesting file to be edited. This
is the file that should be shown to the user.
%(line)d
The line number of the most interesting file to show to the user.
%(otherfiles)s
Other, less-interesting files to show to the user.
%(allfiles)s
All the files.
Filename, otherfiles, and allfiles have shell-relevant characters
escaped with backslashes. otherfiles and allfiles separate files
with config.editor_file_separator (by default '" "', a
double-quote, a space, and a quote). Since all filenames should be
enclosed in double-quotes, this means that otherfiles and allfiles
will create several quoted files. If two double-quotes occur in a
row the string, then they are both removed. (This allows an empty
allfiles to be used.)
A reasonable example is::
init python:
config.editor = 'myeditor "%(filename)s" +line:%(line)d "%(otherfiles)s"'
This defaults to the value of the RENPY_EDITOR environment
variable. If not defined by that variable or user code, this is
set automatically by the Ren'Py launcher.
.. var:: config.editor_file_separator = '" "'
The separator used between filenames when lists of files are
provided to the editor.
.. var:: config.editor_transient = None
If not None, this is expected to be a command line for an editor
that is invoked on transient files, such as lint results, parse
errors, and tracebacks. Substitutions are as for config.editor.
This defaults to the value of the RENPY_EDITOR_TRANSIENT
environment variable. If not defined by that variable or user
code, this is set automatically by the Ren'Py launcher.
.. var:: config.fade_music = 0.0
This is the amount of time in seconds to spend fading the old
+66
View File
@@ -0,0 +1,66 @@
.. _text-editor-integration:
=======================
Text Editor Integration
=======================
Ren'Py uses a text editor to allow the user to edit game scripts from the
launcher, and to report errors to the user. By default, Ren'Py uses jEdit
as the text editor when launched from the launcher and the system default
editor otherwise. This can be customized by the user as necessary.
The editor is customized by creating an Editor class in a .edit.py file. This
class contains methods that are called to manage text editing.
When run directly, Ren'Py first looks at the RENPY_EDIT_PY environment
variable to find an .edit.py file to use. If it can find one, it uses the
Editor class defined in that file. If not, it uses a built-in editor class
that launches the editor in a system-specific manner.
When the Ren'Py Launcher is run, it scans subdirectories of the projects
directory and Ren'Py directory to find files of the form `name`.edit.py. (For
example, it would find launcher/jEdit.edit.py and myeditor/MyEditor.edit.py.)
The latest editor with a given `name` is presented to the creator as part of
the launcher options. The launcher also sets RENPY_EDIT_PY to the selected
file, so that games launched from the launcher will use the selected editor.
Writing an .edit.py File
------------------------
An edit.py file is a Python (not Ren'Py) file that must define a single
class, named Editor. Ren'Py will call methods on this class to cause
editing to occur.
Use of the editor is done as part of an editor transaction, which groups
related operations together. For example, if an editor transaction asks
for a new window, all of the files in that transaction should be opened
in the same new window. An editor transaction starts with a call to the
begin method, may contain one or more calls to operation methods, and ends
with a call to the end method.
The edit.py file should import renpy.editor, and the Editor class should
inherit from renpy.editor.Editor. As additional keyword arguments may be
added to methods, each method you define should ignore unknown keyword
arguments. Since you're expected to define your own
Editor subclass, we present the methods with the `self` parameter.
.. class:: Editor
.. method:: begin(self, new_window=False, **kwargs)
Starts an editor transaction.
If `new_window` is true, the editor should attempt to open a new window.
Otherwise, it should attempt to perform the transaction in an existing editor
window.
.. method:: end(self, **kwargs)
Ends a transaction.
.. method:: open(self, filename, line=None, **kwargs)
Opens a `filename` in the editor.
If `line` is not None, attempts to position the editing cursor at `line`.
+8
View File
@@ -6,6 +6,14 @@
displayable. If it can, it will return them as a (x, y, w, h)
tuple. If not, it will return a (None, None, None, None) tuple.
.. function:: renpy.fsdecode(s)
Converts s from filesystem encoding to unicode.
.. function:: renpy.fsencode(s)
Converts s from unicode to the filesystem encoding.
.. function:: renpy.get_physical_size()
Returns the size of the physical window.
+11 -3
View File
@@ -12,7 +12,7 @@ Much of Ren'Py is only documented in the older documentation, which is
stored in the Ren'Py Wiki:
http://www.renpy.org/wiki/
Getting Started
---------------
@@ -69,7 +69,7 @@ Advanced Displayables
Python and Ren'Py
-----------------
.. toctree::
:maxdepth: 1
@@ -88,7 +88,6 @@ Other Platforms
android
End-User Documentation
----------------------
@@ -97,6 +96,15 @@ End-User Documentation
display_problems
Engine Developer Documentation
------------------------------
.. toctree::
:maxdepth: 1
editor
Changes and License
-------------------