a-calc 3.0.0-beta.20260130.1 → 3.0.0-beta.20260201.1

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.
@@ -0,0 +1,294 @@
1
+ "use strict";
2
+ const constants_1 = require("./constants");
3
+ const utils_1 = require("./utils");
4
+ const config_extractor_1 = require("./config_extractor");
5
+ const variable_extractor_1 = require("./variable_extractor");
6
+ function init(modules) {
7
+ const ts_module = modules.typescript;
8
+ // 缓存提取的配置
9
+ let cached_config = null;
10
+ function create(info) {
11
+ const logger = info.project.projectService.logger;
12
+ logger.info('a-calc-ts-plugin: initializing');
13
+ const ls = info.languageService;
14
+ // 获取或更新配置
15
+ function get_config() {
16
+ const program = ls.getProgram();
17
+ // 简单的缓存策略:每次都重新提取(后续可优化)
18
+ if (program) {
19
+ cached_config = (0, config_extractor_1.extract_config_from_project)(ts_module, program, logger);
20
+ }
21
+ return cached_config || { units: [], shortcut_prefix: '!u' };
22
+ }
23
+ const original_get_completions = ls.getCompletionsAtPosition.bind(ls);
24
+ ls.getCompletionsAtPosition = (file_name, position, options) => {
25
+ const completions = get_acalc_completions(ts_module, ls, file_name, position, logger, get_config);
26
+ if (completions)
27
+ return completions;
28
+ return original_get_completions(file_name, position, options);
29
+ };
30
+ return ls;
31
+ }
32
+ return { create };
33
+ }
34
+ /**
35
+ * 获取 a-calc 表达式的补全
36
+ */
37
+ function get_acalc_completions(ts_module, ls, file_name, position, logger, get_config) {
38
+ const program = ls.getProgram();
39
+ if (!program)
40
+ return undefined;
41
+ const source_file = program.getSourceFile(file_name);
42
+ if (!source_file)
43
+ return undefined;
44
+ const type_checker = program.getTypeChecker();
45
+ const context = (0, utils_1.is_in_acalc_string)(ts_module, source_file, position, type_checker, logger);
46
+ logger.info(`a-calc-ts-plugin: in_string=${context.in_string}, offset=${context.offset_in_string}, has_second_arg=${!!context.second_arg}`);
47
+ if (!context.in_string)
48
+ return undefined;
49
+ const text_before = (0, utils_1.get_text_before_cursor)(context.string_content, context.offset_in_string);
50
+ logger.info(`a-calc-ts-plugin: text_before="${text_before}"`);
51
+ const trigger = (0, utils_1.detect_trigger)(text_before);
52
+ logger.info(`a-calc-ts-plugin: trigger=${JSON.stringify(trigger)}`);
53
+ // 在 a-calc 字符串内,如果没有特定触发,返回空补全列表
54
+ // 避免原始 TypeScript 补全显示奇怪的内容
55
+ if (!trigger) {
56
+ return {
57
+ isGlobalCompletion: false,
58
+ isMemberCompletion: false,
59
+ isNewIdentifierLocation: false,
60
+ entries: [],
61
+ };
62
+ }
63
+ if (trigger.type === 'preset_name') {
64
+ return create_preset_name_completions(ts_module, trigger.prefix);
65
+ }
66
+ if (trigger.type === 'preset_value') {
67
+ return create_preset_value_completions(ts_module, trigger.preset, trigger.prefix, get_config());
68
+ }
69
+ if (trigger.type === 'shortcut') {
70
+ return create_shortcut_completions(ts_module, trigger.prefix, get_config());
71
+ }
72
+ if (trigger.type === 'format_syntax') {
73
+ const result = create_format_syntax_completions(ts_module, position);
74
+ logger.info(`a-calc-ts-plugin: format_syntax entries count=${result.entries.length}`);
75
+ return result;
76
+ }
77
+ if (trigger.type === 'format_variable') {
78
+ logger.info(`a-calc-ts-plugin: format_variable trigger, prefix="${trigger.prefix}"`);
79
+ const program = ls.getProgram();
80
+ if (program && context.second_arg) {
81
+ const type_checker = program.getTypeChecker();
82
+ const variables = (0, variable_extractor_1.extract_variables_from_arg)(ts_module, context.second_arg, type_checker, logger);
83
+ logger.info(`a-calc-ts-plugin: extracted ${variables.length} variables for format area`);
84
+ if (variables.length > 0) {
85
+ const result = create_variable_completions(ts_module, variables, trigger.prefix, position);
86
+ logger.info(`a-calc-ts-plugin: returning ${result.entries.length} format variable completions`);
87
+ return result;
88
+ }
89
+ }
90
+ return undefined;
91
+ }
92
+ if (trigger.type === 'variable') {
93
+ logger.info(`a-calc-ts-plugin: variable trigger, prefix="${trigger.prefix}"`);
94
+ logger.info(`a-calc-ts-plugin: context.second_arg exists=${!!context.second_arg}`);
95
+ const program = ls.getProgram();
96
+ if (program && context.second_arg) {
97
+ const type_checker = program.getTypeChecker();
98
+ const variables = (0, variable_extractor_1.extract_variables_from_arg)(ts_module, context.second_arg, type_checker, logger);
99
+ logger.info(`a-calc-ts-plugin: extracted ${variables.length} variables`);
100
+ if (variables.length > 0) {
101
+ const result = create_variable_completions(ts_module, variables, trigger.prefix, position);
102
+ logger.info(`a-calc-ts-plugin: returning ${result.entries.length} variable completions`);
103
+ return result;
104
+ }
105
+ }
106
+ else {
107
+ logger.info(`a-calc-ts-plugin: no program or second_arg, skipping variable completion`);
108
+ }
109
+ return undefined;
110
+ }
111
+ return undefined;
112
+ }
113
+ /**
114
+ * 创建预设名补全 (! 后)
115
+ */
116
+ function create_preset_name_completions(ts_module, prefix) {
117
+ const entries = constants_1.ACALC_PRESETS
118
+ .filter(p => p.name.startsWith(prefix))
119
+ .map((p, i) => ({
120
+ name: p.name,
121
+ kind: ts_module.ScriptElementKind.keyword,
122
+ kindModifiers: '',
123
+ sortText: String(i).padStart(2, '0'),
124
+ labelDetails: { description: p.description },
125
+ }));
126
+ return {
127
+ isGlobalCompletion: false,
128
+ isMemberCompletion: false,
129
+ isNewIdentifierLocation: false,
130
+ entries,
131
+ };
132
+ }
133
+ /**
134
+ * 创建预设值补全 (!t: 后)
135
+ */
136
+ function create_preset_value_completions(ts_module, preset, prefix, config) {
137
+ // 千分位预设
138
+ if (preset === 't') {
139
+ return create_completions_from_list(ts_module, constants_1.THOUSANDS_PRESETS, prefix);
140
+ }
141
+ // 紧凑格式预设
142
+ if (preset === 'c') {
143
+ return create_completions_from_list(ts_module, constants_1.COMPACT_PRESETS, prefix);
144
+ }
145
+ // 单位相关预设 (u, ua, ub, um, uh)
146
+ if (['u', 'ua', 'ub', 'um', 'uh'].includes(preset)) {
147
+ return create_unit_completions(ts_module, config.units, prefix);
148
+ }
149
+ return undefined;
150
+ }
151
+ /**
152
+ * 创建快捷语法补全 (: 后)
153
+ */
154
+ function create_shortcut_completions(ts_module, prefix, config) {
155
+ const shortcut_prefix = config.shortcut_prefix;
156
+ // 根据 shortcut_prefix 决定补全内容
157
+ if (shortcut_prefix === '!t') {
158
+ return create_completions_from_list(ts_module, constants_1.THOUSANDS_PRESETS, prefix);
159
+ }
160
+ if (shortcut_prefix === '!c') {
161
+ return create_completions_from_list(ts_module, constants_1.COMPACT_PRESETS, prefix);
162
+ }
163
+ // 默认 !u 或其他单位相关前缀
164
+ return create_unit_completions(ts_module, config.units, prefix);
165
+ }
166
+ /**
167
+ * 从列表创建补全
168
+ */
169
+ function create_completions_from_list(ts_module, list, prefix) {
170
+ const entries = list
171
+ .filter(item => item.name.startsWith(prefix))
172
+ .map((item, i) => ({
173
+ name: item.name,
174
+ kind: ts_module.ScriptElementKind.string,
175
+ kindModifiers: '',
176
+ sortText: String(i).padStart(2, '0'),
177
+ labelDetails: { description: item.description },
178
+ }));
179
+ return {
180
+ isGlobalCompletion: false,
181
+ isMemberCompletion: false,
182
+ isNewIdentifierLocation: false,
183
+ entries,
184
+ };
185
+ }
186
+ /**
187
+ * 创建单位补全
188
+ */
189
+ function create_unit_completions(ts_module, units, prefix) {
190
+ const entries = units
191
+ .filter(u => u.startsWith(prefix))
192
+ .map((u, i) => ({
193
+ name: u,
194
+ kind: ts_module.ScriptElementKind.string,
195
+ kindModifiers: '',
196
+ sortText: String(i).padStart(2, '0'),
197
+ }));
198
+ return {
199
+ isGlobalCompletion: false,
200
+ isMemberCompletion: false,
201
+ isNewIdentifierLocation: false,
202
+ entries,
203
+ };
204
+ }
205
+ /**
206
+ * 创建格式化语法补全(| 后)
207
+ */
208
+ function create_format_syntax_completions(ts_module, position) {
209
+ const entries = constants_1.FORMAT_SYNTAX.map((item, i) => ({
210
+ name: item.name,
211
+ insertText: item.insert_text,
212
+ kind: ts_module.ScriptElementKind.keyword,
213
+ kindModifiers: '',
214
+ sortText: String(i).padStart(2, '0'),
215
+ labelDetails: {
216
+ description: `${item.description} (${item.example})`,
217
+ },
218
+ replacementSpan: {
219
+ start: position,
220
+ length: 0,
221
+ },
222
+ }));
223
+ return {
224
+ isGlobalCompletion: false,
225
+ isMemberCompletion: false,
226
+ isNewIdentifierLocation: true,
227
+ entries,
228
+ };
229
+ }
230
+ /**
231
+ * 创建变量补全(表达式区域)
232
+ */
233
+ function create_variable_completions(ts_module, variables, prefix, position) {
234
+ // 解析前缀,支持深度路径如 "user.na"
235
+ const prefix_parts = prefix.split('.');
236
+ const parent_path = prefix_parts.slice(0, -1).join('.');
237
+ const current_prefix = prefix_parts[prefix_parts.length - 1] || '';
238
+ // 过滤变量
239
+ let filtered_variables;
240
+ if (parent_path) {
241
+ // 有父路径:只显示该路径下的直接子属性
242
+ filtered_variables = variables.filter(v => {
243
+ // 检查是否是 parent_path 的直接子属性
244
+ if (!v.path.startsWith(parent_path + '.'))
245
+ return false;
246
+ const remaining = v.path.slice(parent_path.length + 1);
247
+ // 只要直接子属性(不包含更深的路径)
248
+ if (remaining.includes('.'))
249
+ return false;
250
+ // 匹配当前前缀
251
+ return remaining.startsWith(current_prefix);
252
+ });
253
+ }
254
+ else {
255
+ // 无父路径:只显示顶层属性
256
+ filtered_variables = variables.filter(v => {
257
+ // 只要顶层属性
258
+ if (v.path.includes('.'))
259
+ return false;
260
+ // 匹配前缀
261
+ return v.path.startsWith(current_prefix);
262
+ });
263
+ }
264
+ // 计算需要替换的文本长度
265
+ const replace_length = current_prefix.length;
266
+ const entries = filtered_variables.map((v, i) => {
267
+ // 获取要显示和插入的名称(只是当前层级的名称)
268
+ const display_name = parent_path
269
+ ? v.path.slice(parent_path.length + 1)
270
+ : v.path;
271
+ return {
272
+ name: display_name,
273
+ kind: v.has_children
274
+ ? ts_module.ScriptElementKind.moduleElement
275
+ : ts_module.ScriptElementKind.memberVariableElement,
276
+ kindModifiers: '',
277
+ sortText: String(i).padStart(3, '0'),
278
+ labelDetails: {
279
+ description: v.type_desc,
280
+ },
281
+ replacementSpan: {
282
+ start: position - replace_length,
283
+ length: replace_length,
284
+ },
285
+ };
286
+ });
287
+ return {
288
+ isGlobalCompletion: false,
289
+ isMemberCompletion: parent_path.length > 0,
290
+ isNewIdentifierLocation: false,
291
+ entries,
292
+ };
293
+ }
294
+ module.exports = init;
@@ -0,0 +1,49 @@
1
+ import type * as ts from 'typescript/lib/tsserverlibrary';
2
+ /**
3
+ * a-calc 字符串上下文信息
4
+ */
5
+ export interface AcalcStringContext {
6
+ in_string: boolean;
7
+ string_content: string;
8
+ offset_in_string: number;
9
+ /** 第二个参数的 AST 节点(用于提取变量) */
10
+ second_arg?: ts.Expression;
11
+ /** 函数名 */
12
+ func_name?: string;
13
+ }
14
+ /**
15
+ * 检测位置是否在 calc/fmt 等函数的字符串参数内
16
+ */
17
+ export declare function is_in_acalc_string(ts_module: typeof ts, source_file: ts.SourceFile, position: number, type_checker?: ts.TypeChecker, logger?: {
18
+ info: (msg: string) => void;
19
+ }): AcalcStringContext;
20
+ /**
21
+ * 获取光标前的文本,用于判断触发条件
22
+ */
23
+ export declare function get_text_before_cursor(content: string, offset: number): string;
24
+ /**
25
+ * 补全触发类型
26
+ */
27
+ export type TriggerType = {
28
+ type: 'preset_name';
29
+ prefix: string;
30
+ } | {
31
+ type: 'preset_value';
32
+ preset: string;
33
+ prefix: string;
34
+ } | {
35
+ type: 'shortcut';
36
+ prefix: string;
37
+ } | {
38
+ type: 'format_syntax';
39
+ } | {
40
+ type: 'variable';
41
+ prefix: string;
42
+ } | {
43
+ type: 'format_variable';
44
+ prefix: string;
45
+ } | null;
46
+ /**
47
+ * 检测补全触发类型
48
+ */
49
+ export declare function detect_trigger(text_before: string): TriggerType;
@@ -0,0 +1,204 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detect_trigger = exports.get_text_before_cursor = exports.is_in_acalc_string = void 0;
4
+ /**
5
+ * 检测位置是否在 calc/fmt 等函数的字符串参数内
6
+ */
7
+ function is_in_acalc_string(ts_module, source_file, position, type_checker, logger) {
8
+ const result = { in_string: false, string_content: '', offset_in_string: 0 };
9
+ // 目标函数名
10
+ const target_functions = ['calc', 'fmt', 'calc_sum', 'calc_wrap', 'calc_space', 'calc_avg', 'calc_max', 'calc_min', 'calc_count'];
11
+ function visit(node) {
12
+ // 检查是否是函数调用
13
+ if (ts_module.isCallExpression(node)) {
14
+ const func_name = get_function_name(ts_module, node);
15
+ if (func_name && target_functions.includes(func_name)) {
16
+ // 验证函数是否来自 a-calc 库
17
+ if (!is_from_acalc(ts_module, node, type_checker, logger)) {
18
+ return ts_module.forEachChild(node, visit) || false;
19
+ }
20
+ // 检查第一个参数是否是字符串
21
+ const first_arg = node.arguments[0];
22
+ if (first_arg && is_string_literal(ts_module, first_arg)) {
23
+ const start = first_arg.getStart(source_file) + 1; // 跳过引号
24
+ const end = first_arg.getEnd() - 1; // 跳过引号
25
+ if (position >= start && position <= end) {
26
+ result.in_string = true;
27
+ result.string_content = get_string_content(ts_module, first_arg);
28
+ result.offset_in_string = position - start;
29
+ result.func_name = func_name;
30
+ // 保存第二个参数
31
+ if (node.arguments.length > 1) {
32
+ result.second_arg = node.arguments[1];
33
+ }
34
+ return true;
35
+ }
36
+ }
37
+ }
38
+ }
39
+ return ts_module.forEachChild(node, visit) || false;
40
+ }
41
+ visit(source_file);
42
+ return result;
43
+ }
44
+ exports.is_in_acalc_string = is_in_acalc_string;
45
+ /**
46
+ * 检查函数是否来自 a-calc 库
47
+ */
48
+ function is_from_acalc(ts_module, node, type_checker, logger) {
49
+ var _a, _b;
50
+ if (!type_checker)
51
+ return true;
52
+ try {
53
+ const expr = node.expression;
54
+ const symbol = type_checker.getSymbolAtLocation(expr);
55
+ if (!symbol) {
56
+ logger === null || logger === void 0 ? void 0 : logger.info('a-calc-ts-plugin: is_from_acalc - no symbol found');
57
+ return false;
58
+ }
59
+ // 获取声明
60
+ const declarations = symbol.getDeclarations();
61
+ if (!declarations || declarations.length === 0) {
62
+ logger === null || logger === void 0 ? void 0 : logger.info('a-calc-ts-plugin: is_from_acalc - no declarations found');
63
+ return false;
64
+ }
65
+ for (const decl of declarations) {
66
+ const source_file = decl.getSourceFile();
67
+ const file_name = source_file.fileName.replace(/\\/g, '/');
68
+ logger === null || logger === void 0 ? void 0 : logger.info(`a-calc-ts-plugin: is_from_acalc - checking file: ${file_name}`);
69
+ // 匹配 node_modules 中的 a-calc 包
70
+ if (file_name.includes('node_modules/a-calc/')) {
71
+ logger === null || logger === void 0 ? void 0 : logger.info('a-calc-ts-plugin: is_from_acalc - matched node_modules/a-calc');
72
+ return true;
73
+ }
74
+ // 检查是否是 ImportSpecifier(从模块导入的)
75
+ if (ts_module.isImportSpecifier(decl)) {
76
+ const import_decl = (_b = (_a = decl.parent) === null || _a === void 0 ? void 0 : _a.parent) === null || _b === void 0 ? void 0 : _b.parent;
77
+ if (import_decl && ts_module.isImportDeclaration(import_decl)) {
78
+ const module_specifier = import_decl.moduleSpecifier;
79
+ if (ts_module.isStringLiteral(module_specifier)) {
80
+ const module_name = module_specifier.text;
81
+ logger === null || logger === void 0 ? void 0 : logger.info(`a-calc-ts-plugin: is_from_acalc - import from: ${module_name}`);
82
+ if (module_name === 'a-calc') {
83
+ logger === null || logger === void 0 ? void 0 : logger.info('a-calc-ts-plugin: is_from_acalc - matched import from a-calc');
84
+ return true;
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
90
+ logger === null || logger === void 0 ? void 0 : logger.info('a-calc-ts-plugin: is_from_acalc - no match found');
91
+ return false;
92
+ }
93
+ catch (e) {
94
+ logger === null || logger === void 0 ? void 0 : logger.info(`a-calc-ts-plugin: is_from_acalc - error: ${e}`);
95
+ return true;
96
+ }
97
+ }
98
+ /**
99
+ * 获取函数调用的函数名
100
+ */
101
+ function get_function_name(ts_module, node) {
102
+ const expr = node.expression;
103
+ // 直接调用: calc(...)
104
+ if (ts_module.isIdentifier(expr)) {
105
+ return expr.text;
106
+ }
107
+ // 属性访问: obj.calc(...)
108
+ if (ts_module.isPropertyAccessExpression(expr)) {
109
+ return expr.name.text;
110
+ }
111
+ return undefined;
112
+ }
113
+ /**
114
+ * 检查节点是否是字符串字面量
115
+ */
116
+ function is_string_literal(ts_module, node) {
117
+ return ts_module.isStringLiteral(node) ||
118
+ ts_module.isNoSubstitutionTemplateLiteral(node);
119
+ }
120
+ /**
121
+ * 获取字符串内容
122
+ */
123
+ function get_string_content(ts_module, node) {
124
+ if (ts_module.isStringLiteral(node)) {
125
+ return node.text;
126
+ }
127
+ if (ts_module.isNoSubstitutionTemplateLiteral(node)) {
128
+ return node.text;
129
+ }
130
+ return '';
131
+ }
132
+ /**
133
+ * 获取光标前的文本,用于判断触发条件
134
+ */
135
+ function get_text_before_cursor(content, offset) {
136
+ return content.slice(0, offset);
137
+ }
138
+ exports.get_text_before_cursor = get_text_before_cursor;
139
+ /**
140
+ * 检测补全触发类型
141
+ */
142
+ function detect_trigger(text_before) {
143
+ // 检测 !xxx: 后的预设值补全 (如 !t:ind, !c:c)
144
+ const value_match = text_before.match(/!(t|c|i|g|u|ua|ub|um|uh):([\w]*)$/);
145
+ if (value_match) {
146
+ return { type: 'preset_value', preset: value_match[1], prefix: value_match[2] };
147
+ }
148
+ // 检测 ! 后的预设名补全 (如 !t, !c)
149
+ const name_match = text_before.match(/!([\w]*)$/);
150
+ if (name_match) {
151
+ return { type: 'preset_name', prefix: name_match[1] };
152
+ }
153
+ // 检测快捷语法 : 后的补全 (需要前面是空格或 |)
154
+ const shortcut_match = text_before.match(/(?:^|[\s|]):([\w]*)$/);
155
+ if (shortcut_match) {
156
+ return { type: 'shortcut', prefix: shortcut_match[1] };
157
+ }
158
+ // 检测是否在格式化区域 (| 后面)
159
+ if (is_in_format_area(text_before)) {
160
+ // 格式化区域内的变量补全
161
+ // 检测 @ 后的变量补全(用于单位、预设等)
162
+ const at_var_match = text_before.match(/@([\w.]*)$/);
163
+ if (at_var_match) {
164
+ return { type: 'format_variable', prefix: at_var_match[1] };
165
+ }
166
+ // 检测 = 后的变量补全(用于小数位数)
167
+ const eq_var_match = text_before.match(/=([\w.]*)$/);
168
+ if (eq_var_match) {
169
+ const prefix = eq_var_match[1];
170
+ // 空前缀或以字母开头时触发变量补全,纯数字是精度值不触发
171
+ if (prefix === '' || /^[a-zA-Z]/.test(prefix)) {
172
+ return { type: 'format_variable', prefix };
173
+ }
174
+ }
175
+ // 检测 > 或 < 后的变量补全(用于范围限制)
176
+ const range_var_match = text_before.match(/[><]([\w.]*)$/);
177
+ if (range_var_match) {
178
+ const prefix = range_var_match[1];
179
+ if (prefix === '' || /^[a-zA-Z]/.test(prefix)) {
180
+ return { type: 'format_variable', prefix };
181
+ }
182
+ }
183
+ // 其他情况显示格式化语法
184
+ return { type: 'format_syntax' };
185
+ }
186
+ // 表达式区域:检测变量补全
187
+ // 匹配变量名前缀,需要前面是分隔符(空格、运算符、括号等)或字符串开头
188
+ const var_match = text_before.match(/(?:^|[\s+\-*/%(),:])([a-zA-Z_][\w.]*)$/);
189
+ if (var_match) {
190
+ return { type: 'variable', prefix: var_match[1] };
191
+ }
192
+ // 检测刚开始输入变量名(光标前是分隔符)
193
+ if (/(?:^|[\s+\-*/%(),:])$/.test(text_before)) {
194
+ return { type: 'variable', prefix: '' };
195
+ }
196
+ return null;
197
+ }
198
+ exports.detect_trigger = detect_trigger;
199
+ /**
200
+ * 检测是否在格式化区域
201
+ */
202
+ function is_in_format_area(text_before) {
203
+ return text_before.includes('|');
204
+ }
@@ -0,0 +1,16 @@
1
+ import type * as ts from 'typescript/lib/tsserverlibrary';
2
+ /**
3
+ * 变量信息
4
+ */
5
+ export interface VariableInfo {
6
+ /** 变量路径,如 "user.name" */
7
+ path: string;
8
+ /** 变量类型描述 */
9
+ type_desc: string;
10
+ /** 是否有子属性(用于深度补全) */
11
+ has_children: boolean;
12
+ }
13
+ /**
14
+ * 从第二个参数中提取所有可用变量
15
+ */
16
+ export declare function extract_variables_from_arg(ts_module: typeof ts, arg: ts.Expression, type_checker: ts.TypeChecker, logger: ts.server.Logger): VariableInfo[];