zedx 0.6.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/add.js +17 -9
- package/dist/check.js +4 -4
- package/dist/daemon.js +16 -12
- package/dist/generator.js +6 -6
- package/dist/index.js +82 -25
- package/dist/install.d.ts +1 -0
- package/dist/install.js +188 -0
- package/dist/prompts.js +20 -16
- package/dist/snippet.d.ts +1 -0
- package/dist/snippet.js +168 -0
- package/dist/sync.js +89 -34
- 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 -55
package/dist/prompts.js
CHANGED
|
@@ -5,7 +5,7 @@ export async function promptUser() {
|
|
|
5
5
|
const nameDefault = 'my-zed-extension';
|
|
6
6
|
const name = await p.text({
|
|
7
7
|
message: 'Project name:',
|
|
8
|
-
placeholder: nameDefault
|
|
8
|
+
placeholder: nameDefault,
|
|
9
9
|
});
|
|
10
10
|
if (p.isCancel(name)) {
|
|
11
11
|
p.cancel('Cancelled.');
|
|
@@ -21,7 +21,7 @@ export async function promptUser() {
|
|
|
21
21
|
return 'ID cannot contain spaces';
|
|
22
22
|
if (value && value !== value.toLowerCase())
|
|
23
23
|
return 'ID must be lowercase';
|
|
24
|
-
}
|
|
24
|
+
},
|
|
25
25
|
});
|
|
26
26
|
if (p.isCancel(id)) {
|
|
27
27
|
p.cancel('Cancelled.');
|
|
@@ -31,7 +31,7 @@ export async function promptUser() {
|
|
|
31
31
|
const descriptionDefault = 'A Zed extension';
|
|
32
32
|
const description = await p.text({
|
|
33
33
|
message: 'Description:',
|
|
34
|
-
placeholder: descriptionDefault
|
|
34
|
+
placeholder: descriptionDefault,
|
|
35
35
|
});
|
|
36
36
|
if (p.isCancel(description)) {
|
|
37
37
|
p.cancel('Cancelled.');
|
|
@@ -44,7 +44,7 @@ export async function promptUser() {
|
|
|
44
44
|
validate: (value) => {
|
|
45
45
|
if (!value || value.length === 0)
|
|
46
46
|
return 'Author is required';
|
|
47
|
-
}
|
|
47
|
+
},
|
|
48
48
|
});
|
|
49
49
|
if (p.isCancel(author)) {
|
|
50
50
|
p.cancel('Cancelled.');
|
|
@@ -53,7 +53,7 @@ export async function promptUser() {
|
|
|
53
53
|
const repositoryDefault = `https://github.com/username/${idValue}.git`;
|
|
54
54
|
const repository = await p.text({
|
|
55
55
|
message: 'GitHub repository URL:',
|
|
56
|
-
initialValue: repositoryDefault
|
|
56
|
+
initialValue: repositoryDefault,
|
|
57
57
|
});
|
|
58
58
|
if (p.isCancel(repository)) {
|
|
59
59
|
p.cancel('Cancelled.');
|
|
@@ -69,9 +69,9 @@ export async function promptUser() {
|
|
|
69
69
|
{ value: 'GPL-3.0', label: 'GNU GPLv3' },
|
|
70
70
|
{ value: 'LGPL-3.0', label: 'GNU LGPLv3' },
|
|
71
71
|
{ value: 'MIT', label: 'MIT' },
|
|
72
|
-
{ value: 'Zlib', label: 'zlib' }
|
|
72
|
+
{ value: 'Zlib', label: 'zlib' },
|
|
73
73
|
],
|
|
74
|
-
initialValue: 'MIT'
|
|
74
|
+
initialValue: 'MIT',
|
|
75
75
|
});
|
|
76
76
|
if (p.isCancel(license)) {
|
|
77
77
|
p.cancel('Cancelled.');
|
|
@@ -81,9 +81,13 @@ export async function promptUser() {
|
|
|
81
81
|
message: 'What do you want to include in your extension?',
|
|
82
82
|
options: [
|
|
83
83
|
{ value: 'theme', label: 'Theme', hint: 'Color scheme for the editor' },
|
|
84
|
-
{
|
|
84
|
+
{
|
|
85
|
+
value: 'language',
|
|
86
|
+
label: 'Language',
|
|
87
|
+
hint: 'Syntax highlighting, indentation, etc.',
|
|
88
|
+
},
|
|
85
89
|
],
|
|
86
|
-
required: true
|
|
90
|
+
required: true,
|
|
87
91
|
});
|
|
88
92
|
if (p.isCancel(extensionTypes)) {
|
|
89
93
|
p.cancel('Cancelled.');
|
|
@@ -96,14 +100,14 @@ export async function promptUser() {
|
|
|
96
100
|
author: String(author),
|
|
97
101
|
repository: repositoryValue,
|
|
98
102
|
license: license,
|
|
99
|
-
types: extensionTypes
|
|
103
|
+
types: extensionTypes,
|
|
100
104
|
};
|
|
101
105
|
return options;
|
|
102
106
|
}
|
|
103
107
|
export async function promptThemeDetails() {
|
|
104
108
|
const themeName = await p.text({
|
|
105
109
|
message: 'Theme name:',
|
|
106
|
-
placeholder: 'My Theme'
|
|
110
|
+
placeholder: 'My Theme',
|
|
107
111
|
});
|
|
108
112
|
if (p.isCancel(themeName)) {
|
|
109
113
|
p.cancel('Cancelled.');
|
|
@@ -114,9 +118,9 @@ export async function promptThemeDetails() {
|
|
|
114
118
|
options: [
|
|
115
119
|
{ value: 'dark', label: 'Dark' },
|
|
116
120
|
{ value: 'light', label: 'Light' },
|
|
117
|
-
{ value: 'both', label: 'Both (Dark & Light)' }
|
|
121
|
+
{ value: 'both', label: 'Both (Dark & Light)' },
|
|
118
122
|
],
|
|
119
|
-
initialValue: 'dark'
|
|
123
|
+
initialValue: 'dark',
|
|
120
124
|
});
|
|
121
125
|
if (p.isCancel(appearance)) {
|
|
122
126
|
p.cancel('Cancelled.');
|
|
@@ -124,13 +128,13 @@ export async function promptThemeDetails() {
|
|
|
124
128
|
}
|
|
125
129
|
return {
|
|
126
130
|
themeName: String(themeName),
|
|
127
|
-
appearance: appearance
|
|
131
|
+
appearance: appearance,
|
|
128
132
|
};
|
|
129
133
|
}
|
|
130
134
|
export async function promptLanguageDetails() {
|
|
131
135
|
const languageName = await p.text({
|
|
132
136
|
message: 'Language name:',
|
|
133
|
-
placeholder: 'My Language'
|
|
137
|
+
placeholder: 'My Language',
|
|
134
138
|
});
|
|
135
139
|
if (p.isCancel(languageName)) {
|
|
136
140
|
p.cancel('Cancelled.');
|
|
@@ -138,7 +142,7 @@ export async function promptLanguageDetails() {
|
|
|
138
142
|
}
|
|
139
143
|
const result = {
|
|
140
144
|
languageName: String(languageName),
|
|
141
|
-
languageId: String(languageName).toLowerCase().replace(/\s+/g, '-')
|
|
145
|
+
languageId: String(languageName).toLowerCase().replace(/\s+/g, '-'),
|
|
142
146
|
};
|
|
143
147
|
return result;
|
|
144
148
|
}
|
|
@@ -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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os from 'os';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import fs from 'fs-extra';
|
|
4
3
|
import * as p from '@clack/prompts';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
5
|
import color from 'picocolors';
|
|
6
6
|
import simpleGit from 'simple-git';
|
|
7
7
|
import { resolveZedPaths } from './zed-paths.js';
|
|
@@ -16,7 +16,9 @@ async function readSyncConfig() {
|
|
|
16
16
|
async function requireSyncConfig() {
|
|
17
17
|
const config = await readSyncConfig();
|
|
18
18
|
if (!config) {
|
|
19
|
-
p.log.error(color.red('No sync config found. Run ') +
|
|
19
|
+
p.log.error(color.red('No sync config found. Run ') +
|
|
20
|
+
color.cyan('zedx sync init') +
|
|
21
|
+
color.red(' first.'));
|
|
20
22
|
process.exit(1);
|
|
21
23
|
}
|
|
22
24
|
return config;
|
|
@@ -35,6 +37,26 @@ async function withTempDir(fn) {
|
|
|
35
37
|
await fs.remove(tmp);
|
|
36
38
|
}
|
|
37
39
|
}
|
|
40
|
+
// Prepare settings.json for pushing to the repo by stripping auto_install_extensions.
|
|
41
|
+
// That field is derived from extensions/index.json on pull, so storing it in the
|
|
42
|
+
// remote would create stale/conflicting data across machines.
|
|
43
|
+
async function prepareSettingsForPush(localSettingsPath, repoSettingsPath) {
|
|
44
|
+
const raw = await fs.readFile(localSettingsPath, 'utf-8');
|
|
45
|
+
const stripped = raw.replace(/\/\/[^\n]*/g, '');
|
|
46
|
+
let settingsObj = {};
|
|
47
|
+
try {
|
|
48
|
+
settingsObj = JSON.parse(stripped);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// If we can't parse it (e.g. complex comments), push as-is
|
|
52
|
+
await fs.ensureDir(path.dirname(repoSettingsPath));
|
|
53
|
+
await fs.copy(localSettingsPath, repoSettingsPath, { overwrite: true });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
delete settingsObj['auto_install_extensions'];
|
|
57
|
+
await fs.ensureDir(path.dirname(repoSettingsPath));
|
|
58
|
+
await fs.writeFile(repoSettingsPath, JSON.stringify(settingsObj, null, 4), 'utf-8');
|
|
59
|
+
}
|
|
38
60
|
// Extension merge helper
|
|
39
61
|
async function applyRemoteSettings(repoSettings, repoExtensions, localSettingsPath, silent = false) {
|
|
40
62
|
// Backup existing settings
|
|
@@ -48,7 +70,7 @@ async function applyRemoteSettings(repoSettings, repoExtensions, localSettingsPa
|
|
|
48
70
|
if (await fs.pathExists(repoExtensions)) {
|
|
49
71
|
try {
|
|
50
72
|
const indexJson = (await fs.readJson(repoExtensions));
|
|
51
|
-
const extensionIds = Object.keys(indexJson.extensions ?? {}).filter(
|
|
73
|
+
const extensionIds = Object.keys(indexJson.extensions ?? {}).filter(id => !indexJson.extensions[id]?.dev);
|
|
52
74
|
if (extensionIds.length > 0) {
|
|
53
75
|
const stripped = settingsJson.replace(/\/\/[^\n]*/g, '');
|
|
54
76
|
let settingsObj = {};
|
|
@@ -59,12 +81,23 @@ async function applyRemoteSettings(repoSettings, repoExtensions, localSettingsPa
|
|
|
59
81
|
if (!silent)
|
|
60
82
|
p.log.warn(color.yellow('Could not parse settings.json — skipping extension merge.'));
|
|
61
83
|
}
|
|
62
|
-
|
|
84
|
+
// Preserve any existing entries (e.g. false entries for "never install"),
|
|
85
|
+
// then add true for every extension recorded in index.json.
|
|
86
|
+
const existing = typeof settingsObj['auto_install_extensions'] === 'object' &&
|
|
87
|
+
settingsObj['auto_install_extensions'] !== null
|
|
88
|
+
? settingsObj['auto_install_extensions']
|
|
89
|
+
: {};
|
|
90
|
+
const autoInstall = { ...existing };
|
|
63
91
|
for (const id of extensionIds) {
|
|
64
|
-
|
|
92
|
+
// Only set to true if there is no explicit user preference already
|
|
93
|
+
if (!(id in autoInstall)) {
|
|
94
|
+
autoInstall[id] = true;
|
|
95
|
+
}
|
|
65
96
|
}
|
|
66
97
|
settingsObj['auto_install_extensions'] = autoInstall;
|
|
67
98
|
settingsJson = JSON.stringify(settingsObj, null, 4);
|
|
99
|
+
if (!silent)
|
|
100
|
+
p.log.info(`Injected ${color.cyan(String(extensionIds.length))} extension(s) into ${color.dim('auto_install_extensions')}`);
|
|
68
101
|
}
|
|
69
102
|
}
|
|
70
103
|
catch {
|
|
@@ -77,7 +110,7 @@ async function applyRemoteSettings(repoSettings, repoExtensions, localSettingsPa
|
|
|
77
110
|
}
|
|
78
111
|
// zedx sync status
|
|
79
112
|
export async function syncStatus() {
|
|
80
|
-
p.intro(color.bold('zedx sync status'));
|
|
113
|
+
p.intro(`${color.bgBlue(color.bold(' zedx sync status '))} ${color.blue('Checking sync state…')}`);
|
|
81
114
|
const config = await requireSyncConfig();
|
|
82
115
|
const zedPaths = resolveZedPaths();
|
|
83
116
|
p.log.info(`Repo: ${color.dim(config.syncRepo)} ${color.dim(`(${config.branch})`)}`);
|
|
@@ -104,13 +137,13 @@ export async function syncStatus() {
|
|
|
104
137
|
{
|
|
105
138
|
repoPath: path.join(tmp, 'settings.json'),
|
|
106
139
|
localPath: zedPaths.settings,
|
|
107
|
-
label: 'settings.json'
|
|
140
|
+
label: 'settings.json',
|
|
108
141
|
},
|
|
109
142
|
{
|
|
110
143
|
repoPath: path.join(tmp, 'extensions', 'index.json'),
|
|
111
144
|
localPath: zedPaths.extensions,
|
|
112
|
-
label: 'extensions/index.json'
|
|
113
|
-
}
|
|
145
|
+
label: 'extensions/index.json',
|
|
146
|
+
},
|
|
114
147
|
];
|
|
115
148
|
for (const file of files) {
|
|
116
149
|
const localExists = await fs.pathExists(file.localPath);
|
|
@@ -153,17 +186,17 @@ export async function syncStatus() {
|
|
|
153
186
|
}
|
|
154
187
|
// zedx sync init
|
|
155
188
|
export async function syncInit() {
|
|
156
|
-
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…')}`);
|
|
157
190
|
const repo = await p.text({
|
|
158
191
|
message: 'GitHub repo URL (SSH or HTTPS)',
|
|
159
192
|
placeholder: 'https://github.com/you/zed-config.git',
|
|
160
|
-
validate:
|
|
193
|
+
validate: v => {
|
|
161
194
|
if (!v.trim())
|
|
162
195
|
return 'Repo URL is required';
|
|
163
196
|
if (!v.startsWith('https://') && !v.startsWith('git@')) {
|
|
164
197
|
return 'Must be a valid HTTPS or SSH git URL';
|
|
165
198
|
}
|
|
166
|
-
}
|
|
199
|
+
},
|
|
167
200
|
});
|
|
168
201
|
if (p.isCancel(repo)) {
|
|
169
202
|
p.cancel('Cancelled.');
|
|
@@ -172,7 +205,7 @@ export async function syncInit() {
|
|
|
172
205
|
const branch = await p.text({
|
|
173
206
|
message: 'Branch name',
|
|
174
207
|
placeholder: 'main',
|
|
175
|
-
defaultValue: 'main'
|
|
208
|
+
defaultValue: 'main',
|
|
176
209
|
});
|
|
177
210
|
if (p.isCancel(branch)) {
|
|
178
211
|
p.cancel('Cancelled.');
|
|
@@ -190,7 +223,7 @@ export async function syncInit() {
|
|
|
190
223
|
}
|
|
191
224
|
const config = {
|
|
192
225
|
syncRepo: repo.trim(),
|
|
193
|
-
branch: (branch || 'main').trim()
|
|
226
|
+
branch: (branch || 'main').trim(),
|
|
194
227
|
};
|
|
195
228
|
await writeSyncConfig(config);
|
|
196
229
|
p.outro(`${color.green('✓')} Sync config saved to ${color.cyan(ZEDX_CONFIG_PATH)}\n\n` +
|
|
@@ -215,17 +248,17 @@ export async function runSync(opts = {}) {
|
|
|
215
248
|
success: (msg) => {
|
|
216
249
|
if (!silent)
|
|
217
250
|
p.log.success(msg);
|
|
218
|
-
}
|
|
251
|
+
},
|
|
219
252
|
};
|
|
220
253
|
if (!silent)
|
|
221
|
-
p.intro(color.bold('zedx sync'));
|
|
254
|
+
p.intro(`${color.bgBlue(color.bold(' zedx sync '))} ${color.blue('Syncing Zed settings and extensions…')}`);
|
|
222
255
|
const config = await requireSyncConfig();
|
|
223
256
|
const zedPaths = resolveZedPaths();
|
|
224
257
|
// Spinner shim: in silent mode just log to stderr so daemons can capture it
|
|
225
258
|
const spinner = silent
|
|
226
259
|
? {
|
|
227
260
|
start: (m) => console.error(`[zedx] ${m}`),
|
|
228
|
-
stop: (m) => console.error(`[zedx] ${m}`)
|
|
261
|
+
stop: (m) => console.error(`[zedx] ${m}`),
|
|
229
262
|
}
|
|
230
263
|
: p.spinner();
|
|
231
264
|
await withTempDir(async (tmp) => {
|
|
@@ -249,13 +282,13 @@ export async function runSync(opts = {}) {
|
|
|
249
282
|
{
|
|
250
283
|
repoPath: path.join(tmp, 'settings.json'),
|
|
251
284
|
localPath: zedPaths.settings,
|
|
252
|
-
label: 'settings.json'
|
|
285
|
+
label: 'settings.json',
|
|
253
286
|
},
|
|
254
287
|
{
|
|
255
288
|
repoPath: path.join(tmp, 'extensions', 'index.json'),
|
|
256
289
|
localPath: zedPaths.extensions,
|
|
257
|
-
label: 'extensions/index.json'
|
|
258
|
-
}
|
|
290
|
+
label: 'extensions/index.json',
|
|
291
|
+
},
|
|
259
292
|
];
|
|
260
293
|
let anyChanges = false;
|
|
261
294
|
for (const file of files) {
|
|
@@ -269,8 +302,13 @@ export async function runSync(opts = {}) {
|
|
|
269
302
|
// Remote doesn't have it yet — push local
|
|
270
303
|
if (localExists && !remoteFileExists) {
|
|
271
304
|
log.info(`${file.label}: ${color.green('pushing')} (not in remote yet)`);
|
|
272
|
-
|
|
273
|
-
|
|
305
|
+
if (file.label === 'settings.json') {
|
|
306
|
+
await prepareSettingsForPush(file.localPath, file.repoPath);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
await fs.ensureDir(path.dirname(file.repoPath));
|
|
310
|
+
await fs.copy(file.localPath, file.repoPath, { overwrite: true });
|
|
311
|
+
}
|
|
274
312
|
anyChanges = true;
|
|
275
313
|
continue;
|
|
276
314
|
}
|
|
@@ -295,14 +333,21 @@ export async function runSync(opts = {}) {
|
|
|
295
333
|
}
|
|
296
334
|
// Detect which side changed since last sync via mtime
|
|
297
335
|
const localMtime = (await fs.stat(file.localPath)).mtime;
|
|
298
|
-
const remoteMtime = remoteFileExists
|
|
336
|
+
const remoteMtime = remoteFileExists
|
|
337
|
+
? (await fs.stat(file.repoPath)).mtime
|
|
338
|
+
: new Date(0);
|
|
299
339
|
const localChanged = !lastSync || localMtime > lastSync;
|
|
300
340
|
const remoteChanged = !lastSync || remoteMtime > lastSync;
|
|
301
341
|
if (localChanged && !remoteChanged) {
|
|
302
342
|
// Only local changed → push
|
|
303
343
|
log.info(`${file.label}: ${color.green('pushing')} (local is newer)`);
|
|
304
|
-
|
|
305
|
-
|
|
344
|
+
if (file.label === 'settings.json') {
|
|
345
|
+
await prepareSettingsForPush(file.localPath, file.repoPath);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
await fs.ensureDir(path.dirname(file.repoPath));
|
|
349
|
+
await fs.copy(file.localPath, file.repoPath, { overwrite: true });
|
|
350
|
+
}
|
|
306
351
|
anyChanges = true;
|
|
307
352
|
}
|
|
308
353
|
else if (remoteChanged && !localChanged) {
|
|
@@ -321,26 +366,31 @@ export async function runSync(opts = {}) {
|
|
|
321
366
|
if (silent) {
|
|
322
367
|
// Daemon can't prompt — local wins, will be pushed
|
|
323
368
|
log.warn(`${file.label}: conflict detected in unattended mode — keeping local.`);
|
|
324
|
-
|
|
325
|
-
|
|
369
|
+
if (file.label === 'settings.json') {
|
|
370
|
+
await prepareSettingsForPush(file.localPath, file.repoPath);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
await fs.ensureDir(path.dirname(file.repoPath));
|
|
374
|
+
await fs.copy(file.localPath, file.repoPath, { overwrite: true });
|
|
375
|
+
}
|
|
326
376
|
anyChanges = true;
|
|
327
377
|
}
|
|
328
378
|
else {
|
|
329
|
-
p.log.warn(color.yellow(
|
|
379
|
+
p.log.warn(color.yellow(`conflict between local and remote ${file.label}`));
|
|
330
380
|
const choice = await p.select({
|
|
331
381
|
message: `Which version of ${color.bold(file.label)} should win?`,
|
|
332
382
|
options: [
|
|
333
383
|
{
|
|
334
384
|
value: 'local',
|
|
335
385
|
label: 'Keep local',
|
|
336
|
-
hint: `modified ${localMtime.toLocaleString()}
|
|
386
|
+
hint: `modified ${localMtime.toLocaleString()}`,
|
|
337
387
|
},
|
|
338
388
|
{
|
|
339
389
|
value: 'remote',
|
|
340
390
|
label: 'Use remote',
|
|
341
|
-
hint: `modified ${remoteMtime.toLocaleString()}
|
|
342
|
-
}
|
|
343
|
-
]
|
|
391
|
+
hint: `modified ${remoteMtime.toLocaleString()}`,
|
|
392
|
+
},
|
|
393
|
+
],
|
|
344
394
|
});
|
|
345
395
|
if (p.isCancel(choice)) {
|
|
346
396
|
p.cancel('Cancelled.');
|
|
@@ -348,8 +398,13 @@ export async function runSync(opts = {}) {
|
|
|
348
398
|
}
|
|
349
399
|
if (choice === 'local') {
|
|
350
400
|
p.log.info(`${file.label}: ${color.green('keeping local, will push')}`);
|
|
351
|
-
|
|
352
|
-
|
|
401
|
+
if (file.label === 'settings.json') {
|
|
402
|
+
await prepareSettingsForPush(file.localPath, file.repoPath);
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
await fs.ensureDir(path.dirname(file.repoPath));
|
|
406
|
+
await fs.copy(file.localPath, file.repoPath, { overwrite: true });
|
|
407
|
+
}
|
|
353
408
|
anyChanges = true;
|
|
354
409
|
}
|
|
355
410
|
else {
|
|
@@ -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 %>);
|