Skip to content
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
15 changes: 14 additions & 1 deletion packages/chrome-plugin/src/ProtocolClient.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Dialect, LintConfig } from 'harper.js';
import type { UnpackedLintGroups } from 'lint-framework';
import { LRUCache } from 'lru-cache';
import type { ActivationKey } from './protocol';
import type { ActivationKey, Hotkey } from './protocol';

export default class ProtocolClient {
private static readonly lintCache = new LRUCache<string, Promise<UnpackedLintGroups>>({
Expand Down Expand Up @@ -72,6 +72,19 @@ export default class ProtocolClient {
return (await chrome.runtime.sendMessage({ kind: 'getActivationKey' })).key;
}

public static async getHotkey(): Promise<Hotkey> {
return (await chrome.runtime.sendMessage({ kind: 'getHotkey' })).hotkey;
}

public static async setHotkey(hotkey: Hotkey): Promise<void> {
let modifiers = hotkey.modifiers;
let hotkeyCopy = {
modifiers: [...modifiers], // Create a new array
key: hotkey.key
};
await chrome.runtime.sendMessage({ kind: 'setHotkey', hotkey: hotkeyCopy });
}

public static async setActivationKey(key: ActivationKey): Promise<void> {
await chrome.runtime.sendMessage({ kind: 'setActivationKey', key });
}
Expand Down
34 changes: 34 additions & 0 deletions packages/chrome-plugin/src/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { BinaryModule, Dialect, type LintConfig, LocalLinter } from 'harper.js';
import { type UnpackedLintGroups, unpackLint } from 'lint-framework';
import {
ActivationKey,
type Hotkey,
type AddToUserDictionaryRequest,
createUnitResponse,
type GetActivationKeyRequest,
type GetHotkeyResponse,
type GetActivationKeyResponse,
type GetConfigRequest,
type GetConfigResponse,
Expand All @@ -23,6 +25,7 @@ import {
type Request,
type Response,
type SetActivationKeyRequest,
type SetHotkeyRequest,
type SetConfigRequest,
type SetDefaultStatusRequest,
type SetDialectRequest,
Expand Down Expand Up @@ -141,6 +144,10 @@ function handleRequest(message: Request): Promise<Response> {
return handleGetActivationKey();
case 'setActivationKey':
return handleSetActivationKey(message);
case 'getHotkey':
return handleGetHotkey();
case 'setHotkey':
return handleSetHotkey(message);
case 'openOptions':
chrome.runtime.openOptionsPage();
return createUnitResponse();
Expand Down Expand Up @@ -273,6 +280,24 @@ async function handleSetActivationKey(req: SetActivationKeyRequest): Promise<Uni
return createUnitResponse();
}

async function handleGetHotkey(): Promise<GetHotkeyResponse> {
const hotkey = await getHotkey();

return { kind: 'getHotkey', hotkey };
}

async function handleSetHotkey(req: SetHotkeyRequest): Promise<UnitResponse> {
// Create a plain object to avoid proxy cloning issues
const hotkey = {
modifiers: [...req.hotkey.modifiers],
key: req.hotkey.key
};
await setHotkey(hotkey);

return createUnitResponse();
}


/** Set the lint configuration inside the global `linter` and in permanent storage. */
async function setLintConfig(lintConfig: LintConfig): Promise<void> {
await linter.setLintConfig(lintConfig);
Expand Down Expand Up @@ -315,10 +340,19 @@ async function getActivationKey(): Promise<ActivationKey> {
return resp.activationKey;
}

async function getHotkey(): Promise<Hotkey> {
const resp = await chrome.storage.local.get({ hotkey: { modifiers: ['Ctrl'], key: 'e' } });
return resp.hotkey;
}

async function setActivationKey(key: ActivationKey) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

It seem like there's some overlap between your work and my previous attempt with the "Activation Key" stuff. I prefer your implementation, so feel free to remove anything related to the "Activation Key" as a part of this PR.

await chrome.storage.local.set({ activationKey: key });
}

async function setHotkey(hotkey: Hotkey) {
await chrome.storage.local.set({ hotkey: hotkey });
}

function initializeLinter(dialect: Dialect) {
linter = new LocalLinter({
binary: new BinaryModule(chrome.runtime.getURL('./wasm/harper_wasm_bg.wasm')),
Expand Down
72 changes: 71 additions & 1 deletion packages/chrome-plugin/src/options/Options.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Dialect, type LintConfig } from 'harper.js';
import logo from '/logo.png';
import ProtocolClient from '../ProtocolClient';
import { ActivationKey } from '../protocol';
import type { Modifier, Hotkey } from '../protocol';

let lintConfig: LintConfig = $state({});
let lintDescriptions: Record<string, string> = $state({});
Expand All @@ -13,6 +14,8 @@ let dialect = $state(Dialect.American);
let defaultEnabled = $state(false);
let activationKey: ActivationKey = $state(ActivationKey.Off);
let userDict = $state('');
let modifyHotkeyButton: Button;
let hotkey : Hotkey = $state({modifiers: ['Ctrl'], key: 'e'});

$effect(() => {
ProtocolClient.setLintConfig($state.snapshot(lintConfig));
Expand All @@ -31,7 +34,6 @@ $effect(() => {
});

$effect(() => {
console.log('hit');
ProtocolClient.setUserDictionary(stringToDict(userDict));
});

Expand All @@ -55,6 +57,16 @@ ProtocolClient.getActivationKey().then((d) => {
activationKey = d;
});

ProtocolClient.getHotkey().then((d) => {
// Ensure we have a plain object, not a Proxy
hotkey = {
modifiers: [...d.modifiers],
key: d.key
};
buttonText = `Hotkey: ${d.modifiers.join('+')}+${d.key}`;
});


ProtocolClient.getUserDictionary().then((d) => {
userDict = dictToString(d.toSorted());
});
Expand Down Expand Up @@ -116,6 +128,52 @@ async function exportEnabledDomainsCSV() {
}
}

let buttonText = $state('Set Hotkey');
let isBlue = $state(false); // modify color of hotkey button once it is pressed
function startHotkeyCapture(modifyHotkeyButton: Button) {

const handleKeydown = (event: KeyboardEvent) => {
event.preventDefault();

const modifiers: Modifier[] = [];
if (event.ctrlKey) modifiers.push('Ctrl');
if (event.shiftKey) modifiers.push('Shift');
if (event.altKey) modifiers.push('Alt');

let key = event.key;

if (key !== 'Control' && key !== 'Shift' && key !== 'Alt') {
if (modifiers.length === 0) {
return;
}
buttonText = `Hotkey: ${modifiers.join('+')}+${key}`;
// Create a plain object to avoid proxy cloning issues
const newHotkey = {
modifiers: [...modifiers],
key: key
};
hotkey = newHotkey;

// Call ProtocolClient directly with the plain object to avoid proxy issues
ProtocolClient.setHotkey(newHotkey);

// Remove listener
window.removeEventListener('keydown', handleKeydown);

// change button color
isBlue = !isBlue;
}

}

// Add temporary key listener
window.addEventListener('keydown', handleKeydown);


};



// Import removed
</script>

Expand Down Expand Up @@ -179,6 +237,18 @@ async function exportEnabledDomainsCSV() {
</div>
</div>

<div class="space-y-5">
<div class="flex items-center justify-between">
<div class="flex flex-col">
<span class="font-medium">Apply Last Suggestion Hotkey</span>
<span class="font-light">Hotkey to apply the most likely suggestion to previously incorrect lint.</span>
</div>
<textarea bind:value={buttonText} />
<Button size="sm" color="light" style="background-color: {isBlue ? 'blue' : ''}" bind:this={modifyHotkeyButton} on:click={() => {startHotkeyCapture(modifyHotkeyButton); isBlue = !isBlue}}>Modify Hotkey</Button>

</div>
</div>

<div class="space-y-5">
<div class="flex items-center justify-between">
<div class="flex flex-col">
Expand Down
24 changes: 24 additions & 0 deletions packages/chrome-plugin/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export type Request =
| GetUserDictionaryRequest
| GetActivationKeyRequest
| SetActivationKeyRequest
| GetHotkeyRequest
| SetHotkeyRequest
| OpenOptionsRequest;

export type Response =
Expand All @@ -31,6 +33,7 @@ export type Response =
| GetDefaultStatusResponse
| GetEnabledDomainsResponse
| GetUserDictionaryResponse
| GetHotkeyResponse
| GetActivationKeyResponse;

export type LintRequest = {
Expand Down Expand Up @@ -164,6 +167,10 @@ export type GetActivationKeyRequest = {
kind: 'getActivationKey';
};

export type GetHotkeyRequest = {
kind: 'getHotkey';
}

export type GetActivationKeyResponse = {
kind: 'getActivationKey';
key: ActivationKey;
Expand All @@ -177,3 +184,20 @@ export type SetActivationKeyRequest = {
export type OpenOptionsRequest = {
kind: 'openOptions';
};

export type GetHotkeyResponse = {
kind: 'getHotkey';
hotkey: Hotkey;
};

export type SetHotkeyRequest = {
kind: 'setHotkey';
hotkey: Hotkey;
};

export type Modifier = 'Ctrl' | 'Shift' | 'Alt';

export type Hotkey = {
modifiers: Modifier[];
key: string;
};
55 changes: 54 additions & 1 deletion packages/lint-framework/src/lint/LintFramework.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@ import computeLintBoxes from './computeLintBoxes';
import { isVisible } from './domUtils';
import Highlights from './Highlights';
import PopupHandler from './PopupHandler';
import type { UnpackedLintGroups } from './unpackLint';
import type { UnpackedLint, UnpackedLintGroups } from './unpackLint';
import ProtocolClient from '../../../chrome-plugin/src/ProtocolClient'
import type { IgnorableLintBox } from './Box';

type ActivationKey = 'off' | 'shift' | 'control';

type Modifier = 'Ctrl' | 'Shift' | 'Alt';

type Hotkey = {
modifiers: Modifier[];
key: string;
};

/** Events on an input (any kind) that can trigger a re-render. */
const INPUT_EVENTS = ['focus', 'keyup', 'paste', 'change', 'scroll'];
/** Events on the window that can trigger a re-render. */
Expand All @@ -20,6 +29,7 @@ export default class LintFramework {
private lintRequested = false;
private renderRequested = false;
private lastLints: { target: HTMLElement; lints: UnpackedLintGroups }[] = [];
private lastBoxes: IgnorableLintBox[] = [];

/** The function to be called to re-render the highlights. This is a variable because it is used to register/deregister event listeners. */
private updateEventCallback: () => void;
Expand All @@ -30,6 +40,7 @@ export default class LintFramework {
private actions: {
ignoreLint?: (hash: string) => Promise<void>;
getActivationKey?: () => Promise<ActivationKey>;
getHotkey?: () => Promise<Hotkey>;
openOptions?: () => Promise<void>;
addToUserDictionary?: (words: string[]) => Promise<void>;
};
Expand All @@ -39,6 +50,7 @@ export default class LintFramework {
actions: {
ignoreLint?: (hash: string) => Promise<void>;
getActivationKey?: () => Promise<ActivationKey>;
getHotkey?: () => Promise<Hotkey>;
openOptions?: () => Promise<void>;
addToUserDictionary?: (words: string[]) => Promise<void>;
},
Expand Down Expand Up @@ -122,6 +134,45 @@ export default class LintFramework {
this.requestRender();
}

/**
* Hotkey to apply the suggestion of the most likely word
*/
public async lintHotkey() {
let hotkey = await ProtocolClient.getHotkey();

document.addEventListener('keydown', (event: KeyboardEvent) => {

if (!hotkey) return;

let key = event.key.toLowerCase();
let expectedKey = hotkey.key.toLowerCase();

let hasCtrl = event.ctrlKey === hotkey.modifiers.includes('Ctrl');
let hasAlt = event.altKey === hotkey.modifiers.includes('Alt');
let hasShift = event.shiftKey === hotkey.modifiers.includes('Shift');

let match = key === expectedKey && hasCtrl && hasAlt && hasShift;

if (match) {
event.preventDefault();
event.stopImmediatePropagation();
let previousBox = this.lastBoxes[this.lastBoxes.length - 1];
let previousLint = this.lastLints[this.lastLints.length - 1];
if(previousBox.lint.suggestions.length > 0) {
const allLints = Object.values(previousLint.lints).flat();
previousBox.applySuggestion(allLints[allLints.length - 1].suggestions[0]);

} else {
previousBox.ignoreLint?.();
}

}

}, {capture: true});

}


public async addTarget(target: Node) {
if (!this.targets.has(target)) {
this.targets.add(target);
Expand Down Expand Up @@ -174,6 +225,7 @@ export default class LintFramework {
}

private attachWindowListeners() {
this.lintHotkey();
for (const event of PAGE_EVENTS) {
window.addEventListener(event, this.updateEventCallback);
}
Expand Down Expand Up @@ -202,6 +254,7 @@ export default class LintFramework {
this.popupHandler.updateLintBoxes(boxes);

this.renderRequested = false;
this.lastBoxes = boxes;
});
}
}
Expand Down
Loading