diff --git a/.vscode/launch.json b/.vscode/launch.json
index 991528c9..572d996e 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -20,7 +20,8 @@
"name": "Attach to Server",
"port": 9523,
"restart": true,
- "outFiles": ["${workspaceRoot}/server/out/**/*.js"]
+ "outFiles": ["${workspaceRoot}/server/out/**/*.js"],
+ "sourceMaps": true
},
{
"name": "Language Server E2E Test",
diff --git a/README.md b/README.md
index 463b711b..1af9839b 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,12 @@ Features:
+Intelligent module import enhanced
+
+
+
+
+
Supports importing ECMAScript modules

diff --git a/client/src/extension.ts b/client/src/extension.ts
index 99ab3e47..adf18a3f 100644
--- a/client/src/extension.ts
+++ b/client/src/extension.ts
@@ -32,11 +32,6 @@ import execa from "execa";
import * as semver from "semver";
import { TreeViewProvider } from "./tree_view_provider";
-import {
- ImportEnhancementCompletionProvider,
- CACHE_STATE,
-} from "./import_enhancement_provider";
-
import { ImportMap } from "../../core/import_map";
import { HashMeta } from "../../core/hash_meta";
import { isInDeno } from "../../core/deno";
@@ -118,9 +113,6 @@ export class Extension {
},
executablePath: "",
};
- // CGQAQ: ImportEnhancementCompletionProvider instance
- private import_enhancement_completion_provider = new ImportEnhancementCompletionProvider();
-
// get configuration of Deno
public getConfiguration(uri?: Uri): ConfigurationField {
const config: ConfigurationField = {};
@@ -473,14 +465,6 @@ Executable ${this.denoInfo.executablePath}`;
await window.showInformationMessage(`Copied to clipboard.`);
});
- // CGQAQ: deno._clear_import_enhencement_cache
- this.registerCommand("_clear_import_enhencement_cache", async () => {
- this.import_enhancement_completion_provider
- .clearCache()
- .then(() => window.showInformationMessage("Clear success!"))
- .catch(() => window.showErrorMessage("Clear failed!"));
- });
-
this.registerQuickFix({
_fetch_remote_module: async (editor, text) => {
const config = this.getConfiguration(editor.document.uri);
@@ -611,23 +595,6 @@ Executable ${this.denoInfo.executablePath}`;
window.registerTreeDataProvider("deno", treeView)
);
- // CGQAQ: activate import enhance feature
- this.import_enhancement_completion_provider.activate(this.context);
-
- // CGQAQ: Start caching full module list
- this.import_enhancement_completion_provider
- .cacheModList()
- .then((state) => {
- if (state === CACHE_STATE.CACHE_SUCCESS) {
- window.showInformationMessage(
- "deno.land/x module list cached successfully!"
- );
- }
- })
- .catch(() =>
- window.showErrorMessage("deno.land/x module list failed to cache!")
- );
-
this.sync(window.activeTextEditor?.document);
const extension = extensions.getExtension(this.id);
@@ -640,8 +607,6 @@ Executable ${this.denoInfo.executablePath}`;
public async deactivate(context: ExtensionContext): Promise {
this.context = context;
- this.import_enhancement_completion_provider.dispose();
-
if (this.client) {
await this.client.stop();
this.client = undefined;
diff --git a/client/src/import_enhancement_provider.ts b/client/src/import_enhancement_provider.ts
deleted file mode 100644
index f18a0df9..00000000
--- a/client/src/import_enhancement_provider.ts
+++ /dev/null
@@ -1,255 +0,0 @@
-import {
- CompletionItemProvider,
- TextDocument,
- Position,
- CompletionItem,
- Disposable,
- CompletionItemKind,
- CompletionList,
- DocumentSelector,
- languages,
- ExtensionContext,
- Range,
- Command,
- window,
- ProgressLocation,
-} from "vscode";
-
-import Semver from "semver";
-
-import VC = require("vscode-cache");
-
-import {
- listVersionsOfMod,
- modTreeOf,
- parseImportStatement,
- searchX,
- fetchModList,
- ModuleInfo,
-} from "./import_utils";
-
-export enum CACHE_STATE {
- ALREADY_CACHED,
- CACHE_SUCCESS,
-}
-
-export class ImportEnhancementCompletionProvider
- implements CompletionItemProvider, Disposable {
- vc?: VC;
- async provideCompletionItems(
- document: TextDocument,
- position: Position
- // _token: CancellationToken,
- // _context: CompletionContext
- ): Promise {
- const line_text = document.lineAt(position).text;
-
- if (/import.+?from\W+['"].*?['"]/.test(line_text)) {
- // We're at import statement line
- const imp_info = parseImportStatement(line_text);
- if (imp_info?.domain !== "deno.land") {
- return undefined;
- }
- // We'll handle the completion only if the domain is `deno.land` and mod name is not empty
- const at_index = line_text.indexOf("@");
- if (
- /.*?deno.land\/(x\/)?\w+@[\w.-]*$/.test(
- line_text.substring(0, position.character)
- ) &&
- position.character > at_index
- ) {
- // Version completion
- const vers = await listVersionsOfMod(imp_info.module);
-
- const result = vers.versions
- .sort((a, b) => {
- const av = Semver.clean(a);
- const bv = Semver.clean(b);
- if (
- av === null ||
- bv === null ||
- !Semver.valid(av) ||
- !Semver.valid(bv)
- ) {
- return 0;
- }
- return Semver.gt(av, bv) ? -1 : 1;
- })
- .map((it, i) => {
- // let latest version on top
- const ci = new CompletionItem(it, CompletionItemKind.Value);
- ci.sortText = `a${String.fromCharCode(i) + 1}`;
- ci.filterText = it;
- ci.range = new Range(
- position.line,
- at_index + 1,
- position.line,
- position.character
- );
- return ci;
- });
- return new CompletionList(result);
- }
-
- if (
- /.*?deno\.land\/x\/\w*$/.test(
- line_text.substring(line_text.indexOf("'") + 1, position.character)
- )
- ) {
- // x module name completion
- if (this.vc !== undefined) {
- const result: { name: string; description: string }[] = await searchX(
- this.vc,
- imp_info.module
- );
- const r = result.map((it) => {
- const ci = new CompletionItem(it.name, CompletionItemKind.Module);
- ci.detail = it.description;
- ci.sortText = String.fromCharCode(1);
- ci.filterText = it.name;
- return ci;
- });
- return r;
- } else {
- return [];
- }
- }
-
- if (
- !/.*?deno\.land\/(x\/)?\w+(@[\w.-]*)?\//.test(
- line_text.substring(0, position.character)
- )
- ) {
- return [];
- }
-
- const result = await modTreeOf(
- this.vc,
- imp_info.module,
- imp_info.version
- );
- const arr_path = imp_info.path.split("/");
- const path = arr_path.slice(0, arr_path.length - 1).join("/") + "/";
-
- const r = result.directory_listing
- .filter((it) => it.path.startsWith(path))
- .map((it) => ({
- path:
- path.length > 1 ? it.path.replace(path, "") : it.path.substring(1),
- size: it.size,
- type: it.type,
- }))
- .filter((it) => it.path.split("/").length < 2)
- .filter(
- (it) =>
- // exclude tests
- !(it.path.endsWith("_test.ts") || it.path.endsWith("_test.js")) &&
- // include only js and ts
- (it.path.endsWith(".ts") ||
- it.path.endsWith(".js") ||
- it.path.endsWith(".tsx") ||
- it.path.endsWith(".jsx") ||
- it.path.endsWith(".mjs") ||
- it.type !== "file") &&
- // exclude privates
- !it.path.startsWith("_") &&
- // exclude hidden file/folder
- !it.path.startsWith(".") &&
- // exclude testdata dir
- (it.path !== "testdata" || it.type !== "dir") &&
- it.path.length !== 0
- )
- // .sort((a, b) => a.path.length - b.path.length)
- .map((it) => {
- const r = new CompletionItem(
- it.path,
- it.type === "dir"
- ? CompletionItemKind.Folder
- : CompletionItemKind.File
- );
- r.sortText = it.type === "dir" ? "a" : "b";
- r.insertText = it.type === "dir" ? it.path + "/" : it.path;
- r.range = new Range(
- position.line,
- line_text.substring(0, position.character).lastIndexOf("/") + 1,
- position.line,
- position.character
- );
- if (it.type === "dir") {
- // https://github.com/microsoft/vscode-extension-samples/blob/bb4a0c3a5dd9460a5cd64290b4d5c4f6bd79bdc4/completions-sample/src/extension.ts#L37
- r.command = {
- command: "editor.action.triggerSuggest",
- title: "Re-trigger completions...",
- };
- }
- return r;
- });
- return new CompletionList(r, false);
- }
- }
-
- async clearCache(): Promise {
- await this.vc?.flush();
- }
-
- async cacheModList(): Promise {
- return window.withProgress(
- {
- location: ProgressLocation.Notification,
- title: "Fetching deno.land/x module list...",
- },
- async (progress) => {
- const mod_list_key = "mod_list";
- if (this.vc?.isExpired(mod_list_key) || !this.vc?.has(mod_list_key)) {
- this.vc?.forget(mod_list_key);
- progress.report({ increment: 0 });
- for await (const modules of fetchModList()) {
- if (this.vc?.has(mod_list_key)) {
- const list_in_cache = this.vc?.get(mod_list_key) as ModuleInfo[];
- list_in_cache.push(...modules.data);
- await this.vc?.put(
- mod_list_key,
- list_in_cache,
- 60 * 60 * 24 * 7 /* expiration in a week */
- );
- } else {
- this.vc?.put(
- mod_list_key,
- modules.data,
- 60 * 60 * 24 * 7 /* expiration in a week */
- );
- }
- progress.report({
- increment: (1 / modules.total) * 100,
- });
- }
- return CACHE_STATE.CACHE_SUCCESS;
- }
- return CACHE_STATE.ALREADY_CACHED;
- }
- );
- }
-
- activate(ctx: ExtensionContext): void {
- this.vc = new VC(ctx, "import-enhanced");
-
- const document_selector = [
- { language: "javascript" },
- { language: "typescript" },
- { language: "javascriptreact" },
- { language: "typescriptreact" },
- ];
- const trigger_word = ["@", "/"];
- ctx.subscriptions.push(
- languages.registerCompletionItemProvider(
- document_selector,
- this,
- ...trigger_word
- )
- );
- }
-
- dispose(): void {
- /* eslint-disable */
- }
-}
diff --git a/client/src/types/vscode-cache.d.ts b/client/src/types/vscode-cache.d.ts
deleted file mode 100644
index 48279d11..00000000
--- a/client/src/types/vscode-cache.d.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-/* eslint-disable */
-declare module "vscode-cache" {
- export = Cache;
- class Cache {
- constructor(context: any, namespace?: string);
- put(key: string, value: any, expiration?: number): Promise;
- set(key: any, value: any, expiration: any): Promise;
- save(key: any, value: any, expiration: any): Promise;
- store(key: any, value: any, expiration: any): Promise;
- cache: (key: any, value: any, expiration: any) => Promise;
- get(key: string, defaultValue?: any): any;
- fetch(key: any, defaultValue: any): any;
- retrieve(key: any, defaultValue: any): any;
- has(key: string): boolean;
- exists(key: any): boolean;
- forget(key: string): any;
- remove(key: any): any;
- delete(key: any): any;
- keys(): string[];
- all(): object;
- getAll(): object;
- flush(): any;
- clearAll(): any;
- getExpiration(key: string): number;
- isExpired(key: any): boolean;
- }
-}
diff --git a/client/src/import_utils.test.ts b/core/import_enhanced.test.ts
similarity index 69%
rename from client/src/import_utils.test.ts
rename to core/import_enhanced.test.ts
index 3f92cc35..4d339a67 100644
--- a/client/src/import_utils.test.ts
+++ b/core/import_enhanced.test.ts
@@ -2,21 +2,48 @@ import {
listVersionsOfMod,
modTreeOf,
parseImportStatement,
-} from "./import_utils";
+ ModTree,
+ VERSION_REG,
+} from "./import_enhanced";
-test("import-enhance listVersionsOfMod", async () => {
+import { PermCache } from "./permcache";
+
+test("core / import_enhance: test VERSION_REG", async () => {
+ const test_cases: { text: string; result: boolean }[] = [
+ { text: "1.0.0", result: true },
+ { text: "1.0.0@", result: false },
+ { text: "1.0.0嗯?", result: false },
+ { text: "1.0.0-alpha", result: true },
+ { text: "1.0.0-beta_1", result: true },
+ { text: "v1", result: true },
+ { text: "v1/", result: false },
+ { text: "/v1", result: false },
+ ];
+
+ for (const test_case of test_cases) {
+ expect(VERSION_REG.test(test_case.text)).toEqual(test_case.result);
+ }
+});
+
+test("core / import_enhance: listVersionsOfMod", async () => {
const result = await listVersionsOfMod("std");
expect(result.latest).toBeTruthy();
expect(result.versions.length).not.toEqual(0);
});
-test("import-enhance modTreeOf", async () => {
- const result = await modTreeOf(undefined, "std");
+test("core / import_enhance: modTreeOf", async () => {
+ const cache = await PermCache.create>(
+ "import_enhance-test",
+ undefined
+ );
+ await cache.destroy_cache();
+ const result = await modTreeOf("std", undefined, cache);
expect(result.uploaded_at).toBeTruthy();
expect(result.directory_listing.length).not.toEqual(0);
+ await cache.destroy_cache();
});
-test("import-enhance parseImportStatement", async () => {
+test("core / import_enhance: parseImportStatement", async () => {
const test_cases = [
{
imp: "import * from 'http://a.c/xx/a.ts'",
diff --git a/client/src/import_utils.ts b/core/import_enhanced.ts
similarity index 75%
rename from client/src/import_utils.ts
rename to core/import_enhanced.ts
index 8c75cd43..ed2359c6 100644
--- a/client/src/import_utils.ts
+++ b/core/import_enhanced.ts
@@ -1,9 +1,9 @@
-/* eslint @typescript-eslint/triple-slash-reference: "off" */
-// CGQAQ: Without next line the test will fail, using import won't work
-///
-
import got from "got";
-import VC from "vscode-cache";
+import { PermCache } from "./permcache";
+
+export type ModList = ModuleInfo[];
+
+export type ModListCache = PermCache;
export interface ModuleInfo {
name: string;
@@ -44,12 +44,12 @@ export async function* fetchModList(): AsyncGenerator<{
// this function now is search from cache only
export async function searchX(
- cache: VC,
+ cache: PermCache,
keyword: string
-): Promise {
- if (cache.has("mod_list")) {
- const buff = cache.get("mod_list") as ModuleInfo[];
- return buff
+): Promise {
+ const arr = cache.get();
+ if (arr !== undefined) {
+ return arr
.filter((it) => it.name.startsWith(keyword))
.sort((a, b) => b.search_score - a.search_score);
} else {
@@ -79,14 +79,18 @@ interface ModTreeItem {
type: string;
}
-interface ModTree {
+export interface ModTree {
uploaded_at: string; // Use this to update cache
directory_listing: ModTreeItem[];
}
+
+export type ModTreeCacheItem = Record;
+export type ModTreeCache = PermCache;
+
export async function modTreeOf(
- vc: VC | undefined,
module_name: string,
- version = "latest"
+ version = "latest",
+ cache?: PermCache>
): Promise {
// https://cdn.deno.land/$MODULE/versions/$VERSION/meta/meta.json
let ver = version;
@@ -95,10 +99,11 @@ export async function modTreeOf(
ver = vers.latest;
}
- const cache_key = `mod_tree:${module_name}@${ver}`;
- if (vc?.has(cache_key)) {
+ const cache_key = `${module_name}@${ver}`;
+ const cache_content = cache?.get();
+ if (cache_content?.hasOwnProperty(cache_key)) {
// use cache
- return vc.get(cache_key) as ModTree;
+ return cache_content[cache_key] as ModTree;
}
const response: ModTree = await got(
@@ -108,7 +113,14 @@ export async function modTreeOf(
).json();
// cache it
- vc?.put(cache_key, response);
+ if (cache_content !== undefined) {
+ cache_content[cache_key] = response;
+ await cache?.set(cache_content);
+ } else {
+ const obj: Record = {};
+ obj[cache_key] = response;
+ await cache?.set(obj);
+ }
return response;
}
@@ -120,8 +132,12 @@ interface ImportUrlInfo {
path: string;
}
+export const IMP_REG = /^.*?import.+?from.+?['"](?[0-9a-zA-Z-_@~:/.?#:&=%+]*)/;
+export const VERSION_REG = /^([\w.\-_]+)$/;
+export const MOD_NAME_REG = /^[\w-_]+$/;
+
export function parseImportStatement(text: string): ImportUrlInfo | undefined {
- const reg_groups = text.match(/.*?['"](?.*?)['"]/)?.groups;
+ const reg_groups = text.match(IMP_REG)?.groups;
if (!reg_groups) {
return undefined;
}
@@ -153,7 +169,7 @@ export function parseImportStatement(text: string): ImportUrlInfo | undefined {
};
}
};
- if (components.length > 2) {
+ if (components.length > 1) {
const m = components[1];
if (m === "x") {
return parse(components.slice(2, components.length));
diff --git a/core/permcache.test.ts b/core/permcache.test.ts
new file mode 100644
index 00000000..a946aba9
--- /dev/null
+++ b/core/permcache.test.ts
@@ -0,0 +1,52 @@
+import { PermCache, TRANSACTION_STATE } from "./permcache";
+import { sleep } from "./util";
+
+test("core / permcache", async () => {
+ const data = [1, 2];
+ const cache = await PermCache.create("permcache-test", 1);
+
+ // test expired
+ await sleep(500);
+ expect(cache.expired()).toBeFalsy();
+ await sleep(600);
+ expect(cache.expired()).toBeTruthy();
+
+ expect(cache.get()).toEqual(undefined);
+ await cache.set(data);
+ expect(cache.get()).toEqual(data);
+ expect(await cache.reload_get()).toEqual(data);
+
+ // test transaction
+ expect(cache.transaction_begin()).toEqual(TRANSACTION_STATE.SUCCESS);
+ expect(cache.transaction_begin()).toEqual(
+ TRANSACTION_STATE.ALREADY_IN_TRANSACTION
+ );
+ cache
+ .set([2, 3])
+ .then(() => fail())
+ .catch((reason) =>
+ expect(reason).toEqual(TRANSACTION_STATE.ALREADY_IN_TRANSACTION)
+ );
+ expect(cache.transaction_set([2, 3])).toEqual(TRANSACTION_STATE.SUCCESS);
+ expect(cache.get()).toEqual(data);
+ expect(cache.transaction_get().state).toEqual(TRANSACTION_STATE.SUCCESS);
+ expect(cache.transaction_get().data).toEqual([2, 3]);
+ expect(await cache.transaction_commit()).toEqual(TRANSACTION_STATE.SUCCESS);
+ expect(cache.get()).toEqual([2, 3]);
+ expect(await cache.reload_get()).toEqual([2, 3]);
+
+ // test transaction_abort
+ expect(cache.transaction_begin()).toEqual(TRANSACTION_STATE.SUCCESS);
+ expect(cache.transaction_set([1, 2]));
+ expect(cache.transaction_abort()).toEqual(TRANSACTION_STATE.SUCCESS);
+ expect(cache.transaction_abort()).toEqual(
+ TRANSACTION_STATE.NOT_IN_TRANSACTION
+ );
+ expect(cache.get()).toEqual([2, 3]);
+ expect(await cache.reload_get()).toEqual([2, 3]);
+
+ // test destroy_cache
+ await cache.destroy_cache();
+ expect(cache.get()).toEqual(undefined);
+ expect(await cache.reload_get()).toEqual(undefined);
+});
diff --git a/core/permcache.ts b/core/permcache.ts
new file mode 100644
index 00000000..dcccbcad
--- /dev/null
+++ b/core/permcache.ts
@@ -0,0 +1,232 @@
+// Permanent version of the Cache from cache.ts in the same folder
+import { promises as fsp } from "fs";
+import fs from "fs";
+import path from "path";
+
+export function getVSCodeDenoDir(): string {
+ // ref https://deno.land/manual.html
+ // On Linux/Redox: $XDG_CACHE_HOME/deno or $HOME/.cache/deno
+ // On Windows: %LOCALAPPDATA%/deno (%LOCALAPPDATA% = FOLDERID_LocalAppData)
+ // On macOS: $HOME/Library/Caches/deno
+ // If something fails, it falls back to $HOME/.deno
+ let vscodeDenoDir =
+ process.env.VSCODE_DENO_EXTENSION_PATH !== undefined
+ ? path.join(process.env.VSCODE_DENO_EXTENSION_PATH, "cache")
+ : undefined;
+ if (vscodeDenoDir === undefined) {
+ switch (process.platform) {
+ /* istanbul ignore next */
+ case "win32":
+ vscodeDenoDir = `${process.env.LOCALAPPDATA}\\vscode_deno`;
+ break;
+ /* istanbul ignore next */
+ case "darwin":
+ vscodeDenoDir = `${process.env.HOME}/Library/Caches/vscode_deno`;
+ break;
+ /* istanbul ignore next */
+ case "linux":
+ vscodeDenoDir = process.env.XDG_CACHE_HOME
+ ? `${process.env.XDG_CACHE_HOME}/vscode_deno`
+ : `${process.env.HOME}/.cache/vscode_deno`;
+ break;
+ /* istanbul ignore next */
+ default:
+ vscodeDenoDir = `${process.env.HOME}/.vscode_deno`;
+ }
+ }
+
+ return vscodeDenoDir;
+}
+
+interface CacheFormat {
+ expiring_date: number | undefined;
+ data: T | undefined;
+}
+
+export enum TRANSACTION_STATE {
+ SUCCESS = 1,
+ ALREADY_IN_TRANSACTION = -1,
+ NOT_IN_TRANSACTION = -2,
+}
+
+export class PermCache {
+ private cache_file_path: string;
+
+ // In transition mode, write to this instead
+ private transaction: CacheFormat | undefined;
+
+ private inner_data: CacheFormat | undefined;
+
+ static async create(
+ namespace: string,
+ timeout?: number
+ ): Promise> {
+ const vscode_deno_path = getVSCodeDenoDir();
+ const cache_file_path = path.join(vscode_deno_path, `${namespace}.json`);
+ const expiring_date =
+ timeout === undefined ? undefined : new Date().getTime() + timeout * 1000; // to millis
+ if (!fs.existsSync(vscode_deno_path)) {
+ await fsp.mkdir(vscode_deno_path, { recursive: true });
+ }
+
+ let cache_file: fsp.FileHandle;
+ if (!fs.existsSync(cache_file_path)) {
+ // Cache don't exist, create one
+ cache_file = await fsp.open(cache_file_path, "w");
+
+ const cache = { expiring_date, data: undefined } as CacheFormat;
+ await cache_file.writeFile(JSON.stringify(cache));
+ await cache_file.close();
+
+ return new PermCache(cache_file_path, cache);
+ } else {
+ // Cache maybe exist, try read cache from cache file
+ const file_content = await fsp.readFile(cache_file_path, "utf-8");
+ const cache = JSON.parse(file_content) as CacheFormat;
+
+ if (
+ !Object.getOwnPropertyNames(cache).every(
+ (it) => it === "expiring_date" || it === "data"
+ )
+ ) {
+ // If cache format not correct, clear the file
+ await fsp.writeFile(cache_file_path, "");
+ return new PermCache(cache_file_path, {
+ expiring_date,
+ data: undefined,
+ });
+ }
+
+ if (cache.data == undefined) {
+ return new PermCache(cache_file_path, {
+ expiring_date: cache.expiring_date,
+ data: undefined,
+ });
+ }
+
+ return new PermCache(cache_file_path, cache);
+ }
+ }
+
+ private constructor(file: string, cache: CacheFormat | undefined) {
+ this.transaction = undefined;
+ this.cache_file_path = file;
+ this.inner_data = cache;
+ }
+
+ async reload(): Promise {
+ if (fs.existsSync(this.cache_file_path)) {
+ const cache_file_content = await fsp.readFile(
+ this.cache_file_path,
+ "utf-8"
+ );
+ this.inner_data = JSON.parse(cache_file_content) as CacheFormat;
+ return;
+ }
+ this.inner_data = undefined;
+ }
+
+ get(): T | undefined {
+ return this.inner_data?.data;
+ }
+
+ expired(): boolean {
+ const now = new Date().getTime();
+ if (
+ this.inner_data !== undefined &&
+ this.inner_data.expiring_date !== undefined
+ ) {
+ return now >= this.inner_data.expiring_date;
+ }
+ return false;
+ }
+
+ async reload_expired(): Promise {
+ await this.reload();
+ return this.expired();
+ }
+
+ async reload_get(): Promise {
+ await this.reload();
+ return this.inner_data?.data;
+ }
+
+ async set(data: T | undefined): Promise {
+ if (this.transaction !== undefined) {
+ return Promise.reject(TRANSACTION_STATE.ALREADY_IN_TRANSACTION);
+ }
+ if (this.inner_data !== undefined) {
+ this.inner_data.data = data;
+ await fsp.writeFile(
+ this.cache_file_path,
+ JSON.stringify(this.inner_data),
+ "utf-8"
+ );
+ } else {
+ this.inner_data = { expiring_date: undefined, data };
+ await fsp.writeFile(
+ this.cache_file_path,
+ JSON.stringify(this.inner_data),
+ "utf-8"
+ );
+ }
+ }
+
+ // WARNING: this function will completely delete the cache from disk
+ // AND MUST await on this function
+ async destroy_cache(): Promise {
+ if (fs.existsSync(this.cache_file_path)) {
+ await fsp.unlink(this.cache_file_path);
+ this.transaction = undefined;
+ this.inner_data = undefined;
+ }
+ }
+
+ transaction_begin(): TRANSACTION_STATE {
+ if (this.transaction !== undefined) {
+ return TRANSACTION_STATE.ALREADY_IN_TRANSACTION;
+ }
+ this.transaction = {
+ expiring_date: undefined,
+ data: undefined,
+ ...this.inner_data,
+ } as CacheFormat;
+ return TRANSACTION_STATE.SUCCESS;
+ }
+
+ transaction_get(): { state: TRANSACTION_STATE; data: T | undefined } {
+ if (this.transaction !== undefined) {
+ return { state: TRANSACTION_STATE.SUCCESS, data: this.transaction.data };
+ }
+ return { state: TRANSACTION_STATE.NOT_IN_TRANSACTION, data: undefined };
+ }
+
+ transaction_set(data: T | undefined): TRANSACTION_STATE {
+ if (this.transaction !== undefined) {
+ this.transaction.data = data;
+ return TRANSACTION_STATE.SUCCESS;
+ }
+ return TRANSACTION_STATE.NOT_IN_TRANSACTION;
+ }
+
+ transaction_abort(): TRANSACTION_STATE {
+ if (this.transaction !== undefined) {
+ this.transaction = undefined;
+ return TRANSACTION_STATE.SUCCESS;
+ }
+ return TRANSACTION_STATE.NOT_IN_TRANSACTION;
+ }
+
+ async transaction_commit(): Promise {
+ if (this.transaction !== undefined) {
+ await fsp.writeFile(
+ this.cache_file_path,
+ JSON.stringify(this.transaction)
+ );
+ await this.reload_get();
+ this.transaction = undefined;
+ return TRANSACTION_STATE.SUCCESS;
+ }
+ return TRANSACTION_STATE.NOT_IN_TRANSACTION;
+ }
+}
diff --git a/package.json b/package.json
index 770d2710..d5eea00f 100644
--- a/package.json
+++ b/package.json
@@ -169,7 +169,6 @@
"format": "prettier \"**/*.md\" \"**/*.json\" \"**/*.ts\" \"**/*.yml\" --config ./.prettierrc.json --write",
"test": "jest --coverage",
"test-coveralls": "jest --coverage --coverageReporters=text-lcov | coveralls",
- "test.import-enhance": "jest -t 'import-enhance'",
"build": "npx vsce package -o ./vscode-deno.vsix",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
},
@@ -198,7 +197,6 @@
"got": "^11.5.2",
"json5": "^2.1.3",
"semver": "7.3.2",
- "typescript-deno-plugin": "./typescript-deno-plugin",
- "vscode-cache": "^0.3.0"
+ "typescript-deno-plugin": "./typescript-deno-plugin"
}
}
diff --git a/screenshot/import_enhancement.gif b/screenshot/import_enhancement.gif
new file mode 100644
index 00000000..1d42063d
Binary files /dev/null and b/screenshot/import_enhancement.gif differ
diff --git a/server/src/language/completion.ts b/server/src/language/completion.ts
index 3ca5d2b8..03843b36 100644
--- a/server/src/language/completion.ts
+++ b/server/src/language/completion.ts
@@ -12,6 +12,8 @@ import { getDenoDir } from "../../../core/deno";
import { getAllDenoCachedDeps, Deps } from "../../../core/deno_deps";
import { Cache } from "../../../core/cache";
+import { ImportCompletionEnhanced } from "./import_completion_enhanced";
+
// Cache for 30 second or 30 references
const cache = Cache.create(1000 * 30, 30);
@@ -24,7 +26,11 @@ getAllDenoCachedDeps()
});
export class Completion {
- constructor(connection: IConnection, documents: TextDocuments) {
+ constructor(
+ connection: IConnection,
+ documents: TextDocuments,
+ import_enhanced: ImportCompletionEnhanced
+ ) {
connection.onCompletion(async (params) => {
const { position, partialResultToken, textDocument } = params;
@@ -39,7 +45,7 @@ export class Completion {
);
const IMPORT_REG = /import\s['"][a-zA-Z._-]$/;
- const IMPORT_FROM_REG = /import\s(([^\s]*)|(\*\sas\s[^\s]*))\sfrom\s['"][a-zA-Z._-]$/;
+ const IMPORT_FROM_REG = /import\s(([^\s]*)|(\*\sas\s[^\s]*))\sfrom\s['"][a-zA-Z._-]?$/;
const DYNAMIC_REG = /import\s*\(['"][a-zA-Z._-]['"]?$/;
const isImport =
@@ -51,7 +57,7 @@ export class Completion {
currentLine.length > 1000 || // if is a large file
!isImport
) {
- return [];
+ return import_enhanced.please(params);
}
let deps = cache.get();
@@ -66,12 +72,34 @@ export class Completion {
position
);
+ if (/.*?import[^'"]*?'$/.test(currentLine)) {
+ deps = deps.map((it) => {
+ const url = new URL(it.url);
+ return {
+ filepath: it.filepath,
+ url: url.hostname === "deno.land" ? `${url.origin}` : it.url,
+ } as Deps;
+ });
+ const dedup_arr: string[] = [];
+ deps = deps.filter((it) => {
+ if (dedup_arr.includes(it.url)) {
+ return false;
+ } else {
+ dedup_arr.push(it.url);
+ return true;
+ }
+ });
+ }
+
const completes: CompletionItem[] = deps.map((dep) => {
return {
label: dep.url,
detail: dep.url,
sortText: dep.url,
- documentation: dep.filepath.replace(getDenoDir(), "$DENO_DIR"),
+ documentation:
+ dep.url === "https://deno.land"
+ ? ""
+ : dep.filepath.replace(getDenoDir(), "$DENO_DIR"),
kind: CompletionItemKind.File,
insertText: dep.url,
cancel: partialResultToken,
@@ -79,6 +107,8 @@ export class Completion {
} as CompletionItem;
});
+ completes.push(...(await import_enhanced.please(params)).items);
+
return completes;
});
}
diff --git a/server/src/language/import_completion_enhanced.ts b/server/src/language/import_completion_enhanced.ts
new file mode 100644
index 00000000..8d8d6658
--- /dev/null
+++ b/server/src/language/import_completion_enhanced.ts
@@ -0,0 +1,252 @@
+import {
+ IConnection,
+ TextDocument,
+ TextDocuments,
+ Range,
+ CompletionItem,
+ CompletionList,
+ CompletionItemKind,
+ CompletionParams,
+} from "vscode-languageserver";
+
+import semver from "semver";
+
+import {
+ parseImportStatement,
+ // listVersionsOfMod,
+ searchX,
+ modTreeOf,
+ fetchModList,
+ ModList,
+ IMP_REG,
+ VERSION_REG,
+ listVersionsOfMod,
+ ModListCache,
+ ModTreeCache,
+ ModTreeCacheItem,
+} from "../../../core/import_enhanced";
+
+import { PermCache, TRANSACTION_STATE } from "../../../core/permcache";
+
+export enum CACHE_STATE {
+ ALREADY_CACHED = 1,
+ CACHE_SUCCESS = 0,
+ UNKNOWN_ERROR = -1,
+}
+
+export class ImportCompletionEnhanced {
+ mod_tree_cache?: ModTreeCache;
+ mod_list_cache?: ModListCache;
+
+ private connection: IConnection;
+ private documents: TextDocuments;
+
+ constructor(connection: IConnection, documents: TextDocuments) {
+ this.connection = connection;
+ this.documents = documents;
+ }
+
+ async please(param: CompletionParams): Promise {
+ if (!this.mod_list_cache) {
+ this.mod_list_cache = await PermCache.create(
+ "mod_list",
+ 60 * 60 * 24 /* expiring in a day */
+ );
+ }
+ if (!this.mod_tree_cache) {
+ this.mod_tree_cache = await PermCache.create(
+ "mod_tree"
+ );
+ }
+ const { textDocument, position } = param;
+ const doc = this.documents.get(textDocument.uri);
+ if (typeof doc !== "undefined") {
+ const current_line_text = doc.getText(
+ Range.create(position.line, 0, position.line, position.character)
+ );
+
+ if (IMP_REG.test(current_line_text)) {
+ // We're at import statement line
+ const imp_info = parseImportStatement(current_line_text);
+ if (imp_info?.domain !== "deno.land") {
+ return CompletionList.create();
+ }
+
+ const index_of_at_symbol = current_line_text.indexOf("@");
+ if (index_of_at_symbol !== -1) {
+ const maybe_version = current_line_text.substring(
+ index_of_at_symbol + 1
+ );
+ if (
+ (VERSION_REG.test(maybe_version) || maybe_version.length === 0) &&
+ position.character > index_of_at_symbol
+ ) {
+ const vers = await listVersionsOfMod(imp_info.module);
+ const result = vers.versions
+ .sort((a, b) => {
+ const av = semver.clean(a);
+ const bv = semver.clean(b);
+ if (
+ av === null ||
+ bv === null ||
+ !semver.valid(av) ||
+ !semver.valid(bv)
+ ) {
+ return 0;
+ }
+ return semver.gt(av, bv) ? -1 : 1;
+ })
+ .map((it, i) => {
+ const ci = CompletionItem.create(it);
+ ci.sortText = `a${String.fromCharCode(i) + 1}`;
+ ci.filterText = it;
+ ci.kind = CompletionItemKind.Value;
+ return ci;
+ });
+ return CompletionList.create(result);
+ }
+ }
+
+ if (/.*deno.land\/$/.test(current_line_text)) {
+ // x or std
+ return CompletionList.create([
+ {
+ label: "std",
+ insertText: "std",
+ kind: CompletionItemKind.Module,
+ },
+ { label: "x", insertText: "x/", kind: CompletionItemKind.Module },
+ ]);
+ }
+
+ if (/.*deno.land\/x\/([\w-_]+)?$/.test(current_line_text)) {
+ // x modules
+ if (this.mod_list_cache !== undefined) {
+ const result = await searchX(this.mod_list_cache, imp_info.module);
+ const r = result.map((it) => {
+ const ci = CompletionItem.create(it.name);
+ ci.kind = CompletionItemKind.Module;
+ ci.detail = it.description;
+ ci.insertText = `${it.name}@`;
+ ci.sortText = String.fromCharCode(1);
+ ci.filterText = it.name;
+ ci.command = {
+ command: "editor.action.triggerSuggest",
+ title: "Re-trigger completions...",
+ };
+ return ci;
+ });
+ return CompletionList.create(r);
+ } else {
+ return CompletionList.create();
+ }
+ }
+
+ if (/.*deno.land(\/x)?\/.+?(@.+)?\//.test(current_line_text)) {
+ // modules tree completion
+ const result = await modTreeOf(
+ imp_info.module,
+ imp_info.version,
+ this.mod_tree_cache
+ );
+ const arr_path = imp_info.path.split("/");
+ const path = arr_path.slice(0, arr_path.length - 1).join("/") + "/";
+ return CompletionList.create(
+ result.directory_listing
+ .filter((it) => it.path.startsWith(path))
+ .map((it) => ({
+ path:
+ path.length > 1
+ ? it.path.replace(path, "")
+ : it.path.substring(1),
+ size: it.size,
+ type: it.type,
+ }))
+ .filter((it) => it.path.split("/").length < 2)
+ .filter(
+ (it) =>
+ !(
+ it.path.endsWith("_test.ts") || it.path.endsWith("_test.js")
+ ) &&
+ (it.path.endsWith(".ts") ||
+ it.path.endsWith(".js") ||
+ it.path.endsWith(".tsx") ||
+ it.path.endsWith(".jsx") ||
+ it.path.endsWith(".mjs") ||
+ it.type !== "file") &&
+ !it.path.startsWith("_") &&
+ !it.path.startsWith(".") &&
+ (it.path !== "testdata" || it.type !== "dir") &&
+ it.path.length !== 0
+ )
+ .map((it) => {
+ const r = CompletionItem.create(it.path);
+ r.kind =
+ it.type === "dir"
+ ? CompletionItemKind.Folder
+ : CompletionItemKind.File;
+ r.sortText = it.type === "dir" ? "a" : "b";
+ r.insertText = it.type === "dir" ? it.path + "/" : it.path;
+ if (it.type === "dir") {
+ r.command = {
+ command: "editor.action.triggerSuggest",
+ title: "Re-trigger completions...",
+ };
+ }
+ return r;
+ })
+ );
+ }
+ return CompletionList.create();
+ }
+ }
+ return CompletionList.create();
+ }
+
+ async clearCache(): Promise {
+ await this.mod_list_cache?.destroy_cache();
+ await this.mod_tree_cache?.destroy_cache();
+ }
+
+ async cacheModList(): Promise {
+ const progress = await this.connection.window.createWorkDoneProgress();
+ progress.begin("Fetching deno.land/x module list...", 0);
+ if (!this.mod_list_cache) {
+ this.mod_list_cache = await PermCache.create(
+ "mod_list",
+ 60 * 60 * 24 /* expiring in a day */
+ );
+ }
+
+ if (
+ this.mod_list_cache.expired() ||
+ !this.mod_list_cache.get() ||
+ this.mod_list_cache.get()?.length === 0
+ ) {
+ await this.mod_list_cache.set([]);
+ progress.report(0);
+ if (
+ this.mod_list_cache.transaction_begin() === TRANSACTION_STATE.SUCCESS
+ ) {
+ for await (const modules of fetchModList()) {
+ const list_in_cache = this.mod_list_cache.transaction_get()
+ .data as ModList;
+ list_in_cache.push(...modules.data);
+ this.mod_list_cache.transaction_set(list_in_cache);
+ progress.report((modules.current / modules.total) * 100);
+ }
+ if (
+ (await this.mod_list_cache.transaction_commit()) ===
+ TRANSACTION_STATE.SUCCESS
+ ) {
+ progress.done();
+ return CACHE_STATE.CACHE_SUCCESS;
+ }
+ }
+ progress.done();
+ return CACHE_STATE.UNKNOWN_ERROR;
+ }
+ progress.done();
+ return CACHE_STATE.ALREADY_CACHED;
+ }
+}
diff --git a/server/src/server.ts b/server/src/server.ts
index 350321ef..d211f501 100644
--- a/server/src/server.ts
+++ b/server/src/server.ts
@@ -9,6 +9,7 @@ import {
InitializeResult,
TextDocumentSyncKind,
CodeActionKind,
+ ExecuteCommandParams,
} from "vscode-languageserver";
import { TextDocument } from "vscode-languageserver-textdocument";
@@ -23,6 +24,10 @@ import { DocumentFormatting } from "./language/document_formatting";
import { Hover } from "./language/hover";
import { Completion } from "./language/completion";
import { CodeLens } from "./language/code_lens";
+import {
+ ImportCompletionEnhanced,
+ CACHE_STATE,
+} from "./language/import_completion_enhanced";
import { getDenoDir, getDenoDts } from "../../core/deno";
import { pathExists } from "../../core/util";
@@ -42,6 +47,7 @@ const connection: IConnection = createConnection(
const documents = new TextDocuments(TextDocument);
const bridge = new Bridge(connection);
+const import_enhanced = new ImportCompletionEnhanced(connection, documents);
new DependencyTree(connection, bridge);
new Diagnostics(SERVER_NAME, connection, bridge, documents);
new Definition(connection, documents);
@@ -49,11 +55,9 @@ new References(connection, documents);
new DocumentHighlight(connection, documents);
new DocumentFormatting(connection, documents, bridge);
new Hover(connection, documents);
-new Completion(connection, documents);
+new Completion(connection, documents, import_enhanced);
new CodeLens(connection, documents);
-connection;
-
connection.onInitialize(
(): InitializeResult => {
return {
@@ -65,7 +69,7 @@ connection.onInitialize(
change: TextDocumentSyncKind.Full,
},
completionProvider: {
- triggerCharacters: ["http", "https"],
+ triggerCharacters: ["http", "https", "@", '"', "'", "/"],
},
codeActionProvider: {
codeActionKinds: [CodeActionKind.QuickFix],
@@ -75,6 +79,9 @@ connection.onInitialize(
referencesProvider: true,
definitionProvider: true,
codeLensProvider: {},
+ executeCommandProvider: {
+ commands: ["deno._clear_import_enhencement_cache"],
+ },
},
};
}
@@ -118,6 +125,28 @@ connection.onInitialized(async () => {
executablePath: deno.executablePath,
DENO_DIR: getDenoDir(),
});
+ connection.onExecuteCommand(async (params: ExecuteCommandParams) => {
+ if (params.command === "deno._clear_import_enhencement_cache") {
+ import_enhanced
+ .clearCache()
+ .then(() => connection.window.showInformationMessage("Clear success!"))
+ .catch(() => connection.window.showErrorMessage("Clear failed!"));
+ }
+ });
+ import_enhanced
+ .cacheModList()
+ .then((it) => {
+ if (it === CACHE_STATE.CACHE_SUCCESS) {
+ connection.window.showInformationMessage(
+ "deno.land/x module list cached successfully!"
+ );
+ }
+ })
+ .catch(() =>
+ connection.window.showErrorMessage(
+ "deno.land/x module list failed to cache!"
+ )
+ );
connection.console.log("server initialized.");
});
diff --git a/snippets/import_enhancement.json b/snippets/import_enhancement.json
index c8cbf1a9..2152071c 100644
--- a/snippets/import_enhancement.json
+++ b/snippets/import_enhancement.json
@@ -8,7 +8,7 @@
"Import deno.land/x module": {
"scope": "javascript,typescript,javascriptreact,typescriptreact",
"prefix": ["impx", "importx"],
- "body": "import {$4} from 'https://deno.land/x/$1@$2/$3';$0",
+ "body": "import {$3} from 'https://deno.land/x/$1/$2';$0",
"description": "Use this snippet to import a deno.land/x module."
}
}