Skip to content
Open
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
41 changes: 41 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,44 @@ To run the language server manually, follow these steps:
### Project Structure
- `/server` - Implements the language server in Java using [LSP4J](https://github.com/eclipse/lsp4j)
- `/client` - Implements the VS Code extension in TypeScript that connects to the language server via LSP

### Local study logging

Study logging is disabled by default. To enable it, add these settings to the study workspace's `.vscode/settings.json`:

```json
{
"liquidjava.study.enabled": true,
"liquidjava.study.participantId": "P01",
"liquidjava.study.logPath": ".liquidjava/study-log.jsonl"
}
```

The log path is relative to the first workspace folder, which is also the folder the verifier checks. Absolute paths and paths outside that folder are rejected. Logging requires a local filesystem workspace. Changes to any study setting take effect immediately; disabling logging flushes pending events and removes study editor listeners and timers. When disabled, the extension creates no log file and performs no study writes, diagnostic hashing, or server timing notifications. The configuration listener remains active so logging can be enabled later.

Run **LiquidJava: Reveal Study Log** to flush and open the JSONL file. Each line is a JSON object with `t` (UTC ISO timestamp), `pid` (participant ID), `session` (UUID for this extension activation), `event`, and the fields below. Events use workspace-relative paths with `/` separators and **one-based** lines and columns. Logs append across launches; settings changes and server restarts retain the activation's session ID.

| Event | Fields / meaning |
| --- | --- |
| `logging_started`, `logging_stopped` | Boundaries of each enabled logging interval; includes settings changes and normal shutdown. |
| `file_opened`, `file_focused`, `file_blurred`, `file_saved` | `file`; Java files inside the workspace only. Initial focus is recorded even if the file was already open. Switching to another file, a non-Java editor, or no editor ends focus. |
| `file_edited` | `file`, `count`; number of content changes, batched after 500 ms of quiet, and flushed on save, file switch, or shutdown. No edited text is stored. |
| `window_focus`, `window_blur` | VS Code window focus, including its initial state. |
| `verify_started` | `file`, `trigger` (`open`, `save`, `manual`), `run`; emitted when the server begins a queued verification. Run IDs remain unique after server restarts. |
| `verify_finished` | Same fields plus `durationMs` and `result` (`passed`, `failed`, `crashed`, `cancelled`). Duration excludes queue wait. Stopping the server or logging cancels pending runs; cancellations use elapsed client time. |
| `diagnostic_shown`, `diagnostic_resolved` | `file`, `line`, `column` (null if unavailable), `kind` (the diagnostic type), `category`, `key`. Repeated results do not repeat appearances. Only successful diagnostic results resolve previous errors; crashes and stops do not. |
| `view_visible`, `view_hidden` | LiquidJava sidebar visibility, including initial state when logging is enabled. |
| `tab_selected` | `tab` (`diagnostics`, `context`, `fsm`), `file` when available; includes initial selection and diagnostic context / state-machine actions. |
| `section_toggled` | `section` (`context-vars`, `context-ghosts`, `context-aliases`), `expanded`, `file`. |
| `section_shown` | `section` (`counterexample`, `vc-implications`, `hint`), `file`; these sections currently render without collapse controls. Records transitions into the rendered view; redraws of continuously visible sections are deduplicated. |
| `vc_step_selected` | `direction` (`previous`, `next`), `file`; records simplification steps, including the displayed changes between implications. |
| `diagnostic_reveal` | `file`, `line`, `column`; navigation from the webview to source. |
| `highlight` | `file`, `line`, `active` for same-file highlights; cross-file navigation also records its highlight. |
| `clipboard_copy` | `target` (`diagnostic`, `fsm`), `file`; emitted after a successful copy, without clipboard contents. |
| `hover_shown` | `file`, `line`, `column`; LiquidJava supplied a nonempty hover (VS Code does not expose whether it was ultimately displayed). |
| `codelens_clicked` | `file`, `line`; diagnostic CodeLens activation. |
| `command_run` | `command`; registered `liquidjava.*` commands, including Reveal Study Log. |

To compute time focused per exercise, intersect `file_focused`/`file_blurred` intervals with `window_focus`/`window_blur` and `logging_started`/`logging_stopped` intervals. Edits and saves provide activity counts; the analysis can choose its own idle threshold. Match diagnostic appearances and resolutions using `key` to compute observed time-to-fix. Keys preserve the nearest previous diagnostic of the same file, kind, and category when edits move its line, which suits the study's one intended error per exercise. Multiple identical errors are matched by proximity; replacing one with another of the same kind may retain its key. An abrupt process exit has no reliable end event: treat the final interval as incomplete rather than assuming it ended at a later launch.

No source code, expressions, diagnostic messages, counterexamples, hover contents, or clipboard text are logged. Nothing is uploaded, and the VS Code telemetry API is not used. Participants can inspect the file and hand it in themselves. Keep `.liquidjava/` out of the study repository's `.gitignore` if collecting with git. AI explanation events are deferred until the explanation UI in issue #113 exists.
8 changes: 5 additions & 3 deletions client/.vscode-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ const version = process.env.VSCODE_TEST_VERSION === 'minimum'
? minVersion(engines.vscode).version
: 'stable';

export default defineConfig(['failing', 'passing'].map(fixture => ({
export default defineConfig(['failing', 'passing', 'study'].map(fixture => ({
label: fixture,
files: fixture === 'failing' ? 'out/test/**/*.test.js' : 'out/test/smoke.test.js',
files: fixture === 'study' ? 'out/test/study-log.test.js'
: fixture === 'failing' ? ['out/test/lifecycle.test.js', 'out/test/smoke.test.js']
: 'out/test/smoke.test.js',
version,
workspaceFolder: `test-fixtures/${fixture}`,
workspaceFolder: `test-fixtures/${fixture === 'study' ? 'failing' : fixture}`,
extensionDevelopmentPath: '.',
launchArgs: ['--disable-extensions', '--disable-workspace-trust'],
mocha: { ui: 'tdd', timeout: 120_000 },
Expand Down
23 changes: 23 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,24 @@
"default": "off",
"description": "Traces the communication between VSCode and the liquidJavaServer service."
},
"liquidjava.study.enabled": {
"type": "boolean",
"default": false,
"scope": "resource",
"description": "Record LiquidJava study interactions locally in the first workspace folder. Disabled by default."
},
"liquidjava.study.participantId": {
"type": "string",
"default": "",
"scope": "resource",
"description": "Participant ID included in every local study event."
},
"liquidjava.study.logPath": {
"type": "string",
"default": ".liquidjava/study-log.jsonl",
"scope": "resource",
"description": "Study log file path relative to the first workspace folder."
},
"liquidjava.applyItalicOverlay": {
"type": "boolean",
"default": true,
Expand Down Expand Up @@ -121,6 +139,11 @@
"title": "Restart",
"category": "LiquidJava"
},
{
"command": "liquidjava.study.revealLog",
"title": "Reveal Study Log",
"category": "LiquidJava"
},
{
"command": "liquidjava.verify",
"title": "Verify",
Expand Down
3 changes: 3 additions & 0 deletions client/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { refreshCodeLenses, registerCodeLens } from "./services/codelens";
import { runLanguageServer, stopLanguageServer } from "./lsp/server";
import { runClient, stopClient } from "./lsp/client";
import type { LiquidJavaTestApi } from "./types/test-api";
import { registerStudyLog, stopStudyLog } from './services/study-log';

/**
* Activates the LiquidJava extension
Expand All @@ -21,6 +22,7 @@ import type { LiquidJavaTestApi } from "./types/test-api";
export async function activate(context: vscode.ExtensionContext): Promise<LiquidJavaTestApi> {
context.subscriptions.push(extension.diagnosticsEmitter, extension.failureEmitter);
registerLogger(context);
registerStudyLog(context);
extension.logger!.client.info("Activating LiquidJava extension...");

registerStatusBar(context);
Expand Down Expand Up @@ -59,6 +61,7 @@ export async function deactivate() {
extension.logger?.client.info("Deactivating LiquidJava extension...");
await stopClient("Extension was deactivated");
await stopLanguageServer();
await stopStudyLog();
resetExtension();
}

Expand Down
4 changes: 4 additions & 0 deletions client/src/lsp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { onActiveFileChange } from '../services/events';
import type { LJDiagnostic } from "../types/diagnostics";
import { LJContext } from '../types/context';
import { handleContext } from '../services/context';
import { isStudyEnabled, handleStudyVerification, studyVerificationCancelled } from '../services/study-log';

/**
* Starts the client and connects it to the language server
Expand All @@ -29,12 +30,14 @@ export async function runClient(context: vscode.ExtensionContext, port: number)
};
const clientOptions: LanguageClientOptions = {
documentSelector: [{ language: "java" }],
initializationOptions: { studyLogging: isStudyEnabled() },
};
extension.client = new LanguageClient("liquidJavaServer", "LiquidJava Server", serverOptions, clientOptions);

context.subscriptions.push(extension.client); // disposed on deactivation

try {
extension.client.onNotification('liquidjava/verification', handleStudyVerification);
await extension.client.start();
extension.logger!.client.info("Extension is ready");

Expand Down Expand Up @@ -73,6 +76,7 @@ export async function runClient(context: vscode.ExtensionContext, port: number)
* @param reason The reason for stopping the client
*/
export async function stopClient(reason: string) {
studyVerificationCancelled();
if (!extension.client && !extension.serverProcess && !extension.socket) {
extension.logger!.client.info("Extension already stopped");
return;
Expand Down
9 changes: 7 additions & 2 deletions client/src/services/commands.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as vscode from "vscode";
import { startExtension, stopExtension, restartExtension } from "../extension";
import { verify } from "./diagnostics";
import { logStudy } from './study-log';

const commandIcons: Record<string, string> = {
"liquidjava.showLogs": "$(output)",
Expand Down Expand Up @@ -31,14 +32,18 @@ export function registerCommands(context: vscode.ExtensionContext) {
const handler = commandHandlers[cmd.command];
if (handler) {
context.subscriptions.push(
vscode.commands.registerCommand(cmd.command, () => handler(context))
vscode.commands.registerCommand(cmd.command, () => {
logStudy('command_run', { command: cmd.command });
return handler(context);
})
);
}
});

// register command to show all commands
context.subscriptions.push(
vscode.commands.registerCommand("liquidjava.showCommands", async () => {
logStudy('command_run', { command: 'liquidjava.showCommands' });
const quickPickItems = commands
.filter(cmd => cmd.command !== "liquidjava.showCommands")
.map(cmd => ({
Expand All @@ -51,4 +56,4 @@ export function registerCommands(context: vscode.ExtensionContext) {
if (selected) vscode.commands.executeCommand(selected.command);
})
);
}
}
2 changes: 2 additions & 0 deletions client/src/services/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { LJDiagnostic } from "../types/diagnostics";
import { updateStatusBar } from "./status-bar";
import { updateErrorAtCursor } from "./context";
import { refreshCodeLenses } from "./codelens";
import { studyDiagnostics } from './study-log';

/**
* Handles LiquidJava diagnostics received from the language server
* @param diagnostics The array of diagnostics received
*/
export function handleLJDiagnostics(diagnostics: LJDiagnostic[]) {
studyDiagnostics(diagnostics);
const containsError = diagnostics.some(d => d.category === "error");
const statusBarState: ExtensionStatus = containsError ? "failed" : "passed";
updateStatusBar(statusBarState);
Expand Down
2 changes: 2 additions & 0 deletions client/src/services/hover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getSelectionContextVariables } from './context';
import { getOriginalVariableName, normalizeFilePath } from '../utils/utils';
import { definitionMatchesClass, getDefinitions } from './definition';
import { isExtensionRunning } from '../extension';
import { logStudy } from './study-log';

/**
* Initializes hover provider for LiquidJava diagnostics
Expand All @@ -30,6 +31,7 @@ export function registerHover() {
}

if (hoverContent.value.length === 0) return null;
logStudy('hover_shown', { file: document.uri.fsPath, line: position.line + 1, column: position.character + 1 });
return new vscode.Hover(hoverContent);
}
});
Expand Down
8 changes: 6 additions & 2 deletions client/src/services/logger.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as vscode from "vscode";
import { OutputChannel } from "vscode";
import { extension } from "../state";
import { logStudy } from './study-log';

enum LogLevel {
INFO = "INFO",
Expand Down Expand Up @@ -77,5 +78,8 @@ export function registerLogger(context: vscode.ExtensionContext) {
extension.logger = createLogger(outputChannel);
context.subscriptions.push(outputChannel);
context.subscriptions.push(extension.logger);
context.subscriptions.push(vscode.commands.registerCommand("liquidjava.showLogs", () => outputChannel.show(true)));
}
context.subscriptions.push(vscode.commands.registerCommand("liquidjava.showLogs", () => {
logStudy('command_run', { command: 'liquidjava.showLogs' });
outputChannel.show(true);
}));
}
Loading
Loading