mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 17:19:48 +01:00
committed by
GitHub
parent
ac721a2d45
commit
a4262a0612
@@ -41,6 +41,10 @@ import { localize, localize2 } from '../../../../nls.js';
|
||||
import { viewFilterSubmenu } from '../../../browser/parts/views/viewFilter.js';
|
||||
import { ViewAction } from '../../../browser/parts/views/viewPane.js';
|
||||
import { INotificationService } from '../../../../platform/notification/common/notification.js';
|
||||
import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';
|
||||
import { basename } from '../../../../base/common/resources.js';
|
||||
|
||||
const IMPORTED_LOG_ID_PREFIX = 'importedLog.';
|
||||
|
||||
// Register Service
|
||||
registerSingleton(IOutputService, OutputService, InstantiationType.Delayed);
|
||||
@@ -114,6 +118,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution {
|
||||
this.registerConfigureActiveOutputLogLevelAction();
|
||||
this.registerFilterActions();
|
||||
this.registerExportLogsAction();
|
||||
this.registerImportLogAction();
|
||||
}
|
||||
|
||||
private registerSwitchOutputAction(): void {
|
||||
@@ -144,7 +149,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution {
|
||||
const registerOutputChannels = (channels: IOutputChannelDescriptor[]) => {
|
||||
for (const channel of channels) {
|
||||
const title = channel.label;
|
||||
const group = channel.files && channel.files.length > 1 ? '2_compound_logs' : channel.extensionId ? '0_ext_outputchannels' : '1_core_outputchannels';
|
||||
const group = (channel.files?.length && channel.files.length > 1) || channel.id.startsWith(IMPORTED_LOG_ID_PREFIX) ? '2_custom_logs' : channel.extensionId ? '0_ext_outputchannels' : '1_core_outputchannels';
|
||||
registeredChannels.set(channel.id, registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
@@ -186,6 +191,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution {
|
||||
menu: [{
|
||||
id: MenuId.ViewTitle,
|
||||
when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID),
|
||||
group: '2_add',
|
||||
}],
|
||||
});
|
||||
}
|
||||
@@ -405,7 +411,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution {
|
||||
menu: [{
|
||||
id: MenuId.ViewTitle,
|
||||
when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID),
|
||||
group: 'export',
|
||||
group: '1_export',
|
||||
order: 1
|
||||
}],
|
||||
});
|
||||
@@ -711,7 +717,7 @@ class OutputContribution extends Disposable implements IWorkbenchContribution {
|
||||
menu: [{
|
||||
id: MenuId.ViewTitle,
|
||||
when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID),
|
||||
group: 'export',
|
||||
group: '1_export',
|
||||
order: 2,
|
||||
}],
|
||||
});
|
||||
@@ -747,6 +753,53 @@ class OutputContribution extends Disposable implements IWorkbenchContribution {
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private registerImportLogAction(): void {
|
||||
this._register(registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: `workbench.action.importLog`,
|
||||
title: nls.localize2('importLog', "Import Log..."),
|
||||
f1: true,
|
||||
category: Categories.Developer,
|
||||
menu: [{
|
||||
id: MenuId.ViewTitle,
|
||||
when: ContextKeyExpr.equals('view', OUTPUT_VIEW_ID),
|
||||
group: '2_add',
|
||||
order: 2,
|
||||
}],
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const outputService = accessor.get(IOutputService);
|
||||
const fileDialogService = accessor.get(IFileDialogService);
|
||||
const result = await fileDialogService.showOpenDialog({
|
||||
title: nls.localize('importLogFile', "Import Log File"),
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: true,
|
||||
filters: [{
|
||||
name: nls.localize('logFiles', "Log Files"),
|
||||
extensions: ['log']
|
||||
}]
|
||||
});
|
||||
|
||||
if (result?.length) {
|
||||
const channelName = basename(result[0]);
|
||||
const channelId = `${IMPORTED_LOG_ID_PREFIX}${Date.now()}`;
|
||||
// Register and show the channel
|
||||
Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).registerChannel({
|
||||
id: channelId,
|
||||
label: channelName,
|
||||
log: true,
|
||||
files: result,
|
||||
fileNames: result.map(r => basename(r).split('.')[0])
|
||||
});
|
||||
outputService.showChannel(channelId);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(OutputContribution, LifecyclePhase.Restored);
|
||||
|
||||
@@ -13,7 +13,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common
|
||||
import { IContextKeyService, IContextKey } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { IEditorOpenContext } from '../../../common/editor.js';
|
||||
import { AbstractTextResourceEditor } from '../../../browser/parts/editor/textResourceEditor.js';
|
||||
import { OUTPUT_VIEW_ID, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputService, IOutputViewFilters, OUTPUT_FILTER_FOCUS_CONTEXT, LOG_ENTRY_REGEX } from '../../../services/output/common/output.js';
|
||||
import { OUTPUT_VIEW_ID, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputService, IOutputViewFilters, OUTPUT_FILTER_FOCUS_CONTEXT, LOG_ENTRY_REGEX, parseLogEntries, ILogEntry } from '../../../services/output/common/output.js';
|
||||
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';
|
||||
@@ -340,12 +340,6 @@ export class OutputEditor extends AbstractTextResourceEditor {
|
||||
|
||||
}
|
||||
|
||||
|
||||
interface ILogEntry {
|
||||
readonly logLevel: LogLevel;
|
||||
readonly lineRange: [number, number];
|
||||
}
|
||||
|
||||
export class FilterController extends Disposable implements IEditorContribution {
|
||||
|
||||
public static readonly ID = 'output.editor.contrib.filterController';
|
||||
@@ -413,30 +407,8 @@ export class FilterController extends Disposable implements IEditorContribution
|
||||
}
|
||||
|
||||
private computeLogEntriesIncremental(model: ITextModel, fromLine: number): void {
|
||||
if (!this.logEntries) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lineCount = model.getLineCount();
|
||||
for (let lineNumber = fromLine; lineNumber <= lineCount; lineNumber++) {
|
||||
const lineContent = model.getLineContent(lineNumber);
|
||||
const match = LOG_ENTRY_REGEX.exec(lineContent);
|
||||
if (match) {
|
||||
const logLevel = this.parseLogLevel(match[3]);
|
||||
const startLine = lineNumber;
|
||||
let endLine = lineNumber;
|
||||
|
||||
while (endLine < lineCount) {
|
||||
const nextLineContent = model.getLineContent(endLine + 1);
|
||||
if (model.getLineFirstNonWhitespaceColumn(endLine + 1) === 0 || LOG_ENTRY_REGEX.test(nextLineContent)) {
|
||||
break;
|
||||
}
|
||||
endLine++;
|
||||
}
|
||||
|
||||
this.logEntries.push({ logLevel, lineRange: [startLine, endLine] });
|
||||
lineNumber = endLine;
|
||||
}
|
||||
if (this.logEntries) {
|
||||
this.logEntries = this.logEntries.concat(parseLogEntries(model, fromLine));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,17 +428,17 @@ export class FilterController extends Disposable implements IEditorContribution
|
||||
for (let i = from; i < this.logEntries.length; i++) {
|
||||
const entry = this.logEntries[i];
|
||||
if (hasLogLevelFilter && !this.shouldShowEntry(entry, filters)) {
|
||||
this.hiddenAreas.push(new Range(entry.lineRange[0], 1, entry.lineRange[1], model.getLineMaxColumn(entry.lineRange[1])));
|
||||
this.hiddenAreas.push(entry.range);
|
||||
continue;
|
||||
}
|
||||
if (filters.text) {
|
||||
const matches = model.findMatches(filters.text, new Range(entry.lineRange[0], 1, entry.lineRange[1], model.getLineLastNonWhitespaceColumn(entry.lineRange[1])), false, false, null, false);
|
||||
const matches = model.findMatches(filters.text, entry.range, false, false, null, false);
|
||||
if (matches.length) {
|
||||
for (const match of matches) {
|
||||
findMatchesDecorations.push({ range: match.range, options: FindDecorations._FIND_MATCH_DECORATION });
|
||||
}
|
||||
} else {
|
||||
this.hiddenAreas.push(new Range(entry.lineRange[0], 1, entry.lineRange[1], model.getLineMaxColumn(entry.lineRange[1])));
|
||||
this.hiddenAreas.push(entry.range);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -509,21 +481,4 @@ export class FilterController extends Disposable implements IEditorContribution
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private parseLogLevel(level: string): LogLevel {
|
||||
switch (level.toLowerCase()) {
|
||||
case 'trace':
|
||||
return LogLevel.Trace;
|
||||
case 'debug':
|
||||
return LogLevel.Debug;
|
||||
case 'info':
|
||||
return LogLevel.Info;
|
||||
case 'warning':
|
||||
return LogLevel.Warning;
|
||||
case 'error':
|
||||
return LogLevel.Error;
|
||||
default:
|
||||
throw new Error(`Unknown log level: ${level}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,9 @@ import { Range } from '../../../../editor/common/core/range.js';
|
||||
import { VSBuffer } from '../../../../base/common/buffer.js';
|
||||
import { ILogger, ILoggerService, ILogService } from '../../../../platform/log/common/log.js';
|
||||
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
|
||||
import { LOG_ENTRY_REGEX, OutputChannelUpdateMode } from '../../../services/output/common/output.js';
|
||||
import { LOG_ENTRY_REGEX, LOG_MIME, OutputChannelUpdateMode, parseLogEntryAt } from '../../../services/output/common/output.js';
|
||||
import { isCancellationError } from '../../../../base/common/errors.js';
|
||||
import { binarySearch } from '../../../../base/common/arrays.js';
|
||||
import { TextModel } from '../../../../editor/common/model/textModel.js';
|
||||
|
||||
export interface IOutputChannelModel extends IDisposable {
|
||||
readonly onDispose: Event<void>;
|
||||
@@ -168,6 +168,7 @@ class MultiFileContentProvider extends Disposable implements IContentProvider {
|
||||
|
||||
constructor(
|
||||
filesInfos: IOutputChannelFileInfo[],
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IFileService fileService: IFileService,
|
||||
@ILogService logService: ILogService,
|
||||
) {
|
||||
@@ -194,12 +195,37 @@ class MultiFileContentProvider extends Disposable implements IContentProvider {
|
||||
|
||||
async getContent(): Promise<{ readonly content: string; readonly consume: () => void }> {
|
||||
const outputs = await Promise.all(this.fileOutputs.map(output => output.getContent()));
|
||||
const content = combineLogEntries(outputs);
|
||||
const content = this.combineLogEntries(outputs);
|
||||
return {
|
||||
content,
|
||||
consume: () => outputs.forEach(({ consume }) => consume())
|
||||
};
|
||||
}
|
||||
|
||||
private combineLogEntries(outputs: { content: string; name: string }[]): string {
|
||||
|
||||
const logEntries: [number, string][] = [];
|
||||
|
||||
for (const { content, name } of outputs) {
|
||||
const model = this.instantiationService.createInstance(TextModel, content, LOG_MIME, TextModel.DEFAULT_CREATION_OPTIONS, null);
|
||||
for (let lineNumber = 1; lineNumber <= model.getLineCount(); lineNumber++) {
|
||||
const logEntry = parseLogEntryAt(model, lineNumber);
|
||||
if (!logEntry) {
|
||||
continue;
|
||||
}
|
||||
lineNumber = logEntry.range.endLineNumber;
|
||||
const content = model.getValueInRange(logEntry.range).replace(LOG_ENTRY_REGEX, `$1 [${name}] $2`);
|
||||
logEntries.push([logEntry.timestamp, content]);
|
||||
}
|
||||
}
|
||||
|
||||
let result = '';
|
||||
for (const [, content] of logEntries.sort((a, b) => a[0] - b[0])) {
|
||||
result += content + '\n';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export abstract class AbstractFileOutputChannelModel extends Disposable implements IOutputChannelModel {
|
||||
@@ -440,8 +466,9 @@ export class MultiFileOutputChannelModel extends AbstractFileOutputChannelModel
|
||||
@IModelService modelService: IModelService,
|
||||
@ILogService logService: ILogService,
|
||||
@IEditorWorkerService editorWorkerService: IEditorWorkerService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
) {
|
||||
const multifileOutput = new MultiFileContentProvider(filesInfos, fileService, logService);
|
||||
const multifileOutput = new MultiFileContentProvider(filesInfos, instantiationService, fileService, logService);
|
||||
super(modelUri, language, multifileOutput, modelService, editorWorkerService);
|
||||
this.multifileOutput = this._register(multifileOutput);
|
||||
}
|
||||
@@ -549,74 +576,3 @@ export class DelegatedOutputChannelModel extends Disposable implements IOutputCh
|
||||
this.outputChannelModel.then(outputChannelModel => outputChannelModel.replace(value));
|
||||
}
|
||||
}
|
||||
|
||||
function combineLogEntries(outputs: { content: string; name: string }[]): string {
|
||||
const timestampEntries: Date[] = [];
|
||||
const combinedEntries: string[] = [];
|
||||
|
||||
let startTimestampOfLastOutput: Date | undefined;
|
||||
let endTimestampOfLastOutput: Date | undefined;
|
||||
|
||||
for (const output of outputs) {
|
||||
let startTimestamp: Date | undefined;
|
||||
let timestamp: Date | undefined;
|
||||
const logEntries = output.content.split('\n');
|
||||
for (let index = 0; index < logEntries.length; index++) {
|
||||
const entry = logEntries[index];
|
||||
if (!entry.trim()) {
|
||||
continue;
|
||||
}
|
||||
timestamp = new Date(entry.match(LOG_ENTRY_REGEX)?.[1]!);
|
||||
if (!startTimestamp) {
|
||||
startTimestamp = timestamp;
|
||||
}
|
||||
const entriesToAdd = [entry.replace(LOG_ENTRY_REGEX, `$1 [${output.name}] $2`)];
|
||||
const timestampsToAdd = [timestamp];
|
||||
|
||||
if (startTimestampOfLastOutput && timestamp < startTimestampOfLastOutput) {
|
||||
for (index = index + 1; index < logEntries.length; index++) {
|
||||
const entry = logEntries[index];
|
||||
if (!entry.trim()) {
|
||||
continue;
|
||||
}
|
||||
timestamp = new Date(entry.match(LOG_ENTRY_REGEX)?.[1]!);
|
||||
if (timestamp > startTimestampOfLastOutput) {
|
||||
index--;
|
||||
break;
|
||||
}
|
||||
entriesToAdd.push(entry.replace(LOG_ENTRY_REGEX, `$1 [${output.name}] $2`));
|
||||
timestampsToAdd.push(timestamp);
|
||||
}
|
||||
combinedEntries.unshift(...entriesToAdd);
|
||||
timestampEntries.unshift(...timestampsToAdd);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (endTimestampOfLastOutput && timestamp > endTimestampOfLastOutput) {
|
||||
for (index = index + 1; index < logEntries.length; index++) {
|
||||
const entry = logEntries[index];
|
||||
if (!entry.trim()) {
|
||||
continue;
|
||||
}
|
||||
timestamp = new Date(entry.match(LOG_ENTRY_REGEX)?.[1]!);
|
||||
entriesToAdd.push(entry.replace(LOG_ENTRY_REGEX, `$1 [${output.name}] $2`));
|
||||
timestampsToAdd.push(timestamp);
|
||||
}
|
||||
combinedEntries.push(...entriesToAdd);
|
||||
timestampEntries.push(...timestampsToAdd);
|
||||
break;
|
||||
}
|
||||
|
||||
const idx = binarySearch(timestampEntries, timestamp, (a, b) => a.getTime() - b.getTime());
|
||||
const insertionIndex = idx < 0 ? ~idx : idx;
|
||||
combinedEntries.splice(insertionIndex, 0, ...entriesToAdd);
|
||||
timestampEntries.splice(insertionIndex, 0, ...timestampsToAdd);
|
||||
}
|
||||
|
||||
startTimestampOfLastOutput = startTimestamp;
|
||||
endTimestampOfLastOutput = timestamp;
|
||||
}
|
||||
// Add new empty line at the end
|
||||
combinedEntries.push('');
|
||||
return combinedEntries.join('\n');
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ import { Registry } from '../../../../platform/registry/common/platform.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ITextModel } from '../../../../editor/common/model.js';
|
||||
import { LogLevel } from '../../../../platform/log/common/log.js';
|
||||
import { Range } from '../../../../editor/common/core/range.js';
|
||||
|
||||
/**
|
||||
* Mime type used by the output editor.
|
||||
@@ -247,3 +250,81 @@ class OutputChannelRegistry implements IOutputChannelRegistry {
|
||||
}
|
||||
|
||||
Registry.add(Extensions.OutputChannels, new OutputChannelRegistry());
|
||||
|
||||
export interface ILogEntry {
|
||||
readonly timestamp: number;
|
||||
readonly logLevel: LogLevel;
|
||||
readonly timestampRange: Range;
|
||||
readonly range: Range;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses log entries from a given text model starting from a specified line.
|
||||
*
|
||||
* @param model - The text model containing the log entries.
|
||||
* @param fromLine - The line number to start parsing from (default is 1).
|
||||
* @returns An array of log entries, each containing the log level and the range of lines it spans.
|
||||
*/
|
||||
export function parseLogEntries(model: ITextModel, fromLine: number = 1): ILogEntry[] {
|
||||
const logEntries: ILogEntry[] = [];
|
||||
for (let lineNumber = fromLine; lineNumber <= model.getLineCount(); lineNumber++) {
|
||||
const logEntry = parseLogEntryAt(model, lineNumber);
|
||||
if (logEntry) {
|
||||
logEntries.push(logEntry);
|
||||
lineNumber = logEntry.range.endLineNumber;
|
||||
}
|
||||
}
|
||||
return logEntries;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses a log entry at the specified line number in the given text model.
|
||||
*
|
||||
* @param model - The text model containing the log entries.
|
||||
* @param lineNumber - The line number at which to start parsing the log entry.
|
||||
* @returns An object representing the parsed log entry, or `null` if no log entry is found at the specified line.
|
||||
*
|
||||
* The returned log entry object contains:
|
||||
* - `timestamp`: The timestamp of the log entry as a number.
|
||||
* - `logLevel`: The log level of the log entry.
|
||||
* - `range`: The range of lines that the log entry spans.
|
||||
*/
|
||||
export function parseLogEntryAt(model: ITextModel, lineNumber: number): ILogEntry | null {
|
||||
const lineContent = model.getLineContent(lineNumber);
|
||||
const match = LOG_ENTRY_REGEX.exec(lineContent);
|
||||
if (match) {
|
||||
const timestamp = new Date(match[1]).getTime();
|
||||
const timestampRange = new Range(lineNumber, 1, lineNumber, match[1].length + 1);
|
||||
const logLevel = parseLogLevel(match[3]);
|
||||
const startLine = lineNumber;
|
||||
let endLine = lineNumber;
|
||||
|
||||
while (endLine < model.getLineCount()) {
|
||||
const nextLineContent = model.getLineContent(endLine + 1);
|
||||
if (model.getLineFirstNonWhitespaceColumn(endLine + 1) === 0 || LOG_ENTRY_REGEX.test(nextLineContent)) {
|
||||
break;
|
||||
}
|
||||
endLine++;
|
||||
}
|
||||
return { timestamp, logLevel, range: new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine)), timestampRange };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseLogLevel(level: string): LogLevel {
|
||||
switch (level.toLowerCase()) {
|
||||
case 'trace':
|
||||
return LogLevel.Trace;
|
||||
case 'debug':
|
||||
return LogLevel.Debug;
|
||||
case 'info':
|
||||
return LogLevel.Info;
|
||||
case 'warning':
|
||||
return LogLevel.Warning;
|
||||
case 'error':
|
||||
return LogLevel.Error;
|
||||
default:
|
||||
throw new Error(`Unknown log level: ${level}`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user