tmux-fuzzy-motion 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +89 -19
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import { dirname, join, resolve } from "node:path";
6
6
  import { createConnection, createServer } from "node:net";
7
7
  import { tmpdir } from "node:os";
8
8
  import { createHash } from "node:crypto";
9
+ import { statSync } from "node:fs";
9
10
  //#endregion
10
11
  //#region src/infra/process.ts
11
12
  const runProcess = async (command, args) => new Promise((resolve, reject) => {
@@ -4726,7 +4727,7 @@ const capturePane = async (tmux, paneId) => {
4726
4727
  };
4727
4728
  //#endregion
4728
4729
  //#region src/core/width.ts
4729
- const ANSI_PATTERN = /\u001B\[[0-9;]*m/gu;
4730
+ const ANSI_PATTERN$1 = /\u001B\[[0-9;]*m/gu;
4730
4731
  const RESET$2 = "\x1B[0m";
4731
4732
  const displayWidth = (value) => stringWidth(value);
4732
4733
  const codeUnitIndexToColumn = (value, index) => stringWidth(value.slice(0, index));
@@ -4764,11 +4765,52 @@ const codeUnitRangeToColumns = (value, start, end) => {
4764
4765
  }
4765
4766
  return columns;
4766
4767
  };
4768
+ const createCompactStyledDisplayCells = (value) => {
4769
+ const cells = [];
4770
+ let activeStyle = "";
4771
+ let pendingStyle = "";
4772
+ let styleOpen = false;
4773
+ let lastVisibleCellIndex = -1;
4774
+ let lastIndex = 0;
4775
+ const appendChunk = (chunk) => {
4776
+ for (const char of chunk) {
4777
+ const width = Math.max(1, stringWidth(char));
4778
+ const prefix = pendingStyle.length > 0 ? pendingStyle : !styleOpen && activeStyle.length > 0 ? activeStyle : "";
4779
+ cells.push(`${prefix}${char}`);
4780
+ if (prefix.length > 0) {
4781
+ pendingStyle = "";
4782
+ styleOpen = true;
4783
+ }
4784
+ lastVisibleCellIndex = cells.length - 1;
4785
+ for (let offset = 1; offset < width; offset += 1) cells.push("");
4786
+ }
4787
+ };
4788
+ const closeStyle = () => {
4789
+ if (styleOpen && lastVisibleCellIndex >= 0) cells[lastVisibleCellIndex] = `${cells[lastVisibleCellIndex]}${RESET$2}`;
4790
+ activeStyle = "";
4791
+ pendingStyle = "";
4792
+ styleOpen = false;
4793
+ };
4794
+ for (const match of value.matchAll(ANSI_PATTERN$1)) {
4795
+ const start = match.index ?? 0;
4796
+ appendChunk(value.slice(lastIndex, start));
4797
+ const sequence = match[0];
4798
+ if (sequence === RESET$2) closeStyle();
4799
+ else {
4800
+ activeStyle = `${activeStyle}${sequence}`;
4801
+ pendingStyle = `${pendingStyle}${sequence}`;
4802
+ }
4803
+ lastIndex = start + sequence.length;
4804
+ }
4805
+ appendChunk(value.slice(lastIndex));
4806
+ if (styleOpen) closeStyle();
4807
+ return cells;
4808
+ };
4767
4809
  const createStyledDisplayCells = (value) => {
4768
4810
  const cells = [];
4769
4811
  let activeStyle = "";
4770
4812
  let lastIndex = 0;
4771
- for (const match of value.matchAll(ANSI_PATTERN)) {
4813
+ for (const match of value.matchAll(ANSI_PATTERN$1)) {
4772
4814
  const start = match.index ?? 0;
4773
4815
  const chunk = value.slice(lastIndex, start);
4774
4816
  for (const char of chunk) {
@@ -4815,9 +4857,19 @@ const PATTERNS = [
4815
4857
  }
4816
4858
  ];
4817
4859
  const isContainedByHigherPriority = (current, others) => others.some((candidate) => candidate.line === current.line && candidate.priority < current.priority && candidate.startIndex <= current.startIndex && candidate.endIndex >= current.endIndex);
4860
+ const dedupeLineCandidates = (candidates) => {
4861
+ const byStartIndex = /* @__PURE__ */ new Map();
4862
+ for (const candidate of candidates) {
4863
+ const current = byStartIndex.get(candidate.startIndex);
4864
+ if (!current || candidate.priority < current.priority) byStartIndex.set(candidate.startIndex, candidate);
4865
+ }
4866
+ const deduped = [...byStartIndex.values()];
4867
+ return deduped.filter((candidate) => !isContainedByHigherPriority(candidate, deduped));
4868
+ };
4818
4869
  const extractCandidates = (lines) => {
4819
4870
  const collected = [];
4820
4871
  lines.forEach((lineText, lineIndex) => {
4872
+ const lineCandidates = [];
4821
4873
  PATTERNS.forEach(({ kind, pattern }, priority) => {
4822
4874
  const regex = new RegExp(pattern.source, pattern.flags);
4823
4875
  for (const match of lineText.matchAll(regex)) {
@@ -4828,7 +4880,7 @@ const extractCandidates = (lines) => {
4828
4880
  if (width === 1 && kind !== "word") continue;
4829
4881
  const col = codeUnitIndexToColumn(lineText, startIndex);
4830
4882
  const charCol = codeUnitIndexToCharacterIndex(lineText, startIndex);
4831
- collected.push({
4883
+ lineCandidates.push({
4832
4884
  kind,
4833
4885
  text,
4834
4886
  line: lineIndex + 1,
@@ -4841,10 +4893,9 @@ const extractCandidates = (lines) => {
4841
4893
  });
4842
4894
  }
4843
4895
  });
4896
+ collected.push(...dedupeLineCandidates(lineCandidates));
4844
4897
  });
4845
- return collected.filter((candidate, _, source) => {
4846
- return source.find((other) => other.line === candidate.line && other.startIndex === candidate.startIndex && other.priority <= candidate.priority) === candidate;
4847
- }).filter((candidate, _, source) => !isContainedByHigherPriority(candidate, source)).sort((left, right) => left.line - right.line || left.col - right.col || left.priority - right.priority).map((candidate) => ({
4898
+ return collected.sort((left, right) => left.line - right.line || left.col - right.col || left.priority - right.priority).map((candidate) => ({
4848
4899
  kind: candidate.kind,
4849
4900
  text: candidate.text,
4850
4901
  line: candidate.line,
@@ -5749,7 +5800,17 @@ const createMatcher = (candidates, migemo) => {
5749
5800
  //#region src/commands/runtime.ts
5750
5801
  const sleep = async (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
5751
5802
  const resolveCliEntrypoint = () => process.argv[1] ?? resolve(process.cwd(), "dist/cli.js");
5752
- const createDaemonSocketPath = () => join("/tmp", `tfm-${createHash("sha1").update(process.env.TMUX ?? "tmux-fuzzy-motion").digest("hex")}.sock`);
5803
+ const createDaemonIdentity = () => {
5804
+ const entrypoint = resolveCliEntrypoint();
5805
+ const entrypointMtimeMs = statSync(entrypoint).mtimeMs;
5806
+ return [
5807
+ process.env.TMUX ?? "tmux-fuzzy-motion",
5808
+ process.execPath,
5809
+ entrypoint,
5810
+ String(entrypointMtimeMs)
5811
+ ].join("\0");
5812
+ };
5813
+ const createDaemonSocketPath = () => join("/tmp", `tfm-${createHash("sha1").update(createDaemonIdentity()).digest("hex")}.sock`);
5753
5814
  const pathExists = async (path) => {
5754
5815
  try {
5755
5816
  await access(path);
@@ -5798,7 +5859,7 @@ const isDaemonHealthy = async (socketPath) => new Promise((resolve) => {
5798
5859
  });
5799
5860
  });
5800
5861
  const waitForDaemon = async (socketPath) => {
5801
- for (let attempt = 0; attempt < 40; attempt += 1) {
5862
+ for (let attempt = 0; attempt < 200; attempt += 1) {
5802
5863
  if (await isDaemonHealthy(socketPath)) return;
5803
5864
  await sleep(25);
5804
5865
  }
@@ -5950,6 +6011,7 @@ const withRawMode = async (input, output, callback) => {
5950
6011
  //#region src/commands/input.ts
5951
6012
  const QUERY_STYLE = "\x1B[48;5;236;38;5;252m";
5952
6013
  const RESET = "\x1B[0m";
6014
+ const ANSI_PATTERN = /\u001B\[[0-9;]*m/u;
5953
6015
  const HINT_CHARS = /* @__PURE__ */ new Set("ASDFGHJKLQWERTYUIOPZXCVBNM");
5954
6016
  const WORD_CHAR_PATTERN = /[a-z0-9_-]/u;
5955
6017
  const parsePopupArgs = (args) => {
@@ -6025,9 +6087,14 @@ const fitBodyToHeight = (lines, height) => {
6025
6087
  return next;
6026
6088
  };
6027
6089
  const renderQueryOnBottomLine = (line, width, query) => {
6090
+ if (query.length === 0) {
6091
+ const cells = createCompactStyledDisplayCells(line);
6092
+ while (cells.length < width) cells.push(" ");
6093
+ const rendered = cells.slice(0, width).join("");
6094
+ return ANSI_PATTERN.test(rendered) && !rendered.endsWith(RESET) ? `${rendered}${RESET}` : rendered;
6095
+ }
6028
6096
  const cells = createStyledDisplayCells(line);
6029
6097
  while (cells.length < width) cells.push(" ");
6030
- if (query.length === 0) return cells.slice(0, width).join("");
6031
6098
  const queryWidth = Math.min(width, stringWidth(query));
6032
6099
  const start = Math.max(0, width - queryWidth);
6033
6100
  const content = Array.from(query);
@@ -6044,7 +6111,7 @@ const renderQueryOnBottomLine = (line, width, query) => {
6044
6111
  return cells.slice(0, width).join("");
6045
6112
  };
6046
6113
  const createFrame = (state, query, matches, renderOverlay) => {
6047
- const body = fitBodyToHeight(query.length > 0 && matches.length > 0 ? renderOverlay(matches) : state.displayLines, state.height);
6114
+ const body = fitBodyToHeight(query.length > 0 && matches.length > 0 && renderOverlay ? renderOverlay(matches) : state.displayLines, state.height);
6048
6115
  const lastLineIndex = Math.max(0, body.length - 1);
6049
6116
  body[lastLineIndex] = renderQueryOnBottomLine(body[lastLineIndex] ?? "", state.width, query);
6050
6117
  return { body };
@@ -6162,7 +6229,11 @@ const expectResponse = (response, type) => {
6162
6229
  return response;
6163
6230
  };
6164
6231
  const runPopupJob = async (state, options) => {
6165
- const overlayRenderer = createOverlayRenderer(state.displayLines);
6232
+ let overlayRenderer = null;
6233
+ const getOverlayRenderer = () => {
6234
+ overlayRenderer ??= createOverlayRenderer(state.displayLines);
6235
+ return overlayRenderer;
6236
+ };
6166
6237
  let query = "";
6167
6238
  let previousHints = /* @__PURE__ */ new Map();
6168
6239
  let matches = [];
@@ -6170,7 +6241,7 @@ const runPopupJob = async (state, options) => {
6170
6241
  const output = process.stdout;
6171
6242
  await withRawMode(input, output, async () => {
6172
6243
  const reader = createByteReader(input);
6173
- let previousFrame = renderFrame(output, createFrame(state, query, matches, overlayRenderer));
6244
+ let previousFrame = renderFrame(output, createFrame(state, query, matches));
6174
6245
  try {
6175
6246
  while (true) {
6176
6247
  const value = await reader.nextByte();
@@ -6213,7 +6284,7 @@ const runPopupJob = async (state, options) => {
6213
6284
  }
6214
6285
  matches = query.length === 0 ? [] : await options.onMatch(query, previousHints);
6215
6286
  previousHints = new Map(matches.map((target) => [createTargetKey(target), target.hint]));
6216
- previousFrame = renderFrame(output, createFrame(state, query, matches, overlayRenderer), previousFrame);
6287
+ previousFrame = renderFrame(output, createFrame(state, query, matches, matches.length > 0 ? getOverlayRenderer() : void 0), previousFrame);
6217
6288
  }
6218
6289
  } finally {
6219
6290
  reader.close();
@@ -6531,7 +6602,7 @@ const composeDisplayLines = (panes, width, height, borderLines) => {
6531
6602
  for (const pane of panes) pane.displayLines.forEach((line, lineIndex) => {
6532
6603
  const row = rows[pane.top + lineIndex];
6533
6604
  if (!row) return;
6534
- createStyledDisplayCells(line).forEach((cell, cellIndex) => {
6605
+ createCompactStyledDisplayCells(line).forEach((cell, cellIndex) => {
6535
6606
  const column = pane.left + cellIndex;
6536
6607
  if (column < 0 || column >= row.length) return;
6537
6608
  row[column] = cell;
@@ -6570,10 +6641,9 @@ const buildAllPaneState = async (tmux, pane, paneId, clientTty) => {
6570
6641
  right: Number.NEGATIVE_INFINITY,
6571
6642
  bottom: Number.NEGATIVE_INFINITY
6572
6643
  });
6573
- const snapshots = [];
6574
- for (const item of panes) {
6644
+ const snapshots = await Promise.all(panes.map(async (item) => {
6575
6645
  const capture = fitCaptureToHeight(await capturePane(tmux, item.paneId), item.height);
6576
- snapshots.push({
6646
+ return {
6577
6647
  paneId: item.paneId,
6578
6648
  inCopyMode: item.inCopyMode,
6579
6649
  width: item.width,
@@ -6582,8 +6652,8 @@ const buildAllPaneState = async (tmux, pane, paneId, clientTty) => {
6582
6652
  top: item.top - bounds.top,
6583
6653
  plainLines: capture.lines,
6584
6654
  displayLines: capture.displayLines
6585
- });
6586
- }
6655
+ };
6656
+ }));
6587
6657
  const width = Math.max(0, bounds.right - bounds.left);
6588
6658
  const height = Math.max(0, bounds.bottom - bounds.top);
6589
6659
  const x = buildPopupRelativePosition("x", bounds.left);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-fuzzy-motion",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Fuzzy hint motion for tmux copy-mode",
5
5
  "type": "module",
6
6
  "files": [