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-03-11 01:22:19 +01:00
|
|
|
import { ansi2htmlMarkup } from './errorMessage';
|
2023-03-22 02:20:48 +01:00
|
|
|
import { useMeasure } 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
|
|
|
};
|
|
|
|
|
|
2023-01-27 23:18:46 +01:00
|
|
|
export type Language = 'javascript' | 'python' | 'java' | 'csharp';
|
|
|
|
|
|
2022-11-03 17:55:23 +01:00
|
|
|
export interface SourceProps {
|
|
|
|
|
text: string;
|
2023-01-27 23:18:46 +01:00
|
|
|
language: Language;
|
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;
|
|
|
|
|
focusOnChange?: boolean;
|
|
|
|
|
wrapLines?: boolean;
|
|
|
|
|
onChange?: (text: string) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
|
|
|
|
text,
|
|
|
|
|
language,
|
|
|
|
|
readOnly,
|
2023-03-10 04:34:05 +01:00
|
|
|
highlight,
|
2022-11-03 17:55:23 +01:00
|
|
|
revealLine,
|
|
|
|
|
lineNumbers,
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
const element = codemirrorElement.current;
|
|
|
|
|
if (!element)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
let mode = 'javascript';
|
|
|
|
|
if (language === 'python')
|
|
|
|
|
mode = 'python';
|
|
|
|
|
if (language === 'java')
|
|
|
|
|
mode = 'text/x-java';
|
|
|
|
|
if (language === 'csharp')
|
|
|
|
|
mode = 'text/x-csharp';
|
|
|
|
|
|
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-03-10 01:46:31 +01:00
|
|
|
setCodemirror(cm);
|
2023-03-06 19:40:45 +01:00
|
|
|
return cm;
|
|
|
|
|
})();
|
2023-03-10 04:34:05 +01:00
|
|
|
}, [modulePromise, codemirror, codemirrorElement, language, lineNumbers, wrapLines, readOnly]);
|
|
|
|
|
|
2023-03-22 02:20:48 +01:00
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (codemirrorRef.current)
|
|
|
|
|
codemirrorRef.current.cm.setSize(measure.width, measure.height);
|
|
|
|
|
}, [measure]);
|
|
|
|
|
|
2023-03-10 04:34:05 +01:00
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (!codemirror)
|
|
|
|
|
return;
|
2023-04-21 06:35:01 +02:00
|
|
|
codemirror.off('change', (codemirror as any).listenerSymbol);
|
2023-03-10 04:34:05 +01:00
|
|
|
(codemirror as any)[listenerSymbol] = undefined;
|
|
|
|
|
if (onChange) {
|
|
|
|
|
(codemirror as any)[listenerSymbol] = () => onChange(codemirror.getValue());
|
|
|
|
|
codemirror.on('change', (codemirror as any)[listenerSymbol]);
|
|
|
|
|
}
|
|
|
|
|
|
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');
|
|
|
|
|
errorWidgetElement.innerHTML = ansi2htmlMarkup(h.message || '');
|
|
|
|
|
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-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-03-10 04:34:05 +01:00
|
|
|
}, [codemirror, text, highlight, revealLine, focusOnChange, onChange]);
|
2022-11-03 17:55:23 +01:00
|
|
|
|
|
|
|
|
return <div className='cm-wrapper' ref={codemirrorElement}></div>;
|
|
|
|
|
};
|
|
|
|
|
|
2023-03-10 04:34:05 +01:00
|
|
|
const listenerSymbol = Symbol('listener');
|