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
3 changes: 2 additions & 1 deletion lana/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,8 @@
},
"dependencies": {
"@apexdevtools/apex-parser": "5.1.0",
"effect": "^3.22.0"
"effect": "^3.22.0",
"vscode-uri": "^3.1.0"
},
"devDependencies": {
"@salesforce/vscode-services": "^67.12.0",
Expand Down
3 changes: 3 additions & 0 deletions lana/src/__tests__/helpers/test-builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export function createMockApexLog(overrides: PartialApexLog = {}): ApexLog {
export interface MockDisplay {
output: jest.Mock;
showErrorMessage: jest.Mock;
showFile: jest.Mock;
showInformationMessage: jest.Mock;
showWarningMessage: jest.Mock;
}
Expand All @@ -167,6 +168,7 @@ export function createMockDisplay(): MockDisplay {
return {
output: jest.fn(),
showErrorMessage: jest.fn(),
showFile: jest.fn(),
showInformationMessage: jest.fn(),
showWarningMessage: jest.fn(),
};
Expand All @@ -179,6 +181,7 @@ export interface MockContext {
context: MockExtensionContext;
display: MockDisplay;
workspaces: { uri: { fsPath: string }; name: string }[];
workspaceManager?: unknown;
}

/**
Expand Down
50 changes: 23 additions & 27 deletions lana/src/__tests__/mocks/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// a drift from `@types/vscode` surfaces as ONE error at the factory, not at
// every call site.
import type { EndOfLine, TextDocument } from 'vscode';
import { URI, Utils } from 'vscode-uri';

// Track subscriptions for cleanup
const subscriptions: { dispose: jest.Mock }[] = [];
Expand Down Expand Up @@ -110,36 +111,21 @@ export const ViewColumn = {
} as const;
export type ViewColumn = (typeof ViewColumn)[keyof typeof ViewColumn];

// Mock Uri class
// Delegate URI semantics to vscode-uri so virtual URI tests match VS Code.
export const Uri = {
file: jest.fn((path: string) => ({
scheme: 'file',
authority: '',
path,
fsPath: path,
query: '',
fragment: '',
with: jest.fn(),
toString: jest.fn(() => `file://${path}`),
toJSON: jest.fn(() => ({ scheme: 'file', path, fsPath: path })),
})),
parse: jest.fn((value: string) => ({
scheme: value.startsWith('file://') ? 'file' : 'unknown',
authority: '',
path: value.replace('file://', ''),
fsPath: value.replace('file://', ''),
query: '',
fragment: '',
with: jest.fn(),
toString: jest.fn(() => value),
})),
joinPath: jest.fn((base, ...pathSegments) => ({
...base,
path: [base.path, ...pathSegments].join('/'),
fsPath: [base.fsPath, ...pathSegments].join('/'),
})),
file: (path: string) => URI.file(path),
parse: (value: string) => URI.parse(value),
joinPath: (base: URI, ...pathSegments: string[]) => Utils.joinPath(base, ...pathSegments),
};

export class TabInputText {
readonly uri: ReturnType<typeof Uri.parse>;

constructor(uri: ReturnType<typeof Uri.parse>) {
this.uri = uri;
}
}

// Mock RelativePattern (constructor used for glob searches)
export const RelativePattern = jest.fn();

Expand Down Expand Up @@ -346,6 +332,12 @@ export const window = {
replace: jest.fn(),
})),
createWebviewPanel: jest.fn(),
tabGroups: {
activeTabGroup: { activeTab: undefined as { input: unknown } | undefined },
onDidChangeTabs: jest.fn((_listener: (event: unknown) => unknown) => ({
dispose: jest.fn(),
})),
},
activeTextEditor: undefined as unknown,
visibleTextEditors: [],
onDidChangeActiveTextEditor: jest.fn(() => ({ dispose: jest.fn() })),
Expand All @@ -372,6 +364,7 @@ export const commands = {

// Mock languages
export const languages = {
setTextDocumentLanguage: jest.fn().mockResolvedValue(undefined),
registerFoldingRangeProvider: jest.fn((_selector, _provider) => {
const disposable = { dispose: jest.fn() };
subscriptions.push(disposable);
Expand Down Expand Up @@ -541,10 +534,12 @@ export const resetMocks = (): void => {

// Reset workspace folders
workspace.workspaceFolders = [];
workspace.textDocuments = [];

// Reset active editor
window.activeTextEditor = undefined;
window.visibleTextEditors = [];
window.tabGroups.activeTabGroup.activeTab = undefined;
};

// Export as default for module replacement
Expand All @@ -554,6 +549,7 @@ export default {
Selection,
ViewColumn,
Uri,
TabInputText,
RelativePattern,
FoldingRange,
FoldingRangeKind,
Expand Down
20 changes: 10 additions & 10 deletions lana/src/cache/LogEventCache.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/*
* Copyright (c) 2026 Certinia Inc. All rights reserved.
*/
import { readFile } from 'fs/promises';
import { workspace } from 'vscode';

import { parse, type ApexLog, type LogEvent } from 'apex-log-parser';

import type { Context } from '../Context.js';
import { readFile } from '../services/salesforceServices.js';

export interface EventSearchResult {
event: LogEvent;
Expand All @@ -17,17 +17,17 @@ export class LogEventCache {
private static readonly MAX_CACHE_SIZE = 10;
private static cache = new Map<string, ApexLog>();

static async getApexLog(filePath: string): Promise<ApexLog | null> {
const cached = LogEventCache.cache.get(filePath);
static async getApexLog(uriString: string): Promise<ApexLog | null> {
const cached = LogEventCache.cache.get(uriString);
if (cached) {
// Move to end (most recently used)
LogEventCache.cache.delete(filePath);
LogEventCache.cache.set(filePath, cached);
LogEventCache.cache.delete(uriString);
LogEventCache.cache.set(uriString, cached);
return cached;
}

try {
const content = await readFile(filePath, 'utf-8');
const content = await readFile(uriString);
const apexLog = parse(content);

// Evict oldest if at capacity
Expand All @@ -38,7 +38,7 @@ export class LogEventCache {
}
}

LogEventCache.cache.set(filePath, apexLog);
LogEventCache.cache.set(uriString, apexLog);
return apexLog;
} catch {
return null;
Expand All @@ -49,15 +49,15 @@ export class LogEventCache {
return LogEventCache.searchEvents(apexLog.children, timestamp, 0);
}

static clearCache(filePath: string): void {
LogEventCache.cache.delete(filePath);
static clearCache(uriString: string): void {
LogEventCache.cache.delete(uriString);
}

static apply(context: Context): void {
context.context.subscriptions.push(
workspace.onDidCloseTextDocument((doc) => {
if (doc.languageId === 'apexlog') {
LogEventCache.clearCache(doc.uri.fsPath);
LogEventCache.clearCache(doc.uri.toString());
}
}),
);
Expand Down
24 changes: 11 additions & 13 deletions lana/src/cache/__tests__/LogEventCache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* Copyright (c) 2026 Certinia Inc. All rights reserved.
*/
import { beforeEach, describe, expect, it } from '@jest/globals';

import { workspace } from 'vscode';

import {
Expand All @@ -12,18 +11,17 @@ import {
} from '../../__tests__/helpers/test-builders.js';
import { LogEventCache } from '../LogEventCache.js';

// Mock fs/promises
jest.mock('fs/promises', () => ({
readFile: jest.fn(),
}));

// Mock apex-log-parser
jest.mock('apex-log-parser', () => ({
parse: jest.fn(),
}));

import { parse } from 'apex-log-parser';
import { readFile } from 'fs/promises';
import { readFile } from '../../services/salesforceServices.js';

jest.mock('../../services/salesforceServices.js', () => ({
readFile: jest.fn(),
}));

const mockReadFile = readFile as jest.Mock;
const mockParse = parse as jest.Mock;
Expand Down Expand Up @@ -373,8 +371,8 @@ describe('LogEventCache', () => {
await LogEventCache.getApexLog('/test/file.log');

// Capture the callback
let closeCallback: ((doc: { languageId: string; uri: { fsPath: string } }) => void) | null =
null;
let closeCallback:
((doc: { languageId: string; uri: { toString: () => string } }) => void) | null = null;
(workspace.onDidCloseTextDocument as jest.Mock).mockImplementationOnce((cb) => {
closeCallback = cb;
return { dispose: jest.fn() };
Expand All @@ -386,7 +384,7 @@ describe('LogEventCache', () => {
// Simulate closing an apexlog document
closeCallback!({
languageId: 'apexlog',
uri: { fsPath: '/test/file.log' },
uri: { toString: () => '/test/file.log' },
});

// @ts-expect-error - accessing private static for testing
Expand All @@ -401,8 +399,8 @@ describe('LogEventCache', () => {
await LogEventCache.getApexLog('/test/file.log');

// Capture the callback
let closeCallback: ((doc: { languageId: string; uri: { fsPath: string } }) => void) | null =
null;
let closeCallback:
((doc: { languageId: string; uri: { toString: () => string } }) => void) | null = null;
(workspace.onDidCloseTextDocument as jest.Mock).mockImplementationOnce((cb) => {
closeCallback = cb;
return { dispose: jest.fn() };
Expand All @@ -414,7 +412,7 @@ describe('LogEventCache', () => {
// Simulate closing a non-apexlog document
closeCallback!({
languageId: 'javascript',
uri: { fsPath: '/test/file.log' },
uri: { toString: () => '/test/file.log' },
});

// @ts-expect-error - accessing private static for testing
Expand Down
6 changes: 1 addition & 5 deletions lana/src/codelenses/ShowAnalysisCodeLens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@ class ShowAnalysisCodeLens implements CodeLensProvider {
}

static apply(context: Context): void {
const docSelector = [
{ scheme: 'file', language: 'apexlog' },
{ scheme: 'file', pattern: '**/*.log' },
{ scheme: 'file', pattern: '**/*.txt' },
];
const docSelector = [{ language: 'apexlog' }, { pattern: '**/*.log' }, { pattern: '**/*.txt' }];

const codeLensProviderDisposable = languages.registerCodeLensProvider(
docSelector,
Expand Down
Loading
Loading