diff --git a/CHANGELOG.md b/CHANGELOG.md index 167aa95..17ea971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.3.0] — 2026-07-08 + +### Added + +- **Split view (two editor panes)** — a faithful Notepad++ two-view layout. + `View → Split Horizontal` stacks a second editor pane below; `Split Vertical` + places it side-by-side. Each pane has its own tab strip, and every open + document belongs to exactly one pane. Right-click a tab → **Move to Other + View** to relocate it. The pane you last clicked is the focused pane that + Find/Replace, the status bar, macros, the Lua Console, and menu commands act + on. Choosing Split again collapses back to a single pane, as does closing the + last tab in the secondary pane. The split layout — which files are in which + pane, the orientation, and each pane's active tab — is restored on reload. + +### Fixed + +- **New tabs focus the editor.** Opening a tab via the `+` button or + `File → New` now places the caret in the text area so you can type + immediately, without clicking first. +- **Switching tabs focuses the editor.** Activating an existing tab (including + from the `>>` overflow dropdown) focuses its editor pane. +- **Reload focuses the editor.** After a page reload, the active document's + editor is focused so you can keep typing right away. + ## [0.2.0] — 2026-07-03 ### Changed diff --git a/README.md b/README.md index 6cf07d1..ea31bd8 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ powered by **CodeMirror 6** and **Wasmoon** (Lua 5.4 in WASM). ![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg) [![Chrome Web Store](https://img.shields.io/chrome-web-store/v/jfhgpoliojbbmiknmdbefeamimlekgdn?label=Chrome%20Web%20Store&logo=googlechrome&logoColor=white&color=success)](https://chromewebstore.google.com/detail/notepad-web/jfhgpoliojbbmiknmdbefeamimlekgdn) [![users](https://img.shields.io/chrome-web-store/users/jfhgpoliojbbmiknmdbefeamimlekgdn?label=users)](https://chromewebstore.google.com/detail/notepad-web/jfhgpoliojbbmiknmdbefeamimlekgdn) -![version](https://img.shields.io/badge/version-0.2.0-informational) +![version](https://img.shields.io/badge/version-0.3.0-informational)

⬇  Install from the Chrome Web Store @@ -136,7 +136,7 @@ npm run package # manifest-compliance + no-remote-code checks, then zips dist The `npm run package` step verifies that no CDN/remote URLs survived the build and that the manifest declares only allowed permissions before producing -`notepad-web-v0.2.0.zip`. +`notepad-web-v0.3.0.zip`. ## Architecture diff --git a/manifest.json b/manifest.json index 9dfe625..7bf4627 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Notepad Web", - "version": "0.2.0", + "version": "0.3.0", "description": "A CodeMirror-based code editor that runs fully offline in your browser.", "permissions": ["storage", "contextMenus", "activeTab", "scripting"], "background": { "service_worker": "background.js", "type": "module" }, diff --git a/package-lock.json b/package-lock.json index 265747b..09351a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "notepad-web", - "version": "0.1.0", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "notepad-web", - "version": "0.1.0", + "version": "0.3.0", "license": "GPL-3.0-or-later", "dependencies": { "@codemirror/autocomplete": "^6.20.3", diff --git a/package.json b/package.json index 5dc7289..1a98bf3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "notepad-web", - "version": "0.2.0", + "version": "0.3.0", "description": "A Monaco-based code editor as a Chrome MV3 extension.", "license": "GPL-3.0-or-later", "type": "module", diff --git a/src/app/app.ts b/src/app/app.ts index 50b981c..e59131f 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -211,6 +211,7 @@ export class App { store.setActiveForView(viewId, id); this.controllerFor(viewId)?.showDoc(id); this.deps.dockManager?.focusEditorGroup(viewId); + this.viewFor(viewId)?.focus(); }, (id) => { const doc = store.get(id); @@ -230,6 +231,7 @@ export class App { this.applyFocus(viewId); const d = store.create(); // create() adds to the focused view (== viewId) this.controllerFor(viewId)?.showDoc(d.id); + this.viewFor(viewId)?.focus(); }, { onSave: () => void fileActionsRef.current?.saveActive(), @@ -297,7 +299,7 @@ export class App { const srcController = this.controllerFor(source); const tgtController = this.controllerFor(target); if (!tgtController) return; - store.moveToView(id, target); // focus=target, active[target]=id, source active re-pointed + store.moveToView(id, target); srcController?.closeDoc(id); const srcActive = store.activeForView(source); if (srcActive) { @@ -655,6 +657,7 @@ export class App { const doNew = (): void => { const d = this.deps.store.create(); this.controller.showDoc(d.id); + this.view.focus(); }; const doClose = (): void => { @@ -1550,4 +1553,13 @@ export class App { if (a) controller.showDoc(a.id); view.requestMeasure(); } + + /** + * Focus the active editor pane. Called by the bootstrap once the dock is + * initialised (the #editor element has been moved into the dock group by then), + * so after a page reload the user can keep typing without clicking first. + */ + focusActiveEditor(): void { + this.view.focus(); + } } diff --git a/src/editor-page.ts b/src/editor-page.ts index 3ef1957..253291a 100644 --- a/src/editor-page.ts +++ b/src/editor-page.ts @@ -690,6 +690,8 @@ window.__appReady = (async () => { // Settle layout with a rAF. await new Promise((resolve) => requestAnimationFrame(() => resolve())); + + app.focusActiveEditor(); })(); // ── PWA service worker (installability + offline) ──────────────────────────── diff --git a/tests/e2e/new-tab-focus.spec.ts b/tests/e2e/new-tab-focus.spec.ts new file mode 100644 index 0000000..32e3f61 --- /dev/null +++ b/tests/e2e/new-tab-focus.spec.ts @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +/** + * E2E: opening a new tab focuses the editor so the user can type immediately, + * without first clicking into the text area. + */ +import { test, expect } from '@playwright/test'; + +test.describe('New tab focus', () => { + const inEditor = (page: Parameters[1]>[0]['page']) => + page.evaluate(() => !!document.activeElement?.closest('.cm-editor')); + + test.beforeEach(async ({ page }) => { + page.on('dialog', (d) => void d.accept()); + await page.goto('/editor.html'); + await page.waitForFunction( + () => (window as unknown as { __appReady?: unknown }).__appReady !== undefined, + ); + await page.evaluate(() => (window as unknown as { __appReady: Promise }).__appReady); + }); + + test('the + button focuses the editor and accepts typing right away', async ({ page }) => { + await page.locator('#tab-new').click(); + await expect.poll(() => inEditor(page)).toBe(true); + // Type without clicking into the editor first. + await page.keyboard.type('HELLO'); + expect( + await page.evaluate(() => + (window as unknown as { __editor: { getValue(): string } }).__editor.getValue(), + ), + ).toBe('HELLO'); + }); + + test('File → New focuses the editor', async ({ page }) => { + await page.getByRole('menuitem', { name: 'File' }).click(); + await page.getByRole('menuitem', { name: 'New' }).first().click(); + await expect.poll(() => inEditor(page)).toBe(true); + }); + + test('switching to an existing tab focuses the editor', async ({ page }) => { + // Open a second tab, then click back to the first tab. + await page.locator('#tab-new').click(); + const firstTab = page.locator('#tabbar .tab').first(); + await firstTab.click(); + await expect.poll(() => inEditor(page)).toBe(true); + await page.keyboard.type('X'); + expect( + await page.evaluate(() => + (window as unknown as { __editor: { getValue(): string } }).__editor.getValue(), + ), + ).toBe('X'); + }); + + test('the editor is focused after a page reload', async ({ page }) => { + await page.reload(); + await page.waitForFunction( + () => (window as unknown as { __appReady?: unknown }).__appReady !== undefined, + ); + await page.evaluate(() => (window as unknown as { __appReady: Promise }).__appReady); + await expect.poll(() => inEditor(page)).toBe(true); + // Can keep typing without clicking first. + await page.keyboard.type('Y'); + expect( + await page.evaluate(() => + (window as unknown as { __editor: { getValue(): string } }).__editor.getValue(), + ), + ).toContain('Y'); + }); +});