Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ecbc7bd
Add node completion status
DisciplinedSoftware Feb 22, 2026
2aab4c8
Toggle status with keyboard
Feb 22, 2026
4fab2c3
Improve icon placement
Mar 1, 2026
bc1205e
Fix status icon placement and completed node styling
Mar 1, 2026
c0d55d3
Enlarge root node padding to match level-1 proportions
Mar 1, 2026
c50caf3
Center status icon in left padding for root and level-1 nodes
Mar 1, 2026
79f766b
Fix strikethrough on completed nodes
Mar 1, 2026
61fddab
Show status icons immediately on diagram load
Mar 1, 2026
b8274ff
Reserve icon space on level-2+ nodes to prevent text shift
Mar 1, 2026
b3ba1c2
Fix status icons not appearing after loading a diagram
Mar 1, 2026
6113d30
Trigger autosave on node status change
Mar 1, 2026
39419e8
Remove not-started status, simplify to two-state cycle
Mar 1, 2026
b574953
Remove dead node-completed CSS class
Mar 1, 2026
5dd099c
Guard C key handler against MindElixir inline editor
Mar 1, 2026
494cb84
Add status items to right-click context menu
Mar 1, 2026
e85aad2
Close context menu after selecting a status item
Mar 1, 2026
55addab
Fix tutorial status shortcut key to lowercase c
Mar 2, 2026
b9b21cf
Pre-PR cleanup: null safety, timer dedup, fix CSS comment
Mar 2, 2026
73b7900
Remove DEV badge, add v1.20 README entry, bump version
Mar 2, 2026
8dd3e5d
Fix package-lock.json: restore original format, only bump version
Mar 2, 2026
aa9955d
Remove not-started default status from NodeData
Mar 2, 2026
7265680
Remove all not-started status references from Visual Studio extension
Mar 2, 2026
36c18c9
Fix diagram not showing: move schedule handle vars before first use
Mar 2, 2026
6c93bea
Fix syntax error: optional chaining not valid on assignment LHS
Mar 2, 2026
15d1535
Align Visual Studio extension status feature with VS Code extension
Mar 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions VS Code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ See screenshots at [CodeMindMap.com](https://codemindmap.com/)
- Fix: focus map element on node select so keyboard shortcuts work on first click.
- Feature: save diagrams as .json with pretty printing, open accepts .json and .txt.
- Feature: reopen diagram automatically after window reload.
- Node completion status: press `c` on any node to cycle through In Progress (⟳) and Completed (✓) states. Status is saved with the diagram and can also be set via right-click context menu.

- **v1.19**
- "Code node added" status bar notification.
Expand Down
4 changes: 2 additions & 2 deletions VS Code/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

246 changes: 237 additions & 9 deletions VS Code/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,54 @@ export class CodeMindMapPanel {
.hidden {
display: none !important;
}
/* Root node: match the same padding-to-font ratio as level-1 nodes.
Level-1 uses 8px/25px padding with 14px font (ratios 0.57v / 1.79h).
Root has 25px font, so target: 14px vertical / 45px horizontal. */
.map-container me-root me-tpc {
padding: 14px 45px !important;
}

/* Status indicator styles */
/* Root (45px horizontal padding) and level-1 (25px padding) already have enough room for the icon */
.map-container me-tpc {
position: relative;
}
/* Level-2+ nodes: always reserve icon space so text never shifts when status changes.
mind.linkDiv() is called after applying statuses to redraw lines at the new sizes. */
.map-container me-children me-parent me-tpc {
padding-left: 20px;
box-sizing: border-box;
}
.map-container me-tpc[data-status="completed"] {
text-decoration: line-through;
}
.map-container me-tpc[data-status="completed"] > .text {
text-decoration: line-through !important;
}
.map-container me-tpc[data-status]::before {
position: absolute;
left: 2px; /* level-2+: within the 20px padding we add */
top: 50%;
transform: translateY(-50%);
width: 14px;
text-align: center;
font-weight: bold;
}
/* Center icon in the existing left padding for root (45px) and level-1 (25px) */
.map-container me-root me-tpc[data-status]::before {
left: 16px; /* (45px - 14px) / 2 */
}
.map-container me-main > me-wrapper > me-parent > me-tpc[data-status]::before {
left: 6px; /* (25px - 14px) / 2 */
}
.map-container me-tpc[data-status="in-progress"]::before {
content: '⟳';
color: #ff9800;
}
.map-container me-tpc[data-status="completed"]::before {
content: '✓';
color: #4caf50;
}
</style>
</head>
<body>
Expand Down Expand Up @@ -791,13 +839,62 @@ export class CodeMindMapPanel {
const vscode = acquireVsCodeApi();

let mind, data, themeManager, lastSelectedNode;
let pendingImportData = null; // stores importMindMapData payload received before mind is ready
let pendingImport = null; // stores importMindMapData payload received before mind is ready
let linkDivDebounceTimer = null; // debounce timer for the linkDiv bus event
let scheduleRafHandle = null;
let scheduleTimerHandle = null;

function initMindMap() {
const options = {
el: '#map',
allowUndo: true,
toolBar: true,
contextMenu: {
extend: [
{
name: '⟳ In Progress',
onclick: () => {
const node = mind.currentNode?.nodeObj;
if (!node) return;
node.data = node.data || {};
node.data.status = 'in-progress';
updateNodeStatus(node);
vscode.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' });
const cm = document.querySelector('.map-container > .context-menu'); if (cm) cm.hidden = true;
}
},
{
name: '✓ Completed',
onclick: () => {
const node = mind.currentNode?.nodeObj;
if (!node) return;
node.data = node.data || {};
node.data.status = 'completed';
updateNodeStatus(node);
vscode.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' });
const cm = document.querySelector('.map-container > .context-menu'); if (cm) cm.hidden = true;
}
},
{
name: '✕ Clear Status',
onclick: () => {
const node = mind.currentNode?.nodeObj;
if (!node) return;
node.data = node.data || {};
delete node.data.status;
updateNodeStatus(node);
vscode.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' });
const cm = document.querySelector('.map-container > .context-menu'); if (cm) cm.hidden = true;
}
},
]
},
view: {
beforeSelect(el, node) {
mind.currentNode = node;
return true;
}
}
};

const LIGHT_THEME = {
Expand Down Expand Up @@ -949,6 +1046,28 @@ export class CodeMindMapPanel {
topic: 'Space - Expand/collapse nodes',
id: 'bd1bb2ac4bbab458',
},
{
topic: 'c - Cycle node status: (none) → In Progress → Completed → (none)',
id: 'bd1bb2ac4bbab460',
children: [
{
topic: 'In Progress nodes show ⟳ in orange',
id: 'bd1bb2ac4bbab461',
},
{
topic: 'Completed nodes show ✓ with strikethrough text',
id: 'bd1bb2ac4bbab462',
},
{
topic: 'Right-click a node to set status directly from the context menu',
id: 'bd1bb2ac4bbab463',
},
{
topic: 'Status is saved automatically with the diagram',
id: 'bd1bb2ac4bbab464',
},
],
},
],
},
],
Expand All @@ -962,11 +1081,13 @@ export class CodeMindMapPanel {
mind.init(data);

// Apply any import that arrived before mind was ready
if (pendingImportData !== null) {
window.importData(pendingImportData);
pendingImportData = null;
if (pendingImport !== null) {
window.importData(pendingImport);
pendingImport = null;
}

scheduleApplyAllStatuses();

// Intercept direction-change methods so autosave is triggered when the
// user switches between left tree, right tree, and flower (side) views.
['initLeft', 'initRight', 'initSide'].forEach(method => {
Expand All @@ -977,6 +1098,61 @@ export class CodeMindMapPanel {
};
});

// Helper function to update node visual status
function updateNodeStatus(nodeObj) {
if (!nodeObj || !nodeObj.id) return;
const nodeElement = MindElixir.E(nodeObj.id);
if (!nodeElement) return;

const status = nodeObj.data?.status || null;
const domEl = nodeElement.getEl?.() || nodeElement;
if (!domEl) return;

const topicEl = (() => {
if (domEl.tagName === 'ME-TPC') return domEl;
const byQuery = domEl.querySelector?.('me-tpc');
if (byQuery) return byQuery;
const byTag = domEl.getElementsByTagName?.('me-tpc')?.[0];
if (byTag) return byTag;
return domEl;
})();

if (!topicEl) return;

if (!status) {
topicEl.removeAttribute('data-status');
return;
}

topicEl.setAttribute('data-status', status);
}

function applyAllStatuses() {
const root = mind?.nodeData;
if (!root) return;
const stack = [root];
while (stack.length > 0) {
const node = stack.pop();
if (!node) continue;
updateNodeStatus(node);
if (Array.isArray(node.children)) {
for (const child of node.children) {
stack.push(child);
}
}
}
// linkDiv is called by MindElixir itself after layout; we must not call it here
// as that would create an infinite loop via the linkDiv bus listener
}

function scheduleApplyAllStatuses() {
if (!mind) return;
if (scheduleRafHandle !== null) cancelAnimationFrame(scheduleRafHandle);
if (scheduleTimerHandle !== null) clearTimeout(scheduleTimerHandle);
scheduleRafHandle = requestAnimationFrame(() => { applyAllStatuses(); scheduleRafHandle = null; });
scheduleTimerHandle = setTimeout(() => { applyAllStatuses(); scheduleTimerHandle = null; }, 50);
}

mind.bus.addListener('selectNodes', nodes => {
const node = nodes.at(-1);
if (!node) return;
Expand All @@ -987,18 +1163,28 @@ export class CodeMindMapPanel {
nodeTopic: node.topic,
nodeData: node.data,
});
scheduleApplyAllStatuses();
});

mind.bus.addListener('selectNewNode', () => {
// Mirror the same focus behavior for newly created nodes.
mind.map?.focus();
});

// Debounced linkDiv listener: MindElixir fires linkDiv after every layout pass
// (including multiple passes after refresh/changeTheme). Wait for 50ms of silence
// before applying statuses so we always run after the final DOM state.
mind.bus.addListener('linkDiv', () => {
clearTimeout(linkDivDebounceTimer);
linkDivDebounceTimer = setTimeout(applyAllStatuses, 50);
});

mind.bus.addListener('operation', operation => {
vscode.postMessage({
action: 'mindMapOperation',
operationName: operation.name,
});
scheduleApplyAllStatuses();
});

document.addEventListener('click', (e) => {
Expand All @@ -1013,6 +1199,22 @@ export class CodeMindMapPanel {
});
}
}
else if ((e.key === 'c' || e.key === 'C') && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) {
e.preventDefault();
const currentNode = mind.currentNode?.nodeObj;
if (!currentNode) return;

currentNode.data = currentNode.data || {};

// Cycle through status: not-started -> in-progress -> completed -> not-started
const statuses = ['not-started', 'in-progress', 'completed'];
const currentStatus = currentNode.data.status || 'not-started';
const currentIndex = statuses.indexOf(currentStatus);
const nextIndex = (currentIndex + 1) % statuses.length;
currentNode.data.status = statuses[nextIndex];

updateNodeStatus(currentNode);
}
});

document.addEventListener('keydown', function(e) {
Expand All @@ -1034,6 +1236,31 @@ export class CodeMindMapPanel {
});
}
}
else if ((e.key === 'c' || e.key === 'C') && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) {
// Skip if MindElixir's inline editor is open
if (document.getElementById('input-box')) return;
e.preventDefault();
const currentNode = mind.currentNode?.nodeObj;
if (!currentNode) return;
currentNode.data = currentNode.data || {};

// Cycle: (none) -> in-progress -> completed -> (none)
const statuses = [null, 'in-progress', 'completed'];
const currentStatus = currentNode.data.status || null;
const currentIndex = statuses.indexOf(currentStatus);
const next = statuses[(currentIndex + 1) % statuses.length];
if (next === null) {
delete currentNode.data.status;
} else {
currentNode.data.status = next;
}

// Update visual appearance
updateNodeStatus(currentNode);

// Trigger autosave
vscode.postMessage({ action: 'mindMapOperation', operationName: 'updateNodeStatus' });
}
});

}
Expand Down Expand Up @@ -1133,12 +1360,14 @@ export class CodeMindMapPanel {

mind.refresh(mindData);
mind.clearHistory();
mind.clearHistory();

const dataThemeName = getThemeName(mindData);

if (dataThemeName != '' && themeManager.contains(dataThemeName) && dataThemeName != mind.theme?.name) {
mind.changeTheme(themeManager.getTheme(dataThemeName));
}
// Statuses are applied via the debounced linkDiv bus listener
// which fires after MindElixir's layout settles.

return { success: true, error: '' };

Expand Down Expand Up @@ -1171,7 +1400,7 @@ export class CodeMindMapPanel {

};

// Add event listeners for all buttons, then initialize the mind map
// Helper function to set up UI and initialize mind map
function setupUI() {
// Open Dev Tools button
const devBtn = document.getElementById('openDevToolsBtn');
Expand Down Expand Up @@ -1232,8 +1461,7 @@ export class CodeMindMapPanel {
initMindMap();
}

// Handle both early and late execution: type="module" scripts are deferred,
// so DOMContentLoaded may have already fired by the time this runs.
// Initialize when DOM is ready - handle both early and late execution
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupUI);
} else {
Expand Down Expand Up @@ -1285,7 +1513,7 @@ export class CodeMindMapPanel {
if (mind) {
window.importData(message.data);
} else {
pendingImportData = message.data;
pendingImport = message.data; // mind not ready yet; apply after init
}
break;
case 'resetMindMap':
Expand Down
Loading