2022-11-03 17:55:23 +01:00
|
|
|
/*
|
|
|
|
|
Copyright (c) Microsoft Corporation.
|
|
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
|
limitations under the License.
|
|
|
|
|
*/
|
|
|
|
|
|
2023-03-11 01:22:19 +01:00
|
|
|
import './codeMirrorWrapper.css';
|
2022-11-03 17:55:23 +01:00
|
|
|
import * as React from 'react';
|
2023-03-06 19:40:45 +01:00
|
|
|
import type { CodeMirror } from './codeMirrorModule';
|
2023-09-01 18:09:47 +02:00
|
|
|
import { ansi2html } from '../ansi2html';
|
2024-08-01 18:27:45 +02:00
|
|
|
import { useMeasure, kWebLinkRe } from '../uiUtils';
|
2022-11-03 17:55:23 +01:00
|
|
|
|
|
|
|
|
export type SourceHighlight = {
|
|
|
|
|
line: number;
|
|
|
|
|
type: 'running' | 'paused' | 'error';
|
2023-03-11 01:22:19 +01:00
|
|
|
message?: string;
|
2022-11-03 17:55:23 +01:00
|
|
|
};
|
|
|
|
|
|
2024-08-01 18:27:45 +02:00
|
|
|
export type Language = 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl' | 'html' | 'css' | 'markdown';
|
2023-01-27 23:18:46 +01:00
|
|
|
|
2022-11-03 17:55:23 +01:00
|
|
|
export interface SourceProps {
|
|
|
|
|
text: string;
|
2023-08-25 21:10:28 +02:00
|
|
|
language?: Language;
|
2024-08-01 18:27:45 +02:00
|
|
|
mimeType?: string;
|
|
|
|
|
linkify?: boolean;
|
2023-03-11 01:22:19 +01:00
|
|
|
readOnly?: boolean;
|
2022-11-03 17:55:23 +01:00
|
|
|
// 1-based
|
|
|
|
|
highlight?: SourceHighlight[];
|
|
|
|
|
revealLine?: number;
|
|
|
|
|
lineNumbers?: boolean;
|
2023-08-21 19:59:49 +02:00
|
|
|
isFocused?: boolean;
|
2022-11-03 17:55:23 +01:00
|
|
|
focusOnChange?: boolean;
|
|
|
|
|
wrapLines?: boolean;
|
|
|
|
|
onChange?: (text: string) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
|
|
|
|
text,
|
|
|
|
|
language,
|
2024-08-01 18:27:45 +02:00
|
|
|
mimeType,
|
|
|
|
|
linkify,
|
2022-11-03 17:55:23 +01:00
|
|
|
readOnly,
|
2023-03-10 04:34:05 +01:00
|
|
|
highlight,
|
2022-11-03 17:55:23 +01:00
|
|
|
revealLine,
|
|
|
|
|
lineNumbers,
|
2023-08-21 19:59:49 +02:00
|
|
|
isFocused,
|
2022-11-03 17:55:23 +01:00
|
|
|
focusOnChange,
|
|
|
|
|
wrapLines,
|
|
|
|
|
onChange,
|
|
|
|
|
}) => {
|
2023-03-22 02:20:48 +01:00
|
|
|
const [measure, codemirrorElement] = useMeasure<HTMLDivElement>();
|
2023-03-06 19:40:45 +01:00
|
|
|
const [modulePromise] = React.useState<Promise<CodeMirror>>(import('./codeMirrorModule').then(m => m.default));
|
2023-03-17 04:09:09 +01:00
|
|
|
const codemirrorRef = React.useRef<{ cm: CodeMirror.Editor, highlight?: SourceHighlight[], widgets?: CodeMirror.LineWidget[] } | null>(null);
|
2023-03-10 01:46:31 +01:00
|
|
|
const [codemirror, setCodemirror] = React.useState<CodeMirror.Editor>();
|
2022-11-03 17:55:23 +01:00
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
2023-03-06 19:40:45 +01:00
|
|
|
(async () => {
|
|
|
|
|
// Always load the module first.
|
|
|
|
|
const CodeMirror = await modulePromise;
|
2024-08-01 18:27:45 +02:00
|
|
|
defineCustomMode(CodeMirror);
|
2023-03-06 19:40:45 +01:00
|
|
|
|
|
|
|
|
const element = codemirrorElement.current;
|
|
|
|
|
if (!element)
|
|
|
|
|
return;
|
|
|
|
|
|
2024-08-01 18:27:45 +02:00
|
|
|
const mode = languageToMode(language) || mimeTypeToMode(mimeType) || (linkify ? 'text/linkified' : '');
|
2023-03-06 19:40:45 +01:00
|
|
|
|
2023-03-10 04:34:05 +01:00
|
|
|
if (codemirrorRef.current
|
2023-03-11 01:22:19 +01:00
|
|
|
&& mode === codemirrorRef.current.cm.getOption('mode')
|
|
|
|
|
&& !!readOnly === codemirrorRef.current.cm.getOption('readOnly')
|
|
|
|
|
&& lineNumbers === codemirrorRef.current.cm.getOption('lineNumbers')
|
|
|
|
|
&& wrapLines === codemirrorRef.current.cm.getOption('lineWrapping')) {
|
2023-03-10 04:34:05 +01:00
|
|
|
// No need to re-create codemirror.
|
2023-03-06 19:40:45 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Either configuration is different or we don't have a codemirror yet.
|
2023-03-11 01:22:19 +01:00
|
|
|
codemirrorRef.current?.cm?.getWrapperElement().remove();
|
2023-03-06 19:40:45 +01:00
|
|
|
const cm = CodeMirror(element, {
|
|
|
|
|
value: '',
|
|
|
|
|
mode,
|
2023-03-11 01:22:19 +01:00
|
|
|
readOnly: !!readOnly,
|
2023-03-06 19:40:45 +01:00
|
|
|
lineNumbers,
|
|
|
|
|
lineWrapping: wrapLines,
|
|
|
|
|
});
|
2023-03-17 04:09:09 +01:00
|
|
|
codemirrorRef.current = { cm };
|
2023-08-21 19:59:49 +02:00
|
|
|
if (isFocused)
|
|
|
|
|
cm.focus();
|
2023-03-10 01:46:31 +01:00
|
|
|
setCodemirror(cm);
|
2023-03-06 19:40:45 +01:00
|
|
|
return cm;
|
|
|
|
|
})();
|
2024-08-01 18:27:45 +02:00
|
|
|
}, [modulePromise, codemirror, codemirrorElement, language, mimeType, linkify, lineNumbers, wrapLines, readOnly, isFocused]);
|
2023-03-10 04:34:05 +01:00
|
|
|
|
2023-03-22 02:20:48 +01:00
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (codemirrorRef.current)
|
|
|
|
|
codemirrorRef.current.cm.setSize(measure.width, measure.height);
|
|
|
|
|
}, [measure]);
|
|
|
|
|
|
2023-04-21 21:38:39 +02:00
|
|
|
React.useLayoutEffect(() => {
|
2023-03-10 04:34:05 +01:00
|
|
|
if (!codemirror)
|
|
|
|
|
return;
|
|
|
|
|
|
2023-03-17 04:09:09 +01:00
|
|
|
let valueChanged = false;
|
2023-03-10 04:34:05 +01:00
|
|
|
if (codemirror.getValue() !== text) {
|
|
|
|
|
codemirror.setValue(text);
|
2023-03-17 04:09:09 +01:00
|
|
|
valueChanged = true;
|
2023-03-10 04:34:05 +01:00
|
|
|
if (focusOnChange) {
|
|
|
|
|
codemirror.execCommand('selectAll');
|
|
|
|
|
codemirror.focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-11 01:22:19 +01:00
|
|
|
|
2023-03-17 04:09:09 +01:00
|
|
|
if (valueChanged || JSON.stringify(highlight) !== JSON.stringify(codemirrorRef.current!.highlight)) {
|
|
|
|
|
// Line highlight.
|
|
|
|
|
for (const h of codemirrorRef.current!.highlight || [])
|
|
|
|
|
codemirror.removeLineClass(h.line - 1, 'wrap');
|
|
|
|
|
for (const h of highlight || [])
|
|
|
|
|
codemirror.addLineClass(h.line - 1, 'wrap', `source-line-${h.type}`);
|
|
|
|
|
|
|
|
|
|
// Error widgets.
|
|
|
|
|
for (const w of codemirrorRef.current!.widgets || [])
|
|
|
|
|
codemirror.removeLineWidget(w);
|
|
|
|
|
const widgets: CodeMirror.LineWidget[] = [];
|
|
|
|
|
for (const h of highlight || []) {
|
|
|
|
|
if (h.type !== 'error')
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
const line = codemirrorRef.current?.cm.getLine(h.line - 1);
|
|
|
|
|
if (line) {
|
|
|
|
|
const underlineWidgetElement = document.createElement('div');
|
|
|
|
|
underlineWidgetElement.className = 'source-line-error-underline';
|
|
|
|
|
underlineWidgetElement.innerHTML = ' '.repeat(line.length || 1);
|
|
|
|
|
widgets.push(codemirror.addLineWidget(h.line, underlineWidgetElement, { above: true, coverGutter: false }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const errorWidgetElement = document.createElement('div');
|
2023-09-01 18:09:47 +02:00
|
|
|
errorWidgetElement.innerHTML = ansi2html(h.message || '');
|
2023-03-17 04:09:09 +01:00
|
|
|
errorWidgetElement.className = 'source-line-error-widget';
|
|
|
|
|
widgets.push(codemirror.addLineWidget(h.line, errorWidgetElement, { above: true, coverGutter: false }));
|
2023-03-11 01:22:19 +01:00
|
|
|
}
|
2023-03-17 04:09:09 +01:00
|
|
|
codemirrorRef.current!.highlight = highlight;
|
|
|
|
|
codemirrorRef.current!.widgets = widgets;
|
2023-03-11 01:22:19 +01:00
|
|
|
}
|
2023-04-21 21:38:39 +02:00
|
|
|
|
2023-03-19 20:04:19 +01:00
|
|
|
// Line-less locations have line = 0, but they mean to reveal the file.
|
|
|
|
|
if (typeof revealLine === 'number' && codemirrorRef.current!.cm.lineCount() >= revealLine)
|
|
|
|
|
codemirror.scrollIntoView({ line: Math.max(0, revealLine - 1), ch: 0 }, 50);
|
2023-04-21 21:38:39 +02:00
|
|
|
|
|
|
|
|
let changeListener: () => void | undefined;
|
|
|
|
|
if (onChange) {
|
|
|
|
|
changeListener = () => onChange(codemirror.getValue());
|
|
|
|
|
codemirror.on('change', changeListener);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
if (changeListener)
|
|
|
|
|
codemirror.off('change', changeListener);
|
|
|
|
|
};
|
2023-03-10 04:34:05 +01:00
|
|
|
}, [codemirror, text, highlight, revealLine, focusOnChange, onChange]);
|
2022-11-03 17:55:23 +01:00
|
|
|
|
2024-08-01 18:27:45 +02:00
|
|
|
return <div className='cm-wrapper' ref={codemirrorElement} onClick={onCodeMirrorClick}></div>;
|
2022-11-03 17:55:23 +01:00
|
|
|
};
|
2024-08-01 18:27:45 +02:00
|
|
|
|
|
|
|
|
function onCodeMirrorClick(event: React.MouseEvent) {
|
|
|
|
|
if (!(event.target instanceof HTMLElement))
|
|
|
|
|
return;
|
|
|
|
|
let url: string | undefined;
|
|
|
|
|
if (event.target.classList.contains('cm-linkified')) {
|
|
|
|
|
// 'text/linkified' custom mode
|
|
|
|
|
url = event.target.textContent!;
|
|
|
|
|
} else if (event.target.classList.contains('cm-link') && event.target.nextElementSibling?.classList.contains('cm-url')) {
|
|
|
|
|
// 'markdown' mode
|
|
|
|
|
url = event.target.nextElementSibling.textContent!.slice(1, -1);
|
|
|
|
|
}
|
|
|
|
|
if (url) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
window.open(url, '_blank');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let customModeDefined = false;
|
|
|
|
|
function defineCustomMode(cm: CodeMirror) {
|
|
|
|
|
if (customModeDefined)
|
|
|
|
|
return;
|
|
|
|
|
customModeDefined = true;
|
|
|
|
|
(cm as any).defineSimpleMode('text/linkified', {
|
|
|
|
|
start: [
|
|
|
|
|
{ regex: kWebLinkRe, token: 'linkified' },
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mimeTypeToMode(mimeType: string | undefined): string | undefined {
|
|
|
|
|
if (!mimeType)
|
|
|
|
|
return;
|
|
|
|
|
if (mimeType.includes('javascript') || mimeType.includes('json'))
|
|
|
|
|
return 'javascript';
|
|
|
|
|
if (mimeType.includes('python'))
|
|
|
|
|
return 'python';
|
|
|
|
|
if (mimeType.includes('csharp'))
|
|
|
|
|
return 'text/x-csharp';
|
|
|
|
|
if (mimeType.includes('java'))
|
|
|
|
|
return 'text/x-java';
|
|
|
|
|
if (mimeType.includes('markdown'))
|
|
|
|
|
return 'markdown';
|
|
|
|
|
if (mimeType.includes('html') || mimeType.includes('svg'))
|
|
|
|
|
return 'htmlmixed';
|
|
|
|
|
if (mimeType.includes('css'))
|
|
|
|
|
return 'css';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function languageToMode(language: Language | undefined): string | undefined {
|
|
|
|
|
if (!language)
|
|
|
|
|
return;
|
|
|
|
|
return {
|
|
|
|
|
javascript: 'javascript',
|
|
|
|
|
jsonl: 'javascript',
|
|
|
|
|
python: 'python',
|
|
|
|
|
csharp: 'text/x-csharp',
|
|
|
|
|
java: 'text/x-java',
|
|
|
|
|
markdown: 'markdown',
|
|
|
|
|
html: 'htmlmixed',
|
|
|
|
|
css: 'css',
|
|
|
|
|
}[language];
|
|
|
|
|
}
|