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
39 changes: 38 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches: [main, release/**]
pull_request:
branches: [main, release/**]
workflow_dispatch:

# Least-privilege default: CI only needs to read the repository.
Expand Down Expand Up @@ -58,6 +57,44 @@ jobs:
- name: Tests
run: pnpm exec jest --selectProjects ${{ matrix.project }} --runInBand

e2e:
name: Test (web e2e)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: pnpm/action-setup@v6.0.9
with:
version: 10
- name: Set up Node
uses: actions/setup-node@v7
with:
node-version: '24'
cache: 'pnpm'
- name: Install Packages
run: pnpm run ci:install
- name: Install Chromium
run: pnpm exec playwright install --with-deps chromium
- name: Tests
run: pnpm run test:e2e:web
- name: Upload Playwright HTML report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report-web
path: lana/playwright-report/web
if-no-files-found: ignore
retention-days: 14
- name: Upload Playwright test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-test-results-web
path: lana/test-results/web
if-no-files-found: ignore
retention-days: 14

build:
name: Verify VSCode Package Build
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ build/
out/
coverage/
*.tsbuildinfo
/.vscode-test-web
/lana/test-results
/lana/playwright-report
5 changes: 2 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}/lana", "--disable-extensions"],
"args": ["--extensionDevelopmentPath=${workspaceFolder}/lana"],
"outFiles": ["${workspaceFolder}/lana/out/**/*.js"],
"localRoot": "${workspaceFolder}/lana"
},
Expand All @@ -21,8 +21,7 @@
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/${input:worktree}/lana",
"--disable-extensions"
"--extensionDevelopmentPath=${workspaceFolder}/${input:worktree}/lana"
],
"outFiles": ["${workspaceFolder}/${input:worktree}/lana/out/**/*.js"],
"localRoot": "${workspaceFolder}/${input:worktree}/lana"
Expand Down
3 changes: 1 addition & 2 deletions DEVELOPING.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,7 @@ pnpm install
2. Package the extension:

```zsh
cd lana
vsce package --no-dependencies
pnpm --filter lana run build:vsix
```

This command will create a `.vsix` file that you can distribute or install locally.
2 changes: 2 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export default defineConfig(
'**/out/',
'**/coverage/',
'**/.docusaurus/',
'**/.vscode-test-web/',
'**/playwright-report/',
// only TypeScript is linted; without this, `eslint .` selects js/mjs/cjs
// by default and scans them with no rules
'**/*.js',
Expand Down
6 changes: 5 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ const defaultConfig = {
},
],
},
testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/out/'],
testPathIgnorePatterns: [
'<rootDir>/node_modules/',
'<rootDir>/out/',
'<rootDir>/test/playwright/',
],
testMatch: ['**/?(*.)+(spec|test).ts'],
extensionsToTreatAsEsm: ['.ts', '.tsx'],
};
Expand Down
27 changes: 20 additions & 7 deletions lana/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
"profiling",
"soql"
],
"type": "module",
"main": "out/Main.js",
"browser": "out/web/Main.web.js",
"icon": "./certinia-icon-color.png",
"galleryBanner": {
"color": "#000000",
Expand All @@ -52,9 +52,19 @@
"categories": [
"Other"
],
"extensionDependencies": [
"salesforce.salesforcedx-vscode-services"
],
"capabilities": {
"virtualWorkspaces": true,
"untrustedWorkspaces": {
"supported": true
}
},
"activationEvents": [
"onLanguage:apexlog",
"onStartupFinished"
"onStartupFinished",
"onFileSystem:memfs"
],
"contributes": {
"commands": [
Expand Down Expand Up @@ -94,8 +104,7 @@
"menus": {
"commandPalette": [
{
"command": "lana.showLogAnalysis",
"when": "resourceLangId == apexlog || lana.isApexLog"
"command": "lana.showLogAnalysis"
}
],
"editor/context": [
Expand Down Expand Up @@ -378,17 +387,21 @@
]
},
"scripts": {
"vscode:prepublish": "pnpm -w run build && pnpm -w run copy:package-docs"
"build:vsix": "vsce package --no-dependencies",
"vscode:prepublish": "pnpm -w run build && pnpm -w run copy:package-docs",
"vscode:bundle": "pnpm -w run build"
},
"dependencies": {
"@apexdevtools/apex-parser": "5.1.0",
"@salesforce/apex-node": "^9.0.0",
"@salesforce/core": "^9.1.0"
"@salesforce/vscode-services": "^67.12.0",
"effect": "^3.22.0",
"vscode-uri": "^3.1.0"
},
"devDependencies": {
"@types/jest": "^30.0.0",
"@types/node": "~22.20.1",
"@types/vscode": "~1.102.0",
"@vscode/vsce": "^3.9.2",
"typescript": "npm:@typescript/typescript6@^6.0.2"
}
}
7 changes: 5 additions & 2 deletions lana/src/Main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import type { ExtensionContext } from 'vscode';

import { Context } from './Context.js';
import { Display } from './display/Display.js';
import { disposeServices, initServices } from './services/servicesRuntime.js';

export let context: Context | null = null;

export function activate(extensionContext: ExtensionContext) {
export async function activate(extensionContext: ExtensionContext) {
await initServices();
context = new Context(extensionContext, new Display());
}

export function deactivate() {
export async function deactivate() {
context = null;
await disposeServices();
}
5 changes: 5 additions & 0 deletions lana/src/Main.web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/*
* Copyright (c) 2026 Certinia Inc. All rights reserved.
*/

export { activate, context, deactivate } from './Main.js';
1 change: 1 addition & 0 deletions lana/src/__tests__/helpers/test-builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ export interface MockContext {
context: MockExtensionContext;
display: MockDisplay;
workspaces: { uri: { fsPath: string }; name: string }[];
workspaceManager?: unknown;
}

/**
Expand Down
41 changes: 14 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,34 +111,11 @@ 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),
};

// Mock RelativePattern (constructor used for glob searches)
Expand Down Expand Up @@ -300,6 +278,10 @@ export const workspace = {
},
};

export const extensions = {
getExtension: jest.fn(),
};

// Mock window
export const window = {
showInformationMessage: jest.fn().mockResolvedValue(undefined),
Expand Down Expand Up @@ -342,6 +324,10 @@ export const window = {
replace: jest.fn(),
})),
createWebviewPanel: jest.fn(),
tabGroups: {
activeTabGroup: { activeTab: undefined },
onDidChangeTabs: jest.fn(() => ({ dispose: jest.fn() })),
},
activeTextEditor: undefined as unknown,
visibleTextEditors: [],
onDidChangeActiveTextEditor: jest.fn(() => ({ dispose: jest.fn() })),
Expand Down Expand Up @@ -558,6 +544,7 @@ export default {
ThemeColor,
ConfigurationTarget,
workspace,
extensions,
window,
commands,
languages,
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
Loading