Skip to content

Commit 388c2e1

Browse files
committed
Add functions for parsing query output to suggestion options
1 parent d0bb66e commit 388c2e1

File tree

2 files changed

+353
-0
lines changed

2 files changed

+353
-0
lines changed
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { parseAccessPathTokens } from "./shared/access-paths";
2+
import type { AccessPathOption, AccessPathSuggestionRow } from "./suggestions";
3+
import { AccessPathSuggestionDefinitionType } from "./suggestions";
4+
5+
const CodiconSymbols: Record<AccessPathSuggestionDefinitionType, string> = {
6+
[AccessPathSuggestionDefinitionType.Array]: "symbol-array",
7+
[AccessPathSuggestionDefinitionType.Class]: "symbol-class",
8+
[AccessPathSuggestionDefinitionType.Enum]: "symbol-enum",
9+
[AccessPathSuggestionDefinitionType.EnumMember]: "symbol-enum-member",
10+
[AccessPathSuggestionDefinitionType.Field]: "symbol-field",
11+
[AccessPathSuggestionDefinitionType.Interface]: "symbol-interface",
12+
[AccessPathSuggestionDefinitionType.Key]: "symbol-key",
13+
[AccessPathSuggestionDefinitionType.Method]: "symbol-method",
14+
[AccessPathSuggestionDefinitionType.Misc]: "symbol-misc",
15+
[AccessPathSuggestionDefinitionType.Namespace]: "symbol-namespace",
16+
[AccessPathSuggestionDefinitionType.Parameter]: "symbol-parameter",
17+
[AccessPathSuggestionDefinitionType.Property]: "symbol-property",
18+
[AccessPathSuggestionDefinitionType.Structure]: "symbol-structure",
19+
[AccessPathSuggestionDefinitionType.Return]: "symbol-method",
20+
[AccessPathSuggestionDefinitionType.Variable]: "symbol-variable",
21+
};
22+
23+
/**
24+
* Parses the query results from a parsed array of rows to a list of options per method signature.
25+
*
26+
* @param rows The parsed rows from the BQRS chunk
27+
* @return A map from method signature -> options
28+
*/
29+
export function parseAccessPathSuggestionRowsToOptions(
30+
rows: AccessPathSuggestionRow[],
31+
): Record<string, AccessPathOption[]> {
32+
const rowsByMethodSignature = new Map<string, AccessPathSuggestionRow[]>();
33+
34+
for (const row of rows) {
35+
if (!rowsByMethodSignature.has(row.method.signature)) {
36+
rowsByMethodSignature.set(row.method.signature, []);
37+
}
38+
39+
const tuplesForMethodSignature = rowsByMethodSignature.get(
40+
row.method.signature,
41+
);
42+
if (!tuplesForMethodSignature) {
43+
throw new Error("Expected the map to have a value for method signature");
44+
}
45+
46+
tuplesForMethodSignature.push(row);
47+
}
48+
49+
const result: Record<string, AccessPathOption[]> = {};
50+
51+
for (const [methodSignature, tuples] of rowsByMethodSignature) {
52+
result[methodSignature] = parseQueryResultsForPath(tuples);
53+
}
54+
55+
return result;
56+
}
57+
58+
function parseQueryResultsForPath(
59+
rows: AccessPathSuggestionRow[],
60+
): AccessPathOption[] {
61+
const optionsByParentPath = new Map<string, AccessPathOption[]>();
62+
63+
for (const { value, details, definitionType } of rows) {
64+
const tokens = parseAccessPathTokens(value);
65+
const lastToken = tokens[tokens.length - 1];
66+
67+
const parentPath = tokens
68+
.slice(0, tokens.length - 1)
69+
.map((token) => token.text)
70+
.join(".");
71+
72+
const option: AccessPathOption = {
73+
label: lastToken.text,
74+
value,
75+
details,
76+
icon: CodiconSymbols[definitionType],
77+
followup: [],
78+
};
79+
80+
if (!optionsByParentPath.has(parentPath)) {
81+
optionsByParentPath.set(parentPath, []);
82+
}
83+
84+
const options = optionsByParentPath.get(parentPath);
85+
if (!options) {
86+
throw new Error(
87+
"Expected optionsByParentPath to have a value for parentPath",
88+
);
89+
}
90+
91+
options.push(option);
92+
}
93+
94+
for (const options of optionsByParentPath.values()) {
95+
options.sort(compareOptions);
96+
}
97+
98+
for (const options of optionsByParentPath.values()) {
99+
for (const option of options) {
100+
const followup = optionsByParentPath.get(option.value);
101+
if (followup) {
102+
option.followup = followup;
103+
}
104+
}
105+
}
106+
107+
const rootOptions = optionsByParentPath.get("");
108+
if (!rootOptions) {
109+
throw new Error("Expected optionsByParentPath to have a value for ''");
110+
}
111+
112+
return rootOptions;
113+
}
114+
115+
/**
116+
* Compares two options based on a set of predefined rules.
117+
*
118+
* The rules are as follows:
119+
* - Argument[self] is always first
120+
* - Positional arguments (Argument[0], Argument[1], etc.) are sorted in order and are after Argument[self]
121+
* - Keyword arguments (Argument[key:], etc.) are sorted by name and are after the positional arguments
122+
* - Block arguments (Argument[block]) are sorted after keyword arguments
123+
* - Hash splat arguments (Argument[hash-splat]) are sorted after block arguments
124+
* - Parameters (Parameter[0], Parameter[1], etc.) are sorted after and in-order
125+
* - All other values are sorted alphabetically after parameters
126+
*
127+
* @param {Option} a - The first option to compare.
128+
* @param {Option} b - The second option to compare.
129+
* @returns {number} - Returns -1 if a < b, 1 if a > b, 0 if a = b.
130+
*/
131+
function compareOptions(a: AccessPathOption, b: AccessPathOption): number {
132+
const positionalArgRegex = /^Argument\[\d+]$/;
133+
const keywordArgRegex = /^Argument\[[^\d:]+:]$/;
134+
const parameterRegex = /^Parameter\[\d+]$/;
135+
136+
// Check for Argument[self]
137+
if (a.label === "Argument[self]" && b.label !== "Argument[self]") {
138+
return -1;
139+
} else if (b.label === "Argument[self]" && a.label !== "Argument[self]") {
140+
return 1;
141+
}
142+
143+
// Check for positional arguments
144+
const aIsPositional = positionalArgRegex.test(a.label);
145+
const bIsPositional = positionalArgRegex.test(b.label);
146+
if (aIsPositional && bIsPositional) {
147+
return a.label.localeCompare(b.label, "en-US", { numeric: true });
148+
} else if (aIsPositional) {
149+
return -1;
150+
} else if (bIsPositional) {
151+
return 1;
152+
}
153+
154+
// Check for keyword arguments
155+
const aIsKeyword = keywordArgRegex.test(a.label);
156+
const bIsKeyword = keywordArgRegex.test(b.label);
157+
if (aIsKeyword && bIsKeyword) {
158+
return a.label.localeCompare(b.label, "en-US");
159+
} else if (aIsKeyword) {
160+
return -1;
161+
} else if (bIsKeyword) {
162+
return 1;
163+
}
164+
165+
// Check for Argument[block]
166+
if (a.label === "Argument[block]" && b.label !== "Argument[block]") {
167+
return -1;
168+
} else if (b.label === "Argument[block]" && a.label !== "Argument[block]") {
169+
return 1;
170+
}
171+
172+
// Check for Argument[hash-splat]
173+
if (
174+
a.label === "Argument[hash-splat]" &&
175+
b.label !== "Argument[hash-splat]"
176+
) {
177+
return -1;
178+
} else if (
179+
b.label === "Argument[hash-splat]" &&
180+
a.label !== "Argument[hash-splat]"
181+
) {
182+
return 1;
183+
}
184+
185+
// Check for parameters
186+
const aIsParameter = parameterRegex.test(a.label);
187+
const bIsParameter = parameterRegex.test(b.label);
188+
if (aIsParameter && bIsParameter) {
189+
return a.label.localeCompare(b.label, "en-US", { numeric: true });
190+
} else if (aIsParameter) {
191+
return -1;
192+
} else if (bIsParameter) {
193+
return 1;
194+
}
195+
196+
// If none of the above rules apply, compare alphabetically
197+
return a.label.localeCompare(b.label, "en-US");
198+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import type { AccessPathSuggestionRow } from "../../../src/model-editor/suggestions";
2+
import { parseAccessPathSuggestionRowsToOptions } from "../../../src/model-editor/suggestions-bqrs";
3+
4+
describe("parseAccessPathSuggestionRowsToOptions", () => {
5+
const rows = [
6+
{
7+
method: {
8+
packageName: "",
9+
typeName: "Jekyll::Utils",
10+
methodName: "transform_keys",
11+
methodParameters: "",
12+
signature: "Jekyll::Utils#transform_keys",
13+
},
14+
value: "Argument[0]",
15+
details: "hash",
16+
definitionType: "parameter",
17+
},
18+
{
19+
method: {
20+
packageName: "",
21+
typeName: "Jekyll::Utils",
22+
methodName: "transform_keys",
23+
methodParameters: "",
24+
signature: "Jekyll::Utils#transform_keys",
25+
},
26+
value: "ReturnValue",
27+
details: "result",
28+
definitionType: "return",
29+
},
30+
{
31+
method: {
32+
packageName: "",
33+
typeName: "Jekyll::Utils",
34+
methodName: "transform_keys",
35+
methodParameters: "",
36+
signature: "Jekyll::Utils#transform_keys",
37+
},
38+
value: "Argument[self]",
39+
details: "self in transform_keys",
40+
definitionType: "parameter",
41+
},
42+
{
43+
method: {
44+
packageName: "",
45+
typeName: "Jekyll::Utils",
46+
methodName: "transform_keys",
47+
methodParameters: "",
48+
signature: "Jekyll::Utils#transform_keys",
49+
},
50+
value: "Argument[block].Parameter[0]",
51+
details: "key",
52+
definitionType: "parameter",
53+
},
54+
{
55+
method: {
56+
packageName: "",
57+
typeName: "Jekyll::Utils",
58+
methodName: "transform_keys",
59+
methodParameters: "",
60+
signature: "Jekyll::Utils#transform_keys",
61+
},
62+
value: "Argument[block]",
63+
details: "yield ...",
64+
definitionType: "parameter",
65+
},
66+
] as AccessPathSuggestionRow[];
67+
68+
it("should parse the AccessPathSuggestionRows", async () => {
69+
const expectedOptions = {
70+
"Jekyll::Utils#transform_keys": [
71+
{
72+
label: "Argument[self]",
73+
value: "Argument[self]",
74+
details: "self in transform_keys",
75+
icon: "symbol-parameter",
76+
followup: [],
77+
},
78+
{
79+
label: "Argument[0]",
80+
value: "Argument[0]",
81+
details: "hash",
82+
icon: "symbol-parameter",
83+
followup: [],
84+
},
85+
{
86+
label: "Argument[block]",
87+
value: "Argument[block]",
88+
details: "yield ...",
89+
icon: "symbol-parameter",
90+
followup: [
91+
{
92+
label: "Parameter[0]",
93+
value: "Argument[block].Parameter[0]",
94+
details: "key",
95+
icon: "symbol-parameter",
96+
followup: [],
97+
},
98+
],
99+
},
100+
{
101+
label: "ReturnValue",
102+
value: "ReturnValue",
103+
details: "result",
104+
icon: "symbol-method",
105+
followup: [],
106+
},
107+
],
108+
};
109+
110+
const reorderedOptions = {
111+
"Jekyll::Utils#transform_keys": [
112+
{
113+
label: "Argument[self]",
114+
value: "Argument[self]",
115+
details: "self in transform_keys",
116+
icon: "symbol-parameter",
117+
followup: [],
118+
},
119+
{
120+
label: "Argument[block]",
121+
value: "Argument[block]",
122+
details: "yield ...",
123+
icon: "symbol-parameter",
124+
followup: [
125+
{
126+
label: "Parameter[0]",
127+
value: "Argument[block].Parameter[0]",
128+
details: "key",
129+
icon: "symbol-parameter",
130+
followup: [],
131+
},
132+
],
133+
},
134+
{
135+
label: "Argument[0]",
136+
value: "Argument[0]",
137+
details: "hash",
138+
icon: "symbol-parameter",
139+
followup: [],
140+
},
141+
{
142+
label: "ReturnValue",
143+
value: "ReturnValue",
144+
details: "result",
145+
icon: "symbol-method",
146+
followup: [],
147+
},
148+
],
149+
};
150+
151+
const options = parseAccessPathSuggestionRowsToOptions(rows);
152+
expect(options).toEqual(expectedOptions);
153+
expect(options).not.toEqual(reorderedOptions);
154+
});
155+
});

0 commit comments

Comments
 (0)