From 2514bc68ca1449e5a6c16c9316ca20d210579ea5 Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 8 Jan 2026 09:35:41 +0530 Subject: [PATCH 1/5] chore: add save interceptor --- src/document/DocumentCommandHandlers.js | 11 ++++--- src/editor/EditorHelper/ChangeHelper.js | 39 +++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index 3943af04d..9029468d3 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -59,6 +59,7 @@ define(function (require, exports, module) { NewFileContentManager = require("features/NewFileContentManager"), NodeConnector = require("NodeConnector"), NodeUtils = require("utils/NodeUtils"), + ChangeHelper = require("editor/EditorHelper/ChangeHelper"), _ = require("thirdparty/lodash"); const KernalModeTrust = window.KernalModeTrust; @@ -1243,11 +1244,13 @@ define(function (require, exports, module) { * USER_CANCELED object). */ function handleFileSave(commandData) { - // todo save interceptor - var activeEditor = EditorManager.getActiveEditor(), + const activeEditor = EditorManager.getActiveEditor(), activeDoc = activeEditor && activeEditor.document, - doc = (commandData && commandData.doc) || activeDoc, - settings; + doc = (commandData && commandData.doc) || activeDoc; + let settings; + if(ChangeHelper._onBeforeSave(doc)){ + return $.Deferred().reject().promise(); + } if (doc && !doc.isSaving) { if (doc.isUntitled()) { diff --git a/src/editor/EditorHelper/ChangeHelper.js b/src/editor/EditorHelper/ChangeHelper.js index f010e4b03..25fa9bcf4 100644 --- a/src/editor/EditorHelper/ChangeHelper.js +++ b/src/editor/EditorHelper/ChangeHelper.js @@ -19,6 +19,8 @@ * */ +/*global logger*/ + /** * Editor instance helpers change handling. Only to be used from Editor.js. */ @@ -316,19 +318,42 @@ define(function (require, exports, module) { let _undoInterceptor = null; let _redoInterceptor = null; + let _saveInterceptor = null; + + function _onBeforeSave(docBeingSaved) { + if(!_saveInterceptor){ + return false; + } + try{ + return _saveInterceptor(docBeingSaved); + } catch (e) { + logger.reportError(e, "Error in save interceptor"); + return false; + } + } function _onBeforeUndo(editor, codeMirror, event) { if(!_undoInterceptor){ return false; } - return _undoInterceptor(editor, codeMirror, event); + try { + return _undoInterceptor(editor, codeMirror, event); + } catch (e) { + logger.reportError(e, "Error in undo interceptor"); + return false; + } } function _onBeforeRedo(editor, codeMirror, event) { if(!_redoInterceptor){ return false; } - return _redoInterceptor(editor, codeMirror, event); + try { + return _redoInterceptor(editor, codeMirror, event); + } catch (e) { + logger.reportError(e, "Error in redo interceptor"); + return false; + } } /** @@ -379,9 +404,18 @@ define(function (require, exports, module) { _keyEventInterceptor = interceptor; } + /** + * Sets the key down/up/press interceptor function in codemirror + * @param {Function} interceptor - Function(editor, cm, event) that returns true to preventDefault + */ + function setSaveInterceptor(interceptor) { + _saveInterceptor = interceptor; + } + // private exports exports._onBeforeUndo =_onBeforeUndo; exports._onBeforeRedo = _onBeforeRedo; + exports._onBeforeSave = _onBeforeSave; // public exports exports.addHelpers = addHelpers; @@ -391,4 +425,5 @@ define(function (require, exports, module) { exports.setCopyInterceptor = setCopyInterceptor; exports.setPasteInterceptor = setPasteInterceptor; exports.setKeyEventInterceptor = setKeyEventInterceptor; + exports.setSaveInterceptor = setSaveInterceptor; }); From f823e91b5f62f9ffb51432fdd8e1bdc955ae7275 Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 8 Jan 2026 09:40:12 +0530 Subject: [PATCH 2/5] chore: update pro dep --- tracking-repos.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracking-repos.json b/tracking-repos.json index 1f8dc3cab..0f383b17a 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "19d997d0aa98acb3507b3cb19b81c286ebcfd474" + "commitID": "d8e23bb1d62bb5c15ac9addf56a4ccf178bc0cd9" } } From 5e105dcbda5c2421500afb7498e12e56e7b7c232 Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 8 Jan 2026 12:39:58 +0530 Subject: [PATCH 3/5] chore: better live preview file selection when a css/non html file seelcted If a non previewable file is selected (Eg: css/js) then we show the last previewed html/md/priviewable file. this is more predicatable and will lead to an active preview most of teh time than showing the no preview screen. --- src/LiveDevelopment/LiveDevMultiBrowser.js | 28 +++++++++++-- .../BrowserStaticServer.js | 42 +++++++++---------- .../Phoenix-live-preview/NodeStaticServer.js | 42 +++++++++---------- .../Phoenix-live-preview/main.js | 12 +----- src/filesystem/FileSystem.js | 3 ++ 5 files changed, 66 insertions(+), 61 deletions(-) diff --git a/src/LiveDevelopment/LiveDevMultiBrowser.js b/src/LiveDevelopment/LiveDevMultiBrowser.js index b84e15290..93a1ae674 100644 --- a/src/LiveDevelopment/LiveDevMultiBrowser.js +++ b/src/LiveDevelopment/LiveDevMultiBrowser.js @@ -88,6 +88,7 @@ define(function (require, exports, module) { LiveDevProtocol = require("LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol"), Metrics = require("utils/Metrics"), WorkspaceManager = require("view/WorkspaceManager"), + FileSystem = require("filesystem/FileSystem"), PageLoaderWorkerScript = require("text!LiveDevelopment/BrowserScripts/pageLoaderWorker.js"); // Documents @@ -538,15 +539,34 @@ define(function (require, exports, module) { } } + let _lastPreviewedHTMLFilePath; + async function _resolveLivePreviewDocument() { + let activeDocument = DocumentManager.getCurrentDocument(); + if(livePreviewUrlPinned){ + return jsPromise(DocumentManager.getDocumentForPath(currentPreviewFilePath)); + } + if (activeDocument && activeDocument.getLanguage().getId() === "html") { + _lastPreviewedHTMLFilePath = activeDocument.file.fullPath; + return activeDocument; + } + // if a css/js/non html file is active, we fall back to the last previewed HTML file + let fileExists = await FileSystem.existsAsync(_lastPreviewedHTMLFilePath); + if(!fileExists){ + // the last previewed file may have been deleted + return activeDocument; // can be null + } + return jsPromise(DocumentManager.getDocumentForPath(_lastPreviewedHTMLFilePath)); + } + + ProjectManager.on(ProjectManager.EVENT_PROJECT_OPEN, ()=>{ + _lastPreviewedHTMLFilePath = null; + }); /** * Open a live preview on the current docuemnt. */ async function open() { - let doc = DocumentManager.getCurrentDocument(); - if(livePreviewUrlPinned){ - doc = await jsPromise(DocumentManager.getDocumentForPath(currentPreviewFilePath)); - } + let doc = await _resolveLivePreviewDocument(); // wait for server (StaticServer, Base URL or file:) _prepareServer(doc) diff --git a/src/extensionsIntegrated/Phoenix-live-preview/BrowserStaticServer.js b/src/extensionsIntegrated/Phoenix-live-preview/BrowserStaticServer.js index e656669fa..8eeec7bfc 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/BrowserStaticServer.js +++ b/src/extensionsIntegrated/Phoenix-live-preview/BrowserStaticServer.js @@ -128,6 +128,8 @@ define(function (require, exports, module) { return Phoenix.isNativeApp || !(Phoenix.browser.desktop.isSafari || Phoenix.browser.mobile.isIos); } + let _lastPreviewedFilePath; + /** * Finds out a {URL,filePath} to live preview from the project. Will return and empty object if the current * file is not previewable. @@ -179,33 +181,24 @@ define(function (require, exports, module) { }); return; } else if(utils.isPreviewableFile(fullPath)){ - const filePath = httpFilePath || path.relative(projectRoot, fullPath); + // this is the case where the user has html/svg/any previewable file as the active document + _lastPreviewedFilePath = fullPath; + } + let fileExists = await FileSystem.existsAsync(_lastPreviewedFilePath); + if(_lastPreviewedFilePath && fileExists){ + // user either has active document as a previewable file or this is the case where + // user switched to a css/js/other file that is not previewable, but we have on old previewable + // file we will just take the _lastPreviewedFilePath as active + const filePath = httpFilePath || path.relative(projectRoot, _lastPreviewedFilePath); let URL = httpFilePath || `${projectRootUrl}${filePath}`; resolve({ URL, filePath: filePath, - fullPath: fullPath, - isMarkdownFile: utils.isMarkdownFile(fullPath), - isHTMLFile: utils.isHTMLFile(fullPath) + fullPath: _lastPreviewedFilePath, + isMarkdownFile: utils.isMarkdownFile(_lastPreviewedFilePath), + isHTMLFile: utils.isHTMLFile(_lastPreviewedFilePath) }); return; - } else { - const currentLivePreviewDetails = LiveDevelopment.getLivePreviewDetails(); - if(currentLivePreviewDetails && currentLivePreviewDetails.liveDocument - && currentLivePreviewDetails.liveDocument.isRelated - && currentLivePreviewDetails.liveDocument.isRelated(fullPath)){ - fullPath = currentLivePreviewDetails.liveDocument.doc.file.fullPath; - const filePath = path.relative(projectRoot, fullPath); - let URL = `${projectRootUrl}${filePath}`; - resolve({ - URL, - filePath: filePath, - fullPath: fullPath, - isMarkdownFile: utils.isMarkdownFile(fullPath), - isHTMLFile: utils.isHTMLFile(fullPath) - }); - return; - } } } resolve({ @@ -588,14 +581,16 @@ define(function (require, exports, module) { if(!url.startsWith(_staticServerInstance.getBaseUrl())) { return Promise.reject("Not serving content as url belongs to another phcode instance: " + url); } - if(utils.isMarkdownFile(path) && currentFile && currentFile.fullPath === path){ + const shouldServeRendered = ((path === _lastPreviewedFilePath) + || (currentFile && currentFile.fullPath === path)); + if(utils.isMarkdownFile(path) && shouldServeRendered){ return _getMarkdown(path); } if(_staticServerInstance){ return _staticServerInstance._getInstrumentedContent(path, url); } return Promise.reject("Cannot get content"); - }; + } /** * See BaseServer#start. Starts listenting to StaticServerDomain events. @@ -712,6 +707,7 @@ define(function (require, exports, module) { } function _projectOpened(_evt, projectRoot) { + _lastPreviewedFilePath = null; navigatorChannel.postMessage({ type: 'PROJECT_SWITCH', projectRoot: projectRoot.fullPath diff --git a/src/extensionsIntegrated/Phoenix-live-preview/NodeStaticServer.js b/src/extensionsIntegrated/Phoenix-live-preview/NodeStaticServer.js index 77fb640a0..e89fc2dc6 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/NodeStaticServer.js +++ b/src/extensionsIntegrated/Phoenix-live-preview/NodeStaticServer.js @@ -431,7 +431,9 @@ define(function (require, exports, module) { return Promise.reject("Not serving content as url invalid: " + url); } const filePath = _staticServerInstance.urlToPath(url); - if(utils.isMarkdownFile(filePath) && currentFile && currentFile.fullPath === filePath){ + const shouldServeRendered = ((filePath === _lastPreviewedFilePath) + || (currentFile && currentFile.fullPath === path)); + if(utils.isMarkdownFile(filePath) && shouldServeRendered){ return _getMarkdown(filePath); } if(_staticServerInstance){ @@ -580,6 +582,7 @@ define(function (require, exports, module) { } function _projectOpened(_evt, projectRoot) { + _lastPreviewedFilePath = null; liveServerConnector.execPeer('navMessageProjectOpened', { type: 'PROJECT_SWITCH', projectRoot: projectRoot.fullPath @@ -635,6 +638,8 @@ define(function (require, exports, module) { return livePreviewTabs.size > 0; } + let _lastPreviewedFilePath; + /** * Finds out a {URL,filePath} to live preview from the project. Will return and empty object if the current * file is not previewable. @@ -703,33 +708,24 @@ define(function (require, exports, module) { }); return; } else if(utils.isPreviewableFile(fullPath)){ - const relativeFilePath = httpFilePath || path.relative(projectRoot, fullPath); - let URL = httpFilePath || decodeURI(_staticServerInstance.pathToUrl(fullPath)); + // this is the case where the user has html/svg/any previewable file as the active document + _lastPreviewedFilePath = fullPath; + } + let fileExists = await FileSystem.existsAsync(_lastPreviewedFilePath); + if(_lastPreviewedFilePath && fileExists){ + // user either has active document as a previewable file or this is the case where + // user switched to a css/js/other file that is not previewable, but we have on old previewable + // file we will just take the _lastPreviewedFilePath as active + const relativeFilePath = httpFilePath || path.relative(projectRoot, _lastPreviewedFilePath); + let URL = httpFilePath || decodeURI(_staticServerInstance.pathToUrl(_lastPreviewedFilePath)); resolve({ URL, filePath: relativeFilePath, - fullPath: fullPath, - isMarkdownFile: utils.isMarkdownFile(fullPath), - isHTMLFile: utils.isHTMLFile(fullPath) + fullPath: _lastPreviewedFilePath, + isMarkdownFile: utils.isMarkdownFile(_lastPreviewedFilePath), + isHTMLFile: utils.isHTMLFile(_lastPreviewedFilePath) }); return; - } else { - const currentLivePreviewDetails = LiveDevelopment.getLivePreviewDetails(); - if(currentLivePreviewDetails && currentLivePreviewDetails.liveDocument - && currentLivePreviewDetails.liveDocument.isRelated - && currentLivePreviewDetails.liveDocument.isRelated(fullPath)){ - fullPath = currentLivePreviewDetails.liveDocument.doc.file.fullPath; - const relativeFilePath = httpFilePath || path.relative(projectRoot, fullPath); - let URL = httpFilePath || decodeURI(_staticServerInstance.pathToUrl(fullPath)); - resolve({ - URL, - filePath: relativeFilePath, - fullPath: fullPath, - isMarkdownFile: utils.isMarkdownFile(fullPath), - isHTMLFile: utils.isHTMLFile(fullPath) - }); - return; - } } resolve({ URL: getNoPreviewURL(), diff --git a/src/extensionsIntegrated/Phoenix-live-preview/main.js b/src/extensionsIntegrated/Phoenix-live-preview/main.js index 5319ac2ee..65b95842b 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/main.js +++ b/src/extensionsIntegrated/Phoenix-live-preview/main.js @@ -823,13 +823,7 @@ define(function (require, exports, module) { currentLivePreviewURL = newSrc; currentPreviewFile = previewDetails.fullPath; } - const existingPreviewFile = $iframe && $iframe.attr('data-original-path'); - const existingPreviewURL = $iframe && $iframe.attr('data-original-src'); - if(isReload && previewDetails.isNoPreview && existingPreviewURL && - existingPreviewFile && ProjectManager.isWithinProject(existingPreviewFile)) { - currentLivePreviewURL = existingPreviewURL; - currentPreviewFile = existingPreviewFile; - } else if(isReload){ + if(isReload && previewDetails.isHTMLFile){ LiveDevelopment.openLivePreview(); } let relativeOrFullPath= ProjectManager.makeProjectRelativeIfPossible(currentPreviewFile); @@ -852,10 +846,6 @@ define(function (require, exports, module) { $iframe = newIframe; if(_isProjectPreviewTrusted()){ $iframe.attr('src', currentLivePreviewURL); - // we have to save src as the iframe src attribute may have redirected, and we cannot read it as its - // a third party domain once its redirected. - $iframe.attr('data-original-src', currentLivePreviewURL); - $iframe.attr('data-original-path', currentPreviewFile); if(Phoenix.isTestWindow) { window._livePreviewIntegTest.currentLivePreviewURL = currentLivePreviewURL; window._livePreviewIntegTest.urlLoadCount++; diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js index c75ea8dde..3b5d659b6 100644 --- a/src/filesystem/FileSystem.js +++ b/src/filesystem/FileSystem.js @@ -755,6 +755,9 @@ define(function (require, exports, module) { * @param {function(?string, boolean)} callback */ FileSystem.prototype.existsAsync = function (path) { + if(!path) { + return Promise.resolve(false); + } return this._impl.existsAsync(path); }; From a82f0306e6f8afbafdececf48706a86da5c9d06a Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 8 Jan 2026 12:41:20 +0530 Subject: [PATCH 4/5] chore: update pro dep --- tracking-repos.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracking-repos.json b/tracking-repos.json index 0f383b17a..694003a4f 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "d8e23bb1d62bb5c15ac9addf56a4ccf178bc0cd9" + "commitID": "c30d49834ab199d805ce489e9fc858ea0189abeb" } } From 0a2c008c49ec44b3aeef9cb50b5858a4b288a81a Mon Sep 17 00:00:00 2001 From: abose Date: Thu, 8 Jan 2026 12:47:35 +0530 Subject: [PATCH 5/5] chore: fence cut/copy/paste interceptor in try catch blocks --- src/editor/EditorHelper/ChangeHelper.js | 30 ++++++++++++++++++++----- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/editor/EditorHelper/ChangeHelper.js b/src/editor/EditorHelper/ChangeHelper.js index 25fa9bcf4..d9a18c1e0 100644 --- a/src/editor/EditorHelper/ChangeHelper.js +++ b/src/editor/EditorHelper/ChangeHelper.js @@ -177,9 +177,15 @@ define(function (require, exports, module) { // Redispatch these CodeMirror key events as Editor events function _onKeyEvent(instance, event) { - if(_keyEventInterceptor && _keyEventInterceptor(self, self._codeMirror, event)){ - // the interceptor processed it, so don't pass it along to CodeMirror' - return; + if(_keyEventInterceptor){ + try { + if(_keyEventInterceptor(self, self._codeMirror, event)){ + // the interceptor processed it, so don't pass it along to CodeMirror' + return; + } + } catch (e) { + logger.reportError(e, "Error in key event interceptor"); + } } self.trigger("keyEvent", self, event); // deprecated self.trigger(event.type, self, event); @@ -256,7 +262,11 @@ define(function (require, exports, module) { self._codeMirror.on("cut", function(cm, e) { // Let interceptor decide what to do with the event (including preventDefault) if (_cutInterceptor) { - return _cutInterceptor(self, cm, e); + try { + return _cutInterceptor(self, cm, e); + } catch (e) { + logger.reportError(e, "Error in cut interceptor"); + } } // Otherwise allow normal cut behavior }); @@ -264,7 +274,11 @@ define(function (require, exports, module) { self._codeMirror.on("copy", function(cm, e) { // Let interceptor decide what to do with the event (including preventDefault) if (_copyInterceptor) { - return _copyInterceptor(self, cm, e); + try { + return _copyInterceptor(self, cm, e); + } catch (e) { + logger.reportError(e, "Error in copy interceptor"); + } } // Otherwise allow normal copy behavior }); @@ -272,7 +286,11 @@ define(function (require, exports, module) { self._codeMirror.on("paste", function(cm, e) { // Let interceptor decide what to do with the event (including preventDefault) if (_pasteInterceptor) { - return _pasteInterceptor(self, cm, e); + try { + return _pasteInterceptor(self, cm, e); + } catch (e) { + logger.reportError(e, "Error in paste interceptor"); + } } // Otherwise allow normal paste behavior });