Skip to content
Merged
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: 1 addition & 2 deletions lana/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@
"ApexLog",
"DebugLog"
],
"extensions": [],
"firstLine": "^\\d\\d.\\d.+?APEX_CODE,\\w.+$"
"extensions": []
}
],
"menus": {
Expand Down
22 changes: 19 additions & 3 deletions lana/src/decorations/LogTimingDecoration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from 'vscode';

import { Context } from '../Context.js';
import { isApexLogContent } from '../language/ApexLogLanguageDetector.js';
import { APEXLOG_HEADER, isApexLogContent } from '../language/ApexLogLanguageDetector.js';
import { formatDuration, TIMESTAMP_REGEX } from '../log-utils.js';

// Pattern to find EXECUTION_STARTED line
Expand Down Expand Up @@ -98,8 +98,13 @@ export class LogTimingDecoration {

const formattedDuration = formatDuration(duration);

// Create decoration for line 0 (first line)
const line = document.lineAt(0);
const startLine = this.findFirstLogLine(document);
if (startLine === null) {
editor.setDecorations(decorationType, []);
return;
}

const line = document.lineAt(startLine);
const decoration: DecorationOptions = {
range: line.range,
renderOptions: {
Expand All @@ -112,6 +117,17 @@ export class LogTimingDecoration {
editor.setDecorations(decorationType, [decoration]);
}

private findFirstLogLine(doc: TextDocument): number | null {
const limit = Math.min(1000, doc.lineCount);
for (let i = 0; i < limit; i++) {
const text = doc.lineAt(i).text;
if (APEXLOG_HEADER.test(text) || TIMESTAMP_REGEX.test(text)) {
return i;
}
}
return null;
}

private calculateLogDuration(document: TextDocument): number | null {
const startTs = this.findTimestamp(document, false, executionStartedRegex);
const endTs = this.findTimestamp(document, true, TIMESTAMP_REGEX);
Expand Down
12 changes: 8 additions & 4 deletions lana/src/language/ApexLogLanguageDetector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import {

import { Context } from '../Context.js';

const APEXLOG_HEADER = /^\d\d\.\d.+?APEX_CODE,\w.+$/;
export const APEXLOG_HEADER = /^(\d\d\.\d.+?)?APEX_CODE,\w.+$/;
const EXECUTION_STARTED = /^\d{2}:\d{2}:\d{2}\.\d{1,} \(\d+\)\|EXECUTION_STARTED$/;
const USER_INFO = /^\d{2}:\d{2}:\d{2}\.\d{1,} \(\d+\)\|USER_INFO\|/;
const DETECT_EXTENSIONS = new Set(['.log', '.txt']);
const MAX_LINES_TO_CHECK = 100;

Expand All @@ -27,7 +29,8 @@ export function isApexLogContent(doc: TextDocument): boolean {

const linesToCheck = Math.min(MAX_LINES_TO_CHECK, doc.lineCount);
for (let i = 0; i < linesToCheck; i++) {
if (APEXLOG_HEADER.test(doc.lineAt(i).text)) {
const text = doc.lineAt(i).text;
if (APEXLOG_HEADER.test(text) || EXECUTION_STARTED.test(text) || USER_INFO.test(text)) {
return true;
}
}
Expand All @@ -47,11 +50,12 @@ function isApexLogFile(fsPath: string): boolean {
const buf = Buffer.alloc(4096);
const bytesRead = readSync(fd, buf, 0, 4096, 0);
const text = buf.toString('utf8', 0, bytesRead);
const lines = text.split('\n');
const lines = text.split(/\r?\n/);

const linesToCheck = Math.min(MAX_LINES_TO_CHECK, lines.length);
for (let i = 0; i < linesToCheck; i++) {
if (APEXLOG_HEADER.test(lines[i] ?? '')) {
const line = lines[i] ?? '';
if (APEXLOG_HEADER.test(line) || EXECUTION_STARTED.test(line) || USER_INFO.test(line)) {
return true;
}
}
Expand Down
95 changes: 95 additions & 0 deletions lana/src/language/__tests__/ApexLogLanguageDetector.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright (c) 2026 Certinia Inc. All rights reserved.
*/

import { createMockTextDocument } from '../../__tests__/mocks/vscode.js';
import { isApexLogContent } from '../ApexLogLanguageDetector.js';

describe('isApexLogContent', () => {
it('should detect standard log with settings header on line 1', () => {
const doc = createMockTextDocument({
lines: [
'64.0 APEX_CODE,FINE;APEX_PROFILING,NONE;CALLOUT,NONE;DB,INFO;NBA,NONE;SYSTEM,NONE;VALIDATION,NONE;VISUALFORCE,NONE;WAVE,NONE;WORKFLOW,NONE',
'09:45:31.888 (1000)|EXECUTION_STARTED',
],
});

expect(isApexLogContent(doc)).toBe(true);
});

it('should detect log with preamble text before settings header', () => {
const doc = createMockTextDocument({
lines: [
'Some preamble text from browser UI',
'Another line of preamble',
'64.0 APEX_CODE,FINE;APEX_PROFILING,NONE;CALLOUT,NONE;DB,INFO',
'09:45:31.888 (1000)|EXECUTION_STARTED',
],
});

expect(isApexLogContent(doc)).toBe(true);
});

it('should detect log without settings header but with EXECUTION_STARTED', () => {
const doc = createMockTextDocument({
lines: [
'Some preamble text',
'09:45:31.888 (1000)|EXECUTION_STARTED',
'09:45:31.889 (2000)|USER_INFO|[EXTERNAL]|user@example.com',
],
});

expect(isApexLogContent(doc)).toBe(true);
});

it('should detect log without settings header but with USER_INFO', () => {
const doc = createMockTextDocument({
lines: ['Some preamble text', '09:45:31.889 (2000)|USER_INFO|[EXTERNAL]|user@example.com'],
});

expect(isApexLogContent(doc)).toBe(true);
});

it('should detect real-world log starting with USER_INFO and no settings header', () => {
const doc = createMockTextDocument({
lines: [
'17:23:32.3 (3925848)|USER_INFO|[EXTERNAL]|0054R00000B6Q3p|luke.cotter@example.com|(GMT+00:00) Greenwich Mean Time (Europe/London)|GMT+00:00',
'17:23:32.3 (4conversionId)|EXECUTION_STARTED',
],
});

expect(isApexLogContent(doc)).toBe(true);
});

it('should detect log with settings header missing API version', () => {
const doc = createMockTextDocument({
lines: [
'APEX_CODE,FINE;APEX_PROFILING,INFO;CALLOUT,INFO;DB,FINEST;NBA,INFO;SYSTEM,DEBUG;VALIDATION,INFO;VISUALFORCE,INFO;WAVE,INFO;WORKFLOW,FINE',
'17:23:32.3 (3925848)|USER_INFO|[EXTERNAL]|0054R00000B6Q3p|luke.cotter@example.com|(GMT+00:00) Greenwich Mean Time (Europe/London)|GMT+00:00',
'17:23:32.3 (4000)|EXECUTION_STARTED',
],
});

expect(isApexLogContent(doc)).toBe(true);
});

it('should not detect non-apex log file', () => {
const doc = createMockTextDocument({
lines: [
'[2024-01-15 09:45:31] INFO: Application started',
'[2024-01-15 09:45:32] DEBUG: Loading configuration',
'[2024-01-15 09:45:33] ERROR: Connection failed',
],
});

expect(isApexLogContent(doc)).toBe(false);
});

it('should not detect empty document', () => {
const doc = createMockTextDocument({
lines: [],
});

expect(isApexLogContent(doc)).toBe(false);
});
});
Loading