cherry-pick(#21856): chore: pack codemirror on resize

This commit is contained in:
Pavel Feldman 2023-03-21 18:20:48 -07:00
parent 8693fd4743
commit 08422f0651
10 changed files with 95 additions and 82 deletions

View file

@ -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';

View file

@ -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;
}

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -1,6 +1,7 @@
[*]
../theme.ts
../third_party/vscode/codicon.css
../uiUtils.ts
[expandable.spec.tsx]
***

View file

@ -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;

View file

@ -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,10 +32,10 @@ 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);
const terminal = React.useRef<{ terminal: Terminal, fitAddon: FitAddon } | null>(null);
React.useEffect(() => {
addThemeListener(setTheme);
@ -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>;
};

View file

@ -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 '-';

View file

@ -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`);
});