Skip to content

Commit 2b276c0

Browse files
jmbockhorstrazzeee
authored andcommitted
Add a code action for extracting a type alias
1 parent 176ff82 commit 2b276c0

File tree

8 files changed

+792
-22
lines changed

8 files changed

+792
-22
lines changed

src/providers/codeAction/extractFunctionCodeAction.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ CodeActionProvider.registerRefactorAction(refactorName, {
2222

2323
if (
2424
node.type.includes("expr") &&
25+
node.type !== "type_expression" &&
2526
node.startPosition.column === params.range.start.character &&
2627
node.startPosition.row === params.range.start.line &&
2728
node.endPosition.column === params.range.end.character &&
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import { CodeActionKind, Position, TextEdit } from "vscode-languageserver";
2+
import { SyntaxNode } from "web-tree-sitter";
3+
import { RefactorEditUtils } from "../../util/refactorEditUtils";
4+
import { TreeUtils } from "../../util/treeUtils";
5+
import {
6+
CodeActionProvider,
7+
IRefactorCodeAction,
8+
IRefactorEdit,
9+
} from "../codeActionProvider";
10+
import { ICodeActionParams } from "../paramsExtensions";
11+
12+
const refactorName = "extract_type_alias";
13+
CodeActionProvider.registerRefactorAction(refactorName, {
14+
getAvailableActions: (params: ICodeActionParams): IRefactorCodeAction[] => {
15+
const result: IRefactorCodeAction[] = [];
16+
17+
const node = TreeUtils.getNamedDescendantForRange(
18+
params.sourceFile,
19+
params.range,
20+
);
21+
22+
let canExtract =
23+
node.type.includes("type") &&
24+
node.startPosition.column === params.range.start.character &&
25+
node.startPosition.row === params.range.start.line &&
26+
node.endPosition.column === params.range.end.character &&
27+
node.endPosition.row === params.range.end.line;
28+
29+
let actionName = "extract_type_alias";
30+
31+
const rootNode = params.sourceFile.tree.rootNode;
32+
let startNode = node;
33+
let endNode = node;
34+
if (!canExtract) {
35+
startNode = TreeUtils.getDescendantForPosition(
36+
rootNode,
37+
params.range.start,
38+
);
39+
40+
const previousCharColumn =
41+
params.range.end.character === 0 ? 0 : params.range.end.character - 1;
42+
const charBeforeCursor = rootNode.text
43+
.split("\n")
44+
[params.range.end.line].substring(
45+
previousCharColumn,
46+
params.range.end.character,
47+
);
48+
49+
if (charBeforeCursor === ")") {
50+
const endNode = rootNode.descendantForPosition({
51+
row: params.range.end.line,
52+
column: previousCharColumn,
53+
});
54+
55+
if (startNode.type === "(" && endNode.type === ")") {
56+
const node = startNode.nextNamedSibling;
57+
canExtract =
58+
!!node &&
59+
node.id == endNode.previousNamedSibling?.id &&
60+
node.type.includes("type");
61+
actionName = "extract_type_alias_parenthesized_expr";
62+
}
63+
}
64+
}
65+
66+
if (!canExtract) {
67+
endNode = TreeUtils.getDescendantForPosition(rootNode, params.range.end);
68+
69+
// Try to see if they are spanning multiple parameters of a function
70+
const startTypeRef = TreeUtils.findParentOfType("type_ref", startNode);
71+
const endTypeRef = TreeUtils.findParentOfType("type_ref", endNode);
72+
73+
if (startTypeRef && endTypeRef) {
74+
const startTypeExpr = TreeUtils.findParentOfType(
75+
"type_expression",
76+
startTypeRef,
77+
);
78+
const endTypeExpr = TreeUtils.findParentOfType(
79+
"type_expression",
80+
endTypeRef,
81+
);
82+
83+
// They must be from the same type expression
84+
if (
85+
startTypeExpr &&
86+
endTypeExpr &&
87+
startTypeExpr.id === endTypeExpr.id
88+
) {
89+
canExtract =
90+
startTypeRef.startPosition.column ===
91+
params.range.start.character &&
92+
startTypeRef.startPosition.row === params.range.start.line &&
93+
endTypeRef.endPosition.column === params.range.end.character &&
94+
endTypeRef.endPosition.row === params.range.end.line;
95+
actionName = "extract_type_alias_partial_type_expr";
96+
}
97+
}
98+
}
99+
100+
if (canExtract) {
101+
result.push({
102+
title: "Extract type alias",
103+
kind: CodeActionKind.RefactorExtract,
104+
data: {
105+
actionName,
106+
refactorName,
107+
uri: params.sourceFile.uri,
108+
range: params.range,
109+
},
110+
});
111+
}
112+
113+
return result;
114+
},
115+
getEditsForAction: (
116+
params: ICodeActionParams,
117+
action: string,
118+
): IRefactorEdit => {
119+
const edits: TextEdit[] = [];
120+
121+
const nodes: SyntaxNode[] = [];
122+
if (action === "extract_type_alias_partial_type_expr") {
123+
const startNode = TreeUtils.getNamedDescendantForPosition(
124+
params.sourceFile.tree.rootNode,
125+
params.range.start,
126+
);
127+
128+
const endNode = TreeUtils.getNamedDescendantForPosition(
129+
params.sourceFile.tree.rootNode,
130+
params.range.end,
131+
);
132+
133+
const typeExpression = TreeUtils.findParentOfType(
134+
"type_expression",
135+
startNode,
136+
);
137+
138+
typeExpression?.namedChildren
139+
.filter(
140+
(n) =>
141+
n.type === "type_ref" &&
142+
n.startIndex >= startNode.startIndex &&
143+
n.endIndex <= endNode.endIndex,
144+
)
145+
.forEach((n) => nodes.push(n));
146+
} else if (action === "extract_type_alias_parenthesized_expr") {
147+
const exprNode = TreeUtils.getDescendantForPosition(
148+
params.sourceFile.tree.rootNode,
149+
params.range.start,
150+
).nextNamedSibling;
151+
152+
if (!exprNode) {
153+
throw new Error(
154+
"Could not find expression node of parenthisized expression",
155+
);
156+
}
157+
158+
nodes.push(exprNode);
159+
} else {
160+
nodes.push(
161+
TreeUtils.getDescendantForRange(params.sourceFile, params.range),
162+
);
163+
}
164+
165+
const rootNode = params.sourceFile.tree.rootNode;
166+
167+
const insertPosition: Position = {
168+
line:
169+
RefactorEditUtils.findLineNumberBeforeCurrentFunction(nodes[0]) ??
170+
rootNode.endPosition.row,
171+
character: 0,
172+
};
173+
174+
const args: string[] = [];
175+
176+
nodes.forEach((node) =>
177+
node.descendantsOfType(["type_variable"]).forEach((val) => {
178+
if (!args.includes(val.text)) {
179+
args.push(val.text);
180+
}
181+
}),
182+
);
183+
184+
const typeText = nodes.map((n) => n.text).join(" -> ");
185+
186+
edits.push(
187+
RefactorEditUtils.createTypeAlias(
188+
insertPosition.line,
189+
"NewType",
190+
typeText,
191+
args,
192+
),
193+
);
194+
195+
let textToInsert =
196+
args.length > 0 ? `NewType ${args.join(" ")}` : `NewType`;
197+
198+
const needsParenthesis =
199+
action === "extract_type_alias_parenthesized_expr" && args.length > 0;
200+
if (needsParenthesis) {
201+
textToInsert = `(${textToInsert})`;
202+
}
203+
204+
edits.push(TextEdit.replace(params.range, textToInsert));
205+
206+
// Check if we are adding the function before the current range and adjust the rename position
207+
const linesAdded =
208+
edits[0].range.start.line < params.range.start.line
209+
? edits[0].newText.split("\n").length - 1
210+
: 0;
211+
212+
return {
213+
edits,
214+
renamePosition: {
215+
line: params.range.start.line + linesAdded,
216+
character: needsParenthesis
217+
? params.range.start.character + 1
218+
: params.range.start.character,
219+
},
220+
};
221+
},
222+
});

src/providers/codeAction/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import "./makeDeclarationFromUsageCodeAction";
33
import "./makeExternalDeclarationFromUsageCodeAction";
44
import "./addTypeAnnotationCodeAction";
55
import "./extractFunctionCodeAction";
6+
import "./extractTypeAliasCodeAction";
67
import "./exposeUnexposeCodeAction";
78
import "./moveFunctionCodeAction";
89
import "./installPackageCodeAction";

src/util/refactorEditUtils.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,21 @@ export class RefactorEditUtils {
1616

1717
return this.findLineNumberAfterCurrentFunction(nodeAtPosition.parent);
1818
}
19+
20+
public static findLineNumberBeforeCurrentFunction(
21+
nodeAtPosition: SyntaxNode,
22+
): number | undefined {
23+
if (!nodeAtPosition.parent) {
24+
return undefined;
25+
}
26+
27+
if (nodeAtPosition.parent?.type === "file") {
28+
return nodeAtPosition.startPosition.row - 1;
29+
}
30+
31+
return this.findLineNumberBeforeCurrentFunction(nodeAtPosition.parent);
32+
}
33+
1934
public static unexposedValueInModule(
2035
tree: Tree,
2136
valueName: string,
@@ -98,6 +113,20 @@ export class RefactorEditUtils {
98113
}
99114
}
100115

116+
public static createTypeAlias(
117+
insertLineNumber: number,
118+
aliasName: string,
119+
typeString: string,
120+
typeVariables: string[],
121+
): TextEdit {
122+
const typeVariablesString =
123+
typeVariables.length > 0 ? ` ${typeVariables.join(" ")}` : "";
124+
return TextEdit.insert(
125+
Position.create(insertLineNumber, 0),
126+
`\ntype alias ${aliasName}${typeVariablesString} = ${typeString}\n\n`,
127+
);
128+
}
129+
101130
private static argListFromArity(arity: number): string {
102131
return [...Array(arity).keys()].map((a) => `arg${a + 1}`).join(" ");
103132
}

src/util/treeUtils.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,35 @@ export class TreeUtils {
601601
}
602602
}
603603

604+
public static getDescendantForPosition(
605+
node: SyntaxNode,
606+
position: Position,
607+
): SyntaxNode {
608+
const previousCharColumn =
609+
position.character === 0 ? 0 : position.character - 1;
610+
const charBeforeCursor = node.text
611+
.split("\n")
612+
[position.line].substring(previousCharColumn, position.character);
613+
614+
if (!functionNameRegex.test(charBeforeCursor)) {
615+
return node.descendantForPosition({
616+
column: position.character,
617+
row: position.line,
618+
});
619+
} else {
620+
return node.descendantForPosition(
621+
{
622+
column: previousCharColumn,
623+
row: position.line,
624+
},
625+
{
626+
column: position.character,
627+
row: position.line,
628+
},
629+
);
630+
}
631+
}
632+
604633
public static getNamedDescendantForRange(
605634
sourceFile: ISourceFile,
606635
range: Range,
@@ -624,6 +653,29 @@ export class TreeUtils {
624653
}
625654
}
626655

656+
public static getDescendantForRange(
657+
sourceFile: ISourceFile,
658+
range: Range,
659+
): SyntaxNode {
660+
if (positionEquals(range.start, range.end)) {
661+
return this.getDescendantForPosition(sourceFile.tree.rootNode, {
662+
character: range.start.character,
663+
line: range.start.line,
664+
});
665+
} else {
666+
return sourceFile.tree.rootNode.descendantForPosition(
667+
{
668+
column: range.start.character,
669+
row: range.start.line,
670+
},
671+
{
672+
column: range.end.character,
673+
row: range.end.line,
674+
},
675+
);
676+
}
677+
}
678+
627679
public static findPreviousNode(
628680
node: SyntaxNode,
629681
position: Position,

0 commit comments

Comments
 (0)