Skip to content

Commit 60598c6

Browse files
committed
Send editor selection from extension to webview
1 parent 690f63a commit 60598c6

File tree

3 files changed

+132
-3
lines changed

3 files changed

+132
-3
lines changed

extensions/ql-vscode/src/common/interface-types.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,27 @@ interface UntoggleShowProblemsMsg {
220220
t: "untoggleShowProblems";
221221
}
222222

223+
/**
224+
* Information about the current editor selection, sent to the results view
225+
* so it can filter results to only those overlapping the selection.
226+
*/
227+
export interface EditorSelection {
228+
/** The file URI in result-compatible format. */
229+
fileUri: string;
230+
startLine: number;
231+
endLine: number;
232+
startColumn: number;
233+
endColumn: number;
234+
/** True if the selection is empty (just a cursor), in which case we match the whole file. */
235+
isEmpty: boolean;
236+
}
237+
238+
interface SetEditorSelectionMsg {
239+
t: "setEditorSelection";
240+
selection: EditorSelection | undefined;
241+
wasFromUserInteraction?: boolean;
242+
}
243+
223244
/**
224245
* A message sent into the results view.
225246
*/
@@ -229,7 +250,8 @@ export type IntoResultsViewMsg =
229250
| SetUserSettingsMsg
230251
| ShowInterpretedPageMsg
231252
| NavigateMsg
232-
| UntoggleShowProblemsMsg;
253+
| UntoggleShowProblemsMsg
254+
| SetEditorSelectionMsg;
233255

234256
/**
235257
* A message sent from the results view.

extensions/ql-vscode/src/local-queries/results-view.ts

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import type { Location, Result, Run } from "sarif";
2-
import type { WebviewPanel, TextEditorSelectionChangeEvent } from "vscode";
2+
import type {
3+
WebviewPanel,
4+
TextEditorSelectionChangeEvent,
5+
Range,
6+
} from "vscode";
37
import {
48
Diagnostic,
59
DiagnosticRelatedInformation,
@@ -18,6 +22,10 @@ import type {
1822
DatabaseManager,
1923
} from "../databases/local-databases";
2024
import { DatabaseEventKind } from "../databases/local-databases";
25+
import {
26+
decodeSourceArchiveUri,
27+
zipArchiveScheme,
28+
} from "../common/vscode/archive-filesystem-provider";
2129
import {
2230
asError,
2331
assertNever,
@@ -35,6 +43,7 @@ import type {
3543
InterpretedResultsSortState,
3644
RawResultsSortState,
3745
ParsedResultSets,
46+
EditorSelection,
3847
} from "../common/interface-types";
3948
import {
4049
SortDirection,
@@ -197,6 +206,12 @@ export class ResultsView extends AbstractWebview<
197206
),
198207
);
199208

209+
this.disposableEventListeners.push(
210+
window.onDidChangeActiveTextEditor(() => {
211+
this.sendEditorSelectionToWebview();
212+
}),
213+
);
214+
200215
this.disposableEventListeners.push(
201216
this.databaseManager.onDidChangeDatabaseItem(({ kind }) => {
202217
if (kind === DatabaseEventKind.Remove) {
@@ -573,6 +588,9 @@ export class ResultsView extends AbstractWebview<
573588
queryName: this.labelProvider.getLabel(fullQuery),
574589
queryPath: fullQuery.initialInfo.queryPath,
575590
});
591+
592+
// Send the current editor selection so the webview can apply filtering immediately
593+
this.sendEditorSelectionToWebview();
576594
}
577595

578596
/**
@@ -1021,7 +1039,10 @@ export class ResultsView extends AbstractWebview<
10211039
}
10221040

10231041
private handleSelectionChange(event: TextEditorSelectionChangeEvent): void {
1024-
if (event.kind === TextEditorSelectionChangeKind.Command) {
1042+
const wasFromUserInteraction =
1043+
event.kind !== TextEditorSelectionChangeKind.Command;
1044+
this.sendEditorSelectionToWebview(wasFromUserInteraction);
1045+
if (!wasFromUserInteraction) {
10251046
return; // Ignore selection events we caused ourselves.
10261047
}
10271048
const editor = window.activeTextEditor;
@@ -1031,6 +1052,77 @@ export class ResultsView extends AbstractWebview<
10311052
}
10321053
}
10331054

1055+
/**
1056+
* Sends the current editor selection to the webview so it can filter results.
1057+
* Does not send when there is no active text editor (e.g. when the webview
1058+
* gains focus), so the webview retains the last known selection.
1059+
*/
1060+
private sendEditorSelectionToWebview(wasFromUserInteraction = false): void {
1061+
if (!this.isShowingPanel) {
1062+
return;
1063+
}
1064+
const selection = this.computeEditorSelection();
1065+
if (selection === undefined) {
1066+
return;
1067+
}
1068+
void this.postMessage({
1069+
t: "setEditorSelection",
1070+
selection,
1071+
wasFromUserInteraction,
1072+
});
1073+
}
1074+
1075+
/**
1076+
* Computes the current editor selection in a format compatible with result locations.
1077+
*/
1078+
private computeEditorSelection(): EditorSelection | undefined {
1079+
const editor = window.activeTextEditor;
1080+
if (!editor) {
1081+
return undefined;
1082+
}
1083+
1084+
return this.rangeToEditorSelection(editor.document.uri, editor.selection);
1085+
}
1086+
1087+
private rangeToEditorSelection(uri: Uri, range: Range) {
1088+
const fileUri = this.getEditorFileUri(uri);
1089+
if (fileUri == null) {
1090+
return undefined;
1091+
}
1092+
return {
1093+
fileUri,
1094+
// VS Code selections are 0-based; result locations are 1-based
1095+
startLine: range.start.line + 1,
1096+
endLine: range.end.line + 1,
1097+
startColumn: range.start.character + 1,
1098+
endColumn: range.end.character + 1,
1099+
isEmpty: range.isEmpty,
1100+
};
1101+
}
1102+
1103+
/**
1104+
* Gets a file URI from the editor that can be compared with result location URIs.
1105+
*
1106+
* Result URIs (in BQRS and SARIF) use the original source file paths.
1107+
* For `file:` scheme editors, the URI already matches.
1108+
* For source archive editors, we extract the path within the archive,
1109+
* which corresponds to the original source file path.
1110+
*/
1111+
private getEditorFileUri(editorUri: Uri): string | undefined {
1112+
if (editorUri.scheme === "file") {
1113+
return editorUri.toString();
1114+
}
1115+
if (editorUri.scheme === zipArchiveScheme) {
1116+
try {
1117+
const { pathWithinSourceArchive } = decodeSourceArchiveUri(editorUri);
1118+
return `file://${pathWithinSourceArchive}`;
1119+
} catch {
1120+
return undefined;
1121+
}
1122+
}
1123+
return undefined;
1124+
}
1125+
10341126
dispose() {
10351127
super.dispose();
10361128

extensions/ql-vscode/src/view/results/ResultsApp.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { assertNever, getErrorMessage } from "../../common/helpers-pure";
22
import type {
33
DatabaseInfo,
4+
EditorSelection,
45
Interpretation,
56
IntoResultsViewMsg,
67
SortedResultSetInfo,
@@ -66,6 +67,7 @@ interface ResultsViewState {
6667
nextResultsInfo: ResultsInfo | null;
6768
isExpectingResultsUpdate: boolean;
6869
selectionFilterEnabled: boolean;
70+
editorSelection: EditorSelection | undefined;
6971
selectedTable: string | undefined;
7072
}
7173

@@ -82,6 +84,7 @@ export function ResultsApp() {
8284
nextResultsInfo: null,
8385
isExpectingResultsUpdate: true,
8486
selectionFilterEnabled: false,
87+
editorSelection: undefined,
8588
selectedTable: undefined,
8689
});
8790

@@ -224,6 +227,18 @@ export function ResultsApp() {
224227
setProblemsViewSelected(false);
225228
break;
226229

230+
case "setEditorSelection":
231+
if (msg.selection) {
232+
const selection = msg.selection;
233+
setState((prev) => {
234+
return {
235+
...prev,
236+
editorSelection: selection,
237+
};
238+
});
239+
}
240+
break;
241+
227242
default:
228243
assertNever(msg);
229244
}

0 commit comments

Comments
 (0)