Merge "Update atoms descriptor." into main
diff --git a/ui/src/base/hotkeys.ts b/ui/src/base/hotkeys.ts
index 17e3a00..85c194f 100644
--- a/ui/src/base/hotkeys.ts
+++ b/ui/src/base/hotkeys.ts
@@ -88,7 +88,9 @@
   | 'ArrowLeft'
   | 'ArrowRight'
   | '['
-  | ']';
+  | ']'
+  | ','
+  | '.';
 export type Key = Alphabet | Number | Special;
 export type Modifier =
   | ''
diff --git a/ui/src/core_plugins/track_utils/index.ts b/ui/src/core_plugins/track_utils/index.ts
index 369d2fe..e13d479 100644
--- a/ui/src/core_plugins/track_utils/index.ts
+++ b/ui/src/core_plugins/track_utils/index.ts
@@ -18,6 +18,7 @@
 import {AppImpl} from '../../core/app_impl';
 import {getTimeSpanOfSelectionOrVisibleWindow} from '../../public/utils';
 import {exists} from '../../base/utils';
+import {LONG, NUM, NUM_NULL} from '../../trace_processor/query_result';
 
 export default class implements PerfettoPlugin {
   static readonly id = 'perfetto.TrackUtils';
@@ -83,5 +84,57 @@
         id && ctx.workspace.getTrackById(id)?.pin();
       },
     });
+
+    ctx.commands.registerCommand({
+      id: 'perfetto.SelectNextTrackEvent',
+      name: 'Select next track event',
+      defaultHotkey: '.',
+      callback: async () => {
+        await selectAdjacentTrackEvent(ctx, 'next');
+      },
+    });
+
+    ctx.commands.registerCommand({
+      id: 'perfetto.SelectPreviousTrackEvent',
+      name: 'Select previous track event',
+      defaultHotkey: ',',
+      callback: async () => {
+        await selectAdjacentTrackEvent(ctx, 'prev');
+      },
+    });
   }
 }
+
+/**
+ * If a track event is currently selected, select the next or previous event on
+ * that same track chronologically ordered by `ts`.
+ */
+async function selectAdjacentTrackEvent(
+  ctx: Trace,
+  direction: 'next' | 'prev',
+) {
+  const selection = ctx.selection.selection;
+  if (selection.kind !== 'track_event') return;
+
+  const td = ctx.tracks.getTrack(selection.trackUri);
+  const dataset = td?.track.getDataset?.();
+  if (!dataset || !dataset.implements({id: NUM, ts: LONG})) return;
+
+  const windowFunc = direction === 'next' ? 'LEAD' : 'LAG';
+  const result = await ctx.engine.query(`
+      WITH
+        CTE AS (
+          SELECT
+            id,
+            ${windowFunc}(id) OVER (ORDER BY ts) AS resultId
+          FROM (${dataset.query()})
+        )
+      SELECT * FROM CTE WHERE id = ${selection.eventId}
+    `);
+  const resultId = result.maybeFirstRow({resultId: NUM_NULL})?.resultId;
+  if (!exists(resultId)) return;
+
+  ctx.selection.selectTrackEvent(selection.trackUri, resultId, {
+    scrollToSelection: true,
+  });
+}