@sayknow-cli/utils 0.3.6 → 0.3.8
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/types/abortable.d.ts +27 -0
- package/dist/types/async.d.ts +6 -0
- package/dist/types/cli.d.ts +126 -0
- package/dist/types/color.d.ts +82 -0
- package/dist/types/dirs.d.ts +163 -0
- package/dist/types/env.d.ts +68 -0
- package/dist/types/fetch-retry.d.ts +80 -0
- package/dist/types/format.d.ts +37 -0
- package/dist/types/frontmatter.d.ts +25 -0
- package/dist/types/fs-error.d.ts +31 -0
- package/dist/types/glob.d.ts +28 -0
- package/dist/types/hook-fetch.d.ts +16 -0
- package/dist/types/index.d.ts +30 -0
- package/dist/types/json.d.ts +4 -0
- package/dist/types/logger.d.ts +66 -0
- package/dist/types/mermaid-ascii.d.ts +11 -0
- package/dist/types/mime.d.ts +29 -0
- package/dist/types/peek-file.d.ts +9 -0
- package/dist/types/postmortem.d.ts +29 -0
- package/dist/types/procmgr.d.ts +35 -0
- package/dist/types/prompt.d.ts +18 -0
- package/dist/types/ptree.d.ts +108 -0
- package/dist/types/ring.d.ts +93 -0
- package/dist/types/safe-stderr.d.ts +1 -0
- package/dist/types/sanitize-text.d.ts +14 -0
- package/dist/types/snowflake.d.ts +25 -0
- package/dist/types/spawn-env.d.ts +4 -0
- package/dist/types/stream.d.ts +68 -0
- package/dist/types/tab-spacing.d.ts +12 -0
- package/dist/types/temp.d.ts +14 -0
- package/dist/types/type-guards.d.ts +3 -0
- package/dist/types/which.d.ts +37 -0
- package/package.json +8 -7
- package/src/cli.ts +51 -13
- package/src/env.ts +5 -2
- package/src/format.ts +16 -5
- package/src/frontmatter.ts +14 -2
- package/src/glob.ts +29 -22
- package/src/postmortem.ts +4 -1
- package/src/tab-spacing.ts +16 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache policy for which lookups.
|
|
3
|
+
*/
|
|
4
|
+
export declare const enum WhichCachePolicy {
|
|
5
|
+
/**
|
|
6
|
+
* Use cached result if available.
|
|
7
|
+
*/
|
|
8
|
+
Cached = 0,
|
|
9
|
+
/**
|
|
10
|
+
* Bypass cache and perform a new lookup.
|
|
11
|
+
*/
|
|
12
|
+
Bypass = 1,
|
|
13
|
+
/**
|
|
14
|
+
* Always update cache.
|
|
15
|
+
*/
|
|
16
|
+
Fresh = 2,
|
|
17
|
+
/**
|
|
18
|
+
* Read-only, serves from cache if present, but doesn't write.
|
|
19
|
+
*/
|
|
20
|
+
ReadOnly = 3
|
|
21
|
+
}
|
|
22
|
+
export interface WhichOptions extends Bun.WhichOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Cache policy for the lookup.
|
|
25
|
+
* Defaults to `WhichCachePolicy.Fresh`.
|
|
26
|
+
*/
|
|
27
|
+
cache?: WhichCachePolicy;
|
|
28
|
+
}
|
|
29
|
+
export declare const whichFresh: typeof Bun.which;
|
|
30
|
+
/**
|
|
31
|
+
* Locate binary on PATH (with flexible caching).
|
|
32
|
+
*
|
|
33
|
+
* @param command - Binary name to resolve
|
|
34
|
+
* @param options - Bun.WhichOptions plus `cache` control
|
|
35
|
+
* @returns Filesystem path if found, else null
|
|
36
|
+
*/
|
|
37
|
+
export declare function $which(command: string, options?: WhichOptions): string | null;
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@sayknow-cli/utils",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.8",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
|
-
"homepage": "https://
|
|
6
|
+
"homepage": "https://sayknow-cli.com",
|
|
7
7
|
"author": "jaybeyond",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"repository": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"streams"
|
|
22
22
|
],
|
|
23
23
|
"main": "./src/index.ts",
|
|
24
|
-
"types": "./
|
|
24
|
+
"types": "./dist/types/index.d.ts",
|
|
25
25
|
"scripts": {
|
|
26
26
|
"check": "biome check . && bun run check:types",
|
|
27
27
|
"check:types": "tsgo -p tsconfig.json --noEmit",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@sayknow-cli/natives": "0.3.
|
|
34
|
+
"@sayknow-cli/natives": "0.3.8",
|
|
35
35
|
"beautiful-mermaid": "^1.1.3",
|
|
36
36
|
"handlebars": "^4.7.9",
|
|
37
37
|
"winston": "^3.19.0",
|
|
@@ -44,15 +44,16 @@
|
|
|
44
44
|
"bun": ">=1.3.14"
|
|
45
45
|
},
|
|
46
46
|
"files": [
|
|
47
|
-
"src"
|
|
47
|
+
"src",
|
|
48
|
+
"dist/types"
|
|
48
49
|
],
|
|
49
50
|
"exports": {
|
|
50
51
|
".": {
|
|
51
|
-
"types": "./
|
|
52
|
+
"types": "./dist/types/index.d.ts",
|
|
52
53
|
"import": "./src/index.ts"
|
|
53
54
|
},
|
|
54
55
|
"./*": {
|
|
55
|
-
"types": "./
|
|
56
|
+
"types": "./dist/types/*.d.ts",
|
|
56
57
|
"import": "./src/*.ts"
|
|
57
58
|
},
|
|
58
59
|
"./*.js": "./src/*.ts"
|
package/src/cli.ts
CHANGED
|
@@ -68,6 +68,18 @@ export const Args = {
|
|
|
68
68
|
},
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Thrown when CLI argument/flag parsing or validation fails (unknown flag,
|
|
73
|
+
* bad option value, missing required arg, etc.). `run()` catches this to print
|
|
74
|
+
* the message and render usage instead of crashing as an uncaught exception.
|
|
75
|
+
*/
|
|
76
|
+
export class CliParseError extends Error {
|
|
77
|
+
constructor(message: string) {
|
|
78
|
+
super(message);
|
|
79
|
+
this.name = "CliParseError";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
71
83
|
// ---------------------------------------------------------------------------
|
|
72
84
|
// Parse result types — mirrors oclif's typed output from this.parse()
|
|
73
85
|
// ---------------------------------------------------------------------------
|
|
@@ -174,12 +186,22 @@ export abstract class Command {
|
|
|
174
186
|
|
|
175
187
|
// strict=false when command declares args (positionals must pass through)
|
|
176
188
|
// or when the command itself opts out
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
189
|
+
let rawValues: Record<string, string | boolean | Array<string | boolean> | undefined>;
|
|
190
|
+
let positionals: string[];
|
|
191
|
+
try {
|
|
192
|
+
const parsed = nodeParseArgs({
|
|
193
|
+
args: this.argv,
|
|
194
|
+
options,
|
|
195
|
+
allowPositionals: true,
|
|
196
|
+
strict,
|
|
197
|
+
});
|
|
198
|
+
rawValues = parsed.values;
|
|
199
|
+
positionals = parsed.positionals;
|
|
200
|
+
} catch (err) {
|
|
201
|
+
// node:util parseArgs throws on unknown flags / malformed input — surface
|
|
202
|
+
// it as a CliParseError so run() renders usage instead of crashing.
|
|
203
|
+
throw new CliParseError(err instanceof Error ? err.message : String(err));
|
|
204
|
+
}
|
|
183
205
|
|
|
184
206
|
// Convert raw values to proper types and validate
|
|
185
207
|
const flags: Record<string, unknown> = {};
|
|
@@ -191,7 +213,7 @@ export abstract class Command {
|
|
|
191
213
|
} else {
|
|
192
214
|
const n = Number.parseInt(raw as string, 10);
|
|
193
215
|
if (Number.isNaN(n)) {
|
|
194
|
-
throw new
|
|
216
|
+
throw new CliParseError(`Expected integer for --${name}, got "${raw}"`);
|
|
195
217
|
}
|
|
196
218
|
flags[name] = n;
|
|
197
219
|
}
|
|
@@ -204,14 +226,16 @@ export abstract class Command {
|
|
|
204
226
|
// Validate options constraint
|
|
205
227
|
if (val !== undefined && desc.options && !Array.isArray(val)) {
|
|
206
228
|
if (!desc.options.includes(val as string)) {
|
|
207
|
-
throw new
|
|
229
|
+
throw new CliParseError(
|
|
230
|
+
`Expected --${name} to be one of: ${[...desc.options].join(", ")}; got "${val}"`,
|
|
231
|
+
);
|
|
208
232
|
}
|
|
209
233
|
}
|
|
210
234
|
flags[name] = val;
|
|
211
235
|
}
|
|
212
236
|
// Validate required
|
|
213
237
|
if (desc.required && flags[name] === undefined) {
|
|
214
|
-
throw new
|
|
238
|
+
throw new CliParseError(`Missing required flag: --${name}`);
|
|
215
239
|
}
|
|
216
240
|
}
|
|
217
241
|
|
|
@@ -230,13 +254,15 @@ export abstract class Command {
|
|
|
230
254
|
}
|
|
231
255
|
// Validate required
|
|
232
256
|
if (desc.required && args[argName] === undefined) {
|
|
233
|
-
throw new
|
|
257
|
+
throw new CliParseError(`Missing required argument: ${argName}`);
|
|
234
258
|
}
|
|
235
259
|
// Validate options constraint
|
|
236
260
|
const argVal = args[argName];
|
|
237
261
|
if (argVal !== undefined && desc.options && typeof argVal === "string") {
|
|
238
262
|
if (!desc.options.includes(argVal)) {
|
|
239
|
-
throw new
|
|
263
|
+
throw new CliParseError(
|
|
264
|
+
`Expected ${argName} to be one of: ${[...desc.options].join(", ")}; got "${argVal}"`,
|
|
265
|
+
);
|
|
240
266
|
}
|
|
241
267
|
}
|
|
242
268
|
}
|
|
@@ -407,7 +433,7 @@ export async function run(opts: RunOptions): Promise<void> {
|
|
|
407
433
|
const instance = new Cmd(commandArgv, config);
|
|
408
434
|
await instance.run();
|
|
409
435
|
} else {
|
|
410
|
-
const config =
|
|
436
|
+
const config: CliConfig = { bin, version, commands: new Map([[entry.name, Cmd]]) };
|
|
411
437
|
renderCommandHelp(bin, entry.name, config.commands.get(entry.name) ?? Cmd);
|
|
412
438
|
}
|
|
413
439
|
return;
|
|
@@ -425,7 +451,19 @@ export async function run(opts: RunOptions): Promise<void> {
|
|
|
425
451
|
const Cmd = await entry.load();
|
|
426
452
|
const config: CliConfig = { bin, version, commands: new Map([[entry.name, Cmd]]) };
|
|
427
453
|
const instance = new Cmd(commandArgv, config);
|
|
428
|
-
|
|
454
|
+
try {
|
|
455
|
+
await instance.run();
|
|
456
|
+
} catch (err) {
|
|
457
|
+
if (err instanceof CliParseError) {
|
|
458
|
+
// Invalid args/flags for a real command: print the problem + usage and
|
|
459
|
+
// exit with a usage error, instead of crashing as an uncaught exception.
|
|
460
|
+
process.stderr.write(`${err.message}\n\n`);
|
|
461
|
+
renderCommandHelp(bin, entry.name, Cmd);
|
|
462
|
+
process.exitCode = 2;
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
throw err;
|
|
466
|
+
}
|
|
429
467
|
}
|
|
430
468
|
|
|
431
469
|
/** Resolve all command loaders for help/alias display. */
|
package/src/env.ts
CHANGED
|
@@ -272,7 +272,10 @@ export function isCompiledBinary(): boolean {
|
|
|
272
272
|
|
|
273
273
|
const TRUTHY: Dict<boolean> = { "1": true, Y: true, TRUE: true, YES: true, ON: true };
|
|
274
274
|
export function $flag(name: string, def: boolean = false): boolean {
|
|
275
|
-
const value = $env[name];
|
|
275
|
+
const value = $env[name]?.trim();
|
|
276
276
|
if (!value) return def;
|
|
277
|
-
|
|
277
|
+
// Boolean-like env values are documented as case-insensitive (`1`/`true`/`yes`/`on`),
|
|
278
|
+
// so normalize before the lookup — otherwise `FOO=true` (the common lowercase spelling)
|
|
279
|
+
// would silently read as false while only `FOO=TRUE`/`FOO=1` worked.
|
|
280
|
+
return TRUTHY[value.toUpperCase()] === true;
|
|
278
281
|
}
|
package/src/format.ts
CHANGED
|
@@ -33,9 +33,9 @@ export function formatDuration(ms: number): string {
|
|
|
33
33
|
export function formatNumber(n: number): string {
|
|
34
34
|
if (n < 1_000) return n.toString();
|
|
35
35
|
if (n < 10_000) return `${trim1(n / 1_000)}K`;
|
|
36
|
-
if (n < 1_000_000) return `${
|
|
36
|
+
if (n < 1_000_000) return `${roundBelow(n / 1_000, 1_000)}K`;
|
|
37
37
|
if (n < 10_000_000) return `${trim1(n / 1_000_000)}M`;
|
|
38
|
-
if (n < 1_000_000_000) return `${
|
|
38
|
+
if (n < 1_000_000_000) return `${roundBelow(n / 1_000_000, 1_000)}M`;
|
|
39
39
|
if (n < 10_000_000_000) return `${trim1(n / 1_000_000_000)}B`;
|
|
40
40
|
return `${Math.round(n / 1_000_000_000)}B`;
|
|
41
41
|
}
|
|
@@ -46,15 +46,26 @@ function trim1(n: number): string {
|
|
|
46
46
|
return s.endsWith(".0") ? s.slice(0, -2) : s;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** Round to an integer without crossing the next compact suffix boundary. */
|
|
50
|
+
function roundBelow(n: number, nextUnit: number): number {
|
|
51
|
+
return Math.min(Math.round(n), nextUnit - 1);
|
|
52
|
+
}
|
|
53
|
+
|
|
49
54
|
/**
|
|
50
55
|
* Format a byte count to a human-readable string.
|
|
51
56
|
* Examples: "512B", "1.5KB", "2.3MB", "1.2GB"
|
|
52
57
|
*/
|
|
53
58
|
export function formatBytes(bytes: number): string {
|
|
54
59
|
if (bytes < 1024) return `${bytes}B`;
|
|
55
|
-
if (bytes < 1024 * 1024) return `${(bytes
|
|
56
|
-
if (bytes < 1024 * 1024 * 1024) return `${(bytes
|
|
57
|
-
return `${(bytes
|
|
60
|
+
if (bytes < 1024 * 1024) return `${formatByteUnit(bytes, 1024)}KB`;
|
|
61
|
+
if (bytes < 1024 * 1024 * 1024) return `${formatByteUnit(bytes, 1024 * 1024)}MB`;
|
|
62
|
+
return `${formatByteUnit(bytes, 1024 * 1024 * 1024)}GB`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Format bytes to 1 decimal without rounding up into the next byte unit. */
|
|
66
|
+
function formatByteUnit(bytes: number, unit: number): string {
|
|
67
|
+
const tenths = Math.min(Math.round((bytes / unit) * 10), 1024 * 10 - 1);
|
|
68
|
+
return (tenths / 10).toFixed(1);
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
/**
|
package/src/frontmatter.ts
CHANGED
|
@@ -17,15 +17,27 @@ function stripLooseScalarTrailingCommas(metadata: string): string {
|
|
|
17
17
|
.join("\n");
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function asFrontmatterRecord(parsed: unknown): Record<string, unknown> | null {
|
|
21
|
+
if (parsed === null) return null;
|
|
22
|
+
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
23
|
+
throw new Error("YAML frontmatter root must be an object");
|
|
24
|
+
}
|
|
25
|
+
const prototype = Object.getPrototypeOf(parsed);
|
|
26
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
27
|
+
throw new Error("YAML frontmatter root must be an object");
|
|
28
|
+
}
|
|
29
|
+
return parsed as Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
function parseYamlMetadata(metadata: string): Record<string, unknown> | null {
|
|
21
33
|
const normalized = metadata.replaceAll("\t", " ");
|
|
22
34
|
try {
|
|
23
|
-
return YAML.parse(normalized)
|
|
35
|
+
return asFrontmatterRecord(YAML.parse(normalized));
|
|
24
36
|
} catch (strictError) {
|
|
25
37
|
const loose = stripLooseScalarTrailingCommas(normalized);
|
|
26
38
|
if (loose === normalized) throw strictError;
|
|
27
39
|
try {
|
|
28
|
-
return YAML.parse(loose)
|
|
40
|
+
return asFrontmatterRecord(YAML.parse(loose));
|
|
29
41
|
} catch {
|
|
30
42
|
throw strictError;
|
|
31
43
|
}
|
package/src/glob.ts
CHANGED
|
@@ -70,20 +70,25 @@ function parseGitignorePatterns(content: string, gitignoreDir: string, baseDir:
|
|
|
70
70
|
} else {
|
|
71
71
|
patterns.push(pattern);
|
|
72
72
|
}
|
|
73
|
+
} else if (pattern.includes("/") && !pattern.startsWith("**/")) {
|
|
74
|
+
// Separator in the middle: git anchors these to the .gitignore's
|
|
75
|
+
// directory, same as rooted patterns
|
|
76
|
+
const absolutePattern = path.join(gitignoreDir, pattern);
|
|
77
|
+
const relativeToBase = path.relative(baseDir, absolutePattern);
|
|
78
|
+
if (relativeToBase.startsWith("..")) {
|
|
79
|
+
// Pattern is outside the search directory, skip
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
pattern = relativeToBase.replace(/\\/g, "/");
|
|
83
|
+
patterns.push(pattern);
|
|
84
|
+
if (isDirectoryOnly) {
|
|
85
|
+
patterns.push(`${pattern}/**`);
|
|
86
|
+
}
|
|
73
87
|
} else {
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
patterns.push(`**/${pattern}
|
|
78
|
-
if (isDirectoryOnly) {
|
|
79
|
-
patterns.push(`**/${pattern}/**`);
|
|
80
|
-
}
|
|
81
|
-
} else {
|
|
82
|
-
// No slash: match file/dir name anywhere
|
|
83
|
-
patterns.push(`**/${pattern}`);
|
|
84
|
-
if (isDirectoryOnly) {
|
|
85
|
-
patterns.push(`**/${pattern}/**`);
|
|
86
|
-
}
|
|
88
|
+
// No middle separator: match file/dir name anywhere in the tree
|
|
89
|
+
patterns.push(`**/${pattern}`);
|
|
90
|
+
if (isDirectoryOnly) {
|
|
91
|
+
patterns.push(`**/${pattern}/**`);
|
|
87
92
|
}
|
|
88
93
|
}
|
|
89
94
|
}
|
|
@@ -145,8 +150,13 @@ export async function globPaths(patterns: string | string[], options: GlobPathsO
|
|
|
145
150
|
effectiveExclude = [...effectiveExclude, ...gitignorePatterns];
|
|
146
151
|
}
|
|
147
152
|
|
|
153
|
+
const excludeGlobs = effectiveExclude.map(pattern => new Glob(pattern));
|
|
154
|
+
|
|
148
155
|
const base = cwd ?? getProjectDir();
|
|
149
156
|
const allResults: string[] = [];
|
|
157
|
+
// Overlapping patterns (e.g. `["**/*.ts", "src/*.ts"]`) can both match the same
|
|
158
|
+
// file; dedupe so a path is returned at most once regardless of pattern overlap.
|
|
159
|
+
const seen = new Set<string>();
|
|
150
160
|
|
|
151
161
|
// Combine timeout and abort signals
|
|
152
162
|
const timeoutSignal = timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined;
|
|
@@ -171,17 +181,14 @@ export async function globPaths(patterns: string | string[], options: GlobPathsO
|
|
|
171
181
|
|
|
172
182
|
// Check exclusion patterns
|
|
173
183
|
const normalized = entry.replace(/\\/g, "/");
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const excludeGlob = new Glob(excludePattern);
|
|
177
|
-
if (excludeGlob.match(normalized)) {
|
|
178
|
-
excluded = true;
|
|
179
|
-
break;
|
|
180
|
-
}
|
|
184
|
+
if (excludeGlobs.some(excludeGlob => excludeGlob.match(normalized))) {
|
|
185
|
+
continue;
|
|
181
186
|
}
|
|
182
|
-
if (
|
|
183
|
-
|
|
187
|
+
if (seen.has(normalized)) {
|
|
188
|
+
continue;
|
|
184
189
|
}
|
|
190
|
+
seen.add(normalized);
|
|
191
|
+
allResults.push(normalized);
|
|
185
192
|
}
|
|
186
193
|
}
|
|
187
194
|
|
package/src/postmortem.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import inspector from "node:inspector";
|
|
9
9
|
import { isMainThread } from "node:worker_threads";
|
|
10
|
-
import
|
|
10
|
+
import * as logger from "./logger";
|
|
11
11
|
import { safeStderrWrite } from "./safe-stderr";
|
|
12
12
|
|
|
13
13
|
// Cleanup reasons, in order of priority/meaning.
|
|
@@ -39,6 +39,9 @@ function runCleanup(reason: Reason): Promise<void> {
|
|
|
39
39
|
cleanupStage = "running";
|
|
40
40
|
break;
|
|
41
41
|
case "running":
|
|
42
|
+
if (reason === Reason.EXIT) {
|
|
43
|
+
return Promise.resolve();
|
|
44
|
+
}
|
|
42
45
|
logger.error("Cleanup invoked recursively", { stack: new Error().stack });
|
|
43
46
|
return Promise.resolve();
|
|
44
47
|
case "complete":
|
package/src/tab-spacing.ts
CHANGED
|
@@ -13,6 +13,9 @@ export const DEFAULT_TAB_WIDTH = 3;
|
|
|
13
13
|
const EDITORCONFIG_NAME = ".editorconfig";
|
|
14
14
|
|
|
15
15
|
let defaultTabWidth = DEFAULT_TAB_WIDTH;
|
|
16
|
+
type TabWidthChangeListener = (width: number) => void;
|
|
17
|
+
|
|
18
|
+
const tabWidthChangeListeners = new Set<TabWidthChangeListener>();
|
|
16
19
|
|
|
17
20
|
const editorConfigCache = new Map<string, ParsedEditorConfig>();
|
|
18
21
|
const editorConfigChainCache = new Map<string, ChainEntry[]>();
|
|
@@ -278,13 +281,25 @@ function resolveEditorConfigTabWidth(match: EditorConfigMatch | undefined, fallb
|
|
|
278
281
|
|
|
279
282
|
return undefined;
|
|
280
283
|
}
|
|
284
|
+
export function onDefaultTabWidthChange(listener: TabWidthChangeListener): () => void {
|
|
285
|
+
tabWidthChangeListeners.add(listener);
|
|
286
|
+
return () => {
|
|
287
|
+
tabWidthChangeListeners.delete(listener);
|
|
288
|
+
};
|
|
289
|
+
}
|
|
281
290
|
|
|
282
291
|
export function getDefaultTabWidth(): number {
|
|
283
292
|
return defaultTabWidth;
|
|
284
293
|
}
|
|
285
294
|
|
|
286
295
|
export function setDefaultTabWidth(width: number): void {
|
|
287
|
-
|
|
296
|
+
const next = clampTabWidth(width);
|
|
297
|
+
if (defaultTabWidth === next) return;
|
|
298
|
+
defaultTabWidth = next;
|
|
299
|
+
indentationCache.clear();
|
|
300
|
+
for (const listener of tabWidthChangeListeners) {
|
|
301
|
+
listener(next);
|
|
302
|
+
}
|
|
288
303
|
}
|
|
289
304
|
|
|
290
305
|
/**
|