@jupyter-lsp/jupyterlab-lsp
Version:
Language Server Protocol integration for JupyterLab
255 lines (220 loc) • 8.61 kB
text/typescript
import { CodeEditor } from '@jupyterlab/codeeditor';
import {
positionAtOffset,
IExtractedCode,
IForeignCodeExtractor
} from '@jupyterlab/lsp';
import { replacer } from '../overrides/tokens';
export function getIndexOfCaptureGroup(
expression: RegExp,
matchedString: string,
valueOfCapturedGroup: 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 capturedGroups = expression.exec(matchedString);
if (capturedGroups == null) {
console.warn(
`No capture group found for ${expression} in ${matchedString}`
);
return -1;
}
let offsetInMatch = 0;
// first element is a full match
let fullMatched = capturedGroups[0];
for (let group of capturedGroups.slice(1)) {
if (typeof group === 'undefined') {
continue;
}
if (group === valueOfCapturedGroup) {
offsetInMatch += fullMatched.indexOf(group);
break;
}
let groupEndOffset = fullMatched.indexOf(group) + group.length;
fullMatched = fullMatched.slice(groupEndOffset);
offsetInMatch += groupEndOffset;
}
return offsetInMatch;
}
export class RegExpForeignCodeExtractor implements IForeignCodeExtractor {
options: RegExpForeignCodeExtractor.IOptions;
language: string;
globalExpression: RegExp;
testExpression: RegExp;
expression: RegExp;
standalone: boolean;
fileExtension: string;
cellType: string[] = ['code'];
constructor(options: RegExpForeignCodeExtractor.IOptions) {
this.language = options.language;
this.options = options;
this.globalExpression = new RegExp(options.pattern, 'g');
this.testExpression = new RegExp(options.pattern, 'g');
this.expression = new RegExp(options.pattern);
this.standalone = this.options.isStandalone;
this.fileExtension = this.options.fileExtension;
}
hasForeignCode(code: string): boolean {
const result = this.testExpression.test(code);
this.testExpression.lastIndex = 0;
return result;
}
extractForeignCode(code: string): IExtractedCode[] {
const lines = code.split('\n');
const extracts = new Array<IExtractedCode>();
let startedFrom = this.globalExpression.lastIndex;
let match: RegExpExecArray | null = this.globalExpression.exec(code);
let hostCodeFragment: string;
let chosenReplacer: string | replacer;
let isNewApiReplacer: boolean = false;
if (typeof this.options.foreignReplacer !== 'undefined') {
chosenReplacer = this.options.foreignReplacer;
isNewApiReplacer = true;
} else if (typeof this.options.foreignCaptureGroups !== 'undefined') {
chosenReplacer = '$' + this.options.foreignCaptureGroups.join('$');
isNewApiReplacer = true;
} else if (this.options.extractToForeign) {
chosenReplacer = this.options.extractToForeign;
} else {
console.warn(
`Foreign replacer not defined for extractor: {this.expression} - this is deprecated; use 'foreignReplacer' to define it`
);
return [];
}
while (match != null) {
let matchedString = match[0];
let positionShift: CodeEditor.IPosition | null = null;
let foreignCodeFragment = matchedString.replace(
this.expression,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
chosenReplacer
);
let prefix = '';
if (typeof this.options.extractArguments !== 'undefined') {
prefix = matchedString.replace(
this.expression,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.options.extractArguments
);
positionShift = positionAtOffset(prefix.length, prefix.split('\n'));
}
// NOTE:
// match.index + matchedString.length === this.sticky_expression.lastIndex
let endIndex = this.globalExpression.lastIndex;
if (this.options.keepInHost || this.options.keepInHost == null) {
hostCodeFragment = code.substring(startedFrom, endIndex);
} else {
if (startedFrom === match.index) {
hostCodeFragment = '';
} else {
hostCodeFragment = code.substring(startedFrom, match.index) + '\n';
}
}
let foreignCodeGroupValue = foreignCodeFragment;
if (isNewApiReplacer) {
foreignCodeGroupValue = matchedString.replace(
this.expression,
'$' + Math.min(...this.options.foreignCaptureGroups!)
);
}
const foreignGroupIndexInMatch = getIndexOfCaptureGroup(
this.expression,
matchedString,
foreignCodeGroupValue
);
let startOffset = match.index + foreignGroupIndexInMatch;
let start = positionAtOffset(startOffset, lines);
let end = positionAtOffset(
startOffset + foreignCodeFragment.length,
lines
);
extracts.push({
hostCode: hostCodeFragment,
foreignCode: prefix + foreignCodeFragment,
range: { start, end },
virtualShift: positionShift
});
startedFrom = this.globalExpression.lastIndex;
match = this.globalExpression.exec(code);
}
if (startedFrom !== code.length) {
let finalHostCodeFragment = code.substring(startedFrom, code.length);
extracts.push({
hostCode: finalHostCodeFragment,
foreignCode: null,
range: null,
virtualShift: 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 `foreignReplacer` in addition to
* `foreignCaptureGroups` (but not instead!).
*
* `foreignCaptureGroups` is required for proper offset calculation and will no longer be optional in 4.0.
*/
foreignCaptureGroups?: 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, `foreignReplacer` takes precedence.
*
* See:
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_function_as_a_parameter
*/
foreignReplacer?: replacer;
/**
* @deprecated `extractToForeign` will be removed in 4.0; use `foreign_capture_group` or `foreignReplacer` instead
*/
extractToForeign?: string | replacer;
/**
* If arguments from the cell or line magic are to be extracted and prepended before the extracted code,
* set extractArguments to a replacer function taking the code and returning the string to be prepended.
*/
extractArguments?: 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 `keepInHost` will be removed in 4.0
*/
keepInHost?: 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?
*/
isStandalone: boolean;
fileExtension: string;
}
}