zedx 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +77 -20
- package/dist/install.d.ts +1 -0
- package/dist/install.js +188 -0
- package/dist/snippet.d.ts +1 -0
- package/dist/snippet.js +168 -0
- package/dist/sync.js +3 -3
- package/dist/templates/base/extension.toml.ejs +4 -2
- package/dist/templates/language/config.toml.ejs +3 -1
- package/dist/templates/language/highlights.scm +1 -0
- package/dist/templates/lsp/Cargo.toml.ejs +12 -0
- package/dist/templates/lsp/lib.rs.ejs +30 -0
- package/dist/templates/theme/theme.json.ejs +1 -0
- package/package.json +59 -58
package/dist/index.js
CHANGED
|
@@ -8,7 +8,9 @@ import { addTheme, addLanguage } from './add.js';
|
|
|
8
8
|
import { runCheck } from './check.js';
|
|
9
9
|
import { syncInstall, syncUninstall } from './daemon.js';
|
|
10
10
|
import { generateExtension } from './generator.js';
|
|
11
|
+
import { installDevExtension } from './install.js';
|
|
11
12
|
import { promptUser, promptThemeDetails, promptLanguageDetails } from './prompts.js';
|
|
13
|
+
import { addLsp } from './snippet.js';
|
|
12
14
|
import { syncInit, runSync, syncStatus } from './sync.js';
|
|
13
15
|
function bumpVersion(version, type) {
|
|
14
16
|
const [major, minor, patch] = version.split('.').map(Number);
|
|
@@ -43,9 +45,67 @@ async function bumpExtensionVersion(type) {
|
|
|
43
45
|
await fs.writeFile(tomlPath, newContent);
|
|
44
46
|
p.log.success(color.green(`Bumped version from ${currentVersion} to ${newVersion}`));
|
|
45
47
|
}
|
|
48
|
+
function printWelcome() {
|
|
49
|
+
const ascii = String.raw `
|
|
50
|
+
░ ░░ ░░ ░░░ ░░░░ ░
|
|
51
|
+
▒▒▒▒▒▒ ▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒ ▒▒▒ ▒▒ ▒▒
|
|
52
|
+
▓▓▓▓ ▓▓▓▓▓ ▓▓▓▓ ▓▓▓▓ ▓▓▓▓ ▓▓▓
|
|
53
|
+
██ ███████ ████████ ████ ███ ██ ██
|
|
54
|
+
█ ██ ██ ███ ████ █
|
|
55
|
+
|
|
56
|
+
`.trim();
|
|
57
|
+
console.log('\n' + color.cyan(color.bold(ascii)) + '\n');
|
|
58
|
+
console.log(color.bold(' The CLI toolkit for Zed Editor') + '\n');
|
|
59
|
+
const commands = [
|
|
60
|
+
['zedx create', 'Scaffold a new Zed extension'],
|
|
61
|
+
['zedx add theme <name>', 'Add a theme to an existing extension'],
|
|
62
|
+
['zedx add language <id>', 'Add a language to an existing extension'],
|
|
63
|
+
['zedx snippet add lsp', 'Wire up a language server into the extension'],
|
|
64
|
+
['zedx check', 'Validate your extension config'],
|
|
65
|
+
['zedx install', 'Install as a Zed dev extension'],
|
|
66
|
+
['zedx version <major|minor|patch>', 'Bump extension version'],
|
|
67
|
+
['zedx sync', 'Sync Zed settings via a git repo'],
|
|
68
|
+
['zedx sync init', 'Link a git repo as the sync target'],
|
|
69
|
+
['zedx sync status', 'Show sync state between local and remote'],
|
|
70
|
+
['zedx sync install', 'Install the OS daemon for auto-sync'],
|
|
71
|
+
['zedx sync uninstall', 'Remove the auto-sync daemon'],
|
|
72
|
+
];
|
|
73
|
+
console.log(color.dim(' Commands:\n'));
|
|
74
|
+
for (const [cmd, desc] of commands) {
|
|
75
|
+
console.log(` ${color.cyan(cmd.padEnd(38))}${color.dim(desc)}`);
|
|
76
|
+
}
|
|
77
|
+
console.log(`\n ${color.dim('Docs:')} ${color.underline(color.blue('https://zed.dev/docs/extensions'))}\n`);
|
|
78
|
+
}
|
|
79
|
+
async function runCreate() {
|
|
80
|
+
const options = await promptUser();
|
|
81
|
+
if (options.types.includes('theme')) {
|
|
82
|
+
const themeDetails = await promptThemeDetails();
|
|
83
|
+
Object.assign(options, themeDetails);
|
|
84
|
+
}
|
|
85
|
+
if (options.types.includes('language')) {
|
|
86
|
+
const languageDetails = await promptLanguageDetails();
|
|
87
|
+
Object.assign(options, languageDetails);
|
|
88
|
+
}
|
|
89
|
+
const targetDir = path.join(getCallerDir(), options.id);
|
|
90
|
+
await generateExtension(options, targetDir);
|
|
91
|
+
p.outro(`${color.green('✓')} ${color.bold('Extension created successfully!')}\n` +
|
|
92
|
+
`${color.gray('─'.repeat(40))}\n` +
|
|
93
|
+
`${color.dim('Location:')} ${color.cyan(targetDir)}`);
|
|
94
|
+
p.outro(`${color.yellow('⚡')} ${color.bold('Next steps')}\n\n` +
|
|
95
|
+
` ${color.gray('1.')} Open Zed\n` +
|
|
96
|
+
` ${color.gray('2.')} ${color.white('Extensions > Install Dev Extension')}\n` +
|
|
97
|
+
` ${color.gray('3.')} Select ${color.cyan(options.id)} folder\n\n` +
|
|
98
|
+
`${color.dim('Learn more:')} ${color.underline(color.blue('https://zed.dev/docs/extensions/developing-extensions'))}`);
|
|
99
|
+
}
|
|
46
100
|
async function main() {
|
|
47
101
|
const program = new Command();
|
|
48
|
-
program.name('zedx').description('
|
|
102
|
+
program.name('zedx').description('The CLI toolkit for Zed Editor.').helpOption(false);
|
|
103
|
+
program
|
|
104
|
+
.command('create')
|
|
105
|
+
.description('Scaffold a new Zed extension')
|
|
106
|
+
.action(async () => {
|
|
107
|
+
await runCreate();
|
|
108
|
+
});
|
|
49
109
|
program
|
|
50
110
|
.command('version')
|
|
51
111
|
.description('Bump the version of the extension')
|
|
@@ -63,6 +123,12 @@ async function main() {
|
|
|
63
123
|
.action(async () => {
|
|
64
124
|
await runCheck(getCallerDir());
|
|
65
125
|
});
|
|
126
|
+
program
|
|
127
|
+
.command('install')
|
|
128
|
+
.description('Install the current extension as a Zed dev extension')
|
|
129
|
+
.action(async () => {
|
|
130
|
+
await installDevExtension(getCallerDir());
|
|
131
|
+
});
|
|
66
132
|
const addCmd = program
|
|
67
133
|
.command('add')
|
|
68
134
|
.description('Add a theme or language to an existing extension');
|
|
@@ -78,6 +144,15 @@ async function main() {
|
|
|
78
144
|
.action(async (id) => {
|
|
79
145
|
await addLanguage(getCallerDir(), id);
|
|
80
146
|
});
|
|
147
|
+
const snippetCmd = program
|
|
148
|
+
.command('snippet')
|
|
149
|
+
.description('Inject a code snippet into an existing extension');
|
|
150
|
+
snippetCmd
|
|
151
|
+
.command('add lsp')
|
|
152
|
+
.description('Wire up a language server (Rust + WASM) into the extension')
|
|
153
|
+
.action(async () => {
|
|
154
|
+
await addLsp(getCallerDir());
|
|
155
|
+
});
|
|
81
156
|
const syncCmd = program
|
|
82
157
|
.command('sync')
|
|
83
158
|
.description('Sync Zed settings and extensions via a GitHub repo')
|
|
@@ -109,25 +184,7 @@ async function main() {
|
|
|
109
184
|
await syncUninstall();
|
|
110
185
|
});
|
|
111
186
|
if (process.argv.length <= 2) {
|
|
112
|
-
|
|
113
|
-
if (options.types.includes('theme')) {
|
|
114
|
-
const themeDetails = await promptThemeDetails();
|
|
115
|
-
Object.assign(options, themeDetails);
|
|
116
|
-
}
|
|
117
|
-
if (options.types.includes('language')) {
|
|
118
|
-
const languageDetails = await promptLanguageDetails();
|
|
119
|
-
Object.assign(options, languageDetails);
|
|
120
|
-
}
|
|
121
|
-
const targetDir = path.join(getCallerDir(), options.id);
|
|
122
|
-
await generateExtension(options, targetDir);
|
|
123
|
-
p.outro(`${color.green('✓')} ${color.bold('Extension created successfully!')}\n` +
|
|
124
|
-
`${color.gray('─'.repeat(40))}\n` +
|
|
125
|
-
`${color.dim('Location:')} ${color.cyan(targetDir)}`);
|
|
126
|
-
p.outro(`${color.yellow('⚡')} ${color.bold('Next steps')}\n\n` +
|
|
127
|
-
` ${color.gray('1.')} Open Zed\n` +
|
|
128
|
-
` ${color.gray('2.')} ${color.white('Extensions > Install Dev Extension')}\n` +
|
|
129
|
-
` ${color.gray('3.')} Select ${color.cyan(options.id)} folder\n\n` +
|
|
130
|
-
`${color.dim('Learn more:')} ${color.underline(color.blue('https://zed.dev/docs/extensions/developing-extensions'))}`);
|
|
187
|
+
printWelcome();
|
|
131
188
|
return;
|
|
132
189
|
}
|
|
133
190
|
program.parse(process.argv);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function installDevExtension(callerDir: string): Promise<void>;
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import * as p from '@clack/prompts';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import color from 'picocolors';
|
|
6
|
+
// TOML helpers (regex-based — no parser dependency needed for these fields)
|
|
7
|
+
function tomlGetString(content, key) {
|
|
8
|
+
const match = content.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, 'm'));
|
|
9
|
+
return match?.[1];
|
|
10
|
+
}
|
|
11
|
+
function tomlGetNumber(content, key) {
|
|
12
|
+
const match = content.match(new RegExp(`^${key}\\s*=\\s*(\\d+)`, 'm'));
|
|
13
|
+
return match ? Number(match[1]) : undefined;
|
|
14
|
+
}
|
|
15
|
+
function tomlGetAuthors(content) {
|
|
16
|
+
const match = content.match(/^authors\s*=\s*\[([^\]]*)\]/m);
|
|
17
|
+
if (!match)
|
|
18
|
+
return [];
|
|
19
|
+
return [...match[1].matchAll(/"([^"]*)"/g)].map(m => m[1]);
|
|
20
|
+
}
|
|
21
|
+
// Filesystem helpers
|
|
22
|
+
function resolveZedExtensionsDir() {
|
|
23
|
+
const home = os.homedir();
|
|
24
|
+
const platform = process.platform;
|
|
25
|
+
if (platform === 'darwin') {
|
|
26
|
+
return path.join(home, 'Library', 'Application Support', 'Zed', 'extensions');
|
|
27
|
+
}
|
|
28
|
+
if (platform === 'linux') {
|
|
29
|
+
const xdgData = process.env.FLATPAK_XDG_DATA_HOME ||
|
|
30
|
+
process.env.XDG_DATA_HOME ||
|
|
31
|
+
path.join(home, '.local', 'share');
|
|
32
|
+
return path.join(xdgData, 'zed', 'extensions');
|
|
33
|
+
}
|
|
34
|
+
if (platform === 'win32') {
|
|
35
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
36
|
+
return path.join(localAppData, 'Zed', 'extensions');
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
39
|
+
}
|
|
40
|
+
function listSubdirs(dir) {
|
|
41
|
+
try {
|
|
42
|
+
return fs
|
|
43
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
44
|
+
.filter(d => d.isDirectory())
|
|
45
|
+
.map(d => d.name);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function buildManifest(extensionDir, toml) {
|
|
52
|
+
const id = tomlGetString(toml, 'id') ?? 'unknown';
|
|
53
|
+
const name = tomlGetString(toml, 'name') ?? id;
|
|
54
|
+
const version = tomlGetString(toml, 'version') ?? '0.0.1';
|
|
55
|
+
const schemaVersion = tomlGetNumber(toml, 'schema_version') ?? 1;
|
|
56
|
+
const description = tomlGetString(toml, 'description') ?? '';
|
|
57
|
+
const repository = tomlGetString(toml, 'repository') ?? '';
|
|
58
|
+
const authors = tomlGetAuthors(toml);
|
|
59
|
+
// Detect themes
|
|
60
|
+
const themesDir = path.join(extensionDir, 'themes');
|
|
61
|
+
const themes = fs.pathExistsSync(themesDir)
|
|
62
|
+
? fs
|
|
63
|
+
.readdirSync(themesDir)
|
|
64
|
+
.filter(f => f.endsWith('.json'))
|
|
65
|
+
.map(f => `themes/${f}`)
|
|
66
|
+
: [];
|
|
67
|
+
// Detect languages
|
|
68
|
+
const langsDir = path.join(extensionDir, 'languages');
|
|
69
|
+
const languages = fs.pathExistsSync(langsDir)
|
|
70
|
+
? listSubdirs(langsDir).map(d => `languages/${d}`)
|
|
71
|
+
: [];
|
|
72
|
+
// Detect grammars from extension.toml [grammars.<id>] blocks
|
|
73
|
+
const grammars = {};
|
|
74
|
+
const grammarMatches = toml.matchAll(/^\[grammars\.([^\]]+)\]\s*\nrepository\s*=\s*"([^"]*)"\s*\nrev\s*=\s*"([^"]*)"/gm);
|
|
75
|
+
for (const m of grammarMatches) {
|
|
76
|
+
grammars[m[1]] = { repository: m[2], rev: m[3], path: null };
|
|
77
|
+
}
|
|
78
|
+
// Detect language_servers from extension.toml [language_servers.<id>] blocks
|
|
79
|
+
const languageServers = {};
|
|
80
|
+
const lsMatches = toml.matchAll(/^\[language_servers\.([^\]]+)\]\s*\nname\s*=\s*"([^"]*)"\s*\nlanguages\s*=\s*\[([^\]]*)\]/gm);
|
|
81
|
+
for (const m of lsMatches) {
|
|
82
|
+
const langs = [...m[3].matchAll(/"([^"]*)"/g)].map(x => x[1]);
|
|
83
|
+
languageServers[m[1]] = {
|
|
84
|
+
language: langs[0] ?? '',
|
|
85
|
+
languages: langs.slice(1),
|
|
86
|
+
language_ids: {},
|
|
87
|
+
code_action_kinds: null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// Detect whether Rust lib is present
|
|
91
|
+
const hasLib = fs.pathExistsSync(path.join(extensionDir, 'Cargo.toml'));
|
|
92
|
+
return {
|
|
93
|
+
id,
|
|
94
|
+
name,
|
|
95
|
+
version,
|
|
96
|
+
schema_version: schemaVersion,
|
|
97
|
+
description,
|
|
98
|
+
repository,
|
|
99
|
+
authors,
|
|
100
|
+
lib: { kind: hasLib ? 'Rust' : null, version: null },
|
|
101
|
+
themes,
|
|
102
|
+
icon_themes: [],
|
|
103
|
+
languages,
|
|
104
|
+
grammars,
|
|
105
|
+
language_servers: languageServers,
|
|
106
|
+
context_servers: {},
|
|
107
|
+
agent_servers: {},
|
|
108
|
+
slash_commands: {},
|
|
109
|
+
snippets: null,
|
|
110
|
+
capabilities: [],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Main install function
|
|
114
|
+
export async function installDevExtension(callerDir) {
|
|
115
|
+
p.intro(`${color.bgBlue(color.bold(' zedx install '))} ${color.blue('Installing as a Zed dev extension…')}`);
|
|
116
|
+
const tomlPath = path.join(callerDir, 'extension.toml');
|
|
117
|
+
if (!(await fs.pathExists(tomlPath))) {
|
|
118
|
+
p.log.error(color.red('No extension.toml found. Run zedx from an extension directory.'));
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
const toml = await fs.readFile(tomlPath, 'utf-8');
|
|
122
|
+
const extensionId = tomlGetString(toml, 'id');
|
|
123
|
+
if (!extensionId) {
|
|
124
|
+
p.log.error(color.red('Could not read extension id from extension.toml.'));
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
let extensionsDir;
|
|
128
|
+
try {
|
|
129
|
+
extensionsDir = resolveZedExtensionsDir();
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
p.log.error(color.red(String(err)));
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
const installedDir = path.join(extensionsDir, 'installed');
|
|
136
|
+
const indexPath = path.join(extensionsDir, 'index.json');
|
|
137
|
+
const symlinkPath = path.join(installedDir, extensionId);
|
|
138
|
+
await fs.ensureDir(installedDir);
|
|
139
|
+
// --- Handle existing symlink / directory ---
|
|
140
|
+
if (await fs.pathExists(symlinkPath)) {
|
|
141
|
+
const stat = await fs.lstat(symlinkPath);
|
|
142
|
+
if (stat.isSymbolicLink()) {
|
|
143
|
+
const existing = await fs.readlink(symlinkPath);
|
|
144
|
+
if (existing === callerDir) {
|
|
145
|
+
p.log.warn(`${color.yellow(`${extensionId}`)} is already installed and points to this directory.`);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
const overwrite = await p.confirm({
|
|
149
|
+
message: `${extensionId} is already installed (→ ${existing}). Replace it?`,
|
|
150
|
+
initialValue: true,
|
|
151
|
+
});
|
|
152
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
153
|
+
p.cancel('Cancelled.');
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
await fs.remove(symlinkPath);
|
|
157
|
+
await fs.symlink(callerDir, symlinkPath);
|
|
158
|
+
p.log.success(`Replaced symlink ${color.cyan(`installed/${extensionId}`)} → ${color.dim(callerDir)}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
p.log.error(color.red(`${symlinkPath} exists and is not a symlink. Remove it manually first.`));
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
await fs.symlink(callerDir, symlinkPath);
|
|
168
|
+
p.log.success(`Created symlink ${color.cyan(`installed/${extensionId}`)} → ${color.dim(callerDir)}`);
|
|
169
|
+
}
|
|
170
|
+
// --- Upsert index.json ---
|
|
171
|
+
let index = { extensions: {} };
|
|
172
|
+
if (await fs.pathExists(indexPath)) {
|
|
173
|
+
try {
|
|
174
|
+
index = await fs.readJson(indexPath);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// malformed — start fresh
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const manifest = buildManifest(callerDir, toml);
|
|
181
|
+
index.extensions[extensionId] = { manifest, dev: true };
|
|
182
|
+
await fs.writeJson(indexPath, index, { spaces: 2 });
|
|
183
|
+
p.log.success(`Updated ${color.cyan('index.json')}`);
|
|
184
|
+
p.outro(`${color.green('✓')} ${color.bold(`${manifest.name} v${manifest.version}`)} installed as a dev extension.\n\n` +
|
|
185
|
+
` ${color.dim('Reload Zed to pick up the changes:')}\n` +
|
|
186
|
+
` ${color.white('Extensions')} ${color.dim('→')} ${color.white('Reload Extensions')} ${color.dim('(or restart Zed)')}\n\n` +
|
|
187
|
+
` ${color.dim('Run')} ${color.cyan('zedx check')} ${color.dim('to validate your extension.')}`);
|
|
188
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function addLsp(callerDir: string): Promise<void>;
|
package/dist/snippet.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import * as p from '@clack/prompts';
|
|
4
|
+
import ejs from 'ejs';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import color from 'picocolors';
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const isDev = __dirname.includes('/src/');
|
|
10
|
+
const PROJECT_ROOT = isDev ? path.join(__dirname, '..') : __dirname;
|
|
11
|
+
const TEMPLATE_DIR = path.join(PROJECT_ROOT, 'templates');
|
|
12
|
+
async function renderTemplate(templatePath, data) {
|
|
13
|
+
const template = await fs.readFile(templatePath, 'utf-8');
|
|
14
|
+
return ejs.render(template, data);
|
|
15
|
+
}
|
|
16
|
+
function tomlGet(content, key) {
|
|
17
|
+
const match = content.match(new RegExp(`^${key}\\s*=\\s*"([^"]*)"`, 'm'));
|
|
18
|
+
return match?.[1];
|
|
19
|
+
}
|
|
20
|
+
function toPascalCase(str) {
|
|
21
|
+
return str
|
|
22
|
+
.replace(/[-_\s]+(.)/g, (_, c) => c.toUpperCase())
|
|
23
|
+
.replace(/^(.)/, (c) => c.toUpperCase());
|
|
24
|
+
}
|
|
25
|
+
function detectLanguages(callerDir) {
|
|
26
|
+
try {
|
|
27
|
+
const langsDir = path.join(callerDir, 'languages');
|
|
28
|
+
if (!fs.pathExistsSync(langsDir))
|
|
29
|
+
return [];
|
|
30
|
+
return fs
|
|
31
|
+
.readdirSync(langsDir, { withFileTypes: true })
|
|
32
|
+
.filter(d => d.isDirectory())
|
|
33
|
+
.map(d => d.name);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function addLsp(callerDir) {
|
|
40
|
+
p.intro(`${color.bgBlue(color.bold(' zedx snippet add lsp '))} ${color.blue('Wiring up a language server…')}`);
|
|
41
|
+
const tomlPath = path.join(callerDir, 'extension.toml');
|
|
42
|
+
if (!(await fs.pathExists(tomlPath))) {
|
|
43
|
+
p.log.error(color.red('No extension.toml found. Run zedx from an extension directory.'));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
const tomlContent = await fs.readFile(tomlPath, 'utf-8');
|
|
47
|
+
const extensionId = tomlGet(tomlContent, 'id') ?? 'my-extension';
|
|
48
|
+
const extensionName = tomlGet(tomlContent, 'name') ?? extensionId;
|
|
49
|
+
// --- LSP server name ---
|
|
50
|
+
const lspNameDefault = `${extensionName} LSP`;
|
|
51
|
+
const lspName = await p.text({
|
|
52
|
+
message: 'Language server display name:',
|
|
53
|
+
placeholder: lspNameDefault,
|
|
54
|
+
});
|
|
55
|
+
if (p.isCancel(lspName)) {
|
|
56
|
+
p.cancel('Cancelled.');
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
const lspNameValue = String(lspName || lspNameDefault);
|
|
60
|
+
// Derive a TOML-safe ID from the display name
|
|
61
|
+
const lspId = lspNameValue
|
|
62
|
+
.toLowerCase()
|
|
63
|
+
.replace(/\s+/g, '-')
|
|
64
|
+
.replace(/[^a-z0-9-]/g, '');
|
|
65
|
+
// --- Check for duplicate ---
|
|
66
|
+
if (new RegExp(`^\\[language_servers\\.${lspId}\\]`, 'm').test(tomlContent)) {
|
|
67
|
+
p.log.error(color.red(`[language_servers.${lspId}] already exists in extension.toml.`));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
// --- Language association ---
|
|
71
|
+
const detectedLanguages = detectLanguages(callerDir);
|
|
72
|
+
let languageName;
|
|
73
|
+
if (detectedLanguages.length > 0) {
|
|
74
|
+
const choice = await p.select({
|
|
75
|
+
message: 'Which language does this LSP serve?',
|
|
76
|
+
options: [
|
|
77
|
+
...detectedLanguages.map(l => ({
|
|
78
|
+
value: l,
|
|
79
|
+
label: toPascalCase(l),
|
|
80
|
+
})),
|
|
81
|
+
{ value: '__custom__', label: 'Enter manually' },
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
if (p.isCancel(choice)) {
|
|
85
|
+
p.cancel('Cancelled.');
|
|
86
|
+
process.exit(0);
|
|
87
|
+
}
|
|
88
|
+
if (choice === '__custom__') {
|
|
89
|
+
const custom = await p.text({
|
|
90
|
+
message: 'Language name (must match name in config.toml):',
|
|
91
|
+
placeholder: extensionName,
|
|
92
|
+
});
|
|
93
|
+
if (p.isCancel(custom)) {
|
|
94
|
+
p.cancel('Cancelled.');
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
languageName = String(custom || extensionName);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
languageName = toPascalCase(String(choice));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const custom = await p.text({
|
|
105
|
+
message: 'Language name (must match name in config.toml):',
|
|
106
|
+
placeholder: extensionName,
|
|
107
|
+
});
|
|
108
|
+
if (p.isCancel(custom)) {
|
|
109
|
+
p.cancel('Cancelled.');
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
languageName = String(custom || extensionName);
|
|
113
|
+
}
|
|
114
|
+
// --- LSP binary command ---
|
|
115
|
+
const lspCommand = await p.text({
|
|
116
|
+
message: 'LSP binary command (the executable name or path):',
|
|
117
|
+
placeholder: `${lspId}-server`,
|
|
118
|
+
});
|
|
119
|
+
if (p.isCancel(lspCommand)) {
|
|
120
|
+
p.cancel('Cancelled.');
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|
|
123
|
+
const lspCommandValue = String(lspCommand || `${lspId}-server`);
|
|
124
|
+
// --- Append [language_servers.*] block to extension.toml ---
|
|
125
|
+
const lspTomlBlock = `\n[language_servers.${lspId}]\n` +
|
|
126
|
+
`name = "${lspNameValue}"\n` +
|
|
127
|
+
`languages = ["${languageName}"]\n`;
|
|
128
|
+
await fs.appendFile(tomlPath, lspTomlBlock);
|
|
129
|
+
p.log.success(`Updated ${color.cyan('extension.toml')} with [language_servers.${lspId}]`);
|
|
130
|
+
// --- Add lib.path to extension.toml if not present ---
|
|
131
|
+
const updatedToml = await fs.readFile(tomlPath, 'utf-8');
|
|
132
|
+
if (!/^lib\s*=/m.test(updatedToml)) {
|
|
133
|
+
await fs.appendFile(tomlPath, `\nlib.path = "extension.wasm"\n`);
|
|
134
|
+
p.log.success(`Updated ${color.cyan('extension.toml')} with lib.path`);
|
|
135
|
+
}
|
|
136
|
+
const structName = toPascalCase(extensionId).replace(/-/g, '');
|
|
137
|
+
// --- Generate Cargo.toml if not present ---
|
|
138
|
+
const cargoPath = path.join(callerDir, 'Cargo.toml');
|
|
139
|
+
if (!(await fs.pathExists(cargoPath))) {
|
|
140
|
+
const cargoToml = await renderTemplate(path.join(TEMPLATE_DIR, 'lsp/Cargo.toml.ejs'), {
|
|
141
|
+
extensionId,
|
|
142
|
+
});
|
|
143
|
+
await fs.writeFile(cargoPath, cargoToml);
|
|
144
|
+
p.log.success(`Created ${color.cyan('Cargo.toml')}`);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
p.log.warn(`${color.yellow('Cargo.toml already exists')} — skipped`);
|
|
148
|
+
}
|
|
149
|
+
// --- Generate src/lib.rs if not present ---
|
|
150
|
+
const srcDir = path.join(callerDir, 'src');
|
|
151
|
+
const libRsPath = path.join(srcDir, 'lib.rs');
|
|
152
|
+
if (!(await fs.pathExists(libRsPath))) {
|
|
153
|
+
await fs.ensureDir(srcDir);
|
|
154
|
+
const libRs = await renderTemplate(path.join(TEMPLATE_DIR, 'lsp/lib.rs.ejs'), {
|
|
155
|
+
structName,
|
|
156
|
+
lspCommand: lspCommandValue,
|
|
157
|
+
});
|
|
158
|
+
await fs.writeFile(libRsPath, libRs);
|
|
159
|
+
p.log.success(`Created ${color.cyan('src/lib.rs')}`);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
p.log.warn(`${color.yellow('src/lib.rs already exists')} — skipped`);
|
|
163
|
+
}
|
|
164
|
+
p.outro(`${color.green('✓')} LSP snippet added.\n\n` +
|
|
165
|
+
` ${color.dim('1.')} Edit ${color.cyan('src/lib.rs')} — implement ${color.white('language_server_command')}\n` +
|
|
166
|
+
` ${color.dim('2.')} Edit ${color.cyan('Cargo.toml')} — pin ${color.white('zed_extension_api')} to latest version\n` +
|
|
167
|
+
` ${color.dim('3.')} ${color.dim('Docs:')} ${color.underline(color.blue('https://zed.dev/docs/extensions/languages#language-servers'))}`);
|
|
168
|
+
}
|
package/dist/sync.js
CHANGED
|
@@ -110,7 +110,7 @@ async function applyRemoteSettings(repoSettings, repoExtensions, localSettingsPa
|
|
|
110
110
|
}
|
|
111
111
|
// zedx sync status
|
|
112
112
|
export async function syncStatus() {
|
|
113
|
-
p.intro(color.bold('zedx sync status'));
|
|
113
|
+
p.intro(`${color.bgBlue(color.bold(' zedx sync status '))} ${color.blue('Checking sync state…')}`);
|
|
114
114
|
const config = await requireSyncConfig();
|
|
115
115
|
const zedPaths = resolveZedPaths();
|
|
116
116
|
p.log.info(`Repo: ${color.dim(config.syncRepo)} ${color.dim(`(${config.branch})`)}`);
|
|
@@ -186,7 +186,7 @@ export async function syncStatus() {
|
|
|
186
186
|
}
|
|
187
187
|
// zedx sync init
|
|
188
188
|
export async function syncInit() {
|
|
189
|
-
p.intro(color.bold('zedx sync init'));
|
|
189
|
+
p.intro(`${color.bgBlue(color.bold(' zedx sync init '))} ${color.blue('Linking a git repo as the sync target…')}`);
|
|
190
190
|
const repo = await p.text({
|
|
191
191
|
message: 'GitHub repo URL (SSH or HTTPS)',
|
|
192
192
|
placeholder: 'https://github.com/you/zed-config.git',
|
|
@@ -251,7 +251,7 @@ export async function runSync(opts = {}) {
|
|
|
251
251
|
},
|
|
252
252
|
};
|
|
253
253
|
if (!silent)
|
|
254
|
-
p.intro(color.bold('zedx sync'));
|
|
254
|
+
p.intro(`${color.bgBlue(color.bold(' zedx sync '))} ${color.blue('Syncing Zed settings and extensions…')}`);
|
|
255
255
|
const config = await requireSyncConfig();
|
|
256
256
|
const zedPaths = resolveZedPaths();
|
|
257
257
|
// Spinner shim: in silent mode just log to stderr so daemons can capture it
|
|
@@ -2,21 +2,23 @@ id = "<%= id %>"
|
|
|
2
2
|
name = "<%= name %>"
|
|
3
3
|
version = "0.0.1"
|
|
4
4
|
schema_version = 1
|
|
5
|
-
authors = ["<%- author %>"]
|
|
5
|
+
authors = ["<%- author %>"] # TODO(edit-me): replace with your name and email
|
|
6
6
|
description = "<%= description %>"
|
|
7
|
-
repository = "<%= repository %>"
|
|
7
|
+
repository = "<%= repository %>" # TODO(edit-me): update to your actual repository URL
|
|
8
8
|
|
|
9
9
|
<% if (types.includes('language') && grammarRepo) { %>
|
|
10
10
|
[grammars.<%= languageId %>]
|
|
11
11
|
repository = "<%= grammarRepo %>"
|
|
12
12
|
rev = "<%= grammarRev || 'main' %>"
|
|
13
13
|
<% } else if (types.includes('language')) { %>
|
|
14
|
+
# TODO(edit-me): uncomment and fill in the grammar block once you have a Tree-sitter grammar repo
|
|
14
15
|
# [grammars.<%= languageId %>]
|
|
15
16
|
# repository = "https://github.com/user/tree-sitter-<%= languageId %>"
|
|
16
17
|
# rev = "main"
|
|
17
18
|
<% } %>
|
|
18
19
|
|
|
19
20
|
<% if (types.includes('language')) { %>
|
|
21
|
+
# TODO(edit-me): uncomment and fill in the LSP block if your language has a language server
|
|
20
22
|
# [language_servers.my-lsp]
|
|
21
23
|
# name = "My Language LSP"
|
|
22
24
|
# languages = ["<%= languageName %>"]
|
|
@@ -4,10 +4,12 @@ name = "<%= languageName %>"
|
|
|
4
4
|
# grammar (required): The name of a Tree-sitter grammar (must match grammar registration in extension.toml)
|
|
5
5
|
grammar = "<%= languageId %>"
|
|
6
6
|
|
|
7
|
-
#
|
|
7
|
+
# TODO(edit-me): uncomment and set the file extensions for your language (e.g., ["myl", "my"])
|
|
8
|
+
# path_suffixes: Array of file suffixes associated with this language
|
|
8
9
|
# Unlike file_types in settings, this does not support glob patterns
|
|
9
10
|
# path_suffixes = []
|
|
10
11
|
|
|
12
|
+
# TODO(edit-me): uncomment and set the comment syntax for your language (e.g., ["// ", "# "])
|
|
11
13
|
# line_comments: Array of strings used to identify line comments
|
|
12
14
|
# Used for editor::ToggleComments keybind (cmd-/ or ctrl-/)
|
|
13
15
|
# line_comments = ["// ", "# "]
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
; Learn more about Tree-sitter queries:
|
|
28
28
|
; https://tree-sitter.github.io/tree-sitter/using-parsers/queries
|
|
29
29
|
|
|
30
|
+
; TODO(edit-me): replace these with the actual node type names from your Tree-sitter grammar
|
|
30
31
|
(string) @string
|
|
31
32
|
(number) @number
|
|
32
33
|
(comment) @comment
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "<%= extensionId %>"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
edition = "2024"
|
|
5
|
+
|
|
6
|
+
[lib]
|
|
7
|
+
crate-type = ["cdylib"]
|
|
8
|
+
|
|
9
|
+
[dependencies]
|
|
10
|
+
# TODO(edit-me): pin to the latest version compatible with your target Zed version
|
|
11
|
+
# See: https://crates.io/crates/zed_extension_api
|
|
12
|
+
zed_extension_api = "0.4"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
use zed_extension_api::{self as zed, LanguageServerId, Result};
|
|
2
|
+
|
|
3
|
+
struct <%= structName %> {
|
|
4
|
+
// TODO(edit-me): add any cached state here (e.g. cached binary path)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
impl zed::Extension for <%= structName %> {
|
|
8
|
+
fn new() -> Self {
|
|
9
|
+
Self {}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
fn language_server_command(
|
|
13
|
+
&mut self,
|
|
14
|
+
_language_server_id: &LanguageServerId,
|
|
15
|
+
_worktree: &zed::Worktree,
|
|
16
|
+
) -> Result<zed::Command> {
|
|
17
|
+
// TODO(edit-me): resolve the path to the LSP binary.
|
|
18
|
+
// Common patterns:
|
|
19
|
+
// - zed::Command { command: worktree.which("<%= lspCommand %>")?.ok_or("...")?, ... }
|
|
20
|
+
// - download the binary via zed::download_file and cache the path
|
|
21
|
+
// See: https://docs.rs/zed_extension_api
|
|
22
|
+
Ok(zed::Command {
|
|
23
|
+
command: "<%= lspCommand %>".to_string(), // TODO(edit-me): replace with actual binary path
|
|
24
|
+
args: vec![], // TODO(edit-me): add required args
|
|
25
|
+
env: vec![],
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
zed::register_extension!(<%= structName %>);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
|
|
3
3
|
"name": "<%= themeName %>",
|
|
4
4
|
"author": "<%= author %>",
|
|
5
|
+
"_todo": "TODO(edit-me): replace the placeholder grayscale colors in 'style' with your actual palette. Search for hex values like #d4d4d4ff to find them.",
|
|
5
6
|
"themes": [
|
|
6
7
|
<% appearances.forEach((app, index) => { %>
|
|
7
8
|
{
|
package/package.json
CHANGED
|
@@ -1,59 +1,60 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
2
|
+
"name": "zedx",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Scaffold Zed Editor extensions and sync your settings across machines.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"boilerplate",
|
|
7
|
+
"extension",
|
|
8
|
+
"scaffold",
|
|
9
|
+
"zed",
|
|
10
|
+
"zed-editor"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/tahayvr/zedx#readme",
|
|
13
|
+
"license": "Apache-2.0",
|
|
14
|
+
"author": "Taha Nejad <taha@noiserandom.com>",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/tahayvr/zedx.git"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"zedx": "dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "dist/index.js",
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc && cp -r src/templates dist/",
|
|
32
|
+
"start": "node dist/index.js",
|
|
33
|
+
"dev": "tsx src/index.ts",
|
|
34
|
+
"lint": "oxlint",
|
|
35
|
+
"lint:fix": "oxlint --fix",
|
|
36
|
+
"fmt": "oxfmt",
|
|
37
|
+
"fmt:check": "oxfmt --check"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@clack/prompts": "^0.10.1",
|
|
41
|
+
"commander": "^14.0.3",
|
|
42
|
+
"ejs": "^4.0.1",
|
|
43
|
+
"fs-extra": "^11.3.3",
|
|
44
|
+
"picocolors": "^1.1.1",
|
|
45
|
+
"simple-git": "^3.33.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/ejs": "^3.1.5",
|
|
49
|
+
"@types/fs-extra": "^11.0.4",
|
|
50
|
+
"@types/node": "^25.2.3",
|
|
51
|
+
"oxfmt": "^0.42.0",
|
|
52
|
+
"oxlint": "^1.57.0",
|
|
53
|
+
"tsx": "^4.21.0",
|
|
54
|
+
"typescript": "^5.9.3"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=18"
|
|
58
|
+
},
|
|
59
|
+
"packageManager": "pnpm@10.30.0+sha512.2b5753de015d480eeb88f5b5b61e0051f05b4301808a82ec8b840c9d2adf7748eb352c83f5c1593ca703ff1017295bc3fdd3119abb9686efc96b9fcb18200937"
|
|
60
|
+
}
|