├── lib ├── helper.js └── pinegrow-vs.js ├── .vscodeignore ├── icon.png ├── .gitignore ├── .vscode ├── settings.json ├── extensions.json └── launch.json ├── jsconfig.json ├── test ├── extension.test.js └── index.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── extension.js └── package.json /lib/helper.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .gitignore -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pinegrow/PinegrowVSCode/HEAD/icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | .idea 5 | 6 | *.vsix -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "eslint.enable": true 4 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "checkJs": true, /* Typecheck .js files. */ 6 | "lib": [ 7 | "es6" 8 | ] 9 | }, 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } -------------------------------------------------------------------------------- /test/extension.test.js: -------------------------------------------------------------------------------- 1 | /* global suite, test */ 2 | 3 | // 4 | // Note: This example test is leveraging the Mocha test framework. 5 | // Please refer to their documentation on https://mochajs.org/ for help. 6 | // 7 | 8 | // The module 'assert' provides assertion methods from node 9 | const assert = require('assert'); 10 | 11 | // You can import and use all API from the 'vscode' module 12 | // as well as import your extension to test it 13 | // const vscode = require('vscode'); 14 | // const myExtension = require('../extension'); 15 | 16 | // Defines a Mocha test suite to group tests of similar kind together 17 | suite("Extension Tests", function() { 18 | 19 | // Defines a Mocha unit test 20 | test("Something 1", function() { 21 | assert.equal(-1, [1, 2, 3].indexOf(5)); 22 | assert.equal(-1, [1, 2, 3].indexOf(0)); 23 | }); 24 | }); -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.13 2 | 3 | * Automatically reconnect when the Pinegrow URL setting changes. 4 | * Added setting to enable / disable sync for User or Workspace. This lets you enable Pinegrow sync for individual projects. 5 | * Refactored code for compatibility with activate / deactivate flow. 6 | * Updated the icon. 7 | 8 | ## 0.0.12 9 | 10 | * Improved handling of URLs that contain spaces and other special characters. 11 | 12 | ## 0.0.11 - Url matching improvement 13 | 14 | * Skip encoding document urls. 15 | 16 | ## 0.0.10 - Bug fix 17 | 18 | * Correctly handling the situation where active editor is null. 19 | 20 | ## 0.0.9 - Improving realiability of url matching 21 | 22 | * The extension now uses additional logic to map urls between Pinegrow and Visual Studio Code. 23 | * Setting to enable debug output in the console. 24 | 25 | ## 0.0.1 - First Release 26 | 27 | * Initial release. -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ] 16 | }, 17 | { 18 | "name": "Extension Tests", 19 | "type": "extensionHost", 20 | "request": "launch", 21 | "runtimeExecutable": "${execPath}", 22 | "args": [ 23 | "--extensionDevelopmentPath=${workspaceFolder}", 24 | "--extensionTestsPath=${workspaceFolder}/test" 25 | ] 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 3 | // 4 | // This file is providing the test runner to use when running extension tests. 5 | // By default the test runner in use is Mocha based. 6 | // 7 | // You can provide your own test runner if you want to override it by exporting 8 | // a function run(testRoot: string, clb: (error:Error) => void) that the extension 9 | // host can call to run the tests. The test runner is expected to use console.log 10 | // to report the results back to the caller. When the tests are finished, return 11 | // a possible error to the callback or null if none. 12 | 13 | const testRunner = require('vscode/lib/testrunner'); 14 | 15 | // You can directly control Mocha options by uncommenting the following lines 16 | // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info 17 | testRunner.configure({ 18 | ui: 'tdd', // the TDD UI is being used in extension.test.js (suite, test, etc.) 19 | useColors: true // colored output from test results 20 | }); 21 | 22 | module.exports = testRunner; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pinegrow + VSCode 2 | 3 | This Visual Studio Code plugin enables live editing with Pinegrow Web Editor. Changes done in Pinegrow are synced to VS Code and vice versa. 4 | 5 | [Pinegrow](https://pinegrow.com) is a powerful web editor that lets you visually build Bootstrap, Foundation, WordPress and plain HTML websites. 6 | 7 | **Pinegrow 2.9 or newer is required.** 8 | 9 | [Go here](https://pinegrow.com/docs/master-pinegrow/using-external-code-editors/visual-studio-code/) for more details about how to use the plugin. 10 | 11 | ## Live editing in concert 12 | 13 | All edits are live-synced between VS Code and Pinegrow, without having to save changes first. 14 | 15 | ## Navigate the code visually 16 | 17 | Elements selected in Pinegrow are highlighted in VS Code. 18 | 19 | ## Control Pinegrow from VS Code 20 | 21 | Use the context menu or keyboard shortcuts to: 22 | 23 | * **Select an element** in Pinegrow with Cmd+Alt+P on Mac, Ctrl+Alt+P on Win / Linux 24 | * **Open the page** in Pinegrow with Cmd+Alt+O on Mac, Ctrl+Alt+O on Win / Linux 25 | * **Refresh the page** in Pinegrow with Cmd+Alt+R on Mac, Ctrl+Alt+R on Win / Linux 26 | 27 | ## Edit HTML and CSS files 28 | 29 | Live sync works for HTML (or other types that are listed among editable types in Pinegrow's settings) and CSS/SASS/LESS files. 30 | 31 | ## How to install 32 | 33 | In VS Code, go to Code -> Preferences -> Extensions or click on the Extensions icon in the Activity bar. 34 | 35 | There, use the search box to search for "Pinegrow Live Sync". 36 | 37 | Click on Install and reload VS Code if necessary. 38 | 39 | That's all you need to do - if you are using default settings for Pinegrow's internal web server. 40 | 41 | ### Configuring the plugin 42 | 43 | If you are using custom port setting in Pinegrow you have to configure the Pinegrow VS Code plugin accordingly. 44 | 45 | First, in Pinegrow, go to Support -> Show API Url. Copy the value that is displayed. 46 | 47 | In VS Code go to Settings -> Extensions and use the search box to find Pinegrow settings. Paste the API url value in there. -------------------------------------------------------------------------------- /extension.js: -------------------------------------------------------------------------------- 1 | const vscode = require('vscode'); 2 | const PinegrowVS = require('./lib/pinegrow-vs'); 3 | 4 | /** 5 | * Called when VS Code activates your extension. 6 | */ 7 | let pgvs; 8 | 9 | function activate(context) { 10 | // Create the PinegrowVS instance. 11 | pgvs = new PinegrowVS(); 12 | 13 | // Initialize PinegrowVS right away. 14 | pgvs.init(); 15 | 16 | // Track the previously active editor so we can pass it to activeEditorChanged. 17 | let activeEditor = null; 18 | 19 | // Listen for changes in the active text editor. 20 | vscode.window.onDidChangeActiveTextEditor((editor) => { 21 | pgvs.activeEditorChanged(editor, activeEditor); 22 | if (editor) { 23 | pgvs.visibleEditorChanged(editor); 24 | } 25 | activeEditor = editor; 26 | }); 27 | 28 | // Listen for text changes in any open document. 29 | vscode.workspace.onDidChangeTextDocument((event) => { 30 | pgvs.editorTextChanged(event); 31 | }); 32 | 33 | // Listen for saves in any open document. 34 | vscode.workspace.onDidSaveTextDocument((document) => { 35 | pgvs.editorTextSaved(document); 36 | }); 37 | 38 | // Reconnect Pinegrow when workspace folders are added or removed. 39 | const workspaceFoldersWatcher = vscode.workspace.onDidChangeWorkspaceFolders(() => { 40 | pgvs.reconnect(); 41 | }); 42 | 43 | // Whenever the user changes configuration, we check if pinegrow.urlWithPort was changed 44 | // and reconnect if needed. 45 | const configWatcher = vscode.workspace.onDidChangeConfiguration((e) => { 46 | if (e.affectsConfiguration('pinegrow.urlWithPort') || e.affectsConfiguration('pinegrow.enabled')) { 47 | pgvs.reconnect(); 48 | } 49 | }); 50 | 51 | const selectInPg = vscode.commands.registerCommand('extension.selectInPG', () => { 52 | const editor = vscode.window.activeTextEditor; 53 | pgvs.selectInPinegrow(editor); 54 | }); 55 | 56 | const openInPg = vscode.commands.registerCommand('extension.openInPG', () => { 57 | const editor = vscode.window.activeTextEditor; 58 | pgvs.openInPinegrow(editor); 59 | }); 60 | 61 | const reconnect = vscode.commands.registerCommand('extension.reconnect', () => { 62 | pgvs.reconnect(); 63 | }); 64 | 65 | const detectMapping = vscode.commands.registerCommand('extension.detectMapping', () => { 66 | pgvs.autoDetectMapping(); 67 | }); 68 | 69 | const downloadPinegrow = vscode.commands.registerCommand('extension.downloadPinegrow', () => { 70 | vscode.commands.executeCommand('vscode.open', vscode.Uri.parse('https://pinegrow.com')); 71 | }); 72 | 73 | const refreshPage = vscode.commands.registerCommand('extension.refreshPage', () => { 74 | const editor = vscode.window.activeTextEditor; 75 | pgvs.refreshPage(editor); 76 | }); 77 | 78 | context.subscriptions.push( 79 | workspaceFoldersWatcher, 80 | configWatcher, 81 | selectInPg, 82 | openInPg, 83 | reconnect, 84 | detectMapping, 85 | downloadPinegrow, 86 | refreshPage 87 | ); 88 | } 89 | 90 | function deactivate() { 91 | pgvs.destroy(); 92 | } 93 | 94 | exports.activate = activate; 95 | exports.deactivate = deactivate; 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pinegrow-vscode", 3 | "displayName": "Pinegrow Live Sync", 4 | "description": "Visual studio code plugin for live syncing with Pinegrow Web Editor, that lets you build responsive websites faster with live multi-page editing, CSS & SASS styling, CSS Grid editor and smart components for Bootstrap, Foundation and WordPress.", 5 | "version": "0.0.13", 6 | "publisher": "Pinegrow", 7 | "license": "MIT", 8 | "engines": { 9 | "vscode": "^1.28.0" 10 | }, 11 | "categories": [ 12 | "Other" 13 | ], 14 | "activationEvents": [ 15 | "*" 16 | ], 17 | "icon": "icon.png", 18 | "keywords": [ 19 | "html", 20 | "css", 21 | "WordPress", 22 | "CSS grid", 23 | "sass" 24 | ], 25 | "main": "./extension", 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/Pinegrow/PinegrowVSCode.git" 29 | }, 30 | "homepage": "https://pinegrow.com/docs/master-pinegrow/using-external-code-editors/visual-studio-code/", 31 | "contributes": { 32 | "configuration": { 33 | "type": "object", 34 | "title": "Pinegrow", 35 | "properties": { 36 | "pinegrow.urlWithPort": { 37 | "type": "string", 38 | "default": "http://localhost:40000", 39 | "description": "Url with hostname and port of Pinegrow's internal webserver. Note that port + 1 will be used for code editor communication. Default value is http://localhost:40000. In Pinegrow, use `Support -> Show API url` to display the url." 40 | }, 41 | "pinegrow.debug": { 42 | "type": "boolean", 43 | "default": false, 44 | "description": "Output debug information to the console to help troubleshoot issues with the Visual Studio Code - Pinegrow communication. Restart VS Code after changing the setting." 45 | }, 46 | "pinegrow.enabled": { 47 | "type": "boolean", 48 | "default": true, 49 | "description": "Enable live sync with Pinegrow. Use the Workspace setting to enable the sync for individual projects." 50 | } 51 | } 52 | }, 53 | "menus": { 54 | "editor/context": [ 55 | { 56 | "when": "resourceLangId == html", 57 | "command": "extension.selectInPG", 58 | "group": "pinegrow" 59 | }, 60 | { 61 | "when": "resourceLangId == html", 62 | "command": "extension.openInPG", 63 | "group": "pinegrow" 64 | }, 65 | { 66 | "when": "resourceLangId == html", 67 | "command": "extension.refreshPage", 68 | "group": "pinegrow" 69 | } 70 | ] 71 | }, 72 | "commands": [ 73 | { 74 | "command": "extension.selectInPG", 75 | "title": "Pinegrow: Select element" 76 | }, 77 | { 78 | "command": "extension.openInPG", 79 | "title": "Pinegrow: Open file" 80 | }, 81 | { 82 | "command": "extension.reconnect", 83 | "title": "Pinegrow: Reconnect" 84 | }, 85 | { 86 | "command": "extension.downloadPinegrow", 87 | "title": "Pinegrow: Download" 88 | }, 89 | { 90 | "command": "extension.refreshPage", 91 | "title": "Pinegrow: Refresh page" 92 | } 93 | ], 94 | "keybindings": [ 95 | { 96 | "command": "extension.selectInPG", 97 | "key": "ctrl+alt+p", 98 | "mac": "cmd+alt+p", 99 | "when": "editorTextFocus" 100 | }, 101 | { 102 | "command": "extension.openInPG", 103 | "key": "ctrl+alt+o", 104 | "mac": "cmd+alt+o", 105 | "when": "editorTextFocus" 106 | }, 107 | { 108 | "command": "extension.refreshPage", 109 | "key": "ctrl+alt+r", 110 | "mac": "cmd+alt+r", 111 | "when": "editorTextFocus" 112 | } 113 | ] 114 | }, 115 | "scripts": { 116 | "test": "node ./node_modules/vscode/bin/test" 117 | }, 118 | "dependencies": { 119 | "socket.io-client": "2.1.1" 120 | }, 121 | "devDependencies": { 122 | "@types/mocha": "^2.2.42", 123 | "@types/node": "^8.10.62", 124 | "eslint": "^4.11.0", 125 | "typescript": "^2.6.1", 126 | "vscode": "^1.1.37" 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /lib/pinegrow-vs.js: -------------------------------------------------------------------------------- 1 | function PinegrowVS() { 2 | const vscode = require('vscode'); 3 | const fs = require('fs'); 4 | let configurations = vscode.workspace.getConfiguration("pinegrow"); 5 | 6 | const CHANGE_DELAY = 500; 7 | const SAVE_DELAY = 500; 8 | const SAVE_INTERVAL = 100; 9 | 10 | var io = require('socket.io-client'); 11 | var pinegrow_url = configurations.urlWithPort; 12 | 13 | var pg = false; 14 | var editor_api = null; 15 | var currentSourceNodes = {}; 16 | var editorObj = {}; 17 | 18 | var debug = configurations.debug; 19 | 20 | var mapPinegrowPrefix = null; 21 | var mapMyPrefix = null; 22 | 23 | var fileNotUpdated = {}; 24 | 25 | var codeChangedInEditorTimeout = null; 26 | var codeSavedInEditorInterval = null; 27 | 28 | this.filesOpenInPinegrow = []; 29 | 30 | var mapFuzzyUrlToPinegrowUrl = {}; 31 | 32 | function getEditorUrl(document) { 33 | if(document.uri) { 34 | return document.uri.toString(true); //skip encoding 35 | } 36 | var file = document.fileName; 37 | if (!file) return null; 38 | var f = 'file://'; 39 | if (file.match(/^[a-z]\:/i)) { 40 | file = '/' + file 41 | } else if (file.startsWith('\\\\')) { 42 | file = file.substr(2); 43 | } 44 | var r = f + encodeURI(file.replace(/\\/g, "/")); 45 | return r; 46 | } 47 | 48 | function getPgUrlFromEditorUrl(url) { 49 | if (!url || mapPinegrowPrefix === null) return url; 50 | return url.replace(mapMyPrefix, mapPinegrowPrefix); 51 | } 52 | 53 | function getPgEditorUrl(document) { 54 | var url = getPgUrlFromEditorUrl(getEditorUrl(document)); 55 | var fuzzy_url = getFuzzyUrl(url); 56 | if(mapFuzzyUrlToPinegrowUrl[fuzzy_url]) { 57 | return mapFuzzyUrlToPinegrowUrl[fuzzy_url]; 58 | } 59 | return url; 60 | } 61 | 62 | function autoDetectMapping(pg_urls, editor_urls) { 63 | 64 | var getNumberOfSameTokens = function (a, b) { 65 | var c = 0; 66 | var ai = a.length - 1; 67 | var bi = b.length - 1; 68 | while (ai >= 0 && bi >= 0) { 69 | if (a[ai] == b[bi]) { 70 | c++; 71 | ai--; 72 | bi--; 73 | } else { 74 | break; 75 | } 76 | } 77 | return c; 78 | } 79 | 80 | for (var i = 0; i < pg_urls.length; i++) { 81 | pg_urls[i] = pg_urls[i].replace('file://', '').split('/'); 82 | } 83 | 84 | mapPinegrowPrefix = null; 85 | mapMyPrefix = null; 86 | 87 | var mostSimilarCount = 0; 88 | var mostSimilarPgUrl = null; 89 | var mostSimilarEditorUrl = null; 90 | 91 | for (var i = 0; i < editor_urls.length; i++) { 92 | editor_urls[i] = editor_urls[i].replace('file://', '').split('/'); 93 | 94 | for (var j = 0; j < pg_urls.length; j++) { 95 | var c = getNumberOfSameTokens(pg_urls[j], editor_urls[i]); 96 | if (c > mostSimilarCount) { 97 | mostSimilarCount = c; 98 | mostSimilarPgUrl = pg_urls[j]; 99 | mostSimilarEditorUrl = editor_urls[i]; 100 | } 101 | } 102 | } 103 | 104 | if (mostSimilarCount == 0) return false; //failed to auto detect mapPinegrowPrefix 105 | 106 | if (mostSimilarCount == mostSimilarPgUrl.length) return true; 107 | 108 | mostSimilarPgUrl.splice(mostSimilarPgUrl.length - mostSimilarCount, mostSimilarCount); 109 | mostSimilarEditorUrl.splice(mostSimilarEditorUrl.length - mostSimilarCount, mostSimilarCount); 110 | 111 | mapPinegrowPrefix = 'file://' + mostSimilarPgUrl.join('/'); 112 | mapMyPrefix = 'file://' + mostSimilarEditorUrl.join('/'); 113 | 114 | return true; 115 | } 116 | 117 | function doWithCurrentSourceNodeForEditor(document, func) { 118 | var url = getPgEditorUrl(document); 119 | if (currentSourceNodes[url]) { 120 | func(currentSourceNodes[url]); 121 | } else { 122 | if (!pg) { 123 | vscode.window.showErrorMessage("Didn't yet received parser module from Pinegrow."); 124 | if (editor_api) { 125 | editor_api.emit('requestParserModule'); 126 | } 127 | return; 128 | } 129 | var p = new pgParser(); 130 | p.assignIds = false; 131 | p.parse(document.getText(), function () { 132 | currentSourceNodes[url] = p.rootNode; 133 | func(currentSourceNodes[url]); 134 | }); 135 | } 136 | } 137 | 138 | function setText(editor, text) { 139 | editor.edit(function (builder) { 140 | const document = editor.document; 141 | var line = document.lineCount - 1; 142 | if (line < 0) line = 0; 143 | 144 | const lastLine = document.lineAt(line); 145 | 146 | const start = new vscode.Position(0, 0); 147 | const end = new vscode.Position(document.lineCount - 1, lastLine.text.length); 148 | 149 | builder.replace(new vscode.Range(start, end), text); 150 | }); 151 | } 152 | 153 | function scrollEditorTo(editor, lineObject) { 154 | var showLine = 0; 155 | var ranges = editor.visibleRanges; 156 | if (ranges && ranges.length > 0) { 157 | var visibleRange = ranges[0]; 158 | var visibleLine = visibleRange.start.line; 159 | if (visibleLine - lineObject.start > 0) { 160 | showLine = lineObject.start; 161 | if (showLine > 0) showLine--; 162 | } else { 163 | showLine = lineObject.end; 164 | if (showLine < editor.document.lineCount) showLine++; 165 | } 166 | } 167 | 168 | var lineRange = editor.document.lineAt(showLine).range; 169 | editor.revealRange(lineRange); 170 | } 171 | 172 | function getFuzzyUrl(url) { 173 | var n = url.replace(/^file\:\/+/, ''); //remove file:// 174 | n = n.replace(/:/g, ''); 175 | n = n.replace(/\\/g, '/'); //replace all \ with / 176 | n = n.replace(/\/\/+/g, '/'); //replace multiple // with / 177 | n = n.toLowerCase(); 178 | 179 | if(n.indexOf('%') < 0) { 180 | //not encoded yet 181 | n = encodeURI(n); 182 | } 183 | //var a = n.split('/'); 184 | //a.shift(); //ignore the first item in path 185 | //a.join('/'); 186 | return n; 187 | } 188 | 189 | function clearChangedInEditorTimeout(force) { 190 | if (force || codeChangedInEditorTimeout !== null) { 191 | clearTimeout(codeChangedInEditorTimeout); 192 | codeChangedInEditorTimeout = null; 193 | } 194 | } 195 | 196 | function clearSavedInEditorInterval(force) { 197 | if (force || codeSavedInEditorInterval !== null) { 198 | clearInterval(codeSavedInEditorInterval); 199 | codeSavedInEditorInterval = null; 200 | } 201 | } 202 | 203 | this.revertEditorFromDisk = function (editor) { 204 | const path = editor.document.uri.path; 205 | const savedSource = fs.readFileSync(path); 206 | if (savedSource.toString() === editor.document.getText()) { 207 | vscode.commands.executeCommand('workbench.action.files.revert', editor.document.uri); 208 | } 209 | } 210 | 211 | this.sourceChanged = function (url) { 212 | currentSourceNodes[url] = null; 213 | } 214 | 215 | this.isFileOpenInPinegrow = function (url) { 216 | var found = this.filesOpenInPinegrow.indexOf(url) >= 0; 217 | if(debug) { 218 | if(found) { 219 | console.log('Url is open in Pinegrow: ' + url) 220 | } else { 221 | console.log('Url is NOT open in Pinegrow: ' + url) 222 | } 223 | } 224 | return found; 225 | } 226 | 227 | this.activeEditorChanged = function (editor, oldEditor) { 228 | if (oldEditor) { 229 | var url = getPgEditorUrl(oldEditor.document); 230 | editorObj[url] = true; 231 | } 232 | } 233 | 234 | this.editorTextChanged = function (editor) { 235 | if(!this.isEnabled(true)) { 236 | return; 237 | } 238 | if (editor) { 239 | var document = editor.document; 240 | if (document.isDirty) { 241 | var url = getPgEditorUrl(document); 242 | if (editorObj[url]) { 243 | editorObj[url] = false; 244 | return; 245 | } 246 | 247 | if (this.isFileOpenInPinegrow(url)) { 248 | clearChangedInEditorTimeout(); 249 | codeChangedInEditorTimeout = setTimeout(function () { 250 | clearChangedInEditorTimeout(true); 251 | editor_api.emit('codeChangedInEditor', { 252 | url: url, 253 | code: document.getText() 254 | }); 255 | }, CHANGE_DELAY); 256 | } 257 | this.sourceChanged(url); 258 | } 259 | } 260 | } 261 | 262 | this.editorTextSaved = function (document) { 263 | if(!this.isEnabled(true)) { 264 | return; 265 | } 266 | if (document) { 267 | var url = getPgEditorUrl(document); 268 | 269 | if (this.isFileOpenInPinegrow(url)) { 270 | // Wait for the changes to be sent 271 | codeSavedInEditorInterval = setInterval(function () { 272 | if (codeChangedInEditorTimeout == null) { 273 | clearSavedInEditorInterval(true); 274 | // Wait a bit for change reflection 275 | setTimeout(function () { 276 | editor_api.emit('fileSavedInEditor', { 277 | url: url 278 | }); 279 | }, SAVE_DELAY); 280 | } 281 | }, SAVE_INTERVAL); 282 | } 283 | } 284 | } 285 | 286 | this.getPinegrowApiEndPoint = function (endpoint, port_index) { 287 | port_index = port_index || 0; 288 | 289 | var urlparts = pinegrow_url.split(':'); 290 | if (urlparts.length == 2) { 291 | urlparts.push('40000'); 292 | } 293 | if (urlparts.length > 2) { 294 | urlparts[urlparts.length - 1] = parseInt(urlparts[urlparts.length - 1]) + port_index + 1; 295 | } 296 | return urlparts.join(':') + '/' + endpoint; 297 | } 298 | 299 | this.visibleEditorChanged = function (editor) { 300 | var document = editor.document; 301 | var url = getPgEditorUrl(document); 302 | var code = fileNotUpdated[url]; 303 | if (code) { 304 | this.changeEditorSource(editor, url, code); 305 | delete fileNotUpdated[url]; 306 | } 307 | } 308 | 309 | this.changeEditorSource = function (editor, url, code) { 310 | var document = editor.document; 311 | editorObj[url] = true; 312 | 313 | // Unsaved circle will appear 314 | // even if there is no change 315 | if (document.getText() != code) { // @TODO: check this 316 | setText(editor, code); 317 | this.sourceChanged(url); 318 | } 319 | } 320 | 321 | function showErrorMessageWithSettingsButton(msg) { 322 | vscode.window.showErrorMessage( 323 | msg, 324 | 'Open Settings' 325 | ).then(selectedAction => { 326 | if (selectedAction === 'Open Settings') { 327 | vscode.commands.executeCommand('workbench.action.openSettings', 'Pinegrow'); 328 | } 329 | }); 330 | } 331 | 332 | this.isEnabled = function(silent) { 333 | if(!configurations.enabled) { 334 | 335 | if(!silent) { 336 | vscode.window.showInformationMessage( 337 | 'Pinegrow Live Sync is not enabled for this workspace.', 338 | 'Open Settings' 339 | ).then(selectedAction => { 340 | if (selectedAction === 'Open Settings') { 341 | vscode.commands.executeCommand('workbench.action.openSettings', 'Pinegrow'); 342 | } 343 | }); 344 | } 345 | 346 | return false; 347 | } 348 | return true; 349 | } 350 | 351 | this.init = function () { 352 | var _this = this; 353 | configurations = vscode.workspace.getConfiguration("pinegrow"); 354 | pinegrow_url = configurations.urlWithPort; 355 | var url = this.getPinegrowApiEndPoint('editor'); 356 | 357 | if (editor_api) editor_api.destroy(); 358 | 359 | editor_api = null; 360 | 361 | if(!this.isEnabled()) { 362 | return; 363 | } 364 | 365 | try { 366 | var urlinfo = new URL(url); 367 | 368 | if (!urlinfo.hostname || !urlinfo.port) { 369 | showErrorMessageWithSettingsButton('Hostname and port are not set in Pinegrow URL.'); 370 | return; 371 | } 372 | } catch (error) { 373 | showErrorMessageWithSettingsButton(`Invalid Pinegrow URL: "${url || ''}"`); 374 | return; 375 | } 376 | 377 | editor_api = io.connect(url); 378 | 379 | editor_api.on('connect', function () { 380 | vscode.window.showInformationMessage("Connected to Pinegrow."); 381 | if (url.indexOf('localhost') < 0 && url.indexOf('127.0.0.1') < 0) { 382 | vscode.window.showInformationMessage("Use Packages -> Pinegrow -> Detect file paths mapping if Pinegrow runs on a different computer."); 383 | } 384 | if (!pg) { 385 | editor_api.emit('requestParserModule'); 386 | } 387 | }); 388 | 389 | editor_api.on('parserModule', function (data) { 390 | if (!pg) { 391 | require('vm').runInThisContext(data.code, 'remote_modules/pinegrowparser.js'); 392 | pg = true; 393 | } 394 | }); 395 | 396 | editor_api.on('disconnect', function () { 397 | vscode.window.showWarningMessage("Disconnected from Pinegrow."); 398 | }); 399 | 400 | editor_api.on('error', function () { 401 | showErrorMessageWithSettingsButton("Unable to connect to Pinegrow at " + url + '.'); 402 | }); 403 | 404 | var last_error_url = null; 405 | editor_api.on('connect_error', function () { 406 | if (last_error_url != url) { 407 | showErrorMessageWithSettingsButton("Unable to connect to Pinegrow at " + url + '.'); 408 | last_error_url = url; 409 | } 410 | }); 411 | 412 | editor_api.on('codeChanged', function (data) { 413 | var fileFound = false; 414 | var editors = vscode.window.visibleTextEditors; 415 | editors.forEach(function (editor) { 416 | var document = editor.document; 417 | var url = getPgEditorUrl(document); 418 | if (url == data.url) { 419 | _this.changeEditorSource(editor, url, data.code); 420 | fileFound = true; 421 | } 422 | }); 423 | 424 | if (!fileFound) { 425 | fileNotUpdated[data.url] = data.code; 426 | } 427 | }); 428 | 429 | editor_api.on('codeSaved', function(data) { 430 | 431 | var editors = vscode.window.visibleTextEditors; 432 | editors.forEach(function (editor) { 433 | var document = editor.document; 434 | var url = getPgEditorUrl(document); 435 | if (url == data.url) { 436 | _this.revertEditorFromDisk(editor); 437 | } 438 | }) 439 | }); 440 | 441 | editor_api.on('elementSelectedInPreview', function (data) { 442 | console.log('Element selected in preview'); 443 | var editors = vscode.window.visibleTextEditors; 444 | editors.forEach(function (editor) { 445 | var document = editor.document; 446 | var url = getPgEditorUrl(document); 447 | if (url == data.url) { 448 | doWithCurrentSourceNodeForEditor(document, function (sourceNode) { 449 | var node = sourceNode.getNodeFromPath(data.path); 450 | if (node) { 451 | var sourcePos = node.getPositionInSource(); 452 | 453 | var posStart = document.positionAt(sourcePos.start); 454 | var posEnd = document.positionAt(sourcePos.end); 455 | 456 | editor.selection = new vscode.Selection( 457 | posStart.line, posStart.character, 458 | posEnd.line, posEnd.character 459 | ); 460 | 461 | var lineObject = { 462 | start: posStart.line, 463 | end: posEnd.line 464 | } 465 | scrollEditorTo(editor, lineObject); 466 | } 467 | }) 468 | } 469 | }) 470 | }); 471 | 472 | editor_api.on('listOfOpenFiles', function (data) { 473 | _this.filesOpenInPinegrow = data.list; 474 | 475 | mapFuzzyUrlToPinegrowUrl = {}; 476 | _this.filesOpenInPinegrow.forEach(function(url) { 477 | mapFuzzyUrlToPinegrowUrl[ getFuzzyUrl(url) ] = url; 478 | }) 479 | 480 | if(debug) { 481 | console.log('Got list of open files in Pinegrow', data.list); 482 | } 483 | }) 484 | } 485 | 486 | this.selectInPinegrow = function (editor) { 487 | if(!this.isEnabled()) { 488 | return; 489 | } 490 | if (editor) { 491 | var document = editor.document; 492 | var url = getPgEditorUrl(document); 493 | if (this.isFileOpenInPinegrow(url)) { 494 | var idx = document.offsetAt(editor.selection.active); 495 | doWithCurrentSourceNodeForEditor(document, function (sourceNode) { 496 | var node = sourceNode.findNodeAtSourceIndex(idx); 497 | if (node) { 498 | var path = node.getPath(); 499 | editor_api.emit('elementSelectedInEditor', { 500 | url: url, 501 | path: path 502 | }); 503 | } 504 | }) 505 | } 506 | } 507 | } 508 | 509 | this.openInPinegrow = function (editor) { 510 | if(!this.isEnabled()) { 511 | return; 512 | } 513 | if (editor) { 514 | var document = editor.document; 515 | var url = getPgEditorUrl(document); 516 | editor_api.emit('openFile', { 517 | url: url 518 | }); 519 | } 520 | } 521 | 522 | this.reconnect = function () { 523 | this.init(); 524 | } 525 | 526 | this.autoDetectMapping = function () { 527 | var pg_urls = []; 528 | this.filesOpenInPinegrow.forEach(function (url) { 529 | pg_urls.push(url) 530 | }); 531 | 532 | var editor_urls = []; 533 | var documents = vscode.workspace.textDocuments; 534 | documents.forEach(function (document) { 535 | var url = getEditorUrl(document); 536 | if (url) { 537 | editor_urls.push(url); 538 | } 539 | }) 540 | 541 | if (autoDetectMapping(pg_urls, editor_urls)) { 542 | if (mapPinegrowPrefix === null) { 543 | vscode.window.showInformationMessage('File paths are the same in Pinegrow and in VS Code.'); 544 | } else { 545 | vscode.window.showInformationMessage('Mapping detected: ' + mapPinegrowPrefix + ' -> ' + mapMyPrefix); 546 | } 547 | } else { 548 | vscode.window.showErrorMessage('Could not detect PG <---> VS Code mapping. Did you open the same file in both editors?'); 549 | } 550 | } 551 | 552 | this.refreshPage = function (editor) { 553 | if (editor) { 554 | var document = editor.document; 555 | var url = getPgEditorUrl(document); 556 | editor_api.emit('refreshPage', { 557 | url: url 558 | }); 559 | } 560 | } 561 | 562 | this.destroy = function () { 563 | editor_api.destroy(); 564 | editor_api = null; 565 | } 566 | } 567 | 568 | module.exports = PinegrowVS; 569 | --------------------------------------------------------------------------------