source-serialid-cursorline-cursorcolumn-timestamp
Source is the name of the original file whose editing got recorded. Serial id is the number of the frame in the frame sequence. This number gives no information about the time the frame was recorded. Cursorline and cursorcolumn record the position of the cursor at the time the frame was recorded. Timestamp records the time the frame was recorded. The value is produced by C’s strftime(“%s”), whose documentation has this to say about %s:
The number of seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC).
If storing the metadata like this ends up being a bad idea, we’ll fix it. Caveat emptor.
]]>To produce frame 2, we apply diff 2 to frame 1. To produce frame 3, we apply diff 3 to (diff 2 to frame 1). To produce frame N, we apply diff N to (diff N – 1 to (… to frame 1)).
Since we take snapshots at every cursor change, the diffs are likely to be quite small, often a line or two. Grabbing a text movie from the web should be much faster compared to grabbing the complete set of snapshots.
The reconstruction process involves parsing the diff file and applying it just like the Unix patch utility does. What follows is my attempt at understanding the diff protocol and reconstructing patch. The Patcher class takes a complete frame and a patch (both stored as String lists to make indexing into the files easier) and applies the patch to produce the next complete frame.
package org.twodee.undiff;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.twodee.utilities.Utilities;
/**
Patches a source text modified by changes, deletes, and appends.
*/
public class Patcher {
private static final Pattern TRANSACTION_PATTERN = Pattern.compile("(\\d+)(?:,(\\d+))?([acd])(\\d+)(?:,(\\d+))?");
private ArrayList<String> pre;
private ArrayList<String> patch;
private int iPatchLine;
private int iPreLine;
private ArrayList<String> post;
/**
Applies a patch to a source text.
@param pre The source text, given as a list of lines.
@param patch The patch text as produced by diff, given as a list of lines.
@return The destination text, given as a list of lines.
*/
public static ArrayList<String> patch(ArrayList<String> pre, ArrayList<String> patch) {
Patcher patcher = new Patcher(pre, patch);
patcher.apply();
return patcher.post;
}
/*
Not meant for public use.
*/
private Patcher(ArrayList<String> pre, ArrayList<String> patch) {
this.pre = pre;
this.patch = patch;
}
private void apply() {
// We start at the beginning of the patch and the pre. Post gets built up
// as we advance through the patch, but it starts off empty.
iPatchLine = 0;
iPreLine = 0;
post = new ArrayList<String>();
// Read and apply the patch, one transaction at a time. Since not every
// line may be altered by the transactions, before we apply a transaction,
// we make sure all the lines previous to the lines the transaction affects
// get copied over from pre to post.
while (hasTransaction()) {
Transaction transaction = readTransaction();
appendPreLinesToPost(transaction.getPreStart());
transaction.execute();
iPreLine = transaction.getPreEnd() + 1;
}
// We also want to copy any lines after the last transaction left off up
// to the end of the file.
appendPreLinesToPost(pre.size());
}
/*
Determines whether the patch contains another transaction to process. If
there are any unvisited lines in the patch, we have more transactions.
*/
private boolean hasTransaction() {
return iPatchLine < patch.size();
}
/*
Appends lines from pre to post.
*/
private void appendPreLinesToPost(int to) {
for (; iPreLine < to; ++iPreLine) {
post.add(pre.get(iPreLine));
}
}
/*
Gets the next transaction that is to be executed. It does not execute the
transaction. It does not advance iPatchLine.
*/
private Transaction readTransaction() {
Matcher matcher = TRANSACTION_PATTERN.matcher(patch.get(iPatchLine));
if (!matcher.find()) {
throw new RuntimeException("Bad transaction type");
}
Transaction transaction;
String command = matcher.group(3);
if (command.equals("a")) {
transaction = new AppendTransaction();
} else if (command.equals("c")) {
transaction = new ChangeTransaction();
} else {
transaction = new DeleteTransaction();
}
int preStart = Integer.parseInt(matcher.group(1)) - 1;
int preEnd = matcher.group(2) == null ? preStart : Integer.parseInt(matcher.group(2)) - 1;
int postStart = Integer.parseInt(matcher.group(4)) - 1;
int postEnd = matcher.group(5) == null ? postStart : Integer.parseInt(matcher.group(5)) - 1;
transaction.setBounds(preStart, preEnd, postStart, postEnd);
return transaction;
}
/*
Represents a transaction, which turns ranges of lines in pre to ranges of
lines in post.
*/
private abstract class Transaction {
public int preStart;
public int preEnd;
public int postStart;
public int postEnd;
public void setBounds(int preStart, int preEnd, int postStart, int postEnd) {
this.preStart = preStart;
this.preEnd = preEnd;
this.postStart = postStart;
this.postEnd = postEnd;
}
public int getPreStart() {
return preStart;
}
public int getPreEnd() {
return preEnd;
}
/*
Executes this transaction and puts the patch line cursor at the start of
the next transaction. After this method, we are ready to read the next
transaction from the patch.
*/
public abstract void execute();
}
/*
Changes the specified pre lines into the specified post lines. A change
transaction has the following representation in the patch:
9c7,8
< first line replace
---
> first line to insert
> second line to insert
*/
private class ChangeTransaction extends Transaction {
public void execute() {
// Move past the transaction header, the pre lines on the chopping block,
// and the --- separator.
iPatchLine += (preEnd - preStart + 1) + 2;
// Read each line to append from the patch, removing the leading "> ",
// and insert the line into post.
for (int i = postStart; i <= postEnd; ++i) {
post.add(patch.get(iPatchLine).substring(2));
++iPatchLine;
}
}
}
/*
Appends the enumerated lines into post. An append transaction has the
following representation in the patch:
9a10,11
> first line to append
> second line to append
*/
private class AppendTransaction extends Transaction {
public void execute() {
// Move past the transaction header line.
++iPatchLine;
// The transaction is responsible for dealing with pre lines in
// [preStart, preEnd]. On append, only one pre line is in this range,
// which we simply copy over.
appendPreLinesToPost(preStart + 1);
// Read each line to append from the patch, removing the leading "> ",
// and insert the line into post.
for (int i = postStart; i <= postEnd; ++i) {
post.add(patch.get(iPatchLine).substring(2));
++iPatchLine;
}
}
}
/*
Advances past all the deleted pre lines in this transaction in the patch.
We don't really need to do anything more, since the lines in this
transaction are not to appear in post.
A delete transaction that removes two lines has this representation in the
patch:
1,2d0
< first line to be removed
< second line to be removed
We move past the transaction line and the two pre lines, so that the next
thing we read will be the transaction line.
*/
private class DeleteTransaction extends Transaction {
public void execute() {
iPatchLine += (preEnd - preStart + 1) + 1;
}
}
}
My primary goal was to keep the methods short. It didn’t really make sense to create a stateful object, since Patcher has no meaningful lifespan. Thus, I wanted a static method. The code was too long for a single static method. Breaking it up meant passing a bunch of parameters around to the helper methods, and that didn’t sound fun. The solution was to make all instance-related tasks private. That way I could safely store the “global” data in instance variables that I didn’t have to pass around. (This is safe because I’m the only that can alter the Patcher instance I create.) I’m not sure I like the design, but it does have minimal conditional statements and the methods are pretty short. They’re not really individually testable.
]]>My initial thoughts were to intercept every keystroke and record the changes elsewhere on disk. I didn’t really like this idea, given the extreme number of snapshots it would produce, so I went searching for something better. It turns out that the Vim editor recognizes a couple of events, CursorHold and CursorHoldI, that are triggered when the cursor stops moving. Moving cursors are a little different than text changes, but the location of the cursor is probably something I also want to track in the text movie. With Vim’s autocommands, I can register an event handler for these two events.
The following function, when called on a file named foo, starts listening for these CursorHold events. When the first is fired, the current text is written to foo00…0. The next to foo00…1. The next to foo00…2. They’ll be processed into a text movie later.
function! StartKeystrokes()
" CursorHold* events are fired when the user doesn't move the cursor
" for updatetime milliseconds.
set updatetime=500
" We're going to keep a counter for the number of events that are
" fired. I'm sure we'll find some way to use it.
let b:nupdates=0
" Snapshots will be recorded to a directory. Let's put it in junk
" so that it will automatically get wiped. Let's wipe any previous
" session.
let b:dir=$HOME . "/.junk/keystrokes/" . bufname("%")
execute ":!rm -rf " . b:dir . " && mkdir -p " . b:dir
" Attach to this buffer some event handlers. When the user stops
" moving the cursor in either normal or insert mode, let's record
" a snapshot out to disk.
autocmd CursorHold,CursorHoldI <buffer>
\ let b:nupdates=b:nupdates + 1|
\ execute printf('write! %s/%s%010d', b:dir, bufname("%"), b:nupdates)
endfunction
]]>jQuery sliders are discrete; they can only be scrubbed to integral positions. Since the text movies they are scrubbing through may be 100, 103.5, 15.7, etc., second longs, I must range map the slider’s tick position (in [0, n], where n is the maximum number of ticks) to the appropriate timestep (in [0, nseconds], where nseconds is the number of seconds in the movie). I do that by finding the proportion of the current tick position to the maximal tick position, and applying that proportion to the number of seconds.
I make the maximal number of ticks a large number to provide a good number of reachable timesteps. If n was only 10, for example, the slider would only let the user access 10 different timesteps. For a 3 hour text movie, that’s not very helpful.
Here’s the script I ended up with:
When embedded in the page, the script produces:
]]>jQuery has several ways to retrieve file contents: load, ajax, and get. The latter two let you specify the file type, which I found to be important. If I tried to load a JavaScript file, jQuery would automatically execute it. For my purposes, I wanted the file as plain text, which the get function lets me specify.
Here’s a test script I ended up with:
When I execute it, the file is properly embedded:
jQuery’s fun.
]]><link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.9/themes/base/jquery.ui.all.css">
<?php
wp_deregister_script('jquery');
wp_register_script('jquery', "https://ajax.googleapis.com/ajax/libs/jquery/1.5.0/jquery.min.js", false);
wp_enqueue_script('jquery');
wp_register_script('jqueryui', "https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.13/jquery-ui.min.js", false);
wp_enqueue_script('jqueryui');
?>
There are probably later versions I could be grabbing.
Then I embedded a Javascript file into my post which created a slider div:
jQuery(function() {
jQuery("#slider").slider({
value: 100,
min: 0,
max: 500,
step: 50,
slide: function(event, ui) {
// insert event-handling code here
}
});
});
// Add a div which will hold the slider.
document.write('<div id="slider"></div>');
This file produces the following slider:
And I’m satisfied.
]]>