Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat(intention): add "Add explicit type" intention #127

Merged
merged 1 commit into from
Feb 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions server/src/intentions/AddExplicitType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {Intention, IntentionContext} from "@server/intentions/Intention"
import {WorkspaceEdit} from "vscode-languageserver"
import {File} from "@server/psi/File"
import {asParserPoint} from "@server/utils/position"
import {Position} from "vscode-languageclient"
import {VarDeclaration} from "@server/psi/Node"
import {FileDiff} from "@server/utils/FileDiff"

export class AddExplicitType implements Intention {
id: string = "tact.add-explicit-type"
name: string = "Add explicit type"

findVariable(ctx: IntentionContext): VarDeclaration | null {
const referenceNode = nodeAtPosition(ctx.position, ctx.file)
if (referenceNode?.type !== "identifier") return null
const parent = referenceNode.parent
if (parent?.type !== "let_statement") return null
return new VarDeclaration(parent, ctx.file)
}

is_available(ctx: IntentionContext): boolean {
const variable = this.findVariable(ctx)
if (!variable) return false
return !variable.hasTypeHint()
}

invoke(ctx: IntentionContext): WorkspaceEdit | null {
const variable = this.findVariable(ctx)
if (!variable) return null
if (variable.hasTypeHint()) return null

const name = variable.nameIdentifier()
if (!name) return null

const inferredType = variable.value()?.type()
if (!inferredType) return null

const diff = FileDiff.forFile(ctx.file.uri)
diff.appendTo(name.endPosition.row, name.endPosition.column, `: ${inferredType.name()}`)

return diff.toWorkspaceEdit()
}
}

function nodeAtPosition(pos: Position, file: File) {
const cursorPosition = asParserPoint(pos)
return file.rootNode.descendantForPosition(cursorPosition)
}
22 changes: 22 additions & 0 deletions server/src/intentions/Intention.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {WorkspaceEdit} from "vscode-languageserver"
import {File} from "@server/psi/File"
import {Position} from "vscode-languageclient"

export interface IntentionContext {
file: File
position: Position
}

export interface IntentionArguments {
fileUri: string
position: Position
}

export interface Intention {
id: string
name: string

is_available(ctx: IntentionContext): boolean

invoke(ctx: IntentionContext): WorkspaceEdit | null
}
126 changes: 101 additions & 25 deletions server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ import {TraitOrContractConstantsCompletionProvider} from "@server/completion/pro
import {generateTlBTypeDoc} from "@server/documentation/tlb_type_documentation"
import {BouncedTypeCompletionProvider} from "@server/completion/providers/BouncedTypeCompletionProvider"
import {TopLevelCompletionProvider} from "@server/completion/providers/TopLevelCompletionProvider"
import {Intention, IntentionArguments, IntentionContext} from "@server/intentions/Intention"
import {AddExplicitType} from "@server/intentions/AddExplicitType"

/**
* Whenever LS is initialized.
Expand Down Expand Up @@ -1003,6 +1005,100 @@ connection.onInitialize(async (params: lsp.InitializeParams): Promise<lsp.Initia
},
)

const intentions: Intention[] = [new AddExplicitType()]

connection.onRequest(
lsp.ExecuteCommandRequest.type,
async (params: lsp.ExecuteCommandParams) => {
if (params.command === "tact/executeGetScopeProvider") {
const commandParams = params.arguments?.[0] as
| lsp.TextDocumentPositionParams
| undefined
if (!commandParams) return "Invalid parameters"

const file = PARSED_FILES_CACHE.get(commandParams.textDocument.uri)
if (!file) {
return "File not found"
}

const node = nodeAtPosition(commandParams, file)
if (!node) {
return "Node not found"
}

const referent = new Referent(node, file)
const scope = referent.useScope()
if (!scope) return "Scope not found"
if (scope instanceof LocalSearchScope) return scope.toString()
return "GlobalSearchScope"
}

if (!params.arguments || params.arguments.length === 0) return null

const intention = intentions.find(intention => intention.id === params.command)
if (!intention) return null

const args = params.arguments[0] as IntentionArguments

const file = findFile(args.fileUri)

const ctx: IntentionContext = {
file: file,
position: args.position,
}

const edits = intention.invoke(ctx)
if (!edits) return null

await connection.sendRequest(lsp.ApplyWorkspaceEditRequest.method, {
label: `Intention "${intention.name}"`,
edit: edits,
} as lsp.ApplyWorkspaceEditParams)

return null
},
)

connection.onRequest(
lsp.CodeActionRequest.type,
(params: lsp.CodeActionParams): lsp.CodeAction[] | null => {
const uri = params.textDocument.uri
if (uri.endsWith(".fif")) {
return null
}

const file = findFile(uri)

const ctx: IntentionContext = {
file: file,
position: params.range.start,
}

const actions: lsp.CodeAction[] = []

intentions.forEach(intention => {
if (!intention.is_available(ctx)) return

actions.push({
title: intention.name,
kind: lsp.CodeActionKind.QuickFix,
command: {
title: intention.name,
command: intention.id,
arguments: [
{
fileUri: file.uri,
position: params.range.start,
} as IntentionArguments,
],
},
})
})

return actions
},
)

connection.onRequest(
GetTypeAtPositionRequest,
(params: GetTypeAtPositionParams): GetTypeAtPositionResponse => {
Expand Down Expand Up @@ -1207,37 +1303,14 @@ connection.onInitialize(async (params: lsp.InitializeParams): Promise<lsp.Initia
},
)

connection.onExecuteCommand(params => {
if (params.command !== "tact/executeGetScopeProvider") return

const commandParams = params.arguments?.[0] as lsp.TextDocumentPositionParams | undefined
if (!commandParams) return "Invalid parameters"

const file = PARSED_FILES_CACHE.get(commandParams.textDocument.uri)
if (!file) {
return "File not found"
}

const node = nodeAtPosition(commandParams, file)
if (!node) {
return "Node not found"
}

const referent = new Referent(node, file)
const scope = referent.useScope()
if (!scope) return "Scope not found"
if (scope instanceof LocalSearchScope) return scope.toString()
return "GlobalSearchScope"
})

// noinspection JSUnusedLocalSymbols
const _needed = TypeInferer.inferType

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

return {
capabilities: {
textDocumentSync: lsp.TextDocumentSyncKind.Incremental,
// codeActionProvider: true,
documentSymbolProvider: true,
workspaceSymbolProvider: true,
definitionProvider: true,
Expand Down Expand Up @@ -1269,8 +1342,11 @@ connection.onInitialize(async (params: lsp.InitializeParams): Promise<lsp.Initia
codeLensProvider: {
resolveProvider: false,
},
codeActionProvider: {
codeActionKinds: [lsp.CodeActionKind.QuickFix],
},
executeCommandProvider: {
commands: ["tact/executeGetScopeProvider"],
commands: [...["tact/executeGetScopeProvider"], ...intentions.map(it => it.id)],
},
},
}
Expand Down