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
18 changes: 8 additions & 10 deletions lana/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@
"Other"
],
"activationEvents": [
"onLanguage:apexlog"
"onLanguage:apexlog",
"onStartupFinished"
],
"contributes": {
"commands": [
Expand All @@ -77,44 +78,41 @@
"ApexLog",
"DebugLog"
],
"extensions": [
".log",
".txt"
],
"extensions": [],
"firstLine": "^\\d\\d.\\d.+?APEX_CODE,\\w.+$"
}
],
"menus": {
"commandPalette": [
{
"command": "lana.showLogAnalysis",
"when": "resourceLangId == apexlog"
"when": "resourceLangId == apexlog || lana.isApexLog"
}
],
"editor/context": [
{
"command": "lana.showLogAnalysis",
"when": "resourceLangId == apexlog"
"when": "resourceLangId == apexlog || lana.isApexLog"
}
],
"editor/title/context": [
{
"command": "lana.showLogAnalysis",
"when": "resourceLangId == apexlog",
"when": "resourceLangId == apexlog || lana.isApexLog",
"group": "lana"
}
],
"editor/title/run": [
{
"command": "lana.showLogAnalysis",
"when": "resourceLangId == apexlog",
"when": "resourceLangId == apexlog || lana.isApexLog",
"group": "lana"
}
],
"explorer/context": [
{
"command": "lana.showLogAnalysis",
"when": "resourceLangId == apexlog"
"when": "resourceLangId == apexlog || lana.isApexLog"
}
]
},
Expand Down
2 changes: 2 additions & 0 deletions lana/src/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { RawLogLineDecoration } from './decorations/RawLogLineDecoration.js';
import { Display } from './display/Display.js';
import { WhatsNewNotification } from './display/WhatsNewNotification.js';
import { RawLogFoldingProvider } from './folding/RawLogFoldingProvider.js';
import { ApexLogLanguageDetector } from './language/ApexLogLanguageDetector.js';
import { SymbolFinder } from './salesforce/codesymbol/SymbolFinder.js';
import { VSWorkspace } from './workspace/VSWorkspace.js';

Expand All @@ -33,6 +34,7 @@ export class Context {
});
}

ApexLogLanguageDetector.apply(this);
LogEventCache.apply(this);
RetrieveLogFile.apply(this);
ShowLogAnalysis.apply(this);
Expand Down
22 changes: 13 additions & 9 deletions lana/src/codelenses/ShowAnalysisCodeLens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@ import { CodeLens, Range, languages, type CodeLensProvider, type TextDocument }

import { Context } from '../Context.js';
import { ShowLogAnalysis } from '../commands/ShowLogAnalysis.js';
import { isApexLogContent } from '../language/ApexLogLanguageDetector.js';

class ShowAnalysisCodeLens implements CodeLensProvider {
context: Context;

constructor(context: Context) {
this.context = context;
}
// Each provider requires a provideCodeLenses function which will give the various documents the code lenses
async provideCodeLenses(_document: TextDocument): Promise<CodeLens[]> {
// Define where the CodeLens will exist

async provideCodeLenses(document: TextDocument): Promise<CodeLens[]> {
if (!isApexLogContent(document)) {
return [];
}

const topOfDocument = new Range(0, 0, 0, 0);

// Define what command we want to trigger when activating the CodeLens
const command = ShowLogAnalysis.getCommand(this.context);
const codeLens = new CodeLens(topOfDocument, {
command: command.fullName,
Expand All @@ -24,17 +28,17 @@ class ShowAnalysisCodeLens implements CodeLensProvider {
}

static apply(context: Context): void {
// Get a document selector for the CodeLens provider
// This one is any file that has the language of apexlog
const docSelector = [{ scheme: 'file', language: 'apexlog' }];
const docSelector = [
{ scheme: 'file', language: 'apexlog' },
{ scheme: 'file', pattern: '**/*.log' },
{ scheme: 'file', pattern: '**/*.txt' },
];

// Register our CodeLens provider
const codeLensProviderDisposable = languages.registerCodeLensProvider(
docSelector,
new ShowAnalysisCodeLens(context),
);

// Push the command and CodeLens provider to the context so it can be disposed of later
context.context.subscriptions.push(codeLensProviderDisposable);
}
}
Expand Down
4 changes: 2 additions & 2 deletions lana/src/decorations/LogTimingDecoration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from 'vscode';

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

// Pattern to find EXECUTION_STARTED line
Expand Down Expand Up @@ -84,8 +85,7 @@ export class LogTimingDecoration {
private updateDecorations(editor: TextEditor): void {
const document = editor.document;

// Only process apexlog files
if (document.languageId !== 'apexlog') {
if (!isApexLogContent(document)) {
editor.setDecorations(decorationType, []);
return;
}
Expand Down
3 changes: 2 additions & 1 deletion lana/src/decorations/RawLogLineDecoration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { LogEvent } from 'apex-log-parser';

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

// Decoration type for ghost text on cursor line - no isWholeLine so hover only triggers at end
Expand Down Expand Up @@ -72,7 +73,7 @@ export class RawLogLineDecoration {
private async updateDecoration(editor: TextEditor): Promise<void> {
const document = editor.document;

if (document.languageId !== 'apexlog') {
if (!isApexLogContent(document)) {
this.clearDecorations(editor);
return;
}
Expand Down
142 changes: 142 additions & 0 deletions lana/src/language/ApexLogLanguageDetector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Copyright (c) 2026 Certinia Inc. All rights reserved.
*/
import { closeSync, openSync, readSync } from 'node:fs';
import { extname } from 'node:path';

import {
TabInputText,
commands,
languages,
window,
workspace,
type TextDocument,
type Uri,
} from 'vscode';

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

const APEXLOG_HEADER = /^\d\d\.\d.+?APEX_CODE,\w.+$/;
const DETECT_EXTENSIONS = new Set(['.log', '.txt']);
const MAX_LINES_TO_CHECK = 100;

export function isApexLogContent(doc: TextDocument): boolean {
if (doc.lineCount === 0) {
return false;
}

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)) {
return true;
}
}

return false;
}

function isApexLogFile(fsPath: string): boolean {
let fd: number;
try {
fd = openSync(fsPath, 'r');
} catch {
return false;
}

try {
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 linesToCheck = Math.min(MAX_LINES_TO_CHECK, lines.length);
for (let i = 0; i < linesToCheck; i++) {
if (APEXLOG_HEADER.test(lines[i] ?? '')) {
return true;
}
}
return false;
} finally {
closeSync(fd);
}
}

function hasDetectExtension(uri: Uri): boolean {
return DETECT_EXTENSIONS.has(extname(uri.fsPath).toLowerCase());
}

function getActiveTabUri(): Uri | undefined {
const activeTab = window.tabGroups.activeTabGroup.activeTab;
if (activeTab?.input instanceof TabInputText) {
return activeTab.input.uri;
}
return undefined;
}

function updateContextKey(): void {
const editor = window.activeTextEditor;
if (editor && editor.document.uri.scheme === 'file') {
const doc = editor.document;
if (hasDetectExtension(doc.uri)) {
const detected = isApexLogContent(doc);
commands.executeCommand('setContext', 'lana.isApexLog', detected);
return;
}
commands.executeCommand('setContext', 'lana.isApexLog', false);
return;
}

// Fallback to tab API for large files where activeTextEditor is undefined
const tabUri = getActiveTabUri();
if (tabUri && tabUri.scheme === 'file' && hasDetectExtension(tabUri)) {
const detected = isApexLogFile(tabUri.fsPath);
commands.executeCommand('setContext', 'lana.isApexLog', detected);
return;
}

commands.executeCommand('setContext', 'lana.isApexLog', false);
}

export class ApexLogLanguageDetector {
static apply(context: Context): void {
for (const doc of workspace.textDocuments) {
detectAndSetLanguage(doc);
}

context.context.subscriptions.push(
workspace.onDidOpenTextDocument((doc) => {
detectAndSetLanguage(doc);
}),
);

// Update context key when the active editor or tab changes
context.context.subscriptions.push(
window.onDidChangeActiveTextEditor(() => {
updateContextKey();
}),
);

context.context.subscriptions.push(
window.tabGroups.onDidChangeTabs(() => {
updateContextKey();
}),
);

// Set initial context
updateContextKey();
}
}

function detectAndSetLanguage(doc: TextDocument): void {
if (doc.languageId === 'apexlog' || doc.uri.scheme !== 'file') {
return;
}

if (!hasDetectExtension(doc.uri)) {
return;
}

if (isApexLogContent(doc)) {
languages.setTextDocumentLanguage(doc, 'apexlog');
}
}
Loading