Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/0.22.0-beta.2-after'
Browse files Browse the repository at this point in the history
  • Loading branch information
Taitava committed May 5, 2024
2 parents 0e74d77 + afa7778 commit 7c3a7bf
Show file tree
Hide file tree
Showing 50 changed files with 1,650 additions and 322 deletions.
35 changes: 35 additions & 0 deletions src/Common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,27 @@ export function getPluginAbsolutePath(plugin: SC_Plugin, convertSlashToBackslash
);
}

/**
* Retrieves a specific part of a version number.
*
* @param {string} wholeVersion - The complete version number string.
* @param {"major" | "minor" | "patch"} part - The part of the version number to retrieve ("major", "minor", or "patch").
*
* @returns {string | null} - The specified part of the version number, or null if the part is not found.
*
* @example getVersionPart("2.3.4", "major"); // returns "2"
* @example getVersionPart("2.3.4", "minor"); // returns "3"
* @example getVersionPart("2.3.4", "patch"); // returns "4"
* @example getVersionPart("2.3.4", "invalid-part-name"); // returns null
*/
export function getVersionPart(wholeVersion: string, part: "major" | "minor" | "patch"): string | null {
const versionPartsMatch = wholeVersion.match(/^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/); // No $ at the end on purpose: if the version string continues, just ignore the rest.
if (undefined === versionPartsMatch?.groups?.[part]) {
return null;
}
return versionPartsMatch.groups[part];
}

/**
* For some reason there is no Platform.isWindows .
*/
Expand Down Expand Up @@ -392,6 +413,20 @@ export function isInteger(value: string, allow_minus: boolean): boolean {
}
}

/**
* Converts a string input to a floating-point number with limited decimal places. Replaces a possible comma with a dot.
*
* @param {string} input - The input string to be converted.
* @param {number} countDecimals - The number of decimal places to limit the converted number to.
* @return {number} - The converted floating-point number with limited decimal places.
*/
export function inputToFloat(input: string, countDecimals: number): number {
const inputCommaReplaced = input.replace(",", ".");
const number: number = parseFloat(inputCommaReplaced);
const limitedDecimals: string = number.toFixed(countDecimals);
return parseFloat(limitedDecimals); // Use parseFloat() again to remove a possible .0 and to convert it back to a number.
}

/**
* Translates 1-indexed caret line and column to a 0-indexed EditorPosition object. Also translates a possibly negative line
* to a positive line from the end of the file, and a possibly negative column to a positive column from the end of the line.
Expand Down
251 changes: 251 additions & 0 deletions src/Debouncer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
/*
* 'Shell commands' plugin for Obsidian.
* Copyright (C) 2021 - 2024 Jarkko Linnanvirta
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.0 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* Contact the author (Jarkko Linnanvirta): https://github.com/Taitava/
*/
import {TShellCommand} from "./TShellCommand";
import {SC_Event} from "./events/SC_Event";
import {debugLog} from "./Debug";
import {ShellCommandExecutor} from "./ShellCommandExecutor";
import SC_Plugin from "./main";

export class Debouncer {

private state: DebounceState = "idle";
private subsequent: null | {
scEvent: SC_Event,
// Just a single property, but use still an object structure, so it's easy to add more properties later, if needed. E.g. a ParsingProcess.
} = null;
private cooldownTimeout: CooldownTimeout | null = null;

constructor(
private plugin: SC_Plugin,
private configuration: DebounceConfiguration,
private tShellCommand: TShellCommand,
) {
if (!configuration) {
throw new Error("Debouncer can only be instantiated with a shell command that has `debounce` enabled.");
}
}

public async executeWithDebouncing(scEvent: SC_Event): Promise<void> {
switch (this.state) {
case "idle":
// IDLE PHASE.
switch (this.getMode()) {
case "early-execution":
case "early-and-late-execution":{
// Execute immediately.
await this.execute(scEvent);
break;
}
case "late-execution":{
// Begin a cooldown phase and execute after it.
this.debugLog("is delayed.");
this.subsequent = {
scEvent: scEvent,
};
await this.cooldown();
break;
}
}
break;
default:
// EXECUTING OR COOLDOWN PHASE.
if ("cooldown" === this.state && this.configuration.prolongCooldown) {
// Prolong cooldown duration.
this.prolongCooldownTimeout();
}
switch (this.getMode()) {
case "early-execution": {
// Nothing to do - just discard this execution.
this.debugLog("execution is discarded.");
break;
}
case "late-execution": {
// Wait until previous execution is over, then start another cooldown phase + execution.
this.debugLog("execution is postponed and may be merged to a later one.");
this.subsequent = { // Override `subsequent` if it contained an earlier waiter. After the cooldown is over, always execute the newest thing.
scEvent: scEvent,
};
break;
}
case "early-and-late-execution": {
// Wait until previous execution is over and a cooldown phase is passed, too.
this.debugLog("execution is postponed and may be merged to a later one.");
this.subsequent = { // Override `subsequent` if it contained an earlier waiter. After the cooldown is over, always execute the newest thing.
scEvent: scEvent,
};
break;
}
}
break;
}
}

private async execute(scEvent: SC_Event): Promise<void> {
this.state = "executing";
this.debugLog("will be executed now.");
const executor = new ShellCommandExecutor(this.plugin, this.tShellCommand, scEvent);
await executor.doPreactionsAndExecuteShellCommand();
await this.afterExecuting();
}

private async afterExecuting(): Promise<void> {
this.debugLog("execution ended.");
switch (this.getMode()) {
case "early-execution": {
// Not much to do anymore, go to cooldown and clear state after it.
await this.cooldown();
break;
}
case "late-execution": {
if (this.subsequent) {
// Another event triggering happened during execution. Start another cooldown + execution process.
await this.cooldown();
} else {
// No events triggered during execution. Clear state.
this.state = "idle";
}
break;
}
case "early-and-late-execution": {
// Go to cooldown and see after that if there's anything more to execute.
await this.cooldown();
break;
}
}
}

private cooldown(): Promise<void> {
return new Promise((resolve) => {
this.state = "cooldown";
this.debugLog("is in cooldown phase now.");
this.cooldownTimeout = this.createCooldownTimeout(
() => {this.afterCooldown().then(resolve);},
true,
);
});
}

private async afterCooldown(): Promise<void> {
const debugMessageBase = "\"cooldown\" phase ended, ";
this.cooldownTimeout = null;
switch (this.getMode()) {
case "early-execution": {
// Not much to do after cooldown.
this.debugLog(debugMessageBase + "debouncing ended.");
this.state = "idle";
break;
}
case "late-execution":
case "early-and-late-execution": {
if (this.subsequent) {
// There is a next execution waiting to be started.
this.debugLog(debugMessageBase + "will start a previously postponed execution.");
const executeWithEvent: SC_Event = this.subsequent.scEvent;
this.subsequent = null;
await this.execute(executeWithEvent);

} else {
// No need to start another execution process. (We should only end up here in mode "early-and-late-execution", not in mode "late-execution").
this.debugLog(debugMessageBase + "no postponed execution is waiting, so will not re-execute.");
this.state = "idle";
this.subsequent = null;
}
break;
}
}
}

private createCooldownTimeout(callback: () => void, returnObject: true): CooldownTimeout
private createCooldownTimeout(callback: () => void, returnObject: false): number
private createCooldownTimeout(callback: () => void, returnObject: boolean): CooldownTimeout | number {
const timeoutId: number = window.setTimeout(
callback,
this.getCoolDownMilliseconds(),
);
if (returnObject) {
return {
timeoutId: timeoutId,
callback: callback,
};
} else {
return timeoutId;
}
}

private prolongCooldownTimeout(): void {
if (this.cooldownTimeout) {
// Delete and recreate the timeout.
this.debugLog("\"cooldown\" phase will be prolonged.");
window.clearTimeout(this.cooldownTimeout.timeoutId);
this.cooldownTimeout.timeoutId = this.createCooldownTimeout(this.cooldownTimeout.callback, false);
} else {
// Can't find a timeout that should be prolonged.
this.debugLog("\"cooldown\" phase tried to be prolonged, but no timeout function exists. Might be a bug.");
}
}

/**
* Translates this.configuration.executeEarly and this.configuration.executeLate to an earlier mode format that this
* class still uses.
* @private
*/
private getMode(): "early-and-late-execution" | "early-execution" | "late-execution" {
if (this.configuration.executeEarly && this.configuration.executeLate) {
return "early-and-late-execution";
} else if (this.configuration.executeEarly) {
return "early-execution";
} else if (this.configuration.executeLate) {
return "late-execution";
} else {
// Debouncing is disabled.
throw new Error("Debouncer.getMode(): Debouncing is disabled, but it was tried to be used.");
}
}

private getCoolDownMilliseconds(): number {
return this.configuration.cooldownDuration * 1000;
}

private debugLog(message: string): void {
debugLog("Debouncing control: Shell command id " + this.tShellCommand.getId() + " " + message);
}

public static getDefaultConfiguration(executeEarly: boolean, executeLate: boolean): DebounceConfiguration {
return {
executeEarly: executeEarly,
executeLate: executeLate,
cooldownDuration: 0,
prolongCooldown: false,
};
}
}

type DebounceState = "idle" | "executing" | "cooldown";

export interface DebounceConfiguration {
executeEarly: boolean,
executeLate: boolean,
cooldownDuration: number,
prolongCooldown: boolean,
}

interface CooldownTimeout {
timeoutId: number,
callback: () => void,
}
4 changes: 4 additions & 0 deletions src/Documentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,15 @@ export const Documentation = {
},
events: {
folder: "https://publish.obsidian.md/shellcommands/Events/", // Keep the trailing slash!
debouncing: "https://publish.obsidian.md/shellcommands/Events/Events+-+debouncing",
},
outputHandling: {
outputHandlingMode: "https://publish.obsidian.md/shellcommands/Output+handling/Realtime+output+handling",
outputWrappers: "https://publish.obsidian.md/shellcommands/Output+handling/Output+wrappers",
},
problems: {
flatpakInstallation: "https://publish.obsidian.md/shellcommands/Problems/Flatpak+installation",
},
variables: {
folder: "https://publish.obsidian.md/shellcommands/Variables/", // Keep the trailing slash!
allVariables: "https://publish.obsidian.md/shellcommands/Variables/All+variables",
Expand Down
57 changes: 57 additions & 0 deletions src/Migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export async function RunMigrations(plugin: SC_Plugin) {
EnsureCustomVariablesHaveAllFields(plugin),
EnsurePromptFieldsHaveAllFields(plugin),
DeleteEmptyCommandsField(plugin),
MigrateDebouncingModes(plugin), // Temporary.
];
if (should_save.includes(true)) {
// Only save if there were changes to configuration.
Expand All @@ -60,6 +61,62 @@ export async function RunMigrations(plugin: SC_Plugin) {
}
}

/**
* Temporary, only needed for migrating a debouncing `mode` property which was present in SC 0.22.0-beta.1, so not in any
* actual release. The property is removed in 0.22.0-beta.2. Keep this function a few months, then it's not needed anymore.
* @param plugin
* @constructor
*/
function MigrateDebouncingModes(plugin: SC_Plugin): boolean {
let save: boolean = false;
for (const shellCommandConfiguration of plugin.settings.shell_commands) {
if (shellCommandConfiguration.debounce) {
// @ts-ignore
if (undefined !== shellCommandConfiguration.debounce.mode) {
// Found a `mode` property that was present in SC 0.22.0-beta.1 .
// @ts-ignore
switch (shellCommandConfiguration.debounce.mode) {
case "early-and-late-execution":
shellCommandConfiguration.debounce.executeEarly = true;
shellCommandConfiguration.debounce.executeLate = true;
break;
case "early-execution":
shellCommandConfiguration.debounce.executeEarly = true;
shellCommandConfiguration.debounce.executeLate = false;
break;
case "late-execution":
shellCommandConfiguration.debounce.executeEarly = false;
shellCommandConfiguration.debounce.executeLate = true;
break;
}
// @ts-ignore
debugLog("Migration: Shell command #" + shellCommandConfiguration.id + " had a deprecated debounce.mode property (" + shellCommandConfiguration.debounce.mode + "). It was migrated to debounce.executeEarly (" + (shellCommandConfiguration.debounce.executeEarly ? "true" : "false") + ") and debounce.executeLate (" + (shellCommandConfiguration.debounce.executeLate ? "true" : "false") + ").");
// @ts-ignore
delete shellCommandConfiguration.debounce.mode;
save = true;
}

// @ts-ignore
if (undefined !== shellCommandConfiguration.debounce.cooldown) {
// `cooldown` was present in 0.22.0-beta.1, but renamed in 0.22.0-beta.2.
// @ts-ignore
shellCommandConfiguration.debounce.cooldownDuration = shellCommandConfiguration.debounce.cooldown;
// @ts-ignore
delete shellCommandConfiguration.debounce.cooldown;
save = true;
}


// prolongCooldown was not present in 0.22.0-beta.1. Add it, but disable it by default.
if (undefined === shellCommandConfiguration.debounce.prolongCooldown) {
shellCommandConfiguration.debounce.prolongCooldown = false;
save = true;
}
}
}
return save;
}

/**
* Can be removed in the future, but I haven't yet decided will it be done in 1.0 or later.
*/
Expand Down
Loading

0 comments on commit 7c3a7bf

Please sign in to comment.