@kubohiroya/turbowarp-plus-editor-git 0.10.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.
- package/dist/editor-block-locator.d.ts +27 -0
- package/dist/file-diff-view.d.ts +3 -1
- package/dist/index.d.ts +25 -0
- package/dist/index.js +126 -32
- package/dist/project-change-list.d.ts +3 -1
- package/dist/project-source-diff.js +1 -1
- package/dist/project-source-review.d.ts +6 -1
- package/dist/pull-request-panel.d.ts +4 -1
- package/dist/{scratch-block-diff-renderer-DZR8ep-T.js → scratch-block-diff-renderer-CgAGfmAc.js} +65 -9
- package/dist/style.css +2 -0
- package/package.json +1 -1
- package/src/editor-block-locator.ts +55 -0
- package/src/file-diff-view.tsx +4 -3
- package/src/index.tsx +74 -16
- package/src/project-change-list.tsx +4 -3
- package/src/project-source-review.tsx +57 -11
- package/src/pull-request-panel.tsx +9 -4
- package/src/style.css +2 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ProjectBlockLocator, ScratchBlockLocationWorkspace } from './block-diff-location.js';
|
|
2
|
+
/** The parts of a Scratch VM the locator reads to find a stage or sprite. */
|
|
3
|
+
export interface LocatorVM {
|
|
4
|
+
runtime: {
|
|
5
|
+
targets: readonly {
|
|
6
|
+
id: string;
|
|
7
|
+
isOriginal?: boolean;
|
|
8
|
+
isStage: boolean;
|
|
9
|
+
getName(): string;
|
|
10
|
+
}[];
|
|
11
|
+
};
|
|
12
|
+
editingTarget?: {
|
|
13
|
+
id: string;
|
|
14
|
+
} | null;
|
|
15
|
+
}
|
|
16
|
+
export interface EditorBlockLocatorOptions {
|
|
17
|
+
vm: LocatorVM;
|
|
18
|
+
/** Opens the code of one stage or sprite in the Editor, as project search does. */
|
|
19
|
+
openTargetCode(targetId: string): void | Promise<void>;
|
|
20
|
+
/** The Editor's code workspace as it is now, or `null` while none is shown. */
|
|
21
|
+
getWorkspace(): ScratchBlockLocationWorkspace | null | Promise<ScratchBlockLocationWorkspace | null>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Finds the stage or sprite of a compared version in the open project, opens its code and waits for the block,
|
|
25
|
+
* then centers and outlines it. The caller decides which version matches the open project.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createEditorBlockLocator({ vm, openTargetCode, getWorkspace }: EditorBlockLocatorOptions): ProjectBlockLocator;
|
package/dist/file-diff-view.d.ts
CHANGED
|
@@ -3,16 +3,18 @@ import type { FileChange, FileDiff } from './repository.js';
|
|
|
3
3
|
import type { labelsFor } from './messages.js';
|
|
4
4
|
import type { ProjectBlockRenderer } from './scratch-block-diff-renderer.js';
|
|
5
5
|
import type { ProjectAssetLoader } from './project-assets.js';
|
|
6
|
+
import type { ProjectBlockLocator } from './block-diff-location.js';
|
|
6
7
|
/** Project sources are the files the panel opens as projects. */
|
|
7
8
|
export declare const isProjectSource: (path: string) => boolean;
|
|
8
9
|
/**
|
|
9
10
|
* Shows one changed file. Project sources are compared as blocks by default, with their text diff one switch away.
|
|
10
11
|
*/
|
|
11
|
-
export declare function FileDiffView({ change, diff, labels, locale, renderBlock, loadAsset }: {
|
|
12
|
+
export declare function FileDiffView({ change, diff, labels, locale, renderBlock, loadAsset, locateBlock }: {
|
|
12
13
|
change: FileChange;
|
|
13
14
|
diff: FileDiff | null;
|
|
14
15
|
labels: ReturnType<typeof labelsFor>;
|
|
15
16
|
locale: string;
|
|
16
17
|
renderBlock?: ProjectBlockRenderer;
|
|
17
18
|
loadAsset?: ProjectAssetLoader;
|
|
19
|
+
locateBlock?: ProjectBlockLocator;
|
|
18
20
|
}): React.JSX.Element;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { GitConnectionSettings } from './connection-settings.js';
|
|
2
2
|
import type { ProjectBlockRenderer } from './scratch-block-diff-renderer.js';
|
|
3
|
+
import type { ScratchBlockLocationWorkspace } from './block-diff-location.js';
|
|
4
|
+
export { createScratchBlocksDiffLocator } from './block-diff-location.js';
|
|
5
|
+
export { createEditorBlockLocator } from './editor-block-locator.js';
|
|
3
6
|
export { createScratchBlocksDiffRenderer } from './scratch-block-diff-renderer.js';
|
|
4
7
|
export type { ScratchBlockDiffRendererOptions, ScratchBlocksPort } from './scratch-block-diff-renderer.js';
|
|
5
8
|
export { openRepository, createReadOnlyFs, GitPanelError } from './repository.js';
|
|
@@ -11,6 +14,28 @@ export interface GitHost {
|
|
|
11
14
|
isProjectDirty(): boolean;
|
|
12
15
|
isProjectRunning?(): boolean;
|
|
13
16
|
subscribe?(listener: (event: 'loaded' | 'changed') => void): () => void;
|
|
17
|
+
/**
|
|
18
|
+
* Opens the panel as a tab of the host's layout, with the same shape as the layout package's DocumentPanelRequest.
|
|
19
|
+
* Returning `false` keeps the modal dialog.
|
|
20
|
+
*/
|
|
21
|
+
openPanel?(request: GitPanelRequest): boolean;
|
|
22
|
+
/** Opens the code of one stage or sprite, so a diff can point at its block. Without it, no block is located. */
|
|
23
|
+
openTargetCode?(targetId: string): void | Promise<void>;
|
|
24
|
+
/** The Editor's code workspace as it is now. */
|
|
25
|
+
getWorkspace?(): ScratchBlockLocationWorkspace | null | Promise<ScratchBlockLocationWorkspace | null>;
|
|
26
|
+
}
|
|
27
|
+
export interface GitPanelRequest {
|
|
28
|
+
reference: {
|
|
29
|
+
providerId: string;
|
|
30
|
+
documentId: string;
|
|
31
|
+
};
|
|
32
|
+
filename: string;
|
|
33
|
+
title?: string;
|
|
34
|
+
transient?: boolean;
|
|
35
|
+
mount(element: HTMLElement, close: () => void): {
|
|
36
|
+
requestClose(): void;
|
|
37
|
+
dispose(): void;
|
|
38
|
+
};
|
|
14
39
|
}
|
|
15
40
|
export declare function mountGitPanel({ vm, document, host }: {
|
|
16
41
|
vm: object;
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { a as gitHubRepositoryFromOrigin, c as projectPropertyLabel, g as useCostumeImage, h as useAssetImage, i as createGitHubPullRequests, l as ProjectSourceComparison, r as PullRequestError, s as organizeProjectChanges, t as createScratchBlocksDiffRenderer, u as ProjectSourceReview, y as parseProjectSource } from "./scratch-block-diff-renderer-
|
|
1
|
+
import { a as gitHubRepositoryFromOrigin, c as projectPropertyLabel, g as useCostumeImage, h as useAssetImage, i as createGitHubPullRequests, l as ProjectSourceComparison, n as createScratchBlocksDiffLocator, r as PullRequestError, s as organizeProjectChanges, t as createScratchBlocksDiffRenderer, u as ProjectSourceReview, y as parseProjectSource } from "./scratch-block-diff-renderer-CgAGfmAc.js";
|
|
2
2
|
import { n as init_objectSpread2, r as __commonJSMin, t as _objectSpread2$1 } from "./objectSpread2-Dkd68nJH.js";
|
|
3
3
|
import { i as createReadOnlyFs, r as GitPanelError, t as openRepository } from "./repository-CtvWYPQ8.js";
|
|
4
4
|
import * as m from "react";
|
|
5
5
|
import React, { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
-
import { render, unmountComponentAtNode } from "react-dom";
|
|
6
|
+
import { createPortal, render, unmountComponentAtNode } from "react-dom";
|
|
7
7
|
// builtin:esm-external-require-react
|
|
8
8
|
var require_builtin_esm_external_require_react = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
9
9
|
init_objectSpread2();
|
|
@@ -2684,7 +2684,7 @@ var isProjectSource = (path) => path.split("/").pop() === "project.source.json";
|
|
|
2684
2684
|
/**
|
|
2685
2685
|
* Shows one changed file. Project sources are compared as blocks by default, with their text diff one switch away.
|
|
2686
2686
|
*/
|
|
2687
|
-
function FileDiffView({ change, diff, labels, locale, renderBlock, loadAsset }) {
|
|
2687
|
+
function FileDiffView({ change, diff, labels, locale, renderBlock, loadAsset, locateBlock }) {
|
|
2688
2688
|
var _change$oldMode, _change$newMode;
|
|
2689
2689
|
const [view, setView] = useState("blocks");
|
|
2690
2690
|
const names = locale.startsWith("ja") ? ja$3 : en$3;
|
|
@@ -2713,6 +2713,7 @@ function FileDiffView({ change, diff, labels, locale, renderBlock, loadAsset })
|
|
|
2713
2713
|
locale,
|
|
2714
2714
|
fallback: text,
|
|
2715
2715
|
loadAsset,
|
|
2716
|
+
locateBlock,
|
|
2716
2717
|
before: change.before === null ? null : sources.before,
|
|
2717
2718
|
after: change.after === null ? null : sources.after
|
|
2718
2719
|
}) : text);
|
|
@@ -2965,7 +2966,7 @@ var show = (value) => {
|
|
|
2965
2966
|
return text.length > 400 ? text.slice(0, 400) + "…" : text;
|
|
2966
2967
|
};
|
|
2967
2968
|
/** The details of one selected row: its code, pictures or values in both versions, and the files behind it. */
|
|
2968
|
-
function ProjectElementView({ changes, selection, locale, renderBlock, onSelectFile }) {
|
|
2969
|
+
function ProjectElementView({ changes, selection, locale, renderBlock, onSelectFile, locateBlock }) {
|
|
2969
2970
|
var _element$properties2;
|
|
2970
2971
|
const labels = projectChangeLabels(locale);
|
|
2971
2972
|
const { element, target } = selection;
|
|
@@ -3000,7 +3001,8 @@ function ProjectElementView({ changes, selection, locale, renderBlock, onSelectF
|
|
|
3000
3001
|
target: {
|
|
3001
3002
|
isStage: target.isStage,
|
|
3002
3003
|
name: target.name
|
|
3003
|
-
}
|
|
3004
|
+
},
|
|
3005
|
+
locateBlock
|
|
3004
3006
|
}), element.kind === "costumes" && /* @__PURE__ */ React.createElement("div", { className: "twgit-element-pictures" }, ["before", "after"].map((version) => {
|
|
3005
3007
|
var _element$md5ext3, _element$version;
|
|
3006
3008
|
return /* @__PURE__ */ React.createElement(Picture, {
|
|
@@ -3299,6 +3301,7 @@ function ConnectionSettings({ settings, locale, readOrigin, runOperation }) {
|
|
|
3299
3301
|
}, labels[error]));
|
|
3300
3302
|
}
|
|
3301
3303
|
// src/pull-request-panel.tsx
|
|
3304
|
+
init_objectSpread2();
|
|
3302
3305
|
var en = {
|
|
3303
3306
|
title: "Pull requests",
|
|
3304
3307
|
number: "Pull request number",
|
|
@@ -3344,7 +3347,7 @@ var ja = {
|
|
|
3344
3347
|
function errorMessage(error, labels) {
|
|
3345
3348
|
return error instanceof PullRequestError ? labels[error.code] : labels.failed;
|
|
3346
3349
|
}
|
|
3347
|
-
function PullRequests({ name, labels, locale, renderBlock }) {
|
|
3350
|
+
function PullRequests({ name, labels, locale, renderBlock, locateFor }) {
|
|
3348
3351
|
const client = useMemo(() => createGitHubPullRequests(name), [name]);
|
|
3349
3352
|
const [page, setPage] = useState(1);
|
|
3350
3353
|
const [listing, setListing] = useState();
|
|
@@ -3432,12 +3435,12 @@ function PullRequests({ name, labels, locale, renderBlock }) {
|
|
|
3432
3435
|
}, labels.onGitHub)), /* @__PURE__ */ React.createElement(ProjectSourceReview, {
|
|
3433
3436
|
title: pr.title,
|
|
3434
3437
|
description: pr.description,
|
|
3435
|
-
files: pr.files,
|
|
3438
|
+
files: locateFor ? pr.files.map((file) => "patch" in file ? _objectSpread2$1(_objectSpread2$1({}, file), {}, { locateBlock: locateFor(file.path, pr.head.sha) }) : file) : pr.files,
|
|
3436
3439
|
renderBlock,
|
|
3437
3440
|
locale
|
|
3438
3441
|
}))));
|
|
3439
3442
|
}
|
|
3440
|
-
function PullRequestReviewPanel({ repository, locale, renderBlock }) {
|
|
3443
|
+
function PullRequestReviewPanel({ repository, locale, renderBlock, locateFor }) {
|
|
3441
3444
|
const labels = locale.startsWith("ja") ? ja : en;
|
|
3442
3445
|
const [origin, setOrigin] = useState();
|
|
3443
3446
|
useEffect(() => {
|
|
@@ -3471,7 +3474,44 @@ function PullRequestReviewPanel({ repository, locale, renderBlock }) {
|
|
|
3471
3474
|
name: origin.name,
|
|
3472
3475
|
labels,
|
|
3473
3476
|
locale,
|
|
3474
|
-
renderBlock
|
|
3477
|
+
renderBlock,
|
|
3478
|
+
locateFor
|
|
3479
|
+
});
|
|
3480
|
+
}
|
|
3481
|
+
// src/editor-block-locator.ts
|
|
3482
|
+
/**
|
|
3483
|
+
* Finds the stage or sprite of a compared version in the open project, opens its code and waits for the block,
|
|
3484
|
+
* then centers and outlines it. The caller decides which version matches the open project.
|
|
3485
|
+
*/
|
|
3486
|
+
function createEditorBlockLocator({ vm, openTargetCode, getWorkspace }) {
|
|
3487
|
+
return createScratchBlocksDiffLocator(async ({ target, id: blockId, signal }) => {
|
|
3488
|
+
const found = vm.runtime.targets.find((item) => target.isStage ? item.isStage : !item.isStage && item.isOriginal !== false && item.getName() === target.name);
|
|
3489
|
+
if (!found || signal.aborted) return null;
|
|
3490
|
+
await openTargetCode(found.id);
|
|
3491
|
+
const deadline = Date.now() + waitLimit;
|
|
3492
|
+
while (!signal.aborted) {
|
|
3493
|
+
var _vm$editingTarget;
|
|
3494
|
+
if (((_vm$editingTarget = vm.editingTarget) === null || _vm$editingTarget === void 0 ? void 0 : _vm$editingTarget.id) === found.id) {
|
|
3495
|
+
const workspace = await getWorkspace();
|
|
3496
|
+
if (signal.aborted) break;
|
|
3497
|
+
if (workspace === null || workspace === void 0 ? void 0 : workspace.getBlockById(blockId)) return workspace;
|
|
3498
|
+
}
|
|
3499
|
+
if (Date.now() >= deadline) break;
|
|
3500
|
+
await nextFrame(signal);
|
|
3501
|
+
}
|
|
3502
|
+
return null;
|
|
3503
|
+
});
|
|
3504
|
+
}
|
|
3505
|
+
var waitLimit = 2e3;
|
|
3506
|
+
function nextFrame(signal) {
|
|
3507
|
+
return new Promise((resolve) => {
|
|
3508
|
+
const done = () => {
|
|
3509
|
+
signal.removeEventListener("abort", done);
|
|
3510
|
+
resolve();
|
|
3511
|
+
};
|
|
3512
|
+
signal.addEventListener("abort", done, { once: true });
|
|
3513
|
+
if (typeof requestAnimationFrame === "function") requestAnimationFrame(done);
|
|
3514
|
+
else setTimeout(done, 16);
|
|
3475
3515
|
});
|
|
3476
3516
|
}
|
|
3477
3517
|
// src/index.tsx
|
|
@@ -3490,9 +3530,20 @@ var initial = {
|
|
|
3490
3530
|
};
|
|
3491
3531
|
var controllers = /* @__PURE__ */ new WeakMap();
|
|
3492
3532
|
var watchInterval = 2e3;
|
|
3493
|
-
function GitPanel({ document, host, controller }) {
|
|
3494
|
-
var _state$repository, _state$snapshot$head$, _state$snapshot$head, _state$
|
|
3533
|
+
function GitPanel({ vm, document, host, controller }) {
|
|
3534
|
+
var _state$repository, _state$snapshot$head$, _state$snapshot$head, _state$snapshot2;
|
|
3495
3535
|
const [visible, setVisible] = useState(false);
|
|
3536
|
+
const [slot, setSlot] = useState(null);
|
|
3537
|
+
const shown = visible || slot !== null;
|
|
3538
|
+
const locator = useMemo(() => slot && host.openTargetCode && host.getWorkspace ? createEditorBlockLocator({
|
|
3539
|
+
vm,
|
|
3540
|
+
openTargetCode: host.openTargetCode,
|
|
3541
|
+
getWorkspace: host.getWorkspace
|
|
3542
|
+
}) : void 0, [
|
|
3543
|
+
slot !== null,
|
|
3544
|
+
host,
|
|
3545
|
+
vm
|
|
3546
|
+
]);
|
|
3496
3547
|
const [review, setReview] = useState(false);
|
|
3497
3548
|
const [locale, setLocale] = useState(document.documentElement.lang || "en");
|
|
3498
3549
|
const [dirty, setDirty] = useState(host.isProjectDirty());
|
|
@@ -3550,8 +3601,34 @@ function GitPanel({ document, host, controller }) {
|
|
|
3550
3601
|
}
|
|
3551
3602
|
};
|
|
3552
3603
|
controller.open = (language) => {
|
|
3604
|
+
var _host$openPanel;
|
|
3553
3605
|
setLocale(language);
|
|
3554
|
-
|
|
3606
|
+
if (!((_host$openPanel = host.openPanel) === null || _host$openPanel === void 0 ? void 0 : _host$openPanel.call(host, {
|
|
3607
|
+
reference: {
|
|
3608
|
+
providerId: "turbowarp-plus-git",
|
|
3609
|
+
documentId: "local-git"
|
|
3610
|
+
},
|
|
3611
|
+
filename: "Git",
|
|
3612
|
+
title: labelsFor(language).title,
|
|
3613
|
+
transient: true,
|
|
3614
|
+
mount(element, close) {
|
|
3615
|
+
setVisible(false);
|
|
3616
|
+
setSlot({
|
|
3617
|
+
element,
|
|
3618
|
+
close
|
|
3619
|
+
});
|
|
3620
|
+
return {
|
|
3621
|
+
requestClose: close,
|
|
3622
|
+
dispose() {
|
|
3623
|
+
setSlot((previous) => (previous === null || previous === void 0 ? void 0 : previous.element) === element ? null : previous);
|
|
3624
|
+
}
|
|
3625
|
+
};
|
|
3626
|
+
}
|
|
3627
|
+
}))) setVisible(true);
|
|
3628
|
+
};
|
|
3629
|
+
const closePanel = () => {
|
|
3630
|
+
if (slot) slot.close();
|
|
3631
|
+
else setVisible(false);
|
|
3555
3632
|
};
|
|
3556
3633
|
controller.refresh = async (automatic = false) => {
|
|
3557
3634
|
if (!alive.current || !current.current.repository) return;
|
|
@@ -3600,7 +3677,7 @@ function GitPanel({ document, host, controller }) {
|
|
|
3600
3677
|
}, [host]);
|
|
3601
3678
|
useEffect(() => {
|
|
3602
3679
|
const repository = state.repository;
|
|
3603
|
-
if (!
|
|
3680
|
+
if (!shown || !repository) return;
|
|
3604
3681
|
const view = document.defaultView;
|
|
3605
3682
|
let stopped = false;
|
|
3606
3683
|
let checking = false;
|
|
@@ -3647,19 +3724,19 @@ function GitPanel({ document, host, controller }) {
|
|
|
3647
3724
|
observer === null || observer === void 0 || observer.disconnect();
|
|
3648
3725
|
};
|
|
3649
3726
|
}, [
|
|
3650
|
-
|
|
3727
|
+
shown,
|
|
3651
3728
|
state.repository,
|
|
3652
3729
|
locale
|
|
3653
3730
|
]);
|
|
3654
3731
|
useEffect(() => {
|
|
3655
3732
|
var _dialog$current;
|
|
3656
|
-
if (!visible) return;
|
|
3733
|
+
if (!visible || slot) return;
|
|
3657
3734
|
const previous = document.activeElement;
|
|
3658
3735
|
(_dialog$current = dialog.current) === null || _dialog$current === void 0 || _dialog$current.focus();
|
|
3659
3736
|
return () => {
|
|
3660
3737
|
if (previous === null || previous === void 0 ? void 0 : previous.isConnected) previous.focus();
|
|
3661
3738
|
};
|
|
3662
|
-
}, [visible]);
|
|
3739
|
+
}, [visible, slot]);
|
|
3663
3740
|
const chooseRoot = async () => {
|
|
3664
3741
|
const view = document.defaultView;
|
|
3665
3742
|
await run(async () => {
|
|
@@ -3696,6 +3773,7 @@ function GitPanel({ document, host, controller }) {
|
|
|
3696
3773
|
} finally {
|
|
3697
3774
|
opening.current = false;
|
|
3698
3775
|
}
|
|
3776
|
+
if (slot) controller.open(locale);
|
|
3699
3777
|
if (!alive.current) return {};
|
|
3700
3778
|
setDirty(host.isProjectDirty());
|
|
3701
3779
|
setState((previous) => _objectSpread2$1(_objectSpread2$1({}, previous), {}, {
|
|
@@ -3776,7 +3854,7 @@ function GitPanel({ document, host, controller }) {
|
|
|
3776
3854
|
const focusTrap = (event) => {
|
|
3777
3855
|
if (event.key === "Escape") {
|
|
3778
3856
|
event.stopPropagation();
|
|
3779
|
-
|
|
3857
|
+
closePanel();
|
|
3780
3858
|
return;
|
|
3781
3859
|
}
|
|
3782
3860
|
if (event.key !== "Tab") return;
|
|
@@ -3795,23 +3873,21 @@ function GitPanel({ document, host, controller }) {
|
|
|
3795
3873
|
state.snapshot,
|
|
3796
3874
|
state.selected
|
|
3797
3875
|
]);
|
|
3798
|
-
if (!
|
|
3876
|
+
if (!shown) return null;
|
|
3799
3877
|
const groups = state.commit ? ["commit"] : [
|
|
3800
3878
|
"staged",
|
|
3801
3879
|
"unstaged",
|
|
3802
3880
|
"untracked"
|
|
3803
3881
|
];
|
|
3804
3882
|
const projectList = typeof projectChanges === "object" ? projectChanges : null;
|
|
3883
|
+
const projectFile = state.project === null ? null : [state.project, "project.source.json"].filter(Boolean).join("/");
|
|
3884
|
+
const fileLocator = (change) => locator && change.path === projectFile && (change.group === "unstaged" || change.group === "untracked" || change.group === "staged" && !state.changes.some((item) => item.path === change.path && item.group === "unstaged")) ? locator : void 0;
|
|
3885
|
+
const reviewLocator = (path, headSha) => {
|
|
3886
|
+
var _state$snapshot;
|
|
3887
|
+
return locator && path === projectFile && ((_state$snapshot = state.snapshot) === null || _state$snapshot === void 0 ? void 0 : _state$snapshot.head) === headSha && !state.changes.length ? locator : void 0;
|
|
3888
|
+
};
|
|
3805
3889
|
const selection = projectList && !state.selected ? findElement(projectList.list, element) : null;
|
|
3806
|
-
|
|
3807
|
-
className: "twgit-panel",
|
|
3808
|
-
ref: dialog,
|
|
3809
|
-
tabIndex: -1,
|
|
3810
|
-
role: "dialog",
|
|
3811
|
-
"aria-modal": "true",
|
|
3812
|
-
"aria-label": labels.title,
|
|
3813
|
-
onKeyDown: focusTrap
|
|
3814
|
-
}, /* @__PURE__ */ React.createElement("header", null, /* @__PURE__ */ React.createElement("h2", null, labels.title), /* @__PURE__ */ React.createElement("button", {
|
|
3890
|
+
const content = /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("header", null, /* @__PURE__ */ React.createElement("h2", null, labels.title), /* @__PURE__ */ React.createElement("button", {
|
|
3815
3891
|
onClick: chooseRoot,
|
|
3816
3892
|
disabled: state.busy
|
|
3817
3893
|
}, labels.choose), !review && /* @__PURE__ */ React.createElement("button", {
|
|
@@ -3824,7 +3900,7 @@ function GitPanel({ document, host, controller }) {
|
|
|
3824
3900
|
type: "button",
|
|
3825
3901
|
disabled: state.busy,
|
|
3826
3902
|
onClick: () => setReview(!review)
|
|
3827
|
-
}, review ? labels.localChanges : labels.pullRequests), /* @__PURE__ */ React.createElement("button", { onClick:
|
|
3903
|
+
}, review ? labels.localChanges : labels.pullRequests), /* @__PURE__ */ React.createElement("button", { onClick: closePanel }, labels.close)), /* @__PURE__ */ React.createElement("div", { className: "twgit-context" }, /* @__PURE__ */ React.createElement("span", null, labels.repository, " · ", ((_state$repository = state.repository) === null || _state$repository === void 0 ? void 0 : _state$repository.root.name) || "—"), /* @__PURE__ */ React.createElement("span", null, labels.project, " · ", state.project === null ? labels.none : state.project || labels.root), /* @__PURE__ */ React.createElement("span", { "data-git-dirty": "" }, dirty ? labels.dirty : labels.saved), state.snapshot && /* @__PURE__ */ React.createElement("span", {
|
|
3828
3904
|
"data-git-upstream": state.snapshot.upstream.state,
|
|
3829
3905
|
title: labels.checkedAt
|
|
3830
3906
|
}, upstreamText(state.snapshot, labels)), !review && host.connectionSettings && /* @__PURE__ */ React.createElement(ConnectionSettings, {
|
|
@@ -3845,7 +3921,8 @@ function GitPanel({ document, host, controller }) {
|
|
|
3845
3921
|
}, state.notice), state.busy && /* @__PURE__ */ React.createElement("p", { role: "status" }, labels.loading), !state.repository && /* @__PURE__ */ React.createElement("p", null, labels.empty), review && state.repository && host.renderReviewBlock && /* @__PURE__ */ React.createElement(PullRequestReviewPanel, {
|
|
3846
3922
|
repository: state.repository,
|
|
3847
3923
|
locale,
|
|
3848
|
-
renderBlock: host.renderReviewBlock
|
|
3924
|
+
renderBlock: host.renderReviewBlock,
|
|
3925
|
+
locateFor: reviewLocator
|
|
3849
3926
|
}), !review && state.repository && /* @__PURE__ */ React.createElement("div", { className: "twgit-content" }, /* @__PURE__ */ React.createElement("aside", null, state.browser && /* @__PURE__ */ React.createElement("details", null, /* @__PURE__ */ React.createElement("summary", null, labels.browse), /* @__PURE__ */ React.createElement("p", null, /* @__PURE__ */ React.createElement("code", null, state.browser.path || "/")), /* @__PURE__ */ React.createElement("button", {
|
|
3850
3927
|
disabled: state.busy || !state.browser.path,
|
|
3851
3928
|
onClick: () => void browse(state.browser.path.split("/").slice(0, -1).join("/"))
|
|
@@ -3878,7 +3955,7 @@ function GitPanel({ document, host, controller }) {
|
|
|
3878
3955
|
fileListOpen,
|
|
3879
3956
|
setFileListOpen
|
|
3880
3957
|
}) : fileList;
|
|
3881
|
-
})(), !state.changes.length && /* @__PURE__ */ React.createElement("p", null, labels.clean), /* @__PURE__ */ React.createElement("h3", null, labels.history), /* @__PURE__ */ React.createElement("ol", { className: "twgit-history" }, (_state$
|
|
3958
|
+
})(), !state.changes.length && /* @__PURE__ */ React.createElement("p", null, labels.clean), /* @__PURE__ */ React.createElement("h3", null, labels.history), /* @__PURE__ */ React.createElement("ol", { className: "twgit-history" }, (_state$snapshot2 = state.snapshot) === null || _state$snapshot2 === void 0 ? void 0 : _state$snapshot2.commits.map((commit) => /* @__PURE__ */ React.createElement("li", { key: commit.oid }, /* @__PURE__ */ React.createElement("button", {
|
|
3882
3959
|
disabled: state.busy,
|
|
3883
3960
|
"aria-pressed": state.commit === commit.oid,
|
|
3884
3961
|
title: commit.oid,
|
|
@@ -3887,6 +3964,7 @@ function GitPanel({ document, host, controller }) {
|
|
|
3887
3964
|
changes: projectList,
|
|
3888
3965
|
selection,
|
|
3889
3966
|
locale,
|
|
3967
|
+
locateBlock: state.commit ? void 0 : locator,
|
|
3890
3968
|
renderBlock: host.renderReviewBlock,
|
|
3891
3969
|
onSelectFile: openFile
|
|
3892
3970
|
}) : state.selected ? /* @__PURE__ */ React.createElement(FileDiffView, {
|
|
@@ -3894,9 +3972,24 @@ function GitPanel({ document, host, controller }) {
|
|
|
3894
3972
|
diff: state.diff,
|
|
3895
3973
|
labels,
|
|
3896
3974
|
locale,
|
|
3975
|
+
locateBlock: fileLocator(state.selected),
|
|
3897
3976
|
renderBlock: host.renderReviewBlock,
|
|
3898
3977
|
loadAsset
|
|
3899
|
-
}) : /* @__PURE__ */ React.createElement("p", null, labels.select))))
|
|
3978
|
+
}) : /* @__PURE__ */ React.createElement("p", null, labels.select))));
|
|
3979
|
+
if (slot) return createPortal(/* @__PURE__ */ React.createElement("div", {
|
|
3980
|
+
className: "twgit-panel twgit-panel-tab",
|
|
3981
|
+
role: "region",
|
|
3982
|
+
"aria-label": labels.title
|
|
3983
|
+
}, content), slot.element);
|
|
3984
|
+
return /* @__PURE__ */ React.createElement("div", { className: "twgit-backdrop" }, /* @__PURE__ */ React.createElement("div", {
|
|
3985
|
+
className: "twgit-panel",
|
|
3986
|
+
ref: dialog,
|
|
3987
|
+
tabIndex: -1,
|
|
3988
|
+
role: "dialog",
|
|
3989
|
+
"aria-modal": "true",
|
|
3990
|
+
"aria-label": labels.title,
|
|
3991
|
+
onKeyDown: focusTrap
|
|
3992
|
+
}, content));
|
|
3900
3993
|
}
|
|
3901
3994
|
/** The GitHub relation in plain words, with the number of records where it helps. */
|
|
3902
3995
|
function upstreamText({ upstream }, labels) {
|
|
@@ -3919,6 +4012,7 @@ function mountGitPanel({ vm, document, host }) {
|
|
|
3919
4012
|
};
|
|
3920
4013
|
controllers.set(vm, controller);
|
|
3921
4014
|
render(/* @__PURE__ */ React.createElement(GitPanel, {
|
|
4015
|
+
vm,
|
|
3922
4016
|
document,
|
|
3923
4017
|
host,
|
|
3924
4018
|
controller
|
|
@@ -3945,4 +4039,4 @@ function renderGitMenu(createElement, { vm, locale = "en", MenuItem, MenuSection
|
|
|
3945
4039
|
}
|
|
3946
4040
|
}, labelsFor(locale).menu));
|
|
3947
4041
|
}
|
|
3948
|
-
export { GitPanelError, createReadOnlyFs, createScratchBlocksDiffRenderer, mountGitPanel, openRepository, renderGitMenu };
|
|
4042
|
+
export { GitPanelError, createEditorBlockLocator, createReadOnlyFs, createScratchBlocksDiffLocator, createScratchBlocksDiffRenderer, mountGitPanel, openRepository, renderGitMenu };
|
|
@@ -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-
|
|
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
|
-
|
|
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;
|
package/dist/{scratch-block-diff-renderer-DZR8ep-T.js → scratch-block-diff-renderer-CgAGfmAc.js}
RENAMED
|
@@ -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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
@@ -163,4 +163,6 @@
|
|
|
163
163
|
.twgit-element-files { margin-top: 12px; }
|
|
164
164
|
.twgit-panel .twgit-element span.twgit-element-name { width: auto; font-family: inherit; }
|
|
165
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; }
|
|
166
168
|
/*$vite$:1*/
|
package/package.json
CHANGED
|
@@ -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
|
+
}
|
package/src/file-diff-view.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import type {FileChange, FileDiff} from './repository.js';
|
|
|
5
5
|
import type {labelsFor} from './messages.js';
|
|
6
6
|
import type {ProjectBlockRenderer} from './scratch-block-diff-renderer.js';
|
|
7
7
|
import type {ProjectAssetLoader} from './project-assets.js';
|
|
8
|
+
import type {ProjectBlockLocator} from './block-diff-location.js';
|
|
8
9
|
|
|
9
10
|
const en = {view: 'Show changes as', blocks: 'Blocks', text: 'Text'};
|
|
10
11
|
const ja: typeof en = {view: '変更の表示', blocks: 'ブロック', text: 'テキスト'};
|
|
@@ -15,9 +16,9 @@ export const isProjectSource = (path: string) => path.split('/').pop() === 'proj
|
|
|
15
16
|
/**
|
|
16
17
|
* Shows one changed file. Project sources are compared as blocks by default, with their text diff one switch away.
|
|
17
18
|
*/
|
|
18
|
-
export function FileDiffView({change, diff, labels, locale, renderBlock, loadAsset}: {
|
|
19
|
+
export function FileDiffView({change, diff, labels, locale, renderBlock, loadAsset, locateBlock}: {
|
|
19
20
|
change: FileChange; diff: FileDiff | null; labels: ReturnType<typeof labelsFor>; locale: string;
|
|
20
|
-
renderBlock?: ProjectBlockRenderer; loadAsset?: ProjectAssetLoader;
|
|
21
|
+
renderBlock?: ProjectBlockRenderer; loadAsset?: ProjectAssetLoader; locateBlock?: ProjectBlockLocator;
|
|
21
22
|
}) {
|
|
22
23
|
const [view, setView] = useState<'blocks' | 'text'>('blocks');
|
|
23
24
|
const names = locale.startsWith('ja') ? ja : en;
|
|
@@ -40,7 +41,7 @@ export function FileDiffView({change, diff, labels, locale, renderBlock, loadAss
|
|
|
40
41
|
{(['blocks', 'text'] as const).map(mode => <button key={mode} type="button" aria-pressed={view === mode}
|
|
41
42
|
data-git-view={mode} onClick={() => setView(mode)}>{names[mode]}</button>)}
|
|
42
43
|
</div>}
|
|
43
|
-
{sources && view === 'blocks' ? <ProjectSourceComparison renderBlock={renderBlock!} locale={locale} fallback={text} loadAsset={loadAsset}
|
|
44
|
+
{sources && view === 'blocks' ? <ProjectSourceComparison renderBlock={renderBlock!} locale={locale} fallback={text} loadAsset={loadAsset} locateBlock={locateBlock}
|
|
44
45
|
before={change.before === null ? null : sources.before} after={change.after === null ? null : sources.after}/> : text}
|
|
45
46
|
</>;
|
|
46
47
|
}
|
package/src/index.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, {useEffect, useMemo, useRef, useState} from 'react';
|
|
2
|
-
import {render, unmountComponentAtNode} from 'react-dom';
|
|
2
|
+
import {createPortal, render, unmountComponentAtNode} from 'react-dom';
|
|
3
3
|
import {FileDiffView} from './file-diff-view.js';
|
|
4
4
|
import {findElement, ProjectChangeListView, ProjectElementView, projectChangeLabels, useProjectChanges} from './project-change-list.js';
|
|
5
5
|
import type {ChangedFile} from './project-change-model.js';
|
|
@@ -12,6 +12,11 @@ import type {GitConnectionSettings} from './connection-settings.js';
|
|
|
12
12
|
import type {RemoteOperation} from './remote-operations.js';
|
|
13
13
|
import {PullRequestReviewPanel} from './pull-request-panel.js';
|
|
14
14
|
import type {ProjectBlockRenderer} from './scratch-block-diff-renderer.js';
|
|
15
|
+
import type {ScratchBlockLocationWorkspace} from './block-diff-location.js';
|
|
16
|
+
export {createScratchBlocksDiffLocator} from './block-diff-location.js';
|
|
17
|
+
export {createEditorBlockLocator} from './editor-block-locator.js';
|
|
18
|
+
import {createEditorBlockLocator} from './editor-block-locator.js';
|
|
19
|
+
import type {LocatorVM} from './editor-block-locator.js';
|
|
15
20
|
|
|
16
21
|
export {createScratchBlocksDiffRenderer} from './scratch-block-diff-renderer.js';
|
|
17
22
|
export type {ScratchBlockDiffRendererOptions, ScratchBlocksPort} from './scratch-block-diff-renderer.js';
|
|
@@ -26,6 +31,22 @@ export interface GitHost {
|
|
|
26
31
|
isProjectDirty(): boolean;
|
|
27
32
|
isProjectRunning?(): boolean;
|
|
28
33
|
subscribe?(listener: (event: 'loaded' | 'changed') => void): () => void;
|
|
34
|
+
/**
|
|
35
|
+
* Opens the panel as a tab of the host's layout, with the same shape as the layout package's DocumentPanelRequest.
|
|
36
|
+
* Returning `false` keeps the modal dialog.
|
|
37
|
+
*/
|
|
38
|
+
openPanel?(request: GitPanelRequest): boolean;
|
|
39
|
+
/** Opens the code of one stage or sprite, so a diff can point at its block. Without it, no block is located. */
|
|
40
|
+
openTargetCode?(targetId: string): void | Promise<void>;
|
|
41
|
+
/** The Editor's code workspace as it is now. */
|
|
42
|
+
getWorkspace?(): ScratchBlockLocationWorkspace | null | Promise<ScratchBlockLocationWorkspace | null>;
|
|
43
|
+
}
|
|
44
|
+
export interface GitPanelRequest {
|
|
45
|
+
reference: {providerId: string; documentId: string};
|
|
46
|
+
filename: string;
|
|
47
|
+
title?: string;
|
|
48
|
+
transient?: boolean;
|
|
49
|
+
mount(element: HTMLElement, close: () => void): {requestClose(): void; dispose(): void};
|
|
29
50
|
}
|
|
30
51
|
interface Controller {open(locale: string): void; refresh(automatic?: boolean): Promise<void>}
|
|
31
52
|
interface State {
|
|
@@ -51,8 +72,14 @@ type ObserverWindow = Window & {FileSystemObserver?: new (callback: () => void)
|
|
|
51
72
|
}};
|
|
52
73
|
const watchInterval = 2000;
|
|
53
74
|
|
|
54
|
-
function GitPanel({document, host, controller}: {document: Document; host: GitHost; controller: Controller}) {
|
|
75
|
+
function GitPanel({vm, document, host, controller}: {vm: object; document: Document; host: GitHost; controller: Controller}) {
|
|
55
76
|
const [visible, setVisible] = useState(false);
|
|
77
|
+
// A layout tab that holds the panel. The panel component stays mounted, so the repository survives closing the tab.
|
|
78
|
+
const [slot, setSlot] = useState<{element: HTMLElement; close(): void} | null>(null);
|
|
79
|
+
const shown = visible || slot !== null;
|
|
80
|
+
// Only a tab leaves the Editor visible, so only a tab offers to point at blocks in it.
|
|
81
|
+
const locator = useMemo(() => slot && host.openTargetCode && host.getWorkspace ? createEditorBlockLocator({
|
|
82
|
+
vm: vm as LocatorVM, openTargetCode: host.openTargetCode, getWorkspace: host.getWorkspace}) : undefined, [slot !== null, host, vm]);
|
|
56
83
|
const [review, setReview] = useState(false);
|
|
57
84
|
const [locale, setLocale] = useState(document.documentElement.lang || 'en');
|
|
58
85
|
const [dirty, setDirty] = useState(host.isProjectDirty());
|
|
@@ -99,7 +126,21 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
99
126
|
}
|
|
100
127
|
}
|
|
101
128
|
};
|
|
102
|
-
controller.open = language => {
|
|
129
|
+
controller.open = language => {
|
|
130
|
+
setLocale(language);
|
|
131
|
+
const opened = host.openPanel?.({reference: {providerId: 'turbowarp-plus-git', documentId: 'local-git'},
|
|
132
|
+
filename: 'Git', title: labelsFor(language).title, transient: true,
|
|
133
|
+
mount(element, close) {
|
|
134
|
+
setVisible(false);
|
|
135
|
+
setSlot({element, close});
|
|
136
|
+
return {requestClose: close, dispose() {setSlot(previous => previous?.element === element ? null : previous);}};
|
|
137
|
+
}});
|
|
138
|
+
if (!opened) setVisible(true);
|
|
139
|
+
};
|
|
140
|
+
const closePanel = () => {
|
|
141
|
+
if (slot) slot.close();
|
|
142
|
+
else setVisible(false);
|
|
143
|
+
};
|
|
103
144
|
controller.refresh = async (automatic = false) => {
|
|
104
145
|
if (!alive.current || !current.current.repository) return;
|
|
105
146
|
if (running.current) {
|
|
@@ -128,7 +169,7 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
128
169
|
// While the panel is open, notice Git commands, agents and editors that change the repository outside the Editor.
|
|
129
170
|
useEffect(() => {
|
|
130
171
|
const repository = state.repository;
|
|
131
|
-
if (!
|
|
172
|
+
if (!shown || !repository) return;
|
|
132
173
|
const view = document.defaultView as ObserverWindow;
|
|
133
174
|
let stopped = false;
|
|
134
175
|
let checking = false;
|
|
@@ -177,13 +218,13 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
177
218
|
document.removeEventListener('visibilitychange', onFocus);
|
|
178
219
|
observer?.disconnect();
|
|
179
220
|
};
|
|
180
|
-
}, [
|
|
221
|
+
}, [shown, state.repository, locale]);
|
|
181
222
|
useEffect(() => {
|
|
182
|
-
if (!visible) return;
|
|
223
|
+
if (!visible || slot) return;
|
|
183
224
|
const previous = document.activeElement as HTMLElement | null;
|
|
184
225
|
dialog.current?.focus();
|
|
185
226
|
return () => {if (previous?.isConnected) previous.focus();};
|
|
186
|
-
}, [visible]);
|
|
227
|
+
}, [visible, slot]);
|
|
187
228
|
const chooseRoot = async () => {
|
|
188
229
|
const view = document.defaultView as PickerWindow;
|
|
189
230
|
await run(async () => {
|
|
@@ -208,6 +249,8 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
208
249
|
const result = await host.openProject(directory, () => document.defaultView!.confirm(labels.replace));
|
|
209
250
|
if (result === 'cancelled') return {};
|
|
210
251
|
} finally {opening.current = false;}
|
|
252
|
+
// Loading the project brings its code to the front; keep the panel the person is working in in front of it.
|
|
253
|
+
if (slot) controller.open(locale);
|
|
211
254
|
if (!alive.current) return {};
|
|
212
255
|
setDirty(host.isProjectDirty());
|
|
213
256
|
setState(previous => ({...previous, project: path, snapshot: null, changes: [], selected: null, diff: null, commit: null}));
|
|
@@ -256,7 +299,7 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
256
299
|
});
|
|
257
300
|
};
|
|
258
301
|
const focusTrap = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
259
|
-
if (event.key === 'Escape') {event.stopPropagation();
|
|
302
|
+
if (event.key === 'Escape') {event.stopPropagation(); closePanel(); return;}
|
|
260
303
|
if (event.key !== 'Tab') return;
|
|
261
304
|
const items = [...event.currentTarget.querySelectorAll<HTMLElement>('button:not(:disabled), input:not(:disabled), select:not(:disabled), a[href], summary, [tabindex="0"]')]
|
|
262
305
|
.filter(item => item.getClientRects().length > 0);
|
|
@@ -268,13 +311,19 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
268
311
|
// One loader per selected change, so thumbnails read each asset once.
|
|
269
312
|
const loadAsset = useMemo(() => state.repository && state.snapshot && state.selected ?
|
|
270
313
|
state.repository.assetLoader(state.snapshot, state.selected) : undefined, [state.repository, state.snapshot, state.selected]);
|
|
271
|
-
if (!
|
|
314
|
+
if (!shown) return null;
|
|
272
315
|
const groups: ChangeGroup[] = state.commit ? ['commit'] : ['staged', 'unstaged', 'untracked'];
|
|
273
316
|
const projectList = typeof projectChanges === 'object' ? projectChanges : null;
|
|
317
|
+
// The open project matches the working tree, so only the working-tree side of its project file can be located.
|
|
318
|
+
const projectFile = state.project === null ? null : [state.project, 'project.source.json'].filter(Boolean).join('/');
|
|
319
|
+
const fileLocator = (change: FileChange) => locator && change.path === projectFile &&
|
|
320
|
+
(change.group === 'unstaged' || change.group === 'untracked' ||
|
|
321
|
+
(change.group === 'staged' && !state.changes.some(item => item.path === change.path && item.group === 'unstaged'))) ?
|
|
322
|
+
locator : undefined;
|
|
323
|
+
const reviewLocator = (path: string, headSha: string) => locator && path === projectFile && state.snapshot?.head === headSha &&
|
|
324
|
+
!state.changes.length ? locator : undefined;
|
|
274
325
|
const selection = projectList && !state.selected ? findElement(projectList.list, element) : null;
|
|
275
|
-
|
|
276
|
-
<div className="twgit-panel" ref={dialog} tabIndex={-1} role="dialog" aria-modal="true" aria-label={labels.title}
|
|
277
|
-
onKeyDown={focusTrap}>
|
|
326
|
+
const content = <>
|
|
278
327
|
<header>
|
|
279
328
|
<h2>{labels.title}</h2>
|
|
280
329
|
<button onClick={chooseRoot} disabled={state.busy}>{labels.choose}</button>
|
|
@@ -282,7 +331,7 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
282
331
|
{!review && state.project !== null && <button disabled={state.busy} onClick={() => void openProject(state.project!)}>{labels.reload}</button>}
|
|
283
332
|
{state.repository && host.renderReviewBlock && <button type="button" disabled={state.busy}
|
|
284
333
|
onClick={() => setReview(!review)}>{review ? labels.localChanges : labels.pullRequests}</button>}
|
|
285
|
-
<button onClick={
|
|
334
|
+
<button onClick={closePanel}>{labels.close}</button>
|
|
286
335
|
</header>
|
|
287
336
|
<div className="twgit-context">
|
|
288
337
|
<span>{labels.repository} · {state.repository?.root.name || '—'}</span>
|
|
@@ -312,7 +361,8 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
312
361
|
{state.busy && <p role="status">{labels.loading}</p>}
|
|
313
362
|
{!state.repository && <p>{labels.empty}</p>}
|
|
314
363
|
{review && state.repository && host.renderReviewBlock &&
|
|
315
|
-
<PullRequestReviewPanel repository={state.repository} locale={locale} renderBlock={host.renderReviewBlock}
|
|
364
|
+
<PullRequestReviewPanel repository={state.repository} locale={locale} renderBlock={host.renderReviewBlock}
|
|
365
|
+
locateFor={reviewLocator}/>}
|
|
316
366
|
{!review && state.repository && <div className="twgit-content">
|
|
317
367
|
<aside>
|
|
318
368
|
{state.browser && <details>
|
|
@@ -350,12 +400,20 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
350
400
|
</aside>
|
|
351
401
|
<main className="twgit-diff">
|
|
352
402
|
{selection && projectList ? <ProjectElementView changes={projectList} selection={selection} locale={locale}
|
|
403
|
+
locateBlock={state.commit ? undefined : locator}
|
|
353
404
|
renderBlock={host.renderReviewBlock} onSelectFile={openFile}/> :
|
|
354
405
|
state.selected ? <FileDiffView change={state.selected} diff={state.diff} labels={labels} locale={locale}
|
|
406
|
+
locateBlock={fileLocator(state.selected)}
|
|
355
407
|
renderBlock={host.renderReviewBlock} loadAsset={loadAsset}/> : <p>{labels.select}</p>}
|
|
356
408
|
</main>
|
|
357
409
|
</div>}
|
|
358
|
-
|
|
410
|
+
</>;
|
|
411
|
+
// In a tab the Editor stays usable beside the panel, so there is no backdrop, focus trap or Escape to close.
|
|
412
|
+
if (slot) return createPortal(<div className="twgit-panel twgit-panel-tab" role="region" aria-label={labels.title}>{content}</div>,
|
|
413
|
+
slot.element);
|
|
414
|
+
return <div className="twgit-backdrop">
|
|
415
|
+
<div className="twgit-panel" ref={dialog} tabIndex={-1} role="dialog" aria-modal="true" aria-label={labels.title}
|
|
416
|
+
onKeyDown={focusTrap}>{content}</div>
|
|
359
417
|
</div>;
|
|
360
418
|
}
|
|
361
419
|
|
|
@@ -376,7 +434,7 @@ export function mountGitPanel({vm, document, host}: {vm: object; document: Docum
|
|
|
376
434
|
document.body.append(root);
|
|
377
435
|
const controller: Controller = {open() {}, async refresh() {}};
|
|
378
436
|
controllers.set(vm, controller);
|
|
379
|
-
render(<GitPanel document={document} host={host} controller={controller}/>, root);
|
|
437
|
+
render(<GitPanel vm={vm} document={document} host={host} controller={controller}/>, root);
|
|
380
438
|
return {refresh: () => controller.refresh(), dispose() {
|
|
381
439
|
if (controllers.get(vm) === controller) controllers.delete(vm);
|
|
382
440
|
void host.connectionSettings?.disconnect().catch(() => {});
|
|
@@ -7,6 +7,7 @@ import type {ProjectAssetLoader, ProjectVersion} from './project-assets.js';
|
|
|
7
7
|
import {ProjectSourceComparison} from './project-source-review.js';
|
|
8
8
|
import type {ChangeGroup, FileChange, Repository, Snapshot} from './repository.js';
|
|
9
9
|
import type {ProjectBlockRenderer} from './scratch-block-diff-renderer.js';
|
|
10
|
+
import type {ProjectBlockLocator} from './block-diff-location.js';
|
|
10
11
|
|
|
11
12
|
const en = {
|
|
12
13
|
title: 'Changes by project element', stage: 'Stage', code: (count: number) => `Code · ${count} ${count === 1 ? 'change' : 'changes'}`,
|
|
@@ -164,9 +165,9 @@ const show = (value: unknown) => {
|
|
|
164
165
|
};
|
|
165
166
|
|
|
166
167
|
/** The details of one selected row: its code, pictures or values in both versions, and the files behind it. */
|
|
167
|
-
export function ProjectElementView({changes, selection, locale, renderBlock, onSelectFile}: {
|
|
168
|
+
export function ProjectElementView({changes, selection, locale, renderBlock, onSelectFile, locateBlock}: {
|
|
168
169
|
changes: ProjectChanges; selection: {element: ProjectElementChange; target: ProjectTargetChanges | null}; locale: string;
|
|
169
|
-
renderBlock?: ProjectBlockRenderer; onSelectFile(file: ChangedFile): void;
|
|
170
|
+
renderBlock?: ProjectBlockRenderer; onSelectFile(file: ChangedFile): void; locateBlock?: ProjectBlockLocator;
|
|
170
171
|
}) {
|
|
171
172
|
const labels = projectChangeLabels(locale);
|
|
172
173
|
const {element, target} = selection;
|
|
@@ -189,7 +190,7 @@ export function ProjectElementView({changes, selection, locale, renderBlock, onS
|
|
|
189
190
|
<h3>{target && <span>{target.isStage ? labels.stage : target.name} · </span>}{elementTitle(element, target, labels, locale)}</h3>
|
|
190
191
|
{element.kind === 'code' && target && renderBlock && <ProjectSourceComparison before={changes.sources.before}
|
|
191
192
|
after={changes.sources.after} renderBlock={renderBlock} locale={locale} loadAsset={changes.loadAsset}
|
|
192
|
-
target={{isStage: target.isStage, name: target.name}}/>}
|
|
193
|
+
target={{isStage: target.isStage, name: target.name}} locateBlock={locateBlock}/>}
|
|
193
194
|
{element.kind === 'costumes' && <div className="twgit-element-pictures">
|
|
194
195
|
{(['before', 'after'] as const).map(version => <Picture key={version} md5ext={element.md5ext?.[version]} version={version}
|
|
195
196
|
loadAsset={changes.loadAsset} labels={labels} title={element[version]?.name === undefined ? labels[version] : `${labels[version]} · ${String(element[version]!.name)}`}/>)}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import React, {useMemo, useState} from 'react';
|
|
1
|
+
import React, {useEffect, useMemo, useRef, useState} from 'react';
|
|
2
2
|
import {BlockPreview, usePreviewSync} from './block-diff-preview.js';
|
|
3
3
|
import {isRecord, parseProjectSource, readProjectSourceDiff} from './project-source-patch.js';
|
|
4
4
|
import type {BlockSide} from './project-source-patch.js';
|
|
5
5
|
import {compareProjectForReview} from './project-source-review-model.js';
|
|
6
6
|
import type {ScriptReview, TargetReview} from './project-source-review-model.js';
|
|
7
7
|
import type {ProjectBlockRenderer} from './scratch-block-diff-renderer.js';
|
|
8
|
+
import type {ProjectBlockLocator} from './block-diff-location.js';
|
|
9
|
+
import type {LocationStatus} from './block-diff-preview.js';
|
|
8
10
|
import {currentCostume, useCostumeImage} from './project-assets.js';
|
|
9
11
|
import type {ProjectAssetLoader, ProjectVersion} from './project-assets.js';
|
|
10
12
|
|
|
@@ -15,7 +17,9 @@ export type ProjectSourceReviewFile = {
|
|
|
15
17
|
summary?: string;
|
|
16
18
|
previousPath?: string;
|
|
17
19
|
unavailable?: boolean;
|
|
18
|
-
} & ({patch: string; beforeSource?: string; loadAsset?: ProjectAssetLoader
|
|
20
|
+
} & ({patch: string; beforeSource?: string; loadAsset?: ProjectAssetLoader;
|
|
21
|
+
/** Offers "Highlight in Editor" on this file's after version, which the caller has matched with the open project. */
|
|
22
|
+
locateBlock?: ProjectBlockLocator} | {status: 'added' | 'removed' | 'updated'});
|
|
19
23
|
|
|
20
24
|
export interface ProjectSourceReviewProps {
|
|
21
25
|
title: string;
|
|
@@ -37,7 +41,9 @@ const ja = {
|
|
|
37
41
|
related: '関連ファイル', properties: 'コード以外の変更', invalid: 'この作品の差分を読み込めません。',
|
|
38
42
|
flag: '緑の旗が押されたとき', broadcast: 'を受け取ったとき', key: 'キーが押されたとき',
|
|
39
43
|
click: 'このスプライトが押されたとき', script: 'コード', noChanges: '作品に内容の変更はありません。',
|
|
40
|
-
comparison: '作品の変更', unreadable: '作品データとして読み込めないため、テキストの差分を表示します。'
|
|
44
|
+
comparison: '作品の変更', unreadable: '作品データとして読み込めないため、テキストの差分を表示します。',
|
|
45
|
+
locate: '編集画面で強調', locating: '編集画面でブロックを検索中…', located: '編集画面で強調しました。',
|
|
46
|
+
missingLocation: '編集画面に該当する版またはブロックがありません。'
|
|
41
47
|
};
|
|
42
48
|
const en: typeof ja = {
|
|
43
49
|
review: 'Change review', fullDescription: 'Show full PR description', checks: 'Review checklist', files: 'Changed files', targets: 'Affected stages and sprites',
|
|
@@ -49,7 +55,9 @@ const en: typeof ja = {
|
|
|
49
55
|
related: 'Related file', properties: 'Changes outside the code', invalid: 'This project diff could not be read.',
|
|
50
56
|
flag: 'When green flag clicked', broadcast: 'received', key: 'When key pressed',
|
|
51
57
|
click: 'When this sprite clicked', script: 'Code', noChanges: 'No changes to the project contents.',
|
|
52
|
-
comparison: 'Project changes', unreadable: 'This file could not be read as project data, so its text diff is shown.'
|
|
58
|
+
comparison: 'Project changes', unreadable: 'This file could not be read as project data, so its text diff is shown.',
|
|
59
|
+
locate: 'Highlight in Editor', locating: 'Finding the block in the Editor…', located: 'Highlighted in the Editor.',
|
|
60
|
+
missingLocation: 'This version or block is unavailable in the Editor.'
|
|
53
61
|
};
|
|
54
62
|
|
|
55
63
|
const properties = new Map<string, readonly [string, string]>([
|
|
@@ -74,9 +82,39 @@ function scriptTitle(side: BlockSide, labels: typeof ja) {
|
|
|
74
82
|
return typeof block.opcode === 'string' ? block.opcode : labels.script;
|
|
75
83
|
}
|
|
76
84
|
|
|
77
|
-
|
|
85
|
+
type BlockLocation = {locate?(side: BlockSide): void; status(side: BlockSide): LocationStatus | undefined};
|
|
86
|
+
const sideKey = (side: BlockSide) => JSON.stringify([side.target.isStage, side.target.name, side.id]);
|
|
87
|
+
|
|
88
|
+
/** Highlights the after version of a script in the Editor, one request per comparison, and cancels it on unmount. */
|
|
89
|
+
function useBlockLocation(comparison: Comparison, locateBlock?: ProjectBlockLocator): BlockLocation {
|
|
90
|
+
const request = useRef<AbortController>();
|
|
91
|
+
const [location, setLocation] = useState<{comparison: Comparison; key: string; locator: ProjectBlockLocator; status: LocationStatus}>();
|
|
92
|
+
useEffect(() => () => request.current?.abort(), [comparison, locateBlock]);
|
|
93
|
+
const locate = async (side: BlockSide) => {
|
|
94
|
+
if (!locateBlock) return;
|
|
95
|
+
request.current?.abort();
|
|
96
|
+
const controller = new AbortController();
|
|
97
|
+
request.current = controller;
|
|
98
|
+
const selected = {comparison, key: sideKey(side), locator: locateBlock};
|
|
99
|
+
setLocation({...selected, status: 'locating'});
|
|
100
|
+
let found = false;
|
|
101
|
+
try {
|
|
102
|
+
found = await locateBlock({target: side.target, id: side.id, version: 'after', parts: [], signal: controller.signal});
|
|
103
|
+
} catch {
|
|
104
|
+
// The host may replace the active project while resolving its workspace.
|
|
105
|
+
}
|
|
106
|
+
if (!controller.signal.aborted) setLocation({...selected, status: found ? 'located' : 'missingLocation'});
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
locate: locateBlock && (side => void locate(side)),
|
|
110
|
+
status: side => location?.comparison === comparison && location.locator === locateBlock && location.key === sideKey(side) ?
|
|
111
|
+
location.status : undefined
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function ScriptComparison({script, labels, reviewed = false, setReviewed, renderBlock, loadAsset, location}: {
|
|
78
116
|
script: ScriptReview; labels: typeof ja; reviewed?: boolean; setReviewed?(value: boolean): void;
|
|
79
|
-
renderBlock: ProjectBlockRenderer; loadAsset?: ProjectAssetLoader;
|
|
117
|
+
renderBlock: ProjectBlockRenderer; loadAsset?: ProjectAssetLoader; location?: BlockLocation;
|
|
80
118
|
}) {
|
|
81
119
|
const [open, setOpen] = useState(false);
|
|
82
120
|
const {sync, toggle} = usePreviewSync(labels === ja ? 'ja' : 'en');
|
|
@@ -98,7 +136,9 @@ function ScriptComparison({script, labels, reviewed = false, setReviewed, render
|
|
|
98
136
|
{script[version].length ? script[version].map((side, index, sides) => <BlockPreview key={side.id} side={side}
|
|
99
137
|
title={sides.length > 1 ? `${labels[version]} ${index + 1}` : labels[version]}
|
|
100
138
|
marks={side.marks} renderBlock={renderBlock} withContext labels={labels}
|
|
101
|
-
version={version} loadAsset={loadAsset} sync={sync}
|
|
139
|
+
version={version} loadAsset={loadAsset} sync={sync}
|
|
140
|
+
onLocate={version === 'after' && location?.locate ? () => location.locate!(side) : undefined}
|
|
141
|
+
locationStatus={version === 'after' ? location?.status(side) : undefined}/>) :
|
|
102
142
|
<BlockPreview title={labels[version]} renderBlock={renderBlock} withContext labels={labels}/>}
|
|
103
143
|
</div>)}
|
|
104
144
|
</div>
|
|
@@ -135,10 +175,12 @@ function TargetThumbnails({target, labels, loadAsset}: {target: TargetReview; la
|
|
|
135
175
|
}
|
|
136
176
|
|
|
137
177
|
/** Lists the changed stages and sprites of one project file, each with its code comparisons. */
|
|
138
|
-
function ComparisonTargets({comparison, labels, renderBlock, loadAsset, review}: {
|
|
178
|
+
function ComparisonTargets({comparison, labels, renderBlock, loadAsset, locateBlock, review}: {
|
|
139
179
|
comparison: Comparison; labels: typeof ja; renderBlock: ProjectBlockRenderer; loadAsset?: ProjectAssetLoader;
|
|
180
|
+
locateBlock?: ProjectBlockLocator;
|
|
140
181
|
review?: {file: string; keys: ReadonlySet<string>; set(key: string, value: boolean): void};
|
|
141
182
|
}) {
|
|
183
|
+
const location = useBlockLocation(comparison, locateBlock);
|
|
142
184
|
return <>
|
|
143
185
|
{comparison.properties.length > 0 && <p>{labels.properties} · {propertyNames(comparison.properties, labels)}</p>}
|
|
144
186
|
{!comparison.targets.length && !comparison.properties.length && <p>{labels.noChanges}</p>}
|
|
@@ -149,7 +191,7 @@ function ComparisonTargets({comparison, labels, renderBlock, loadAsset, review}:
|
|
|
149
191
|
{target.scripts.map(script => {
|
|
150
192
|
const key = JSON.stringify([review?.file, target.isStage, target.name, script.id]);
|
|
151
193
|
return <ScriptComparison key={key} script={script} labels={labels} renderBlock={renderBlock} loadAsset={loadAsset}
|
|
152
|
-
reviewed={review?.keys.has(key)} setReviewed={review && (value => review.set(key, value))}/>;
|
|
194
|
+
location={location} reviewed={review?.keys.has(key)} setReviewed={review && (value => review.set(key, value))}/>;
|
|
153
195
|
})}
|
|
154
196
|
</section>)}
|
|
155
197
|
</>;
|
|
@@ -168,12 +210,14 @@ export interface ProjectSourceComparisonProps {
|
|
|
168
210
|
fallback?: React.ReactNode;
|
|
169
211
|
/** Reads the images of both versions for thumbnails. Without it, stages and sprites are shown by name. */
|
|
170
212
|
loadAsset?: ProjectAssetLoader;
|
|
213
|
+
/** Offers "Highlight in Editor" on the after version, which the caller has matched with the open project. */
|
|
214
|
+
locateBlock?: ProjectBlockLocator;
|
|
171
215
|
/** Shows only the code of this stage or sprite, without the overview and the changes outside the code. */
|
|
172
216
|
target?: {isStage: boolean; name: string};
|
|
173
217
|
}
|
|
174
218
|
|
|
175
219
|
/** Compares two versions of a project.source.json by stage, sprite and code, as the pull request review does. */
|
|
176
|
-
export function ProjectSourceComparison({before, after, renderBlock, locale = 'en', fallback, loadAsset, target}: ProjectSourceComparisonProps) {
|
|
220
|
+
export function ProjectSourceComparison({before, after, renderBlock, locale = 'en', fallback, loadAsset, locateBlock, target}: ProjectSourceComparisonProps) {
|
|
177
221
|
const labels = locale.startsWith('ja') ? ja : en;
|
|
178
222
|
const only = target && JSON.stringify([target.isStage, target.isStage || target.name]);
|
|
179
223
|
const comparison = useMemo(() => {
|
|
@@ -196,7 +240,8 @@ export function ProjectSourceComparison({before, after, renderBlock, locale = 'e
|
|
|
196
240
|
<div><dt>{labels.scripts}</dt><dd>{countScripts(comparison)}</dd></div>
|
|
197
241
|
</dl>
|
|
198
242
|
</header>}
|
|
199
|
-
<ComparisonTargets comparison={comparison} labels={labels} renderBlock={renderBlock} loadAsset={loadAsset}
|
|
243
|
+
<ComparisonTargets comparison={comparison} labels={labels} renderBlock={renderBlock} loadAsset={loadAsset}
|
|
244
|
+
locateBlock={locateBlock}/>
|
|
200
245
|
</section>;
|
|
201
246
|
}
|
|
202
247
|
|
|
@@ -251,6 +296,7 @@ export function ProjectSourceReview({title, description, verification, files, re
|
|
|
251
296
|
{(invalid || file.unavailable) && <p role="alert">{labels.invalid}</p>}
|
|
252
297
|
{comparison && <ComparisonTargets comparison={comparison} labels={labels} renderBlock={renderBlock}
|
|
253
298
|
loadAsset={'loadAsset' in file ? file.loadAsset : undefined}
|
|
299
|
+
locateBlock={'locateBlock' in file ? file.locateBlock : undefined}
|
|
254
300
|
review={{file: file.path, keys: reviewed, set: setReviewed}}/>}
|
|
255
301
|
</section>)}
|
|
256
302
|
</section>;
|
|
@@ -4,6 +4,7 @@ import type {GitHubPullRequests} from './github-pull-requests.js';
|
|
|
4
4
|
import {ProjectSourceReview} from './project-source-review.js';
|
|
5
5
|
import type {ProjectBlockRenderer} from './scratch-block-diff-renderer.js';
|
|
6
6
|
import type {Repository} from './repository.js';
|
|
7
|
+
import type {ProjectBlockLocator} from './block-diff-location.js';
|
|
7
8
|
|
|
8
9
|
const en = {
|
|
9
10
|
title: 'Pull requests', number: 'Pull request number', openNumber: 'Open pull request',
|
|
@@ -37,8 +38,9 @@ type Result<T> = {data: T; error?: never} | {data?: never; error: string};
|
|
|
37
38
|
type Listing = Awaited<ReturnType<GitHubPullRequests['list']>>;
|
|
38
39
|
type Pull = Awaited<ReturnType<GitHubPullRequests['load']>>;
|
|
39
40
|
|
|
40
|
-
function PullRequests({name, labels, locale, renderBlock}: {
|
|
41
|
+
function PullRequests({name, labels, locale, renderBlock, locateFor}: {
|
|
41
42
|
name: string; labels: typeof en; locale: string; renderBlock: ProjectBlockRenderer;
|
|
43
|
+
locateFor?(path: string, headSha: string): ProjectBlockLocator | undefined;
|
|
42
44
|
}) {
|
|
43
45
|
const client = useMemo(() => createGitHubPullRequests(name), [name]);
|
|
44
46
|
const [page, setPage] = useState(1);
|
|
@@ -108,14 +110,17 @@ function PullRequests({name, labels, locale, renderBlock}: {
|
|
|
108
110
|
<code title={pr.head.sha}>{pr.head.ref} → {pr.base.ref}</code>
|
|
109
111
|
<a href={pr.url} target="_blank" rel="noreferrer">{labels.onGitHub}</a>
|
|
110
112
|
</p>
|
|
111
|
-
<ProjectSourceReview title={pr.title} description={pr.description} files={pr.files
|
|
113
|
+
<ProjectSourceReview title={pr.title} description={pr.description} files={locateFor ? pr.files.map(file => 'patch' in file ? {...file, locateBlock: locateFor(file.path, pr.head.sha)} : file) : pr.files}
|
|
114
|
+
renderBlock={renderBlock} locale={locale}/>
|
|
112
115
|
</>}
|
|
113
116
|
</main>
|
|
114
117
|
</div>;
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
export function PullRequestReviewPanel({repository, locale, renderBlock}: {
|
|
120
|
+
export function PullRequestReviewPanel({repository, locale, renderBlock, locateFor}: {
|
|
118
121
|
repository: Repository; locale: string; renderBlock: ProjectBlockRenderer;
|
|
122
|
+
/** Returns a locator for a project file whose PR head matches the open project, or `undefined`. */
|
|
123
|
+
locateFor?(path: string, headSha: string): ProjectBlockLocator | undefined;
|
|
119
124
|
}) {
|
|
120
125
|
const labels = locale.startsWith('ja') ? ja : en;
|
|
121
126
|
const [origin, setOrigin] = useState<{repository: Repository; name: string | null; failed?: boolean}>();
|
|
@@ -130,5 +135,5 @@ export function PullRequestReviewPanel({repository, locale, renderBlock}: {
|
|
|
130
135
|
}, [repository]);
|
|
131
136
|
if (origin?.repository !== repository) return <p role="status" className="twgit-context">{labels.loading}</p>;
|
|
132
137
|
if (!origin.name) return <p className="twgit-context" role="status">{origin.failed ? labels.failed : labels.noOrigin}</p>;
|
|
133
|
-
return <PullRequests key={origin.name} name={origin.name} labels={labels} locale={locale} renderBlock={renderBlock}/>;
|
|
138
|
+
return <PullRequests key={origin.name} name={origin.name} labels={labels} locale={locale} renderBlock={renderBlock} locateFor={locateFor}/>;
|
|
134
139
|
}
|
package/src/style.css
CHANGED
|
@@ -55,3 +55,5 @@
|
|
|
55
55
|
.twgit-element-files { margin-top: 12px; }
|
|
56
56
|
.twgit-panel .twgit-element span.twgit-element-name { width: auto; font-family: inherit; }
|
|
57
57
|
.twgit-git-details > .twgit-connection-body { left: auto; right: 0; }
|
|
58
|
+
/* In a layout tab the panel fills the tab and leaves the Editor beside it. */
|
|
59
|
+
.twgit-panel.twgit-panel-tab { width: 100%; height: 100%; border: 0; border-radius: 0; box-shadow: none; }
|