@jupyter-lsp/jupyterlab-lsp
Version:
Language Server Protocol integration for JupyterLab
234 lines (207 loc) • 6.1 kB
text/typescript
import { PageConfig } from '@jupyterlab/coreutils';
import { ReadonlyJSONObject, ReadonlyJSONValue } from '@lumino/coreutils';
import mergeWith from 'lodash.mergewith';
const RE_PATH_ANCHOR = /^file:\/\/([^\/]+|\/[a-zA-Z](?::|%3A))/;
export async function sleep(timeout: number) {
return new Promise<void>(resolve => {
setTimeout(() => {
resolve();
}, timeout);
});
}
export type ModifierKey =
| 'Shift'
| 'Alt'
| 'AltGraph'
| 'Control'
| 'Meta'
| 'Accel';
/**
* CodeMirror-proof implementation of event.getModifierState()
*/
export function getModifierState(
event: MouseEvent | KeyboardEvent,
modifierKey: ModifierKey
): boolean {
// Note: Safari does not support getModifierState on MouseEvent, see:
// https://github.com/krassowski/jupyterlab-go-to-definition/issues/3
// thus AltGraph and others are not supported on Safari
// Full list of modifier keys and mappings to physical keys on different OSes:
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState
// the key approach is needed for CodeMirror events which do not set
// *key (e.g. ctrlKey) correctly
const key = (event as KeyboardEvent).key || null;
let value = false;
switch (modifierKey) {
case 'Shift':
value = event.shiftKey || key == 'Shift';
break;
case 'Alt':
value = event.altKey || key == 'Alt';
break;
case 'AltGraph':
value = key == 'AltGraph';
break;
case 'Control':
value = event.ctrlKey || key == 'Control';
break;
case 'Meta':
value = event.metaKey || key == 'Meta';
break;
case 'Accel':
value =
event.metaKey || key == 'Meta' || event.ctrlKey || key == 'Control';
break;
}
if (event.getModifierState !== undefined) {
return value || event.getModifierState(modifierKey);
}
return value;
}
export class DefaultMap<K, V> extends Map<K, V> {
constructor(
private defaultFactory: (...args: any[]) => V,
entries?: ReadonlyArray<readonly [K, V]> | null
) {
super(entries);
}
get(k: K): V {
return this.getOrCreate(k);
}
getOrCreate(k: K, ...args: any[]): V {
if (this.has(k)) {
return super.get(k)!;
} else {
let v = this.defaultFactory(k, ...args);
this.set(k, v);
return v;
}
}
}
function serverRootUri() {
return PageConfig.getOption('rootUri');
}
/**
* compare two URIs, discounting:
* - drive capitalization
* - uri encoding
* TODO: probably use vscode-uri
*/
export function urisEqual(a: string, b: string) {
const winPaths = isWinPath(a) && isWinPath(b);
if (winPaths) {
a = normalizeWinPath(a);
b = normalizeWinPath(b);
}
return a === b || decodeURI(a) === decodeURI(b);
}
/**
* grossly detect whether a URI represents a file on a windows drive
*/
export function isWinPath(uri: string) {
return uri.match(RE_PATH_ANCHOR);
}
/**
* lowercase the drive component of a URI
*/
export function normalizeWinPath(uri: string) {
// Pyright encodes colon on Windows, see:
// https://github.com/jupyter-lsp/jupyterlab-lsp/pull/587#issuecomment-844225253
return uri.replace(RE_PATH_ANCHOR, it =>
it.replace('%3A', ':').toLowerCase()
);
}
export function uriToContentsPath(child: string, parent?: string) {
parent = parent || serverRootUri();
if (parent == null) {
return null;
}
const winPaths = isWinPath(parent) && isWinPath(child);
if (winPaths) {
parent = normalizeWinPath(parent);
child = normalizeWinPath(child);
}
if (child.startsWith(parent)) {
// 'decodeURIComponent' is needed over 'decodeURI' for '@' in TS/JS paths
return decodeURIComponent(child.replace(parent, ''));
}
return null;
}
/**
* The docs for many language servers show settings in the
* VSCode format, e.g.: "pyls.plugins.pyflakes.enabled"
*
* VSCode converts that dot notation to JSON behind the scenes,
* as the language servers themselves don't accept that syntax.
*/
export const expandPath = (
path: string[],
value: ReadonlyJSONValue
): ReadonlyJSONObject => {
const obj: any = {};
let curr = obj;
path.forEach((prop: string, i: any) => {
curr[prop] = {};
if (i === path.length - 1) {
curr[prop] = value;
} else {
curr = curr[prop];
}
});
return obj;
};
export const expandDottedPaths = (
obj: ReadonlyJSONObject
): ReadonlyJSONObject => {
const settings: any = [];
for (let key in obj) {
const parsed = expandPath(key.split('.'), obj[key]);
settings.push(parsed);
}
return mergeWith({}, ...settings);
};
interface ICollapsingResult {
result: Record<string, ReadonlyJSONValue>;
conflicts: Record<string, any[]>;
}
export function collapseToDotted(obj: ReadonlyJSONObject): ICollapsingResult {
const result: Record<string, ReadonlyJSONValue> = {};
const conflicts: Record<string, any[]> = {};
const collapse = (obj: any, root = ''): void => {
for (let [key, value] of Object.entries(obj)) {
const prefix = root ? root + '.' + key : key;
if (
value != null &&
typeof value === 'object' &&
!Array.isArray(value) &&
Object.keys(value!).length !== 0
) {
collapse(value, prefix);
} else {
if (result.hasOwnProperty(prefix) && result[prefix] !== value) {
if (!conflicts.hasOwnProperty(prefix)) {
conflicts[prefix] = [];
conflicts[prefix].push(result[prefix]);
}
if (!conflicts[prefix].includes(value)) {
conflicts[prefix].push(value);
}
}
result[prefix] = value as ReadonlyJSONValue;
}
}
};
collapse(obj);
return {
result: result as any as ReadonlyJSONObject,
conflicts: conflicts
};
}
export function escapeMarkdown(text: string) {
// note: keeping backticks for highlighting of code sections
text = text.replace(/([\\#*_[\]])/g, '\\$1');
// escape HTML
const span = document.createElement('span');
span.textContent = text;
return span.innerHTML.replace(/\n/g, '<br>').replace(/ {2}/g, '\u00A0\u00A0');
}