Skip to content

Commit

Permalink
Implement tab completion of user names
Browse files Browse the repository at this point in the history
This commit implements a feature commonly found in IRC clients
and often referred to as "tab complete". This allows a user to
type part of a user's name in the chat box and then press tab, which
causes the program to complete the user's name in the chat box. The
user can then press tab additional times in order to cycle through
the list of possible matches.

Tab complete has been implemented for the main chat, for battles,
and for private messages.

The algorithm implemented is that when you press tab, the program
looks backward to the last space (or start of the line) and
considers the substring between the last space and the position of
the cursor to be the prefix of a name. It then builds a list of
all names that it could be, which is then sorted. The sort is
done first based on activity (i.e. users who have spoken more
recently are sorted higher), and if users are tied as to activity,
then by alphabetical order.

After a name has been tab-completed, you can then press tab
subsequent times in order to cycle through the remainder of the list.

The idea behind this algorithm is that a user will usually want
to refer to somebody who has spoken recently in the chat. This
is especially helpful for Pokemon Showdown because with 1500+
users, it would be difficult to complete the correct name if it
were based purely on alphabetical order.

Matching of names is based on userids, which are strictly
alphanumeric. Therefore, when intending to use tab complete,
a user should type only the letters and numbers from a user's
name, not any spaces or symbols. After pressing tab, the spaces
and symbols will appear in the chatbox, however.
  • Loading branch information
cathyjf committed Jan 27, 2013
1 parent 3de94d4 commit 7510edb
Showing 1 changed file with 95 additions and 1 deletion.
96 changes: 95 additions & 1 deletion js/sim.js
Expand Up @@ -439,6 +439,8 @@ function BattleRoom(id, elem) {
if (me.named) {
selfR.chatAddElem.html('<form onsubmit="return false" class="chatbox"><label style="' + hashColor(me.userid) + '">' + sanitize(me.name) + ':</label> <textarea class="textbox" type="text" size="70" autocomplete="off" onkeypress="return rooms[\'' + selfR.id + '\'].formKeyPress(event)"></textarea></form>');
selfR.chatboxElem = selfR.chatAddElem.find('textarea');
// The keypress event does not capture tab, so use keydown.
selfR.chatboxElem.keydown(rooms['lobby'].formKeyDown);
selfR.chatboxElem.autoResize({
animateDuration: 100,
extraSpace: 0
Expand Down Expand Up @@ -863,6 +865,7 @@ function BattleRoom(id, elem) {
selfR.callback(selfR.battle, 'decision');
return false;
};
// Key press in the battle chat textbox.
this.formKeyPress = function (e) {
hideTooltip();
if (e.keyCode === 13) {
Expand Down Expand Up @@ -949,6 +952,13 @@ function Lobby(id, elem) {
this.joinLeaveElem = null;
this.userCount = {};
this.userList = {};
this.userActivity = [];
this.tabComplete = {
candidates: null,
index: 0,
prefix: null,
cursor: -1
};
this.searcher = null;
this.selectedTeam = 0;
this.selectedFormat = '';
Expand Down Expand Up @@ -1198,6 +1208,7 @@ function Lobby(id, elem) {
selfR.popupElem.prepend(code);
}
selfR.popupChatboxElem = selfR.popupElem.find('textarea').last();
selfR.popupChatboxElem.keydown(rooms['lobby'].formKeyDown);
selfR.popupElem.show();
$('#' + selfR.id + '-pmlog-frame').scrollTop($('#' + selfR.id + '-pmlog').height());
selfR.popupChatboxElem.autoResize({
Expand All @@ -1215,6 +1226,18 @@ function Lobby(id, elem) {
}
}
};
// Mark a user as active for the purpose of tab complete.
this.markUserActive = function (userid) {
var idx = selfR.userActivity.indexOf(userid);
if (idx != -1) {
selfR.userActivity.splice(idx, 1);
}
selfR.userActivity.push(userid);
if (selfR.userActivity.length > 400) {
// Prune the list.
selfR.userActivity.splice(0, 200);
}
};
this.add = function (log) {
if (typeof log === 'string') log = log.split('\n');
var autoscroll = false;
Expand Down Expand Up @@ -1332,6 +1355,9 @@ function Lobby(id, elem) {

if (me.ignore[userid] && log[i].name.substr(0, 1) === ' ') continue;

// Add this user to the list of people who have spoken recently.
selfR.markUserActive(userid);

selfR.joinLeaveElem = null;
selfR.joinLeave = {
'join': [],
Expand Down Expand Up @@ -1840,6 +1866,8 @@ function Lobby(id, elem) {
if (me.named) {
selfR.chatAddElem.html('<form onsubmit="return false" class="chatbox"><label style="' + hashColor(me.userid) + '">' + sanitize(me.name) + ':</label> <textarea class="textbox" type="text" size="70" autocomplete="off" onkeypress="return rooms[\'' + selfR.id + '\'].formKeyPress(event)"></textarea></form>');
selfR.chatboxElem = selfR.chatAddElem.find('textarea');
// The keypress event does not capture tab, so use keydown.
selfR.chatboxElem.keydown(this.formKeyDown);
selfR.chatboxElem.autoResize({
animateDuration: 100,
extraSpace: 0
Expand All @@ -1853,9 +1881,10 @@ function Lobby(id, elem) {
selfR.meIdent.named = me.named;
}
};
// Key press in the chat textbox.
this.formKeyPress = function (e) {
hideTooltip();
if (e.keyCode === 13) {
if (e.keyCode === 13) { // Enter
var text;
if ((text = selfR.chatboxElem.val())) {
text = selfR.parseCommand(text);
Expand All @@ -1868,6 +1897,71 @@ function Lobby(id, elem) {
}
return true;
};
this.formKeyDown = function (e) {
hideTooltip();
// We only handle the tab key.
if (e.keyCode !== 9) return true;

// No matter what, we don't want to tab away from this box.
e.preventDefault();

// Don't tab complete at the start of the text box.
var chatbox = $(e.delegateTarget);
var idx = chatbox.prop('selectionStart');
if (idx === 0) return true;

var text = chatbox.val();

if (idx === selfR.tabComplete.cursor) {
// The user is cycling through the candidate names.
if (++selfR.tabComplete.index >= selfR.tabComplete.candidates.length) {
selfR.tabComplete.index = 0;
}
} else {
// This is a new tab completion.

// There needs to be non-whitespace to the left of the cursor.
var m = /^(.*?)([^ ]*)$/.exec(text.substr(0, idx));
if (!m) return true;

selfR.tabComplete.prefix = m[1];
var idprefix = toId(m[2]);
var candidates = [];

for (var i in selfR.userList) {
if (!selfR.userList.hasOwnProperty(i)) continue;
if (!(typeof i === 'string')) continue;
if (i.substr(0, idprefix.length) !== idprefix) continue;
candidates.push(i);
}

// Sort by most recent to speak in the chat, or, in the case of a tie,
// in alphabetical order.
candidates.sort(function(a, b) {
var aidx = selfR.userActivity.indexOf(a);
var bidx = selfR.userActivity.indexOf(b);
if (aidx != -1) {
if (bidx != -1) {
return bidx - aidx;
}
return -1; // a comes first
} else if (bidx != -1) {
return 1; // b comes first
}
return a < b; // alphabetical order
});
selfR.tabComplete.candidates = candidates;
selfR.tabComplete.index = 0;
}

// Substitute in the tab-completed name.
var substituteUserId = selfR.tabComplete.candidates[selfR.tabComplete.index];
var name = selfR.userList[substituteUserId].substr(1);
chatbox.val(selfR.tabComplete.prefix + name + text.substr(idx));
var pos = selfR.tabComplete.prefix.length + name.length;
chatbox[0].setSelectionRange(pos, pos);
selfR.tabComplete.cursor = pos;
};
this.formRename = function () {
overlay('rename');
return false;
Expand Down

0 comments on commit 7510edb

Please sign in to comment.