diff --git a/packages/core-browser/__tests__/dom/fastdom.test.ts b/packages/core-browser/__tests__/dom/fastdom.test.ts new file mode 100644 index 0000000000..ae2e1753ec --- /dev/null +++ b/packages/core-browser/__tests__/dom/fastdom.test.ts @@ -0,0 +1,114 @@ +import { fastdom } from '@opensumi/ide-core-browser'; + +/** + * 用于 mock requestAnimationFrame 的表现,可以手动触发动画帧 + */ +class AnimationFrameController { + private callbacks: Array<() => void> = []; + private currentFrame: number = 0; + private running: boolean = false; + + constructor() { + this.run = this.run.bind(this); + } + + /** + * 触发动画帧 + */ + public run() { + if (this.running) { + return; + } + this.running = true; + this.callbacks.forEach((callback) => { + callback(); + }); + this.running = false; + this.currentFrame++; + } + + /** + * 注册回调函数 + * @param callback + */ + + public register(callback: () => void) { + this.callbacks.push(callback); + } + + /** + * 注销回调函数 + * @param callback + */ + + public unregister(callback: () => void) { + const index = this.callbacks.indexOf(callback); + if (index !== -1) { + this.callbacks.splice(index, 1); + } + } + + /** + * 获取当前的动画帧 + */ + public getCurrentFrame() { + return this.currentFrame; + } +} + +describe('fastdom', () => { + it('should measure', () => { + const animationFrameController = new AnimationFrameController(); + + let originalAnimationFrame = global.requestAnimationFrame; + let originalCancelAnimationFrame = global.cancelAnimationFrame; + + global.requestAnimationFrame = (callback) => { + animationFrameController.register(callback); + return 1; + }; + + global.cancelAnimationFrame = () => {}; + let count = 0; + fastdom.measure(() => { + count++; + expect(count).toBe(1); + + fastdom.measure(() => { + // will run on current frame + count++; + expect(count).toBe(3); + }); + + fastdom.measureAtNextFrame(() => { + // will run on next frame + count++; + expect(count).toBe(4); + + fastdom.measure(() => { + count += 2; + expect(count).toBe(6); + }); + fastdom.mutate(() => { + count += 3; + expect(count).toBe(9); + }); + }); + }); + + fastdom.mutate(() => { + count++; + expect(count).toBe(2); + }); + + animationFrameController.run(); + animationFrameController.run(); + expect(count).toBe(9); + animationFrameController.run(); + animationFrameController.run(); + expect(count).toBe(9); + + global.requestAnimationFrame = originalAnimationFrame; + global.cancelAnimationFrame = originalCancelAnimationFrame; + }); +}); diff --git a/packages/core-browser/src/bootstrap/app.view.tsx b/packages/core-browser/src/bootstrap/app.view.tsx index fb5754b019..81ec81598a 100644 --- a/packages/core-browser/src/bootstrap/app.view.tsx +++ b/packages/core-browser/src/bootstrap/app.view.tsx @@ -38,6 +38,7 @@ export function App(props: AppProps) { lastFrame = null; allSlot.forEach((item) => { eventBus.fire(new ResizeEvent({ slotLocation: item.slot })); + eventBus.fireDirective(ResizeEvent.createDirective(item.slot)); }); }); }; diff --git a/packages/core-browser/src/components/layout/split-panel.tsx b/packages/core-browser/src/components/layout/split-panel.tsx index ac1f862deb..367e63f88d 100644 --- a/packages/core-browser/src/components/layout/split-panel.tsx +++ b/packages/core-browser/src/components/layout/split-panel.tsx @@ -224,6 +224,7 @@ export const SplitPanel: React.FC = (props) => { (location?: string) => { if (location) { eventBus.fire(new ResizeEvent({ slotLocation: location })); + eventBus.fireDirective(ResizeEvent.createDirective(location)); } }, [eventBus], @@ -332,12 +333,10 @@ export const SplitPanel: React.FC = (props) => { if (rootRef.current) { splitPanelService.setRootNode(rootRef.current); } - const disposer = eventBus.on(ResizeEvent, (e) => { - if (e.payload.slotLocation === id) { - childList.forEach((c) => { - fireResizeEvent(getProp(c, 'slot') || getProp(c, 'id')); - }); - } + const disposer = eventBus.onDirective(ResizeEvent.createDirective(id), () => { + childList.forEach((c) => { + fireResizeEvent(getProp(c, 'slot') || getProp(c, 'id')); + }); }); return () => { disposer.dispose(); @@ -345,7 +344,8 @@ export const SplitPanel: React.FC = (props) => { }, []); const renderSplitPanel = React.useMemo(() => { - const { minResize, flexGrow, minSize, maxSize, savedSize, defaultSize, flex, noResize, slot, ...rest } = props; + const { minResize, flexGrow, minSize, maxSize, savedSize, defaultSize, flex, noResize, slot, headerSize, ...rest } = + props; delete rest['resizeHandleClassName']; delete rest['dynamicTarget']; @@ -364,6 +364,7 @@ export const SplitPanel: React.FC = (props) => { data-max-size={maxSize} data-saved-size={savedSize} data-default-size={defaultSize} + data-header-size={headerSize} data-flex={flex} data-flex-grow={flexGrow} data-no-resize={noResize} diff --git a/packages/core-browser/src/components/resize/resize.tsx b/packages/core-browser/src/components/resize/resize.tsx index 28d97f4be7..0a7ac7f74a 100644 --- a/packages/core-browser/src/components/resize/resize.tsx +++ b/packages/core-browser/src/components/resize/resize.tsx @@ -1,6 +1,10 @@ import cls from 'classnames'; import React from 'react'; +import { IDisposable } from '@opensumi/ide-core-common'; + +import { fastdom } from '../../dom'; + import styles from './resize.module.less'; export const RESIZE_LOCK = 'resize-lock'; @@ -45,6 +49,8 @@ export interface IResizeHandleDelegate { export function preventWebviewCatchMouseEvents() { const iframes = document.getElementsByTagName('iframe'); const webviews = document.getElementsByTagName('webview'); + const shadowRootHost = document.getElementsByClassName('shadow-root-host'); + for (const webview of webviews as unknown as HTMLElement[]) { webview.classList.add('none-pointer-event'); } @@ -52,7 +58,6 @@ export function preventWebviewCatchMouseEvents() { iframe.classList.add('none-pointer-event'); } - const shadowRootHost = document.getElementsByClassName('shadow-root-host'); for (const host of shadowRootHost as unknown as HTMLElement[]) { host?.classList.add('none-pointer-event'); } @@ -61,6 +66,8 @@ export function preventWebviewCatchMouseEvents() { export function allowWebviewCatchMouseEvents() { const iframes = document.getElementsByTagName('iframe'); const webviews = document.getElementsByTagName('webview'); + const shadowRootHost = document.getElementsByClassName('shadow-root-host'); + for (const webview of webviews as unknown as HTMLElement[]) { webview.classList.remove('none-pointer-event'); } @@ -68,7 +75,6 @@ export function allowWebviewCatchMouseEvents() { iframe.classList.remove('none-pointer-event'); } - const shadowRootHost = document.getElementsByClassName('shadow-root-host'); for (const host of shadowRootHost as unknown as HTMLElement[]) { host?.classList.remove('none-pointer-event'); } @@ -82,16 +88,18 @@ export const ResizeHandleHorizontal = (props: ResizeHandleProps) => { const startNextWidth = React.useRef(0); const prevElement = React.useRef(); const nextElement = React.useRef(); - const requestFrame = React.useRef(); + const requestFrameToDispose = React.useRef(); const setSize = (prev: number, next: number) => { - const parentWidth = ref.current!.parentElement!.offsetWidth; const prevEle = props.findPrevElement ? props.findPrevElement() : prevElement.current!; const nextEle = props.findNextElement ? props.findNextElement() : nextElement.current!; if ((prevEle && prevEle.classList.contains(RESIZE_LOCK)) || (nextEle && nextEle.classList.contains(RESIZE_LOCK))) { return; } + + const parentWidth = ref.current!.parentElement!.offsetWidth; + const prevMinResize = Number(prevEle?.dataset.minResize || 0); const nextMinResize = Number(nextEle?.dataset.minResize || 0); @@ -107,6 +115,7 @@ export const ResizeHandleHorizontal = (props: ResizeHandleProps) => { return; } } + if (nextEle) { nextEle.style.width = next * 100 + '%'; } @@ -245,32 +254,39 @@ export const ResizeHandleHorizontal = (props: ResizeHandleProps) => { }; const setAbsoluteSize = (size: number, isLatter?: boolean) => { - const currentPrev = prevElement.current!.clientWidth; - const currentNext = nextElement.current!.clientWidth; - const totalSize = currentPrev + currentNext; - if (props.flexMode) { - const prevWidth = props.flexMode === ResizeFlexMode.Prev ? size : totalSize - size; - const nextWidth = props.flexMode === ResizeFlexMode.Next ? size : totalSize - size; - flexModeSetSize(prevWidth, nextWidth, true); - } else { - const currentTotalWidth = - +nextElement.current!.style.width!.replace('%', '') + +prevElement.current!.style.width!.replace('%', ''); - if (isLatter) { - nextElement.current!.style.width = currentTotalWidth * (size / totalSize) + '%'; - prevElement.current!.style.width = currentTotalWidth * (1 - size / totalSize) + '%'; - } else { - prevElement.current!.style.width = currentTotalWidth * (size / totalSize) + '%'; - nextElement.current!.style.width = currentTotalWidth * (1 - size / totalSize) + '%'; - } - } - if (isLatter) { - handleZeroSize(totalSize - size, size); - } else { - handleZeroSize(size, totalSize - size); - } - if (props.onResize) { - props.onResize(prevElement.current!, nextElement.current!); - } + fastdom.measure(() => { + const currentPrev = prevElement.current!.clientWidth; + const currentNext = nextElement.current!.clientWidth; + + const nextTotolWidth = +nextElement.current!.style.width!.replace('%', ''); + const prevTotalWidth = +prevElement.current!.style.width!.replace('%', ''); + fastdom.mutate(() => { + const totalSize = currentPrev + currentNext; + if (props.flexMode) { + const prevWidth = props.flexMode === ResizeFlexMode.Prev ? size : totalSize - size; + const nextWidth = props.flexMode === ResizeFlexMode.Next ? size : totalSize - size; + flexModeSetSize(prevWidth, nextWidth, true); + } else { + const currentTotalWidth = nextTotolWidth + prevTotalWidth; + + if (isLatter) { + nextElement.current!.style.width = currentTotalWidth * (size / totalSize) + '%'; + prevElement.current!.style.width = currentTotalWidth * (1 - size / totalSize) + '%'; + } else { + prevElement.current!.style.width = currentTotalWidth * (size / totalSize) + '%'; + nextElement.current!.style.width = currentTotalWidth * (1 - size / totalSize) + '%'; + } + } + if (isLatter) { + handleZeroSize(totalSize - size, size); + } else { + handleZeroSize(size, totalSize - size); + } + if (props.onResize) { + props.onResize(prevElement.current!, nextElement.current!); + } + }); + }); }; const getAbsoluteSize = (isLatter?: boolean) => { @@ -303,43 +319,57 @@ export const ResizeHandleHorizontal = (props: ResizeHandleProps) => { } const prevWidth = startPrevWidth.current + e.pageX - startX.current; const nextWidth = startNextWidth.current - (e.pageX - startX.current); - if (requestFrame.current) { - window.cancelAnimationFrame(requestFrame.current); + if (requestFrameToDispose.current) { + requestFrameToDispose.current.dispose(); } - const parentWidth = ref.current!.parentElement!.offsetWidth; - requestFrame.current = window.requestAnimationFrame(() => { + + requestFrameToDispose.current = fastdom.mutate(() => { if (props.flexMode) { flexModeSetSize(prevWidth, nextWidth); } else { + const parentWidth = ref.current!.parentElement!.offsetWidth; setSize(prevWidth / parentWidth, nextWidth / parentWidth); } }); }; - const onMouseUp = (e) => { + const onMouseUp = () => { resizing.current = false; - ref.current?.classList.remove(styles.active); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); - // 结束拖拽时恢复拖拽区域滚动条 - restoreScrollBar(prevElement.current!); - restoreScrollBar(nextElement.current!); + if (props.onFinished) { props.onFinished(); } - allowWebviewCatchMouseEvents(); + + fastdom.mutate(() => { + ref.current?.classList.remove(styles.active); + + // 结束拖拽时恢复拖拽区域滚动条 + restoreScrollBar(prevElement.current!); + restoreScrollBar(nextElement.current!); + + allowWebviewCatchMouseEvents(); + }); }; const onMouseDown = (e) => { resizing.current = true; - ref.current?.classList.add(styles.active); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); startX.current = e.pageX; - startPrevWidth.current = prevElement.current!.offsetWidth; - startNextWidth.current = nextElement.current!.offsetWidth; - // 开始拖拽时隐藏拖拽区域滚动条 - hideScrollBar(prevElement.current!); - hideScrollBar(nextElement.current!); - preventWebviewCatchMouseEvents(); + + fastdom.measure(() => { + startPrevWidth.current = prevElement.current!.offsetWidth; + startNextWidth.current = nextElement.current!.offsetWidth; + + fastdom.mutate(() => { + ref.current?.classList.add(styles.active); + + // 开始拖拽时隐藏拖拽区域滚动条 + hideScrollBar(prevElement.current!); + hideScrollBar(nextElement.current!); + preventWebviewCatchMouseEvents(); + }); + }); }; React.useEffect(() => { @@ -395,7 +425,6 @@ export const ResizeHandleVertical = (props: ResizeHandleProps) => { const cachedPrevElement = React.useRef(); const cachedNextElement = React.useRef(); - const requestFrame = React.useRef(); // direction: true 为向下,false 为向上 const setSize = (prev: number, next: number, direction?: boolean) => { const prevEle = props.findPrevElement ? props.findPrevElement(direction) : prevElement.current!; @@ -465,7 +494,6 @@ export const ResizeHandleVertical = (props: ResizeHandleProps) => { let currentTotalHeight; if (props.flexMode) { currentTotalHeight = ((prevEle.offsetHeight + nextEle.offsetHeight) / prevEle.parentElement!.offsetHeight) * 100; - // flexModeSetSize(prev / (prev + next) * totalHeight, next / (prev + next) * totalHeight, true); } else { currentTotalHeight = +nextEle.style.height!.replace('%', '') + +prevEle.style.height!.replace('%', ''); } @@ -584,15 +612,21 @@ export const ResizeHandleVertical = (props: ResizeHandleProps) => { const onMouseDown = (e) => { resizing.current = true; - ref.current?.classList.add(styles.active); document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); startY.current = e.pageY; cachedNextElement.current = nextElement.current; cachedPrevElement.current = prevElement.current; - startPrevHeight.current = prevElement.current!.offsetHeight; - startNextHeight.current = nextElement.current!.offsetHeight; - preventWebviewCatchMouseEvents(); + + fastdom.measure(() => { + startPrevHeight.current = prevElement.current!.offsetHeight; + startNextHeight.current = nextElement.current!.offsetHeight; + + fastdom.mutate(() => { + ref.current?.classList.add(styles.active); + preventWebviewCatchMouseEvents(); + }); + }); }; const onMouseMove = (e: MouseEvent) => { @@ -600,32 +634,30 @@ export const ResizeHandleVertical = (props: ResizeHandleProps) => { if (ref.current && ref.current.classList.contains('no-resize')) { return; } - const direction = e.pageY > startY.current; - // 若上层未传入findNextElement,dynamicNext为null,否则找不到符合要求的panel时返回undefined - const dynamicNext = props.findNextElement ? props.findNextElement(direction) : null; - const dynamicPrev = props.findPrevElement ? props.findPrevElement(direction) : null; - // 作用元素变化重新初始化当前位置,传入findNextElement时默认已传入findPrevElement - if ( - (dynamicNext !== null && cachedNextElement.current !== dynamicNext) || - (dynamicPrev !== null && cachedPrevElement.current !== dynamicPrev) - ) { - if (!dynamicNext || !dynamicPrev) { - return; + + fastdom.measure(() => { + const direction = e.pageY > startY.current; + // 若上层未传入findNextElement,dynamicNext为null,否则找不到符合要求的panel时返回undefined + const dynamicNext = props.findNextElement ? props.findNextElement(direction) : null; + const dynamicPrev = props.findPrevElement ? props.findPrevElement(direction) : null; + // 作用元素变化重新初始化当前位置,传入findNextElement时默认已传入findPrevElement + if ( + (dynamicNext !== null && cachedNextElement.current !== dynamicNext) || + (dynamicPrev !== null && cachedPrevElement.current !== dynamicPrev) + ) { + if (!dynamicNext || !dynamicPrev) { + return; + } + cachedNextElement.current = dynamicNext!; + cachedPrevElement.current = dynamicPrev!; + startY.current = e.pageY; + startPrevHeight.current = cachedPrevElement.current!.offsetHeight; + startNextHeight.current = cachedNextElement.current!.offsetHeight; } - cachedNextElement.current = dynamicNext!; - cachedPrevElement.current = dynamicPrev!; - startY.current = e.pageY; - startPrevHeight.current = cachedPrevElement.current!.offsetHeight; - startNextHeight.current = cachedNextElement.current!.offsetHeight; - } - const prevHeight = startPrevHeight.current + e.pageY - startY.current; - const nextHeight = startNextHeight.current - (e.pageY - startY.current); - if (requestFrame.current) { - window.cancelAnimationFrame(requestFrame.current); - } + const prevHeight = startPrevHeight.current + e.pageY - startY.current; + const nextHeight = startNextHeight.current - (e.pageY - startY.current); - requestFrame.current = window.requestAnimationFrame(() => { const prevMinResize = Number(cachedPrevElement.current!.dataset.minResize || 0); const nextMinResize = Number(cachedNextElement.current!.dataset.minResize || 0); if (prevMinResize || nextMinResize) { @@ -633,26 +665,32 @@ export const ResizeHandleVertical = (props: ResizeHandleProps) => { return; } } - if (props.flexMode === ResizeFlexMode.Prev || props.flexMode === ResizeFlexMode.Next) { - flexModeSetSize(prevHeight, nextHeight); - } else if (props.flexMode === ResizeFlexMode.Percentage) { - const parentHeight = ref.current!.parentElement!.offsetHeight; - setSize(prevHeight / parentHeight, nextHeight / parentHeight); - } else { - setDomSize(prevHeight, nextHeight, cachedPrevElement.current!, cachedNextElement.current!); - } + + fastdom.mutate(() => { + if (props.flexMode === ResizeFlexMode.Prev || props.flexMode === ResizeFlexMode.Next) { + flexModeSetSize(prevHeight, nextHeight); + } else if (props.flexMode === ResizeFlexMode.Percentage) { + const parentHeight = ref.current!.parentElement!.offsetHeight; + setSize(prevHeight / parentHeight, nextHeight / parentHeight); + } else { + setDomSize(prevHeight, nextHeight, cachedPrevElement.current!, cachedNextElement.current!); + } + }); }); }; const onMouseUp = (e) => { resizing.current = false; - ref.current?.classList.remove(styles.active); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); - if (props.onFinished) { - props.onFinished(); - } - allowWebviewCatchMouseEvents(); + + fastdom.mutate(() => { + ref.current?.classList.remove(styles.active); + if (props.onFinished) { + props.onFinished(); + } + allowWebviewCatchMouseEvents(); + }); }; React.useEffect(() => { diff --git a/packages/core-browser/src/dom/fastdom.ts b/packages/core-browser/src/dom/fastdom.ts new file mode 100644 index 0000000000..3c17ecdbb1 --- /dev/null +++ b/packages/core-browser/src/dom/fastdom.ts @@ -0,0 +1,110 @@ +import { Heap, IDisposable, onUnexpectedError } from '@opensumi/ide-utils'; + +class DomOperation { + private _disposed = false; + + constructor(protected _runner: () => void, public priority: number) {} + + run() { + if (this._disposed) { + return; + } + + try { + this._runner(); + } catch (error) { + onUnexpectedError(error); + } + } + + dispose() { + this._disposed = true; + } +} + +const aQueue = new Heap({ + comparator: (a, b) => b.priority - a.priority, +}); + +const bQueue = new Heap({ + comparator: (a, b) => b.priority - a.priority, +}); + +let runningQueue = aQueue; +let nextQueue = bQueue; + +function swapQueue() { + if (runningQueue === aQueue) { + runningQueue = bQueue; + nextQueue = aQueue; + } else { + runningQueue = aQueue; + nextQueue = bQueue; + } +} + +let currentFlushHandle: number | undefined; +let inAnimationFrame = false; + +function flush() { + currentFlushHandle = undefined; + + inAnimationFrame = true; + + swapQueue(); + + while (runningQueue.size > 0) { + const op = runningQueue.pop()!; + op.run(); + } + + inAnimationFrame = false; +} + +function schedule() { + if (currentFlushHandle) { + return; + } + + currentFlushHandle = requestAnimationFrame(flush); +} + +/** + * 如果当前在动画帧中,将操作加入当前队列,否则加入下一帧队列 + */ +function runAtThisOrScheduleAtNext(op: DomOperation) { + if (inAnimationFrame) { + runningQueue.add(op); + } else { + nextQueue.add(op); + } +} + +function measure(fn: () => void): IDisposable { + const op = new DomOperation(fn, 10000); + runAtThisOrScheduleAtNext(op); + schedule(); + return op; +} + +function mutate(fn: () => void): IDisposable { + const op = new DomOperation(fn, -10000); + runAtThisOrScheduleAtNext(op); + schedule(); + return op; +} + +function measureAtNextFrame(fn: () => void): IDisposable { + const op = new DomOperation(fn, 10000); + nextQueue.add(op); + schedule(); + return op; +} + +const fastdom = { + measure, + measureAtNextFrame, + mutate, +}; + +export default fastdom; diff --git a/packages/core-browser/src/dom/index.ts b/packages/core-browser/src/dom/index.ts index 7e8bb64b1a..e69bdd4499 100644 --- a/packages/core-browser/src/dom/index.ts +++ b/packages/core-browser/src/dom/index.ts @@ -1,6 +1,8 @@ import { Event as BaseEvent, Disposable, Emitter, IDisposable, isWebKit } from '@opensumi/ide-core-common'; import { space } from '@opensumi/ide-utils/lib/strings'; +import fastdom from './fastdom'; + export * from './event'; export const EventType = { @@ -177,6 +179,7 @@ export function trackFocus(element: HTMLElement | Window): IFocusTracker { return new FocusTracker(element); } +export { fastdom }; /** * https://developer.mozilla.org/zh-CN/docs/Web/API/MouseEvent/button */ diff --git a/packages/core-browser/src/dom/resize-observer.ts b/packages/core-browser/src/dom/resize-observer.ts new file mode 100644 index 0000000000..e0e1d32cfc --- /dev/null +++ b/packages/core-browser/src/dom/resize-observer.ts @@ -0,0 +1,52 @@ +import { DisposableStore, Emitter, IDisposable } from '@opensumi/ide-core-common'; + +import fastdom from './fastdom'; + +export interface IDimension { + width: number; + height: number; +} + +export class ResizeObserverWrapper implements IDisposable { + private _disposables = new DisposableStore(); + + private _onDidChange = this._disposables.add(new Emitter()); + public onDidChange = this._onDidChange.event; + + private _resizeObserver: ResizeObserver; + + constructor(private _container: HTMLElement) { + this._resizeObserver = new ResizeObserver(this._callback); + } + + private _callback = (entries: ResizeObserverEntry[]) => { + if (entries[0] && entries[0].contentRect) { + const width = entries[0].contentRect.width; + const height = entries[0].contentRect.height; + + this._onDidChange.fire({ width, height }); + } + }; + + private _observed = false; + observe() { + if (this._observed) { + return; + } + this._observed = true; + + this._resizeObserver.observe(this._container); + fastdom.measure(() => { + this._onDidChange.fire({ + width: this._container.clientWidth, + height: this._container.clientHeight, + }); + }); + } + + dispose() { + this._observed = false; + this._disposables.dispose(); + this._resizeObserver.disconnect(); + } +} diff --git a/packages/core-browser/src/layout/layout-hooks.ts b/packages/core-browser/src/layout/layout-hooks.ts index 5783da6a65..094c08594f 100644 --- a/packages/core-browser/src/layout/layout-hooks.ts +++ b/packages/core-browser/src/layout/layout-hooks.ts @@ -1,7 +1,9 @@ +import throttle from 'lodash/throttle'; import React from 'react'; import { IEventBus } from '@opensumi/ide-core-common'; +import { fastdom } from '../dom'; import { useInjectable } from '../react-hooks'; import { ResizeEvent } from './layout.interface'; @@ -20,16 +22,35 @@ export const useViewState = ( const [viewState, setViewState] = React.useState({ width: 0, height: 0 }); const viewStateRef = React.useRef(viewState); + const updateViewState = throttle( + (height: number, width: number) => { + // 当视图被隐藏 (display: none) 时不更新 viewState + // 避免视图切换时触发无效的渲染 + // 真正的 resize 操作不会出现 width/height 为 0 的情况 + if ( + (width !== viewStateRef.current.width || height !== viewStateRef.current.height) && + (width !== 0 || height !== 0) + ) { + setViewState({ width, height }); + viewStateRef.current = { width, height }; + } + }, + 16 * 3, + { + leading: true, + trailing: true, + }, + ); + React.useEffect(() => { - let lastFrame: number | null; - const disposer = eventBus.on(ResizeEvent, (e) => { - if (!manualObserve && e.payload.slotLocation === location) { - if (lastFrame) { - window.cancelAnimationFrame(lastFrame); - } - lastFrame = window.requestAnimationFrame(() => { - if (containerRef.current && containerRef.current.clientHeight && containerRef.current.clientWidth) { - setViewState({ height: containerRef.current.clientHeight, width: containerRef.current.clientWidth }); + const disposer = eventBus.onDirective(ResizeEvent.createDirective(location), () => { + if (!manualObserve) { + fastdom.measureAtNextFrame(() => { + if (containerRef.current) { + const height = containerRef.current.clientHeight; + const width = containerRef.current.clientWidth; + + updateViewState(height, width); } }); } @@ -37,27 +58,17 @@ export const useViewState = ( return () => { disposer.dispose(); }; - }, [containerRef.current]); + }, []); React.useEffect(() => { + const ResizeObserver = window.ResizeObserver; // TODO: 统一收敛到 resizeEvent 内 if (manualObserve && containerRef.current) { - const ResizeObserver = (window as any).ResizeObserver; - const doUpdate = (entries) => { + const resizeObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => { const width = entries[0].contentRect.width; const height = entries[0].contentRect.height; - // 当视图被隐藏 (display: none) 时不更新 viewState - // 避免视图切换时触发无效的渲染 - // 真正的 resize 操作不会出现 width/height 为 0 的情况 - if ( - (width !== viewStateRef.current.width || height !== viewStateRef.current.height) && - (width !== 0 || height !== 0) - ) { - setViewState({ width, height }); - viewStateRef.current = { width, height }; - } - }; - const resizeObserver = new ResizeObserver(doUpdate); + updateViewState(height, width); + }); resizeObserver.observe(containerRef.current); return () => { if (containerRef.current) { diff --git a/packages/core-browser/src/layout/layout.interface.ts b/packages/core-browser/src/layout/layout.interface.ts index 35c3b50791..6485d3ade2 100644 --- a/packages/core-browser/src/layout/layout.interface.ts +++ b/packages/core-browser/src/layout/layout.interface.ts @@ -104,6 +104,10 @@ export class ResizePayload { */ constructor(public slotLocation: SlotLocation) {} } -export class ResizeEvent extends BasicEvent {} +export class ResizeEvent extends BasicEvent { + static createDirective(location: SlotLocation) { + return `resize:${location}`; + } +} export class RenderedEvent extends BasicEvent {} diff --git a/packages/core-browser/src/layout/render.tsx b/packages/core-browser/src/layout/render.tsx index e5c232a23d..e113d2f6cf 100644 --- a/packages/core-browser/src/layout/render.tsx +++ b/packages/core-browser/src/layout/render.tsx @@ -6,9 +6,9 @@ import { ErrorBoundary } from '../react-providers'; import { View } from './layout.interface'; export const renderView = (view?: View) => ( - <> - {view && ReactIs.isValidElementType(view.component) ? ( - {view.component && React.createElement(view.component, view.initialProps)} - ) : null} - - ); + <> + {view && ReactIs.isValidElementType(view.component) ? ( + {view.component && React.createElement(view.component, view.initialProps)} + ) : null} + +); diff --git a/packages/core-browser/src/react-providers/slot.tsx b/packages/core-browser/src/react-providers/slot.tsx index f0bf14b257..88633e4855 100644 --- a/packages/core-browser/src/react-providers/slot.tsx +++ b/packages/core-browser/src/react-providers/slot.tsx @@ -35,6 +35,9 @@ export const SlotLocation = { }; export function getSlotLocation(moduleName: string, layoutConfig: LayoutConfig) { + if (!layoutConfig) { + return ''; + } for (const location of Object.keys(layoutConfig)) { if (layoutConfig[location].modules && layoutConfig[location].modules.indexOf(moduleName) > -1) { return location; diff --git a/packages/core-common/src/event-bus/event-bus-types.ts b/packages/core-common/src/event-bus/event-bus-types.ts index c4d1b28b8a..bbe9017cde 100644 --- a/packages/core-common/src/event-bus/event-bus-types.ts +++ b/packages/core-common/src/event-bus/event-bus-types.ts @@ -65,4 +65,7 @@ export interface IEventBus { * 监听 EventBus 中的事件,只会触发一次 */ once(constructor: ConstructorOf, listener: IEventListener): IDisposable; + + onDirective(directive: string, listener: IEventListener): IDisposable; + fireDirective(directive: string, payload?: any): void; } diff --git a/packages/core-common/src/event-bus/event-bus.ts b/packages/core-common/src/event-bus/event-bus.ts index faf2b77dce..4c1e40db95 100644 --- a/packages/core-common/src/event-bus/event-bus.ts +++ b/packages/core-common/src/event-bus/event-bus.ts @@ -1,5 +1,5 @@ import { Injectable } from '@opensumi/di'; -import { Emitter, Event, IAsyncResult } from '@opensumi/ide-utils'; +import { Emitter, Event, IAsyncResult, IDisposable } from '@opensumi/ide-utils'; import { ConstructorOf } from '../declare'; @@ -54,4 +54,16 @@ export class EventBusImpl implements IEventBus { this.emitterMap.set(key, emitter); return emitter; } + + onDirective(directive: string, listener: IEventListener): IDisposable { + const emitter = this.getOrCreateEmitter(directive); + return emitter.event(listener); + } + + fireDirective(directive: string, payload: any) { + const emitter = this.emitterMap.get(directive); + if (emitter) { + emitter.fire(payload); + } + } } diff --git a/packages/editor/src/browser/doc-model/types.ts b/packages/editor/src/browser/doc-model/types.ts index 57f04cda9c..ea559cf347 100644 --- a/packages/editor/src/browser/doc-model/types.ts +++ b/packages/editor/src/browser/doc-model/types.ts @@ -8,16 +8,12 @@ import { MaybePromise, URI, } from '@opensumi/ide-core-browser'; -import * as monaco from '@opensumi/ide-monaco'; import { EOL, EndOfLineSequence } from '@opensumi/ide-monaco/lib/browser/monaco-api/types'; import { IEditorDocumentModelContentChange, SaveReason } from '../../common'; import { IEditorDocumentDescription, IEditorDocumentModel, IEditorDocumentModelRef } from '../../common/editor'; -export interface IDocModelUpdateOptions extends monaco.editor.ITextModelUpdateOptions { - detectIndentation?: boolean; -} - +export { IDocModelUpdateOptions } from '../../common/types'; export interface IEditorDocumentModelContentProvider { /** * 是否处理这个Scheme的uri diff --git a/packages/editor/src/browser/editor-collection.service.ts b/packages/editor/src/browser/editor-collection.service.ts index 83aca61f99..5c48fab7fb 100644 --- a/packages/editor/src/browser/editor-collection.service.ts +++ b/packages/editor/src/browser/editor-collection.service.ts @@ -430,6 +430,7 @@ export abstract class BaseMonacoEditorWrapper extends WithEventBus implements IE } } +@Injectable({ multiple: true }) export class BrowserCodeEditor extends BaseMonacoEditorWrapper implements ICodeEditor { @Autowired(EditorCollectionService) private collectionService: EditorCollectionServiceImpl; @@ -439,15 +440,11 @@ export class BrowserCodeEditor extends BaseMonacoEditorWrapper implements ICodeE private editorState: Map = new Map(); - private readonly toDispose: monaco.IDisposable[] = []; - protected _currentDocumentModelRef: IEditorDocumentModelRef; private _onCursorPositionChanged = new EventEmitter(); public onCursorPositionChanged = this._onCursorPositionChanged.event; - public _disposed = false; - private _onRefOpen = new Emitter(); public onRefOpen = this._onRefOpen.event; @@ -466,6 +463,7 @@ export class BrowserCodeEditor extends BaseMonacoEditorWrapper implements ICodeE constructor(public readonly monacoEditor: IMonacoCodeEditor, options: any = {}) { super(monacoEditor, EditorType.CODE); + this._specialEditorOptions = options; this.collectionService.addEditors([this]); // 防止浏览器后退前进手势 @@ -473,7 +471,7 @@ export class BrowserCodeEditor extends BaseMonacoEditorWrapper implements ICodeE bindPreventNavigation(this.monacoEditor.getDomNode()!); disposer.dispose(); }); - this.toDispose.push( + this.addDispose( monacoEditor.onDidChangeCursorPosition(() => { if (!this.currentDocumentModel) { return; @@ -485,15 +483,12 @@ export class BrowserCodeEditor extends BaseMonacoEditorWrapper implements ICodeE }); }), ); - this.addDispose({ - dispose: () => { - this.monacoEditor.dispose(); - }, - }); + + this.addDispose(this.monacoEditor); } - layout(): void { - this.monacoEditor.layout(); + layout(dimension?: monaco.IDimension, postponeRendering: boolean = false): void { + this.monacoEditor.layout(dimension, postponeRendering); } focus(): void { @@ -504,8 +499,6 @@ export class BrowserCodeEditor extends BaseMonacoEditorWrapper implements ICodeE super.dispose(); this.saveCurrentState(); this.collectionService.removeEditors([this]); - this._disposed = true; - this.toDispose.forEach((disposable) => disposable.dispose()); } protected saveCurrentState() { diff --git a/packages/editor/src/browser/grid/grid.service.ts b/packages/editor/src/browser/grid/grid.service.ts index 44e94b8b54..70bfc4352f 100644 --- a/packages/editor/src/browser/grid/grid.service.ts +++ b/packages/editor/src/browser/grid/grid.service.ts @@ -130,6 +130,7 @@ export class EditorGrid implements IDisposable { public emitResizeWithEventBus(eventBus: IEventBus) { eventBus.fire(new GridResizeEvent({ gridId: this.uid })); + eventBus.fireDirective(GridResizeEvent.createDirective(this.uid)); this.children.forEach((c) => { c.emitResizeWithEventBus(eventBus); }); diff --git a/packages/editor/src/browser/navigation.view.tsx b/packages/editor/src/browser/navigation.view.tsx index 61a345b33d..2ba84c5a11 100644 --- a/packages/editor/src/browser/navigation.view.tsx +++ b/packages/editor/src/browser/navigation.view.tsx @@ -8,6 +8,7 @@ import { Icon, Scrollbars } from '@opensumi/ide-components'; import { Disposable, DomListener, + fastdom, getIcon, useDesignStyles, useInjectable, @@ -122,16 +123,18 @@ export const NavigationMenu = observer(({ model }: { model: NavigationMenuModel const viewService = useInjectable(NavigationBarViewService) as NavigationBarViewService; const scrollToCurrent = useCallback(() => { - if (scrollerContainer.current) { + fastdom.measure(() => { try { - const current = scrollerContainer.current.querySelector(`.${styles.navigation_menu_item_current}`); - if (current) { - current.scrollIntoView({ behavior: 'auto', block: 'center' }); + if (scrollerContainer.current) { + const current = scrollerContainer.current.querySelector(`.${styles.navigation_menu_item_current}`); + if (current) { + current.scrollIntoView({ behavior: 'auto', block: 'center' }); + } } } catch (e) { // noop } - } + }); }, [scrollerContainer.current]); return ( diff --git a/packages/editor/src/browser/preference/converter.ts b/packages/editor/src/browser/preference/converter.ts index 68f12dbf03..ec20ac267d 100644 --- a/packages/editor/src/browser/preference/converter.ts +++ b/packages/editor/src/browser/preference/converter.ts @@ -1,114 +1,10 @@ import { Uri, objects } from '@opensumi/ide-core-browser'; -import * as monaco from '@opensumi/ide-monaco'; import { IConfigurationService } from '@opensumi/monaco-editor-core/esm/vs/platform/configuration/common/configuration'; import { IConvertedMonacoOptions } from '../types'; const { removeUndefined } = objects; -/** - * 计算由ConfigurationService设置值带来的monaco编辑器的属性 - * @param configurationService IConfigurationService - * @param updatingKey 需要处理的Preference key。如果没有这个值,默认处理全部。 - */ -export function getConvertedMonacoOptions( - configurationService: IConfigurationService, - resourceUri?: string, - language?: string, - updatingKey?: string[], -): IConvertedMonacoOptions { - const editorOptions: Partial = {}; - const diffOptions: Partial = {}; - const modelOptions: Partial = {}; - const editorOptionsKeys = [] as string[]; - const textModelUpdateOptionsKeys = [] as string[]; - const diffEditorOptionsKeys = [] as string[]; - - if (updatingKey) { - updatingKey.forEach((key) => { - if (editorOptionsConverters.has(key)) { - editorOptionsKeys.push(key); - } else if (textModelUpdateOptionsConverters.has(key)) { - textModelUpdateOptionsKeys.push(key); - } else if (diffEditorOptionsConverters.has(key)) { - diffEditorOptionsKeys.push(key); - } - }); - } else { - editorOptionsKeys.push(...editorOptionsConverters.keys()); - textModelUpdateOptionsKeys.push(...textModelUpdateOptionsConverters.keys()); - diffEditorOptionsKeys.push(...diffEditorOptionsConverters.keys()); - } - - editorOptionsKeys.forEach((key) => { - const value = configurationService.getValue(key, { - resource: resourceUri ? Uri.parse(resourceUri) : undefined, - overrideIdentifier: language, - }); - if (value === undefined) { - return; - } - if (!editorOptionsConverters.get(key)) { - editorOptions[key] = value; - } else { - const converter: IMonacoOptionsConverter = editorOptionsConverters.get(key)! as IMonacoOptionsConverter; - if (!editorOptions[converter.monaco]) { - editorOptions[converter.monaco] = converter.convert ? converter.convert(value) : value; - } else { - editorOptions[converter.monaco] = { - ...editorOptions[converter.monaco], - ...(converter.convert ? converter.convert(value) : value), - }; - } - } - }); - - textModelUpdateOptionsKeys.forEach((key) => { - const value = configurationService.getValue(key, { - resource: resourceUri ? Uri.parse(resourceUri) : undefined, - overrideIdentifier: language, - }); - if (value === undefined) { - return; - } - if (!textModelUpdateOptionsConverters.get(key)) { - modelOptions[key] = value; - } else { - const converter: IMonacoOptionsConverter = textModelUpdateOptionsConverters.get(key)! as IMonacoOptionsConverter; - modelOptions[converter.monaco] = converter.convert ? converter.convert(value) : value; - } - }); - - diffEditorOptionsKeys.forEach((key) => { - const value = configurationService.getValue(key, { - resource: resourceUri ? Uri.parse(resourceUri) : undefined, - overrideIdentifier: language, - }); - if (value === undefined) { - return; - } - if (!diffEditorOptionsConverters.get(key)) { - editorOptions[key] = value; - } else { - const converter: IMonacoOptionsConverter = diffEditorOptionsConverters.get(key)! as IMonacoOptionsConverter; - if (diffOptions[converter.monaco]) { - diffOptions[converter.monaco] = { - ...diffOptions[converter.monaco], - ...(converter.convert ? converter.convert(value) : value), - }; - } else { - diffOptions[converter.monaco] = converter.convert ? converter.convert(value) : value; - } - } - }); - - return { - editorOptions: removeUndefined(editorOptions), - modelOptions: removeUndefined(modelOptions), - diffOptions: removeUndefined(diffOptions), - }; -} - type NoConverter = false; type KaitianPreferenceKey = string; type MonacoPreferenceKey = string; @@ -900,3 +796,65 @@ export function isEditorOption(key: string) { export function isDiffEditorOption(key: string): boolean { return isContainOptionKey(key, diffEditorOptionsConverters); } + +const editorOptionsConvertersKey = [...editorOptionsConverters.keys()]; +const textModelUpdateOptionsConvertersKey = [...textModelUpdateOptionsConverters.keys()]; +const diffEditorOptionsConvertersKey = [...diffEditorOptionsConverters.keys()]; + +/** + * 计算由ConfigurationService设置值带来的monaco编辑器的属性 + * @param configurationService IConfigurationService + * @param updatingKey 需要处理的Preference key。如果没有这个值,默认处理全部。 + */ +export function getConvertedMonacoOptions( + configurationService: IConfigurationService, + resourceUri?: string, + language?: string, + updatingKey?: string[], +): IConvertedMonacoOptions { + const resource = resourceUri ? Uri.parse(resourceUri) : undefined; + + const getOptions = (keys: string[], converters: Map): Partial => + keys.reduce((options, key) => { + const value = configurationService.getValue(key, { resource, overrideIdentifier: language }); + if (value !== undefined) { + const converter = converters.get(key); + if (!converter) { + options[key] = value; + return options; + } + + const targetKey = converter ? converter.monaco : key; + const convertedValue = converter?.convert ? converter.convert(value) : value; + + if (!options[targetKey]) { + options[targetKey] = convertedValue; + } else { + Object.assign(options[targetKey], convertedValue); + } + } + return options; + }, {}); + + const editorOptionsKeys = updatingKey + ? updatingKey.filter((key) => editorOptionsConverters.has(key)) + : [...editorOptionsConvertersKey]; + + const textModelUpdateOptionsKeys = updatingKey + ? updatingKey.filter((key) => textModelUpdateOptionsConverters.has(key)) + : [...textModelUpdateOptionsConvertersKey]; + + const diffEditorOptionsKeys = updatingKey + ? updatingKey.filter((key) => diffEditorOptionsConverters.has(key)) + : [...diffEditorOptionsConvertersKey]; + + const editorOptions = removeUndefined(getOptions(editorOptionsKeys, editorOptionsConverters)); + const modelOptions = removeUndefined(getOptions(textModelUpdateOptionsKeys, textModelUpdateOptionsConverters)); + const diffOptions = removeUndefined(getOptions(diffEditorOptionsKeys, diffEditorOptionsConverters)); + + return { + editorOptions, + modelOptions, + diffOptions, + }; +} diff --git a/packages/editor/src/browser/preference/schema.ts b/packages/editor/src/browser/preference/schema.ts index adfbcb368d..eb1d3dbbd5 100644 --- a/packages/editor/src/browser/preference/schema.ts +++ b/packages/editor/src/browser/preference/schema.ts @@ -112,7 +112,7 @@ export const EDITOR_DEFAULTS = { readOnly: false, mouseStyle: 'text', disableLayerHinting: false, - automaticLayout: true, // Modified + automaticLayout: true, wordWrap: 'off', wordWrapColumn: 80, wordWrapMinified: true, diff --git a/packages/editor/src/browser/tab.view.tsx b/packages/editor/src/browser/tab.view.tsx index 3ff8dd7391..e1458fa9a2 100644 --- a/packages/editor/src/browser/tab.view.tsx +++ b/packages/editor/src/browser/tab.view.tsx @@ -26,6 +26,7 @@ import { PreferenceService, ResizeEvent, URI, + fastdom, getExternalIcon, getIcon, getSlotLocation, @@ -184,9 +185,9 @@ export const Tabs = ({ group }: ITabsProps) => { ); const scrollToCurrent = useCallback(() => { - if (tabContainer.current) { - if (group.currentResource) { - try { + fastdom.measure(() => { + try { + if (tabContainer.current && group.currentResource) { const currentTab = tabContainer.current.querySelector( '.' + styles.kt_editor_tab + "[data-uri='" + group.currentResource.uri.toString() + "']", ); @@ -196,24 +197,25 @@ export const Tabs = ({ group }: ITabsProps) => { inline: 'nearest', }); } - } catch (e) { - // noop } + } catch (e) { + // noop } - } + }); }, [group, tabContainer.current]); const updateTabMarginRight = useCallback(() => { if (editorActionUpdateTimer.current) { clearTimeout(editorActionUpdateTimer.current); - editorActionUpdateTimer.current = null; } - const timer = setTimeout(() => { - if (editorActionRef.current?.offsetWidth !== lastMarginRight) { - setLastMarginRight(editorActionRef.current?.offsetWidth); - } + editorActionUpdateTimer.current = setTimeout(() => { + fastdom.measure(() => { + const _marginReight = editorActionRef.current?.offsetWidth; + if (_marginReight !== lastMarginRight) { + setLastMarginRight(_marginReight); + } + }); }, 200); - editorActionUpdateTimer.current = timer; }, [editorActionRef.current, editorActionUpdateTimer.current, lastMarginRight]); useEffect(() => { @@ -231,17 +233,13 @@ export const Tabs = ({ group }: ITabsProps) => { disposer.addDispose(new DomListener(tabContainer.current, 'mousewheel', preventNavigation)); } disposer.addDispose( - eventBus.on(ResizeEvent, (event) => { - if (event.payload.slotLocation === slotLocation) { - scrollToCurrent(); - } + eventBus.onDirective(ResizeEvent.createDirective(slotLocation), () => { + scrollToCurrent(); }), ); disposer.addDispose( - eventBus.on(GridResizeEvent, (event) => { - if (event.payload.gridId === group.grid.uid) { - scrollToCurrent(); - } + eventBus.onDirective(GridResizeEvent.createDirective(group.grid.uid), () => { + scrollToCurrent(); }), ); return () => { @@ -251,27 +249,29 @@ export const Tabs = ({ group }: ITabsProps) => { }, [wrapMode]); const layoutLastInRow = useCallback(() => { - if (contentRef.current && wrapMode) { - const newMap: Map = new Map(); - - let currentTabY: number | undefined; - let lastTab: HTMLDivElement | undefined; - const tabs = Array.from(contentRef.current.children); - // 最后一个元素是editorAction - tabs.pop(); - tabs.forEach((child: HTMLDivElement) => { - if (child.offsetTop !== currentTabY) { - currentTabY = child.offsetTop; - if (lastTab) { - newMap.set(tabs.indexOf(lastTab), true); + fastdom.measureAtNextFrame(() => { + if (contentRef.current && wrapMode) { + const newMap: Map = new Map(); + + let currentTabY: number | undefined; + let lastTab: HTMLDivElement | undefined; + const tabs = Array.from(contentRef.current.children); + // 最后一个元素是editorAction + tabs.pop(); + tabs.forEach((child: HTMLDivElement) => { + if (child.offsetTop !== currentTabY) { + currentTabY = child.offsetTop; + if (lastTab) { + newMap.set(tabs.indexOf(lastTab), true); + } } - } - lastTab = child; - newMap.set(tabs.indexOf(child), false); - }); - // 最后一个 tab 不做 grow 处理 - setTabMap(newMap); - } + lastTab = child; + newMap.set(tabs.indexOf(child), false); + }); + // 最后一个 tab 不做 grow 处理 + setTabMap(newMap); + } + }); }, [contentRef.current, wrapMode]); useEffect(() => { @@ -282,10 +282,8 @@ export const Tabs = ({ group }: ITabsProps) => { useEffect(() => { const disposable = new DisposableCollection(); disposable.push( - eventBus.on(ResizeEvent, (e) => { - if (e.payload.slotLocation === slotLocation) { - layoutLastInRow(); - } + eventBus.onDirective(ResizeEvent.createDirective(slotLocation), () => { + layoutLastInRow(); }), ); disposable.push( @@ -298,7 +296,7 @@ export const Tabs = ({ group }: ITabsProps) => { // 当前选中的group变化时宽度变化 disposable.push( editorService.onDidCurrentEditorGroupChanged(() => { - window.requestAnimationFrame(updateTabMarginRight); + updateTabMarginRight(); }), ); // editorMenu变化时宽度可能变化 @@ -308,7 +306,7 @@ export const Tabs = ({ group }: ITabsProps) => { () => {}, 200, )(() => { - window.requestAnimationFrame(updateTabMarginRight); + updateTabMarginRight(); }), ); diff --git a/packages/editor/src/browser/types.ts b/packages/editor/src/browser/types.ts index 1254b47b30..2152b4f10e 100644 --- a/packages/editor/src/browser/types.ts +++ b/packages/editor/src/browser/types.ts @@ -192,7 +192,11 @@ export interface IGridResizeEventPayload { gridId: string; } -export class GridResizeEvent extends BasicEvent {} +export class GridResizeEvent extends BasicEvent { + static createDirective(uid: string) { + return `grid-resize-${uid}`; + } +} export class EditorGroupOpenEvent extends BasicEvent<{ group: IEditorGroup; resource: IResource }> {} export class EditorGroupCloseEvent extends BasicEvent<{ group: IEditorGroup; resource: IResource }> {} diff --git a/packages/editor/src/browser/workbench-editor.service.ts b/packages/editor/src/browser/workbench-editor.service.ts index 391017079b..5f3a27d373 100644 --- a/packages/editor/src/browser/workbench-editor.service.ts +++ b/packages/editor/src/browser/workbench-editor.service.ts @@ -13,6 +13,7 @@ import { RecentFilesManager, ResizeEvent, ServiceNames, + fastdom, getSlotLocation, toMarkdown, } from '@opensumi/ide-core-browser'; @@ -794,11 +795,19 @@ export class EditorGroup extends WithEventBus implements IGridEditorGroup { constructor(public readonly name: string, public readonly groupId: number) { super(); - this.eventBus.on(ResizeEvent, (e: ResizeEvent) => { - if (e.payload.slotLocation === getSlotLocation('@opensumi/ide-editor', this.config.layoutConfig)) { - this.doLayoutEditors(); - } - }); + let toDispose: IDisposable | undefined; + this.eventBus.onDirective( + ResizeEvent.createDirective(getSlotLocation('@opensumi/ide-editor', this.config.layoutConfig)), + () => { + if (toDispose) { + toDispose.dispose(); + } + + toDispose = fastdom.mutate(() => { + this._layoutEditorWorker(); + }); + }, + ); this.eventBus.on(GridResizeEvent, (e: GridResizeEvent) => { if (e.payload.gridId === this.grid.uid) { this.doLayoutEditors(); @@ -849,19 +858,20 @@ export class EditorGroup extends WithEventBus implements IGridEditorGroup { } layoutEditors() { - if (this._domNode) { - const currentWidth = this._domNode.offsetWidth; - const currentHeight = this._domNode.offsetHeight; - if (currentWidth !== this._prevDomWidth || currentHeight !== this._prevDomHeight) { - this.doLayoutEditors(); + fastdom.measure(() => { + if (this._domNode) { + const currentWidth = this._domNode.offsetWidth; + const currentHeight = this._domNode.offsetHeight; + if (currentWidth !== this._prevDomWidth || currentHeight !== this._prevDomHeight) { + this.doLayoutEditors(); + } + this._prevDomWidth = currentWidth; + this._prevDomHeight = currentHeight; } - this._prevDomWidth = currentWidth; - this._prevDomHeight = currentHeight; - } + }); } - @debounce(100) - doLayoutEditors() { + private _layoutEditorWorker() { if (this.codeEditor) { if (this.currentOpenType && this.currentOpenType.type === EditorOpenType.code) { this.codeEditor.layout(); @@ -888,6 +898,11 @@ export class EditorGroup extends WithEventBus implements IGridEditorGroup { } } + @debounce(16 * 5) + doLayoutEditors() { + this._layoutEditorWorker(); + } + setContextKeys() { if (!this._resourceContext) { const getLanguageFromModel = (uri: URI) => { @@ -1124,9 +1139,15 @@ export class EditorGroup extends WithEventBus implements IGridEditorGroup { [ServiceNames.CONTEXT_KEY_SERVICE]: this.contextKeyService.contextKeyService, }, ); + setTimeout(() => { this.codeEditor.layout(); }); + this.addDispose( + this.codeEditor.onRefOpen(() => { + this.codeEditor.layout(); + }), + ); this.toDispose.push( this.codeEditor.onCursorPositionChanged((e) => { this._onCurrentEditorCursorChange.fire(e); diff --git a/packages/editor/src/common/editor.ts b/packages/editor/src/common/editor.ts index 3808386991..79cbcb5320 100644 --- a/packages/editor/src/common/editor.ts +++ b/packages/editor/src/common/editor.ts @@ -15,17 +15,18 @@ import { MaybeNull, URI, } from '@opensumi/ide-core-common'; - -// eslint-disable-next-line import/no-restricted-paths -import { IDocModelUpdateOptions } from '../browser/doc-model/types'; +import { IDimension } from '@opensumi/ide-monaco'; import { IResource } from './resource'; +import { IDocModelUpdateOptions } from './types'; -// eslint-disable-next-line import/no-restricted-paths -import type { EOL, ICodeEditor as IMonacoCodeEditor } from '@opensumi/ide-monaco/lib/browser/monaco-api/types'; -// eslint-disable-next-line import/no-restricted-paths -import type { IEditorOptions } from '@opensumi/monaco-editor-core/esm/vs/editor/common/config/editorOptions'; -import type { ITextModel, ITextModelUpdateOptions } from '@opensumi/monaco-editor-core/esm/vs/editor/common/model'; +import type { + EOL, + IEditorOptions, + ICodeEditor as IMonacoCodeEditor, + ITextModel, + ITextModelUpdateOptions, +} from '@opensumi/ide-monaco'; export { ShowLightbulbIconMode } from '@opensumi/ide-monaco'; @@ -175,7 +176,8 @@ export interface IEditorDocumentModel extends IDisposable { updateEncoding(encoding: string): Promise; // setEncoding(encoding: string, preferredEncoding, mode: EncodingMode): Promise; - updateOptions(options: IDocModelUpdateOptions); + + updateOptions(options: IDocModelUpdateOptions): void; } export type IEditorDocumentModelRef = IRef; @@ -205,7 +207,7 @@ export enum EditorType { } /** - * 一个IEditor代表了一个最小的编辑器单元,可以是 CodeEditor 中的一个,也可以是 DiffEditor 中的两个 + * 一个 IEditor 代表了一个最小的编辑器单元,可以是 CodeEditor 中的一个,也可以是 DiffEditor 中的两个 */ export interface IEditor { /** @@ -278,7 +280,7 @@ export interface IUndoStopOptions { } export interface ICodeEditor extends IEditor, IDisposable { - layout(): void; + layout(dimension?: IDimension, postponeRendering?: boolean): void; /** * 打开一个 document diff --git a/packages/editor/src/common/types.ts b/packages/editor/src/common/types.ts new file mode 100644 index 0000000000..99ff615173 --- /dev/null +++ b/packages/editor/src/common/types.ts @@ -0,0 +1,5 @@ +import * as monaco from '@opensumi/ide-monaco'; + +export interface IDocModelUpdateOptions extends monaco.editor.ITextModelUpdateOptions { + detectIndentation?: boolean; +} diff --git a/packages/main-layout/src/browser/accordion/accordion.service.ts b/packages/main-layout/src/browser/accordion/accordion.service.ts index add08232a5..5548467881 100644 --- a/packages/main-layout/src/browser/accordion/accordion.service.ts +++ b/packages/main-layout/src/browser/accordion/accordion.service.ts @@ -17,6 +17,7 @@ import { View, ViewContextKeyRegistry, WithEventBus, + fastdom, isDefined, localize, } from '@opensumi/ide-core-browser'; @@ -376,12 +377,14 @@ export class AccordionService extends WithEventBus { if (e.payload.slotLocation) { if (this.state[e.payload.slotLocation]) { const id = e.payload.slotLocation; - // get dom of viewId - const sectionDom = document.getElementById(id); - if (sectionDom) { - this.state[id].size = sectionDom.clientHeight; - this.storeState(); - } + fastdom.measureAtNextFrame(() => { + // get dom of viewId + const sectionDom = document.getElementById(id); + if (sectionDom) { + this.state[id].size = sectionDom.clientHeight; + this.storeState(); + } + }); } } } diff --git a/packages/main-layout/src/browser/tabbar/renderer.view.tsx b/packages/main-layout/src/browser/tabbar/renderer.view.tsx index ac0a9c7058..0511a107f3 100644 --- a/packages/main-layout/src/browser/tabbar/renderer.view.tsx +++ b/packages/main-layout/src/browser/tabbar/renderer.view.tsx @@ -17,7 +17,7 @@ import { IEventBus, ResizeEvent, SlotLocation, - runWhenIdle, + fastdom, useInjectable, } from '@opensumi/ide-core-browser'; import { EDirection, PanelContext } from '@opensumi/ide-core-browser/lib/components'; @@ -63,26 +63,28 @@ export const TabRendererBase: FC<{ tabbarService.ensureViewReady(); }, [components]); - const handleResize = useCallback(() => { + const refreshFullSize = useCallback(() => { if (rootRef.current) { setFullSize(rootRef.current[Layout.getDomSizeProperty(direction)]); } - }, [fullSize, rootRef.current]); + }, []); useEffect(() => { - if (rootRef.current) { - handleResize(); - let dispose: IDisposable | null; - eventBus.on(ResizeEvent, (e) => { - if (e.payload.slotLocation === side) { - if (dispose) { - dispose.dispose(); - dispose = null; - } - dispose = runWhenIdle(handleResize); - } + fastdom.measure(() => { + refreshFullSize(); + }); + + let toDispose: IDisposable | undefined; + + eventBus.onDirective(ResizeEvent.createDirective(side), () => { + if (toDispose) { + toDispose.dispose(); + } + + toDispose = fastdom.measureAtNextFrame(() => { + refreshFullSize(); }); - } + }); }, []); return ( diff --git a/packages/main-layout/src/browser/tabbar/tabbar.service.ts b/packages/main-layout/src/browser/tabbar/tabbar.service.ts index c666743238..d5cbda208e 100644 --- a/packages/main-layout/src/browser/tabbar/tabbar.service.ts +++ b/packages/main-layout/src/browser/tabbar/tabbar.service.ts @@ -14,12 +14,12 @@ import { IContextKeyService, IScopedContextKeyService, KeybindingRegistry, - OnEvent, ResizeEvent, SlotLocation, ViewContextKeyRegistry, WithEventBus, createFormatLocalizedStr, + fastdom, formatLocalize, getTabbarCtxKey, isDefined, @@ -168,6 +168,10 @@ export class TabbarService extends WithEventBus { this.registerPanelCommands(); this.registerPanelMenus(); } + + this.eventBus.onDirective(ResizeEvent.createDirective(this.location), () => { + this.onResize(); + }); } get onDidRegisterContainer() { @@ -863,19 +867,19 @@ export class TabbarService extends WithEventBus { return info && info.options && info.options.expanded; } - @OnEvent(ResizeEvent) - protected onResize(e: ResizeEvent) { - if (e.payload.slotLocation === this.location) { + protected onResize() { + fastdom.measureAtNextFrame(() => { if (!this.currentContainerId || !this.resizeHandle) { // 折叠时不监听变化 return; } + const size = this.resizeHandle.getSize(); if (size !== this.barSize && !this.shouldExpand(this.currentContainerId)) { this.prevSize = size; this.onSizeChangeEmitter.fire({ size }); } - } + }); } protected listenCurrentChange() { diff --git a/packages/monaco/src/browser/monaco.service.ts b/packages/monaco/src/browser/monaco.service.ts index 1513d6b6c5..092ba5d2a9 100644 --- a/packages/monaco/src/browser/monaco.service.ts +++ b/packages/monaco/src/browser/monaco.service.ts @@ -53,10 +53,11 @@ export default class MonacoServiceImpl extends Disposable implements MonacoServi lightbulb: { enabled: ShowLightbulbIconMode.OnCode, }, - automaticLayout: true, model: undefined, wordBasedSuggestions: 'off', renderLineHighlight: 'none', + automaticLayout: true, + ignoreTrimWhitespace: false, } as IStandaloneEditorConstructionOptions; } @@ -125,7 +126,6 @@ export default class MonacoServiceImpl extends Disposable implements MonacoServi monacoContainer, { ...this.monacoBaseOptions, - ignoreTrimWhitespace: false, ...options, } as any, { ...this.overrideServiceRegistry.all(), ...overrides }, diff --git a/packages/terminal-next/src/browser/terminal.controller.ts b/packages/terminal-next/src/browser/terminal.controller.ts index c995389b0a..d1859751a1 100644 --- a/packages/terminal-next/src/browser/terminal.controller.ts +++ b/packages/terminal-next/src/browser/terminal.controller.ts @@ -476,15 +476,14 @@ export class TerminalController extends WithEventBus implements ITerminalControl ); this.addDispose( - this.eventBus.on(ResizeEvent, (e: ResizeEvent) => { - if ( - this._tabBarHandler && - this._tabBarHandler.isActivated() && - e.payload.slotLocation === getSlotLocation('@opensumi/ide-terminal-next', this.config.layoutConfig) - ) { - this.terminalView.resize(); - } - }), + this.eventBus.onDirective( + ResizeEvent.createDirective(getSlotLocation('@opensumi/ide-terminal-next', this.config.layoutConfig)), + () => { + if (this._tabBarHandler && this._tabBarHandler.isActivated()) { + this.terminalView.resize(); + } + }, + ), ); this.registerContributedProfilesCommandAndMenu(); diff --git a/packages/utils/__tests__/heap.test.ts b/packages/utils/__tests__/heap.test.ts new file mode 100644 index 0000000000..afdff41f35 --- /dev/null +++ b/packages/utils/__tests__/heap.test.ts @@ -0,0 +1,48 @@ +import { Heap } from '../src/heap'; + +describe('heap', () => { + test('min heap should work', () => { + const minHeap = new Heap({ + comparator: (a, b) => a - b, + }); + minHeap.add(1); + minHeap.add(6); + minHeap.add(2); + minHeap.add(0); + minHeap.add(5); + minHeap.add(9); + + expect(minHeap.peek()).toBe(0); + + expect(minHeap.pop()).toBe(0); + expect(minHeap.pop()).toBe(1); + expect(minHeap.pop()).toBe(2); + expect(minHeap.pop()).toBe(5); + expect(minHeap.pop()).toBe(6); + expect(minHeap.pop()).toBe(9); + }); + + test('max heap should work', () => { + const maxHeap = new Heap({ + comparator: (a, b) => b - a, + }); + maxHeap.add(1); + maxHeap.add(6); + maxHeap.add(2); + maxHeap.add(0); + maxHeap.add(5); + maxHeap.add(9); + + expect(maxHeap.peek()).toBe(9); + + expect(maxHeap.pop()).toBe(9); + + expect(maxHeap.size).toBe(5); + + expect(maxHeap.pop()).toBe(6); + expect(maxHeap.pop()).toBe(5); + expect(maxHeap.pop()).toBe(2); + expect(maxHeap.pop()).toBe(1); + expect(maxHeap.pop()).toBe(0); + }); +}); diff --git a/packages/utils/src/heap.ts b/packages/utils/src/heap.ts new file mode 100644 index 0000000000..06ff8ac195 --- /dev/null +++ b/packages/utils/src/heap.ts @@ -0,0 +1,106 @@ +/** + * 大小堆(优先队列) + */ + +/** + * return a negative value if a < b, 0 if a == b, a positive value if a > b + */ +export type Comparator = (a: K, b: K) => number; + +interface HeapOptions { + /** + * A function that defines the sort order. The return value should be a number whose sign + * indicates the relative order of the two elements: negative if a is less than b, + * positive if a is greater than b, and zero if they are equal. + * NaN is treated as 0. + */ + comparator: Comparator; +} + +export class Heap { + private readonly elements: Array = []; + + // 堆元素数量 + size: number = 0; + + private cmp: Comparator; + + constructor(options: HeapOptions) { + this.cmp = options.comparator; + } + + peek(): T { + return this.elements[0]; + } + + pop(): T | undefined { + if (this.elements.length === 0) { + return undefined; + } + if (this.elements.length === 1) { + this.size--; + return this.elements.pop(); + } + + const res = this.elements[0]; + this.elements[0] = this.elements.pop()!; + this.size--; + + // 维护最大堆的特性:下沉操作 + this._sink(0); + + return res; + } + + add(data: T): void { + this.elements.push(data); + this.size++; + + this._float(); + } + + toArray(): Array { + return this.elements.slice(0, this.size); + } + + private _float(): void { + let idx: number = this.size - 1; + + let p: number; + while (idx > 0) { + // 获取父节点索引 + p = (idx - 1) >> 1; + + if (this.cmp(this.elements[p], this.elements[idx]) <= 0) { + break; + } + + this.swap(p, idx); + idx = p; + } + } + + private _sink(start: number): void { + const halfLength: number = this.size >> 1; + + while (start < halfLength) { + const l: number = (start << 1) | 1; + const r: number = l + 1; + + const minIdx: number = r < this.size && this.cmp(this.elements[r], this.elements[l]) < 0 ? r : l; + + if (this.cmp(this.elements[minIdx], this.elements[start]) >= 0) { + break; + } + + this.swap(start, minIdx); + start = minIdx; + } + } + + private swap(i: number, j: number): void { + const tmp = this.elements[i]; + this.elements[i] = this.elements[j]; + this.elements[j] = tmp; + } +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 7f4244675d..2f212db8d2 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,3 +1,9 @@ +export * as process from './process'; +export * as strings from './strings'; +export * as arrays from './arrays'; +export * as objects from './objects'; +export * as path from './path'; + export * from './ansi'; export * from './async'; export * from './buffer'; @@ -33,8 +39,4 @@ export * from './uint'; export * from './uri'; export * from './uuid'; export * from './const'; -export * as process from './process'; -export * as strings from './strings'; -export * as arrays from './arrays'; -export * as objects from './objects'; -export * as path from './path'; +export * from './heap'; diff --git a/tools/dev-tool/src/index.html b/tools/dev-tool/src/index.html index 410ce4fca4..b010c55141 100644 --- a/tools/dev-tool/src/index.html +++ b/tools/dev-tool/src/index.html @@ -2,6 +2,9 @@ + + + OpenSumi IDE