Skip to content

Commit e0851bf

Browse files
authored
feat(intention): add "Add explicit type" intention (#127)
Fixes #126
1 parent 51931bf commit e0851bf

File tree

3 files changed

+171
-25
lines changed

3 files changed

+171
-25
lines changed
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import {Intention, IntentionContext} from "@server/intentions/Intention"
2+
import {WorkspaceEdit} from "vscode-languageserver"
3+
import {File} from "@server/psi/File"
4+
import {asParserPoint} from "@server/utils/position"
5+
import {Position} from "vscode-languageclient"
6+
import {VarDeclaration} from "@server/psi/Node"
7+
import {FileDiff} from "@server/utils/FileDiff"
8+
9+
export class AddExplicitType implements Intention {
10+
id: string = "tact.add-explicit-type"
11+
name: string = "Add explicit type"
12+
13+
findVariable(ctx: IntentionContext): VarDeclaration | null {
14+
const referenceNode = nodeAtPosition(ctx.position, ctx.file)
15+
if (referenceNode?.type !== "identifier") return null
16+
const parent = referenceNode.parent
17+
if (parent?.type !== "let_statement") return null
18+
return new VarDeclaration(parent, ctx.file)
19+
}
20+
21+
is_available(ctx: IntentionContext): boolean {
22+
const variable = this.findVariable(ctx)
23+
if (!variable) return false
24+
return !variable.hasTypeHint()
25+
}
26+
27+
invoke(ctx: IntentionContext): WorkspaceEdit | null {
28+
const variable = this.findVariable(ctx)
29+
if (!variable) return null
30+
if (variable.hasTypeHint()) return null
31+
32+
const name = variable.nameIdentifier()
33+
if (!name) return null
34+
35+
const inferredType = variable.value()?.type()
36+
if (!inferredType) return null
37+
38+
const diff = FileDiff.forFile(ctx.file.uri)
39+
diff.appendTo(name.endPosition.row, name.endPosition.column, `: ${inferredType.name()}`)
40+
41+
return diff.toWorkspaceEdit()
42+
}
43+
}
44+
45+
function nodeAtPosition(pos: Position, file: File) {
46+
const cursorPosition = asParserPoint(pos)
47+
return file.rootNode.descendantForPosition(cursorPosition)
48+
}

server/src/intentions/Intention.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {WorkspaceEdit} from "vscode-languageserver"
2+
import {File} from "@server/psi/File"
3+
import {Position} from "vscode-languageclient"
4+
5+
export interface IntentionContext {
6+
file: File
7+
position: Position
8+
}
9+
10+
export interface IntentionArguments {
11+
fileUri: string
12+
position: Position
13+
}
14+
15+
export interface Intention {
16+
id: string
17+
name: string
18+
19+
is_available(ctx: IntentionContext): boolean
20+
21+
invoke(ctx: IntentionContext): WorkspaceEdit | null
22+
}

server/src/server.ts

+101-25
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ import {TraitOrContractConstantsCompletionProvider} from "@server/completion/pro
8080
import {generateTlBTypeDoc} from "@server/documentation/tlb_type_documentation"
8181
import {BouncedTypeCompletionProvider} from "@server/completion/providers/BouncedTypeCompletionProvider"
8282
import {TopLevelCompletionProvider} from "@server/completion/providers/TopLevelCompletionProvider"
83+
import {Intention, IntentionArguments, IntentionContext} from "@server/intentions/Intention"
84+
import {AddExplicitType} from "@server/intentions/AddExplicitType"
8385

8486
/**
8587
* Whenever LS is initialized.
@@ -1003,6 +1005,100 @@ connection.onInitialize(async (params: lsp.InitializeParams): Promise<lsp.Initia
10031005
},
10041006
)
10051007

1008+
const intentions: Intention[] = [new AddExplicitType()]
1009+
1010+
connection.onRequest(
1011+
lsp.ExecuteCommandRequest.type,
1012+
async (params: lsp.ExecuteCommandParams) => {
1013+
if (params.command === "tact/executeGetScopeProvider") {
1014+
const commandParams = params.arguments?.[0] as
1015+
| lsp.TextDocumentPositionParams
1016+
| undefined
1017+
if (!commandParams) return "Invalid parameters"
1018+
1019+
const file = PARSED_FILES_CACHE.get(commandParams.textDocument.uri)
1020+
if (!file) {
1021+
return "File not found"
1022+
}
1023+
1024+
const node = nodeAtPosition(commandParams, file)
1025+
if (!node) {
1026+
return "Node not found"
1027+
}
1028+
1029+
const referent = new Referent(node, file)
1030+
const scope = referent.useScope()
1031+
if (!scope) return "Scope not found"
1032+
if (scope instanceof LocalSearchScope) return scope.toString()
1033+
return "GlobalSearchScope"
1034+
}
1035+
1036+
if (!params.arguments || params.arguments.length === 0) return null
1037+
1038+
const intention = intentions.find(intention => intention.id === params.command)
1039+
if (!intention) return null
1040+
1041+
const args = params.arguments[0] as IntentionArguments
1042+
1043+
const file = findFile(args.fileUri)
1044+
1045+
const ctx: IntentionContext = {
1046+
file: file,
1047+
position: args.position,
1048+
}
1049+
1050+
const edits = intention.invoke(ctx)
1051+
if (!edits) return null
1052+
1053+
await connection.sendRequest(lsp.ApplyWorkspaceEditRequest.method, {
1054+
label: `Intention "${intention.name}"`,
1055+
edit: edits,
1056+
} as lsp.ApplyWorkspaceEditParams)
1057+
1058+
return null
1059+
},
1060+
)
1061+
1062+
connection.onRequest(
1063+
lsp.CodeActionRequest.type,
1064+
(params: lsp.CodeActionParams): lsp.CodeAction[] | null => {
1065+
const uri = params.textDocument.uri
1066+
if (uri.endsWith(".fif")) {
1067+
return null
1068+
}
1069+
1070+
const file = findFile(uri)
1071+
1072+
const ctx: IntentionContext = {
1073+
file: file,
1074+
position: params.range.start,
1075+
}
1076+
1077+
const actions: lsp.CodeAction[] = []
1078+
1079+
intentions.forEach(intention => {
1080+
if (!intention.is_available(ctx)) return
1081+
1082+
actions.push({
1083+
title: intention.name,
1084+
kind: lsp.CodeActionKind.QuickFix,
1085+
command: {
1086+
title: intention.name,
1087+
command: intention.id,
1088+
arguments: [
1089+
{
1090+
fileUri: file.uri,
1091+
position: params.range.start,
1092+
} as IntentionArguments,
1093+
],
1094+
},
1095+
})
1096+
})
1097+
1098+
return actions
1099+
},
1100+
)
1101+
10061102
connection.onRequest(
10071103
GetTypeAtPositionRequest,
10081104
(params: GetTypeAtPositionParams): GetTypeAtPositionResponse => {
@@ -1207,37 +1303,14 @@ connection.onInitialize(async (params: lsp.InitializeParams): Promise<lsp.Initia
12071303
},
12081304
)
12091305

1210-
connection.onExecuteCommand(params => {
1211-
if (params.command !== "tact/executeGetScopeProvider") return
1212-
1213-
const commandParams = params.arguments?.[0] as lsp.TextDocumentPositionParams | undefined
1214-
if (!commandParams) return "Invalid parameters"
1215-
1216-
const file = PARSED_FILES_CACHE.get(commandParams.textDocument.uri)
1217-
if (!file) {
1218-
return "File not found"
1219-
}
1220-
1221-
const node = nodeAtPosition(commandParams, file)
1222-
if (!node) {
1223-
return "Node not found"
1224-
}
1225-
1226-
const referent = new Referent(node, file)
1227-
const scope = referent.useScope()
1228-
if (!scope) return "Scope not found"
1229-
if (scope instanceof LocalSearchScope) return scope.toString()
1230-
return "GlobalSearchScope"
1231-
})
1232-
1306+
// noinspection JSUnusedLocalSymbols
12331307
const _needed = TypeInferer.inferType
12341308

12351309
console.info("Tact language server is ready!")
12361310

12371311
return {
12381312
capabilities: {
12391313
textDocumentSync: lsp.TextDocumentSyncKind.Incremental,
1240-
// codeActionProvider: true,
12411314
documentSymbolProvider: true,
12421315
workspaceSymbolProvider: true,
12431316
definitionProvider: true,
@@ -1269,8 +1342,11 @@ connection.onInitialize(async (params: lsp.InitializeParams): Promise<lsp.Initia
12691342
codeLensProvider: {
12701343
resolveProvider: false,
12711344
},
1345+
codeActionProvider: {
1346+
codeActionKinds: [lsp.CodeActionKind.QuickFix],
1347+
},
12721348
executeCommandProvider: {
1273-
commands: ["tact/executeGetScopeProvider"],
1349+
commands: [...["tact/executeGetScopeProvider"], ...intentions.map(it => it.id)],
12741350
},
12751351
},
12761352
}

0 commit comments

Comments
 (0)