From 4fb257e1079bac52e83fefc1271e995612d17f3d Mon Sep 17 00:00:00 2001 From: Sebastian Schneider Date: Thu, 12 Feb 2026 12:27:06 +0000 Subject: [PATCH] compact mode --- package.json | 12 ++- src/config.ts | 4 + src/graphWebview.ts | 5 +- src/webview/graph.css | 105 +++++++++++++++++++- src/webview/graph.html | 217 +++++++++++++++++++++++------------------ 5 files changed, 241 insertions(+), 102 deletions(-) diff --git a/package.json b/package.json index 473a44a..b6b9930 100644 --- a/package.json +++ b/package.json @@ -504,6 +504,16 @@ "description": "Path to the jj executable. If not set, your PATH and common locations will be searched for a jj executable.", "scope": "resource" }, + "ukemi.graph.viewLayout": { + "type": "string", + "enum": [ + "floating", + "compact" + ], + "default": "floating", + "description": "Layout style for the graph rows. 'floating' allows wrapping, 'compact' forces a single line.", + "scope": "resource" + }, "ukemi.graph.showCommitId": { "type": "boolean", "default": true, @@ -513,7 +523,7 @@ "ukemi.graph.showAuthor": { "type": "boolean", "default": true, - "markdownDescription": "Whether to show the author in the graph.", + "markdownDescription": "Whether to show the author in the graph (Floating view only).", "scope": "resource" }, "ukemi.graph.showBookmarks": { diff --git a/src/config.ts b/src/config.ts index 210f8bf..20401c7 100644 --- a/src/config.ts +++ b/src/config.ts @@ -12,6 +12,8 @@ export interface GraphConfig { useConfigLogRevset: boolean; revset: string; limit: number; + + viewLayout: "floating" | "compact"; } /** Extension configuration. */ @@ -32,6 +34,8 @@ export function getGraphConfig(scope?: vscode.Uri): GraphConfig { useConfigLogRevset: config.get("useConfigLogRevset", false), revset: config.get("revset", "::"), limit: config.get("limit", 50), + + viewLayout: config.get<"floating" | "compact">("viewLayout", "floating"), }; } diff --git a/src/graphWebview.ts b/src/graphWebview.ts index 63d8166..d25f69d 100644 --- a/src/graphWebview.ts +++ b/src/graphWebview.ts @@ -167,6 +167,7 @@ export class JJGraphWebview implements vscode.WebviewViewProvider { showBookmarks, showCommitId, showTimestamp, + viewLayout, } = getGraphConfig(); // Collect all changes in a single pass (graph structure + data) @@ -187,11 +188,11 @@ export class JJGraphWebview implements vscode.WebviewViewProvider { command: "updateGraph", changes: changes, workingCopyId, - preserveScroll: true, showAuthor, showBookmarks, showCommitId, showTimestamp, + viewLayout, }); } @@ -350,3 +351,5 @@ export function parseJJLog(output: string): ChangeNode[] { } return changeNodes; } + + diff --git a/src/webview/graph.css b/src/webview/graph.css index 842d817..b72870a 100644 --- a/src/webview/graph.css +++ b/src/webview/graph.css @@ -98,9 +98,103 @@ body { justify-content: center; margin-left: var(--curve-offset, 0px); padding-left: 12px; +} + +#nodes.compact .text-content { + display: grid; + grid-template-columns: min-content 1fr; + column-gap: 0px; + align-items: center; + padding-left: 0; + min-height: 20px; +} + +#nodes.compact .change-node { + /* Tighter padding */ + padding: 2px 4px; +} + +/* Column 1: IDs */ +.id-column { + display: flex; + flex-direction: column; + /* Right align IDs */ + align-items: flex-end; + justify-content: center; + flex-shrink: 0; + padding-left: calc(var(--curve-offset, 0px) + 12px); + padding-right: 8px; + border-right: 1px solid var(--vscode-widget-border); +} + +.id-column .change-id, +.id-column .commit-id { + font-family: var(--vscode-editor-font-family); + font-size: 0.85em; + white-space: nowrap; +} + +/* Column 2: Info */ +.info-column { + display: flex; + flex-direction: column; + justify-content: center; + gap: 2px; + overflow: hidden; +} + +#nodes.compact .info-column { + flex-direction: row; + align-items: center; + gap: 8px; + white-space: nowrap; + min-width: 0; /* Crucial for flex child truncation */ +} + +#nodes.compact .info-column .commit-message { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; +} + +#nodes.compact .info-column .bookmarks { + /* Allow shrinking */ + flex-shrink: 1; + overflow: hidden; + text-overflow: ellipsis; + /* Don't take up too much space */ + max-width: 40%; + display: flex; gap: 4px; + min-width: 0; /* Crucial for flex child truncation */ +} + +#nodes.compact .info-column .bookmarks .bookmark-badge { + flex-shrink: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; +} + +#nodes.compact .info-column .timestamp { + font-size: 0.8em; + opacity: 0.7; + flex-shrink: 0; +} + +#nodes.compact .info-column .author-email { + font-size: 0.8em; + opacity: 0.7; + /* Hidden by default in compact */ + display: none; } +.change-node.selected .info-column .author-email { + display: inline; +} + + .commit-message { line-height: 1.2; font-weight: normal; @@ -141,15 +235,16 @@ body { .bookmark-badge { background-color: var(--vscode-badge-background); - color: var(--vscode-badge-foreground); - border-radius: 2px; - padding: 1px 4px; - font-size: 0.9em; - margin-left: 2px; + color: var(--vscode-button-foreground); + border-radius: 4px; + padding: 2px 4px; + font-size: 0.8em; + font-weight: bold; } .separator { opacity: 0.5; + margin: 0 4px; } /* Edit button styling */ diff --git a/src/webview/graph.html b/src/webview/graph.html index d431d1b..c4b6bf2 100644 --- a/src/webview/graph.html +++ b/src/webview/graph.html @@ -47,6 +47,7 @@ message.showBookmarks, message.showCommitId, message.showTimestamp, + message.viewLayout, ); break; } @@ -119,6 +120,36 @@ return g; } + function createIdSpan(fullId, shortId, className) { + const span = document.createElement("span"); + span.className = className; + span.textContent = shortId; + const remaining = document.createElement("span"); + remaining.className = className + '-remaining'; + remaining.textContent = fullId.substring(shortId.length, 8); + span.appendChild(remaining); + return span; + } + + function createBookmarks(bookmarks) { + const bookmarkDiv = document.createElement("div"); + bookmarkDiv.className = 'bookmarks'; + bookmarks.forEach(bookmark => { + const badge = document.createElement("span"); + badge.className = 'bookmark-badge'; + badge.textContent = bookmark; + bookmarkDiv.appendChild(badge); + }); + return bookmarkDiv; + } + + function createSeparator() { + const sep = document.createElement("span"); + sep.className = 'separator'; + sep.textContent = " • "; + return sep; + } + function updateConnections() { const nodes = document.querySelectorAll('.change-node'); @@ -138,6 +169,7 @@ nodes.forEach((node, index) => { const rawParentIds = node.dataset.parentIds; const parentIds = JSON.parse(node.dataset.parentIds || '[]'); + const nodeRect = node.getBoundingClientRect(); // Convert to SVG coordinates with vertical centering @@ -154,6 +186,7 @@ const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("class", "connection-line"); + const isLatestParent = index === 0; const isAdjacent = !Array.from(nodes).some(otherNode => { if (otherNode === node || otherNode === parentNode) return false; @@ -336,7 +369,7 @@ return [fromNode, toNode]; } - function updateGraph(changes, workingCopyId, preserveScroll = false, showAuthor = true, showBookmarks = true, showCommitId = true, showTimestamp = 'always') { + function updateGraph(changes, workingCopyId, preserveScroll = false, showAuthor = true, showBookmarks = true, showCommitId = true, showTimestamp = 'always', viewLayout = 'floating') { // Save current scroll position only when preserveScroll is true const scrollTop = preserveScroll ? (window.scrollY || document.documentElement.scrollTop) : 0; @@ -346,6 +379,9 @@ nodesContainer.innerHTML = ''; circlesContainer.innerHTML = ''; + // Set view layout class on container + nodesContainer.className = viewLayout; + // Calculate maximum curve offset needed let maxParentCount = 0; changes.forEach(change => { @@ -361,6 +397,11 @@ changes.forEach(change => { if (!change.contextValue) return; + + const shouldShowBookmarks = showBookmarks && change.bookmarks && change.bookmarks.length > 0; + const shouldShowAuthor = showAuthor && change.email; + const shouldShowTimestamp = (showTimestamp === 'always' || (showTimestamp === 'immutable_only' && change.isImmutable)) && change.timestampAgo; + const node = document.createElement('div'); node.className = 'change-node'; node.title = change.tooltip; @@ -372,95 +413,96 @@ const textContent = document.createElement('div'); textContent.className = 'text-content'; - // 1. Description (Normal Weight) - const nodeDescription = document.createElement("div"); - nodeDescription.className = 'commit-message'; - nodeDescription.textContent = change.description; - textContent.append(nodeDescription); + if (viewLayout === 'compact') { + // Column 1: IDs + const idColumn = document.createElement('div'); + idColumn.className = 'id-column'; + + const changeIdDiv = document.createElement('div'); + changeIdDiv.className = 'change-id-container'; + changeIdDiv.appendChild(createIdSpan(change.contextValue, change.shortestChangeId, 'change-id')); + idColumn.appendChild(changeIdDiv); + + if (showCommitId && change.commitId) { + const commitIdDiv = document.createElement('div'); + commitIdDiv.className = 'commit-id-container'; + commitIdDiv.appendChild(createIdSpan(change.commitId, change.shortestCommitId, 'commit-id')); + idColumn.appendChild(commitIdDiv); + } - // 2. Metadata Row (IDs + Bookmarks) - const metaInfo = document.createElement("div"); - metaInfo.className = 'meta-info'; + textContent.appendChild(idColumn); - const shouldShowCommitId = showCommitId && change.commitId; - const shouldShowBookmarks = showBookmarks && change.bookmarks && change.bookmarks.length > 0; - const shouldShowAuthor = showAuthor && change.email; - const shouldAlwaysShowTimestamp = showTimestamp === 'always' && change.timestampAgo; - const shouldShowImmutableTimestamp = showTimestamp === 'immutable_only' && change.isImmutable && change.timestampAgo; - const shouldShowTimestamp = shouldAlwaysShowTimestamp || shouldShowImmutableTimestamp; - - // Change ID - const changeIdSpan = document.createElement("span"); - changeIdSpan.className = 'change-id'; - changeIdSpan.textContent = change.shortestChangeId; - metaInfo.append(changeIdSpan); - - const changeIdRemainingSpan = document.createElement("span"); - changeIdRemainingSpan.className = 'change-id-remaining'; - changeIdRemainingSpan.textContent = change.contextValue.substring(change.shortestChangeId.length, 8); - changeIdSpan.append(changeIdRemainingSpan); - - // Commit ID - if (shouldShowCommitId) { - const sep = document.createElement("span"); - sep.className = 'separator'; - sep.textContent = " • "; - metaInfo.append(sep); - - const commitIdSpan = document.createElement("span"); - commitIdSpan.className = 'commit-id'; - commitIdSpan.textContent = change.shortestCommitId; - metaInfo.append(commitIdSpan); - - const commitIdRemainingSpan = document.createElement("span"); - commitIdRemainingSpan.className = 'commit-id-remaining'; - commitIdRemainingSpan.textContent = change.commitId.substring(change.shortestCommitId.length, 8); - commitIdSpan.append(commitIdRemainingSpan); - } + // Column 2: Info (Description + Meta) + const infoColumn = document.createElement('div'); + infoColumn.className = 'info-column'; - if (shouldShowAuthor) { - const sep = document.createElement("span"); - sep.className = 'separator'; - sep.textContent = " • "; - metaInfo.append(sep); + if (shouldShowBookmarks) { + infoColumn.appendChild(createBookmarks(change.bookmarks)); + } - const emailSpan = document.createElement("span"); - emailSpan.className = 'author-email'; - emailSpan.textContent = change.email; - metaInfo.append(emailSpan); - } + const nodeDescription = document.createElement("div"); + nodeDescription.className = 'commit-message'; + nodeDescription.textContent = change.description; + infoColumn.appendChild(nodeDescription); - if (shouldShowTimestamp) { - const sep = document.createElement("span"); - sep.className = 'separator'; - sep.textContent = " • "; - metaInfo.append(sep); + if (shouldShowTimestamp) { + const timeSpan = document.createElement("span"); + timeSpan.className = 'timestamp'; + timeSpan.textContent = change.timestampAgo; + infoColumn.appendChild(timeSpan); + } - const timeSpan = document.createElement("span"); - timeSpan.className = 'timestamp'; - timeSpan.textContent = change.timestampAgo; - metaInfo.append(timeSpan); - } + if (shouldShowAuthor) { + const emailSpan = document.createElement("span"); + emailSpan.className = 'author-email'; + emailSpan.textContent = change.email; + infoColumn.appendChild(emailSpan); + } - textContent.append(metaInfo); - - if (shouldShowBookmarks) { - const bookmarkDiv = document.createElement("div"); - bookmarkDiv.className = 'bookmarks'; - - change.bookmarks.forEach(bookmark => { - const badge = document.createElement("span"); - badge.className = 'bookmark-badge'; - badge.textContent = bookmark; - bookmarkDiv.append(badge); - }); - - textContent.append(bookmarkDiv); + textContent.appendChild(infoColumn); + } else { + // Floating Mode + const nodeDescription = document.createElement("div"); + nodeDescription.className = 'commit-message'; + nodeDescription.textContent = change.description; + textContent.appendChild(nodeDescription); + + const metaInfo = document.createElement("div"); + metaInfo.className = 'meta-info'; + + metaInfo.appendChild(createIdSpan(change.contextValue, change.shortestChangeId, 'change-id')); + + if (showCommitId && change.commitId) { + metaInfo.appendChild(createSeparator()); + metaInfo.appendChild(createIdSpan(change.commitId, change.shortestCommitId, 'commit-id')); + } + + if (shouldShowAuthor) { + metaInfo.appendChild(createSeparator()); + const emailSpan = document.createElement("span"); + emailSpan.className = 'author-email'; + emailSpan.textContent = change.email; + metaInfo.appendChild(emailSpan); + } + + if (shouldShowTimestamp) { + metaInfo.appendChild(createSeparator()); + const timeSpan = document.createElement("span"); + timeSpan.className = 'timestamp'; + timeSpan.textContent = change.timestampAgo; + metaInfo.appendChild(timeSpan); + } + + textContent.appendChild(metaInfo); + + if (shouldShowBookmarks) { + textContent.appendChild(createBookmarks(change.bookmarks)); + } } - + node.appendChild(textContent); - // Create and append edit button + // Create and append edit button (keep existing logic) const editButton = document.createElement('button'); editButton.className = 'edit-button'; editButton.innerHTML = ''; @@ -481,21 +523,6 @@ node.appendChild(editButton); nodesContainer.appendChild(node); - // Create circle and add it to SVG after node is in DOM - const circle = createCircle({ - contextValue: change.contextValue, - branchType: change.branchType - }, workingCopyId); - - // Get positions relative to SVG - const nodeRect = node.getBoundingClientRect(); - const svgRect = document.getElementById('connections').getBoundingClientRect(); - const x = nodeRect.left - svgRect.left; - const y = nodeRect.top - svgRect.top + (nodeRect.height / 2) - 6; // -6 to account for circle radius - - circle.setAttribute("transform", `translate(${x}, ${y})`); - circlesContainer.appendChild(circle); - // Add hover handlers node.addEventListener('mouseenter', () => { highlightConnectedNodes(node, true);