Skip to content

Commit

Permalink
fix: Log word replacements (#3583)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jason3S authored Sep 1, 2024
1 parent f19f4f7 commit a379c78
Show file tree
Hide file tree
Showing 18 changed files with 480 additions and 26 deletions.
3 changes: 2 additions & 1 deletion cspell-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ quotemark
readonly
restructuredtext
rfdc
rmwc
RGBA
rmwc
RRGGBB
RRGGBBAA
scminput
Expand All @@ -92,6 +92,7 @@ sirv
sourcemap
squigglies
streetsidesoftware
stringifyable
stylesheets
subfolder
subpath
Expand Down
2 changes: 1 addition & 1 deletion packages/__utils/src/EventEmitter/index.mts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { createEmitter, isEventEmitter } from './createEmitter.mjs';
export * from './operators/index.mjs';
export { pipe } from './pipe.mjs';
export type { EventEmitter } from './types.mjs';
export type { EmitterEvent, EventEmitter, EventListener, EventOperator } from './types.mjs';
18 changes: 9 additions & 9 deletions packages/__utils/src/EventEmitter/pipe.mts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
/* eslint-disable @typescript-eslint/unified-signatures */
import type { Event, EventListener, EventOperator } from './types.mjs';
import type { EmitterEvent, EventListener, EventOperator } from './types.mjs';

export type Subscribable<T> = Event<T> | { event: Event<T> };
export type Subscribable<T> = EmitterEvent<T> | { event: EmitterEvent<T> };

export function pipe<T>(subscribable: Subscribable<T>): Event<T>;
export function pipe<T, U0>(subscribable: Subscribable<T>, op0: EventOperator<T, U0>): Event<U0>;
export function pipe<T, U0, U1>(subscribable: Subscribable<T>, op0: EventOperator<T, U0>, op1: EventOperator<U0, U1>): Event<U1>;
export function pipe<T>(subscribable: Subscribable<T>): EmitterEvent<T>;
export function pipe<T, U0>(subscribable: Subscribable<T>, op0: EventOperator<T, U0>): EmitterEvent<U0>;
export function pipe<T, U0, U1>(subscribable: Subscribable<T>, op0: EventOperator<T, U0>, op1: EventOperator<U0, U1>): EmitterEvent<U1>;
export function pipe<T, U0, U1, U2>(
subscribable: Subscribable<T>,
op0: EventOperator<T, U0>,
op1: EventOperator<U0, U1>,
op2: EventOperator<U1, U2>,
): Event<U2>;
export function pipe<T>(subscribable: Subscribable<T>, ...ops: EventOperator<T, T>[]): Event<T>;
export function pipe<T>(subscribable: Subscribable<T>, ...ops: EventOperator<T, T>[]): Event<T> {
): EmitterEvent<U2>;
export function pipe<T>(subscribable: Subscribable<T>, ...ops: EventOperator<T, T>[]): EmitterEvent<T>;
export function pipe<T>(subscribable: Subscribable<T>, ...ops: EventOperator<T, T>[]): EmitterEvent<T> {
const subscribeFn = 'event' in subscribable ? (fn: EventListener<T>) => subscribable.event(fn) : subscribable;

let finalEventFn: Event<T> | undefined;
let finalEventFn: EmitterEvent<T> | undefined;

/**
* Late binding of the final event function
Expand Down
6 changes: 3 additions & 3 deletions packages/__utils/src/EventEmitter/types.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export interface Disposable {

export type EventListener<T> = (data: T) => unknown;

export type Event<T> = (listener: EventListener<T>) => Disposable;
export type EmitterEvent<T> = (listener: EventListener<T>) => Disposable;

export interface EventEmitter<T> {
/**
Expand All @@ -15,7 +15,7 @@ export interface EventEmitter<T> {
/**
* The event listeners can subscribe to.
*/
event: Event<T>;
event: EmitterEvent<T>;

/**
* Notify all subscribers of the {@link EventEmitter.event event}. Failure
Expand All @@ -31,4 +31,4 @@ export interface EventEmitter<T> {
dispose(): void;
}

export type EventOperator<T, U> = (source: Event<T>) => Event<U>;
export type EventOperator<T, U> = (source: EmitterEvent<T>) => EmitterEvent<U>;
9 changes: 9 additions & 0 deletions packages/client/src/applyCorrections.mts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,15 @@ async function calcWorkspaceEditsForDocument(doc: TextDocument, edits: TextEdit[
}

async function applyTextEditsToDocumentWithRename(doc: TextDocument, edits: TextEdit[], refInfo: UseRefInfo): Promise<boolean | undefined> {
const eventLogger = di.get('eventLogger');

for (const edit of edits) {
const word = doc.getText(edit.range);
if (word !== edit.newText && word && edit.newText) {
eventLogger.logReplace(word, edit.newText);
}
}

const wsEdit = await calcWorkspaceEditsForDocument(doc, edits, refInfo);
return applyWorkspaceEdit(wsEdit, 'applyTextEditsToDocumentWithRename');
}
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/decorators/decorateIssues.mts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import vscode, { ColorThemeKind, MarkdownString, Range, window, workspace } from

import type { PartialCSpellUserSettings } from '../client/index.mjs';
import { commandUri, createTextEditCommand } from '../commands.mjs';
import { setContext } from '../context.mjs';
import type { Disposable } from '../disposable.js';
import type { IssueTracker, SpellingCheckerIssue } from '../issueTracker.mjs';
import type { CSpellSettings } from '../settings/index.mjs';
import { ConfigFields } from '../settings/index.mjs';
import { setContext } from '../storage/context.mjs';
import { findEditor } from '../util/findEditor.js';

const defaultHideIssuesWhileTyping: Required<PartialCSpellUserSettings<'hideIssuesWhileTyping'>>['hideIssuesWhileTyping'] = 'Word';
Expand Down
3 changes: 3 additions & 0 deletions packages/client/src/di.mts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import type { ExtensionContext } from 'vscode';
import type { CSpellClient } from './client/index.mjs';
import type { IssueTracker } from './issueTracker.mjs';
import type { DictionaryHelper } from './settings/DictionaryHelper.mjs';
import type { EventLogger } from './storage/index.mjs';

export interface GlobalDependencies {
name: string;
extensionContext: ExtensionContext;
client: CSpellClient;
issueTracker: IssueTracker;
dictionaryHelper: DictionaryHelper;
eventLogger: EventLogger;
}

type KeysForGlobalDependencies = {
Expand All @@ -22,6 +24,7 @@ const definedDependencyKeys: KeysForGlobalDependencies = {
client: undefined,
dictionaryHelper: undefined,
issueTracker: undefined,
eventLogger: undefined,
};

type GlobalDependenciesKeys = keyof GlobalDependencies;
Expand Down
23 changes: 21 additions & 2 deletions packages/client/src/extension.mts
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,21 @@ import { registerSpellCheckerCodeActionProvider } from './codeAction.mjs';
import type { InjectableCommandHandlers } from './commands.mjs';
import * as commands from './commands.mjs';
import { createConfigWatcher } from './configWatcher.mjs';
import { updateDocumentRelatedContext } from './context.mjs';
import { SpellingExclusionsDecorator, SpellingIssueDecorator } from './decorate.mjs';
import * as di from './di.mjs';
import { registerDiagWatcher } from './diags.mjs';
import type { ExtensionApi } from './extensionApi.mjs';
import * as ExtensionRegEx from './extensionRegEx/index.mjs';
import { IssueTracker } from './issueTracker.mjs';
import { activateFileIssuesViewer, activateIssueViewer } from './issueViewer/index.mjs';
import { createLanguageStatus } from './languageStatus.mjs';
import * as modules from './modules.mjs';
import { createTerminal, registerTerminalProfileProvider } from './repl/index.mjs';
import type { ConfigTargetLegacy, CSpellSettings } from './settings/index.mjs';
import * as settings from './settings/index.mjs';
import { sectionCSpell } from './settings/index.mjs';
import { getSectionName } from './settings/vsConfig.mjs';
import { createLanguageStatus } from './statusbar/languageStatus.mjs';
import { createEventLogger, updateDocumentRelatedContext } from './storage/index.mjs';
import { logErrors, silenceErrors } from './util/errors.js';
import { performance } from './util/perf.js';
import { activate as activateWebview } from './webview/index.mjs';
Expand All @@ -43,6 +43,9 @@ modules.init();
export async function activate(context: ExtensionContext): Promise<ExtensionApi> {
performance.mark('cspell_activate_start');
di.set('extensionContext', context);
const eventLogger = createEventLogger(context.globalStorageUri);
di.set('eventLogger', eventLogger);
eventLogger.logActivate();

const logOutput = vscode.window.createOutputChannel('Code Spell Checker', { log: true });
const dLogger = bindLoggerToOutput(logOutput);
Expand All @@ -60,6 +63,22 @@ export async function activate(context: ExtensionContext): Promise<ExtensionApi>

di.set('client', client);

if (debugMode) {
// const cki = (await vscode.commands.executeCommand('getContextKeyInfo')) as { key: string }[];
// const commands = await vscode.commands.getCommands();
const dataFileUri = vscode.Uri.joinPath(context.globalStorageUri, 'data.json');
console.log('Extension "Code Spell Checker" is now active! %o', {
extensionUri: context.extensionUri.toJSON(),
globalStorageUri: context.globalStorageUri.toJSON(),
dataFileUri: dataFileUri.toJSON(),
dataFileDirUri: vscode.Uri.joinPath(dataFileUri, '..').toJSON(),
workspaceState: context.workspaceState.keys(),
globalState: context.globalState.keys(),
// contextKeys: cki.filter((k) => k.key.startsWith('cSpell')),
// commands: commands.filter((c) => c.toLowerCase().includes('command')),
});
}

ExtensionRegEx.activate(context, client);

// Start the client.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { createDisposableList } from 'utils-disposables';
import type { Disposable } from 'vscode';
import vscode from 'vscode';

import type { ServerResponseIsSpellCheckEnabledForFile } from './client/client.mjs';
import { knownCommands } from './commands.mjs';
import { getClient, getIssueTracker } from './di.mjs';
import type { IssuesStats } from './issueTracker.mjs';
import { handleErrors } from './util/errors.js';
import type { ServerResponseIsSpellCheckEnabledForFile } from '../client/client.mjs';
import { knownCommands } from '../commands.mjs';
import { getClient, getIssueTracker } from '../di.mjs';
import type { IssuesStats } from '../issueTracker.mjs';
import { handleErrors } from '../util/errors.js';

const showLanguageStatus = true;

Expand Down
43 changes: 43 additions & 0 deletions packages/client/src/storage/EventLog.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export type MachineId = string;
export type Timestamp = number;

export type Events = 'replace' | 'activate';

/**
* A log entry.
* - ts - The timestamp of the event.
* - range - The timestamp of the end of the event range. (this is used to group events).
* - event - The event that occurred.
* - count - The number of times the event occurred. (this is used to group events).
* - args - Additional arguments for the event.
*/
export type LogEntryBase = [ts: Timestamp, range: Timestamp | 0, event: string, count: number, ...args: unknown[]];

/** Replace a word with a suggestion. */
export type LogEntryReplace = [ts: Timestamp, range: Timestamp | 0, 'replace', count: number, word: string, suggestion: string];

/** Indicates that the extension was activated. */
export type LogEntryActivate = [ts: Timestamp, range: Timestamp | 0, 'activate', count: number];

export type LogEntry = LogEntryReplace | LogEntryActivate;

export interface EventLog {
/**
* The log of events that have occurred.
*/
log: LogEntryBase[];

/**
* The synchronization timestamp of each machine.
* The timestamp is the timestamp of the last event added to the log for a given machine id.
*/
sync: Record<MachineId, Timestamp>;
}

export function isLogEntryReplace(entry: LogEntryBase): entry is LogEntryReplace {
return entry[2] === 'replace';
}

export function isLogEntryActivate(entry: LogEntryBase): entry is LogEntryActivate {
return entry[2] === 'activate';
}
97 changes: 97 additions & 0 deletions packages/client/src/storage/EventLogger.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { Disposable } from 'vscode';
import { env, Uri } from 'vscode';

import { logErrors } from '../util/errors.js';
import type { LogEntry, LogEntryBase } from './EventLog.mjs';
import { MementoFile } from './mementoFile.mjs';

export interface EventLogger {
readonly eventLog: readonly LogEntryBase[];
log(event: LogEntry): void;
logActivate(): void;
logReplace(word: string, suggestion: string): void;
flush(): Promise<void>;
}

export function createReplaceEvent(word: string, suggestion: string): LogEntry {
return [Date.now(), 0, 'replace', 1, word, suggestion];
}

export function createActivateEvent(): LogEntry {
return [Date.now(), 0, 'activate', 1];
}

interface LocalEventLogData {
machineId: string;
ts: number;
log: LogEntry[];
}

class EventLoggerImpl implements EventLogger, Disposable {
#data: MementoFile<LocalEventLogData> | undefined;
#pData: Promise<MementoFile<LocalEventLogData>>;
#pending: LogEntry[] = [];
#pFlush: Promise<void> | undefined = undefined;
#timeout: NodeJS.Timeout | undefined = undefined;

constructor(readonly uriStorageDir: Uri) {
this.#pData = MementoFile.createMemento<LocalEventLogData>(Uri.joinPath(uriStorageDir, 'eventLog.json'));
this.#pData.then((data) => (this.#data = data));
}

log(event: LogEntry): void {
this.#pending.push(event);
this.queueFlush();
}

logActivate(): void {
this.log(createActivateEvent());
}

logReplace(word: string, suggestion: string): void {
this.log(createReplaceEvent(word, suggestion));
}

get eventLog(): readonly LogEntryBase[] {
if (!this.#data) return [];
return this.#data.get('log', []);
}

queueFlush(): void {
if (this.#timeout) return;
this.#timeout = setTimeout(() => {
this.#timeout = undefined;
logErrors(this.flush(), 'EventLogger.flush');
}, 10);
}

flush(): Promise<void> {
const doFlush = async () => {
if (!this.#pending.length) return;
const memo = this.#data ?? (await this.#pData);
const machineId = memo.get('machineId', env.machineId);
const ts = Date.now();
const log = [...memo.get('log', []), ...this.#pending];
this.#pending = [];
await memo.update({ machineId, ts, log });
};
const pFlush = this.#pFlush ? this.#pFlush.then(doFlush) : doFlush();
this.#pFlush = pFlush;
pFlush.finally(() => {
if (this.#pFlush === pFlush) this.#pFlush = undefined;
});
return pFlush;
}

dispose(): void {
if (this.#data) {
this.#data.dispose();
} else {
this.#pData.then((data) => data.dispose());
}
}
}

export function createEventLogger(uriStorageDir: Uri): EventLogger {
return new EventLoggerImpl(uriStorageDir);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import assert from 'node:assert';
import type { TextDocument } from 'vscode';
import { commands, workspace } from 'vscode';

import type { ConfigKind, ConfigScope, ConfigTarget, CSpellClient } from './client/index.mjs';
import { extensionId } from './constants.js';
import { getCSpellDiags } from './diags.mjs';
import { toUri } from './util/uriHelper.mjs';
import type { ConfigKind, ConfigScope, ConfigTarget, CSpellClient } from '../client/index.mjs';
import { extensionId } from '../constants.js';
import { getCSpellDiags } from '../diags.mjs';
import { toUri } from '../util/uriHelper.mjs';

const prefix = extensionId;

Expand Down
24 changes: 24 additions & 0 deletions packages/client/src/storage/globalState.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { EventLog, Timestamp } from './EventLog.mjs';
import type { Memento } from './memento.mjs';

export interface GlobalStateData {
eventLog: EventLog;
}

export interface GlobalStateControl extends Memento<GlobalStateData> {
/**
* List of fields that are synced across machines.
*/
readonly syncFields: string[];

/**
* This field is used to store the signature of last time the global state was updated on this machine.
* It is used to detect changes to the global state.
*/
readonly signature: string;

/**
* The timestamp of the last time the global state was updated on this machine.
*/
readonly ts: Timestamp;
}
4 changes: 4 additions & 0 deletions packages/client/src/storage/index.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type { ContextTypes } from './context.mjs';
export { setContext, updateDocumentRelatedContext } from './context.mjs';
export type { EventLogger } from './EventLogger.mjs';
export { createActivateEvent, createEventLogger, createReplaceEvent } from './EventLogger.mjs';
Loading

0 comments on commit a379c78

Please sign in to comment.