Skip to content
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
3 changes: 2 additions & 1 deletion client/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { registerWebview } from "./services/webview";
import { registerHover } from "./services/hover";
import { registerEvents } from "./services/events";
import { registerAutocomplete } from "./services/autocomplete";
import { registerCodeLens } from "./services/codelens";
import { runLanguageServer, stopLanguageServer } from "./lsp/server";
import { runClient, stopClient } from "./lsp/client";

Expand All @@ -25,6 +26,7 @@ export async function activate(context: vscode.ExtensionContext) {
registerEvents(context);
registerWebview(context);
registerAutocomplete(context);
registerCodeLens(context);
registerHover();
await applyItalicOverlay();
await startExtension(context);
Expand Down Expand Up @@ -100,4 +102,3 @@ export async function restartExtension(context: vscode.ExtensionContext) {
// start again
await startExtension(context);
}

41 changes: 41 additions & 0 deletions client/src/services/codelens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as vscode from "vscode";
import { extension } from "../state";
import type { LJDiagnostic } from "../types/diagnostics";
import { getDiagnosticRevealTarget } from "../webview/diagnostic-reveal";
import { normalizeFilePath } from "../utils/utils";

const codeLensEmitter = new vscode.EventEmitter<void>();

export function registerCodeLens(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.languages.registerCodeLensProvider("java", {
onDidChangeCodeLenses: codeLensEmitter.event,
provideCodeLenses(document) {
const file = normalizeFilePath(document.uri.fsPath);
return (extension.diagnostics || [])
.map(diagnostic => createDiagnosticCodeLens(diagnostic, file))
.filter((codeLens): codeLens is vscode.CodeLens => Boolean(codeLens));
}
})
);
}

export function refreshCodeLenses() {
codeLensEmitter.fire();
}

function createDiagnosticCodeLens(diagnostic: LJDiagnostic, file: string): vscode.CodeLens | undefined {
const targetDiagnostic = getDiagnosticRevealTarget(diagnostic);
if (!targetDiagnostic || targetDiagnostic.file !== file) return undefined;

const position = targetDiagnostic.position;
const range = new vscode.Range(
new vscode.Position(position.lineStart, position.colStart),
new vscode.Position(position.lineStart, position.colStart)
);
return new vscode.CodeLens(range, {
title: diagnostic.title,
command: "liquidjava.showView",
arguments: [targetDiagnostic]
});
}
2 changes: 2 additions & 0 deletions client/src/services/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { extension } from "../state";
import { LJDiagnostic } from "../types/diagnostics";
import { StatusBarState, updateStatusBar } from "./status-bar";
import { updateErrorAtCursor } from "./context";
import { refreshCodeLenses } from "./codelens";

/**
* Handles LiquidJava diagnostics received from the language server
Expand All @@ -13,6 +14,7 @@ export function handleLJDiagnostics(diagnostics: LJDiagnostic[]) {
const statusBarState: StatusBarState = containsError ? "failed" : "passed";
updateStatusBar(statusBarState);
extension.diagnostics = diagnostics;
refreshCodeLenses();
updateErrorAtCursor();
extension.webview?.sendMessage({ type: "diagnostics", diagnostics });
if (extension.context)
Expand Down
6 changes: 0 additions & 6 deletions client/src/services/hover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,6 @@ export function registerHover() {
}
}

const diagnostics = vscode.languages.getDiagnostics(document.uri);
const containsDiagnostic = !!diagnostics.find(d => d.range.contains(position) && d.source === 'liquidjava');
if (containsDiagnostic) {
if (hoverContent.value.length > 0) hoverContent.appendMarkdown(`\n\n`);
hoverContent.appendMarkdown(`[Open LiquidJava view](command:liquidjava.showView) for more details.`);
}
if (hoverContent.value.length === 0) return null;
return new vscode.Hover(hoverContent);
}
Expand Down
4 changes: 3 additions & 1 deletion client/src/services/webview.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as vscode from "vscode";
import { LiquidJavaWebviewProvider } from "../webview/provider";
import { extension } from "../state";
import type { DiagnosticRevealTarget } from "../types/diagnostics";

/**
* Initializes the webview panel for the extension
Expand All @@ -15,8 +16,9 @@ export function registerWebview(context: vscode.ExtensionContext) {
);
// show view command
context.subscriptions.push(
vscode.commands.registerCommand("liquidjava.showView", async () => {
vscode.commands.registerCommand("liquidjava.showView", async (diagnostic?: DiagnosticRevealTarget) => {
await vscode.commands.executeCommand("liquidJavaView.focus");
if (diagnostic) extension.webview.sendMessage({ type: "revealDiagnostic", diagnostic });
})
);
// listen for messages from the webview
Expand Down
7 changes: 6 additions & 1 deletion client/src/types/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,9 @@ export type ExternalMethodNotFoundWarning = BaseDiagnostic & {
overloads: string[];
}

export type RefinementMismatchError = RefinementError | StateRefinementError;
export type RefinementMismatchError = RefinementError | StateRefinementError;

export type DiagnosticRevealTarget = {
file: string;
position: SourcePosition;
}
31 changes: 31 additions & 0 deletions client/src/webview/diagnostic-reveal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { DiagnosticRevealTarget, LJDiagnostic } from "../types/diagnostics";

export function getDiagnosticRevealTargetKey(target: DiagnosticRevealTarget): string {
const { lineStart, colStart, lineEnd, colEnd } = target.position;
return `${target.file}:${lineStart}:${colStart}-${lineEnd}:${colEnd}`;
}

export function getDiagnosticRevealTarget(diagnostic: LJDiagnostic): DiagnosticRevealTarget | undefined {
if (!diagnostic.position) return undefined;
return {
file: diagnostic.position.file || diagnostic.file,
position: diagnostic.position
};
}

export function getDiagnosticRevealTargetFromKey(value?: string): DiagnosticRevealTarget | undefined {
const match = value?.match(/^(.*):(\d+):(\d+)-(\d+):(\d+)$/);
if (!match) return undefined;

const [, file, lineStart, colStart, lineEnd, colEnd] = match;
return {
file,
position: {
file,
lineStart: parseInt(lineStart, 10),
colStart: parseInt(colStart, 10),
lineEnd: parseInt(lineEnd, 10),
colEnd: parseInt(colEnd, 10)
}
};
}
55 changes: 52 additions & 3 deletions client/src/webview/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,22 @@ import type { NavTab } from "./views/sections";
import { copyDiagnosticToClipboard, getDisplayDiagnostics, renderDiagnosticsView } from "./views/diagnostics/diagnostics";
import type { LJContext } from "../types/context";
import { ContextSectionState, renderContextView } from "./views/context/context";
import type { DiagnosticRevealTarget } from "../types/diagnostics";
import { getDiagnosticRevealTargetFromKey, getDiagnosticRevealTargetKey } from "./diagnostic-reveal";

type VSCodeApi = {
postMessage(message: unknown): void;
};

/**
* Initializes the webview script
* @param vscode
* @param document
* @param window
*/
export function getScript(vscode: any, document: any, window: any) {
export function getScript(vscode: VSCodeApi, document: Document, window: Window) {
const root = document.getElementById('root');
if (!root) return;
let diagnostics: LJDiagnostic[] = [];
let showAllDiagnostics = false;
let currentFile: string;
Expand All @@ -28,6 +35,7 @@ export function getScript(vscode: any, document: any, window: any) {
let selectedTab: NavTab = 'diagnostics';
let diagramOrientation: "LR" | "TB" = "TB";
let currentDiagram: string = '';
let revealTimeout: ReturnType<typeof setTimeout> | undefined;
const contextSectionState: ContextSectionState = {
aliases: false,
ghosts: false,
Expand All @@ -39,8 +47,8 @@ export function getScript(vscode: any, document: any, window: any) {
vscode.postMessage({ type: 'ready' });

// on click
root.addEventListener('click', (e: any) => {
const target = e.target as any;
root.addEventListener('click', (e: MouseEvent) => {
const target = e.target instanceof Element ? e.target : null;
if (!target) return;

// context section toggle
Expand Down Expand Up @@ -90,6 +98,17 @@ export function getScript(vscode: any, document: any, window: any) {
return;
}

// reveal failing refinement diagnostic
if (target.classList.contains('diagnostic-reveal-btn')) {
e.preventDefault();
e.stopPropagation();

const revealTarget = getDiagnosticRevealTargetFromKey(target.getAttribute('data-diagnostic-target'));
if (!revealTarget) return;
revealDiagnostic(revealTarget);
return;
}

// derivation expansion click
if (target.classList.contains('derivable-node')) {
e.stopPropagation();
Expand Down Expand Up @@ -244,6 +263,9 @@ export function getScript(vscode: any, document: any, window: any) {
errorAtCursor = msg.errorAtCursor as RefinementMismatchError;
if (selectedTab === 'context') updateView();
break;
case 'revealDiagnostic':
revealDiagnostic(msg.diagnostic as DiagnosticRevealTarget);
break;
}
});

Expand All @@ -266,4 +288,31 @@ export function getScript(vscode: any, document: any, window: any) {
break;
}
}

function revealDiagnostic(target: DiagnosticRevealTarget) {
selectedTab = 'diagnostics';

const isVisibleInCurrentFile = showAllDiagnostics || !target.file || target.file.toLowerCase() === currentFile?.toLowerCase();
if (!isVisibleInCurrentFile) {
showAllDiagnostics = true;
}
Comment on lines +295 to +298
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here.


updateView();
const element = Array.from(root.querySelectorAll<HTMLElement>('.diagnostic-item')).find(item =>
item.getAttribute('data-diagnostic-target') === getDiagnosticRevealTargetKey(target)
);
if (!element) return;

const previousRevealed = root.querySelector('.diagnostic-item.revealed');
if (previousRevealed) previousRevealed.classList.remove('revealed');
if (revealTimeout) clearTimeout(revealTimeout);

element.classList.add('revealed');
element.scrollIntoView({ block: 'center', behavior: 'smooth' });
revealTimeout = setTimeout(() => {
element.classList.remove('revealed');
revealTimeout = undefined;
}, 1800);
}

}
43 changes: 33 additions & 10 deletions client/src/webview/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,30 @@ export function getStyles(): string {
opacity: 0.8;
cursor: default;
}
.diagnostic-item.revealed {
outline: 2px solid var(--vscode-focusBorder);
animation: diagnostic-reveal-flash 1.8s ease-out;
}
@keyframes diagnostic-reveal-flash {
0% {
box-shadow: 0 0 0 0 var(--vscode-focusBorder);
transform: translateX(0);
}
12% {
box-shadow: 0 0 0 3px var(--vscode-focusBorder);
transform: translateX(3px);
}
24% {
transform: translateX(0);
}
55% {
box-shadow: 0 0 0 2px var(--vscode-focusBorder);
}
100% {
box-shadow: 0 0 0 0 transparent;
transform: translateX(0);
}
}
.error-item {
border-left: 4px solid var(--vscode-editorError-foreground);
}
Expand Down Expand Up @@ -335,15 +359,10 @@ export function getStyles(): string {
.context-variables-table th:first-child {
padding-left: calc(0.75rem + 0.8rem);
}

.context-variables-table th:last-child,
.context-section table td:last-child {
text-align: left;
}
.context-variables-table td.failing-refinement {
text-align: center;
}
.failing-refinement .highlight-var-btn {
.failing-refinement .diagnostic-reveal-btn {
display: flex;
justify-content: center;
width: 100%;
Expand Down Expand Up @@ -375,26 +394,30 @@ export function getStyles(): string {
.context-section-content.collapsed {
display: none;
}
.highlight-var-btn {
.highlight-var-btn,
.diagnostic-reveal-btn {
background-color: transparent;
border: none;
transition: background-color 0.1s;
text-align: left;
padding: 0.2rem 0.8rem;
}
.highlight-var-btn code {
.highlight-var-btn code,
.diagnostic-reveal-btn code {
pointer-events: none;
}
.highlight-var-btn.selected {
background-color: var(--vscode-button-background);
}
.highlight-var-btn.error {
.highlight-var-btn.error,
.diagnostic-reveal-btn.error {
background-color: #d6382f;
}
.highlight-var-btn.error.selected {
background-color: #c92e26;
}
.highlight-var-btn.error:hover {
.highlight-var-btn.error:hover,
.diagnostic-reveal-btn.error:hover {
background-color: #c92e26;
}
.diagram-section {
Expand Down
6 changes: 3 additions & 3 deletions client/src/webview/views/context/variables.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { LJVariable } from "../../../types/context";
import { RefinementMismatchError } from "../../../types/diagnostics";
import { renderToggleSection, renderHighlightButton } from "../sections";
import { renderToggleSection, renderHighlightButton, renderDiagnosticRevealButton } from "../sections";

export function renderContextVariables(variables: LJVariable[], isExpanded: boolean, errorAtCursor?: RefinementMismatchError): string {
const expected = errorAtCursor ? errorAtCursor.type == "refinement-error" ? errorAtCursor.expected.value : errorAtCursor.expected : undefined;
Expand All @@ -27,8 +27,8 @@ export function renderContextVariables(variables: LJVariable[], isExpanded: bool
<td><code>${variable.refinement}</code></td>
</tr>
`).join('')}
${errorAtCursor ? /*html*/`
<tr><td class="failing-refinement" colspan="2">${renderHighlightButton(errorAtCursor.position, '⊢ ' + expected, true)}</td></tr>`
${errorAtCursor?.position ? /*html*/`
<tr><td class="failing-refinement" colspan="2">${renderDiagnosticRevealButton(errorAtCursor.position, '⊢ ' + expected)}</td></tr>`
: ''}
</tbody>
</table>
Expand Down
4 changes: 2 additions & 2 deletions client/src/webview/views/diagnostics/errors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { renderDiagnosticHeader, renderLocation, renderSection, renderCustomSection, renderTranslationTable, } from "../sections";
import { renderDiagnosticDataAttributes, renderDiagnosticHeader, renderLocation, renderSection, renderCustomSection, renderTranslationTable, } from "../sections";
import { renderDerivationNode } from "./derivation-nodes";
import type {
ArgumentMismatchError,
Expand All @@ -19,7 +19,7 @@ export function renderErrors(errors: LJError[], expandedErrors: Set<number>): st
${errors.map((error, index) => {
const isExpanded = expandedErrors.has(index);
return /*html*/`
<li class="diagnostic-item error-item">
<li class="diagnostic-item error-item" ${renderDiagnosticDataAttributes(error)}>
${renderCopyDiagnosticButton('error', index)}
${renderError(error, index, isExpanded)}
</li>
Expand Down
6 changes: 3 additions & 3 deletions client/src/webview/views/diagnostics/warnings.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { ExternalClassNotFoundWarning, ExternalMethodNotFoundWarning, LJWarning } from "../../../types/diagnostics";
import { renderDiagnosticHeader, renderLocation, renderSection } from "../sections";
import { renderDiagnosticDataAttributes, renderDiagnosticHeader, renderLocation, renderSection } from "../sections";
import { renderCopyDiagnosticButton } from "./diagnostics";

export function renderWarnings(warnings: LJWarning[]): string {
return /*html*/`
<ul>
${warnings.map((warning, index) => /*html*/`
<li class="diagnostic-item warning-item">
${warnings.map((warning, index) => /*html*/`
<li class="diagnostic-item warning-item" ${renderDiagnosticDataAttributes(warning)}>
${renderCopyDiagnosticButton('warning', index)}
${renderWarning(warning)}
</li>
Expand Down
Loading