-
Notifications
You must be signed in to change notification settings - Fork 226
Expand file tree
/
Copy pathsummary-language-support.ts
More file actions
198 lines (175 loc) · 5.78 KB
/
summary-language-support.ts
File metadata and controls
198 lines (175 loc) · 5.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
import { readFile } from "fs-extra";
import type { RawSourceMap } from "source-map";
import { SourceMapConsumer } from "source-map";
import type {
TextDocument,
TextEditor,
TextEditorSelectionChangeEvent,
} from "vscode";
import {
Position,
Selection,
TextEditorRevealType,
ViewColumn,
window,
workspace,
} from "vscode";
import { DisposableObject } from "../common/disposable-object";
import { extLogger } from "../common/logging/vscode";
import { getErrorMessage } from "../common/helpers-pure";
import type { SummaryLanguageSupportCommands } from "../common/commands";
import type { App } from "../common/app";
/** A `Position` within a specified file on disk. */
interface PositionInFile {
filePath: string;
position: Position;
}
/**
* Opens the specified source location in a text editor.
* @param position The position (including file path) to show.
*/
async function showSourceLocation(position: PositionInFile): Promise<void> {
const document = await workspace.openTextDocument(position.filePath);
const editor = await window.showTextDocument(document, ViewColumn.Active);
editor.selection = new Selection(position.position, position.position);
editor.revealRange(
editor.selection,
TextEditorRevealType.InCenterIfOutsideViewport,
);
}
/**
* Simple language support for human-readable evaluator log summaries.
*
* This class implements the `codeQL.gotoQL` command, which jumps from RA code to the corresponding
* QL code that generated it. It also tracks the current selection and active editor to enable and
* disable that command based on whether there is a QL mapping for the current selection.
*/
export class SummaryLanguageSupport extends DisposableObject {
/**
* The last `TextDocument` (with language `ql-summary`) for which we tried to find a sourcemap, or
* `undefined` if we have not seen such a document yet.
*/
private lastDocument: TextDocument | undefined = undefined;
/**
* The sourcemap for `lastDocument`, or `undefined` if there was no such sourcemap or document.
*/
private sourceMap: SourceMapConsumer | undefined = undefined;
constructor(private readonly app: App) {
super();
this.push(
window.onDidChangeActiveTextEditor(
this.handleDidChangeActiveTextEditor.bind(this),
),
);
this.push(
window.onDidChangeTextEditorSelection(
this.handleDidChangeTextEditorSelection.bind(this),
),
);
this.push(
workspace.onDidCloseTextDocument(
this.handleDidCloseTextDocument.bind(this),
),
);
}
public getCommands(): SummaryLanguageSupportCommands {
return {
"codeQL.gotoQL": this.handleGotoQL.bind(this),
"codeQL.gotoQLContextEditor": this.handleGotoQL.bind(this),
};
}
/**
* Gets the location of the QL code that generated the RA at the current selection in the active
* editor, or `undefined` if there is no mapping.
*/
private async getQLSourceLocation(): Promise<PositionInFile | undefined> {
const editor = window.activeTextEditor;
if (editor === undefined) {
return undefined;
}
const document = editor.document;
if (document.languageId !== "ql-summary") {
return undefined;
}
if (document.uri.scheme !== "file") {
return undefined;
}
if (this.lastDocument !== document) {
this.clearCache();
const mapPath = `${document.uri.fsPath}.map`;
try {
const sourceMapText = await readFile(mapPath, "utf-8");
const rawMap: RawSourceMap = JSON.parse(sourceMapText);
this.sourceMap = await new SourceMapConsumer(rawMap);
} catch (e) {
// Error reading sourcemap. Pretend there was no sourcemap.
void extLogger.log(
`Error reading sourcemap file '${mapPath}': ${getErrorMessage(e)}`,
);
this.sourceMap = undefined;
}
this.lastDocument = document;
}
if (this.sourceMap === undefined) {
return undefined;
}
const qlPosition = this.sourceMap.originalPositionFor({
line: editor.selection.start.line + 1,
column: editor.selection.start.character,
bias: SourceMapConsumer.GREATEST_LOWER_BOUND,
});
if (qlPosition.source === null || qlPosition.line === null) {
// No position found.
return undefined;
}
const line = qlPosition.line - 1; // In `source-map`, lines are 1-based...
const column = qlPosition.column ?? 0; // ...but columns are 0-based :(
return {
filePath: qlPosition.source,
position: new Position(line, column),
};
}
/**
* Clears the cached sourcemap and its corresponding `TextDocument`.
*/
private clearCache(): void {
if (this.sourceMap !== undefined) {
this.sourceMap.destroy();
this.sourceMap = undefined;
this.lastDocument = undefined;
}
}
/**
* Updates the `codeql.hasQLSource` context variable based on the current selection. This variable
* controls whether or not the `codeQL.gotoQL` command is enabled.
*/
private async updateContext(): Promise<void> {
const position = await this.getQLSourceLocation();
await this.app.commands.execute(
"setContext",
"codeql.hasQLSource",
position !== undefined,
);
}
handleDidChangeActiveTextEditor = async (
_editor: TextEditor | undefined,
): Promise<void> => {
await this.updateContext();
};
handleDidChangeTextEditorSelection = async (
_e: TextEditorSelectionChangeEvent,
): Promise<void> => {
await this.updateContext();
};
handleDidCloseTextDocument = (document: TextDocument): void => {
if (this.lastDocument === document) {
this.clearCache();
}
};
handleGotoQL = async (): Promise<void> => {
const position = await this.getQLSourceLocation();
if (position !== undefined) {
await showSourceLocation(position);
}
};
}