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"], }, });