-
Notifications
You must be signed in to change notification settings - Fork 10
/
jumper.ts
317 lines (251 loc) · 10.7 KB
/
jumper.ts
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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
import { CodeMirrorEditor } from "@jupyterlab/codemirror";
import { CodeEditor } from "@jupyterlab/codeeditor";
import { IJump, IJumpPosition } from "../jump";
import { chooseLanguageAnalyzer } from "../languages/chooser";
import { CodeMirrorExtension } from "../editors/codemirror";
import { IDocumentManager } from "@jupyterlab/docmanager";
import { LanguageAnalyzer, TokenContext } from "../languages/analyzer";
import { Kernel, KernelMessage } from "@jupyterlab/services";
import IIOPubMessage = KernelMessage.IIOPubMessage;
import { IDocumentWidget } from "@jupyterlab/docregistry";
import { JumpHistory } from "../history";
import { FileEditor } from "@jupyterlab/fileeditor";
import IEditor = CodeEditor.IEditor;
import { Dialog, showDialog } from "@jupyterlab/apputils";
function hasCellMagic(tokens: CodeEditor.IToken[]) {
return (
// CodeMirror Python-tokenizer
(tokens.length >= 3 && tokens[0].value == '%' && tokens[1].value == '%')
||
// CodeMirror R-tokenizer: although IRkernel does not support magics,
// cell-magic recognition is still needed whe operating on an R-cell
// inside of IPython notebook.
(tokens.length >= 2 && tokens[0].value == '%%')
)
}
const cell_magic_lang_to_tokenizer : any = {
// on the right-hand side is the CodeMirror mode specification
// TODO: new class for mode spec?
'bash': 'bash',
'R': 'r',
'python': 'python',
'python2': {name: 'python', version: 2},
'python3': {name: 'python', version: 3},
'javascript': 'javascript',
'js': 'javascript',
'svg': 'application/xml',
'html': 'text/html',
'latex': 'text/x-stex'
// not working as for now:
// 'ruby': 'text/x-ruby',
// require additional logic/scripting:
// 'script': '',
// 'sh': '',
};
export abstract class CodeJumper {
abstract language: string;
document_manager: IDocumentManager;
widget: IDocumentWidget;
history: JumpHistory;
abstract jump_to_definition(jump: IJump): void
abstract get editors(): ReadonlyArray<CodeEditor.IEditor>
protected _getLanguageAnalyzerForCell(cell_editor: CodeEditor.IEditor) {
let language = this.language;
// if a cell starts with %%[language] magic, use the other language:
let tokens = cell_editor.getTokens();
// TODO: move this out to a separate jupyterlab-extension?
// this could be run after each change of cell content
if (hasCellMagic(tokens)) {
let magic_name = tokens[0].value == '%' ? tokens[2].value : tokens[1].value;
if (cell_magic_lang_to_tokenizer.hasOwnProperty(magic_name)) {
language = magic_name;
// to get properly parsed tokens for given language,
// force the CodeMirror tokenizer to use the corresponding mode
let cm = cell_editor as CodeMirrorEditor;
cm.editor.setOption('mode', cell_magic_lang_to_tokenizer[language]);
}
}
let analyzerClass = chooseLanguageAnalyzer(language);
// TODO: make this dynamic, depending on editor implementation in use (like with languages)
let editor = new CodeMirrorExtension(cell_editor as CodeMirrorEditor, this);
return new analyzerClass(editor);
}
/**
* Find the last definition of given variable.
*/
protected _findLastDefinition(token: CodeEditor.IToken, stopIndex: number) {
let definitionToken = null;
let definitionIndex = null;
const originToken = token;
for (let i = 0; i <= stopIndex; i++) {
let cell_editor = this.editors[i];
let analyzer = this._getLanguageAnalyzerForCell(cell_editor);
// try to find variable assignment
let definitions = analyzer.getDefinitions(token.value);
if (definitions.length) {
// Get definitions / assignments that appear before
// token of origin (are in an earlier cells or have lower offset),
let in_earlier_cell = i < stopIndex;
let filtered = (
in_earlier_cell
? definitions // all are in an earlier cell
: definitions.filter(otherToken => otherToken.offset < originToken.offset) // all are in same cell
);
// but ignore ones that are part of the same assignment expression,
// for example in a cell like this:
// >>> a = 1
// >>> a = a + 1
// clicking on the last 'a' should jump to the first line,
// and not to beginning of the second line.
filtered = filtered.filter(otherToken => {
// If otherToken is in previous cell, we don't need to worry.
if (i < stopIndex) {
return true;
}
return !analyzer.isTokenInSameAssignmentExpression(
otherToken, token
);
});
if (filtered.length) {
definitionToken = filtered[filtered.length - 1];
definitionIndex = i;
} else if (!definitionToken && i === stopIndex) {
// but if there is no definition at all, and we are in the last cell,
// just return the token of origin (the clicked element), so the
// editor will focus on the clicked element rather than ignore the
// click altogether.
definitionToken = token;
definitionIndex = i;
}
}
}
return {
token: definitionToken,
cellIndex: definitionIndex
};
}
private go_to_position(document_widget: IDocumentWidget, jumper: string, column: number, line_number: number, input_number = 0) {
let document_jumper: CodeJumper;
let position = {line: line_number, column: column};
let document_jumper_type = jumpers.get(jumper);
document_jumper = new document_jumper_type(document_widget, this.history, this.document_manager);
document_jumper.jump(document_jumper.getJumpPosition(position, input_number));
}
try_to_open_document(path: string, is_symlink: boolean, line_number = 0, input_number: number = null, column: number = 0) {
if (input_number && !path && this.constructor.name === 'NotebookJumper') {
// the definition is in this notebook
this.go_to_position(this.widget, 'notebook', column, line_number, input_number)
} else {
this.document_manager.services.contents.get(path, {content: false})
.then(() => {
let document_widget = this.document_manager.openOrReveal(path);
document_widget.revealed.then(() => {
this.go_to_position(document_widget, 'fileeditor', column, line_number, input_number);
// protect external files from accidental edition
if (is_symlink) {
let editor_widget = (document_widget as IDocumentWidget<FileEditor>);
editor_widget.title.label = editor_widget.title.label + ' (external)';
let editor = editor_widget.content.editor;
let disposable = editor.addKeydownHandler((editor: IEditor, event: KeyboardEvent) => {
let dialog_promise = showDialog({
title: 'Edit external file?',
body:
'This file is located outside of the root of the JupyterLab start directory. '
+ 'do you really wish to edit it?',
buttons: [
Dialog.cancelButton({label: 'Cancel'}),
Dialog.warnButton({label: 'Edit anyway'})
]
});
dialog_promise.then(result => {
if (result.button.accept)
disposable.dispose();
});
// prevent default
return true;
});
}
});
})
.catch(() => {
});
}
}
handle_path_from_kernel(response: IIOPubMessage, fallback_paths: string[]) {
let obj: any = response.content;
if (obj.name === 'stdout') {
let data = JSON.parse(obj.text);
this.try_to_open_document(data['path'], data['is_symlink']);
} else if (response.header.msg_type === 'error') {
console.error('Failed to resolve the paths from kernel; falling back to guessing the path locations');
console.log(response);
for (let path of fallback_paths) {
this.try_to_open_document(path, false);
}
}
}
get kernel(): Kernel.IKernelConnection {
return null
}
abstract get cwd(): string;
queryKernel(code: string, kernel: Kernel.IKernelConnection, callback: (msg: KernelMessage.IIOPubMessage) => any) {
let request = {code: code, stop_on_error: false, silent: true};
kernel.ready.then(() => {
let future = kernel.requestExecute(request);
future.onIOPub = callback;
return future.done;
})
}
protected jump_to_cross_file_reference(context: TokenContext, cell_of_origin_analyzer: LanguageAnalyzer) {
let potential_paths = cell_of_origin_analyzer.guessReferencePath(context);
if (this.cwd) {
let prefixed_with_cwd = potential_paths.map(path => this.cwd + '/' + path);
potential_paths = prefixed_with_cwd.concat(potential_paths);
}
let code = cell_of_origin_analyzer.referencePathQuery(context);
if (cell_of_origin_analyzer.supportsKernel && this.kernel && code) {
this.queryKernel(
code, this.kernel,
msg => this.handle_path_from_kernel(msg, potential_paths) // TODO: extract fallback?
);
} else {
// TODO: extract fallback?
// if kernel is not available, try use the guessed paths
// try one by one
for (let path of potential_paths) {
this.try_to_open_document(path, false);
}
}
}
handle_kernel_inspect(response: IIOPubMessage, fallback: Function) {
let obj: any = response.content;
if (obj.name === 'stdout') {
let data = JSON.parse(obj.text);
if (!data) {
// not a definition that the server can resolve, carry on
fallback()
}
let line_number = data['line_number'];
this.try_to_open_document(data['path'], data['is_symlink'], line_number - 1, data['input_number'], data['column']);
} else if (response.header.msg_type === 'error') {
console.error('Failed to resolve the paths from kernel; falling back to guessing the path locations');
console.log(response);
fallback()
}
}
protected inspect_and_jump(context: TokenContext, cell_of_origin_analyzer: LanguageAnalyzer, fallback: Function) {
let code = cell_of_origin_analyzer.definitionLocationQuery(context);
if (cell_of_origin_analyzer.supportsKernel && this.kernel && code) {
this.queryKernel(
code, this.kernel,
msg => this.handle_kernel_inspect(msg, fallback)
);
} else {
fallback()
}
}
abstract jump(position: IJumpPosition): void;
abstract getOffset(position: CodeEditor.IPosition, cell?: number): number;
abstract getJumpPosition(position: CodeEditor.IPosition, input_number?: number): IJumpPosition;
}
export let jumpers: Map<string, any> = new Map();