aws-iam-language-server 0.0.13 → 0.0.15
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/package.json +1 -1
- package/readme.md +12 -0
- package/src/handlers/completion/action-value.d.ts +2 -0
- package/src/handlers/completion/action-value.js +1 -1
- package/src/handlers/completion/condition-key.d.ts +2 -0
- package/src/handlers/completion/condition-key.js +3 -3
- package/src/handlers/completion/index.d.ts +2 -2
- package/src/handlers/completion/index.js +4 -4
- package/src/handlers/completion/principal-identifier-completions.d.ts +1 -1
- package/src/handlers/diagnostics/diagnostics.js +4 -2
- package/src/handlers/diagnostics/resource.js +4 -4
- package/src/handlers/diagnostics/sid.d.ts +1 -1
- package/src/handlers/diagnostics/sid.js +11 -2
- package/src/handlers/diagnostics/utils.js +1 -13
- package/src/handlers/document-link/document-link.js +1 -3
- package/src/handlers/hover/action-value.d.ts +3 -0
- package/src/handlers/hover/action-value.js +21 -0
- package/src/handlers/hover/condition-block.d.ts +3 -0
- package/src/handlers/hover/condition-block.js +18 -0
- package/src/handlers/hover/condition-key.d.ts +3 -0
- package/src/handlers/hover/condition-key.js +53 -0
- package/src/handlers/hover/condition-operator.d.ts +3 -0
- package/src/handlers/hover/condition-operator.js +14 -0
- package/src/handlers/hover/effect-value.d.ts +3 -0
- package/src/handlers/hover/effect-value.js +17 -0
- package/src/handlers/hover/index.d.ts +3 -0
- package/src/handlers/hover/index.js +69 -0
- package/src/handlers/hover/principal-block.d.ts +3 -0
- package/src/handlers/hover/principal-block.js +17 -0
- package/src/handlers/hover/principal-type.d.ts +3 -0
- package/src/handlers/hover/principal-type.js +25 -0
- package/src/handlers/hover/principal-typed-value.d.ts +3 -0
- package/src/handlers/hover/principal-typed-value.js +118 -0
- package/src/handlers/hover/principal-value.d.ts +3 -0
- package/src/handlers/hover/principal-value.js +13 -0
- package/src/handlers/hover/resource-value.d.ts +3 -0
- package/src/handlers/hover/resource-value.js +45 -0
- package/src/handlers/hover/statement-block.d.ts +3 -0
- package/src/handlers/hover/statement-block.js +14 -0
- package/src/handlers/hover/statement-key.d.ts +3 -0
- package/src/handlers/hover/statement-key.js +14 -0
- package/src/lib/iam-policy/arn.d.ts +17 -0
- package/src/lib/iam-policy/arn.js +77 -0
- package/src/lib/iam-policy/location.d.ts +31 -1
- package/src/lib/iam-policy/location.js +56 -19
- package/src/lib/iam-policy/reference/services.d.ts +5 -3
- package/src/lib/iam-policy/reference/services.js +58 -19
- package/src/lib/iam-policy/reference/types.d.ts +5 -0
- package/src/lib/treesitter/base.d.ts +3 -8
- package/src/lib/treesitter/base.js +3 -3
- package/src/lib/treesitter/hcl.js +32 -24
- package/src/lib/treesitter/json.js +14 -11
- package/src/lib/treesitter/yaml.js +30 -27
- package/src/server.js +5 -3
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { arnMatches, parseArn } from "../arn.js";
|
|
2
3
|
export class ServiceReference {
|
|
3
4
|
static #serviceDataMap = {};
|
|
4
5
|
static #allActions;
|
|
@@ -7,30 +8,53 @@ export class ServiceReference {
|
|
|
7
8
|
static #globalConditionKeys;
|
|
8
9
|
static getServiceData(service) {
|
|
9
10
|
if (!ServiceReference.#serviceDataMap[service]) {
|
|
10
|
-
|
|
11
|
+
try {
|
|
12
|
+
ServiceReference.#serviceDataMap[service] = JSON.parse(readFileSync(`${import.meta.dirname}/../../../data/servicereference/services/${service}.json`, 'utf-8'));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
11
17
|
}
|
|
12
18
|
return ServiceReference.#serviceDataMap[service];
|
|
13
19
|
}
|
|
14
20
|
static getServicePrincipals() {
|
|
15
21
|
if (!ServiceReference.#servicePrincipals) {
|
|
16
|
-
|
|
22
|
+
try {
|
|
23
|
+
ServiceReference.#servicePrincipals = JSON.parse(readFileSync(`${import.meta.dirname}/../../../data/servicereference/service-principals.json`, 'utf-8'));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
17
28
|
}
|
|
18
29
|
return ServiceReference.#servicePrincipals;
|
|
19
30
|
}
|
|
20
31
|
static getAllActions() {
|
|
21
32
|
if (!ServiceReference.#allActions) {
|
|
22
|
-
|
|
33
|
+
try {
|
|
34
|
+
ServiceReference.#allActions = JSON.parse(readFileSync(`${import.meta.dirname}/../../../data/servicereference/actions.json`, 'utf-8'));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
23
39
|
}
|
|
24
40
|
return ServiceReference.#allActions;
|
|
25
41
|
}
|
|
26
42
|
static getAllServices() {
|
|
27
43
|
if (!ServiceReference.#allServices) {
|
|
28
|
-
|
|
44
|
+
try {
|
|
45
|
+
ServiceReference.#allServices = JSON.parse(readFileSync(`${import.meta.dirname}/../../../data/servicereference/services.json`, 'utf-8'));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
29
50
|
}
|
|
30
51
|
return ServiceReference.#allServices;
|
|
31
52
|
}
|
|
32
53
|
static getActionsForService(service) {
|
|
33
|
-
|
|
54
|
+
const serviceData = ServiceReference.getServiceData(service);
|
|
55
|
+
if (!serviceData)
|
|
56
|
+
return [];
|
|
57
|
+
return Object.entries(serviceData.actions).map(([actionName, action]) => {
|
|
34
58
|
if (!action.name)
|
|
35
59
|
action.name = `${service}:${actionName}`;
|
|
36
60
|
return action;
|
|
@@ -41,6 +65,8 @@ export class ServiceReference {
|
|
|
41
65
|
for (const action of actions) {
|
|
42
66
|
const [service, actionName] = action.split(':');
|
|
43
67
|
const serviceData = ServiceReference.getServiceData(service);
|
|
68
|
+
if (!serviceData)
|
|
69
|
+
continue;
|
|
44
70
|
const actionDef = serviceData.actions[actionName];
|
|
45
71
|
if (actionDef?.conditionKeys) {
|
|
46
72
|
for (const keyName of actionDef.conditionKeys) {
|
|
@@ -56,33 +82,46 @@ export class ServiceReference {
|
|
|
56
82
|
}
|
|
57
83
|
static getGlobalConditionKeys() {
|
|
58
84
|
if (!ServiceReference.#globalConditionKeys) {
|
|
59
|
-
|
|
85
|
+
try {
|
|
86
|
+
ServiceReference.#globalConditionKeys = JSON.parse(readFileSync(`${import.meta.dirname}/../../../data/condition-keys/global.json`, 'utf-8'));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
60
91
|
}
|
|
61
92
|
return ServiceReference.#globalConditionKeys;
|
|
62
93
|
}
|
|
63
94
|
static getAction(action) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return ServiceReference.getServiceData(serviceName).actions[actionName];
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
return undefined;
|
|
71
|
-
}
|
|
95
|
+
const serviceName = action.split(':')[0];
|
|
96
|
+
const actionName = action.split(':')[1];
|
|
97
|
+
return ServiceReference.getServiceData(serviceName)?.actions[actionName];
|
|
72
98
|
}
|
|
73
99
|
static getConditionKey(service, keyName) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
100
|
+
return ServiceReference.getServiceData(service)?.conditionKeys[keyName];
|
|
101
|
+
}
|
|
102
|
+
static getResources(arn) {
|
|
103
|
+
const serviceData = ServiceReference.getServiceData(arn.service);
|
|
104
|
+
if (!serviceData)
|
|
105
|
+
return [];
|
|
106
|
+
const matches = [];
|
|
107
|
+
for (const resource of serviceData.resources) {
|
|
108
|
+
for (const format of resource.arnFormats) {
|
|
109
|
+
const templateArn = parseArn(format);
|
|
110
|
+
if (templateArn && arnMatches(arn, templateArn)) {
|
|
111
|
+
matches.push(resource);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
79
115
|
}
|
|
116
|
+
return matches;
|
|
80
117
|
}
|
|
81
118
|
static getResourcesForActions(actions) {
|
|
82
119
|
const resources = new Map();
|
|
83
120
|
for (const action of actions) {
|
|
84
121
|
const [service, actionName] = action.split(':');
|
|
85
122
|
const serviceData = ServiceReference.getServiceData(service);
|
|
123
|
+
if (!serviceData)
|
|
124
|
+
continue;
|
|
86
125
|
const actionDef = serviceData.actions[actionName];
|
|
87
126
|
if (!actionDef?.resources)
|
|
88
127
|
continue;
|
|
@@ -70,6 +70,11 @@ export type Action = {
|
|
|
70
70
|
operationUrl?: string;
|
|
71
71
|
iamUrl?: string;
|
|
72
72
|
};
|
|
73
|
+
export type ResourceDef = {
|
|
74
|
+
name: string;
|
|
75
|
+
arnFormats: Array<string>;
|
|
76
|
+
conditionKeys: Array<string>;
|
|
77
|
+
};
|
|
73
78
|
export type ConditionKey = {
|
|
74
79
|
types: Array<string>;
|
|
75
80
|
description?: string;
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
+
import type { Position, Range } from 'vscode-languageserver';
|
|
1
2
|
import type { Language, Node, Tree } from 'web-tree-sitter';
|
|
2
|
-
export type Position
|
|
3
|
-
line: number;
|
|
4
|
-
column: number;
|
|
5
|
-
};
|
|
6
|
-
export type Range = {
|
|
7
|
-
start: Position;
|
|
8
|
-
end: Position;
|
|
9
|
-
};
|
|
3
|
+
export type { Position, Range };
|
|
10
4
|
export type StatementValue = {
|
|
11
5
|
text: string;
|
|
12
6
|
range: Range;
|
|
@@ -34,6 +28,7 @@ export type CursorContext = {
|
|
|
34
28
|
role: 'key' | 'value';
|
|
35
29
|
partial: string;
|
|
36
30
|
value: string;
|
|
31
|
+
range?: Range;
|
|
37
32
|
policyFormat: PolicyFormat;
|
|
38
33
|
};
|
|
39
34
|
export type StatementContext = {
|
|
@@ -2,8 +2,8 @@ import { Parser } from 'web-tree-sitter';
|
|
|
2
2
|
await Parser.init();
|
|
3
3
|
export function nodeRange(node) {
|
|
4
4
|
return {
|
|
5
|
-
start: { line: node.startPosition.row,
|
|
6
|
-
end: { line: node.endPosition.row,
|
|
5
|
+
start: { line: node.startPosition.row, character: node.startPosition.column },
|
|
6
|
+
end: { line: node.endPosition.row, character: node.endPosition.column },
|
|
7
7
|
};
|
|
8
8
|
}
|
|
9
9
|
export class TreeBase {
|
|
@@ -40,7 +40,7 @@ export class TreeBase {
|
|
|
40
40
|
return null;
|
|
41
41
|
const node = tree.rootNode.descendantForPosition({
|
|
42
42
|
row: position.line,
|
|
43
|
-
column: position.
|
|
43
|
+
column: position.character,
|
|
44
44
|
});
|
|
45
45
|
return node;
|
|
46
46
|
}
|
|
@@ -11,7 +11,9 @@ export class TreeHcl extends TreeBase {
|
|
|
11
11
|
const node = this.getNodeAtPosition(uri, position);
|
|
12
12
|
if (!node)
|
|
13
13
|
return null;
|
|
14
|
-
const nodeBefore = position.
|
|
14
|
+
const nodeBefore = position.character > 0
|
|
15
|
+
? this.getNodeAtPosition(uri, { line: position.line, character: position.character - 1 })
|
|
16
|
+
: null;
|
|
15
17
|
// Try jsonencode mode first (more specific structure)
|
|
16
18
|
const jsonencodeContext = this.#tryJsonencodeMode(node, nodeBefore, position);
|
|
17
19
|
if (jsonencodeContext)
|
|
@@ -174,8 +176,8 @@ export class TreeHcl extends TreeBase {
|
|
|
174
176
|
const valueRange = expression
|
|
175
177
|
? nodeRange(expression)
|
|
176
178
|
: {
|
|
177
|
-
start: { line: child.endPosition.row,
|
|
178
|
-
end: { line: child.endPosition.row,
|
|
179
|
+
start: { line: child.endPosition.row, character: child.endPosition.column },
|
|
180
|
+
end: { line: child.endPosition.row, character: child.endPosition.column },
|
|
179
181
|
};
|
|
180
182
|
entries.push({ key: id.text, keyRange, values, valueRange });
|
|
181
183
|
}
|
|
@@ -242,8 +244,8 @@ export class TreeHcl extends TreeBase {
|
|
|
242
244
|
const valueRange = valueExpression
|
|
243
245
|
? nodeRange(valueExpression)
|
|
244
246
|
: {
|
|
245
|
-
start: { line: child.endPosition.row,
|
|
246
|
-
end: { line: child.endPosition.row,
|
|
247
|
+
start: { line: child.endPosition.row, character: child.endPosition.column },
|
|
248
|
+
end: { line: child.endPosition.row, character: child.endPosition.column },
|
|
247
249
|
};
|
|
248
250
|
const nestedKeys = new Set(['Condition', 'Principal', 'NotPrincipal']);
|
|
249
251
|
let children;
|
|
@@ -310,8 +312,8 @@ export class TreeHcl extends TreeBase {
|
|
|
310
312
|
const valueRange = valueExpression
|
|
311
313
|
? nodeRange(valueExpression)
|
|
312
314
|
: {
|
|
313
|
-
start: { line: child.endPosition.row,
|
|
314
|
-
end: { line: child.endPosition.row,
|
|
315
|
+
start: { line: child.endPosition.row, character: child.endPosition.column },
|
|
316
|
+
end: { line: child.endPosition.row, character: child.endPosition.column },
|
|
315
317
|
};
|
|
316
318
|
let nestedObject;
|
|
317
319
|
if (valueExpression) {
|
|
@@ -424,8 +426,8 @@ export class TreeHcl extends TreeBase {
|
|
|
424
426
|
}
|
|
425
427
|
if (role === null)
|
|
426
428
|
role = 'key';
|
|
427
|
-
const { partial, value } = this.#extractJsonencodePartialAndValue(cursorNode, position);
|
|
428
|
-
return { keys, role, partial, value };
|
|
429
|
+
const { partial, value, range } = this.#extractJsonencodePartialAndValue(cursorNode, position);
|
|
430
|
+
return { keys, role, partial, value, range };
|
|
429
431
|
}
|
|
430
432
|
#getObjectElemKey(element) {
|
|
431
433
|
const keyExpression = element.namedChildren.find((child) => child.type === 'expression');
|
|
@@ -457,10 +459,11 @@ export class TreeHcl extends TreeBase {
|
|
|
457
459
|
while (current) {
|
|
458
460
|
if (current.type === 'identifier' || current.type === 'template_literal') {
|
|
459
461
|
const value = current.text;
|
|
462
|
+
const range = nodeRange(current);
|
|
460
463
|
if (position.line === current.startPosition.row) {
|
|
461
|
-
return { partial: value.slice(0, position.
|
|
464
|
+
return { partial: value.slice(0, position.character - current.startPosition.column), value, range };
|
|
462
465
|
}
|
|
463
|
-
return { partial: value, value };
|
|
466
|
+
return { partial: value, value, range };
|
|
464
467
|
}
|
|
465
468
|
if (current.type === 'object' || current.type === 'object_elem')
|
|
466
469
|
break;
|
|
@@ -488,7 +491,7 @@ export class TreeHcl extends TreeBase {
|
|
|
488
491
|
return this.#pickBestCursorContext(context1, context2);
|
|
489
492
|
}
|
|
490
493
|
// Cursor is on the block itself (outside body span) — statement-key level
|
|
491
|
-
const { partial, value } = this.#extractBlockPartialAndValue(node, position);
|
|
494
|
+
const { partial, value, range } = this.#extractBlockPartialAndValue(node, position);
|
|
492
495
|
// Check for incomplete attributes on the same line (may be in ERROR nodes
|
|
493
496
|
// when tree-sitter can't parse the body due to a missing value)
|
|
494
497
|
const searchParent = statementBody ?? statementBlock;
|
|
@@ -515,6 +518,7 @@ export class TreeHcl extends TreeBase {
|
|
|
515
518
|
role: 'key',
|
|
516
519
|
partial,
|
|
517
520
|
value,
|
|
521
|
+
range,
|
|
518
522
|
policyFormat: 'hcl-block',
|
|
519
523
|
};
|
|
520
524
|
}
|
|
@@ -603,8 +607,8 @@ export class TreeHcl extends TreeBase {
|
|
|
603
607
|
}
|
|
604
608
|
if (role === null)
|
|
605
609
|
role = 'key';
|
|
606
|
-
const { partial, value } = this.#extractBlockPartialAndValue(cursorNode, position);
|
|
607
|
-
return { keys, role, partial, value };
|
|
610
|
+
const { partial, value, range } = this.#extractBlockPartialAndValue(cursorNode, position);
|
|
611
|
+
return { keys, role, partial, value, range };
|
|
608
612
|
}
|
|
609
613
|
#findEnclosingBlock(node, blockName, statementBody) {
|
|
610
614
|
let current = node;
|
|
@@ -659,10 +663,11 @@ export class TreeHcl extends TreeBase {
|
|
|
659
663
|
while (current) {
|
|
660
664
|
if (current.type === 'identifier' || current.type === 'template_literal') {
|
|
661
665
|
const value = current.text;
|
|
666
|
+
const range = nodeRange(current);
|
|
662
667
|
if (position.line === current.startPosition.row) {
|
|
663
|
-
return { partial: value.slice(0, position.
|
|
668
|
+
return { partial: value.slice(0, position.character - current.startPosition.column), value, range };
|
|
664
669
|
}
|
|
665
|
-
return { partial: value, value };
|
|
670
|
+
return { partial: value, value, range };
|
|
666
671
|
}
|
|
667
672
|
if (current.type === 'body' || current.type === 'attribute' || current.type === 'block')
|
|
668
673
|
break;
|
|
@@ -711,7 +716,7 @@ export class TreeHcl extends TreeBase {
|
|
|
711
716
|
(nodeBefore && this.#isInsideQuote(nodeBefore)) ||
|
|
712
717
|
errorNode.children.some((child) => child.type === 'quoted_template_start' &&
|
|
713
718
|
child.startPosition.row === position.line &&
|
|
714
|
-
child.endPosition.column <= position.
|
|
719
|
+
child.endPosition.column <= position.character);
|
|
715
720
|
if (!hasQuoteOnLine)
|
|
716
721
|
return null;
|
|
717
722
|
let foundStatement = false;
|
|
@@ -804,12 +809,13 @@ export class TreeHcl extends TreeBase {
|
|
|
804
809
|
// extract from the template_literal. Otherwise extract from ERROR node text.
|
|
805
810
|
let partial = '';
|
|
806
811
|
let value = '';
|
|
812
|
+
let range;
|
|
807
813
|
if (!lastKeyFromAttribute) {
|
|
808
814
|
if (this.#isInsideQuote(node)) {
|
|
809
|
-
({ partial, value } = this.#extractQuotedPartialAndValue(node, position));
|
|
815
|
+
({ partial, value, range } = this.#extractQuotedPartialAndValue(node, position));
|
|
810
816
|
}
|
|
811
817
|
else if (nodeBefore && this.#isInsideQuote(nodeBefore)) {
|
|
812
|
-
({ partial, value } = this.#extractQuotedPartialAndValue(nodeBefore, position));
|
|
818
|
+
({ partial, value, range } = this.#extractQuotedPartialAndValue(nodeBefore, position));
|
|
813
819
|
}
|
|
814
820
|
else {
|
|
815
821
|
({ partial, value } = this.#extractPartialAndValueFromErrorNode(errorNode, position));
|
|
@@ -828,6 +834,7 @@ export class TreeHcl extends TreeBase {
|
|
|
828
834
|
role: 'value',
|
|
829
835
|
partial,
|
|
830
836
|
value,
|
|
837
|
+
range,
|
|
831
838
|
policyFormat,
|
|
832
839
|
};
|
|
833
840
|
}
|
|
@@ -840,10 +847,11 @@ export class TreeHcl extends TreeBase {
|
|
|
840
847
|
while (current) {
|
|
841
848
|
if (current.type === 'template_literal') {
|
|
842
849
|
const value = current.text;
|
|
850
|
+
const range = nodeRange(current);
|
|
843
851
|
if (position.line === current.startPosition.row) {
|
|
844
|
-
return { partial: value.slice(0, position.
|
|
852
|
+
return { partial: value.slice(0, position.character - current.startPosition.column), value, range };
|
|
845
853
|
}
|
|
846
|
-
return { partial: '', value };
|
|
854
|
+
return { partial: '', value, range };
|
|
847
855
|
}
|
|
848
856
|
if (current.type === 'quoted_template_start')
|
|
849
857
|
return { partial: '', value: '' };
|
|
@@ -865,10 +873,10 @@ export class TreeHcl extends TreeBase {
|
|
|
865
873
|
// like attributes have endPosition from a different row)
|
|
866
874
|
if (child.endPosition.row !== position.line)
|
|
867
875
|
continue;
|
|
868
|
-
if (child.endPosition.column <= position.
|
|
876
|
+
if (child.endPosition.column <= position.character) {
|
|
869
877
|
afterColumn = child.endPosition.column;
|
|
870
878
|
}
|
|
871
|
-
else if (nextChildColumn === null && child.startPosition.column > position.
|
|
879
|
+
else if (nextChildColumn === null && child.startPosition.column > position.character) {
|
|
872
880
|
nextChildColumn = child.startPosition.column;
|
|
873
881
|
}
|
|
874
882
|
}
|
|
@@ -878,7 +886,7 @@ export class TreeHcl extends TreeBase {
|
|
|
878
886
|
return { partial: '', value: '' };
|
|
879
887
|
const line = lines[lineIndex];
|
|
880
888
|
const startColumn = lineIndex === 0 ? errorNode.startPosition.column : 0;
|
|
881
|
-
const partial = line.slice(afterColumn - startColumn, position.
|
|
889
|
+
const partial = line.slice(afterColumn - startColumn, position.character - startColumn);
|
|
882
890
|
const valueEnd = nextChildColumn !== null ? nextChildColumn - startColumn : line.length;
|
|
883
891
|
const value = line.slice(afterColumn - startColumn, valueEnd);
|
|
884
892
|
return { partial, value };
|
|
@@ -11,7 +11,9 @@ export class TreeJson extends TreeBase {
|
|
|
11
11
|
const node = this.getNodeAtPosition(uri, position);
|
|
12
12
|
if (!node)
|
|
13
13
|
return null;
|
|
14
|
-
const nodeBefore = position.
|
|
14
|
+
const nodeBefore = position.character > 0
|
|
15
|
+
? this.getNodeAtPosition(uri, { line: position.line, character: position.character - 1 })
|
|
16
|
+
: null;
|
|
15
17
|
// Cursor right after closing quote — no completions
|
|
16
18
|
if (this.#isCursorAfterClosingQuote(nodeBefore))
|
|
17
19
|
return null;
|
|
@@ -36,8 +38,8 @@ export class TreeJson extends TreeBase {
|
|
|
36
38
|
if (!node)
|
|
37
39
|
return null;
|
|
38
40
|
let statementObject = this.#findStatementObject(node);
|
|
39
|
-
if (!statementObject && position.
|
|
40
|
-
const nodeBefore = this.getNodeAtPosition(uri, { line: position.line,
|
|
41
|
+
if (!statementObject && position.character > 0) {
|
|
42
|
+
const nodeBefore = this.getNodeAtPosition(uri, { line: position.line, character: position.character - 1 });
|
|
41
43
|
if (nodeBefore)
|
|
42
44
|
statementObject = this.#findStatementObject(nodeBefore);
|
|
43
45
|
}
|
|
@@ -50,8 +52,8 @@ export class TreeJson extends TreeBase {
|
|
|
50
52
|
if (!node)
|
|
51
53
|
return [];
|
|
52
54
|
let statementObject = this.#findStatementObject(node);
|
|
53
|
-
if (!statementObject && position.
|
|
54
|
-
const nodeBefore = this.getNodeAtPosition(uri, { line: position.line,
|
|
55
|
+
if (!statementObject && position.character > 0) {
|
|
56
|
+
const nodeBefore = this.getNodeAtPosition(uri, { line: position.line, character: position.character - 1 });
|
|
55
57
|
if (nodeBefore)
|
|
56
58
|
statementObject = this.#findStatementObject(nodeBefore);
|
|
57
59
|
}
|
|
@@ -178,8 +180,8 @@ export class TreeJson extends TreeBase {
|
|
|
178
180
|
const valueNode = pair.namedChildren[1];
|
|
179
181
|
if (!valueNode) {
|
|
180
182
|
return {
|
|
181
|
-
start: { line: pair.endPosition.row,
|
|
182
|
-
end: { line: pair.endPosition.row,
|
|
183
|
+
start: { line: pair.endPosition.row, character: pair.endPosition.column },
|
|
184
|
+
end: { line: pair.endPosition.row, character: pair.endPosition.column },
|
|
183
185
|
};
|
|
184
186
|
}
|
|
185
187
|
return nodeRange(valueNode);
|
|
@@ -247,8 +249,8 @@ export class TreeJson extends TreeBase {
|
|
|
247
249
|
}
|
|
248
250
|
if (role === null)
|
|
249
251
|
role = 'key';
|
|
250
|
-
const { partial, value } = this.#extractPartialAndValue(cursorNode, position);
|
|
251
|
-
return { keys, role, partial, value };
|
|
252
|
+
const { partial, value, range } = this.#extractPartialAndValue(cursorNode, position);
|
|
253
|
+
return { keys, role, partial, value, range };
|
|
252
254
|
}
|
|
253
255
|
/**
|
|
254
256
|
* Walk up from a node to find an object that sits inside a Statement array.
|
|
@@ -367,10 +369,11 @@ export class TreeJson extends TreeBase {
|
|
|
367
369
|
while (current) {
|
|
368
370
|
if (current.type === 'string_content') {
|
|
369
371
|
const value = current.text;
|
|
372
|
+
const range = nodeRange(current);
|
|
370
373
|
if (position.line === current.startPosition.row) {
|
|
371
|
-
return { partial: value.slice(0, position.
|
|
374
|
+
return { partial: value.slice(0, position.character - current.startPosition.column), value, range };
|
|
372
375
|
}
|
|
373
|
-
return { partial: value, value };
|
|
376
|
+
return { partial: value, value, range };
|
|
374
377
|
}
|
|
375
378
|
if (current.type === 'object' || current.type === 'pair')
|
|
376
379
|
break;
|
|
@@ -11,7 +11,9 @@ export class TreeYaml extends TreeBase {
|
|
|
11
11
|
const node = this.getNodeAtPosition(uri, position);
|
|
12
12
|
if (!node)
|
|
13
13
|
return null;
|
|
14
|
-
const nodeBefore = position.
|
|
14
|
+
const nodeBefore = position.character > 0
|
|
15
|
+
? this.getNodeAtPosition(uri, { line: position.line, character: position.character - 1 })
|
|
16
|
+
: null;
|
|
15
17
|
const root = this.getTree(uri)?.rootNode;
|
|
16
18
|
if (!root)
|
|
17
19
|
return null;
|
|
@@ -189,8 +191,8 @@ export class TreeYaml extends TreeBase {
|
|
|
189
191
|
if (pair.namedChildren.length < 2) {
|
|
190
192
|
// No value — zero-width range at key end
|
|
191
193
|
return {
|
|
192
|
-
start: { line: pair.endPosition.row,
|
|
193
|
-
end: { line: pair.endPosition.row,
|
|
194
|
+
start: { line: pair.endPosition.row, character: pair.endPosition.column },
|
|
195
|
+
end: { line: pair.endPosition.row, character: pair.endPosition.column },
|
|
194
196
|
};
|
|
195
197
|
}
|
|
196
198
|
return nodeRange(pair.namedChildren[1]);
|
|
@@ -236,10 +238,10 @@ export class TreeYaml extends TreeBase {
|
|
|
236
238
|
let extendedMatch = null;
|
|
237
239
|
for (const mapping of candidates) {
|
|
238
240
|
const pastEnd = position.line > mapping.endPosition.row ||
|
|
239
|
-
(position.line === mapping.endPosition.row && position.
|
|
241
|
+
(position.line === mapping.endPosition.row && position.character >= mapping.endPosition.column);
|
|
240
242
|
if (!pastEnd)
|
|
241
243
|
continue;
|
|
242
|
-
if (position.
|
|
244
|
+
if (position.character < mapping.startPosition.column)
|
|
243
245
|
continue;
|
|
244
246
|
// Don't extend across sibling statement items. If a sibling block_sequence_item
|
|
245
247
|
// starts between this mapping's end and the cursor, the cursor is in a different statement.
|
|
@@ -284,12 +286,13 @@ export class TreeYaml extends TreeBase {
|
|
|
284
286
|
const resolved = this.#resolveWithinMapping(statementMapping, position);
|
|
285
287
|
if (!resolved)
|
|
286
288
|
return null;
|
|
287
|
-
const { partial, value } = this.#extractPartialAndValue(cursorNode, position);
|
|
289
|
+
const { partial, value, range } = this.#extractPartialAndValue(cursorNode, position);
|
|
288
290
|
return {
|
|
289
291
|
keys: resolved.keys,
|
|
290
292
|
role: resolved.role,
|
|
291
293
|
partial,
|
|
292
294
|
value,
|
|
295
|
+
range,
|
|
293
296
|
policyFormat: 'standard',
|
|
294
297
|
};
|
|
295
298
|
}
|
|
@@ -413,7 +416,7 @@ export class TreeYaml extends TreeBase {
|
|
|
413
416
|
const keyColumn = ancestorMapping
|
|
414
417
|
? ancestorMapping.startPosition.column
|
|
415
418
|
: statementMapping.startPosition.column;
|
|
416
|
-
if (position.
|
|
419
|
+
if (position.character > keyColumn)
|
|
417
420
|
return null;
|
|
418
421
|
}
|
|
419
422
|
}
|
|
@@ -443,7 +446,7 @@ export class TreeYaml extends TreeBase {
|
|
|
443
446
|
const pairOnLine = current.namedChildren.find((child) => child.type === 'block_mapping_pair' &&
|
|
444
447
|
child.startPosition.row === position.line &&
|
|
445
448
|
child.namedChildren.length === 1 &&
|
|
446
|
-
position.
|
|
449
|
+
position.character > child.endPosition.column);
|
|
447
450
|
if (pairOnLine) {
|
|
448
451
|
const pairKey = this.#getPairKeyText(pairOnLine);
|
|
449
452
|
if (pairKey) {
|
|
@@ -457,7 +460,7 @@ export class TreeYaml extends TreeBase {
|
|
|
457
460
|
// When cursor lands on an inner block_mapping (not the statement mapping),
|
|
458
461
|
// check if the cursor is actually outside this mapping (column < key column).
|
|
459
462
|
// If so, skip it — the cursor belongs to a parent mapping level.
|
|
460
|
-
if (current.id !== statementMapping.id && position.
|
|
463
|
+
if (current.id !== statementMapping.id && position.character < current.startPosition.column) {
|
|
461
464
|
previous = current;
|
|
462
465
|
current = current.parent;
|
|
463
466
|
continue;
|
|
@@ -500,7 +503,7 @@ export class TreeYaml extends TreeBase {
|
|
|
500
503
|
// If the cursor column matches the parent mapping's key column, the user
|
|
501
504
|
// is typing a new sibling key, not editing this pair's value.
|
|
502
505
|
const parentMapping = current.parent;
|
|
503
|
-
const isAtParentKeyColumn = parentMapping?.type === 'block_mapping' && position.
|
|
506
|
+
const isAtParentKeyColumn = parentMapping?.type === 'block_mapping' && position.character === parentMapping.startPosition.column;
|
|
504
507
|
if (isOnKey || isAtParentKeyColumn) {
|
|
505
508
|
role = 'key';
|
|
506
509
|
}
|
|
@@ -532,10 +535,10 @@ export class TreeYaml extends TreeBase {
|
|
|
532
535
|
}
|
|
533
536
|
if (role === null)
|
|
534
537
|
role = 'key';
|
|
535
|
-
const { partial: extractedPartial, value: extractedValue } = this.#extractPartialAndValue(cursorNode, position);
|
|
538
|
+
const { partial: extractedPartial, value: extractedValue, range, } = this.#extractPartialAndValue(cursorNode, position);
|
|
536
539
|
const partial = colonPartial || extractedPartial;
|
|
537
540
|
const value = colonPartial ? colonPartial : extractedValue;
|
|
538
|
-
return { keys, role, partial, value };
|
|
541
|
+
return { keys, role, partial, value, range };
|
|
539
542
|
}
|
|
540
543
|
/**
|
|
541
544
|
* Resolve the cursor's position within a block_mapping by finding the pair covering
|
|
@@ -543,9 +546,9 @@ export class TreeYaml extends TreeBase {
|
|
|
543
546
|
*/
|
|
544
547
|
#resolveWithinMapping(mapping, position) {
|
|
545
548
|
const keyColumn = mapping.startPosition.column;
|
|
546
|
-
if (position.
|
|
549
|
+
if (position.character < keyColumn)
|
|
547
550
|
return null;
|
|
548
|
-
if (position.
|
|
551
|
+
if (position.character === keyColumn)
|
|
549
552
|
return { keys: [], role: 'key' };
|
|
550
553
|
let pairBeforeCursor = null;
|
|
551
554
|
for (const child of mapping.namedChildren) {
|
|
@@ -582,7 +585,7 @@ export class TreeYaml extends TreeBase {
|
|
|
582
585
|
const valueSequence = this.#findValueBlockSequence(pairBeforeCursor);
|
|
583
586
|
if (valueSequence &&
|
|
584
587
|
position.line <= valueSequence.endPosition.row &&
|
|
585
|
-
position.
|
|
588
|
+
position.character >= valueSequence.startPosition.column) {
|
|
586
589
|
return { keys: [pairKey], role: 'value' };
|
|
587
590
|
}
|
|
588
591
|
return null;
|
|
@@ -737,10 +740,10 @@ export class TreeYaml extends TreeBase {
|
|
|
737
740
|
let current = node;
|
|
738
741
|
while (current) {
|
|
739
742
|
if (current.type === 'string_scalar') {
|
|
740
|
-
return this.#sliceToPositionAndValue(current.text, current.startPosition.column, position, node);
|
|
743
|
+
return this.#sliceToPositionAndValue(current.text, current.startPosition.column, position, node, current);
|
|
741
744
|
}
|
|
742
745
|
if (current.type === 'double_quote_scalar' || current.type === 'single_quote_scalar') {
|
|
743
|
-
return this.#sliceToPositionAndValue(current.text.slice(1, -1), current.startPosition.column + 1, position, node);
|
|
746
|
+
return this.#sliceToPositionAndValue(current.text.slice(1, -1), current.startPosition.column + 1, position, node, current);
|
|
744
747
|
}
|
|
745
748
|
if (current.type === 'block_mapping' || current.type === 'block_mapping_pair')
|
|
746
749
|
break;
|
|
@@ -748,11 +751,11 @@ export class TreeYaml extends TreeBase {
|
|
|
748
751
|
}
|
|
749
752
|
return { partial: '', value: '' };
|
|
750
753
|
}
|
|
751
|
-
#sliceToPositionAndValue(text, startColumn, position, node) {
|
|
754
|
+
#sliceToPositionAndValue(text, startColumn, position, node, scalarNode) {
|
|
752
755
|
if (position.line === node.startPosition.row) {
|
|
753
|
-
return { partial: text.slice(0, position.
|
|
756
|
+
return { partial: text.slice(0, position.character - startColumn), value: text, range: nodeRange(scalarNode) };
|
|
754
757
|
}
|
|
755
|
-
return { partial: text, value: text };
|
|
758
|
+
return { partial: text, value: text, range: nodeRange(scalarNode) };
|
|
756
759
|
}
|
|
757
760
|
/**
|
|
758
761
|
* When tree-sitter produces a root-level ERROR (e.g., unterminated quote in
|
|
@@ -796,8 +799,8 @@ export class TreeYaml extends TreeBase {
|
|
|
796
799
|
if (!keyText)
|
|
797
800
|
continue;
|
|
798
801
|
// Cursor within the key text — still typing the key
|
|
799
|
-
if (position.
|
|
800
|
-
const partial = keyText.slice(0, position.
|
|
802
|
+
if (position.character < child.endPosition.column) {
|
|
803
|
+
const partial = keyText.slice(0, position.character - child.startPosition.column);
|
|
801
804
|
return {
|
|
802
805
|
keys: [],
|
|
803
806
|
role: 'key',
|
|
@@ -807,7 +810,7 @@ export class TreeYaml extends TreeBase {
|
|
|
807
810
|
};
|
|
808
811
|
}
|
|
809
812
|
// Cursor at key end — key is complete but ":" hasn't been typed yet
|
|
810
|
-
if (position.
|
|
813
|
+
if (position.character === child.endPosition.column) {
|
|
811
814
|
return null;
|
|
812
815
|
}
|
|
813
816
|
return {
|
|
@@ -826,7 +829,7 @@ export class TreeYaml extends TreeBase {
|
|
|
826
829
|
continue;
|
|
827
830
|
const valueText = this.#getPairValueText(child) ?? '';
|
|
828
831
|
// If cursor is past the ERROR end (colon continuation), append ":"
|
|
829
|
-
const colonSuffix = errorNode.endPosition.row === position.line && position.
|
|
832
|
+
const colonSuffix = errorNode.endPosition.row === position.line && position.character > errorNode.endPosition.column ? ':' : '';
|
|
830
833
|
const fullValue = valueText + colonSuffix;
|
|
831
834
|
return {
|
|
832
835
|
keys: [pairKey],
|
|
@@ -873,10 +876,10 @@ export class TreeYaml extends TreeBase {
|
|
|
873
876
|
for (const child of errorNode.children) {
|
|
874
877
|
if (child.startPosition.row !== position.line)
|
|
875
878
|
continue;
|
|
876
|
-
if (child.endPosition.column <= position.
|
|
879
|
+
if (child.endPosition.column <= position.character) {
|
|
877
880
|
afterColumn = child.endPosition.column;
|
|
878
881
|
}
|
|
879
|
-
else if (nextChildColumn === null && child.startPosition.column > position.
|
|
882
|
+
else if (nextChildColumn === null && child.startPosition.column > position.character) {
|
|
880
883
|
nextChildColumn = child.startPosition.column;
|
|
881
884
|
}
|
|
882
885
|
}
|
|
@@ -887,7 +890,7 @@ export class TreeYaml extends TreeBase {
|
|
|
887
890
|
return { partial: '', value: '' };
|
|
888
891
|
const line = lines[lineIndex];
|
|
889
892
|
const startColumn = lineIndex === 0 ? errorNode.startPosition.column : 0;
|
|
890
|
-
const partial = line.slice(afterColumn - startColumn, position.
|
|
893
|
+
const partial = line.slice(afterColumn - startColumn, position.character - startColumn);
|
|
891
894
|
const valueEnd = nextChildColumn !== null ? nextChildColumn - startColumn : line.length;
|
|
892
895
|
const value = line.slice(afterColumn - startColumn, valueEnd);
|
|
893
896
|
return { partial, value };
|