@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 +84 -0
- package/dist/SourceMap.js +168 -0
- package/dist/index.js +694 -0
- package/license.txt +8 -0
- package/package.json +39 -0
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
|
+
}
|