Skip to content

Commit cfe57b7

Browse files
ndemiancclaude
andcommitted
Atom++ M3: package generator + live hot-reload dev loader
New packages.js in atom-hackability: - "Atom++: Generate Package…" scaffolds a runnable package (package.json, extension.js with a sample command + status item, README, .vscode/launch.json) into ~/.atom-plus-plus/packages/<name>/ and offers to start hot-reload dev. - "Atom++: Develop Package (Hot Reload)…" loads a package into the RUNNING window and re-runs activate() on every save — Atom-style, no restart. Loads via a fresh Module with require('vscode') wired to the real API; a recursive watcher (150ms debounce) tears down (deactivate + dispose subscriptions) then reactivates. Errors are caught to an output channel so a broken edit never crashes the session. Status-bar indicator. - "Atom++: Stop Developing Package". Runtime code hot-reloads; manifest contributes still use the generated F5 launch config. Docs: CLAUDE.md/PLAN.md note the package generator + hot reload. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d118733 commit cfe57b7

4 files changed

Lines changed: 260 additions & 6 deletions

File tree

‎CLAUDE.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ branding/icons/ app icon source PNG, generated .icns, 1024 png
3636
extensions/atom-npp-pack/ Notepad++ power-editing pack (plain JS, no build step)
3737
extensions/atom-ai/ native Claude AI (chat, inline completion, providers, edit-with-diff, LM provider)
3838
extensions/atom-themes/ signature One Dark / One Light themes (JSON, default via configurationDefaults)
39-
extensions/atom-hackability/ user init script (~/.atom-plus-plus/init.js, live-reloadable) + Atom/NPP keymap presets
39+
extensions/atom-hackability/ user init script + Atom/NPP keymap presets + package generator & hot-reload dev loader
4040
patches/atom-core.patch our core source edits, applied on bootstrap
4141
scripts/ bootstrap.sh, apply-branding.mjs, run-dev.sh, build-macos.sh, make-dmg.sh, make-icon.sh
4242
vscode/ GITIGNORED upstream Code-OSS checkout (generated)
@@ -119,10 +119,10 @@ big-file mode badge. Files: extension.js + fileOps/lineOps/columnOps/encodingEol
119119
multi-session core effort. We use the diff-tab review instead.
120120
- Built since M2 shipped: inline tab-completion (`inlineComplete.js`), multi-file + auto-retrieval chat context
121121
(`gatherAutoContext`), chat moved to the right side bar, typewriter streaming, `make-dmg.sh` packaging.
122-
- M3 so far: One Dark/Light themes (`atom-themes`), user init script + Atom/Notepad++ keymap presets
123-
(`atom-hackability`). The Atom/NPP keymap also clears the old M1 leftover.
124-
- Not yet built: agentic multi-file tasks (M4); remaining M3 hackability — settings UI, package generator +
125-
hot reload, theme studio, VS Code settings import.
122+
- M3 so far: One Dark/Light themes (`atom-themes`); user init script + Atom/Notepad++ keymap presets +
123+
package generator with live hot-reload (`atom-hackability`). The Atom/NPP keymap also clears the old M1 leftover.
124+
- Not yet built: agentic multi-file tasks (M4); remaining M3 hackability — settings UI, theme studio,
125+
VS Code settings import.
126126

127127
## Conventions
128128

‎extensions/atom-hackability/extension.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const fs = require('fs');
1717
const path = require('path');
1818
const { createRequire } = require('module');
1919
const { registerKeymaps } = require('./keymaps');
20+
const { registerPackages } = require('./packages');
2021

2122
const INIT_DIR = path.join(os.homedir(), '.atom-plus-plus');
2223
const INIT_PATH = path.join(INIT_DIR, 'init.js');
@@ -146,6 +147,7 @@ function activate(context) {
146147
{ dispose: disposeUser }
147148
);
148149
registerKeymaps(context);
150+
registerPackages(context);
149151
runInit(false);
150152
}
151153

‎extensions/atom-hackability/package.json‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222
"commands": [
2323
{ "command": "atompp.init.edit", "title": "Open Init Script", "category": "Atom++" },
2424
{ "command": "atompp.init.reload", "title": "Reload Init Script", "category": "Atom++" },
25-
{ "command": "atompp.keymap.apply", "title": "Apply Keymap Preset…", "category": "Atom++" }
25+
{ "command": "atompp.keymap.apply", "title": "Apply Keymap Preset…", "category": "Atom++" },
26+
{ "command": "atompp.package.generate", "title": "Generate Package…", "category": "Atom++" },
27+
{ "command": "atompp.package.develop", "title": "Develop Package (Hot Reload)…", "category": "Atom++" },
28+
{ "command": "atompp.package.stop", "title": "Stop Developing Package", "category": "Atom++" }
2629
],
2730
"configuration": {
2831
"title": "Atom++ Hackability",
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Atom++ — Hackability: package generator + hot reload
3+
*
4+
* "Generate Package" scaffolds a runnable Atom++ package (plain JS, no build step).
5+
* "Develop Package (Hot Reload)" loads a package's extension.js into the RUNNING window and
6+
* re-runs activate() on every save — Atom-style live development, no window restart. (Code
7+
* runs live; manifest-only `contributes` still need the generated F5 launch config.)
8+
*--------------------------------------------------------------------------------------------*/
9+
// @ts-check
10+
'use strict';
11+
12+
const vscode = require('vscode');
13+
const os = require('os');
14+
const fs = require('fs');
15+
const path = require('path');
16+
const Module = require('module');
17+
18+
const PACKAGES_DIR = path.join(os.homedir(), '.atom-plus-plus', 'packages');
19+
20+
/** @type {vscode.OutputChannel} */
21+
let output;
22+
/** Active dev session, or null. */
23+
let dev = null; // { dir, name, exports, ctx, watcher, status, timer }
24+
25+
// ---- generator -------------------------------------------------------------
26+
27+
function pkgJson(name, title) {
28+
return JSON.stringify({
29+
name,
30+
displayName: title,
31+
description: 'An Atom++ package.',
32+
version: '0.0.1',
33+
publisher: 'you',
34+
engines: { vscode: '^1.126.0' },
35+
categories: ['Other'],
36+
activationEvents: ['onStartupFinished'],
37+
main: './extension.js',
38+
contributes: { commands: [{ command: name + '.hello', title: title + ': Hello' }] }
39+
}, null, 2) + '\n';
40+
}
41+
42+
function extJs(name) {
43+
return `// ${name} — an Atom++ package.
44+
//
45+
// Develop it live: run "Atom++: Develop Package (Hot Reload)" and pick this folder.
46+
// Edit + save and activate() re-runs instantly — no window restart. (For manifest
47+
// contributions like keybindings, press F5 for a full Extension Development Host.)
48+
49+
const vscode = require('vscode');
50+
51+
function activate(context) {
52+
\tconst hello = vscode.commands.registerCommand('${name}.hello', () => {
53+
\t\tvscode.window.showInformationMessage('Hello from ${name}! 👋');
54+
\t});
55+
\tcontext.subscriptions.push(hello);
56+
57+
\t// A status bar item — try changing this text and saving while hot-reload is running:
58+
\tconst item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 0);
59+
\titem.text = '$(rocket) ${name}';
60+
\titem.tooltip = 'From the ${name} package';
61+
\titem.show();
62+
\tcontext.subscriptions.push(item);
63+
}
64+
65+
function deactivate() {}
66+
67+
module.exports = { activate, deactivate };
68+
`;
69+
}
70+
71+
function launchJson() {
72+
return JSON.stringify({
73+
version: '0.2.0',
74+
configurations: [{
75+
name: 'Run Package',
76+
type: 'extensionHost',
77+
request: 'launch',
78+
args: ['--extensionDevelopmentPath=${workspaceFolder}']
79+
}]
80+
}, null, 2) + '\n';
81+
}
82+
83+
async function generatePackage() {
84+
const name = await vscode.window.showInputBox({
85+
title: 'Atom++ — Generate Package',
86+
prompt: 'Package name (lowercase, hyphens — e.g. my-cool-package)',
87+
validateInput: (v) => /^[a-z][a-z0-9-]*$/.test(v || '') ? null : 'Use lowercase letters, digits and hyphens; start with a letter.'
88+
});
89+
if (!name) { return; }
90+
const dir = path.join(PACKAGES_DIR, name);
91+
if (fs.existsSync(dir)) {
92+
vscode.window.showErrorMessage('Atom++: a package named “' + name + '” already exists at ' + dir);
93+
return;
94+
}
95+
const title = name.replace(/(^|-)([a-z])/g, (_, s, c) => (s ? ' ' : '') + c.toUpperCase());
96+
try {
97+
fs.mkdirSync(path.join(dir, '.vscode'), { recursive: true });
98+
fs.writeFileSync(path.join(dir, 'package.json'), pkgJson(name, title), 'utf8');
99+
fs.writeFileSync(path.join(dir, 'extension.js'), extJs(name), 'utf8');
100+
fs.writeFileSync(path.join(dir, 'README.md'), '# ' + title + '\n\nAn Atom++ package.\n', 'utf8');
101+
fs.writeFileSync(path.join(dir, '.vscode', 'launch.json'), launchJson(), 'utf8');
102+
} catch (e) {
103+
vscode.window.showErrorMessage('Atom++: could not scaffold package: ' + ((e && e.message) || e));
104+
return;
105+
}
106+
const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(path.join(dir, 'extension.js')));
107+
await vscode.window.showTextDocument(doc);
108+
const go = 'Develop (Hot Reload)';
109+
const c = await vscode.window.showInformationMessage('Atom++: created package “' + name + '”.', go);
110+
if (c === go) { startDev(dir); }
111+
}
112+
113+
// ---- hot-reload dev loader -------------------------------------------------
114+
115+
/** Load a package's main module fresh, with require('vscode') wired to the real API. */
116+
function loadPackageExports(dir) {
117+
let main = path.join(dir, 'extension.js');
118+
try {
119+
const pj = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
120+
if (pj.main) { main = path.join(dir, pj.main); }
121+
} catch { /* default to extension.js */ }
122+
123+
// Bust any cached modules under this package so sub-requires reload too.
124+
for (const k of Object.keys(require.cache)) { if (k.startsWith(dir + path.sep)) { delete require.cache[k]; } }
125+
126+
const m = new Module(main, null);
127+
m.filename = main;
128+
m.paths = Module._nodeModulePaths(path.dirname(main));
129+
const baseRequire = m.require.bind(m);
130+
// @ts-ignore — intercept the bare 'vscode' specifier
131+
m.require = (id) => (id === 'vscode' ? vscode : baseRequire(id));
132+
m._compile(fs.readFileSync(main, 'utf8'), main);
133+
return m.exports;
134+
}
135+
136+
function makeContext(dir) {
137+
const noState = { get: () => undefined, update: async () => {}, keys: () => [] };
138+
return {
139+
subscriptions: [],
140+
extensionPath: dir,
141+
extensionUri: vscode.Uri.file(dir),
142+
globalState: noState,
143+
workspaceState: noState,
144+
extensionMode: 2 // Development
145+
};
146+
}
147+
148+
function teardownDev(keepStatus) {
149+
if (!dev) { return; }
150+
try { if (dev.exports && typeof dev.exports.deactivate === 'function') { dev.exports.deactivate(); } } catch (e) { output.appendLine('[dev] deactivate error: ' + e); }
151+
for (const d of dev.ctx.subscriptions) { try { if (d && d.dispose) { d.dispose(); } } catch { /* ignore */ } }
152+
dev.ctx.subscriptions = [];
153+
if (!keepStatus) {
154+
if (dev.timer) { clearTimeout(dev.timer); }
155+
try { dev.watcher.close(); } catch { /* ignore */ }
156+
dev.status.dispose();
157+
}
158+
}
159+
160+
function activatePackage() {
161+
const ctx = makeContext(dev.dir);
162+
dev.ctx = ctx;
163+
const exports = loadPackageExports(dev.dir);
164+
dev.exports = exports;
165+
if (typeof exports.activate === 'function') { exports.activate(ctx); }
166+
output.appendLine('[dev] activated ' + dev.name + ' (' + ctx.subscriptions.length + ' disposable(s))');
167+
}
168+
169+
function reloadDev() {
170+
if (!dev) { return; }
171+
teardownDev(true);
172+
try {
173+
activatePackage();
174+
dev.status.text = '$(sync) dev: ' + dev.name;
175+
vscode.window.setStatusBarMessage('Atom++: reloaded ' + dev.name, 1500);
176+
setTimeout(() => { if (dev) { dev.status.text = '$(circle-filled) dev: ' + dev.name; } }, 600);
177+
} catch (e) {
178+
const msg = (e && e.stack) ? e.stack : String(e);
179+
output.appendLine('[dev] ERROR activating ' + dev.name + ':\n' + msg);
180+
vscode.window.showErrorMessage('Atom++: ' + dev.name + ' failed to load: ' + ((e && e.message) || e), 'Show Log')
181+
.then((c) => { if (c) { output.show(true); } });
182+
if (dev) { dev.status.text = '$(error) dev: ' + dev.name; }
183+
}
184+
}
185+
186+
function startDev(dir) {
187+
if (dev) { stopDev(); }
188+
const name = path.basename(dir);
189+
const status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, -10);
190+
status.command = 'atompp.package.stop';
191+
status.text = '$(circle-filled) dev: ' + name;
192+
status.tooltip = 'Atom++ is hot-reloading “' + name + '”. Click to stop.';
193+
status.show();
194+
195+
const watcher = fs.watch(dir, { recursive: true }, (_event, file) => {
196+
if (file && /node_modules|\.git|\.vscode/.test(String(file))) { return; }
197+
if (file && !/\.js$/.test(String(file))) { return; }
198+
if (dev && dev.timer) { clearTimeout(dev.timer); }
199+
if (dev) { dev.timer = setTimeout(reloadDev, 150); } // debounce burst saves
200+
});
201+
202+
dev = { dir, name, exports: null, ctx: { subscriptions: [] }, watcher, status, timer: null };
203+
reloadDev();
204+
vscode.window.showInformationMessage('Atom++: developing “' + name + '” with hot reload. Save to re-run.');
205+
}
206+
207+
function stopDev() {
208+
if (!dev) { vscode.window.showInformationMessage('Atom++: no package is being developed.'); return; }
209+
const name = dev.name;
210+
teardownDev(false);
211+
dev = null;
212+
vscode.window.setStatusBarMessage('Atom++: stopped developing ' + name, 2000);
213+
}
214+
215+
async function developPackage() {
216+
let dirs = [];
217+
try { dirs = fs.existsSync(PACKAGES_DIR) ? fs.readdirSync(PACKAGES_DIR).map((n) => path.join(PACKAGES_DIR, n)).filter((p) => { try { return fs.statSync(p).isDirectory(); } catch { return false; } }) : []; } catch { dirs = []; }
218+
219+
/** @type {any[]} */
220+
const items = dirs.map((d) => ({ label: '$(package) ' + path.basename(d), description: d, _dir: d }));
221+
items.push({ label: '$(folder-opened) Choose a folder…', _dir: null });
222+
const pick = await vscode.window.showQuickPick(items, { placeHolder: 'Develop which package? (hot-reloads into this window)' });
223+
if (!pick) { return; }
224+
225+
let dir = pick._dir;
226+
if (!dir) {
227+
const picked = await vscode.window.showOpenDialog({ canSelectFolders: true, canSelectFiles: false, canSelectMany: false, openLabel: 'Develop this package', title: 'Pick an Atom++ package folder' });
228+
if (!picked || !picked.length) { return; }
229+
dir = picked[0].fsPath;
230+
}
231+
if (!fs.existsSync(path.join(dir, 'extension.js')) && !fs.existsSync(path.join(dir, 'package.json'))) {
232+
vscode.window.showErrorMessage('Atom++: that folder has no extension.js / package.json.');
233+
return;
234+
}
235+
startDev(dir);
236+
}
237+
238+
function registerPackages(context) {
239+
output = vscode.window.createOutputChannel('Atom++ Packages');
240+
context.subscriptions.push(
241+
output,
242+
vscode.commands.registerCommand('atompp.package.generate', generatePackage),
243+
vscode.commands.registerCommand('atompp.package.develop', developPackage),
244+
vscode.commands.registerCommand('atompp.package.stop', stopDev),
245+
{ dispose: () => { if (dev) { teardownDev(false); dev = null; } } }
246+
);
247+
}
248+
249+
module.exports = { registerPackages };

0 commit comments

Comments
 (0)