blob: 9cb8d57c216a2cbf1e14d3540bc2b2f2a12ceeb0 [file] [edit]
// Copyright (C) 2026 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Opt-in plugin wiring the QueryPage editor to syntaqlite's in-process LSP
// server (running inside the WASM engine) via the stock @codemirror/lsp-client
// extensions: completion, diagnostics, hover, signature help, go-to-def,
// references, rename, formatting. Our side is only the transport bridge
// (lsp.ts) and feeding the stdlib schema. When disabled, the editor behaves
// exactly as before.
import {LSPClient, languageServerExtensions} from '@codemirror/lsp-client';
import type {Extension} from '@codemirror/state';
import type {PerfettoPlugin} from '../../public/plugin';
import type {Trace} from '../../public/trace';
import QueryPagePlugin from '../dev.perfetto.QueryPage';
import SqlModulesPlugin from '../dev.perfetto.SqlModules';
import {createSqlEngine} from './engine';
import {engineTransport} from './lsp';
export default class SqlLspPlugin implements PerfettoPlugin {
static readonly id = 'dev.perfetto.SqlLsp';
static readonly description =
'Schema-aware autocomplete, live diagnostics, and hover docs in the ' +
'PerfettoSQL query editor (powered by the syntaqlite language server).';
static readonly dependencies = [QueryPagePlugin, SqlModulesPlugin];
async onTraceLoad(trace: Trace): Promise<void> {
// Register synchronously — editors mounted while the WASM engine is still
// loading must already carry the LSP extension (the client queues didOpen
// and replays it once the transport connects; editors mounted before this
// registration pick the extension up via the Editor widget's
// reconfiguration path).
//
// One document URI per editor tab: all query tabs stay mounted
// concurrently, and the client's default workspace throws on a second
// view of the same file. Extensions are memoized per document — a fresh
// instance would make the editor recreate the plugin (closing and
// reopening the server document) on every redraw.
const client = new LSPClient({extensions: languageServerExtensions()});
const byDoc = new Map<string, Extension>();
trace.plugins.getPlugin(QueryPagePlugin).setEditorExtensions((docId) => {
let extensions = byDoc.get(docId);
if (extensions === undefined) {
extensions = client.plugin(`file:///${docId}.sql`, 'sql');
byDoc.set(docId, extensions);
}
return extensions;
});
// Each trace gets its own WASM session: an LSP server accepts exactly one
// initialize, so sharing a session would reject the next trace's client.
const engine = await createSqlEngine();
if (!engine) return;
trace.trash.defer(() => {
// Disconnect first: it parks the client's message queue, so didClose
// notifications from editors unmounting after trace teardown no longer
// reach the freed session.
client.disconnect();
engine.dispose();
});
client.connect(engineTransport(engine));
// Feed the stdlib schema to the server, which re-runs analysis and
// re-publishes diagnostics for open documents on receipt. SqlModules is a
// declared dependency, so its onTraceLoad — which awaits the catalog — has
// already completed by the time ours runs; the modules are ready now.
const modules = trace.plugins.getPlugin(SqlModulesPlugin).getSqlModules();
if (modules !== undefined) {
client
.request('syntaqlite/setSessionContext', {
context: {
tables: modules.listTables().map((t) => ({
name: t.name,
columns: t.columns.map((c) => c.name),
})),
views: [],
functions: [],
},
})
.catch((e) => {
console.warn('failed to feed SQL schema to the language server', e);
});
}
}
}