diff --git a/aegisub/src/ass_file.cpp b/aegisub/src/ass_file.cpp
index 025b86441d4097c093215d579e745694a1b64f5a..90671e469ba27dae0e4288cbb61d936d70adff11 100644
--- a/aegisub/src/ass_file.cpp
+++ b/aegisub/src/ass_file.cpp
@@ -42,78 +42,55 @@
 #include "options.h"
 #include "utils.h"
 
-#include <libaegisub/dispatch.h>
 #include <libaegisub/of_type_adaptor.h>
 
 #include <algorithm>
 #include <boost/algorithm/string/case_conv.hpp>
 #include <boost/filesystem/path.hpp>
 
-namespace std {
-	template<>
-	void swap(AssFile &lft, AssFile &rgt) {
-		lft.swap(rgt);
-	}
-}
-
 AssFile::~AssFile() {
-	auto copy = new EntryList;
-	copy->swap(Line);
-	agi::dispatch::Background().Async([=]{
-		copy->clear_and_dispose([](AssEntry *e) { delete e; });
-		delete copy;
-	});
+	Info.clear_and_dispose([](AssEntry *e) { delete e; });
+	Styles.clear_and_dispose([](AssEntry *e) { delete e; });
+	Events.clear_and_dispose([](AssEntry *e) { delete e; });
+	Attachments.clear_and_dispose([](AssEntry *e) { delete e; });
 }
 
 void AssFile::LoadDefault(bool defline) {
-	Line.push_back(*new AssInfo("Title", "Default Aegisub file"));
-	Line.push_back(*new AssInfo("ScriptType", "v4.00+"));
-	Line.push_back(*new AssInfo("WrapStyle", "0"));
-	Line.push_back(*new AssInfo("ScaledBorderAndShadow", "yes"));
+	Info.push_back(*new AssInfo("Title", "Default Aegisub file"));
+	Info.push_back(*new AssInfo("ScriptType", "v4.00+"));
+	Info.push_back(*new AssInfo("WrapStyle", "0"));
+	Info.push_back(*new AssInfo("ScaledBorderAndShadow", "yes"));
 	if (!OPT_GET("Subtitle/Default Resolution/Auto")->GetBool()) {
-		Line.push_back(*new AssInfo("PlayResX", std::to_string(OPT_GET("Subtitle/Default Resolution/Width")->GetInt())));
-		Line.push_back(*new AssInfo("PlayResY", std::to_string(OPT_GET("Subtitle/Default Resolution/Height")->GetInt())));
+		Info.push_back(*new AssInfo("PlayResX", std::to_string(OPT_GET("Subtitle/Default Resolution/Width")->GetInt())));
+		Info.push_back(*new AssInfo("PlayResY", std::to_string(OPT_GET("Subtitle/Default Resolution/Height")->GetInt())));
 	}
-	Line.push_back(*new AssInfo("YCbCr Matrix", "None"));
+	Info.push_back(*new AssInfo("YCbCr Matrix", "None"));
 
-	Line.push_back(*new AssStyle);
+	Styles.push_back(*new AssStyle);
 
 	if (defline)
-		Line.push_back(*new AssDialogue);
+		Events.push_back(*new AssDialogue);
 }
 
-void AssFile::swap(AssFile &that) throw() {
-	Line.swap(that.Line);
+AssFile::AssFile(const AssFile &from) {
+	Info.clone_from(from.Info, std::mem_fun_ref(&AssEntry::Clone), [](AssEntry *e) { delete e; });
+	Styles.clone_from(from.Styles, std::mem_fun_ref(&AssEntry::Clone), [](AssEntry *e) { delete e; });
+	Events.clone_from(from.Events, std::mem_fun_ref(&AssEntry::Clone), [](AssEntry *e) { delete e; });
+	Attachments.clone_from(from.Attachments, std::mem_fun_ref(&AssEntry::Clone), [](AssEntry *e) { delete e; });
 }
 
-AssFile::AssFile(const AssFile &from) {
-	Line.clone_from(from.Line, std::mem_fun_ref(&AssEntry::Clone), [](AssEntry *e) { delete e; });
+void AssFile::swap(AssFile& from) throw() {
+	Info.swap(from.Info);
+	Styles.swap(from.Styles);
+	Events.swap(from.Events);
+	Attachments.swap(from.Attachments);
 }
 
 AssFile& AssFile::operator=(AssFile from) {
-	std::swap(*this, from);
+	swap(from);
 	return *this;
 }
 
-void AssFile::InsertLine(AssEntry *entry) {
-	if (Line.empty()) {
-		Line.push_back(*entry);
-		return;
-	}
-
-	// Search for insertion point
-	entryIter it = Line.end();
-	do {
-		--it;
-		if (it->Group() <= entry->Group()) {
-			Line.insert(++it, *entry);
-			return;
-		}
-	} while (it != Line.begin());
-
-	Line.push_front(*entry);
-}
-
 void AssFile::InsertAttachment(agi::fs::path const& filename) {
 	AssEntryGroup group = AssEntryGroup::GRAPHIC;
 
@@ -121,11 +98,11 @@ void AssFile::InsertAttachment(agi::fs::path const& filename) {
 	if (ext == ".ttf" || ext == ".ttc" || ext == ".pfb")
 		group = AssEntryGroup::FONT;
 
-	InsertLine(new AssAttachment(filename, group));
+	Attachments.push_back(*new AssAttachment(filename, group));
 }
 
 std::string AssFile::GetScriptInfo(std::string const& key) const {
-	for (const auto info : Line | agi::of_type<AssInfo>()) {
+	for (const auto info : Info | agi::of_type<AssInfo>()) {
 		if (boost::iequals(key, info->Key()))
 			return info->Value();
 	}
@@ -154,7 +131,7 @@ void AssFile::SaveUIState(std::string const& key, std::string const& value) {
 }
 
 void AssFile::SetScriptInfo(std::string const& key, std::string const& value) {
-	for (auto info : Line | agi::of_type<AssInfo>()) {
+	for (auto info : Info | agi::of_type<AssInfo>()) {
 		if (boost::iequals(key, info->Key())) {
 			if (value.empty())
 				delete info;
@@ -165,7 +142,7 @@ void AssFile::SetScriptInfo(std::string const& key, std::string const& value) {
 	}
 
 	if (!value.empty())
-		InsertLine(new AssInfo(key, value));
+		Info.push_back(*new AssInfo(key, value));
 }
 
 void AssFile::GetResolution(int &sw,int &sh) const {
@@ -192,13 +169,13 @@ void AssFile::GetResolution(int &sw,int &sh) const {
 
 std::vector<std::string> AssFile::GetStyles() const {
 	std::vector<std::string> styles;
-	for (auto style : Line | agi::of_type<AssStyle>())
+	for (auto style : Styles | agi::of_type<AssStyle>())
 		styles.push_back(style->name);
 	return styles;
 }
 
 AssStyle *AssFile::GetStyle(std::string const& name) {
-	for (auto style : Line | agi::of_type<AssStyle>()) {
+	for (auto style : Styles | agi::of_type<AssStyle>()) {
 		if (boost::iequals(style->name, name))
 			return style;
 	}
@@ -237,7 +214,7 @@ bool AssFile::CompLayer(const AssDialogue* lft, const AssDialogue* rgt) {
 }
 
 void AssFile::Sort(CompFunc comp, std::set<AssDialogue*> const& limit) {
-	Sort(Line, comp, limit);
+	Sort(Events, comp, limit);
 }
 namespace {
 	inline bool is_dialogue(AssEntry *e, std::set<AssDialogue*> const& limit) {
diff --git a/aegisub/src/ass_file.h b/aegisub/src/ass_file.h
index c37fc6b5480543c7fc003100538caec7a25ddaac..dd36272e578910361d9583faf84202ba22b2ddd2 100644
--- a/aegisub/src/ass_file.h
+++ b/aegisub/src/ass_file.h
@@ -62,7 +62,10 @@ class AssFile {
 	agi::signal::Signal<AssFileCommit> PushState;
 public:
 	/// The lines in the file
-	EntryList Line;
+	EntryList Info;
+	EntryList Styles;
+	EntryList Events;
+	EntryList Attachments;
 
 	AssFile() { }
 	AssFile(const AssFile &from);
@@ -72,8 +75,6 @@ public:
 	/// @brief Load default file
 	/// @param defline Add a blank line to the file
 	void LoadDefault(bool defline=true);
-	/// Add a line to the file at the end of the appropriate section
-	void InsertLine(AssEntry *line);
 	/// Attach a file to the ass file
 	void InsertAttachment(agi::fs::path const& filename);
 	/// Get the names of all of the styles available
diff --git a/aegisub/src/ass_karaoke.cpp b/aegisub/src/ass_karaoke.cpp
index b22b9e48f27a40a81941db1ac239f3887c62d824..9181d78e012a544bf246be3ac7e4a346f7c807ab 100644
--- a/aegisub/src/ass_karaoke.cpp
+++ b/aegisub/src/ass_karaoke.cpp
@@ -280,7 +280,7 @@ void AssKaraoke::SplitLines(std::set<AssDialogue*> const& lines, agi::Context *c
 	SubtitleSelection sel = c->selectionController->GetSelectedSet();
 
 	bool did_split = false;
-	for (entryIter it = c->ass->Line.begin(); it != c->ass->Line.end(); ++it) {
+	for (entryIter it = c->ass->Events.begin(); it != c->ass->Events.end(); ++it) {
 		AssDialogue *diag = dynamic_cast<AssDialogue*>(&*it);
 		if (!diag || !lines.count(diag)) continue;
 
@@ -298,7 +298,7 @@ void AssKaraoke::SplitLines(std::set<AssDialogue*> const& lines, agi::Context *c
 			new_line->End = syl.start_time + syl.duration;
 			new_line->Text = syl.GetText(false);
 
-			c->ass->Line.insert(it, *new_line);
+			c->ass->Events.insert(it, *new_line);
 
 			if (in_sel)
 				sel.insert(new_line);
diff --git a/aegisub/src/ass_parser.cpp b/aegisub/src/ass_parser.cpp
index d5862767561abdc7566e9ed74fe7a4db31779519..262db94afd36d3434d26ec52ccc088d26fe54a94 100644
--- a/aegisub/src/ass_parser.cpp
+++ b/aegisub/src/ass_parser.cpp
@@ -33,7 +33,6 @@ AssParser::AssParser(AssFile *target, int version)
 , version(version)
 , state(&AssParser::ParseScriptInfoLine)
 {
-	std::fill(begin(insertion_positions), end(insertion_positions), nullptr);
 }
 
 AssParser::~AssParser() {
@@ -52,7 +51,7 @@ void AssParser::ParseAttachmentLine(std::string const& data) {
 
 	// Data is over, add attachment to the file
 	if (!valid_data || is_filename) {
-		InsertLine(attach.release());
+		target->Attachments.push_back(*attach.release());
 		AddLine(data);
 	}
 	else {
@@ -60,7 +59,7 @@ void AssParser::ParseAttachmentLine(std::string const& data) {
 
 		// Done building
 		if (data.size() < 80)
-			InsertLine(attach.release());
+			target->Attachments.push_back(*attach.release());
 	}
 }
 
@@ -91,17 +90,17 @@ void AssParser::ParseScriptInfoLine(std::string const& data) {
 	size_t pos = data.find(':');
 	if (pos == data.npos) return;
 
-	InsertLine(new AssInfo(data.substr(0, pos), boost::trim_left_copy(data.substr(pos + 1))));
+	target->Info.push_back(*new AssInfo(data.substr(0, pos), boost::trim_left_copy(data.substr(pos + 1))));
 }
 
 void AssParser::ParseEventLine(std::string const& data) {
 	if (boost::starts_with(data, "Dialogue:") || boost::starts_with(data, "Comment:"))
-		InsertLine(new AssDialogue(data));
+		target->Events.push_back(*new AssDialogue(data));
 }
 
 void AssParser::ParseStyleLine(std::string const& data) {
 	if (boost::starts_with(data, "Style:"))
-		InsertLine(new AssStyle(data, version));
+		target->Styles.push_back(*new AssStyle(data, version));
 }
 
 void AssParser::ParseFontLine(std::string const& data) {
@@ -152,12 +151,3 @@ void AssParser::AddLine(std::string const& data) {
 
 	(this->*state)(data);
 }
-
-void AssParser::InsertLine(AssEntry *entry) {
-	AssEntry *position = insertion_positions[(size_t)entry->Group()];
-	if (position)
-		target->Line.insert(++target->Line.iterator_to(*position), *entry);
-	else
-		target->Line.push_back(*entry);
-	insertion_positions[(size_t)entry->Group()] = entry;
-}
diff --git a/aegisub/src/ass_parser.h b/aegisub/src/ass_parser.h
index 820638a8d09c3bb8c87d581385afc903fcedb0f1..b97bba7665c013cc10404a978fe0ed9325ec72b4 100644
--- a/aegisub/src/ass_parser.h
+++ b/aegisub/src/ass_parser.h
@@ -16,8 +16,6 @@
 #include <map>
 #include <memory>
 
-#include "ass_entry.h"
-
 class AssAttachment;
 class AssFile;
 
@@ -26,9 +24,6 @@ class AssParser {
 	int version;
 	std::unique_ptr<AssAttachment> attach;
 	void (AssParser::*state)(std::string const&);
-	std::array<AssEntry*, (size_t)AssEntryGroup::GROUP_MAX> insertion_positions;
-
-	void InsertLine(AssEntry *entry);
 
 	void ParseAttachmentLine(std::string const& data);
 	void ParseEventLine(std::string const& data);
diff --git a/aegisub/src/audio_timing_dialogue.cpp b/aegisub/src/audio_timing_dialogue.cpp
index 7944f2f50c0546d3d7618f87d1d53c379d604a75..f29ea64d109c1d321302704341a570dcce610429 100644
--- a/aegisub/src/audio_timing_dialogue.cpp
+++ b/aegisub/src/audio_timing_dialogue.cpp
@@ -742,20 +742,20 @@ void AudioTimingControllerDialogue::RegenerateInactiveLines()
 	case 2: // Previous and next lines
 		if (AssDialogue *line = context->selectionController->GetActiveLine())
 		{
-			entryIter current_line = context->ass->Line.iterator_to(*line);
-			if (current_line == context->ass->Line.end())
+			entryIter current_line = context->ass->Events.iterator_to(*line);
+			if (current_line == context->ass->Events.end())
 				break;
 
 			entryIter prev = current_line;
-			while (--prev != context->ass->Line.begin() && !predicate(*prev)) ;
-			if (prev != context->ass->Line.begin())
+			while (--prev != context->ass->Events.begin() && !predicate(*prev)) ;
+			if (prev != context->ass->Events.begin())
 				AddInactiveLine(sel, static_cast<AssDialogue*>(&*prev));
 
 			if (mode == 2)
 			{
 				entryIter next =
-					find_if(++current_line, context->ass->Line.end(), predicate);
-				if (next != context->ass->Line.end())
+					find_if(++current_line, context->ass->Events.end(), predicate);
+				if (next != context->ass->Events.end())
 					AddInactiveLine(sel, static_cast<AssDialogue*>(&*next));
 			}
 		}
@@ -763,7 +763,7 @@ void AudioTimingControllerDialogue::RegenerateInactiveLines()
 	case 3: // All inactive lines
 	{
 		AssDialogue *active_line = context->selectionController->GetActiveLine();
-		for (auto& line : context->ass->Line)
+		for (auto& line : context->ass->Events)
 		{
 			if (&line != active_line && predicate(line))
 				AddInactiveLine(sel, static_cast<AssDialogue*>(&line));
diff --git a/aegisub/src/auto4_lua.cpp b/aegisub/src/auto4_lua.cpp
index 268f49d920329bf2c5e18e39f5948212e080b7ab..6ba8f4123ac514902ef2d96acb5530d509ddc24b 100644
--- a/aegisub/src/auto4_lua.cpp
+++ b/aegisub/src/auto4_lua.cpp
@@ -37,6 +37,7 @@
 #include "auto4_lua.h"
 
 #include "auto4_lua_utils.h"
+#include "ass_attachment.h"
 #include "ass_dialogue.h"
 #include "ass_file.h"
 #include "ass_style.h"
@@ -838,13 +839,11 @@ namespace Automation4 {
 		lua_newtable(L);
 		int active_idx = -1;
 
-		int row = 0;
+		int row = c->ass->Info.size() + c->ass->Styles.size();
 		int idx = 1;
-		for (auto& line : c->ass->Line) {
+		for (auto& line : c->ass->Events) {
 			++row;
-			AssDialogue *diag = dynamic_cast<AssDialogue*>(&line);
-			if (!diag) continue;
-
+			auto diag = static_cast<AssDialogue*>(&line);
 			if (diag == active_line) active_idx = row;
 			if (sel.count(diag)) {
 				push_value(L, row);
@@ -901,7 +900,7 @@ namespace Automation4 {
 		try {
 			LuaThreadedCall(L, 3, 2, from_wx(StrDisplay(c)), c->parent, true);
 
-			subsobj->ProcessingComplete(StrDisplay(c));
+			auto lines = subsobj->ProcessingComplete(StrDisplay(c));
 
 			AssDialogue *active_line = nullptr;
 			int active_idx = 0;
@@ -909,8 +908,8 @@ namespace Automation4 {
 			// Check for a new active row
 			if (lua_isnumber(L, -1)) {
 				active_idx = lua_tointeger(L, -1);
-				if (active_idx < 1 || active_idx > (int)c->ass->Line.size()) {
-					wxLogError("Active row %d is out of bounds (must be 1-%u)", active_idx, c->ass->Line.size());
+				if (active_idx < 1 || active_idx > (int)lines.size()) {
+					wxLogError("Active row %d is out of bounds (must be 1-%u)", active_idx, lines.size());
 					active_idx = 0;
 				}
 			}
@@ -921,26 +920,21 @@ namespace Automation4 {
 			// top of stack will be selected lines array, if any was returned
 			if (lua_istable(L, -1)) {
 				std::set<AssDialogue*> sel;
-				entryIter it = c->ass->Line.begin();
-				int last_idx = 1;
 				lua_for_each(L, [&] {
 					if (lua_isnumber(L, -1)) {
 						int cur = lua_tointeger(L, -1);
-						if (cur < 1 || cur > (int)c->ass->Line.size()) {
-							wxLogError("Selected row %d is out of bounds (must be 1-%u)", cur, c->ass->Line.size());
+						if (cur < 1 || cur > (int)lines.size()) {
+							wxLogError("Selected row %d is out of bounds (must be 1-%u)", cur, lines.size());
 							throw LuaForEachBreak();
 						}
 
-						advance(it, cur - last_idx);
-
-						AssDialogue *diag = dynamic_cast<AssDialogue*>(&*it);
+						AssDialogue *diag = dynamic_cast<AssDialogue*>(lines[cur - 1]);
 						if (!diag) {
 							wxLogError("Selected row %d is not a dialogue line", cur);
 							throw LuaForEachBreak();
 						}
 
 						sel.insert(diag);
-						last_idx = cur;
 						if (!active_line || active_idx == cur)
 							active_line = diag;
 					}
diff --git a/aegisub/src/auto4_lua.h b/aegisub/src/auto4_lua.h
index d0e743dc99554f65e44f08f0c0c5e1fc97b34aec..fe0fdbb85855c08cf48103a1421d21790c14fd29 100644
--- a/aegisub/src/auto4_lua.h
+++ b/aegisub/src/auto4_lua.h
@@ -115,7 +115,7 @@ namespace Automation4 {
 		/// @param set_undo If there's any uncommitted changes to the file,
 		///                 they will be automatically committed with this
 		///                 description
-		void ProcessingComplete(wxString const& undo_description = wxString());
+		std::vector<AssEntry *> ProcessingComplete(wxString const& undo_description = wxString());
 
 		/// End processing without applying any changes made
 		void Cancel();
diff --git a/aegisub/src/auto4_lua_assfile.cpp b/aegisub/src/auto4_lua_assfile.cpp
index 7f18265bf13d3c25840490efe437884fe4025687..87350f3e36ed59a73df5574c99eae84d0b7cad55 100644
--- a/aegisub/src/auto4_lua_assfile.cpp
+++ b/aegisub/src/auto4_lua_assfile.cpp
@@ -121,9 +121,7 @@ namespace {
 		{
 			case AssEntryGroup::DIALOGUE: return AssFile::COMMIT_DIAG_ADDREM;
 			case AssEntryGroup::STYLE:    return AssFile::COMMIT_STYLES;
-			case AssEntryGroup::FONT:     return AssFile::COMMIT_ATTACHMENT;
-			case AssEntryGroup::GRAPHIC:  return AssFile::COMMIT_ATTACHMENT;
-			default:             return AssFile::COMMIT_SCRIPTINFO;
+			default:                      return AssFile::COMMIT_SCRIPTINFO;
 		}
 	}
 }
@@ -217,8 +215,7 @@ namespace Automation4 {
 			set_field(L, "class", "style");
 		}
 		else {
-			// Attachments
-			set_field(L, "class", "unknown");
+			assert(false);
 		}
 	}
 
@@ -593,27 +590,40 @@ namespace Automation4 {
 		return *((LuaAssFile**)ud);
 	}
 
-	void LuaAssFile::ProcessingComplete(wxString const& undo_description)
+	std::vector<AssEntry *> LuaAssFile::ProcessingComplete(wxString const& undo_description)
 	{
+		auto apply_lines = [&](std::vector<AssEntry *> const& lines) {
+			ass->Info.clear();
+			ass->Styles.clear();
+			ass->Events.clear();
+
+			for (auto line : lines) {
+				switch (line->Group()) {
+					case AssEntryGroup::INFO:     ass->Info.push_back(*static_cast<AssInfo *>(line)); break;
+					case AssEntryGroup::STYLE:    ass->Styles.push_back(*static_cast<AssStyle *>(line)); break;
+					case AssEntryGroup::DIALOGUE: ass->Events.push_back(*static_cast<AssDialogue *>(line)); break;
+					default: break;
+				}
+			}
+		};
 		// Apply any pending commits
 		for (auto const& pc : pending_commits) {
-			ass->Line.clear();
-			boost::push_back(ass->Line, pc.lines | boost::adaptors::indirected);
+			apply_lines(pc.lines);
 			ass->Commit(pc.mesage, pc.modification_type);
 		}
 
 		// Commit any changes after the last undo point was set
-		if (modification_type) {
-			ass->Line.clear();
-			boost::push_back(ass->Line, lines | boost::adaptors::indirected);
-		}
+		if (modification_type)
+			apply_lines(lines);
 		if (modification_type && can_set_undo && !undo_description.empty())
 			ass->Commit(undo_description, modification_type);
 
 		lines_to_delete.clear();
 
+		auto ret = std::move(lines);
 		references--;
 		if (!references) delete this;
+		return ret;
 	}
 
 	void LuaAssFile::Cancel()
@@ -631,7 +641,11 @@ namespace Automation4 {
 	, modification_type(0)
 	, references(2)
 	{
-		for (auto& line : ass->Line)
+		for (auto& line : ass->Info)
+			lines.push_back(&line);
+		for (auto& line : ass->Styles)
+			lines.push_back(&line);
+		for (auto& line : ass->Events)
 			lines.push_back(&line);
 
 		// prepare userdata object
diff --git a/aegisub/src/base_grid.cpp b/aegisub/src/base_grid.cpp
index 5d10910189ff7a7ecd96939ebc4ca3059434ba3a..0e00ff47a9e795b6e0fce154f3fb2fdab7b22a1f 100644
--- a/aegisub/src/base_grid.cpp
+++ b/aegisub/src/base_grid.cpp
@@ -265,7 +265,7 @@ void BaseGrid::UpdateMaps() {
 	index_line_map.clear();
 	line_index_map.clear();
 
-	for (auto curdiag : context->ass->Line | agi::of_type<AssDialogue>()) {
+	for (auto curdiag : context->ass->Events | agi::of_type<AssDialogue>()) {
 		line_index_map[curdiag] = (int)index_line_map.size();
 		index_line_map.push_back(curdiag);
 	}
diff --git a/aegisub/src/command/edit.cpp b/aegisub/src/command/edit.cpp
index cdbd6513021a45d272a96df4abfc24ec4d1777a3..a85e4e96c510ba85a9493fe1376eee60f334169a 100644
--- a/aegisub/src/command/edit.cpp
+++ b/aegisub/src/command/edit.cpp
@@ -488,7 +488,7 @@ struct edit_find_replace : public Command {
 static std::string get_entry_data(AssDialogue *d) { return d->GetEntryData(); }
 static void copy_lines(agi::Context *c) {
 	SubtitleSelection sel = c->selectionController->GetSelectedSet();
-	SetClipboard(join(c->ass->Line
+	SetClipboard(join(c->ass->Events
 		| agi::of_type<AssDialogue>()
 		| filtered([&](AssDialogue *d) { return sel.count(d); })
 		| transformed(get_entry_data),
@@ -503,7 +503,7 @@ static void delete_lines(agi::Context *c, wxString const& commit_message) {
 	AssDialogue *post_sel = nullptr;
 	bool hit_selection = false;
 
-	for (auto diag : c->ass->Line | agi::of_type<AssDialogue>()) {
+	for (auto diag : c->ass->Events | agi::of_type<AssDialogue>()) {
 		if (sel.count(diag))
 			hit_selection = true;
 		else if (hit_selection && !post_sel) {
@@ -519,7 +519,7 @@ static void delete_lines(agi::Context *c, wxString const& commit_message) {
 	// need to create a new dialogue line for it, and we can't select dialogue
 	// lines until after they're committed.
 	std::vector<std::unique_ptr<AssEntry>> to_delete;
-	c->ass->Line.remove_and_dispose_if([&sel](AssEntry const& e) {
+	c->ass->Events.remove_and_dispose_if([&sel](AssEntry const& e) {
 		return sel.count(const_cast<AssDialogue *>(static_cast<const AssDialogue*>(&e)));
 	}, [&](AssEntry *e) {
 		to_delete.emplace_back(e);
@@ -532,7 +532,7 @@ static void delete_lines(agi::Context *c, wxString const& commit_message) {
 	// lines, so make a new one
 	if (!new_active) {
 		new_active = new AssDialogue;
-		c->ass->InsertLine(new_active);
+		c->ass->Events.push_back(*new_active);
 	}
 
 	c->ass->Commit(commit_message, AssFile::COMMIT_DIAG_ADDREM);
@@ -605,8 +605,8 @@ static void duplicate_lines(agi::Context *c, int shift) {
 	SubtitleSelectionController::Selection new_sel;
 	AssDialogue *new_active = nullptr;
 
-	entryIter start = c->ass->Line.begin();
-	entryIter end = c->ass->Line.end();
+	entryIter start = c->ass->Events.begin();
+	entryIter end = c->ass->Events.end();
 	while (start != end) {
 		// Find the first line in the selection
 		start = find_if(start, end, sel);
@@ -622,7 +622,7 @@ static void duplicate_lines(agi::Context *c, int shift) {
 			auto old_diag = static_cast<AssDialogue*>(&*start);
 			auto  new_diag = new AssDialogue(*old_diag);
 
-			c->ass->Line.insert(insert_pos, *new_diag);
+			c->ass->Events.insert(insert_pos, *new_diag);
 			new_sel.insert(new_diag);
 			if (!new_active)
 				new_active = new_diag;
@@ -703,7 +703,7 @@ static void combine_lines(agi::Context *c, void (*combiner)(AssDialogue *, AssDi
 	SubtitleSelection sel = c->selectionController->GetSelectedSet();
 
 	AssDialogue *first = nullptr;
-	for (entryIter it = c->ass->Line.begin(); it != c->ass->Line.end(); ) {
+	for (entryIter it = c->ass->Events.begin(); it != c->ass->Events.end(); ) {
 		AssDialogue *diag = dynamic_cast<AssDialogue*>(&*it++);
 		if (!diag || !sel.count(diag))
 			continue;
@@ -790,8 +790,8 @@ static bool try_paste_lines(agi::Context *c) {
 	for (auto& line : parsed)
 		new_selection.insert(static_cast<AssDialogue *>(&line));
 
-	auto pos = c->ass->Line.iterator_to(*c->selectionController->GetActiveLine());
-	c->ass->Line.splice(pos, parsed, parsed.begin(), parsed.end());
+	auto pos = c->ass->Events.iterator_to(*c->selectionController->GetActiveLine());
+	c->ass->Events.splice(pos, parsed, parsed.begin(), parsed.end());
 	c->ass->Commit(_("paste"), AssFile::COMMIT_DIAG_ADDREM);
 	c->selectionController->SetSelectionAndActive(new_selection, new_active);
 
@@ -821,9 +821,9 @@ struct edit_line_paste : public Command {
 				ctrl->Paste();
 		}
 		else {
-			auto pos = c->ass->Line.iterator_to(*c->selectionController->GetActiveLine());
+			auto pos = c->ass->Events.iterator_to(*c->selectionController->GetActiveLine());
 			paste_lines(c, false, [=](AssDialogue *new_line) -> AssDialogue * {
-				c->ass->Line.insert(pos, *new_line);
+				c->ass->Events.insert(pos, *new_line);
 				return new_line;
 			});
 		}
@@ -852,15 +852,15 @@ struct edit_line_paste_over : public Command {
 
 		// Only one line selected, so paste over downwards from the active line
 		if (sel.size() < 2) {
-			auto pos = c->ass->Line.iterator_to(*c->selectionController->GetActiveLine());
+			auto pos = c->ass->Events.iterator_to(*c->selectionController->GetActiveLine());
 
 			paste_lines(c, true, [&](AssDialogue *new_line) -> AssDialogue * {
 				std::unique_ptr<AssDialogue> deleter(new_line);
-				if (pos == c->ass->Line.end()) return nullptr;
+				if (pos == c->ass->Events.end()) return nullptr;
 
 				AssDialogue *ret = paste_over(c->parent, pasteOverOptions, new_line, static_cast<AssDialogue*>(&*pos));
 				if (ret)
-					pos = find_if(next(pos), c->ass->Line.end(), cast<AssDialogue*>());
+					pos = find_if(next(pos), c->ass->Events.end(), cast<AssDialogue*>());
 				return ret;
 			});
 		}
@@ -870,7 +870,7 @@ struct edit_line_paste_over : public Command {
 			// Sort the selection by grid order
 			std::vector<AssDialogue*> sorted_selection;
 			sorted_selection.reserve(sel.size());
-			for (auto& line : c->ass->Line) {
+			for (auto& line : c->ass->Events) {
 				if (sel.count(static_cast<AssDialogue*>(&line)))
 					sorted_selection.push_back(static_cast<AssDialogue*>(&line));
 			}
@@ -983,7 +983,7 @@ struct edit_line_recombine : public validate_sel_multiple {
 
 		// Remove now non-existent lines from the selection
 		SubtitleSelection lines, new_sel;
-		boost::copy(c->ass->Line | agi::of_type<AssDialogue>(), inserter(lines, lines.begin()));
+		boost::copy(c->ass->Events | agi::of_type<AssDialogue>(), inserter(lines, lines.begin()));
 		boost::set_intersection(lines, sel_set, inserter(new_sel, new_sel.begin()));
 
 		if (new_sel.empty())
@@ -1015,7 +1015,7 @@ void split_lines(agi::Context *c, Func&& set_time) {
 
 	AssDialogue *n1 = c->selectionController->GetActiveLine();
 	auto n2 = new AssDialogue(*n1);
-	c->ass->Line.insert(++c->ass->Line.iterator_to(*n1), *n2);
+	c->ass->Events.insert(++c->ass->Events.iterator_to(*n1), *n2);
 
 	std::string orig = n1->Text;
 	n1->Text = boost::trim_right_copy(orig.substr(0, pos));
diff --git a/aegisub/src/command/grid.cpp b/aegisub/src/command/grid.cpp
index 2378718c614aaeed161617d49d4763d729a45160..b51043c63fa229a3dde3604fe3d5279535b48c87 100644
--- a/aegisub/src/command/grid.cpp
+++ b/aegisub/src/command/grid.cpp
@@ -79,8 +79,8 @@ struct grid_line_next_create : public Command {
 			newline->End = cur->End + OPT_GET("Timing/Default Duration")->GetInt();
 			newline->Style = cur->Style;
 
-			entryIter pos = c->ass->Line.iterator_to(*cur);
-			c->ass->Line.insert(++pos, *newline);
+			entryIter pos = c->ass->Events.iterator_to(*cur);
+			c->ass->Events.insert(++pos, *newline);
 			c->ass->Commit(_("line insertion"), AssFile::COMMIT_DIAG_ADDREM);
 			c->selectionController->NextLine();
 		}
@@ -356,7 +356,7 @@ struct grid_move_up : public Command {
 	}
 
 	void operator()(agi::Context *c) override {
-		if (move_one(c->ass->Line.begin(), c->ass->Line.end(), c->selectionController->GetSelectedSet(), 1))
+		if (move_one(c->ass->Events.begin(), c->ass->Events.end(), c->selectionController->GetSelectedSet(), 1))
 			c->ass->Commit(_("move lines"), AssFile::COMMIT_ORDER);
 	}
 };
@@ -373,7 +373,7 @@ struct grid_move_down : public Command {
 	}
 
 	void operator()(agi::Context *c) override {
-		if (move_one(--c->ass->Line.end(), c->ass->Line.begin(), c->selectionController->GetSelectedSet(), -1))
+		if (move_one(--c->ass->Events.end(), c->ass->Events.begin(), c->selectionController->GetSelectedSet(), -1))
 			c->ass->Commit(_("move lines"), AssFile::COMMIT_ORDER);
 	}
 };
diff --git a/aegisub/src/command/subtitle.cpp b/aegisub/src/command/subtitle.cpp
index 8cdebfe8ca022bd7259f137b121cf1cded652ebc..89ee24ef6beb2563078ba9fc2e68cd3a44af3642 100644
--- a/aegisub/src/command/subtitle.cpp
+++ b/aegisub/src/command/subtitle.cpp
@@ -54,6 +54,7 @@
 #include "../video_context.h"
 
 #include <libaegisub/charset_conv.h>
+#include <libaegisub/of_type_adaptor.h>
 #include <libaegisub/util.h>
 
 #include <wx/msgdlg.h>
@@ -123,10 +124,10 @@ static void insert_subtitle_at_video(agi::Context *c, bool after) {
 	def->End = video_ms + OPT_GET("Timing/Default Duration")->GetInt();
 	def->Style = c->selectionController->GetActiveLine()->Style;
 
-	entryIter pos = c->ass->Line.iterator_to(*c->selectionController->GetActiveLine());
+	entryIter pos = c->ass->Events.iterator_to(*c->selectionController->GetActiveLine());
 	if (after) ++pos;
 
-	c->ass->Line.insert(pos, *def);
+	c->ass->Events.insert(pos, *def);
 	c->ass->Commit(_("line insertion"), AssFile::COMMIT_DIAG_ADDREM);
 
 	c->selectionController->SetSelectionAndActive({ def }, def);
@@ -146,17 +147,17 @@ struct subtitle_insert_after : public validate_nonempty_selection {
 		new_line->Start = active_line->End;
 		new_line->End = new_line->Start + OPT_GET("Timing/Default Duration")->GetInt();
 
-		for (entryIter it = c->ass->Line.begin(); it != c->ass->Line.end(); ++it) {
-			AssDialogue *diag = dynamic_cast<AssDialogue*>(&*it);
+		for (entryIter it = c->ass->Events.begin(); it != c->ass->Events.end(); ++it) {
+			AssDialogue *diag = static_cast<AssDialogue*>(&*it);
 
 			// Limit the line to the available time
-			if (diag && diag->Start >= new_line->Start)
+			if (diag->Start >= new_line->Start)
 				new_line->End = std::min(new_line->End, diag->Start);
 
 			// If we just hit the active line, insert the new line after it
 			if (diag == active_line) {
 				++it;
-				c->ass->Line.insert(it, *new_line);
+				c->ass->Events.insert(it, *new_line);
 				--it;
 			}
 		}
@@ -191,16 +192,16 @@ struct subtitle_insert_before : public validate_nonempty_selection {
 		new_line->End = active_line->Start;
 		new_line->Start = new_line->End - OPT_GET("Timing/Default Duration")->GetInt();
 
-		for (entryIter it = c->ass->Line.begin(); it != c->ass->Line.end(); ++it) {
-			AssDialogue *diag = dynamic_cast<AssDialogue*>(&*it);
+		for (entryIter it = c->ass->Events.begin(); it != c->ass->Events.end(); ++it) {
+			auto diag = static_cast<AssDialogue*>(&*it);
 
 			// Limit the line to the available time
-			if (diag && diag->End <= new_line->End)
+			if (diag->End <= new_line->End)
 				new_line->Start = std::max(new_line->Start, diag->End);
 
 			// If we just hit the active line, insert the new line before it
 			if (diag == active_line)
-				c->ass->Line.insert(it, *new_line);
+				c->ass->Events.insert(it, *new_line);
 		}
 
 		c->ass->Commit(_("line insertion"), AssFile::COMMIT_DIAG_ADDREM);
@@ -371,7 +372,7 @@ struct subtitle_select_all : public Command {
 
 	void operator()(agi::Context *c) override {
 		SubtitleSelection sel;
-		transform(c->ass->Line.begin(), c->ass->Line.end(),
+		transform(c->ass->Events.begin(), c->ass->Events.end(),
 			inserter(sel, sel.begin()), cast<AssDialogue*>());
 		sel.erase(nullptr);
 		c->selectionController->SetSelectedSet(sel);
@@ -393,10 +394,8 @@ struct subtitle_select_visible : public Command {
 		SubtitleSelectionController::Selection new_selection;
 		int frame = c->videoController->GetFrameN();
 
-		for (entryIter it = c->ass->Line.begin(); it != c->ass->Line.end(); ++it) {
-			AssDialogue *diag = dynamic_cast<AssDialogue*>(&*it);
-			if (diag &&
-				c->videoController->FrameAtTime(diag->Start, agi::vfr::START) <= frame &&
+		for (auto diag : c->ass->Events | agi::of_type<AssDialogue>()) {
+			if (c->videoController->FrameAtTime(diag->Start, agi::vfr::START) <= frame &&
 				c->videoController->FrameAtTime(diag->End, agi::vfr::END) >= frame)
 			{
 				if (new_selection.empty())
diff --git a/aegisub/src/command/time.cpp b/aegisub/src/command/time.cpp
index cdd2b423ffb5082ff3a6665d9f41c73055d87a41..5bceb71554791a5243fdfb27bc20c7b85a0db7bf 100644
--- a/aegisub/src/command/time.cpp
+++ b/aegisub/src/command/time.cpp
@@ -67,7 +67,7 @@ namespace {
 			if (sel.size() < 2) return !sel.empty();
 
 			size_t found = 0;
-			for (auto diag : c->ass->Line | agi::of_type<AssDialogue>()) {
+			for (auto diag : c->ass->Events | agi::of_type<AssDialogue>()) {
 				if (sel.count(diag)) {
 					if (++found == sel.size())
 						return true;
@@ -84,7 +84,7 @@ static void adjoin_lines(agi::Context *c, bool set_start) {
 	AssDialogue *prev = nullptr;
 	size_t seen = 0;
 	bool prev_sel = false;
-	for (auto diag : c->ass->Line | agi::of_type<AssDialogue>()) {
+	for (auto diag : c->ass->Events | agi::of_type<AssDialogue>()) {
 		bool cur_sel = !!sel.count(diag);
 		if (prev) {
 			// One row selections act as if the previous or next line was selected
diff --git a/aegisub/src/dialog_attachments.cpp b/aegisub/src/dialog_attachments.cpp
index 2ff696e47c5067ec692bc7a7c81715567aa0401f..dc4de6e18a5655b88045f80764fb43b54779654b 100644
--- a/aegisub/src/dialog_attachments.cpp
+++ b/aegisub/src/dialog_attachments.cpp
@@ -99,7 +99,7 @@ void DialogAttachments::UpdateList() {
 	listView->InsertColumn(1, _("Size"), wxLIST_FORMAT_LEFT, 100);
 	listView->InsertColumn(2, _("Group"), wxLIST_FORMAT_LEFT, 100);
 
-	for (auto attach : ass->Line | agi::of_type<AssAttachment>()) {
+	for (auto attach : ass->Attachments | agi::of_type<AssAttachment>()) {
 		int row = listView->GetItemCount();
 		listView->InsertItem(row, to_wx(attach->GetFileName(true)));
 		listView->SetItem(row, 1, PrettySize(attach->GetSize()));
diff --git a/aegisub/src/dialog_kara_timing_copy.cpp b/aegisub/src/dialog_kara_timing_copy.cpp
index b0217b00504b7d8022da3af486d30df35bb0cb81..135cca6ee87e8cdb962560d197c62a4008d48dab 100644
--- a/aegisub/src/dialog_kara_timing_copy.cpp
+++ b/aegisub/src/dialog_kara_timing_copy.cpp
@@ -557,8 +557,8 @@ void DialogKanjiTimer::OnStart(wxCommandEvent &) {
 	else if (SourceStyle->GetValue() == DestStyle->GetValue())
 		wxMessageBox(_("The source and destination styles must be different."),_("Error"),wxICON_EXCLAMATION | wxOK);
 	else {
-		currentSourceLine = FindNextStyleMatch(&*subs->Line.begin(), from_wx(SourceStyle->GetValue()));
-		currentDestinationLine = FindNextStyleMatch(&*subs->Line.begin(), from_wx(DestStyle->GetValue()));
+		currentSourceLine = FindNextStyleMatch(&*subs->Events.begin(), from_wx(SourceStyle->GetValue()));
+		currentDestinationLine = FindNextStyleMatch(&*subs->Events.begin(), from_wx(DestStyle->GetValue()));
 		ResetForNewLine();
 	}
 	LinesToChange.clear();
@@ -686,11 +686,11 @@ static AssEntry *find_next(Iterator from, Iterator to, std::string const& style_
 AssEntry *DialogKanjiTimer::FindNextStyleMatch(AssEntry *search_from, const std::string &search_style)
 {
 	if (!search_from) return search_from;
-	return find_next(++subs->Line.iterator_to(*search_from), subs->Line.end(), search_style);
+	return find_next(++subs->Events.iterator_to(*search_from), subs->Events.end(), search_style);
 }
 
 AssEntry *DialogKanjiTimer::FindPrevStyleMatch(AssEntry *search_from, const std::string &search_style)
 {
 	if (!search_from) return search_from;
-	return find_next(EntryList::reverse_iterator(subs->Line.iterator_to(*search_from)), subs->Line.rend(), search_style);
+	return find_next(EntryList::reverse_iterator(subs->Events.iterator_to(*search_from)), subs->Events.rend(), search_style);
 }
diff --git a/aegisub/src/dialog_selection.cpp b/aegisub/src/dialog_selection.cpp
index 34d4c4ed3af144b440b08f26b3063fe36982e89c..fdc29027e95071c959b3d8788572eb8bcfb1b169 100644
--- a/aegisub/src/dialog_selection.cpp
+++ b/aegisub/src/dialog_selection.cpp
@@ -77,7 +77,7 @@ static std::set<AssDialogue*> process(std::string const& match_text, bool match_
 	auto predicate = SearchReplaceEngine::GetMatcher(settings);
 
 	std::set<AssDialogue*> matches;
-	for (auto diag : ass->Line | agi::of_type<AssDialogue>()) {
+	for (auto diag : ass->Events | agi::of_type<AssDialogue>()) {
 		if (diag->Comment && !comments) continue;
 		if (!diag->Comment && !dialogue) continue;
 
diff --git a/aegisub/src/dialog_shift_times.cpp b/aegisub/src/dialog_shift_times.cpp
index 01b0d6272e4df6f2ecf95e9945288b48a2c68fb2..42f3cea2b1aceb146f2d481260a7d1b7f7e3190c 100644
--- a/aegisub/src/dialog_shift_times.cpp
+++ b/aegisub/src/dialog_shift_times.cpp
@@ -354,7 +354,7 @@ void DialogShiftTimes::Process(wxCommandEvent &) {
 	int block_start = 0;
 	json::Array shifted_blocks;
 
-	for (auto line : context->ass->Line | agi::of_type<AssDialogue>()) {
+	for (auto line : context->ass->Events | agi::of_type<AssDialogue>()) {
 		++row_number;
 
 		if (!sel.count(line)) {
diff --git a/aegisub/src/dialog_spellchecker.cpp b/aegisub/src/dialog_spellchecker.cpp
index 9ef34e7f2fa4c55eb5355ae049984128627fa529..541e5d2f9f6868768458e1a22bcf07535765a834 100644
--- a/aegisub/src/dialog_spellchecker.cpp
+++ b/aegisub/src/dialog_spellchecker.cpp
@@ -212,7 +212,7 @@ bool DialogSpellChecker::FindNext() {
 	if (CheckLine(active_line, start_pos, &commit_id))
 		return true;
 
-	entryIter it = context->ass->Line.iterator_to(*active_line);
+	entryIter it = context->ass->Events.iterator_to(*active_line);
 
 	// Note that it is deliberate that the start line is checked twice, as if
 	// the cursor is past the first misspelled word in the current line, that
@@ -220,8 +220,8 @@ bool DialogSpellChecker::FindNext() {
 	while(!has_looped || active_line != start_line) {
 		do {
 			// Wrap around to the beginning if we hit the end
-			if (++it == context->ass->Line.end()) {
-				it = context->ass->Line.begin();
+			if (++it == context->ass->Events.end()) {
+				it = context->ass->Events.begin();
 				has_looped = true;
 			}
 		} while (!(active_line = dynamic_cast<AssDialogue*>(&*it)));
diff --git a/aegisub/src/dialog_style_editor.cpp b/aegisub/src/dialog_style_editor.cpp
index fc8c5cb66d62828fc22d495fce73c8203908b2df..bc5bdfefdb01a2376bb27a502b7aac497a6bcfbe 100644
--- a/aegisub/src/dialog_style_editor.cpp
+++ b/aegisub/src/dialog_style_editor.cpp
@@ -86,7 +86,7 @@ class StyleRenamer {
 		found_any = false;
 		do_replace = replace;
 
-		for (auto diag : c->ass->Line | agi::of_type<AssDialogue>()) {
+		for (auto diag : c->ass->Events | agi::of_type<AssDialogue>()) {
 			if (diag->Style == source_name) {
 				if (replace)
 					diag->Style = new_name;
@@ -437,7 +437,7 @@ void DialogStyleEditor::Apply(bool apply, bool close) {
 			if (store)
 				store->push_back(std::unique_ptr<AssStyle>(style));
 			else
-				c->ass->InsertLine(style);
+				c->ass->Styles.push_back(*style);
 			is_new = false;
 		}
 		if (!store)
diff --git a/aegisub/src/dialog_style_manager.cpp b/aegisub/src/dialog_style_manager.cpp
index 0b466dba9cc10b2808c87ab998e3d95312ced5ce..8737a6a1b69624c3c5d4887c5d0512a7978e1509 100644
--- a/aegisub/src/dialog_style_manager.cpp
+++ b/aegisub/src/dialog_style_manager.cpp
@@ -288,7 +288,7 @@ void DialogStyleManager::LoadCurrentStyles(int commit_type) {
 		CurrentList->Clear();
 		styleMap.clear();
 
-		for (auto style : c->ass->Line | agi::of_type<AssStyle>()) {
+		for (auto style : c->ass->Styles | agi::of_type<AssStyle>()) {
 			CurrentList->Append(to_wx(style->name));
 			styleMap.push_back(style);
 		}
@@ -448,7 +448,7 @@ void DialogStyleManager::OnCopyToCurrent() {
 			}
 		}
 		else {
-			c->ass->InsertLine(new AssStyle(*Store[selections[i]]));
+			c->ass->Styles.push_back(*new AssStyle(*Store[selections[i]]));
 			copied.push_back(styleName);
 		}
 	}
@@ -480,7 +480,7 @@ void DialogStyleManager::CopyToClipboard(wxListBox *list, T const& v) {
 void DialogStyleManager::PasteToCurrent() {
 	add_styles(
 		std::bind(&AssFile::GetStyle, c->ass, _1),
-		std::bind(&AssFile::InsertLine, c->ass, _1));
+		[=](AssStyle *s) { c->ass->Styles.push_back(*s); });
 
 	c->ass->Commit(_("style paste"), AssFile::COMMIT_STYLES);
 }
@@ -630,7 +630,7 @@ void DialogStyleManager::OnCurrentImport() {
 
 		// Copy
 		modified = true;
-		c->ass->InsertLine(temp.GetStyle(styles[sel])->Clone());
+		c->ass->Styles.push_back(*temp.GetStyle(styles[sel])->Clone());
 	}
 
 	// Update
@@ -779,10 +779,10 @@ void DialogStyleManager::MoveStyles(bool storage, int type) {
 
 		// Replace styles
 		size_t curn = 0;
-		for (auto it = c->ass->Line.begin(); it != c->ass->Line.end(); ++it) {
+		for (auto it = c->ass->Styles.begin(); it != c->ass->Styles.end(); ++it) {
 			if (!dynamic_cast<AssStyle*>(&*it)) continue;
 
-			auto new_style_at_pos = c->ass->Line.iterator_to(*styleMap[curn]);
+			auto new_style_at_pos = c->ass->Styles.iterator_to(*styleMap[curn]);
 			EntryList::node_algorithms::swap_nodes(it.pointed_node(), new_style_at_pos.pointed_node());
 			if (++curn == styleMap.size()) break;
 			it = new_style_at_pos;
diff --git a/aegisub/src/dialog_timing_processor.cpp b/aegisub/src/dialog_timing_processor.cpp
index a9c4e8042692923439ccd6d28c8c3498e9e858cd..2c83d9046eb20b698f81422a13b1dc04c75fec4c 100644
--- a/aegisub/src/dialog_timing_processor.cpp
+++ b/aegisub/src/dialog_timing_processor.cpp
@@ -307,14 +307,14 @@ std::vector<AssDialogue*> DialogTimingProcessor::SortDialogues() {
 			[&](AssDialogue *d) { return !d->Comment && styles.count(d->Style); });
 	}
 	else {
-		transform(c->ass->Line.begin(), c->ass->Line.end(), back_inserter(sorted), cast<AssDialogue*>());
+		transform(c->ass->Events.begin(), c->ass->Events.end(), back_inserter(sorted), cast<AssDialogue*>());
 		sorted.erase(boost::remove_if(sorted, bind(bad_line, &styles, _1)), sorted.end());
 	}
 
 	// Check if rows are valid
 	for (auto diag : sorted) {
 		if (diag->Start > diag->End) {
-			int line = count_if(c->ass->Line.begin(), c->ass->Line.iterator_to(*diag), cast<const AssDialogue*>());
+			int line = distance(c->ass->Events.begin(), c->ass->Events.iterator_to(*diag));
 			wxMessageBox(
 				wxString::Format(_("One of the lines in the file (%i) has negative duration. Aborting."), line),
 				_("Invalid script"),
diff --git a/aegisub/src/dialog_translation.cpp b/aegisub/src/dialog_translation.cpp
index 60a70357c107b4a9f180f76364e2615bcefc59a6..64652e54ffa04cf9f22eb657cbbd17793be0ea49 100644
--- a/aegisub/src/dialog_translation.cpp
+++ b/aegisub/src/dialog_translation.cpp
@@ -66,8 +66,8 @@ DialogTranslation::DialogTranslation(agi::Context *c)
 , file_change_connection(c->ass->AddCommitListener(&DialogTranslation::OnExternalCommit, this))
 , active_line_connection(c->selectionController->AddActiveLineListener(&DialogTranslation::OnActiveLineChanged, this))
 , active_line(c->selectionController->GetActiveLine())
-, line_count(count_if(c->ass->Line.begin(), c->ass->Line.end(), cast<AssDialogue*>()))
-, line_number(count_if(c->ass->Line.begin(), c->ass->Line.iterator_to(*active_line), cast<AssDialogue*>()) + 1)
+, line_count(c->ass->Events.size())
+, line_number(distance(c->ass->Events.begin(), c->ass->Events.iterator_to(*active_line)) + 1)
 {
 	SetIcon(GETICON(translation_toolbutton_16));
 
@@ -175,7 +175,7 @@ void DialogTranslation::OnActiveLineChanged(AssDialogue *new_line) {
 	active_line = new_line;
 	blocks = active_line->ParseTags();
 	cur_block = 0;
-	line_number = count_if(c->ass->Line.begin(), c->ass->Line.iterator_to(*new_line), cast<AssDialogue*>()) + 1;
+	line_number = distance(c->ass->Events.begin(), c->ass->Events.iterator_to(*new_line)) + 1;
 
 	if (bad_block(blocks[cur_block]) && !NextBlock()) {
 		wxMessageBox(_("No more lines to translate."));
@@ -185,7 +185,7 @@ void DialogTranslation::OnActiveLineChanged(AssDialogue *new_line) {
 
 void DialogTranslation::OnExternalCommit(int commit_type) {
 	if (commit_type == AssFile::COMMIT_NEW || commit_type & AssFile::COMMIT_DIAG_ADDREM) {
-		line_count = count_if(c->ass->Line.begin(), c->ass->Line.end(), cast<AssDialogue*>());
+		line_count = c->ass->Events.size();
 		line_number_display->SetLabel(wxString::Format(_("Current line: %d/%d"), (int)line_number, (int)line_count));
 	}
 
diff --git a/aegisub/src/export_fixstyle.cpp b/aegisub/src/export_fixstyle.cpp
index de613258feef54478e36582b7b17844aebc11b54..04a71c6723f1a6eda3ef3577cb983fe30426ef30 100644
--- a/aegisub/src/export_fixstyle.cpp
+++ b/aegisub/src/export_fixstyle.cpp
@@ -57,7 +57,7 @@ void AssFixStylesFilter::ProcessSubs(AssFile *subs, wxWindow *) {
 	for_each(begin(styles), end(styles), [](std::string& str) { boost::to_lower(str); });
 	sort(begin(styles), end(styles));
 
-	for (auto diag : subs->Line | agi::of_type<AssDialogue>()) {
+	for (auto diag : subs->Events | agi::of_type<AssDialogue>()) {
 		if (!binary_search(begin(styles), end(styles), boost::to_lower_copy(diag->Style.get())))
 			diag->Style = "Default";
 	}
diff --git a/aegisub/src/export_framerate.cpp b/aegisub/src/export_framerate.cpp
index 8cd26545477095f5d65239ddc294c01761083f59..a827479fdfe30cce40c8de14a258807ef9b54c75 100644
--- a/aegisub/src/export_framerate.cpp
+++ b/aegisub/src/export_framerate.cpp
@@ -201,7 +201,7 @@ void AssTransformFramerateFilter::TransformTimeTags(std::string const& name, Ass
 
 void AssTransformFramerateFilter::TransformFrameRate(AssFile *subs) {
 	if (!Input->IsLoaded() || !Output->IsLoaded()) return;
-	for (auto curDialogue : subs->Line | agi::of_type<AssDialogue>()) {
+	for (auto curDialogue : subs->Events | agi::of_type<AssDialogue>()) {
 		line = curDialogue;
 		newK = 0;
 		oldK = 0;
diff --git a/aegisub/src/font_file_lister.cpp b/aegisub/src/font_file_lister.cpp
index 9a7316e8ddf2e89e1a5f81e2c72694a37ba16153..f6d57d932d95ddceacab76ec6f5f4e1b22554a34 100644
--- a/aegisub/src/font_file_lister.cpp
+++ b/aegisub/src/font_file_lister.cpp
@@ -182,7 +182,7 @@ std::vector<agi::fs::path> FontCollector::GetFontPaths(const AssFile *file) {
 
 	status_callback(_("Parsing file\n"), 0);
 
-	for (auto style : file->Line | agi::of_type<const AssStyle>()) {
+	for (auto style : file->Styles | agi::of_type<const AssStyle>()) {
 		StyleInfo &info = styles[style->name];
 		info.facename = style->font;
 		info.bold     = style->bold;
@@ -191,7 +191,7 @@ std::vector<agi::fs::path> FontCollector::GetFontPaths(const AssFile *file) {
 	}
 
 	int index = 0;
-	for (auto diag : file->Line | agi::of_type<const AssDialogue>())
+	for (auto diag : file->Events | agi::of_type<const AssDialogue>())
 		ProcessDialogueLine(diag, ++index);
 
 	status_callback(_("Searching for font files\n"), 0);
diff --git a/aegisub/src/resolution_resampler.cpp b/aegisub/src/resolution_resampler.cpp
index fbc1799637fc83d3cabbf96b62a207b12e580297..069e0f7be6526fff3b55162f66599fa7183b7c2d 100644
--- a/aegisub/src/resolution_resampler.cpp
+++ b/aegisub/src/resolution_resampler.cpp
@@ -172,7 +172,9 @@ void ResampleResolution(AssFile *ass, ResampleSettings const& settings) {
 	if (settings.change_ar)
 		state.ar = state.rx / state.ry;
 
-	for (auto& line : ass->Line)
+	for (auto& line : ass->Styles)
+		resample_line(&state, line);
+	for (auto& line : ass->Events)
 		resample_line(&state, line);
 
 	ass->SetScriptInfo("PlayResX", std::to_string(settings.script_x));
diff --git a/aegisub/src/search_replace_engine.cpp b/aegisub/src/search_replace_engine.cpp
index 53a50e7cb2cc72fb03c17d833639b08eea8b6eac..b821e08dda05bc958fc4d9f3a63bb637b1a28adb 100644
--- a/aegisub/src/search_replace_engine.cpp
+++ b/aegisub/src/search_replace_engine.cpp
@@ -230,7 +230,7 @@ bool SearchReplaceEngine::FindReplace(bool replace) {
 	auto matches = GetMatcher(settings);
 
 	AssDialogue *line = context->selectionController->GetActiveLine();
-	auto it = context->ass->Line.iterator_to(*line);
+	auto it = context->ass->Events.iterator_to(*line);
 	size_t pos = 0;
 
 	MatchState replace_ms;
@@ -263,7 +263,7 @@ bool SearchReplaceEngine::FindReplace(bool replace) {
 	// For non-text fields we just look for matching lines rather than each
 	// match within the line, so move to the next line
 	else if (settings.field != SearchReplaceSettings::Field::TEXT)
-		it = circular_next(it, context->ass->Line);
+		it = circular_next(it, context->ass->Events);
 
 	auto const& sel = context->selectionController->GetSelectedSet();
 	bool selection_only = sel.size() > 1 && settings.limit_to == SearchReplaceSettings::Limit::SELECTED;
@@ -286,7 +286,7 @@ bool SearchReplaceEngine::FindReplace(bool replace) {
 
 			return true;
 		}
-	} while (pos = 0, &*(it = circular_next(it, context->ass->Line)) != line);
+	} while (pos = 0, &*(it = circular_next(it, context->ass->Events)) != line);
 
 	// Replaced something and didn't find another match, so select the newly
 	// inserted text
@@ -307,7 +307,7 @@ bool SearchReplaceEngine::ReplaceAll() {
 	SubtitleSelection const& sel = context->selectionController->GetSelectedSet();
 	bool selection_only = settings.limit_to == SearchReplaceSettings::Limit::SELECTED;
 
-	for (auto diag : context->ass->Line | agi::of_type<AssDialogue>()) {
+	for (auto diag : context->ass->Events | agi::of_type<AssDialogue>()) {
 		if (selection_only && !sel.count(diag)) continue;
 		if (settings.ignore_comments && diag->Comment) continue;
 
diff --git a/aegisub/src/subs_controller.cpp b/aegisub/src/subs_controller.cpp
index c176c8f66a7200e82da01db139a18d272b3823c1..d1835f9b6da9a0bf9eb2a75282c7ecad31e8227a 100644
--- a/aegisub/src/subs_controller.cpp
+++ b/aegisub/src/subs_controller.cpp
@@ -69,35 +69,22 @@ struct SubsController::UndoInfo {
 	UndoInfo(const agi::Context *c, wxString const& d, int commit_id)
 	: undo_description(d), commit_id(commit_id)
 	{
-		size_t info_count = 0, style_count = 0, event_count = 0, font_count = 0, graphics_count = 0;
-		for (auto const& line : c->ass->Line) {
-			switch (line.Group()) {
-				case AssEntryGroup::DIALOGUE: ++event_count;    break;
-				case AssEntryGroup::INFO:     ++info_count;     break;
-				case AssEntryGroup::STYLE:    ++style_count;    break;
-				case AssEntryGroup::FONT:     ++font_count;     break;
-				case AssEntryGroup::GRAPHIC:  ++graphics_count; break;
-				default: assert(false); break;
-			}
+		script_info.reserve(c->ass->Info.size());
+		for (auto const& line : c->ass->Info) {
+			auto info = static_cast<const AssInfo *>(&line);
+			script_info.emplace_back(info->Key(), info->Value());
 		}
 
-		script_info.reserve(info_count);
-		styles.reserve(style_count);
-		events.reserve(event_count);
+		styles.reserve(c->ass->Styles.size());
+		for (auto const& line : c->ass->Styles)
+			styles.push_back(static_cast<AssStyle const&>(line));
+
+		events.reserve(c->ass->Events.size());
+		for (auto const& line : c->ass->Events)
+			events.push_back(static_cast<AssDialogue const&>(line));
 
-		for (auto const& line : c->ass->Line) {
+		for (auto const& line : c->ass->Attachments) {
 			switch (line.Group()) {
-			case AssEntryGroup::DIALOGUE:
-				events.push_back(static_cast<AssDialogue const&>(line));
-				break;
-			case AssEntryGroup::INFO: {
-				auto info = static_cast<const AssInfo *>(&line);
-				script_info.emplace_back(info->Key(), info->Value());
-				break;
-			}
-			case AssEntryGroup::STYLE:
-				styles.push_back(static_cast<AssStyle const&>(line));
-				break;
 			case AssEntryGroup::FONT:
 				fonts.push_back(static_cast<AssAttachment const&>(line));
 				break;
@@ -125,16 +112,16 @@ struct SubsController::UndoInfo {
 		SubtitleSelection new_sel;
 
 		for (auto const& info : script_info)
-			c->ass->Line.push_back(*new AssInfo(info.first, info.second));
+			c->ass->Info.push_back(*new AssInfo(info.first, info.second));
 		for (auto const& style : styles)
-			c->ass->Line.push_back(*new AssStyle(style));
+			c->ass->Styles.push_back(*new AssStyle(style));
 		for (auto const& attachment : fonts)
-			c->ass->Line.push_back(*new AssAttachment(attachment));
+			c->ass->Attachments.push_back(*new AssAttachment(attachment));
 		for (auto const& attachment : graphics)
-			c->ass->Line.push_back(*new AssAttachment(attachment));
+			c->ass->Attachments.push_back(*new AssAttachment(attachment));
 		for (auto const& event : events) {
 			auto copy = new AssDialogue(event);
-			c->ass->Line.push_back(*copy);
+			c->ass->Events.push_back(*copy);
 			if (copy->Id == active_line_id)
 				active_line = copy;
 			if (binary_search(begin(selection), end(selection), copy->Id))
@@ -142,7 +129,7 @@ struct SubsController::UndoInfo {
 		}
 
 		c->subsGrid->BeginBatch();
-		c->selectionController->SetSelectedSet({ });
+		c->selectionController->SetSelectedSet(std::set<AssDialogue *>{});
 		c->ass->Commit("", AssFile::COMMIT_NEW);
 		c->selectionController->SetSelectionAndActive(new_sel, active_line);
 		c->subsGrid->EndBatch();
@@ -222,22 +209,11 @@ void SubsController::Load(agi::fs::path const& filename, std::string charset) {
 		AssFile temp;
 		reader->ReadFile(&temp, filename, charset);
 
-		bool found_style = false;
-		bool found_dialogue = false;
-
-		// Check if the file has at least one style and at least one dialogue line
-		for (auto const& line : temp.Line) {
-			AssEntryGroup type = line.Group();
-			if (type == AssEntryGroup::STYLE) found_style = true;
-			if (type == AssEntryGroup::DIALOGUE) found_dialogue = true;
-			if (found_style && found_dialogue) break;
-		}
-
-		// And if it doesn't add defaults for each
-		if (!found_style)
-			temp.InsertLine(new AssStyle);
-		if (!found_dialogue)
-			temp.InsertLine(new AssDialogue);
+		// Make sure the file has at least one style and one dialogue line
+		if (temp.Styles.empty())
+			temp.Styles.push_back(*new AssStyle);
+		if (temp.Events.empty())
+			temp.Events.push_back(*new AssDialogue);
 
 		context->ass->swap(temp);
 	}
@@ -318,7 +294,7 @@ void SubsController::Close() {
 	autosaved_commit_id = saved_commit_id = commit_id + 1;
 	filename.clear();
 	AssFile blank;
-	swap(blank.Line, context->ass->Line);
+	blank.swap(*context->ass);
 	context->ass->LoadDefault();
 	context->ass->Commit("", AssFile::COMMIT_NEW);
 }
diff --git a/aegisub/src/subs_edit_box.cpp b/aegisub/src/subs_edit_box.cpp
index 4d0ccfc688503e22265d5f8e54d15e392a145144..e365b7a203a6178ff6db020cfb02dbc4ff171d9c 100644
--- a/aegisub/src/subs_edit_box.cpp
+++ b/aegisub/src/subs_edit_box.cpp
@@ -343,8 +343,7 @@ void SubsEditBox::PopulateList(wxComboBox *combo, boost::flyweight<std::string>
 	wxEventBlocker blocker(this);
 
 	std::unordered_set<std::string> values;
-	for (auto const& line : c->ass->Line) {
-		if (line.Group() != AssEntryGroup::DIALOGUE) continue;
+	for (auto const& line : c->ass->Events) {
 		auto const& value = static_cast<const AssDialogue *>(&line)->*field;
 		if (!value.get().empty())
 			values.insert(value);
@@ -435,12 +434,12 @@ void SubsEditBox::SetSelectedRows(setter set, wxString const& desc, int type, bo
 }
 
 template<class T>
-void SubsEditBox::SetSelectedRows(T AssDialogue::*field, T value, wxString const& desc, int type, bool amend) {
+void SubsEditBox::SetSelectedRows(T AssDialogueBase::*field, T value, wxString const& desc, int type, bool amend) {
 	SetSelectedRows([&](AssDialogue *d) { d->*field = value; }, desc, type, amend);
 }
 
 template<class T>
-void SubsEditBox::SetSelectedRows(T AssDialogue::*field, wxString const& value, wxString const& desc, int type, bool amend) {
+void SubsEditBox::SetSelectedRows(T AssDialogueBase::*field, wxString const& value, wxString const& desc, int type, bool amend) {
 	boost::flyweight<std::string> conv_value(from_wx(value));
 	SetSelectedRows([&](AssDialogue *d) { d->*field = conv_value; }, desc, type, amend);
 }
diff --git a/aegisub/src/subs_edit_box.h b/aegisub/src/subs_edit_box.h
index a0e3d2f77ee8fad35f92de4758c077990a1f18ac..47b5661d5ae270eaf643585495353ff687a54eab 100644
--- a/aegisub/src/subs_edit_box.h
+++ b/aegisub/src/subs_edit_box.h
@@ -62,6 +62,7 @@ class wxSpinCtrl;
 class wxStyledTextCtrl;
 class wxStyledTextEvent;
 class wxTextCtrl;
+struct AssDialogueBase;
 
 template<class Base> class Placeholder;
 
@@ -177,10 +178,10 @@ class SubsEditBox : public wxPanel {
 	/// @param type  Commit type to use
 	/// @param amend Coalesce sequences of commits of the same type
 	template<class T>
-	void SetSelectedRows(T AssDialogue::*field, T value, wxString const& desc, int type, bool amend = false);
+	void SetSelectedRows(T AssDialogueBase::*field, T value, wxString const& desc, int type, bool amend = false);
 
 	template<class T>
-	void SetSelectedRows(T AssDialogue::*field, wxString const& value, wxString const& desc, int type, bool amend = false);
+	void SetSelectedRows(T AssDialogueBase::*field, wxString const& value, wxString const& desc, int type, bool amend = false);
 
 	/// @brief Reload the current line from the file
 	/// @param type AssFile::COMMITType
diff --git a/aegisub/src/subs_preview.cpp b/aegisub/src/subs_preview.cpp
index ab1d1a87a22e50acfbe7d62c87d565ec9d7a53ac..33d62ab46488e1718dbf27e84c0ebbf92595d151 100644
--- a/aegisub/src/subs_preview.cpp
+++ b/aegisub/src/subs_preview.cpp
@@ -60,8 +60,8 @@ SubtitlesPreview::SubtitlesPreview(wxWindow *parent, wxSize size, int winStyle,
 	SetStyle(*style);
 
 	sub_file->LoadDefault();
-	sub_file->InsertLine(style);
-	sub_file->Line.push_back(*line);
+	sub_file->Styles.push_back(*style);
+	sub_file->Events.push_back(*line);
 
 	SetSizeHints(size.GetWidth(), size.GetHeight(), -1, -1);
 	wxSizeEvent evt(size);
diff --git a/aegisub/src/subtitle_format.cpp b/aegisub/src/subtitle_format.cpp
index 0c64b167003490e76de0062c6a9357ca09ef6552..ec599d648fb666e5120051b99e62a5f39d768812 100644
--- a/aegisub/src/subtitle_format.cpp
+++ b/aegisub/src/subtitle_format.cpp
@@ -89,22 +89,19 @@ bool SubtitleFormat::CanWriteFile(agi::fs::path const& filename) const {
 }
 
 bool SubtitleFormat::CanSave(const AssFile *subs) const {
-	AssStyle defstyle;
-	for (auto const& line : subs->Line) {
-		// Check style, if anything non-default is found, return false
-		if (const AssStyle *curstyle = dynamic_cast<const AssStyle*>(&line)) {
-			if (curstyle->GetEntryData() != defstyle.GetEntryData())
-				return false;
-		}
+	if (!subs->Attachments.empty())
+		return false;
 
-		// Check for attachments, if any is found, return false
-		if (dynamic_cast<const AssAttachment*>(&line)) return false;
+	std::string defstyle = AssStyle().GetEntryData();
+	for (auto const& line : subs->Styles) {
+		if (static_cast<const AssStyle *>(&line)->GetEntryData() != defstyle)
+			return false;
+	}
 
-		// Check dialog
-		if (const AssDialogue *curdiag = dynamic_cast<const AssDialogue*>(&line)) {
-			if (curdiag->GetStrippedText() != curdiag->Text)
-				return false;
-		}
+	for (auto const& line : subs->Events) {
+		auto diag = static_cast<const AssDialogue *>(&line);
+		if (diag->GetStrippedText() != diag->Text)
+			return false;
 	}
 
 	return true;
@@ -176,12 +173,12 @@ agi::vfr::Framerate SubtitleFormat::AskForFPS(bool allow_vfr, bool show_smpte) {
 }
 
 void SubtitleFormat::StripTags(AssFile &file) {
-	for (auto current : file.Line | agi::of_type<AssDialogue>())
+	for (auto current : file.Events | agi::of_type<AssDialogue>())
 		current->StripTags();
 }
 
 void SubtitleFormat::ConvertNewlines(AssFile &file, std::string const& newline, bool mergeLineBreaks) {
-	for (auto current : file.Line | agi::of_type<AssDialogue>()) {
+	for (auto current : file.Events | agi::of_type<AssDialogue>()) {
 		std::string repl = current->Text;
 		boost::replace_all(repl, "\\h", " ");
 		boost::ireplace_all(repl, "\\n", newline);
@@ -196,18 +193,12 @@ void SubtitleFormat::ConvertNewlines(AssFile &file, std::string const& newline,
 }
 
 void SubtitleFormat::StripComments(AssFile &file) {
-	file.Line.remove_and_dispose_if([](AssEntry const& e) {
+	file.Events.remove_and_dispose_if([](AssEntry const& e) {
 		const AssDialogue *diag = dynamic_cast<const AssDialogue*>(&e);
 		return diag && (diag->Comment || diag->Text.get().empty());
 	}, [](AssEntry *e) { delete e; });
 }
 
-void SubtitleFormat::StripNonDialogue(AssFile &file) {
-	file.Line.remove_and_dispose_if([](AssEntry const& e) {
-		return e.Group() != AssEntryGroup::DIALOGUE;
-	}, [](AssEntry *e) { delete e; });
-}
-
 static bool dialog_start_lt(AssEntry &pos, AssDialogue *to_insert) {
 	AssDialogue *diag = dynamic_cast<AssDialogue*>(&pos);
 	return diag && diag->Start > to_insert->Start;
@@ -217,10 +208,10 @@ static bool dialog_start_lt(AssEntry &pos, AssDialogue *to_insert) {
 ///
 /// Algorithm described at http://devel.aegisub.org/wiki/Technical/SplitMerge
 void SubtitleFormat::RecombineOverlaps(AssFile &file) {
-	entryIter cur, next = file.Line.begin();
+	entryIter cur, next = file.Events.begin();
 	cur = next++;
 
-	for (; next != file.Line.end(); cur = next++) {
+	for (; next != file.Events.end(); cur = next++) {
 		AssDialogue *prevdlg = dynamic_cast<AssDialogue*>(&*cur);
 		AssDialogue *curdlg = dynamic_cast<AssDialogue*>(&*next);
 
@@ -236,8 +227,8 @@ void SubtitleFormat::RecombineOverlaps(AssFile &file) {
 		// std::list::insert() inserts items before the given iterator, so
 		// we need 'next' for inserting. 'prev' and 'cur' can safely be erased
 		// from the list now.
-		file.Line.erase(prev);
-		file.Line.erase(cur);
+		file.Events.erase(prev);
+		file.Events.erase(cur);
 
 		//Is there an A part before the overlap?
 		if (curdlg->Start > prevdlg->Start) {
@@ -247,7 +238,7 @@ void SubtitleFormat::RecombineOverlaps(AssFile &file) {
 			newdlg->End = curdlg->Start;
 			newdlg->Text = prevdlg->Text;
 
-			file.Line.insert(find_if(next, file.Line.end(), std::bind(dialog_start_lt, _1, newdlg)), *newdlg);
+			file.Events.insert(find_if(next, file.Events.end(), std::bind(dialog_start_lt, _1, newdlg)), *newdlg);
 		}
 
 		// Overlapping A+B part
@@ -258,7 +249,7 @@ void SubtitleFormat::RecombineOverlaps(AssFile &file) {
 			// Put an ASS format hard linewrap between lines
 			newdlg->Text = curdlg->Text.get() + "\\N" + prevdlg->Text.get();
 
-			file.Line.insert(find_if(next, file.Line.end(), std::bind(dialog_start_lt, _1, newdlg)), *newdlg);
+			file.Events.insert(find_if(next, file.Events.end(), std::bind(dialog_start_lt, _1, newdlg)), *newdlg);
 		}
 
 		// Is there an A part after the overlap?
@@ -269,7 +260,7 @@ void SubtitleFormat::RecombineOverlaps(AssFile &file) {
 			newdlg->End = prevdlg->End;
 			newdlg->Text = prevdlg->Text;
 
-			file.Line.insert(find_if(next, file.Line.end(), std::bind(dialog_start_lt, _1, newdlg)), *newdlg);
+			file.Events.insert(find_if(next, file.Events.end(), std::bind(dialog_start_lt, _1, newdlg)), *newdlg);
 		}
 
 		// Is there a B part after the overlap?
@@ -280,7 +271,7 @@ void SubtitleFormat::RecombineOverlaps(AssFile &file) {
 			newdlg->End = curdlg->End;
 			newdlg->Text = curdlg->Text;
 
-			file.Line.insert(find_if(next, file.Line.end(), std::bind(dialog_start_lt, _1, newdlg)), *newdlg);
+			file.Events.insert(find_if(next, file.Events.end(), std::bind(dialog_start_lt, _1, newdlg)), *newdlg);
 		}
 
 		next--;
@@ -289,10 +280,10 @@ void SubtitleFormat::RecombineOverlaps(AssFile &file) {
 
 /// @brief Merge identical lines that follow each other
 void SubtitleFormat::MergeIdentical(AssFile &file) {
-	entryIter cur, next = file.Line.begin();
+	entryIter cur, next = file.Events.begin();
 	cur = next++;
 
-	for (; next != file.Line.end(); cur = next++) {
+	for (; next != file.Events.end(); cur = next++) {
 		AssDialogue *curdlg = dynamic_cast<AssDialogue*>(&*cur);
 		AssDialogue *nextdlg = dynamic_cast<AssDialogue*>(&*next);
 
diff --git a/aegisub/src/subtitle_format.h b/aegisub/src/subtitle_format.h
index 9036ce4cf4d52f39a01f72d82538ee5e5e6a5349..3a66f93a84d355e6ea392dd932a7b6bfbc388fe2 100644
--- a/aegisub/src/subtitle_format.h
+++ b/aegisub/src/subtitle_format.h
@@ -64,8 +64,6 @@ public:
 	static void ConvertNewlines(AssFile &file, std::string const& newline, bool mergeLineBreaks = true);
 	/// Remove All commented and empty lines
 	static void StripComments(AssFile &file);
-	/// Remove everything but the dialogue lines
-	static void StripNonDialogue(AssFile &file);
 	/// @brief Split and merge lines so there are no overlapping lines
 	///
 	/// Algorithm described at http://devel.aegisub.org/wiki/Technical/SplitMerge
diff --git a/aegisub/src/subtitle_format_ass.cpp b/aegisub/src/subtitle_format_ass.cpp
index 4c9201cc3a4e1ea87f723faf184b60fa0169dbd2..898573037c64b8079ca258e66ac5015277ebb463 100644
--- a/aegisub/src/subtitle_format_ass.cpp
+++ b/aegisub/src/subtitle_format_ass.cpp
@@ -115,17 +115,24 @@ void AssSubtitleFormat::WriteFile(const AssFile *src, agi::fs::path const& filen
 	bool ssa = agi::fs::HasExtension(filename, "ssa");
 	AssEntryGroup group = AssEntryGroup::INFO;
 
-	for (auto const& line : src->Line) {
-		if (line.Group() != group) {
-			// Add a blank line between each group
-			file.WriteLineToFile("");
+	auto write = [&](EntryList const& list) {
+		for (auto const& line : list) {
+			if (line.Group() != group) {
+				// Add a blank line between each group
+				file.WriteLineToFile("");
 
-			file.WriteLineToFile(line.GroupHeader(ssa));
-			file.WriteLineToFile(format(line.Group(), ssa), false);
+				file.WriteLineToFile(line.GroupHeader(ssa));
+				file.WriteLineToFile(format(line.Group(), ssa), false);
 
-			group = line.Group();
+				group = line.Group();
+			}
+
+			file.WriteLineToFile(ssa ? line.GetSSAText() : line.GetEntryData());
 		}
+	};
 
-		file.WriteLineToFile(ssa ? line.GetSSAText() : line.GetEntryData());
-	}
+	write(src->Info);
+	write(src->Styles);
+	write(src->Attachments);
+	write(src->Events);
 }
diff --git a/aegisub/src/subtitle_format_ebu3264.cpp b/aegisub/src/subtitle_format_ebu3264.cpp
index 30913febc3e4d6e41bba8a802c8e4411834e228c..79411960fc1a493a17c8fc6b62e669bda01f917f 100644
--- a/aegisub/src/subtitle_format_ebu3264.cpp
+++ b/aegisub/src/subtitle_format_ebu3264.cpp
@@ -370,10 +370,10 @@ namespace
 
 		AssStyle default_style;
 		std::vector<EbuSubtitle> subs_list;
-		subs_list.reserve(copy.Line.size());
+		subs_list.reserve(copy.Events.size());
 
 		// convert to intermediate format
-		for (auto line : copy.Line | agi::of_type<AssDialogue>())
+		for (auto line : copy.Events | agi::of_type<AssDialogue>())
 		{
 			// add a new subtitle and work on it
 			subs_list.emplace_back();
diff --git a/aegisub/src/subtitle_format_encore.cpp b/aegisub/src/subtitle_format_encore.cpp
index 08f5e325eade6dca6842ca2991098c2831e42a40..d710345bc674258ec8cea3f4ea0aaed30f882efb 100644
--- a/aegisub/src/subtitle_format_encore.cpp
+++ b/aegisub/src/subtitle_format_encore.cpp
@@ -77,6 +77,6 @@ void EncoreSubtitleFormat::WriteFile(const AssFile *src, agi::fs::path const& fi
 	// Write lines
 	int i = 0;
 	TextFileWriter file(filename, "UTF-8");
-	for (auto current : copy.Line | agi::of_type<AssDialogue>())
+	for (auto current : copy.Events | agi::of_type<AssDialogue>())
 		file.WriteLineToFile(str(boost::format("%i %s %s %s") % ++i % ft.ToSMPTE(current->Start) % ft.ToSMPTE(current->End) % current->Text));
 }
diff --git a/aegisub/src/subtitle_format_microdvd.cpp b/aegisub/src/subtitle_format_microdvd.cpp
index 916b7d4c23af621729682370d352e9251597428b..c14553da0c6b7f28f980ba3ae89d0eb0c4931465 100644
--- a/aegisub/src/subtitle_format_microdvd.cpp
+++ b/aegisub/src/subtitle_format_microdvd.cpp
@@ -120,7 +120,7 @@ void MicroDVDSubtitleFormat::ReadFile(AssFile *target, agi::fs::path const& file
 		diag->Start = fps.TimeAtFrame(f1, agi::vfr::START);
 		diag->End = fps.TimeAtFrame(f2, agi::vfr::END);
 		diag->Text = text;
-		target->Line.push_back(*diag);
+		target->Events.push_back(*diag);
 	}
 }
 
@@ -143,7 +143,7 @@ void MicroDVDSubtitleFormat::WriteFile(const AssFile *src, agi::fs::path const&
 		file.WriteLineToFile(str(boost::format("{1}{1}%.6f") % fps.FPS()));
 
 	// Write lines
-	for (auto current : copy.Line | agi::of_type<AssDialogue>()) {
+	for (auto current : copy.Events | agi::of_type<AssDialogue>()) {
 		int start = fps.FrameAtTime(current->Start, agi::vfr::START);
 		int end = fps.FrameAtTime(current->End, agi::vfr::END);
 
diff --git a/aegisub/src/subtitle_format_srt.cpp b/aegisub/src/subtitle_format_srt.cpp
index c1a3206efcaeb2d4e5e93eefc25ccaf333774d62..c5078b4eba91b84eb74029c58f19f8c15ff4760c 100644
--- a/aegisub/src/subtitle_format_srt.cpp
+++ b/aegisub/src/subtitle_format_srt.cpp
@@ -404,7 +404,7 @@ found_timestamps:
 				line->Start = ReadSRTTime(timestamp_match.str(1));
 				line->End = ReadSRTTime(timestamp_match.str(2));
 				// store pointer to subtitle, we'll continue working on it
-				target->Line.push_back(*line);
+				target->Events.push_back(*line);
 				// next we're reading the text
 				state = STATE_FIRST_LINE_OF_BODY;
 				break;
@@ -480,7 +480,7 @@ void SRTSubtitleFormat::WriteFile(const AssFile *src, agi::fs::path const& filen
 
 	// Write lines
 	int i=0;
-	for (auto current : copy.Line | agi::of_type<AssDialogue>()) {
+	for (auto current : copy.Events | agi::of_type<AssDialogue>()) {
 		file.WriteLineToFile(std::to_string(++i));
 		file.WriteLineToFile(WriteSRTTime(current->Start) + " --> " + WriteSRTTime(current->End));
 		file.WriteLineToFile(ConvertTags(current));
@@ -491,26 +491,23 @@ void SRTSubtitleFormat::WriteFile(const AssFile *src, agi::fs::path const& filen
 bool SRTSubtitleFormat::CanSave(const AssFile *file) const {
 	std::string supported_tags[] = { "\\b", "\\i", "\\s", "\\u" };
 
-	AssStyle defstyle;
-	for (auto const& line : file->Line) {
-		// Check style, if anything non-default is found, return false
-		if (const AssStyle *curstyle = dynamic_cast<const AssStyle*>(&line)) {
-			if (curstyle->GetEntryData() != defstyle.GetEntryData())
-				return false;
-		}
+	if (!file->Attachments.empty())
+		return false;
 
-		// Check for attachments, if any is found, return false
-		if (dynamic_cast<const AssAttachment*>(&line)) return false;
-
-		// Check dialogue
-		if (const AssDialogue *curdiag = dynamic_cast<const AssDialogue*>(&line)) {
-			boost::ptr_vector<AssDialogueBlock> blocks(curdiag->ParseTags());
-			for (auto ovr : blocks | agi::of_type<AssDialogueBlockOverride>()) {
-				// Verify that all overrides used are supported
-				for (auto const& tag : ovr->Tags) {
-					if (!std::binary_search(supported_tags, std::end(supported_tags), tag.Name))
-						return false;
-				}
+	std::string defstyle = AssStyle().GetEntryData();
+	for (auto const& line : file->Styles) {
+		if (static_cast<const AssStyle *>(&line)->GetEntryData() != defstyle)
+			return false;
+	}
+
+	for (auto const& line : file->Events) {
+		auto diag = static_cast<const AssDialogue *>(&line);
+		boost::ptr_vector<AssDialogueBlock> blocks(diag->ParseTags());
+		for (auto ovr : blocks | agi::of_type<AssDialogueBlockOverride>()) {
+			// Verify that all overrides used are supported
+			for (auto const& tag : ovr->Tags) {
+				if (!std::binary_search(supported_tags, std::end(supported_tags), tag.Name))
+					return false;
 			}
 		}
 	}
diff --git a/aegisub/src/subtitle_format_transtation.cpp b/aegisub/src/subtitle_format_transtation.cpp
index eee8c1937941fdfbc0a4523fd8c940b87597a902..4cb3d58aa2b7609574b522cadc5f85ed258bf05b 100644
--- a/aegisub/src/subtitle_format_transtation.cpp
+++ b/aegisub/src/subtitle_format_transtation.cpp
@@ -77,7 +77,7 @@ void TranStationSubtitleFormat::WriteFile(const AssFile *src, agi::fs::path cons
 	SmpteFormatter ft(fps);
 	TextFileWriter file(filename, encoding);
 	AssDialogue *prev = nullptr;
-	for (auto cur : copy.Line | agi::of_type<AssDialogue>()) {
+	for (auto cur : copy.Events | agi::of_type<AssDialogue>()) {
 		if (prev) {
 			file.WriteLineToFile(ConvertLine(&copy, prev, fps, ft, cur->Start));
 			file.WriteLineToFile("");
diff --git a/aegisub/src/subtitle_format_ttxt.cpp b/aegisub/src/subtitle_format_ttxt.cpp
index 96c7b23d8c68ab3d3bbe492b1fc691e8b92e4080..28a293d987ae700f14e862f0f3cd178665ef90d2 100644
--- a/aegisub/src/subtitle_format_ttxt.cpp
+++ b/aegisub/src/subtitle_format_ttxt.cpp
@@ -92,7 +92,7 @@ void TTXTSubtitleFormat::ReadFile(AssFile *target, agi::fs::path const& filename
 		if (child->GetName() == "TextSample") {
 			if ((diag = ProcessLine(child, diag, version))) {
 				lines++;
-				target->Line.push_back(*diag);
+				target->Events.push_back(*diag);
 			}
 		}
 		// Header
@@ -103,7 +103,7 @@ void TTXTSubtitleFormat::ReadFile(AssFile *target, agi::fs::path const& filename
 
 	// No lines?
 	if (lines == 0)
-		target->Line.push_back(*new AssDialogue);
+		target->Events.push_back(*new AssDialogue);
 }
 
 AssDialogue *TTXTSubtitleFormat::ProcessLine(wxXmlNode *node, AssDialogue *prev, int version) const {
@@ -177,7 +177,7 @@ void TTXTSubtitleFormat::WriteFile(const AssFile *src, agi::fs::path const& file
 
 	// Create lines
 	const AssDialogue *prev = nullptr;
-	for (auto current : copy.Line | agi::of_type<AssDialogue>()) {
+	for (auto current : copy.Events | agi::of_type<AssDialogue>()) {
 		WriteLine(root, prev, current);
 		prev = current;
 	}
@@ -264,7 +264,7 @@ void TTXTSubtitleFormat::ConvertToTTXT(AssFile &file) const {
 
 	// Find last line
 	AssTime lastTime;
-	for (auto line : file.Line | boost::adaptors::reversed | agi::of_type<AssDialogue>()) {
+	for (auto line : file.Events | boost::adaptors::reversed | agi::of_type<AssDialogue>()) {
 		lastTime = line->End;
 		break;
 	}
@@ -273,5 +273,5 @@ void TTXTSubtitleFormat::ConvertToTTXT(AssFile &file) const {
 	auto diag = new AssDialogue;
 	diag->Start = lastTime;
 	diag->End = lastTime+OPT_GET("Timing/Default Duration")->GetInt();
-	file.Line.push_back(*diag);
+	file.Events.push_back(*diag);
 }
diff --git a/aegisub/src/subtitle_format_txt.cpp b/aegisub/src/subtitle_format_txt.cpp
index 2b4985672453f224239a32424174384f432cbb2e..3ced45aec36d4461e6ad011f831b9e9bc6e49a6a 100644
--- a/aegisub/src/subtitle_format_txt.cpp
+++ b/aegisub/src/subtitle_format_txt.cpp
@@ -123,7 +123,7 @@ void TXTSubtitleFormat::ReadFile(AssFile *target, agi::fs::path const& filename,
 		line->Text = value;
 		line->End = 0;
 
-		target->Line.push_back(*line);
+		target->Events.push_back(*line);
 	}
 }
 
@@ -131,7 +131,7 @@ void TXTSubtitleFormat::WriteFile(const AssFile *src, agi::fs::path const& filen
 	size_t num_actor_names = 0, num_dialogue_lines = 0;
 
 	// Detect number of lines with Actor field filled out
-	for (auto dia : src->Line | agi::of_type<AssDialogue>()) {
+	for (auto dia : src->Events | agi::of_type<AssDialogue>()) {
 		if (!dia->Comment) {
 			num_dialogue_lines++;
 			if (!dia->Actor.get().empty())
@@ -147,7 +147,7 @@ void TXTSubtitleFormat::WriteFile(const AssFile *src, agi::fs::path const& filen
 	file.WriteLineToFile(std::string("# Exported by Aegisub ") + GetAegisubShortVersionString());
 
 	// Write the file
-	for (auto dia : src->Line | agi::of_type<AssDialogue>()) {
+	for (auto dia : src->Events | agi::of_type<AssDialogue>()) {
 		std::string out_line;
 
 		if (dia->Comment)
diff --git a/aegisub/src/subtitles_provider_libass.cpp b/aegisub/src/subtitles_provider_libass.cpp
index 8c7152990eb76704d06bc05e845fbe932f154cad..2e4e660083c8ef05e3854371bc9ce09871707ea6 100644
--- a/aegisub/src/subtitles_provider_libass.cpp
+++ b/aegisub/src/subtitles_provider_libass.cpp
@@ -118,13 +118,19 @@ void LibassSubtitlesProvider::LoadSubtitles(AssFile *subs) {
 	data.reserve(0x4000);
 
 	AssEntryGroup group = AssEntryGroup::GROUP_MAX;
-	for (auto const& line : subs->Line) {
-		if (group != line.Group()) {
-			group = line.Group();
-			boost::push_back(data, line.GroupHeader() + "\r\n");
+	auto write_group = [&](EntryList const& list) {
+		for (auto const& line : list) {
+			if (group != line.Group()) {
+				group = line.Group();
+				boost::push_back(data, line.GroupHeader() + "\r\n");
+			}
+			boost::push_back(data, line.GetEntryData() + "\r\n");
 		}
-		boost::push_back(data, line.GetEntryData() + "\r\n");
-	}
+	};
+
+	write_group(subs->Info);
+	write_group(subs->Styles);
+	write_group(subs->Events);
 
 	if (ass_track) ass_free_track(ass_track);
 	ass_track = ass_read_memory(library, &data[0], data.size(), nullptr);
diff --git a/aegisub/src/threaded_frame_source.cpp b/aegisub/src/threaded_frame_source.cpp
index 9cf07337634469351a07c92cde1493215793eda6..87b37a297974132c3607ad742550412b411d295f 100644
--- a/aegisub/src/threaded_frame_source.cpp
+++ b/aegisub/src/threaded_frame_source.cpp
@@ -71,9 +71,9 @@ std::shared_ptr<VideoFrame> ThreadedFrameSource::ProcFrame(int frame_number, dou
 				// instead muck around with its innards to just temporarily
 				// remove the non-visible lines without deleting them
 				std::deque<AssEntry*> full;
-				for (auto& line : subs->Line)
+				for (auto& line : subs->Events)
 					full.push_back(&line);
-				subs->Line.remove_if([=](AssEntry const& e) -> bool {
+				subs->Events.remove_if([=](AssEntry const& e) -> bool {
 					const AssDialogue *diag = dynamic_cast<const AssDialogue*>(&e);
 					return diag && (diag->Start > time || diag->End <= time);
 				});
@@ -81,12 +81,12 @@ std::shared_ptr<VideoFrame> ThreadedFrameSource::ProcFrame(int frame_number, dou
 				try {
 					subs_provider->LoadSubtitles(subs.get());
 
-					subs->Line.clear();
-					boost::push_back(subs->Line, full | boost::adaptors::indirected);
+					subs->Events.clear();
+					boost::push_back(subs->Events, full | boost::adaptors::indirected);
 				}
 				catch (...) {
-					subs->Line.clear();
-					boost::push_back(subs->Line, full | boost::adaptors::indirected);
+					subs->Events.clear();
+					boost::push_back(subs->Events, full | boost::adaptors::indirected);
 					throw;
 				}
 			}
@@ -140,7 +140,7 @@ void ThreadedFrameSource::UpdateSubtitles(const AssFile *new_subs, std::set<cons
 	// same indices in the worker's copy of the file with the new entries
 	std::deque<std::pair<size_t, AssEntry*>> changed;
 	size_t i = 0;
-	for (auto const& e : new_subs->Line) {
+	for (auto const& e : new_subs->Events) {
 		if (changes.count(&e))
 			changed.emplace_back(i, e.Clone());
 		++i;
@@ -148,11 +148,11 @@ void ThreadedFrameSource::UpdateSubtitles(const AssFile *new_subs, std::set<cons
 
 	worker->Async([=]{
 		size_t i = 0;
-		auto it = subs->Line.begin();
+		auto it = subs->Events.begin();
 		for (auto& update : changed) {
 			advance(it, update.first - i);
 			i = update.first;
-			subs->Line.insert(it, *update.second);
+			subs->Events.insert(it, *update.second);
 			delete &*it--;
 		}
 
diff --git a/aegisub/src/visual_tool_drag.cpp b/aegisub/src/visual_tool_drag.cpp
index 4f8c75e84258399a7131083e652d6b7ec86e119f..3dd7b49c44a29f71bd8ab8b7581715682757914f 100644
--- a/aegisub/src/visual_tool_drag.cpp
+++ b/aegisub/src/visual_tool_drag.cpp
@@ -115,7 +115,7 @@ void VisualToolDrag::OnFileChanged() {
 	primary = nullptr;
 	active_feature = nullptr;
 
-	for (auto diag : c->ass->Line | agi::of_type<AssDialogue>()) {
+	for (auto diag : c->ass->Events | agi::of_type<AssDialogue>()) {
 		if (IsDisplayed(diag))
 			MakeFeatures(diag);
 	}
@@ -130,7 +130,7 @@ void VisualToolDrag::OnFrameChanged() {
 	auto feat = features.begin();
 	auto end = features.end();
 
-	for (auto diag : c->ass->Line | agi::of_type<AssDialogue>()) {
+	for (auto diag : c->ass->Events | agi::of_type<AssDialogue>()) {
 		if (IsDisplayed(diag)) {
 			// Features don't exist and should
 			if (feat == end || feat->line != diag)