@kubohiroya/turbowarp-plus-editor-git 0.17.0 → 0.18.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/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { a as openProjectSourceTabs, b as useCostumeImage, c as gitHubRepositoryFromOrigin, f as organizeProjectChanges, i as createScratchBlocksDiffLocator, l as ProjectSourceComparison, n as openProjectComparison, o as PullRequestError, p as projectPropertyLabel, r as createScratchBlocksDiffRenderer, s as createGitHubPullRequests, u as ProjectSourceReview, x as CloseButton, y as useAssetImage } from "./project-comparison-DjMlwyp8.js";
1
+ import { a as openProjectSourceTabs, b as useCostumeImage, c as gitHubRepositoryFromOrigin, f as organizeProjectChanges, i as createScratchBlocksDiffLocator, l as ProjectSourceComparison, n as openProjectComparison, o as PullRequestError, p as projectPropertyLabel, r as createScratchBlocksDiffRenderer, s as createGitHubPullRequests, u as ProjectSourceReview, x as CloseButton, y as useAssetImage } from "./project-comparison-BXOQcZ73.js";
2
2
  import { n as init_objectSpread2, r as __commonJSMin, t as _objectSpread2$1 } from "./objectSpread2-Dkd68nJH.js";
3
- import { l as parseProjectSource, u as readProjectSourceDiff } from "./project-asset-paths-nL5sV35A.js";
4
- import { a as pullRequestRef, i as pullRequestBranch, n as openRepository, o as GitPanelError, s as createReadOnlyFs } from "./repository-xIv0iZkj.js";
3
+ import { f as _objectWithoutProperties$1, l as parseProjectSource, u as readProjectSourceDiff } from "./project-asset-paths-BzwVf9HE.js";
4
+ import { a as pullRequestRef, i as pullRequestBranch, n as openRepository, o as GitPanelError, s as createReadOnlyFs } from "./repository-ByKRBftJ.js";
5
5
  import * as m from "react";
6
- import React, { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
6
+ import React, { createContext, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
7
7
  import { createPortal, render, unmountComponentAtNode } from "react-dom";
8
8
  // builtin:esm-external-require-react
9
9
  var require_builtin_esm_external_require_react = /* @__PURE__ */ __commonJSMin(((exports, module) => {
@@ -3026,91 +3026,12 @@ function ProjectElementView({ changes, selection, locale, renderBlock, onSelectF
3026
3026
  onClick: () => onSelectFile(file)
3027
3027
  }, file.path, " ", /* @__PURE__ */ React.createElement("small", null, labels.groups[file.group])))))));
3028
3028
  }
3029
- /**
3030
- * Lays out commits ordered newest first with every child above its parents. Parents missing from `commits`, such as
3031
- * those on a page not loaded yet or cut off by a shallow clone, keep their lane running to the bottom.
3032
- */
3033
- function layoutHistory(commits) {
3034
- const lanes = [];
3035
- let nextColor = 0;
3036
- const free = () => {
3037
- const index = lanes.indexOf(null);
3038
- return index === -1 ? lanes.length : index;
3039
- };
3040
- const rows = [];
3041
- for (const commit of commits) {
3042
- const segments = [];
3043
- const before = lanes.slice();
3044
- const waiting = before.flatMap((lane, index) => (lane === null || lane === void 0 ? void 0 : lane.oid) === commit.oid ? [index] : []);
3045
- const column = waiting.length ? waiting[0] : free();
3046
- const color = waiting.length ? before[column].color : nextColor++ % 8;
3047
- for (const [index, lane] of before.entries()) {
3048
- if (!lane) continue;
3049
- segments.push({
3050
- part: "top",
3051
- from: index,
3052
- to: lane.oid === commit.oid ? column : index,
3053
- color: lane.color
3054
- });
3055
- }
3056
- for (const index of waiting) lanes[index] = null;
3057
- const parents = [...new Set(commit.parents)];
3058
- lanes[column] = parents.length ? {
3059
- oid: parents[0],
3060
- color
3061
- } : null;
3062
- const joins = parents.length ? [{
3063
- index: column,
3064
- color
3065
- }] : [];
3066
- for (const parent of parents.slice(1)) {
3067
- let index = lanes.findIndex((lane) => (lane === null || lane === void 0 ? void 0 : lane.oid) === parent);
3068
- if (index === -1) {
3069
- index = free();
3070
- lanes[index] = {
3071
- oid: parent,
3072
- color: nextColor++ % 8
3073
- };
3074
- }
3075
- joins.push({
3076
- index,
3077
- color: lanes[index].color
3078
- });
3079
- }
3080
- while (lanes.length && lanes[lanes.length - 1] === null) lanes.pop();
3081
- for (const [index, lane] of before.entries()) if (lane && lane.oid !== commit.oid) segments.push({
3082
- part: "bottom",
3083
- from: index,
3084
- to: index,
3085
- color: lane.color
3086
- });
3087
- for (const join of joins) segments.push({
3088
- part: "bottom",
3089
- from: column,
3090
- to: join.index,
3091
- color: join.color
3092
- });
3093
- const width = Math.max(column + 1, ...segments.map((segment) => Math.max(segment.from, segment.to) + 1));
3094
- rows.push({
3095
- oid: commit.oid,
3096
- column,
3097
- color,
3098
- segments,
3099
- width
3100
- });
3101
- }
3102
- return {
3103
- rows,
3104
- width: Math.max(0, ...rows.map((row) => row.width))
3105
- };
3106
- }
3107
3029
  // src/messages.ts
3108
3030
  var messages = {
3109
3031
  en: {
3110
3032
  pullRequests: "Pull requests",
3111
3033
  localChanges: "Local changes",
3112
3034
  title: "Local Git",
3113
- menu: "Local Git…",
3114
3035
  close: "Close",
3115
3036
  choose: "Choose Repository…",
3116
3037
  refresh: "Refresh",
@@ -3216,13 +3137,16 @@ var messages = {
3216
3137
  historyCopied: "Copied the commit ID.",
3217
3138
  historyCopyFailed: "Could not copy the commit ID.",
3218
3139
  historyRefs: "Branches",
3219
- cancel: "Cancel"
3140
+ cancel: "Cancel",
3141
+ branchMenu: "Branches",
3142
+ openPanel: "Open Local Git",
3143
+ newBranchItem: "New Branch…",
3144
+ newBranchTitle: "New Branch"
3220
3145
  },
3221
3146
  ja: {
3222
3147
  pullRequests: "プルリクエスト",
3223
3148
  localChanges: "ローカルの変更",
3224
3149
  title: "ローカル Git",
3225
- menu: "ローカル Git…",
3226
3150
  close: "閉じる",
3227
3151
  choose: "リポジトリを選択…",
3228
3152
  refresh: "再取得",
@@ -3328,10 +3252,369 @@ var messages = {
3328
3252
  historyCopied: "コミットの ID をコピーしました。",
3329
3253
  historyCopyFailed: "コミットの ID をコピーできませんでした。",
3330
3254
  historyRefs: "ブランチ",
3331
- cancel: "キャンセル"
3255
+ cancel: "キャンセル",
3256
+ branchMenu: "ブランチ",
3257
+ openPanel: "ローカル Git を開く",
3258
+ newBranchItem: "新しいブランチ…",
3259
+ newBranchTitle: "新しいブランチ"
3332
3260
  }
3333
3261
  };
3334
3262
  var labelsFor = (locale) => locale.startsWith("ja") ? messages.ja : messages.en;
3263
+ // src/branch-button.tsx
3264
+ var _excluded = ["state", "locale"];
3265
+ var menuItems = "[role=\"menuitem\"], [role=\"menuitemradio\"]";
3266
+ /** Git's branch glyph, drawn in the text color. */
3267
+ function BranchIcon() {
3268
+ return /* @__PURE__ */ React.createElement("svg", {
3269
+ className: "twgit-branch-icon",
3270
+ viewBox: "0 0 16 16",
3271
+ width: "16",
3272
+ height: "16",
3273
+ "aria-hidden": "true",
3274
+ focusable: "false"
3275
+ }, /* @__PURE__ */ React.createElement("g", {
3276
+ fill: "none",
3277
+ stroke: "currentColor",
3278
+ strokeWidth: "1.5"
3279
+ }, /* @__PURE__ */ React.createElement("circle", {
3280
+ cx: "4.5",
3281
+ cy: "3.5",
3282
+ r: "1.75"
3283
+ }), /* @__PURE__ */ React.createElement("circle", {
3284
+ cx: "4.5",
3285
+ cy: "12.5",
3286
+ r: "1.75"
3287
+ }), /* @__PURE__ */ React.createElement("circle", {
3288
+ cx: "11.5",
3289
+ cy: "5.5",
3290
+ r: "1.75"
3291
+ }), /* @__PURE__ */ React.createElement("path", { d: "M4.5 5.25v5.5M11.5 7.25c0 2.5-2.5 2.75-6 3.75" })));
3292
+ }
3293
+ /**
3294
+ * The current branch as a menu button for the host's menu bar, with the menu of branches. Before a repository is
3295
+ * chosen it reads "Git" and offers to choose one. A detached HEAD shows its short commit ID. The menu moves with the
3296
+ * arrow keys, Home and End; Escape closes it and returns to the button. Without `writable` it lists the branches only.
3297
+ */
3298
+ function BranchButton(_ref) {
3299
+ var _ref3;
3300
+ let { state, locale = "en" } = _ref, actions = _objectWithoutProperties$1(_ref, _excluded);
3301
+ const labels = labelsFor(locale);
3302
+ const [mode, setMode] = useState("closed");
3303
+ const [initial, setInitial] = useState("checked");
3304
+ const [name, setName] = useState("");
3305
+ const [checkout, setCheckout] = useState(true);
3306
+ const [creating, setCreating] = useState(false);
3307
+ const [createError, setCreateError] = useState("");
3308
+ const root = useRef(null);
3309
+ const trigger = useRef(null);
3310
+ const popup = useRef(null);
3311
+ const mounted = useRef(true);
3312
+ useEffect(() => () => {
3313
+ mounted.current = false;
3314
+ }, []);
3315
+ useLayoutEffect(() => {
3316
+ var _ref2;
3317
+ const element = popup.current;
3318
+ if (!element) return;
3319
+ if (mode === "create") {
3320
+ var _element$querySelecto;
3321
+ (_element$querySelecto = element.querySelector("input")) === null || _element$querySelecto === void 0 || _element$querySelecto.focus();
3322
+ return;
3323
+ }
3324
+ const items = [...element.querySelectorAll(menuItems)];
3325
+ const checked = items.find((item) => item.getAttribute("aria-checked") === "true");
3326
+ (_ref2 = initial === "last" ? items[items.length - 1] : initial === "checked" && checked || items[0]) === null || _ref2 === void 0 || _ref2.focus();
3327
+ }, [mode]);
3328
+ useEffect(() => {
3329
+ if (mode === "closed") return;
3330
+ const node = root.current;
3331
+ const dismiss = (event) => {
3332
+ if (!node.contains(event.target) && !creating) setMode("closed");
3333
+ };
3334
+ node.ownerDocument.addEventListener("pointerdown", dismiss, true);
3335
+ return () => node.ownerDocument.removeEventListener("pointerdown", dismiss, true);
3336
+ }, [mode, creating]);
3337
+ useEffect(() => {
3338
+ var _popup$current;
3339
+ if (!createError || creating) return;
3340
+ const input = (_popup$current = popup.current) === null || _popup$current === void 0 ? void 0 : _popup$current.querySelector("input:not([type])");
3341
+ input === null || input === void 0 || input.focus();
3342
+ input === null || input === void 0 || input.select();
3343
+ }, [createError, creating]);
3344
+ const { branch, head } = state;
3345
+ const unborn = state.repository && !head;
3346
+ const text = !state.repository ? "Git" : branch !== null && branch !== void 0 ? branch : head ? head.slice(0, 7) : labels.unborn;
3347
+ const accessibleName = !state.repository ? labels.title : `${branch ? labels.branch : labels.detached}: ${text}${unborn && branch ? ` (${labels.unborn})` : ""}`;
3348
+ const close = (focus = true) => {
3349
+ var _trigger$current;
3350
+ setMode("closed");
3351
+ if (focus) (_trigger$current = trigger.current) === null || _trigger$current === void 0 || _trigger$current.focus({ preventScroll: true });
3352
+ };
3353
+ const openMenu = (from) => {
3354
+ setInitial(from);
3355
+ setMode("menu");
3356
+ };
3357
+ /** Runs a branch operation and shows its result, or why it did not run, in the panel. */
3358
+ const change = async (operation) => {
3359
+ const result = await operation();
3360
+ if (result.done || result.error) actions.open();
3361
+ };
3362
+ const items = [];
3363
+ if (!state.repository) items.push({
3364
+ id: "choose",
3365
+ label: labels.choose,
3366
+ run: () => {
3367
+ close();
3368
+ actions.choose();
3369
+ }
3370
+ });
3371
+ else {
3372
+ const switching = state.writable && !state.busy;
3373
+ for (const local of state.branches) items.push({
3374
+ id: "branch:" + local,
3375
+ label: local,
3376
+ checked: local === branch,
3377
+ disabled: !state.writable || state.busy && local !== branch,
3378
+ run: () => {
3379
+ close();
3380
+ if (local !== branch && switching) change(() => actions.switchBranch(local));
3381
+ }
3382
+ });
3383
+ if (state.writable) {
3384
+ state.remoteBranches.forEach((remote, index) => items.push({
3385
+ id: "remote:" + remote,
3386
+ label: labels.historyCreateFromRemote.replace("{name}", remote),
3387
+ disabled: state.busy,
3388
+ separatorBefore: index === 0,
3389
+ run: () => {
3390
+ close();
3391
+ if (!state.busy) change(() => actions.createFromRemote(remote));
3392
+ }
3393
+ }));
3394
+ if (head) items.push({
3395
+ id: "create",
3396
+ label: labels.newBranchItem,
3397
+ disabled: state.busy,
3398
+ separatorBefore: true,
3399
+ run: () => {
3400
+ if (state.busy) return;
3401
+ setName("");
3402
+ setCheckout(true);
3403
+ setCreateError("");
3404
+ setMode("create");
3405
+ }
3406
+ });
3407
+ }
3408
+ }
3409
+ items.push({
3410
+ id: "open",
3411
+ label: labels.openPanel,
3412
+ separatorBefore: items.length > 0 && items[items.length - 1].id !== "create",
3413
+ run: () => {
3414
+ close();
3415
+ actions.open();
3416
+ }
3417
+ });
3418
+ const onMenuKey = (event) => {
3419
+ if (event.key === "Escape") {
3420
+ event.preventDefault();
3421
+ event.stopPropagation();
3422
+ if (!creating) close();
3423
+ return;
3424
+ }
3425
+ if (mode !== "menu") return;
3426
+ if (event.key === "Tab") {
3427
+ close(false);
3428
+ return;
3429
+ }
3430
+ const elements = [...event.currentTarget.querySelectorAll(menuItems)];
3431
+ const index = elements.indexOf(event.target);
3432
+ const target = {
3433
+ ArrowDown: index + 1,
3434
+ ArrowUp: index - 1,
3435
+ Home: 0,
3436
+ End: elements.length - 1
3437
+ }[event.key];
3438
+ if (target === void 0 || !elements.length) return;
3439
+ event.preventDefault();
3440
+ elements[(target + elements.length) % elements.length].focus();
3441
+ };
3442
+ const submit = async (event) => {
3443
+ var _result$error;
3444
+ event.preventDefault();
3445
+ const branchName = name.trim();
3446
+ if (!branchName || creating) return;
3447
+ setCreating(true);
3448
+ setCreateError("");
3449
+ let result;
3450
+ try {
3451
+ result = await actions.createBranch(branchName, checkout);
3452
+ } catch (error) {
3453
+ result = {
3454
+ done: false,
3455
+ error: error instanceof Error && error.message ? error.message : labels.failed
3456
+ };
3457
+ }
3458
+ if (!mounted.current) return;
3459
+ setCreating(false);
3460
+ if (result.done) {
3461
+ close();
3462
+ actions.open();
3463
+ } else setCreateError((_result$error = result.error) !== null && _result$error !== void 0 ? _result$error : "");
3464
+ };
3465
+ return /* @__PURE__ */ React.createElement("div", {
3466
+ className: "twgit-branch-entry",
3467
+ ref: root,
3468
+ onBlur: (event) => {
3469
+ const next = event.relatedTarget;
3470
+ if (next && !event.currentTarget.contains(next) && !creating) setMode("closed");
3471
+ }
3472
+ }, /* @__PURE__ */ React.createElement("button", {
3473
+ type: "button",
3474
+ className: "twgit-branch-button",
3475
+ ref: trigger,
3476
+ "aria-label": accessibleName,
3477
+ title: accessibleName,
3478
+ "aria-haspopup": "menu",
3479
+ "aria-expanded": mode === "menu",
3480
+ "data-git-branch": state.repository ? (_ref3 = branch !== null && branch !== void 0 ? branch : head) !== null && _ref3 !== void 0 ? _ref3 : "" : void 0,
3481
+ onClick: () => mode === "closed" ? openMenu("checked") : close(false),
3482
+ onKeyDown: (event) => {
3483
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
3484
+ event.preventDefault();
3485
+ openMenu(event.key === "ArrowUp" ? "last" : "checked");
3486
+ }
3487
+ }, /* @__PURE__ */ React.createElement(BranchIcon, null), /* @__PURE__ */ React.createElement("span", { className: "twgit-branch-name" }, text), unborn && branch && /* @__PURE__ */ React.createElement("small", { className: "twgit-branch-unborn" }, labels.unborn), /* @__PURE__ */ React.createElement("span", {
3488
+ className: "twgit-branch-caret",
3489
+ "aria-hidden": "true"
3490
+ }, "▾")), mode === "menu" && /* @__PURE__ */ React.createElement("div", {
3491
+ ref: popup,
3492
+ className: "twgit-branch-popup",
3493
+ role: "menu",
3494
+ "aria-label": labels.branchMenu,
3495
+ onKeyDown: onMenuKey
3496
+ }, items.map((item) => /* @__PURE__ */ React.createElement("button", {
3497
+ key: item.id,
3498
+ type: "button",
3499
+ tabIndex: -1,
3500
+ "data-action": item.id,
3501
+ role: item.checked === void 0 ? "menuitem" : "menuitemradio",
3502
+ "aria-checked": item.checked,
3503
+ "aria-disabled": item.disabled || void 0,
3504
+ "data-separator-before": item.separatorBefore || void 0,
3505
+ onClick: () => {
3506
+ if (!item.disabled || item.checked) item.run();
3507
+ }
3508
+ }, item.checked !== void 0 && /* @__PURE__ */ React.createElement("span", {
3509
+ className: "twgit-branch-check",
3510
+ "aria-hidden": "true"
3511
+ }, item.checked ? "✓" : ""), item.label))), mode === "create" && /* @__PURE__ */ React.createElement("div", {
3512
+ ref: popup,
3513
+ className: "twgit-branch-popup",
3514
+ role: "dialog",
3515
+ "aria-label": labels.newBranchTitle,
3516
+ onKeyDown: onMenuKey
3517
+ }, /* @__PURE__ */ React.createElement("form", { onSubmit: (event) => void submit(event) }, /* @__PURE__ */ React.createElement("label", null, labels.newBranch, " ", /* @__PURE__ */ React.createElement("input", {
3518
+ value: name,
3519
+ required: true,
3520
+ disabled: creating,
3521
+ "aria-invalid": createError ? true : void 0,
3522
+ onChange: (event) => setName(event.currentTarget.value)
3523
+ })), /* @__PURE__ */ React.createElement("label", null, /* @__PURE__ */ React.createElement("input", {
3524
+ type: "checkbox",
3525
+ checked: checkout,
3526
+ disabled: creating,
3527
+ onChange: (event) => setCheckout(event.currentTarget.checked)
3528
+ }), " ", labels.switchAfterCreate), createError && /* @__PURE__ */ React.createElement("p", {
3529
+ className: "twgit-graph-form-error",
3530
+ role: "alert"
3531
+ }, createError), /* @__PURE__ */ React.createElement("span", { className: "twgit-graph-form-actions" }, /* @__PURE__ */ React.createElement("button", {
3532
+ type: "submit",
3533
+ disabled: creating || state.busy || !name.trim()
3534
+ }, labels.createBranch), /* @__PURE__ */ React.createElement("button", {
3535
+ type: "button",
3536
+ disabled: creating,
3537
+ onClick: () => close()
3538
+ }, labels.cancel)))));
3539
+ }
3540
+ /**
3541
+ * Lays out commits ordered newest first with every child above its parents. Parents missing from `commits`, such as
3542
+ * those on a page not loaded yet or cut off by a shallow clone, keep their lane running to the bottom.
3543
+ */
3544
+ function layoutHistory(commits) {
3545
+ const lanes = [];
3546
+ let nextColor = 0;
3547
+ const free = () => {
3548
+ const index = lanes.indexOf(null);
3549
+ return index === -1 ? lanes.length : index;
3550
+ };
3551
+ const rows = [];
3552
+ for (const commit of commits) {
3553
+ const segments = [];
3554
+ const before = lanes.slice();
3555
+ const waiting = before.flatMap((lane, index) => (lane === null || lane === void 0 ? void 0 : lane.oid) === commit.oid ? [index] : []);
3556
+ const column = waiting.length ? waiting[0] : free();
3557
+ const color = waiting.length ? before[column].color : nextColor++ % 8;
3558
+ for (const [index, lane] of before.entries()) {
3559
+ if (!lane) continue;
3560
+ segments.push({
3561
+ part: "top",
3562
+ from: index,
3563
+ to: lane.oid === commit.oid ? column : index,
3564
+ color: lane.color
3565
+ });
3566
+ }
3567
+ for (const index of waiting) lanes[index] = null;
3568
+ const parents = [...new Set(commit.parents)];
3569
+ lanes[column] = parents.length ? {
3570
+ oid: parents[0],
3571
+ color
3572
+ } : null;
3573
+ const joins = parents.length ? [{
3574
+ index: column,
3575
+ color
3576
+ }] : [];
3577
+ for (const parent of parents.slice(1)) {
3578
+ let index = lanes.findIndex((lane) => (lane === null || lane === void 0 ? void 0 : lane.oid) === parent);
3579
+ if (index === -1) {
3580
+ index = free();
3581
+ lanes[index] = {
3582
+ oid: parent,
3583
+ color: nextColor++ % 8
3584
+ };
3585
+ }
3586
+ joins.push({
3587
+ index,
3588
+ color: lanes[index].color
3589
+ });
3590
+ }
3591
+ while (lanes.length && lanes[lanes.length - 1] === null) lanes.pop();
3592
+ for (const [index, lane] of before.entries()) if (lane && lane.oid !== commit.oid) segments.push({
3593
+ part: "bottom",
3594
+ from: index,
3595
+ to: index,
3596
+ color: lane.color
3597
+ });
3598
+ for (const join of joins) segments.push({
3599
+ part: "bottom",
3600
+ from: column,
3601
+ to: join.index,
3602
+ color: join.color
3603
+ });
3604
+ const width = Math.max(column + 1, ...segments.map((segment) => Math.max(segment.from, segment.to) + 1));
3605
+ rows.push({
3606
+ oid: commit.oid,
3607
+ column,
3608
+ color,
3609
+ segments,
3610
+ width
3611
+ });
3612
+ }
3613
+ return {
3614
+ rows,
3615
+ width: Math.max(0, ...rows.map((row) => row.width))
3616
+ };
3617
+ }
3335
3618
  // src/history-graph-view.tsx
3336
3619
  /** Lane colors that read on light and dark backgrounds. */
3337
3620
  var laneColors = [
@@ -4154,6 +4437,41 @@ function nextFrame(signal) {
4154
4437
  }
4155
4438
  // src/index.tsx
4156
4439
  init_objectSpread2();
4440
+ function createController() {
4441
+ const listeners = /* @__PURE__ */ new Set();
4442
+ const controller = {
4443
+ open() {},
4444
+ async refresh() {},
4445
+ async choose() {},
4446
+ async changeBranch() {
4447
+ return { done: false };
4448
+ },
4449
+ branch: {
4450
+ repository: false,
4451
+ branch: void 0,
4452
+ head: null,
4453
+ branches: [],
4454
+ remoteBranches: [],
4455
+ writable: false,
4456
+ busy: false
4457
+ },
4458
+ publish(state) {
4459
+ controller.branch = state;
4460
+ for (const listener of [...listeners]) listener();
4461
+ },
4462
+ follow(listener) {
4463
+ listeners.add(listener);
4464
+ controller.onFollowers(listeners.size);
4465
+ return () => {
4466
+ listeners.delete(listener);
4467
+ controller.onFollowers(listeners.size);
4468
+ };
4469
+ },
4470
+ followers: () => listeners.size,
4471
+ onFollowers() {}
4472
+ };
4473
+ return controller;
4474
+ }
4157
4475
  var initial = {
4158
4476
  repository: null,
4159
4477
  snapshot: null,
@@ -4167,9 +4485,56 @@ var initial = {
4167
4485
  busy: false
4168
4486
  };
4169
4487
  var controllers = /* @__PURE__ */ new WeakMap();
4488
+ var registrations = /* @__PURE__ */ new Set();
4170
4489
  var watchInterval = 2e3;
4490
+ /**
4491
+ * Calls `check` every `watchInterval`, when the window regains focus and shortly after a File System Observer reports
4492
+ * a change under `root`, one call at a time and never while the page is hidden. Returns the function that stops it.
4493
+ */
4494
+ function watchRepository(document, root, check) {
4495
+ const view = document.defaultView;
4496
+ let stopped = false;
4497
+ let checking = false;
4498
+ const run = async () => {
4499
+ if (stopped || checking || document.visibilityState === "hidden") return;
4500
+ checking = true;
4501
+ try {
4502
+ await check(() => stopped);
4503
+ } catch (_unused) {} finally {
4504
+ checking = false;
4505
+ }
4506
+ };
4507
+ let soon;
4508
+ const schedule = () => {
4509
+ view.clearTimeout(soon);
4510
+ soon = view.setTimeout(() => void run(), 300);
4511
+ };
4512
+ const interval = view.setInterval(() => void run(), watchInterval);
4513
+ const onFocus = () => void run();
4514
+ view.addEventListener("focus", onFocus);
4515
+ document.addEventListener("visibilitychange", onFocus);
4516
+ let observer = null;
4517
+ try {
4518
+ if (view.FileSystemObserver) {
4519
+ observer = new view.FileSystemObserver(schedule);
4520
+ observer.observe(root, { recursive: true }).catch(() => {});
4521
+ }
4522
+ } catch (_unused2) {
4523
+ observer = null;
4524
+ }
4525
+ run();
4526
+ return () => {
4527
+ stopped = true;
4528
+ view.clearInterval(interval);
4529
+ view.clearTimeout(soon);
4530
+ view.removeEventListener("focus", onFocus);
4531
+ document.removeEventListener("visibilitychange", onFocus);
4532
+ observer === null || observer === void 0 || observer.disconnect();
4533
+ };
4534
+ }
4535
+ var sameRefs = (a, b) => !!a && a.branch === b.branch && a.head === b.head && a.branches.join("\n") === b.branches.join("\n") && a.remoteBranches.join("\n") === b.remoteBranches.join("\n");
4171
4536
  function GitPanel({ vm, document, host, controller }) {
4172
- var _state$snapshot, _state$repository, _state$snapshot$head$, _state$snapshot$head;
4537
+ var _refs$head, _refs$branches, _refs$remoteBranches, _state$snapshot, _state$repository, _state$snapshot$head$, _state$snapshot$head;
4173
4538
  const [visible, setVisible] = useState(false);
4174
4539
  const [slot, setSlot] = useState(null);
4175
4540
  const shown = visible || slot !== null;
@@ -4342,57 +4707,42 @@ function GitPanel({ vm, document, host, controller }) {
4342
4707
  controller.refresh();
4343
4708
  }
4344
4709
  }, [shown]);
4710
+ const [refs, setRefs] = useState(null);
4711
+ useEffect(() => {
4712
+ if (!state.repository) setRefs(null);
4713
+ else if (state.snapshot) setRefs(refsOf(state.snapshot));
4714
+ }, [state.repository, state.snapshot]);
4715
+ const [followers, setFollowers] = useState(controller.followers());
4716
+ useEffect(() => {
4717
+ controller.onFollowers = setFollowers;
4718
+ setFollowers(controller.followers());
4719
+ return () => {
4720
+ controller.onFollowers = () => {};
4721
+ };
4722
+ }, []);
4723
+ const following = followers > 0;
4345
4724
  useEffect(() => {
4346
4725
  const repository = state.repository;
4347
- if (!shown || !repository) return;
4348
- const view = document.defaultView;
4349
- let stopped = false;
4350
- let checking = false;
4351
- const check = async () => {
4726
+ if (!repository || !shown && !following) return;
4727
+ return watchRepository(document, repository.root, async (stopped) => {
4352
4728
  const { snapshot } = current.current;
4353
- const hidden = !dialog.current || !dialog.current.getClientRects().length;
4354
- if (stopped || checking || running.current || document.visibilityState === "hidden" || hidden || !snapshot || current.current.repository !== repository) return;
4355
- checking = true;
4356
- try {
4357
- const probe = await repository.probe(snapshot.scope);
4358
- if (stopped || probe === "locked" || probe === snapshot.probe || probe === handled.current || current.current.snapshot !== snapshot) return;
4359
- handled.current = probe;
4360
- const { selected, commit } = current.current;
4361
- if (selected || commit || reviewing.current) setState((previous) => previous.snapshot === snapshot ? _objectSpread2$1(_objectSpread2$1({}, previous), {}, { notice: labelsFor(locale).outdated }) : previous);
4362
- else await controller.refresh(true);
4363
- } catch (_unused) {} finally {
4364
- checking = false;
4365
- }
4366
- };
4367
- let soon;
4368
- const schedule = () => {
4369
- view.clearTimeout(soon);
4370
- soon = view.setTimeout(() => void check(), 300);
4371
- };
4372
- const interval = view.setInterval(() => void check(), watchInterval);
4373
- const onFocus = () => void check();
4374
- view.addEventListener("focus", onFocus);
4375
- document.addEventListener("visibilitychange", onFocus);
4376
- let observer = null;
4377
- try {
4378
- if (view.FileSystemObserver) {
4379
- observer = new view.FileSystemObserver(schedule);
4380
- observer.observe(repository.root, { recursive: true }).catch(() => {});
4729
+ if (running.current || current.current.repository !== repository) return;
4730
+ if (!(shown && !!dialog.current && dialog.current.getClientRects().length > 0) || !snapshot) {
4731
+ if (!following) return;
4732
+ const next = await repository.refs();
4733
+ if (!stopped() && current.current.repository === repository) setRefs((previous) => sameRefs(previous, next) ? previous : next);
4734
+ return;
4381
4735
  }
4382
- } catch (_unused2) {
4383
- observer = null;
4384
- }
4385
- check();
4386
- return () => {
4387
- stopped = true;
4388
- view.clearInterval(interval);
4389
- view.clearTimeout(soon);
4390
- view.removeEventListener("focus", onFocus);
4391
- document.removeEventListener("visibilitychange", onFocus);
4392
- observer === null || observer === void 0 || observer.disconnect();
4393
- };
4736
+ const probe = await repository.probe(snapshot.scope);
4737
+ if (stopped() || probe === "locked" || probe === snapshot.probe || probe === handled.current || current.current.snapshot !== snapshot) return;
4738
+ handled.current = probe;
4739
+ const { selected, commit } = current.current;
4740
+ if (selected || commit || reviewing.current) setState((previous) => previous.snapshot === snapshot ? _objectSpread2$1(_objectSpread2$1({}, previous), {}, { notice: labelsFor(locale).outdated }) : previous);
4741
+ else await controller.refresh(true);
4742
+ });
4394
4743
  }, [
4395
4744
  shown,
4745
+ following,
4396
4746
  state.repository,
4397
4747
  locale
4398
4748
  ]);
@@ -4407,7 +4757,7 @@ function GitPanel({ vm, document, host, controller }) {
4407
4757
  }, [visible, slot]);
4408
4758
  const chooseRoot = async () => {
4409
4759
  const view = document.defaultView;
4410
- await run(async () => {
4760
+ return run(async () => {
4411
4761
  var _host$connectionSetti;
4412
4762
  let root;
4413
4763
  try {
@@ -4673,6 +5023,24 @@ function GitPanel({ vm, document, host, controller }) {
4673
5023
  state.snapshot,
4674
5024
  state.selected
4675
5025
  ]);
5026
+ controller.changeBranch = changeBranch;
5027
+ controller.choose = async (language) => {
5028
+ setLocale(language);
5029
+ const before = current.current.repository;
5030
+ const result = await chooseRoot();
5031
+ if (!result.done && result.error || current.current.repository !== before) controller.open(language);
5032
+ };
5033
+ const branchState = {
5034
+ repository: !!state.repository,
5035
+ branch: refs === null || refs === void 0 ? void 0 : refs.branch,
5036
+ head: (_refs$head = refs === null || refs === void 0 ? void 0 : refs.head) !== null && _refs$head !== void 0 ? _refs$head : null,
5037
+ branches: (_refs$branches = refs === null || refs === void 0 ? void 0 : refs.branches) !== null && _refs$branches !== void 0 ? _refs$branches : [],
5038
+ remoteBranches: (_refs$remoteBranches = refs === null || refs === void 0 ? void 0 : refs.remoteBranches) !== null && _refs$remoteBranches !== void 0 ? _refs$remoteBranches : [],
5039
+ writable: !!host.localWrite && !!refs,
5040
+ busy: state.busy
5041
+ };
5042
+ const published = JSON.stringify(branchState);
5043
+ useEffect(() => controller.publish(branchState), [published]);
4676
5044
  if (!shown) return null;
4677
5045
  const groups = state.commit ? ["commit"] : [
4678
5046
  "staged",
@@ -4939,15 +5307,21 @@ function upstreamText({ upstream }, labels) {
4939
5307
  default: return labels.upstreamUnknown;
4940
5308
  }
4941
5309
  }
5310
+ var refsOf = ({ branch, head, branches, remoteBranches }) => ({
5311
+ branch,
5312
+ head,
5313
+ branches,
5314
+ remoteBranches
5315
+ });
5316
+ var announce = () => {
5317
+ for (const listener of [...registrations]) listener();
5318
+ };
4942
5319
  function mountGitPanel({ vm, document, host }) {
4943
5320
  var _document$defaultView3;
4944
5321
  if (!((_document$defaultView3 = document.defaultView) === null || _document$defaultView3 === void 0 ? void 0 : _document$defaultView3.showDirectoryPicker)) return null;
4945
5322
  const root = document.createElement("div");
4946
5323
  document.body.append(root);
4947
- const controller = {
4948
- open() {},
4949
- async refresh() {}
4950
- };
5324
+ const controller = createController();
4951
5325
  controllers.set(vm, controller);
4952
5326
  render(/* @__PURE__ */ React.createElement(GitPanel, {
4953
5327
  vm,
@@ -4955,26 +5329,70 @@ function mountGitPanel({ vm, document, host }) {
4955
5329
  host,
4956
5330
  controller
4957
5331
  }), root);
5332
+ announce();
4958
5333
  return {
4959
5334
  refresh: () => controller.refresh(),
5335
+ open: (locale) => controller.open(locale),
4960
5336
  dispose() {
4961
5337
  var _host$connectionSetti3;
4962
5338
  if (controllers.get(vm) === controller) controllers.delete(vm);
4963
5339
  (_host$connectionSetti3 = host.connectionSettings) === null || _host$connectionSetti3 === void 0 || _host$connectionSetti3.disconnect().catch(() => {});
4964
5340
  unmountComponentAtNode(root);
4965
5341
  root.remove();
5342
+ announce();
4966
5343
  }
4967
5344
  };
4968
5345
  }
4969
- function renderGitMenu(createElement, { vm, locale = "en", MenuItem, MenuSection, onRequestClose }) {
4970
- const controller = controllers.get(vm);
4971
- if (!controller) return null;
4972
- return createElement(MenuSection, null, createElement(MenuItem, {
4973
- command: "local-git",
4974
- onClick() {
4975
- onRequestClose();
4976
- controller.open(locale);
4977
- }
4978
- }, labelsFor(locale).menu));
5346
+ /** The branch button of the panel mounted for `vm`, or nothing while no panel is mounted for it. */
5347
+ function BranchEntry({ vm, locale }) {
5348
+ const [controller, setController] = useState(() => controllers.get(vm));
5349
+ useEffect(() => {
5350
+ const update = () => setController(controllers.get(vm));
5351
+ registrations.add(update);
5352
+ update();
5353
+ return () => {
5354
+ registrations.delete(update);
5355
+ };
5356
+ }, [vm]);
5357
+ const [state, setState] = useState(controller === null || controller === void 0 ? void 0 : controller.branch);
5358
+ useEffect(() => {
5359
+ if (!controller) return;
5360
+ const update = () => setState(controller.branch);
5361
+ update();
5362
+ return controller.follow(update);
5363
+ }, [controller]);
5364
+ if (!controller || !state) return null;
5365
+ return /* @__PURE__ */ React.createElement(BranchButton, {
5366
+ state,
5367
+ locale,
5368
+ open: () => controller.open(locale),
5369
+ choose: () => void controller.choose(locale),
5370
+ switchBranch: (name) => controller.changeBranch({
5371
+ kind: "switchBranch",
5372
+ name
5373
+ }),
5374
+ createBranch: (name, checkout) => controller.changeBranch({
5375
+ kind: "createBranch",
5376
+ name,
5377
+ checkout
5378
+ }),
5379
+ createFromRemote: (name) => controller.changeBranch({
5380
+ kind: "createBranch",
5381
+ name,
5382
+ fromRemote: true,
5383
+ checkout: true
5384
+ })
5385
+ });
5386
+ }
5387
+ /**
5388
+ * Renders the button for the host's menu bar that shows the current branch of the panel mounted for `vm` and opens the
5389
+ * menu of branches, the panel and the repository choice. It renders nothing while no panel is mounted for `vm`, such as
5390
+ * when the host does not offer local Git, and shares the panel's repository, operations and checks.
5391
+ */
5392
+ function renderBranchButton(createElement, { vm, locale = "en" }) {
5393
+ return createElement(BranchEntry, {
5394
+ vm,
5395
+ locale
5396
+ });
4979
5397
  }
4980
- export { GitPanelError, HistoryGraph, createEditorBlockLocator, createReadOnlyFs, createScratchBlocksDiffLocator, createScratchBlocksDiffRenderer, laneColors, layoutHistory, mountGitPanel, openProjectComparison, openProjectSourceTabs, openRepository, pullRequestBranch, pullRequestRef, renderGitMenu };
5398
+ export { BranchButton, GitPanelError, HistoryGraph, createEditorBlockLocator, createReadOnlyFs, createScratchBlocksDiffLocator, createScratchBlocksDiffRenderer, laneColors, layoutHistory, mountGitPanel, openProjectComparison, openProjectSourceTabs, openRepository, pullRequestBranch, pullRequestRef, renderBranchButton };