diff --git a/aegisub/src/ass_dialogue.cpp b/aegisub/src/ass_dialogue.cpp
index 95f3b8e7351507e8eb015e9fd2faa32be83b6234..6954fb974262b1feaead12e3eae88dd2c19064a8 100644
--- a/aegisub/src/ass_dialogue.cpp
+++ b/aegisub/src/ass_dialogue.cpp
@@ -61,9 +61,7 @@ AssDialogue::AssDialogue(AssDialogue const& that) : AssDialogueBase(that) {
 	Id = ++next_id;
 }
 
-AssDialogue::AssDialogue(AssDialogueBase const& that) : AssDialogueBase(that) {
-	Id = ++next_id;
-}
+AssDialogue::AssDialogue(AssDialogueBase const& that) : AssDialogueBase(that) { }
 
 AssDialogue::AssDialogue(std::string const& data) {
 	Id = ++next_id;
diff --git a/aegisub/src/base_grid.cpp b/aegisub/src/base_grid.cpp
index 669b91dda63472e6f3c245f0d5e0e2bd36fc6185..92c6f0963be7d657ad484da08c9690920fd10199 100644
--- a/aegisub/src/base_grid.cpp
+++ b/aegisub/src/base_grid.cpp
@@ -141,9 +141,7 @@ BaseGrid::BaseGrid(wxWindow* parent, agi::Context *context, const wxSize& size,
 	Bind(wxEVT_CONTEXT_MENU, &BaseGrid::OnContextMenu, this);
 }
 
-BaseGrid::~BaseGrid() {
-	ClearMaps();
-}
+BaseGrid::~BaseGrid() { }
 
 BEGIN_EVENT_TABLE(BaseGrid,wxWindow)
 	EVT_PAINT(BaseGrid::OnPaint)
@@ -156,10 +154,8 @@ BEGIN_EVENT_TABLE(BaseGrid,wxWindow)
 END_EVENT_TABLE()
 
 void BaseGrid::OnSubtitlesCommit(int type) {
-	if (type == AssFile::COMMIT_NEW)
-		UpdateMaps(true);
-	else if (type & AssFile::COMMIT_ORDER || type & AssFile::COMMIT_DIAG_ADDREM)
-		UpdateMaps(false);
+	if (type == AssFile::COMMIT_NEW || type & AssFile::COMMIT_ORDER || type & AssFile::COMMIT_DIAG_ADDREM)
+		UpdateMaps();
 
 	if (type & AssFile::COMMIT_DIAG_META) {
 		SetColumnWidths();
@@ -264,17 +260,10 @@ void BaseGrid::ClearMaps() {
 	AnnounceSelectedSetChanged(Selection(), old_selection);
 }
 
-void BaseGrid::UpdateMaps(bool preserve_selected_rows) {
+void BaseGrid::UpdateMaps() {
 	BeginBatch();
 	int active_row = line_index_map[active_line];
 
-	std::vector<int> sel_rows;
-	if (preserve_selected_rows) {
-		sel_rows.reserve(selection.size());
-		transform(selection.begin(), selection.end(), back_inserter(sel_rows),
-			[this](AssDialogue *diag) { return GetDialogueIndex(diag); });
-	}
-
 	index_line_map.clear();
 	line_index_map.clear();
 
@@ -283,44 +272,19 @@ void BaseGrid::UpdateMaps(bool preserve_selected_rows) {
 		index_line_map.push_back(curdiag);
 	}
 
-	if (preserve_selected_rows) {
-		Selection sel;
-
-		// If the file shrank enough that no selected rows are left, select the
-		// last row
-		if (sel_rows.empty())
-			sel_rows.push_back(index_line_map.size() - 1);
-		else if (sel_rows[0] >= (int)index_line_map.size())
-			sel_rows[0] = index_line_map.size() - 1;
+	auto sorted = index_line_map;
+	sort(begin(sorted), end(sorted));
+	Selection new_sel;
+	// Remove lines which no longer exist from the selection
+	set_intersection(selection.begin(), selection.end(),
+		sorted.begin(), sorted.end(),
+		inserter(new_sel, new_sel.begin()));
 
-		for (int row : sel_rows) {
-			if (row >= (int)index_line_map.size()) break;
-			sel.insert(index_line_map[row]);
-		}
-
-		SetSelectedSet(sel);
-	}
-	else {
-		auto sorted = index_line_map;
-		sort(begin(sorted), end(sorted));
-		Selection new_sel;
-		// Remove lines which no longer exist from the selection
-		set_intersection(selection.begin(), selection.end(),
-			sorted.begin(), sorted.end(),
-			inserter(new_sel, new_sel.begin()));
-
-		SetSelectedSet(new_sel);
-	}
+	SetSelectedSet(new_sel);
 
 	// The active line may have ceased to exist; pick a new one if so
-	if (line_index_map.size() && !line_index_map.count(active_line)) {
-		if (active_row < (int)index_line_map.size())
-			SetActiveLine(index_line_map[active_row]);
-		else if (preserve_selected_rows && !selection.empty())
-			SetActiveLine(index_line_map[sel_rows[0]]);
-		else
-			SetActiveLine(index_line_map.back());
-	}
+	if (line_index_map.size() && !line_index_map.count(active_line))
+		SetActiveLine(index_line_map[std::min((size_t)active_row, index_line_map.size() - 1)]);
 
 	if (selection.empty() && active_line)
 		SetSelectedSet({ active_line });
@@ -394,15 +358,6 @@ void BaseGrid::SelectRow(int row, bool addToSelected, bool select) {
 	RefreshRect(wxRect(0, (row + 1 - yPos) * lineHeight, w, lineHeight), false);
 }
 
-wxArrayInt BaseGrid::GetSelection() const {
-	wxArrayInt res;
-	res.reserve(selection.size());
-	transform(selection.begin(), selection.end(), std::back_inserter(res),
-		std::bind(&BaseGrid::GetDialogueIndex, this, std::placeholders::_1));
-	std::sort(res.begin(), res.end());
-	return res;
-}
-
 void BaseGrid::OnPaint(wxPaintEvent &) {
 	// Get size and pos
 	wxSize cs = GetClientSize();
diff --git a/aegisub/src/base_grid.h b/aegisub/src/base_grid.h
index 2d829fd04ee4f6a460b150bbaf0f45d5f8121b6a..76cbf982a8fefcea980f6aed72db4fdee8e8c72f 100644
--- a/aegisub/src/base_grid.h
+++ b/aegisub/src/base_grid.h
@@ -142,13 +142,10 @@ public:
 	void SetByFrame(bool state);
 
 	void SelectRow(int row, bool addToSelected = false, bool select=true);
-	wxArrayInt GetSelection() const;
 
 	void ClearMaps();
 	/// @brief Update the row <-> AssDialogue mappings
-	/// @param preserve_selected_rows Try to keep the same rows selected rather
-	///                               rather than the same lines
-	void UpdateMaps(bool preserve_selected_rows = false);
+	void UpdateMaps();
 	void UpdateStyle();
 
 	int GetRows() const { return index_line_map.size(); }
diff --git a/aegisub/src/frame_main.cpp b/aegisub/src/frame_main.cpp
index 84a53b7e71977175ff356c2366875ac8dbfd5ba9..c3c7ff0c9e053aeb6a4baec81da025a4695796eb 100644
--- a/aegisub/src/frame_main.cpp
+++ b/aegisub/src/frame_main.cpp
@@ -264,6 +264,7 @@ FrameMain::FrameMain()
 
 	StartupLog("Complete context initialization");
 	context->videoController->SetContext(context.get());
+	context->subsController->SetSelectionController(context->selectionController);
 
 	StartupLog("Set up drag/drop target");
 	SetDropTarget(new AegisubFileDropTarget(this));
diff --git a/aegisub/src/subs_controller.cpp b/aegisub/src/subs_controller.cpp
index c38baf13a029362eea6f7e3e5b66ea649aa26edb..b398aefb4a2bb711b9f4e8016128b42231a974f1 100644
--- a/aegisub/src/subs_controller.cpp
+++ b/aegisub/src/subs_controller.cpp
@@ -23,11 +23,13 @@
 #include "ass_file.h"
 #include "ass_info.h"
 #include "ass_style.h"
+#include "base_grid.h"
 #include "charset_detect.h"
 #include "compat.h"
 #include "command/command.h"
 #include "include/aegisub/context.h"
 #include "options.h"
+#include "selection_controller.h"
 #include "subtitle_format.h"
 #include "text_file_reader.h"
 #include "utils.h"
@@ -58,19 +60,23 @@ struct SubsController::UndoInfo {
 	std::vector<AssAttachment> graphics;
 	std::vector<AssAttachment> fonts;
 
+	mutable std::vector<int> selection;
+	int active_line_id = 0;
+
 	wxString undo_description;
 	int commit_id;
-	UndoInfo(AssFile const& f, wxString const& d, int c)
-	: undo_description(d), commit_id(c)
+
+	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 : f.Line) {
+		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;
+				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;
 			}
 		}
@@ -79,7 +85,7 @@ struct SubsController::UndoInfo {
 		styles.reserve(style_count);
 		events.reserve(event_count);
 
-		for (auto const& line : f.Line) {
+		for (auto const& line : c->ass->Line) {
 			switch (line.Group()) {
 			case AssEntryGroup::DIALOGUE:
 				events.push_back(static_cast<AssDialogue const&>(line));
@@ -103,21 +109,57 @@ struct SubsController::UndoInfo {
 				break;
 			}
 		}
+
+		UpdateActiveLine(c);
+		UpdateSelection(c);
 	}
 
-	operator AssFile() const {
-		AssFile ret;
+	void Apply(agi::Context *c) const {
+		// Keep old lines alive until after the commit is complete
+		AssFile old;
+		old.swap(*c->ass);
+
+		sort(begin(selection), end(selection));
+
+		AssDialogue *active_line = nullptr;
+		SubtitleSelection new_sel;
+
 		for (auto const& info : script_info)
-			ret.Line.push_back(*new AssInfo(info.first, info.second));
+			c->ass->Line.push_back(*new AssInfo(info.first, info.second));
 		for (auto const& style : styles)
-			ret.Line.push_back(*new AssStyle(style));
-		for (auto const& event : events)
-			ret.Line.push_back(*new AssDialogue(event));
+			c->ass->Line.push_back(*new AssStyle(style));
+		for (auto const& event : events) {
+			auto copy = new AssDialogue(event);
+			c->ass->Line.push_back(*copy);
+			if (copy->Id == active_line_id)
+				active_line = copy;
+			if (binary_search(begin(selection), end(selection), copy->Id))
+				new_sel.insert(copy);
+		}
 		for (auto const& attachment : graphics)
-			ret.Line.push_back(*new AssAttachment(attachment));
+			c->ass->Line.push_back(*new AssAttachment(attachment));
 		for (auto const& attachment : fonts)
-			ret.Line.push_back(*new AssAttachment(attachment));
-		return ret;
+			c->ass->Line.push_back(*new AssAttachment(attachment));
+
+		c->subsGrid->BeginBatch();
+		c->selectionController->SetSelectedSet({ });
+		c->ass->Commit("", AssFile::COMMIT_NEW);
+		c->selectionController->SetSelectionAndActive(new_sel, active_line);
+		c->subsGrid->EndBatch();
+	}
+
+	void UpdateActiveLine(const agi::Context *c) {
+		auto line = c->selectionController->GetActiveLine();
+		if (line)
+			active_line_id = line->Id;
+	}
+
+	void UpdateSelection(const agi::Context *c) {
+		auto const& sel = c->selectionController->GetSelectedSet();
+		selection.clear();
+		selection.reserve(sel.size());
+		for (const auto diag : sel)
+			selection.push_back(diag->Id);
 	}
 };
 
@@ -144,6 +186,11 @@ SubsController::SubsController(agi::Context *context)
 	});
 }
 
+void SubsController::SetSelectionController(SelectionController<AssDialogue *> *selection_controller) {
+	active_line_connection = context->selectionController->AddActiveLineListener(&SubsController::OnActiveLineChanged, this);
+	selection_connection = context->selectionController->AddSelectionListener(&SubsController::OnSelectionChanged, this);
+}
+
 void SubsController::Load(agi::fs::path const& filename, std::string charset) {
 	try {
 		try {
@@ -357,7 +404,7 @@ void SubsController::OnCommit(AssFileCommit c) {
 
 	redo_stack.clear();
 
-	undo_stack.emplace_back(*context->ass, c.message, commit_id);
+	undo_stack.emplace_back(context, c.message, commit_id);
 
 	int depth = std::max<int>(OPT_GET("Limits/Undo Levels")->GetInt(), 2);
 	while ((int)undo_stack.size() > depth)
@@ -369,27 +416,30 @@ void SubsController::OnCommit(AssFileCommit c) {
 	*c.commit_id = commit_id;
 }
 
-void SubsController::ApplyUndo() {
-	// Keep old lines alive until after the commit is complete
-	AssFile old;
-	old.swap(*context->ass);
-
-	*context->ass = undo_stack.back();
-	commit_id = undo_stack.back().commit_id;
+void SubsController::OnActiveLineChanged() {
+	if (!undo_stack.empty())
+		undo_stack.back().UpdateActiveLine(context);
+}
 
-	context->ass->Commit("", AssFile::COMMIT_NEW);
+void SubsController::OnSelectionChanged() {
+	if (!undo_stack.empty())
+		undo_stack.back().UpdateSelection(context);
 }
 
 void SubsController::Undo() {
 	if (undo_stack.size() <= 1) return;
 	redo_stack.splice(redo_stack.end(), undo_stack, std::prev(undo_stack.end()));
-	ApplyUndo();
+
+	commit_id = undo_stack.back().commit_id;
+	undo_stack.back().Apply(context);
 }
 
 void SubsController::Redo() {
 	if (redo_stack.empty()) return;
 	undo_stack.splice(undo_stack.end(), redo_stack, std::prev(redo_stack.end()));
-	ApplyUndo();
+
+	commit_id = undo_stack.back().commit_id;
+	undo_stack.back().Apply(context);
 }
 
 wxString SubsController::GetUndoDescription() const {
diff --git a/aegisub/src/subs_controller.h b/aegisub/src/subs_controller.h
index c2a179fa03ff83cbc8a7e9278abae8d2074f074e..f0c5fbd9997794a57c461e7e20e9ae6425f63135 100644
--- a/aegisub/src/subs_controller.h
+++ b/aegisub/src/subs_controller.h
@@ -22,15 +22,19 @@
 #include <set>
 #include <wx/timer.h>
 
+class AssDialogue;
 class AssEntry;
 class AssFile;
 struct AssFileCommit;
+template<typename T> class SelectionController;
 
 namespace agi { struct Context; }
 
 class SubsController {
 	agi::Context *context;
 	agi::signal::Connection undo_connection;
+	agi::signal::Connection active_line_connection;
+	agi::signal::Connection selection_connection;
 
 	struct UndoInfo;
 	boost::container::list<UndoInfo> undo_stack;
@@ -57,17 +61,22 @@ class SubsController {
 	/// The filename of the currently open file, if any
 	agi::fs::path filename;
 
-	void OnCommit(AssFileCommit c);
-
 	/// Set the filename, updating things like the MRU and last used path
 	void SetFileName(agi::fs::path const& file);
 
-	/// Set the current file to the file on top of the undo stack
-	void ApplyUndo();
+	void OnCommit(AssFileCommit c);
+	void OnActiveLineChanged();
+	void OnSelectionChanged();
 
 public:
 	SubsController(agi::Context *context);
 
+	/// Set the selection controller to use
+	///
+	/// Required due to that the selection controller is the subtitles grid, and
+	/// so is created long after the subtitles controller
+	void SetSelectionController(SelectionController<AssDialogue *> *selection_controller);
+
 	/// The file's path and filename if any, or platform-appropriate "untitled"
 	agi::fs::path Filename() const;