in mail/components/im/content/chat-conversation.js [631:994]
inputKeyPress(event) {
const text = this.inputBox.value;
const navKeyCodes = [
KeyEvent.DOM_VK_PAGE_UP,
KeyEvent.DOM_VK_PAGE_DOWN,
KeyEvent.DOM_VK_HOME,
KeyEvent.DOM_VK_END,
KeyEvent.DOM_VK_UP,
KeyEvent.DOM_VK_DOWN,
];
// Pass navigation keys to the browser if
// 1) the textbox is empty or 2) it's an IB-specific key combination
if (
(!text && navKeyCodes.includes(event.keyCode)) ||
((event.shiftKey || event.altKey) &&
(event.keyCode == KeyEvent.DOM_VK_PAGE_UP ||
event.keyCode == KeyEvent.DOM_VK_PAGE_DOWN))
) {
const newEvent = new KeyboardEvent("keypress", event);
event.preventDefault();
event.stopPropagation();
// Keyboard events must be sent to the focused element for bubbling to work.
this.convBrowser.focus();
this.convBrowser.dispatchEvent(newEvent);
this.inputBox.focus();
return;
}
// When attempting to copy an empty selection, copy the
// browser selection instead (see bug 693).
// The 'C' won't be lowercase if caps lock is enabled.
if (
(event.charCode == 99 /* 'c' */ ||
(event.charCode == 67 /* 'C' */ && !event.shiftKey)) &&
(navigator.platform.includes("Mac") ? event.metaKey : event.ctrlKey) &&
this.inputBox.selectionStart == this.inputBox.selectionEnd
) {
this.convBrowser.doCommand();
return;
}
// We don't want to enable tab completion if the user has selected
// some text, as it's not clear what the user would expect
// to happen in that case.
const noSelection = !(
this.inputBox.selectionEnd - this.inputBox.selectionStart
);
// Undo tab complete.
if (
noSelection &&
this._completions &&
event.keyCode == KeyEvent.DOM_VK_BACK_SPACE &&
!event.altKey &&
!event.ctrlKey &&
!event.metaKey &&
!event.shiftKey
) {
if (text == this._beforeTabComplete) {
// Nothing to undo, so let backspace act normally.
delete this._completions;
} else {
event.preventDefault();
// First undo the comma separating multiple nicks or the suffix.
// More than one nick:
// "nick1, nick2: " -> "nick1: nick2"
// Single nick: remove the suffix
// "nick1: " -> "nick1"
const pos = this.inputBox.selectionStart;
const suffix = ": ";
if (
pos > suffix.length &&
text.substring(pos - suffix.length, pos) == suffix
) {
const completions = Array.from(this.buddies.keys());
// Check if the preceding words are a sequence of nick completions.
const preceding = text
.substring(0, pos - suffix.length)
.split(", ");
if (preceding.every(n => completions.includes(n))) {
let s = preceding.pop();
if (preceding.length) {
s = suffix + s;
}
this.inputBox.selectionStart -= s.length + suffix.length;
this.addString(s);
if (this._completions[0].slice(-suffix.length) == suffix) {
this._completions = this._completions.map(c =>
c.slice(0, -suffix.length)
);
}
if (
this._completions.length == 1 &&
this.inputBox.value == this._beforeTabComplete
) {
// Nothing left to undo or to cycle through.
delete this._completions;
}
return;
}
}
// Full undo.
this.inputBox.selectionStart = 0;
this.addString(this._beforeTabComplete);
delete this._completions;
return;
}
}
// Tab complete.
// Keep the default behavior of the tab key if the input box
// is empty or a modifier is used.
if (
event.keyCode == KeyEvent.DOM_VK_TAB &&
text.length != 0 &&
noSelection &&
!event.altKey &&
!event.ctrlKey &&
!event.metaKey &&
(!event.shiftKey || this._completions)
) {
event.preventDefault();
if (this._completions) {
// Tab has been pressed more than once.
if (this._completions.length == 1) {
return;
}
if (this._shouldListCompletionsLater) {
this._conv.systemMessage(this._shouldListCompletionsLater);
delete this._shouldListCompletionsLater;
}
this.inputBox.selectionStart = this._completionsStart;
if (event.shiftKey) {
// Reverse cycle completions.
this._completionsIndex -= 2;
if (this._completionsIndex < 0) {
this._completionsIndex += this._completions.length;
}
}
this.addString(this._completions[this._completionsIndex++]);
this._completionsIndex %= this._completions.length;
return;
}
let completions = [];
let firstWordSuffix = " ";
let secondNick = false;
// Second regex result will contain word without leading special characters.
this._beforeTabComplete = text.substring(
0,
this.inputBox.selectionStart
);
const words = this._beforeTabComplete.match(/\S*?([\w-]+)?$/);
let word = words[0];
if (!word) {
return;
}
let isFirstWord = this.inputBox.selectionStart == word.length;
// Check if we are completing a command.
const completingCommand = isFirstWord && word[0] == "/";
if (completingCommand) {
for (const cmd of IMServices.cmd.listCommandsForConversation(
this._conv
)) {
// It's possible to have a global and a protocol specific command
// with the same name. Avoid duplicates in the |completions| array.
const name = "/" + cmd.name;
if (!completions.includes(name)) {
completions.push(name);
}
}
} else {
// If it's not a command, the only thing we can complete is a nick.
if (!this._conv.isChat) {
return;
}
firstWordSuffix = ": ";
completions = Array.from(this.buddies.keys());
const outgoingNick = this._conv.nick;
completions = completions.filter(c => c != outgoingNick);
// Check if the preceding words are a sequence of nick completions.
const wordStart = this.inputBox.selectionStart - word.length;
if (wordStart > 2) {
const separator = text.substring(wordStart - 2, wordStart);
if (separator == ": " || separator == ", ") {
const preceding = text.substring(0, wordStart - 2).split(", ");
if (preceding.every(n => completions.includes(n))) {
secondNick = true;
isFirstWord = true;
// Remove preceding completions from possible completions.
completions = completions.filter(c => !preceding.includes(c));
}
}
}
}
// Keep only the completions that share |word| as a prefix.
// Be case insensitive only if |word| is entirely lower case.
let condition;
if (word.toLocaleLowerCase() == word) {
condition = c => c.toLocaleLowerCase().startsWith(word);
} else {
condition = c => c.startsWith(word);
}
let matchingCompletions = completions.filter(condition);
if (!matchingCompletions.length && words[1]) {
word = words[1];
firstWordSuffix = " ";
matchingCompletions = completions.filter(condition);
}
if (!matchingCompletions.length) {
return;
}
// If the cursor is in the middle of a word, and the word is a nick,
// there is no need to complete - just jump to the end of the nick.
const wholeWord = text.substring(
this.inputBox.selectionStart - word.length
);
for (const completion of matchingCompletions) {
if (wholeWord.lastIndexOf(completion, 0) == 0) {
const moveCursor = completion.length - word.length;
this.inputBox.selectionStart += moveCursor;
const separator = text.substring(
this.inputBox.selectionStart,
this.inputBox.selectionStart + 2
);
if (separator == ": " || separator == ", ") {
this.inputBox.selectionStart += 2;
} else if (!moveCursor) {
// If we're already at the end of a nick, carry on to display
// a list of possible alternatives and/or apply punctuation.
break;
}
return;
}
}
// We have possible completions!
this._completions = matchingCompletions.sort();
this._completionsIndex = 0;
// Save now the first and last completions in alphabetical order,
// as we will need them to find a common prefix. However they may
// not be the first and last completions in the list of completions
// actually exposed to the user, as if there are active nicks
// they will be moved to the beginning of the list.
const firstCompletion = this._completions[0];
const lastCompletion = this._completions.slice(-1)[0];
let preferredNick = false;
if (this._conv.isChat && !completingCommand) {
// If there are active nicks, prefer those.
const activeCompletions = this._completions.filter(
c =>
this.buddies.has(c) &&
!this.buddies.get(c).hasAttribute("inactive")
);
if (activeCompletions.length == 1) {
preferredNick = true;
}
if (activeCompletions.length) {
// Move active nicks to the front of the queue.
activeCompletions.reverse();
activeCompletions.forEach(function (c) {
this._completions.splice(this._completions.indexOf(c), 1);
this._completions.unshift(c);
}, this);
}
// If one of the completions is the sender of the last ping,
// take it, if it was less than an hour ago.
if (
this._lastPing &&
this.buddies.has(this._lastPing) &&
this._completions.includes(this._lastPing) &&
Date.now() / 1000 - this._lastPingTime < 3600
) {
preferredNick = true;
this._completionsIndex = this._completions.indexOf(this._lastPing);
}
}
// Display the possible completions in a system message.
delete this._shouldListCompletionsLater;
if (this._completions.length > 1) {
const completionsList = this._completions.join(" ");
if (preferredNick) {
// If we have a preferred nick (which is completed as a whole
// even if there are alternatives), only show the list of
// completions on the next <tab> press.
this._shouldListCompletionsLater = completionsList;
} else {
this._conv.systemMessage(completionsList);
}
}
const suffix = isFirstWord ? firstWordSuffix : "";
this._completions = this._completions.map(c => c + suffix);
let completion;
if (this._completions.length == 1 || preferredNick) {
// Only one possible completion? Apply it! :-)
completion = this._completions[this._completionsIndex++];
this._completionsIndex %= this._completions.length;
} else {
// We have several possible completions, attempt to find a common prefix.
const maxLength = Math.min(
firstCompletion.length,
lastCompletion.length
);
let i = 0;
while (i < maxLength && firstCompletion[i] == lastCompletion[i]) {
++i;
}
if (i) {
completion = firstCompletion.substring(0, i);
} else {
// Include this case so that secondNick is applied anyway,
// in case a completion is added by another tab press.
completion = word;
}
}
// Always replace what the user typed as its upper/lowercase may
// not be correct.
this.inputBox.selectionStart -= word.length;
this._completionsStart = this.inputBox.selectionStart;
if (secondNick) {
// Replace the trailing colon with a comma before the completed nick.
this.inputBox.selectionStart -= 2;
completion = ", " + completion;
}
this.addString(completion);
} else if (this._completions) {
delete this._completions;
}
if (event.keyCode != 13) {
return;
}
if (!event.ctrlKey && !event.shiftKey && !event.altKey) {
// Prevent the default action before calling sendMsg to avoid having
// a line break inserted in the textbox if sendMsg throws.
event.preventDefault();
this.sendMsg(text);
} else if (!event.shiftKey) {
this.addString("\n");
}
}