@boperators/plugin-ts-language-server 0.1.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/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # @boperators/plugin-ts-language-server
2
+
3
+ TypeScript Language Server plugin for [boperators](https://www.npmjs.com/package/boperators) - provides IDE support with source mapping.
4
+
5
+ This plugin transforms operator overloads in the background and remaps positions between original and transformed source, so IDE features (hover, go-to-definition, diagnostics, completions, etc.) work correctly even though the language server sees the transformed code.
6
+
7
+ ## Installation
8
+
9
+ ```sh
10
+ npm install -D boperators @boperators/plugin-ts-language-server
11
+ ```
12
+
13
+ ## Setup
14
+
15
+ ### 1. Add the plugin to your `tsconfig.json`
16
+
17
+ ```json
18
+ {
19
+ "compilerOptions": {
20
+ "plugins": [
21
+ { "name": "@boperators/plugin-ts-language-server" }
22
+ ]
23
+ }
24
+ }
25
+ ```
26
+
27
+ ### 2. Configure your editor
28
+
29
+ TypeScript Language Service plugins are loaded by `tsserver`, which resolves them relative to the TypeScript installation it's using. If your editor uses its own bundled TypeScript (the default in VS Code), it won't find plugins installed in your project's `node_modules`.
30
+
31
+ #### VS Code
32
+
33
+ **1.** Add to your project's `.vscode/settings.json`:
34
+
35
+ ```json
36
+ {
37
+ "typescript.tsdk": "./node_modules/typescript/lib"
38
+ }
39
+ ```
40
+
41
+ This tells VS Code where to find the workspace TypeScript installation.
42
+
43
+ **2.** Select the workspace TypeScript version: open the command palette (Ctrl/Cmd+Shift+P) → "TypeScript: Select TypeScript Version" → "Use Workspace Version".
44
+
45
+ This is the critical step. VS Code defaults to its own bundled TypeScript, which resolves plugins relative to VS Code's installation directory — not your project's `node_modules`. Switching to the workspace version makes tsserver resolve plugins from your project's `node_modules`, where `@boperators/plugin-ts-language-server` is installed.
46
+
47
+ > This choice is remembered per-workspace, so you only need to do it once.
48
+
49
+ #### Other editors
50
+
51
+ Ensure your editor's TypeScript integration uses the `typescript` package from your project's `node_modules` rather than a bundled version. The plugin must be resolvable via `require("@boperators/plugin-ts-language-server")` from the TypeScript installation directory.
52
+
53
+ ### Troubleshooting
54
+
55
+ If the plugin isn't loading, enable verbose tsserver logging to diagnose:
56
+
57
+ ```json
58
+ {
59
+ "typescript.tsserver.log": "verbose"
60
+ }
61
+ ```
62
+
63
+ Restart the TS server, then check the log output (shown in the VS Code Output panel → "TypeScript") for lines like:
64
+
65
+ - `Enabling plugin @boperators/plugin-ts-language-server from candidate paths: ...` — shows where tsserver is looking
66
+ - `Couldn't find @boperators/plugin-ts-language-server` — the plugin wasn't found in any candidate path
67
+ - `[boperators] Plugin loaded` — the plugin loaded successfully
68
+
69
+ ## Features
70
+
71
+ - **Hover info**: Hovering over an overloaded operator shows the overload signature and JSDoc
72
+ - **Diagnostics remapping**: Errors and warnings point to the correct positions in your original source
73
+ - **Go-to-definition**: Works correctly across transformed files
74
+ - **Completions**: Autocomplete positions are remapped
75
+ - **References and rename**: Find references and rename work across transformed boundaries
76
+ - **Signature help**: Parameter hints are position-remapped
77
+
78
+ ## How It Works
79
+
80
+ The plugin intercepts `getScriptSnapshot` to transform each file on the fly, building a source map between original and transformed code. All Language Service methods that accept or return positions are proxied to remap through this source map. Overload definitions are cached and invalidated when files change.
81
+
82
+ ## License
83
+
84
+ MIT
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SourceMap = void 0;
4
+ /**
5
+ * Bidirectional position mapping between original and transformed source text.
6
+ *
7
+ * Computes a list of edit records by diffing the two texts, then provides
8
+ * O(edits) position and span mapping in both directions.
9
+ */
10
+ class SourceMap {
11
+ constructor(original, transformed) {
12
+ this.edits = computeEdits(original, transformed);
13
+ }
14
+ /** Returns true if no edits were detected (original === transformed). */
15
+ get isEmpty() {
16
+ return this.edits.length === 0;
17
+ }
18
+ /** Map a position from original source to transformed source. */
19
+ originalToTransformed(pos) {
20
+ let delta = 0;
21
+ for (const edit of this.edits) {
22
+ if (pos < edit.origStart) {
23
+ // Before this edit — just apply accumulated delta
24
+ break;
25
+ }
26
+ if (pos < edit.origEnd) {
27
+ // Inside an edited region — map to start of the transformed replacement
28
+ return edit.transStart;
29
+ }
30
+ // Past this edit — accumulate the delta
31
+ delta = edit.transEnd - edit.origEnd;
32
+ }
33
+ return pos + delta;
34
+ }
35
+ /** Map a position from transformed source to original source. */
36
+ transformedToOriginal(pos) {
37
+ let delta = 0;
38
+ for (const edit of this.edits) {
39
+ if (pos < edit.transStart) {
40
+ break;
41
+ }
42
+ if (pos < edit.transEnd) {
43
+ // Inside a transformed region — map to start of the original span
44
+ return edit.origStart;
45
+ }
46
+ delta = edit.origEnd - edit.transEnd;
47
+ }
48
+ return pos + delta;
49
+ }
50
+ /** Map a text span { start, length } from transformed positions to original positions. */
51
+ remapSpan(span) {
52
+ const origStart = this.transformedToOriginal(span.start);
53
+ const origEnd = this.transformedToOriginal(span.start + span.length);
54
+ return { start: origStart, length: origEnd - origStart };
55
+ }
56
+ /** Check if an original-source position falls inside an edited region. */
57
+ isInsideEdit(originalPos) {
58
+ for (const edit of this.edits) {
59
+ if (originalPos < edit.origStart)
60
+ return false;
61
+ if (originalPos < edit.origEnd)
62
+ return true;
63
+ }
64
+ return false;
65
+ }
66
+ /**
67
+ * For a position inside an edited region (in original coords),
68
+ * return the EditRecord it falls in, or undefined.
69
+ */
70
+ getEditAt(originalPos) {
71
+ for (const edit of this.edits) {
72
+ if (originalPos < edit.origStart)
73
+ return undefined;
74
+ if (originalPos < edit.origEnd)
75
+ return edit;
76
+ }
77
+ return undefined;
78
+ }
79
+ }
80
+ exports.SourceMap = SourceMap;
81
+ /**
82
+ * Compute edit records by scanning both texts for mismatches.
83
+ *
84
+ * Uses character-level comparison with anchor-based convergence:
85
+ * identical characters are skipped, mismatches start an edit region,
86
+ * and convergence is found by searching for a matching anchor substring
87
+ * in the remaining text.
88
+ */
89
+ function computeEdits(original, transformed) {
90
+ if (original === transformed)
91
+ return [];
92
+ const edits = [];
93
+ let i = 0; // index in original
94
+ let j = 0; // index in transformed
95
+ while (i < original.length && j < transformed.length) {
96
+ // Skip matching characters
97
+ if (original[i] === transformed[j]) {
98
+ i++;
99
+ j++;
100
+ continue;
101
+ }
102
+ // Mismatch — start of an edit
103
+ const origEditStart = i;
104
+ const transEditStart = j;
105
+ // Find where the texts converge again.
106
+ // Search for an anchor: a substring from `original` that also
107
+ // appears at the corresponding position in `transformed`.
108
+ const ANCHOR_LEN = 8;
109
+ let found = false;
110
+ // Scan ahead in original from the mismatch point
111
+ for (let oi = origEditStart + 1; oi <= original.length - ANCHOR_LEN; oi++) {
112
+ const anchor = original.substring(oi, oi + ANCHOR_LEN);
113
+ const transPos = transformed.indexOf(anchor, transEditStart);
114
+ if (transPos >= 0) {
115
+ // Verify the anchor actually converges by checking a few more chars
116
+ let valid = true;
117
+ const verifyLen = Math.min(ANCHOR_LEN * 2, original.length - oi, transformed.length - transPos);
118
+ for (let k = ANCHOR_LEN; k < verifyLen; k++) {
119
+ if (original[oi + k] !== transformed[transPos + k]) {
120
+ valid = false;
121
+ break;
122
+ }
123
+ }
124
+ if (valid) {
125
+ edits.push({
126
+ origStart: origEditStart,
127
+ origEnd: oi,
128
+ transStart: transEditStart,
129
+ transEnd: transPos,
130
+ });
131
+ i = oi;
132
+ j = transPos;
133
+ found = true;
134
+ break;
135
+ }
136
+ }
137
+ }
138
+ if (!found) {
139
+ // No convergence — remaining text is all part of the edit.
140
+ // Use common suffix to tighten the bounds.
141
+ let suffixLen = 0;
142
+ while (suffixLen < original.length - origEditStart &&
143
+ suffixLen < transformed.length - transEditStart &&
144
+ original[original.length - 1 - suffixLen] ===
145
+ transformed[transformed.length - 1 - suffixLen]) {
146
+ suffixLen++;
147
+ }
148
+ edits.push({
149
+ origStart: origEditStart,
150
+ origEnd: original.length - suffixLen,
151
+ transStart: transEditStart,
152
+ transEnd: transformed.length - suffixLen,
153
+ });
154
+ i = original.length;
155
+ j = transformed.length;
156
+ }
157
+ }
158
+ // Handle remaining text at the end
159
+ if (i < original.length || j < transformed.length) {
160
+ edits.push({
161
+ origStart: i,
162
+ origEnd: original.length,
163
+ transStart: j,
164
+ transEnd: transformed.length,
165
+ });
166
+ }
167
+ return edits;
168
+ }
package/dist/index.js ADDED
@@ -0,0 +1,694 @@
1
+ "use strict";
2
+ const boperators_1 = require("boperators");
3
+ // ----- Overload edit scanner -----
4
+ /**
5
+ * Before transformation, find all expressions (binary, prefix unary, postfix unary)
6
+ * that match registered overloads and record their operator token positions.
7
+ * This is used to provide hover info for overloaded operators.
8
+ */
9
+ /**
10
+ * Recursively resolve the effective type of an expression, accounting for
11
+ * operator overloads. For sub-expressions that match a registered overload,
12
+ * uses the overload's declared return type instead of what TypeScript infers
13
+ * (since TS doesn't know about operator overloading).
14
+ */
15
+ function resolveOverloadedType(node, overloadStore) {
16
+ if (boperators_1.Node.isParenthesizedExpression(node)) {
17
+ return resolveOverloadedType(node.getExpression(), overloadStore);
18
+ }
19
+ if (boperators_1.Node.isBinaryExpression(node)) {
20
+ const operatorKind = node.getOperatorToken().getKind();
21
+ if ((0, boperators_1.isOperatorSyntaxKind)(operatorKind)) {
22
+ const leftType = resolveOverloadedType(node.getLeft(), overloadStore);
23
+ const rightType = resolveOverloadedType(node.getRight(), overloadStore);
24
+ const overload = overloadStore.findOverload(operatorKind, leftType, rightType);
25
+ if (overload)
26
+ return overload.returnType;
27
+ }
28
+ }
29
+ if (boperators_1.Node.isPrefixUnaryExpression(node)) {
30
+ const operatorKind = node.getOperatorToken();
31
+ if ((0, boperators_1.isPrefixUnaryOperatorSyntaxKind)(operatorKind)) {
32
+ const operandType = resolveOverloadedType(node.getOperand(), overloadStore);
33
+ const overload = overloadStore.findPrefixUnaryOverload(operatorKind, operandType);
34
+ if (overload)
35
+ return overload.returnType;
36
+ }
37
+ }
38
+ if (boperators_1.Node.isPostfixUnaryExpression(node)) {
39
+ const operatorKind = node.getOperatorToken();
40
+ if ((0, boperators_1.isPostfixUnaryOperatorSyntaxKind)(operatorKind)) {
41
+ const operandType = resolveOverloadedType(node.getOperand(), overloadStore);
42
+ const overload = overloadStore.findPostfixUnaryOverload(operatorKind, operandType);
43
+ if (overload)
44
+ return overload.returnType;
45
+ }
46
+ }
47
+ return (0, boperators_1.resolveExpressionType)(node);
48
+ }
49
+ function findOverloadEdits(sourceFile, overloadStore) {
50
+ const edits = [];
51
+ const binaryExpressions = sourceFile.getDescendantsOfKind(boperators_1.SyntaxKind.BinaryExpression);
52
+ for (const expression of binaryExpressions) {
53
+ const operatorToken = expression.getOperatorToken();
54
+ const operatorKind = operatorToken.getKind();
55
+ if (!(0, boperators_1.isOperatorSyntaxKind)(operatorKind))
56
+ continue;
57
+ const leftType = resolveOverloadedType(expression.getLeft(), overloadStore);
58
+ const rightType = resolveOverloadedType(expression.getRight(), overloadStore);
59
+ const overloadDesc = overloadStore.findOverload(operatorKind, leftType, rightType);
60
+ if (!overloadDesc)
61
+ continue;
62
+ edits.push({
63
+ operatorStart: operatorToken.getStart(),
64
+ operatorEnd: operatorToken.getEnd(),
65
+ hoverStart: expression.getLeft().getEnd(),
66
+ hoverEnd: expression.getRight().getStart(),
67
+ exprStart: expression.getStart(),
68
+ exprEnd: expression.getEnd(),
69
+ className: overloadDesc.className,
70
+ classFilePath: overloadDesc.classFilePath,
71
+ operatorString: overloadDesc.operatorString,
72
+ index: overloadDesc.index,
73
+ isStatic: overloadDesc.isStatic,
74
+ kind: "binary",
75
+ });
76
+ }
77
+ // Scan prefix unary expressions
78
+ const prefixExpressions = sourceFile.getDescendantsOfKind(boperators_1.SyntaxKind.PrefixUnaryExpression);
79
+ for (const expression of prefixExpressions) {
80
+ const operatorKind = expression.getOperatorToken();
81
+ if (!(0, boperators_1.isPrefixUnaryOperatorSyntaxKind)(operatorKind))
82
+ continue;
83
+ const operandType = resolveOverloadedType(expression.getOperand(), overloadStore);
84
+ const overloadDesc = overloadStore.findPrefixUnaryOverload(operatorKind, operandType);
85
+ if (!overloadDesc)
86
+ continue;
87
+ const exprStart = expression.getStart();
88
+ const operand = expression.getOperand();
89
+ edits.push({
90
+ operatorStart: exprStart,
91
+ operatorEnd: operand.getStart(),
92
+ hoverStart: exprStart,
93
+ hoverEnd: operand.getStart(),
94
+ exprStart,
95
+ exprEnd: expression.getEnd(),
96
+ className: overloadDesc.className,
97
+ classFilePath: overloadDesc.classFilePath,
98
+ operatorString: overloadDesc.operatorString,
99
+ index: overloadDesc.index,
100
+ isStatic: overloadDesc.isStatic,
101
+ kind: "prefixUnary",
102
+ });
103
+ }
104
+ // Scan postfix unary expressions
105
+ const postfixExpressions = sourceFile.getDescendantsOfKind(boperators_1.SyntaxKind.PostfixUnaryExpression);
106
+ for (const expression of postfixExpressions) {
107
+ const operatorKind = expression.getOperatorToken();
108
+ if (!(0, boperators_1.isPostfixUnaryOperatorSyntaxKind)(operatorKind))
109
+ continue;
110
+ const operandType = resolveOverloadedType(expression.getOperand(), overloadStore);
111
+ const overloadDesc = overloadStore.findPostfixUnaryOverload(operatorKind, operandType);
112
+ if (!overloadDesc)
113
+ continue;
114
+ const operand = expression.getOperand();
115
+ const operatorStart = operand.getEnd();
116
+ edits.push({
117
+ operatorStart,
118
+ operatorEnd: expression.getEnd(),
119
+ hoverStart: operatorStart,
120
+ hoverEnd: expression.getEnd(),
121
+ exprStart: expression.getStart(),
122
+ exprEnd: expression.getEnd(),
123
+ className: overloadDesc.className,
124
+ classFilePath: overloadDesc.classFilePath,
125
+ operatorString: overloadDesc.operatorString,
126
+ index: overloadDesc.index,
127
+ isStatic: overloadDesc.isStatic,
128
+ kind: "postfixUnary",
129
+ });
130
+ }
131
+ return edits;
132
+ }
133
+ // ----- Overload hover info -----
134
+ /**
135
+ * Build a QuickInfo response for hovering over an operator token
136
+ * that corresponds to an overloaded operator. Extracts the function
137
+ * signature and JSDoc from the overload definition.
138
+ */
139
+ function getOverloadHoverInfo(ts, project, edit) {
140
+ try {
141
+ const classSourceFile = project.getSourceFile(edit.classFilePath);
142
+ if (!classSourceFile)
143
+ return undefined;
144
+ const classDecl = classSourceFile.getClass(edit.className);
145
+ if (!classDecl)
146
+ return undefined;
147
+ // Find the property with the matching operator string
148
+ const prop = classDecl.getProperties().find((p) => {
149
+ if (!boperators_1.Node.isPropertyDeclaration(p))
150
+ return false;
151
+ return (0, boperators_1.getOperatorStringFromProperty)(p) === edit.operatorString;
152
+ });
153
+ if (!prop || !boperators_1.Node.isPropertyDeclaration(prop))
154
+ return undefined;
155
+ // Extract param types and return type from either the initializer (regular
156
+ // .ts files) or the type annotation (.d.ts files where initializers are
157
+ // stripped by TypeScript's declaration emit).
158
+ let params = [];
159
+ let returnTypeName;
160
+ let docText;
161
+ const initializer = (0, boperators_1.unwrapInitializer)(prop.getInitializer());
162
+ if (initializer && boperators_1.Node.isArrayLiteralExpression(initializer)) {
163
+ const element = initializer.getElements()[edit.index];
164
+ if (!element ||
165
+ (!boperators_1.Node.isFunctionExpression(element) && !boperators_1.Node.isArrowFunction(element)))
166
+ return undefined;
167
+ const nonThisParams = element
168
+ .getParameters()
169
+ .filter((p) => p.getName() !== "this");
170
+ params = nonThisParams.map((p) => ({
171
+ typeName: p.getType().getText(element),
172
+ }));
173
+ returnTypeName = element.getReturnType().getText(element);
174
+ const jsDocs = element.getJsDocs();
175
+ if (jsDocs.length > 0) {
176
+ const raw = jsDocs[0].getText();
177
+ docText = raw
178
+ .replace(/^\/\*\*\s*/, "")
179
+ .replace(/\s*\*\/$/, "")
180
+ .replace(/^\s*\* ?/gm, "")
181
+ .trim();
182
+ }
183
+ }
184
+ else {
185
+ // Type-annotation fallback for .d.ts files
186
+ const propertyType = prop.getType();
187
+ if (!propertyType.isTuple())
188
+ return undefined;
189
+ const tupleElements = propertyType.getTupleElements();
190
+ if (edit.index >= tupleElements.length)
191
+ return undefined;
192
+ const elementType = tupleElements[edit.index];
193
+ const callSigs = elementType.getCallSignatures();
194
+ if (callSigs.length === 0)
195
+ return undefined;
196
+ const sig = callSigs[0];
197
+ for (const sym of sig.getParameters()) {
198
+ if (sym.getName() === "this")
199
+ continue;
200
+ const decl = sym.getValueDeclaration();
201
+ if (!decl)
202
+ continue;
203
+ params.push({ typeName: decl.getType().getText(prop) });
204
+ }
205
+ returnTypeName = sig.getReturnType().getText(prop);
206
+ }
207
+ // Build display signature parts based on overload kind
208
+ const displayParts = [];
209
+ if (edit.kind === "prefixUnary") {
210
+ // Prefix unary: "-Vector3 = Vector3"
211
+ displayParts.push({
212
+ text: edit.operatorString,
213
+ kind: "operator",
214
+ });
215
+ const operandType = params.length >= 1 ? params[0].typeName : edit.className;
216
+ displayParts.push({ text: operandType, kind: "className" });
217
+ if (returnTypeName !== "void") {
218
+ displayParts.push({ text: " = ", kind: "punctuation" });
219
+ displayParts.push({
220
+ text: returnTypeName,
221
+ kind: "className",
222
+ });
223
+ }
224
+ }
225
+ else if (edit.kind === "postfixUnary") {
226
+ // Postfix unary: "Vector3++"
227
+ displayParts.push({ text: edit.className, kind: "className" });
228
+ displayParts.push({
229
+ text: edit.operatorString,
230
+ kind: "operator",
231
+ });
232
+ }
233
+ else if (edit.isStatic && params.length >= 2) {
234
+ // Binary static: "LhsType + RhsType = ReturnType"
235
+ const lhsType = params[0].typeName;
236
+ const rhsType = params[1].typeName;
237
+ displayParts.push({ text: lhsType, kind: "className" });
238
+ displayParts.push({ text: " ", kind: "space" });
239
+ displayParts.push({
240
+ text: edit.operatorString,
241
+ kind: "operator",
242
+ });
243
+ displayParts.push({ text: " ", kind: "space" });
244
+ displayParts.push({ text: rhsType, kind: "className" });
245
+ if (returnTypeName !== "void") {
246
+ displayParts.push({ text: " = ", kind: "punctuation" });
247
+ displayParts.push({
248
+ text: returnTypeName,
249
+ kind: "className",
250
+ });
251
+ }
252
+ }
253
+ else {
254
+ // Binary instance: "ClassName += RhsType"
255
+ const rhsType = params.length >= 1 ? params[0].typeName : "unknown";
256
+ displayParts.push({ text: edit.className, kind: "className" });
257
+ displayParts.push({ text: " ", kind: "space" });
258
+ displayParts.push({
259
+ text: edit.operatorString,
260
+ kind: "operator",
261
+ });
262
+ displayParts.push({ text: " ", kind: "space" });
263
+ displayParts.push({ text: rhsType, kind: "className" });
264
+ if (returnTypeName !== "void") {
265
+ displayParts.push({ text: " = ", kind: "punctuation" });
266
+ displayParts.push({
267
+ text: returnTypeName,
268
+ kind: "className",
269
+ });
270
+ }
271
+ }
272
+ return {
273
+ kind: ts.ScriptElementKind.functionElement,
274
+ kindModifiers: edit.isStatic ? "static" : "",
275
+ textSpan: {
276
+ start: edit.operatorStart,
277
+ length: edit.operatorEnd - edit.operatorStart,
278
+ },
279
+ displayParts,
280
+ documentation: docText ? [{ text: docText, kind: "text" }] : undefined,
281
+ tags: [],
282
+ };
283
+ }
284
+ catch (_a) {
285
+ return undefined;
286
+ }
287
+ }
288
+ // ----- LanguageService proxy -----
289
+ function getSourceMapForFile(cache, fileName) {
290
+ const entry = cache.get(fileName);
291
+ if (!entry || entry.sourceMap.isEmpty)
292
+ return undefined;
293
+ return entry.sourceMap;
294
+ }
295
+ function remapDiagnosticSpan(diag, sourceMap) {
296
+ if (diag.start !== undefined && diag.length !== undefined) {
297
+ const remapped = sourceMap.remapSpan({
298
+ start: diag.start,
299
+ length: diag.length,
300
+ });
301
+ diag.start = remapped.start;
302
+ diag.length = remapped.length;
303
+ }
304
+ }
305
+ function createProxy(ts, ls, cache, project) {
306
+ // Copy all methods from the underlying language service
307
+ const proxy = Object.create(null);
308
+ for (const key of Object.keys(ls)) {
309
+ proxy[key] = ls[key];
310
+ }
311
+ // --- Diagnostics: remap output spans + suppress overload errors ---
312
+ const isOverloadSuppressed = (code, start, entry) => {
313
+ if (!(entry === null || entry === void 0 ? void 0 : entry.overloadEdits.length) || start === undefined)
314
+ return false;
315
+ // TS2588: "Cannot assign to 'x' because it is a constant."
316
+ if (code === 2588) {
317
+ return entry.overloadEdits.some((e) => !e.isStatic && start >= e.exprStart && start < e.exprEnd);
318
+ }
319
+ return false;
320
+ };
321
+ proxy.getSemanticDiagnostics = (fileName) => {
322
+ const result = ls.getSemanticDiagnostics(fileName);
323
+ const entry = cache.get(fileName);
324
+ const sourceMap = (entry === null || entry === void 0 ? void 0 : entry.sourceMap.isEmpty) === false ? entry.sourceMap : undefined;
325
+ if (sourceMap) {
326
+ for (const diag of result) {
327
+ remapDiagnosticSpan(diag, sourceMap);
328
+ if (diag.relatedInformation) {
329
+ for (const related of diag.relatedInformation) {
330
+ const relatedMap = related.file
331
+ ? getSourceMapForFile(cache, related.file.fileName)
332
+ : undefined;
333
+ if (relatedMap)
334
+ remapDiagnosticSpan(related, relatedMap);
335
+ }
336
+ }
337
+ }
338
+ }
339
+ return result.filter((diag) => !isOverloadSuppressed(diag.code, diag.start, entry));
340
+ };
341
+ proxy.getSyntacticDiagnostics = (fileName) => {
342
+ const result = ls.getSyntacticDiagnostics(fileName);
343
+ const entry = cache.get(fileName);
344
+ const sourceMap = (entry === null || entry === void 0 ? void 0 : entry.sourceMap.isEmpty) === false ? entry.sourceMap : undefined;
345
+ if (sourceMap) {
346
+ for (const diag of result) {
347
+ remapDiagnosticSpan(diag, sourceMap);
348
+ if (diag.relatedInformation) {
349
+ for (const related of diag.relatedInformation) {
350
+ const relatedMap = related.file
351
+ ? getSourceMapForFile(cache, related.file.fileName)
352
+ : undefined;
353
+ if (relatedMap)
354
+ remapDiagnosticSpan(related, relatedMap);
355
+ }
356
+ }
357
+ }
358
+ }
359
+ return result.filter((diag) => !isOverloadSuppressed(diag.code, diag.start, entry));
360
+ };
361
+ proxy.getSuggestionDiagnostics = (fileName) => {
362
+ const result = ls.getSuggestionDiagnostics(fileName);
363
+ const sourceMap = getSourceMapForFile(cache, fileName);
364
+ if (!sourceMap)
365
+ return result;
366
+ for (const diag of result) {
367
+ remapDiagnosticSpan(diag, sourceMap);
368
+ }
369
+ return result;
370
+ };
371
+ // --- Hover: remap input position + output span, custom operator hover ---
372
+ proxy.getQuickInfoAtPosition = (fileName, position) => {
373
+ // Check if hovering over an overloaded operator
374
+ const entry = cache.get(fileName);
375
+ if (entry) {
376
+ const operatorEdit = entry.overloadEdits.find((e) => position >= e.hoverStart && position < e.hoverEnd);
377
+ if (operatorEdit) {
378
+ const hoverInfo = getOverloadHoverInfo(ts, project, operatorEdit);
379
+ if (hoverInfo)
380
+ return hoverInfo;
381
+ }
382
+ }
383
+ const sourceMap = (entry === null || entry === void 0 ? void 0 : entry.sourceMap.isEmpty) === false ? entry.sourceMap : undefined;
384
+ const transformedPos = sourceMap
385
+ ? sourceMap.originalToTransformed(position)
386
+ : position;
387
+ const result = ls.getQuickInfoAtPosition(fileName, transformedPos);
388
+ if (!result || !sourceMap)
389
+ return result;
390
+ result.textSpan = sourceMap.remapSpan(result.textSpan);
391
+ return result;
392
+ };
393
+ // --- Go-to-definition: remap input position + output spans ---
394
+ proxy.getDefinitionAndBoundSpan = (fileName, position) => {
395
+ const sourceMap = getSourceMapForFile(cache, fileName);
396
+ const transformedPos = sourceMap
397
+ ? sourceMap.originalToTransformed(position)
398
+ : position;
399
+ const result = ls.getDefinitionAndBoundSpan(fileName, transformedPos);
400
+ if (!result)
401
+ return result;
402
+ // Remap the bound span (in the current file)
403
+ if (sourceMap) {
404
+ result.textSpan = sourceMap.remapSpan(result.textSpan);
405
+ }
406
+ // Remap definition spans (may be in other files)
407
+ if (result.definitions) {
408
+ for (const def of result.definitions) {
409
+ const defMap = getSourceMapForFile(cache, def.fileName);
410
+ if (defMap) {
411
+ def.textSpan = defMap.remapSpan(def.textSpan);
412
+ if (def.contextSpan) {
413
+ def.contextSpan = defMap.remapSpan(def.contextSpan);
414
+ }
415
+ }
416
+ }
417
+ }
418
+ return result;
419
+ };
420
+ proxy.getDefinitionAtPosition = (fileName, position) => {
421
+ const sourceMap = getSourceMapForFile(cache, fileName);
422
+ const transformedPos = sourceMap
423
+ ? sourceMap.originalToTransformed(position)
424
+ : position;
425
+ const result = ls.getDefinitionAtPosition(fileName, transformedPos);
426
+ if (!result)
427
+ return result;
428
+ return result.map((def) => {
429
+ const defMap = getSourceMapForFile(cache, def.fileName);
430
+ if (!defMap)
431
+ return def;
432
+ return Object.assign(Object.assign({}, def), { textSpan: defMap.remapSpan(def.textSpan), contextSpan: def.contextSpan
433
+ ? defMap.remapSpan(def.contextSpan)
434
+ : undefined });
435
+ });
436
+ };
437
+ proxy.getTypeDefinitionAtPosition = (fileName, position) => {
438
+ const sourceMap = getSourceMapForFile(cache, fileName);
439
+ const transformedPos = sourceMap
440
+ ? sourceMap.originalToTransformed(position)
441
+ : position;
442
+ const result = ls.getTypeDefinitionAtPosition(fileName, transformedPos);
443
+ if (!result)
444
+ return result;
445
+ return result.map((def) => {
446
+ const defMap = getSourceMapForFile(cache, def.fileName);
447
+ if (!defMap)
448
+ return def;
449
+ return Object.assign(Object.assign({}, def), { textSpan: defMap.remapSpan(def.textSpan), contextSpan: def.contextSpan
450
+ ? defMap.remapSpan(def.contextSpan)
451
+ : undefined });
452
+ });
453
+ };
454
+ // --- Completions: remap input position ---
455
+ proxy.getCompletionsAtPosition = (fileName, position, options, formattingSettings) => {
456
+ const sourceMap = getSourceMapForFile(cache, fileName);
457
+ const transformedPos = sourceMap
458
+ ? sourceMap.originalToTransformed(position)
459
+ : position;
460
+ const result = ls.getCompletionsAtPosition(fileName, transformedPos, options, formattingSettings);
461
+ if (!result || !sourceMap)
462
+ return result;
463
+ // Remap replacement spans in completion entries
464
+ if (result.optionalReplacementSpan) {
465
+ result.optionalReplacementSpan = sourceMap.remapSpan(result.optionalReplacementSpan);
466
+ }
467
+ for (const entry of result.entries) {
468
+ if (entry.replacementSpan) {
469
+ entry.replacementSpan = sourceMap.remapSpan(entry.replacementSpan);
470
+ }
471
+ }
472
+ return result;
473
+ };
474
+ // --- References: remap input + output ---
475
+ proxy.getReferencesAtPosition = (fileName, position) => {
476
+ const sourceMap = getSourceMapForFile(cache, fileName);
477
+ const transformedPos = sourceMap
478
+ ? sourceMap.originalToTransformed(position)
479
+ : position;
480
+ const result = ls.getReferencesAtPosition(fileName, transformedPos);
481
+ if (!result)
482
+ return result;
483
+ return result.map((ref) => {
484
+ const refMap = getSourceMapForFile(cache, ref.fileName);
485
+ if (!refMap)
486
+ return ref;
487
+ return Object.assign(Object.assign({}, ref), { textSpan: refMap.remapSpan(ref.textSpan), contextSpan: ref.contextSpan
488
+ ? refMap.remapSpan(ref.contextSpan)
489
+ : undefined });
490
+ });
491
+ };
492
+ proxy.findReferences = (fileName, position) => {
493
+ const sourceMap = getSourceMapForFile(cache, fileName);
494
+ const transformedPos = sourceMap
495
+ ? sourceMap.originalToTransformed(position)
496
+ : position;
497
+ const result = ls.findReferences(fileName, transformedPos);
498
+ if (!result)
499
+ return result;
500
+ return result.map((group) => (Object.assign(Object.assign({}, group), { references: group.references.map((ref) => {
501
+ const refMap = getSourceMapForFile(cache, ref.fileName);
502
+ if (!refMap)
503
+ return ref;
504
+ return Object.assign(Object.assign({}, ref), { textSpan: refMap.remapSpan(ref.textSpan), contextSpan: ref.contextSpan
505
+ ? refMap.remapSpan(ref.contextSpan)
506
+ : undefined });
507
+ }) })));
508
+ };
509
+ // --- Classifications: remap output spans for syntax coloring ---
510
+ proxy.getEncodedSemanticClassifications = (fileName, span, format) => {
511
+ const sourceMap = getSourceMapForFile(cache, fileName);
512
+ const transformedSpan = sourceMap
513
+ ? {
514
+ start: sourceMap.originalToTransformed(span.start),
515
+ length: sourceMap.originalToTransformed(span.start + span.length) -
516
+ sourceMap.originalToTransformed(span.start),
517
+ }
518
+ : span;
519
+ const result = ls.getEncodedSemanticClassifications(fileName, transformedSpan, format);
520
+ if (!sourceMap)
521
+ return result;
522
+ // spans are triples: [start, length, classification, ...]
523
+ for (let i = 0; i < result.spans.length; i += 3) {
524
+ const remapped = sourceMap.remapSpan({
525
+ start: result.spans[i],
526
+ length: result.spans[i + 1],
527
+ });
528
+ result.spans[i] = remapped.start;
529
+ result.spans[i + 1] = remapped.length;
530
+ }
531
+ return result;
532
+ };
533
+ proxy.getEncodedSyntacticClassifications = (fileName, span) => {
534
+ const sourceMap = getSourceMapForFile(cache, fileName);
535
+ const transformedSpan = sourceMap
536
+ ? {
537
+ start: sourceMap.originalToTransformed(span.start),
538
+ length: sourceMap.originalToTransformed(span.start + span.length) -
539
+ sourceMap.originalToTransformed(span.start),
540
+ }
541
+ : span;
542
+ const result = ls.getEncodedSyntacticClassifications(fileName, transformedSpan);
543
+ if (!sourceMap)
544
+ return result;
545
+ for (let i = 0; i < result.spans.length; i += 3) {
546
+ const remapped = sourceMap.remapSpan({
547
+ start: result.spans[i],
548
+ length: result.spans[i + 1],
549
+ });
550
+ result.spans[i] = remapped.start;
551
+ result.spans[i + 1] = remapped.length;
552
+ }
553
+ return result;
554
+ };
555
+ // --- Signature help: remap input position + applicableSpan ---
556
+ proxy.getSignatureHelpItems = (fileName, position, options) => {
557
+ const sourceMap = getSourceMapForFile(cache, fileName);
558
+ const transformedPos = sourceMap
559
+ ? sourceMap.originalToTransformed(position)
560
+ : position;
561
+ const result = ls.getSignatureHelpItems(fileName, transformedPos, options);
562
+ if (!result || !sourceMap)
563
+ return result;
564
+ result.applicableSpan = sourceMap.remapSpan(result.applicableSpan);
565
+ return result;
566
+ };
567
+ // --- Rename: remap input + output ---
568
+ proxy.findRenameLocations = (fileName, position, findInStrings, findInComments, preferences) => {
569
+ const sourceMap = getSourceMapForFile(cache, fileName);
570
+ const transformedPos = sourceMap
571
+ ? sourceMap.originalToTransformed(position)
572
+ : position;
573
+ const result = ls.findRenameLocations(fileName, transformedPos, findInStrings, findInComments, preferences);
574
+ if (!result)
575
+ return result;
576
+ return result.map((loc) => {
577
+ const locMap = getSourceMapForFile(cache, loc.fileName);
578
+ if (!locMap)
579
+ return loc;
580
+ return Object.assign(Object.assign({}, loc), { textSpan: locMap.remapSpan(loc.textSpan), contextSpan: loc.contextSpan
581
+ ? locMap.remapSpan(loc.contextSpan)
582
+ : undefined });
583
+ });
584
+ };
585
+ proxy.getRenameInfo = (fileName, position, preferences) => {
586
+ const sourceMap = getSourceMapForFile(cache, fileName);
587
+ const transformedPos = sourceMap
588
+ ? sourceMap.originalToTransformed(position)
589
+ : position;
590
+ const result = ls.getRenameInfo(fileName, transformedPos, preferences);
591
+ if (!sourceMap)
592
+ return result;
593
+ if ("triggerSpan" in result && result.triggerSpan) {
594
+ result.triggerSpan = sourceMap.remapSpan(result.triggerSpan);
595
+ }
596
+ return result;
597
+ };
598
+ // --- Implementation location: remap input + output ---
599
+ proxy.getImplementationAtPosition = (fileName, position) => {
600
+ const sourceMap = getSourceMapForFile(cache, fileName);
601
+ const transformedPos = sourceMap
602
+ ? sourceMap.originalToTransformed(position)
603
+ : position;
604
+ const result = ls.getImplementationAtPosition(fileName, transformedPos);
605
+ if (!result)
606
+ return result;
607
+ return result.map((impl) => {
608
+ const implMap = getSourceMapForFile(cache, impl.fileName);
609
+ if (!implMap)
610
+ return impl;
611
+ return Object.assign(Object.assign({}, impl), { textSpan: implMap.remapSpan(impl.textSpan), contextSpan: impl.contextSpan
612
+ ? implMap.remapSpan(impl.contextSpan)
613
+ : undefined });
614
+ });
615
+ };
616
+ return proxy;
617
+ }
618
+ module.exports = function init(modules) {
619
+ const ts = modules.typescript;
620
+ function create(info) {
621
+ var _a, _b;
622
+ const tsServerLogger = {
623
+ debug: (msg) => info.project.projectService.logger.info(`[boperators] [debug] ${msg}`),
624
+ info: (msg) => info.project.projectService.logger.info(`[boperators] ${msg}`),
625
+ warn: (msg) => info.project.projectService.logger.info(`[boperators] [warn] ${msg}`),
626
+ error: (msg) => info.project.projectService.logger.info(`[boperators] [error] ${msg}`),
627
+ };
628
+ const config = (0, boperators_1.loadConfig)({ logger: tsServerLogger });
629
+ config.logger.info(`Creating language service plugin for project: ${info.project.getProjectName()}`);
630
+ const host = info.languageServiceHost;
631
+ // Set up ts-morph transformation pipeline
632
+ const project = new boperators_1.Project({ skipFileDependencyResolution: true });
633
+ const errorManager = new boperators_1.ErrorManager(config);
634
+ const overloadStore = new boperators_1.OverloadStore(project, errorManager, config.logger);
635
+ const overloadInjector = new boperators_1.OverloadInjector(project, overloadStore, config.logger);
636
+ const originalGetSnapshot = (_a = host.getScriptSnapshot) === null || _a === void 0 ? void 0 : _a.bind(host);
637
+ const originalGetVersion = (_b = host.getScriptVersion) === null || _b === void 0 ? void 0 : _b.bind(host);
638
+ const cache = new Map();
639
+ host.getScriptSnapshot = (fileName) => {
640
+ var _a;
641
+ const snap = originalGetSnapshot === null || originalGetSnapshot === void 0 ? void 0 : originalGetSnapshot(fileName);
642
+ if (!snap || !fileName.endsWith(".ts") || fileName.endsWith(".d.ts"))
643
+ return snap;
644
+ const version = (_a = originalGetVersion === null || originalGetVersion === void 0 ? void 0 : originalGetVersion(fileName)) !== null && _a !== void 0 ? _a : "0";
645
+ const cached = cache.get(fileName);
646
+ if ((cached === null || cached === void 0 ? void 0 : cached.version) === version) {
647
+ return ts.ScriptSnapshot.fromString(cached.text);
648
+ }
649
+ const source = snap.getText(0, snap.getLength());
650
+ try {
651
+ // Invalidate this file's old overload entries before overwriting.
652
+ const hadOverloads = overloadStore.invalidateFile(fileName);
653
+ if (hadOverloads)
654
+ cache.clear();
655
+ // Add/update the file in our ts-morph project
656
+ project.createSourceFile(fileName, source, { overwrite: true });
657
+ // Resolve any new dependencies and scan for overloads.
658
+ const deps = project.resolveSourceFileDependencies();
659
+ for (const dep of deps)
660
+ overloadStore.addOverloadsFromFile(dep);
661
+ overloadStore.addOverloadsFromFile(fileName);
662
+ errorManager.throwIfErrorsElseLogWarnings();
663
+ // Before transforming, scan for overloaded expressions
664
+ // so we can record their operator positions for hover info.
665
+ const sourceFile = project.getSourceFileOrThrow(fileName);
666
+ const overloadEdits = findOverloadEdits(sourceFile, overloadStore);
667
+ // Transform expressions (returns text + source map)
668
+ const result = overloadInjector.overloadFile(fileName);
669
+ cache.set(fileName, {
670
+ version,
671
+ text: result.text,
672
+ sourceMap: result.sourceMap,
673
+ overloadEdits,
674
+ });
675
+ return ts.ScriptSnapshot.fromString(result.text);
676
+ }
677
+ catch (e) {
678
+ config.logger.error(`Error transforming ${fileName}: ${e}`);
679
+ cache.set(fileName, {
680
+ version,
681
+ text: source,
682
+ sourceMap: new boperators_1.SourceMap(source, source),
683
+ overloadEdits: [],
684
+ });
685
+ return snap;
686
+ }
687
+ };
688
+ // Create the language service proxy with position remapping
689
+ const proxy = createProxy(ts, info.languageService, cache, project);
690
+ config.logger.info("Plugin loaded");
691
+ return proxy;
692
+ }
693
+ return { create };
694
+ };
package/license.txt ADDED
@@ -0,0 +1,8 @@
1
+ Copyright 2025 Dief Bell
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@boperators/plugin-ts-language-server",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "description": "TypeScript Language Server plugin for boperators - IDE support with source mapping.",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/DiefBell/boperators",
9
+ "directory": "plugins/ts-language-server"
10
+ },
11
+ "homepage": "https://github.com/DiefBell/boperators/tree/main/plugins/ts-language-server",
12
+ "type": "commonjs",
13
+ "main": "./dist/index.js",
14
+ "keywords": [
15
+ "boperators",
16
+ "typescript",
17
+ "operator",
18
+ "overload",
19
+ "operator-overloading",
20
+ "language-server",
21
+ "tsserver",
22
+ "ide"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsc",
26
+ "watch": "tsc --watch",
27
+ "prepublish": "bun run build"
28
+ },
29
+ "files": [
30
+ "package.json",
31
+ "README.md",
32
+ "license.txt",
33
+ "dist"
34
+ ],
35
+ "peerDependencies": {
36
+ "boperators": "0.1.0",
37
+ "typescript": ">=5.0.0 <5.10.0"
38
+ }
39
+ }