Localhost OpenerLocal web server URL mapper and live-reloading split preview extension for VS Code: maps files inside your MAMP, XAMPP, WampServer, Laragon, or Apache/Nginx document root to their BackgroundWhen developing PHP sites, Cascade CMS templates, WordPress or Drupal builds, or Apache/Nginx projects locally, previewing the file you are editing in a browser has three recurring friction points:
Features
InstallationOption 1: Install from VSIX (Recommended)
For VS Code Insiders:
Option 2: Package from Source
Step-by-Step SetupPrerequisite: Local Web Server (MAMP or XAMPP)Localhost Opener does not bundle a web server. Start MAMP, XAMPP, WampServer, Laragon, or your local Apache/Nginx instance before opening previews.
Method 1: Interactive Folder Picker (Recommended)
Method 2: Manual
|
| Command | ID | Keybinding | UI Location | Description |
|---|---|---|---|---|
Localhost: Add Document Root (MAMP / XAMPP) |
localhostOpener.addRoot |
(none) | Sidebar Header (+), Command Palette |
Opens a folder picker and URL input box to add or update an entry in localhostOpener.roots. |
Localhost: Open Active File |
localhostOpener.open |
Ctrl+Alt+O (Cmd+Alt+O) |
Left Activity Bar Globe, Command Palette | Opens the active file's localhost URL in the configured browser. |
Localhost: Open in Split Pane (Live Reload) |
localhostOpener.openSplit |
Ctrl+Alt+L (Cmd+Alt+L) |
Editor Title Bar (Split-Globe), Command Palette | Opens the active file's http://localhost/... URL in VS Code's Integrated Browser beside the editor and reloads on save. |
Open in Localhost (Browser) |
localhostOpener.openFromExplorer |
(none) | Explorer & Editor Context Menu | Opens the right-clicked file in the configured browser. |
Open in Localhost Split Pane (Live Reload) |
localhostOpener.openSplitFromExplorer |
(none) | Explorer & Editor Context Menu | Opens the right-clicked file in a split browser pane beside the editor with live reload on save. |
Localhost: Copy URL |
localhostOpener.copy |
(none) | Sidebar Tree Item Inline | Copies the resolved http://localhost/... URL to the clipboard. |
How It Works (Architecture & Internals)
1. Path Normalization & Longest-Prefix Matching (fileToUrl & isSafeHttpUrl)
fileToUrl(fsPath) normalizes Windows backslashes to forward slashes, validates each root URL with isSafeHttpUrl(), and compares the file path against every entry in localhostOpener.roots:
function fileToUrl(fsPath) {
if (!fsPath || typeof fsPath !== 'string') return null;
const roots = vscode.workspace.getConfiguration('localhostOpener').get('roots', []);
if (!Array.isArray(roots)) return null;
const norm = fsPath.replace(/\\/g, '/');
const matches = roots
.filter(r => r && typeof r.path === 'string' && typeof r.url === 'string' && isSafeHttpUrl(r.url))
.map(r => ({ root: r.path.replace(/\\/g, '/').replace(/\/+$/, ''), url: r.url.trim().replace(/\/+$/, '') }))
.filter(r => r.root && norm.toLowerCase().startsWith(r.root.toLowerCase() + '/'))
.sort((a, b) => b.root.length - a.root.length);
if (!matches.length) return null;
const m = matches[0];
const rel = norm.slice(m.root.length);
return m.url + encodeURI(rel);
}
Appending '/' to r.root.toLowerCase() before .startsWith() prevents a root like C:/MAMP from matching a sibling folder named C:/MAMP-old/index.html. Candidate roots are sorted by character length descending (b.root.length - a.root.length), and the remaining relative path is passed through encodeURI(rel) so path slashes stay intact while spaces and special characters are percent-encoded.
2. Activity Bar Launch & Unmapped File Fallback (UrlProvider & activate)
activate() registers localhostOpener.view with vscode.window.createTreeView and subscribes to treeView.onDidChangeVisibility. When the view becomes visible (e.visible === true), the listener inspects vscode.window.activeTextEditor:
- If the active editor holds a
file:URI that matches a configured root, the listener callsopenUrl(url)and executesworkbench.view.explorerso the sidebar switches immediately back to the file tree. - If no editor is active or
fileToUrl()returnsnull, the sidebar stays open andUrlProvider.getChildren()renders aUrlItemwired tolocalhostOpener.addRoot(No localhost root matches this file (click to add)).
3. Document Root Registration (promptAddRoot & warnNoRoot)
promptAddRoot() calls vscode.window.showOpenDialog({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false }) to get the folder path, then prompts for the base URL via vscode.window.showInputBox({ value: 'http://localhost', ignoreFocusOut: true }) and strips trailing slashes. Before writing to vscode.ConfigurationTarget.Global, it deduplicates localhostOpener.roots case-insensitively by normalized path (r.path.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase() !== normNew) so updating a folder's port replaces the existing entry in place.
4. Split-Pane Integrated Browser & Live Reload (openSplit, supportsIntegratedBrowser, isBrowserPreviewOpen, trackSave)
openSplit(url) stores lastSplitUrl = url and checks whether VS Code's Integrated Browser command (workbench.action.browser.open) is available via supportsIntegratedBrowser(). When present, it calls vscode.commands.executeCommand('workbench.action.browser.open', { url, openToSide: true, reuseUrlFilter: url }), which opens or reuses the Integrated Browser in the side editor group at the resolved http://localhost/... URL. On older VS Code builds without workbench.action.browser.open, it falls back to simpleBrowser.api.open with { viewColumn: vscode.ViewColumn.Beside, preserveFocus: true }.
When any file is saved (vscode.workspace.onDidSaveTextDocument), trackSave() verifies that fileToUrl(doc.uri.fsPath) is non-null and inspects vscode.window.tabGroups.all via isBrowserPreviewOpen() to confirm the browser tab is still open. While open, openSplit(lastSplitUrl) runs workbench.action.browser.open with reuseUrlFilter: lastSplitUrl followed by workbench.action.browser.hardReload and restores focus to the active text editor (vscode.window.showTextDocument). Calling hardReload bypasses Chromium's HTTP heuristic cache ((Date - Last-Modified) * 0.10) so the very first save and external .css or .js edits reload immediately. Closing the browser tab clears lastSplitUrl via tabGroups.onDidChangeTabs so subsequent saves do not reopen the panel.
5. Browser Dispatch (openUrl)
openUrl(url) reads localhostOpener.browser. When set to "simple", it delegates to openSplit(url) to open the preview pane beside the editor. When set to "system" (the default), it opens the URL in the OS default browser via vscode.env.openExternal(vscode.Uri.parse(url)).
Configuration Options
{
"localhostOpener.roots": [
{
"path": "C:\\xampp\\htdocs",
"url": "http://localhost"
}
],
"localhostOpener.browser": "system"
}
| Setting | Type | Default | Description |
|---|---|---|---|
localhostOpener.roots |
Array<{ path: string, url: string }> |
[] |
Local document root folders and their localhost base URLs. Longest matching path wins. |
localhostOpener.browser |
"system" \| "simple" |
"system" |
Open URLs in the OS default browser ("system") or VS Code's integrated split browser ("simple"). |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Warning: No localhost root matches this file. |
The active file is outside every path in localhostOpener.roots. |
Click Add Document Root... on the notification (or run Localhost: Add Document Root (MAMP / XAMPP)) and pick your htdocs folder. |
| Split pane is blank or shows connection refused | Apache/Nginx is stopped, the port in localhostOpener.roots is wrong, or the response sends X-Frame-Options: DENY. |
Start MAMP/XAMPP, check the port in localhostOpener.roots, or press Ctrl+Alt+O to open in your system browser. |
License
MIT