Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6695c13
fix: reconnect live preview pages after a heartbeat drop
devvaannsh Sep 18, 2026
493ccf5
fix: reconnect the live preview worker socket after it closes
devvaannsh Sep 18, 2026
5b62bc6
feat: expose a live preview reload for the layers panel
devvaannsh Sep 18, 2026
d5ccebd
test: cover live preview tab heartbeats
devvaannsh Sep 18, 2026
2c6c46b
fix: styles and property content getting out of viewport
devvaannsh Sep 18, 2026
598a8dd
refactor: remove dead code and regex literals from layers panel
devvaannsh Sep 18, 2026
4820e0d
fix: layers panel scrollbars
devvaannsh Sep 18, 2026
1990104
feat: only show outline on hover in live preview element
devvaannsh Sep 18, 2026
4ff93e9
feat: debounce hover highlights in live preview
devvaannsh Sep 18, 2026
47529dc
fix: caret highlight in live preview around a held layers selection
devvaannsh Sep 18, 2026
e069edd
fix: stale html caret taking the selection on live preview click
devvaannsh Sep 18, 2026
f8581e2
feat: pending dots in layers details
devvaannsh Sep 18, 2026
06d6aec
feat: editor caret only highlights in live preview while layers panel…
devvaannsh Sep 18, 2026
487fc6f
chore: shorten comments in live preview caret highlight
devvaannsh Sep 18, 2026
a018507
build: update pro deps
devvaannsh Sep 18, 2026
d95f9fe
fix: layers panel showing an empty page message above its sections
devvaannsh Sep 19, 2026
e396698
fix: html file list flashing in layers panel while the live preview l…
devvaannsh Sep 19, 2026
bc9aafc
fix: layers tree taking seconds to load after a markdown preview
devvaannsh Sep 19, 2026
7070419
feat: folder tree for the html file list in layers panel
devvaannsh Sep 19, 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
163 changes: 139 additions & 24 deletions src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

Large diffs are not rendered by default.

36 changes: 29 additions & 7 deletions src/LiveDevelopment/BrowserScripts/pageLoaderWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,26 @@ function splitMetadataAndBuffer(concatenatedBuffer) {
}

let messageQueue = [];
const MESSAGE_QUEUE_MAX = 200;
const WS_RECONNECT_MIN_MS = 1000;
const WS_RECONNECT_MAX_MS = 10000;
let _wsReconnectDelayMs = WS_RECONNECT_MIN_MS;
let _heartbeatStarted = false;

function _sendMessage(message) {
if(_livePreviewWebSocket && _livePreviewWebSocketOpen) {
_livePreviewWebSocket.send(mergeMetadataAndArrayBuffer(message));
} else if(_livePreviewBroadcastChannel){
_livePreviewBroadcastChannel.postMessage(message);
} else if(message.type === 'TAB_ONLINE') {
// a heartbeat that cannot go now is worthless later
return;
} else {
livePreviewDebugModeEnabled && console.warn("No Channels available for live preview worker messaging," +
" queueing request, waiting for channel..");
if(messageQueue.length >= MESSAGE_QUEUE_MAX) {
messageQueue.shift();
}
messageQueue.push(message);
}
}
Expand All @@ -124,6 +135,10 @@ function flushPendingMessages() {
}

function _setupHearbeatMessenger(clientID) {
if(_heartbeatStarted) {
return;
}
_heartbeatStarted = true;
function _sendOnlineHeartbeat() {
_sendMessage({
type: 'TAB_ONLINE',
Expand Down Expand Up @@ -151,11 +166,13 @@ function _setupBroadcastChannel(broadcastChannel, clientID) {

function _setupWebsocketChannel(wssEndpoint, clientID) {
_debugLog("live preview worker websocket url: ", wssEndpoint);
_livePreviewWebSocket = new WebSocket(wssEndpoint);
_livePreviewWebSocket.binaryType = 'arraybuffer';
_livePreviewWebSocket.addEventListener("open", () =>{
const socket = new WebSocket(wssEndpoint);
socket.binaryType = 'arraybuffer';
socket.addEventListener("open", () =>{
_debugLog("live preview worker websocket opened", wssEndpoint);
_livePreviewWebSocket = socket;
_livePreviewWebSocketOpen = true;
_wsReconnectDelayMs = WS_RECONNECT_MIN_MS;
_sendMessage({
type: 'CHANNEL_TYPE',
channelName: 'livePreviewChannel',
Expand All @@ -165,7 +182,7 @@ function _setupWebsocketChannel(wssEndpoint, clientID) {
_setupHearbeatMessenger(clientID);
});

_livePreviewWebSocket.addEventListener('message', function (event) {
socket.addEventListener('message', function (event) {
const message = event.data;
const {metadata} = splitMetadataAndBuffer(message);
_debugLog("Live Preview worker socket channel: Browser received event from Phoenix: ", metadata);
Expand All @@ -176,13 +193,18 @@ function _setupWebsocketChannel(wssEndpoint, clientID) {
}
});

_livePreviewWebSocket.addEventListener('error', function (event) {
socket.addEventListener('error', function (event) {
console.error("Live Preview worker socket channel: error event: ", event);
});

_livePreviewWebSocket.addEventListener('close', function () {
// The page is still here when the socket goes, so keep trying to get back to the editor.
socket.addEventListener('close', function () {
_livePreviewWebSocketOpen = false;
_debugLog("Live Preview worker websocket closed");
_debugLog("Live Preview worker websocket closed, reconnecting in ms: ", _wsReconnectDelayMs);
setTimeout(() => {
_setupWebsocketChannel(wssEndpoint, clientID);
}, _wsReconnectDelayMs);
_wsReconnectDelayMs = Math.min(_wsReconnectDelayMs * 2, WS_RECONNECT_MAX_MS);
});
}

Expand Down
44 changes: 23 additions & 21 deletions src/LiveDevelopment/MultiBrowserImpl/documents/LiveDocument.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ define(function (require, exports, module) {
// A held arrow key moves the caret far faster than the rule under it can be
// resolved, so the highlight follows the caret once it settles.
const CURSOR_HIGHLIGHT_DEBOUNCE_MS = 80;
const HELD_HIGHLIGHT_PREFIX = "held:";

function _simpleHash(str) {
let hash = 5381;
Expand Down Expand Up @@ -160,9 +161,7 @@ define(function (require, exports, module) {
this.setInstrumentationEnabled(true, true);
this.editor.off("cursorActivity", this._onCursorActivity);
this.editor.on("cursorActivity", this._onCursorActivity);
if (!_isCursorHighlightGated(this)) {
this.updateHighlight();
}
this.updateHighlight();
}
};

Expand All @@ -173,28 +172,29 @@ define(function (require, exports, module) {
LiveDocument.prototype._detachFromEditor = function () {
if (this.editor) {
this._cancelPendingHighlight();
if (!_isCursorHighlightGated(this)) {
this.hideHighlight();
}
this.hideHighlight();
this.editor.off("cursorActivity", this._onCursorActivity);
}
};

let _disableHighlightOnCursor = false;
let _cursorHighlightGeneration = 0;
let _cursorHighlightGate = null;
let _selectionHolder = null;

/**
* Lets something outside the live documents decide whether the caret may move
* the preview highlight, such as a panel holding a selection of its own.
* @param {?function(LiveDocument): boolean} gate Returns false to leave the preview alone; null removes it.
* While the holder returns true the caret still highlights in the preview but never
* selects, so a selection made elsewhere (the layers panel) stays.
* @param {?function(LiveDocument): boolean} holder null removes it.
*/
LiveDocument.setCursorHighlightGate = function (gate) {
_cursorHighlightGate = gate || null;
LiveDocument.setSelectionHolder = function (holder) {
_selectionHolder = holder || null;
};

function _isCursorHighlightGated(liveDoc) {
return !!_cursorHighlightGate && _cursorHighlightGate(liveDoc) === false;
// for anything else that follows the caret and must settle on the same clock
LiveDocument.CURSOR_HIGHLIGHT_DEBOUNCE_MS = CURSOR_HIGHLIGHT_DEBOUNCE_MS;

function _isSelectionHeld(liveDoc) {
return !!_selectionHolder && _selectionHolder(liveDoc) === true;
}

/**
Expand Down Expand Up @@ -228,15 +228,14 @@ define(function (require, exports, module) {
*/
LiveDocument.prototype._onCursorActivity = function (event, editor) {
this._cancelPendingHighlight();
if (!this.editor || _disableHighlightOnCursor || _isCursorHighlightGated(this)) {
if (!this.editor || _disableHighlightOnCursor) {
return;
}
const self = this;
const generation = _cursorHighlightGeneration;
this._highlightTimer = window.setTimeout(function () {
self._highlightTimer = null;
if (self.editor && !_disableHighlightOnCursor && generation === _cursorHighlightGeneration &&
!_isCursorHighlightGated(self)) {
if (self.editor && !_disableHighlightOnCursor && generation === _cursorHighlightGeneration) {
self.updateHighlight();
}
}, CURSOR_HIGHLIGHT_DEBOUNCE_MS);
Expand Down Expand Up @@ -342,7 +341,7 @@ define(function (require, exports, module) {
}
// The preview can have been selected directly or by another live
// document, so this document's cached selector cannot prove it is clear.
this.protocol.evaluate("_LD.hideHighlight()");
this.protocol.evaluate("_LD.hideHighlight(" + _isSelectionHeld(this) + ")");
};

/**
Expand All @@ -351,11 +350,14 @@ define(function (require, exports, module) {
* @param {string} name The selector whose matched nodes should be highlighted.
*/
LiveDocument.prototype.highlightRule = function (name) {
if (this._lastHighlight === name) {
const keepSelection = _isSelectionHeld(this);
// the same rule draws differently around a held selection
const highlight =(keepSelection ? HELD_HIGHLIGHT_PREFIX : "") + name;
if (this._lastHighlight === highlight) {
return;
}
this._lastHighlight = name;
this.protocol.evaluate("_LD.highlightRule(" + JSON.stringify(name) + ")");
this._lastHighlight = highlight;
this.protocol.evaluate("_LD.highlightRule(" + JSON.stringify(name) + ", " + keepSelection + ")");
};

/**
Expand Down
12 changes: 10 additions & 2 deletions src/LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,11 @@ define(function (require, exports, module) {
}
}

function _isInFront(liveDoc) {
const fullEditor = EditorManager.getCurrentFullEditor();
return !!fullEditor && fullEditor.document.file.fullPath === liveDoc.doc.file.fullPath;
}

const processedMessageIDs = new Phoenix.libs.LRUCache({
max: MAX_PENDING_LP_CALLS_1000
// we dont need to set a ttl here as message ids are unique throughout lifetime. And old ids will
Expand Down Expand Up @@ -397,8 +402,11 @@ define(function (require, exports, module) {
console.error("error in tag selection", e);
}
editMode && liveDoc && liveDoc.disableHighlightOnCursorActivity(false);
// the caret did not move for a script-added element, re-highlighting would drop its selection
liveDoc && !msg.sourceless && liveDoc.updateHighlight();
// the caret did not move for a script-added element, re-highlighting would drop its selection.
// Nor did it move in the html while another file is in front: that stale caret would take it.
if (liveDoc &&!msg.sourceless && _isInFront(liveDoc)) {
liveDoc.updateHighlight();
}
} else {
// enrich received message with clientId
msg.clientId = clientId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ define(function (require, exports, module) {
HilightJSText = require("text!thirdparty/highlight.js/highlight.min.js"),
GFMCSSText = require("text!thirdparty/gfm.min.css"),
markdownHTMLTemplate = require("text!./markdown.html"),
LivePreviewTabs = require("./LivePreviewTabs"),
redirectionHTMLTemplate = require("text!./redirectPage.html");

const EVENT_GET_PHOENIX_INSTANCE_ID = 'GET_PHOENIX_INSTANCE_ID';
Expand All @@ -59,7 +60,7 @@ define(function (require, exports, module) {

EventDispatcher.makeEventDispatcher(exports);

const livePreviewTabs = new Map();
const livePreviewTabs = LivePreviewTabs.livePreviewTabs;
const PHCODE_LIVE_PREVIEW_QUERY_PARAM = "phcodeLivePreview";

// Communication Channels for PHCode.dev Editor and Live Preview
Expand Down Expand Up @@ -225,11 +226,7 @@ define(function (require, exports, module) {
_sendInitialURL(event.data.pageLoaderID);
return;
case 'TAB_LOADER_ONLINE':
livePreviewTabs.set(event.data.pageLoaderID, {
lastSeen: new Date(),
URL: event.data.URL,
navigationTab: true
});
LivePreviewTabs.tabOnline(event.data.pageLoaderID, event.data.URL, true);
return;
default: return; // ignore messages not intended for us.
}
Expand Down Expand Up @@ -274,14 +271,23 @@ define(function (require, exports, module) {
.catch(console.error);
return;
case EVENT_TAB_ONLINE:
livePreviewTabs.set(message.clientID, {
lastSeen: new Date(),
URL: message.URL
});
LivePreviewTabs.tabOnline(message.clientID, message.URL);
return;
case EVENT_REPORT_ERROR:
logger.reportError(new Error(message));
return;
case 'BROWSER_CONNECT':
LivePreviewTabs.tabConnected(message.clientID, message.url);
exports.trigger(eventName, {
data
});
return;
case 'BROWSER_CLOSE':
LivePreviewTabs.dropTab(message.clientID);
exports.trigger(eventName, {
data
});
return;
default:
exports.trigger(eventName, {
data
Expand Down Expand Up @@ -645,30 +651,19 @@ define(function (require, exports, module) {
});

exports.on(EVENT_TAB_ONLINE, function(_ev, event){
livePreviewTabs.set(event.data.message.clientID, {
lastSeen: new Date(),
URL: event.data.message.URL
});
LivePreviewTabs.tabOnline(event.data.message.clientID, event.data.message.URL);
});

// A tab silent for too long is closed; one that heartbeats again is connected again.
function _startHeartBeatListeners() {
// If we didn't receive heartbeat message from a tab for 10 seconds, we assume tab closed
const TAB_HEARTBEAT_TIMEOUT = 10000; // in millis secs
setInterval(()=>{
let endTime = new Date();
for(let tab of livePreviewTabs.keys()){
const tabInfo = livePreviewTabs.get(tab);
let timeDiff = endTime - tabInfo.lastSeen; // in ms
if(timeDiff > TAB_HEARTBEAT_TIMEOUT){
livePreviewTabs.delete(tab);
// the parent navigationTab `phcode.dev/live-preview-loader.html` which loads the live preview tab
// is in the list too. We should not raise browser close for a live-preview-loader tab.
if(!tabInfo.navigationTab) {
exports.trigger('BROWSER_CLOSE', { data: { message: {clientID: tab}}});
}
}
LivePreviewTabs.start({
close: function (clientID) {
exports.trigger('BROWSER_CLOSE', { data: { message: {clientID}}});
},
reconnect: function (clientID, url) {
exports.trigger('BROWSER_CONNECT', { data: { message: {clientID, url}}});
}
}, 1000);
});
}

/**
Expand Down Expand Up @@ -782,6 +777,7 @@ define(function (require, exports, module) {
exports.messageToLivePreviewTabs = messageToLivePreviewTabs;
exports.getPreviewDetails = getPreviewDetails;
exports.livePreviewTabs = livePreviewTabs;
exports.dropTab = LivePreviewTabs.dropTab;
exports.redirectAllTabs = redirectAllTabs;
exports.getTabPopoutURL = getTabPopoutURL;
exports.hasActiveLivePreviews = hasActiveLivePreviews;
Expand Down
Loading
Loading