Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

autocomplete and import vals #8

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,23 @@ Use the `Val Town: Set Token` command to set your api token, that you can get fr

### Author Vals from VS Code

![val demo](https://raw.githubusercontent.com/pomdtr/valtown-vscode/master/img/vals.png)
![val demo](https://raw.githubusercontent.com/val-town/val-town-vscode/main/img/vals.png)

## Preview Web Endpoints

![preview demo](https://raw.githubusercontent.com/pomdtr/valtown-vscode/master/img/preview.png)
![preview demo](https://raw.githubusercontent.com/val-town/val-town-vscode/main/img/preview.png)

### Edit/Manage your Blobs

![blob demo](https://raw.githubusercontent.com/pomdtr/valtown-vscode/master/img/blobs.png)
![blob demo](https://raw.githubusercontent.com/val-town/val-town-vscode/main/img/blobs.png)

### Run SQLite Queries

![sqlite demo](https://raw.githubusercontent.com/pomdtr/valtown-vscode/master/img/sqlite.png)
![sqlite demo](https://raw.githubusercontent.com/val-town/val-town-vscode/main/img/sqlite.png)

### Quick Import Vals

![autocomplete demo](https://raw.githubusercontent.com/val-town/val-town-vscode/main/img/autocomplete.png)

## Sidebar Configuration

Expand Down
Binary file added img/autocomplete.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 31 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "valtown",
"displayName": "Val Town",
"description": "VS Code integration for val.town",
"version": "0.8.10",
"version": "0.8.12",
"publisher": "pomdtr",
"private": true,
"icon": "assets/icon.png",
Expand Down Expand Up @@ -358,6 +358,26 @@
"url": "https://api.val.town/v1/users/${user:me}/vals"
}
]
},
"valtown.autoImport.staticVersion": {
"type": "boolean",
"markdownDescription": "Use a static version when automatically importing vals, like this.\n\n| Option | Example |\n|--------|---------|\n| `false` | `import \"https://esm.town/v/steveKrouse/helloWorld\"` |\n| `true` | `import \"https://esm.town/v/steveKrouse/helloWorld?v=14\"` |\n\nThis is useful for ensuring that your code doesn't break when the val is updated.",
"default": false
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Starting to think that this should be true by default for parity with the editor.

},
"valtown.autoImport.upgradeBehavior": {
"type": "string",
"markdownDescription": "What to do when a val which has already been imported is auto-imported again with a different version.\n\n| Option | Description |\n|---------|-------------|\n| `prompt` | Prompt the user to upgrade the val. |\n| `upgrade` | Automatically upgrade the val. |\n| `nothing` | Do nothing. |",
"enum": [
"prompt",
"upgrade",
"nothing"
],
"default": "prompt"
},
"valtown.autoImport.showUpgradeNotifications": {
"type": "boolean",
"markdownDescription": "Show notifications when a val which has already been imported is auto-imported again with a different version.",
"default": true
}
}
}
Expand Down Expand Up @@ -719,7 +739,15 @@
"id": "valtown.privacy",
"label": "Set Privacy"
}
]
],
"completionItemProvider": {
"selector": [
{
"language": "typescriptreact",
"scheme": "vt+val"
}
]
}
},
"scripts": {
"vscode:prepublish": "npm run package",
Expand All @@ -739,4 +767,4 @@
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4"
}
}
}
41 changes: 39 additions & 2 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ export type Version = {
createdAt: string;
};

export type CompletionVal = {
handle: string;
name: string;
author: string;
createdAt: string;
code: string;
version: number;
exportedName: string;
};

const templates = {
http: `export default async function (req: Request): Promise<Response> {
return Response.json({ ok: true })
Expand Down Expand Up @@ -122,8 +132,12 @@ export class ValtownClient {
throw new Error("No token");
}

const { hostname } = new URL(url);
if (hostname !== "api.val.town") {
const { hostname, pathname } = new URL(url);
if (
hostname !== "api.val.town" &&
// completions are fetched from the frontend API
!(hostname === "www.val.town" && pathname.startsWith("/api/"))
) {
return fetch(url, init);
}

Expand Down Expand Up @@ -463,4 +477,27 @@ export class ValtownClient {

return res.json();
}

async autocomplete(
handle: string | undefined,
name: string | undefined
): Promise<CompletionVal[]> {
const params = new URLSearchParams({
batch: "1",
input: JSON.stringify({
"0": {
handle: handle === "me" ? (await this.user()).username : handle ?? "",
name: name || null,
},
}),
});
const url = new URL("https://www.val.town/api/trpc/autocomplete");
url.search = params.toString();
const res = await this.fetch(url, {
headers: {
"Content-Type": "application/json",
},
});
return ((await res.json()) as any)[0].result.data;
}
}
2 changes: 2 additions & 0 deletions src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as vscode from "vscode";
import { clearToken, saveToken } from "./secrets";
import { BaseVal, ValTemplate, ValtownClient } from "./client";
import importValCommand from "./importValCommand";

export function registerCommands(
context: vscode.ExtensionContext,
Expand Down Expand Up @@ -272,5 +273,6 @@ export function registerCommands(
httpEndpoint,
);
}),
vscode.commands.registerCommand("valtown.importVal", importValCommand)
);
}
95 changes: 95 additions & 0 deletions src/completions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import * as vscode from "vscode";
import { ValtownClient } from "./client";

enum ValImportKind {
Default,
Named,
SideEffect,
}

// TODO: are we hammering the API? should we try to debounce or throttle?
export function registerCompletions(
context: vscode.ExtensionContext,
client: ValtownClient
) {
context.subscriptions.push(
vscode.languages.registerCompletionItemProvider(
{ scheme: "vt+val", language: "typescriptreact" },
{
async provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position
) {
// see if we can match backwards from the cursor to find @[<handle>[/<val>]]
const atImportMatch = /@([a-zA-Z0-9]*)(?:\/([a-zA-Z0-9_]*))?$/.exec(
document.getText(
new vscode.Range(
position.line,
0,
position.line,
position.character
)
)
);

// if we can't match, don't provide any completions
if (!atImportMatch) {
return [];
}

const [atImport, typedHandle, name] = atImportMatch;
const startOfAtImport = position.translate(0, -atImport.length);

// get completions from the API
const data = await client.autocomplete(typedHandle, name);

return data.map((completionVal) => {
const { handle, name, code, version, exportedName } = completionVal;
let importKind: ValImportKind = ValImportKind.SideEffect;
if (exportedName === "default") {
importKind = ValImportKind.Default;
} else if (exportedName) {
importKind = ValImportKind.Named;
}

const snippetCompletion = new vscode.CompletionItem({
label: `@${typedHandle === "me" ? "me" : handle}/${name}`,
detail: ` ${exportedName ?? "(no export)"}`,
description: `v${version}`,
});
snippetCompletion.documentation = new vscode.MarkdownString(
"```tsx\n" + code + "\n```"
);
let insertText = "";
if (importKind === ValImportKind.Default) {
insertText = name;
} else if (importKind === ValImportKind.Named) {
insertText = exportedName;
}
snippetCompletion.insertText = insertText;
snippetCompletion.range = new vscode.Range(
startOfAtImport,
position
);
// might not be a function, but we can't tell
snippetCompletion.kind = vscode.CompletionItemKind.Function;
snippetCompletion.command = {
title: "Import Val",
command: "valtown.importVal",
arguments: [
completionVal,
// this is the range of the replaced text
new vscode.Range(
startOfAtImport,
startOfAtImport.translate(insertText.length)
),
],
};
return snippetCompletion;
});
},
}
// "@", "/"
)
);
}
3 changes: 3 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { registerCommands } from "./commands";
import { registerUriHandler } from "./uri";
import * as sqliteDoc from "./sqlite/document";
import * as definition from "./definition";
import { registerCompletions } from "./completions";

export async function activate(context: vscode.ExtensionContext) {
// set output channel
Expand Down Expand Up @@ -78,6 +79,8 @@ export async function activate(context: vscode.ExtensionContext) {
definition.register(client, context);
outputChannel.appendLine("Registering commands");
registerCommands(context, client);
outputChannel.appendLine("Registering completions");
registerCompletions(context, client);
outputChannel.appendLine("ValTown extension activated");
}

Expand Down
Loading