diff --git a/backend/src/api.ts b/backend/src/api.ts
index 8e8689d..cf7c6c8 100644
--- a/backend/src/api.ts
+++ b/backend/src/api.ts
@@ -473,7 +473,7 @@ export class Session {
if (!search) {
this.send({
event: "packageSearched",
- results: this.config.pkg!.popular || [],
+ results: [],
search: "",
});
return;
diff --git a/backend/src/langs.ts b/backend/src/langs.ts
index fd54507..92e4570 100644
--- a/backend/src/langs.ts
+++ b/backend/src/langs.ts
@@ -832,6 +832,7 @@ output = "Hello, world!"
code: `(defvar x (* 123 234))`,
},
pkg: {
+ index: ["https://melpa.org/#/", "https://elpa.gnu.org/"],
install: `emacs -Q --batch --eval "(progn (require 'package) (push '(\"melpa\" . \"https://melpa.org/packages/\") package-archives) (package-initialize) (unless (ignore-errors (>= (length (directory-files \"~/.emacs.d/elpa/archives\")) 4)) (package-refresh-contents)) (package-install 'NAME))"`,
uninstall: `ls ~/.emacs.d/elpa | grep -- - | grep '^NAME-[0-9]' | while read pkg; do emacs -Q --batch --eval "(progn (require 'package) (push '(\"melpa\" . \"https://melpa.org/packages/\") package-archives) (package-initialize) (unless (ignore-errors (>= (length (directory-files \"~/.emacs.d/elpa/archives\")) 4)) (package-refresh-contents)) (call-interactively 'package-delete))" <<< "$pkg"; done`,
all: `set -o pipefail; (curl -sS https://elpa.gnu.org/packages/ | grep '
' | grep -Eo '[^>]+' | grep -Eo '^[^<]+' && curl -sS https://melpa.org/archive.json | jq -r 'keys | .[]') | sort | uniq`,
@@ -1377,6 +1378,7 @@ PLEASE GIVE UP
`,
},
pkg: {
+ index: "https://www.npmjs.com/",
install: "yarn add NAME",
uninstall: "yarn remove NAME",
search:
@@ -2114,6 +2116,7 @@ binding_irb.run(IRB.conf)
`,
},
pkg: {
+ index: "https://rubygems.org/",
install: "gem install --user-install NAME",
uninstall: "gem uninstall --user-install NAME",
search: `curl -sS 'https://rubygems.org/api/v1/search.json?query=NAME' | jq -r 'map(.name) | .[]'`,
diff --git a/frontend/src/app.ts b/frontend/src/app.ts
index 0dd9db0..637a0a2 100644
--- a/frontend/src/app.ts
+++ b/frontend/src/app.ts
@@ -22,6 +22,8 @@ import "bootstrap";
import "xterm/css/xterm.css";
+import { autocomplete } from "./util";
+
const DEBUG = window.location.hash === "#debug";
const config: RijuConfig = (window as any).rijuConfig;
@@ -362,6 +364,10 @@ async function main() {
if (config.pkg) {
document.getElementById("packagesButton")!.classList.add("visible");
$("#packagesModal").on("shown.bs.modal", () => {
+ const searchInput = document.getElementById(
+ "packagesSearch"
+ ) as HTMLInputElement;
+
if (!packagesTermOpened) {
packagesTermOpened = true;
@@ -374,19 +380,17 @@ async function main() {
packagesFitAddon.fit();
window.addEventListener("resize", () => packagesFitAddon.fit());
- const searchInput = document.getElementById(
- "packagesSearch"
- ) as HTMLInputElement;
searchInput.addEventListener(
"input",
_.debounce(() => {
sendMessage({ event: "packageSearch", search: searchInput.value });
}, 100)
);
- handlePackageSearchResults = (results: string[]) => {
- console.log("got results:", results);
- };
+ handlePackageSearchResults = autocomplete(searchInput);
}
+
+ searchInput.value = "";
+ searchInput.focus();
});
}
}
diff --git a/frontend/src/util.ts b/frontend/src/util.ts
new file mode 100644
index 0000000..4da086d
--- /dev/null
+++ b/frontend/src/util.ts
@@ -0,0 +1,75 @@
+const MAX_AUTOCOMPLETE_ITEMS = 5;
+
+export function autocomplete(
+ input: HTMLInputElement
+): (results: string[]) => void {
+ let results: string[] = [];
+ let currentIndex = -1;
+ let origValue = input.value;
+
+ const close = () => {
+ currentIndex = -1;
+ for (const elt of Array.from(
+ input.parentElement!.getElementsByClassName("autocomplete-items")
+ )) {
+ input.parentElement!.removeChild(elt);
+ }
+ };
+
+ const updateActive = () => {
+ Array.from(
+ input.parentElement!.querySelectorAll(".autocomplete-items div")
+ ).forEach((elt, index) => {
+ if (index === currentIndex) {
+ elt.classList.add("autocomplete-active");
+ } else {
+ elt.classList.remove("autocomplete-active");
+ }
+ });
+ if (currentIndex === -1) {
+ input.value = origValue;
+ } else if (currentIndex >= 0 && currentIndex < results.length) {
+ input.value = results[currentIndex];
+ }
+ };
+
+ // input.addEventListener("blur", () => autocompleteClose(input));
+ input.addEventListener("keydown", (e) => {
+ if (currentIndex === -1) {
+ origValue = input.value;
+ }
+ switch (e.key) {
+ case "ArrowUp":
+ currentIndex -= 1;
+ if (currentIndex < -1) {
+ currentIndex = results.length - 1;
+ }
+ updateActive();
+ e.preventDefault();
+ break;
+ case "ArrowDown":
+ currentIndex += 1;
+ if (currentIndex >= results.length) {
+ currentIndex = -1;
+ }
+ updateActive();
+ e.preventDefault();
+ break;
+ }
+ });
+ return (newResults: string[]) => {
+ results = newResults.slice(0, MAX_AUTOCOMPLETE_ITEMS);
+ if (document.activeElement !== input) return;
+ close();
+ if (results.length === 0) return;
+ const eltsDiv = document.createElement("div");
+ eltsDiv.classList.add("autocomplete-items");
+ results.forEach((result, index) => {
+ const eltDiv = document.createElement("div");
+ eltDiv.innerText = result;
+ eltDiv.addEventListener("click", () => select(index));
+ eltsDiv.appendChild(eltDiv);
+ });
+ input.parentNode!.appendChild(eltsDiv);
+ };
+}
diff --git a/frontend/styles/app.css b/frontend/styles/app.css
index 0f4c254..37b66f7 100644
--- a/frontend/styles/app.css
+++ b/frontend/styles/app.css
@@ -71,3 +71,28 @@ body {
#packagesSearch {
width: 100%;
}
+
+.autocomplete-items {
+ position: absolute;
+ z-index: 50;
+ border: 1px solid #d4d4d4;
+ border-bottom: none;
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.autocomplete-items div {
+ background-color: white;
+ cursor: pointer;
+ padding: 5px 11px;
+ border-bottom: 1px solid #d4d4d4;
+}
+
+.autocomplete-items div:hover {
+ background-color: #e9e9e9;
+}
+
+.autocomplete-active {
+ background-color: DodgerBlue !important;
+ color: white;
+}
diff --git a/webpack.config.js b/webpack.config.js
index f742d30..1fcf1e3 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -56,5 +56,6 @@ module.exports = (_, argv) => ({
alias: {
vscode: require.resolve("monaco-languageclient/lib/vscode-compatibility"),
},
+ extensions: [".js", ".ts"],
},
});
|