|
1580 | 1580 | action: saveSnapshot |
1581 | 1581 | }); |
1582 | 1582 |
|
| 1583 | + if (!gui.session.isEmpty()) { |
| 1584 | + // Surface the console "history" command via the snapshot menu so users |
| 1585 | + // can browse the session's command history without knowing about the |
| 1586 | + // console keyword. Hidden when there's nothing to show. |
| 1587 | + addMenuLink({ |
| 1588 | + slug: 'history', |
| 1589 | + label: 'view session history', |
| 1590 | + action: function(gui) { |
| 1591 | + gui.console.runCommand('history'); |
| 1592 | + } |
| 1593 | + }); |
| 1594 | + } |
| 1595 | + |
1583 | 1596 | // var available = await getAvailableStorage(); |
1584 | 1597 | // if (available) { |
1585 | 1598 | // El('div').addClass('save-menu-entry').text(available + ' available').appendTo(menu); |
|
1703 | 1716 | } |
1704 | 1717 | gui.model.clear(); |
1705 | 1718 | importDatasets(data.datasets, gui); |
| 1719 | + // Reinstate the session history (including its saved/unsaved boundary) that |
| 1720 | + // was in effect when the snapshot was taken. If the snapshot has no history |
| 1721 | + // field (e.g. older snapshots), this resets to a clean state. |
| 1722 | + gui.session.restoreHistorySnapshot(data.history); |
1706 | 1723 | gui.clearMode(); |
1707 | 1724 | } |
1708 | 1725 |
|
1709 | | - // Add datasets to the current project |
| 1726 | + // Import datasets from a packed .msx buffer. |
| 1727 | + // Behavior depends on whether the current session contains data: |
| 1728 | + // - empty session: full project restore -- datasets and any embedded session |
| 1729 | + // history are loaded as if continuing the original session. |
| 1730 | + // - non-empty session: merge -- datasets are added to the current project, |
| 1731 | + // but any embedded session history is discarded (the imported commands |
| 1732 | + // assume different layer indices and a different starting state, so |
| 1733 | + // merging them into the current session would produce a misleading history). |
| 1734 | + // Returns true if a full restore occurred, false if a merge occurred. The |
| 1735 | + // caller uses this to decide whether to record an additional -i command in |
| 1736 | + // the current session's history (see gui-import-control.mjs). |
1710 | 1737 | // TODO: figure out if interface data should be imported (e.g. should |
1711 | 1738 | // visibility flag of imported layers be imported) |
1712 | 1739 | async function importSessionData(buf, gui) { |
1713 | 1740 | if (buf instanceof ArrayBuffer) { |
1714 | 1741 | buf = new Uint8Array(buf); |
1715 | 1742 | } |
1716 | 1743 | var data = await internal.unpackSessionData(buf); |
| 1744 | + var fullRestore = gui.model.isEmpty(); |
1717 | 1745 | importDatasets(data.datasets, gui); |
| 1746 | + if (fullRestore) { |
| 1747 | + gui.session.restoreHistorySnapshot(data.history); |
| 1748 | + } |
| 1749 | + return fullRestore; |
1718 | 1750 | } |
1719 | 1751 |
|
1720 | 1752 | function importDatasets(datasets, gui) { |
|
1730 | 1762 | if (!lyr) return null; // no data -- no snapshot |
1731 | 1763 | // compact: true applies compression to vector coordinates, for ~30% reduction |
1732 | 1764 | // in file size in a typical polygon or polyline file, but longer processing time |
1733 | | - var opts = {compact: false, active_layer: lyr}; |
| 1765 | + // history: capture session commands + saved/unsaved boundary so the history |
| 1766 | + // can be reinstated if this snapshot is restored or re-imported later. |
| 1767 | + var opts = { |
| 1768 | + compact: false, |
| 1769 | + active_layer: lyr, |
| 1770 | + history: gui.session.getHistorySnapshot() |
| 1771 | + }; |
1734 | 1772 | var datasets = gui.model.getDatasets(); |
1735 | 1773 | var obj = await internal.exportDatasetsToPack(datasets, opts); |
1736 | 1774 | obj.gui = getGuiState(gui); |
|
2348 | 2386 | await wait(35); |
2349 | 2387 | } |
2350 | 2388 | if (group[internal.PACKAGE_EXT]) { |
2351 | | - await importSessionData(group[internal.PACKAGE_EXT].content, gui); |
| 2389 | + var fullRestore = await importSessionData(group[internal.PACKAGE_EXT].content, gui); |
| 2390 | + importCount++; |
| 2391 | + // Skip recording an -i command if the .msx import was a full project |
| 2392 | + // restore: the previous session's history (including the original -i) |
| 2393 | + // has already been reinstated, so adding another entry here would be |
| 2394 | + // misleading. For the merge case, record the import as a regular -i |
| 2395 | + // so the snapshot contributes a CLI-replayable entry to the session. |
| 2396 | + if (!fullRestore) { |
| 2397 | + gui.session.fileImported(group[internal.PACKAGE_EXT].filename, optStr); |
| 2398 | + } |
2352 | 2399 | } else if (await importDataset(group, groupImportOpts)) { |
2353 | 2400 | importCount++; |
2354 | 2401 | gui.session.fileImported(group.filename, optStr); |
|
3958 | 4005 | // expose this function, so other components can run commands (e.g. box tool) |
3959 | 4006 | this.runMapshaperCommands = runMapshaperCommands; |
3960 | 4007 |
|
3961 | | - this.runInitialCommands = function(str) { |
| 4008 | + // Open the console (if closed) and run a command, as if the user had |
| 4009 | + // typed it. Used by UI controls that surface console functionality, e.g. |
| 4010 | + // the "view command history" link in the snapshot menu. |
| 4011 | + this.runCommand = function(str) { |
3962 | 4012 | str = str.trim(); |
3963 | 4013 | if (!str) return; |
3964 | 4014 | turnOn(); |
3965 | 4015 | submit(str); |
3966 | 4016 | }; |
3967 | 4017 |
|
| 4018 | + this.runInitialCommands = this.runCommand; |
| 4019 | + |
3968 | 4020 | consoleMessage(PROMPT); |
3969 | 4021 | gui.keyboard.on('keydown', onKeyDown); |
3970 | 4022 | window.addEventListener('beforeunload', saveHistory); // save history if console is open on refresh |
|
4783 | 4835 | await loadGeopackageLib(); |
4784 | 4836 | } |
4785 | 4837 | opts.active_layer = gui.model.getActiveLayer().layer; // kludge to support restoring active layer in gui |
| 4838 | + if (opts.format == internal.PACKAGE_EXT) { |
| 4839 | + // Embed the session history in .msx exports so that re-importing the |
| 4840 | + // file into a fresh session restores the original command history. |
| 4841 | + // The .msx file itself is a durable artifact, so mark every captured |
| 4842 | + // command as "saved" -- a user reloading the file shouldn't see an |
| 4843 | + // unsaved-changes warning for work that lives in the file they just |
| 4844 | + // opened. |
| 4845 | + var snapshot = gui.session.getHistorySnapshot(); |
| 4846 | + snapshot.savedAtIndex = snapshot.commands.length; |
| 4847 | + opts.history = snapshot; |
| 4848 | + } |
4786 | 4849 | try { |
4787 | 4850 | var files = await internal.exportTargetLayers(model, targets, opts); |
4788 | 4851 | } catch(e) { |
|
5592 | 5655 |
|
5593 | 5656 | function SessionHistory(gui) { |
5594 | 5657 | var commands = []; |
| 5658 | + // index of first command after the last "save" boundary; commands at indices |
| 5659 | + // [savedAtIndex .. commands.length) are considered unsaved |
| 5660 | + var savedAtIndex = 0; |
5595 | 5661 | // commands that can be ignored when checking for unsaved changes |
5596 | | - var nonEditingCommands = 'i,target,info,version,verbose,projections,inspect,help,h,encodings,calc'.split(','); |
| 5662 | + var nonEditingCommands = 'i,target,info,version,verbose,projections,inspect,help,h,encodings,calc,comment'.split(','); |
5597 | 5663 |
|
5598 | 5664 | this.unsavedChanges = function() { |
5599 | | - var cmd, cmdName; |
5600 | | - for (var i=commands.length - 1; i >= 0; i--) { |
5601 | | - cmdName = getCommandName(commands[i]); |
5602 | | - if (cmdName == 'o') break; |
| 5665 | + for (var i = commands.length - 1; i >= savedAtIndex; i--) { |
| 5666 | + var cmdName = getCommandName(commands[i]); |
5603 | 5667 | if (nonEditingCommands.includes(cmdName)) continue; |
5604 | 5668 | return true; |
5605 | 5669 | } |
5606 | 5670 | return false; |
5607 | 5671 | }; |
5608 | 5672 |
|
| 5673 | + this.isEmpty = function() { |
| 5674 | + return commands.length === 0; |
| 5675 | + }; |
| 5676 | + |
| 5677 | + // Mark the current end of the history as a "saved" boundary -- called after |
| 5678 | + // data has been written somewhere durable (e.g. an -o export). Snapshots |
| 5679 | + // are session-scoped and are NOT durable, so creating one does not mark saved. |
| 5680 | + this.markSaved = function() { |
| 5681 | + savedAtIndex = commands.length; |
| 5682 | + }; |
| 5683 | + |
| 5684 | + // Capture a serializable copy of the history for inclusion in a snapshot. |
| 5685 | + this.getHistorySnapshot = function() { |
| 5686 | + return { |
| 5687 | + commands: commands.slice(), |
| 5688 | + savedAtIndex: savedAtIndex |
| 5689 | + }; |
| 5690 | + }; |
| 5691 | + |
| 5692 | + // Replace the current history with one captured by getHistorySnapshot(). |
| 5693 | + // Used when restoring an in-session snapshot. If the snapshot has no history |
| 5694 | + // (e.g. older snapshots, or external .msx files), starts from a clean state. |
| 5695 | + this.restoreHistorySnapshot = function(obj) { |
| 5696 | + if (obj && Array.isArray(obj.commands)) { |
| 5697 | + commands = obj.commands.slice(); |
| 5698 | + savedAtIndex = typeof obj.savedAtIndex == 'number' ? |
| 5699 | + Math.min(obj.savedAtIndex, commands.length) : commands.length; |
| 5700 | + } else { |
| 5701 | + commands = []; |
| 5702 | + savedAtIndex = 0; |
| 5703 | + } |
| 5704 | + }; |
| 5705 | + |
5609 | 5706 | this.fileImported = function(file, optStr) { |
5610 | 5707 | var cmd = '-i ' + file; |
5611 | 5708 | if (optStr) { |
|
5665 | 5762 | cmd += ' ' + optStr; |
5666 | 5763 | } |
5667 | 5764 | commands.push(cmd); |
| 5765 | + // -o writes data to a durable location, so treat this as a save boundary |
| 5766 | + savedAtIndex = commands.length; |
5668 | 5767 | }; |
5669 | 5768 |
|
5670 | 5769 | this.setTargetLayer = function(lyr) { |
|
0 commit comments