@kubohiroya/turbowarp-plus-editor-git 0.9.0 → 0.11.0

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.
@@ -51,6 +51,23 @@ export declare const messages: {
51
51
  remoteDone: string;
52
52
  pulledProject: string;
53
53
  confirmPush: string;
54
+ details: string;
55
+ branch: string;
56
+ head: string;
57
+ groups: string;
58
+ added: string;
59
+ modified: string;
60
+ deleted: string;
61
+ gitStaged: string;
62
+ gitUnstaged: string;
63
+ gitUntracked: string;
64
+ upstreamSame: string;
65
+ upstreamAhead: string;
66
+ upstreamBehind: string;
67
+ upstreamDiverged: string;
68
+ upstreamUnknown: string;
69
+ records: string;
70
+ checkedAt: string;
54
71
  failed: string;
55
72
  };
56
73
  ja: {
@@ -105,6 +122,23 @@ export declare const messages: {
105
122
  remoteDone: string;
106
123
  pulledProject: string;
107
124
  confirmPush: string;
125
+ details: string;
126
+ branch: string;
127
+ head: string;
128
+ groups: string;
129
+ added: string;
130
+ modified: string;
131
+ deleted: string;
132
+ gitStaged: string;
133
+ gitUnstaged: string;
134
+ gitUntracked: string;
135
+ upstreamSame: string;
136
+ upstreamAhead: string;
137
+ upstreamBehind: string;
138
+ upstreamDiverged: string;
139
+ upstreamUnknown: string;
140
+ records: string;
141
+ checkedAt: string;
108
142
  failed: string;
109
143
  };
110
144
  };
@@ -3,6 +3,7 @@ import type { ChangedFile, ProjectChangeList, ProjectElementChange, ProjectTarge
3
3
  import type { ProjectAssetLoader } from './project-assets.js';
4
4
  import type { ChangeGroup, FileChange, Repository, Snapshot } from './repository.js';
5
5
  import type { ProjectBlockRenderer } from './scratch-block-diff-renderer.js';
6
+ import type { ProjectBlockLocator } from './block-diff-location.js';
6
7
  declare const en: {
7
8
  title: string;
8
9
  stage: string;
@@ -68,7 +69,7 @@ export declare function findElement(list: ProjectChangeList, key: string | null)
68
69
  target: null;
69
70
  } | null;
70
71
  /** The details of one selected row: its code, pictures or values in both versions, and the files behind it. */
71
- export declare function ProjectElementView({ changes, selection, locale, renderBlock, onSelectFile }: {
72
+ export declare function ProjectElementView({ changes, selection, locale, renderBlock, onSelectFile, locateBlock }: {
72
73
  changes: ProjectChanges;
73
74
  selection: {
74
75
  element: ProjectElementChange;
@@ -77,5 +78,6 @@ export declare function ProjectElementView({ changes, selection, locale, renderB
77
78
  locale: string;
78
79
  renderBlock?: ProjectBlockRenderer;
79
80
  onSelectFile(file: ChangedFile): void;
81
+ locateBlock?: ProjectBlockLocator;
80
82
  }): React.JSX.Element;
81
83
  export {};
@@ -1,4 +1,4 @@
1
- import { _ as compareProjectBlocks, a as gitHubRepositoryFromOrigin, b as readProjectSourceDiff, c as projectPropertyLabel, d as BlockPreview, f as createPreviewSync, i as createGitHubPullRequests, l as ProjectSourceComparison, m as currentCostume, n as createScratchBlocksDiffLocator, o as assetFileName, p as usePreviewSync, s as organizeProjectChanges, t as createScratchBlocksDiffRenderer, u as ProjectSourceReview, v as createProjectSourceDiff } from "./scratch-block-diff-renderer-DZR8ep-T.js";
1
+ import { _ as compareProjectBlocks, a as gitHubRepositoryFromOrigin, b as readProjectSourceDiff, c as projectPropertyLabel, d as BlockPreview, f as createPreviewSync, i as createGitHubPullRequests, l as ProjectSourceComparison, m as currentCostume, n as createScratchBlocksDiffLocator, o as assetFileName, p as usePreviewSync, s as organizeProjectChanges, t as createScratchBlocksDiffRenderer, u as ProjectSourceReview, v as createProjectSourceDiff } from "./scratch-block-diff-renderer-CgAGfmAc.js";
2
2
  import { n as init_objectSpread2, t as _objectSpread2 } from "./objectSpread2-Dkd68nJH.js";
3
3
  import React, { useEffect, useMemo, useRef, useState } from "react";
4
4
  // src/project-source-diff.tsx
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import type { ProjectBlockRenderer } from './scratch-block-diff-renderer.js';
3
+ import type { ProjectBlockLocator } from './block-diff-location.js';
3
4
  import type { ProjectAssetLoader } from './project-assets.js';
4
5
  export type ProjectSourceReviewFile = {
5
6
  path: string;
@@ -10,6 +11,8 @@ export type ProjectSourceReviewFile = {
10
11
  patch: string;
11
12
  beforeSource?: string;
12
13
  loadAsset?: ProjectAssetLoader;
14
+ /** Offers "Highlight in Editor" on this file's after version, which the caller has matched with the open project. */
15
+ locateBlock?: ProjectBlockLocator;
13
16
  } | {
14
17
  status: 'added' | 'removed' | 'updated';
15
18
  });
@@ -32,6 +35,8 @@ export interface ProjectSourceComparisonProps {
32
35
  fallback?: React.ReactNode;
33
36
  /** Reads the images of both versions for thumbnails. Without it, stages and sprites are shown by name. */
34
37
  loadAsset?: ProjectAssetLoader;
38
+ /** Offers "Highlight in Editor" on the after version, which the caller has matched with the open project. */
39
+ locateBlock?: ProjectBlockLocator;
35
40
  /** Shows only the code of this stage or sprite, without the overview and the changes outside the code. */
36
41
  target?: {
37
42
  isStage: boolean;
@@ -39,5 +44,5 @@ export interface ProjectSourceComparisonProps {
39
44
  };
40
45
  }
41
46
  /** Compares two versions of a project.source.json by stage, sprite and code, as the pull request review does. */
42
- export declare function ProjectSourceComparison({ before, after, renderBlock, locale, fallback, loadAsset, target }: ProjectSourceComparisonProps): React.JSX.Element;
47
+ export declare function ProjectSourceComparison({ before, after, renderBlock, locale, fallback, loadAsset, locateBlock, target }: ProjectSourceComparisonProps): React.JSX.Element;
43
48
  export declare function ProjectSourceReview({ title, description, verification, files, renderBlock, locale }: ProjectSourceReviewProps): React.JSX.Element;
@@ -1,8 +1,11 @@
1
1
  import React from 'react';
2
2
  import type { ProjectBlockRenderer } from './scratch-block-diff-renderer.js';
3
3
  import type { Repository } from './repository.js';
4
- export declare function PullRequestReviewPanel({ repository, locale, renderBlock }: {
4
+ import type { ProjectBlockLocator } from './block-diff-location.js';
5
+ export declare function PullRequestReviewPanel({ repository, locale, renderBlock, locateFor }: {
5
6
  repository: Repository;
6
7
  locale: string;
7
8
  renderBlock: ProjectBlockRenderer;
9
+ /** Returns a locator for a project file whose PR head matches the open project, or `undefined`. */
10
+ locateFor?(path: string, headSha: string): ProjectBlockLocator | undefined;
8
11
  }): React.JSX.Element;
@@ -14562,6 +14562,37 @@ async function fetch({ fs, http, onProgress, onMessage, onAuth, onAuthSuccess, o
14562
14562
  }
14563
14563
  }
14564
14564
  /**
14565
+ * Find the merge base for a set of commits
14566
+ *
14567
+ * @param {object} args
14568
+ * @param {FsClient} args.fs - a file system client
14569
+ * @param {string} [args.dir] - The [working tree](dir-vs-gitdir.md) directory path
14570
+ * @param {string} [args.gitdir=join(dir,'.git')] - [required] The [git directory](dir-vs-gitdir.md) path
14571
+ * @param {string[]} args.oids - Which commits
14572
+ * @param {object} [args.cache] - a [cache](cache.md) object
14573
+ *
14574
+ */
14575
+ async function findMergeBase({ fs, dir, gitdir = join(dir, ".git"), oids, cache = {} }) {
14576
+ try {
14577
+ assertParameter("fs", fs);
14578
+ assertParameter("gitdir", gitdir);
14579
+ assertParameter("oids", oids);
14580
+ const fsp = new FileSystem(fs);
14581
+ return await _findMergeBase({
14582
+ fs: fsp,
14583
+ cache,
14584
+ gitdir: await discoverGitdir({
14585
+ fsp,
14586
+ dotgit: gitdir
14587
+ }),
14588
+ oids
14589
+ });
14590
+ } catch (err) {
14591
+ err.caller = "git.findMergeBase";
14592
+ throw err;
14593
+ }
14594
+ }
14595
+ /**
14565
14596
  * Read an entry from the git config files.
14566
14597
  *
14567
14598
  * *Caveats:*
@@ -16800,6 +16831,9 @@ var emptyTree = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
16800
16831
  var textLimit = 1048576;
16801
16832
  var inScope = (path, scope) => !scope || path === scope || path.startsWith(scope + "/");
16802
16833
  var probeLimit = 5e3;
16834
+ /** Most commits read to count how far a branch is ahead of or behind its tracking branch. */
16835
+ var upstreamWalkLimit = 1e3;
16836
+ var trackingRef = (branchRef) => "refs/remotes/origin/" + branchRef.slice(11);
16803
16837
  var sameMetadata = (a, b) => a.size === b.size && [...a].every(([path, bytes]) => {
16804
16838
  var _b$get;
16805
16839
  return sameBytes(bytes, (_b$get = b.get(path)) !== null && _b$get !== void 0 ? _b$get : null);
@@ -16839,7 +16873,7 @@ async function openRepository(root) {
16839
16873
  if (text.startsWith("ref: refs/heads/")) {
16840
16874
  const ref = text.slice(5);
16841
16875
  pathParts(ref);
16842
- paths.push("/.git/" + ref);
16876
+ paths.push("/.git/" + ref, "/.git/" + trackingRef(ref));
16843
16877
  } else if (!/^[0-9a-f]{40}$/.test(text)) throw new GitPanelError("invalid");
16844
16878
  const result = /* @__PURE__ */ new Map();
16845
16879
  for (const path of paths) {
@@ -16885,7 +16919,7 @@ async function openRepository(root) {
16885
16919
  } catch (_unused) {
16886
16920
  return "locked";
16887
16921
  }
16888
- paths.push(".git/" + head.slice(5));
16922
+ paths.push(".git/" + head.slice(5), ".git/" + trackingRef(head.slice(5)));
16889
16923
  }
16890
16924
  const lines = [];
16891
16925
  for (const path of paths) {
@@ -16957,6 +16991,86 @@ async function openRepository(root) {
16957
16991
  mode
16958
16992
  }]));
16959
16993
  };
16994
+ /**
16995
+ * Compares `refs/heads/<branch>` with `refs/remotes/origin/<branch>` from local refs only. Counts by painting
16996
+ * commits from both tips in committer-time order until every queued commit is reachable from both, like
16997
+ * `git rev-list --left-right --count`, reading at most `upstreamWalkLimit` commits.
16998
+ */
16999
+ const upstreamRelation = async (branch, head) => {
17000
+ const unknown = {
17001
+ state: "unknown",
17002
+ ahead: 0,
17003
+ behind: 0
17004
+ };
17005
+ if (!branch || !head) return unknown;
17006
+ pathParts(trackingRef("refs/heads/" + branch));
17007
+ try {
17008
+ const remote = await resolveRef(_objectSpread2(_objectSpread2({}, options), {}, { ref: trackingRef("refs/heads/" + branch) }));
17009
+ if (remote === head) return {
17010
+ state: "same",
17011
+ ahead: 0,
17012
+ behind: 0
17013
+ };
17014
+ const bases = await findMergeBase(_objectSpread2(_objectSpread2({}, options), {}, {
17015
+ cache,
17016
+ oids: [head, remote]
17017
+ }));
17018
+ const hasAhead = !bases.includes(head), hasBehind = !bases.includes(remote);
17019
+ const local = 1, tracking = 2, both = 3;
17020
+ const flags = /* @__PURE__ */ new Map([[head, local], [remote, tracking]]);
17021
+ const time = async (oid) => (await readCommit(_objectSpread2(_objectSpread2({}, options), {}, {
17022
+ cache,
17023
+ oid
17024
+ }))).commit;
17025
+ const queue = [];
17026
+ const enqueue = async (oid) => {
17027
+ const commit = await time(oid);
17028
+ queue.push({
17029
+ oid,
17030
+ time: commit.committer.timestamp,
17031
+ parents: commit.parent
17032
+ });
17033
+ };
17034
+ await enqueue(head);
17035
+ await enqueue(remote);
17036
+ let read = 2;
17037
+ let complete = true;
17038
+ while (queue.some((item) => flags.get(item.oid) !== both)) {
17039
+ if (read >= 1e3) {
17040
+ complete = false;
17041
+ break;
17042
+ }
17043
+ queue.sort((a, b) => b.time - a.time);
17044
+ const item = queue.shift();
17045
+ const flag = flags.get(item.oid);
17046
+ for (const parent of item.parents) {
17047
+ var _flags$get;
17048
+ const previous = (_flags$get = flags.get(parent)) !== null && _flags$get !== void 0 ? _flags$get : 0;
17049
+ if ((previous | flag) === previous) continue;
17050
+ flags.set(parent, previous | flag);
17051
+ if (!queue.some((entry) => entry.oid === parent)) {
17052
+ await enqueue(parent);
17053
+ read++;
17054
+ }
17055
+ }
17056
+ }
17057
+ let ahead = 0, behind = 0;
17058
+ for (const flag of flags.values()) if (flag === local) ahead++;
17059
+ else if (flag === tracking) behind++;
17060
+ if (!complete) {
17061
+ ahead = hasAhead ? upstreamWalkLimit : 0;
17062
+ behind = hasBehind ? upstreamWalkLimit : 0;
17063
+ }
17064
+ return {
17065
+ state: hasAhead && hasBehind ? "diverged" : hasAhead ? "ahead" : hasBehind ? "behind" : "same",
17066
+ ahead: hasAhead ? Math.max(ahead, 1) : 0,
17067
+ behind: hasBehind ? Math.max(behind, 1) : 0
17068
+ };
17069
+ } catch (error) {
17070
+ if (errorCode(error) === "NotFoundError") return unknown;
17071
+ throw error;
17072
+ }
17073
+ };
16960
17074
  const assertSnapshot = async (snapshot) => {
16961
17075
  if (!sameMetadata(snapshot.metadata, await metadata())) throw new GitPanelError("changed");
16962
17076
  };
@@ -16985,6 +17099,7 @@ async function openRepository(root) {
16985
17099
  author: commit.author.name,
16986
17100
  timestamp: commit.author.timestamp
16987
17101
  })) : [];
17102
+ const upstream = await upstreamRelation(branch || void 0, head);
16988
17103
  const matrix = await statusMatrix(_objectSpread2(_objectSpread2({}, options), {}, {
16989
17104
  cache,
16990
17105
  refresh: false,
@@ -17033,7 +17148,8 @@ async function openRepository(root) {
17033
17148
  changes,
17034
17149
  scope,
17035
17150
  metadata: before,
17036
- probe: fingerprint
17151
+ probe: fingerprint,
17152
+ upstream
17037
17153
  };
17038
17154
  };
17039
17155
  const historyChanges = async (snapshot, oid) => {
@@ -17340,4 +17456,4 @@ async function openRepository(root) {
17340
17456
  projectAssetLoader: (snapshot, folder, comparison) => ({ version, md5ext, signal }) => !isAssetName(md5ext) || signal.aborted ? Promise.resolve(null) : serial(() => readFolderAsset(snapshot, pathParts(folder), () => comparisonSource(snapshot, comparison, version), md5ext, signal)).catch(() => null)
17341
17457
  };
17342
17458
  }
17343
- export { GitPanelError as n, createReadOnlyFs as r, openRepository as t };
17459
+ export { createReadOnlyFs as i, upstreamWalkLimit as n, GitPanelError as r, openRepository as t };
@@ -40,6 +40,21 @@ export interface Snapshot {
40
40
  metadata: Metadata;
41
41
  /** Size and time fingerprint taken before the read. Compare it with `probe(scope)` to see later changes. */
42
42
  probe: string;
43
+ /**
44
+ * How the current branch stands against `refs/remotes/origin/<branch>` as of the last fetch, from local refs only.
45
+ * `unknown` when there is no current branch or no tracking ref yet.
46
+ */
47
+ upstream: UpstreamRelation;
48
+ }
49
+ export interface UpstreamRelation {
50
+ state: 'same' | 'ahead' | 'behind' | 'diverged' | 'unknown';
51
+ /**
52
+ * Commits on the local branch that the tracking branch lacks, and the reverse. Zero when unknown. Counting reads at
53
+ * most `upstreamWalkLimit` (1000) commits; past that, a side that has commits of its own reports exactly that limit,
54
+ * meaning "this many or more". The state itself comes from the merge base and stays exact.
55
+ */
56
+ ahead: number;
57
+ behind: number;
43
58
  }
44
59
  export interface FileDiff {
45
60
  binary: boolean;
@@ -56,6 +71,8 @@ export interface FileDiff {
56
71
  afterSize: number;
57
72
  tooLarge: boolean;
58
73
  }
74
+ /** Most commits read to count how far a branch is ahead of or behind its tracking branch. */
75
+ export declare const upstreamWalkLimit = 1000;
59
76
  export declare function openRepository(root: FileSystemDirectoryHandle): Promise<{
60
77
  root: FileSystemDirectoryHandle;
61
78
  openProject: (path: string) => Promise<FileSystemDirectoryHandle>;
@@ -1 +1 @@
1
- export { n as GitPanelError, r as createReadOnlyFs, t as openRepository } from "./repository-VBp-UNWc.js";
1
+ export { r as GitPanelError, i as createReadOnlyFs, t as openRepository, n as upstreamWalkLimit } from "./repository-CtvWYPQ8.js";
@@ -1007,7 +1007,11 @@ var ja = {
1007
1007
  script: "コード",
1008
1008
  noChanges: "作品に内容の変更はありません。",
1009
1009
  comparison: "作品の変更",
1010
- unreadable: "作品データとして読み込めないため、テキストの差分を表示します。"
1010
+ unreadable: "作品データとして読み込めないため、テキストの差分を表示します。",
1011
+ locate: "編集画面で強調",
1012
+ locating: "編集画面でブロックを検索中…",
1013
+ located: "編集画面で強調しました。",
1014
+ missingLocation: "編集画面に該当する版またはブロックがありません。"
1011
1015
  };
1012
1016
  var en = {
1013
1017
  review: "Change review",
@@ -1037,7 +1041,11 @@ var en = {
1037
1041
  script: "Code",
1038
1042
  noChanges: "No changes to the project contents.",
1039
1043
  comparison: "Project changes",
1040
- unreadable: "This file could not be read as project data, so its text diff is shown."
1044
+ unreadable: "This file could not be read as project data, so its text diff is shown.",
1045
+ locate: "Highlight in Editor",
1046
+ locating: "Finding the block in the Editor…",
1047
+ located: "Highlighted in the Editor.",
1048
+ missingLocation: "This version or block is unavailable in the Editor."
1041
1049
  };
1042
1050
  var properties = /* @__PURE__ */ new Map([
1043
1051
  ["variables", ["変数", "Variables"]],
@@ -1068,7 +1076,49 @@ function scriptTitle(side, labels) {
1068
1076
  }
1069
1077
  return typeof block.opcode === "string" ? block.opcode : labels.script;
1070
1078
  }
1071
- function ScriptComparison({ script, labels, reviewed = false, setReviewed, renderBlock, loadAsset }) {
1079
+ var sideKey = (side) => JSON.stringify([
1080
+ side.target.isStage,
1081
+ side.target.name,
1082
+ side.id
1083
+ ]);
1084
+ /** Highlights the after version of a script in the Editor, one request per comparison, and cancels it on unmount. */
1085
+ function useBlockLocation(comparison, locateBlock) {
1086
+ const request = useRef();
1087
+ const [location, setLocation] = useState();
1088
+ useEffect(() => () => {
1089
+ var _request$current;
1090
+ return (_request$current = request.current) === null || _request$current === void 0 ? void 0 : _request$current.abort();
1091
+ }, [comparison, locateBlock]);
1092
+ const locate = async (side) => {
1093
+ var _request$current2;
1094
+ if (!locateBlock) return;
1095
+ (_request$current2 = request.current) === null || _request$current2 === void 0 || _request$current2.abort();
1096
+ const controller = new AbortController();
1097
+ request.current = controller;
1098
+ const selected = {
1099
+ comparison,
1100
+ key: sideKey(side),
1101
+ locator: locateBlock
1102
+ };
1103
+ setLocation(_objectSpread2(_objectSpread2({}, selected), {}, { status: "locating" }));
1104
+ let found = false;
1105
+ try {
1106
+ found = await locateBlock({
1107
+ target: side.target,
1108
+ id: side.id,
1109
+ version: "after",
1110
+ parts: [],
1111
+ signal: controller.signal
1112
+ });
1113
+ } catch (_unused) {}
1114
+ if (!controller.signal.aborted) setLocation(_objectSpread2(_objectSpread2({}, selected), {}, { status: found ? "located" : "missingLocation" }));
1115
+ };
1116
+ return {
1117
+ locate: locateBlock && ((side) => void locate(side)),
1118
+ status: (side) => (location === null || location === void 0 ? void 0 : location.comparison) === comparison && location.locator === locateBlock && location.key === sideKey(side) ? location.status : void 0
1119
+ };
1120
+ }
1121
+ function ScriptComparison({ script, labels, reviewed = false, setReviewed, renderBlock, loadAsset, location }) {
1072
1122
  const [open, setOpen] = useState(false);
1073
1123
  const { sync, toggle } = usePreviewSync(labels === ja ? "ja" : "en");
1074
1124
  const status = !script.before.length ? "added" : !script.after.length ? "removed" : script.before.length !== 1 || script.after.length !== 1 ? "reorganized" : "updated";
@@ -1088,7 +1138,9 @@ function ScriptComparison({ script, labels, reviewed = false, setReviewed, rende
1088
1138
  labels,
1089
1139
  version,
1090
1140
  loadAsset,
1091
- sync
1141
+ sync,
1142
+ onLocate: version === "after" && (location === null || location === void 0 ? void 0 : location.locate) ? () => location.locate(side) : void 0,
1143
+ locationStatus: version === "after" ? location === null || location === void 0 ? void 0 : location.status(side) : void 0
1092
1144
  })) : /* @__PURE__ */ React.createElement(BlockPreview, {
1093
1145
  title: labels[version],
1094
1146
  renderBlock,
@@ -1132,7 +1184,8 @@ function TargetThumbnails({ target, labels, loadAsset }) {
1132
1184
  }));
1133
1185
  }
1134
1186
  /** Lists the changed stages and sprites of one project file, each with its code comparisons. */
1135
- function ComparisonTargets({ comparison, labels, renderBlock, loadAsset, review }) {
1187
+ function ComparisonTargets({ comparison, labels, renderBlock, loadAsset, locateBlock, review }) {
1188
+ const location = useBlockLocation(comparison, locateBlock);
1136
1189
  return /* @__PURE__ */ React.createElement(React.Fragment, null, comparison.properties.length > 0 && /* @__PURE__ */ React.createElement("p", null, labels.properties, " · ", propertyNames(comparison.properties, labels)), !comparison.targets.length && !comparison.properties.length && /* @__PURE__ */ React.createElement("p", null, labels.noChanges), comparison.targets.map((target) => /* @__PURE__ */ React.createElement("section", {
1137
1190
  className: "twgit-review-target",
1138
1191
  key: JSON.stringify([target.isStage, target.name])
@@ -1153,6 +1206,7 @@ function ComparisonTargets({ comparison, labels, renderBlock, loadAsset, review
1153
1206
  labels,
1154
1207
  renderBlock,
1155
1208
  loadAsset,
1209
+ location,
1156
1210
  reviewed: review === null || review === void 0 ? void 0 : review.keys.has(key),
1157
1211
  setReviewed: review && ((value) => review.set(key, value))
1158
1212
  });
@@ -1160,14 +1214,14 @@ function ComparisonTargets({ comparison, labels, renderBlock, loadAsset, review
1160
1214
  }
1161
1215
  var countScripts = (comparison) => comparison.targets.reduce((count, target) => count + target.scripts.length, 0);
1162
1216
  /** Compares two versions of a project.source.json by stage, sprite and code, as the pull request review does. */
1163
- function ProjectSourceComparison({ before, after, renderBlock, locale = "en", fallback, loadAsset, target }) {
1217
+ function ProjectSourceComparison({ before, after, renderBlock, locale = "en", fallback, loadAsset, locateBlock, target }) {
1164
1218
  const labels = locale.startsWith("ja") ? ja : en;
1165
1219
  const only = target && JSON.stringify([target.isStage, target.isStage || target.name]);
1166
1220
  const comparison = useMemo(() => {
1167
1221
  let result;
1168
1222
  try {
1169
1223
  result = compareProjectForReview(parseProjectSource(before), parseProjectSource(after));
1170
- } catch (_unused) {
1224
+ } catch (_unused2) {
1171
1225
  return null;
1172
1226
  }
1173
1227
  if (!only) return result;
@@ -1191,7 +1245,8 @@ function ProjectSourceComparison({ before, after, renderBlock, locale = "en", fa
1191
1245
  comparison,
1192
1246
  labels,
1193
1247
  renderBlock,
1194
- loadAsset
1248
+ loadAsset,
1249
+ locateBlock
1195
1250
  }));
1196
1251
  }
1197
1252
  function ProjectSourceReview({ title, description, verification, files, renderBlock, locale = "en" }) {
@@ -1207,7 +1262,7 @@ function ProjectSourceReview({ title, description, verification, files, renderBl
1207
1262
  properties: []
1208
1263
  }
1209
1264
  };
1210
- } catch (_unused2) {
1265
+ } catch (_unused3) {
1211
1266
  return {
1212
1267
  file,
1213
1268
  invalid: true
@@ -1244,6 +1299,7 @@ function ProjectSourceReview({ title, description, verification, files, renderBl
1244
1299
  labels,
1245
1300
  renderBlock,
1246
1301
  loadAsset: "loadAsset" in file ? file.loadAsset : void 0,
1302
+ locateBlock: "locateBlock" in file ? file.locateBlock : void 0,
1247
1303
  review: {
1248
1304
  file: file.path,
1249
1305
  keys: reviewed,
package/dist/style.css CHANGED
@@ -116,14 +116,24 @@
116
116
  .twgit-panel button:focus-visible, .twgit-panel summary:focus-visible { outline: 2px solid Highlight; outline-offset: 2px; }
117
117
  .twgit-panel button[aria-pressed="true"] { color: HighlightText; background: Highlight; }
118
118
  .twgit-context { display: flex; gap: 8px 20px; flex-wrap: wrap; padding: 8px 16px; }
119
- .twgit-connection { margin: 0 16px 8px; max-height: 45%; overflow: auto; }
119
+ .twgit-context { align-items: center; }
120
+ .twgit-connection, .twgit-git-details { position: relative; }
121
+ .twgit-connection > summary, .twgit-git-details > summary { cursor: pointer; }
122
+ /* Settings and Git details open over the list, so the list and the diff stay where they are. */
123
+ .twgit-connection-body { position: absolute; z-index: 5; top: calc(100% + 4px); left: 0; width: min(440px, 80vw); max-height: 60vh;
124
+ overflow: auto; padding: 10px 12px; background: Canvas; color: CanvasText; border: 1px solid GrayText; border-radius: 6px;
125
+ box-shadow: 0 8px 24px #0004; }
126
+ .twgit-connection-body > p { margin: 4px 0; overflow-wrap: anywhere; }
127
+ .twgit-sync { display: inline-flex; flex-wrap: wrap; gap: 4px; }
128
+ .twgit-change-kind { display: inline-block; min-width: 2.6em; font-size: 11px; opacity: .8; }
129
+ .twgit-file-details { font-size: 12px; margin-bottom: 6px; }
130
+ .twgit-file-details > p { margin: 2px 0; }
120
131
  .twgit-content { display: grid; grid-template-columns: minmax(230px, 30%) minmax(0, 1fr); flex: 1; min-height: 0; }
121
132
  .twgit-panel aside { padding: 0 14px 14px; overflow: auto; border-right: 1px solid GrayText; }
122
133
  .twgit-panel aside ul { list-style: none; padding: 0; margin: 4px 0; }
123
134
  .twgit-panel aside li { overflow-wrap: anywhere; }
124
135
  .twgit-panel summary { cursor: pointer; }
125
136
  .twgit-panel .twgit-file { width: 100%; border: 0; margin: 1px 0; }
126
- .twgit-file span { display: inline-block; width: 1.4em; font-family: monospace; }
127
137
  .twgit-diff { padding: 0 16px 16px; min-width: 0; overflow: auto; }
128
138
  .twgit-diff .diff { font-size: 12px; }
129
139
  .twgit-history { list-style: none; margin: 0; padding: 0; }
@@ -152,4 +162,7 @@
152
162
  .twgit-element-values td { font-family: ui-monospace, monospace; }
153
163
  .twgit-element-files { margin-top: 12px; }
154
164
  .twgit-panel .twgit-element span.twgit-element-name { width: auto; font-family: inherit; }
165
+ .twgit-git-details > .twgit-connection-body { left: auto; right: 0; }
166
+ /* In a layout tab the panel fills the tab and leaves the Editor beside it. */
167
+ .twgit-panel.twgit-panel-tab { width: 100%; height: 100%; border: 0; border-radius: 0; box-shadow: none; }
155
168
  /*$vite$:1*/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubohiroya/turbowarp-plus-editor-git",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Read-only local Git panel for TurboWarp Plus Editor",
5
5
  "type": "module",
6
6
  "license": "MPL-2.0",
@@ -28,7 +28,7 @@ const en = {
28
28
  unavailable: 'No GitHub connection is configured.', changed: 'Switching disconnects the current session.',
29
29
  error: 'GitHub connection failed. Retry using the selected connection.',
30
30
  invalid: 'The selected repository needs a GitHub HTTPS origin.',
31
- allowPush: 'Authorize push', fetch: 'Fetch', pull: 'Pull (fast-forward only)', push: 'Push'
31
+ allowPush: 'Allow sending to GitHub (push)', fetch: 'Check GitHub (fetch)', pull: 'Get from GitHub (pull)', push: 'Send to GitHub (push)'
32
32
  };
33
33
  const ja: typeof en = {
34
34
  title: 'GitHub 接続', mode: '接続方式', select: '接続方式を選択',
@@ -39,7 +39,7 @@ const ja: typeof en = {
39
39
  unavailable: 'GitHub 接続先が設定されていません。', changed: '方式を切り替えると現在のセッションを切断します。',
40
40
  error: 'GitHub 接続に失敗しました。選択中の方式で再試行してください。',
41
41
  invalid: '選択したリポジトリには GitHub の HTTPS リモート origin が必要です。',
42
- allowPush: 'push を許可する', fetch: 'Fetch', pull: 'Pull(fast-forward のみ)', push: 'Push'
42
+ allowPush: 'GitHub に送ることを許可する(push)', fetch: 'GitHub と確かめる(fetch)', pull: 'GitHub から取り込む(pull)', push: 'GitHub に送る(push)'
43
43
  };
44
44
 
45
45
  export function ConnectionSettings({settings, locale, readOrigin, runOperation}: {
@@ -69,8 +69,15 @@ export function ConnectionSettings({settings, locale, readOrigin, runOperation}:
69
69
  if (!remote) throw new Error('INVALID_REMOTE');
70
70
  await settings.connect(remote, allowPush);
71
71
  });
72
- return <details className="twgit-connection">
72
+ const connected = !!mode && state.status === 'connected';
73
+ // Sync sits beside the GitHub status; the settings open over the list instead of pushing it down.
74
+ return <>
75
+ {runOperation && connected && <span className="twgit-sync">{(['fetch', 'pull', 'push'] as const).map(operation =>
76
+ <button key={operation} disabled={busy || !readOrigin || (operation === 'push' && !write)}
77
+ onClick={() => void run(() => runOperation(operation))}>{labels[operation]}</button>)}</span>}
78
+ <details className="twgit-connection">
73
79
  <summary>{labels.title}</summary>
80
+ <div className="twgit-connection-body">
74
81
  {!settings.modes.length ? <p>{labels.unavailable}</p> : <>
75
82
  <label>{labels.mode} <select value={mode ?? ''} disabled={busy}
76
83
  onChange={event => {const choice = event.currentTarget.value as Mode; void run(() => settings.select(choice));}}>
@@ -93,11 +100,10 @@ export function ConnectionSettings({settings, locale, readOrigin, runOperation}:
93
100
  {state.status !== 'disconnected' && <button disabled={busy}
94
101
  onClick={() => void run(settings.refresh)}>{labels.check}</button>}
95
102
  <button disabled={busy} onClick={() => void run(settings.disconnect)}>{labels.disconnect}</button>
96
- {runOperation && state.status === 'connected' && (['fetch', 'pull', 'push'] as const).map(operation =>
97
- <button key={operation} disabled={busy || !readOrigin || (operation === 'push' && !write)}
98
- onClick={() => void run(() => runOperation(operation))}>{labels[operation]}</button>)}
99
103
  </>}
100
- {error && <p className="twgit-error" role="alert">{labels[error]}</p>}
101
104
  </>}
102
- </details>;
105
+ </div>
106
+ </details>
107
+ {error && <p className="twgit-error" role="alert">{labels[error]}</p>}
108
+ </>;
103
109
  }
@@ -0,0 +1,55 @@
1
+ import {createScratchBlocksDiffLocator} from './block-diff-location.js';
2
+ import type {ProjectBlockLocator, ScratchBlockLocationWorkspace} from './block-diff-location.js';
3
+
4
+ /** The parts of a Scratch VM the locator reads to find a stage or sprite. */
5
+ export interface LocatorVM {
6
+ runtime: {targets: readonly {id: string; isOriginal?: boolean; isStage: boolean; getName(): string}[]};
7
+ editingTarget?: {id: string} | null;
8
+ }
9
+
10
+ export interface EditorBlockLocatorOptions {
11
+ vm: LocatorVM;
12
+ /** Opens the code of one stage or sprite in the Editor, as project search does. */
13
+ openTargetCode(targetId: string): void | Promise<void>;
14
+ /** The Editor's code workspace as it is now, or `null` while none is shown. */
15
+ getWorkspace(): ScratchBlockLocationWorkspace | null | Promise<ScratchBlockLocationWorkspace | null>;
16
+ }
17
+
18
+ /**
19
+ * Finds the stage or sprite of a compared version in the open project, opens its code and waits for the block,
20
+ * then centers and outlines it. The caller decides which version matches the open project.
21
+ */
22
+ export function createEditorBlockLocator({vm, openTargetCode, getWorkspace}: EditorBlockLocatorOptions): ProjectBlockLocator {
23
+ return createScratchBlocksDiffLocator(async ({target, id: blockId, signal}) => {
24
+ const found = vm.runtime.targets.find(item => target.isStage ? item.isStage :
25
+ !item.isStage && item.isOriginal !== false && item.getName() === target.name);
26
+ if (!found || signal.aborted) return null;
27
+ await openTargetCode(found.id);
28
+ // The Editor switches targets and fills its workspace over the next frames.
29
+ const deadline = Date.now() + waitLimit;
30
+ while (!signal.aborted) {
31
+ if (vm.editingTarget?.id === found.id) {
32
+ const workspace = await getWorkspace();
33
+ if (signal.aborted) break;
34
+ if (workspace?.getBlockById(blockId)) return workspace;
35
+ }
36
+ if (Date.now() >= deadline) break;
37
+ await nextFrame(signal);
38
+ }
39
+ return null;
40
+ });
41
+ }
42
+
43
+ const waitLimit = 2000;
44
+
45
+ function nextFrame(signal: AbortSignal) {
46
+ return new Promise<void>(resolve => {
47
+ const done = () => {
48
+ signal.removeEventListener('abort', done);
49
+ resolve();
50
+ };
51
+ signal.addEventListener('abort', done, {once: true});
52
+ if (typeof requestAnimationFrame === 'function') requestAnimationFrame(done);
53
+ else setTimeout(done, 16);
54
+ });
55
+ }