@@ -219,41 +219,60 @@ export class LogViewerProvider implements vscode.TreeDataProvider<LogItem>, vsco
219219
220220 return Array . from ( groupedByType . entries ( ) ) . map ( ( [ level , entries ] ) => {
221221 if ( this . groupByMessage ) {
222- const groupedByMessage = new Map < string , { line : string , lineNumber : number } [ ] > ( ) ;
223-
224- entries . forEach ( entry => {
225- // Apply additional search filtering on message level
226- if ( this . matchesSearchTerm ( entry . message ) || this . matchesSearchTerm ( entry . line ) ) {
227- const messageGroup = groupedByMessage . get ( entry . message ) || [ ] ;
228- messageGroup . push ( { line : entry . line , lineNumber : entry . lineNumber } ) ;
229- groupedByMessage . set ( entry . message , messageGroup ) ;
222+ // Erste Gruppierung nach Themen (z.B. "Broken reference")
223+ const groupedByTopic = this . groupByTopics ( entries ) ;
224+
225+ const topicGroups = Array . from ( groupedByTopic . entries ( ) ) . map ( ( [ topic , topicEntries ] ) => {
226+ // Zweite Gruppierung nach spezifischen Nachrichten innerhalb des Themas
227+ const groupedByMessage = new Map < string , { line : string , lineNumber : number } [ ] > ( ) ;
228+
229+ topicEntries . forEach ( entry => {
230+ // Apply additional search filtering on message level
231+ if ( this . matchesSearchTerm ( entry . message ) || this . matchesSearchTerm ( entry . line ) ) {
232+ const messageGroup = groupedByMessage . get ( entry . message ) || [ ] ;
233+ messageGroup . push ( { line : entry . line , lineNumber : entry . lineNumber } ) ;
234+ groupedByMessage . set ( entry . message , messageGroup ) ;
235+ }
236+ } ) ;
237+
238+ const messageGroups = Array . from ( groupedByMessage . entries ( ) ) . map ( ( [ message , messageEntries ] ) => {
239+ const count = messageEntries . length ;
240+ const label = `${ message } (${ count } )` ;
241+ return new LogItem ( label , vscode . TreeItemCollapsibleState . Collapsed , undefined ,
242+ messageEntries . map ( entry => {
243+ const lineNumber = ( entry . lineNumber + 1 ) . toString ( ) . padStart ( 2 , '0' ) ;
244+ // Format the timestamp in the log entry
245+ const formattedLine = formatTimestamp ( entry . line ) ;
246+ return new LogItem (
247+ `Line ${ lineNumber } : ${ formattedLine } ` ,
248+ vscode . TreeItemCollapsibleState . None ,
249+ {
250+ command : 'magento-log-viewer.openFileAtLine' ,
251+ title : 'Open Log File at Line' ,
252+ arguments : [ filePath , entry . lineNumber ]
253+ }
254+ ) ;
255+ } ) . sort ( ( a , b ) => a . label . localeCompare ( b . label ) ) // Sort entries alphabetically
256+ ) ;
257+ } ) . sort ( ( a , b ) => a . label . localeCompare ( b . label ) ) ; // Sort message groups alphabetically
258+
259+ // Erstelle Themen-Gruppe nur wenn sie Nachrichten enthält
260+ if ( messageGroups . length > 0 ) {
261+ const topicCount = topicEntries . length ;
262+ const topicLabel = topic === 'Other' ? `${ topic } (${ topicCount } )` : `${ topic } (${ topicCount } )` ;
263+ return new LogItem ( topicLabel , vscode . TreeItemCollapsibleState . Collapsed , undefined , messageGroups ) ;
230264 }
265+ return null ;
266+ } ) . filter ( ( item ) : item is LogItem => item !== null ) . sort ( ( a , b ) => {
267+ // "Other" immer am Ende
268+ if ( a . label . startsWith ( 'Other' ) ) { return 1 ; }
269+ if ( b . label . startsWith ( 'Other' ) ) { return - 1 ; }
270+ return a . label . localeCompare ( b . label ) ;
231271 } ) ;
232272
233- const messageGroups = Array . from ( groupedByMessage . entries ( ) ) . map ( ( [ message , messageEntries ] ) => {
234- const count = messageEntries . length ;
235- const label = `${ message } (${ count } )` ;
236- return new LogItem ( label , vscode . TreeItemCollapsibleState . Collapsed , undefined ,
237- messageEntries . map ( entry => {
238- const lineNumber = ( entry . lineNumber + 1 ) . toString ( ) . padStart ( 2 , '0' ) ;
239- // Format the timestamp in the log entry
240- const formattedLine = formatTimestamp ( entry . line ) ;
241- return new LogItem (
242- `Line ${ lineNumber } : ${ formattedLine } ` ,
243- vscode . TreeItemCollapsibleState . None ,
244- {
245- command : 'magento-log-viewer.openFileAtLine' ,
246- title : 'Open Log File at Line' ,
247- arguments : [ filePath , entry . lineNumber ]
248- }
249- ) ;
250- } ) . sort ( ( a , b ) => a . label . localeCompare ( b . label ) ) // Sort entries alphabetically
251- ) ;
252- } ) . sort ( ( a , b ) => a . label . localeCompare ( b . label ) ) ; // Sort message groups alphabetically
253-
254273 // Only add log level if it has matching entries after filtering
255- if ( messageGroups . length > 0 ) {
256- const logFile = new LogItem ( `${ level } (${ entries . length } , grouped)` , vscode . TreeItemCollapsibleState . Collapsed , undefined , messageGroups ) ;
274+ if ( topicGroups . length > 0 ) {
275+ const logFile = new LogItem ( `${ level } (${ entries . length } , grouped)` , vscode . TreeItemCollapsibleState . Collapsed , undefined , topicGroups ) ;
257276 logFile . iconPath = getIconForLogLevel ( level ) ;
258277 return logFile ;
259278 }
@@ -343,6 +362,80 @@ export class LogViewerProvider implements vscode.TreeDataProvider<LogItem>, vsco
343362 LogViewerProvider . statusBarItem . text = `Magento Log-Entries: ${ totalEntries } ${ searchInfo } ` ;
344363 }
345364
365+ /**
366+ * Gruppiert Log-Einträge nach wiederkehrenden Themen
367+ */
368+ private groupByTopics ( entries : { message : string , line : string , lineNumber : number } [ ] ) : Map < string , { message : string , line : string , lineNumber : number } [ ] > {
369+ const groupedByTopic = new Map < string , { message : string , line : string , lineNumber : number } [ ] > ( ) ;
370+
371+ entries . forEach ( entry => {
372+ let assigned = false ;
373+
374+ // Erste Priorität: Dynamische Erkennung von Themen im Format "[Thema]:"
375+ const dynamicTopicMatch = entry . message . match ( / ^ ( [ ^ : ] + ) : / ) ;
376+ if ( dynamicTopicMatch ) {
377+ const topic = dynamicTopicMatch [ 1 ] . trim ( ) ;
378+ const topicEntries = groupedByTopic . get ( topic ) || [ ] ;
379+ topicEntries . push ( entry ) ;
380+ groupedByTopic . set ( topic , topicEntries ) ;
381+ assigned = true ;
382+ }
383+
384+ // Fallback: Statische Themen-Muster für Nachrichten ohne ":" Format
385+ if ( ! assigned ) {
386+ const fallbackPatterns = [
387+ { pattern : / d a t a b a s e | s q l | t r a n s a c t i o n / i, topic : 'Database' } ,
388+ { pattern : / c a c h e | r e d i s | v a r n i s h / i, topic : 'Cache' } ,
389+ { pattern : / s e s s i o n / i, topic : 'Session' } ,
390+ { pattern : / p a y m e n t / i, topic : 'Payment' } ,
391+ { pattern : / c h e c k o u t / i, topic : 'Checkout' } ,
392+ { pattern : / c a t a l o g / i, topic : 'Catalog' } ,
393+ { pattern : / c u s t o m e r / i, topic : 'Customer' } ,
394+ { pattern : / o r d e r / i, topic : 'Order' } ,
395+ { pattern : / s h i p p i n g / i, topic : 'Shipping' } ,
396+ { pattern : / t a x / i, topic : 'Tax' } ,
397+ { pattern : / i n v e n t o r y / i, topic : 'Inventory' } ,
398+ { pattern : / i n d e x e r / i, topic : 'Indexer' } ,
399+ { pattern : / c r o n / i, topic : 'Cron' } ,
400+ { pattern : / e m a i l | n e w s l e t t e r / i, topic : 'Email' } ,
401+ { pattern : / s e a r c h | a l g o l i a | e l a s t i c s e a r c h / i, topic : 'Search' } ,
402+ { pattern : / a p i | g r a p h q l | r e s t | s o a p / i, topic : 'API' } ,
403+ { pattern : / a d m i n / i, topic : 'Admin' } ,
404+ { pattern : / f r o n t e n d | b a c k e n d / i, topic : 'Frontend/Backend' } ,
405+ { pattern : / t h e m e | l a y o u t | t e m p l a t e | b l o c k | w i d g e t / i, topic : 'Theme/Layout' } ,
406+ { pattern : / m o d u l e | p l u g i n | o b s e r v e r | e v e n t / i, topic : 'Module/Plugin' } ,
407+ { pattern : / u r l | r e w r i t e / i, topic : 'URL' } ,
408+ { pattern : / m e d i a | i m a g e | u p l o a d / i, topic : 'Media' } ,
409+ { pattern : / i m p o r t | e x p o r t / i, topic : 'Import/Export' } ,
410+ { pattern : / t r a n s l a t i o n | l o c a l e / i, topic : 'Translation' } ,
411+ { pattern : / s t o r e | w e b s i t e | s c o p e / i, topic : 'Store' } ,
412+ { pattern : / c o n f i g / i, topic : 'Configuration' } ,
413+ { pattern : / m e m o r y | t i m e o u t | p e r f o r m a n c e / i, topic : 'Performance' } ,
414+ { pattern : / s e c u r i t y | a u t h e n t i c a t i o n | a u t h o r i z a t i o n | p e r m i s s i o n | a c c e s s | l o g i n | l o g o u t / i, topic : 'Security' }
415+ ] ;
416+
417+ for ( const { pattern, topic } of fallbackPatterns ) {
418+ if ( pattern . test ( entry . message ) ) {
419+ const topicEntries = groupedByTopic . get ( topic ) || [ ] ;
420+ topicEntries . push ( entry ) ;
421+ groupedByTopic . set ( topic , topicEntries ) ;
422+ assigned = true ;
423+ break ;
424+ }
425+ }
426+ }
427+
428+ // Falls kein Thema gefunden wurde, füge zu "Other" hinzu
429+ if ( ! assigned ) {
430+ const otherEntries = groupedByTopic . get ( 'Other' ) || [ ] ;
431+ otherEntries . push ( entry ) ;
432+ groupedByTopic . set ( 'Other' , otherEntries ) ;
433+ }
434+ } ) ;
435+
436+ return groupedByTopic ;
437+ }
438+
346439 dispose ( ) {
347440 this . _onDidChangeTreeData . dispose ( ) ;
348441 if ( LogViewerProvider . statusBarItem ) {
0 commit comments