Skip to content

Commit 1d11f53

Browse files
committed
Implement jumping to modules imported by relative imports, and:
- fix and old regression which rejected results of the first filtering step of definitions - fix the element counter mixing up variables and properties instead of focusing on one type only - add more Python jump tests, reuse testing code - add test for CodeMirrorExtension.selectToken(), using a challenging edge case
1 parent aea333a commit 1d11f53

File tree

10 files changed

+275
-92
lines changed

10 files changed

+275
-92
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"@jupyterlab/codemirror": "^1.0.0",
3939
"@jupyterlab/coreutils": "^3.0.0",
4040
"@jupyterlab/docmanager": "^1.0.0",
41+
"@jupyterlab/docregistry": "^1.0.0",
4142
"@jupyterlab/fileeditor": "^1.0.0",
4243
"@jupyterlab/notebook": "^1.0.0",
4344
"@jupyterlab/services": "^4.0.0",
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { expect } from 'chai';
2+
3+
import { CodeEditor } from "@jupyterlab/codeeditor";
4+
import { CodeMirrorEditor, Mode } from '@jupyterlab/codemirror';
5+
6+
import { CodeMirrorTokensProvider } from "../../editors/codemirror/tokens";
7+
8+
import { Jumper, matchToken } from "../../testutils";
9+
import { CodeMirrorExtension } from "../../editors/codemirror";
10+
11+
12+
describe('CodeMirrorExtension', () => {
13+
14+
let editor: CodeMirrorEditor;
15+
let tokensProvider: CodeMirrorTokensProvider;
16+
let model: CodeEditor.Model;
17+
let host: HTMLElement;
18+
let jumper: Jumper;
19+
let extension: CodeMirrorExtension;
20+
21+
beforeEach(() => {
22+
host = document.createElement('div');
23+
document.body.appendChild(host);
24+
25+
model = new CodeEditor.Model({mimeType: 'text/x-python'});
26+
editor = new CodeMirrorEditor({host, model, config: {mode: 'python'}});
27+
tokensProvider = new CodeMirrorTokensProvider(editor);
28+
29+
jumper = new Jumper(editor);
30+
31+
extension = new CodeMirrorExtension(editor, jumper);
32+
extension.connect();
33+
});
34+
35+
afterEach(() => {
36+
editor.dispose();
37+
document.body.removeChild(host);
38+
});
39+
40+
describe('#selectToken', () => {
41+
42+
it('should select token based on given DOM element of event origin', () => {
43+
model.value.text = `def x():
44+
a = 1
45+
a
46+
47+
a = 1
48+
y = a`;
49+
50+
let tokens = tokensProvider.getTokens();
51+
let token = matchToken(tokens, 'a', 4);
52+
53+
let position = {line: 5, column: 5};
54+
55+
editor.setCursorPosition(position);
56+
57+
expect(editor.getCursorPosition()).to.deep.equal(position);
58+
expect(editor.getTokenForPosition(position)).to.deep.equal(token);
59+
60+
let mode = editor.editor.getOption('mode');
61+
Mode.run(model.value.text, mode, editor.host);
62+
63+
let node = editor.host;
64+
65+
while (node.className !== 'cm-variable')
66+
node = node.lastElementChild as HTMLElement;
67+
68+
// sanity checks
69+
expect(node.innerHTML).to.be.equal('a');
70+
expect(node.previousElementSibling.previousElementSibling.innerHTML).to.be.equal('y');
71+
72+
expect(extension.selectToken('a', node)).to.deep.equal(token);
73+
});
74+
75+
});
76+
});

src/editors/codemirror/extension.ts

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,28 @@ export class CodeMirrorExtension extends CodeMirrorTokensProvider implements IEd
106106

107107
let cellTokens = this.editor.getTokens();
108108

109-
let usagesBeforeTarget = CodeMirrorExtension._countUsagesBefore(lookupName, target);
109+
let typeFilterOn = (
110+
target.className.includes('cm-variable')
111+
||
112+
target.className.includes('cm-property')
113+
);
114+
115+
let lookupType = (
116+
target.className.indexOf('cm-variable') !== -1
117+
? 'variable'
118+
: 'property'
119+
);
120+
121+
let classFilter = 'cm-' + lookupType;
122+
123+
let usagesBeforeTarget = CodeMirrorExtension._countUsagesBefore(lookupName, target, classFilter, typeFilterOn);
110124

111125
// select relevant token
112126
let token = null;
113127
let matchedTokensCount = 0;
114128
for (let j = 0; j < cellTokens.length; j++) {
115129
let testedToken = cellTokens[j];
116-
if (testedToken.value === lookupName) {
130+
if (testedToken.value === lookupName && (!typeFilterOn || lookupType === testedToken.type)) {
117131
matchedTokensCount += 1;
118132
if (matchedTokensCount - 1 === usagesBeforeTarget) {
119133
token = testedToken;
@@ -131,48 +145,63 @@ export class CodeMirrorExtension extends CodeMirrorTokensProvider implements IEd
131145
token = {
132146
value: lookupName,
133147
offset: 0, // dummy offset
134-
type:
135-
target.className.indexOf('cm-variable') !== -1
136-
? 'variable'
137-
: 'property'
148+
type: lookupType
138149
};
139150
}
140151

141152
return token;
142153
}
143154

144-
static _countUsagesBefore(lookupName: string, target: Node) {
155+
static _countUsagesBefore(lookupName: string, target: Node, classFilter: string, classFilterOn: boolean) {
145156

146157
// count tokens with same value that occur before
147158
// (not all the tokens - to reduce the hurdle of
148159
// mapping DOM into tokens)
149160
let usagesBeforeTarget = -1;
150161
let sibling = target as Node;
151162

152-
while (sibling != null) {
153-
if (sibling.textContent === lookupName) {
163+
const root = sibling.getRootNode();
164+
165+
// usually should not exceed that level, but to prevent huge files from trashing the UI...
166+
let max_iter = 10000;
167+
168+
function stop_condition(node: Node) {
169+
return !node || (node.nodeType == 1 && (node as HTMLElement).className.includes('CodeMirror-lines'))
170+
}
171+
172+
while (!stop_condition(sibling) && !sibling.isEqualNode(root) && max_iter) {
173+
174+
if (
175+
sibling.textContent === lookupName &&
176+
(
177+
!classFilterOn ||
178+
(
179+
sibling.nodeType === 1 &&
180+
(sibling as HTMLElement).className.includes(classFilter)
181+
)
182+
)
183+
) {
154184
usagesBeforeTarget += 1;
155185
}
156186

157187
let nextSibling = sibling.previousSibling;
158188

159-
if (nextSibling == null) {
160-
// Try to traverse to previous line (if there is one).
161-
162-
// The additional parent (child) which is traversed
163-
// both ways is a non-relevant presentation container.
164-
let thisLine = sibling.parentNode.parentNode as HTMLElement;
165-
let previousLine = thisLine.previousElementSibling;
166-
167-
// is is really a line?
168-
if (
169-
previousLine &&
170-
previousLine.className.indexOf('CodeMirror-line') !== -1
171-
) {
172-
nextSibling = previousLine.firstChild.lastChild;
189+
while (nextSibling == null) {
190+
while(!sibling.previousSibling) {
191+
sibling = sibling.parentNode;
192+
if (stop_condition(sibling)) {
193+
return usagesBeforeTarget;
194+
}
195+
}
196+
sibling = sibling.previousSibling;
197+
while (sibling.lastChild && sibling.textContent != lookupName) {
198+
sibling = sibling.lastChild;
173199
}
200+
nextSibling = sibling;
174201
}
175202
sibling = nextSibling;
203+
204+
max_iter -= 1;
176205
}
177206

178207
return usagesBeforeTarget;

src/index.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
3939

4040
if (fileEditor.editor instanceof CodeMirrorEditor) {
4141

42-
let jumper = new FileEditorJumper(fileEditor, jump_history, documentManager);
42+
let jumper = new FileEditorJumper(widget, jump_history, documentManager);
4343
let extension = new CodeMirrorExtension(fileEditor.editor, jumper);
4444

4545
extension.connect();
@@ -159,9 +159,10 @@ const plugin: JupyterFrontEndPlugin<void> = {
159159
app.commands.addCommand(cmdIds.jumpFileEditor, {
160160
label: 'Jump to definition',
161161
execute: () => {
162-
let fileEditor = fileEditorTracker.currentWidget.content;
162+
let fileEditorWidget = fileEditorTracker.currentWidget;
163+
let fileEditor = fileEditorWidget.content;
163164

164-
let jumper = new FileEditorJumper(fileEditor, jump_history, documentManager);
165+
let jumper = new FileEditorJumper(fileEditorWidget, jump_history, documentManager);
165166
let editor = fileEditor.editor;
166167

167168
let position = editor.getCursorPosition();
@@ -175,9 +176,9 @@ const plugin: JupyterFrontEndPlugin<void> = {
175176
app.commands.addCommand(cmdIds.jumpBackFileEditor, {
176177
label: 'Jump back',
177178
execute: () => {
178-
let fileEditor = fileEditorTracker.currentWidget.content;
179+
let fileEditorWidget = fileEditorTracker.currentWidget;
179180

180-
let jumper = new FileEditorJumper(fileEditor, jump_history, documentManager);
181+
let jumper = new FileEditorJumper(fileEditorWidget, jump_history, documentManager);
181182
jumper.jump_back()
182183
},
183184
isEnabled: isEnabled(fileEditorTracker)

src/jumpers/fileeditor.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,33 @@ import { CodeJumper } from "./jumper";
44
import { JumpHistory } from "../history";
55
import { TokenContext } from "../languages/analyzer";
66
import { IDocumentManager } from "@jupyterlab/docmanager";
7+
import { IDocumentWidget } from "@jupyterlab/docregistry";
78

89

910
export class FileEditorJumper extends CodeJumper {
1011

1112
editor: FileEditor;
1213
language: string;
1314
history: JumpHistory;
15+
widget: IDocumentWidget;
1416

15-
constructor(editor: FileEditor, history: JumpHistory, document_manager: IDocumentManager) {
17+
constructor(editor_widget: IDocumentWidget<FileEditor>, history: JumpHistory, document_manager: IDocumentManager) {
1618
super();
19+
this.widget = editor_widget;
1720
this.document_manager = document_manager;
18-
this.editor = editor;
21+
this.editor = editor_widget.content;
1922
this.history = history;
20-
this.setLanguageFromMime(editor.model.mimeType);
23+
this.setLanguageFromMime(this.editor.model.mimeType);
2124

22-
editor.model.mimeTypeChanged.connect((session, mimeChanged) => {
25+
this.editor.model.mimeTypeChanged.connect((session, mimeChanged) => {
2326
this.setLanguageFromMime(mimeChanged.newValue)
2427
});
2528
}
2629

30+
get cwd() {
31+
return this.widget.context.path.split('/').slice(0, -1).join('/')
32+
}
33+
2734
setLanguageFromMime(mime: string){
2835
let type = mime.replace('text/x-', '');
2936
switch (type) {

src/jumpers/jumper.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,14 @@ export abstract class CodeJumper {
101101
// try to find variable assignment
102102
let definitions = analyzer.getDefinitions(token.value);
103103

104-
105104
if (definitions.length) {
106-
// Get the last definition / assignment that appears before
107-
// token of origin (is in an earlier cell or has lower offset),
108-
let filtered = definitions.filter(
109-
otherToken => i < stopIndex || otherToken.offset < originToken.offset
105+
// Get definitions / assignments that appear before
106+
// token of origin (are in an earlier cells or have lower offset),
107+
let in_earlier_cell = i < stopIndex;
108+
let filtered = (
109+
in_earlier_cell
110+
? definitions // all are in an earlier cell
111+
: definitions.filter( otherToken => otherToken.offset < originToken.offset) // all are in same cell
110112
);
111113

112114
// but ignore ones that are part of the same assignment expression,
@@ -115,11 +117,12 @@ export abstract class CodeJumper {
115117
// >>> a = a + 1
116118
// clicking on the last 'a' should jump to the first line,
117119
// and not to beginning of the second line.
118-
filtered = definitions.filter(otherToken => {
120+
filtered = filtered.filter(otherToken => {
119121
// If otherToken is in previous cell, we don't need to worry.
120122
if (i < stopIndex) {
121123
return true;
122124
}
125+
123126
return !analyzer.isTokenInSameAssignmentExpression(
124127
otherToken, token
125128
);
@@ -169,13 +172,21 @@ export abstract class CodeJumper {
169172
return null
170173
}
171174

175+
abstract get cwd(): string;
176+
172177
protected jump_to_cross_file_reference(context: TokenContext, cell_of_origin_analyzer: LanguageAnalyzer) {
173178

174179
let potential_paths = cell_of_origin_analyzer.guessReferencePath(context);
180+
if(this.cwd) {
181+
let prefixed_with_cwd = potential_paths.map(path => this.cwd + '/' + path);
182+
potential_paths = prefixed_with_cwd.concat(potential_paths);
183+
}
184+
185+
let code = cell_of_origin_analyzer.referencePathQuery(context);
175186

176-
if (cell_of_origin_analyzer.supportsKernel && this.kernel) {
187+
if (cell_of_origin_analyzer.supportsKernel && this.kernel && code) {
177188
cell_of_origin_analyzer.requestReferencePathFromKernel(
178-
context, this.kernel,
189+
code, this.kernel,
179190
msg => this.handle_path_from_kernel(msg, potential_paths)
180191
);
181192
} else {

src/jumpers/notebook.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ export class NotebookJumper extends CodeJumper {
2828
return this.widget.session.kernel;
2929
}
3030

31+
get cwd() {
32+
return this.widget.model.modelDB.basePath.split('/').slice(0, -1).join('/')
33+
}
34+
3135
get editors() {
3236
return this.notebook.widgets.map((cell) => cell.editor)
3337
}

src/languages/analyzer.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,7 @@ export abstract class LanguageAnalyzer {
8585
return '';
8686
}
8787

88-
requestReferencePathFromKernel(context: TokenContext, kernel: Kernel.IKernelConnection, callback: (msg: KernelMessage.IIOPubMessage) => any) {
89-
let code = this.referencePathQuery(context);
90-
91-
if(!code)
92-
return;
88+
requestReferencePathFromKernel(code: string, kernel: Kernel.IKernelConnection, callback: (msg: KernelMessage.IIOPubMessage) => any) {
9389

9490
let request = {code: code, stop_on_error: false, silent: true};
9591
kernel.ready.then(() => {
@@ -109,7 +105,7 @@ export abstract class LanguageAnalyzer {
109105

110106
_get_token_index(token: CodeEditor.IToken) {
111107
this._maybe_setup_tokens();
112-
return this.tokens.findIndex(t => (t.value == token.value && t.offset == token.offset))
108+
return this.tokens.findIndex(t => (t.value == token.value && t.offset == token.offset && t.type == token.type))
113109
}
114110

115111
isDefinition(token: CodeEditor.IToken, i: number) {

0 commit comments

Comments
 (0)