Skip to content

Commit 0309429

Browse files
authored
Merge pull request #206 from krassowski/tests/rpy2
Address some issues with positioning system in foreign documents
2 parents a1aedf7 + de24070 commit 0309429

File tree

7 files changed

+150
-76
lines changed

7 files changed

+150
-76
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
- cleans up documents, handlers, events, and signals more aggressively ([#165][])
1111
- ignores malformed diagnostic ranges, enabling markdown support ([#165][])
1212
- passes tests on Python 3.8 on Windows ([#165][])
13+
- improves support for rpy2 magic cells with parameters (
14+
[#206](https://github.com/krassowski/jupyterlab-lsp/pull/206)
15+
)
1316

1417
- bug fixes
1518

packages/jupyterlab-lsp/src/extractors/defaults.spec.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,21 @@ describe('Default extractors', () => {
1717
});
1818
}
1919

20-
function get_the_only_virtual(
20+
function get_the_only_pair(
2121
foreign_document_map: Map<CodeEditor.IRange, IVirtualDocumentBlock>
2222
) {
2323
expect(foreign_document_map.size).to.equal(1);
2424

25-
let { virtual_document } = foreign_document_map.get(
26-
foreign_document_map.keys().next().value
27-
);
25+
let range = foreign_document_map.keys().next().value;
26+
let { virtual_document } = foreign_document_map.get(range);
2827

28+
return { range, virtual_document };
29+
}
30+
31+
function get_the_only_virtual(
32+
foreign_document_map: Map<CodeEditor.IRange, IVirtualDocumentBlock>
33+
) {
34+
let { virtual_document } = get_the_only_pair(foreign_document_map);
2935
return virtual_document;
3036
}
3137

@@ -76,7 +82,9 @@ describe('Default extractors', () => {
7682

7783
it('parses multiple inputs (into dummy data frames)', () => {
7884
let code = wrap_in_python_lines('%R -i df -i x ggplot(df)');
79-
let r_document = get_the_only_virtual(extract(code).foreign_document_map);
85+
let { virtual_document: r_document } = get_the_only_pair(
86+
extract(code).foreign_document_map
87+
);
8088
expect(r_document.value).to.equal(
8189
'df <- data.frame(); x <- data.frame(); ggplot(df)\n'
8290
);

packages/jupyterlab-lsp/src/extractors/defaults.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,26 @@ import {
66
RPY2_MAX_ARGS
77
} from '../magics/rpy2';
88

9-
function rpy2_replacer(match: string, ...args: string[]) {
9+
function rpy2_code_extractor(match: string, ...args: string[]) {
1010
let r = extract_r_args(args, -3);
11-
// define dummy input variables using empty data frames
12-
let inputs = r.inputs.map(i => i + ' <- data.frame();').join(' ');
1311
let code: string;
1412
if (r.rest == null) {
1513
code = '';
1614
} else {
1715
code = r.rest.startsWith(' ') ? r.rest.slice(1) : r.rest;
1816
}
17+
return code;
18+
}
19+
20+
function rpy2_args(match: string, ...args: string[]) {
21+
let r = extract_r_args(args, -3);
22+
// define dummy input variables using empty data frames
23+
let inputs = r.inputs.map(i => i + ' <- data.frame();').join(' ');
24+
let code = rpy2_code_extractor(match, ...args);
1925
if (inputs !== '' && code) {
2026
inputs += ' ';
2127
}
22-
return `${inputs}${code}`;
28+
return inputs;
2329
}
2430

2531
// TODO: make the regex code extractors configurable
@@ -32,14 +38,16 @@ export let foreign_code_extractors: IForeignCodeExtractorsRegistry = {
3238
new RegExpForeignCodeExtractor({
3339
language: 'r',
3440
pattern: '^%%R' + rpy2_args_pattern(RPY2_MAX_ARGS) + '\n([^]*)',
35-
extract_to_foreign: rpy2_replacer,
41+
extract_to_foreign: rpy2_code_extractor,
42+
extract_arguments: rpy2_args,
3643
is_standalone: false,
3744
file_extension: 'R'
3845
}),
3946
new RegExpForeignCodeExtractor({
4047
language: 'r',
4148
pattern: '(?:^|\n)%R' + rpy2_args_pattern(RPY2_MAX_ARGS) + '( .*)?\n?',
42-
extract_to_foreign: rpy2_replacer,
49+
extract_to_foreign: rpy2_code_extractor,
50+
extract_arguments: rpy2_args,
4351
is_standalone: false,
4452
file_extension: 'R'
4553
}),

packages/jupyterlab-lsp/src/extractors/regexp.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { IExtractedCode, IForeignCodeExtractor } from './types';
22
import { position_at_offset } from '../positioning';
33
import { replacer } from '../magics/overrides';
4+
import { CodeEditor } from '@jupyterlab/codeeditor';
45

56
export class RegExpForeignCodeExtractor implements IForeignCodeExtractor {
67
options: RegExpForeignCodeExtractor.IOptions;
@@ -38,19 +39,29 @@ export class RegExpForeignCodeExtractor implements IForeignCodeExtractor {
3839

3940
while (match !== null) {
4041
let matched_string = match[0];
42+
let position_shift: CodeEditor.IPosition = null;
4143
let foreign_code_fragment = matched_string.replace(
4244
this.expression,
4345
// @ts-ignore
4446
this.options.extract_to_foreign
4547
);
48+
let prefix = '';
49+
if (typeof this.options.extract_arguments !== 'undefined') {
50+
prefix = matched_string.replace(
51+
this.expression,
52+
// @ts-ignore
53+
this.options.extract_arguments
54+
);
55+
position_shift = position_at_offset(prefix.length, prefix.split('\n'));
56+
}
4657

4758
// NOTE:
4859
// match.index + matched_string.length === this.sticky_expression.lastIndex
4960

50-
let end = this.global_expression.lastIndex;
61+
let end_index = this.global_expression.lastIndex;
5162

5263
if (this.options.keep_in_host || this.options.keep_in_host == null) {
53-
host_code_fragment = code.substring(started_from, end);
64+
host_code_fragment = code.substring(started_from, end_index);
5465
} else {
5566
if (started_from === match.index) {
5667
host_code_fragment = '';
@@ -62,15 +73,19 @@ export class RegExpForeignCodeExtractor implements IForeignCodeExtractor {
6273
// TODO: this could be slightly optimized (start at start) by using the match[n],
6374
// where n is the group to be used; while this reduces the flexibility of extract_to_foreign,
6475
// it might be better to enforce such strict requirement
65-
let start = match.index + matched_string.indexOf(foreign_code_fragment);
76+
let start_offset =
77+
match.index + matched_string.indexOf(foreign_code_fragment);
78+
let start = position_at_offset(start_offset, lines);
79+
let end = position_at_offset(
80+
start_offset + foreign_code_fragment.length,
81+
lines
82+
);
6683

6784
extracts.push({
6885
host_code: host_code_fragment,
69-
foreign_code: foreign_code_fragment,
70-
range: {
71-
start: position_at_offset(start, lines),
72-
end: position_at_offset(start + foreign_code_fragment.length, lines)
73-
}
86+
foreign_code: prefix + foreign_code_fragment,
87+
range: { start, end },
88+
virtual_shift: position_shift
7489
});
7590

7691
started_from = this.global_expression.lastIndex;
@@ -82,7 +97,8 @@ export class RegExpForeignCodeExtractor implements IForeignCodeExtractor {
8297
extracts.push({
8398
host_code: final_host_code_fragment,
8499
foreign_code: null,
85-
range: null
100+
range: null,
101+
virtual_shift: null
86102
});
87103
}
88104

@@ -111,7 +127,12 @@ namespace RegExpForeignCodeExtractor {
111127
*/
112128
extract_to_foreign: string | replacer;
113129
/**
114-
* String boolean if everything (true, default) or nothing (false) should be kept in the host document.
130+
* If arguments from the cell or line magic are to be extracted and prepended before the extracted code,
131+
* set extract_arguments to a replacer function taking the code and returning the string to be prepended.
132+
*/
133+
extract_arguments?: replacer;
134+
/**
135+
* Boolean if everything (true, default) or nothing (false) should be kept in the host document.
115136
*
116137
* For the R example this should be empty if we wish to ignore the cell,
117138
* but usually a better option is to retain the foreign code and use language

packages/jupyterlab-lsp/src/extractors/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ export interface IExtractedCode {
99
* Range of the foreign code relative to the original source.
1010
*/
1111
range: CodeEditor.IRange;
12+
/**
13+
* Shift due to any additional code inserted at the beginning of the virtual document
14+
* (usually in order to mock the arguments passed to a magic, or to provide other context clues for the linters)
15+
*/
16+
virtual_shift: CodeEditor.IPosition | null;
1217
/**
1318
* Code to be retained in the virtual document of the host.
1419
*/

packages/jupyterlab-lsp/src/virtual/document.spec.ts

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { expect } from 'chai';
2-
import { RegExpForeignCodeExtractor } from '../extractors/regexp';
32
import { is_within_range, VirtualDocument } from './document';
43
import * as CodeMirror from 'codemirror';
54
import { ISourcePosition, IVirtualPosition } from '../positioning';
65
import { CodeEditor } from '@jupyterlab/codeeditor';
6+
import { foreign_code_extractors } from '../extractors/defaults';
77

88
let R_LINE_MAGICS = `%R df = data.frame()
99
print("df created")
@@ -46,26 +46,18 @@ describe('is_within_range', () => {
4646
});
4747

4848
describe('VirtualDocument', () => {
49-
let r_line_extractor_removing = new RegExpForeignCodeExtractor({
50-
language: 'R',
51-
pattern: '(^|\n)%R (.*)\n?',
52-
extract_to_foreign: '$2',
53-
keep_in_host: false,
54-
is_standalone: false,
55-
file_extension: 'R'
56-
});
5749
let document = new VirtualDocument(
5850
'python',
5951
'test.ipynb',
6052
{},
61-
{ python: [r_line_extractor_removing] },
53+
foreign_code_extractors,
6254
false,
6355
'py',
6456
false
6557
);
6658

6759
describe('#extract_foreign_code', () => {
68-
it('joins non-standalone fragments together for both foreign and host code', () => {
60+
it('joins non-standalone fragments together', () => {
6961
let {
7062
cell_code_kept,
7163
foreign_document_map
@@ -74,15 +66,14 @@ describe('VirtualDocument', () => {
7466
column: 0
7567
});
7668

77-
expect(cell_code_kept).to.equal(
78-
'print("df created")\nprint("plotted")\n'
79-
);
69+
// note R cell lines are kept in code (keep_in_host=true)
70+
expect(cell_code_kept).to.equal(R_LINE_MAGICS);
8071
expect(foreign_document_map.size).to.equal(2);
8172

8273
let { virtual_document: r_document } = foreign_document_map.get(
8374
foreign_document_map.keys().next().value
8475
);
85-
expect(r_document.language).to.equal('R');
76+
expect(r_document.language).to.equal('r');
8677
expect(r_document.value).to.equal('df = data.frame()\n\n\nggplot(df)\n');
8778
});
8879
});
@@ -94,14 +85,24 @@ describe('VirtualDocument', () => {
9485
let init_document_with_Python_and_R = () => {
9586
let cm_editor_for_cell_1 = {} as CodeMirror.Editor;
9687
let cm_editor_for_cell_2 = {} as CodeMirror.Editor;
88+
let cm_editor_for_cell_3 = {} as CodeMirror.Editor;
89+
let cm_editor_for_cell_4 = {} as CodeMirror.Editor;
9790
document.append_code_block(
98-
'test line in Python 1\n%R test line in R 1',
91+
'test line in Python 1\n%R 1st test line in R line magic 1',
9992
cm_editor_for_cell_1
10093
);
10194
document.append_code_block(
102-
'test line in Python 2\n%R test line in R 2',
95+
'test line in Python 2\n%R 1st test line in R line magic 2',
10396
cm_editor_for_cell_2
10497
);
98+
document.append_code_block(
99+
'%%R\n1st test line in R cell magic 1',
100+
cm_editor_for_cell_3
101+
);
102+
document.append_code_block(
103+
'%%R -i imported_variable\n1st test line in R cell magic 2',
104+
cm_editor_for_cell_4
105+
);
105106
};
106107

107108
describe('transform_virtual_to_editor', () => {
@@ -131,14 +132,30 @@ describe('VirtualDocument', () => {
131132
ch: 3
132133
} as ISourcePosition);
133134
expect(foreign_document).to.not.equal(document);
135+
expect(foreign_document.value).to.equal(
136+
'1st test line in R line magic 1\n\n\n' +
137+
'1st test line in R line magic 2\n\n\n' +
138+
'1st test line in R cell magic 1\n\n\n' +
139+
'imported_variable <- data.frame(); 1st test line in R cell magic 2\n'
140+
);
134141

135-
// The second (R) line in the first block
136-
let editor_position = foreign_document.transform_virtual_to_editor({
142+
// The second (R) line in the first block ("s" in "1st", "1st" in "1st test line in R line magic")
143+
let virtual_r_1_1 = {
137144
line: 0,
138-
ch: 0
139-
} as IVirtualPosition);
145+
ch: 1
146+
} as IVirtualPosition;
147+
148+
// For future reference, the code below would be wrong:
149+
// let source_position = foreign_document.transform_virtual_to_source(virtual_r_1_1);
150+
// expect(source_position.line).to.equal(1);
151+
// expect(source_position.ch).to.equal(4);
152+
// because it checks R source position, rather than checking root source positions.
153+
154+
let editor_position = foreign_document.transform_virtual_to_editor(
155+
virtual_r_1_1
156+
);
140157
expect(editor_position.line).to.equal(1);
141-
expect(editor_position.ch).to.equal(3);
158+
expect(editor_position.ch).to.equal(4);
142159

143160
// The second (R) line in the second block
144161
editor_position = foreign_document.transform_virtual_to_editor({

0 commit comments

Comments
 (0)