diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c55a923..7b9167b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,6 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: lts/-1 - cache: pnpm - name: 📦 Install dependencies run: pnpm install --frozen-lockfile @@ -42,7 +41,6 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: lts/-1 - cache: pnpm - run: npx changelogithub env: diff --git a/shared/src/messages.ts b/shared/src/messages.ts index 70c04e7..498f26e 100644 --- a/shared/src/messages.ts +++ b/shared/src/messages.ts @@ -93,6 +93,7 @@ export const filterTree = z.object({ startsWith: z.string(), sourceFileName: z.string(), position: z.literal('').or(z.number()), + excludePathIncludes: z.string().default(''), }) export type FilterTree = z.infer diff --git a/src/handleMessages.ts b/src/handleMessages.ts index 4f321c3..e73c2f6 100644 --- a/src/handleMessages.ts +++ b/src/handleMessages.ts @@ -35,7 +35,7 @@ export function handleMessage(panel: vscode.WebviewPanel, message: unknown): voi log(...data.value) break case 'filterTree': { - showTree(data.startsWith, data.sourceFileName, data.position, false) + showTree(data.startsWith, data.sourceFileName, data.position, false, data.excludePathIncludes) break } case 'saveOpen': { diff --git a/src/traceTree.ts b/src/traceTree.ts index 0618d33..17b6f9e 100644 --- a/src/traceTree.ts +++ b/src/traceTree.ts @@ -4,6 +4,7 @@ import type { TraceData, TraceLine, TypeLine } from '../shared/src/traceData' import { getWorkspacePath } from './storage' import { postMessage } from './webview' import { traceFiles } from './appState' +import { isPathExcluded, parsePathExcludes } from './treeFilters' export interface Tree { id: number, line: TraceLine, children: Tree[], types: TypeLine[], childCnt: number, childTypeCnt: number, typeCnt: number } function getRoot(): Tree { @@ -80,13 +81,21 @@ export async function processTraceFiles() { traceTree = toTree(Object.values(traceFiles.value).flat(1), workspacePath) } -export function filterTree(startsWith: string, sourceFileName: string, position: number | '', tree = traceTree): Tree[] { +export function filterTree(startsWith: string, sourceFileName: string, position: number | '', excludePathIncludes = '', tree = traceTree): Tree[] { if (position === '') position = 0 + const pathExcludes = parsePathExcludes(excludePathIncludes) + return filterTreeInner(startsWith, sourceFileName, position, pathExcludes, tree) +} + +function filterTreeInner(startsWith: string, sourceFileName: string, position: number, pathExcludes: readonly string[], tree = traceTree): Tree[] { if (!tree) return [] + if (isPathExcluded(tree.line.args?.path, pathExcludes)) + return [] + if ( ('name' in tree.line && tree.line.name.startsWith(startsWith)) && (!sourceFileName || ((tree.line.args?.path ?? '').endsWith(sourceFileName))) @@ -95,21 +104,21 @@ export function filterTree(startsWith: string, sourceFileName: string, position: return [tree] } - return tree.children.map(child => filterTree(startsWith, sourceFileName, position, child)).flat() + return tree.children.map(child => filterTreeInner(startsWith, sourceFileName, position, pathExcludes, child)).flat() } export const treeIdNodes = new Map() let showTreeInterval: undefined | ReturnType -export function showTree(startsWith: string, sourceFileName: string, position: number | '', updateUi = true, tree = traceTree) { +export function showTree(startsWith: string, sourceFileName: string, position: number | '', updateUi = true, excludePathIncludes = '', tree = traceTree) { if (showTreeInterval) { clearInterval(showTreeInterval) showTreeInterval = undefined } - const nodes = filterTree(startsWith, sourceFileName, position, tree) + const nodes = filterTree(startsWith, sourceFileName, position, excludePathIncludes, tree) const skinnyNodes = nodes.map(x => ({ ...x, children: [], types: [] })) if (updateUi) - postMessage({ message: 'filterTree', startsWith, sourceFileName, position }) + postMessage({ message: 'filterTree', startsWith, sourceFileName, position, excludePathIncludes }) postMessage({ message: 'showTree', nodes: [], step: 'start' }) diff --git a/src/treeFilters.ts b/src/treeFilters.ts new file mode 100644 index 0000000..8575af1 --- /dev/null +++ b/src/treeFilters.ts @@ -0,0 +1,14 @@ +export function parsePathExcludes(input: string): string[] { + return input + .split(/[,\n]/) + .map(value => value.trim().replace(/\\/g, '/').toLowerCase()) + .filter(Boolean) +} + +export function isPathExcluded(path: string | undefined, excludes: readonly string[]) { + if (!path || excludes.length === 0) + return false + + const normalizedPath = path.replace(/\\/g, '/').toLowerCase() + return excludes.some(exclude => normalizedPath.includes(exclude)) +} diff --git a/test/index.test.ts b/test/index.test.ts index 401553c..bc517a7 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,7 +1,23 @@ import { describe, expect, it } from 'vitest' +import { isPathExcluded, parsePathExcludes } from '../src/treeFilters' describe('should', () => { it('exported', () => { expect(1).toEqual(1) }) + + it('parses path exclude filters', () => { + expect(parsePathExcludes('node_modules, src/generated\nlib.dom.d.ts')) + .toEqual(['node_modules', 'src/generated', 'lib.dom.d.ts']) + }) + + it('matches excluded paths case-insensitively', () => { + const excludes = parsePathExcludes('node_modules,lib.dom.d.ts') + + expect(isPathExcluded('packages/app/node_modules/typescript/lib/lib.dom.d.ts', excludes)) + .toBe(true) + + expect(isPathExcluded('src/components/Button.tsx', excludes)) + .toBe(false) + }) }) diff --git a/ui/app.vue b/ui/app.vue index bbd026b..9fc9e67 100644 --- a/ui/app.vue +++ b/ui/app.vue @@ -7,7 +7,7 @@ const sendMesage = useNuxtApp().$sendMessage const sortOptions = ['Timestamp', 'Duration', 'Types', 'Total Types'] as const -const filters = useState('treeFilters', () => ({ startsWith: 'check', sourceFileName: '', position: 0 as number | '' })) +const filters = useState('treeFilters', () => ({ startsWith: 'check', sourceFileName: '', position: 0 as number | '', excludePathIncludes: '' })) function setStartsWith(event: any) { filters.value.startsWith = event.target.value @@ -21,13 +21,17 @@ function setPosition(event: any) { filters.value.position = +event.target.value } +function setExcludePathIncludes(event: any) { + filters.value.excludePathIncludes = event.target.value +} + function handleMessage(e: MessageEvent) { const message = Messages.message.safeParse(e.data) if (!message.success) return if (message.data.message === 'gotoTracePosition') - filters.value = { startsWith: '', position: message.data.position, sourceFileName: message.data.fileName } + filters.value = { startsWith: '', position: message.data.position, sourceFileName: message.data.fileName, excludePathIncludes: filters.value.excludePathIncludes } else if (message.data.message === 'filterTree') filters.value = message.data @@ -59,6 +63,7 @@ onMounted(() => { + Filter Trace