diff --git a/src/Device.js b/src/Device.js index 1702d381..badcc4ff 100644 --- a/src/Device.js +++ b/src/Device.js @@ -81,14 +81,17 @@ class Device { this.updateHideStatus(); if (!this.config.hidden) this.updateConnection(); subscribe(() => this.onChanged()); + this.pymakr.config.subscribe(() => this.updateHideStatus()); } /** * Hides / unhides this device depending on how it matches user's config.devices.include and config.devices.exclude */ updateHideStatus() { + const oldStatus = this.config.hidden; const { include, exclude } = this.pymakr.config.get().get("devices"); this.config.hidden = !createIsIncluded(include, exclude)(serializeKeyValuePairs(this.raw)); + if (oldStatus != this.config.hidden) this.onChanged(); } /** @@ -254,8 +257,7 @@ class Device { */ onChanged() { this.state.save(); - this.pymakr.devicesProvider.refresh(); - this.pymakr.projectsProvider.refresh(); + this.pymakr.refreshProvidersThrottled(); } /** diff --git a/src/PyMakr.js b/src/PyMakr.js index a207b7ab..63c9de7b 100644 --- a/src/PyMakr.js +++ b/src/PyMakr.js @@ -10,7 +10,7 @@ const { resolve } = require("path"); const { FileSystemProvider } = require("./providers/FilesystemProvider"); const { createLogger } = require("./utils/createLogger"); const { writable } = require("./utils/store"); -const { coerceDisposable } = require("./utils/misc"); +const { coerceDisposable, createThrottledFunction } = require("./utils/misc"); const manifest = require("../package.json"); const { createVSCodeHelpers } = require("./utils/vscodeHelpers"); const { TextDocumentProvider } = require("./providers/TextDocumentProvider"); @@ -28,6 +28,8 @@ class PyMakr { * @param {vscode.ExtensionContext} context */ constructor(context) { + this.refreshProvidersThrottled = createThrottledFunction(this.refreshProviders.bind(this)); + /** Reactive Pymakr user configuration */ this.config = writable(vscode.workspace.getConfiguration("pymakr")); @@ -39,7 +41,7 @@ class PyMakr { this.log.info(`${manifest.name} v${manifest.version}`); // avoid port collisions between multiple vscode instances running on the same machine - this.terminalPort = 5364 + (Math.random() * 10240 | 0); + this.terminalPort = 5364 + ((Math.random() * 10240) | 0); this.onUpdatedConfig("silent"); this.context = context; @@ -64,7 +66,7 @@ class PyMakr { /** Provides device access for the file explorer */ this.fileSystemProvider = new FileSystemProvider(this); - this.textDocumentProvider = new TextDocumentProvider(this) + this.textDocumentProvider = new TextDocumentProvider(this); this.registerWithIde(); this.setup(); @@ -128,6 +130,11 @@ class PyMakr { async registerProjects() { await this.projectsStore.refresh(); } + + refreshProviders() { + this.devicesProvider.refresh(); + this.projectsProvider.refresh(); + } } module.exports = { PyMakr }; diff --git a/src/utils/misc.js b/src/utils/misc.js index e50df9ea..f1c22e49 100644 --- a/src/utils/misc.js +++ b/src/utils/misc.js @@ -86,7 +86,7 @@ const mapEnumsToQuickPick = (descriptions) => (_enum, index) => ({ * @returns {{[P in K]: T[P]}} */ const cherryPick = (obj, props) => - props.reduce((newObj, key) => ({ ...newObj, [key]: obj[key] }), /** @type {obj} */({})); + props.reduce((newObj, key) => ({ ...newObj, [key]: obj[key] }), /** @type {obj} */ ({})); /** * Curried function. Returns the nearest parent from an array of folders @@ -139,7 +139,7 @@ const readJsonFile = (path) => JSON.parse(readFileSync(path, "utf8")); const getNearestPymakrConfig = (path) => { if (!path) return null; const projectPath = getNearestPymakrProjectDir(path); - if (projectPath) return readJsonFile(join(projectPath, 'pymakr.conf')); + if (projectPath) return readJsonFile(join(projectPath, "pymakr.conf")); else return null; }; @@ -149,7 +149,7 @@ const getNearestPymakrConfig = (path) => { * @returns {string} */ const getNearestPymakrProjectDir = (path) => { - const configPath = join(path, 'pymakr.conf'); + const configPath = join(path, "pymakr.conf"); if (existsSync(configPath)) return path; else { const parentDir = dirname(path); @@ -189,7 +189,7 @@ const createIsIncluded = (includes, excludes, cb = (x) => x) => { * Serializes flat object * @example default behavior * ```javascript - * serializeKeyValuePairs ({foo: 123, bar: 'bar'}) + * serializeKeyValuePairs ({foo: 123, bar: 'bar'}) * // foo=123 * // bar=bar * ``` @@ -216,6 +216,40 @@ const resolvablePromise = () => { return Object.assign(origPromise, { resolve, reject }); }; +/** + * Subsequent calls to an active throttled function will return the same promise as the first call. + * A function is active until it's first call is resolved + * @template {Function} T + * @param {T} fn callback + * @param {number=} time leave at 0 to only throttle calls made within the same cycle + * @returns {(...params: Parameters)=>Promise>} + */ +const createThrottledFunction = (fn, time) => { + let isRunning = false; + /** @type {{resolve: any, reject: any}[]} */ + const subs = []; + const fnWrapper = (...params) => + new Promise((resolve, reject) => { + subs.push({ resolve, reject }); + if (!isRunning) { + isRunning = true; + setTimeout(async () => { + this._isRefreshingProviders = false; + try { + const result = await fn(...params); + subs.forEach((sub) => sub.resolve(result)); + } catch (err) { + subs.forEach((sub) => sub.reject(err)); + } + subs.splice(0) + isRunning = false + }, time); + } + }); + return fnWrapper; +}; + + module.exports = { once, coerceArray, @@ -234,4 +268,5 @@ module.exports = { createIsIncluded, arrayToRegexStr, resolvablePromise, + createThrottledFunction }; diff --git a/src/utils/specs/misc.spec.mjs b/src/utils/specs/misc.spec.mjs index 61f5582e..b09de99d 100644 --- a/src/utils/specs/misc.spec.mjs +++ b/src/utils/specs/misc.spec.mjs @@ -4,6 +4,7 @@ import { arrayToRegexStr, cherryPick, createIsIncluded, + createThrottledFunction, getDifference, getNearestParent, getNearestPymakrConfig, @@ -65,15 +66,14 @@ test("cherryPick", () => { test("getNearestParent + relative", () => { // use different test paths on windows / linux - if (process.platform === "win32"){ + if (process.platform === "win32") { const parents = ["c:\\some\\folder\\path", "c:\\some\\folder", "c:\\some"]; const child = "c:\\some\\folder\\child\\path"; assert.equal(getNearestParent(parents)(child), "c:\\some\\folder"); assert.equal(getRelativeFromNearestParent(parents)(child), "child\\path"); assert.equal(getRelativeFromNearestParentPosix(parents)(child), "child/path"); - - } else{ + } else { const parents = ["/some/folder/path", "/some/folder", "/some"]; const child = "/some/folder/child/path"; @@ -121,9 +121,18 @@ test("createIsIncluded", () => { }); test("specific excludes excludes only specific matches", () => { - const result = items.filter( - createIsIncluded([".*"], ["someField=exclude-me"], serializeKeyValuePairs) - ); + const result = items.filter(createIsIncluded([".*"], ["someField=exclude-me"], serializeKeyValuePairs)); assert.deepEqual(result, [items[0], items[1]]); }); }); + +test("createThrottledFunction", async () => { + const getRandom = () => Math.random(); + const throttledRandom = createThrottledFunction(getRandom); + const call1 = throttledRandom() + const call2 = throttledRandom() + const call3 = throttledRandom() + const [r1, r2, r3] = await Promise.all([call1, call2, call3]) + assert.equal(r1, r2) + assert.equal(r2, r3) +});