cherry-pick(#21856): chore: pack codemirror on resize
This commit is contained in:
parent
8693fd4743
commit
08422f0651
|
|
@ -17,8 +17,7 @@
|
|||
import './filmStrip.css';
|
||||
import type { Boundaries, Size } from '../geometry';
|
||||
import * as React from 'react';
|
||||
import { useMeasure } from './helpers';
|
||||
import { upperBound } from '@web/uiUtils';
|
||||
import { useMeasure, upperBound } from '@web/uiUtils';
|
||||
import type { PageEntry } from '../entries';
|
||||
import type { MultiTraceModel } from './modelUtil';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
// Recalculates the value when dependencies change.
|
||||
export function useAsyncMemo<T>(fn: () => Promise<T>, deps: React.DependencyList, initialValue: T, resetValue?: T) {
|
||||
const [value, setValue] = React.useState<T>(initialValue);
|
||||
React.useEffect(() => {
|
||||
let canceled = false;
|
||||
if (resetValue !== undefined)
|
||||
setValue(resetValue);
|
||||
fn().then(value => {
|
||||
if (!canceled)
|
||||
setValue(value);
|
||||
});
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, deps);
|
||||
return value;
|
||||
}
|
||||
|
||||
// Tracks the element size and returns it's contentRect (always has x=0, y=0).
|
||||
export function useMeasure<T extends Element>() {
|
||||
const ref = React.useRef<T | null>(null);
|
||||
const [measure, setMeasure] = React.useState(new DOMRect(0, 0, 10, 10));
|
||||
React.useLayoutEffect(() => {
|
||||
const target = ref.current;
|
||||
if (!target)
|
||||
return;
|
||||
const resizeObserver = new ResizeObserver((entries: any) => {
|
||||
const entry = entries[entries.length - 1];
|
||||
if (entry && entry.contentRect)
|
||||
setMeasure(entry.contentRect);
|
||||
});
|
||||
resizeObserver.observe(target);
|
||||
return () => resizeObserver.unobserve(target);
|
||||
}, [ref]);
|
||||
return [measure, ref] as const;
|
||||
}
|
||||
|
|
@ -16,13 +16,12 @@
|
|||
|
||||
import './snapshotTab.css';
|
||||
import * as React from 'react';
|
||||
import { useMeasure } from './helpers';
|
||||
import type { ActionTraceEvent } from '@trace/trace';
|
||||
import { context, prevInList } from './modelUtil';
|
||||
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
||||
import { Toolbar } from '@web/components/toolbar';
|
||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||
import { copy } from '@web/uiUtils';
|
||||
import { copy, useMeasure } from '@web/uiUtils';
|
||||
import { InjectedScript } from '@injected/injectedScript';
|
||||
import { Recorder } from '@injected/recorder';
|
||||
import { asLocator } from '@isomorphic/locatorGenerators';
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
import type { ActionTraceEvent } from '@trace/trace';
|
||||
import { SplitView } from '@web/components/splitView';
|
||||
import * as React from 'react';
|
||||
import { useAsyncMemo } from './helpers';
|
||||
import { useAsyncMemo } from '@web/uiUtils';
|
||||
import './sourceTab.css';
|
||||
import { StackTraceView } from './stackTrace';
|
||||
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
||||
|
|
|
|||
|
|
@ -16,11 +16,10 @@
|
|||
*/
|
||||
|
||||
import type { ActionTraceEvent, EventTraceEvent } from '@trace/trace';
|
||||
import { msToString } from '@web/uiUtils';
|
||||
import { msToString, useMeasure } from '@web/uiUtils';
|
||||
import * as React from 'react';
|
||||
import type { Boundaries } from '../geometry';
|
||||
import { FilmStrip } from './filmStrip';
|
||||
import { useMeasure } from './helpers';
|
||||
import type { MultiTraceModel } from './modelUtil';
|
||||
import './timeline.css';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
[*]
|
||||
../theme.ts
|
||||
../third_party/vscode/codicon.css
|
||||
../uiUtils.ts
|
||||
|
||||
[expandable.spec.tsx]
|
||||
***
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import './codeMirrorWrapper.css';
|
|||
import * as React from 'react';
|
||||
import type { CodeMirror } from './codeMirrorModule';
|
||||
import { ansi2htmlMarkup } from './errorMessage';
|
||||
import { useMeasure } from '../uiUtils';
|
||||
|
||||
export type SourceHighlight = {
|
||||
line: number;
|
||||
|
|
@ -51,7 +52,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
|||
wrapLines,
|
||||
onChange,
|
||||
}) => {
|
||||
const codemirrorElement = React.useRef<HTMLDivElement>(null);
|
||||
const [measure, codemirrorElement] = useMeasure<HTMLDivElement>();
|
||||
const [modulePromise] = React.useState<Promise<CodeMirror>>(import('./codeMirrorModule').then(m => m.default));
|
||||
const codemirrorRef = React.useRef<{ cm: CodeMirror.Editor, highlight?: SourceHighlight[], widgets?: CodeMirror.LineWidget[] } | null>(null);
|
||||
const [codemirror, setCodemirror] = React.useState<CodeMirror.Editor>();
|
||||
|
|
@ -98,6 +99,11 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
|||
})();
|
||||
}, [modulePromise, codemirror, codemirrorElement, language, lineNumbers, wrapLines, readOnly]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (codemirrorRef.current)
|
||||
codemirrorRef.current.cm.setSize(measure.width, measure.height);
|
||||
}, [measure]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!codemirror)
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import type { ITheme, Terminal } from 'xterm';
|
|||
import type { FitAddon } from 'xterm-addon-fit';
|
||||
import type { XtermModule } from './xtermModule';
|
||||
import { currentTheme, addThemeListener, removeThemeListener } from '@web/theme';
|
||||
import { useMeasure } from '@web/uiUtils';
|
||||
|
||||
export type XtermDataSource = {
|
||||
pending: (string | Uint8Array)[];
|
||||
|
|
@ -31,7 +32,7 @@ export type XtermDataSource = {
|
|||
export const XtermWrapper: React.FC<{ source: XtermDataSource }> = ({
|
||||
source,
|
||||
}) => {
|
||||
const xtermElement = React.useRef<HTMLDivElement>(null);
|
||||
const [measure, xtermElement] = useMeasure<HTMLDivElement>();
|
||||
const [theme, setTheme] = React.useState(currentTheme());
|
||||
const [modulePromise] = React.useState<Promise<XtermModule>>(import('./xtermModule').then(m => m.default));
|
||||
const terminal = React.useRef<{ terminal: Terminal, fitAddon: FitAddon } | null>(null);
|
||||
|
|
@ -44,7 +45,6 @@ export const XtermWrapper: React.FC<{ source: XtermDataSource }> = ({
|
|||
React.useEffect(() => {
|
||||
const oldSourceWrite = source.write;
|
||||
const oldSourceClear = source.clear;
|
||||
let resizeObserver: ResizeObserver | undefined;
|
||||
|
||||
(async () => {
|
||||
// Always load the module first.
|
||||
|
|
@ -53,15 +53,19 @@ export const XtermWrapper: React.FC<{ source: XtermDataSource }> = ({
|
|||
if (!element)
|
||||
return;
|
||||
|
||||
if (terminal.current && terminal)
|
||||
const terminalTheme = theme === 'dark-mode' ? darkTheme : lightTheme;
|
||||
if (terminal.current && terminal.current.terminal.options.theme === terminalTheme)
|
||||
return;
|
||||
|
||||
if (terminal.current)
|
||||
element.textContent = '';
|
||||
|
||||
const newTerminal = new Terminal({
|
||||
convertEol: true,
|
||||
fontSize: 13,
|
||||
scrollback: 10000,
|
||||
fontFamily: 'var(--vscode-editor-font-family)',
|
||||
theme: theme === 'dark-mode' ? darkTheme : lightTheme
|
||||
theme: terminalTheme,
|
||||
});
|
||||
|
||||
const fitAddon = new FitAddon();
|
||||
|
|
@ -79,27 +83,23 @@ export const XtermWrapper: React.FC<{ source: XtermDataSource }> = ({
|
|||
newTerminal.open(element);
|
||||
fitAddon.fit();
|
||||
terminal.current = { terminal: newTerminal, fitAddon };
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
// Fit reads data from the terminal itself, which updates lazily, probably on some timer
|
||||
// or mutation observer. Work around it.
|
||||
setTimeout(() => {
|
||||
fitAddon.fit();
|
||||
source.resize(newTerminal.cols, newTerminal.rows);
|
||||
}, 100);
|
||||
});
|
||||
resizeObserver.observe(element);
|
||||
})();
|
||||
return () => {
|
||||
source.clear = oldSourceClear;
|
||||
source.write = oldSourceWrite;
|
||||
resizeObserver?.disconnect();
|
||||
};
|
||||
}, [modulePromise, terminal, xtermElement, source, theme]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (terminal.current)
|
||||
terminal.current.terminal.options.theme = theme === 'dark-mode' ? darkTheme : lightTheme;
|
||||
}, [theme]);
|
||||
// Fit reads data from the terminal itself, which updates lazily, probably on some timer
|
||||
// or mutation observer. Work around it.
|
||||
setTimeout(() => {
|
||||
if (!terminal.current)
|
||||
return;
|
||||
terminal.current.fitAddon.fit();
|
||||
source.resize(terminal.current.terminal.cols, terminal.current.terminal.rows);
|
||||
}, 250);
|
||||
}, [measure, source]);
|
||||
|
||||
return <div data-testid='output' className='xterm-wrapper' style={{ flex: 'auto' }} ref={xtermElement}></div>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,6 +16,44 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
// Recalculates the value when dependencies change.
|
||||
export function useAsyncMemo<T>(fn: () => Promise<T>, deps: React.DependencyList, initialValue: T, resetValue?: T) {
|
||||
const [value, setValue] = React.useState<T>(initialValue);
|
||||
React.useEffect(() => {
|
||||
let canceled = false;
|
||||
if (resetValue !== undefined)
|
||||
setValue(resetValue);
|
||||
fn().then(value => {
|
||||
if (!canceled)
|
||||
setValue(value);
|
||||
});
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, deps);
|
||||
return value;
|
||||
}
|
||||
|
||||
// Tracks the element size and returns it's contentRect (always has x=0, y=0).
|
||||
export function useMeasure<T extends Element>() {
|
||||
const ref = React.useRef<T | null>(null);
|
||||
const [measure, setMeasure] = React.useState(new DOMRect(0, 0, 10, 10));
|
||||
React.useLayoutEffect(() => {
|
||||
const target = ref.current;
|
||||
if (!target)
|
||||
return;
|
||||
const resizeObserver = new ResizeObserver((entries: any) => {
|
||||
const entry = entries[entries.length - 1];
|
||||
if (entry && entry.contentRect)
|
||||
setMeasure(entry.contentRect);
|
||||
});
|
||||
resizeObserver.observe(target);
|
||||
return () => resizeObserver.disconnect();
|
||||
}, [ref]);
|
||||
return [measure, ref] as const;
|
||||
}
|
||||
|
||||
export function msToString(ms: number): string {
|
||||
if (!isFinite(ms))
|
||||
return '-';
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { test, expect } from './ui-mode-fixtures';
|
|||
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
|
||||
test('should list tests', async ({ runUITest }) => {
|
||||
test('should print load errors', async ({ runUITest }) => {
|
||||
const page = await runUITest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
|
@ -30,3 +30,29 @@ test('should list tests', async ({ runUITest }) => {
|
|||
await page.getByTitle('Toggle output').click();
|
||||
await expect(page.getByTestId('output')).toContainText(`Unexpected reserved word 'await'`);
|
||||
});
|
||||
|
||||
test('should work after theme switch', async ({ runUITest, writeFiles }) => {
|
||||
const page = await runUITest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('syntax error', async () => {
|
||||
console.log('Hello world 1');
|
||||
});
|
||||
`,
|
||||
});
|
||||
await page.getByTitle('Toggle output').click();
|
||||
await page.getByTitle('Run all').click();
|
||||
await expect(page.getByTestId('output')).toContainText(`Hello world 1`);
|
||||
|
||||
await page.getByTitle('Toggle color mode').click();
|
||||
writeFiles({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('syntax error', async () => {
|
||||
console.log('Hello world 2');
|
||||
});
|
||||
`,
|
||||
});
|
||||
await page.getByTitle('Run all').click();
|
||||
await expect(page.getByTestId('output')).toContainText(`Hello world 2`);
|
||||
});
|
||||
Loading…
Reference in a new issue