@krassowski/jupyterlab-lsp
Version:
Language Server Protocol integration for JupyterLab
252 lines (216 loc) • 8.77 kB
text/typescript
import { CodeEditor } from '@jupyterlab/codeeditor';
import { replacer } from '../overrides/tokens';
import { position_at_offset } from '../positioning';
import { IExtractedCode, IForeignCodeExtractor } from './types';
export function getIndexOfCaptureGroup(
expression: RegExp,
matched_string: string,
value_of_captured_group: string
): number {
// TODO: use https://github.com/tc39/proposal-regexp-match-indices once supported in >95% of browsers
// (probably around 2025)
// get index of the part that is being extracted to foreign document
let captured_groups = expression.exec(matched_string);
if (captured_groups == null) {
console.warn(
`No capture group found for ${expression} in ${matched_string}`
);
return -1;
}
let offset_in_match = 0;
// first element is a full match
let full_matched = captured_groups[0];
for (let group of captured_groups.slice(1)) {
if (typeof group === 'undefined') {
continue;
}
if (group === value_of_captured_group) {
offset_in_match += full_matched.indexOf(group);
break;
}
let group_end_offset = full_matched.indexOf(group) + group.length;
full_matched = full_matched.slice(group_end_offset);
offset_in_match += group_end_offset;
}
return offset_in_match;
}
export class RegExpForeignCodeExtractor implements IForeignCodeExtractor {
options: RegExpForeignCodeExtractor.IOptions;
language: string;
global_expression: RegExp;
test_expression: RegExp;
expression: RegExp;
standalone: boolean;
file_extension: string;
constructor(options: RegExpForeignCodeExtractor.IOptions) {
this.language = options.language;
this.options = options;
this.global_expression = new RegExp(options.pattern, 'g');
this.test_expression = new RegExp(options.pattern, 'g');
this.expression = new RegExp(options.pattern);
this.standalone = this.options.is_standalone;
this.file_extension = this.options.file_extension;
}
has_foreign_code(code: string): boolean {
let result = this.test_expression.test(code);
this.test_expression.lastIndex = 0;
return result;
}
extract_foreign_code(code: string): IExtractedCode[] {
let lines = code.split('\n');
let extracts = new Array<IExtractedCode>();
let started_from = this.global_expression.lastIndex;
let match: RegExpExecArray | null = this.global_expression.exec(code);
let host_code_fragment: string;
let chosen_replacer: string | replacer;
let is_new_api_replacer: boolean = false;
if (typeof this.options.foreign_replacer !== 'undefined') {
chosen_replacer = this.options.foreign_replacer;
is_new_api_replacer = true;
} else if (typeof this.options.foreign_capture_groups !== 'undefined') {
chosen_replacer = '$' + this.options.foreign_capture_groups.join('$');
is_new_api_replacer = true;
} else if (this.options.extract_to_foreign) {
chosen_replacer = this.options.extract_to_foreign;
} else {
console.warn(
`Foreign replacer not defined for extractor: {this.expression} - this is deprecated; use 'foreign_replacer' to define it`
);
return [];
}
while (match != null) {
let matched_string = match[0];
let position_shift: CodeEditor.IPosition | null = null;
let foreign_code_fragment = matched_string.replace(
this.expression,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
chosen_replacer
);
let prefix = '';
if (typeof this.options.extract_arguments !== 'undefined') {
prefix = matched_string.replace(
this.expression,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.options.extract_arguments
);
position_shift = position_at_offset(prefix.length, prefix.split('\n'));
}
// NOTE:
// match.index + matched_string.length === this.sticky_expression.lastIndex
let end_index = this.global_expression.lastIndex;
if (this.options.keep_in_host || this.options.keep_in_host == null) {
host_code_fragment = code.substring(started_from, end_index);
} else {
if (started_from === match.index) {
host_code_fragment = '';
} else {
host_code_fragment = code.substring(started_from, match.index) + '\n';
}
}
let foreign_code_group_value = foreign_code_fragment;
if (is_new_api_replacer) {
foreign_code_group_value = matched_string.replace(
this.expression,
'$' + Math.min(...this.options.foreign_capture_groups!)
);
}
const foreign_group_index_in_match = getIndexOfCaptureGroup(
this.expression,
matched_string,
foreign_code_group_value
);
let start_offset = match.index + foreign_group_index_in_match;
let start = position_at_offset(start_offset, lines);
let end = position_at_offset(
start_offset + foreign_code_fragment.length,
lines
);
extracts.push({
host_code: host_code_fragment,
foreign_code: prefix + foreign_code_fragment,
range: { start, end },
virtual_shift: position_shift
});
started_from = this.global_expression.lastIndex;
match = this.global_expression.exec(code);
}
if (started_from !== code.length) {
let final_host_code_fragment = code.substring(started_from, code.length);
extracts.push({
host_code: final_host_code_fragment,
foreign_code: null,
range: null,
virtual_shift: null
});
}
return extracts;
}
}
namespace RegExpForeignCodeExtractor {
export interface IOptions {
/**
* The foreign language.
*/
language: string;
/**
* String giving regular expression to test cells for the foreign language presence.
*
* For example:
* - `%%R( (.*))?\n(.*)` will match R cells of rpy2
* - `(.*)'<html>(.*)</html>'(.*)` will match html documents in strings of any language using single ticks
*/
pattern: string;
/**
* Array of numbers specifying match groups to be extracted from the regular expression match,
* for the use in virtual document of the foreign language.
* For the R example this should be `3`. Please not that these are 1-based, as the 0th index is the full match.
* If multiple groups are given, those will be concatenated.
*
* If additional code is needed in between the groups, use `foreign_replacer` in addition to
* `foreign_capture_groups` (but not instead!).
*
* `foreign_capture_groups` is required for proper offset calculation and will no longer be optional in 4.0.
*/
foreign_capture_groups?: number[];
/**
* Function to compose the foreign document code, in case if using a capture group alone is not sufficient;
* If specified, `foreign_capture_group` should be specified as well, so that it points to the first occurrence
* of the foreign code. When both are specified, `foreign_replacer` takes precedence.
*
* See:
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_function_as_a_parameter
*/
foreign_replacer?: replacer;
/**
* @deprecated `extract_to_foreign` will be removed in 4.0; use `foreign_capture_group` or `foreign_replacer` instead
*/
extract_to_foreign?: string | replacer;
/**
* If arguments from the cell or line magic are to be extracted and prepended before the extracted code,
* set extract_arguments to a replacer function taking the code and returning the string to be prepended.
*/
extract_arguments?: replacer;
/**
* Boolean if everything (true, default) or nothing (false) should be kept in the host document.
*
* For the R example this should be empty if we wish to ignore the cell,
* but usually a better option is to retain the foreign code and use language
* specific overrides to suppress the magic in a more controlled way, providing
* dummy python code to handle cell input/output.
*
* Setting to false is DEPRECATED as it breaks the edit feature (while it could be fixed,
* it would make the code considerably more complex).
*
* @deprecated `keep_in_host` will be removed in 4.0
*/
keep_in_host?: boolean;
/**
* Should the foreign code be appended (False) to the previously established virtual document of the same language,
* or is it standalone snippet which requires separate connection?
*/
is_standalone: boolean;
file_extension: string;
}
}