CampBuddy/Camp.Buddy v2.2.1/Camp_Buddy-2.2.1-pc/renpy/scriptedit.py
2025-03-03 23:00:33 +01:00

439 lines
11 KiB
Python

# Copyright 2004-2019 Tom Rothamel <pytom@bishoujo.us>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# This file contains code to add and remove statements from the AST
# and the textual representation of Ren'Py code.
from __future__ import print_function
import renpy
import re
import codecs
# A map from line loc (elided filename, line) to the Line object representing
# that line.
lines = { }
# The set of files that have been loaded.
files = set()
class Line(object):
"""
Represents a logical line in a file.
"""
def __init__(self, filename, number, start):
filename = filename.replace("\\", "/")
# The full path to the file with the line in it.
self.filename = filename
# The line number.
self.number = number
# The offset inside the file at which the line starts.
self.start = start
# The offset inside the file at which the line ends.
self.end = start
# The offset inside the lime where the line delimiter ends.
self.end_delim = start
# The text of the line.
self.text = ''
def __repr__(self):
return "<Line {}:{} {!r}>".format(self.filename, self.number, self.text)
def ensure_loaded(filename):
"""
Ensures that the given filename and linenumber are loaded. Doesn't do
anything if the filename can't be loaded.
"""
if not (filename.endswith(".rpy") or filename.endswith(".rpyc")):
return
if filename in files:
return
files.add(filename)
fn = renpy.parser.unelide_filename(filename)
renpy.parser.list_logical_lines(fn, add_lines=True)
def get_line_text(filename, linenumber):
"""
Gets the text of the line with `filename` and `linenumber`, or the None if
the line does not exist.
"""
filename = filename.replace("\\", "/")
ensure_loaded(filename)
if (filename, linenumber) in lines:
return lines[filename, linenumber].text
else:
return None
def adjust_line_locations(filename, linenumber, char_offset, line_offset):
"""
Adjusts the locations in the line data structure.
`filename`, `linenumber`
The filename and first line number to adjust.
`char_offset`
The number of characters in the file to offset the code by,.
`line_offset`
The number of line in the file to offset the code by.
"""
filename = filename.replace("\\", "/")
ensure_loaded(filename)
global lines
new_lines = { }
for key, line in lines.iteritems():
(fn, ln) = key
if (fn == filename) and (linenumber <= ln):
ln += line_offset
line.number += line_offset
line.start += char_offset
line.end += char_offset
line.end_delim += char_offset
new_lines[fn, ln] = line
lines = new_lines
def insert_line_before(code, filename, linenumber):
"""
Adds `code` immediately before `filename` and `linenumber`. Those must
correspond to an existing line, and the code is inserted with the same
indentation as that line.
"""
filename = filename.replace("\\", "/")
if renpy.config.clear_lines:
raise Exception("config.clear_lines must be False for script editing to work.")
ensure_loaded(filename)
old_line = lines[filename, linenumber]
m = re.match(r' *', old_line.text)
indent = m.group(0)
if not code:
indent = ''
if old_line.text.endswith("\r\n") or not old_line.text.endswith("\n"):
line_ending = "\r\n"
else:
line_ending = "\n"
raw_code = indent + code
code = indent + code + line_ending
new_line = Line(old_line.filename, old_line.number, old_line.start)
new_line.text = raw_code
new_line.full_text = code
new_line.end = new_line.start + len(raw_code)
new_line.end_delim = new_line.start + len(code)
with codecs.open(old_line.filename, "r", "utf-8") as f:
data = f.read()
data = data[:old_line.start] + code + data[old_line.start:]
adjust_line_locations(filename, linenumber, len(code), code.count("\n"))
with renpy.loader.auto_lock:
with codecs.open(old_line.filename, "w", "utf-8") as f:
f.write(data)
renpy.loader.add_auto(old_line.filename, force=True)
lines[filename, linenumber] = new_line
def remove_line(filename, linenumber):
"""
Removes `linenumber` from `filename`. The line must exist and correspond
to a logical line.
"""
filename = filename.replace("\\", "/")
if renpy.config.clear_lines:
raise Exception("config.clear_lines must be False for script editing to work.")
ensure_loaded(filename)
line = lines[filename, linenumber]
with codecs.open(line.filename, "r", "utf-8") as f:
data = f.read()
code = data[line.start:line.end_delim]
data = data[:line.start] + data[line.end_delim:]
del lines[filename, linenumber]
adjust_line_locations(filename, linenumber, -len(code), -code.count("\n"))
with renpy.loader.auto_lock:
with codecs.open(line.filename, "w", "utf-8") as f:
f.write(data)
renpy.loader.add_auto(line.filename, force=True)
def get_full_text(filename, linenumber):
"""
Returns the full text of `linenumber` from `filename`, including
any comment or delimiter characters that exist.
"""
filename = filename.replace("\\", "/")
ensure_loaded(filename)
if (filename, linenumber) not in lines:
return None
return lines[filename, linenumber].full_text
def nodes_on_line(filename, linenumber):
"""
Returns a list of nodes that are found on the given line.
"""
ensure_loaded(filename)
rv = [ ]
for i in renpy.game.script.all_stmts:
if (i.filename == filename) and (i.linenumber == linenumber):
rv.append(i)
return rv
def nodes_on_line_at_or_after(filename, linenumber):
"""
Returns a list of nodes that are found at or after the given line.
"""
ensure_loaded(filename)
lines = [ i.linenumber
for i in renpy.game.script.all_stmts
if (i.filename == filename)
if (i.linenumber >= linenumber) ]
if not lines:
return [ ]
return nodes_on_line(filename, min(lines))
def first_and_last_nodes(nodes):
"""
Finds the first and last nodes in `nodes`, a list of nodes. This assumes
that all the nodes are "simple", with no control flow, and that all of
the relevant nodes are in `nodes`.
"""
firsts = [ ]
lasts = [ ]
for i in nodes:
for j in nodes:
if j.next is i:
break
else:
firsts.append(i)
for j in nodes:
if i.next is j:
break
else:
lasts.append(i)
if len(firsts) != 1:
raise Exception("Could not find unique first AST node.")
if len(lasts) != 1:
raise Exception("Could not find unique last AST node.")
return firsts[0], lasts[0]
def adjust_ast_linenumbers(filename, linenumber, offset):
"""
This adjusts the line numbers in the the ast.
`filename`
The filename to adjust.
`linenumber`
The first line to adjust.
`offset`
The amount to adjust by. Positive numbers increase the line
"""
for i in renpy.game.script.all_stmts:
if (i.filename == filename) and (i.linenumber >= linenumber):
i.linenumber += offset
def add_to_ast_before(code, filename, linenumber):
"""
Adds `code`, which must be a textual line of Ren'Py code,
before the given filename and line number.
"""
nodes = nodes_on_line_at_or_after(filename, linenumber)
old, _ = first_and_last_nodes(nodes)
adjust_ast_linenumbers(old.filename, linenumber, 1)
block, _init = renpy.game.script.load_string(old.filename, code, linenumber=linenumber)
# Remove the return statement at the end of the block.
ret_stmt = block.pop()
renpy.game.script.all_stmts.remove(ret_stmt)
if not block:
return
for i in renpy.game.script.all_stmts:
i.replace_next(old, block[0])
renpy.ast.chain_block(block, old)
for i in renpy.game.contexts:
i.replace_node(old, block[0])
renpy.game.log.replace_node(old, block[0])
def can_add_before(filename, linenumber):
"""
Returns True if it's possible to add a line before the given filename
and linenumber, and False if it's not possible.
"""
try:
nodes = nodes_on_line(filename, linenumber)
first_and_last_nodes(nodes)
return True
except:
return False
def remove_from_ast(filename, linenumber):
"""
Removes from the AST all statements that happen to be at `filename`
and `linenumber`, then adjusts the line numbers appropriately.
There's an assumption that the statement(s) on the line are "simple",
not involving control flow.
"""
nodes = nodes_on_line(filename, linenumber)
first, last = first_and_last_nodes(nodes)
new_stmts = [ ]
for i in renpy.game.script.all_stmts:
if i in nodes:
continue
i.replace_next(first, last.next)
new_stmts.append(i)
renpy.game.script.all_stmts = new_stmts
namemap = renpy.game.script.namemap
# This is fairly slow - when we remove a node, we have to replace it with
# the next node. But if we then remove the next node, we have to replace it
# again. So we just walk all the known names to do this.
for k in list(namemap):
if namemap[k] in nodes:
namemap[k] = last.next
adjust_ast_linenumbers(filename, linenumber, -1)
serial = 1
def test_add():
global serial
s = "'Hello world %f'" % serial
serial += 1
node = renpy.game.script.lookup(renpy.game.context().current)
filename = node.filename
linenumber = node.linenumber
add_to_ast_before(s, filename, linenumber)
insert_line_before(s, filename, linenumber)
renpy.exports.restart_interaction()
def test_remove():
node = renpy.game.script.lookup(renpy.game.context().current)
filename = node.filename
linenumber = node.linenumber
remove_from_ast(filename, linenumber)
remove_line(filename, linenumber)
renpy.exports.rollback(checkpoints=0, force=True, greedy=True)