From 20be67503bd073f6f3672c20259f3928e72c2b54 Mon Sep 17 00:00:00 2001
From: Simon Ser <contact@emersion.fr>
Date: Mon, 29 Jun 2020 12:36:17 +0200
Subject: [PATCH] Add basic autocompletion

---
 components/app.js      | 23 ++++++++++++++++++++++-
 components/composer.js | 33 +++++++++++++++++++++++++++++++--
 2 files changed, 53 insertions(+), 3 deletions(-)

diff --git a/components/app.js b/components/app.js
index cdd50e9..70011f2 100644
--- a/components/app.js
+++ b/components/app.js
@@ -55,6 +55,7 @@ export default class App extends Component {
 		this.handleComposerSubmit = this.handleComposerSubmit.bind(this);
 		this.handleNickClick = this.handleNickClick.bind(this);
 		this.handleJoinClick = this.handleJoinClick.bind(this);
+		this.autocomplete = this.autocomplete.bind(this);
 
 		if (window.localStorage && localStorage.getItem("autoconnect")) {
 			var connectParams = JSON.parse(localStorage.getItem("autoconnect"));
@@ -554,6 +555,26 @@ export default class App extends Component {
 		this.client.send({ command: "JOIN", params: [channel] });
 	}
 
+	autocomplete(prefix) {
+		if (!this.state.activeBuffer) {
+			return null;
+		}
+		var buf = this.state.buffers.get(this.state.activeBuffer);
+
+		prefix = prefix.toLowerCase();
+
+		var repl = null;
+		for (var nick of buf.members.keys()) {
+			if (nick.toLowerCase().startsWith(prefix)) {
+				if (repl) {
+					return null;
+				}
+				repl = nick;
+			}
+		}
+		return repl;
+	}
+
 	componentDidMount() {
 		if (this.state.connectParams.autoconnect) {
 			this.connect(this.state.connectParams);
@@ -609,7 +630,7 @@ export default class App extends Component {
 				</section>
 			</>
 			${memberList}
-			<${Composer} ref=${this.composer} readOnly=${this.state.activeBuffer == SERVER_BUFFER} onSubmit=${this.handleComposerSubmit}/>
+			<${Composer} ref=${this.composer} readOnly=${this.state.activeBuffer == SERVER_BUFFER} onSubmit=${this.handleComposerSubmit} autocomplete=${this.autocomplete}/>
 		`;
 	}
 }
diff --git a/components/composer.js b/components/composer.js
index eacc483..7d581a2 100644
--- a/components/composer.js
+++ b/components/composer.js
@@ -11,6 +11,7 @@ export default class Composer extends Component {
 
 		this.handleInput = this.handleInput.bind(this);
 		this.handleSubmit = this.handleSubmit.bind(this);
+		this.handleInputKeyDown = this.handleInputKeyDown.bind(this);
 		this.handleWindowKeyDown = this.handleWindowKeyDown.bind(this);
 	}
 
@@ -24,8 +25,36 @@ export default class Composer extends Component {
 		this.setState({ text: "" });
 	}
 
+	handleInputKeyDown(event) {
+		if (!this.props.autocomplete || event.key !== "Tab") {
+			return;
+		}
+
+		var text = this.state.text;
+		var i;
+		for (i = text.length - 1; i >= 0; i--) {
+			if (text[i] === " ") {
+				break;
+			}
+		}
+		var prefix = text.slice(i + 1);
+		if (!prefix) {
+			return;
+		}
+
+		event.preventDefault();
+
+		var repl = this.props.autocomplete(prefix);
+		if (!repl) {
+			return;
+		}
+
+		text = text.slice(0, i + 1) + repl;
+		this.setState({ text });
+	}
+
 	handleWindowKeyDown(event) {
-		if (document.activeElement == document.body && event.key == "/" && !this.state.text) {
+		if (document.activeElement === document.body && event.key === "/" && !this.state.text) {
 			event.preventDefault();
 			this.setState({ text: "/" }, () => {
 				this.focus();
@@ -49,7 +78,7 @@ export default class Composer extends Component {
 	render() {
 		return html`
 			<form id="composer" class="${this.props.readOnly && !this.state.text ? "read-only" : ""}" onInput=${this.handleInput} onSubmit=${this.handleSubmit}>
-				<input type="text" name="text" ref=${this.textInput} value=${this.state.text} placeholder="Type a message"/>
+				<input type="text" name="text" ref=${this.textInput} value=${this.state.text} placeholder="Type a message" onKeyDown=${this.handleInputKeyDown}/>
 			</form>
 		`;
 	}