From 58d0c65736dbfec5865290ec69d1dd6826ff1f7a Mon Sep 17 00:00:00 2001 From: Anton Arnaudov Date: Sat, 17 Oct 2026 22:45:00 +0300 Subject: [PATCH] fix(graph): a ticked SHA column is on screen, or the Columns menu says why (#124) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Columns menu showed SHA ticked over a graph with no SHA column. The bottom panel's compact layout had no SHA track at all, and the editor tab dropped Date and SHA below an 860px host whether or not they fitted; the menu counted only unticked columns, so "1 hidden" meant Date. - Compact gets a real, pinned SHA track that follows the toggle. - Date and SHA (and Author in compact) are fitted to the width: the message gives way to its floor and Branch/Tag keeps a readable width first; SHA keeps its place longest. The narrow table tier keeps SHA too. - A ticked column with no room is marked "no room" in the menu, and the footer names it ("SHA hidden — panel too narrow"). - The refs legibility floor carries one chip gap of slack, so the current branch chip is not cut to "m…" now that the tail stays below 860px. Co-Authored-By: Claude Opus 5.5 --- apps/desktop/l10n/bundle.l10n.json | 2 + apps/extension/l10n/bundle.l10n.cs.json | 2 + apps/extension/l10n/bundle.l10n.de.json | 2 + apps/extension/l10n/bundle.l10n.es.json | 2 + apps/extension/l10n/bundle.l10n.fr.json | 2 + apps/extension/l10n/bundle.l10n.it.json | 2 + apps/extension/l10n/bundle.l10n.ja.json | 2 + apps/extension/l10n/bundle.l10n.json | 2 + apps/extension/l10n/bundle.l10n.ko.json | 2 + apps/extension/l10n/bundle.l10n.pl.json | 2 + apps/extension/l10n/bundle.l10n.pt-br.json | 2 + apps/extension/l10n/bundle.l10n.ru.json | 2 + apps/extension/l10n/bundle.l10n.tr.json | 2 + apps/extension/l10n/bundle.l10n.zh-cn.json | 2 + apps/extension/l10n/bundle.l10n.zh-tw.json | 2 + apps/merge-studio/l10n/bundle.l10n.cs.json | 2 + apps/merge-studio/l10n/bundle.l10n.de.json | 2 + apps/merge-studio/l10n/bundle.l10n.es.json | 2 + apps/merge-studio/l10n/bundle.l10n.fr.json | 2 + apps/merge-studio/l10n/bundle.l10n.it.json | 2 + apps/merge-studio/l10n/bundle.l10n.ja.json | 2 + apps/merge-studio/l10n/bundle.l10n.json | 2 + apps/merge-studio/l10n/bundle.l10n.ko.json | 2 + apps/merge-studio/l10n/bundle.l10n.pl.json | 2 + apps/merge-studio/l10n/bundle.l10n.pt-br.json | 2 + apps/merge-studio/l10n/bundle.l10n.ru.json | 2 + apps/merge-studio/l10n/bundle.l10n.tr.json | 2 + apps/merge-studio/l10n/bundle.l10n.zh-cn.json | 2 + apps/merge-studio/l10n/bundle.l10n.zh-tw.json | 2 + l10n/cs.json | 2 + l10n/de.json | 2 + l10n/es.json | 2 + l10n/fr.json | 2 + l10n/it.json | 2 + l10n/ja.json | 2 + l10n/ko.json | 2 + l10n/pl.json | 2 + l10n/pt-br.json | 2 + l10n/ru.json | 2 + l10n/tr.json | 2 + l10n/zh-cn.json | 2 + l10n/zh-tw.json | 2 + packages/webview-ui/src/graph/commit-graph.ts | 363 ++++++++++++++---- packages/webview-ui/src/graph/refLayout.ts | 10 +- .../webview-ui/test/graphColumnFit.test.ts | 235 ++++++++++++ .../webview-ui/test/refFilter.cov.test.ts | 7 +- 46 files changed, 625 insertions(+), 74 deletions(-) create mode 100644 packages/webview-ui/test/graphColumnFit.test.ts diff --git a/apps/desktop/l10n/bundle.l10n.json b/apps/desktop/l10n/bundle.l10n.json index dcbd6dec..1cc5fc1d 100644 --- a/apps/desktop/l10n/bundle.l10n.json +++ b/apps/desktop/l10n/bundle.l10n.json @@ -4088,6 +4088,7 @@ "no matches": "no matches", "no model set": "no model set", "no one": "no one", + "no room": "no room", "no sha": "no sha", "not a pull request number: {0}": "not a pull request number: {0}", "not a worktree": "not a worktree", @@ -4458,6 +4459,7 @@ "{0} has {1} that pull request #{2} doesn't — made here, or from before ": "{0} has {1} that pull request #{2} doesn't — made here, or from before ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.", "{0} hidden · drag header edges to resize": "{0} hidden · drag header edges to resize", + "{0} hidden — panel too narrow": "{0} hidden — panel too narrow", "{0} in Theirs": "{0} in Theirs", "{0} in Yours": "{0} in Yours", "{0} in the Result, marked so you can check {1}.": "{0} in the Result, marked so you can check {1}.", diff --git a/apps/extension/l10n/bundle.l10n.cs.json b/apps/extension/l10n/bundle.l10n.cs.json index efc1b3cb..84df7d41 100644 --- a/apps/extension/l10n/bundle.l10n.cs.json +++ b/apps/extension/l10n/bundle.l10n.cs.json @@ -2784,6 +2784,7 @@ "no email recorded": "žádný zaznamenaný e-mail", "no one": "nikdo", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "žádný vzdálený repozitář tohoto repozitáře ({0}) není adresa GitHub, GitLab nebo Bitbucket.", + "no room": "není místo", "not a pull request number: {0}": "nejedná se o číslo pull requestu: {0}", "not a worktree": "není pracovní strom", "not published": "nezveřejněno", @@ -3080,6 +3081,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0} má {1}, které {2} nemá, a {3} má {4}, které {5} nemá — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} mají změny, které nejsou připravené. Nejdřív je připravte nebo stashněte — git nebude pokračovat v rebase s nepřipravenými změnami.", "{0} hidden · drag header edges to resize": "{0} skryto · velikost změníte tažením okrajů záhlaví", + "{0} hidden — panel too narrow": "{0} skryto — panel je příliš úzký", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} narazilo na konflikty. Vyřešte je, pak pokračujte nebo to zrušte.", "{0} in Theirs": "{0} v Jejich", "{0} in Yours": "{0} ve Vašem", diff --git a/apps/extension/l10n/bundle.l10n.de.json b/apps/extension/l10n/bundle.l10n.de.json index d950794d..01fcc0a0 100644 --- a/apps/extension/l10n/bundle.l10n.de.json +++ b/apps/extension/l10n/bundle.l10n.de.json @@ -2784,6 +2784,7 @@ "no email recorded": "keine E-Mail hinterlegt", "no one": "niemand", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "kein Remote dieses Repositorys ({0}) ist eine GitHub-, GitLab- oder Bitbucket-Adresse.", + "no room": "kein Platz", "not a pull request number: {0}": "keine Pull-Request-Nummer: {0}", "not a worktree": "kein Worktree", "not published": "nicht veröffentlicht", @@ -3080,6 +3081,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0} hat {1}, was {2} nicht hat, und {3} hat {4}, was {5} nicht hat — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} haben nicht bereitgestellte Änderungen. Zuerst bereitstellen oder stashen — Git setzt einen Rebase nicht mit nicht bereitgestellten Änderungen fort.", "{0} hidden · drag header edges to resize": "{0} ausgeblendet · Kopfzeilenränder ziehen, um die Größe anzupassen", + "{0} hidden — panel too narrow": "{0} ausgeblendet — Bereich zu schmal", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} ist auf Konflikte gestoßen. Lösen, dann fortsetzen oder abbrechen.", "{0} in Theirs": "{0} in deren Version", "{0} in Yours": "{0} in Ihrer Version", diff --git a/apps/extension/l10n/bundle.l10n.es.json b/apps/extension/l10n/bundle.l10n.es.json index c7b86c94..51c80541 100644 --- a/apps/extension/l10n/bundle.l10n.es.json +++ b/apps/extension/l10n/bundle.l10n.es.json @@ -2784,6 +2784,7 @@ "no email recorded": "sin correo electrónico registrado", "no one": "nadie", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "ningún remoto de este repositorio ({0}) es una dirección de GitHub, GitLab o Bitbucket.", + "no room": "sin espacio", "not a pull request number: {0}": "no es un número de pull request: {0}", "not a worktree": "no es un worktree", "not published": "no publicado", @@ -3080,6 +3081,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0} tiene {1} que {2} no tiene, y {3} tiene {4} que {5} no tiene — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} tienen cambios que no están preparados. Prepáralos o guárdalos en el stash primero — git no continuará un rebase con cambios sin preparar.", "{0} hidden · drag header edges to resize": "{0} oculto · arrastra los bordes del encabezado para cambiar el tamaño", + "{0} hidden — panel too narrow": "{0} oculto — panel demasiado estrecho", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} tuvo conflictos. Resuélvelos, y luego continúa o aborta.", "{0} in Theirs": "{0} en Suyos", "{0} in Yours": "{0} en Tuyos", diff --git a/apps/extension/l10n/bundle.l10n.fr.json b/apps/extension/l10n/bundle.l10n.fr.json index 51045aa5..b374e71a 100644 --- a/apps/extension/l10n/bundle.l10n.fr.json +++ b/apps/extension/l10n/bundle.l10n.fr.json @@ -2784,6 +2784,7 @@ "no email recorded": "aucun e-mail enregistré", "no one": "personne", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "aucun dépôt distant de ce dépôt ({0}) n'est une adresse GitHub, GitLab ou Bitbucket.", + "no room": "pas de place", "not a pull request number: {0}": "pas un numéro de pull request : {0}", "not a worktree": "pas une arborescence de travail", "not published": "non publiée", @@ -3080,6 +3081,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0} a {1} que {2} n'a pas, et {3} a {4} que {5} n'a pas — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} ont des modifications non indexées. Indexez-les ou remisez-les d'abord — git ne continuera pas un rebase avec des modifications non indexées.", "{0} hidden · drag header edges to resize": "{0} masqué(e)s · faites glisser les bords de l'en-tête pour redimensionner", + "{0} hidden — panel too narrow": "{0} masqué(e)s — panneau trop étroit", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} a rencontré des conflits. Résolvez-les, puis continuez ou abandonnez.", "{0} in Theirs": "{0} dans la leur", "{0} in Yours": "{0} dans la vôtre", diff --git a/apps/extension/l10n/bundle.l10n.it.json b/apps/extension/l10n/bundle.l10n.it.json index 3cef1fe7..3383a770 100644 --- a/apps/extension/l10n/bundle.l10n.it.json +++ b/apps/extension/l10n/bundle.l10n.it.json @@ -2784,6 +2784,7 @@ "no email recorded": "nessuna email registrata", "no one": "nessuno", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "nessun remoto di questo repository ({0}) è un indirizzo GitHub, GitLab o Bitbucket.", + "no room": "spazio insufficiente", "not a pull request number: {0}": "non è un numero di pull request: {0}", "not a worktree": "non è un worktree", "not published": "non pubblicato", @@ -3080,6 +3081,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0} ha {1} che {2} non ha, e {3} ha {4} che {5} non ha — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} hanno modifiche non in stage. Mettile prima in stage o in stash — git non continuerà un rebase con modifiche non in stage.", "{0} hidden · drag header edges to resize": "{0} nascosti · trascina i bordi dell'intestazione per ridimensionare", + "{0} hidden — panel too narrow": "{0} nascosti — pannello troppo stretto", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} ha incontrato conflitti. Risolvili, poi continua o interrompi.", "{0} in Theirs": "{0} in Loro", "{0} in Yours": "{0} in Tuo", diff --git a/apps/extension/l10n/bundle.l10n.ja.json b/apps/extension/l10n/bundle.l10n.ja.json index 8ee9d280..502ab4d7 100644 --- a/apps/extension/l10n/bundle.l10n.ja.json +++ b/apps/extension/l10n/bundle.l10n.ja.json @@ -2784,6 +2784,7 @@ "no email recorded": "メールアドレスが記録されていません", "no one": "だれもいません", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "このリポジトリ ({0}) のリモートに、GitHub、GitLab、Bitbucketのアドレスはありません。", + "no room": "スペース不足", "not a pull request number: {0}": "プルリクエスト番号ではありません: {0}", "not a worktree": "ワークツリーではありません", "not published": "公開されていません", @@ -3080,6 +3081,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0} には {2} にはない {1} があり、{3} には {5} にはない {4} があります —", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} にはステージされていない変更があります。先にステージまたはスタッシュしてください — gitは未ステージの変更があるとリベースを続行できません。", "{0} hidden · drag header edges to resize": "{0} 個を非表示 · ヘッダーの端をドラッグしてサイズ変更", + "{0} hidden — panel too narrow": "{0} を非表示 — パネルが狭すぎます", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} でコンフリクトが発生しました。解決してから続行するか、中止してください。", "{0} in Theirs": "相手の {0}", "{0} in Yours": "自分の {0}", diff --git a/apps/extension/l10n/bundle.l10n.json b/apps/extension/l10n/bundle.l10n.json index 6fb3cf91..7fb1a920 100644 --- a/apps/extension/l10n/bundle.l10n.json +++ b/apps/extension/l10n/bundle.l10n.json @@ -2784,6 +2784,7 @@ "no email recorded": "no email recorded", "no one": "no one", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.", + "no room": "no room", "not a pull request number: {0}": "not a pull request number: {0}", "not a worktree": "not a worktree", "not published": "not published", @@ -3080,6 +3081,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.", "{0} hidden · drag header edges to resize": "{0} hidden · drag header edges to resize", + "{0} hidden — panel too narrow": "{0} hidden — panel too narrow", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} hit conflicts. Resolve them, then continue or abort.", "{0} in Theirs": "{0} in Theirs", "{0} in Yours": "{0} in Yours", diff --git a/apps/extension/l10n/bundle.l10n.ko.json b/apps/extension/l10n/bundle.l10n.ko.json index 59cc5350..9549c540 100644 --- a/apps/extension/l10n/bundle.l10n.ko.json +++ b/apps/extension/l10n/bundle.l10n.ko.json @@ -2784,6 +2784,7 @@ "no email recorded": "기록된 이메일 없음", "no one": "없음", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "이 리포지토리의 원격({0}) 중 GitHub, GitLab 또는 Bitbucket 주소인 것이 없습니다.", + "no room": "공간 부족", "not a pull request number: {0}": "풀 리퀘스트 번호가 아님: {0}", "not a worktree": "작업 폴더가 아님", "not published": "게시되지 않음", @@ -3080,6 +3081,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0}에는 {2}에 없는 {1}이 있고, {3}에는 {5}에 없는 {4}이 있습니다 — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0}에 스테이징되지 않은 변경 내용이 있습니다. 먼저 스테이지에 추가하거나 스태시하세요 — git은 스테이징되지 않은 변경 내용이 있으면 리베이스를 계속하지 않습니다.", "{0} hidden · drag header edges to resize": "{0} 숨겨짐 · 헤더 경계를 드래그하여 크기 조절", + "{0} hidden — panel too narrow": "{0} 숨겨짐 — 패널이 너무 좁음", "{0} hit conflicts. Resolve them, then continue or abort.": "{0}에서 충돌이 발생했습니다. 해결 후 계속하거나 중단하세요.", "{0} in Theirs": "상대 변경 내용의 {0}", "{0} in Yours": "내 변경 내용의 {0}", diff --git a/apps/extension/l10n/bundle.l10n.pl.json b/apps/extension/l10n/bundle.l10n.pl.json index 890844de..3a5f6b92 100644 --- a/apps/extension/l10n/bundle.l10n.pl.json +++ b/apps/extension/l10n/bundle.l10n.pl.json @@ -2784,6 +2784,7 @@ "no email recorded": "nie zapisano adresu e-mail", "no one": "nikt", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "żadne zdalne repozytorium tego repozytorium ({0}) nie jest adresem GitHub, GitLab ani Bitbucket.", + "no room": "brak miejsca", "not a pull request number: {0}": "nie jest numerem pull requesta: {0}", "not a worktree": "to nie worktree", "not published": "nieopublikowany", @@ -3080,6 +3081,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0} ma {1}, których {2} nie ma, a {3} ma {4}, których {5} nie ma — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} mają zmiany, które nie są przygotowane. Najpierw je przygotuj albo zapisz w schowku — git nie kontynuuje rebase z nieprzygotowanymi zmianami.", "{0} hidden · drag header edges to resize": "{0} ukryte · przeciągnij krawędzie nagłówka, aby zmienić rozmiar", + "{0} hidden — panel too narrow": "{0} ukryte — panel jest za wąski", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} natrafiło na konflikty. Rozwiąż je, a potem kontynuuj albo przerwij.", "{0} in Theirs": "{0} w Ich", "{0} in Yours": "{0} w Twoich", diff --git a/apps/extension/l10n/bundle.l10n.pt-br.json b/apps/extension/l10n/bundle.l10n.pt-br.json index 69d83221..fdd99b78 100644 --- a/apps/extension/l10n/bundle.l10n.pt-br.json +++ b/apps/extension/l10n/bundle.l10n.pt-br.json @@ -2784,6 +2784,7 @@ "no email recorded": "nenhum e-mail registrado", "no one": "ninguém", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "nenhum remoto deste repositório ({0}) é um endereço do GitHub, GitLab ou Bitbucket.", + "no room": "sem espaço", "not a pull request number: {0}": "não é um número de pull request: {0}", "not a worktree": "não é um worktree", "not published": "não publicado", @@ -3080,6 +3081,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0} tem {1} que {2} não tem, e {3} tem {4} que {5} não tem — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} têm alterações que não estão em stage. Coloque-as em stage ou guarde-as no stash primeiro — o git não continua um rebase com alterações fora do stage.", "{0} hidden · drag header edges to resize": "{0} ocultos · arraste as bordas do cabeçalho para redimensionar", + "{0} hidden — panel too narrow": "{0} ocultos — painel estreito demais", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} encontrou conflitos. Resolva-os, depois continue ou aborte.", "{0} in Theirs": "{0} em Deles", "{0} in Yours": "{0} em Seu", diff --git a/apps/extension/l10n/bundle.l10n.ru.json b/apps/extension/l10n/bundle.l10n.ru.json index 3090c18c..6e683865 100644 --- a/apps/extension/l10n/bundle.l10n.ru.json +++ b/apps/extension/l10n/bundle.l10n.ru.json @@ -2784,6 +2784,7 @@ "no email recorded": "email не записан", "no one": "никто", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "ни один удалённый репозиторий этого репозитория ({0}) не является адресом GitHub, GitLab или Bitbucket.", + "no room": "нет места", "not a pull request number: {0}": "не номер pull request: {0}", "not a worktree": "не рабочий каталог", "not published": "не опубликовано", @@ -3080,6 +3081,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "В {0} есть {1}, которых нет в {2}, а в {3} есть {4}, которых нет в {5} — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "В {0} есть изменения, не добавленные в индекс. Сначала добавьте их в индекс или спрячьте — git не продолжит перебазирование с неиндексированными изменениями.", "{0} hidden · drag header edges to resize": "{0} скрыто · перетащите края заголовка для изменения размера", + "{0} hidden — panel too narrow": "{0} скрыто — панель слишком узкая", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} привело к конфликтам. Разрешите их, затем продолжите или прервите.", "{0} in Theirs": "{0} в «Их»", "{0} in Yours": "{0} в «Ваши»", diff --git a/apps/extension/l10n/bundle.l10n.tr.json b/apps/extension/l10n/bundle.l10n.tr.json index b786efa7..e729e5fc 100644 --- a/apps/extension/l10n/bundle.l10n.tr.json +++ b/apps/extension/l10n/bundle.l10n.tr.json @@ -2784,6 +2784,7 @@ "no email recorded": "e-posta kaydedilmemiş", "no one": "hiç kimse", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "bu deponun hiçbir uzak konumu ({0}) bir GitHub, GitLab veya Bitbucket adresi değil.", + "no room": "yer yok", "not a pull request number: {0}": "bir çekme isteği numarası değil: {0}", "not a worktree": "bir çalışma alanı değil", "not published": "yayınlanmadı", @@ -3080,6 +3081,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0}'da {2}'de olmayan {1} var ve {3}'de {5}'de olmayan {4} var — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0}'da hazırlanmamış değişiklikler var. Önce hazırlayın veya zulalayın — git hazırlanmamış değişikliklerle bir yeniden temellendirmeyi sürdürmez.", "{0} hidden · drag header edges to resize": "{0} gizli · boyutlandırmak için başlık kenarlarını sürükleyin", + "{0} hidden — panel too narrow": "{0} gizli — panel çok dar", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} çakışmalara çarptı. Çözün, sonra sürdürün veya iptal edin.", "{0} in Theirs": "Onlarınki'nde {0}", "{0} in Yours": "Sizinki'nde {0}", diff --git a/apps/extension/l10n/bundle.l10n.zh-cn.json b/apps/extension/l10n/bundle.l10n.zh-cn.json index 512de70f..6dfe19dd 100644 --- a/apps/extension/l10n/bundle.l10n.zh-cn.json +++ b/apps/extension/l10n/bundle.l10n.zh-cn.json @@ -2784,6 +2784,7 @@ "no email recorded": "未记录邮箱", "no one": "无人", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "此仓库的所有远程仓库({0})都不是 GitHub、GitLab 或 Bitbucket 地址。", + "no room": "空间不足", "not a pull request number: {0}": "不是拉取请求编号:{0}", "not a worktree": "不是工作树", "not published": "未发布", @@ -3080,6 +3081,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0} 有 {1} 是 {2} 没有的,而 {3} 有 {4} 是 {5} 没有的 — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} 有未暂存的更改。请先暂存或贮藏它们 — git 不会带着未暂存的更改继续变基。", "{0} hidden · drag header edges to resize": "已隐藏 {0} · 拖动表头边缘可调整大小", + "{0} hidden — panel too narrow": "已隐藏 {0} — 面板太窄", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} 遇到冲突。请解决它们,然后继续或中止。", "{0} in Theirs": "{0} 在“他们的”中", "{0} in Yours": "{0} 在“你的”中", diff --git a/apps/extension/l10n/bundle.l10n.zh-tw.json b/apps/extension/l10n/bundle.l10n.zh-tw.json index 4e49fed4..5d51e3b1 100644 --- a/apps/extension/l10n/bundle.l10n.zh-tw.json +++ b/apps/extension/l10n/bundle.l10n.zh-tw.json @@ -2784,6 +2784,7 @@ "no email recorded": "未記錄郵箱", "no one": "無人", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "此儲存庫的所有遠端儲存庫({0})都不是 GitHub、GitLab 或 Bitbucket 地址。", + "no room": "空間不足", "not a pull request number: {0}": "不是拉取請求編號:{0}", "not a worktree": "不是工作樹", "not published": "未釋出", @@ -3080,6 +3081,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0} 有 {1} 是 {2} 沒有的,而 {3} 有 {4} 是 {5} 沒有的 — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} 有未暫存的更改。請先暫存或儲藏它們 — git 不會帶著未暫存的更改繼續變基。", "{0} hidden · drag header edges to resize": "已隱藏 {0} · 拖動表頭邊緣可調整大小", + "{0} hidden — panel too narrow": "已隱藏 {0} — 面板太窄", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} 遇到衝突。請解決它們,然後繼續或中止。", "{0} in Theirs": "{0} 在“他們的”中", "{0} in Yours": "{0} 在“你的”中", diff --git a/apps/merge-studio/l10n/bundle.l10n.cs.json b/apps/merge-studio/l10n/bundle.l10n.cs.json index 5f58f97e..61ee3195 100644 --- a/apps/merge-studio/l10n/bundle.l10n.cs.json +++ b/apps/merge-studio/l10n/bundle.l10n.cs.json @@ -1508,6 +1508,7 @@ "no commits yet": "zatím žádné commity", "no email recorded": "žádný zaznamenaný e-mail", "no one": "nikdo", + "no room": "není místo", "not a pull request number: {0}": "nejedná se o číslo pull requestu: {0}", "not a worktree": "není pracovní strom", "not pushed": "nenahráno (push)", @@ -1703,6 +1704,7 @@ "{0} has {1} that pull request #{2} doesn't — made here, or from before ": "{0} má {1}, které pull request #{2} nemá — vytvořené tady, nebo odtud dřív ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} mají změny, které nejsou připravené. Nejdřív je připravte nebo stashněte — git nebude pokračovat v rebase s nepřipravenými změnami.", "{0} hidden · drag header edges to resize": "{0} skryto · velikost změníte tažením okrajů záhlaví", + "{0} hidden — panel too narrow": "{0} skryto — panel je příliš úzký", "{0} in Theirs": "{0} v Jejich", "{0} in Yours": "{0} ve Vašem", "{0} in the Result, marked so you can check {1}.": "{0} ve Výsledku, označeno tak, abyste to mohli zkontrolovat {1}.", diff --git a/apps/merge-studio/l10n/bundle.l10n.de.json b/apps/merge-studio/l10n/bundle.l10n.de.json index c537c5d2..b6350c09 100644 --- a/apps/merge-studio/l10n/bundle.l10n.de.json +++ b/apps/merge-studio/l10n/bundle.l10n.de.json @@ -1508,6 +1508,7 @@ "no commits yet": "noch keine Commits", "no email recorded": "keine E-Mail hinterlegt", "no one": "niemand", + "no room": "kein Platz", "not a pull request number: {0}": "keine Pull-Request-Nummer: {0}", "not a worktree": "kein Worktree", "not pushed": "nicht gepusht", @@ -1703,6 +1704,7 @@ "{0} has {1} that pull request #{2} doesn't — made here, or from before ": "{0} hat {1}, was Pull Request #{2} nicht hat — hier gemacht, oder von davor ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} haben nicht bereitgestellte Änderungen. Zuerst bereitstellen oder stashen — Git setzt einen Rebase nicht mit nicht bereitgestellten Änderungen fort.", "{0} hidden · drag header edges to resize": "{0} ausgeblendet · Kopfzeilenränder ziehen, um die Größe anzupassen", + "{0} hidden — panel too narrow": "{0} ausgeblendet — Bereich zu schmal", "{0} in Theirs": "{0} in deren Version", "{0} in Yours": "{0} in Ihrer Version", "{0} in the Result, marked so you can check {1}.": "{0} im Ergebnis, markiert, damit {1} geprüft werden kann.", diff --git a/apps/merge-studio/l10n/bundle.l10n.es.json b/apps/merge-studio/l10n/bundle.l10n.es.json index 058c3cc9..03e99991 100644 --- a/apps/merge-studio/l10n/bundle.l10n.es.json +++ b/apps/merge-studio/l10n/bundle.l10n.es.json @@ -1508,6 +1508,7 @@ "no commits yet": "sin commits todavía", "no email recorded": "sin correo electrónico registrado", "no one": "nadie", + "no room": "sin espacio", "not a pull request number: {0}": "no es un número de pull request: {0}", "not a worktree": "no es un worktree", "not pushed": "no publicado con push", @@ -1703,6 +1704,7 @@ "{0} has {1} that pull request #{2} doesn't — made here, or from before ": "{0} tiene {1} que el pull request #{2} no tiene — hechos aquí, o de antes de ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} tienen cambios que no están preparados. Prepáralos o guárdalos en el stash primero — git no continuará un rebase con cambios sin preparar.", "{0} hidden · drag header edges to resize": "{0} oculto · arrastra los bordes del encabezado para cambiar el tamaño", + "{0} hidden — panel too narrow": "{0} oculto — panel demasiado estrecho", "{0} in Theirs": "{0} en Suyos", "{0} in Yours": "{0} en Tuyos", "{0} in the Result, marked so you can check {1}.": "{0} en el Resultado, marcados para que puedas revisar {1}.", diff --git a/apps/merge-studio/l10n/bundle.l10n.fr.json b/apps/merge-studio/l10n/bundle.l10n.fr.json index a8bfd5fb..fc89f0f0 100644 --- a/apps/merge-studio/l10n/bundle.l10n.fr.json +++ b/apps/merge-studio/l10n/bundle.l10n.fr.json @@ -1508,6 +1508,7 @@ "no commits yet": "aucun commit pour l'instant", "no email recorded": "aucun e-mail enregistré", "no one": "personne", + "no room": "pas de place", "not a pull request number: {0}": "pas un numéro de pull request : {0}", "not a worktree": "pas une arborescence de travail", "not pushed": "non poussé", @@ -1703,6 +1704,7 @@ "{0} has {1} that pull request #{2} doesn't — made here, or from before ": "{0} a {1} que la pull request #{2} n'a pas — faits ici, ou d'avant ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} ont des modifications non indexées. Indexez-les ou remisez-les d'abord — git ne continuera pas un rebase avec des modifications non indexées.", "{0} hidden · drag header edges to resize": "{0} masqué(e)s · faites glisser les bords de l'en-tête pour redimensionner", + "{0} hidden — panel too narrow": "{0} masqué(e)s — panneau trop étroit", "{0} in Theirs": "{0} dans la leur", "{0} in Yours": "{0} dans la vôtre", "{0} in the Result, marked so you can check {1}.": "{0} dans le résultat, marqué pour que vous puissiez vérifier {1}.", diff --git a/apps/merge-studio/l10n/bundle.l10n.it.json b/apps/merge-studio/l10n/bundle.l10n.it.json index 965e3458..5e424cec 100644 --- a/apps/merge-studio/l10n/bundle.l10n.it.json +++ b/apps/merge-studio/l10n/bundle.l10n.it.json @@ -1508,6 +1508,7 @@ "no commits yet": "ancora nessun commit", "no email recorded": "nessuna email registrata", "no one": "nessuno", + "no room": "spazio insufficiente", "not a pull request number: {0}": "non è un numero di pull request: {0}", "not a worktree": "non è un worktree", "not pushed": "non sottoposto a push", @@ -1703,6 +1704,7 @@ "{0} has {1} that pull request #{2} doesn't — made here, or from before ": "{0} ha {1} che la pull request #{2} non ha — fatti qui, o da prima ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} hanno modifiche non in stage. Mettile prima in stage o in stash — git non continuerà un rebase con modifiche non in stage.", "{0} hidden · drag header edges to resize": "{0} nascosti · trascina i bordi dell'intestazione per ridimensionare", + "{0} hidden — panel too narrow": "{0} nascosti — pannello troppo stretto", "{0} in Theirs": "{0} in Loro", "{0} in Yours": "{0} in Tuo", "{0} in the Result, marked so you can check {1}.": "{0} nel Risultato, segnati così puoi verificare {1}.", diff --git a/apps/merge-studio/l10n/bundle.l10n.ja.json b/apps/merge-studio/l10n/bundle.l10n.ja.json index aac8f065..ba76392e 100644 --- a/apps/merge-studio/l10n/bundle.l10n.ja.json +++ b/apps/merge-studio/l10n/bundle.l10n.ja.json @@ -1508,6 +1508,7 @@ "no commits yet": "まだコミットがありません", "no email recorded": "メールアドレスが記録されていません", "no one": "だれもいません", + "no room": "スペース不足", "not a pull request number: {0}": "プルリクエスト番号ではありません: {0}", "not a worktree": "ワークツリーではありません", "not pushed": "プッシュされていません", @@ -1703,6 +1704,7 @@ "{0} has {1} that pull request #{2} doesn't — made here, or from before ": "{0} には プルリクエスト #{2} にはない {1} があります — ここで行われたか、それ以前のものです", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} にはステージされていない変更があります。先にステージまたはスタッシュしてください — gitは未ステージの変更があるとリベースを続行できません。", "{0} hidden · drag header edges to resize": "{0} 個を非表示 · ヘッダーの端をドラッグしてサイズ変更", + "{0} hidden — panel too narrow": "{0} を非表示 — パネルが狭すぎます", "{0} in Theirs": "相手の {0}", "{0} in Yours": "自分の {0}", "{0} in the Result, marked so you can check {1}.": "結果内の {0} は、{1} を確認できるようにマークされています。", diff --git a/apps/merge-studio/l10n/bundle.l10n.json b/apps/merge-studio/l10n/bundle.l10n.json index 201bfe6c..69240aeb 100644 --- a/apps/merge-studio/l10n/bundle.l10n.json +++ b/apps/merge-studio/l10n/bundle.l10n.json @@ -1508,6 +1508,7 @@ "no commits yet": "no commits yet", "no email recorded": "no email recorded", "no one": "no one", + "no room": "no room", "not a pull request number: {0}": "not a pull request number: {0}", "not a worktree": "not a worktree", "not pushed": "not pushed", @@ -1703,6 +1704,7 @@ "{0} has {1} that pull request #{2} doesn't — made here, or from before ": "{0} has {1} that pull request #{2} doesn't — made here, or from before ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.", "{0} hidden · drag header edges to resize": "{0} hidden · drag header edges to resize", + "{0} hidden — panel too narrow": "{0} hidden — panel too narrow", "{0} in Theirs": "{0} in Theirs", "{0} in Yours": "{0} in Yours", "{0} in the Result, marked so you can check {1}.": "{0} in the Result, marked so you can check {1}.", diff --git a/apps/merge-studio/l10n/bundle.l10n.ko.json b/apps/merge-studio/l10n/bundle.l10n.ko.json index 2e362380..d107938e 100644 --- a/apps/merge-studio/l10n/bundle.l10n.ko.json +++ b/apps/merge-studio/l10n/bundle.l10n.ko.json @@ -1508,6 +1508,7 @@ "no commits yet": "아직 커밋 없음", "no email recorded": "기록된 이메일 없음", "no one": "없음", + "no room": "공간 부족", "not a pull request number: {0}": "풀 리퀘스트 번호가 아님: {0}", "not a worktree": "작업 폴더가 아님", "not pushed": "푸시되지 않음", @@ -1703,6 +1704,7 @@ "{0} has {1} that pull request #{2} doesn't — made here, or from before ": "{0}에는 풀 리퀘스트 #{1}에 없는 {2}이 있습니다 — 여기서 만들어졌거나 ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0}에 스테이징되지 않은 변경 내용이 있습니다. 먼저 스테이지에 추가하거나 스태시하세요 — git은 스테이징되지 않은 변경 내용이 있으면 리베이스를 계속하지 않습니다.", "{0} hidden · drag header edges to resize": "{0} 숨겨짐 · 헤더 경계를 드래그하여 크기 조절", + "{0} hidden — panel too narrow": "{0} 숨겨짐 — 패널이 너무 좁음", "{0} in Theirs": "상대 변경 내용의 {0}", "{0} in Yours": "내 변경 내용의 {0}", "{0} in the Result, marked so you can check {1}.": "결과의 {0}은 확인할 수 있도록 표시되어 있습니다 {1}.", diff --git a/apps/merge-studio/l10n/bundle.l10n.pl.json b/apps/merge-studio/l10n/bundle.l10n.pl.json index 341351a0..74d0d91e 100644 --- a/apps/merge-studio/l10n/bundle.l10n.pl.json +++ b/apps/merge-studio/l10n/bundle.l10n.pl.json @@ -1508,6 +1508,7 @@ "no commits yet": "brak commitów", "no email recorded": "nie zapisano adresu e-mail", "no one": "nikt", + "no room": "brak miejsca", "not a pull request number: {0}": "nie jest numerem pull requesta: {0}", "not a worktree": "to nie worktree", "not pushed": "nie wypchnięto", @@ -1703,6 +1704,7 @@ "{0} has {1} that pull request #{2} doesn't — made here, or from before ": "{0} ma {1}, których pull request #{2} nie ma — zrobione tutaj albo sprzed ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} mają zmiany, które nie są przygotowane. Najpierw je przygotuj albo zapisz w schowku — git nie kontynuuje rebase z nieprzygotowanymi zmianami.", "{0} hidden · drag header edges to resize": "{0} ukryte · przeciągnij krawędzie nagłówka, aby zmienić rozmiar", + "{0} hidden — panel too narrow": "{0} ukryte — panel jest za wąski", "{0} in Theirs": "{0} w Ich", "{0} in Yours": "{0} w Twoich", "{0} in the Result, marked so you can check {1}.": "{0} w Wyniku, oznaczone, abyś mógł sprawdzić {1}.", diff --git a/apps/merge-studio/l10n/bundle.l10n.pt-br.json b/apps/merge-studio/l10n/bundle.l10n.pt-br.json index bf97779b..6f45a99b 100644 --- a/apps/merge-studio/l10n/bundle.l10n.pt-br.json +++ b/apps/merge-studio/l10n/bundle.l10n.pt-br.json @@ -1508,6 +1508,7 @@ "no commits yet": "ainda sem commits", "no email recorded": "nenhum e-mail registrado", "no one": "ninguém", + "no room": "sem espaço", "not a pull request number: {0}": "não é um número de pull request: {0}", "not a worktree": "não é um worktree", "not pushed": "sem push", @@ -1703,6 +1704,7 @@ "{0} has {1} that pull request #{2} doesn't — made here, or from before ": "{0} tem {1} que a pull request #{2} não tem — feito aqui, ou de antes ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} têm alterações que não estão em stage. Coloque-as em stage ou guarde-as no stash primeiro — o git não continua um rebase com alterações fora do stage.", "{0} hidden · drag header edges to resize": "{0} ocultos · arraste as bordas do cabeçalho para redimensionar", + "{0} hidden — panel too narrow": "{0} ocultos — painel estreito demais", "{0} in Theirs": "{0} em Deles", "{0} in Yours": "{0} em Seu", "{0} in the Result, marked so you can check {1}.": "{0} no Resultado, marcado para que você possa verificar {1}.", diff --git a/apps/merge-studio/l10n/bundle.l10n.ru.json b/apps/merge-studio/l10n/bundle.l10n.ru.json index fb63dd3f..9c5b83ba 100644 --- a/apps/merge-studio/l10n/bundle.l10n.ru.json +++ b/apps/merge-studio/l10n/bundle.l10n.ru.json @@ -1508,6 +1508,7 @@ "no commits yet": "пока нет коммитов", "no email recorded": "email не записан", "no one": "никто", + "no room": "нет места", "not a pull request number: {0}": "не номер pull request: {0}", "not a worktree": "не рабочий каталог", "not pushed": "не отправлено", @@ -1703,6 +1704,7 @@ "{0} has {1} that pull request #{2} doesn't — made here, or from before ": "В {0} есть {1}, которых нет в pull request #{2} — сделанные здесь, или из состояния до ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "В {0} есть изменения, не добавленные в индекс. Сначала добавьте их в индекс или спрячьте — git не продолжит перебазирование с неиндексированными изменениями.", "{0} hidden · drag header edges to resize": "{0} скрыто · перетащите края заголовка для изменения размера", + "{0} hidden — panel too narrow": "{0} скрыто — панель слишком узкая", "{0} in Theirs": "{0} в «Их»", "{0} in Yours": "{0} в «Ваши»", "{0} in the Result, marked so you can check {1}.": "{0} в Результате, отмечены, чтобы вы могли проверить {1}.", diff --git a/apps/merge-studio/l10n/bundle.l10n.tr.json b/apps/merge-studio/l10n/bundle.l10n.tr.json index 07f0ceaa..d763c682 100644 --- a/apps/merge-studio/l10n/bundle.l10n.tr.json +++ b/apps/merge-studio/l10n/bundle.l10n.tr.json @@ -1508,6 +1508,7 @@ "no commits yet": "henüz işleme yok", "no email recorded": "e-posta kaydedilmemiş", "no one": "hiç kimse", + "no room": "yer yok", "not a pull request number: {0}": "bir çekme isteği numarası değil: {0}", "not a worktree": "bir çalışma alanı değil", "not pushed": "gönderilmedi", @@ -1703,6 +1704,7 @@ "{0} has {1} that pull request #{2} doesn't — made here, or from before ": "{0}'da #{2} çekme isteğinde olmayan {1} var — burada yapılmış veya ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0}'da hazırlanmamış değişiklikler var. Önce hazırlayın veya zulalayın — git hazırlanmamış değişikliklerle bir yeniden temellendirmeyi sürdürmez.", "{0} hidden · drag header edges to resize": "{0} gizli · boyutlandırmak için başlık kenarlarını sürükleyin", + "{0} hidden — panel too narrow": "{0} gizli — panel çok dar", "{0} in Theirs": "Onlarınki'nde {0}", "{0} in Yours": "Sizinki'nde {0}", "{0} in the Result, marked so you can check {1}.": "Sonuç'ta {0}, kontrol edebilmeniz için {1} işaretli.", diff --git a/apps/merge-studio/l10n/bundle.l10n.zh-cn.json b/apps/merge-studio/l10n/bundle.l10n.zh-cn.json index c6389da6..35919d68 100644 --- a/apps/merge-studio/l10n/bundle.l10n.zh-cn.json +++ b/apps/merge-studio/l10n/bundle.l10n.zh-cn.json @@ -1508,6 +1508,7 @@ "no commits yet": "尚无提交", "no email recorded": "未记录邮箱", "no one": "无人", + "no room": "空间不足", "not a pull request number: {0}": "不是拉取请求编号:{0}", "not a worktree": "不是工作树", "not pushed": "未推送", @@ -1703,6 +1704,7 @@ "{0} has {1} that pull request #{2} doesn't — made here, or from before ": "{0} 有 {1},而拉取请求 #{2} 没有 — 在此处创建的,或来自之前 ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} 有未暂存的更改。请先暂存或贮藏它们 — git 不会带着未暂存的更改继续变基。", "{0} hidden · drag header edges to resize": "已隐藏 {0} · 拖动表头边缘可调整大小", + "{0} hidden — panel too narrow": "已隐藏 {0} — 面板太窄", "{0} in Theirs": "{0} 在“他们的”中", "{0} in Yours": "{0} 在“你的”中", "{0} in the Result, marked so you can check {1}.": "{0} 在结果中,已标记,便于你检查 {1}。", diff --git a/apps/merge-studio/l10n/bundle.l10n.zh-tw.json b/apps/merge-studio/l10n/bundle.l10n.zh-tw.json index 7ffd4bfc..7629518d 100644 --- a/apps/merge-studio/l10n/bundle.l10n.zh-tw.json +++ b/apps/merge-studio/l10n/bundle.l10n.zh-tw.json @@ -1508,6 +1508,7 @@ "no commits yet": "尚無提交", "no email recorded": "未記錄郵箱", "no one": "無人", + "no room": "空間不足", "not a pull request number: {0}": "不是拉取請求編號:{0}", "not a worktree": "不是工作樹", "not pushed": "未推送", @@ -1703,6 +1704,7 @@ "{0} has {1} that pull request #{2} doesn't — made here, or from before ": "{0} 有 {1},而拉取請求 #{2} 沒有 — 在此處建立的,或來自之前 ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} 有未暫存的更改。請先暫存或儲藏它們 — git 不會帶著未暫存的更改繼續變基。", "{0} hidden · drag header edges to resize": "已隱藏 {0} · 拖動表頭邊緣可調整大小", + "{0} hidden — panel too narrow": "已隱藏 {0} — 面板太窄", "{0} in Theirs": "{0} 在“他們的”中", "{0} in Yours": "{0} 在“你的”中", "{0} in the Result, marked so you can check {1}.": "{0} 在結果中,已標記,便於你檢查 {1}。", diff --git a/l10n/cs.json b/l10n/cs.json index ce229e18..d54dfaf0 100644 --- a/l10n/cs.json +++ b/l10n/cs.json @@ -5268,6 +5268,7 @@ "no model set": "žádný model nastaven", "no one": "nikdo", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "žádný vzdálený repozitář tohoto repozitáře ({0}) není adresa GitHub, GitLab nebo Bitbucket.", + "no room": "není místo", "no sha": "žádné sha", "not a pull request number: {0}": "nejedná se o číslo pull requestu: {0}", "not a worktree": "není pracovní strom", @@ -5734,6 +5735,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0} má {1}, které {2} nemá, a {3} má {4}, které {5} nemá — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} mají změny, které nejsou připravené. Nejdřív je připravte nebo stashněte — git nebude pokračovat v rebase s nepřipravenými změnami.", "{0} hidden · drag header edges to resize": "{0} skryto · velikost změníte tažením okrajů záhlaví", + "{0} hidden — panel too narrow": "{0} skryto — panel je příliš úzký", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} narazilo na konflikty. Vyřešte je, pak pokračujte nebo to zrušte.", "{0} in Theirs": "{0} v Jejich", "{0} in Yours": "{0} ve Vašem", diff --git a/l10n/de.json b/l10n/de.json index 4d3dc64d..6b074cd2 100644 --- a/l10n/de.json +++ b/l10n/de.json @@ -5268,6 +5268,7 @@ "no model set": "kein Modell festgelegt", "no one": "niemand", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "kein Remote dieses Repositorys ({0}) ist eine GitHub-, GitLab- oder Bitbucket-Adresse.", + "no room": "kein Platz", "no sha": "kein SHA", "not a pull request number: {0}": "keine Pull-Request-Nummer: {0}", "not a worktree": "kein Worktree", @@ -5734,6 +5735,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0} hat {1}, was {2} nicht hat, und {3} hat {4}, was {5} nicht hat — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} haben nicht bereitgestellte Änderungen. Zuerst bereitstellen oder stashen — Git setzt einen Rebase nicht mit nicht bereitgestellten Änderungen fort.", "{0} hidden · drag header edges to resize": "{0} ausgeblendet · Kopfzeilenränder ziehen, um die Größe anzupassen", + "{0} hidden — panel too narrow": "{0} ausgeblendet — Bereich zu schmal", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} ist auf Konflikte gestoßen. Lösen, dann fortsetzen oder abbrechen.", "{0} in Theirs": "{0} in deren Version", "{0} in Yours": "{0} in Ihrer Version", diff --git a/l10n/es.json b/l10n/es.json index 88e2c6bd..dbb6a710 100644 --- a/l10n/es.json +++ b/l10n/es.json @@ -5268,6 +5268,7 @@ "no model set": "sin modelo establecido", "no one": "nadie", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "ningún remoto de este repositorio ({0}) es una dirección de GitHub, GitLab o Bitbucket.", + "no room": "sin espacio", "no sha": "sin sha", "not a pull request number: {0}": "no es un número de pull request: {0}", "not a worktree": "no es un worktree", @@ -5734,6 +5735,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0} tiene {1} que {2} no tiene, y {3} tiene {4} que {5} no tiene — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} tienen cambios que no están preparados. Prepáralos o guárdalos en el stash primero — git no continuará un rebase con cambios sin preparar.", "{0} hidden · drag header edges to resize": "{0} oculto · arrastra los bordes del encabezado para cambiar el tamaño", + "{0} hidden — panel too narrow": "{0} oculto — panel demasiado estrecho", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} tuvo conflictos. Resuélvelos, y luego continúa o aborta.", "{0} in Theirs": "{0} en Suyos", "{0} in Yours": "{0} en Tuyos", diff --git a/l10n/fr.json b/l10n/fr.json index 3b8d7d5c..b17da7a2 100644 --- a/l10n/fr.json +++ b/l10n/fr.json @@ -5268,6 +5268,7 @@ "no model set": "aucun modèle défini", "no one": "personne", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "aucun dépôt distant de ce dépôt ({0}) n'est une adresse GitHub, GitLab ou Bitbucket.", + "no room": "pas de place", "no sha": "pas de sha", "not a pull request number: {0}": "pas un numéro de pull request : {0}", "not a worktree": "pas une arborescence de travail", @@ -5734,6 +5735,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0} a {1} que {2} n'a pas, et {3} a {4} que {5} n'a pas — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} ont des modifications non indexées. Indexez-les ou remisez-les d'abord — git ne continuera pas un rebase avec des modifications non indexées.", "{0} hidden · drag header edges to resize": "{0} masqué(e)s · faites glisser les bords de l'en-tête pour redimensionner", + "{0} hidden — panel too narrow": "{0} masqué(e)s — panneau trop étroit", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} a rencontré des conflits. Résolvez-les, puis continuez ou abandonnez.", "{0} in Theirs": "{0} dans la leur", "{0} in Yours": "{0} dans la vôtre", diff --git a/l10n/it.json b/l10n/it.json index 3e9941f8..650da755 100644 --- a/l10n/it.json +++ b/l10n/it.json @@ -5268,6 +5268,7 @@ "no model set": "nessun modello impostato", "no one": "nessuno", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "nessun remoto di questo repository ({0}) è un indirizzo GitHub, GitLab o Bitbucket.", + "no room": "spazio insufficiente", "no sha": "nessun sha", "not a pull request number: {0}": "non è un numero di pull request: {0}", "not a worktree": "non è un worktree", @@ -5734,6 +5735,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0} ha {1} che {2} non ha, e {3} ha {4} che {5} non ha — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} hanno modifiche non in stage. Mettile prima in stage o in stash — git non continuerà un rebase con modifiche non in stage.", "{0} hidden · drag header edges to resize": "{0} nascosti · trascina i bordi dell'intestazione per ridimensionare", + "{0} hidden — panel too narrow": "{0} nascosti — pannello troppo stretto", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} ha incontrato conflitti. Risolvili, poi continua o interrompi.", "{0} in Theirs": "{0} in Loro", "{0} in Yours": "{0} in Tuo", diff --git a/l10n/ja.json b/l10n/ja.json index 4b94fa0e..fb8cc27c 100644 --- a/l10n/ja.json +++ b/l10n/ja.json @@ -5268,6 +5268,7 @@ "no model set": "モデル未設定", "no one": "だれもいません", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "このリポジトリ ({0}) のリモートに、GitHub、GitLab、Bitbucketのアドレスはありません。", + "no room": "スペース不足", "no sha": "SHA なし", "not a pull request number: {0}": "プルリクエスト番号ではありません: {0}", "not a worktree": "ワークツリーではありません", @@ -5734,6 +5735,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0} には {2} にはない {1} があり、{3} には {5} にはない {4} があります —", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} にはステージされていない変更があります。先にステージまたはスタッシュしてください — gitは未ステージの変更があるとリベースを続行できません。", "{0} hidden · drag header edges to resize": "{0} 個を非表示 · ヘッダーの端をドラッグしてサイズ変更", + "{0} hidden — panel too narrow": "{0} を非表示 — パネルが狭すぎます", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} でコンフリクトが発生しました。解決してから続行するか、中止してください。", "{0} in Theirs": "相手の {0}", "{0} in Yours": "自分の {0}", diff --git a/l10n/ko.json b/l10n/ko.json index 2425bd4e..3770dd1f 100644 --- a/l10n/ko.json +++ b/l10n/ko.json @@ -5268,6 +5268,7 @@ "no model set": "설정된 모델 없음", "no one": "없음", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "이 리포지토리의 원격({0}) 중 GitHub, GitLab 또는 Bitbucket 주소인 것이 없습니다.", + "no room": "공간 부족", "no sha": "SHA 없음", "not a pull request number: {0}": "풀 리퀘스트 번호가 아님: {0}", "not a worktree": "작업 폴더가 아님", @@ -5734,6 +5735,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0}에는 {2}에 없는 {1}이 있고, {3}에는 {5}에 없는 {4}이 있습니다 — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0}에 스테이징되지 않은 변경 내용이 있습니다. 먼저 스테이지에 추가하거나 스태시하세요 — git은 스테이징되지 않은 변경 내용이 있으면 리베이스를 계속하지 않습니다.", "{0} hidden · drag header edges to resize": "{0} 숨겨짐 · 헤더 경계를 드래그하여 크기 조절", + "{0} hidden — panel too narrow": "{0} 숨겨짐 — 패널이 너무 좁음", "{0} hit conflicts. Resolve them, then continue or abort.": "{0}에서 충돌이 발생했습니다. 해결 후 계속하거나 중단하세요.", "{0} in Theirs": "상대 변경 내용의 {0}", "{0} in Yours": "내 변경 내용의 {0}", diff --git a/l10n/pl.json b/l10n/pl.json index 82b740ca..bf9066fa 100644 --- a/l10n/pl.json +++ b/l10n/pl.json @@ -5268,6 +5268,7 @@ "no model set": "brak ustawionego modelu", "no one": "nikt", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "żadne zdalne repozytorium tego repozytorium ({0}) nie jest adresem GitHub, GitLab ani Bitbucket.", + "no room": "brak miejsca", "no sha": "brak sha", "not a pull request number: {0}": "nie jest numerem pull requesta: {0}", "not a worktree": "to nie worktree", @@ -5734,6 +5735,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0} ma {1}, których {2} nie ma, a {3} ma {4}, których {5} nie ma — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} mają zmiany, które nie są przygotowane. Najpierw je przygotuj albo zapisz w schowku — git nie kontynuuje rebase z nieprzygotowanymi zmianami.", "{0} hidden · drag header edges to resize": "{0} ukryte · przeciągnij krawędzie nagłówka, aby zmienić rozmiar", + "{0} hidden — panel too narrow": "{0} ukryte — panel jest za wąski", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} natrafiło na konflikty. Rozwiąż je, a potem kontynuuj albo przerwij.", "{0} in Theirs": "{0} w Ich", "{0} in Yours": "{0} w Twoich", diff --git a/l10n/pt-br.json b/l10n/pt-br.json index f8fc6f89..5e92e9be 100644 --- a/l10n/pt-br.json +++ b/l10n/pt-br.json @@ -5268,6 +5268,7 @@ "no model set": "nenhum modelo definido", "no one": "ninguém", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "nenhum remoto deste repositório ({0}) é um endereço do GitHub, GitLab ou Bitbucket.", + "no room": "sem espaço", "no sha": "nenhum sha", "not a pull request number: {0}": "não é um número de pull request: {0}", "not a worktree": "não é um worktree", @@ -5734,6 +5735,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0} tem {1} que {2} não tem, e {3} tem {4} que {5} não tem — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} têm alterações que não estão em stage. Coloque-as em stage ou guarde-as no stash primeiro — o git não continua um rebase com alterações fora do stage.", "{0} hidden · drag header edges to resize": "{0} ocultos · arraste as bordas do cabeçalho para redimensionar", + "{0} hidden — panel too narrow": "{0} ocultos — painel estreito demais", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} encontrou conflitos. Resolva-os, depois continue ou aborte.", "{0} in Theirs": "{0} em Deles", "{0} in Yours": "{0} em Seu", diff --git a/l10n/ru.json b/l10n/ru.json index 556dffdc..0a5f6206 100644 --- a/l10n/ru.json +++ b/l10n/ru.json @@ -5268,6 +5268,7 @@ "no model set": "модель не задана", "no one": "никто", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "ни один удалённый репозиторий этого репозитория ({0}) не является адресом GitHub, GitLab или Bitbucket.", + "no room": "нет места", "no sha": "нет sha", "not a pull request number: {0}": "не номер pull request: {0}", "not a worktree": "не рабочий каталог", @@ -5734,6 +5735,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "В {0} есть {1}, которых нет в {2}, а в {3} есть {4}, которых нет в {5} — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "В {0} есть изменения, не добавленные в индекс. Сначала добавьте их в индекс или спрячьте — git не продолжит перебазирование с неиндексированными изменениями.", "{0} hidden · drag header edges to resize": "{0} скрыто · перетащите края заголовка для изменения размера", + "{0} hidden — panel too narrow": "{0} скрыто — панель слишком узкая", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} привело к конфликтам. Разрешите их, затем продолжите или прервите.", "{0} in Theirs": "{0} в «Их»", "{0} in Yours": "{0} в «Ваши»", diff --git a/l10n/tr.json b/l10n/tr.json index f5a1b3a8..518429eb 100644 --- a/l10n/tr.json +++ b/l10n/tr.json @@ -5268,6 +5268,7 @@ "no model set": "model ayarlanmadı", "no one": "hiç kimse", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "bu deponun hiçbir uzak konumu ({0}) bir GitHub, GitLab veya Bitbucket adresi değil.", + "no room": "yer yok", "no sha": "sha yok", "not a pull request number: {0}": "bir çekme isteği numarası değil: {0}", "not a worktree": "bir çalışma alanı değil", @@ -5734,6 +5735,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0}'da {2}'de olmayan {1} var ve {3}'de {5}'de olmayan {4} var — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0}'da hazırlanmamış değişiklikler var. Önce hazırlayın veya zulalayın — git hazırlanmamış değişikliklerle bir yeniden temellendirmeyi sürdürmez.", "{0} hidden · drag header edges to resize": "{0} gizli · boyutlandırmak için başlık kenarlarını sürükleyin", + "{0} hidden — panel too narrow": "{0} gizli — panel çok dar", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} çakışmalara çarptı. Çözün, sonra sürdürün veya iptal edin.", "{0} in Theirs": "Onlarınki'nde {0}", "{0} in Yours": "Sizinki'nde {0}", diff --git a/l10n/zh-cn.json b/l10n/zh-cn.json index f439357d..eab95f38 100644 --- a/l10n/zh-cn.json +++ b/l10n/zh-cn.json @@ -5268,6 +5268,7 @@ "no model set": "未设置模型", "no one": "无人", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "此仓库的所有远程仓库({0})都不是 GitHub、GitLab 或 Bitbucket 地址。", + "no room": "空间不足", "no sha": "无 SHA", "not a pull request number: {0}": "不是拉取请求编号:{0}", "not a worktree": "不是工作树", @@ -5734,6 +5735,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0} 有 {1} 是 {2} 没有的,而 {3} 有 {4} 是 {5} 没有的 — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} 有未暂存的更改。请先暂存或贮藏它们 — git 不会带着未暂存的更改继续变基。", "{0} hidden · drag header edges to resize": "已隐藏 {0} · 拖动表头边缘可调整大小", + "{0} hidden — panel too narrow": "已隐藏 {0} — 面板太窄", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} 遇到冲突。请解决它们,然后继续或中止。", "{0} in Theirs": "{0} 在“他们的”中", "{0} in Yours": "{0} 在“你的”中", diff --git a/l10n/zh-tw.json b/l10n/zh-tw.json index cfd36aed..b7b27067 100644 --- a/l10n/zh-tw.json +++ b/l10n/zh-tw.json @@ -5268,6 +5268,7 @@ "no model set": "未設定模型", "no one": "無人", "no remote of this repository ({0}) is a GitHub, GitLab or Bitbucket address.": "此儲存庫的所有遠端儲存庫({0})都不是 GitHub、GitLab 或 Bitbucket 地址。", + "no room": "空間不足", "no sha": "無 SHA", "not a pull request number: {0}": "不是拉取請求編號:{0}", "not a worktree": "不是工作樹", @@ -5734,6 +5735,7 @@ "{0} has {1} that {2} doesn't, and {3} has {4} that {5} doesn't — ": "{0} 有 {1} 是 {2} 沒有的,而 {3} 有 {4} 是 {5} 沒有的 — ", "{0} have changes that aren't staged. Stage or stash them first — git won't continue a rebase with unstaged changes.": "{0} 有未暫存的更改。請先暫存或儲藏它們 — git 不會帶著未暫存的更改繼續變基。", "{0} hidden · drag header edges to resize": "已隱藏 {0} · 拖動表頭邊緣可調整大小", + "{0} hidden — panel too narrow": "已隱藏 {0} — 面板太窄", "{0} hit conflicts. Resolve them, then continue or abort.": "{0} 遇到衝突。請解決它們,然後繼續或中止。", "{0} in Theirs": "{0} 在“他們的”中", "{0} in Yours": "{0} 在“你的”中", diff --git a/packages/webview-ui/src/graph/commit-graph.ts b/packages/webview-ui/src/graph/commit-graph.ts index 96aa991f..3bbb8cf6 100644 --- a/packages/webview-ui/src/graph/commit-graph.ts +++ b/packages/webview-ui/src/graph/commit-graph.ts @@ -103,6 +103,12 @@ const SUBJECT_MIN_WIDTH = 220; * fixed width: the user's own dragged width still wins above it. */ const REFS_MIN_READABLE = 150; +/** The Branch/Tag track's cap at and below COLUMN_DROP_TAIL_AT (the CSS tier). */ +const REFS_CAP_NARROW = 300; +/** …and in the narrow table tier, at and below COLUMN_DROP_TAIL_AT - 200. */ +const REFS_CAP_TIGHT = 240; +/** The message's floor in that narrow table tier. */ +const SUBJECT_MIN_TIGHT = Math.round(SUBJECT_MIN_WIDTH * 0.5); /** * The width at which a commit message stops feeling cramped. Above the hard * SUBJECT_MIN_WIDTH floor: the Branch/Tag track auto-fits only with whatever is @@ -121,21 +127,39 @@ const COMPACT_DROP_DATE_AT = 580; const COMPACT_DROP_AUTHOR_AT = 430; const SUBJECT_MIN_COMPACT_MID = 150; const SUBJECT_MIN_COMPACT_TIGHT = 120; +/** Compact's own floor: below this it is a one-line list with no header. */ +const COMPACT_LIST_AT = 330; +/** Compact's default track widths — narrower than the editor tab's (`def`). */ +const COMPACT_DEF = { changes: 84, author: 112, date: 64, sha: 66 } as const; /** - * Below this host width, column mode drops its Date and SHA tracks. + * Date and SHA stay on screen for as long as they FIT (issue #124). * - * The threshold is set by what the REFS column needs when the tail comes back, - * not by when the tail itself starts to feel tight. At 760 the tail returned - * ~36px too early: crossing it (a 1296px window) collapsed the branch/tag track - * from 168px to 87px and rendered zero readable characters in it, so WIDENING - * the window made a column narrower and emptied it. That put the dead band over - * exactly the maximised-laptop widths, and nothing on screen said the column had - * been starved — it just looked empty. + * They used to go at a fixed breakpoint — below an 860px host in the editor + * tab, and always in the bottom panel, which had no SHA track at all — whether + * or not there was room, while the Columns menu went on showing them ticked. + * "The SHA column is enabled and not shown" is what a person sees then, and + * nothing on screen says why. * - * Date and SHA are the right things to give up for it: both are still on the - * row's hover tooltip and in the details dock, and both can be turned back on - * from the Columns menu. A branch name has nowhere else to be. + * Now the message gives way first, down to its floor, and the Branch/Tag + * track keeps the width at which a ref name is still readable — the reason the + * breakpoint sat as high as it did: at 760 the tail came back ~36px too early, + * collapsed the branch/tag track from 168px to 87px and rendered zero readable + * characters in it. Only what is left over is offered to the tail, SHA first: + * it is the narrowest column and the one that names the commit. Date goes + * first; both are still on the row's tooltip and in the details pane, and a + * column that has no room says so in the Columns menu. See fitTail. */ +const TAIL_FIT_ORDER: readonly ColumnSpec["id"][] = ["sha", "date"]; +/** + * Compact fits Author too, between the two. Its ladder drops Author at 430px + * anyway; without this, the band just above that had no room for SHA and hid + * it, and narrowing the panel by ten more pixels brought it BACK — a column + * that blinks out and in as you drag. Taking Author first keeps it one way. + */ +const TAIL_FIT_ORDER_COMPACT: readonly ColumnSpec["id"][] = ["sha", "author", "date"]; +/** The row's own horizontal chrome: the 2px spacer border + padding-right. */ +const ROW_CHROME = 2 + 12; +const ROW_CHROME_COMPACT = 2 + 10; /** `:host([compact]) .content .refs { max-width }` — a share of the MESSAGE track. */ const COMPACT_REFS_SHARE = 0.44; @@ -590,6 +614,15 @@ export class CommitGraph extends LitElement { } .gh-menuitem[aria-checked="true"] .codicon-check { opacity: 1; } .gh-menuitem .lbl { flex: 1 1 auto; } + /* "no room": a ticked column this width cannot hold (issue #124). Muted, + like the footer that says the same at more length. */ + .gh-menu-note { + flex: 0 0 auto; + font-size: 10.5px; + color: var(--gs-menu-muted); + } + .gh-menuitem:hover .gh-menu-note, + .gh-menuitem:focus-visible .gh-menu-note { color: inherit; opacity: 0.8; } .gh-menuitem[disabled] { opacity: 0.5; cursor: default; @@ -822,29 +855,31 @@ export class CommitGraph extends LitElement { A short, wide surface. Two things wasted space here: a fixed Branch/Tag track that sat empty on most rows, and a wide gap between the message and the trailing metadata. So in compact the refs flow INLINE just before the - message (a ref-less commit uses the full width), the SHA track goes away - (it is right there in the details pane), and what is left — changes / - author / date — forms one quiet cluster on the right. The message leads; - everything else recedes. */ + message (a ref-less commit uses the full width), and what is left — + changes / author / date / sha — forms one quiet cluster on the right. + The message leads; everything else recedes. + The SHA track is a real one. It used to be left out here ("it is right + there in the details pane") while the Columns menu went on offering SHA, + ticked, with nothing behind it (issue #124). It is on when the user has + it on and there is room — see fitTail. */ :host([compact]) { --gs-grid: var(--gs-gutter-w, ${MIN_GUTTER_WIDTH}px) minmax(${SUBJECT_MIN_WIDTH}px, 1fr) - minmax(0, var(--col-changes-w, 84px)) - minmax(0, var(--col-author-w, 112px)) - minmax(0, var(--col-date-w, 64px)); + minmax(0, var(--col-changes-w, ${COMPACT_DEF.changes}px)) + minmax(0, var(--col-author-w, ${COMPACT_DEF.author}px)) + minmax(0, var(--col-date-w, ${COMPACT_DEF.date}px)) + minmax(0, var(--col-sha-w, ${COMPACT_DEF.sha}px)); } - :host([compact]) .colhead .ch-refs, - :host([compact]) .colhead .ch-sha, - :host([compact]) .row .sha { display: none; } + :host([compact]) .colhead .ch-refs { display: none; } /* Compact's tracks are graph | message | changes | author | date, so four of the six grips land on a real boundary and stay draggable. (The claim they "would sit on the wrong boundaries" only held for the refs grip, whose cell is hidden here anyway — and a short panel is precisely where width is scarce and re-balancing matters most.) */ :host([compact]) .col-resize[data-col="refs"] { display: none; } - /* Date is compact's last track: its right-edge grip divides nothing. */ - :host([compact]) .col-resize[data-col="date"] { display: none; } + /* Date's grip divides Date | SHA; with SHA off it divides nothing, and the + shared :host(.hide-sha) rule below takes it away. */ :host([compact]) .content { display: flex; align-items: center; gap: 7px; min-width: 0; } @@ -907,21 +942,16 @@ export class CommitGraph extends LitElement { :host(:not([compact])) .row { --gs-grid: var(--gs-gutter-w, ${MIN_GUTTER_WIDTH}px) - minmax(0, clamp(0px, var(--col-refs-w, ${col("refs")}px), 300px)) + minmax(0, clamp(0px, var(--col-refs-w, ${col("refs")}px), ${REFS_CAP_NARROW}px)) minmax(${SUBJECT_MIN_WIDTH}px, 1fr) minmax(0, var(--col-changes-w, ${col("changes")}px)) - minmax(0, var(--col-author-w, ${col("author")}px)); + minmax(0, var(--col-author-w, ${col("author")}px)) + minmax(0, var(--col-date-w, ${col("date")}px)) + minmax(0, var(--col-sha-w, ${col("sha")}px)); } - :host(:not([compact])) .colhead .ch-date, - :host(:not([compact])) .colhead .ch-sha, - :host(:not([compact])) .row .date, - :host(:not([compact])) .row .sha { display: none; } - /* The cells are gone, but their grips are absolutely positioned inside - them and survived as 0x0 hit targets — a handle you can see the cursor - change on but can never drag. Author becomes the last visible column - here, so it must not keep a trailing grip either. */ - :host(:not([compact])) .colhead .col-resize[data-col="date"], - :host(:not([compact])) .colhead .col-resize[data-col="author"] { display: none; } + /* Date and SHA no longer go here by breakpoint: they stay while they fit + and fitTail hides them (the .hide-* classes, with their grips) when + they do not — issue #124. */ } /* ── Still a table, just a narrower one ───────────────────────────────── Between the inline floor and the tail drop, the two things that identify @@ -935,15 +965,24 @@ export class CommitGraph extends LitElement { @container (max-width: ${COLUMN_DROP_TAIL_AT - 200}px) { :host(:not([compact])) .colhead, :host(:not([compact])) .row { + /* SHA stays (issue #124): ~66px that names the commit, where a + desktop window with the details pane open usually lands. Seven + tracks, the dropped ones at 0, because every cell is pinned. */ --gs-grid: var(--gs-gutter-w, ${MIN_GUTTER_WIDTH}px) - minmax(0, clamp(${REFS_MIN_READABLE}px, var(--col-refs-w, ${col("refs")}px), 240px)) - minmax(${Math.round(SUBJECT_MIN_WIDTH * 0.5)}px, 1fr); + minmax(0, clamp(${REFS_MIN_READABLE}px, var(--col-refs-w, ${col("refs")}px), ${REFS_CAP_TIGHT}px)) + minmax(${SUBJECT_MIN_TIGHT}px, 1fr) + 0px + 0px + 0px + minmax(0, var(--col-sha-w, ${col("sha")}px)); } :host(:not([compact])) .colhead .ch-changes, :host(:not([compact])) .colhead .ch-author, + :host(:not([compact])) .colhead .ch-date, :host(:not([compact])) .row .changes, - :host(:not([compact])) .row .author { display: none; } + :host(:not([compact])) .row .author, + :host(:not([compact])) .row .date { display: none; } /* Their grips would survive as 0x0 hit targets otherwise. */ :host(:not([compact])) .colhead .col-resize[data-col="changes"], :host(:not([compact])) .colhead .col-resize[data-col="refs"][data-invert="0"] { display: none; } @@ -953,18 +992,23 @@ export class CommitGraph extends LitElement { so the message keeps the width. Mirrors the column-mode ladder above without ever reintroducing a Branch/Tag track (refs flow inline here). */ @container (max-width: ${COMPACT_DROP_DATE_AT}px) { + /* Six tracks still, the dropped one at 0: every cell is pinned to its + track (see "Every cell is PINNED"), so removing a track would move + the ones after it. */ :host([compact]) .colhead, :host([compact]) .row { --gs-grid: var(--gs-gutter-w, ${MIN_GUTTER_WIDTH}px) minmax(${SUBJECT_MIN_COMPACT_MID}px, 1fr) - minmax(0, var(--col-changes-w, 84px)) - minmax(0, var(--col-author-w, 112px)); + minmax(0, var(--col-changes-w, ${COMPACT_DEF.changes}px)) + minmax(0, var(--col-author-w, ${COMPACT_DEF.author}px)) + 0px + minmax(0, var(--col-sha-w, ${COMPACT_DEF.sha}px)); } :host([compact]) .colhead .ch-date, :host([compact]) .row .date { display: none; } - /* Author is last now, so its grip divides nothing. */ - :host([compact]) .col-resize[data-col="author"] { display: none; } + /* Without SHA, Author is last, so its grip divides nothing. */ + :host([compact].hide-sha) .col-resize[data-col="author"] { display: none; } } @container (max-width: ${COMPACT_DROP_AUTHOR_AT}px) { :host([compact]) .colhead, @@ -972,11 +1016,16 @@ export class CommitGraph extends LitElement { --gs-grid: var(--gs-gutter-w, ${MIN_GUTTER_WIDTH}px) minmax(${SUBJECT_MIN_COMPACT_TIGHT}px, 1fr) - minmax(0, var(--col-changes-w, 84px)); + minmax(0, var(--col-changes-w, ${COMPACT_DEF.changes}px)) + 0px + 0px + minmax(0, var(--col-sha-w, ${COMPACT_DEF.sha}px)); } :host([compact]) .colhead .ch-author, :host([compact]) .row .author { display: none; } - :host([compact]) .col-resize[data-col="changes"][data-invert="0"] { display: none; } + :host([compact].hide-sha) .col-resize[data-col="changes"][data-invert="0"] { display: none; } + /* Author's grip went with its cell; with SHA on, the Changes | SHA + boundary keeps Changes' own. */ } /* ── Sidebar (inline) mode ────────────────────────────────────────────── @@ -1006,7 +1055,8 @@ export class CommitGraph extends LitElement { the two tracks that still exist and says what they are. */ :host(:not([compact])) .colhead .ch-refs, :host(:not([compact])) .colhead .ch-changes, - :host(:not([compact])) .colhead .ch-author { display: none; } + :host(:not([compact])) .colhead .ch-author, + :host(:not([compact])) .colhead .ch-sha { display: none; } :host(:not([compact])) .col-resize[data-col="refs"], :host(:not([compact])) .col-resize[data-col="changes"], :host(:not([compact])) .col-resize[data-col="author"] { display: none; } @@ -1027,13 +1077,14 @@ export class CommitGraph extends LitElement { } /* Compact's own floor: below this even three columns are too many, so it becomes the same clean one-line list the sidebar uses. */ - @container (max-width: 330px) { + @container (max-width: ${COMPACT_LIST_AT}px) { :host([compact]) .colhead { display: none; } :host([compact]) .col-resize { display: none; } :host([compact]) .colhead, :host([compact]) .row { --gs-grid: var(--gs-gutter-w, ${MIN_GUTTER_WIDTH}px) minmax(0, 1fr); } - :host([compact]) .row .changes { display: none; } + :host([compact]) .row .changes, + :host([compact]) .row .sha { display: none; } } /* ── Column header row (aligned to the row grid) ──────────────────── */ @@ -1151,7 +1202,7 @@ export class CommitGraph extends LitElement { Columns menu still showed SHA as checked, which is a menu lying about what is on screen. With an explicit grid-column, removing a cell moves nothing. Compact - mode lays .content out as flex and is unaffected. */ + lays .content out as flex, but its tracks are pinned too (below). */ :host(:not([compact])) .row > .gutter, :host(:not([compact])) .colhead .ch-graph { grid-column: 1; } :host(:not([compact])) .row .refs, @@ -1166,6 +1217,21 @@ export class CommitGraph extends LitElement { :host(:not([compact])) .colhead .ch-date { grid-column: 6; } :host(:not([compact])) .row .meta.sha, :host(:not([compact])) .colhead .ch-sha { grid-column: 7; } + /* Compact pins its own six tracks the same way. It did not need to while + SHA had no track there; now a user who turns Date off (issue #124's + reporter had) would see SHA slide into Date's 0px track and vanish. */ + :host([compact]) .row > .gutter, + :host([compact]) .colhead .ch-graph { grid-column: 1; } + :host([compact]) .row > .content, + :host([compact]) .colhead .ch-subject { grid-column: 2; } + :host([compact]) .row .changes, + :host([compact]) .colhead .ch-changes { grid-column: 3; } + :host([compact]) .row .meta.author, + :host([compact]) .colhead .ch-author { grid-column: 4; } + :host([compact]) .row .meta.date, + :host([compact]) .colhead .ch-date { grid-column: 5; } + :host([compact]) .row .meta.sha, + :host([compact]) .colhead .ch-sha { grid-column: 6; } /* ── Hidden columns: hide the cells/header (the track is collapsed to 0 on the inline :host style by applyColumnStyles, which outranks any saved @@ -1937,11 +2003,19 @@ export class CommitGraph extends LitElement { } | undefined; - /** Memoised auto-fit for the refs track, keyed on row count + host width. */ - private autoRefs = { n: -1, host: -1, w: 0 }; + /** Memoised auto-fit for the refs track, keyed on row count + host width + + * what the other tracks take (a column turned on or off moves the budget). */ + private autoRefs = { n: -1, host: -1, non: -1, w: 0 }; private resizeObs: ResizeObserver | undefined; /** Hidden column ids; persisted to localStorage. */ private hiddenCols = new Set(); + /** + * Columns the user has ON that this width has no room for (issue #124) — + * never persisted, recomputed on every update and resize. See fitTail. + */ + private fitHidden = new Set(); + /** Memo for fitTail: its inputs, joined, and the answer they gave. */ + private fitMemo = { key: "", out: new Set() }; /** Live column-drag bookkeeping (null when not dragging). */ private drag: { id: ColumnSpec["id"]; @@ -2059,11 +2133,15 @@ export class CommitGraph extends LitElement { // responsive behaviour never call back into JS. Without this the track // keeps whatever width the last Lit update computed, so a plain window // resize leaves it stale until something else happens to re-render. + // + // Date and SHA are fitted to the width as well (fitTail), so this runs for + // a user-set refs width too. And the Columns menu says which ticked + // columns have no room, which a resize changes while it is open. this.resizeObs = new ResizeObserver(() => { - if (this.colWidths.refs !== undefined) return; // user-set: nothing to fit - const before = this.autoRefs.w; + const before = `${this.autoRefs.w}|${[...this.fitHidden].join()}`; this.applyColumnStyles(); - if (this.autoRefs.w !== before) this.renderRows(); // re-fold the chips + if (`${this.autoRefs.w}|${[...this.fitHidden].join()}` !== before) this.renderRows(); // re-fold the chips + if (this.columnsOpen) this.requestUpdate(); }); this.resizeObs.observe(this); // A click in another editor, the Explorer or the terminal never reaches @@ -2423,26 +2501,27 @@ export class CommitGraph extends LitElement { } /** - * Which column tracks are actually on screen — the user's hidden set AND the + * Which column tracks are actually on screen — the user's hidden set, the * responsive ladder, which drops trailing columns via container queries that - * never call back into JS. + * never call back into JS, and (unless `fitted` is false) the columns + * fitTail found no room for. */ - private visibleTrackIds(): ColumnSpec["id"][] { + private visibleTrackIds(fitted = true): ColumnSpec["id"][] { const host = this.clientWidth; const compact = this.hasAttribute("compact"); const out: ColumnSpec["id"][] = []; for (const spec of COLUMN_SPECS) { if (this.hiddenCols.has(spec.id)) continue; + if (fitted && this.fitHidden.has(spec.id)) continue; if (compact) { - // Compact has no Branch/Tag track (chips flow inline) and no SHA track. - if (spec.id === "refs" || spec.id === "sha") continue; + // Compact has no Branch/Tag track: its chips flow inline. + if (spec.id === "refs") continue; if (host > 0 && host <= COMPACT_DROP_DATE_AT && spec.id === "date") continue; if (host > 0 && host <= COMPACT_DROP_AUTHOR_AT && spec.id === "author") continue; - } else if ( - host > 0 && - host <= COLUMN_DROP_TAIL_AT && - (spec.id === "date" || spec.id === "sha") - ) { + if (host > 0 && host <= COMPACT_LIST_AT && spec.id === "sha") continue; + } else if (host > 0 && host <= COLUMN_DROP_TAIL_AT - 200 && spec.id === "date") { + continue; + } else if (host > 0 && host <= INLINE_LIST_BELOW && spec.id === "sha") { continue; } out.push(spec.id); @@ -2450,6 +2529,127 @@ export class CommitGraph extends LitElement { return out; } + /** + * Is a column the user has ON missing from the screen for want of room? + * What the Columns menu says beside it (issue #124), so it mirrors every + * rung of the CSS ladder — including Changes and Author, which the narrow + * table tier and compact's floor drop — and not only the tracks that + * visibleTrackIds budgets for. Branch/Tag never is: where it is not a track + * its chips flow inline before the message. + */ + private noRoomFor(id: ColumnSpec["id"]): boolean { + if (id === "refs" || id === "graph" || this.hiddenCols.has(id)) return false; + return this.fitHidden.has(id) || this.ladderDrops(id); + } + + /** Does the CSS ladder (the container queries) drop this column here? */ + private ladderDrops(id: ColumnSpec["id"]): boolean { + const host = this.clientWidth; + if (host <= 0 || id === "graph" || id === "refs") return false; + if (this.hasAttribute("compact")) { + if (host <= COMPACT_LIST_AT) return true; + if (host <= COMPACT_DROP_AUTHOR_AT) return id === "author" || id === "date"; + if (host <= COMPACT_DROP_DATE_AT) return id === "date"; + return false; + } + if (host <= INLINE_LIST_BELOW) return true; + if (host <= COLUMN_DROP_TAIL_AT - 200) return id !== "sha"; + return false; + } + + /** + * Which of Date and SHA — those the user has on and the ladder still lays + * out — this width has no room for. The rule is in TAIL_FIT_ORDER's comment: + * every other track at its own width, the message at its floor and the + * Branch/Tag track at a readable width come first; SHA, then Date, take + * what is left, and whatever does not fit is hidden and named in the + * Columns menu. + * + * Memoised: it runs on every Lit update, and the refs reserve is O(rows). + */ + private fitTail(): Set { + const host = this.clientWidth; + const compact = this.hasAttribute("compact"); + const tracks = host > 0 ? this.visibleTrackIds(false) : []; + const key = [ + host, + compact ? 1 : 0, + this.rows.length, + tracks.join(), + JSON.stringify(this.colWidths), + this.colWidths.graph ?? this.gutterWidth(), + ].join("|"); + if (key === this.fitMemo.key) return this.fitMemo.out; + + const out = new Set(); + const order = compact ? TAIL_FIT_ORDER_COMPACT : TAIL_FIT_ORDER; + const tail = order.filter((id) => tracks.includes(id)); + if (tail.length > 0) { + const tight = !compact && host <= COLUMN_DROP_TAIL_AT - 200; + let room = + host - (compact ? ROW_CHROME_COMPACT : ROW_CHROME) - (tight ? SUBJECT_MIN_TIGHT : this.subjectFloor()); + for (const id of tracks) { + // visibleTrackIds budgets the refs track and keeps Changes and Author + // in the narrow tier; the CSS has them at 0px there, so they cost + // nothing here. + if (tail.includes(id) || this.ladderDrops(id)) continue; + room -= this.fitNeed(id, compact); + } + for (const id of tail) { + const need = this.fitNeed(id, compact); + if (need <= room) room -= need; + else out.add(id); + } + } + this.fitMemo = { key, out }; + return out; + } + + /** + * The width fitTail charges a track. In compact, Changes and Author may + * narrow to their own minimum to make room: they are minmax(0, …) tracks, + * and the grid takes a deficit from the widest track first, so SHA keeps its + * width and the author's name ellipsizes. Not in the editor tab, where the + * Branch/Tag auto-fit budgets against their FULL widths and would be the + * track starved instead. + */ + private fitNeed(id: ColumnSpec["id"], compact: boolean): number { + const need = this.trackNeed(id); + return compact && (id === "changes" || id === "author") + ? Math.min(need, COLUMN_BY_ID.get(id)!.min) + : need; + } + + /** The width a track takes when it is on screen, for fitTail. */ + private trackNeed(id: ColumnSpec["id"]): number { + if (id === "graph") return this.colWidths.graph ?? this.gutterWidth(); + if (id === "refs") return this.refsReserve(); + const own = this.colWidths[id]; + if (own !== undefined) return own; + const compactDef = (COMPACT_DEF as Partial>)[id]; + return (this.hasAttribute("compact") ? compactDef : undefined) ?? COLUMN_BY_ID.get(id)!.def; + } + + /** + * What the Branch/Tag track must keep before Date and SHA get any room: the + * user's own width, or the auto-fit's floor — the width at which the + * busiest ref name is still readable (fitRefsWidth's `legible`), never more + * than the refs want. Capped as the CSS caps the track. + */ + private refsReserve(): number { + const spec = COLUMN_BY_ID.get("refs")!; + const host = this.clientWidth; + // The narrow table tier clamps the track to [REFS_MIN_READABLE, 240]. + const tight = host <= COLUMN_DROP_TAIL_AT - 200; + const cap = tight ? REFS_CAP_TIGHT : host <= COLUMN_DROP_TAIL_AT ? REFS_CAP_NARROW : spec.max; + const floor = tight ? REFS_MIN_READABLE : 0; + const own = this.colWidths.refs; + if (own !== undefined) return Math.max(floor, Math.min(cap, own)); + const wanted = wantedRefsWidth(this.rows, spec.max, spec.min); + const auto = Math.max(spec.min, Math.min(wanted, legibleRefsWidth(this.rows))); + return Math.max(floor, Math.min(cap, auto)); + } + /** The floor the message track is entitled to at this width — matches the CSS. */ private subjectFloor(): number { const host = this.clientWidth; @@ -2465,10 +2665,14 @@ export class CommitGraph extends LitElement { * outranks the `:host(.hide-*)` stylesheet rule, so we must zero it here or a * previously-resized-then-hidden column would leave a ghost gap. */ private applyColumnStyles(): void { + // First: the refs auto-fit below budgets against the tracks on screen. + this.fitHidden = this.fitTail(); for (const spec of COLUMN_SPECS) { // The graph track is owned by applyGutterWidth (auto-size + override). if (spec.id === "graph") continue; - const hidden = this.hiddenCols.has(spec.id); + // A column with no room collapses exactly as an unticked one does — + // track at 0, cells and grip gone — but stays ticked in the menu. + const hidden = this.hiddenCols.has(spec.id) || this.fitHidden.has(spec.id); // refs falls back to its auto-fit rather than to the CSS literal, so the // track, the chip cap and the fold all read the same number. const w = @@ -2672,14 +2876,19 @@ export class CommitGraph extends LitElement { // (row count, host width): the ref set cannot change without the rows // changing, and the budget cannot change without a resize. const host = this.clientWidth; - if (this.autoRefs.n === this.rows.length && this.autoRefs.host === host) { + const nonRefs = this.nonRefsWidth(); + if ( + this.autoRefs.n === this.rows.length && + this.autoRefs.host === host && + this.autoRefs.non === nonRefs + ) { return this.autoRefs.w; } const fitted = fitRefsWidth({ wanted: wantedRefsWidth(this.rows, spec.max, spec.min), host, - nonRefs: this.nonRefsWidth(), + nonRefs, // A flat 420px comfort floor is most of a narrow host, which starved the // refs track to its minimum on any window under ~1000px. Scale it: the // message still leads, but it stops claiming the entire surface. @@ -2693,7 +2902,7 @@ export class CommitGraph extends LitElement { subjectFloor: this.subjectFloor(), legible: legibleRefsWidth(this.rows), }); - this.autoRefs = { n: this.rows.length, host, w: fitted }; + this.autoRefs = { n: this.rows.length, host, non: nonRefs, w: fitted }; return fitted; } @@ -4591,6 +4800,13 @@ export class CommitGraph extends LitElement { /** The "Columns" button + show/hide popover. */ private columnsControlHtml() { const hiddenCount = this.hiddenCols.size; + // Ticked columns this width has no room for (issue #124): the menu used to + // count only the unticked ones, so "1 hidden" meant Date while SHA was + // ticked and nowhere to be seen. Each is named, beside its row and below. + const noRoom = COLUMN_SPECS.filter((s) => s.hideable !== false && this.noRoomFor(s.id)); + const noRoomHint = noRoom.length + ? l10n.t("{0} hidden — panel too narrow", noRoom.map((s) => s.label).join(", ")) + : ""; return html` `; })}
@@ -4636,9 +4857,11 @@ export class CommitGraph extends LitElement {
- ${hiddenCount === 0 - ? l10n.t("Graph & message are always shown") - : l10n.t("{0} hidden · drag header edges to resize", hiddenCount)} + ${noRoomHint + ? noRoomHint + : hiddenCount === 0 + ? l10n.t("Graph & message are always shown") + : l10n.t("{0} hidden · drag header edges to resize", hiddenCount)}
` : nothing} diff --git a/packages/webview-ui/src/graph/refLayout.ts b/packages/webview-ui/src/graph/refLayout.ts index fa2609d6..15ebe532 100644 --- a/packages/webview-ui/src/graph/refLayout.ts +++ b/packages/webview-ui/src/graph/refLayout.ts @@ -167,6 +167,14 @@ const LEGIBLE_CHIP_CAP = 150; * a chip is 16px of icon + 14px of gap + the name + an optional 14px remote * tail, and the track adds REFS_PADDING around it. A floor short of that draws * the chip's furniture and none of its name. + * + * Plus one REF_CHIP_GAP of slack, the same trailing gap `wanted` carries for + * every chip. The estimate is an estimate: the current HEAD's chip is set at + * weight 650 against the 550 it is calibrated for, and "main" measured 57px + * against 55 estimated — so a track at the bare floor read "m…". It used to + * land there only in an 8px band above 860px; once Date and SHA stayed on + * screen below that (issue #124), it was every width down to 660. With the + * slack a one-chip row's floor equals its `wanted`, which never cut it. */ export function legibleRefsWidth(rows: readonly { refs?: WireRef[] }[]): number { let widest = 0; @@ -176,7 +184,7 @@ export function legibleRefsWidth(rows: readonly { refs?: WireRef[] }[]): number widest = Math.max(widest, estimateChipWidth(entry, LEGIBLE_CHIP_CAP)); } } - return widest === 0 ? 0 : REFS_PADDING + widest; + return widest === 0 ? 0 : REFS_PADDING + widest + REF_CHIP_GAP; } export function fitRefsWidth(opts: { diff --git a/packages/webview-ui/test/graphColumnFit.test.ts b/packages/webview-ui/test/graphColumnFit.test.ts new file mode 100644 index 00000000..7e08cf67 --- /dev/null +++ b/packages/webview-ui/test/graphColumnFit.test.ts @@ -0,0 +1,235 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { fileURLToPath } from "node:url"; +import { findChrome, runInChrome } from "./headless"; + +/** + * A column the user has on is on screen, or the Columns menu says why not + * (issue #124). + * + * The report: the Columns menu showed SHA ticked and the graph had no SHA + * column. Two things did that. The bottom panel's compact layout had no SHA + * track at all, whatever the menu said; and the editor tab dropped Date and + * SHA below an 860px host whether or not they would have fitted — a + * maximised laptop window with the details pane open is under that. The menu + * counted only the columns the user had unticked, so it said "1 hidden" about + * Date and nothing about SHA. + * + * Driven through the real webview entry in headless Chrome: what is on screen + * is grid tracks, container queries and shadow DOM. Widths are changed in the + * page and the graph is asked to update, because a ResizeObserver never runs + * under the headless shell's virtual time; the observer calls the same + * applyColumnStyles an update does. + */ +const ENTRY = fileURLToPath(new URL("../src/graph/main.ts", import.meta.url)); +const CHROME = findChrome(); +const skip = !CHROME && "no Chrome on this machine"; + +const prelude = (hidden: string[] = []) => ` + window.acquireVsCodeApi = () => ({ postMessage: () => {}, getState: () => undefined, setState: () => {} }); + try { + localStorage.removeItem("gitstudio.graph.cols.widths"); + localStorage.setItem("gitstudio.graph.cols.hidden", ${JSON.stringify(JSON.stringify(hidden))}); + } catch {} +`; + +const MOUNT = ` + const sha = (i) => (i + 1).toString(16).padStart(4, "0").repeat(10); + const row = (i, refs) => ({ + sha: sha(i), shortSha: sha(i).slice(0, 7), column: 0, color: 0, isMerge: false, + segments: [{ fromColumn: 0, toColumn: 0, color: 0 }], + subject: "fix(shots): site shots splice page code through jsLiteral " + i, + author: "Anton Arnaudov", authorEmail: "a@example.com", + authorDate: 1700000000 - i * 3600, refs: refs || [], + }); + const rows = [row(0, [{ name: "main", fullName: "refs/heads/main", kind: "currentHead" }])]; + for (let i = 1; i < 30; i++) rows.push(row(i)); + const root = document.getElementById("root"); + const graph = () => document.querySelector("gitstudio-graph"); + const sr = () => graph().shadowRoot; + // A poll for a condition, not a fixed wait: it returns as soon as it holds. + const until = async (probe, ms = 4000) => { + const t0 = Date.now(); + while (!probe() && Date.now() - t0 < ms) await new Promise((r) => setTimeout(r, 10)); + return probe(); + }; + window.postMessage({ type: "graphInit", rows, head: sha(0), totalColumns: 1, hasMore: false, refFilter: null }, "*"); + await until(() => graph() && sr() && sr().querySelector('.row[data-sha="' + sha(0) + '"]')); + const settle = async () => { + window.dispatchEvent(new Event("resize")); + graph().requestUpdate(); + await graph().updateComplete; + // A second pass: the first may have moved the fit, which re-renders. + graph().requestUpdate(); + await graph().updateComplete; + }; + const resize = async (w) => { root.style.width = w + "px"; await settle(); }; + const CELL = { refs: ".refs", changes: ".changes", author: ".meta.author", date: ".meta.date", sha: ".meta.sha" }; + const HEAD = { refs: ".ch-refs", changes: ".ch-changes", author: ".ch-author", date: ".ch-date", sha: ".ch-sha" }; + const firstRow = () => sr().querySelector('.row[data-sha="' + sha(0) + '"]'); + const box = (el) => { + if (!el) return null; + const cs = getComputedStyle(el); + if (cs.display === "none" || cs.visibility === "hidden") return null; + const r = el.getBoundingClientRect(); + return r.width >= 1 ? r : null; + }; + /** Is this column's cell on screen in the first row? */ + const cellBox = (id) => box(firstRow().querySelector(CELL[id])); + /** The sha text's own extent, so a cell that clips its text is caught. */ + const shaTextFits = () => { + const cell = firstRow().querySelector(CELL.sha); + const r = box(cell); + if (!r) return false; + const text = [...cell.childNodes].find((n) => n.nodeType === 3); + const range = document.createRange(); + range.selectNodeContents(text); + const t = range.getBoundingClientRect(); + return t.width > 20 && t.right <= r.right + 0.5 && t.left >= r.left - 0.5; + }; + const openMenu = async () => { + if (!sr().querySelector(".gh-pop[role=menu]")) sr().querySelector(".gh-columns-btn").click(); + await graph().updateComplete; + return sr().querySelector(".gh-pop[role=menu]"); + }; + /** What the Columns menu claims about each column. */ + const menuState = async () => { + const pop = await openMenu(); + const items = [...pop.querySelectorAll("[role=menuitemcheckbox]")]; + return { + items: items.map((it) => ({ + label: it.querySelector(".lbl").textContent.trim(), + on: it.getAttribute("aria-checked") === "true", + squeezed: !!it.querySelector(".gh-menu-note"), + })), + hint: (pop.querySelector(".gh-pop-hint")?.textContent || "").replace(/\\s+/g, " ").trim(), + }; + }; + /** The current branch's chip shows its whole name, not "m…". */ + const chipWhole = () => { + const nm = firstRow().querySelector(".chip .nm"); + return !!nm && nm.scrollWidth <= nm.clientWidth; + }; + const LABEL = { refs: "Branch / Tag", changes: "Changes", author: "Author", date: "Date", sha: "SHA" }; +`; + +const run = (script: string, o: { layout: "side" | "dock"; width: number; hidden?: string[] }) => + runInChrome(CHROME!, ENTRY, MOUNT + script, { + css: `#root{height:640px;width:${o.width}px}`, + prelude: prelude(o.hidden), + rootAttrs: o.layout === "side" ? 'data-layout="side"' : "", + width: 1600, + height: 720, + }); + +test("the bottom panel shows an SHA column the user left on — with Date off, as reported", { skip }, async () => { + const v = await run(` + for (const w of [1170, 900, 700]) { + await resize(w); + const shaBox = cellBox("sha"); + expect(!!shaBox, w + "px: the SHA cell is on screen"); + if (!shaBox) continue; + expect(shaTextFits(), w + "px: the whole short sha shows, not clipped"); + const author = cellBox("author"); + expect(!author || shaBox.left >= author.right - 1, w + "px: SHA sits in its own track, right of Author (" + (author && author.right) + " / " + shaBox.left + ")"); + const head = box(sr().querySelector(HEAD.sha)); + expect(!!head && Math.abs(head.left - shaBox.left) <= 2, w + "px: the SHA header sits over the SHA cells (" + (head && head.left) + " / " + shaBox.left + ")"); + expect(!cellBox("date"), w + "px: Date stays off, as the user set it"); + } + `, { layout: "side", width: 1170, hidden: ["date"] }); + assert.deepEqual(v.fails, [], v.fails.join("\n")); +}); + +test("the bottom panel keeps both Date and SHA when both are on and there is room", { skip }, async () => { + const v = await run(` + await resize(1170); + const dateBox = cellBox("date"), shaBox = cellBox("sha"); + expect(!!dateBox && !!shaBox, "both on screen (date " + !!dateBox + ", sha " + !!shaBox + ")"); + expect(!!dateBox && !!shaBox && shaBox.left >= dateBox.right - 1, "SHA follows Date"); + expect(shaTextFits(), "the whole short sha shows"); + `, { layout: "side", width: 1170 }); + assert.deepEqual(v.fails, [], v.fails.join("\n")); +}); + +test("the editor tab keeps Date and SHA under 860px while they fit, and gives up Date before SHA", { skip }, async () => { + const v = await run(` + for (const w of [840, 780]) { + await resize(w); + expect(!!cellBox("sha") && !!cellBox("date"), w + "px: Date and SHA on screen (date " + !!cellBox("date") + ", sha " + !!cellBox("sha") + ")"); + const subject = box(firstRow().querySelector(".subject")); + expect(!!subject && subject.width >= 200, w + "px: the message keeps its floor (" + (subject && subject.width) + ")"); + expect(chipWhole(), w + "px: the branch chip still reads main, whole"); + const last = cellBox("sha"); + expect(!!last && last.right <= firstRow().getBoundingClientRect().right + 0.5, w + "px: nothing runs off the row"); + } + // Narrower: there is room for one of them, and it is SHA. + await resize(690); + expect(!!cellBox("sha"), "690px: SHA on screen"); + expect(shaTextFits(), "690px: the whole short sha shows"); + expect(!cellBox("date"), "690px: Date has given way"); + const m = await menuState(); + const date = m.items.find((i) => i.label === "Date"); + expect(!!date && date.on && date.squeezed, "690px: the menu keeps Date ticked and says it has no room (" + JSON.stringify(date) + ")"); + expect(/Date/.test(m.hint), "690px: the menu's footer names Date (" + m.hint + ")"); + `, { layout: "dock", width: 840 }); + assert.deepEqual(v.fails, [], v.fails.join("\n")); +}); + +/** + * The state table: every width, both layouts, with and without a column the + * user unticked. A ticked column is on screen, or its menu row says it has no + * room — never both, never neither. An unticked one is neither. And the + * footer names every column that has no room. + */ +for (const layout of ["dock", "side"] as const) { + for (const hidden of [[], ["date"]]) { + test(`the Columns menu never claims a column that is not on screen — ${layout}, hidden ${JSON.stringify(hidden)}`, { skip }, async () => { + const v = await run(` + const seen = []; + // Where each column went while the pane narrowed: a column that has + // gone must not come back at a NARROWER width — one that blinks out + // and in as you drag reads as a bug, whatever the reason. + const goneAt = {}; + for (let w = 1400; w >= 300; w -= 10) { + await resize(w); + const host = graph().clientWidth; + const m = await menuState(); + const squeezedLabels = []; + for (const [id, label] of Object.entries(LABEL)) { + const item = m.items.find((i) => i.label === label); + if (!item) { expect(false, host + "px: no menu row for " + label); continue; } + const shown = !!cellBox(id); + // Where both are drawn, the header sits over its own cells. + const hb = box(sr().querySelector(".colhead " + HEAD[id])), cb = cellBox(id); + if (hb && cb && id !== "refs") expect(Math.abs(hb.left - cb.left) <= 2, host + "px: the " + label + " header sits over its cells (" + Math.round(hb.left) + " / " + Math.round(cb.left) + ")"); + if (!shown && item.on && goneAt[id] === undefined) goneAt[id] = host; + if (shown && goneAt[id] !== undefined) expect(false, label + " went at " + goneAt[id] + "px and came back at " + host + "px"); + if (!item.on) { + expect(!shown, host + "px: " + label + " is unticked yet on screen"); + expect(!item.squeezed, host + "px: " + label + " is unticked, so it is not 'no room'"); + } else { + expect(shown !== item.squeezed, host + "px: " + label + " ticked, on screen " + shown + ", menu says no room " + item.squeezed); + if (item.squeezed) squeezedLabels.push(label); + } + } + for (const label of squeezedLabels) { + expect(m.hint.includes(label), host + "px: the footer names " + label + " (" + m.hint + ")"); + } + seen.push(host + ":" + squeezedLabels.join("+")); + } + notes.seen = seen; + notes.goneAt = goneAt; + // SHA is the narrowest column and the one that names a commit: it + // stays wherever the layout is still a table of columns. + const floor = ${layout === "dock" ? 470 : 350}; + for (let w = 1400; w >= floor; w -= 10) { + await resize(w); + expect(!!cellBox("sha"), graph().clientWidth + "px: SHA on screen"); + // ...and not at the cost of the branch name it sits beside. + expect(chipWhole(), graph().clientWidth + "px: the branch chip reads main, whole"); + } + `, { layout, width: 1400, hidden }); + assert.deepEqual(v.fails, [], v.fails.join("\n")); + }); + } +} diff --git a/packages/webview-ui/test/refFilter.cov.test.ts b/packages/webview-ui/test/refFilter.cov.test.ts index 6d48d29c..bf400c5f 100644 --- a/packages/webview-ui/test/refFilter.cov.test.ts +++ b/packages/webview-ui/test/refFilter.cov.test.ts @@ -2,7 +2,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import type { GraphRefEntry, WireRef, WireRow } from "@gitstudio/host-bridge/graphProtocol"; import { presetUnavailable, refFilterHint } from "../src/graph/refFilter"; -import { REFS_PADDING, estimateChipWidth, foldRefs, legibleRefsWidth } from "../src/graph/refLayout"; +import { REFS_PADDING, REF_CHIP_GAP, estimateChipWidth, foldRefs, legibleRefsWidth } from "../src/graph/refLayout"; import { rowMatches } from "../src/graph/search"; // The Branches picker's footnote and excuses, the refs column's legibility @@ -49,13 +49,14 @@ test("the refs column's floor is one readable chip of the busiest row, or nothin ]; const widest = foldRefs(rows[1].refs).map((e) => estimateChipWidth(e, 150)); assert.equal(widest.length, 1, "the remote twin folds into its local chip"); - assert.equal(legibleRefsWidth(rows), REFS_PADDING + widest[0]); + // The chip, the track's padding, and one chip gap of slack for the estimate. + assert.equal(legibleRefsWidth(rows), REFS_PADDING + widest[0] + REF_CHIP_GAP); assert.ok(legibleRefsWidth(rows) > legibleRefsWidth([rows[0]]), "the busier chip sets the floor"); }); test("a very long branch name cannot make the floor demand half the window", () => { const long = [{ refs: [ref(`refs/heads/${"x".repeat(200)}`, "head")] }]; - assert.equal(legibleRefsWidth(long), REFS_PADDING + 150); + assert.equal(legibleRefsWidth(long), REFS_PADDING + 150 + REF_CHIP_GAP); }); test("the 'refs' search scope matches a ref's name and nothing else", () => {