fix: address CodeRabbit review comments

This commit is contained in:
2026-04-27 20:10:33 -07:00
parent c150fce782
commit 2fbc90cf3a
13 changed files with 226 additions and 54 deletions

View File

@@ -3086,6 +3086,27 @@ test('tokenizeSubtitle uses Yomitan word classes to classify standalone particle
assert.equal(result.tokens?.[0]?.jlptLevel, undefined); assert.equal(result.tokens?.[0]?.jlptLevel, undefined);
}); });
test('tokenizeSubtitle uses Yomitan word classes to classify auxiliary subclasses', async () => {
const result = await tokenizeSubtitle(
'です',
makeDepsFromYomitanTokens(
[{ surface: 'です', reading: 'です', headword: 'です', wordClasses: ['aux-v'] }],
{
getFrequencyDictionaryEnabled: () => true,
getFrequencyRank: () => 10,
getJlptLevel: () => 'N5',
tokenizeWithMecab: async () => null,
},
),
);
assert.equal(result.tokens?.length, 1);
assert.equal(result.tokens?.[0]?.partOfSpeech, PartOfSpeech.bound_auxiliary);
assert.equal(result.tokens?.[0]?.pos1, '助動詞');
assert.equal(result.tokens?.[0]?.frequencyRank, undefined);
assert.equal(result.tokens?.[0]?.jlptLevel, undefined);
});
test('tokenizeSubtitle fills detailed MeCab POS when Yomitan word class supplies coarse POS', async () => { test('tokenizeSubtitle fills detailed MeCab POS when Yomitan word class supplies coarse POS', async () => {
const result = await tokenizeSubtitle( const result = await tokenizeSubtitle(
'は', 'は',

View File

@@ -359,7 +359,7 @@ function resolvePartOfSpeechFromYomitanWordClasses(wordClasses: string[]): {
if (wordClasses.includes('prt')) { if (wordClasses.includes('prt')) {
return { partOfSpeech: PartOfSpeech.particle, pos1: '助詞' }; return { partOfSpeech: PartOfSpeech.particle, pos1: '助詞' };
} }
if (wordClasses.includes('aux')) { if (wordClasses.some((wordClass) => wordClass === 'aux' || wordClass.startsWith('aux-'))) {
return { partOfSpeech: PartOfSpeech.bound_auxiliary, pos1: '助動詞' }; return { partOfSpeech: PartOfSpeech.bound_auxiliary, pos1: '助動詞' };
} }
if (wordClasses.some((wordClass) => wordClass.startsWith('v'))) { if (wordClasses.some((wordClass) => wordClass.startsWith('v'))) {

View File

@@ -713,6 +713,57 @@ test('annotateTokens N+1 sentence word count respects source punctuation gaps om
assert.equal(result[3]?.isNPlusOneTarget, false); assert.equal(result[3]?.isNPlusOneTarget, false);
}); });
test('annotateTokens N+1 sentence word count normalizes line breaks before gap detection', () => {
const tokens = [
makeToken({
surface: '私',
headword: '私',
pos1: '名詞',
startPos: 0,
endPos: 1,
}),
makeToken({
surface: '猫',
headword: '猫',
pos1: '名詞',
startPos: 2,
endPos: 3,
}),
makeToken({
surface: '犬',
headword: '犬',
pos1: '名詞',
startPos: 3,
endPos: 4,
}),
makeToken({
surface: 'ふざけん',
headword: 'ふざける',
partOfSpeech: PartOfSpeech.verb,
pos1: '動詞',
pos2: '自立',
startPos: 5,
endPos: 9,
}),
];
const result = annotateTokens(
tokens,
makeDeps({
isKnownWord: (text) => text === '私' || text === '猫' || text === '犬',
}),
{
minSentenceWordsForNPlusOne: 3,
sourceText: '私\r\n猫犬ふざけんなよ',
},
);
assert.equal(result[0]?.isNPlusOneTarget, false);
assert.equal(result[1]?.isNPlusOneTarget, false);
assert.equal(result[2]?.isNPlusOneTarget, false);
assert.equal(result[3]?.isNPlusOneTarget, false);
});
test('annotateTokens applies configured pos1 exclusions to both frequency and N+1', () => { test('annotateTokens applies configured pos1 exclusions to both frequency and N+1', () => {
const tokens = [ const tokens = [
makeToken({ makeToken({

View File

@@ -33,6 +33,10 @@ import {
import { applyControllerConfigUpdate } from './main/controller-config-update.js'; import { applyControllerConfigUpdate } from './main/controller-config-update.js';
import { openPlaylistBrowser as openPlaylistBrowserRuntime } from './main/runtime/playlist-browser-open'; import { openPlaylistBrowser as openPlaylistBrowserRuntime } from './main/runtime/playlist-browser-open';
import { createDiscordRpcClient } from './main/runtime/discord-rpc-client.js'; import { createDiscordRpcClient } from './main/runtime/discord-rpc-client.js';
import {
clearLinuxMpvFullscreenOverlayRefreshTimeouts,
scheduleLinuxVisibleOverlayFullscreenRefreshBurst,
} from './main/runtime/linux-mpv-fullscreen-overlay-refresh';
import { mergeAiConfig } from './ai/config'; import { mergeAiConfig } from './ai/config';
function getPasswordStoreArg(argv: string[]): string | null { function getPasswordStoreArg(argv: string[]): string | null {
@@ -1911,7 +1915,6 @@ const WINDOWS_VISIBLE_OVERLAY_BLUR_REFRESH_DELAYS_MS = [0, 25, 100, 250] as cons
const WINDOWS_VISIBLE_OVERLAY_Z_ORDER_RETRY_DELAYS_MS = [0, 48, 120, 240, 480] as const; const WINDOWS_VISIBLE_OVERLAY_Z_ORDER_RETRY_DELAYS_MS = [0, 48, 120, 240, 480] as const;
const WINDOWS_VISIBLE_OVERLAY_FOREGROUND_POLL_INTERVAL_MS = 75; const WINDOWS_VISIBLE_OVERLAY_FOREGROUND_POLL_INTERVAL_MS = 75;
const WINDOWS_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS = 200; const WINDOWS_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS = 200;
const LINUX_MPV_FULLSCREEN_OVERLAY_REFRESH_DELAYS_MS = [0, 50, 150, 300, 600] as const;
let windowsVisibleOverlayBlurRefreshTimeouts: Array<ReturnType<typeof setTimeout>> = []; let windowsVisibleOverlayBlurRefreshTimeouts: Array<ReturnType<typeof setTimeout>> = [];
let windowsVisibleOverlayZOrderRetryTimeouts: Array<ReturnType<typeof setTimeout>> = []; let windowsVisibleOverlayZOrderRetryTimeouts: Array<ReturnType<typeof setTimeout>> = [];
let windowsVisibleOverlayZOrderSyncInFlight = false; let windowsVisibleOverlayZOrderSyncInFlight = false;
@@ -1919,7 +1922,6 @@ let windowsVisibleOverlayZOrderSyncQueued = false;
let windowsVisibleOverlayForegroundPollInterval: ReturnType<typeof setInterval> | null = null; let windowsVisibleOverlayForegroundPollInterval: ReturnType<typeof setInterval> | null = null;
let lastWindowsVisibleOverlayForegroundProcessName: string | null = null; let lastWindowsVisibleOverlayForegroundProcessName: string | null = null;
let lastWindowsVisibleOverlayBlurredAtMs = 0; let lastWindowsVisibleOverlayBlurredAtMs = 0;
let linuxMpvFullscreenOverlayRefreshTimeouts: Array<ReturnType<typeof setTimeout>> = [];
function clearWindowsVisibleOverlayBlurRefreshTimeouts(): void { function clearWindowsVisibleOverlayBlurRefreshTimeouts(): void {
for (const timeout of windowsVisibleOverlayBlurRefreshTimeouts) { for (const timeout of windowsVisibleOverlayBlurRefreshTimeouts) {
@@ -1935,48 +1937,6 @@ function clearWindowsVisibleOverlayZOrderRetryTimeouts(): void {
windowsVisibleOverlayZOrderRetryTimeouts = []; windowsVisibleOverlayZOrderRetryTimeouts = [];
} }
function clearLinuxMpvFullscreenOverlayRefreshTimeouts(): void {
for (const timeout of linuxMpvFullscreenOverlayRefreshTimeouts) {
clearTimeout(timeout);
}
linuxMpvFullscreenOverlayRefreshTimeouts = [];
}
function refreshLinuxVisibleOverlayAfterMpvFullscreenChange(): void {
if (process.platform !== 'linux' || !overlayManager.getVisibleOverlayVisible()) {
return;
}
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
const mainWindow = overlayManager.getMainWindow();
if (!mainWindow || mainWindow.isDestroyed() || !mainWindow.isVisible()) {
return;
}
mainWindow.hide();
mainWindow.showInactive();
ensureOverlayWindowLevel(mainWindow);
}
function scheduleLinuxVisibleOverlayFullscreenRefreshBurst(): void {
if (process.platform !== 'linux') {
return;
}
clearLinuxMpvFullscreenOverlayRefreshTimeouts();
for (const delayMs of LINUX_MPV_FULLSCREEN_OVERLAY_REFRESH_DELAYS_MS) {
const refreshTimeout = setTimeout(() => {
linuxMpvFullscreenOverlayRefreshTimeouts = linuxMpvFullscreenOverlayRefreshTimeouts.filter(
(timeout) => timeout !== refreshTimeout,
);
refreshLinuxVisibleOverlayAfterMpvFullscreenChange();
}, delayMs);
refreshTimeout.unref?.();
linuxMpvFullscreenOverlayRefreshTimeouts.push(refreshTimeout);
}
}
function getWindowsNativeWindowHandle(window: BrowserWindow): string { function getWindowsNativeWindowHandle(window: BrowserWindow): string {
const handle = window.getNativeWindowHandle(); const handle = window.getNativeWindowHandle();
return handle.length >= 8 return handle.length >= 8
@@ -3146,6 +3106,8 @@ const {
stopTexthookerService: () => texthookerService.stop(), stopTexthookerService: () => texthookerService.stop(),
clearWindowsVisibleOverlayForegroundPollLoop: () => clearWindowsVisibleOverlayForegroundPollLoop: () =>
clearWindowsVisibleOverlayForegroundPollLoop(), clearWindowsVisibleOverlayForegroundPollLoop(),
clearLinuxMpvFullscreenOverlayRefreshTimeouts: () =>
clearLinuxMpvFullscreenOverlayRefreshTimeouts(),
getMainOverlayWindow: () => overlayManager.getMainWindow(), getMainOverlayWindow: () => overlayManager.getMainWindow(),
clearMainOverlayWindow: () => overlayManager.setMainWindow(null), clearMainOverlayWindow: () => overlayManager.setMainWindow(null),
getModalOverlayWindow: () => overlayManager.getModalWindow(), getModalOverlayWindow: () => overlayManager.getModalWindow(),
@@ -3851,7 +3813,14 @@ const {
lastObservedTimePos = time; lastObservedTimePos = time;
}, },
onFullscreenChange: () => { onFullscreenChange: () => {
scheduleLinuxVisibleOverlayFullscreenRefreshBurst(); scheduleLinuxVisibleOverlayFullscreenRefreshBurst({
overlayManager: {
getMainWindow: () => overlayManager.getMainWindow(),
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
},
overlayVisibilityRuntime,
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
});
}, },
onSubtitleTrackChange: (sid) => { onSubtitleTrackChange: (sid) => {
scheduleSubtitlePrefetchRefresh(); scheduleSubtitlePrefetchRefresh();

View File

@@ -18,6 +18,8 @@ test('on will quit cleanup handler runs all cleanup steps', () => {
stopTexthookerService: () => calls.push('stop-texthooker'), stopTexthookerService: () => calls.push('stop-texthooker'),
clearWindowsVisibleOverlayForegroundPollLoop: () => clearWindowsVisibleOverlayForegroundPollLoop: () =>
calls.push('clear-windows-visible-overlay-poll'), calls.push('clear-windows-visible-overlay-poll'),
clearLinuxMpvFullscreenOverlayRefreshTimeouts: () =>
calls.push('clear-linux-mpv-fullscreen-overlay-refresh-timeouts'),
destroyMainOverlayWindow: () => calls.push('destroy-main-overlay-window'), destroyMainOverlayWindow: () => calls.push('destroy-main-overlay-window'),
destroyModalOverlayWindow: () => calls.push('destroy-modal-overlay-window'), destroyModalOverlayWindow: () => calls.push('destroy-modal-overlay-window'),
destroyYomitanParserWindow: () => calls.push('destroy-yomitan-window'), destroyYomitanParserWindow: () => calls.push('destroy-yomitan-window'),
@@ -42,10 +44,11 @@ test('on will quit cleanup handler runs all cleanup steps', () => {
}); });
cleanup(); cleanup();
assert.equal(calls.length, 29); assert.equal(calls.length, 30);
assert.equal(calls[0], 'destroy-tray'); assert.equal(calls[0], 'destroy-tray');
assert.equal(calls[calls.length - 1], 'stop-discord-presence'); assert.equal(calls[calls.length - 1], 'stop-discord-presence');
assert.ok(calls.includes('clear-windows-visible-overlay-poll')); assert.ok(calls.includes('clear-windows-visible-overlay-poll'));
assert.ok(calls.includes('clear-linux-mpv-fullscreen-overlay-refresh-timeouts'));
assert.ok(calls.indexOf('flush-mpv-log') < calls.indexOf('destroy-socket')); assert.ok(calls.indexOf('flush-mpv-log') < calls.indexOf('destroy-socket'));
}); });

View File

@@ -7,6 +7,7 @@ export function createOnWillQuitCleanupHandler(deps: {
stopSubtitleWebsocket: () => void; stopSubtitleWebsocket: () => void;
stopTexthookerService: () => void; stopTexthookerService: () => void;
clearWindowsVisibleOverlayForegroundPollLoop: () => void; clearWindowsVisibleOverlayForegroundPollLoop: () => void;
clearLinuxMpvFullscreenOverlayRefreshTimeouts: () => void;
destroyMainOverlayWindow: () => void; destroyMainOverlayWindow: () => void;
destroyModalOverlayWindow: () => void; destroyModalOverlayWindow: () => void;
destroyYomitanParserWindow: () => void; destroyYomitanParserWindow: () => void;
@@ -38,6 +39,7 @@ export function createOnWillQuitCleanupHandler(deps: {
deps.stopSubtitleWebsocket(); deps.stopSubtitleWebsocket();
deps.stopTexthookerService(); deps.stopTexthookerService();
deps.clearWindowsVisibleOverlayForegroundPollLoop(); deps.clearWindowsVisibleOverlayForegroundPollLoop();
deps.clearLinuxMpvFullscreenOverlayRefreshTimeouts();
deps.destroyMainOverlayWindow(); deps.destroyMainOverlayWindow();
deps.destroyModalOverlayWindow(); deps.destroyModalOverlayWindow();
deps.destroyYomitanParserWindow(); deps.destroyYomitanParserWindow();

View File

@@ -20,6 +20,8 @@ test('cleanup deps builder returns handlers that guard optional runtime objects'
stopTexthookerService: () => calls.push('stop-texthooker'), stopTexthookerService: () => calls.push('stop-texthooker'),
clearWindowsVisibleOverlayForegroundPollLoop: () => clearWindowsVisibleOverlayForegroundPollLoop: () =>
calls.push('clear-windows-visible-overlay-foreground-poll-loop'), calls.push('clear-windows-visible-overlay-foreground-poll-loop'),
clearLinuxMpvFullscreenOverlayRefreshTimeouts: () =>
calls.push('clear-linux-mpv-fullscreen-overlay-refresh-timeouts'),
getMainOverlayWindow: () => ({ getMainOverlayWindow: () => ({
isDestroyed: () => false, isDestroyed: () => false,
destroy: () => calls.push('destroy-main-overlay-window'), destroy: () => calls.push('destroy-main-overlay-window'),
@@ -88,6 +90,7 @@ test('cleanup deps builder returns handlers that guard optional runtime objects'
assert.ok(calls.includes('stop-jellyfin-remote')); assert.ok(calls.includes('stop-jellyfin-remote'));
assert.ok(calls.includes('stop-discord-presence')); assert.ok(calls.includes('stop-discord-presence'));
assert.ok(calls.includes('clear-windows-visible-overlay-foreground-poll-loop')); assert.ok(calls.includes('clear-windows-visible-overlay-foreground-poll-loop'));
assert.ok(calls.includes('clear-linux-mpv-fullscreen-overlay-refresh-timeouts'));
assert.equal(reconnectTimer, null); assert.equal(reconnectTimer, null);
assert.equal(immersionTracker, null); assert.equal(immersionTracker, null);
}); });
@@ -103,6 +106,7 @@ test('cleanup deps builder skips destroyed yomitan window', () => {
stopSubtitleWebsocket: () => {}, stopSubtitleWebsocket: () => {},
stopTexthookerService: () => {}, stopTexthookerService: () => {},
clearWindowsVisibleOverlayForegroundPollLoop: () => {}, clearWindowsVisibleOverlayForegroundPollLoop: () => {},
clearLinuxMpvFullscreenOverlayRefreshTimeouts: () => {},
getMainOverlayWindow: () => ({ getMainOverlayWindow: () => ({
isDestroyed: () => true, isDestroyed: () => true,
destroy: () => calls.push('destroy-main-overlay-window'), destroy: () => calls.push('destroy-main-overlay-window'),

View File

@@ -26,6 +26,7 @@ export function createBuildOnWillQuitCleanupDepsHandler(deps: {
stopSubtitleWebsocket: () => void; stopSubtitleWebsocket: () => void;
stopTexthookerService: () => void; stopTexthookerService: () => void;
clearWindowsVisibleOverlayForegroundPollLoop: () => void; clearWindowsVisibleOverlayForegroundPollLoop: () => void;
clearLinuxMpvFullscreenOverlayRefreshTimeouts: () => void;
getMainOverlayWindow: () => DestroyableWindow | null; getMainOverlayWindow: () => DestroyableWindow | null;
clearMainOverlayWindow: () => void; clearMainOverlayWindow: () => void;
getModalOverlayWindow: () => DestroyableWindow | null; getModalOverlayWindow: () => DestroyableWindow | null;
@@ -67,6 +68,8 @@ export function createBuildOnWillQuitCleanupDepsHandler(deps: {
stopTexthookerService: () => deps.stopTexthookerService(), stopTexthookerService: () => deps.stopTexthookerService(),
clearWindowsVisibleOverlayForegroundPollLoop: () => clearWindowsVisibleOverlayForegroundPollLoop: () =>
deps.clearWindowsVisibleOverlayForegroundPollLoop(), deps.clearWindowsVisibleOverlayForegroundPollLoop(),
clearLinuxMpvFullscreenOverlayRefreshTimeouts: () =>
deps.clearLinuxMpvFullscreenOverlayRefreshTimeouts(),
destroyMainOverlayWindow: () => { destroyMainOverlayWindow: () => {
const window = deps.getMainOverlayWindow(); const window = deps.getMainOverlayWindow();
if (!window) return; if (!window) return;

View File

@@ -22,6 +22,7 @@ test('composeStartupLifecycleHandlers returns callable startup lifecycle handler
stopSubtitleWebsocket: () => {}, stopSubtitleWebsocket: () => {},
stopTexthookerService: () => {}, stopTexthookerService: () => {},
clearWindowsVisibleOverlayForegroundPollLoop: () => {}, clearWindowsVisibleOverlayForegroundPollLoop: () => {},
clearLinuxMpvFullscreenOverlayRefreshTimeouts: () => {},
getMainOverlayWindow: () => null, getMainOverlayWindow: () => null,
clearMainOverlayWindow: () => {}, clearMainOverlayWindow: () => {},
getModalOverlayWindow: () => null, getModalOverlayWindow: () => null,

View File

@@ -0,0 +1,47 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
clearLinuxMpvFullscreenOverlayRefreshTimeouts,
scheduleLinuxVisibleOverlayFullscreenRefreshBurst,
} from './linux-mpv-fullscreen-overlay-refresh';
test('linux mpv fullscreen overlay refresh burst schedules overlay refresh work on linux', async () => {
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
Object.defineProperty(process, 'platform', {
configurable: true,
value: 'linux',
});
const calls: string[] = [];
try {
scheduleLinuxVisibleOverlayFullscreenRefreshBurst({
overlayManager: {
getMainWindow: () =>
({
hide: () => calls.push('hide'),
isDestroyed: () => false,
isVisible: () => true,
showInactive: () => calls.push('showInactive'),
}) as never,
getVisibleOverlayVisible: () => true,
},
overlayVisibilityRuntime: {
updateVisibleOverlayVisibility: () => calls.push('updateVisibleOverlayVisibility'),
},
ensureOverlayWindowLevel: () => calls.push('ensureOverlayWindowLevel'),
});
await new Promise((resolve) => setTimeout(resolve, 10));
assert.ok(calls.includes('updateVisibleOverlayVisibility'));
assert.ok(calls.includes('hide'));
assert.ok(calls.includes('showInactive'));
assert.ok(calls.includes('ensureOverlayWindowLevel'));
} finally {
clearLinuxMpvFullscreenOverlayRefreshTimeouts();
if (originalPlatformDescriptor) {
Object.defineProperty(process, 'platform', originalPlatformDescriptor);
}
}
});

View File

@@ -0,0 +1,68 @@
type LinuxMpvFullscreenOverlayWindow = {
hide: () => void;
isDestroyed: () => boolean;
isVisible: () => boolean;
showInactive: () => void;
};
export type LinuxMpvFullscreenOverlayRefreshDeps = {
overlayManager: {
getMainWindow: () => LinuxMpvFullscreenOverlayWindow | null;
getVisibleOverlayVisible: () => boolean;
};
overlayVisibilityRuntime: {
updateVisibleOverlayVisibility: () => void;
};
ensureOverlayWindowLevel: (window: LinuxMpvFullscreenOverlayWindow) => void;
};
const LINUX_MPV_FULLSCREEN_OVERLAY_REFRESH_DELAYS_MS = [0, 50, 150, 300, 600] as const;
let linuxMpvFullscreenOverlayRefreshTimeouts: Array<ReturnType<typeof setTimeout>> = [];
function clearLinuxMpvFullscreenOverlayRefreshTimeouts(): void {
for (const timeout of linuxMpvFullscreenOverlayRefreshTimeouts) {
clearTimeout(timeout);
}
linuxMpvFullscreenOverlayRefreshTimeouts = [];
}
function refreshLinuxVisibleOverlayAfterMpvFullscreenChange(
deps: LinuxMpvFullscreenOverlayRefreshDeps,
): void {
if (process.platform !== 'linux' || !deps.overlayManager.getVisibleOverlayVisible()) {
return;
}
deps.overlayVisibilityRuntime.updateVisibleOverlayVisibility();
const mainWindow = deps.overlayManager.getMainWindow();
if (!mainWindow || mainWindow.isDestroyed() || !mainWindow.isVisible()) {
return;
}
mainWindow.hide();
mainWindow.showInactive();
deps.ensureOverlayWindowLevel(mainWindow);
}
export function scheduleLinuxVisibleOverlayFullscreenRefreshBurst(
deps: LinuxMpvFullscreenOverlayRefreshDeps,
): void {
if (process.platform !== 'linux') {
return;
}
clearLinuxMpvFullscreenOverlayRefreshTimeouts();
for (const delayMs of LINUX_MPV_FULLSCREEN_OVERLAY_REFRESH_DELAYS_MS) {
const refreshTimeout = setTimeout(() => {
linuxMpvFullscreenOverlayRefreshTimeouts = linuxMpvFullscreenOverlayRefreshTimeouts.filter(
(timeout) => timeout !== refreshTimeout,
);
refreshLinuxVisibleOverlayAfterMpvFullscreenChange(deps);
}, delayMs);
refreshTimeout.unref?.();
linuxMpvFullscreenOverlayRefreshTimeouts.push(refreshTimeout);
}
}
export { clearLinuxMpvFullscreenOverlayRefreshTimeouts };

View File

@@ -177,8 +177,7 @@ export function mergeTokens(
} }
const result: MergedToken[] = []; const result: MergedToken[] = [];
const normalizedSourceText = const normalizedSourceText = normalizeSourceTextForTokenOffsets(sourceText);
typeof sourceText === 'string' ? sourceText.replace(/\r?\n/g, ' ').trim() : null;
let charOffset = 0; let charOffset = 0;
let sourceCursor = 0; let sourceCursor = 0;
let lastStandaloneToken: Token | null = null; let lastStandaloneToken: Token | null = null;
@@ -191,7 +190,9 @@ export function mergeTokens(
for (const token of tokens) { for (const token of tokens) {
const matchedStart = const matchedStart =
normalizedSourceText !== null ? normalizedSourceText.indexOf(token.word, sourceCursor) : -1; typeof normalizedSourceText === 'string'
? normalizedSourceText.indexOf(token.word, sourceCursor)
: -1;
const start = matchedStart >= sourceCursor ? matchedStart : charOffset; const start = matchedStart >= sourceCursor ? matchedStart : charOffset;
const end = start + token.word.length; const end = start + token.word.length;
charOffset = end; charOffset = end;
@@ -302,6 +303,10 @@ function isKanaOnlyText(text: string): boolean {
return normalized.length > 0 && Array.from(normalized).every((char) => isKanaChar(char)); return normalized.length > 0 && Array.from(normalized).every((char) => isKanaChar(char));
} }
function normalizeSourceTextForTokenOffsets(sourceText: string | undefined): string | undefined {
return typeof sourceText === 'string' ? sourceText.replace(/\r?\n/g, ' ').trim() : undefined;
}
export function isNPlusOneCandidateToken( export function isNPlusOneCandidateToken(
token: MergedToken, token: MergedToken,
pos1Exclusions: ReadonlySet<string> = N_PLUS_ONE_IGNORED_POS1, pos1Exclusions: ReadonlySet<string> = N_PLUS_ONE_IGNORED_POS1,
@@ -394,8 +399,7 @@ export function markNPlusOneTargets(
return []; return [];
} }
const normalizedSourceText = const normalizedSourceText = normalizeSourceTextForTokenOffsets(sourceText);
typeof sourceText === 'string' ? sourceText.replace(/\r?\n/g, ' ').trim() : undefined;
const markedTokens = tokens.map((token) => ({ const markedTokens = tokens.map((token) => ({
...token, ...token,

View File

@@ -302,11 +302,10 @@ export class HyprlandWindowTracker extends BaseWindowTracker {
} }
private scheduleGeometryPollBurst(): void { private scheduleGeometryPollBurst(): void {
this.pollGeometry();
for (const timeout of this.pollTimeouts) { for (const timeout of this.pollTimeouts) {
clearTimeout(timeout); clearTimeout(timeout);
} }
this.pollTimeouts = [50, 150, 300].map((delayMs) => { this.pollTimeouts = [0, 50, 150, 300].map((delayMs) => {
const pollTimeout = setTimeout(() => { const pollTimeout = setTimeout(() => {
this.pollTimeouts = this.pollTimeouts.filter((timeout) => timeout !== pollTimeout); this.pollTimeouts = this.pollTimeouts.filter((timeout) => timeout !== pollTimeout);
this.pollGeometry(); this.pollGeometry();