|
1 | 1 | import * as vscode from 'vscode'; |
2 | 2 | import { extension } from '../state'; |
3 | | -import type { Range, LJVariable } from '../types/context'; |
| 3 | +import type { LJMethod, LJVariable } from '../types/context'; |
4 | 4 | import { getSelectionContextVariables } from './context'; |
5 | | -import { getOriginalVariableName, normalizeFilePath, toRange } from '../utils/utils'; |
| 5 | +import { getOriginalVariableName, normalizeFilePath } from '../utils/utils'; |
| 6 | +import { definitionMatchesClass, getDefinitions } from './definition'; |
6 | 7 |
|
7 | 8 | /** |
8 | 9 | * Initializes hover provider for LiquidJava diagnostics |
9 | 10 | */ |
10 | 11 | export function registerHover() { |
11 | 12 | vscode.languages.registerHoverProvider('java', { |
12 | | - provideHover(document, position) { |
| 13 | + async provideHover(document, position) { |
13 | 14 | const hoverContent = new vscode.MarkdownString(); |
14 | 15 | hoverContent.isTrusted = true; |
15 | 16 |
|
16 | 17 | const variable = getHoveredVariable(document, position); |
17 | 18 | if (variable && variable.mainRefinement && variable.mainRefinement !== 'true') |
18 | | - hoverContent.appendCodeblock(`@Refinement("${variable.mainRefinement}")`, 'java'); |
| 19 | + hoverContent.appendCodeblock(formatRefinement(variable.mainRefinement), 'java'); |
| 20 | + else { |
| 21 | + const method = await getHoveredMethod(document, position); |
| 22 | + if (method) hoverContent.appendCodeblock(formatMethodHover(method), 'java'); |
| 23 | + } |
19 | 24 |
|
20 | 25 | const diagnostics = vscode.languages.getDiagnostics(document.uri); |
21 | 26 | const containsDiagnostic = !!diagnostics.find(d => d.range.contains(position) && d.source === 'liquidjava'); |
@@ -48,3 +53,63 @@ function getHoveredVariable(document: vscode.TextDocument, position: vscode.Posi |
48 | 53 | const { allVars } = getSelectionContextVariables(file, positionAfterVariable); |
49 | 54 | return allVars.find(variable => getOriginalVariableName(variable.name) === hoveredWord); |
50 | 55 | } |
| 56 | + |
| 57 | +async function getHoveredMethod(document: vscode.TextDocument, position: vscode.Position): Promise<LJMethod | null> { |
| 58 | + if (!extension.context) return null; |
| 59 | + |
| 60 | + const wordRange = document.getWordRangeAtPosition(position, /[A-Za-z_][A-Za-z0-9_]*/); |
| 61 | + if (!wordRange) return null; |
| 62 | + |
| 63 | + const hoveredWord = document.getText(wordRange); |
| 64 | + const file = normalizeFilePath(document.uri.fsPath); |
| 65 | + const methods = extension.context.methods.filter(method => methodNameMatches(method, hoveredWord)); |
| 66 | + |
| 67 | + const definitions = await getDefinitions(document, position); |
| 68 | + const resolvedMethod = methods.find(method => definitions.some(definition => definitionMatchesClass(definition, method.targetClass))); |
| 69 | + if (resolvedMethod) return resolvedMethod; |
| 70 | + |
| 71 | + const receiver = document.lineAt(wordRange.start.line).text |
| 72 | + .slice(0, wordRange.start.character) |
| 73 | + .match(/([A-Za-z_][A-Za-z0-9_]*)\s*\.\s*$/)?.[1]; |
| 74 | + if (!receiver) return null; |
| 75 | + const receiverVariable = [...(extension.context?.globalVars || []), ...(extension.context?.localVars || [])] |
| 76 | + .find(variable => |
| 77 | + getOriginalVariableName(variable.name) === receiver && |
| 78 | + (!variable.position || variable.position.file === file && isBefore(variable.position, wordRange.start)) |
| 79 | + ); |
| 80 | + if (!receiverVariable) return null; |
| 81 | + |
| 82 | + return methods.find(method => typeMatchesTargetClass(receiverVariable.type, method.targetClass)) || null; |
| 83 | +} |
| 84 | + |
| 85 | +function methodNameMatches(method: LJMethod, hoveredWord: string): boolean { |
| 86 | + return method.name === hoveredWord || method.name.endsWith(`.${hoveredWord}`); |
| 87 | +} |
| 88 | + |
| 89 | +function typeMatchesTargetClass(type: string, targetClass: string): boolean { |
| 90 | + return type === targetClass || targetClass.endsWith(`.${type}`) || type.endsWith(`.${targetClass}`); |
| 91 | +} |
| 92 | + |
| 93 | +function isBefore(range: { lineStart: number; colStart: number }, position: vscode.Position): boolean { |
| 94 | + return range.lineStart < position.line || range.lineStart === position.line && range.colStart < position.character; |
| 95 | +} |
| 96 | + |
| 97 | +function formatRefinement(refinement: string): string { |
| 98 | + return `@Refinement("${refinement}")`; |
| 99 | +} |
| 100 | + |
| 101 | +function formatStateRefinement(from: string | null, to: string | null): string { |
| 102 | + return `@StateRefinement(${[from && `from="${from}"`, to && `to="${to}"`].filter(Boolean).join(', ')})`; |
| 103 | +} |
| 104 | + |
| 105 | +function formatMethodHover(method: LJMethod): string { |
| 106 | + return [ |
| 107 | + method.returnRefinement && method.returnRefinement !== 'true' && formatRefinement(method.returnRefinement), |
| 108 | + ...method.parameters |
| 109 | + .filter(p => p.mainRefinement && p.mainRefinement !== 'true') |
| 110 | + .map(p => `${formatRefinement(p.mainRefinement)} ${p.type} ${p.name}`), |
| 111 | + ...method.stateRefinements |
| 112 | + .filter(s => s.from || s.to) |
| 113 | + .map(s => formatStateRefinement(s.from, s.to)) |
| 114 | + ].filter(Boolean).join('\n'); |
| 115 | +} |
0 commit comments