From e253928b521af1473a132ebaf9e491c863c6f07c Mon Sep 17 00:00:00 2001
From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com>
Date: Sun, 28 Jun 2026 23:30:45 +0530
Subject: [PATCH 01/20] feat: add multi pane view
---
src/cm/commandRegistry.js | 50 +++
src/handlers/editorFileTab.js | 4 +
src/lib/commands.js | 44 +-
src/lib/editorFile.js | 92 +++-
src/lib/editorManager.js | 764 +++++++++++++++++++++++++++++-----
src/lib/openFile.js | 18 +-
src/styles/codemirror.scss | 124 ++++++
7 files changed, 960 insertions(+), 136 deletions(-)
diff --git a/src/cm/commandRegistry.js b/src/cm/commandRegistry.js
index 43a0fb45e..41a08304b 100644
--- a/src/cm/commandRegistry.js
+++ b/src/cm/commandRegistry.js
@@ -213,6 +213,56 @@ function registerCoreCommands() {
return true;
},
});
+ addCommand({
+ name: "newPane",
+ description: "Create new editor pane",
+ readOnly: true,
+ requiresView: false,
+ run() {
+ acode.exec("new-pane");
+ return true;
+ },
+ });
+ addCommand({
+ name: "moveTabToNewPane",
+ description: "Move current tab to new pane",
+ readOnly: true,
+ requiresView: false,
+ run() {
+ acode.exec("move-tab-to-new-pane");
+ return true;
+ },
+ });
+ addCommand({
+ name: "closePane",
+ description: "Close active editor pane",
+ readOnly: true,
+ requiresView: false,
+ run() {
+ acode.exec("close-pane");
+ return true;
+ },
+ });
+ addCommand({
+ name: "focusNextPane",
+ description: "Focus next editor pane",
+ readOnly: true,
+ requiresView: false,
+ run() {
+ acode.exec("focus-next-pane");
+ return true;
+ },
+ });
+ addCommand({
+ name: "focusPreviousPane",
+ description: "Focus previous editor pane",
+ readOnly: true,
+ requiresView: false,
+ run() {
+ acode.exec("focus-previous-pane");
+ return true;
+ },
+ });
addCommand({
name: "closeAllTabs",
description: "Close all tabs",
diff --git a/src/handlers/editorFileTab.js b/src/handlers/editorFileTab.js
index a0c2699a9..dae21b84f 100644
--- a/src/handlers/editorFileTab.js
+++ b/src/handlers/editorFileTab.js
@@ -432,6 +432,10 @@ function getClientPos(e) {
* @param {HTMLElement} $parent
*/
function updateFileList($parent) {
+ if (typeof editorManager.updatePaneFileOrderFromTabs === "function") {
+ if (editorManager.updatePaneFileOrderFromTabs($parent)) return;
+ }
+
const pinnedCount = editorManager.files.filter((file) => file.pinned).length;
const children = [...$parent.children];
const newFileList = [];
diff --git a/src/lib/commands.js b/src/lib/commands.js
index 2b2a1cc80..eebb9e4d2 100644
--- a/src/lib/commands.js
+++ b/src/lib/commands.js
@@ -59,8 +59,8 @@ export function canSaveFile(file = editorManager.activeFile) {
}
function getTabsRelativeToFile(side, referenceFile) {
- const { files } = editorManager;
const file = resolveReferenceFile(referenceFile);
+ const files = editorManager.getPaneFiles?.(file) || editorManager.files;
const activeIndex = files.indexOf(file);
if (activeIndex === -1) return [];
@@ -163,7 +163,25 @@ export default {
});
},
"close-current-tab"() {
- editorManager.activeFile.remove();
+ editorManager.activeFile?.remove();
+ },
+ "new-pane"() {
+ return editorManager.createPane?.();
+ },
+ "split-pane"() {
+ return editorManager.splitPane?.();
+ },
+ "close-pane"() {
+ return editorManager.closeActivePane?.();
+ },
+ "focus-next-pane"() {
+ return editorManager.focusNextPane?.();
+ },
+ "focus-previous-pane"() {
+ return editorManager.focusPreviousPane?.();
+ },
+ "move-tab-to-new-pane"() {
+ return editorManager.moveActiveFileToNewPane?.();
},
"toggle-pin-tab"(referenceFile) {
resolveReferenceFile(referenceFile)?.togglePinned?.();
@@ -246,13 +264,18 @@ export default {
});
},
"next-file"() {
- const len = editorManager.files.length;
- let fileIndex = editorManager.files.indexOf(editorManager.activeFile);
+ const files =
+ editorManager.getPaneFiles?.(editorManager.activeFile) ||
+ editorManager.files;
+ const len = files.length;
+ let fileIndex = files.indexOf(editorManager.activeFile);
+
+ if (!len || fileIndex === -1) return;
if (fileIndex === len - 1) fileIndex = 0;
else ++fileIndex;
- editorManager.files[fileIndex].makeActive();
+ files[fileIndex].makeActive();
},
async open(page) {
switch (page) {
@@ -317,13 +340,18 @@ export default {
.catch(FileBrowser.openFolderError);
},
"prev-file"() {
- const len = editorManager.files.length;
- let fileIndex = editorManager.files.indexOf(editorManager.activeFile);
+ const files =
+ editorManager.getPaneFiles?.(editorManager.activeFile) ||
+ editorManager.files;
+ const len = files.length;
+ let fileIndex = files.indexOf(editorManager.activeFile);
+
+ if (!len || fileIndex === -1) return;
if (fileIndex === 0) fileIndex = len - 1;
else --fileIndex;
- editorManager.files[fileIndex].makeActive();
+ files[fileIndex].makeActive();
},
"read-only"() {
const file = editorManager.activeFile;
diff --git a/src/lib/editorFile.js b/src/lib/editorFile.js
index 2fe93edc1..8877413ed 100644
--- a/src/lib/editorFile.js
+++ b/src/lib/editorFile.js
@@ -269,6 +269,9 @@ function maybeRecommendLanguageModeExtension(file, modeInfo) {
* @property {number} [savedMtime] file mtime last saved or loaded from disk
* @property {number} [diskMtime] latest known file mtime on disk
* @property {boolean} [hasDiskConflict] whether editor and disk both changed
+ * @property {string} [paneId] target editor pane id
+ * @property {object} [pane] target editor pane
+ * @property {boolean} [isPanePlaceholder] temporary empty tab for an empty pane
*/
export default class EditorFile {
@@ -437,6 +440,7 @@ export default class EditorFile {
savedMtime = null;
diskMtime = null;
hasDiskConflict = false;
+ isPanePlaceholder = false;
/**
*
@@ -448,6 +452,8 @@ export default class EditorFile {
let doesExists = null;
this.hideQuickTools = options?.hideQuickTools || false;
+ this.paneId = options?.paneId || options?.pane?.id || null;
+ this.isPanePlaceholder = !!options?.isPanePlaceholder;
// if options are passed
if (options) {
@@ -960,6 +966,7 @@ export default class EditorFile {
markEdited() {
if (this.type !== "editor") return;
+ this.isPanePlaceholder = false;
if (this.id === config.DEFAULT_FILE_SESSION) {
this.id = helpers.uuid();
}
@@ -1198,7 +1205,11 @@ export default class EditorFile {
* @param {boolean} force if true, will prompt to save the file
*/
async remove(force = false, options = {}) {
- const { ignorePinned = false, silentPinned = false } = options || {};
+ const {
+ ignorePinned = false,
+ silentPinned = false,
+ suppressPanePlaceholder = false,
+ } = options || {};
if (this.id === config.DEFAULT_FILE_SESSION && !editorManager.files.length)
return false;
@@ -1221,9 +1232,12 @@ export default class EditorFile {
this.#destroy();
- editorManager.files = editorManager.files.filter(
- (file) => file.id !== this.id,
- );
+ const removal = editorManager.removeFileFromPane?.(this);
+ if (!removal) {
+ editorManager.files = editorManager.files.filter(
+ (file) => file.id !== this.id,
+ );
+ }
const { files, activeFile } = editorManager;
const wasActive = activeFile?.id === this.id;
if (wasActive) {
@@ -1233,8 +1247,24 @@ export default class EditorFile {
Sidebar.hide();
editorManager.activeFile = null;
new EditorFile();
+ } else if (removal?.wasPaneActive && removal.nextFile) {
+ removal.nextFile.makeActive();
+ } else if (
+ removal?.wasPaneActive &&
+ removal.pane &&
+ !removal.nextFile &&
+ !suppressPanePlaceholder
+ ) {
+ new EditorFile(config.DEFAULT_FILE_NAME, {
+ paneId: removal.pane.id,
+ text: "",
+ isUnsaved: false,
+ isPanePlaceholder: true,
+ });
} else if (wasActive) {
- files[files.length - 1].makeActive();
+ (
+ editorManager.activePane?.activeFile || files[files.length - 1]
+ ).makeActive();
}
editorManager.onupdate("remove-file");
editorManager.emit("remove-file", this);
@@ -1326,20 +1356,24 @@ export default class EditorFile {
* Makes this file active
*/
makeActive() {
- const { activeFile, editor, switchFile } = editorManager;
-
- if (activeFile) {
- if (activeFile.id === this.id) return;
+ const pane = editorManager.getFilePane?.(this) || editorManager.activePane;
+ const wasActivePane = editorManager.activePane?.id === pane?.id;
+ const { activeFile, switchFile } = editorManager;
+ const paneActiveFile = pane?.activeFile;
+
+ if (paneActiveFile && paneActiveFile.id !== this.id) {
+ paneActiveFile.focusedBefore = paneActiveFile.focused;
+ paneActiveFile.removeActive();
+ } else if (activeFile && (activeFile.id !== this.id || !wasActivePane)) {
activeFile.focusedBefore = activeFile.focused;
activeFile.removeActive();
-
- // Hide previous content if it exists
- if (activeFile.type !== "editor" && activeFile.content) {
- activeFile.content.style.display = "none";
- }
}
- switchFile(this.id);
+ if (activeFile?.id === this.id && wasActivePane) return;
+
+ switchFile(this.id, pane);
+
+ const { editor } = editorManager;
// Show/hide appropriate content
if (this.type === "editor") {
@@ -1359,7 +1393,9 @@ export default class EditorFile {
editorManager.container.style.display = "none";
if (this.content) {
this.content.style.display = "block";
- if (!this.content.parentElement) {
+ if (
+ this.content.parentElement !== editorManager.container.parentElement
+ ) {
editorManager.container.parentElement.appendChild(this.content);
}
}
@@ -1440,11 +1476,27 @@ export default class EditorFile {
this.makeActive();
if (this.id !== config.DEFAULT_FILE_SESSION) {
+ const pane = editorManager.getFilePane?.(this);
const defaultFile = editorManager.getFile(
config.DEFAULT_FILE_SESSION,
"id",
);
- defaultFile?.remove();
+ if (defaultFile && editorManager.getFilePane?.(defaultFile) === pane) {
+ defaultFile.remove();
+ }
+
+ editorManager
+ .getPaneFiles?.(this)
+ ?.filter(
+ (file) =>
+ file !== this &&
+ file.isPanePlaceholder &&
+ !file.isUnsaved &&
+ editorManager.getFilePane?.(file) === pane,
+ )
+ .forEach((file) => {
+ file.remove(true, { ignorePinned: true });
+ });
}
// Show/hide editor based on content type
@@ -1455,7 +1507,11 @@ export default class EditorFile {
editorManager.container.style.display = "none";
if (this.#content) {
this.#content.style.display = "block";
- editorManager.container.parentElement.appendChild(this.#content);
+ if (
+ this.#content.parentElement !== editorManager.container.parentElement
+ ) {
+ editorManager.container.parentElement.appendChild(this.#content);
+ }
}
}
}
diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js
index 3f96314b0..27c653e89 100644
--- a/src/lib/editorManager.js
+++ b/src/lib/editorManager.js
@@ -76,6 +76,7 @@ import quickTools from "components/quickTools";
import ScrollBar from "components/scrollbar";
import SideButton, { sideButtonContainer } from "components/sideButton";
import keyboardHandler, { keydownState } from "handlers/keyboard";
+import config from "./config";
import EditorFile from "./editorFile";
import openFile from "./openFile";
import { addedFolder } from "./openFolder";
@@ -112,6 +113,8 @@ async function EditorManager($header, $body) {
let scrollRestoreFrame = 0;
let scrollRestoreNestedFrame = 0;
let scrollRestoreTimeout = 0;
+ const MIN_PANE_WIDTH = 360;
+ const MIN_RESIZED_PANE_WIDTH = 280;
// Debounce timers for CodeMirror change handling
let checkTimeout = null;
@@ -176,12 +179,14 @@ async function EditorManager($header, $body) {
events[event].forEach((fn) => fn(...args));
},
};
- const $container =
;
- // Ensure the container participates well in flex layouts and can constrain the editor
- $container.style.flex = "1 1 auto";
- $container.style.minHeight = "0"; // allow child scroller to size correctly
- $container.style.height = "100%";
- $container.style.width = "100%";
+ let manager;
+ let paneIdCounter = 0;
+ let activePane = null;
+ let editor = null;
+ const panes = [];
+ const $paneRoot = ;
+ let $container = createEditorContainer();
+ const primaryPane = createPaneShell($container);
const problemButton = SideButton({
text: strings.problems,
icon: "warningreport_problem",
@@ -192,6 +197,151 @@ async function EditorManager($header, $body) {
},
});
+ function createEditorContainer() {
+ const $el = ;
+ // Ensure the container participates well in flex layouts and can constrain the editor.
+ $el.style.flex = "1 1 auto";
+ $el.style.minHeight = "0";
+ $el.style.height = "100%";
+ $el.style.width = "100%";
+ return $el;
+ }
+
+ function createPaneShell(editorContainer = createEditorContainer()) {
+ const pane = {
+ id: `pane-${++paneIdCounter}`,
+ files: [],
+ activeFile: null,
+ editor: null,
+ editorContainer,
+ touchSelectionController: null,
+ element: ,
+ resizeHandle: ,
+ tabList: ,
+ content: ,
+ };
+
+ pane.element.dataset.paneId = pane.id;
+ pane.tabList.dataset.paneId = pane.id;
+ pane.element.__editorPane = pane;
+ pane.tabList.__editorPane = pane;
+ pane.content.__editorPane = pane;
+ pane.resizeHandle.__editorPane = pane;
+ pane.editorContainer.__editorPane = pane;
+ pane.content.append(pane.editorContainer);
+ pane.element.append(pane.resizeHandle, pane.tabList, pane.content);
+ pane.resizeHandle.addEventListener("pointerdown", (event) => {
+ startPaneResize(event, pane);
+ });
+ pane.element.addEventListener(
+ "pointerdown",
+ () => {
+ setActivePane(pane);
+ },
+ true,
+ );
+ panes.push(pane);
+ $paneRoot.append(pane.element);
+ updatePaneResizeHandles();
+ return pane;
+ }
+
+ function updatePaneResizeHandles() {
+ panes.forEach((pane, index) => {
+ pane.resizeHandle.hidden = index === 0 || panes.length <= 1;
+ });
+ }
+
+ function startPaneResize(event, pane) {
+ const paneIndex = panes.indexOf(pane);
+ const previousPane = panes[paneIndex - 1];
+ if (!previousPane) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ const startX = event.clientX;
+ const previousRect = previousPane.element.getBoundingClientRect();
+ const paneRect = pane.element.getBoundingClientRect();
+ const totalWidth = previousRect.width + paneRect.width;
+ const minWidth = Math.min(MIN_RESIZED_PANE_WIDTH, totalWidth / 2);
+
+ document.body.classList.add("resizing-editor-pane");
+ pane.resizeHandle.setPointerCapture?.(event.pointerId);
+
+ const resize = (moveEvent) => {
+ const delta = moveEvent.clientX - startX;
+ const previousWidth = Math.max(
+ minWidth,
+ Math.min(totalWidth - minWidth, previousRect.width + delta),
+ );
+ const nextWidth = totalWidth - previousWidth;
+ previousPane.element.style.flex = `0 1 ${previousWidth}px`;
+ pane.element.style.flex = `0 1 ${nextWidth}px`;
+ };
+
+ const stop = () => {
+ document.removeEventListener("pointermove", resize);
+ document.removeEventListener("pointerup", stop);
+ document.removeEventListener("pointercancel", stop);
+ document.body.classList.remove("resizing-editor-pane");
+ };
+
+ document.addEventListener("pointermove", resize);
+ document.addEventListener("pointerup", stop);
+ document.addEventListener("pointercancel", stop);
+ }
+
+ function getActivePane() {
+ return activePane || panes[0] || null;
+ }
+
+ function setActivePane(pane, options = {}) {
+ if (!pane || activePane === pane) return pane;
+
+ activePane?.element.classList.remove("active");
+ activePane = pane;
+ editor = pane.editor || editor;
+ $container = pane.editorContainer || $container;
+ touchSelectionController = pane.touchSelectionController || null;
+ pane.element.classList.add("active");
+
+ if (manager) {
+ manager.activeFile = pane.activeFile || null;
+ updateHeaderForFile(manager.activeFile);
+ updateActivePaneScrollbars();
+ toggleProblemButton();
+ if (options.emitSwitch !== false && manager.activeFile) {
+ manager.onupdate("switch-file");
+ events.emit("switch-file", manager.activeFile);
+ }
+ if (options.configureLsp !== false) {
+ if (manager.activeFile?.type === "editor") {
+ void configureLspForFile(manager.activeFile);
+ } else {
+ detachActiveLsp();
+ }
+ }
+ }
+
+ return pane;
+ }
+
+ function updateHeaderForFile(file) {
+ if (!$header) return;
+ $header.text = file?.filename || "";
+ $header.subText = file?.headerSubtitle || "";
+ }
+
+ function updateActivePaneScrollbars() {
+ $hScrollbar?.hideImmediately?.();
+ $vScrollbar?.hideImmediately?.();
+ setVScrollValue();
+ if (!appSettings.value.textWrap) {
+ setHScrollValue();
+ }
+ }
+
const pointerCursorVisibilityExtension = EditorView.updateListener.of(
(update) => {
if (!update.transactions.length) return;
@@ -686,26 +836,40 @@ async function EditorManager($header, $body) {
];
}
- function applyOptions(keys) {
+ function applyOptions(keys, targetEditor = null) {
const filter = keys ? new Set(keys) : null;
- for (const spec of cmOptionSpecs) {
- if (filter && !spec.keys.some((k) => filter.has(k))) continue;
- const built = spec.build();
- const effects = [];
- if (spec.compartments.length === 1) {
- effects.push(spec.compartments[0].reconfigure(built));
- } else {
- const arr = Array.isArray(built) ? built : [built];
- for (let i = 0; i < spec.compartments.length; i++) {
- const comp = spec.compartments[i];
- const ext = arr[i] ?? [];
- effects.push(comp.reconfigure(ext));
+ const targetEditors = targetEditor
+ ? [targetEditor]
+ : panes.map((pane) => pane.editor).filter(Boolean);
+
+ for (const target of targetEditors) {
+ for (const spec of cmOptionSpecs) {
+ if (filter && !spec.keys.some((k) => filter.has(k))) continue;
+ const built = spec.build();
+ const effects = [];
+ if (spec.compartments.length === 1) {
+ effects.push(spec.compartments[0].reconfigure(built));
+ } else {
+ const arr = Array.isArray(built) ? built : [built];
+ for (let i = 0; i < spec.compartments.length; i++) {
+ const comp = spec.compartments[i];
+ const ext = arr[i] ?? [];
+ effects.push(comp.reconfigure(ext));
+ }
}
+ target.dispatch({ effects });
}
- editor.dispatch({ effects });
}
}
+ function setThemeForEditors(themeId) {
+ panes.forEach((pane) => {
+ if (pane.editor) {
+ applyThemeToEditor(pane.editor, themeId);
+ }
+ });
+ }
+
function buildLspMetadata(file) {
if (!file || file.type !== "editor") return null;
const uri = getFileLspUri(file);
@@ -937,32 +1101,41 @@ async function EditorManager($header, $body) {
}
// Create minimal CodeMirror editor
-
- const editorState = EditorState.create({
- doc: "",
- extensions: createMainEditorExtensions({
- // Emmet needs highest precedence so place before default keymaps
- emmetExtensions: createEmmetExtensionSet({
- syntax: EmmetKnownSyntax.html,
+ function createEmptyEditorState() {
+ return EditorState.create({
+ doc: "",
+ extensions: createMainEditorExtensions({
+ // Emmet needs highest precedence so place before default keymaps
+ emmetExtensions: createEmmetExtensionSet({
+ syntax: EmmetKnownSyntax.html,
+ }),
+ baseExtensions: createConfiguredBaseExtensions(),
+ commandKeymapExtension: getCommandKeymapExtension(),
+ themeExtension: themeCompartment.of(getConfiguredThemeExtension()),
+ pointerCursorVisibilityExtension,
+ shiftClickSelectionExtension,
+ touchSelectionUpdateExtension,
+ searchExtension: search(),
+ // Ensure read-only can be toggled later via compartment
+ readOnlyExtension: readOnlyCompartment.of(
+ EditorState.readOnly.of(false),
+ ),
+ // Editor options driven by settings via compartments
+ optionExtensions: getBaseExtensionsFromOptions(),
}),
- baseExtensions: createConfiguredBaseExtensions(),
- commandKeymapExtension: getCommandKeymapExtension(),
- themeExtension: themeCompartment.of(getConfiguredThemeExtension()),
- pointerCursorVisibilityExtension,
- shiftClickSelectionExtension,
- touchSelectionUpdateExtension,
- searchExtension: search(),
- // Ensure read-only can be toggled later via compartment
- readOnlyExtension: readOnlyCompartment.of(EditorState.readOnly.of(false)),
- // Editor options driven by settings via compartments
- optionExtensions: getBaseExtensionsFromOptions(),
- }),
- });
+ });
+ }
- const editor = new EditorView({
+ const editorState = createEmptyEditorState();
+
+ editor = new EditorView({
state: editorState,
parent: $container,
});
+ editor.__editorPane = primaryPane;
+ primaryPane.editor = editor;
+ activePane = primaryPane;
+ primaryPane.element.classList.add("active");
await applyKeyBindings(editor);
@@ -1007,6 +1180,7 @@ async function EditorManager($header, $body) {
getActiveFile: () => manager?.activeFile || null,
isShiftSelectionActive,
});
+ primaryPane.touchSelectionController = touchSelectionController;
// Provide minimal Ace-like API compatibility used by plugins
/**
@@ -1033,15 +1207,19 @@ async function EditorManager($header, $body) {
};
// Set CodeMirror theme by id registered in our registry
- editor.setTheme = function (themeId) {
+ function applyThemeToEditor(targetEditor, themeId) {
try {
const id = String(themeId || "");
const ext = getThemeExtensions(id, [oneDark]);
- editor.dispatch({ effects: themeCompartment.reconfigure(ext) });
+ targetEditor.dispatch({ effects: themeCompartment.reconfigure(ext) });
return true;
} catch (_) {
return false;
}
+ }
+
+ editor.setTheme = function (themeId) {
+ return applyThemeToEditor(editor, themeId);
};
/**
@@ -1301,6 +1479,172 @@ async function EditorManager($header, $body) {
touchSelectionController?.setMenu(!!value);
};
+ const editorCompatibilityKeys = [
+ "execCommand",
+ "commands",
+ "session",
+ "insert",
+ "setTheme",
+ "gotoLine",
+ "getCursorPosition",
+ "getSelectionRange",
+ "scrollToRow",
+ "moveCursorToPosition",
+ "getValue",
+ "selection",
+ "getCopyText",
+ "setSelection",
+ "setMenu",
+ ];
+ const editorCompatibilityDescriptors = Object.fromEntries(
+ editorCompatibilityKeys
+ .map((key) => [key, Object.getOwnPropertyDescriptor(editor, key)])
+ .filter(([, descriptor]) => descriptor),
+ );
+
+ function applyEditorCompatibility(targetEditor) {
+ Object.defineProperties(targetEditor, editorCompatibilityDescriptors);
+ }
+
+ function canCreatePane() {
+ const width =
+ $paneRoot.getBoundingClientRect?.().width || $body.clientWidth || 0;
+ if (!width) return true;
+ return width / (panes.length + 1) >= MIN_PANE_WIDTH;
+ }
+
+ function createUntitledPaneFile(pane) {
+ return new EditorFile(config.DEFAULT_FILE_NAME, {
+ paneId: pane.id,
+ text: "",
+ isUnsaved: false,
+ isPanePlaceholder: true,
+ });
+ }
+
+ async function createPane(options = {}) {
+ if (!canCreatePane()) {
+ window.toast?.(
+ strings["not enough space"] ||
+ "Not enough space to create another editor pane.",
+ );
+ return null;
+ }
+
+ const pane = createPaneShell();
+ const paneEditor = new EditorView({
+ state: createEmptyEditorState(),
+ parent: pane.editorContainer,
+ });
+ pane.editor = paneEditor;
+ paneEditor.__editorPane = pane;
+ applyEditorCompatibility(paneEditor);
+ await applyKeyBindings(paneEditor);
+ pane.touchSelectionController = createTouchSelectionMenu(paneEditor, {
+ container: pane.editorContainer,
+ getActiveFile: () => pane.activeFile || null,
+ isShiftSelectionActive,
+ });
+ try {
+ paneEditor.dispatch({
+ effects: StateEffect.appendConfig.of(getDocSyncListener()),
+ });
+ } catch (error) {
+ warnRecoverable(
+ "Failed to attach document sync listener to split editor.",
+ error,
+ `doc-sync-listener-${pane.id}`,
+ );
+ }
+ await setupEditor(pane);
+ $paneRoot.classList.toggle("multi-pane", panes.length > 1);
+ updatePaneResizeHandles();
+ syncOpenFileList();
+
+ if (options.moveFile) {
+ moveFileToPane(options.moveFile, pane, { activate: true });
+ } else if (options.createUntitled !== false) {
+ createUntitledPaneFile(pane);
+ } else if (options.activate !== false) {
+ setActivePane(pane);
+ pane.editor?.focus();
+ }
+
+ return pane;
+ }
+
+ function splitPane() {
+ return createPane();
+ }
+
+ async function moveActiveFileToNewPane() {
+ const file = manager.activeFile;
+ if (!file) return null;
+ return createPane({ moveFile: file });
+ }
+
+ function closeActivePane() {
+ const pane = getActivePane();
+ if (!pane || panes.length <= 1) return false;
+
+ const paneIndex = panes.indexOf(pane);
+ const targetPane =
+ panes[paneIndex - 1] || panes[paneIndex + 1] || panes[0] || null;
+ if (!targetPane) return false;
+
+ for (const file of [...pane.files]) {
+ if (file.isPanePlaceholder && !file.isUnsaved) {
+ file.remove(true, {
+ ignorePinned: true,
+ suppressPanePlaceholder: true,
+ });
+ continue;
+ }
+
+ moveFileToPane(file, targetPane, {
+ activate: false,
+ createSourcePlaceholder: false,
+ });
+ }
+
+ pane.editor?.destroy?.();
+ pane.element.remove();
+ panes.splice(paneIndex, 1);
+ $paneRoot.classList.toggle("multi-pane", panes.length > 1);
+ updatePaneResizeHandles();
+ rebuildFileListFromPanes();
+ const fileToActivate = targetPane.activeFile;
+ targetPane.activeFile = null;
+ setActivePane(targetPane, { emitSwitch: false });
+ fileToActivate?.makeActive();
+ syncOpenFileList();
+ return true;
+ }
+
+ function focusPaneByOffset(offset) {
+ if (panes.length <= 1) return false;
+ const index = Math.max(0, panes.indexOf(getActivePane()));
+ const nextPane = panes[(index + offset + panes.length) % panes.length];
+ if (!nextPane) return false;
+ const fileToActivate = nextPane.activeFile;
+ nextPane.activeFile = null;
+ setActivePane(nextPane, { emitSwitch: false });
+ if (fileToActivate) {
+ fileToActivate.makeActive();
+ } else {
+ nextPane.editor?.focus();
+ }
+ return true;
+ }
+
+ function focusNextPane() {
+ return focusPaneByOffset(1);
+ }
+
+ function focusPreviousPane() {
+ return focusPaneByOffset(-1);
+ }
+
function getEditorExtensionSignature(file) {
return JSON.stringify({
syntax: getEmmetSyntaxForFile(file),
@@ -1715,31 +2059,54 @@ async function EditorManager($header, $body) {
parent: $body,
placement: "bottom",
});
- const manager = {
+ manager = {
files: [],
onupdate: () => {},
activeFile: null,
isCodeMirror: true,
addFile,
- editor,
readOnlyCompartment,
getFile,
+ getFilePane,
+ getPaneFiles,
+ setActivePane,
reapplyActiveFile,
switchFile,
+ createPane,
+ splitPane,
+ closeActivePane,
+ focusNextPane,
+ focusPreviousPane,
+ moveActiveFileToNewPane,
+ moveFileToPane,
+ removeFileFromPane,
moveFileByPinnedState,
normalizePinnedTabOrder,
+ updatePaneFileOrderFromTabs,
syncOpenFileList,
hasUnsavedFiles,
getEditorHeight,
getEditorWidth,
header: $header,
- container: $container,
getLspMetadata: buildLspMetadata,
+ get editor() {
+ return getActivePane()?.editor || editor;
+ },
+ get activePane() {
+ return getActivePane();
+ },
+ get panes() {
+ return panes.slice();
+ },
+ get container() {
+ return getActivePane()?.editorContainer || $container;
+ },
get isScrolling() {
return isScrolling;
},
get openFileList() {
if (!$openFileList) initFileTabContainer();
+ if (isPaneTabLayout()) return getActivePane()?.tabList || $openFileList;
return $openFileList;
},
get TIMEOUT_VALUE() {
@@ -1884,9 +2251,9 @@ async function EditorManager($header, $body) {
});
applyLspSettings();
- $body.append($container);
+ $body.append($paneRoot);
initModes(); // Initialize CodeMirror modes
- await setupEditor();
+ await setupEditor(primaryPane);
// Initialize theme from settings or fallback
try {
@@ -2047,7 +2414,7 @@ async function EditorManager($header, $body) {
appSettings.on("update:editorTheme", function () {
const desiredTheme = appSettings?.value?.editorTheme || "one_dark";
- editor.setTheme(desiredTheme);
+ setThemeForEditors(desiredTheme);
applyOptions(["rainbowBrackets"]);
});
@@ -2108,7 +2475,8 @@ async function EditorManager($header, $body) {
// Keep file.session and cache in sync on every edit
function getDocSyncListener() {
return EditorView.updateListener.of((update) => {
- const file = manager.activeFile;
+ const pane = update.view.__editorPane || getActivePane();
+ const file = pane?.activeFile || manager.activeFile;
if (!file || file.type !== "editor") return;
if (update.docChanged) {
@@ -2221,10 +2589,9 @@ async function EditorManager($header, $body) {
*/
function addFile(file) {
if (manager.files.includes(file)) return;
- const insertAt = file.pinned
- ? getPinnedInsertIndex()
- : manager.files.length;
- manager.files.splice(insertAt, 0, file);
+ const pane = getPaneById(file.paneId) || getActivePane() || primaryPane;
+ insertFileIntoPane(file, pane);
+ rebuildFileListFromPanes();
syncOpenFileList();
if (!manager.activeFile) {
$header.text = file.name;
@@ -2232,14 +2599,47 @@ async function EditorManager($header, $body) {
toggleProblemButton();
}
- function getPinnedInsertIndex(skipFile = null) {
- return manager.files.reduce((count, file) => {
+ function getPinnedInsertIndex(files, skipFile = null) {
+ return files.reduce((count, file) => {
if (file === skipFile) return count;
return count + (file.pinned ? 1 : 0);
}, 0);
}
+ function insertFileIntoPane(file, pane, index = null) {
+ if (!file || !pane) return;
+ const oldPane = getFilePane(file);
+ if (oldPane) {
+ oldPane.files = oldPane.files.filter((paneFile) => paneFile !== file);
+ }
+ file.paneId = pane.id;
+ const insertAt =
+ Number.isInteger(index) && index >= 0
+ ? Math.min(index, pane.files.length)
+ : file.pinned
+ ? getPinnedInsertIndex(pane.files)
+ : pane.files.length;
+ pane.files.splice(insertAt, 0, file);
+ }
+
+ function rebuildFileListFromPanes() {
+ manager.files = panes.flatMap((pane) => pane.files);
+ return manager.files;
+ }
+
function syncOpenFileList() {
+ if (isPaneTabLayout()) {
+ $paneRoot.classList.remove("hide-pane-tabs");
+ panes.forEach((pane) => {
+ pane.files.forEach((file) => {
+ pane.tabList.append(file.tab);
+ });
+ pane.element.classList.toggle("empty", pane.files.length === 0);
+ });
+ return;
+ }
+
+ $paneRoot.classList.add("hide-pane-tabs");
const $list = manager.openFileList;
manager.files.forEach((file) => {
$list.append(file.tab);
@@ -2247,17 +2647,44 @@ async function EditorManager($header, $body) {
}
function moveFileByPinnedState(file) {
- if (!manager.files.includes(file)) return;
+ const pane = getFilePane(file);
+ if (!pane) return;
+ pane.files = normalizePinnedFiles(pane.files);
+ rebuildFileListFromPanes();
+ syncOpenFileList();
if (manager.activeFile?.id === file.id) {
file.tab.scrollIntoView();
}
}
function normalizePinnedTabOrder(nextFiles = manager.files) {
+ const pane =
+ nextFiles.length &&
+ nextFiles.every((file) => getFilePane(file) === getFilePane(nextFiles[0]))
+ ? getFilePane(nextFiles[0])
+ : null;
+
+ if (pane) {
+ pane.files = normalizePinnedFiles(nextFiles);
+ rebuildFileListFromPanes();
+ syncOpenFileList();
+ return pane.files;
+ }
+
+ panes.forEach((pane) => {
+ pane.files = normalizePinnedFiles(pane.files);
+ });
+ rebuildFileListFromPanes();
+ syncOpenFileList();
+
+ return manager.files;
+ }
+
+ function normalizePinnedFiles(files) {
const pinnedFiles = [];
const regularFiles = [];
- nextFiles.forEach((file) => {
+ files.forEach((file) => {
if (file.pinned) {
pinnedFiles.push(file);
return;
@@ -2265,17 +2692,143 @@ async function EditorManager($header, $body) {
regularFiles.push(file);
});
- manager.files = [...pinnedFiles, ...regularFiles];
+ return [...pinnedFiles, ...regularFiles];
+ }
+
+ function getPaneById(id) {
+ if (!id) return null;
+ return panes.find((pane) => pane.id === id) || null;
+ }
+
+ function getFilePane(fileOrId) {
+ const id = typeof fileOrId === "string" ? fileOrId : fileOrId?.id || null;
+ if (!id) return null;
+ return (
+ panes.find((pane) => pane.files.some((file) => file.id === id)) || null
+ );
+ }
+
+ function getPaneFiles(fileOrPane = getActivePane()) {
+ const pane = fileOrPane?.files ? fileOrPane : getFilePane(fileOrPane);
+ return pane?.files || manager.files;
+ }
+
+ function moveFileToPane(file, targetPane, options = {}) {
+ if (!file || !targetPane) return false;
+ const {
+ activate = true,
+ index = null,
+ createSourcePlaceholder = true,
+ } = options;
+ const sourcePane = getFilePane(file);
+ if (sourcePane === targetPane) {
+ if (activate) file.makeActive();
+ return true;
+ }
+
+ if (sourcePane?.activeFile?.id === file.id && file.type === "editor") {
+ const sourceEditor = sourcePane.editor;
+ file.session = getRawEditorState(sourceEditor?.state);
+ file.lastScrollTop = sourceEditor?.scrollDOM?.scrollTop || 0;
+ file.lastScrollLeft = sourceEditor?.scrollDOM?.scrollLeft || 0;
+ }
+
+ insertFileIntoPane(file, targetPane, index);
+ if (!targetPane.activeFile && !activate) {
+ targetPane.activeFile = file;
+ }
+ rebuildFileListFromPanes();
syncOpenFileList();
- return manager.files;
+ if (sourcePane?.activeFile?.id === file.id) {
+ const nextSourceFile =
+ sourcePane.files[sourcePane.files.length - 1] || null;
+ sourcePane.activeFile = null;
+ if (nextSourceFile) {
+ nextSourceFile.makeActive();
+ } else if (createSourcePlaceholder) {
+ sourcePane.editor?.setState(createEmptyEditorState());
+ sourcePane.editorContainer.style.display = "block";
+ createUntitledPaneFile(sourcePane);
+ }
+ }
+
+ if (activate) {
+ file.makeActive();
+ }
+ return true;
+ }
+
+ function removeFileFromPane(file) {
+ const pane = getFilePane(file);
+ if (!pane) return null;
+ const wasPaneActive = pane.activeFile?.id === file.id;
+ pane.files = pane.files.filter((paneFile) => paneFile !== file);
+ let nextFile = pane.activeFile;
+ if (wasPaneActive) {
+ nextFile = pane.files[pane.files.length - 1] || null;
+ pane.activeFile = null;
+ if (!nextFile) {
+ pane.editor?.setState(createEmptyEditorState());
+ pane.editorContainer.style.display = "block";
+ }
+ }
+ rebuildFileListFromPanes();
+ syncOpenFileList();
+ return { pane, wasPaneActive, nextFile };
+ }
+
+ function updatePaneFileOrderFromTabs($tabList) {
+ const pane = $tabList?.__editorPane;
+ if (!pane) return false;
+
+ const nextFiles = [...$tabList.children]
+ .map(($tab) => pane.files.find((file) => file.tab === $tab))
+ .filter(Boolean);
+ if (!nextFiles.length) return false;
+
+ pane.files = nextFiles;
+ const pinnedCount = pane.files.filter((file) => file.pinned).length;
+ const draggedFile = pane.files.find(
+ (file) => file.tab?.style.opacity === "0.35",
+ );
+ if (draggedFile) {
+ const draggedIndex = pane.files.indexOf(draggedFile);
+ let nextPinnedState;
+
+ if (!draggedFile.pinned && draggedIndex < pinnedCount) {
+ nextPinnedState = true;
+ } else if (draggedFile.pinned && draggedIndex >= pinnedCount) {
+ nextPinnedState = false;
+ }
+
+ if (nextPinnedState !== undefined) {
+ draggedFile.setPinnedState(nextPinnedState, { reorder: false });
+ pane.files = normalizePinnedFiles(pane.files);
+ }
+ }
+
+ rebuildFileListFromPanes();
+ syncOpenFileList();
+ return true;
+ }
+
+ function isPaneTabLayout() {
+ const { openFileListPos } = appSettings.value;
+ return (
+ openFileListPos === appSettings.OPEN_FILE_LIST_POS_HEADER ||
+ openFileListPos === appSettings.OPEN_FILE_LIST_POS_BOTTOM
+ );
}
/**
* Sets up the editor with various configurations and event listeners.
* @returns {Promise} A promise that resolves once the editor is set up.
*/
- async function setupEditor() {
+ async function setupEditor(pane = getActivePane()) {
+ const editor = pane?.editor;
+ const touchSelectionController = pane?.touchSelectionController;
+ if (!pane || !editor) return;
const settings = appSettings.value;
const { leftMargin, textWrap, colorPreview, fontSize, lineHeight } =
appSettings.value;
@@ -2291,6 +2844,7 @@ async function EditorManager($header, $body) {
const scroller = editor.scrollDOM;
function syncScrollUi() {
+ if (pane !== activePane) return;
scrollSyncRaf = 0;
editor.requestMeasure({
read: () => readScrollMetrics(),
@@ -2299,6 +2853,7 @@ async function EditorManager($header, $body) {
}
function handleEditorScroll() {
+ if (pane !== activePane) return;
if (!scroller) return;
if (restoreScrollbarScrollLock()) return;
if (!isScrolling) {
@@ -2355,8 +2910,9 @@ async function EditorManager($header, $body) {
setNativeContextMenuDisabled(isFocused);
contentDOM.addEventListener("focus", (_event) => {
+ setActivePane(pane);
setNativeContextMenuDisabled(true);
- const { activeFile } = manager;
+ const activeFile = pane.activeFile;
if (activeFile) {
activeFile.focused = true;
}
@@ -2369,7 +2925,7 @@ async function EditorManager($header, $body) {
const { hardKeyboardHidden, keyboardHeight } =
await getSystemConfiguration();
const blur = () => {
- const { activeFile } = manager;
+ const activeFile = pane.activeFile;
if (activeFile) {
activeFile.focused = false;
activeFile.focusedBefore = false;
@@ -2807,8 +3363,9 @@ async function EditorManager($header, $body) {
function getDiagnosticStateForFile(file) {
if (!file || file.type !== "editor") return null;
- if (manager.activeFile?.id === file.id && editor?.state) {
- return editor.state;
+ const pane = getFilePane(file);
+ if (pane?.activeFile?.id === file.id && pane.editor?.state) {
+ return pane.editor.state;
}
return file.session || null;
}
@@ -2853,26 +3410,28 @@ async function EditorManager($header, $body) {
* Switches the active file in the editor.
* @param {string} id - The ID of the file to switch to.
*/
- function switchFile(id) {
- const { id: activeFileId } = manager.activeFile || {};
- if (activeFileId === id) return;
+ function switchFile(id, targetPane = null) {
+ const pane = targetPane || getFilePane(id) || getActivePane();
+ if (!pane) return;
+ const paneActiveFile = pane.activeFile;
+ if (paneActiveFile?.id === id && activePane === pane) return;
const file = manager.getFile(id);
if (!file) return;
- manager.activeFile?.tab.classList.remove("active");
+ setActivePane(pane, { emitSwitch: false });
// Hide previous content if it was non-editor
- if (manager.activeFile?.type !== "editor" && manager.activeFile?.content) {
- manager.activeFile.content.style.display = "none";
+ if (paneActiveFile?.type !== "editor" && paneActiveFile?.content) {
+ paneActiveFile.content.style.display = "none";
}
// Persist the previous editor's state before switching away
- const prev = manager.activeFile;
+ const prev = paneActiveFile;
if (prev?.type === "editor") {
- prev.session = getRawEditorState(editor.state);
- prev.lastScrollTop = editor.scrollDOM?.scrollTop || 0;
- prev.lastScrollLeft = editor.scrollDOM?.scrollLeft || 0;
+ prev.session = getRawEditorState(pane.editor.state);
+ prev.lastScrollTop = pane.editor.scrollDOM?.scrollTop || 0;
+ prev.lastScrollLeft = pane.editor.scrollDOM?.scrollLeft || 0;
window.setTimeout(() => {
prev.flushCacheWrite?.().catch((error) => {
warnRecoverable(
@@ -2884,21 +3443,22 @@ async function EditorManager($header, $body) {
}, 1000);
}
+ paneActiveFile?.tab.classList.remove("active");
+ pane.activeFile = file;
manager.activeFile = file;
file.tab.classList.add("active");
file.tab.scrollIntoView();
- $header.text = file.filename;
- $header.subText = file.headerSubtitle || "";
+ updateHeaderForFile(file);
if (file.type === "editor") {
- touchSelectionController?.setEnabled(true);
+ pane.touchSelectionController?.setEnabled(true);
if (!file.loaded && !file.loading) {
showLoadingEditor(file);
} else {
// Apply active file content and language to CodeMirror
applyFileToEditor(file);
}
- $container.style.display = "block";
+ pane.editorContainer.style.display = "block";
$hScrollbar.hideImmediately();
$vScrollbar.hideImmediately();
@@ -2908,12 +3468,12 @@ async function EditorManager($header, $body) {
setHScrollValue();
}
} else {
- touchSelectionController?.setEnabled(false);
- $container.style.display = "none";
+ pane.touchSelectionController?.setEnabled(false);
+ pane.editorContainer.style.display = "none";
if (file.content) {
file.content.style.display = "block";
- if (!file.content.parentElement) {
- $container.parentElement.appendChild(file.content);
+ if (file.content.parentElement !== pane.content) {
+ pane.content.appendChild(file.content);
}
}
}
@@ -2935,7 +3495,10 @@ async function EditorManager($header, $body) {
function initFileTabContainer() {
let $list;
- if ($openFileList) {
+ if (
+ $openFileList &&
+ !$openFileList.classList.contains("editor-pane-tabs")
+ ) {
if ($openFileList.classList.contains("collapsible")) {
$list = Array.from($openFileList.$ul.children);
} else {
@@ -2946,30 +3509,14 @@ async function EditorManager($header, $body) {
// show open file list in header
const { openFileListPos } = appSettings.value;
- if (
- openFileListPos === appSettings.OPEN_FILE_LIST_POS_HEADER ||
- openFileListPos === appSettings.OPEN_FILE_LIST_POS_BOTTOM
- ) {
- if (!$openFileList?.classList.contains("open-file-list")) {
- $openFileList = ;
- }
- if ($list) $openFileList.append(...$list);
-
- if (openFileListPos === appSettings.OPEN_FILE_LIST_POS_BOTTOM) {
- $container.parentElement.insertAdjacentElement(
- "afterend",
- $openFileList,
- );
- } else {
- $header.insertAdjacentElement("afterend", $openFileList);
- }
-
- root.classList.add("top-bar");
-
- const oldAppend = $openFileList.append;
- $openFileList.append = (...args) => {
- oldAppend.apply($openFileList, args);
- };
+ if (isPaneTabLayout()) {
+ $openFileList = getActivePane()?.tabList || null;
+ $paneRoot.dataset.tabsPosition =
+ openFileListPos === appSettings.OPEN_FILE_LIST_POS_BOTTOM
+ ? "bottom"
+ : "top";
+ root.classList.remove("top-bar");
+ syncOpenFileList();
} else {
$openFileList = list(strings["active files"]);
$openFileList.classList.add("file-list");
@@ -2984,6 +3531,7 @@ async function EditorManager($header, $body) {
const files = sidebarApps.get("files");
files.insertBefore($openFileList, files.firstElementChild);
root.classList.remove("top-bar");
+ syncOpenFileList();
}
root.setAttribute("open-file-list-pos", openFileListPos);
diff --git a/src/lib/openFile.js b/src/lib/openFile.js
index c803a55c2..bbb5e5812 100644
--- a/src/lib/openFile.js
+++ b/src/lib/openFile.js
@@ -21,6 +21,7 @@ import appSettings from "./settings";
* @property {string} encoding
* @property {string} mode
* @property {string} uri
+ * @property {string} paneId
*/
/**
@@ -36,7 +37,7 @@ export default async function openFile(file, options = {}) {
/**@type {EditorFile} */
const existingFile = editorManager.getFile(uri, "uri");
- const { cursorPos, render, onsave, text, mode, encoding } = options;
+ const { cursorPos, render, onsave, text, mode, encoding, paneId } = options;
if (existingFile) {
// If file is already opened and new text is provided
@@ -44,7 +45,16 @@ export default async function openFile(file, options = {}) {
text != null ? Text.of(String(text).split("\n")) : null;
// If file is already opened
- existingFile.makeActive();
+ const targetPane = paneId
+ ? editorManager.panes?.find((pane) => pane.id === paneId)
+ : null;
+ if (targetPane) {
+ editorManager.moveFileToPane?.(existingFile, targetPane, {
+ activate: true,
+ });
+ } else {
+ existingFile.makeActive();
+ }
const { editor } = editorManager;
@@ -109,6 +119,7 @@ export default async function openFile(file, options = {}) {
SAFMode: mode,
savedMtime: helpers.getStatMtime(fileInfo),
diskMtime: helpers.getStatMtime(fileInfo),
+ paneId,
});
};
@@ -181,6 +192,7 @@ export default async function openFile(file, options = {}) {
content: videoContainer,
render: true,
hideQuickTools: true,
+ paneId,
});
return;
}
@@ -349,6 +361,7 @@ export default async function openFile(file, options = {}) {
content: imageContainer,
render: true,
hideQuickTools: true,
+ paneId,
});
return;
}
@@ -377,6 +390,7 @@ export default async function openFile(file, options = {}) {
content: audioPlayer.container,
render: true,
hideQuickTools: true,
+ paneId,
});
audioTab.onclose = () => {
audioPlayer.cleanup();
diff --git a/src/styles/codemirror.scss b/src/styles/codemirror.scss
index f0321b160..d3b91e8f3 100644
--- a/src/styles/codemirror.scss
+++ b/src/styles/codemirror.scss
@@ -2,6 +2,130 @@
position: relative;
}
+.editor-pane-root {
+ display: flex;
+ flex: 1 1 auto;
+ width: 100%;
+ height: 100%;
+ min-height: 0;
+ min-width: 0;
+ overflow: hidden;
+
+ &[data-tabs-position="bottom"] {
+ .editor-pane-tabs {
+ order: 2;
+ border-top: 1px solid var(--border-color);
+ border-bottom: 0;
+ }
+ }
+
+ &.hide-pane-tabs {
+ .editor-pane-tabs {
+ display: none;
+ }
+ }
+}
+
+.editor-pane {
+ position: relative;
+ display: flex;
+ flex: 1 1 0;
+ flex-direction: column;
+ min-width: min(360px, 100%);
+ min-height: 0;
+ overflow: hidden;
+ border-left: 1px solid var(--border-color);
+ background: var(--secondary-color);
+
+ &:first-child {
+ border-left: 0;
+ }
+}
+
+.editor-pane-root.multi-pane {
+ .editor-pane.active {
+ box-shadow: inset 0 2px 0 var(--active-color);
+ }
+
+ .editor-pane-resize-handle:not([hidden]) {
+ display: block;
+ }
+}
+
+.editor-pane-root:not(.multi-pane) {
+ .editor-pane-tabs {
+ border-top: 0;
+ border-bottom: 0;
+ }
+}
+
+.editor-pane-resize-handle {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: -3px;
+ z-index: 5;
+ display: none;
+ width: 6px;
+ cursor: col-resize;
+ touch-action: none;
+
+ &::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 2px;
+ width: 1px;
+ background: var(--border-color);
+ }
+
+ &:hover::after,
+ &:active::after {
+ background: var(--active-color);
+ }
+}
+
+body.resizing-editor-pane {
+ cursor: col-resize;
+ user-select: none;
+}
+
+.editor-pane-tabs {
+ flex: 0 0 30px;
+ border-bottom: 1px solid var(--border-color);
+
+ li.tile {
+ min-width: min(var(--file-tab-width), 44%);
+ max-width: var(--file-tab-width);
+ }
+}
+
+.editor-pane-content {
+ position: relative;
+ flex: 1 1 auto;
+ min-height: 0;
+ min-width: 0;
+ overflow: hidden;
+}
+
+.editor-pane-content > .editor-container {
+ height: 100%;
+}
+
+@media (max-width: 720px) {
+ .editor-pane-root.multi-pane {
+ .editor-pane {
+ display: none;
+ min-width: 100%;
+ }
+
+ .editor-pane.active {
+ display: flex;
+ }
+ }
+}
+
.editor-container > .cursor-menu {
z-index: 600;
}
From e04709047da6097b57c41c6c566c6ae50ebed8de Mon Sep 17 00:00:00 2001
From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com>
Date: Mon, 29 Jun 2026 12:01:32 +0530
Subject: [PATCH 02/20] add ability to drag one tab from one pane to other
---
src/handlers/editorFileTab.js | 172 ++++++++++++++++++++++++++--------
1 file changed, 131 insertions(+), 41 deletions(-)
diff --git a/src/handlers/editorFileTab.js b/src/handlers/editorFileTab.js
index dae21b84f..c7180d742 100644
--- a/src/handlers/editorFileTab.js
+++ b/src/handlers/editorFileTab.js
@@ -1,5 +1,6 @@
import config from "lib/config";
import settings from "lib/settings";
+import { animate } from "motion";
const opts = { passive: false };
@@ -18,6 +19,9 @@ let $tab = null;
* @type {HTMLDivElement}
*/
let $parent = null;
+let $originParent = null;
+let draggedFile = null;
+let allowPaneTransfer = true;
let MAX_SCROLL = 0;
let MIN_SCROLL = 0;
@@ -75,11 +79,11 @@ let didReorder = false;
const MIN_SCROLL_SPEED = 2;
const MAX_SCROLL_SPEED = 14;
-const REORDER_DURATION = 280;
-const RELEASE_DURATION = 250;
-const SPRING_EASING = "cubic-bezier(0.2, 1.2, 0.4, 1)";
+const REORDER_DURATION = 0.28;
+const RELEASE_DURATION = 0.25;
+const SPRING_EASING = [0.2, 1, 0.4, 1];
-/** @type {WeakMap} */
+/** @type {WeakMap} */
const reorderAnimations = new WeakMap();
/**
@@ -98,8 +102,12 @@ export default function startDrag(e) {
navigator.vibrate(config.VIBRATION_TIME);
}
- $tab = e.target;
+ $tab = e.currentTarget || e.target.closest?.(".tile") || e.target;
$parent = $tab.parentElement;
+ $originParent = $parent;
+ draggedFile = editorManager.files.find((file) => file.tab === $tab) || null;
+ allowPaneTransfer =
+ !!draggedFile && !(draggedFile.isPanePlaceholder && !draggedFile.isUnsaved);
$tabClone = $tab.cloneNode(true);
initialNextSibling = $tab.nextElementSibling;
didReorder = false;
@@ -196,11 +204,18 @@ function releaseDrag(e) {
/**@type {HTMLDivElement} target tab */
const $target = document.elementFromPoint(clientX, clientY);
- const shouldCommitReorder = $parent.contains($target);
+ const $dropParent = getDropTabList(clientX, clientY);
+ if ($dropParent && $dropParent !== $parent) {
+ moveDragToParent($dropParent);
+ }
+ const shouldCommitReorder =
+ $parent.contains($target) || $dropParent === $parent;
if (shouldCommitReorder) {
updateDragPreview(clientX, clientY);
- if (didReorder) {
+ if ($parent !== $originParent) {
+ commitPaneTransfer();
+ } else if (didReorder) {
updateFileList($parent);
}
} else if (
@@ -245,18 +260,16 @@ function finishDrag(shouldSettleClone) {
if (shouldSettleClone) {
const rect = $tab.getBoundingClientRect();
- const anim = $tabClone.animate(
- [{ transform: `translate3d(${rect.left}px, ${rect.top}px, 0)` }],
+ animate(
+ $tabClone,
+ { transform: `translate3d(${rect.left}px, ${rect.top}px, 0)` },
{
duration: document.body.classList.contains("no-animation")
? 0
: RELEASE_DURATION,
- easing: SPRING_EASING,
- fill: "forwards",
+ ease: SPRING_EASING,
},
- );
- anim.onfinish = cleanupDrag;
- anim.oncancel = cleanupDrag;
+ ).then(cleanupDrag);
return;
}
@@ -267,6 +280,9 @@ function cleanupDrag() {
$tab.style.opacity = "";
$tabClone.remove();
$tabClone = null;
+ $originParent = null;
+ draggedFile = null;
+ allowPaneTransfer = true;
initialNextSibling = null;
didReorder = false;
}
@@ -277,16 +293,19 @@ function preventDefaultScroll() {
function updateDragPreview(clientX, clientY) {
const $target = document.elementFromPoint(clientX, clientY);
- if (
- !$parent.contains($target) ||
- $target === $tab ||
- $tab.contains($target)
- ) {
+ const $dropParent = getDropTabList(clientX, clientY);
+ if ($dropParent && $dropParent !== $parent) {
+ moveDragToParent($dropParent);
+ }
+
+ if (!$target || $target === $tab || $tab.contains($target)) {
return;
}
const $targetTab = $target.closest(".tile");
- if (!$targetTab) return;
+ if (!$targetTab || !$parent.contains($targetTab)) {
+ return;
+ }
const rect = $targetTab.getBoundingClientRect();
const midX = rect.left + rect.width / 2;
@@ -302,11 +321,49 @@ function updateDragPreview(clientX, clientY) {
}
function restoreInitialTabPosition() {
+ if ($parent !== $originParent) {
+ moveDragToParent($originParent, initialNextSibling);
+ return;
+ }
+
reorderTab(
initialNextSibling?.parentElement === $parent ? initialNextSibling : null,
);
}
+function moveDragToParent($nextParent, $insertBefore = null) {
+ if (!$nextParent || $nextParent === $parent) return;
+
+ const previousParents = [$parent, $nextParent].filter(Boolean);
+ const previousRects = captureVisualPositionsForParents(previousParents);
+ $parent.removeEventListener("scroll", preventDefaultScroll, opts);
+
+ if ($insertBefore?.parentElement === $nextParent) {
+ $nextParent.insertBefore($tab, $insertBefore);
+ } else {
+ $nextParent.appendChild($tab);
+ }
+
+ previousParents.forEach(($list) => animateTabReorder($list, previousRects));
+ $parent = $nextParent;
+ updateParentMetrics();
+ prevScrollLeft = $parent.scrollLeft;
+ $parent.addEventListener("scroll", preventDefaultScroll, opts);
+ didReorder = true;
+}
+
+function commitPaneTransfer() {
+ const targetPane = $parent?.__editorPane;
+ if (!targetPane || !draggedFile || !allowPaneTransfer) return;
+
+ const index = [...$parent.children].indexOf($tab);
+ editorManager.moveFileToPane?.(draggedFile, targetPane, {
+ activate: true,
+ index,
+ });
+ editorManager.updatePaneFileOrderFromTabs?.($parent);
+}
+
function reorderTab($insertBefore) {
const previousRects = captureVisualPositions($parent);
@@ -335,10 +392,47 @@ function captureVisualPositions($parent) {
);
}
+function captureVisualPositionsForParents(parents) {
+ const rects = new Map();
+ parents.forEach(($list) => {
+ if (!$list) return;
+ for (const $child of $list.children) {
+ rects.set($child, $child.getBoundingClientRect());
+ }
+ });
+ return rects;
+}
+
+function updateParentMetrics() {
+ const parentRect = $parent.getBoundingClientRect();
+ parentLeft = parentRect.left;
+ parentRight = parentRect.right;
+ MAX_SCROLL = $parent.scrollWidth - parentRect.width;
+ MIN_SCROLL = 0;
+}
+
+function getDropTabList(clientX, clientY) {
+ const $target = document.elementFromPoint(clientX, clientY);
+ const $tabList = $target?.closest?.(".editor-pane-tabs");
+ if (isValidDropTabList($tabList)) return $tabList;
+
+ const pane = $target?.closest?.(".editor-pane")?.__editorPane;
+ if (isValidDropTabList(pane?.tabList)) return pane.tabList;
+
+ return $parent;
+}
+
+function isValidDropTabList($tabList) {
+ if (!$tabList?.__editorPane) return false;
+ if ($tabList === $originParent) return true;
+ if (!allowPaneTransfer) return false;
+ return $tabList.getClientRects().length > 0;
+}
+
/**
* Animates the visual change after the DOM order is updated using FLIP.
- * Uses WAAPI directly for reliable mid-animation compositing and to
- * properly respect the app's no-animation setting.
+ * Uses Motion for reliable mid-animation compositing and to respect the
+ * app's no-animation setting.
* @param {HTMLElement} $parent
* @param {Map} previousRects
*/
@@ -348,7 +442,7 @@ function animateTabReorder($parent, previousRects) {
const oldAnim = reorderAnimations.get($child);
if (oldAnim) {
- oldAnim.cancel();
+ oldAnim.cancel?.();
reorderAnimations.delete($child);
}
@@ -361,32 +455,28 @@ function animateTabReorder($parent, previousRects) {
if (Math.abs(deltaX) < 0.5 && Math.abs(deltaY) < 0.5) continue;
- const anim = $child.animate(
- [
- { transform: `translate3d(${deltaX}px, ${deltaY}px, 0)` },
- { transform: "translate3d(0, 0, 0)" },
- ],
+ const anim = animate(
+ $child,
+ {
+ transform: [
+ `translate3d(${deltaX}px, ${deltaY}px, 0)`,
+ "translate3d(0, 0, 0)",
+ ],
+ },
{
duration: document.body.classList.contains("no-animation")
? 0
: REORDER_DURATION,
- easing: SPRING_EASING,
- fill: "none",
- composite: "replace",
+ ease: SPRING_EASING,
},
);
reorderAnimations.set($child, anim);
- anim.onfinish = () => {
- if (reorderAnimations.get($child) === anim) {
- reorderAnimations.delete($child);
- }
- };
- anim.oncancel = () => {
+ anim.then(() => {
if (reorderAnimations.get($child) === anim) {
reorderAnimations.delete($child);
}
- };
+ });
}
}
@@ -394,13 +484,13 @@ function animateTabReorder($parent, previousRects) {
* Scrolls the container using animation frame
*/
function scrollContainer() {
- return animate();
+ return step();
- function animate() {
+ function step() {
const scroll = getScroll();
if (!scroll) return;
prevScrollLeft = $parent.scrollLeft += scroll;
- animationFrame = requestAnimationFrame(animate);
+ animationFrame = requestAnimationFrame(step);
}
}
From f35b1c2c3a154d052f0a5cb41f7f141bd81182f3 Mon Sep 17 00:00:00 2001
From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com>
Date: Mon, 29 Jun 2026 12:02:36 +0530
Subject: [PATCH 03/20] add vertical pane support and keybinds and bug fixes
- added vertical pane creation too
- vscode style keybinds
- resizing of pane
- few bug fixes
---
src/lib/commands.js | 21 ++
src/lib/editorManager.js | 442 ++++++++++++++++++++++++++++++++-----
src/lib/keyBindings.js | 70 ++++++
src/styles/codemirror.scss | 86 ++++++--
4 files changed, 547 insertions(+), 72 deletions(-)
diff --git a/src/lib/commands.js b/src/lib/commands.js
index eebb9e4d2..dfdeea43f 100644
--- a/src/lib/commands.js
+++ b/src/lib/commands.js
@@ -171,6 +171,12 @@ export default {
"split-pane"() {
return editorManager.splitPane?.();
},
+ "split-pane-right"() {
+ return editorManager.splitPaneRight?.();
+ },
+ "split-pane-down"() {
+ return editorManager.splitPaneDown?.();
+ },
"close-pane"() {
return editorManager.closeActivePane?.();
},
@@ -180,9 +186,24 @@ export default {
"focus-previous-pane"() {
return editorManager.focusPreviousPane?.();
},
+ "focus-pane-left"() {
+ return editorManager.focusPaneByDirection?.("left");
+ },
+ "focus-pane-right"() {
+ return editorManager.focusPaneByDirection?.("right");
+ },
+ "focus-pane-up"() {
+ return editorManager.focusPaneByDirection?.("up");
+ },
+ "focus-pane-down"() {
+ return editorManager.focusPaneByDirection?.("down");
+ },
"move-tab-to-new-pane"() {
return editorManager.moveActiveFileToNewPane?.();
},
+ "move-tab-to-new-pane-down"() {
+ return editorManager.moveActiveFileToNewPane?.("vertical");
+ },
"toggle-pin-tab"(referenceFile) {
resolveReferenceFile(referenceFile)?.togglePinned?.();
},
diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js
index 27c653e89..8b5d383ec 100644
--- a/src/lib/editorManager.js
+++ b/src/lib/editorManager.js
@@ -76,6 +76,7 @@ import quickTools from "components/quickTools";
import ScrollBar from "components/scrollbar";
import SideButton, { sideButtonContainer } from "components/sideButton";
import keyboardHandler, { keydownState } from "handlers/keyboard";
+import { animate } from "motion";
import config from "./config";
import EditorFile from "./editorFile";
import openFile from "./openFile";
@@ -114,7 +115,11 @@ async function EditorManager($header, $body) {
let scrollRestoreNestedFrame = 0;
let scrollRestoreTimeout = 0;
const MIN_PANE_WIDTH = 360;
+ const MIN_PANE_HEIGHT = 220;
const MIN_RESIZED_PANE_WIDTH = 280;
+ const MIN_RESIZED_PANE_HEIGHT = 180;
+ const PANE_SPLIT_HORIZONTAL = "horizontal";
+ const PANE_SPLIT_VERTICAL = "vertical";
// Debounce timers for CodeMirror change handling
let checkTimeout = null;
@@ -186,7 +191,10 @@ async function EditorManager($header, $body) {
const panes = [];
const $paneRoot = ;
let $container = createEditorContainer();
+ let paneLayoutRoot = null;
const primaryPane = createPaneShell($container);
+ paneLayoutRoot = createPaneNode(primaryPane);
+ $paneRoot.append(paneLayoutRoot.element);
const problemButton = SideButton({
text: strings.problems,
icon: "warningreport_problem",
@@ -216,9 +224,9 @@ async function EditorManager($header, $body) {
editorContainer,
touchSelectionController: null,
element: ,
- resizeHandle: ,
tabList: ,
content: ,
+ layoutNode: null,
};
pane.element.dataset.paneId = pane.id;
@@ -226,13 +234,9 @@ async function EditorManager($header, $body) {
pane.element.__editorPane = pane;
pane.tabList.__editorPane = pane;
pane.content.__editorPane = pane;
- pane.resizeHandle.__editorPane = pane;
pane.editorContainer.__editorPane = pane;
pane.content.append(pane.editorContainer);
- pane.element.append(pane.resizeHandle, pane.tabList, pane.content);
- pane.resizeHandle.addEventListener("pointerdown", (event) => {
- startPaneResize(event, pane);
- });
+ pane.element.append(pane.tabList, pane.content);
pane.element.addEventListener(
"pointerdown",
() => {
@@ -241,53 +245,261 @@ async function EditorManager($header, $body) {
true,
);
panes.push(pane);
- $paneRoot.append(pane.element);
- updatePaneResizeHandles();
return pane;
}
- function updatePaneResizeHandles() {
- panes.forEach((pane, index) => {
- pane.resizeHandle.hidden = index === 0 || panes.length <= 1;
+ function createPaneNode(pane) {
+ const node = {
+ type: "pane",
+ pane,
+ parent: null,
+ element: pane.element,
+ };
+ pane.layoutNode = node;
+ return node;
+ }
+
+ function createSplitNode(direction) {
+ const node = {
+ type: "split",
+ direction: normalizePaneDirection(direction),
+ children: [],
+ parent: null,
+ element: ,
+ };
+ node.element.dataset.direction = node.direction;
+ return node;
+ }
+
+ function normalizePaneDirection(direction) {
+ return direction === PANE_SPLIT_VERTICAL ||
+ direction === "down" ||
+ direction === "below"
+ ? PANE_SPLIT_VERTICAL
+ : PANE_SPLIT_HORIZONTAL;
+ }
+
+ function renderPaneLayout(node = paneLayoutRoot) {
+ if (!node || node.type !== "split") return;
+
+ node.element.dataset.direction = node.direction;
+ node.element.replaceChildren();
+ node.children.forEach((child, index) => {
+ if (index > 0) {
+ node.element.append(createPaneSplitHandle(node, index));
+ }
+ node.element.append(child.element);
+ renderPaneLayout(child);
+ });
+ }
+
+ function createPaneSplitHandle(splitNode, childIndex) {
+ const $handle = ;
+ $handle.dataset.direction = splitNode.direction;
+ $handle.addEventListener("pointerdown", (event) => {
+ startPaneResize(event, splitNode, childIndex, $handle);
+ });
+ return $handle;
+ }
+
+ function replacePaneLayoutNode(oldNode, nextNode) {
+ const parent = oldNode?.parent || null;
+ if (parent) {
+ const index = parent.children.indexOf(oldNode);
+ if (index >= 0) {
+ parent.children[index] = nextNode;
+ nextNode.parent = parent;
+ oldNode.parent = null;
+ renderPaneLayout(parent);
+ }
+ return;
+ }
+
+ paneLayoutRoot = nextNode;
+ nextNode.parent = null;
+ $paneRoot.replaceChildren(nextNode.element);
+ renderPaneLayout(nextNode);
+ }
+
+ function insertPaneIntoLayout(sourcePane, pane, direction) {
+ const sourceNode = sourcePane?.layoutNode || paneLayoutRoot;
+ const paneNode = createPaneNode(pane);
+ const splitDirection = normalizePaneDirection(direction);
+
+ if (!sourceNode) {
+ paneLayoutRoot = paneNode;
+ $paneRoot.replaceChildren(paneNode.element);
+ return paneNode;
+ }
+
+ if (
+ sourceNode.parent &&
+ sourceNode.parent.type === "split" &&
+ sourceNode.parent.direction === splitDirection
+ ) {
+ const parent = sourceNode.parent;
+ const index = parent.children.indexOf(sourceNode);
+ parent.children.splice(index + 1, 0, paneNode);
+ paneNode.parent = parent;
+ renderPaneLayout(parent);
+ return paneNode;
+ }
+
+ const splitNode = createSplitNode(splitDirection);
+ const oldParent = sourceNode.parent;
+ const previousFlex = sourceNode.element.style.flex;
+ splitNode.children = [sourceNode, paneNode];
+ splitNode.element.style.flex = previousFlex;
+ sourceNode.element.style.flex = "";
+ paneNode.element.style.flex = "";
+ sourceNode.parent = splitNode;
+ paneNode.parent = splitNode;
+
+ if (oldParent) {
+ const index = oldParent.children.indexOf(sourceNode);
+ if (index >= 0) {
+ oldParent.children[index] = splitNode;
+ splitNode.parent = oldParent;
+ renderPaneLayout(oldParent);
+ }
+ } else {
+ paneLayoutRoot = splitNode;
+ splitNode.parent = null;
+ $paneRoot.replaceChildren(splitNode.element);
+ renderPaneLayout(splitNode);
+ }
+ return paneNode;
+ }
+
+ function removePaneFromLayout(pane) {
+ const node = pane?.layoutNode;
+ if (!node) {
+ pane?.element?.remove();
+ return;
+ }
+
+ const parent = node.parent;
+ if (!parent) {
+ paneLayoutRoot = null;
+ node.element.remove();
+ pane.layoutNode = null;
+ return;
+ }
+
+ const index = parent.children.indexOf(node);
+ if (index >= 0) {
+ parent.children.splice(index, 1);
+ }
+ node.parent = null;
+ pane.layoutNode = null;
+
+ if (parent.children.length === 1) {
+ const onlyChild = parent.children[0];
+ onlyChild.element.style.flex =
+ parent.element.style.flex || onlyChild.element.style.flex;
+ replacePaneLayoutNode(parent, onlyChild);
+ parent.children = [];
+ parent.element.remove();
+ return;
+ }
+
+ renderPaneLayout(parent);
+ }
+
+ function getOrderedPanes(node = paneLayoutRoot) {
+ if (!node) return panes.slice();
+ if (node.type === "pane") return node.pane ? [node.pane] : [];
+ return node.children.flatMap((child) => getOrderedPanes(child));
+ }
+
+ function updatePaneLayoutState() {
+ $paneRoot.classList.toggle("multi-pane", panes.length > 1);
+ renderPaneLayout();
+ updateActivePaneLayoutPath(activePane);
+ }
+
+ function animatePaneEntry(pane) {
+ if (!pane?.element || document.body.classList.contains("no-animation")) {
+ return;
+ }
+
+ pane.element.style.opacity = "0";
+ pane.element.style.transform = "scale(0.985)";
+ animate(
+ pane.element,
+ { opacity: 1, transform: "scale(1)" },
+ { type: "spring", stiffness: 360, damping: 32 },
+ ).then(() => {
+ pane.element.style.opacity = "";
+ pane.element.style.transform = "";
});
}
- function startPaneResize(event, pane) {
- const paneIndex = panes.indexOf(pane);
- const previousPane = panes[paneIndex - 1];
- if (!previousPane) return;
+ function startPaneResize(event, splitNode, childIndex, handle) {
+ const previousNode = splitNode.children[childIndex - 1];
+ const nextNode = splitNode.children[childIndex];
+ if (!previousNode || !nextNode) return;
event.preventDefault();
event.stopPropagation();
- const startX = event.clientX;
- const previousRect = previousPane.element.getBoundingClientRect();
- const paneRect = pane.element.getBoundingClientRect();
- const totalWidth = previousRect.width + paneRect.width;
- const minWidth = Math.min(MIN_RESIZED_PANE_WIDTH, totalWidth / 2);
+ const isVertical = splitNode.direction === PANE_SPLIT_VERTICAL;
+ const axis = isVertical ? "y" : "x";
+ const start = isVertical ? event.clientY : event.clientX;
+ const previousRect = previousNode.element.getBoundingClientRect();
+ const nextRect = nextNode.element.getBoundingClientRect();
+ const previousSize = isVertical ? previousRect.height : previousRect.width;
+ const nextSize = isVertical ? nextRect.height : nextRect.width;
+ const totalSize = previousSize + nextSize;
+ const minSize = Math.min(
+ isVertical ? MIN_RESIZED_PANE_HEIGHT : MIN_RESIZED_PANE_WIDTH,
+ totalSize / 2,
+ );
+ let pendingDelta = 0;
+ let resizeFrame = 0;
document.body.classList.add("resizing-editor-pane");
- pane.resizeHandle.setPointerCapture?.(event.pointerId);
+ document.body.dataset.editorPaneResizeAxis = axis;
+ handle.setPointerCapture?.(event.pointerId);
const resize = (moveEvent) => {
- const delta = moveEvent.clientX - startX;
- const previousWidth = Math.max(
- minWidth,
- Math.min(totalWidth - minWidth, previousRect.width + delta),
- );
- const nextWidth = totalWidth - previousWidth;
- previousPane.element.style.flex = `0 1 ${previousWidth}px`;
- pane.element.style.flex = `0 1 ${nextWidth}px`;
+ pendingDelta =
+ (isVertical ? moveEvent.clientY : moveEvent.clientX) - start;
+ if (resizeFrame) return;
+ resizeFrame = requestAnimationFrame(() => {
+ resizeFrame = 0;
+ const nextPreviousSize = Math.max(
+ minSize,
+ Math.min(totalSize - minSize, previousSize + pendingDelta),
+ );
+ const nextCurrentSize = totalSize - nextPreviousSize;
+ previousNode.element.style.flex = `0 1 ${nextPreviousSize}px`;
+ nextNode.element.style.flex = `0 1 ${nextCurrentSize}px`;
+ });
+ };
+
+ const refreshEditors = () => {
+ getOrderedPanes().forEach((pane) => {
+ pane.editor?.requestMeasure?.();
+ });
+ updateActivePaneScrollbars();
};
const stop = () => {
+ if (resizeFrame) {
+ cancelAnimationFrame(resizeFrame);
+ resizeFrame = 0;
+ }
document.removeEventListener("pointermove", resize);
document.removeEventListener("pointerup", stop);
document.removeEventListener("pointercancel", stop);
document.body.classList.remove("resizing-editor-pane");
+ delete document.body.dataset.editorPaneResizeAxis;
+ handle.releasePointerCapture?.(event.pointerId);
+ requestAnimationFrame(refreshEditors);
};
- document.addEventListener("pointermove", resize);
+ document.addEventListener("pointermove", resize, { passive: true });
document.addEventListener("pointerup", stop);
document.addEventListener("pointercancel", stop);
}
@@ -305,6 +517,7 @@ async function EditorManager($header, $body) {
$container = pane.editorContainer || $container;
touchSelectionController = pane.touchSelectionController || null;
pane.element.classList.add("active");
+ updateActivePaneLayoutPath(pane);
if (manager) {
manager.activeFile = pane.activeFile || null;
@@ -327,6 +540,18 @@ async function EditorManager($header, $body) {
return pane;
}
+ function updateActivePaneLayoutPath(pane) {
+ $paneRoot
+ .querySelectorAll(".editor-pane-split.active-path")
+ .forEach(($split) => $split.classList.remove("active-path"));
+
+ let node = pane?.layoutNode?.parent || null;
+ while (node) {
+ node.element?.classList.add("active-path");
+ node = node.parent;
+ }
+ }
+
function updateHeaderForFile(file) {
if (!$header) return;
$header.text = file?.filename || "";
@@ -1506,14 +1731,33 @@ async function EditorManager($header, $body) {
Object.defineProperties(targetEditor, editorCompatibilityDescriptors);
}
- function canCreatePane() {
- const width =
- $paneRoot.getBoundingClientRect?.().width || $body.clientWidth || 0;
- if (!width) return true;
- return width / (panes.length + 1) >= MIN_PANE_WIDTH;
+ function canCreatePane(
+ direction = PANE_SPLIT_HORIZONTAL,
+ sourcePane = getActivePane(),
+ ) {
+ const normalizedDirection = normalizePaneDirection(direction);
+ const rect = sourcePane?.element?.getBoundingClientRect?.() ||
+ $paneRoot.getBoundingClientRect?.() || {
+ width: $body.clientWidth || 0,
+ height: $body.clientHeight || 0,
+ };
+ if (normalizedDirection === PANE_SPLIT_VERTICAL) {
+ if (!rect.height) return true;
+ return rect.height / 2 >= MIN_PANE_HEIGHT;
+ }
+ if (!rect.width) return true;
+ return rect.width / 2 >= MIN_PANE_WIDTH;
}
function createUntitledPaneFile(pane) {
+ const existingPlaceholder = pane?.files?.find(
+ (file) => file.isPanePlaceholder && !file.isUnsaved,
+ );
+ if (existingPlaceholder) {
+ if (!pane.activeFile) existingPlaceholder.makeActive();
+ return existingPlaceholder;
+ }
+
return new EditorFile(config.DEFAULT_FILE_NAME, {
paneId: pane.id,
text: "",
@@ -1522,8 +1766,23 @@ async function EditorManager($header, $body) {
});
}
+ function removePanePlaceholders(pane, exceptFile = null) {
+ const placeholders = [...(pane?.files || [])].filter(
+ (file) =>
+ file !== exceptFile && file.isPanePlaceholder && !file.isUnsaved,
+ );
+ placeholders.forEach((file) => {
+ file.remove(true, {
+ ignorePinned: true,
+ suppressPanePlaceholder: true,
+ });
+ });
+ }
+
async function createPane(options = {}) {
- if (!canCreatePane()) {
+ const direction = normalizePaneDirection(options.direction);
+ const sourcePane = options.sourcePane || getActivePane() || primaryPane;
+ if (!canCreatePane(direction, sourcePane)) {
window.toast?.(
strings["not enough space"] ||
"Not enough space to create another editor pane.",
@@ -1532,6 +1791,9 @@ async function EditorManager($header, $body) {
}
const pane = createPaneShell();
+ insertPaneIntoLayout(sourcePane, pane, direction);
+ updatePaneLayoutState();
+ animatePaneEntry(pane);
const paneEditor = new EditorView({
state: createEmptyEditorState(),
parent: pane.editorContainer,
@@ -1557,8 +1819,7 @@ async function EditorManager($header, $body) {
);
}
await setupEditor(pane);
- $paneRoot.classList.toggle("multi-pane", panes.length > 1);
- updatePaneResizeHandles();
+ updatePaneLayoutState();
syncOpenFileList();
if (options.moveFile) {
@@ -1573,23 +1834,35 @@ async function EditorManager($header, $body) {
return pane;
}
- function splitPane() {
- return createPane();
+ function splitPane(direction = PANE_SPLIT_HORIZONTAL) {
+ return createPane({ direction });
+ }
+
+ function splitPaneRight() {
+ return splitPane(PANE_SPLIT_HORIZONTAL);
}
- async function moveActiveFileToNewPane() {
+ function splitPaneDown() {
+ return splitPane(PANE_SPLIT_VERTICAL);
+ }
+
+ async function moveActiveFileToNewPane(direction = PANE_SPLIT_HORIZONTAL) {
const file = manager.activeFile;
if (!file) return null;
- return createPane({ moveFile: file });
+ return createPane({ moveFile: file, direction });
}
function closeActivePane() {
const pane = getActivePane();
if (!pane || panes.length <= 1) return false;
- const paneIndex = panes.indexOf(pane);
+ const orderedPanes = getOrderedPanes();
+ const paneIndex = orderedPanes.indexOf(pane);
const targetPane =
- panes[paneIndex - 1] || panes[paneIndex + 1] || panes[0] || null;
+ orderedPanes[paneIndex - 1] ||
+ orderedPanes[paneIndex + 1] ||
+ orderedPanes[0] ||
+ null;
if (!targetPane) return false;
for (const file of [...pane.files]) {
@@ -1608,10 +1881,10 @@ async function EditorManager($header, $body) {
}
pane.editor?.destroy?.();
- pane.element.remove();
- panes.splice(paneIndex, 1);
- $paneRoot.classList.toggle("multi-pane", panes.length > 1);
- updatePaneResizeHandles();
+ removePaneFromLayout(pane);
+ const storedPaneIndex = panes.indexOf(pane);
+ if (storedPaneIndex >= 0) panes.splice(storedPaneIndex, 1);
+ updatePaneLayoutState();
rebuildFileListFromPanes();
const fileToActivate = targetPane.activeFile;
targetPane.activeFile = null;
@@ -1623,8 +1896,12 @@ async function EditorManager($header, $body) {
function focusPaneByOffset(offset) {
if (panes.length <= 1) return false;
- const index = Math.max(0, panes.indexOf(getActivePane()));
- const nextPane = panes[(index + offset + panes.length) % panes.length];
+ const orderedPanes = getOrderedPanes();
+ const index = Math.max(0, orderedPanes.indexOf(getActivePane()));
+ const nextPane =
+ orderedPanes[
+ (index + offset + orderedPanes.length) % orderedPanes.length
+ ];
if (!nextPane) return false;
const fileToActivate = nextPane.activeFile;
nextPane.activeFile = null;
@@ -1645,6 +1922,63 @@ async function EditorManager($header, $body) {
return focusPaneByOffset(-1);
}
+ function focusPaneByDirection(direction) {
+ const active = getActivePane();
+ if (!active || panes.length <= 1) return false;
+
+ const activeRect = active.element.getBoundingClientRect();
+ const activeCenterX = activeRect.left + activeRect.width / 2;
+ const activeCenterY = activeRect.top + activeRect.height / 2;
+ let bestPane = null;
+ let bestScore = Number.POSITIVE_INFINITY;
+
+ for (const pane of getOrderedPanes()) {
+ if (pane === active) continue;
+ const rect = pane.element.getBoundingClientRect();
+ const centerX = rect.left + rect.width / 2;
+ const centerY = rect.top + rect.height / 2;
+ let axisDistance = 0;
+ let crossDistance = 0;
+
+ if (direction === "left") {
+ if (centerX >= activeCenterX) continue;
+ axisDistance = activeRect.left - rect.right;
+ crossDistance = Math.abs(centerY - activeCenterY);
+ } else if (direction === "right") {
+ if (centerX <= activeCenterX) continue;
+ axisDistance = rect.left - activeRect.right;
+ crossDistance = Math.abs(centerY - activeCenterY);
+ } else if (direction === "up") {
+ if (centerY >= activeCenterY) continue;
+ axisDistance = activeRect.top - rect.bottom;
+ crossDistance = Math.abs(centerX - activeCenterX);
+ } else if (direction === "down") {
+ if (centerY <= activeCenterY) continue;
+ axisDistance = rect.top - activeRect.bottom;
+ crossDistance = Math.abs(centerX - activeCenterX);
+ } else {
+ return false;
+ }
+
+ const score = Math.max(0, axisDistance) * 1000 + crossDistance;
+ if (score < bestScore) {
+ bestScore = score;
+ bestPane = pane;
+ }
+ }
+
+ if (!bestPane) return false;
+ const fileToActivate = bestPane.activeFile;
+ bestPane.activeFile = null;
+ setActivePane(bestPane, { emitSwitch: false });
+ if (fileToActivate) {
+ fileToActivate.makeActive();
+ } else {
+ bestPane.editor?.focus();
+ }
+ return true;
+ }
+
function getEditorExtensionSignature(file) {
return JSON.stringify({
syntax: getEmmetSyntaxForFile(file),
@@ -2074,9 +2408,12 @@ async function EditorManager($header, $body) {
switchFile,
createPane,
splitPane,
+ splitPaneRight,
+ splitPaneDown,
closeActivePane,
focusNextPane,
focusPreviousPane,
+ focusPaneByDirection,
moveActiveFileToNewPane,
moveFileToPane,
removeFileFromPane,
@@ -2623,7 +2960,7 @@ async function EditorManager($header, $body) {
}
function rebuildFileListFromPanes() {
- manager.files = panes.flatMap((pane) => pane.files);
+ manager.files = getOrderedPanes().flatMap((pane) => pane.files);
return manager.files;
}
@@ -2734,6 +3071,9 @@ async function EditorManager($header, $body) {
}
insertFileIntoPane(file, targetPane, index);
+ if (!file.isPanePlaceholder || file.isUnsaved) {
+ removePanePlaceholders(targetPane, file);
+ }
if (!targetPane.activeFile && !activate) {
targetPane.activeFile = file;
}
diff --git a/src/lib/keyBindings.js b/src/lib/keyBindings.js
index c80edd32f..39ff3575e 100644
--- a/src/lib/keyBindings.js
+++ b/src/lib/keyBindings.js
@@ -122,6 +122,76 @@ const APP_BINDING_CONFIG = [
action: "prev-file",
readOnly: true,
},
+ {
+ name: "splitPaneRight",
+ description: "Split editor pane right",
+ key: "Ctrl-\\",
+ action: "split-pane-right",
+ readOnly: true,
+ },
+ {
+ name: "splitPaneDown",
+ description: "Split editor pane down",
+ key: "Ctrl-Shift-\\",
+ action: "split-pane-down",
+ readOnly: true,
+ },
+ {
+ name: "closePane",
+ description: "Close active editor pane",
+ key: "Ctrl-Shift-W",
+ action: "close-pane",
+ readOnly: true,
+ },
+ {
+ name: "focusNextPane",
+ description: "Focus next editor pane",
+ key: null,
+ action: "focus-next-pane",
+ readOnly: true,
+ },
+ {
+ name: "focusPreviousPane",
+ description: "Focus previous editor pane",
+ key: null,
+ action: "focus-previous-pane",
+ readOnly: true,
+ },
+ {
+ name: "focusPaneLeft",
+ description: "Focus editor pane to the left",
+ key: "Ctrl-Alt-Left",
+ action: "focus-pane-left",
+ readOnly: true,
+ },
+ {
+ name: "focusPaneRight",
+ description: "Focus editor pane to the right",
+ key: "Ctrl-Alt-Right",
+ action: "focus-pane-right",
+ readOnly: true,
+ },
+ {
+ name: "focusPaneUp",
+ description: "Focus editor pane above",
+ key: "Ctrl-Alt-Up",
+ action: "focus-pane-up",
+ readOnly: true,
+ },
+ {
+ name: "focusPaneDown",
+ description: "Focus editor pane below",
+ key: "Ctrl-Alt-Down",
+ action: "focus-pane-down",
+ readOnly: true,
+ },
+ {
+ name: "moveTabToNewPane",
+ description: "Move current tab to new pane",
+ key: "Ctrl-Alt-\\",
+ action: "move-tab-to-new-pane",
+ readOnly: true,
+ },
{
name: "showSettingsMenu",
description: "Show settings menu",
diff --git a/src/styles/codemirror.scss b/src/styles/codemirror.scss
index d3b91e8f3..cac97f3a2 100644
--- a/src/styles/codemirror.scss
+++ b/src/styles/codemirror.scss
@@ -26,20 +26,32 @@
}
}
+.editor-pane-split {
+ position: relative;
+ display: flex;
+ flex: 1 1 0;
+ min-width: 0;
+ min-height: 0;
+ overflow: hidden;
+
+ &[data-direction="horizontal"] {
+ flex-direction: row;
+ }
+
+ &[data-direction="vertical"] {
+ flex-direction: column;
+ }
+}
+
.editor-pane {
position: relative;
display: flex;
flex: 1 1 0;
flex-direction: column;
min-width: min(360px, 100%);
- min-height: 0;
+ min-height: min(220px, 100%);
overflow: hidden;
- border-left: 1px solid var(--border-color);
background: var(--secondary-color);
-
- &:first-child {
- border-left: 0;
- }
}
.editor-pane-root.multi-pane {
@@ -47,7 +59,7 @@
box-shadow: inset 0 2px 0 var(--active-color);
}
- .editor-pane-resize-handle:not([hidden]) {
+ .editor-pane-split-handle {
display: block;
}
}
@@ -59,36 +71,54 @@
}
}
-.editor-pane-resize-handle {
- position: absolute;
- top: 0;
- bottom: 0;
- left: -3px;
+.editor-pane-split-handle {
+ position: relative;
z-index: 5;
display: none;
- width: 6px;
- cursor: col-resize;
+ flex: 0 0 1px;
+ background: var(--border-color);
touch-action: none;
&::after {
content: "";
position: absolute;
- top: 0;
- bottom: 0;
- left: 2px;
- width: 1px;
- background: var(--border-color);
+ inset: 0;
+ background: transparent;
}
- &:hover::after,
- &:active::after {
+ &:hover,
+ &:active {
background: var(--active-color);
}
+
+ &[data-direction="horizontal"] {
+ width: 1px;
+ cursor: col-resize;
+
+ &::after {
+ left: -5px;
+ right: -5px;
+ }
+ }
+
+ &[data-direction="vertical"] {
+ height: 1px;
+ cursor: row-resize;
+
+ &::after {
+ top: -5px;
+ bottom: -5px;
+ }
+ }
}
body.resizing-editor-pane {
cursor: col-resize;
user-select: none;
+
+ &[data-editor-pane-resize-axis="y"] {
+ cursor: row-resize;
+ }
}
.editor-pane-tabs {
@@ -115,14 +145,28 @@ body.resizing-editor-pane {
@media (max-width: 720px) {
.editor-pane-root.multi-pane {
+ .editor-pane-split {
+ min-width: 100%;
+ min-height: 100%;
+ }
+
+ .editor-pane-split > .editor-pane-split:not(.active-path) {
+ display: none;
+ }
+
.editor-pane {
display: none;
min-width: 100%;
+ min-height: 100%;
}
.editor-pane.active {
display: flex;
}
+
+ .editor-pane-split-handle {
+ display: none;
+ }
}
}
From 97add327ce9053110f317994c25b1633dcad8906 Mon Sep 17 00:00:00 2001
From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com>
Date: Mon, 29 Jun 2026 13:22:16 +0530
Subject: [PATCH 04/20] fix resize pane empty space
---
src/lib/editorManager.js | 15 ++++++++----
src/styles/codemirror.scss | 47 ++++++++++++++++++++++++--------------
2 files changed, 40 insertions(+), 22 deletions(-)
diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js
index 8b5d383ec..d24d1c52a 100644
--- a/src/lib/editorManager.js
+++ b/src/lib/editorManager.js
@@ -395,8 +395,9 @@ async function EditorManager($header, $body) {
if (parent.children.length === 1) {
const onlyChild = parent.children[0];
- onlyChild.element.style.flex =
- parent.element.style.flex || onlyChild.element.style.flex;
+ onlyChild.element.style.flex = parent.parent
+ ? parent.element.style.flex || "1 1 0"
+ : "1 1 0";
replacePaneLayoutNode(parent, onlyChild);
parent.children = [];
parent.element.remove();
@@ -473,8 +474,8 @@ async function EditorManager($header, $body) {
Math.min(totalSize - minSize, previousSize + pendingDelta),
);
const nextCurrentSize = totalSize - nextPreviousSize;
- previousNode.element.style.flex = `0 1 ${nextPreviousSize}px`;
- nextNode.element.style.flex = `0 1 ${nextCurrentSize}px`;
+ previousNode.element.style.flex = `1 1 ${nextPreviousSize}px`;
+ nextNode.element.style.flex = `1 1 ${nextCurrentSize}px`;
});
};
@@ -1877,6 +1878,7 @@ async function EditorManager($header, $body) {
moveFileToPane(file, targetPane, {
activate: false,
createSourcePlaceholder: false,
+ activateSourceFallback: false,
});
}
@@ -2969,6 +2971,7 @@ async function EditorManager($header, $body) {
$paneRoot.classList.remove("hide-pane-tabs");
panes.forEach((pane) => {
pane.files.forEach((file) => {
+ file.tab.classList.toggle("active", pane.activeFile?.id === file.id);
pane.tabList.append(file.tab);
});
pane.element.classList.toggle("empty", pane.files.length === 0);
@@ -3056,6 +3059,7 @@ async function EditorManager($header, $body) {
activate = true,
index = null,
createSourcePlaceholder = true,
+ activateSourceFallback = true,
} = options;
const sourcePane = getFilePane(file);
if (sourcePane === targetPane) {
@@ -3084,7 +3088,8 @@ async function EditorManager($header, $body) {
const nextSourceFile =
sourcePane.files[sourcePane.files.length - 1] || null;
sourcePane.activeFile = null;
- if (nextSourceFile) {
+ file.tab?.classList.remove("active");
+ if (nextSourceFile && activateSourceFallback) {
nextSourceFile.makeActive();
} else if (createSourcePlaceholder) {
sourcePane.editor?.setState(createEmptyEditorState());
diff --git a/src/styles/codemirror.scss b/src/styles/codemirror.scss
index cac97f3a2..2205bfb7a 100644
--- a/src/styles/codemirror.scss
+++ b/src/styles/codemirror.scss
@@ -75,49 +75,62 @@
position: relative;
z-index: 5;
display: none;
- flex: 0 0 1px;
- background: var(--border-color);
+ flex: 0 0 9px;
+ background: transparent;
touch-action: none;
- &::after {
+ &::before {
content: "";
position: absolute;
- inset: 0;
- background: transparent;
+ background: var(--border-color);
}
- &:hover,
- &:active {
+ &:hover::before,
+ &:active::before {
background: var(--active-color);
}
&[data-direction="horizontal"] {
- width: 1px;
+ margin-right: -4px;
+ margin-left: -4px;
cursor: col-resize;
- &::after {
- left: -5px;
- right: -5px;
+ &::before {
+ top: 0;
+ bottom: 0;
+ left: 4px;
+ width: 1px;
}
}
&[data-direction="vertical"] {
- height: 1px;
+ margin-top: -4px;
+ margin-bottom: -4px;
cursor: row-resize;
- &::after {
- top: -5px;
- bottom: -5px;
+ &::before {
+ top: 4px;
+ right: 0;
+ left: 0;
+ height: 1px;
}
}
}
body.resizing-editor-pane {
- cursor: col-resize;
+ cursor: col-resize !important;
user-select: none;
+ * {
+ cursor: col-resize !important;
+ }
+
&[data-editor-pane-resize-axis="y"] {
- cursor: row-resize;
+ cursor: row-resize !important;
+
+ * {
+ cursor: row-resize !important;
+ }
}
}
From 3a545438bf58413b6304109d9bb3b54f9731c01d Mon Sep 17 00:00:00 2001
From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com>
Date: Mon, 29 Jun 2026 18:34:45 +0530
Subject: [PATCH 05/20] fix: issues and bugs
---
src/lib/editorFile.js | 15 +-
src/lib/editorManager.js | 405 ++++++++++++++++++++++++++++++++++---
src/lib/keyBindings.js | 2 +-
src/styles/codemirror.scss | 4 +
src/styles/page.scss | 14 +-
5 files changed, 396 insertions(+), 44 deletions(-)
diff --git a/src/lib/editorFile.js b/src/lib/editorFile.js
index 8877413ed..849dae1e3 100644
--- a/src/lib/editorFile.js
+++ b/src/lib/editorFile.js
@@ -1360,13 +1360,14 @@ export default class EditorFile {
const wasActivePane = editorManager.activePane?.id === pane?.id;
const { activeFile, switchFile } = editorManager;
const paneActiveFile = pane?.activeFile;
-
- if (paneActiveFile && paneActiveFile.id !== this.id) {
- paneActiveFile.focusedBefore = paneActiveFile.focused;
- paneActiveFile.removeActive();
- } else if (activeFile && (activeFile.id !== this.id || !wasActivePane)) {
- activeFile.focusedBefore = activeFile.focused;
- activeFile.removeActive();
+ const inactiveFiles = [paneActiveFile, !wasActivePane ? activeFile : null];
+ const blurredFileIds = new Set();
+
+ for (const file of inactiveFiles) {
+ if (!file || file.id === this.id || blurredFileIds.has(file.id)) continue;
+ file.focusedBefore = file.focused;
+ file.removeActive();
+ blurredFileIds.add(file.id);
}
if (activeFile?.id === this.id && wasActivePane) return;
diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js
index d24d1c52a..1fa9c7831 100644
--- a/src/lib/editorManager.js
+++ b/src/lib/editorManager.js
@@ -190,6 +190,9 @@ async function EditorManager($header, $body) {
let editor = null;
const panes = [];
const $paneRoot = ;
+ const $globalOpenFileList = (
+
+ );
let $container = createEditorContainer();
let paneLayoutRoot = null;
const primaryPane = createPaneShell($container);
@@ -523,6 +526,7 @@ async function EditorManager($header, $body) {
if (manager) {
manager.activeFile = pane.activeFile || null;
updateHeaderForFile(manager.activeFile);
+ if (isPaneTabLayout()) syncGlobalOpenFileListMirror();
updateActivePaneScrollbars();
toggleProblemButton();
if (options.emitSwitch !== false && manager.activeFile) {
@@ -1384,6 +1388,7 @@ async function EditorManager($header, $body) {
};
Object.defineProperty(editor.commands, "commands", {
+ configurable: true,
get() {
const map = {};
getRegisteredCommands().forEach((cmd) => {
@@ -1396,8 +1401,9 @@ async function EditorManager($header, $body) {
// Provide editor.session for Ace API compatibility
// Returns the active file's session (Proxy with Ace-like methods)
Object.defineProperty(editor, "session", {
+ configurable: true,
get() {
- return manager.activeFile?.session ?? null;
+ return editor.__editorPane?.activeFile?.session ?? null;
},
});
@@ -1705,33 +1711,336 @@ async function EditorManager($header, $body) {
touchSelectionController?.setMenu(!!value);
};
- const editorCompatibilityKeys = [
- "execCommand",
- "commands",
- "session",
- "insert",
- "setTheme",
- "gotoLine",
- "getCursorPosition",
- "getSelectionRange",
- "scrollToRow",
- "moveCursorToPosition",
- "getValue",
- "selection",
- "getCopyText",
- "setSelection",
- "setMenu",
- ];
- const editorCompatibilityDescriptors = Object.fromEntries(
- editorCompatibilityKeys
- .map((key) => [key, Object.getOwnPropertyDescriptor(editor, key)])
- .filter(([, descriptor]) => descriptor),
- );
+ function getEditorCompatibilityPane(targetEditor) {
+ return targetEditor?.__editorPane || getActivePane();
+ }
+
+ function refreshPaneCommandKeymaps() {
+ panes.forEach((pane) => {
+ if (pane.editor) refreshCommandKeymap(pane.editor);
+ });
+ }
+
+ function createEditorCommands() {
+ const commands = {
+ addCommand(descriptor) {
+ const command = registerExternalCommand(descriptor);
+ refreshPaneCommandKeymaps();
+ return command;
+ },
+ removeCommand(name) {
+ if (!name) return;
+ removeExternalCommand(name);
+ refreshPaneCommandKeymaps();
+ },
+ };
+
+ Object.defineProperty(commands, "commands", {
+ configurable: true,
+ get() {
+ const map = {};
+ getRegisteredCommands().forEach((cmd) => {
+ map[cmd.name] = cmd;
+ });
+ return map;
+ },
+ });
+
+ return commands;
+ }
+
+ function createEditorCompatibilityDescriptors(targetEditor) {
+ const getState = () => targetEditor.state;
+ const getDoc = () => getState().doc;
+ const getSelection = () => getState().selection.main;
+ const getTouchSelectionController = () =>
+ getEditorCompatibilityPane(targetEditor)?.touchSelectionController ||
+ touchSelectionController;
+
+ return {
+ execCommand: {
+ configurable: true,
+ writable: true,
+ value(commandName, args) {
+ if (!commandName) return false;
+ return executeCommand(String(commandName), targetEditor, args);
+ },
+ },
+ commands: {
+ configurable: true,
+ writable: true,
+ value: createEditorCommands(),
+ },
+ session: {
+ configurable: true,
+ get() {
+ return (
+ getEditorCompatibilityPane(targetEditor)?.activeFile?.session ??
+ null
+ );
+ },
+ },
+ insert: {
+ configurable: true,
+ writable: true,
+ value(text) {
+ try {
+ const { from, to } = getSelection();
+ const insertText = String(text ?? "");
+ targetEditor.dispatch({
+ changes: { from, to, insert: insertText },
+ selection: {
+ anchor: from + insertText.length,
+ head: from + insertText.length,
+ },
+ });
+ return true;
+ } catch (_) {
+ return false;
+ }
+ },
+ },
+ setTheme: {
+ configurable: true,
+ writable: true,
+ value(themeId) {
+ return applyThemeToEditor(targetEditor, themeId);
+ },
+ },
+ gotoLine: {
+ configurable: true,
+ writable: true,
+ value(line, column = 0, animate = false) {
+ try {
+ const state = getState();
+ const { doc } = state;
+
+ let targetLine;
+ let targetColumn = column;
+
+ if (typeof line === "string") {
+ const match = /^([+-])?(\d+)?(:\d+)?(%)?$/.exec(line.trim());
+ if (!match) {
+ console.warn("Invalid gotoLine format:", line);
+ return false;
+ }
+
+ const currentLine = doc.lineAt(state.selection.main.head);
+ const [, sign, lineNum, colonColumn, percent] = match;
+
+ if (colonColumn) {
+ targetColumn = Math.max(0, +colonColumn.slice(1) - 1);
+ }
+
+ const parsedLine = lineNum ? +lineNum : currentLine.number;
+
+ if (lineNum && percent) {
+ let percentage = parsedLine / 100;
+ if (sign) {
+ percentage =
+ percentage * (sign === "-" ? -1 : 1) +
+ currentLine.number / doc.lines;
+ }
+ targetLine = Math.round(doc.lines * percentage);
+ } else if (lineNum && sign) {
+ targetLine =
+ parsedLine * (sign === "-" ? -1 : 1) + currentLine.number;
+ } else if (lineNum) {
+ targetLine = parsedLine;
+ } else {
+ targetLine = currentLine.number;
+ }
+ } else {
+ targetLine = line;
+ }
+
+ const lineNum = Math.max(1, Math.min(targetLine, doc.lines));
+ const docLine = doc.line(lineNum);
+ const col = Math.max(0, Math.min(targetColumn, docLine.length));
+ const pos = docLine.from + col;
+
+ targetEditor.dispatch({
+ selection: { anchor: pos, head: pos },
+ effects: EditorView.scrollIntoView(pos, { y: "center" }),
+ });
+ targetEditor.focus();
+ return true;
+ } catch (error) {
+ console.error("Error in gotoLine:", error);
+ return false;
+ }
+ },
+ },
+ getCursorPosition: {
+ configurable: true,
+ writable: true,
+ value() {
+ try {
+ const head = getSelection().head;
+ const cursor = getDoc().lineAt(head);
+ return { row: cursor.number, column: head - cursor.from };
+ } catch (_) {
+ return { row: 1, column: 0 };
+ }
+ },
+ },
+ getSelectionRange: {
+ configurable: true,
+ writable: true,
+ value() {
+ try {
+ const { from, to } = getSelection();
+ const doc = getDoc();
+ const fromLine = doc.lineAt(from);
+ const toLine = doc.lineAt(to);
+ return {
+ start: {
+ row: Math.max(0, fromLine.number - 1),
+ column: from - fromLine.from,
+ },
+ end: {
+ row: Math.max(0, toLine.number - 1),
+ column: to - toLine.from,
+ },
+ };
+ } catch (_) {
+ return { start: { row: 0, column: 0 }, end: { row: 0, column: 0 } };
+ }
+ },
+ },
+ scrollToRow: {
+ configurable: true,
+ writable: true,
+ value(row) {
+ try {
+ const scroller = targetEditor.scrollDOM;
+ if (!scroller) return false;
+
+ if (row === Number.POSITIVE_INFINITY) {
+ clearScrollbarScrollLock();
+ scroller.scrollTop = Math.max(
+ scroller.scrollHeight - scroller.clientHeight,
+ 0,
+ );
+ return true;
+ }
+
+ const parsedRow = Number(row);
+ if (!Number.isFinite(parsedRow)) return false;
+ const aceRow = Math.max(0, Math.floor(parsedRow));
+ const lineNum = Math.min(getDoc().lines, aceRow + 1);
+ const line = getDoc().line(lineNum);
+ targetEditor.dispatch({
+ effects: EditorView.scrollIntoView(line.from, { y: "start" }),
+ });
+ return true;
+ } catch (_) {
+ return false;
+ }
+ },
+ },
+ moveCursorToPosition: {
+ configurable: true,
+ writable: true,
+ value(pos) {
+ try {
+ const lineNum = Math.max(1, pos.row || 1);
+ const col = Math.max(0, pos.column || 0);
+ targetEditor.gotoLine(lineNum, col);
+ } catch (_) {
+ // ignore
+ }
+ },
+ },
+ getValue: {
+ configurable: true,
+ writable: true,
+ value() {
+ try {
+ return getDoc().toString();
+ } catch (_) {
+ return "";
+ }
+ },
+ },
+ selection: {
+ configurable: true,
+ writable: true,
+ value: {
+ get anchor() {
+ try {
+ return getSelection().anchor;
+ } catch (_) {
+ return 0;
+ }
+ },
+ getRange() {
+ try {
+ const { from, to } = getSelection();
+ const doc = getDoc();
+ const fromLine = doc.lineAt(from);
+ const toLine = doc.lineAt(to);
+ return {
+ start: {
+ row: fromLine.number,
+ column: from - fromLine.from,
+ },
+ end: {
+ row: toLine.number,
+ column: to - toLine.from,
+ },
+ };
+ } catch (_) {
+ return {
+ start: { row: 1, column: 0 },
+ end: { row: 1, column: 0 },
+ };
+ }
+ },
+ getCursor() {
+ return targetEditor.getCursorPosition();
+ },
+ },
+ },
+ getCopyText: {
+ configurable: true,
+ writable: true,
+ value() {
+ try {
+ const { from, to } = getSelection();
+ if (from === to) return "";
+ return getDoc().sliceString(from, to);
+ } catch (_) {
+ return "";
+ }
+ },
+ },
+ setSelection: {
+ configurable: true,
+ writable: true,
+ value(value) {
+ getTouchSelectionController()?.setSelection(!!value);
+ },
+ },
+ setMenu: {
+ configurable: true,
+ writable: true,
+ value(value) {
+ getTouchSelectionController()?.setMenu(!!value);
+ },
+ },
+ };
+ }
function applyEditorCompatibility(targetEditor) {
- Object.defineProperties(targetEditor, editorCompatibilityDescriptors);
+ Object.defineProperties(
+ targetEditor,
+ createEditorCompatibilityDescriptors(targetEditor),
+ );
}
+ applyEditorCompatibility(editor);
+
function canCreatePane(
direction = PANE_SPLIT_HORIZONTAL,
sourcePane = getActivePane(),
@@ -1856,6 +2165,7 @@ async function EditorManager($header, $body) {
function closeActivePane() {
const pane = getActivePane();
if (!pane || panes.length <= 1) return false;
+ const preferredFile = pane.activeFile;
const orderedPanes = getOrderedPanes();
const paneIndex = orderedPanes.indexOf(pane);
@@ -1888,7 +2198,9 @@ async function EditorManager($header, $body) {
if (storedPaneIndex >= 0) panes.splice(storedPaneIndex, 1);
updatePaneLayoutState();
rebuildFileListFromPanes();
- const fileToActivate = targetPane.activeFile;
+ const fileToActivate = targetPane.files.includes(preferredFile)
+ ? preferredFile
+ : targetPane.activeFile;
targetPane.activeFile = null;
setActivePane(targetPane, { emitSwitch: false });
fileToActivate?.makeActive();
@@ -2405,6 +2717,7 @@ async function EditorManager($header, $body) {
getFile,
getFilePane,
getPaneFiles,
+ getPaneTabList,
setActivePane,
reapplyActiveFile,
switchFile,
@@ -2437,6 +2750,9 @@ async function EditorManager($header, $body) {
get panes() {
return panes.slice();
},
+ get activePaneTabList() {
+ return getPaneTabList();
+ },
get container() {
return getActivePane()?.editorContainer || $container;
},
@@ -2444,8 +2760,10 @@ async function EditorManager($header, $body) {
return isScrolling;
},
get openFileList() {
- if (!$openFileList) initFileTabContainer();
- if (isPaneTabLayout()) return getActivePane()?.tabList || $openFileList;
+ if (isPaneTabLayout()) return $globalOpenFileList;
+ if (!$openFileList || $openFileList === $globalOpenFileList) {
+ initFileTabContainer();
+ }
return $openFileList;
},
get TIMEOUT_VALUE() {
@@ -2966,6 +3284,18 @@ async function EditorManager($header, $body) {
return manager.files;
}
+ function syncGlobalOpenFileListMirror() {
+ $globalOpenFileList.replaceChildren(
+ ...manager.files.map((file) => {
+ const tab = file.tab.cloneNode(true);
+ tab.dataset.fileId = file.id;
+ tab.dataset.paneId = file.paneId || "";
+ tab.classList.toggle("active", manager.activeFile?.id === file.id);
+ return tab;
+ }),
+ );
+ }
+
function syncOpenFileList() {
if (isPaneTabLayout()) {
$paneRoot.classList.remove("hide-pane-tabs");
@@ -2976,11 +3306,15 @@ async function EditorManager($header, $body) {
});
pane.element.classList.toggle("empty", pane.files.length === 0);
});
+ syncGlobalOpenFileListMirror();
return;
}
$paneRoot.classList.add("hide-pane-tabs");
- const $list = manager.openFileList;
+ if (!$openFileList || $openFileList === $globalOpenFileList) {
+ initFileTabContainer();
+ }
+ const $list = $openFileList;
manager.files.forEach((file) => {
$list.append(file.tab);
});
@@ -3053,6 +3387,15 @@ async function EditorManager($header, $body) {
return pane?.files || manager.files;
}
+ function getPaneTabList(fileOrPane = getActivePane()) {
+ const pane = fileOrPane?.tabList
+ ? fileOrPane
+ : typeof fileOrPane === "string"
+ ? getPaneById(fileOrPane) || getFilePane(fileOrPane)
+ : getFilePane(fileOrPane);
+ return pane?.tabList || null;
+ }
+
function moveFileToPane(file, targetPane, options = {}) {
if (!file || !targetPane) return false;
const {
@@ -3096,6 +3439,7 @@ async function EditorManager($header, $body) {
sourcePane.editorContainer.style.display = "block";
createUntitledPaneFile(sourcePane);
}
+ syncOpenFileList();
}
if (activate) {
@@ -3160,6 +3504,7 @@ async function EditorManager($header, $body) {
function isPaneTabLayout() {
const { openFileListPos } = appSettings.value;
+ // Sidebar mode keeps the global sidebar list, so pane-local tab bars stay hidden.
return (
openFileListPos === appSettings.OPEN_FILE_LIST_POS_HEADER ||
openFileListPos === appSettings.OPEN_FILE_LIST_POS_BOTTOM
@@ -3794,6 +4139,7 @@ async function EditorManager($header, $body) {
file.tab.classList.add("active");
file.tab.scrollIntoView();
updateHeaderForFile(file);
+ if (isPaneTabLayout()) syncGlobalOpenFileListMirror();
if (file.type === "editor") {
pane.touchSelectionController?.setEnabled(true);
@@ -3842,6 +4188,7 @@ async function EditorManager($header, $body) {
if (
$openFileList &&
+ $openFileList !== $globalOpenFileList &&
!$openFileList.classList.contains("editor-pane-tabs")
) {
if ($openFileList.classList.contains("collapsible")) {
@@ -3855,7 +4202,7 @@ async function EditorManager($header, $body) {
// show open file list in header
const { openFileListPos } = appSettings.value;
if (isPaneTabLayout()) {
- $openFileList = getActivePane()?.tabList || null;
+ $openFileList = $globalOpenFileList;
$paneRoot.dataset.tabsPosition =
openFileListPos === appSettings.OPEN_FILE_LIST_POS_BOTTOM
? "bottom"
diff --git a/src/lib/keyBindings.js b/src/lib/keyBindings.js
index 39ff3575e..1f9a00feb 100644
--- a/src/lib/keyBindings.js
+++ b/src/lib/keyBindings.js
@@ -139,7 +139,7 @@ const APP_BINDING_CONFIG = [
{
name: "closePane",
description: "Close active editor pane",
- key: "Ctrl-Shift-W",
+ key: "Ctrl-Alt-W",
action: "close-pane",
readOnly: true,
},
diff --git a/src/styles/codemirror.scss b/src/styles/codemirror.scss
index 2205bfb7a..19fd31694 100644
--- a/src/styles/codemirror.scss
+++ b/src/styles/codemirror.scss
@@ -144,6 +144,10 @@ body.resizing-editor-pane {
}
}
+.editor-global-open-file-list {
+ display: none !important;
+}
+
.editor-pane-content {
position: relative;
flex: 1 1 auto;
diff --git a/src/styles/page.scss b/src/styles/page.scss
index 9c770f5a6..62b2d5c20 100644
--- a/src/styles/page.scss
+++ b/src/styles/page.scss
@@ -14,7 +14,7 @@ body {
}
&.fullscreen-mode {
- .open-file-list {
+ .open-file-list:not(.editor-pane-tabs) {
height: 30px;
left: 40px;
}
@@ -101,7 +101,7 @@ body {
box-shadow: none !important;
}
- .open-file-list {
+ .open-file-list:not(.editor-pane-tabs) {
top: 0;
width: calc(100% - 140px);
}
@@ -233,7 +233,7 @@ wc-page {
box-shadow: 0 -32px 4px var(--box-shadow-color);
}
- .open-file-list {
+ .open-file-list:not(.editor-pane-tabs) {
top: auto !important;
bottom: 0;
z-index: 99;
@@ -293,7 +293,7 @@ wc-page {
&[footer-height="1"] {
&[open-file-list-pos="bottom"] {
root {
- .open-file-list {
+ .open-file-list:not(.editor-pane-tabs) {
bottom: 40px;
}
}
@@ -322,7 +322,7 @@ wc-page {
&[footer-height="2"] {
&[open-file-list-pos="bottom"] {
root {
- .open-file-list {
+ .open-file-list:not(.editor-pane-tabs) {
bottom: 80px;
}
}
@@ -351,7 +351,7 @@ wc-page {
&[footer-height="3"] {
&[open-file-list-pos="bottom"] {
root {
- .open-file-list {
+ .open-file-list:not(.editor-pane-tabs) {
bottom: 120px;
}
}
@@ -501,4 +501,4 @@ wc-page {
.editor-container {
height: 100%;
}
-}
\ No newline at end of file
+}
From 701d1e687d105e904195cd1c973d9d530d4bf5d1 Mon Sep 17 00:00:00 2001
From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com>
Date: Mon, 29 Jun 2026 19:23:48 +0530
Subject: [PATCH 06/20] fixed pane focus lifecycle
---
src/lib/editorManager.js | 82 +++++++++++++++++++++++-----------------
1 file changed, 47 insertions(+), 35 deletions(-)
diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js
index 1fa9c7831..b17179097 100644
--- a/src/lib/editorManager.js
+++ b/src/lib/editorManager.js
@@ -243,7 +243,7 @@ async function EditorManager($header, $body) {
pane.element.addEventListener(
"pointerdown",
() => {
- setActivePane(pane);
+ activatePane(pane);
},
true,
);
@@ -545,6 +545,24 @@ async function EditorManager($header, $body) {
return pane;
}
+ function activatePane(pane, options = {}) {
+ if (!pane) return null;
+ if (activePane === pane) {
+ if (options.focusEditor !== false) pane.editor?.focus?.();
+ return pane;
+ }
+
+ const fileToActivate = pane.activeFile || null;
+ if (fileToActivate) {
+ fileToActivate.makeActive();
+ return pane;
+ }
+
+ setActivePane(pane, { emitSwitch: false });
+ if (options.focusEditor !== false) pane.editor?.focus?.();
+ return pane;
+ }
+
function updateActivePaneLayoutPath(pane) {
$paneRoot
.querySelectorAll(".editor-pane-split.active-path")
@@ -2089,21 +2107,7 @@ async function EditorManager($header, $body) {
});
}
- async function createPane(options = {}) {
- const direction = normalizePaneDirection(options.direction);
- const sourcePane = options.sourcePane || getActivePane() || primaryPane;
- if (!canCreatePane(direction, sourcePane)) {
- window.toast?.(
- strings["not enough space"] ||
- "Not enough space to create another editor pane.",
- );
- return null;
- }
-
- const pane = createPaneShell();
- insertPaneIntoLayout(sourcePane, pane, direction);
- updatePaneLayoutState();
- animatePaneEntry(pane);
+ async function createPaneEditor(pane) {
const paneEditor = new EditorView({
state: createEmptyEditorState(),
parent: pane.editorContainer,
@@ -2129,6 +2133,25 @@ async function EditorManager($header, $body) {
);
}
await setupEditor(pane);
+ return paneEditor;
+ }
+
+ async function createPane(options = {}) {
+ const direction = normalizePaneDirection(options.direction);
+ const sourcePane = options.sourcePane || getActivePane() || primaryPane;
+ if (!canCreatePane(direction, sourcePane)) {
+ window.toast?.(
+ strings["not enough space"] ||
+ "Not enough space to create another editor pane.",
+ );
+ return null;
+ }
+
+ const pane = createPaneShell();
+ insertPaneIntoLayout(sourcePane, pane, direction);
+ updatePaneLayoutState();
+ animatePaneEntry(pane);
+ await createPaneEditor(pane);
updatePaneLayoutState();
syncOpenFileList();
@@ -2201,9 +2224,11 @@ async function EditorManager($header, $body) {
const fileToActivate = targetPane.files.includes(preferredFile)
? preferredFile
: targetPane.activeFile;
- targetPane.activeFile = null;
- setActivePane(targetPane, { emitSwitch: false });
- fileToActivate?.makeActive();
+ if (fileToActivate) {
+ fileToActivate.makeActive();
+ } else {
+ activatePane(targetPane);
+ }
syncOpenFileList();
return true;
}
@@ -2217,14 +2242,7 @@ async function EditorManager($header, $body) {
(index + offset + orderedPanes.length) % orderedPanes.length
];
if (!nextPane) return false;
- const fileToActivate = nextPane.activeFile;
- nextPane.activeFile = null;
- setActivePane(nextPane, { emitSwitch: false });
- if (fileToActivate) {
- fileToActivate.makeActive();
- } else {
- nextPane.editor?.focus();
- }
+ activatePane(nextPane);
return true;
}
@@ -2282,14 +2300,7 @@ async function EditorManager($header, $body) {
}
if (!bestPane) return false;
- const fileToActivate = bestPane.activeFile;
- bestPane.activeFile = null;
- setActivePane(bestPane, { emitSwitch: false });
- if (fileToActivate) {
- fileToActivate.makeActive();
- } else {
- bestPane.editor?.focus();
- }
+ activatePane(bestPane);
return true;
}
@@ -3248,6 +3259,7 @@ async function EditorManager($header, $body) {
if (manager.files.includes(file)) return;
const pane = getPaneById(file.paneId) || getActivePane() || primaryPane;
insertFileIntoPane(file, pane);
+ if (!pane.activeFile) pane.activeFile = file;
rebuildFileListFromPanes();
syncOpenFileList();
if (!manager.activeFile) {
From c9b22f449940e10358d5cf96686ca3ec80077617 Mon Sep 17 00:00:00 2001
From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com>
Date: Mon, 29 Jun 2026 22:50:49 +0530
Subject: [PATCH 07/20] fix
---
src/handlers/editorFileTab.js | 7 +-
src/lib/editorFile.js | 14 ++-
src/lib/editorManager.js | 156 ++++++++++++++++++++++++++++++++--
3 files changed, 165 insertions(+), 12 deletions(-)
diff --git a/src/handlers/editorFileTab.js b/src/handlers/editorFileTab.js
index c7180d742..4ee7d5734 100644
--- a/src/handlers/editorFileTab.js
+++ b/src/handlers/editorFileTab.js
@@ -145,6 +145,7 @@ export default function startDrag(e) {
$tabClone.style.width = `${rect.width}px`;
$tabClone.style.transform = `translate3d(${rect.x}px, ${rect.y}px, 0)`;
$tab.style.opacity = "0.35";
+ $tab.dataset.editorTabDragging = "true";
app.append($tabClone);
$tab.click();
@@ -278,6 +279,7 @@ function finishDrag(shouldSettleClone) {
function cleanupDrag() {
$tab.style.opacity = "";
+ delete $tab.dataset.editorTabDragging;
$tabClone.remove();
$tabClone = null;
$originParent = null;
@@ -361,7 +363,7 @@ function commitPaneTransfer() {
activate: true,
index,
});
- editorManager.updatePaneFileOrderFromTabs?.($parent);
+ editorManager.updatePaneFileOrderFromTabs?.($parent, { draggedFile });
}
function reorderTab($insertBefore) {
@@ -523,7 +525,8 @@ function getClientPos(e) {
*/
function updateFileList($parent) {
if (typeof editorManager.updatePaneFileOrderFromTabs === "function") {
- if (editorManager.updatePaneFileOrderFromTabs($parent)) return;
+ if (editorManager.updatePaneFileOrderFromTabs($parent, { draggedFile }))
+ return;
}
const pinnedCount = editorManager.files.filter((file) => file.pinned).length;
diff --git a/src/lib/editorFile.js b/src/lib/editorFile.js
index 849dae1e3..afc62d13a 100644
--- a/src/lib/editorFile.js
+++ b/src/lib/editorFile.js
@@ -1210,6 +1210,8 @@ export default class EditorFile {
silentPinned = false,
suppressPanePlaceholder = false,
} = options || {};
+ const suppressFallback =
+ suppressPanePlaceholder && this.isPanePlaceholder && !this.isUnsaved;
if (this.id === config.DEFAULT_FILE_SESSION && !editorManager.files.length)
return false;
@@ -1246,14 +1248,18 @@ export default class EditorFile {
if (!files.length) {
Sidebar.hide();
editorManager.activeFile = null;
- new EditorFile();
- } else if (removal?.wasPaneActive && removal.nextFile) {
+ if (!suppressFallback) new EditorFile();
+ } else if (
+ removal?.wasPaneActive &&
+ removal.nextFile &&
+ !suppressFallback
+ ) {
removal.nextFile.makeActive();
} else if (
removal?.wasPaneActive &&
removal.pane &&
!removal.nextFile &&
- !suppressPanePlaceholder
+ !suppressFallback
) {
new EditorFile(config.DEFAULT_FILE_NAME, {
paneId: removal.pane.id,
@@ -1261,7 +1267,7 @@ export default class EditorFile {
isUnsaved: false,
isPanePlaceholder: true,
});
- } else if (wasActive) {
+ } else if (wasActive && !suppressFallback) {
(
editorManager.activePane?.activeFile || files[files.length - 1]
).makeActive();
diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js
index b17179097..216b341ef 100644
--- a/src/lib/editorManager.js
+++ b/src/lib/editorManager.js
@@ -193,6 +193,16 @@ async function EditorManager($header, $body) {
const $globalOpenFileList = (
);
+ const globalOpenFileListNative = {
+ append: $globalOpenFileList.append.bind($globalOpenFileList),
+ appendChild: $globalOpenFileList.appendChild.bind($globalOpenFileList),
+ insertBefore: $globalOpenFileList.insertBefore.bind($globalOpenFileList),
+ prepend: $globalOpenFileList.prepend.bind($globalOpenFileList),
+ replaceChildren:
+ $globalOpenFileList.replaceChildren.bind($globalOpenFileList),
+ };
+ const $paneAwareOpenFileList =
+ createPaneAwareOpenFileListProxy($globalOpenFileList);
let $container = createEditorContainer();
let paneLayoutRoot = null;
const primaryPane = createPaneShell($container);
@@ -218,6 +228,71 @@ async function EditorManager($header, $body) {
return $el;
}
+ function createPaneAwareOpenFileListProxy(target) {
+ return new Proxy(target, {
+ get(target, prop) {
+ if (prop === "children" || prop === "childNodes") {
+ return toDomCollection(getOpenFileListChildren());
+ }
+ if (prop === "childElementCount")
+ return getOpenFileListChildren().length;
+ if (prop === "firstChild" || prop === "firstElementChild") {
+ return getOpenFileListChildren()[0] || null;
+ }
+ if (prop === "lastChild" || prop === "lastElementChild") {
+ const children = getOpenFileListChildren();
+ return children[children.length - 1] || null;
+ }
+ if (prop === "append" || prop === "appendChild" || prop === "prepend") {
+ return (...args) => mutateVisibleOpenFileList(prop, args);
+ }
+ if (prop === "insertBefore") {
+ return (node, child) =>
+ mutateVisibleOpenFileList("insertBefore", [node, child]);
+ }
+ if (prop === "contains") {
+ return (node) =>
+ getOpenFileListChildren().some(
+ (child) => child === node || child.contains(node),
+ ) || target.contains(node);
+ }
+ if (prop === "querySelector") {
+ return (selector) => queryOpenFileList(selector)[0] || null;
+ }
+ if (prop === "querySelectorAll") {
+ return (selector) => toDomCollection(queryOpenFileList(selector));
+ }
+ if (prop === "getElementsByClassName") {
+ return (className) =>
+ toDomCollection(
+ collectFromOpenFileListChildren((child) => [
+ ...(child.classList.contains(className) ? [child] : []),
+ ...child.getElementsByClassName(className),
+ ]),
+ );
+ }
+ if (prop === "getElementsByTagName") {
+ return (tagName) => {
+ const selector = tagName === "*" ? "*" : tagName;
+ return toDomCollection(queryOpenFileList(selector));
+ };
+ }
+ if (prop === "getClientRects" || prop === "getBoundingClientRect") {
+ return (...args) => {
+ const targetList = getOpenFileListMutationTarget();
+ return targetList[prop](...args);
+ };
+ }
+ if (typeof prop === "string" && /^\d+$/.test(prop)) {
+ return getOpenFileListChildren()[Number(prop)];
+ }
+
+ const value = Reflect.get(target, prop, target);
+ return typeof value === "function" ? value.bind(target) : value;
+ },
+ });
+ }
+
function createPaneShell(editorContainer = createEditorContainer()) {
const pane = {
id: `pane-${++paneIdCounter}`,
@@ -2771,7 +2846,10 @@ async function EditorManager($header, $body) {
return isScrolling;
},
get openFileList() {
- if (isPaneTabLayout()) return $globalOpenFileList;
+ if (isPaneTabLayout()) {
+ syncGlobalOpenFileListMirror();
+ return $paneAwareOpenFileList;
+ }
if (!$openFileList || $openFileList === $globalOpenFileList) {
initFileTabContainer();
}
@@ -3296,8 +3374,72 @@ async function EditorManager($header, $body) {
return manager.files;
}
+ function toDomCollection(nodes) {
+ const collection = [...nodes].filter(Boolean);
+ collection.item = (index) => collection[index] || null;
+ return collection;
+ }
+
+ function getOpenFileListChildren() {
+ if (!isPaneTabLayout()) {
+ const list = $openFileList?.$ul || $openFileList;
+ return list ? [...list.children] : [];
+ }
+
+ return getOrderedPanes().flatMap((pane) => [...pane.tabList.children]);
+ }
+
+ function collectFromOpenFileListChildren(collector) {
+ const result = [];
+ getOpenFileListChildren().forEach((child) => {
+ result.push(...collector(child));
+ });
+ return result;
+ }
+
+ function queryOpenFileList(selector) {
+ return collectFromOpenFileListChildren((child) => [
+ ...(child.matches?.(selector) ? [child] : []),
+ ...child.querySelectorAll(selector),
+ ]);
+ }
+
+ function getOpenFileListMutationTarget(referenceNode = null) {
+ if (!isPaneTabLayout()) {
+ if (!$openFileList || $openFileList === $globalOpenFileList) {
+ initFileTabContainer();
+ }
+ return $openFileList?.$ul || $openFileList || $globalOpenFileList;
+ }
+
+ if (referenceNode?.parentElement?.classList?.contains("editor-pane-tabs")) {
+ return referenceNode.parentElement;
+ }
+
+ return getPaneTabList() || getActivePane()?.tabList || $globalOpenFileList;
+ }
+
+ function mutateVisibleOpenFileList(method, args) {
+ const target = getOpenFileListMutationTarget(
+ method === "insertBefore" ? args[1] : null,
+ );
+ if (!target || target === $globalOpenFileList) {
+ return globalOpenFileListNative[method]?.(...args);
+ }
+
+ if (method === "insertBefore") {
+ const [node, referenceNode] = args;
+ return target.insertBefore(
+ node,
+ referenceNode?.parentElement === target ? referenceNode : null,
+ );
+ }
+
+ return target[method](...args);
+ }
+
function syncGlobalOpenFileListMirror() {
- $globalOpenFileList.replaceChildren(
+ globalOpenFileListNative.replaceChildren(
...manager.files.map((file) => {
const tab = file.tab.cloneNode(true);
tab.dataset.fileId = file.id;
@@ -3479,7 +3621,7 @@ async function EditorManager($header, $body) {
return { pane, wasPaneActive, nextFile };
}
- function updatePaneFileOrderFromTabs($tabList) {
+ function updatePaneFileOrderFromTabs($tabList, options = {}) {
const pane = $tabList?.__editorPane;
if (!pane) return false;
@@ -3490,9 +3632,11 @@ async function EditorManager($header, $body) {
pane.files = nextFiles;
const pinnedCount = pane.files.filter((file) => file.pinned).length;
- const draggedFile = pane.files.find(
- (file) => file.tab?.style.opacity === "0.35",
- );
+ const draggedFile = pane.files.includes(options.draggedFile)
+ ? options.draggedFile
+ : pane.files.find(
+ (file) => file.tab?.dataset.editorTabDragging === "true",
+ );
if (draggedFile) {
const draggedIndex = pane.files.indexOf(draggedFile);
let nextPinnedState;
From e7f4d09489da7e15528eb0cab837ca62b6d7f523 Mon Sep 17 00:00:00 2001
From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com>
Date: Mon, 29 Jun 2026 23:11:57 +0530
Subject: [PATCH 08/20] fix debounce timer and load related issue
---
src/lib/editorFile.js | 34 +++++++----
src/lib/editorManager.js | 118 ++++++++++++++++++++++++++++++++-------
2 files changed, 121 insertions(+), 31 deletions(-)
diff --git a/src/lib/editorFile.js b/src/lib/editorFile.js
index afc62d13a..8e6c619ba 100644
--- a/src/lib/editorFile.js
+++ b/src/lib/editorFile.js
@@ -1296,15 +1296,26 @@ export default class EditorFile {
}
setReadOnly(value) {
+ const readOnly = !!value;
+ this.readOnly = readOnly;
+ this.#editable = !readOnly;
+
try {
- const { editor, readOnlyCompartment } = editorManager;
- if (!editor) return;
- if (!readOnlyCompartment) return;
- editor.dispatch({
- effects: readOnlyCompartment.reconfigure(
- EditorState.readOnly.of(!!value),
- ),
- });
+ const { readOnlyCompartment } = editorManager;
+ if (readOnlyCompartment) {
+ const pane = editorManager.getFilePane?.(this);
+ const targetEditor =
+ pane?.activeFile?.id === this.id
+ ? pane.editor
+ : editorManager.activeFile?.id === this.id
+ ? editorManager.editor
+ : null;
+ targetEditor?.dispatch({
+ effects: readOnlyCompartment.reconfigure(
+ EditorState.readOnly.of(readOnly),
+ ),
+ });
+ }
} catch (error) {
console.warn(
`Failed to update read-only state for ${this.filename || this.uri}`,
@@ -1312,9 +1323,6 @@ export default class EditorFile {
);
}
- // Sync internal flags and header
- this.readOnly = !!value;
- this.#editable = !this.readOnly;
if (editorManager.activeFile?.id === this.id) {
editorManager.header.subText = this.#getTitle();
}
@@ -1715,7 +1723,9 @@ export default class EditorFile {
this.loading = false;
const { activeFile, emit } = editorManager;
- if (activeFile?.id === this.id) {
+ const pane = editorManager.getFilePane?.(this);
+ const isActiveInPane = pane?.activeFile?.id === this.id;
+ if (isActiveInPane || activeFile?.id === this.id) {
this.setReadOnly(editable === false);
emit("file-loaded", this);
} else if (editable !== undefined) {
diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js
index 216b341ef..c52cee025 100644
--- a/src/lib/editorManager.js
+++ b/src/lib/editorManager.js
@@ -121,9 +121,7 @@ async function EditorManager($header, $body) {
const PANE_SPLIT_HORIZONTAL = "horizontal";
const PANE_SPLIT_VERTICAL = "vertical";
- // Debounce timers for CodeMirror change handling
- let checkTimeout = null;
- let autosaveTimeout = null;
+ const docSyncTimers = new WeakMap();
let touchSelectionController = null;
let touchSelectionSyncRaf = 0;
let nativeContextMenuDisabled = null;
@@ -137,6 +135,26 @@ async function EditorManager($header, $body) {
console.warn(message, error);
}
+ function getDocSyncTimers(file) {
+ let timers = docSyncTimers.get(file);
+ if (!timers) {
+ timers = {
+ checkTimeout: null,
+ autosaveTimeout: null,
+ };
+ docSyncTimers.set(file, timers);
+ }
+ return timers;
+ }
+
+ function clearDocSyncTimers(file) {
+ const timers = docSyncTimers.get(file);
+ if (!timers) return;
+ if (timers.checkTimeout) clearTimeout(timers.checkTimeout);
+ if (timers.autosaveTimeout) clearTimeout(timers.autosaveTimeout);
+ docSyncTimers.delete(file);
+ }
+
function isCoarsePointerDevice() {
if (typeof window !== "undefined") {
try {
@@ -2479,12 +2497,23 @@ async function EditorManager($header, $body) {
markLanguageReady(file, languageSignature, false);
result
.then((ext) => {
+ const pane = getFilePane(file);
+ const isGlobalActive = manager.activeFile?.id === fileId;
+ const isPaneActive = pane?.activeFile?.id === fileId;
if (
- manager.activeFile?.id !== fileId ||
+ (!isGlobalActive && !isPaneActive) ||
file.__cmLanguageSignature !== languageSignature
) {
return;
}
+
+ if (isPaneActive && pane !== getActivePane()) {
+ withPaneEditorContext(pane, () => {
+ dispatchLanguageExtension(file, languageSignature, ext, warnKey);
+ });
+ return;
+ }
+
dispatchLanguageExtension(file, languageSignature, ext, warnKey);
})
.catch(() => {
@@ -2546,10 +2575,51 @@ async function EditorManager($header, $body) {
touchSelectionController?.onSessionChanged();
}
+ function withPaneEditorContext(pane, callback) {
+ if (!pane?.editor) return callback();
+
+ const previousEditor = editor;
+ const previousContainer = $container;
+ const previousTouchSelectionController = touchSelectionController;
+
+ editor = pane.editor;
+ $container = pane.editorContainer || $container;
+ touchSelectionController = pane.touchSelectionController || null;
+
+ try {
+ return callback();
+ } finally {
+ editor = previousEditor;
+ $container = previousContainer;
+ touchSelectionController = previousTouchSelectionController;
+ }
+ }
+
+ function applyFileToPaneEditor(file, pane, options = {}) {
+ if (!file || file.type !== "editor") return false;
+ if (!pane?.editor || pane.activeFile?.id !== file.id) return false;
+ const isCurrentPane = pane === getActivePane();
+ const paneOptions = {
+ ...options,
+ restoreScroll: options.restoreScroll ?? isCurrentPane,
+ scheduleLsp: options.scheduleLsp ?? isCurrentPane,
+ };
+ if (isCurrentPane) {
+ applyFileToEditor(file, paneOptions);
+ } else {
+ withPaneEditorContext(pane, () => applyFileToEditor(file, paneOptions));
+ }
+ return true;
+ }
+
// Helper: apply a file's content and language to the editor view
function applyFileToEditor(file, options = {}) {
if (!file || file.type !== "editor") return;
- const { forceRecreate = false } = options;
+ const {
+ forceRecreate = false,
+ restoreScroll = true,
+ scheduleLsp = true,
+ } = options;
const extensionSignature = getEditorExtensionSignature(file);
const languageSignature = getFileLanguageSignature(
file,
@@ -2577,8 +2647,8 @@ async function EditorManager($header, $body) {
}
}
- restoreFileScrollPosition(file);
- scheduleLspForFile(file);
+ if (restoreScroll) restoreFileScrollPosition(file);
+ if (scheduleLsp) scheduleLspForFile(file);
return;
}
@@ -2672,9 +2742,8 @@ async function EditorManager($header, $body) {
);
}
- restoreFileScrollPosition(file);
-
- scheduleLspForFile(file);
+ if (restoreScroll) restoreFileScrollPosition(file);
+ if (scheduleLsp) scheduleLspForFile(file);
}
function restoreFileScrollPosition(file) {
@@ -3242,10 +3311,12 @@ async function EditorManager($header, $body) {
file.markEdited();
// Debounced change handling (unsaved flag, cache, autosave)
- if (checkTimeout) clearTimeout(checkTimeout);
- if (autosaveTimeout) clearTimeout(autosaveTimeout);
+ const timers = getDocSyncTimers(file);
+ if (timers.checkTimeout) clearTimeout(timers.checkTimeout);
+ if (timers.autosaveTimeout) clearTimeout(timers.autosaveTimeout);
- checkTimeout = setTimeout(async () => {
+ timers.checkTimeout = setTimeout(async () => {
+ timers.checkTimeout = null;
try {
file.scheduleCacheWrite();
} catch (error) {
@@ -3263,8 +3334,15 @@ async function EditorManager($header, $body) {
const { autosave } = appSettings.value;
if (file.uri && file.isUnsaved && autosave) {
- autosaveTimeout = setTimeout(() => {
- acode.exec("save", false);
+ timers.autosaveTimeout = setTimeout(() => {
+ timers.autosaveTimeout = null;
+ file.save()?.catch?.((error) => {
+ warnRecoverable(
+ `Failed to autosave ${file.filename || file.uri}`,
+ error,
+ `autosave-${file.id}`,
+ );
+ });
}, autosave);
}
@@ -3275,8 +3353,11 @@ async function EditorManager($header, $body) {
// Register critical listeners
manager.on(["file-loaded"], (file) => {
- if (!file) return;
- if (manager.activeFile?.id === file.id && file.type === "editor") {
+ if (!file || file.type !== "editor") return;
+ const pane = getFilePane(file);
+ if (pane?.activeFile?.id === file.id) {
+ applyFileToPaneEditor(file, pane);
+ } else if (manager.activeFile?.id === file.id) {
applyFileToEditor(file);
}
});
@@ -3302,6 +3383,7 @@ async function EditorManager($header, $body) {
});
manager.on(["remove-file"], (file) => {
+ clearDocSyncTimers(file);
detachLspForFile(file);
toggleProblemButton();
});
@@ -3683,8 +3765,6 @@ async function EditorManager($header, $body) {
const scrollMarginRight = textWrap ? 0 : leftMargin;
const scrollMarginBottom = 0;
- let checkTimeout = null;
- let autosaveTimeout;
let scrollTimeout;
let scrollSyncRaf = 0;
const scroller = editor.scrollDOM;
From 538d96884f98bf5da891a081b1d5733180acd374 Mon Sep 17 00:00:00 2001
From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com>
Date: Tue, 30 Jun 2026 05:38:54 +0530
Subject: [PATCH 09/20] fix
---
src/lib/editorManager.js | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js
index c52cee025..d56b45358 100644
--- a/src/lib/editorManager.js
+++ b/src/lib/editorManager.js
@@ -2308,6 +2308,8 @@ async function EditorManager($header, $body) {
});
}
+ pane.touchSelectionController?.destroy?.();
+ pane.touchSelectionController = null;
pane.editor?.destroy?.();
removePaneFromLayout(pane);
const storedPaneIndex = panes.indexOf(pane);
From 8d9eaeb44189aae349fa0dcd48b316e0520ade60 Mon Sep 17 00:00:00 2001
From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com>
Date: Tue, 30 Jun 2026 09:39:02 +0530
Subject: [PATCH 10/20] fix the drag snap-back
---
src/handlers/editorFileTab.js | 1 +
src/lib/editorManager.js | 20 ++++++++++++++++++--
2 files changed, 19 insertions(+), 2 deletions(-)
diff --git a/src/handlers/editorFileTab.js b/src/handlers/editorFileTab.js
index 4ee7d5734..c9795fe44 100644
--- a/src/handlers/editorFileTab.js
+++ b/src/handlers/editorFileTab.js
@@ -281,6 +281,7 @@ function cleanupDrag() {
$tab.style.opacity = "";
delete $tab.dataset.editorTabDragging;
$tabClone.remove();
+ editorManager.syncOpenFileList?.();
$tabClone = null;
$originParent = null;
draggedFile = null;
diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js
index d56b45358..d3f0e0503 100644
--- a/src/lib/editorManager.js
+++ b/src/lib/editorManager.js
@@ -3534,12 +3534,20 @@ async function EditorManager($header, $body) {
);
}
+ function isDraggingFileTab(file) {
+ return file?.tab?.dataset.editorTabDragging === "true";
+ }
+
function syncOpenFileList() {
if (isPaneTabLayout()) {
$paneRoot.classList.remove("hide-pane-tabs");
panes.forEach((pane) => {
+ const preserveCurrentTabOrder = !!pane.tabList.querySelector(
+ '[data-editor-tab-dragging="true"]',
+ );
pane.files.forEach((file) => {
file.tab.classList.toggle("active", pane.activeFile?.id === file.id);
+ if (isDraggingFileTab(file) || preserveCurrentTabOrder) return;
pane.tabList.append(file.tab);
});
pane.element.classList.toggle("empty", pane.files.length === 0);
@@ -3634,6 +3642,15 @@ async function EditorManager($header, $body) {
return pane?.tabList || null;
}
+ function getPaneFallbackFile(pane) {
+ if (!pane?.files?.length) return null;
+ for (let i = pane.files.length - 1; i >= 0; i--) {
+ const file = pane.files[i];
+ if (!isDraggingFileTab(file)) return file;
+ }
+ return null;
+ }
+
function moveFileToPane(file, targetPane, options = {}) {
if (!file || !targetPane) return false;
const {
@@ -3666,8 +3683,7 @@ async function EditorManager($header, $body) {
syncOpenFileList();
if (sourcePane?.activeFile?.id === file.id) {
- const nextSourceFile =
- sourcePane.files[sourcePane.files.length - 1] || null;
+ const nextSourceFile = getPaneFallbackFile(sourcePane);
sourcePane.activeFile = null;
file.tab?.classList.remove("active");
if (nextSourceFile && activateSourceFallback) {
From 6278f4dd55f8234fa35014984fe0dd094b326cf9 Mon Sep 17 00:00:00 2001
From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com>
Date: Tue, 30 Jun 2026 10:06:43 +0530
Subject: [PATCH 11/20] fix
---
src/handlers/editorFileTab.js | 58 ++++++++++++++++++++++-------------
1 file changed, 37 insertions(+), 21 deletions(-)
diff --git a/src/handlers/editorFileTab.js b/src/handlers/editorFileTab.js
index c9795fe44..81b091620 100644
--- a/src/handlers/editorFileTab.js
+++ b/src/handlers/editorFileTab.js
@@ -205,12 +205,15 @@ function releaseDrag(e) {
/**@type {HTMLDivElement} target tab */
const $target = document.elementFromPoint(clientX, clientY);
- const $dropParent = getDropTabList(clientX, clientY);
+ const isPathDropTarget = isFilePathDropTarget($target);
+ const $dropParent = isPathDropTarget
+ ? null
+ : getDropTabList(clientX, clientY);
if ($dropParent && $dropParent !== $parent) {
moveDragToParent($dropParent);
}
const shouldCommitReorder =
- $parent.contains($target) || $dropParent === $parent;
+ !!$dropParent && ($parent.contains($target) || $dropParent === $parent);
if (shouldCommitReorder) {
updateDragPreview(clientX, clientY);
@@ -219,23 +222,8 @@ function releaseDrag(e) {
} else if (didReorder) {
updateFileList($parent);
}
- } else if (
- $target &&
- ($target.tagName === "INPUT" ||
- $target.tagName === "TEXTAREA" ||
- $target.isContentEditable ||
- $target.closest(".cm-editor"))
- ) {
- // If released on an input area or CodeMirror editor
- const filePath = editorManager.activeFile.uri;
- if (filePath) {
- if ($target.closest(".cm-editor")) {
- const view = editorManager.editor;
- view.dispatch(view.state.replaceSelection(filePath));
- } else {
- $target.value += filePath;
- }
- }
+ } else if (isPathDropTarget) {
+ insertDraggedFilePath($target);
}
const shouldSettleClone = shouldCommitReorder || didReorder;
@@ -419,10 +407,38 @@ function getDropTabList(clientX, clientY) {
const $tabList = $target?.closest?.(".editor-pane-tabs");
if (isValidDropTabList($tabList)) return $tabList;
+ if (isFilePathDropTarget($target)) return null;
+
const pane = $target?.closest?.(".editor-pane")?.__editorPane;
- if (isValidDropTabList(pane?.tabList)) return pane.tabList;
+ if (pane?.tabList !== $parent && isValidDropTabList(pane?.tabList)) {
+ return pane.tabList;
+ }
+
+ return null;
+}
- return $parent;
+function isFilePathDropTarget($target) {
+ return !!(
+ $target &&
+ ($target.tagName === "INPUT" ||
+ $target.tagName === "TEXTAREA" ||
+ $target.isContentEditable ||
+ $target.closest(".cm-editor"))
+ );
+}
+
+function insertDraggedFilePath($target) {
+ const filePath = draggedFile?.uri || editorManager.activeFile?.uri;
+ if (!filePath) return;
+
+ if ($target.closest(".cm-editor")) {
+ const view = editorManager.editor;
+ view.dispatch(view.state.replaceSelection(filePath));
+ } else if ($target.isContentEditable) {
+ $target.textContent += filePath;
+ } else {
+ $target.value += filePath;
+ }
}
function isValidDropTabList($tabList) {
From 38e3de78feb4a0934a8348fbd692487384cd5adf Mon Sep 17 00:00:00 2001
From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com>
Date: Tue, 30 Jun 2026 11:16:30 +0530
Subject: [PATCH 12/20] fix tab cleanup
---
src/handlers/editorFileTab.js | 30 +++++++++++++++++++++---------
1 file changed, 21 insertions(+), 9 deletions(-)
diff --git a/src/handlers/editorFileTab.js b/src/handlers/editorFileTab.js
index 81b091620..635b6a4c0 100644
--- a/src/handlers/editorFileTab.js
+++ b/src/handlers/editorFileTab.js
@@ -249,6 +249,12 @@ function finishDrag(shouldSettleClone) {
if (shouldSettleClone) {
const rect = $tab.getBoundingClientRect();
+ let cleaned = false;
+ const safeCleanup = () => {
+ if (cleaned) return;
+ cleaned = true;
+ cleanupDrag();
+ };
animate(
$tabClone,
{ transform: `translate3d(${rect.left}px, ${rect.top}px, 0)` },
@@ -258,7 +264,10 @@ function finishDrag(shouldSettleClone) {
: RELEASE_DURATION,
ease: SPRING_EASING,
},
- ).then(cleanupDrag);
+ )
+ .then(safeCleanup)
+ .catch(safeCleanup);
+ setTimeout(safeCleanup, 500);
return;
}
@@ -404,7 +413,9 @@ function updateParentMetrics() {
function getDropTabList(clientX, clientY) {
const $target = document.elementFromPoint(clientX, clientY);
- const $tabList = $target?.closest?.(".editor-pane-tabs");
+ const $tabList =
+ $target?.closest?.(".open-file-list") ||
+ $target?.closest?.(".file-list > ul");
if (isValidDropTabList($tabList)) return $tabList;
if (isFilePathDropTarget($target)) return null;
@@ -442,8 +453,9 @@ function insertDraggedFilePath($target) {
}
function isValidDropTabList($tabList) {
- if (!$tabList?.__editorPane) return false;
+ if (!$tabList) return false;
if ($tabList === $originParent) return true;
+ if (!$tabList.__editorPane) return false;
if (!allowPaneTransfer) return false;
return $tabList.getClientRects().length > 0;
}
@@ -560,19 +572,19 @@ function updateFileList($parent) {
editorManager.files = newFileList;
- const draggedFile = newFileList.find((file) => file.tab === $tab);
- if (draggedFile) {
- const draggedIndex = newFileList.indexOf(draggedFile);
+ const localDraggedFile = newFileList.find((file) => file.tab === $tab);
+ if (localDraggedFile) {
+ const draggedIndex = newFileList.indexOf(localDraggedFile);
let nextPinnedState;
- if (!draggedFile.pinned && draggedIndex < pinnedCount) {
+ if (!localDraggedFile.pinned && draggedIndex < pinnedCount) {
nextPinnedState = true;
- } else if (draggedFile.pinned && draggedIndex >= pinnedCount) {
+ } else if (localDraggedFile.pinned && draggedIndex >= pinnedCount) {
nextPinnedState = false;
}
if (nextPinnedState !== undefined) {
- draggedFile.setPinnedState(nextPinnedState, { reorder: false });
+ localDraggedFile.setPinnedState(nextPinnedState, { reorder: false });
if (typeof editorManager.normalizePinnedTabOrder === "function") {
editorManager.normalizePinnedTabOrder(editorManager.files);
}
From 76035a53f5d39981f247dceebdf3b363559a2a44 Mon Sep 17 00:00:00 2001
From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com>
Date: Tue, 30 Jun 2026 16:13:20 +0530
Subject: [PATCH 13/20] added cleanup
---
src/handlers/editorFileTab.js | 8 ++++--
src/lib/editorManager.js | 49 +++++++++++++++++++++++++++++------
2 files changed, 47 insertions(+), 10 deletions(-)
diff --git a/src/handlers/editorFileTab.js b/src/handlers/editorFileTab.js
index 635b6a4c0..4c3de6e16 100644
--- a/src/handlers/editorFileTab.js
+++ b/src/handlers/editorFileTab.js
@@ -475,6 +475,7 @@ function animateTabReorder($parent, previousRects) {
if (oldAnim) {
oldAnim.cancel?.();
reorderAnimations.delete($child);
+ $child.style.transform = "";
}
const previousRect = previousRects.get($child);
@@ -503,11 +504,14 @@ function animateTabReorder($parent, previousRects) {
);
reorderAnimations.set($child, anim);
- anim.then(() => {
+ const cleanup = () => {
if (reorderAnimations.get($child) === anim) {
+ $child.style.transform = "";
reorderAnimations.delete($child);
}
- });
+ };
+
+ anim.then(cleanup).catch(cleanup);
}
}
diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js
index d3f0e0503..81075a425 100644
--- a/src/lib/editorManager.js
+++ b/src/lib/editorManager.js
@@ -509,6 +509,17 @@ async function EditorManager($header, $body) {
return node.children.flatMap((child) => getOrderedPanes(child));
}
+ function getVisiblePaneRect(pane) {
+ const element = pane?.element;
+ if (!element?.isConnected || element.getClientRects().length === 0) {
+ return null;
+ }
+
+ const rect = element.getBoundingClientRect();
+ if (rect.width < 1 || rect.height < 1) return null;
+ return rect;
+ }
+
function updatePaneLayoutState() {
$paneRoot.classList.toggle("multi-pane", panes.length > 1);
renderPaneLayout();
@@ -522,14 +533,21 @@ async function EditorManager($header, $body) {
pane.element.style.opacity = "0";
pane.element.style.transform = "scale(0.985)";
+ const element = pane.element;
+ let cleaned = false;
+ const cleanup = () => {
+ if (cleaned) return;
+ cleaned = true;
+ element.style.opacity = "";
+ element.style.transform = "";
+ };
animate(
- pane.element,
+ element,
{ opacity: 1, transform: "scale(1)" },
{ type: "spring", stiffness: 360, damping: 32 },
- ).then(() => {
- pane.element.style.opacity = "";
- pane.element.style.transform = "";
- });
+ )
+ .then(cleanup)
+ .catch(cleanup);
}
function startPaneResize(event, splitNode, childIndex, handle) {
@@ -2353,15 +2371,30 @@ async function EditorManager($header, $body) {
const active = getActivePane();
if (!active || panes.length <= 1) return false;
- const activeRect = active.element.getBoundingClientRect();
+ const orderedPanes = getOrderedPanes();
+ const visiblePanes = orderedPanes
+ .map((pane) => ({ pane, rect: getVisiblePaneRect(pane) }))
+ .filter((entry) => entry.rect);
+ const activeEntry = visiblePanes.find((entry) => entry.pane === active);
+
+ if (!activeEntry || visiblePanes.length <= 1) {
+ if (direction === "left" || direction === "up") {
+ return focusPaneByOffset(-1);
+ }
+ if (direction === "right" || direction === "down") {
+ return focusPaneByOffset(1);
+ }
+ return false;
+ }
+
+ const activeRect = activeEntry.rect;
const activeCenterX = activeRect.left + activeRect.width / 2;
const activeCenterY = activeRect.top + activeRect.height / 2;
let bestPane = null;
let bestScore = Number.POSITIVE_INFINITY;
- for (const pane of getOrderedPanes()) {
+ for (const { pane, rect } of visiblePanes) {
if (pane === active) continue;
- const rect = pane.element.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
let axisDistance = 0;
From 140a706b7d842216311111a605c5942e60fa1f34 Mon Sep 17 00:00:00 2001
From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com>
Date: Tue, 30 Jun 2026 16:33:52 +0530
Subject: [PATCH 14/20] fix
---
src/handlers/editorFileTab.js | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/handlers/editorFileTab.js b/src/handlers/editorFileTab.js
index 4c3de6e16..548c77d37 100644
--- a/src/handlers/editorFileTab.js
+++ b/src/handlers/editorFileTab.js
@@ -443,7 +443,9 @@ function insertDraggedFilePath($target) {
if (!filePath) return;
if ($target.closest(".cm-editor")) {
- const view = editorManager.editor;
+ const view =
+ $target.closest(".editor-pane")?.__editorPane?.editor ||
+ editorManager.editor;
view.dispatch(view.state.replaceSelection(filePath));
} else if ($target.isContentEditable) {
$target.textContent += filePath;
From 2c78d6dadf7df4bed52f72adc46a74c1e32b3471 Mon Sep 17 00:00:00 2001
From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com>
Date: Tue, 30 Jun 2026 21:07:12 +0530
Subject: [PATCH 15/20] fixed bunch of issues introduced accidently
---
src/components/sidebar/index.js | 7 +-
src/lib/commands.js | 18 +++--
src/lib/editorFile.js | 76 ++++++++++++--------
src/lib/editorManager.js | 123 ++++++++++++++++++++++++--------
src/lib/fileList.js | 2 +-
src/lib/showFileInfo.js | 11 +--
src/main.js | 14 ++--
7 files changed, 177 insertions(+), 74 deletions(-)
diff --git a/src/components/sidebar/index.js b/src/components/sidebar/index.js
index 7920facfb..495601abb 100644
--- a/src/components/sidebar/index.js
+++ b/src/components/sidebar/index.js
@@ -644,7 +644,12 @@ function create($container, $toggler) {
root.style.width = `calc(100% - ${width}px)`;
clearTimeout(setWidthTimeout);
setWidthTimeout = setTimeout(() => {
- editorManager?.editor?.resize(true);
+ const editor = editorManager?.editor;
+ if (typeof editor?.resize === "function") {
+ editor.resize(true);
+ } else {
+ editor?.requestMeasure?.();
+ }
}, 300);
}
diff --git a/src/lib/commands.js b/src/lib/commands.js
index dfdeea43f..c704008b0 100644
--- a/src/lib/commands.js
+++ b/src/lib/commands.js
@@ -238,7 +238,9 @@ export default {
navigator.app.exitApp();
},
"edit-with"() {
- editorManager.activeFile.editWith();
+ const { activeFile } = editorManager;
+ if (!activeFile?.uri) return;
+ activeFile.editWith?.();
},
async "find-file"() {
const { default: findFile } = await import(
@@ -344,7 +346,9 @@ export default {
editorManager.editor.contentDOM.blur();
},
"open-with"() {
- editorManager.activeFile.openWith();
+ const { activeFile } = editorManager;
+ if (!activeFile?.uri) return;
+ activeFile.openWith?.();
},
async "open-file"() {
editorManager.editor.contentDOM.blur();
@@ -408,12 +412,13 @@ export default {
browser.open(url);
},
run() {
- editorManager.activeFile[
+ const { activeFile } = editorManager;
+ activeFile?.[
appSettings.value.useCurrentFileForPreview ? "runFile" : "run"
]?.();
},
"run-file"() {
- editorManager.activeFile.runFile?.();
+ editorManager.activeFile?.runFile?.();
},
async save(showToast) {
try {
@@ -443,7 +448,9 @@ export default {
saveState();
},
share() {
- editorManager.activeFile.share();
+ const { activeFile } = editorManager;
+ if (!activeFile?.uri) return;
+ activeFile.share?.();
},
async "pin-file-shortcut"() {
const file = editorManager.activeFile;
@@ -637,6 +644,7 @@ export default {
}
},
async eol() {
+ if (editorManager.activeFile?.type !== "editor") return;
const eol = await select(strings["new line mode"], ["unix", "windows"], {
default: editorManager.activeFile.eol,
});
diff --git a/src/lib/editorFile.js b/src/lib/editorFile.js
index 8e6c619ba..3629d6379 100644
--- a/src/lib/editorFile.js
+++ b/src/lib/editorFile.js
@@ -28,6 +28,44 @@ import run from "./run";
import saveFile from "./saveFile";
import appSettings from "./settings";
+function syncQuickToolsVisibility(file) {
+ const { $toggler } = quickTools;
+ const hideForFile = !!file?.hideQuickTools;
+
+ clearTimeout($toggler._hideTimeout);
+ if (hideForFile || !appSettings.value.floatingButton) {
+ $toggler.classList.add("hide");
+ $toggler._hideTimeout = setTimeout(() => {
+ $toggler.remove();
+ $toggler._hideTimeout = null;
+ }, 300);
+ } else {
+ $toggler._hideTimeout = null;
+ $toggler.classList.remove("hide");
+ if (!$toggler.isConnected) {
+ root.appendOuter($toggler);
+ }
+ }
+
+ if (hideForFile) {
+ actions("set-height", { height: 0, save: false });
+ return;
+ }
+
+ const quickToolsHeight =
+ appSettings.value.quickTools !== undefined
+ ? appSettings.value.quickTools
+ : 1;
+ actions("set-height", { height: quickToolsHeight, save: false });
+}
+
+function isTouchDevice() {
+ return (
+ typeof navigator !== "undefined" &&
+ Number(navigator.maxTouchPoints || 0) > 0
+ );
+}
+
/**
* Creates a Proxy around an EditorState that provides Ace-compatible methods.
* @param {EditorState} state - The raw CodeMirror EditorState
@@ -1374,6 +1412,10 @@ export default class EditorFile {
const wasActivePane = editorManager.activePane?.id === pane?.id;
const { activeFile, switchFile } = editorManager;
const paneActiveFile = pane?.activeFile;
+ const activeEditor = editorManager.editor;
+ const editorHadDomFocus =
+ activeEditor?.contentDOM === document.activeElement ||
+ activeEditor?.contentDOM?.contains(document.activeElement);
const inactiveFiles = [paneActiveFile, !wasActivePane ? activeFile : null];
const blurredFileIds = new Set();
@@ -1384,7 +1426,10 @@ export default class EditorFile {
blurredFileIds.add(file.id);
}
- if (activeFile?.id === this.id && wasActivePane) return;
+ if (activeFile?.id === this.id && wasActivePane) {
+ syncQuickToolsVisibility(this);
+ return;
+ }
switchFile(this.id, pane);
@@ -1393,7 +1438,7 @@ export default class EditorFile {
// Show/hide appropriate content
if (this.type === "editor") {
editorManager.container.style.display = "block";
- if (this.focused) {
+ if (this.focused && editorHadDomFocus && !isTouchDevice()) {
editor.focus();
} else {
editor.contentDOM.blur();
@@ -1427,32 +1472,7 @@ export default class EditorFile {
this.#loadText();
}
- // Handle quicktools visibility based on hideQuickTools property
- if (this.hideQuickTools) {
- const { $toggler } = quickTools;
- clearTimeout($toggler._hideTimeout);
- $toggler.classList.add("hide");
- $toggler._hideTimeout = setTimeout(() => {
- $toggler.remove();
- $toggler._hideTimeout = null;
- }, 300);
- actions("set-height", { height: 0, save: false });
- } else {
- const { $toggler } = quickTools;
- if (appSettings.value.floatingButton) {
- clearTimeout($toggler._hideTimeout);
- $toggler._hideTimeout = null;
- $toggler.classList.remove("hide");
- if (!$toggler.isConnected) {
- root.appendOuter($toggler);
- }
- }
- const quickToolsHeight =
- appSettings.value.quickTools !== undefined
- ? appSettings.value.quickTools
- : 1;
- actions("set-height", { height: quickToolsHeight, save: false });
- }
+ syncQuickToolsVisibility(this);
editorManager.header.subText = this.#getTitle();
diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js
index 81075a425..c683953a9 100644
--- a/src/lib/editorManager.js
+++ b/src/lib/editorManager.js
@@ -317,6 +317,7 @@ async function EditorManager($header, $body) {
files: [],
activeFile: null,
editor: null,
+ cleanupEditorListeners: null,
editorContainer,
touchSelectionController: null,
element: ,
@@ -336,7 +337,7 @@ async function EditorManager($header, $body) {
pane.element.addEventListener(
"pointerdown",
() => {
- activatePane(pane);
+ activatePane(pane, { focusEditor: false });
},
true,
);
@@ -727,6 +728,39 @@ async function EditorManager($header, $body) {
if (!appSettings.value.shiftClickSelection) return false;
return !!event?.shiftKey || quickTools?.$footer?.dataset?.shift != null;
};
+
+ function registerSoftKeyboardCursorReveal() {
+ const shouldRevealCursor = () => {
+ const view = editor;
+ if (!view || manager?.activeFile?.type !== "editor") return false;
+ const activeElement = document.activeElement;
+ return (
+ view.contentDOM === activeElement ||
+ view.contentDOM.contains(activeElement)
+ );
+ };
+
+ keyboardHandler.on("keyboardShowStart", () => {
+ requestAnimationFrame(() => {
+ if (!shouldRevealCursor()) return;
+ if (isCursorRevealSuppressed()) return;
+ scrollCursorIntoView({ behavior: "instant" });
+ });
+ });
+ keyboardHandler.on("keyboardShow", () => {
+ if (!shouldRevealCursor()) return;
+ if (isCursorRevealSuppressed()) return;
+ scrollCursorIntoView();
+ });
+ keyboardHandler.on("keyboardHide", () => {
+ requestAnimationFrame(() => {
+ if (!shouldRevealCursor()) return;
+ if (isCursorRevealSuppressed()) return;
+ scrollCursorIntoView({ behavior: "instant" });
+ });
+ });
+ }
+
const shiftClickSelectionExtension = EditorView.domEventHandlers({
click(event) {
if (!touchSelectionController?.consumePendingShiftSelectionClick(event)) {
@@ -2272,7 +2306,6 @@ async function EditorManager($header, $body) {
createUntitledPaneFile(pane);
} else if (options.activate !== false) {
setActivePane(pane);
- pane.editor?.focus();
}
return pane;
@@ -2328,7 +2361,10 @@ async function EditorManager($header, $body) {
pane.touchSelectionController?.destroy?.();
pane.touchSelectionController = null;
+ pane.cleanupEditorListeners?.();
+ pane.cleanupEditorListeners = null;
pane.editor?.destroy?.();
+ pane.editor = null;
removePaneFromLayout(pane);
const storedPaneIndex = panes.indexOf(pane);
if (storedPaneIndex >= 0) panes.splice(storedPaneIndex, 1);
@@ -2340,7 +2376,7 @@ async function EditorManager($header, $body) {
if (fileToActivate) {
fileToActivate.makeActive();
} else {
- activatePane(targetPane);
+ activatePane(targetPane, { focusEditor: false });
}
syncOpenFileList();
return true;
@@ -2355,7 +2391,7 @@ async function EditorManager($header, $body) {
(index + offset + orderedPanes.length) % orderedPanes.length
];
if (!nextPane) return false;
- activatePane(nextPane);
+ activatePane(nextPane, { focusEditor: false });
return true;
}
@@ -2428,7 +2464,7 @@ async function EditorManager($header, $body) {
}
if (!bestPane) return false;
- activatePane(bestPane);
+ activatePane(bestPane, { focusEditor: false });
return true;
}
@@ -3103,6 +3139,7 @@ async function EditorManager($header, $body) {
$body.append($paneRoot);
initModes(); // Initialize CodeMirror modes
+ registerSoftKeyboardCursorReveal();
await setupEditor(primaryPane);
// Initialize theme from settings or fallback
@@ -3819,6 +3856,9 @@ async function EditorManager($header, $body) {
let scrollTimeout;
let scrollSyncRaf = 0;
const scroller = editor.scrollDOM;
+ let pendingKeyboardHideBlur = null;
+ pane.cleanupEditorListeners?.();
+ pane.cleanupEditorListeners = null;
function syncScrollUi() {
if (pane !== activePane) return;
@@ -3862,23 +3902,6 @@ async function EditorManager($header, $body) {
});
syncScrollUi();
- keyboardHandler.on("keyboardShowStart", () => {
- requestAnimationFrame(() => {
- if (isCursorRevealSuppressed()) return;
- scrollCursorIntoView({ behavior: "instant" });
- });
- });
- keyboardHandler.on("keyboardShow", () => {
- if (isCursorRevealSuppressed()) return;
- scrollCursorIntoView();
- });
- keyboardHandler.on("keyboardHide", () => {
- requestAnimationFrame(() => {
- if (isCursorRevealSuppressed()) return;
- scrollCursorIntoView({ behavior: "instant" });
- });
- });
-
// Attach native DOM event listeners directly to the editor's contentDOM
const contentDOM = editor.contentDOM;
const isFocused =
@@ -3886,7 +3909,7 @@ async function EditorManager($header, $body) {
contentDOM.contains(document.activeElement);
setNativeContextMenuDisabled(isFocused);
- contentDOM.addEventListener("focus", (_event) => {
+ function handleContentFocus(_event) {
setActivePane(pane);
setNativeContextMenuDisabled(true);
const activeFile = pane.activeFile;
@@ -3894,9 +3917,9 @@ async function EditorManager($header, $body) {
activeFile.focused = true;
}
touchSelectionController?.onStateChanged();
- });
+ }
- contentDOM.addEventListener("blur", async (_event) => {
+ async function handleContentBlur(_event) {
setNativeContextMenuDisabled(false);
touchSelectionController?.setMenu(false);
const { hardKeyboardHidden, keyboardHeight } =
@@ -3919,16 +3942,46 @@ async function EditorManager($header, $body) {
// soft keyboard - wait for keyboard to hide
const onKeyboardHide = () => {
keyboardHandler.off("keyboardHide", onKeyboardHide);
+ if (pendingKeyboardHideBlur === onKeyboardHide) {
+ pendingKeyboardHideBlur = null;
+ }
blur();
};
+ if (pendingKeyboardHideBlur) {
+ keyboardHandler.off("keyboardHide", pendingKeyboardHideBlur);
+ }
+ pendingKeyboardHideBlur = onKeyboardHide;
keyboardHandler.on("keyboardHide", onKeyboardHide);
- });
+ }
- contentDOM.addEventListener("keydown", (event) => {
+ function handleContentKeydown(event) {
if (event.key === "Escape") {
keydownState.esc = { value: true, target: contentDOM };
}
- });
+ }
+
+ contentDOM.addEventListener("focus", handleContentFocus);
+ contentDOM.addEventListener("blur", handleContentBlur);
+ contentDOM.addEventListener("keydown", handleContentKeydown);
+
+ pane.cleanupEditorListeners = () => {
+ scroller?.removeEventListener("scroll", handleEditorScroll);
+ scroller?.removeEventListener("pointerdown", clearScrollbarScrollLock);
+ scroller?.removeEventListener("touchstart", clearScrollbarScrollLock);
+ scroller?.removeEventListener("wheel", clearScrollbarScrollLock);
+ contentDOM.removeEventListener("focus", handleContentFocus);
+ contentDOM.removeEventListener("blur", handleContentBlur);
+ contentDOM.removeEventListener("keydown", handleContentKeydown);
+ clearTimeout(scrollTimeout);
+ if (scrollSyncRaf) {
+ cancelAnimationFrame(scrollSyncRaf);
+ scrollSyncRaf = 0;
+ }
+ if (pendingKeyboardHideBlur) {
+ keyboardHandler.off("keyboardHide", pendingKeyboardHideBlur);
+ pendingKeyboardHideBlur = null;
+ }
+ };
updateMargin(true);
updateSideButtonContainer();
@@ -4391,10 +4444,20 @@ async function EditorManager($header, $body) {
const pane = targetPane || getFilePane(id) || getActivePane();
if (!pane) return;
const paneActiveFile = pane.activeFile;
- if (paneActiveFile?.id === id && activePane === pane) return;
-
const file = manager.getFile(id);
if (!file) return;
+ if (paneActiveFile?.id === id && activePane === pane) {
+ if (manager.activeFile?.id !== id) {
+ manager.activeFile = file;
+ file.tab?.classList.add("active");
+ updateHeaderForFile(file);
+ if (isPaneTabLayout()) syncGlobalOpenFileListMirror();
+ manager.onupdate("switch-file");
+ events.emit("switch-file", file);
+ toggleProblemButton();
+ }
+ return;
+ }
setActivePane(pane, { emitSwitch: false });
diff --git a/src/lib/fileList.js b/src/lib/fileList.js
index b57b062fe..39f4807de 100644
--- a/src/lib/fileList.js
+++ b/src/lib/fileList.js
@@ -21,7 +21,7 @@ const events = {
};
export function initFileList() {
- if (editorManager?.activeFile.loading) {
+ if (editorManager?.activeFile?.loading) {
editorManager.activeFile.on("loadend", initFileList);
return;
}
diff --git a/src/lib/showFileInfo.js b/src/lib/showFileInfo.js
index 9b0650c3c..1520f7c3c 100644
--- a/src/lib/showFileInfo.js
+++ b/src/lib/showFileInfo.js
@@ -13,7 +13,9 @@ import settings from "./settings";
* @param {String} [url]
*/
export default async function showFileInfo(url) {
- if (!url) url = editorManager.activeFile.uri;
+ const activeFile = editorManager.activeFile;
+ if (!url) url = activeFile?.uri;
+ if (!url) return;
loader.showTitleLoader();
try {
const fs = fsOperation(url);
@@ -24,7 +26,7 @@ export default async function showFileInfo(url) {
lastModified = new Date(lastModified).toLocaleString();
const protocol = Url.getProtocol(url);
- const fileType = type.toLowerCase();
+ const fileType = String(type || "").toLowerCase();
const options = {
name: name.slice(0, name.length - Url.extname(name).length),
extension: Url.extname(name),
@@ -34,10 +36,11 @@ export default async function showFileInfo(url) {
lang: strings,
showUri: helpers.getVirtualPath(url),
isEditor:
- fileType === "text/plain" || editorManager.activeFile.type === "editor",
+ fileType === "text/plain" ||
+ (activeFile?.uri === url && activeFile.type === "editor"),
};
- if (editorManager.activeFile.type === "editor") {
+ if (options.isEditor) {
const value = await fs.readFile(settings.value.defaultFileEncoding);
options.lineCount = value.split(/\n+/).length;
options.wordCount = value.split(/\s+|\n+/).length;
diff --git a/src/main.js b/src/main.js
index 08c64a618..efe610526 100644
--- a/src/main.js
+++ b/src/main.js
@@ -725,12 +725,16 @@ async function loadApp() {
// if (!$editMenuToggler.isConnected) {
// $header.insertBefore($editMenuToggler, $header.lastChild);
// }
- if (activeFile?.type === "page" || activeFile?.type === "terminal") {
- $editMenuToggler.remove();
- } else {
+ if (
+ activeFile &&
+ activeFile.type !== "page" &&
+ activeFile.type !== "terminal"
+ ) {
if (!$editMenuToggler.isConnected) {
$header.insertBefore($editMenuToggler, $header.lastChild);
}
+ } else {
+ $editMenuToggler.remove();
}
if (mode === "switch-file") {
@@ -825,9 +829,9 @@ function createFileMenu({ top, bottom, toggler }) {
toggler,
transformOrigin: top ? "top right" : "bottom right",
innerHTML: () => {
- const file = window.editorManager.activeFile;
+ const file = window.editorManager?.activeFile;
- if (file.type === "page") {
+ if (!file || file.type === "page" || file.type === "terminal") {
return "";
}
From 32b41d35a2d6c88400f7eb1147ff9e0b19337146 Mon Sep 17 00:00:00 2001
From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com>
Date: Tue, 30 Jun 2026 22:18:19 +0530
Subject: [PATCH 16/20] remove double doc sync and add Ctrl-touch for multi
cursor too
---
src/cm/touchSelectionMenu.js | 112 ++++++++++++++++++++++++++++++++++-
src/lib/editorManager.js | 59 +++++++++---------
2 files changed, 139 insertions(+), 32 deletions(-)
diff --git a/src/cm/touchSelectionMenu.js b/src/cm/touchSelectionMenu.js
index 44a9e761c..390a81378 100644
--- a/src/cm/touchSelectionMenu.js
+++ b/src/cm/touchSelectionMenu.js
@@ -128,10 +128,12 @@ class TouchSelectionMenuController {
#container;
#getActiveFile;
#isShiftSelectionActive;
+ #isCtrlSelectionActive;
#stateSyncRaf = 0;
#isScrolling = false;
#isPointerInteracting = false;
#shiftSelectionSession = null;
+ #ctrlSelectionSession = null;
#pendingShiftSelectionClick = null;
#menuActive = false;
#menuRequested = false;
@@ -147,6 +149,8 @@ class TouchSelectionMenuController {
this.#getActiveFile = options.getActiveFile || (() => null);
this.#isShiftSelectionActive =
options.isShiftSelectionActive || (() => false);
+ this.#isCtrlSelectionActive =
+ options.isCtrlSelectionActive || (() => false);
this.$menu = document.createElement("menu");
this.$menu.className = "cursor-menu";
this.#bindEvents();
@@ -196,6 +200,7 @@ class TouchSelectionMenuController {
cancelAnimationFrame(this.#stateSyncRaf);
this.#stateSyncRaf = 0;
this.#shiftSelectionSession = null;
+ this.#ctrlSelectionSession = null;
this.#pendingShiftSelectionClick = null;
this.#tooltipObserver?.disconnect();
this.#hideMenu(true);
@@ -205,6 +210,7 @@ class TouchSelectionMenuController {
this.#enabled = !!enabled;
if (this.#enabled) return;
this.#shiftSelectionSession = null;
+ this.#ctrlSelectionSession = null;
this.#pendingShiftSelectionClick = null;
this.#menuRequested = false;
this.#isPointerInteracting = false;
@@ -274,6 +280,7 @@ class TouchSelectionMenuController {
onSessionChanged() {
if (!this.#enabled) return;
this.#shiftSelectionSession = null;
+ this.#ctrlSelectionSession = null;
this.#pendingShiftSelectionClick = null;
this.#menuRequested = false;
this.#isPointerInteracting = false;
@@ -296,15 +303,19 @@ class TouchSelectionMenuController {
if (this.$menu.contains(target)) return;
if (this.#isIgnoredPointerTarget(target)) {
this.#shiftSelectionSession = null;
+ this.#ctrlSelectionSession = null;
return;
}
if (target instanceof Node && this.#view.dom.contains(target)) {
- this.#captureShiftSelection(event);
+ if (!this.#captureCtrlSelection(event)) {
+ this.#captureShiftSelection(event);
+ }
this.#isPointerInteracting = true;
this.#clearMenuShowTimer();
return;
}
this.#shiftSelectionSession = null;
+ this.#ctrlSelectionSession = null;
this.#isPointerInteracting = false;
this.#menuRequested = false;
this.#hideMenu();
@@ -312,9 +323,12 @@ class TouchSelectionMenuController {
#onGlobalPointerUp = (event) => {
if (event.type === "pointerup") {
- this.#commitShiftSelection(event);
+ if (!this.#commitCtrlSelection(event)) {
+ this.#commitShiftSelection(event);
+ }
} else {
this.#shiftSelectionSession = null;
+ this.#ctrlSelectionSession = null;
}
if (!this.#isPointerInteracting) return;
this.#isPointerInteracting = false;
@@ -330,6 +344,7 @@ class TouchSelectionMenuController {
};
#captureShiftSelection(event) {
+ this.#ctrlSelectionSession = null;
if (!this.#canExtendSelection(event)) {
this.#shiftSelectionSession = null;
return;
@@ -377,6 +392,92 @@ class TouchSelectionMenuController {
event.preventDefault();
}
+ #captureCtrlSelection(event) {
+ this.#shiftSelectionSession = null;
+ if (!this.#canAddCursor(event)) {
+ this.#ctrlSelectionSession = null;
+ return false;
+ }
+
+ this.#ctrlSelectionSession = {
+ pointerId: event.pointerId,
+ x: event.clientX,
+ y: event.clientY,
+ };
+ event.preventDefault();
+ event.stopPropagation();
+ return true;
+ }
+
+ #commitCtrlSelection(event) {
+ const session = this.#ctrlSelectionSession;
+ this.#ctrlSelectionSession = null;
+ if (!session) return false;
+ if (!this.#canAddCursor(event)) return false;
+ if (event.pointerId !== session.pointerId) return false;
+ if (
+ Math.hypot(event.clientX - session.x, event.clientY - session.y) >
+ TAP_MAX_DISTANCE
+ ) {
+ return false;
+ }
+ const target = event.target;
+ if (!(target instanceof Node) || !this.#view.dom.contains(target)) {
+ return false;
+ }
+ if (this.#isIgnoredPointerTarget(target)) return false;
+
+ const pos = this.#view.posAtCoords(
+ { x: event.clientX, y: event.clientY },
+ false,
+ );
+ if (pos == null) return false;
+
+ const selection = this.#view.state.selection;
+ const ranges = selection.ranges;
+ const existingIndex = ranges.findIndex(
+ (range) => range.from <= pos && range.to >= pos,
+ );
+ let nextRanges;
+ let mainIndex;
+
+ if (existingIndex >= 0) {
+ if (ranges.length <= 1) {
+ this.#pendingShiftSelectionClick = {
+ x: event.clientX,
+ y: event.clientY,
+ timeStamp: event.timeStamp,
+ };
+ event.preventDefault();
+ event.stopPropagation();
+ return true;
+ }
+ nextRanges = ranges.filter((_, index) => index !== existingIndex);
+ mainIndex = Math.min(selection.mainIndex, nextRanges.length - 1);
+ } else {
+ const cursor = EditorSelection.cursor(pos);
+ const insertAt = ranges.findIndex(
+ (range) => range.from > pos || (range.from === pos && range.to >= pos),
+ );
+ mainIndex = insertAt === -1 ? ranges.length : insertAt;
+ nextRanges = ranges.slice();
+ nextRanges.splice(mainIndex, 0, cursor);
+ }
+
+ this.#view.dispatch({
+ selection: EditorSelection.create(nextRanges, mainIndex),
+ userEvent: "select.pointer",
+ });
+ this.#pendingShiftSelectionClick = {
+ x: event.clientX,
+ y: event.clientY,
+ timeStamp: event.timeStamp,
+ };
+ event.preventDefault();
+ event.stopPropagation();
+ return true;
+ }
+
#canExtendSelection(event) {
if (!this.#enabled) return false;
if (!(event.isTrusted && event.isPrimary)) return false;
@@ -384,6 +485,13 @@ class TouchSelectionMenuController {
return !!this.#isShiftSelectionActive(event);
}
+ #canAddCursor(event) {
+ if (!this.#enabled) return false;
+ if (!(event.isTrusted && event.isPrimary)) return false;
+ if (typeof event.button === "number" && event.button !== 0) return false;
+ return !!this.#isCtrlSelectionActive(event);
+ }
+
consumePendingShiftSelectionClick(event) {
const pending = this.#pendingShiftSelectionClick;
this.#pendingShiftSelectionClick = null;
diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js
index c683953a9..02873d178 100644
--- a/src/lib/editorManager.js
+++ b/src/lib/editorManager.js
@@ -1,7 +1,7 @@
import sidebarApps from "sidebarApps";
import { indentUnit, language as languageFacet } from "@codemirror/language";
import { search } from "@codemirror/search";
-import { Compartment, EditorState, Prec, StateEffect } from "@codemirror/state";
+import { Compartment, EditorState, Prec } from "@codemirror/state";
import { oneDark } from "@codemirror/theme-one-dark";
import {
closeHoverTooltips,
@@ -311,7 +311,10 @@ async function EditorManager($header, $body) {
});
}
- function createPaneShell(editorContainer = createEditorContainer()) {
+ function createPaneShell(
+ editorContainer = createEditorContainer(),
+ registerPane = true,
+ ) {
const pane = {
id: `pane-${++paneIdCounter}`,
files: [],
@@ -341,7 +344,7 @@ async function EditorManager($header, $body) {
},
true,
);
- panes.push(pane);
+ if (registerPane) panes.push(pane);
return pane;
}
@@ -728,6 +731,9 @@ async function EditorManager($header, $body) {
if (!appSettings.value.shiftClickSelection) return false;
return !!event?.shiftKey || quickTools?.$footer?.dataset?.shift != null;
};
+ const isCtrlSelectionActive = (event) => {
+ return !!event?.ctrlKey || quickTools?.$footer?.dataset?.ctrl != null;
+ };
function registerSoftKeyboardCursorReveal() {
const shouldRevealCursor = () => {
@@ -1574,6 +1580,7 @@ async function EditorManager($header, $body) {
container: $container,
getActiveFile: () => manager?.activeFile || null,
isShiftSelectionActive,
+ isCtrlSelectionActive,
});
primaryPane.touchSelectionController = touchSelectionController;
@@ -2265,18 +2272,8 @@ async function EditorManager($header, $body) {
container: pane.editorContainer,
getActiveFile: () => pane.activeFile || null,
isShiftSelectionActive,
+ isCtrlSelectionActive,
});
- try {
- paneEditor.dispatch({
- effects: StateEffect.appendConfig.of(getDocSyncListener()),
- });
- } catch (error) {
- warnRecoverable(
- "Failed to attach document sync listener to split editor.",
- error,
- `doc-sync-listener-${pane.id}`,
- );
- }
await setupEditor(pane);
return paneEditor;
}
@@ -2292,12 +2289,27 @@ async function EditorManager($header, $body) {
return null;
}
- const pane = createPaneShell();
+ const pane = createPaneShell(undefined, false);
+ try {
+ await createPaneEditor(pane);
+ } catch (error) {
+ pane.touchSelectionController?.destroy?.();
+ pane.touchSelectionController = null;
+ pane.editor?.destroy?.();
+ pane.editor = null;
+ warnRecoverable(
+ "Failed to create split editor pane.",
+ error,
+ `create-pane-editor-${pane.id}`,
+ );
+ window.toast?.(strings.error || "Error");
+ return null;
+ }
+ panes.push(pane);
insertPaneIntoLayout(sourcePane, pane, direction);
updatePaneLayoutState();
animatePaneEntry(pane);
- await createPaneEditor(pane);
- updatePaneLayoutState();
+ pane.editor?.requestMeasure?.();
syncOpenFileList();
if (options.moveFile) {
@@ -3468,19 +3480,6 @@ async function EditorManager($header, $body) {
}
});
- // Attach doc-sync listener to the current editor instance
- try {
- editor.dispatch({
- effects: StateEffect.appendConfig.of(getDocSyncListener()),
- });
- } catch (error) {
- warnRecoverable(
- "Failed to attach document sync listener to editor.",
- error,
- "doc-sync-listener",
- );
- }
-
return manager;
/**
From 7cb916fd05c9640b0c0417a6a569b89e133b3cfb Mon Sep 17 00:00:00 2001
From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com>
Date: Wed, 1 Jul 2026 09:04:32 +0530
Subject: [PATCH 17/20] correctly drop at carret
---
src/handlers/editorFileTab.js | 150 +++++++++++++++++++++++++++++++++-
1 file changed, 146 insertions(+), 4 deletions(-)
diff --git a/src/handlers/editorFileTab.js b/src/handlers/editorFileTab.js
index 548c77d37..159b96d03 100644
--- a/src/handlers/editorFileTab.js
+++ b/src/handlers/editorFileTab.js
@@ -223,7 +223,7 @@ function releaseDrag(e) {
updateFileList($parent);
}
} else if (isPathDropTarget) {
- insertDraggedFilePath($target);
+ insertDraggedFilePath($target, clientX, clientY);
}
const shouldSettleClone = shouldCommitReorder || didReorder;
@@ -438,7 +438,7 @@ function isFilePathDropTarget($target) {
);
}
-function insertDraggedFilePath($target) {
+function insertDraggedFilePath($target, clientX, clientY) {
const filePath = draggedFile?.uri || editorManager.activeFile?.uri;
if (!filePath) return;
@@ -448,12 +448,154 @@ function insertDraggedFilePath($target) {
editorManager.editor;
view.dispatch(view.state.replaceSelection(filePath));
} else if ($target.isContentEditable) {
- $target.textContent += filePath;
+ insertTextIntoContentEditable($target, filePath, clientX, clientY);
} else {
- $target.value += filePath;
+ insertTextIntoInput($target, filePath);
}
}
+function insertTextIntoContentEditable($target, text, clientX, clientY) {
+ const $editable = getContentEditableRoot($target);
+ const range = getContentEditableRange($editable, clientX, clientY);
+ const selection = document.getSelection();
+
+ if (!range || !selection) {
+ $editable.append(document.createTextNode(text));
+ $editable.dispatchEvent(createInputEvent(text));
+ return;
+ }
+
+ focusWithoutScrolling($editable);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ if (
+ typeof document.execCommand === "function" &&
+ document.queryCommandSupported?.("insertText")
+ ) {
+ const inserted = document.execCommand("insertText", false, text);
+ if (inserted) return;
+ }
+
+ range.deleteContents();
+ const textNode = document.createTextNode(text);
+ range.insertNode(textNode);
+ range.setStartAfter(textNode);
+ range.collapse(true);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ $editable.dispatchEvent(createInputEvent(text));
+}
+
+function getContentEditableRoot($target) {
+ let $editable = $target;
+ while ($editable.parentElement?.isContentEditable) {
+ $editable = $editable.parentElement;
+ }
+
+ return $editable;
+}
+
+function focusWithoutScrolling($target) {
+ try {
+ $target.focus({ preventScroll: true });
+ } catch {
+ $target.focus();
+ }
+}
+
+function getContentEditableRange($target, clientX, clientY) {
+ const range = getCaretRangeFromPoint(clientX, clientY);
+ if (range && isRangeInsideElement(range, $target)) return range;
+
+ const selection = document.getSelection();
+ if (selection?.rangeCount) {
+ const selectionRange = selection.getRangeAt(0);
+ if (isRangeInsideElement(selectionRange, $target)) {
+ return selectionRange.cloneRange();
+ }
+ }
+
+ const fallbackRange = document.createRange();
+ fallbackRange.selectNodeContents($target);
+ fallbackRange.collapse(false);
+ return fallbackRange;
+}
+
+function getCaretRangeFromPoint(clientX, clientY) {
+ const caretRange = document.caretRangeFromPoint?.(clientX, clientY);
+ if (caretRange) return caretRange;
+
+ const caretPosition = document.caretPositionFromPoint?.(clientX, clientY);
+ if (!caretPosition) return null;
+
+ const range = document.createRange();
+ range.setStart(caretPosition.offsetNode, caretPosition.offset);
+ range.collapse(true);
+ return range;
+}
+
+function isRangeInsideElement(range, $element) {
+ const { commonAncestorContainer } = range;
+ return (
+ commonAncestorContainer === $element ||
+ $element.contains(commonAncestorContainer)
+ );
+}
+
+function insertTextIntoInput($target, text) {
+ const value = $target.value || "";
+ const start = getInputSelectionOffset(
+ $target,
+ "selectionStart",
+ value.length,
+ );
+ const end = getInputSelectionOffset($target, "selectionEnd", start);
+
+ if (typeof $target.setRangeText === "function") {
+ try {
+ $target.setRangeText(text, start, end, "end");
+ } catch {
+ $target.value = `${value}${text}`;
+ }
+ } else {
+ const nextValue = `${value.slice(0, start)}${text}${value.slice(end)}`;
+ $target.value = nextValue;
+ setInputSelectionOffset($target, start + text.length);
+ }
+
+ $target.dispatchEvent(createInputEvent(text));
+}
+
+function getInputSelectionOffset($target, key, fallback) {
+ try {
+ return typeof $target[key] === "number" ? $target[key] : fallback;
+ } catch {
+ return fallback;
+ }
+}
+
+function setInputSelectionOffset($target, offset) {
+ try {
+ $target.selectionStart = $target.selectionEnd = offset;
+ } catch {
+ return;
+ }
+}
+
+function createInputEvent(text) {
+ if (typeof InputEvent === "function") {
+ return new InputEvent("input", {
+ bubbles: true,
+ cancelable: false,
+ data: text,
+ inputType: "insertText",
+ });
+ }
+
+ return new Event("input", { bubbles: true, cancelable: false });
+}
+
function isValidDropTabList($tabList) {
if (!$tabList) return false;
if ($tabList === $originParent) return true;
From a55de81a9a327e8b0973fae9b2cc7c5c686679d0 Mon Sep 17 00:00:00 2001
From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com>
Date: Wed, 1 Jul 2026 09:32:24 +0530
Subject: [PATCH 18/20] fix and use doc cache instead of direct doc.toString
---
src/cm/editorUtils.ts | 18 ++++
src/cm/lsp/references.ts | 3 +-
src/lib/editorFile.js | 27 ++++--
src/lib/editorManager.js | 124 +++++++++++++++++++++++--
src/lib/prettierFormatter.js | 3 +-
src/lib/run.js | 13 +--
src/lib/saveFile.js | 3 +-
src/pages/markdownPreview/index.js | 3 +-
src/sidebarApps/searchInFiles/index.js | 5 +-
9 files changed, 172 insertions(+), 27 deletions(-)
diff --git a/src/cm/editorUtils.ts b/src/cm/editorUtils.ts
index ff750f061..a8f2569e6 100644
--- a/src/cm/editorUtils.ts
+++ b/src/cm/editorUtils.ts
@@ -22,6 +22,24 @@ export interface ScrollPosition {
scrollLeft: number;
}
+const docTextCache = new WeakMap