From e7afc11ca1ebadea6d872e532d0b02a842129f15 Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Sat, 2 Sep 2023 15:42:35 +0800 Subject: [PATCH 01/24] refactor(core): move `makeProvider` to single file --- core/src/plugins/typescript/index.ts | 59 +--------------------------- core/src/utils/index.ts | 1 + core/src/utils/makeProvider.ts | 59 ++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 57 deletions(-) create mode 100644 core/src/utils/makeProvider.ts diff --git a/core/src/plugins/typescript/index.ts b/core/src/plugins/typescript/index.ts index e755bdb0..e9ccc3e5 100644 --- a/core/src/plugins/typescript/index.ts +++ b/core/src/plugins/typescript/index.ts @@ -1,8 +1,8 @@ import './index.scss' import { useEffect, useMemo } from 'react' -import type { Editor, IStandaloneCodeEditor } from '@power-playground/core' -import { asyncDebounce, messenger } from '@power-playground/core' +import type { Editor } from '@power-playground/core' +import { makeProvider, messenger } from '@power-playground/core' import { getDefaultStore, useAtom } from 'jotai' import type * as monacoEditor from 'monaco-editor' import { mergeAll, mergeDeepLeft } from 'ramda' @@ -71,61 +71,6 @@ if (import.meta.hot) { const modelDecorationIdsSymbol = '_modelDecorationIds' -type Provider = ( - model: monacoEditor.editor.ITextModel, - opts: { mountInitValue: T; isCancel: { value: boolean } }, -) => Promise<() => void> | (() => void) - -function makeProvider( - mount: ( - editor: IStandaloneCodeEditor - ) => T, - clear: ( - editor: IStandaloneCodeEditor, - mountInitValue: T - ) => void, - anytime?: () => void -) { - return ( - editor: IStandaloneCodeEditor, - selector: { languages: string[] }, - provider: Provider - ) => { - const mountInitValue = mount(editor) - - const debounce = asyncDebounce() - let isCancel = { value: false } - let prevDispose: (() => void) | undefined = undefined - - async function callback() { - anytime?.() - const model = editor.getModel() - if (!model) return - - if (!selector.languages.includes(model.getLanguageId())) { - clear(editor, mountInitValue) - return - } - try { await debounce(300) } catch { return } - - isCancel.value = true - isCancel = { value: false } - - prevDispose?.() - prevDispose = await provider(model, { mountInitValue, isCancel }) - } - callback().catch(console.error) - return [ - editor.onDidChangeModel(callback).dispose, - editor.onDidChangeModelContent(callback).dispose, - editor.onDidFocusEditorWidget(callback).dispose - ].reduce((acc, cur) => () => (acc(), cur()), () => { - clear(editor, mountInitValue) - prevDispose?.() - }) - } -} - const addDecorationProvider = makeProvider(editor => { const decorationsCollection = editor.createDecorationsCollection() diff --git a/core/src/utils/index.ts b/core/src/utils/index.ts index cdeb369c..7d5e3692 100644 --- a/core/src/utils/index.ts +++ b/core/src/utils/index.ts @@ -2,5 +2,6 @@ export * from './asyncDebounce' export * from './classnames' export * from './copyToClipboard' export * from './isMacOS' +export * from './makeProvider' export * from './scrollIntoViewIfNeeded' export * from './typescriptVersionMeta' diff --git a/core/src/utils/makeProvider.ts b/core/src/utils/makeProvider.ts new file mode 100644 index 00000000..02fa7468 --- /dev/null +++ b/core/src/utils/makeProvider.ts @@ -0,0 +1,59 @@ +import type { IStandaloneCodeEditor } from '@power-playground/core' +import type * as monacoEditor from 'monaco-editor' + +import { asyncDebounce } from './asyncDebounce' + +export type Provider = ( + model: monacoEditor.editor.ITextModel, + opts: { mountInitValue: T; isCancel: { value: boolean } }, +) => Promise<() => void> | (() => void) + +export function makeProvider( + mount: ( + editor: IStandaloneCodeEditor + ) => T, + clear: ( + editor: IStandaloneCodeEditor, + mountInitValue: T + ) => void, + anytime?: () => void +) { + return ( + editor: IStandaloneCodeEditor, + selector: { languages: string[] }, + provider: Provider + ) => { + const mountInitValue = mount(editor) + + const debounce = asyncDebounce() + let isCancel = { value: false } + let prevDispose: (() => void) | undefined = undefined + + async function callback() { + anytime?.() + const model = editor.getModel() + if (!model) return + + if (!selector.languages.includes(model.getLanguageId())) { + clear(editor, mountInitValue) + return + } + try { await debounce(300) } catch { return } + + isCancel.value = true + isCancel = { value: false } + + prevDispose?.() + prevDispose = await provider(model, { mountInitValue, isCancel }) + } + callback().catch(console.error) + return [ + editor.onDidChangeModel(callback).dispose, + editor.onDidChangeModelContent(callback).dispose, + editor.onDidFocusEditorWidget(callback).dispose + ].reduce((acc, cur) => () => (acc(), cur()), () => { + clear(editor, mountInitValue) + prevDispose?.() + }) + } +} From 6d1113b0561a409ef48b50f5dcf537a1d95dca77 Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Sat, 2 Sep 2023 16:53:51 +0800 Subject: [PATCH 02/24] feat(core): add TODO --- core/src/plugins/urlCache.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/plugins/urlCache.ts b/core/src/plugins/urlCache.ts index e4c58b8e..175eb37e 100644 --- a/core/src/plugins/urlCache.ts +++ b/core/src/plugins/urlCache.ts @@ -26,6 +26,7 @@ export default definePlugin({ }], load(editor, monaco) { editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => { + // TODO prevent event of browser default behavior const code = editor.getValue() history.pushState(null, '', '#' + btoa(encodeURIComponent(code))) copyToClipboard(location.href) From 3777ba1c434f071a94548b70bbb73b177d6c0b53 Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Sat, 2 Sep 2023 20:44:46 +0800 Subject: [PATCH 03/24] refactor(core): change dispose register logic --- core/src/plugins/typescript/index.ts | 207 ++++++++++++++------------- 1 file changed, 106 insertions(+), 101 deletions(-) diff --git a/core/src/plugins/typescript/index.ts b/core/src/plugins/typescript/index.ts index e9ccc3e5..a5b827d4 100644 --- a/core/src/plugins/typescript/index.ts +++ b/core/src/plugins/typescript/index.ts @@ -227,115 +227,120 @@ const editor: Editor = { load: (editor, monaco) => { const re = require as unknown as (id: string[], cb: (...args: any[]) => void) => void let typescript: typeof import('typescript') | undefined = undefined - return addDecorationProvider( - editor, - { languages: ['javascript', 'typescript'] }, - async (model, { mountInitValue: { - modelDecorationIds, - decorationsCollection, - dependencyLoadErrorReason - }, isCancel }) => { - const uri = model.uri.toString() - const ids = modelDecorationIds.get(uri) - ?? modelDecorationIds.set(uri, []).get(uri)! + return [ + addDecorationProvider( + editor, + { languages: ['javascript', 'typescript'] }, + async (model, { mountInitValue: { + modelDecorationIds, + decorationsCollection, + dependencyLoadErrorReason + }, isCancel }) => { + const uri = model.uri.toString() + const ids = modelDecorationIds.get(uri) + ?? modelDecorationIds.set(uri, []).get(uri)! - const content = model.getValue() - const ts = await new Promise(resolve => { - if (typescript === undefined) { - re(['vs/language/typescript/tsWorker'], () => { - // @ts-ignore - typescript = window.ts - // @ts-ignore - resolve(window.ts as unknown as typeof import('typescript')) - }) - } else { - resolve(typescript) - } - }) + const content = model.getValue() + const ts = await new Promise(resolve => { + if (typescript === undefined) { + re(['vs/language/typescript/tsWorker'], () => { + // @ts-ignore + typescript = window.ts + // @ts-ignore + resolve(window.ts as unknown as typeof import('typescript')) + }) + } else { + resolve(typescript) + } + }) - const extraModules = store.get(extraModulesAtom).reduce((acc, { filePath }) => { - const name = /((?:@[^/]*\/)?[^/]+)/.exec(filePath)?.[1] - if (name && !acc.includes(name)) { - acc.push(name) - } - return acc - }, [] as string[]) - const references = getReferencesForModule(ts, content) - .filter(ref => !ref.module.startsWith('.')) - .filter(ref => !extraModules.includes(ref.module)) - .map(ref => ({ - ...ref, - module: mapModuleNameToModule(ref.module) - })) - .reduce((acc, cur) => { - const index = acc.findIndex(({ module }) => module === cur.module) - if (index === -1) { - acc.push(cur) + const extraModules = store.get(extraModulesAtom).reduce((acc, { filePath }) => { + const name = /((?:@[^/]*\/)?[^/]+)/.exec(filePath)?.[1] + if (name && !acc.includes(name)) { + acc.push(name) } return acc - }, [] as RefForModule) - let resolveModulesFulfilled = () => void 0 - resolveModules(monaco, prevRefs, references, { - onDepLoadError({ depName, error }) { - dependencyLoadErrorReason[depName] = `⚠️ ${error.message}` - } - }) - .then(() => { - if (isCancel.value) return - resolveModulesFulfilled() - }) - prevRefs = references - if (import.meta.hot) { - import.meta.hot.data['ppd:typescript:prevRefs'] = prevRefs - } - resolveReferences(references) - if (import.meta.hot) { - import.meta.hot.data['ppd:typescript:referencesPromise'] = referencesPromise - } - editor.removeDecorations(ids) - const loadingDecorations: ( - & { loadedVersion: string, loadModule: string } - & monacoEditor.editor.IModelDeltaDecoration - )[] = references.map(ref => { - const [start, end] = ref.position - const startP = model.getPositionAt(start) - const endP = model.getPositionAt(end) - const range = new monaco.Range( - startP.lineNumber, - startP.column + 1, - endP.lineNumber, - endP.column + 1 - ) - const inlineClassName = `ts__button-decoration ts__button-decoration__position-${start}__${end}` - return { - loadModule: ref.module, - loadedVersion: ref.version ?? 'latest', - range, - options: { - isWholeLine: true, - after: { - content: '⚡️ Downloading...', - inlineClassName + }, [] as string[]) + const references = getReferencesForModule(ts, content) + .filter(ref => !ref.module.startsWith('.')) + .filter(ref => !extraModules.includes(ref.module)) + .map(ref => ({ + ...ref, + module: mapModuleNameToModule(ref.module) + })) + .reduce((acc, cur) => { + const index = acc.findIndex(({ module }) => module === cur.module) + if (index === -1) { + acc.push(cur) } + return acc + }, [] as RefForModule) + let resolveModulesFulfilled = () => void 0 + resolveModules(monaco, prevRefs, references, { + onDepLoadError({ depName, error }) { + dependencyLoadErrorReason[depName] = `⚠️ ${error.message}` } + }) + .then(() => { + if (isCancel.value) return + resolveModulesFulfilled() + }) + prevRefs = references + if (import.meta.hot) { + import.meta.hot.data['ppd:typescript:prevRefs'] = prevRefs } - }) - const newIds = decorationsCollection.set(loadingDecorations) - modelDecorationIds.set(uri, newIds) + resolveReferences(references) + if (import.meta.hot) { + import.meta.hot.data['ppd:typescript:referencesPromise'] = referencesPromise + } + editor.removeDecorations(ids) + const loadingDecorations: ( + & { loadedVersion: string, loadModule: string } + & monacoEditor.editor.IModelDeltaDecoration + )[] = references.map(ref => { + const [start, end] = ref.position + const startP = model.getPositionAt(start) + const endP = model.getPositionAt(end) + const range = new monaco.Range( + startP.lineNumber, + startP.column + 1, + endP.lineNumber, + endP.column + 1 + ) + const inlineClassName = `ts__button-decoration ts__button-decoration__position-${start}__${end}` + return { + loadModule: ref.module, + loadedVersion: ref.version ?? 'latest', + range, + options: { + isWholeLine: true, + after: { + content: '⚡️ Downloading...', + inlineClassName + } + } + } + }) + const newIds = decorationsCollection.set(loadingDecorations) + modelDecorationIds.set(uri, newIds) - resolveModulesFulfilled = () => { - editor.removeDecorations(newIds) - const loadedDecorations = loadingDecorations.map(d => { - const error = dependencyLoadErrorReason[`${d.loadModule}@${d.loadedVersion}`] - return mergeDeepLeft({ - options: { after: { content: error ?? `@${d.loadedVersion}` } } - }, d) - }) - const loadedIds = decorationsCollection.set(loadedDecorations) - modelDecorationIds.set(uri, loadedIds) - } - return () => {} - }) + resolveModulesFulfilled = () => { + editor.removeDecorations(newIds) + const loadedDecorations = loadingDecorations.map(d => { + const error = dependencyLoadErrorReason[`${d.loadModule}@${d.loadedVersion}`] + return mergeDeepLeft({ + options: { after: { content: error ?? `@${d.loadedVersion}` } } + }, d) + }) + const loadedIds = decorationsCollection.set(loadedDecorations) + modelDecorationIds.set(uri, loadedIds) + } + return () => {} + }) + ].reduce( + (acc, cur) => () => (acc(), cur()), + () => void 0 + ) }, topbar: [Langs], statusbar: [Versions, Setting] From f8130749abf03d2511033cdc09b728720c56c6e6 Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Sat, 2 Sep 2023 20:48:50 +0800 Subject: [PATCH 04/24] refactor(core): make plugins load hook support array return value --- core/src/components/EditorZone.tsx | 20 ++++++++++++++++---- core/src/plugins/index.tsx | 2 +- core/src/plugins/typescript/index.ts | 5 +---- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/core/src/components/EditorZone.tsx b/core/src/components/EditorZone.tsx index 1477bb24..268c490a 100644 --- a/core/src/components/EditorZone.tsx +++ b/core/src/components/EditorZone.tsx @@ -131,10 +131,22 @@ export default function EditorZone(props: EditorZoneProps) { useEffect(() => { if (!monaco || !editor) return - const dispose = plugins.map(plugin => plugin?.editor?.load?.( - editor as IStandaloneCodeEditor, - monaco - )) + const dispose = plugins + .map(plugin => { + const loadRT = plugin?.editor?.load?.( + editor as IStandaloneCodeEditor, + monaco + ) + if (typeof loadRT === 'function') { + return loadRT + } + if (Array.isArray(loadRT)) { + return loadRT.reduce( + (acc, func) => () => (acc?.(), func?.()), + () => void 0 + ) + } + }) return () => dispose.forEach(func => func?.()) }, [monaco, editor, plugins]) diff --git a/core/src/plugins/index.tsx b/core/src/plugins/index.tsx index fc013444..3811486c 100644 --- a/core/src/plugins/index.tsx +++ b/core/src/plugins/index.tsx @@ -191,7 +191,7 @@ export type Editor Dispose | void + ) => Dispose | void | Dispose[] topbar?: React.ComponentType>[] statusbar?: React.ComponentType< diff --git a/core/src/plugins/typescript/index.ts b/core/src/plugins/typescript/index.ts index a5b827d4..6f2740f7 100644 --- a/core/src/plugins/typescript/index.ts +++ b/core/src/plugins/typescript/index.ts @@ -337,10 +337,7 @@ const editor: Editor = { } return () => {} }) - ].reduce( - (acc, cur) => () => (acc(), cur()), - () => void 0 - ) + ] }, topbar: [Langs], statusbar: [Versions, Setting] From cabd2590ae868d77f8ca7d7fe2a63876146f2ef4 Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Sat, 2 Sep 2023 20:54:45 +0800 Subject: [PATCH 05/24] fix(core): popover hover will break the hover element style --- core/src/components/base/Popover.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/components/base/Popover.scss b/core/src/components/base/Popover.scss index 791472ca..d4863148 100644 --- a/core/src/components/base/Popover.scss +++ b/core/src/components/base/Popover.scss @@ -24,6 +24,7 @@ width: 0; height: 0; border: 6px solid transparent; + pointer-events: none; } &[data-position^=top]::before, &[data-position^=bottom]::before { left: 50%; From 01343a9b2f3121cdd1732a98a5f96c1e351f2832 Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Sun, 3 Sep 2023 03:06:23 +0800 Subject: [PATCH 06/24] feat(core): getNamespaces util --- .../plugins/typescript/utils/getNamespaces.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 core/src/plugins/typescript/utils/getNamespaces.ts diff --git a/core/src/plugins/typescript/utils/getNamespaces.ts b/core/src/plugins/typescript/utils/getNamespaces.ts new file mode 100644 index 00000000..69308b79 --- /dev/null +++ b/core/src/plugins/typescript/utils/getNamespaces.ts @@ -0,0 +1,37 @@ +import type * as TS from 'typescript' + +export type NameSpacesChain = ( + & TS.ModuleDeclaration + & { top: TS.ModuleDeclaration } +)[] + +export const getNamespaces = (ts: typeof TS, code: string) => { + const sourceFile = ts.createSourceFile('file.ts', code, ts.ScriptTarget.Latest, true) + const namespaces: Record = {} + const visit = (node: import('typescript').Node) => { + if (ts.isModuleDeclaration(node) && node.name && ts.isIdentifier(node.name)) { + if (node.body && ts.isModuleBlock(node.body)) { + const namespaceChain = [node] + let _node = node + let parent = node.parent + while (parent && ts.isModuleDeclaration(parent) && parent.name && ts.isIdentifier(parent.name)) { + namespaceChain.unshift(parent) + _node = parent + parent = parent.parent + } + const namespaceChainNames = namespaceChain.map(n => n.name.text).join('.') + if (!namespaces[namespaceChainNames]) { + namespaces[namespaceChainNames] = [] + } + + namespaces[namespaceChainNames].push({ + ..._node, + top: node + }) + } + } + ts.forEachChild(node, visit) + } + visit(sourceFile) + return namespaces +} From 3561c3b2a0cfd69d9c8bfb9365cabc0b4cbaa2d4 Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Sun, 3 Sep 2023 03:09:33 +0800 Subject: [PATCH 07/24] feat(core): identify unit test mode and support display a run glyph button --- core/src/plugins/typescript/index.scss | 16 ++- core/src/plugins/typescript/index.ts | 149 ++++++++++++++++++++++--- core/src/utils/makeProvider.ts | 13 ++- 3 files changed, 154 insertions(+), 24 deletions(-) diff --git a/core/src/plugins/typescript/index.scss b/core/src/plugins/typescript/index.scss index ec8d9d1f..0a71be18 100644 --- a/core/src/plugins/typescript/index.scss +++ b/core/src/plugins/typescript/index.scss @@ -3,16 +3,28 @@ color: var(--vscode-editorInlayHint-foreground) !important; background-color: var(--vscode-editorInlayHint-background); } -div[widgetid^=typescript-test-glyph-margin] { +div[widgetid^=ppd-plugins-typescript-glyph-margin] { position: relative; cursor: pointer; > .codicon { + --ic-color: #888; + display: flex; align-items: center; justify-content: center; + color: var(--ic-color); + transition: .1s; + &.codicon-run-errors { + --ic-color: #ff5370; + } + &.codicon-run-check, &.codicon-run-check-all { + --ic-color: #3ea638; + } &::before { - color: #3ea638; height: 18px; } + &:hover { + filter: drop-shadow(0 0 2px var(--ic-color)); + } } } diff --git a/core/src/plugins/typescript/index.ts b/core/src/plugins/typescript/index.ts index 6f2740f7..138a02c7 100644 --- a/core/src/plugins/typescript/index.ts +++ b/core/src/plugins/typescript/index.ts @@ -2,7 +2,7 @@ import './index.scss' import { useEffect, useMemo } from 'react' import type { Editor } from '@power-playground/core' -import { makeProvider, messenger } from '@power-playground/core' +import { classnames, makeProvider, messenger } from '@power-playground/core' import { getDefaultStore, useAtom } from 'jotai' import type * as monacoEditor from 'monaco-editor' import { mergeAll, mergeDeepLeft } from 'ramda' @@ -13,6 +13,7 @@ import { definePlugin } from '..' import { Setting } from './statusbar/Setting' import { Versions } from './statusbar/Versions' import { Langs } from './topbar/Langs' +import { getNamespaces } from './utils/getNamespaces' import { compilerOptionsAtom, extraFilesAtom, extraModulesAtom } from './atoms' import { resolveModules } from './modules' import { use } from './use' @@ -113,6 +114,62 @@ const addDecorationProvider = makeProvider(editor => { } }) +const GLYPH_PREFIX = 'ppd-plugins-typescript-glyph-margin' +const createGlyph = ( + monaco: typeof monacoEditor, + editor: monacoEditor.editor.IStandaloneCodeEditor, + line: number, content: string, + eleResolver?: (ele: HTMLDivElement) => void +): monacoEditor.editor.IGlyphMarginWidget => ({ + getDomNode() { + const domNode = document.createElement('div') + domNode.innerHTML = content + eleResolver?.(domNode) + return domNode + }, + getPosition() { + return { + lane: monaco.editor.GlyphMarginLane.Right, + zIndex: 100, + range: new monaco.Range(line, 1, line, 1) + } + }, + getId: () => `${GLYPH_PREFIX} ${GLYPH_PREFIX}__${line}` +}) + +const addGlyphProvider = makeProvider((editor, monaco) => { + const glyphMarginWidgets: monacoEditor.editor.IGlyphMarginWidget[] = [] + + return { + glyphMarginWidgets, + addGlyph: (line: number, content: string, eleResolver?: (ele: HTMLDivElement) => void) => { + const model = editor.getModel() + if (!model) throw new Error('model not found') + + const widget = createGlyph(monaco, editor, line, content, eleResolver) + editor.addGlyphMarginWidget(widget) + glyphMarginWidgets.push(widget) + editor.updateOptions({ glyphMargin: true }) + return widget.getId() + }, + removeGlyph: (id: string) => { + const index = glyphMarginWidgets.findIndex(widget => widget.getId() === id) + if (index !== -1) { + editor.removeGlyphMarginWidget(glyphMarginWidgets[index]) + glyphMarginWidgets.splice(index, 1) + } + if (glyphMarginWidgets.length === 0) { + editor.updateOptions({ glyphMargin: false }) + } + } + } +}, (editor, { + glyphMarginWidgets +}) => { + editor.updateOptions({ glyphMargin: false }) + glyphMarginWidgets.forEach(widget => editor.removeGlyphMarginWidget(widget)) +}) + const editor: Editor = { use, useShare({ @@ -227,11 +284,26 @@ const editor: Editor = { load: (editor, monaco) => { const re = require as unknown as (id: string[], cb: (...args: any[]) => void) => void let typescript: typeof import('typescript') | undefined = undefined + const lazyTS = new Promise(resolve => { + if (typescript === undefined) { + re(['vs/language/typescript/tsWorker'], () => { + // @ts-ignore + typescript = window.ts + // @ts-ignore + resolve(window.ts as unknown as typeof import('typescript')) + }) + } else { + resolve(typescript) + } + }) + + type ProviderDefaultParams = Parameters> extends [ + ...infer T, infer _Ignore + ] ? T : never + const providerDefaultParams: ProviderDefaultParams = [monaco, editor, { languages: ['javascript', 'typescript'] }] return [ addDecorationProvider( - editor, - { languages: ['javascript', 'typescript'] }, - async (model, { mountInitValue: { + ...providerDefaultParams, async (model, { mountInitValue: { modelDecorationIds, decorationsCollection, dependencyLoadErrorReason @@ -241,18 +313,7 @@ const editor: Editor = { ?? modelDecorationIds.set(uri, []).get(uri)! const content = model.getValue() - const ts = await new Promise(resolve => { - if (typescript === undefined) { - re(['vs/language/typescript/tsWorker'], () => { - // @ts-ignore - typescript = window.ts - // @ts-ignore - resolve(window.ts as unknown as typeof import('typescript')) - }) - } else { - resolve(typescript) - } - }) + const ts = await lazyTS const extraModules = store.get(extraModulesAtom).reduce((acc, { filePath }) => { const name = /((?:@[^/]*\/)?[^/]+)/.exec(filePath)?.[1] @@ -335,7 +396,61 @@ const editor: Editor = { const loadedIds = decorationsCollection.set(loadedDecorations) modelDecorationIds.set(uri, loadedIds) } - return () => {} + return () => void 0 + }), + addGlyphProvider( + ...providerDefaultParams, async (model, { mountInitValue: { + addGlyph, removeGlyph + } }) => { + const ts = await lazyTS + const namespaces = getNamespaces(ts, editor.getValue()) + const visibleRanges = editor.getVisibleRanges() + + const gids: string[] = [] + Object.entries(namespaces) + .forEach(([name, ns]) => { + if (!['describe', 'it'].some(n => name.startsWith(n))) return + + const isDescribe = name.startsWith('describe') + ns.forEach(namespace => { + const realLine = model.getPositionAt(namespace.top.pos).lineNumber + let line = realLine + let inFold = false + let prevVisibleEndLineNumber = 1 + visibleRanges.forEach(({ startLineNumber, endLineNumber }) => { + if (realLine >= endLineNumber && realLine <= startLineNumber) { + inFold = true + return + } + if (realLine <= startLineNumber) + return + + const offset = startLineNumber - prevVisibleEndLineNumber + line -= offset === 0 ? 0 : offset - 1 + prevVisibleEndLineNumber = endLineNumber + }) + if (inFold) return + + gids.push( + addGlyph(line, ``, ele => { + ele.onclick = () => { + messenger.then(m => m.display('warning', 'Running test is not supported yet')) + // ele.innerHTML = `` + ele.innerHTML = `` + } + }) + ) + }) + }) + return () => { + gids.forEach(id => removeGlyph(id)) + } }) ] }, diff --git a/core/src/utils/makeProvider.ts b/core/src/utils/makeProvider.ts index 02fa7468..7d662832 100644 --- a/core/src/utils/makeProvider.ts +++ b/core/src/utils/makeProvider.ts @@ -10,20 +10,23 @@ export type Provider = ( export function makeProvider( mount: ( - editor: IStandaloneCodeEditor + editor: IStandaloneCodeEditor, + monaco: typeof monacoEditor ) => T, clear: ( editor: IStandaloneCodeEditor, - mountInitValue: T + mountInitValue: T, + monaco: typeof monacoEditor ) => void, anytime?: () => void ) { return ( + monaco: typeof monacoEditor, editor: IStandaloneCodeEditor, selector: { languages: string[] }, provider: Provider ) => { - const mountInitValue = mount(editor) + const mountInitValue = mount(editor, monaco) const debounce = asyncDebounce() let isCancel = { value: false } @@ -35,7 +38,7 @@ export function makeProvider( if (!model) return if (!selector.languages.includes(model.getLanguageId())) { - clear(editor, mountInitValue) + clear(editor, mountInitValue, monaco) return } try { await debounce(300) } catch { return } @@ -52,7 +55,7 @@ export function makeProvider( editor.onDidChangeModelContent(callback).dispose, editor.onDidFocusEditorWidget(callback).dispose ].reduce((acc, cur) => () => (acc(), cur()), () => { - clear(editor, mountInitValue) + clear(editor, mountInitValue, monaco) prevDispose?.() }) } From a79c439a5e18734a0f625d7a23119561407b3b78 Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Sun, 3 Sep 2023 03:34:00 +0800 Subject: [PATCH 08/24] chore: add a GitHub issue link for the not implement hint message --- core/src/components/LeftBar.tsx | 14 +++++++------- .../history-for-local/satusbar/HistoryDialog.tsx | 2 +- src/components/I18N.tsx | 6 +++++- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/core/src/components/LeftBar.tsx b/core/src/components/LeftBar.tsx index 5a2e1cb1..608342fa 100644 --- a/core/src/components/LeftBar.tsx +++ b/core/src/components/LeftBar.tsx @@ -15,32 +15,32 @@ export function LeftBar(props: LeftBarProps) { return
- - - - -
- - Power Playground menu icon. diff --git a/core/src/plugins/history-for-local/satusbar/HistoryDialog.tsx b/core/src/plugins/history-for-local/satusbar/HistoryDialog.tsx index fcba2e3d..ad78d9b7 100644 --- a/core/src/plugins/history-for-local/satusbar/HistoryDialog.tsx +++ b/core/src/plugins/history-for-local/satusbar/HistoryDialog.tsx @@ -123,7 +123,7 @@ export const HistoryDialog = forwardRef(function } if (e.key === 'Backspace') { // TODO remove history item - messenger.then(m => m.display('warning', 'Not implemented yet')) + messenger.then(m => m.display('warning', <>Not implemented yet, it will come soon, help us)) } }} handleKeyDownOnOpen={e => { diff --git a/src/components/I18N.tsx b/src/components/I18N.tsx index 77e4f597..16ae9f84 100644 --- a/src/components/I18N.tsx +++ b/src/components/I18N.tsx @@ -6,7 +6,11 @@ import { Popover } from '@power-playground/core/components/base/Popover.tsx' export function I18N() { return + Not implemented yet, it will come soon, + help us + + } placement='bottom-start' trigger='click' offset={[0, 10]} From 1d99664645d84369a1d51dbc2172207c9495c430 Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Sun, 3 Sep 2023 17:09:49 +0800 Subject: [PATCH 09/24] refactor(core): no callback no headache --- core/src/plugins/typescript/index.ts | 80 ++++++++++++++-------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/core/src/plugins/typescript/index.ts b/core/src/plugins/typescript/index.ts index 138a02c7..2a6c1256 100644 --- a/core/src/plugins/typescript/index.ts +++ b/core/src/plugins/typescript/index.ts @@ -408,49 +408,49 @@ const editor: Editor = { const gids: string[] = [] Object.entries(namespaces) - .forEach(([name, ns]) => { - if (!['describe', 'it'].some(n => name.startsWith(n))) return - - const isDescribe = name.startsWith('describe') - ns.forEach(namespace => { - const realLine = model.getPositionAt(namespace.top.pos).lineNumber - let line = realLine - let inFold = false - let prevVisibleEndLineNumber = 1 - visibleRanges.forEach(({ startLineNumber, endLineNumber }) => { - if (realLine >= endLineNumber && realLine <= startLineNumber) { - inFold = true - return - } - if (realLine <= startLineNumber) - return - - const offset = startLineNumber - prevVisibleEndLineNumber - line -= offset === 0 ? 0 : offset - 1 - prevVisibleEndLineNumber = endLineNumber - }) - if (inFold) return - - gids.push( - addGlyph(line, ``, ele => { - ele.onclick = () => { - messenger.then(m => m.display('warning', 'Running test is not supported yet')) - // ele.innerHTML = `` - ele.innerHTML = `` + .filter(([name]) => ['describe', 'it'].some(n => name.startsWith(n))) + .map(([name, ns]) => { + type FlatItem = [number, string, ReturnType['describe'][0]] + return ns + .reduce((acc, namespace) => { + const realLine = model.getPositionAt(namespace.top.pos).lineNumber + let line = realLine + let inFold = false + let prevVisibleEndLineNumber = 1 + visibleRanges.forEach(({ startLineNumber, endLineNumber }) => { + if (realLine >= endLineNumber && realLine <= startLineNumber) { + inFold = true + return } + if (realLine <= startLineNumber) + return + + const offset = startLineNumber - prevVisibleEndLineNumber + line -= offset === 0 ? 0 : offset - 1 + prevVisibleEndLineNumber = endLineNumber }) - ) - }) + if (inFold) return acc + return acc.concat([ + [line, name, namespace] + ]) + }, [] as FlatItem[]) + .flat() as FlatItem }) - return () => { - gids.forEach(id => removeGlyph(id)) - } + .forEach(([line, name, namespace]) => { + const isDescribe = name.startsWith('describe') + gids.push(addGlyph(line, ``, ele => { + ele.onclick = () => { + messenger.then(m => m.display('warning', 'Running test is not supported yet')) + // ele.innerHTML = `` + ele.innerHTML = `` + } + })) + }) + return () => gids.forEach(id => removeGlyph(id)) }) ] }, From bf1c722bcd8bbb97a37cdbbdbeceaf197401ee94 Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Sun, 3 Sep 2023 17:59:25 +0800 Subject: [PATCH 10/24] refactor(core): adjust anytime as field of options --- core/src/plugins/typescript/index.ts | 8 +++++--- core/src/utils/makeProvider.ts | 7 ++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/core/src/plugins/typescript/index.ts b/core/src/plugins/typescript/index.ts index 2a6c1256..637e2c25 100644 --- a/core/src/plugins/typescript/index.ts +++ b/core/src/plugins/typescript/index.ts @@ -108,9 +108,11 @@ const addDecorationProvider = makeProvider(editor => { if (import.meta.hot) { import.meta.hot.data['ppd:typescript:dependencyLoadErrorReason'] = {} } -}, async () => { - if (await promiseStatus(referencesPromise) === 'fulfilled') { - referencesPromise = new Promise(re => resolveReferences = re) +}, { + anytime: async () => { + if (await promiseStatus(referencesPromise) === 'fulfilled') { + referencesPromise = new Promise(re => resolveReferences = re) + } } }) diff --git a/core/src/utils/makeProvider.ts b/core/src/utils/makeProvider.ts index 7d662832..03b5704e 100644 --- a/core/src/utils/makeProvider.ts +++ b/core/src/utils/makeProvider.ts @@ -18,8 +18,13 @@ export function makeProvider( mountInitValue: T, monaco: typeof monacoEditor ) => void, - anytime?: () => void + opts?: { + anytime?: () => void + } ) { + const { + anytime + } = opts ?? {} return ( monaco: typeof monacoEditor, editor: IStandaloneCodeEditor, From 2b97b3a86e0c1319477cea99074d3845ee3bd3b2 Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Sun, 3 Sep 2023 19:12:43 +0800 Subject: [PATCH 11/24] fix(core): fold not trigger glyph provider update which needed the `onDidChangeModelDecorations` event --- core/src/plugins/typescript/index.ts | 7 ++++++- core/src/utils/makeProvider.ts | 24 +++++++++++++++--------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/core/src/plugins/typescript/index.ts b/core/src/plugins/typescript/index.ts index 637e2c25..033d00c7 100644 --- a/core/src/plugins/typescript/index.ts +++ b/core/src/plugins/typescript/index.ts @@ -2,7 +2,7 @@ import './index.scss' import { useEffect, useMemo } from 'react' import type { Editor } from '@power-playground/core' -import { classnames, makeProvider, messenger } from '@power-playground/core' +import { classnames, DEFAULT_WATCH_EVENT_KEYS, makeProvider, messenger } from '@power-playground/core' import { getDefaultStore, useAtom } from 'jotai' import type * as monacoEditor from 'monaco-editor' import { mergeAll, mergeDeepLeft } from 'ramda' @@ -170,6 +170,11 @@ const addGlyphProvider = makeProvider((editor, monaco) => { }) => { editor.updateOptions({ glyphMargin: false }) glyphMarginWidgets.forEach(widget => editor.removeGlyphMarginWidget(widget)) +}, { + watchEventKeys: [ + ...DEFAULT_WATCH_EVENT_KEYS, + 'onDidChangeModelDecorations' + ] }) const editor: Editor = { diff --git a/core/src/utils/makeProvider.ts b/core/src/utils/makeProvider.ts index 03b5704e..671b2d66 100644 --- a/core/src/utils/makeProvider.ts +++ b/core/src/utils/makeProvider.ts @@ -8,6 +8,12 @@ export type Provider = ( opts: { mountInitValue: T; isCancel: { value: boolean } }, ) => Promise<() => void> | (() => void) +export const DEFAULT_WATCH_EVENT_KEYS = [ + 'onDidChangeModel', + 'onDidChangeModelContent', + 'onDidFocusEditorWidget' +] as const + export function makeProvider( mount: ( editor: IStandaloneCodeEditor, @@ -20,10 +26,12 @@ export function makeProvider( ) => void, opts?: { anytime?: () => void + watchEventKeys?: Extract[] } ) { const { - anytime + anytime, + watchEventKeys = DEFAULT_WATCH_EVENT_KEYS } = opts ?? {} return ( monaco: typeof monacoEditor, @@ -55,13 +63,11 @@ export function makeProvider( prevDispose = await provider(model, { mountInitValue, isCancel }) } callback().catch(console.error) - return [ - editor.onDidChangeModel(callback).dispose, - editor.onDidChangeModelContent(callback).dispose, - editor.onDidFocusEditorWidget(callback).dispose - ].reduce((acc, cur) => () => (acc(), cur()), () => { - clear(editor, mountInitValue, monaco) - prevDispose?.() - }) + return watchEventKeys + .map(key => editor[key](callback).dispose) + .reduce((acc, cur) => () => (acc(), cur()), () => { + clear(editor, mountInitValue, monaco) + prevDispose?.() + }) } } From 0ea23c7dbe94236f18e9d6bf7bf187a13b68ff43 Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Sun, 3 Sep 2023 21:41:06 +0800 Subject: [PATCH 12/24] feat(core): better performance to resolve glyph effect --- core/src/plugins/typescript/index.ts | 19 ++++++++++-- core/src/utils/makeProvider.ts | 46 ++++++++++++++++++++++------ 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/core/src/plugins/typescript/index.ts b/core/src/plugins/typescript/index.ts index 033d00c7..a4150756 100644 --- a/core/src/plugins/typescript/index.ts +++ b/core/src/plugins/typescript/index.ts @@ -2,7 +2,14 @@ import './index.scss' import { useEffect, useMemo } from 'react' import type { Editor } from '@power-playground/core' -import { classnames, DEFAULT_WATCH_EVENT_KEYS, makeProvider, messenger } from '@power-playground/core' +import { + classnames, + DEFAULT_WATCH_EVENT_KEYS, + isWhatArgs, + makeProvider, + messenger, + StopThisTimeError +} from '@power-playground/core' import { getDefaultStore, useAtom } from 'jotai' import type * as monacoEditor from 'monaco-editor' import { mergeAll, mergeDeepLeft } from 'ramda' @@ -174,7 +181,15 @@ const addGlyphProvider = makeProvider((editor, monaco) => { watchEventKeys: [ ...DEFAULT_WATCH_EVENT_KEYS, 'onDidChangeModelDecorations' - ] + ], + anytime(type, ...args) { + if (isWhatArgs(type, 'onDidChangeModelDecorations', args)) { + const [e] = args + if (!e.affectsMinimap) { + throw StopThisTimeError.instance + } + } + } }) const editor: Editor = { diff --git a/core/src/utils/makeProvider.ts b/core/src/utils/makeProvider.ts index 671b2d66..3bdd91b4 100644 --- a/core/src/utils/makeProvider.ts +++ b/core/src/utils/makeProvider.ts @@ -8,13 +8,34 @@ export type Provider = ( opts: { mountInitValue: T; isCancel: { value: boolean } }, ) => Promise<() => void> | (() => void) +export type WatchEventKeys = Extract + export const DEFAULT_WATCH_EVENT_KEYS = [ 'onDidChangeModel', 'onDidChangeModelContent', 'onDidFocusEditorWidget' -] as const +] as WatchEventKeys[] + +export class StopThisTimeError extends Error { + constructor() { + super('stop this time') + } + static instance = new StopThisTimeError() +} -export function makeProvider( +export function isWhatArgs< + T extends WatchEventKeys | null +>(lt: string | null, rt: T, args: unknown[]): args is ( + T extends keyof IStandaloneCodeEditor + ? Parameters< + Parameters[0] + > + : [] +) { + return lt === rt +} + +export function makeProvider( mount: ( editor: IStandaloneCodeEditor, monaco: typeof monacoEditor @@ -25,13 +46,13 @@ export function makeProvider( monaco: typeof monacoEditor ) => void, opts?: { - anytime?: () => void - watchEventKeys?: Extract[] + anytime?: (type: Keys | null, ...args: unknown[]) => void + watchEventKeys?: Keys[] } ) { const { anytime, - watchEventKeys = DEFAULT_WATCH_EVENT_KEYS + watchEventKeys = DEFAULT_WATCH_EVENT_KEYS as Keys[] } = opts ?? {} return ( monaco: typeof monacoEditor, @@ -45,8 +66,15 @@ export function makeProvider( let isCancel = { value: false } let prevDispose: (() => void) | undefined = undefined - async function callback() { - anytime?.() + async function callback(type: Keys | null, ...args: unknown[]) { + try { + anytime?.(type, ...args) + } catch (e) { + if (e instanceof StopThisTimeError) { + return + } + console.error(e) + } const model = editor.getModel() if (!model) return @@ -62,9 +90,9 @@ export function makeProvider( prevDispose?.() prevDispose = await provider(model, { mountInitValue, isCancel }) } - callback().catch(console.error) + callback(null).catch(console.error) return watchEventKeys - .map(key => editor[key](callback).dispose) + .map(key => editor[key](callback.bind(null, key)).dispose) .reduce((acc, cur) => () => (acc(), cur()), () => { clear(editor, mountInitValue, monaco) prevDispose?.() From 6c1649bb6087f0970d7cf8020191fc2c7d5bf5a2 Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Sun, 3 Sep 2023 21:53:15 +0800 Subject: [PATCH 13/24] feat(core): better performance to get namespaces --- core/src/plugins/typescript/index.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/core/src/plugins/typescript/index.ts b/core/src/plugins/typescript/index.ts index a4150756..beb5521f 100644 --- a/core/src/plugins/typescript/index.ts +++ b/core/src/plugins/typescript/index.ts @@ -323,6 +323,10 @@ const editor: Editor = { ...infer T, infer _Ignore ] ? T : never const providerDefaultParams: ProviderDefaultParams = [monaco, editor, { languages: ['javascript', 'typescript'] }] + const modelNamespacesCache = new Map, + ]>() return [ addDecorationProvider( ...providerDefaultParams, async (model, { mountInitValue: { @@ -425,7 +429,12 @@ const editor: Editor = { addGlyph, removeGlyph } }) => { const ts = await lazyTS - const namespaces = getNamespaces(ts, editor.getValue()) + const uri = model.uri.toString() + const cache = modelNamespacesCache.get(uri) + const namespaces = cache?.[1] ?? modelNamespacesCache.set(uri, [ + model.getValue(), + getNamespaces(ts, model.getValue()) + ]).get(uri)![1] const visibleRanges = editor.getVisibleRanges() const gids: string[] = [] From 2f0a1e5b8c0416421c0fd6728b33fba6f58a72e3 Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Sun, 3 Sep 2023 22:39:45 +0800 Subject: [PATCH 14/24] refactor(core): abstract as single file for glyphProvider --- core/src/plugins/typescript/index.ts | 141 +--------------- .../typescript/providers/GlyphProvider.ts | 156 ++++++++++++++++++ core/src/plugins/typescript/utils/index.ts | 1 + 3 files changed, 160 insertions(+), 138 deletions(-) create mode 100644 core/src/plugins/typescript/providers/GlyphProvider.ts diff --git a/core/src/plugins/typescript/index.ts b/core/src/plugins/typescript/index.ts index beb5521f..d8c41cc5 100644 --- a/core/src/plugins/typescript/index.ts +++ b/core/src/plugins/typescript/index.ts @@ -3,12 +3,8 @@ import './index.scss' import { useEffect, useMemo } from 'react' import type { Editor } from '@power-playground/core' import { - classnames, - DEFAULT_WATCH_EVENT_KEYS, - isWhatArgs, makeProvider, - messenger, - StopThisTimeError + messenger } from '@power-playground/core' import { getDefaultStore, useAtom } from 'jotai' import type * as monacoEditor from 'monaco-editor' @@ -17,10 +13,10 @@ import { mergeAll, mergeDeepLeft } from 'ramda' import { useDocumentEventListener } from '../../hooks/useDocumentEventListener' import { definePlugin } from '..' +import glyphProvider from './providers/GlyphProvider.ts' import { Setting } from './statusbar/Setting' import { Versions } from './statusbar/Versions' import { Langs } from './topbar/Langs' -import { getNamespaces } from './utils/getNamespaces' import { compilerOptionsAtom, extraFilesAtom, extraModulesAtom } from './atoms' import { resolveModules } from './modules' import { use } from './use' @@ -123,75 +119,6 @@ const addDecorationProvider = makeProvider(editor => { } }) -const GLYPH_PREFIX = 'ppd-plugins-typescript-glyph-margin' -const createGlyph = ( - monaco: typeof monacoEditor, - editor: monacoEditor.editor.IStandaloneCodeEditor, - line: number, content: string, - eleResolver?: (ele: HTMLDivElement) => void -): monacoEditor.editor.IGlyphMarginWidget => ({ - getDomNode() { - const domNode = document.createElement('div') - domNode.innerHTML = content - eleResolver?.(domNode) - return domNode - }, - getPosition() { - return { - lane: monaco.editor.GlyphMarginLane.Right, - zIndex: 100, - range: new monaco.Range(line, 1, line, 1) - } - }, - getId: () => `${GLYPH_PREFIX} ${GLYPH_PREFIX}__${line}` -}) - -const addGlyphProvider = makeProvider((editor, monaco) => { - const glyphMarginWidgets: monacoEditor.editor.IGlyphMarginWidget[] = [] - - return { - glyphMarginWidgets, - addGlyph: (line: number, content: string, eleResolver?: (ele: HTMLDivElement) => void) => { - const model = editor.getModel() - if (!model) throw new Error('model not found') - - const widget = createGlyph(monaco, editor, line, content, eleResolver) - editor.addGlyphMarginWidget(widget) - glyphMarginWidgets.push(widget) - editor.updateOptions({ glyphMargin: true }) - return widget.getId() - }, - removeGlyph: (id: string) => { - const index = glyphMarginWidgets.findIndex(widget => widget.getId() === id) - if (index !== -1) { - editor.removeGlyphMarginWidget(glyphMarginWidgets[index]) - glyphMarginWidgets.splice(index, 1) - } - if (glyphMarginWidgets.length === 0) { - editor.updateOptions({ glyphMargin: false }) - } - } - } -}, (editor, { - glyphMarginWidgets -}) => { - editor.updateOptions({ glyphMargin: false }) - glyphMarginWidgets.forEach(widget => editor.removeGlyphMarginWidget(widget)) -}, { - watchEventKeys: [ - ...DEFAULT_WATCH_EVENT_KEYS, - 'onDidChangeModelDecorations' - ], - anytime(type, ...args) { - if (isWhatArgs(type, 'onDidChangeModelDecorations', args)) { - const [e] = args - if (!e.affectsMinimap) { - throw StopThisTimeError.instance - } - } - } -}) - const editor: Editor = { use, useShare({ @@ -323,10 +250,6 @@ const editor: Editor = { ...infer T, infer _Ignore ] ? T : never const providerDefaultParams: ProviderDefaultParams = [monaco, editor, { languages: ['javascript', 'typescript'] }] - const modelNamespacesCache = new Map, - ]>() return [ addDecorationProvider( ...providerDefaultParams, async (model, { mountInitValue: { @@ -424,65 +347,7 @@ const editor: Editor = { } return () => void 0 }), - addGlyphProvider( - ...providerDefaultParams, async (model, { mountInitValue: { - addGlyph, removeGlyph - } }) => { - const ts = await lazyTS - const uri = model.uri.toString() - const cache = modelNamespacesCache.get(uri) - const namespaces = cache?.[1] ?? modelNamespacesCache.set(uri, [ - model.getValue(), - getNamespaces(ts, model.getValue()) - ]).get(uri)![1] - const visibleRanges = editor.getVisibleRanges() - - const gids: string[] = [] - Object.entries(namespaces) - .filter(([name]) => ['describe', 'it'].some(n => name.startsWith(n))) - .map(([name, ns]) => { - type FlatItem = [number, string, ReturnType['describe'][0]] - return ns - .reduce((acc, namespace) => { - const realLine = model.getPositionAt(namespace.top.pos).lineNumber - let line = realLine - let inFold = false - let prevVisibleEndLineNumber = 1 - visibleRanges.forEach(({ startLineNumber, endLineNumber }) => { - if (realLine >= endLineNumber && realLine <= startLineNumber) { - inFold = true - return - } - if (realLine <= startLineNumber) - return - - const offset = startLineNumber - prevVisibleEndLineNumber - line -= offset === 0 ? 0 : offset - 1 - prevVisibleEndLineNumber = endLineNumber - }) - if (inFold) return acc - return acc.concat([ - [line, name, namespace] - ]) - }, [] as FlatItem[]) - .flat() as FlatItem - }) - .forEach(([line, name, namespace]) => { - const isDescribe = name.startsWith('describe') - gids.push(addGlyph(line, ``, ele => { - ele.onclick = () => { - messenger.then(m => m.display('warning', 'Running test is not supported yet')) - // ele.innerHTML = `` - ele.innerHTML = `` - } - })) - }) - return () => gids.forEach(id => removeGlyph(id)) - }) + glyphProvider(editor, monaco, lazyTS) ] }, topbar: [Langs], diff --git a/core/src/plugins/typescript/providers/GlyphProvider.ts b/core/src/plugins/typescript/providers/GlyphProvider.ts new file mode 100644 index 00000000..87935418 --- /dev/null +++ b/core/src/plugins/typescript/providers/GlyphProvider.ts @@ -0,0 +1,156 @@ +import type { + IStandaloneCodeEditor +} from '@power-playground/core' +import { + classnames, + DEFAULT_WATCH_EVENT_KEYS, + isWhatArgs, + makeProvider, messenger, + StopThisTimeError +} from '@power-playground/core' +import type * as monacoEditor from 'monaco-editor' + +import { getNamespaces } from '../utils' + +const GLYPH_PREFIX = 'ppd-plugins-typescript-glyph-margin' +const createGlyph = ( + monaco: typeof monacoEditor, + editor: monacoEditor.editor.IStandaloneCodeEditor, + line: number, content: string, + eleResolver?: (ele: HTMLDivElement) => void +): monacoEditor.editor.IGlyphMarginWidget => ({ + getDomNode() { + const domNode = document.createElement('div') + domNode.innerHTML = content + eleResolver?.(domNode) + return domNode + }, + getPosition() { + return { + lane: monaco.editor.GlyphMarginLane.Right, + zIndex: 100, + range: new monaco.Range(line, 1, line, 1) + } + }, + getId: () => `${GLYPH_PREFIX} ${GLYPH_PREFIX}__${line}` +}) + +const addGlyphProvider = makeProvider((editor, monaco) => { + const glyphMarginWidgets: monacoEditor.editor.IGlyphMarginWidget[] = [] + + return { + glyphMarginWidgets, + addGlyph: (line: number, content: string, eleResolver?: (ele: HTMLDivElement) => void) => { + const model = editor.getModel() + if (!model) throw new Error('model not found') + + const widget = createGlyph(monaco, editor, line, content, eleResolver) + editor.addGlyphMarginWidget(widget) + glyphMarginWidgets.push(widget) + editor.updateOptions({ glyphMargin: true }) + return widget.getId() + }, + removeGlyph: (id: string) => { + const index = glyphMarginWidgets.findIndex(widget => widget.getId() === id) + if (index !== -1) { + editor.removeGlyphMarginWidget(glyphMarginWidgets[index]) + glyphMarginWidgets.splice(index, 1) + } + if (glyphMarginWidgets.length === 0) { + editor.updateOptions({ glyphMargin: false }) + } + } + } +}, (editor, { + glyphMarginWidgets +}) => { + editor.updateOptions({ glyphMargin: false }) + glyphMarginWidgets.forEach(widget => editor.removeGlyphMarginWidget(widget)) +}, { + watchEventKeys: [ + ...DEFAULT_WATCH_EVENT_KEYS, + 'onDidChangeModelDecorations' + ], + anytime(type, ...args) { + if (isWhatArgs(type, 'onDidChangeModelDecorations', args)) { + const [e] = args + if (!e.affectsMinimap) { + throw StopThisTimeError.instance + } + } + } +}) + +export default ( + editor: IStandaloneCodeEditor, + monaco: typeof monacoEditor, + lazyTS: Promise +) => { + type ProviderDefaultParams = Parameters> extends [ + ...infer T, infer _Ignore + ] ? T : never + const providerDefaultParams: ProviderDefaultParams = [monaco, editor, { languages: ['javascript', 'typescript'] }] + const modelNamespacesCache = new Map, + ]>() + return addGlyphProvider( + ...providerDefaultParams, async (model, { mountInitValue: { + addGlyph, removeGlyph + } }) => { + const ts = await lazyTS + const uri = model.uri.toString() + const cache = modelNamespacesCache.get(uri) + const namespaces = cache?.[1] ?? modelNamespacesCache.set(uri, [ + model.getValue(), + getNamespaces(ts, model.getValue()) + ]).get(uri)![1] + const visibleRanges = editor.getVisibleRanges() + + const gids: string[] = [] + Object.entries(namespaces) + .filter(([name]) => ['describe', 'it'].some(n => name.startsWith(n))) + .map(([name, ns]) => { + type FlatItem = [number, string, ReturnType['describe'][0]] + return ns + .reduce((acc, namespace) => { + const realLine = model.getPositionAt(namespace.top.pos).lineNumber + let line = realLine + let inFold = false + let prevVisibleEndLineNumber = 1 + visibleRanges.forEach(({ startLineNumber, endLineNumber }) => { + if (realLine >= endLineNumber && realLine <= startLineNumber) { + inFold = true + return + } + if (realLine <= startLineNumber) + return + + const offset = startLineNumber - prevVisibleEndLineNumber + line -= offset === 0 ? 0 : offset - 1 + prevVisibleEndLineNumber = endLineNumber + }) + if (inFold) return acc + return acc.concat([ + [line, name, namespace] + ]) + }, [] as FlatItem[]) + .flat() as FlatItem + }) + .forEach(([line, name, namespace]) => { + const isDescribe = name.startsWith('describe') + gids.push(addGlyph(line, ``, ele => { + ele.onclick = () => { + messenger.then(m => m.display('warning', 'Running test is not supported yet')) + // ele.innerHTML = `` + ele.innerHTML = `` + } + })) + }) + return () => gids.forEach(id => removeGlyph(id)) + }) +} diff --git a/core/src/plugins/typescript/utils/index.ts b/core/src/plugins/typescript/utils/index.ts index aca708b8..cc62cae5 100644 --- a/core/src/plugins/typescript/utils/index.ts +++ b/core/src/plugins/typescript/utils/index.ts @@ -1,3 +1,4 @@ export * from './getDTName' +export * from './getNamespaces' export * from './getReferencesForModule' export * from './mapModuleNameToModule' From fe484f59c128fc2ebbb068a3d4baf917e4308bff Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Sun, 3 Sep 2023 22:52:53 +0800 Subject: [PATCH 15/24] refactor(core): rename `createProviderMaker` function --- core/src/plugins/typescript/index.ts | 10 ++++++---- .../typescript/providers/GlyphProvider.ts | 17 +++++++++-------- .../{makeProvider.ts => createProviderMaker.ts} | 2 +- core/src/utils/index.ts | 2 +- 4 files changed, 17 insertions(+), 14 deletions(-) rename core/src/utils/{makeProvider.ts => createProviderMaker.ts} (97%) diff --git a/core/src/plugins/typescript/index.ts b/core/src/plugins/typescript/index.ts index d8c41cc5..669ca5ee 100644 --- a/core/src/plugins/typescript/index.ts +++ b/core/src/plugins/typescript/index.ts @@ -3,7 +3,6 @@ import './index.scss' import { useEffect, useMemo } from 'react' import type { Editor } from '@power-playground/core' import { - makeProvider, messenger } from '@power-playground/core' import { getDefaultStore, useAtom } from 'jotai' @@ -11,6 +10,7 @@ import type * as monacoEditor from 'monaco-editor' import { mergeAll, mergeDeepLeft } from 'ramda' import { useDocumentEventListener } from '../../hooks/useDocumentEventListener' +import { createProviderMaker } from '../../utils' import { definePlugin } from '..' import glyphProvider from './providers/GlyphProvider.ts' @@ -20,7 +20,9 @@ import { Langs } from './topbar/Langs' import { compilerOptionsAtom, extraFilesAtom, extraModulesAtom } from './atoms' import { resolveModules } from './modules' import { use } from './use' -import { getReferencesForModule, mapModuleNameToModule } from './utils' +import { + getReferencesForModule, mapModuleNameToModule +} from './utils' export interface ExtraFile { content: string @@ -75,7 +77,7 @@ if (import.meta.hot) { const modelDecorationIdsSymbol = '_modelDecorationIds' -const addDecorationProvider = makeProvider(editor => { +const addDecorationProvider = createProviderMaker(editor => { const decorationsCollection = editor.createDecorationsCollection() const modelDecorationIdsConfigurableEditor = editor as unknown as { @@ -246,7 +248,7 @@ const editor: Editor = { } }) - type ProviderDefaultParams = Parameters> extends [ + type ProviderDefaultParams = Parameters> extends [ ...infer T, infer _Ignore ] ? T : never const providerDefaultParams: ProviderDefaultParams = [monaco, editor, { languages: ['javascript', 'typescript'] }] diff --git a/core/src/plugins/typescript/providers/GlyphProvider.ts b/core/src/plugins/typescript/providers/GlyphProvider.ts index 87935418..dd39530b 100644 --- a/core/src/plugins/typescript/providers/GlyphProvider.ts +++ b/core/src/plugins/typescript/providers/GlyphProvider.ts @@ -2,14 +2,15 @@ import type { IStandaloneCodeEditor } from '@power-playground/core' import { - classnames, - DEFAULT_WATCH_EVENT_KEYS, - isWhatArgs, - makeProvider, messenger, - StopThisTimeError + classnames, messenger } from '@power-playground/core' import type * as monacoEditor from 'monaco-editor' +import { + createProviderMaker, DEFAULT_WATCH_EVENT_KEYS, + isWhatArgs, + StopThisTimeError +} from '../../../utils' import { getNamespaces } from '../utils' const GLYPH_PREFIX = 'ppd-plugins-typescript-glyph-margin' @@ -35,7 +36,7 @@ const createGlyph = ( getId: () => `${GLYPH_PREFIX} ${GLYPH_PREFIX}__${line}` }) -const addGlyphProvider = makeProvider((editor, monaco) => { +const glyphProviderMaker = createProviderMaker((editor, monaco) => { const glyphMarginWidgets: monacoEditor.editor.IGlyphMarginWidget[] = [] return { @@ -86,7 +87,7 @@ export default ( monaco: typeof monacoEditor, lazyTS: Promise ) => { - type ProviderDefaultParams = Parameters> extends [ + type ProviderDefaultParams = Parameters> extends [ ...infer T, infer _Ignore ] ? T : never const providerDefaultParams: ProviderDefaultParams = [monaco, editor, { languages: ['javascript', 'typescript'] }] @@ -94,7 +95,7 @@ export default ( content: string, namespaces: ReturnType, ]>() - return addGlyphProvider( + return glyphProviderMaker( ...providerDefaultParams, async (model, { mountInitValue: { addGlyph, removeGlyph } }) => { diff --git a/core/src/utils/makeProvider.ts b/core/src/utils/createProviderMaker.ts similarity index 97% rename from core/src/utils/makeProvider.ts rename to core/src/utils/createProviderMaker.ts index 3bdd91b4..e7ed158d 100644 --- a/core/src/utils/makeProvider.ts +++ b/core/src/utils/createProviderMaker.ts @@ -35,7 +35,7 @@ export function isWhatArgs< return lt === rt } -export function makeProvider( +export function createProviderMaker( mount: ( editor: IStandaloneCodeEditor, monaco: typeof monacoEditor diff --git a/core/src/utils/index.ts b/core/src/utils/index.ts index 7d5e3692..8e3d51a8 100644 --- a/core/src/utils/index.ts +++ b/core/src/utils/index.ts @@ -2,6 +2,6 @@ export * from './asyncDebounce' export * from './classnames' export * from './copyToClipboard' export * from './isMacOS' -export * from './makeProvider' +export * from './createProviderMaker.ts' export * from './scrollIntoViewIfNeeded' export * from './typescriptVersionMeta' From f3a10c4449cb5c29c188d5b4ac54b6faf9b418f8 Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Sun, 3 Sep 2023 22:59:31 +0800 Subject: [PATCH 16/24] refactor(core): simplify code --- core/src/plugins/typescript/index.ts | 11 ++++------- .../src/plugins/typescript/providers/GlyphProvider.ts | 6 +----- core/src/utils/createProviderMaker.ts | 2 ++ 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/core/src/plugins/typescript/index.ts b/core/src/plugins/typescript/index.ts index 669ca5ee..21f2c923 100644 --- a/core/src/plugins/typescript/index.ts +++ b/core/src/plugins/typescript/index.ts @@ -1,7 +1,9 @@ import './index.scss' import { useEffect, useMemo } from 'react' -import type { Editor } from '@power-playground/core' +import type { + Editor +} from '@power-playground/core' import { messenger } from '@power-playground/core' @@ -247,14 +249,9 @@ const editor: Editor = { resolve(typescript) } }) - - type ProviderDefaultParams = Parameters> extends [ - ...infer T, infer _Ignore - ] ? T : never - const providerDefaultParams: ProviderDefaultParams = [monaco, editor, { languages: ['javascript', 'typescript'] }] return [ addDecorationProvider( - ...providerDefaultParams, async (model, { mountInitValue: { + monaco, editor, { languages: ['javascript', 'typescript'] }, async (model, { mountInitValue: { modelDecorationIds, decorationsCollection, dependencyLoadErrorReason diff --git a/core/src/plugins/typescript/providers/GlyphProvider.ts b/core/src/plugins/typescript/providers/GlyphProvider.ts index dd39530b..23f37ac9 100644 --- a/core/src/plugins/typescript/providers/GlyphProvider.ts +++ b/core/src/plugins/typescript/providers/GlyphProvider.ts @@ -87,16 +87,12 @@ export default ( monaco: typeof monacoEditor, lazyTS: Promise ) => { - type ProviderDefaultParams = Parameters> extends [ - ...infer T, infer _Ignore - ] ? T : never - const providerDefaultParams: ProviderDefaultParams = [monaco, editor, { languages: ['javascript', 'typescript'] }] const modelNamespacesCache = new Map, ]>() return glyphProviderMaker( - ...providerDefaultParams, async (model, { mountInitValue: { + monaco, editor, { languages: ['javascript', 'typescript'] }, async (model, { mountInitValue: { addGlyph, removeGlyph } }) => { const ts = await lazyTS diff --git a/core/src/utils/createProviderMaker.ts b/core/src/utils/createProviderMaker.ts index e7ed158d..6da35234 100644 --- a/core/src/utils/createProviderMaker.ts +++ b/core/src/utils/createProviderMaker.ts @@ -35,6 +35,8 @@ export function isWhatArgs< return lt === rt } +export type ProviderMaker = ReturnType + export function createProviderMaker( mount: ( editor: IStandaloneCodeEditor, From ead4bd1afd361f1e99e159a3c7c1fb119e39d19f Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Sun, 3 Sep 2023 23:07:26 +0800 Subject: [PATCH 17/24] refactor(core): abstract decoration provider --- core/src/plugins/typescript/index.ts | 178 +---------------- .../providers/DecorationProvider.ts | 186 ++++++++++++++++++ 2 files changed, 190 insertions(+), 174 deletions(-) create mode 100644 core/src/plugins/typescript/providers/DecorationProvider.ts diff --git a/core/src/plugins/typescript/index.ts b/core/src/plugins/typescript/index.ts index 21f2c923..23832010 100644 --- a/core/src/plugins/typescript/index.ts +++ b/core/src/plugins/typescript/index.ts @@ -9,22 +9,18 @@ import { } from '@power-playground/core' import { getDefaultStore, useAtom } from 'jotai' import type * as monacoEditor from 'monaco-editor' -import { mergeAll, mergeDeepLeft } from 'ramda' +import { mergeAll } from 'ramda' import { useDocumentEventListener } from '../../hooks/useDocumentEventListener' -import { createProviderMaker } from '../../utils' import { definePlugin } from '..' -import glyphProvider from './providers/GlyphProvider.ts' +import decorationProvider, { referencesPromise } from './providers/DecorationProvider' +import glyphProvider from './providers/GlyphProvider' import { Setting } from './statusbar/Setting' import { Versions } from './statusbar/Versions' import { Langs } from './topbar/Langs' import { compilerOptionsAtom, extraFilesAtom, extraModulesAtom } from './atoms' -import { resolveModules } from './modules' import { use } from './use' -import { - getReferencesForModule, mapModuleNameToModule -} from './utils' export interface ExtraFile { content: string @@ -52,77 +48,6 @@ export interface TypeScriptPluginX { } } -function promiseStatus(promise: Promise) { - let status = 'pending' - return Promise.race([ - promise.then(() => status = 'fulfilled'), - promise.catch(() => status = 'rejected'), - new Promise(resolve => setTimeout(() => resolve(status), 0)) - ]) -} - -type RefForModule = ReturnType -let resolveReferences: (value: RefForModule) => void = () => void 0 -// TODO refactor as Map to reveal the promise by filePath -let referencesPromise = new Promise(re => { - resolveReferences = re -}) -if (import.meta.hot) { - const hotReferencesPromise = import.meta.hot.data['ppd:typescript:referencesPromise'] - hotReferencesPromise && (referencesPromise = hotReferencesPromise) -} -let prevRefs: RefForModule = [] -if (import.meta.hot) { - const hotPrevRefs = import.meta.hot.data['ppd:typescript:prevRefs'] - hotPrevRefs && (prevRefs = hotPrevRefs) -} - -const modelDecorationIdsSymbol = '_modelDecorationIds' - -const addDecorationProvider = createProviderMaker(editor => { - const decorationsCollection = editor.createDecorationsCollection() - - const modelDecorationIdsConfigurableEditor = editor as unknown as { - [modelDecorationIdsSymbol]?: Map - } - const modelDecorationIds = modelDecorationIdsConfigurableEditor[modelDecorationIdsSymbol] - ?? (modelDecorationIdsConfigurableEditor[modelDecorationIdsSymbol] = new Map()) - - let dependencyLoadErrorReason: Record - if (import.meta.hot) { - const hotDependencyLoadErrorReason = import.meta.hot.data['ppd:typescript:dependencyLoadErrorReason'] - hotDependencyLoadErrorReason - ? (dependencyLoadErrorReason = hotDependencyLoadErrorReason) - : (dependencyLoadErrorReason = import.meta.hot.data['ppd:typescript:dependencyLoadErrorReason'] = {}) - } else { - dependencyLoadErrorReason = {} - } - - return { decorationsCollection, modelDecorationIds, dependencyLoadErrorReason } -}, (editor, { - decorationsCollection, - modelDecorationIds -}) => { - const uri = editor.getModel()?.uri.toString() - if (!uri) return - - const ids = modelDecorationIds.get(uri) - if (!ids) return - - editor.removeDecorations(ids) - modelDecorationIds.delete(uri) - decorationsCollection.clear() - if (import.meta.hot) { - import.meta.hot.data['ppd:typescript:dependencyLoadErrorReason'] = {} - } -}, { - anytime: async () => { - if (await promiseStatus(referencesPromise) === 'fulfilled') { - referencesPromise = new Promise(re => resolveReferences = re) - } - } -}) - const editor: Editor = { use, useShare({ @@ -250,102 +175,7 @@ const editor: Editor = { } }) return [ - addDecorationProvider( - monaco, editor, { languages: ['javascript', 'typescript'] }, async (model, { mountInitValue: { - modelDecorationIds, - decorationsCollection, - dependencyLoadErrorReason - }, isCancel }) => { - const uri = model.uri.toString() - const ids = modelDecorationIds.get(uri) - ?? modelDecorationIds.set(uri, []).get(uri)! - - const content = model.getValue() - const ts = await lazyTS - - const extraModules = store.get(extraModulesAtom).reduce((acc, { filePath }) => { - const name = /((?:@[^/]*\/)?[^/]+)/.exec(filePath)?.[1] - if (name && !acc.includes(name)) { - acc.push(name) - } - return acc - }, [] as string[]) - const references = getReferencesForModule(ts, content) - .filter(ref => !ref.module.startsWith('.')) - .filter(ref => !extraModules.includes(ref.module)) - .map(ref => ({ - ...ref, - module: mapModuleNameToModule(ref.module) - })) - .reduce((acc, cur) => { - const index = acc.findIndex(({ module }) => module === cur.module) - if (index === -1) { - acc.push(cur) - } - return acc - }, [] as RefForModule) - let resolveModulesFulfilled = () => void 0 - resolveModules(monaco, prevRefs, references, { - onDepLoadError({ depName, error }) { - dependencyLoadErrorReason[depName] = `⚠️ ${error.message}` - } - }) - .then(() => { - if (isCancel.value) return - resolveModulesFulfilled() - }) - prevRefs = references - if (import.meta.hot) { - import.meta.hot.data['ppd:typescript:prevRefs'] = prevRefs - } - resolveReferences(references) - if (import.meta.hot) { - import.meta.hot.data['ppd:typescript:referencesPromise'] = referencesPromise - } - editor.removeDecorations(ids) - const loadingDecorations: ( - & { loadedVersion: string, loadModule: string } - & monacoEditor.editor.IModelDeltaDecoration - )[] = references.map(ref => { - const [start, end] = ref.position - const startP = model.getPositionAt(start) - const endP = model.getPositionAt(end) - const range = new monaco.Range( - startP.lineNumber, - startP.column + 1, - endP.lineNumber, - endP.column + 1 - ) - const inlineClassName = `ts__button-decoration ts__button-decoration__position-${start}__${end}` - return { - loadModule: ref.module, - loadedVersion: ref.version ?? 'latest', - range, - options: { - isWholeLine: true, - after: { - content: '⚡️ Downloading...', - inlineClassName - } - } - } - }) - const newIds = decorationsCollection.set(loadingDecorations) - modelDecorationIds.set(uri, newIds) - - resolveModulesFulfilled = () => { - editor.removeDecorations(newIds) - const loadedDecorations = loadingDecorations.map(d => { - const error = dependencyLoadErrorReason[`${d.loadModule}@${d.loadedVersion}`] - return mergeDeepLeft({ - options: { after: { content: error ?? `@${d.loadedVersion}` } } - }, d) - }) - const loadedIds = decorationsCollection.set(loadedDecorations) - modelDecorationIds.set(uri, loadedIds) - } - return () => void 0 - }), + decorationProvider(editor, monaco, lazyTS), glyphProvider(editor, monaco, lazyTS) ] }, diff --git a/core/src/plugins/typescript/providers/DecorationProvider.ts b/core/src/plugins/typescript/providers/DecorationProvider.ts new file mode 100644 index 00000000..797c0040 --- /dev/null +++ b/core/src/plugins/typescript/providers/DecorationProvider.ts @@ -0,0 +1,186 @@ +import type { IStandaloneCodeEditor } from '@power-playground/core' +import { getDefaultStore } from 'jotai' +import type * as monacoEditor from 'monaco-editor' +import { mergeDeepLeft } from 'ramda' + +import { + createProviderMaker +} from '../../../utils' +import { extraModulesAtom } from '../atoms' +import { resolveModules } from '../modules' +import { getReferencesForModule, mapModuleNameToModule } from '../utils' + +const store = getDefaultStore() + +type RefForModule = ReturnType +let resolveReferences: (value: RefForModule) => void = () => void 0 +// TODO refactor as Map to reveal the promise by filePath +export let referencesPromise = new Promise(re => { + resolveReferences = re +}) +if (import.meta.hot) { + const hotReferencesPromise = import.meta.hot.data['ppd:typescript:referencesPromise'] + hotReferencesPromise && (referencesPromise = hotReferencesPromise) +} +let prevRefs: RefForModule = [] +if (import.meta.hot) { + const hotPrevRefs = import.meta.hot.data['ppd:typescript:prevRefs'] + hotPrevRefs && (prevRefs = hotPrevRefs) +} + +// TODO make utility +function promiseStatus(promise: Promise) { + let status = 'pending' + return Promise.race([ + promise.then(() => status = 'fulfilled'), + promise.catch(() => status = 'rejected'), + new Promise(resolve => setTimeout(() => resolve(status), 0)) + ]) +} + +const modelDecorationIdsSymbol = '_modelDecorationIds' + +const addDecorationProvider = createProviderMaker(editor => { + const decorationsCollection = editor.createDecorationsCollection() + + const modelDecorationIdsConfigurableEditor = editor as unknown as { + [modelDecorationIdsSymbol]?: Map + } + const modelDecorationIds = modelDecorationIdsConfigurableEditor[modelDecorationIdsSymbol] + ?? (modelDecorationIdsConfigurableEditor[modelDecorationIdsSymbol] = new Map()) + + let dependencyLoadErrorReason: Record + if (import.meta.hot) { + const hotDependencyLoadErrorReason = import.meta.hot.data['ppd:typescript:dependencyLoadErrorReason'] + hotDependencyLoadErrorReason + ? (dependencyLoadErrorReason = hotDependencyLoadErrorReason) + : (dependencyLoadErrorReason = import.meta.hot.data['ppd:typescript:dependencyLoadErrorReason'] = {}) + } else { + dependencyLoadErrorReason = {} + } + + return { decorationsCollection, modelDecorationIds, dependencyLoadErrorReason } +}, (editor, { + decorationsCollection, + modelDecorationIds +}) => { + const uri = editor.getModel()?.uri.toString() + if (!uri) return + + const ids = modelDecorationIds.get(uri) + if (!ids) return + + editor.removeDecorations(ids) + modelDecorationIds.delete(uri) + decorationsCollection.clear() + if (import.meta.hot) { + import.meta.hot.data['ppd:typescript:dependencyLoadErrorReason'] = {} + } +}, { + anytime: async () => { + if (await promiseStatus(referencesPromise) === 'fulfilled') { + referencesPromise = new Promise(re => resolveReferences = re) + } + } +}) + +export default ( + editor: IStandaloneCodeEditor, + monaco: typeof monacoEditor, + lazyTS: Promise +) => addDecorationProvider( + monaco, editor, { languages: ['javascript', 'typescript'] }, async (model, { mountInitValue: { + modelDecorationIds, + decorationsCollection, + dependencyLoadErrorReason + }, isCancel }) => { + const uri = model.uri.toString() + const ids = modelDecorationIds.get(uri) + ?? modelDecorationIds.set(uri, []).get(uri)! + + const content = model.getValue() + const ts = await lazyTS + + const extraModules = store.get(extraModulesAtom).reduce((acc, { filePath }) => { + const name = /((?:@[^/]*\/)?[^/]+)/.exec(filePath)?.[1] + if (name && !acc.includes(name)) { + acc.push(name) + } + return acc + }, [] as string[]) + const references = getReferencesForModule(ts, content) + .filter(ref => !ref.module.startsWith('.')) + .filter(ref => !extraModules.includes(ref.module)) + .map(ref => ({ + ...ref, + module: mapModuleNameToModule(ref.module) + })) + .reduce((acc, cur) => { + const index = acc.findIndex(({ module }) => module === cur.module) + if (index === -1) { + acc.push(cur) + } + return acc + }, [] as RefForModule) + let resolveModulesFulfilled = () => void 0 + resolveModules(monaco, prevRefs, references, { + onDepLoadError({ depName, error }) { + dependencyLoadErrorReason[depName] = `⚠️ ${error.message}` + } + }) + .then(() => { + if (isCancel.value) return + resolveModulesFulfilled() + }) + prevRefs = references + if (import.meta.hot) { + import.meta.hot.data['ppd:typescript:prevRefs'] = prevRefs + } + resolveReferences(references) + if (import.meta.hot) { + import.meta.hot.data['ppd:typescript:referencesPromise'] = referencesPromise + } + editor.removeDecorations(ids) + const loadingDecorations: ( + & { loadedVersion: string, loadModule: string } + & monacoEditor.editor.IModelDeltaDecoration + )[] = references.map(ref => { + const [start, end] = ref.position + const startP = model.getPositionAt(start) + const endP = model.getPositionAt(end) + const range = new monaco.Range( + startP.lineNumber, + startP.column + 1, + endP.lineNumber, + endP.column + 1 + ) + const inlineClassName = `ts__button-decoration ts__button-decoration__position-${start}__${end}` + return { + loadModule: ref.module, + loadedVersion: ref.version ?? 'latest', + range, + options: { + isWholeLine: true, + after: { + content: '⚡️ Downloading...', + inlineClassName + } + } + } + }) + const newIds = decorationsCollection.set(loadingDecorations) + modelDecorationIds.set(uri, newIds) + + resolveModulesFulfilled = () => { + editor.removeDecorations(newIds) + const loadedDecorations = loadingDecorations.map(d => { + const error = dependencyLoadErrorReason[`${d.loadModule}@${d.loadedVersion}`] + return mergeDeepLeft({ + options: { after: { content: error ?? `@${d.loadedVersion}` } } + }, d) + }) + const loadedIds = decorationsCollection.set(loadedDecorations) + modelDecorationIds.set(uri, loadedIds) + } + return () => void 0 + }) From 436340e21fce84331619a443a7155937d5c4e1a0 Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Sun, 3 Sep 2023 23:16:02 +0800 Subject: [PATCH 18/24] fix(core): namespaces cache logic is error --- .../plugins/typescript/providers/GlyphProvider.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/core/src/plugins/typescript/providers/GlyphProvider.ts b/core/src/plugins/typescript/providers/GlyphProvider.ts index 23f37ac9..f8f3b24d 100644 --- a/core/src/plugins/typescript/providers/GlyphProvider.ts +++ b/core/src/plugins/typescript/providers/GlyphProvider.ts @@ -97,11 +97,15 @@ export default ( } }) => { const ts = await lazyTS const uri = model.uri.toString() - const cache = modelNamespacesCache.get(uri) - const namespaces = cache?.[1] ?? modelNamespacesCache.set(uri, [ - model.getValue(), - getNamespaces(ts, model.getValue()) - ]).get(uri)![1] + const cot = model.getValue() + const [prevContent, prevNamespaces] = modelNamespacesCache.get(uri) ?? [] + let namespaces: ReturnType + if (prevNamespaces && prevContent === cot) { + namespaces = prevNamespaces + } else { + namespaces = getNamespaces(ts, cot) + modelNamespacesCache.set(uri, [cot, namespaces]) + } const visibleRanges = editor.getVisibleRanges() const gids: string[] = [] From 7058bccc1a666de8ba0f0d568634a5dab3f5ef9f Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Sun, 3 Sep 2023 23:48:33 +0800 Subject: [PATCH 19/24] feat(core): clean dom and better animation for the popover component --- core/src/components/base/Popover.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/core/src/components/base/Popover.tsx b/core/src/components/base/Popover.tsx index fd097cbb..a86d9a96 100644 --- a/core/src/components/base/Popover.tsx +++ b/core/src/components/base/Popover.tsx @@ -4,6 +4,7 @@ import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useSta import { createPortal } from 'react-dom' import type { Placement } from '@popperjs/core' import { createPopper } from '@popperjs/core' +import { useDebouncedValue } from 'foxact/use-debounced-value' export interface PopoverProps { tabIndex?: number @@ -102,6 +103,7 @@ export const Popover = forwardRef(function Popover(pro open: () => changeVisible(true), hide: () => changeVisible(false) }), [changeVisible]) + const display = useDebouncedValue(visible, 200) return <>
(function Popover(pro > {children}
- {createPortal(
{ if (trigger === 'hover') { changeVisible(true) From 74aede5ddb53f2f10b50e62ddf657f98fe974c5a Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Mon, 4 Sep 2023 00:22:06 +0800 Subject: [PATCH 20/24] chore: add quotes lint rule --- .eslintrc.cjs | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index aadab599..3cbca52a 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -96,6 +96,7 @@ module.exports = { }], 'simple-import-sort/exports': 'error', 'semi': ['error', 'never'], + 'quotes': ['error', 'single'], 'keyword-spacing': ['error', { before: true, after: true }], 'space-before-blocks': ['error', 'always'], 'comma-dangle': ['error', 'never'], From 86cf04d45830824e82ee24ded61b1a15cc68a5a0 Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Mon, 4 Sep 2023 00:46:01 +0800 Subject: [PATCH 21/24] feat(core): add save action and topbar --- .../{urlCache.ts => urlCache/index.ts} | 37 ++++++++++++------- core/src/plugins/urlCache/topbar/Save.tsx | 26 +++++++++++++ 2 files changed, 49 insertions(+), 14 deletions(-) rename core/src/plugins/{urlCache.ts => urlCache/index.ts} (53%) create mode 100644 core/src/plugins/urlCache/topbar/Save.tsx diff --git a/core/src/plugins/urlCache.ts b/core/src/plugins/urlCache/index.ts similarity index 53% rename from core/src/plugins/urlCache.ts rename to core/src/plugins/urlCache/index.ts index 175eb37e..a6df7395 100644 --- a/core/src/plugins/urlCache.ts +++ b/core/src/plugins/urlCache/index.ts @@ -1,7 +1,9 @@ import { useEffect, useState } from 'react' import { copyToClipboard, definePlugin, messenger } from '@power-playground/core' -import { dispatchEditState } from './history-for-local/store' +import { dispatchEditState } from '../history-for-local/store' + +import { Save } from './topbar/Save' export default definePlugin({ editor: { @@ -25,20 +27,27 @@ export default definePlugin({ } }], load(editor, monaco) { + editor.addAction({ + id: 'ppd.save', + label: 'Save code to url', + run(editor) { + const code = editor.getValue() + history.pushState(null, '', '#' + btoa(encodeURIComponent(code))) + copyToClipboard(location.href) + dispatchEditState('add', { + code, + cursor: editor.getPosition() ?? undefined + }) + messenger.then(m => m.display( + 'success', 'Saved to clipboard, you can share it to your friends!' + )) + editor.focus() + } + }) editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => { - // TODO prevent event of browser default behavior - const code = editor.getValue() - history.pushState(null, '', '#' + btoa(encodeURIComponent(code))) - copyToClipboard(location.href) - dispatchEditState('add', { - code, - cursor: editor.getPosition() ?? undefined - }) - messenger.then(m => m.display( - 'success', 'Saved to clipboard, you can share it to your friends!' - )) - editor.focus() + editor.trigger('whatever', 'ppd.save', {}) }) - } + }, + topbar: [Save] } }) diff --git a/core/src/plugins/urlCache/topbar/Save.tsx b/core/src/plugins/urlCache/topbar/Save.tsx new file mode 100644 index 00000000..d9537541 --- /dev/null +++ b/core/src/plugins/urlCache/topbar/Save.tsx @@ -0,0 +1,26 @@ +import React, { useContext } from 'react' + +import { Popover } from '../../../components/base/Popover' +import { MonacoScopeContext } from '../../../contextes/MonacoScope' +import { isMacOS } from '../../../utils' +import type { BarItemProps } from '../..' + +export const Save: React.ComponentType = () => { + const { editorInstance } = useContext(MonacoScopeContext) ?? {} + + return + Save +    + {isMacOS ? '⌘' : 'Ctrl'} + S + } + offset={[0, 6]} + > + + +} From 1953d4f02b62a502509d50f50b77514698eaadd0 Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Mon, 4 Sep 2023 01:45:36 +0800 Subject: [PATCH 22/24] feat(core): support save top bar button --- core/src/components/TopBar.scss | 1 + core/src/plugins/urlCache/atoms.ts | 5 ++++ core/src/plugins/urlCache/index.ts | 26 ++++++++++++++++++- core/src/plugins/urlCache/topbar/Save.scss | 20 +++++++++++++++ core/src/plugins/urlCache/topbar/Save.tsx | 30 +++++++++++++++++++--- 5 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 core/src/plugins/urlCache/atoms.ts create mode 100644 core/src/plugins/urlCache/topbar/Save.scss diff --git a/core/src/components/TopBar.scss b/core/src/components/TopBar.scss index 5c8920c7..513ffa63 100644 --- a/core/src/components/TopBar.scss +++ b/core/src/components/TopBar.scss @@ -19,6 +19,7 @@ div.ppd-top-bar { button { --inner-color: var(--btn-color, var(--primary)); + position: relative; display: flex; align-items: center; justify-content: center; diff --git a/core/src/plugins/urlCache/atoms.ts b/core/src/plugins/urlCache/atoms.ts new file mode 100644 index 00000000..27b2cb76 --- /dev/null +++ b/core/src/plugins/urlCache/atoms.ts @@ -0,0 +1,5 @@ +import { atom } from 'jotai' + +export const saveStatusAtom = atom<{ + [uri: string]: boolean +}>({}) diff --git a/core/src/plugins/urlCache/index.ts b/core/src/plugins/urlCache/index.ts index a6df7395..f0a0103a 100644 --- a/core/src/plugins/urlCache/index.ts +++ b/core/src/plugins/urlCache/index.ts @@ -1,9 +1,13 @@ import { useEffect, useState } from 'react' -import { copyToClipboard, definePlugin, messenger } from '@power-playground/core' +import { asyncDebounce, copyToClipboard, definePlugin, messenger } from '@power-playground/core' +import { getDefaultStore } from 'jotai' import { dispatchEditState } from '../history-for-local/store' import { Save } from './topbar/Save' +import { saveStatusAtom } from './atoms' + +const store = getDefaultStore() export default definePlugin({ editor: { @@ -27,10 +31,20 @@ export default definePlugin({ } }], load(editor, monaco) { + const uri = editor.getModel()?.uri.toString() + if (uri) { + store.set(saveStatusAtom, { ...store.get(saveStatusAtom), [uri]: true }) + } editor.addAction({ id: 'ppd.save', label: 'Save code to url', run(editor) { + const uri = editor.getModel()?.uri.toString() + if (!uri) { + messenger.then(m => m.display('error', '')) + return + } + const code = editor.getValue() history.pushState(null, '', '#' + btoa(encodeURIComponent(code))) copyToClipboard(location.href) @@ -42,11 +56,21 @@ export default definePlugin({ 'success', 'Saved to clipboard, you can share it to your friends!' )) editor.focus() + store.set(saveStatusAtom, { ...store.get(saveStatusAtom), [uri]: true }) } }) editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => { editor.trigger('whatever', 'ppd.save', {}) }) + const contentDebounce = asyncDebounce() + editor.onDidChangeModelContent(async () => { + await contentDebounce(100) + const model = editor.getModel() + if (!model) return + + const uri = model.uri.toString() + store.set(saveStatusAtom, { ...store.get(saveStatusAtom), [uri]: false }) + }) }, topbar: [Save] } diff --git a/core/src/plugins/urlCache/topbar/Save.scss b/core/src/plugins/urlCache/topbar/Save.scss new file mode 100644 index 00000000..6ae0ee63 --- /dev/null +++ b/core/src/plugins/urlCache/topbar/Save.scss @@ -0,0 +1,20 @@ +.url-cache__topbar__save { + &.saved > &-status { + background-color: #1ad04d; + } + &.save-needed > &-status { + background-color: #ff5370; + } + &.disable > &-status { + display: none; + } + &-status { + position: absolute; + bottom: 3px; + right: 3px; + width: 6px; + height: 6px; + border-radius: 50%; + transition: .1s; + } +} diff --git a/core/src/plugins/urlCache/topbar/Save.tsx b/core/src/plugins/urlCache/topbar/Save.tsx index d9537541..1f8fa791 100644 --- a/core/src/plugins/urlCache/topbar/Save.tsx +++ b/core/src/plugins/urlCache/topbar/Save.tsx @@ -1,12 +1,30 @@ -import React, { useContext } from 'react' +import './Save.scss' + +import React, { useContext, useMemo } from 'react' +import { useAtom } from 'jotai' import { Popover } from '../../../components/base/Popover' import { MonacoScopeContext } from '../../../contextes/MonacoScope' -import { isMacOS } from '../../../utils' +import { classnames, isMacOS } from '../../../utils' import type { BarItemProps } from '../..' +import { saveStatusAtom } from '../atoms' + +const prefix = 'url-cache__topbar__save' export const Save: React.ComponentType = () => { const { editorInstance } = useContext(MonacoScopeContext) ?? {} + const model = useMemo(() => editorInstance?.getModel(), [editorInstance]) + const [saveStatus] = useAtom(saveStatusAtom) + const curSaveStatus = useMemo(() => { + if (!model) return 'disabled' + const uri = model.uri.toString() + const s = saveStatus?.[uri] + return s === true + ? 'saved' + : s === false + ? 'save-needed' + : 'disabled' + }, [model, saveStatus]) return = () => { } offset={[0, 6]} > - } From 59654aa98b0455423252b63ccf3139662d675419 Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Mon, 4 Sep 2023 02:45:38 +0800 Subject: [PATCH 23/24] fix(core): use span not div --- core/src/plugins/urlCache/topbar/Save.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/plugins/urlCache/topbar/Save.tsx b/core/src/plugins/urlCache/topbar/Save.tsx index 1f8fa791..84ecf64e 100644 --- a/core/src/plugins/urlCache/topbar/Save.tsx +++ b/core/src/plugins/urlCache/topbar/Save.tsx @@ -41,7 +41,7 @@ export const Save: React.ComponentType = () => { prefix, curSaveStatus )} onClick={() => editorInstance?.trigger('whatever', 'ppd.save', {})}> -
+ From 70c87811090d6174114a90171c41f28986482d90 Mon Sep 17 00:00:00 2001 From: yijie4188 Date: Mon, 4 Sep 2023 02:58:08 +0800 Subject: [PATCH 24/24] feat(core): add social plugin --- core/src/plugins/social/index.ts | 9 +++++++ core/src/plugins/social/topbar/Share.tsx | 34 ++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 core/src/plugins/social/index.ts create mode 100644 core/src/plugins/social/topbar/Share.tsx diff --git a/core/src/plugins/social/index.ts b/core/src/plugins/social/index.ts new file mode 100644 index 00000000..86bfa739 --- /dev/null +++ b/core/src/plugins/social/index.ts @@ -0,0 +1,9 @@ +import { definePlugin } from '@power-playground/core' + +import { Share } from './topbar/Share' + +export default definePlugin({ + editor: { + topbar: [Share] + } +}) diff --git a/core/src/plugins/social/topbar/Share.tsx b/core/src/plugins/social/topbar/Share.tsx new file mode 100644 index 00000000..4007c73d --- /dev/null +++ b/core/src/plugins/social/topbar/Share.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import type { BarItemProps } from '@power-playground/core' +import { messenger } from '@power-playground/core' + +import { Popover } from '../../../components/base/Popover' +import { isMacOS } from '../../../utils' + +const prefix = 'social__share' + +export const Share: React.ComponentType = () => { + return + Share to Social +    + {isMacOS ? '⌘' : 'Ctrl'} + SHIFT + S +
+ (Change mode by context menu.) + } + offset={[0, 6]} + > + +
+}