From cb37a05ac0577a6c168ea92fcaffbe5347d743e0 Mon Sep 17 00:00:00 2001 From: RheeseyB <1044774+Rheeseyb@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:59:32 +0100 Subject: [PATCH] Update VS Code and enable Intellisense (#6222) **Problem:** Our version of VS Code is extremely outdated and built using a reverse engineered build, but since then the web version of VS Code has come a long way and can now support Intellisense. **Fix:** This PR changes 3 fundamental things: 1. Rather than using IndexedDB for communicating between Utopia and the VS Code extension, we now use VS Code's commands service. This allows us to register a command handler on either side of the air gap, and pass any arbitrary information in the command's parameters. We therefore are able to use two new commands for passing messages directly across that air gap, removing the reliance on async polling of the store. 2. The version of VS Code has been bumped up to 1.91.1, which required a newer version of node, and a load of changes to the patch file to actually be able to build it. When we first implemented the VS Code embed, we had to reverse engineer a build file based on the electron build. This newer version of VS Code includes a build script, so it was a case of finding and calling that, and figuring out where the patches needed to be applied now. The end result is significantly simpler than the previous approach. 3. To actually support Intellisense, we needed to add some new cross origin headers to the editor assets and vs code assets, and serve both the editor and vs code iframe up on the same TLD to ensure that `crossOriginIsolated` would be set to true. There is also a `vscode-coi` query param that we have to include in the iframe's URL. For the VS Code parts of the diff (the patch and the extension changes) it might be easier to view some files in full rather than trying to read the diff... **Commit Details:** - Update the vscode-build's `shell.nix` to use newer versions of node and yarn, and supply an extra package required for building it - Update the pulled version of VS Code to 1.91.1 - Remove the reliance on IndexedDB for communicating between the outer edge of the iframe and the extension, and instead use VS Code's command service for passing messages between the two, meaning that we no longer need to use polling to watch for file changes, and can instead directly trigger them - This required the registering of some extra commands, and the code for then communicating those across the iframe boundary - Remove the `fs` files from `utopia-vscode-common`, and instead use an in memory fs implementation in the extension. This stores the files in a `Map`, and stores both the saved and unsaved content of text files - Since we are now passing messages directly between Utopia and VS Code, I've flattened the message types (previously there were two types: one for file changes; one for other changes such as cursor position, highlight bounds etc) - The server now passes down the headers required to enable [`crossOriginIsolated`](https://developer.mozilla.org/en-US/docs/Web/API/Window/crossOriginIsolated). `crossOriginIsolated` is required for full Intellisense on the web, which requires 2 new cross origin headers, and for the code editor iframe to be served up from the same TLD as the rest of the editor. - This does mean that the code editor will be running on the same process as Utopia - To actually build this version of VS Code I had to: - Disable code mangling. I'm, not sure why, but with that enabled the build was just silently hanging on my machine - Remove some native keyboard mapping code. This was causing an issue related to electron, which we are clearly not interested in, so I just ripped it out after failing to fix it. - To simplify the VS Code UI I have: - Forcibly enabled Zen mode (the `isZenModeActive` check always returns true) - Changed some Zen mode defaults to prevent the code editor trying to fullscreen itself, or centre the code pane inside itself - Forcibly hidden other parts of the UI that would normally still be shown when in non-fullscreen Zen mode **Manual Tests:** I hereby swear that: - [x] I opened a hydrogen project and it loaded - [x] I could navigate to various routes in Preview mode --- .../code-editor/code-editor-container.tsx | 6 +- .../components/editor/store/vscode-changes.ts | 14 +- .../navigator-item-clickable-wrapper.tsx | 5 +- editor/src/core/vscode/vscode-bridge.ts | 50 +- .../templates/vscode-editor-iframe/index.html | 8 +- release.nix | 6 +- server/src/Utopia/Web/Endpoints.hs | 39 +- server/src/Utopia/Web/Types.hs | 14 +- shell.nix | 2 +- utopia-vscode-common/src/fs/fs-core.ts | 158 -- utopia-vscode-common/src/fs/fs-types.ts | 101 - utopia-vscode-common/src/fs/fs-utils.ts | 638 ------- utopia-vscode-common/src/index.ts | 8 +- utopia-vscode-common/src/lite-either.ts | 65 - utopia-vscode-common/src/mailbox.ts | 253 --- .../src/messages-to-utopia.ts | 162 ++ .../src/messages-to-vscode.ts | 328 ++++ utopia-vscode-common/src/messages.ts | 360 ---- utopia-vscode-common/src/path-utils.ts | 8 +- .../src/vscode-communication.ts | 191 -- utopia-vscode-common/src/window-messages.ts | 252 --- utopia-vscode-extension/package.json | 20 +- utopia-vscode-extension/pnpm-lock.yaml | 74 +- utopia-vscode-extension/src/extension.ts | 286 +-- utopia-vscode-extension/src/in-mem-fs.ts | 400 ++++ utopia-vscode-extension/src/path-utils.ts | 34 +- utopia-vscode-extension/src/utopia-fs.ts | 362 ++-- vscode-build/build.js | 15 +- vscode-build/package.json | 5 +- vscode-build/pull-utopia-extension.js | 2 +- vscode-build/shell.nix | 4 +- vscode-build/vscode.patch | 1691 ++++++++--------- vscode-build/yarn.lock | 17 +- website-next/components/common/env-vars.ts | 1 - 34 files changed, 2225 insertions(+), 3354 deletions(-) delete mode 100644 utopia-vscode-common/src/fs/fs-core.ts delete mode 100644 utopia-vscode-common/src/fs/fs-types.ts delete mode 100644 utopia-vscode-common/src/fs/fs-utils.ts delete mode 100644 utopia-vscode-common/src/lite-either.ts delete mode 100644 utopia-vscode-common/src/mailbox.ts create mode 100644 utopia-vscode-common/src/messages-to-utopia.ts create mode 100644 utopia-vscode-common/src/messages-to-vscode.ts delete mode 100644 utopia-vscode-common/src/messages.ts delete mode 100644 utopia-vscode-common/src/vscode-communication.ts delete mode 100644 utopia-vscode-common/src/window-messages.ts create mode 100644 utopia-vscode-extension/src/in-mem-fs.ts diff --git a/editor/src/components/code-editor/code-editor-container.tsx b/editor/src/components/code-editor/code-editor-container.tsx index 9fd6556af0a1..85d5d4495e88 100644 --- a/editor/src/components/code-editor/code-editor-container.tsx +++ b/editor/src/components/code-editor/code-editor-container.tsx @@ -1,17 +1,17 @@ import React from 'react' import { Substores, useEditorState } from '../editor/store/store-hook' -import { MONACO_EDITOR_IFRAME_BASE_URL } from '../../common/env-vars' import { createIframeUrl } from '../../core/shared/utils' import { getUnderlyingVSCodeBridgeID } from '../editor/store/editor-state' import { VSCodeLoadingScreen } from './vscode-editor-loading-screen' -import { getEditorBranchNameFromURL, setBranchNameFromURL } from '../../utils/branches' +import { setBranchNameFromURL } from '../../utils/branches' import { VSCODE_EDITOR_IFRAME_ID } from '../../core/vscode/vscode-bridge' const VSCodeIframeContainer = React.memo((props: { vsCodeSessionID: string }) => { const vsCodeSessionID = props.vsCodeSessionID - const baseIframeSrc = createIframeUrl(MONACO_EDITOR_IFRAME_BASE_URL, 'vscode-editor-iframe/') + const baseIframeSrc = createIframeUrl(window.location.origin, 'vscode-editor-iframe/') const url = new URL(baseIframeSrc) url.searchParams.append('vs_code_session_id', vsCodeSessionID) + url.searchParams.append('vscode-coi', '') // Required to enable intellisense setBranchNameFromURL(url.searchParams) diff --git a/editor/src/components/editor/store/vscode-changes.ts b/editor/src/components/editor/store/vscode-changes.ts index c509fc658103..43c1385493f6 100644 --- a/editor/src/components/editor/store/vscode-changes.ts +++ b/editor/src/components/editor/store/vscode-changes.ts @@ -21,10 +21,8 @@ import { import type { UpdateDecorationsMessage, SelectedElementChanged, - AccumulatedToVSCodeMessage, - ToVSCodeMessageNoAccumulated, + FromUtopiaToVSCodeMessage, } from 'utopia-vscode-common' -import { accumulatedToVSCodeMessage, toVSCodeExtensionMessage } from 'utopia-vscode-common' import type { EditorState } from './editor-state' import { getHighlightBoundsForElementPaths } from './editor-state' import { shallowEqual } from '../../../core/shared/equality-utils' @@ -302,15 +300,15 @@ export const emptyProjectChanges: ProjectChanges = { selectedChanged: null, } -export function projectChangesToVSCodeMessages(local: ProjectChanges): AccumulatedToVSCodeMessage { - let messages: Array = [] +function projectChangesToVSCodeMessages(local: ProjectChanges): Array { + let messages: Array = [] if (local.updateDecorations != null) { messages.push(local.updateDecorations) } if (local.selectedChanged != null) { messages.push(local.selectedChanged) } - return accumulatedToVSCodeMessage(messages) + return messages } export function getProjectChanges( @@ -334,7 +332,5 @@ export function getProjectChanges( export function sendVSCodeChanges(changes: ProjectChanges) { applyProjectChanges(changes.fileChanges.changesForVSCode) const toVSCodeAccumulated = projectChangesToVSCodeMessages(changes) - if (toVSCodeAccumulated.messages.length > 0) { - sendMessage(toVSCodeExtensionMessage(toVSCodeAccumulated)) - } + toVSCodeAccumulated.forEach((message) => sendMessage(message)) } diff --git a/editor/src/components/navigator/navigator-item/navigator-item-clickable-wrapper.tsx b/editor/src/components/navigator/navigator-item/navigator-item-clickable-wrapper.tsx index a42ba01e2b34..8b1deca408b7 100644 --- a/editor/src/components/navigator/navigator-item/navigator-item-clickable-wrapper.tsx +++ b/editor/src/components/navigator/navigator-item/navigator-item-clickable-wrapper.tsx @@ -21,7 +21,6 @@ import { selectedElementChangedMessageFromHighlightBounds, sendMessage, } from '../../../core/vscode/vscode-bridge' -import { toVSCodeExtensionMessage } from 'utopia-vscode-common' import { isRegulaNavigatorRow, type NavigatorRow } from '../navigator-row' import { useDispatch } from '../../editor/store/dispatch-context' import { isRight } from '../../../core/shared/either' @@ -100,9 +99,7 @@ export function useGetNavigatorClickActions( // when we click on an already selected item we should force vscode to navigate there if (selected && highlightBounds != null) { sendMessage( - toVSCodeExtensionMessage( - selectedElementChangedMessageFromHighlightBounds(highlightBounds, 'force-navigation'), - ), + selectedElementChangedMessageFromHighlightBounds(highlightBounds, 'force-navigation'), ) } return actionsForSingleSelection(targetPath, row, conditionalOverrideUpdate) diff --git a/editor/src/core/vscode/vscode-bridge.ts b/editor/src/core/vscode/vscode-bridge.ts index b03135cb0fa9..da9ff01d9153 100644 --- a/editor/src/core/vscode/vscode-bridge.ts +++ b/editor/src/core/vscode/vscode-bridge.ts @@ -14,19 +14,20 @@ import { deletePathChange, ensureDirectoryExistsChange, initProject, - isFromVSCodeExtensionMessage, - isIndexedDBFailure, + isClearLoadingScreen, + isEditorCursorPositionChanged, isMessageListenersReady, + isUtopiaVSCodeConfigValues, isVSCodeBridgeReady, isVSCodeFileChange, isVSCodeFileDelete, + isVSCodeReady, openFileMessage, projectDirectory, projectTextFile, selectedElementChanged, setFollowSelectionConfig, setVSCodeTheme, - toVSCodeExtensionMessage, updateDecorationsMessage, writeProjectFileChange, } from 'utopia-vscode-common' @@ -131,27 +132,16 @@ export function initVSCodeBridge( // Store the source vscodeIFrame = messageEvent.source dispatch([markVSCodeBridgeReady(true)]) - } else if (isFromVSCodeExtensionMessage(data)) { - const message = data.message - switch (message.type) { - case 'EDITOR_CURSOR_POSITION_CHANGED': - dispatch([selectFromFileAndPosition(message.filePath, message.line, message.column)]) - break - case 'UTOPIA_VSCODE_CONFIG_VALUES': - dispatch([updateConfigFromVSCode(message.config)]) - break - case 'VSCODE_READY': - dispatch([sendCodeEditorInitialisation()]) - break - case 'CLEAR_LOADING_SCREEN': - if (!loadingScreenHidden) { - loadingScreenHidden = true - dispatch([hideVSCodeLoadingScreen()]) - } - break - default: - const _exhaustiveCheck: never = message - throw new Error(`Unhandled message type${JSON.stringify(message)}`) + } else if (isEditorCursorPositionChanged(data)) { + dispatch([selectFromFileAndPosition(data.filePath, data.line, data.column)]) + } else if (isUtopiaVSCodeConfigValues(data)) { + dispatch([updateConfigFromVSCode(data.config)]) + } else if (isVSCodeReady(data)) { + dispatch([sendCodeEditorInitialisation()]) + } else if (isClearLoadingScreen(data)) { + if (!loadingScreenHidden) { + loadingScreenHidden = true + dispatch([hideVSCodeLoadingScreen()]) } } else if (isVSCodeFileChange(data)) { const { filePath, fileContent } = data @@ -169,8 +159,6 @@ export function initVSCodeBridge( dispatch(actionsToDispatch) } else if (isVSCodeFileDelete(data)) { dispatch([deleteFileFromVSCode(data.filePath)]) - } else if (isIndexedDBFailure(data)) { - dispatch([setIndexedDBFailed(true)]) } } @@ -182,11 +170,11 @@ export function sendMessage(message: FromUtopiaToVSCodeMessage) { } export function sendOpenFileMessage(filePath: string, bounds: Bounds | null) { - sendMessage(toVSCodeExtensionMessage(openFileMessage(filePath, bounds))) + sendMessage(openFileMessage(filePath, bounds)) } export function sendSetFollowSelectionEnabledMessage(enabled: boolean) { - sendMessage(toVSCodeExtensionMessage(setFollowSelectionConfig(enabled))) + sendMessage(setFollowSelectionConfig(enabled)) } export function applyProjectChanges(changes: Array) { @@ -243,7 +231,7 @@ export function getCodeEditorDecorations(editorState: EditorState): UpdateDecora export function sendCodeEditorDecorations(editorState: EditorState) { const decorationsMessage = getCodeEditorDecorations(editorState) - sendMessage(toVSCodeExtensionMessage(decorationsMessage)) + sendMessage(decorationsMessage) } export function getSelectedElementChangedMessage( @@ -284,7 +272,7 @@ export function sendSelectedElement(newEditorState: EditorState) { 'do-not-force-navigation', ) if (selectedElementChangedMessage != null) { - sendMessage(toVSCodeExtensionMessage(selectedElementChangedMessage)) + sendMessage(selectedElementChangedMessage) } } @@ -302,5 +290,5 @@ function vsCodeThemeForTheme(theme: Theme): string { export function sendSetVSCodeTheme(theme: Theme) { const vsCodeTheme = vsCodeThemeForTheme(theme) - sendMessage(toVSCodeExtensionMessage(setVSCodeTheme(vsCodeTheme))) + sendMessage(setVSCodeTheme(vsCodeTheme)) } diff --git a/editor/src/templates/vscode-editor-iframe/index.html b/editor/src/templates/vscode-editor-iframe/index.html index f3af22f2caca..ac95ec7e08b7 100644 --- a/editor/src/templates/vscode-editor-iframe/index.html +++ b/editor/src/templates/vscode-editor-iframe/index.html @@ -10,9 +10,9 @@ @@ -36,8 +36,8 @@ - - + + diff --git a/release.nix b/release.nix index 87ac2f60e35f..8430c16f1fd4 100644 --- a/release.nix +++ b/release.nix @@ -13,10 +13,10 @@ let }) { inherit config; }; recentPkgs = import (builtins.fetchTarball { - name = "nixos-23.11"; - url = https://github.com/NixOS/nixpkgs/archive/23.11.tar.gz; + name = "nixos-24.05"; + url = https://github.com/NixOS/nixpkgs/archive/24.05.tar.gz; # Hash obtained using `nix-prefetch-url --unpack ` - sha256 = "1ndiv385w1qyb3b18vw13991fzb9wg4cl21wglk89grsfsnra41k"; + sha256 = "1lr1h35prqkd1mkmzriwlpvxcb34kmhc9dnr48gkm8hh089hifmx"; }) { inherit config; }; in diff --git a/server/src/Utopia/Web/Endpoints.hs b/server/src/Utopia/Web/Endpoints.hs index 2b67ff818df3..3932a65d51e1 100644 --- a/server/src/Utopia/Web/Endpoints.hs +++ b/server/src/Utopia/Web/Endpoints.hs @@ -277,7 +277,7 @@ injectIntoPage toInject@(_, _, _, _, editorScriptTags) (TagComment "editorScript injectIntoPage toInject (firstTag : remainder) = firstTag : injectIntoPage toInject remainder injectIntoPage _ [] = [] -renderPageWithMetadata :: Maybe ProjectIdWithSuffix -> Maybe ProjectMetadata -> Maybe DB.DecodedProject -> Maybe Text -> Text -> ServerMonad H.Html +renderPageWithMetadata :: Maybe ProjectIdWithSuffix -> Maybe ProjectMetadata -> Maybe DB.DecodedProject -> Maybe Text -> Text -> ServerMonad ProjectPageResponse renderPageWithMetadata possibleProjectID possibleMetadata possibleProject branchName pagePath = do indexHtml <- getEditorTextContent branchName pagePath siteRoot <- getSiteRoot @@ -294,38 +294,38 @@ renderPageWithMetadata possibleProjectID possibleMetadata possibleProject branch let reversedEditorScriptTags = partitionOutScriptDefer False $ reverse parsedTags let editorScriptPreloads = preloadsForScripts $ reverse reversedEditorScriptTags let updatedContent = injectIntoPage (ogTags, projectIDScriptTags, dependenciesTags, vscodePreloadTags, editorScriptPreloads) parsedTags - return $ H.preEscapedToHtml $ renderTags updatedContent + return $ addHeader "cross-origin" $ addHeader "same-origin" $ addHeader "credentialless" $ H.preEscapedToHtml $ renderTags updatedContent -innerProjectPage :: Maybe ProjectIdWithSuffix -> ProjectDetails -> Maybe DB.DecodedProject -> Maybe Text -> ServerMonad H.Html +innerProjectPage :: Maybe ProjectIdWithSuffix -> ProjectDetails -> Maybe DB.DecodedProject -> Maybe Text -> ServerMonad ProjectPageResponse innerProjectPage (Just _) UnknownProject _ branchName = do projectNotFoundHtml <- getEditorTextContent branchName "project-not-found/index.html" - return $ H.preEscapedToHtml projectNotFoundHtml + return $ addHeader "cross-origin" $ addHeader "same-origin" $ addHeader "credentialless" $ H.preEscapedToHtml projectNotFoundHtml innerProjectPage possibleProjectID details possibleProject branchName = renderPageWithMetadata possibleProjectID (projectDetailsToPossibleMetadata details) possibleProject branchName "index.html" -projectPage :: ProjectIdWithSuffix -> Maybe Text -> ServerMonad H.Html +projectPage :: ProjectIdWithSuffix -> Maybe Text -> ServerMonad ProjectPageResponse projectPage projectIDWithSuffix@(ProjectIdWithSuffix projectID _) branchName = do possibleMetadata <- getProjectMetadata projectID possibleProject <- loadProject projectID innerProjectPage (Just projectIDWithSuffix) possibleMetadata possibleProject branchName -emptyProjectPage :: Maybe Text -> ServerMonad H.Html +emptyProjectPage :: Maybe Text -> ServerMonad ProjectPageResponse emptyProjectPage = innerProjectPage Nothing UnknownProject Nothing -innerPreviewPage :: Maybe ProjectIdWithSuffix -> ProjectDetails -> Maybe DB.DecodedProject -> Maybe Text -> ServerMonad H.Html +innerPreviewPage :: Maybe ProjectIdWithSuffix -> ProjectDetails -> Maybe DB.DecodedProject -> Maybe Text -> ServerMonad ProjectPageResponse innerPreviewPage (Just _) UnknownProject _ branchName = do projectNotFoundHtml <- getEditorTextContent branchName "project-not-found/index.html" - return $ H.preEscapedToHtml projectNotFoundHtml + return $ addHeader "cross-origin" $ addHeader "same-origin" $ addHeader "credentialless" $ H.preEscapedToHtml projectNotFoundHtml innerPreviewPage possibleProjectID details possibleProject branchName = renderPageWithMetadata possibleProjectID (projectDetailsToPossibleMetadata details) possibleProject branchName "preview/index.html" -previewPage :: ProjectIdWithSuffix -> Maybe Text -> ServerMonad H.Html +previewPage :: ProjectIdWithSuffix -> Maybe Text -> ServerMonad ProjectPageResponse previewPage projectIDWithSuffix@(ProjectIdWithSuffix projectID _) branchName = do possibleMetadata <- getProjectMetadata projectID possibleProject <- loadProject projectID innerPreviewPage (Just projectIDWithSuffix) possibleMetadata possibleProject branchName -emptyPreviewPage :: Maybe Text -> ServerMonad H.Html +emptyPreviewPage :: Maybe Text -> ServerMonad ProjectPageResponse emptyPreviewPage = innerPreviewPage Nothing UnknownProject Nothing getUserEndpoint :: Maybe Text -> ServerMonad UserResponse @@ -529,12 +529,27 @@ addCacheControl = addMiddlewareHeader "Cache-Control" "public, immutable, max-ag addCacheControlRevalidate :: Middleware addCacheControlRevalidate = addMiddlewareHeader "Cache-Control" "public, must-revalidate, proxy-revalidate, max-age=0" +addCrossOriginResourcePolicy :: Middleware +addCrossOriginResourcePolicy = addMiddlewareHeader "Cross-Origin-Resource-Policy" "cross-origin" + +addCrossOriginOpenerPolicy :: Middleware +addCrossOriginOpenerPolicy = addMiddlewareHeader "Cross-Origin-Opener-Policy" "same-origin" + +addCrossOriginEmbedderPolicy :: Middleware +addCrossOriginEmbedderPolicy = addMiddlewareHeader "Cross-Origin-Embedder-Policy" "require-corp" + addCDNHeaders :: Middleware addCDNHeaders = addCacheControl . addAccessControlAllowOrigin addCDNHeadersCacheRevalidate :: Middleware addCDNHeadersCacheRevalidate = addCacheControlRevalidate . addAccessControlAllowOrigin +addEditorAssetsHeaders :: Middleware +addEditorAssetsHeaders = addCDNHeaders . addCrossOriginResourcePolicy . addCrossOriginOpenerPolicy . addCrossOriginEmbedderPolicy + +addVSCodeHeaders :: Middleware +addVSCodeHeaders = addCDNHeadersCacheRevalidate . addCrossOriginResourcePolicy . addCrossOriginOpenerPolicy . addCrossOriginEmbedderPolicy + fallbackOn404 :: Application -> Application -> Application fallbackOn404 firstApplication secondApplication request sendResponse = firstApplication request $ \firstAppResponse -> do @@ -557,7 +572,7 @@ editorAssetsEndpoint notProxiedPath possibleBranchName = do mainApp <- case possibleBranchName of Just _ -> pure loadLocally Nothing -> maybe (pure loadLocally) loadFromProxy possibleProxyManager - pure $ addCDNHeaders $ downloadWithFallbacks mainApp + pure $ addEditorAssetsHeaders $ downloadWithFallbacks mainApp downloadGithubProjectEndpoint :: Maybe Text -> Text -> Text -> ServerMonad BL.ByteString downloadGithubProjectEndpoint cookie owner repo = requireUser cookie $ \_ -> do @@ -580,7 +595,7 @@ websiteAssetsEndpoint notProxiedPath = do vsCodeAssetsEndpoint :: ServerMonad Application vsCodeAssetsEndpoint = do pathToServeFrom <- getVSCodeAssetRoot - addCDNHeadersCacheRevalidate <$> servePath pathToServeFrom Nothing + addVSCodeHeaders <$> servePath pathToServeFrom Nothing wrappedWebAppLookup :: (Pieces -> IO LookupResult) -> Pieces -> IO LookupResult wrappedWebAppLookup defaultLookup _ = diff --git a/server/src/Utopia/Web/Types.hs b/server/src/Utopia/Web/Types.hs index cf7fba0b6561..9501b4e2bf27 100644 --- a/server/src/Utopia/Web/Types.hs +++ b/server/src/Utopia/Web/Types.hs @@ -76,15 +76,17 @@ type LogoutAPI = "logout" :> Get '[HTML] (SetSessionCookies H.Html) type GetUserAPI = "v1" :> "user" :> Get '[JSON] UserResponse -type EmptyProjectPageAPI = "p" :> BranchNameParam :> Get '[HTML] H.Html +type ProjectPageResponse = Headers '[Header "Cross-Origin-Resource-Policy" Text, Header "Cross-Origin-Opener-Policy" Text, Header "Cross-Origin-Embedder-Policy" Text] (H.Html) -type ProjectPageAPI = "p" :> Capture "project_id" ProjectIdWithSuffix :> BranchNameParam :> Get '[HTML] H.Html +type EmptyProjectPageAPI = "p" :> BranchNameParam :> Get '[HTML] ProjectPageResponse + +type ProjectPageAPI = "p" :> Capture "project_id" ProjectIdWithSuffix :> BranchNameParam :> Get '[HTML] ProjectPageResponse type LoadProjectFileAPI = "p" :> Capture "project_id" ProjectIdWithSuffix :> Header "If-None-Match" Text :> CaptureAll "file_path" Text :> RawM -type EmptyPreviewPageAPI = "share" :> BranchNameParam :> Get '[HTML] H.Html +type EmptyPreviewPageAPI = "share" :> BranchNameParam :> Get '[HTML] ProjectPageResponse -type PreviewPageAPI = "share" :> Capture "project_id" ProjectIdWithSuffix :> BranchNameParam :> Get '[HTML] H.Html +type PreviewPageAPI = "share" :> Capture "project_id" ProjectIdWithSuffix :> BranchNameParam :> Get '[HTML] ProjectPageResponse type DownloadProjectResponse = Headers '[Header "Access-Control-Allow-Origin" Text] Value @@ -261,6 +263,4 @@ packagerAPI = Proxy packagerLink :: Text -> Text -> Text packagerLink jsPackageName jsPackageVersion = let versionedName = jsPackageName <> "@" <> jsPackageVersion - in toUrlPiece $ safeLink apiProxy packagerAPI versionedName - - + in toUrlPiece $ safeLink apiProxy packagerAPI versionedName \ No newline at end of file diff --git a/shell.nix b/shell.nix index c4c630a9c99a..4a47f0bead5f 100644 --- a/shell.nix +++ b/shell.nix @@ -785,7 +785,7 @@ let pythonAndPackages = pkgs.python3.withPackages(ps: with ps; [ pyusb tkinter pkgconfig ]); - basePackages = [ node pkgs.libsecret pythonAndPackages pkgs.pkg-config pkgs.tmux pkgs.git pkgs.wget ] ++ nodePackages ++ linuxOnlyPackages ++ macOSOnlyPackages; + basePackages = [ node pkgs.libsecret pkgs.libkrb5 pythonAndPackages pkgs.pkg-config pkgs.tmux pkgs.git pkgs.wget ] ++ nodePackages ++ linuxOnlyPackages ++ macOSOnlyPackages; withServerBasePackages = basePackages ++ (lib.optionals includeServerBuildSupport baseServerPackages); withServerRunPackages = withServerBasePackages ++ (lib.optionals includeRunLocallySupport serverRunPackages); withReleasePackages = withServerRunPackages ++ (lib.optionals includeReleaseSupport releasePackages); diff --git a/utopia-vscode-common/src/fs/fs-core.ts b/utopia-vscode-common/src/fs/fs-core.ts deleted file mode 100644 index 940211be00ae..000000000000 --- a/utopia-vscode-common/src/fs/fs-core.ts +++ /dev/null @@ -1,158 +0,0 @@ -import * as localforage from 'localforage' -import type { Either } from '../lite-either' -import { left, mapEither, right } from '../lite-either' -import { stripTrailingSlash } from '../path-utils' -import type { FSNode } from './fs-types' -import { defer } from './fs-utils' - -let dbHeartbeatsStore: LocalForage // There is no way to request a list of existing stores, so we have to explicitly track them - -let store: LocalForage | null -let thisDBName: string - -const StoreExistsKey = '.store-exists' - -const firstInitialize = defer() -let initializeStoreChain: Promise = Promise.resolve() - -export async function initializeStore( - storeName: string, - driver: string = localforage.INDEXEDDB, -): Promise { - async function innerInitialize(): Promise { - thisDBName = `utopia-project-${storeName}` - - store = localforage.createInstance({ - name: thisDBName, - driver: driver, - }) - - await store.ready() - await store.setItem(StoreExistsKey, true) - - dbHeartbeatsStore = localforage.createInstance({ - name: 'utopia-all-store-heartbeats', - driver: localforage.INDEXEDDB, - }) - - await dbHeartbeatsStore.ready() - - triggerHeartbeat().then(dropOldStores) - } - initializeStoreChain = initializeStoreChain.then(innerInitialize) - firstInitialize.resolve() - return initializeStoreChain -} - -export interface StoreDoesNotExist { - type: 'StoreDoesNotExist' -} - -const StoreDoesNotExistConst: StoreDoesNotExist = { - type: 'StoreDoesNotExist', -} - -export function isStoreDoesNotExist(t: unknown): t is StoreDoesNotExist { - return (t as any)?.type === 'StoreDoesNotExist' -} - -export type AsyncFSResult = Promise> - -const StoreExistsKeyInterval = 1000 - -interface StoreKeyExistsCheck { - lastCheckedTime: number - exists: boolean -} - -let lastCheckedForStoreKeyExists: StoreKeyExistsCheck | null = null - -async function checkStoreKeyExists(): Promise { - if (store == null) { - return false - } else { - const now = Date.now() - if ( - lastCheckedForStoreKeyExists == null || - lastCheckedForStoreKeyExists.lastCheckedTime + StoreExistsKeyInterval < now - ) { - const exists = (await store.getItem(StoreExistsKey)) ?? false - lastCheckedForStoreKeyExists = { - lastCheckedTime: now, - exists: exists, - } - return exists - } else { - return lastCheckedForStoreKeyExists.exists - } - } -} - -async function withSanityCheckedStore( - withStore: (sanityCheckedStore: LocalForage) => Promise, -): AsyncFSResult { - await firstInitialize - await initializeStoreChain - const storeExists = await checkStoreKeyExists() - if (store != null && storeExists) { - const result = await withStore(store) - return right(result) - } else { - store = null - return left(StoreDoesNotExistConst) - } -} - -export async function keys(): AsyncFSResult { - return withSanityCheckedStore((sanityCheckedStore: LocalForage) => sanityCheckedStore.keys()) -} - -export async function getItem(path: string): AsyncFSResult { - return withSanityCheckedStore((sanityCheckedStore: LocalForage) => - sanityCheckedStore.getItem(stripTrailingSlash(path)), - ) -} - -export async function setItem(path: string, value: FSNode): AsyncFSResult { - return withSanityCheckedStore((sanityCheckedStore: LocalForage) => - sanityCheckedStore.setItem(stripTrailingSlash(path), value), - ) -} - -export async function removeItem(path: string): AsyncFSResult { - return withSanityCheckedStore((sanityCheckedStore: LocalForage) => - sanityCheckedStore.removeItem(stripTrailingSlash(path)), - ) -} - -const ONE_HOUR = 1000 * 60 * 60 -const ONE_DAY = ONE_HOUR * 24 - -async function triggerHeartbeat(): Promise { - await dbHeartbeatsStore.setItem(thisDBName, Date.now()) - setTimeout(triggerHeartbeat, ONE_HOUR) -} - -async function dropOldStores(): Promise { - const now = Date.now() - const allStores = await dbHeartbeatsStore.keys() - const allDBsWithLastHeartbeatTS = await Promise.all( - allStores.map(async (k) => { - const ts = await dbHeartbeatsStore.getItem(k) - return { - dbName: k, - ts: ts ?? now, - } - }), - ) - const dbsToDrop = allDBsWithLastHeartbeatTS.filter((v) => now - v.ts > ONE_DAY) - if (dbsToDrop.length > 0) { - const dbNamesToDrop = dbsToDrop.map((v) => v.dbName) - dbNamesToDrop.forEach((dbName) => { - dbHeartbeatsStore.removeItem(dbName) - localforage.dropInstance({ - name: dbName, - }) - }) - } -} diff --git a/utopia-vscode-common/src/fs/fs-types.ts b/utopia-vscode-common/src/fs/fs-types.ts deleted file mode 100644 index 5cb0a99a426d..000000000000 --- a/utopia-vscode-common/src/fs/fs-types.ts +++ /dev/null @@ -1,101 +0,0 @@ -type FSNodeType = 'FILE' | 'DIRECTORY' -export type FSUser = 'UTOPIA' | 'VSCODE' - -export interface FSNode { - type: FSNodeType - ctime: number - mtime: number - lastSavedTime: number - sourceOfLastChange: FSUser -} - -export interface FSNodeWithPath { - path: string - node: FSNode -} - -export interface FSStat extends FSNode { - size: number -} - -export interface FileContent { - content: Uint8Array - unsavedContent: Uint8Array | null -} - -export interface FSFile extends FSNode, FileContent { - type: 'FILE' -} - -export function fsFile( - content: Uint8Array, - unsavedContent: Uint8Array | null, - ctime: number, - mtime: number, - lastSavedTime: number, - sourceOfLastChange: FSUser, -): FSFile { - return { - type: 'FILE', - ctime: ctime, - mtime: mtime, - lastSavedTime: lastSavedTime, - content: content, - unsavedContent: unsavedContent, - sourceOfLastChange: sourceOfLastChange, - } -} - -export interface FSDirectory extends FSNode { - type: 'DIRECTORY' -} - -export function fsDirectory(ctime: number, mtime: number, sourceOfLastChange: FSUser): FSDirectory { - return { - type: 'DIRECTORY', - ctime: ctime, - mtime: mtime, - lastSavedTime: mtime, - sourceOfLastChange: sourceOfLastChange, - } -} - -export function newFSDirectory(sourceOfLastChange: FSUser): FSDirectory { - const now = Date.now() - return { - type: 'DIRECTORY', - ctime: now, - mtime: now, - lastSavedTime: now, - sourceOfLastChange: sourceOfLastChange, - } -} - -export function isFile(node: FSNode): node is FSFile { - return node.type === 'FILE' -} - -export function isDirectory(node: FSNode): node is FSDirectory { - return node.type === 'DIRECTORY' -} - -export type FSErrorCode = 'ENOENT' | 'EEXIST' | 'EISDIR' | 'ENOTDIR' | 'FS_UNAVAILABLE' -export interface FSError { - code: FSErrorCode - path: string -} - -export type FSErrorHandler = (e: FSError) => Error - -function fsError(code: FSErrorCode, path: string): FSError { - return { - code: code, - path: path, - } -} - -export const enoent = (path: string) => fsError('ENOENT', path) -export const eexist = (path: string) => fsError('EEXIST', path) -export const eisdir = (path: string) => fsError('EISDIR', path) -export const enotdir = (path: string) => fsError('ENOTDIR', path) -export const fsUnavailable = (path: string) => fsError('FS_UNAVAILABLE', path) diff --git a/utopia-vscode-common/src/fs/fs-utils.ts b/utopia-vscode-common/src/fs/fs-utils.ts deleted file mode 100644 index d72231b81e25..000000000000 --- a/utopia-vscode-common/src/fs/fs-utils.ts +++ /dev/null @@ -1,638 +0,0 @@ -import { INDEXEDDB } from 'localforage' -import { isRight } from '../lite-either' -import { appendToPath, stripLeadingSlash, stripTrailingSlash } from '../path-utils' -import type { AsyncFSResult } from './fs-core' -import { - getItem as getItemCore, - initializeStore, - keys as keysCore, - removeItem as removeItemCore, - setItem as setItemCore, -} from './fs-core' -import type { - FSError, - FSErrorHandler, - FSNode, - FSStat, - FSDirectory, - FSNodeWithPath, - FSFile, - FileContent, - FSUser, -} from './fs-types' -import { - isDirectory, - isFile, - enoent, - eexist, - eisdir, - enotdir, - fsFile, - newFSDirectory, - fsDirectory, - fsUnavailable, -} from './fs-types' - -const encoder = new TextEncoder() -const decoder = new TextDecoder() - -let fsUser: FSUser // Used to determine if changes came from this user or another - -const SanityCheckFolder = '/SanityCheckFolder' - -let handleError: FSErrorHandler = (e: FSError) => { - let error = Error(`FS Error: ${JSON.stringify(e)}`) - error.name = e.code - return error -} - -export function setErrorHandler(handler: FSErrorHandler): void { - handleError = handler -} - -const missingFileError = (path: string) => handleError(enoent(path)) -const existingFileError = (path: string) => handleError(eexist(path)) -const isDirectoryError = (path: string) => handleError(eisdir(path)) -const isNotDirectoryError = (path: string) => handleError(enotdir(path)) -const isUnavailableError = (path: string) => handleError(fsUnavailable(path)) - -export async function initializeFS( - storeName: string, - user: FSUser, - driver: string = INDEXEDDB, -): Promise { - fsUser = user - await initializeStore(storeName, driver) - await simpleCreateDirectoryIfMissing('/') -} - -async function withAvailableFS( - path: string, - fn: (path: string) => AsyncFSResult, -): Promise { - const result = await fn(path) - if (isRight(result)) { - return result.value - } else { - return Promise.reject(isUnavailableError(path)) - } -} - -const getItem = (path: string) => withAvailableFS(path, getItemCore) -const keys = () => withAvailableFS('', (_path: string) => keysCore()) -const removeItem = (path: string) => withAvailableFS(path, removeItemCore) -const setItem = (path: string, v: FSNode) => withAvailableFS(path, (p) => setItemCore(p, v)) - -export async function exists(path: string): Promise { - const value = await getItem(path) - return value != null -} - -export async function pathIsDirectory(path: string): Promise { - const node = await getItem(path) - return node != null && isDirectory(node) -} - -export async function pathIsFile(path: string): Promise { - const node = await getItem(path) - return node != null && isFile(node) -} - -export async function pathIsFileWithUnsavedContent(path: string): Promise { - const node = await getItem(path) - return node != null && isFile(node) && node.unsavedContent != null -} - -async function getNode(path: string): Promise { - const node = await getItem(path) - if (node == null) { - return Promise.reject(missingFileError(path)) - } else { - return node - } -} - -async function getFile(path: string): Promise { - const node = await getNode(path) - if (isFile(node)) { - return node - } else { - return Promise.reject(isDirectoryError(path)) - } -} - -export async function readFile(path: string): Promise { - return getFile(path) -} - -export async function readFileSavedContent(path: string): Promise { - const fileNode = await getFile(path) - return fileNode.content -} - -export async function readFileUnsavedContent(path: string): Promise { - const fileNode = await getFile(path) - return fileNode.unsavedContent -} - -export interface StoredFile { - content: string - unsavedContent: string | null -} - -export async function readFileAsUTF8(path: string): Promise { - const { content, unsavedContent } = await getFile(path) - return { - content: decoder.decode(content), - unsavedContent: unsavedContent == null ? null : decoder.decode(unsavedContent), - } -} - -export async function readFileSavedContentAsUTF8(path: string): Promise { - const { content } = await readFileAsUTF8(path) - return content -} - -export async function readFileUnsavedContentAsUTF8(path: string): Promise { - const { unsavedContent } = await readFileAsUTF8(path) - return unsavedContent -} - -function fsStatForNode(node: FSNode): FSStat { - return { - type: node.type, - ctime: node.ctime, - mtime: node.mtime, - lastSavedTime: node.lastSavedTime, - size: isFile(node) ? node.content.length : 0, - sourceOfLastChange: node.sourceOfLastChange, - } -} - -export async function stat(path: string): Promise { - const node = await getNode(path) - return fsStatForNode(node) -} - -export function getDescendentPathsWithAllPaths( - path: string, - allPaths: Array, -): Array { - return allPaths.filter((k) => k != path && k.startsWith(path)) -} - -export async function getDescendentPaths(path: string): Promise { - const allPaths = await keys() - return getDescendentPathsWithAllPaths(path, allPaths) -} - -async function targetsForOperation(path: string, recursive: boolean): Promise { - if (recursive) { - const allDescendents = await getDescendentPaths(path) - let result = [path, ...allDescendents] - result.sort() - result.reverse() - return result - } else { - return [path] - } -} - -function getParentPath(path: string): string | null { - const withoutLeadingOrTrailingSlash = stripLeadingSlash(stripTrailingSlash(path)) - const pathElems = withoutLeadingOrTrailingSlash.split('/') - if (pathElems.length <= 1) { - return null - } else { - return `/${pathElems.slice(0, -1).join('/')}` - } -} - -function filenameOfPath(path: string): string { - const target = path.endsWith('/') ? path.slice(0, -1) : path - const lastSlashIndex = target.lastIndexOf('/') - return lastSlashIndex >= 0 ? path.slice(lastSlashIndex + 1) : path -} - -export function childPathsWithAllPaths(path: string, allPaths: Array): Array { - const allDescendents = getDescendentPathsWithAllPaths(path, allPaths) - const pathAsDir = stripTrailingSlash(path) - return allDescendents.filter((k) => getParentPath(k) === pathAsDir) -} - -export async function childPaths(path: string): Promise { - const allDescendents = await getDescendentPaths(path) - return childPathsWithAllPaths(path, allDescendents) -} - -async function getDirectory(path: string): Promise { - const node = await getNode(path) - if (isDirectory(node)) { - return node - } else { - return Promise.reject(isNotDirectoryError(path)) - } -} - -async function getParent(path: string): Promise { - // null signifies we're already at the root - const parentPath = getParentPath(path) - if (parentPath == null) { - return null - } else { - const parentDir = await getDirectory(parentPath) - return { - path: parentPath, - node: parentDir, - } - } -} - -export async function readDirectory(path: string): Promise { - await getDirectory(path) // Ensure the path exists and is a directory - const children = await childPaths(path) - return children.map(filenameOfPath) -} - -export async function createDirectory(path: string): Promise { - const parent = await getParent(path) - const pathExists = await getItem(path) - if (pathExists != null) { - return Promise.reject(existingFileError(path)) - } - - await setItem(path, newFSDirectory(fsUser)) - if (parent != null) { - await markModified(parent) - } -} - -function allPathsUpToPath(path: string): string[] { - const directories = path.split('/') - const { paths: allPaths } = directories.reduce( - ({ paths, workingPath }, next) => { - const nextPath = appendToPath(workingPath, next) - return { - paths: paths.concat(nextPath), - workingPath: nextPath, - } - }, - { paths: ['/'], workingPath: '/' }, - ) - return allPaths -} - -async function simpleCreateDirectoryIfMissing(path: string): Promise { - const existingNode = await getItem(path) - if (existingNode == null) { - await setItem(path, newFSDirectory(fsUser)) - - // Attempt to mark the parent as modified, but don't fail if it doesn't exist - // since it might not have been created yet - const parentPath = getParentPath(path) - if (parentPath != null) { - const parentNode = await getItem(parentPath) - if (parentNode != null) { - await markModified({ path: parentPath, node: parentNode }) - } - } - } else if (isFile(existingNode)) { - return Promise.reject(isNotDirectoryError(path)) - } -} - -export async function ensureDirectoryExists(path: string): Promise { - const allPaths = allPathsUpToPath(path) - for (const pathToCreate of allPaths) { - await simpleCreateDirectoryIfMissing(pathToCreate) - } -} - -export async function writeFile( - path: string, - content: Uint8Array, - unsavedContent: Uint8Array | null, -): Promise { - const parent = await getParent(path) - const maybeExistingFile = await getItem(path) - if (maybeExistingFile != null && isDirectory(maybeExistingFile)) { - return Promise.reject(isDirectoryError(path)) - } - - const now = Date.now() - const fileCTime = maybeExistingFile == null ? now : maybeExistingFile.ctime - const lastSavedTime = - unsavedContent == null || maybeExistingFile == null ? now : maybeExistingFile.lastSavedTime - const fileToWrite = fsFile(content, unsavedContent, fileCTime, now, lastSavedTime, fsUser) - await setItem(path, fileToWrite) - if (parent != null) { - await markModified(parent) - } -} - -export async function writeFileSavedContent(path: string, content: Uint8Array): Promise { - return writeFile(path, content, null) -} - -export async function writeFileUnsavedContent( - path: string, - unsavedContent: Uint8Array, -): Promise { - const savedContent = await readFileSavedContent(path) - return writeFile(path, savedContent, unsavedContent) -} - -export async function writeFileAsUTF8( - path: string, - content: string, - unsavedContent: string | null, -): Promise { - return writeFile( - path, - encoder.encode(content), - unsavedContent == null ? null : encoder.encode(unsavedContent), - ) -} - -export async function writeFileSavedContentAsUTF8( - path: string, - savedContent: string, -): Promise { - return writeFileAsUTF8(path, savedContent, null) -} - -export async function writeFileUnsavedContentAsUTF8( - path: string, - unsavedContent: string, -): Promise { - return writeFileUnsavedContent(path, encoder.encode(unsavedContent)) -} - -export async function clearFileUnsavedContent(path: string): Promise { - const savedContent = await readFileSavedContent(path) - return writeFileSavedContent(path, savedContent) -} - -function updateMTime(node: FSNode): FSNode { - const now = Date.now() - if (isFile(node)) { - const lastSavedTime = node.unsavedContent == null ? now : node.lastSavedTime - return fsFile(node.content, node.unsavedContent, node.ctime, now, lastSavedTime, fsUser) - } else { - return fsDirectory(node.ctime, now, fsUser) - } -} - -async function markModified(nodeWithPath: FSNodeWithPath): Promise { - await setItem(nodeWithPath.path, updateMTime(nodeWithPath.node)) - resetPollingFrequency() -} - -async function uncheckedMove(oldPath: string, newPath: string): Promise { - const node = await getNode(oldPath) - await setItem(newPath, updateMTime(node)) - await removeItem(oldPath) -} - -export async function rename(oldPath: string, newPath: string): Promise { - const oldParent = await getParent(oldPath) - const newParent = await getParent(newPath) - - const pathsToMove = await targetsForOperation(oldPath, true) - const toNewPath = (p: string) => `${newPath}${p.slice(0, oldPath.length)}` - await Promise.all( - pathsToMove.map((pathToMove) => uncheckedMove(pathToMove, toNewPath(pathToMove))), - ) - if (oldParent != null) { - await markModified(oldParent) - } - if (newParent != null) { - await markModified(newParent) - } -} - -export async function deletePath(path: string, recursive: boolean): Promise { - const parent = await getParent(path) - const targets = await targetsForOperation(path, recursive) - - // Really this should fail if recursive isn't set to true when trying to delete a - // non-empty directory, but for some reason VSCode doesn't provide an error suitable for that - for (const target of targets) { - await removeItem(target) - } - - if (parent != null) { - await markModified(parent) - } - return targets -} - -interface WatchConfig { - recursive: boolean - onCreated: (path: string) => void - onModified: (path: string, modifiedBySelf: boolean) => void - onDeleted: (path: string) => void -} - -let watchTimeout: number | null = null -let watchedPaths: Map = new Map() -let lastModifiedTSs: Map = new Map() - -const MIN_POLLING_TIMEOUT = 128 -const MAX_POLLING_TIMEOUT = MIN_POLLING_TIMEOUT * Math.pow(2, 2) // Max out at 512ms -let POLLING_TIMEOUT = MIN_POLLING_TIMEOUT - -let reducePollingAttemptsCount = 0 - -function reducePollingFrequency() { - if (POLLING_TIMEOUT < MAX_POLLING_TIMEOUT) { - reducePollingAttemptsCount++ - if (reducePollingAttemptsCount >= 5) { - reducePollingAttemptsCount = 0 - POLLING_TIMEOUT = POLLING_TIMEOUT + MIN_POLLING_TIMEOUT - } - } -} - -function resetPollingFrequency() { - reducePollingAttemptsCount = 0 - POLLING_TIMEOUT = MIN_POLLING_TIMEOUT -} - -function watchPath(path: string, config: WatchConfig) { - watchedPaths.set(path, config) - lastModifiedTSs.set(path, Date.now()) -} - -function isFSUnavailableError(e: unknown): boolean { - return (e as any)?.name === 'FS_UNAVAILABLE' -} - -type FileModifiedStatus = 'modified' | 'not-modified' | 'unknown' - -async function onPolledWatch(paths: Map): Promise> { - const allKeys = await keys() - const results = Array.from(paths).map(async ([path, config]) => { - const { recursive, onCreated, onModified, onDeleted } = config - - try { - const node = await getItem(path) - if (node == null) { - watchedPaths.delete(path) - lastModifiedTSs.delete(path) - onDeleted(path) - return 'modified' - } else { - const stats = fsStatForNode(node) - - const modifiedTS = stats.mtime - const wasModified = modifiedTS > (lastModifiedTSs.get(path) ?? 0) - const modifiedBySelf = stats.sourceOfLastChange === fsUser - - if (isDirectory(node)) { - if (recursive) { - const children = childPathsWithAllPaths(path, allKeys) - const unsupervisedChildren = children.filter((p) => !watchedPaths.has(p)) - unsupervisedChildren.forEach((childPath) => { - watchPath(childPath, config) - onCreated(childPath) - }) - if (unsupervisedChildren.length > 0) { - onModified(path, modifiedBySelf) - lastModifiedTSs.set(path, modifiedTS) - return 'modified' - } - } - } else { - if (wasModified) { - onModified(path, modifiedBySelf) - lastModifiedTSs.set(path, modifiedTS) - return 'modified' - } - } - - return 'not-modified' - } - } catch (e) { - if (isFSUnavailableError(e)) { - // Explicitly handle unavailable errors here by removing the watchers, then re-throw - watchedPaths.delete(path) - lastModifiedTSs.delete(path) - throw e - } - // Something was changed mid-poll, likely the file or its parent was deleted. We'll catch it on the next poll. - return 'unknown' - } - }) - return Promise.all(results) -} - -async function polledWatch(): Promise { - let promises: Array>> = [] - promises.push(onPolledWatch(watchedPaths)) - - const results = await Promise.all(promises).then((nestedResults) => nestedResults.flat()) - - let shouldReducePollingFrequency = true - for (var i = 0, len = results.length; i < len; i++) { - if (i in results) { - const fileModifiedStatus = results[i] - if (fileModifiedStatus === 'modified') { - resetPollingFrequency() - shouldReducePollingFrequency = false - return - } else if (fileModifiedStatus === 'unknown') { - shouldReducePollingFrequency = false - } - } - } - - if (shouldReducePollingFrequency) { - reducePollingFrequency() - } -} - -export async function watch( - target: string, - recursive: boolean, - onCreated: (path: string) => void, - onModified: (path: string, modifiedBySelf: boolean) => void, - onDeleted: (path: string) => void, - onIndexedDBFailure: () => void, -): Promise { - try { - await simpleCreateDirectoryIfMissing(SanityCheckFolder) - const fileExists = await exists(target) - if (fileExists) { - // This has the limitation that calling `watch` on a path will replace any existing subscriber - const startWatchingPath = (path: string) => - watchPath(path, { - recursive: recursive, - onCreated: onCreated, - onModified: onModified, - onDeleted: onDeleted, - }) - - const targets = await targetsForOperation(target, recursive) - targets.forEach(startWatchingPath) - - if (watchTimeout == null) { - async function pollThenFireAgain(): Promise { - try { - await polledWatch() - } catch (e) { - if (isFSUnavailableError(e)) { - onIndexedDBFailure() - } else { - throw e - } - } - - watchTimeout = setTimeout(pollThenFireAgain, POLLING_TIMEOUT) as any - } - - watchTimeout = setTimeout(pollThenFireAgain, POLLING_TIMEOUT) as any - } - } - } catch (e) { - if (isFSUnavailableError(e)) { - onIndexedDBFailure() - } else { - throw e - } - } -} - -export async function stopWatching(target: string, recursive: boolean) { - const stopWatchingPath = (path: string) => { - watchedPaths.delete(path) - } - - const targets = await targetsForOperation(target, recursive) - targets.forEach(stopWatchingPath) -} - -export function stopWatchingAll() { - if (watchTimeout != null) { - clearTimeout(watchTimeout) - watchTimeout = null - } - watchedPaths = new Map() - lastModifiedTSs = new Map() -} - -export function defer(): Promise & { - resolve: (value?: T) => void - reject: (reason?: any) => void -} { - var res, rej - - var promise = new Promise((resolve, reject) => { - res = resolve - rej = reject - }) - Object.defineProperty(promise, 'resolve', { value: res }) - Object.defineProperty(promise, 'reject', { value: rej }) - - return promise as any -} diff --git a/utopia-vscode-common/src/index.ts b/utopia-vscode-common/src/index.ts index 8d78399fe43e..67e5855a1424 100644 --- a/utopia-vscode-common/src/index.ts +++ b/utopia-vscode-common/src/index.ts @@ -1,11 +1,7 @@ export * from './path-utils' -export * from './mailbox' -export * from './messages' -export * from './fs/fs-types' -export * from './fs/fs-utils' export * from './prettier-utils' +export * from './messages-to-utopia' +export * from './messages-to-vscode' export * from './utopia-vscode-config' -export * from './vscode-communication' -export * from './window-messages' export const ProjectIDPlaceholderPrefix = 'PLACEHOLDER_DURING_LOADING' diff --git a/utopia-vscode-common/src/lite-either.ts b/utopia-vscode-common/src/lite-either.ts deleted file mode 100644 index 3feb989cf214..000000000000 --- a/utopia-vscode-common/src/lite-either.ts +++ /dev/null @@ -1,65 +0,0 @@ -// TODO Move either.ts here? - -// Often treated as the "failure" case. -export interface Left { - type: 'LEFT' - value: L -} - -// Often treated as the "success" case. -export interface Right { - type: 'RIGHT' - value: R -} - -// Usually treated as having bias to the right. -export type Either = Left | Right - -export function left(value: L): Either { - return { - type: 'LEFT', - value: value, - } -} - -export function right(value: R): Either { - return { - type: 'RIGHT', - value: value, - } -} - -// http://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Either.html#v:isLeft -export function isLeft(either: Either): either is Left { - return either.type === 'LEFT' -} - -// http://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Either.html#v:isRight -export function isRight(either: Either): either is Right { - return either.type === 'RIGHT' -} - -// http://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Either.html#v:either -export function foldEither( - foldLeft: (l: L) => X, - foldRight: (r: R) => X, - either: Either, -): X { - if (isLeft(either)) { - return foldLeft(either.value) - } else { - return foldRight(either.value) - } -} - -// http://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Functor.html#v:fmap -export function mapEither( - transform: (r: R1) => R2, - either: Either, -): Either { - if (isLeft(either)) { - return either - } else { - return right(transform(either.value)) - } -} diff --git a/utopia-vscode-common/src/mailbox.ts b/utopia-vscode-common/src/mailbox.ts deleted file mode 100644 index 79bd92ab4c8f..000000000000 --- a/utopia-vscode-common/src/mailbox.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { - childPaths, - deletePath, - ensureDirectoryExists, - exists, - readDirectory, - readFileSavedContentAsUTF8, - writeFileSavedContentAsUTF8, -} from './fs/fs-utils' -import type { FromVSCodeMessage, ToVSCodeMessage } from './messages' -import { appendToPath } from './path-utils' - -type Mailbox = 'VSCODE_MAILBOX' | 'UTOPIA_MAILBOX' -export const VSCodeInbox: Mailbox = 'VSCODE_MAILBOX' -export const UtopiaInbox: Mailbox = 'UTOPIA_MAILBOX' - -let inbox: Mailbox -let outbox: Mailbox -let onMessageCallback: (message: any) => void -let lastSentMessage: number = 0 -let lastConsumedMessage: number = -1 -let mailboxLastClearedTimestamp: number = Date.now() -let queuedMessages: Array = [] -const MIN_POLLING_TIMEOUT = 8 -const MAX_POLLING_TIMEOUT = MIN_POLLING_TIMEOUT * Math.pow(2, 4) // Max out at 128ms -let POLLING_TIMEOUT = MIN_POLLING_TIMEOUT -let pollTimeout: any | null = null - -let reducePollingAttemptsCount = 0 - -function reducePollingFrequency() { - if (POLLING_TIMEOUT < MAX_POLLING_TIMEOUT) { - reducePollingAttemptsCount++ - if (reducePollingAttemptsCount >= 5) { - reducePollingAttemptsCount = 0 - POLLING_TIMEOUT = POLLING_TIMEOUT * 2 - } - } -} - -function resetPollingFrequency() { - reducePollingAttemptsCount = 0 - POLLING_TIMEOUT = MIN_POLLING_TIMEOUT -} - -function lastConsumedMessageKey(mailbox: Mailbox): string { - return `/${mailbox}_LAST_CONSUMED` -} - -function mailboxClearedAtTimestampKey(mailbox: Mailbox): string { - return `/${mailbox}_CLEARED` -} - -function pathForMailbox(mailbox: Mailbox): string { - return `/${mailbox}` -} - -function pathForMessage(messageName: string, mailbox: Mailbox): string { - return appendToPath(pathForMailbox(mailbox), messageName) -} - -const pathForInboxMessage = (messageName: string) => pathForMessage(messageName, inbox) -const pathForOutboxMessage = (messageName: string) => pathForMessage(messageName, outbox) - -function generateMessageName(): string { - return `${lastSentMessage++}` -} - -export async function sendMessage(message: ToVSCodeMessage | FromVSCodeMessage): Promise { - resetPollingFrequency() - - if (outbox == null) { - queuedMessages.push(message) - } else { - return sendNamedMessage(generateMessageName(), JSON.stringify(message)) - } -} - -async function sendNamedMessage(messageName: string, content: string): Promise { - return writeFileSavedContentAsUTF8(pathForOutboxMessage(messageName), content) -} - -function maxMessageNumber(messageNames: Array, minValue: number = 0): number { - return Math.max(minValue, ...messageNames.map((messageName) => Number.parseInt(messageName))) -} - -async function initOutbox(outboxToUse: Mailbox): Promise { - await ensureMailboxExists(outboxToUse) - const previouslySentMessages = await readDirectory(pathForMailbox(outboxToUse)) - lastSentMessage = maxMessageNumber(previouslySentMessages) - - outbox = outboxToUse - if (queuedMessages.length > 0) { - queuedMessages.forEach(sendMessage) - queuedMessages = [] - } -} - -async function receiveMessage( - messageName: string, - parseMessage: (msg: string) => T, -): Promise { - const messagePath = pathForInboxMessage(messageName) - const content = await readFileSavedContentAsUTF8(messagePath) - return parseMessage(content) -} - -async function waitForPathToExist(path: string, maxWaitTime: number = 5000): Promise { - if (maxWaitTime >= 0) { - const doesItExist: boolean = await exists(path) - if (!doesItExist) { - return waitForPathToExist(path, maxWaitTime - 100) - } else { - return Promise.resolve() - } - } else { - return Promise.reject(`Waited too long for ${path} to exist.`) - } -} - -async function checkAndResetIfMailboxCleared(mailbox: Mailbox): Promise { - const mailboxClearedAtTimestamp = await getMailboxClearedAtTimestamp(mailbox) - if (mailboxClearedAtTimestamp > mailboxLastClearedTimestamp) { - // The mailbox was cleared since we last polled it, meaning our last consumed message - // count is now invalid, and we need to start consuming messages from the beginning again. - lastConsumedMessage = -1 - mailboxLastClearedTimestamp = mailboxClearedAtTimestamp - } -} - -async function pollInbox(parseMessage: (msg: string) => T): Promise { - await checkAndResetIfMailboxCleared(inbox) - - const mailboxPath = pathForMailbox(inbox) - waitForPathToExist(mailboxPath) - const allMessages = await readDirectory(mailboxPath) - - // Filter messages to only those that haven't been processed yet. We do this rather than deleting processed - // messages so that multiple instances in different browser tabs won't drive over eachother. - const messagesToProcess = allMessages.filter( - (messageName) => Number.parseInt(messageName) > lastConsumedMessage, - ) - if (messagesToProcess.length > 0) { - try { - const messages = await Promise.all( - messagesToProcess.map((m) => receiveMessage(m, parseMessage)), - ) - lastConsumedMessage = maxMessageNumber(messagesToProcess, lastConsumedMessage) - await updateLastConsumedMessageFile(inbox, lastConsumedMessage) - messages.forEach(onMessageCallback) - } catch (e) { - // It's possible that the mailbox was cleared whilst something was trying to read the messages. - // If that happens, we bail out of this poll, and the call `checkAndResetIfMailboxCleared` will - // correct things on the next poll - } - resetPollingFrequency() - } else { - reducePollingFrequency() - } - pollTimeout = setTimeout(() => pollInbox(parseMessage), POLLING_TIMEOUT) -} - -async function initInbox( - inboxToUse: Mailbox, - parseMessage: (msg: string) => T, - onMessage: (message: T) => void, -): Promise { - inbox = inboxToUse - await ensureMailboxExists(inboxToUse) - mailboxLastClearedTimestamp = await getMailboxClearedAtTimestamp(inbox) - lastConsumedMessage = await getLastConsumedMessageNumber(inbox) - onMessageCallback = onMessage - pollInbox(parseMessage) -} - -async function ensureMailboxExists(mailbox: Mailbox): Promise { - await ensureDirectoryExists(pathForMailbox(mailbox)) -} - -async function clearMailbox(mailbox: Mailbox): Promise { - const messagePaths = await childPaths(pathForMailbox(mailbox)) - await Promise.all(messagePaths.map((messagePath) => deletePath(messagePath, false))) -} - -async function clearLastConsumedMessageFile(mailbox: Mailbox): Promise { - await deletePath(lastConsumedMessageKey(mailbox), false) -} - -async function updateLastConsumedMessageFile(mailbox: Mailbox, value: number): Promise { - await writeFileSavedContentAsUTF8(lastConsumedMessageKey(mailbox), `${value}`) -} - -async function getLastConsumedMessageNumber(mailbox: Mailbox): Promise { - const lastConsumedMessageValueExists = await exists(lastConsumedMessageKey(mailbox)) - if (lastConsumedMessageValueExists) { - try { - const lastConsumedMessageName = await readFileSavedContentAsUTF8( - lastConsumedMessageKey(mailbox), - ) - return Number.parseInt(lastConsumedMessageName) - } catch (e) { - // This can be cleared by the VSCode Bridge in between the above line and now, in which case we want to consume all messages from the start - return -1 - } - } else { - return -1 - } -} - -async function updateMailboxClearedAtTimestamp(mailbox: Mailbox, timestamp: number): Promise { - await writeFileSavedContentAsUTF8(mailboxClearedAtTimestampKey(mailbox), `${timestamp}`) -} - -async function getMailboxClearedAtTimestamp(mailbox: Mailbox): Promise { - const mailboxClearedAtTimestampExists = await exists(mailboxClearedAtTimestampKey(mailbox)) - if (mailboxClearedAtTimestampExists) { - const mailboxClearedAtTimestamp = await readFileSavedContentAsUTF8( - mailboxClearedAtTimestampKey(mailbox), - ) - return Number.parseInt(mailboxClearedAtTimestamp) - } else { - return -1 - } -} - -export async function clearBothMailboxes(): Promise { - await ensureMailboxExists(UtopiaInbox) - await clearMailbox(UtopiaInbox) - await ensureMailboxExists(VSCodeInbox) - await clearMailbox(VSCodeInbox) - await clearLastConsumedMessageFile(UtopiaInbox) - await clearLastConsumedMessageFile(VSCodeInbox) - await updateMailboxClearedAtTimestamp(UtopiaInbox, Date.now()) - await updateMailboxClearedAtTimestamp(VSCodeInbox, Date.now()) -} - -export function stopPollingMailbox(): void { - if (pollTimeout != null) { - clearTimeout(pollTimeout) - pollTimeout = null - lastConsumedMessage = -1 - lastSentMessage = 0 - } -} - -export async function initMailbox( - inboxToUse: Mailbox, - parseMessage: (msg: string) => T, - onMessage: (message: T) => void, -): Promise { - await initOutbox(inboxToUse === VSCodeInbox ? UtopiaInbox : VSCodeInbox) - await initInbox(inboxToUse, parseMessage, onMessage) -} diff --git a/utopia-vscode-common/src/messages-to-utopia.ts b/utopia-vscode-common/src/messages-to-utopia.ts new file mode 100644 index 000000000000..b63d6e5c2109 --- /dev/null +++ b/utopia-vscode-common/src/messages-to-utopia.ts @@ -0,0 +1,162 @@ +import type { UtopiaVSCodeConfig } from './utopia-vscode-config' + +export interface MessageListenersReady { + type: 'MESSAGE_LISTENERS_READY' +} + +export function messageListenersReady(): MessageListenersReady { + return { + type: 'MESSAGE_LISTENERS_READY', + } +} + +export function isMessageListenersReady( + messageData: unknown, +): messageData is MessageListenersReady { + return ( + typeof messageData === 'object' && (messageData as any)?.['type'] === 'MESSAGE_LISTENERS_READY' + ) +} + +interface StoredFile { + content: string + unsavedContent: string | null +} + +export interface VSCodeFileChange { + type: 'VSCODE_FILE_CHANGE' + filePath: string + fileContent: StoredFile +} + +export function vsCodeFileChange(filePath: string, fileContent: StoredFile): VSCodeFileChange { + return { + type: 'VSCODE_FILE_CHANGE', + filePath: filePath, + fileContent: fileContent, + } +} + +export function isVSCodeFileChange(messageData: unknown): messageData is VSCodeFileChange { + return typeof messageData === 'object' && (messageData as any)?.['type'] === 'VSCODE_FILE_CHANGE' +} + +export interface VSCodeFileDelete { + type: 'VSCODE_FILE_DELETE' + filePath: string +} + +export function vsCodeFileDelete(filePath: string): VSCodeFileDelete { + return { + type: 'VSCODE_FILE_DELETE', + filePath: filePath, + } +} + +export function isVSCodeFileDelete(messageData: unknown): messageData is VSCodeFileDelete { + return typeof messageData === 'object' && (messageData as any)?.['type'] === 'VSCODE_FILE_DELETE' +} + +export interface VSCodeBridgeReady { + type: 'VSCODE_BRIDGE_READY' +} + +export function vsCodeBridgeReady(): VSCodeBridgeReady { + return { + type: 'VSCODE_BRIDGE_READY', + } +} + +export function isVSCodeBridgeReady(messageData: unknown): messageData is VSCodeBridgeReady { + return typeof messageData === 'object' && (messageData as any)?.['type'] === 'VSCODE_BRIDGE_READY' +} + +export interface EditorCursorPositionChanged { + type: 'EDITOR_CURSOR_POSITION_CHANGED' + filePath: string + line: number + column: number +} + +export function editorCursorPositionChanged( + filePath: string, + line: number, + column: number, +): EditorCursorPositionChanged { + return { + type: 'EDITOR_CURSOR_POSITION_CHANGED', + filePath: filePath, + line: line, + column: column, + } +} + +export function isEditorCursorPositionChanged( + messageData: unknown, +): messageData is EditorCursorPositionChanged { + return ( + typeof messageData === 'object' && + (messageData as any)?.['type'] === 'EDITOR_CURSOR_POSITION_CHANGED' + ) +} + +export interface UtopiaVSCodeConfigValues { + type: 'UTOPIA_VSCODE_CONFIG_VALUES' + config: UtopiaVSCodeConfig +} + +export function utopiaVSCodeConfigValues(config: UtopiaVSCodeConfig): UtopiaVSCodeConfigValues { + return { + type: 'UTOPIA_VSCODE_CONFIG_VALUES', + config: config, + } +} + +export function isUtopiaVSCodeConfigValues( + messageData: unknown, +): messageData is UtopiaVSCodeConfigValues { + return ( + typeof messageData === 'object' && + (messageData as any)?.['type'] === 'UTOPIA_VSCODE_CONFIG_VALUES' + ) +} + +export interface VSCodeReady { + type: 'VSCODE_READY' +} + +export function vsCodeReady(): VSCodeReady { + return { + type: 'VSCODE_READY', + } +} + +export function isVSCodeReady(messageData: unknown): messageData is VSCodeReady { + return typeof messageData === 'object' && (messageData as any)?.['type'] === 'VSCODE_READY' +} + +export interface ClearLoadingScreen { + type: 'CLEAR_LOADING_SCREEN' +} + +export function clearLoadingScreen(): ClearLoadingScreen { + return { + type: 'CLEAR_LOADING_SCREEN', + } +} + +export function isClearLoadingScreen(messageData: unknown): messageData is ClearLoadingScreen { + return ( + typeof messageData === 'object' && (messageData as any)?.['type'] === 'CLEAR_LOADING_SCREEN' + ) +} + +export type FromVSCodeToUtopiaMessage = + | MessageListenersReady + | VSCodeFileChange + | VSCodeFileDelete + | VSCodeBridgeReady + | EditorCursorPositionChanged + | UtopiaVSCodeConfigValues + | VSCodeReady + | ClearLoadingScreen diff --git a/utopia-vscode-common/src/messages-to-vscode.ts b/utopia-vscode-common/src/messages-to-vscode.ts new file mode 100644 index 000000000000..2b31ee0b8a22 --- /dev/null +++ b/utopia-vscode-common/src/messages-to-vscode.ts @@ -0,0 +1,328 @@ +import type { UtopiaVSCodeConfig } from './utopia-vscode-config' + +export interface ProjectDirectory { + type: 'PROJECT_DIRECTORY' + filePath: string +} + +export function projectDirectory(filePath: string): ProjectDirectory { + return { + type: 'PROJECT_DIRECTORY', + filePath: filePath, + } +} + +export interface ProjectTextFile { + type: 'PROJECT_TEXT_FILE' + filePath: string + savedContent: string + unsavedContent: string | null +} + +export function projectTextFile( + filePath: string, + savedContent: string, + unsavedContent: string | null, +): ProjectTextFile { + return { + type: 'PROJECT_TEXT_FILE', + filePath: filePath, + savedContent: savedContent, + unsavedContent: unsavedContent, + } +} + +export type ProjectFile = ProjectDirectory | ProjectTextFile + +export interface InitProject { + type: 'INIT_PROJECT' + projectContents: Array + openFilePath: string | null +} + +export function initProject( + projectContents: Array, + openFilePath: string | null, +): InitProject { + return { + type: 'INIT_PROJECT', + projectContents: projectContents, + openFilePath: openFilePath, + } +} + +export function isInitProject(messageData: unknown): messageData is InitProject { + return typeof messageData === 'object' && (messageData as any)?.['type'] === 'INIT_PROJECT' +} + +export interface WriteProjectFileChange { + type: 'WRITE_PROJECT_FILE' + projectFile: ProjectFile +} + +export function writeProjectFileChange(projectFile: ProjectFile): WriteProjectFileChange { + return { + type: 'WRITE_PROJECT_FILE', + projectFile: projectFile, + } +} + +export function isWriteProjectFileChange( + messageData: unknown, +): messageData is WriteProjectFileChange { + return typeof messageData === 'object' && (messageData as any)?.['type'] === 'WRITE_PROJECT_FILE' +} + +export interface DeletePathChange { + type: 'DELETE_PATH' + fullPath: string + recursive: boolean +} + +export function deletePathChange(fullPath: string, recursive: boolean): DeletePathChange { + return { + type: 'DELETE_PATH', + fullPath: fullPath, + recursive: recursive, + } +} + +export function isDeletePathChange(messageData: unknown): messageData is DeletePathChange { + return typeof messageData === 'object' && (messageData as any)?.['type'] === 'DELETE_PATH' +} + +export interface EnsureDirectoryExistsChange { + type: 'ENSURE_DIRECTORY_EXISTS' + fullPath: string +} + +export function ensureDirectoryExistsChange(fullPath: string): EnsureDirectoryExistsChange { + return { + type: 'ENSURE_DIRECTORY_EXISTS', + fullPath: fullPath, + } +} + +export function isEnsureDirectoryExistsChange( + messageData: unknown, +): messageData is EnsureDirectoryExistsChange { + return ( + typeof messageData === 'object' && (messageData as any)?.['type'] === 'ENSURE_DIRECTORY_EXISTS' + ) +} + +export interface OpenFileMessage { + type: 'OPEN_FILE' + filePath: string + bounds: Bounds | null +} + +export function openFileMessage(filePath: string, bounds: Bounds | null): OpenFileMessage { + return { + type: 'OPEN_FILE', + filePath: filePath, + bounds: bounds, + } +} + +export function isOpenFileMessage(messageData: unknown): messageData is OpenFileMessage { + return typeof messageData === 'object' && (messageData as any)?.['type'] === 'OPEN_FILE' +} + +export type DecorationRangeType = 'selection' | 'highlight' + +export interface Bounds { + startLine: number + startCol: number + endLine: number + endCol: number +} + +export interface BoundsInFile extends Bounds { + filePath: string +} + +export function boundsInFile( + filePath: string, + startLine: number, + startCol: number, + endLine: number, + endCol: number, +): BoundsInFile { + return { + filePath: filePath, + startLine: startLine, + startCol: startCol, + endLine: endLine, + endCol: endCol, + } +} + +export interface DecorationRange extends BoundsInFile { + rangeType: DecorationRangeType +} + +export function decorationRange( + rangeType: DecorationRangeType, + filePath: string, + startLine: number, + startCol: number, + endLine: number, + endCol: number, +): DecorationRange { + return { + rangeType: rangeType, + filePath: filePath, + startLine: startLine, + startCol: startCol, + endLine: endLine, + endCol: endCol, + } +} + +export interface UpdateDecorationsMessage { + type: 'UPDATE_DECORATIONS' + decorations: Array +} + +export function updateDecorationsMessage( + decorations: Array, +): UpdateDecorationsMessage { + return { + type: 'UPDATE_DECORATIONS', + decorations: decorations, + } +} + +export function isUpdateDecorationsMessage( + messageData: unknown, +): messageData is UpdateDecorationsMessage { + return typeof messageData === 'object' && (messageData as any)?.['type'] === 'UPDATE_DECORATIONS' +} + +export type ForceNavigation = 'do-not-force-navigation' | 'force-navigation' + +export interface SelectedElementChanged { + type: 'SELECTED_ELEMENT_CHANGED' + boundsInFile: BoundsInFile + forceNavigation: ForceNavigation +} + +export function selectedElementChanged( + bounds: BoundsInFile, + forceNavigation: ForceNavigation, +): SelectedElementChanged { + return { + type: 'SELECTED_ELEMENT_CHANGED', + boundsInFile: bounds, + forceNavigation: forceNavigation, + } +} + +export function isSelectedElementChanged( + messageData: unknown, +): messageData is SelectedElementChanged { + return ( + typeof messageData === 'object' && (messageData as any)?.['type'] === 'SELECTED_ELEMENT_CHANGED' + ) +} + +export interface GetUtopiaVSCodeConfig { + type: 'GET_UTOPIA_VSCODE_CONFIG' +} + +export function getUtopiaVSCodeConfig(): GetUtopiaVSCodeConfig { + return { + type: 'GET_UTOPIA_VSCODE_CONFIG', + } +} + +export function isGetUtopiaVSCodeConfig( + messageData: unknown, +): messageData is GetUtopiaVSCodeConfig { + return ( + typeof messageData === 'object' && (messageData as any)?.['type'] === 'GET_UTOPIA_VSCODE_CONFIG' + ) +} + +export interface SetFollowSelectionConfig { + type: 'SET_FOLLOW_SELECTION_CONFIG' + enabled: boolean +} + +export function setFollowSelectionConfig(enabled: boolean): SetFollowSelectionConfig { + return { + type: 'SET_FOLLOW_SELECTION_CONFIG', + enabled: enabled, + } +} + +export function isSetFollowSelectionConfig( + messageData: unknown, +): messageData is SetFollowSelectionConfig { + return ( + typeof messageData === 'object' && + (messageData as any)?.['type'] === 'SET_FOLLOW_SELECTION_CONFIG' + ) +} + +export interface SetVSCodeTheme { + type: 'SET_VSCODE_THEME' + theme: string +} + +export function setVSCodeTheme(theme: string): SetVSCodeTheme { + return { + type: 'SET_VSCODE_THEME', + theme: theme, + } +} + +export function isSetVSCodeTheme(messageData: unknown): messageData is SetVSCodeTheme { + return typeof messageData === 'object' && (messageData as any)?.['type'] === 'SET_VSCODE_THEME' +} + +export interface UtopiaReady { + type: 'UTOPIA_READY' +} + +export function utopiaReady(): UtopiaReady { + return { + type: 'UTOPIA_READY', + } +} + +export function isUtopiaReady(messageData: unknown): messageData is UtopiaReady { + return typeof messageData === 'object' && (messageData as any)?.['type'] === 'UTOPIA_READY' +} + +export function isFromUtopiaToVSCodeMessage( + messageData: unknown, +): messageData is FromUtopiaToVSCodeMessage { + return ( + isInitProject(messageData) || + isWriteProjectFileChange(messageData) || + isDeletePathChange(messageData) || + isEnsureDirectoryExistsChange(messageData) || + isOpenFileMessage(messageData) || + isUpdateDecorationsMessage(messageData) || + isSelectedElementChanged(messageData) || + isGetUtopiaVSCodeConfig(messageData) || + isSetFollowSelectionConfig(messageData) || + isSetVSCodeTheme(messageData) || + isUtopiaReady(messageData) + ) +} + +export type FromUtopiaToVSCodeMessage = + | InitProject + | WriteProjectFileChange + | DeletePathChange + | EnsureDirectoryExistsChange + | OpenFileMessage + | UpdateDecorationsMessage + | SelectedElementChanged + | GetUtopiaVSCodeConfig + | SetFollowSelectionConfig + | SetVSCodeTheme + | UtopiaReady diff --git a/utopia-vscode-common/src/messages.ts b/utopia-vscode-common/src/messages.ts deleted file mode 100644 index 7d5528c07287..000000000000 --- a/utopia-vscode-common/src/messages.ts +++ /dev/null @@ -1,360 +0,0 @@ -import type { UtopiaVSCodeConfig } from './utopia-vscode-config' - -export interface OpenFileMessage { - type: 'OPEN_FILE' - filePath: string - bounds: Bounds | null -} - -export function openFileMessage(filePath: string, bounds: Bounds | null): OpenFileMessage { - return { - type: 'OPEN_FILE', - filePath: filePath, - bounds: bounds, - } -} - -export type DecorationRangeType = 'selection' | 'highlight' - -export interface Bounds { - startLine: number - startCol: number - endLine: number - endCol: number -} - -export interface BoundsInFile extends Bounds { - filePath: string -} - -export function boundsInFile( - filePath: string, - startLine: number, - startCol: number, - endLine: number, - endCol: number, -): BoundsInFile { - return { - filePath: filePath, - startLine: startLine, - startCol: startCol, - endLine: endLine, - endCol: endCol, - } -} - -export interface DecorationRange extends BoundsInFile { - rangeType: DecorationRangeType -} - -export function decorationRange( - rangeType: DecorationRangeType, - filePath: string, - startLine: number, - startCol: number, - endLine: number, - endCol: number, -): DecorationRange { - return { - rangeType: rangeType, - filePath: filePath, - startLine: startLine, - startCol: startCol, - endLine: endLine, - endCol: endCol, - } -} - -export interface UpdateDecorationsMessage { - type: 'UPDATE_DECORATIONS' - decorations: Array -} - -export function updateDecorationsMessage( - decorations: Array, -): UpdateDecorationsMessage { - return { - type: 'UPDATE_DECORATIONS', - decorations: decorations, - } -} - -export type ForceNavigation = 'do-not-force-navigation' | 'force-navigation' - -export interface SelectedElementChanged { - type: 'SELECTED_ELEMENT_CHANGED' - boundsInFile: BoundsInFile - forceNavigation: ForceNavigation -} - -export function selectedElementChanged( - bounds: BoundsInFile, - forceNavigation: ForceNavigation, -): SelectedElementChanged { - return { - type: 'SELECTED_ELEMENT_CHANGED', - boundsInFile: bounds, - forceNavigation: forceNavigation, - } -} - -export interface GetUtopiaVSCodeConfig { - type: 'GET_UTOPIA_VSCODE_CONFIG' -} - -export function getUtopiaVSCodeConfig(): GetUtopiaVSCodeConfig { - return { - type: 'GET_UTOPIA_VSCODE_CONFIG', - } -} - -export interface SetFollowSelectionConfig { - type: 'SET_FOLLOW_SELECTION_CONFIG' - enabled: boolean -} - -export function setFollowSelectionConfig(enabled: boolean): SetFollowSelectionConfig { - return { - type: 'SET_FOLLOW_SELECTION_CONFIG', - enabled: enabled, - } -} - -export interface SetVSCodeTheme { - type: 'SET_VSCODE_THEME' - theme: string -} - -export function setVSCodeTheme(theme: string): SetVSCodeTheme { - return { - type: 'SET_VSCODE_THEME', - theme: theme, - } -} - -export interface UtopiaReady { - type: 'UTOPIA_READY' -} - -export function utopiaReady(): UtopiaReady { - return { - type: 'UTOPIA_READY', - } -} - -export type ToVSCodeMessageNoAccumulated = - | OpenFileMessage - | UpdateDecorationsMessage - | SelectedElementChanged - | GetUtopiaVSCodeConfig - | SetFollowSelectionConfig - | SetVSCodeTheme - | UtopiaReady - -export interface AccumulatedToVSCodeMessage { - type: 'ACCUMULATED_TO_VSCODE_MESSAGE' - messages: Array -} - -export function accumulatedToVSCodeMessage( - messages: Array, -): AccumulatedToVSCodeMessage { - return { - type: 'ACCUMULATED_TO_VSCODE_MESSAGE', - messages: messages, - } -} - -export type ToVSCodeMessage = ToVSCodeMessageNoAccumulated | AccumulatedToVSCodeMessage - -export function isOpenFileMessage(message: unknown): message is OpenFileMessage { - return ( - typeof message === 'object' && - !Array.isArray(message) && - (message as OpenFileMessage).type === 'OPEN_FILE' - ) -} - -export function isUpdateDecorationsMessage(message: unknown): message is UpdateDecorationsMessage { - return ( - typeof message === 'object' && - !Array.isArray(message) && - (message as UpdateDecorationsMessage).type === 'UPDATE_DECORATIONS' - ) -} - -export function isSelectedElementChanged(message: unknown): message is SelectedElementChanged { - return ( - typeof message === 'object' && - !Array.isArray(message) && - (message as SelectedElementChanged).type === 'SELECTED_ELEMENT_CHANGED' - ) -} - -export function isGetUtopiaVSCodeConfig(message: unknown): message is GetUtopiaVSCodeConfig { - return ( - typeof message === 'object' && - !Array.isArray(message) && - (message as GetUtopiaVSCodeConfig).type === 'GET_UTOPIA_VSCODE_CONFIG' - ) -} - -export function isSetFollowSelectionConfig(message: unknown): message is SetFollowSelectionConfig { - return ( - typeof message === 'object' && - !Array.isArray(message) && - (message as SetFollowSelectionConfig).type === 'SET_FOLLOW_SELECTION_CONFIG' - ) -} - -export function isSetVSCodeTheme(message: unknown): message is SetVSCodeTheme { - return ( - typeof message === 'object' && - !Array.isArray(message) && - (message as SetVSCodeTheme).type === 'SET_VSCODE_THEME' - ) -} - -export function isUtopiaReadyMessage(message: unknown): message is UtopiaReady { - return ( - typeof message === 'object' && - !Array.isArray(message) && - (message as UtopiaReady).type === 'UTOPIA_READY' - ) -} - -export function isAccumulatedToVSCodeMessage( - message: unknown, -): message is AccumulatedToVSCodeMessage { - return ( - typeof message === 'object' && - !Array.isArray(message) && - (message as AccumulatedToVSCodeMessage).type === 'ACCUMULATED_TO_VSCODE_MESSAGE' - ) -} - -export function parseToVSCodeMessage(unparsed: string): ToVSCodeMessage { - const message = JSON.parse(unparsed) - if ( - isOpenFileMessage(message) || - isUpdateDecorationsMessage(message) || - isSelectedElementChanged(message) || - isGetUtopiaVSCodeConfig(message) || - isSetFollowSelectionConfig(message) || - isSetVSCodeTheme(message) || - isUtopiaReadyMessage(message) || - isAccumulatedToVSCodeMessage(message) - ) { - return message - } else { - // FIXME This should return an Either - throw new Error(`Invalid message type ${JSON.stringify(message)}`) - } -} - -export interface EditorCursorPositionChanged { - type: 'EDITOR_CURSOR_POSITION_CHANGED' - filePath: string - line: number - column: number -} - -export function editorCursorPositionChanged( - filePath: string, - line: number, - column: number, -): EditorCursorPositionChanged { - return { - type: 'EDITOR_CURSOR_POSITION_CHANGED', - filePath: filePath, - line: line, - column: column, - } -} - -export interface UtopiaVSCodeConfigValues { - type: 'UTOPIA_VSCODE_CONFIG_VALUES' - config: UtopiaVSCodeConfig -} - -export function utopiaVSCodeConfigValues(config: UtopiaVSCodeConfig): UtopiaVSCodeConfigValues { - return { - type: 'UTOPIA_VSCODE_CONFIG_VALUES', - config: config, - } -} - -export interface VSCodeReady { - type: 'VSCODE_READY' -} - -export function vsCodeReady(): VSCodeReady { - return { - type: 'VSCODE_READY', - } -} - -export interface ClearLoadingScreen { - type: 'CLEAR_LOADING_SCREEN' -} - -export function clearLoadingScreen(): ClearLoadingScreen { - return { - type: 'CLEAR_LOADING_SCREEN', - } -} - -export type FromVSCodeMessage = - | EditorCursorPositionChanged - | UtopiaVSCodeConfigValues - | VSCodeReady - | ClearLoadingScreen - -export function isEditorCursorPositionChanged( - message: unknown, -): message is EditorCursorPositionChanged { - return ( - typeof message === 'object' && - !Array.isArray(message) && - (message as EditorCursorPositionChanged).type === 'EDITOR_CURSOR_POSITION_CHANGED' - ) -} - -export function isUtopiaVSCodeConfigValues(message: unknown): message is UtopiaVSCodeConfigValues { - return ( - typeof message === 'object' && - !Array.isArray(message) && - (message as UtopiaVSCodeConfigValues).type === 'UTOPIA_VSCODE_CONFIG_VALUES' - ) -} - -export function isVSCodeReady(message: unknown): message is VSCodeReady { - return ( - typeof message === 'object' && - !Array.isArray(message) && - (message as VSCodeReady).type === 'VSCODE_READY' - ) -} - -export function isClearLoadingScreen(message: unknown): message is ClearLoadingScreen { - return ( - typeof message === 'object' && - !Array.isArray(message) && - (message as ClearLoadingScreen).type === 'CLEAR_LOADING_SCREEN' - ) -} - -export function parseFromVSCodeMessage(unparsed: string): FromVSCodeMessage { - const message = JSON.parse(unparsed) - if ( - isEditorCursorPositionChanged(message) || - isUtopiaVSCodeConfigValues(message) || - isVSCodeReady(message) || - isClearLoadingScreen(message) - ) { - return message - } else { - // FIXME This should return an Either - throw new Error(`Invalid message type ${JSON.stringify(message)}`) - } -} diff --git a/utopia-vscode-common/src/path-utils.ts b/utopia-vscode-common/src/path-utils.ts index 9a9451dc42b3..9fee9c07cd83 100644 --- a/utopia-vscode-common/src/path-utils.ts +++ b/utopia-vscode-common/src/path-utils.ts @@ -1,5 +1,3 @@ -export const RootDir = `/utopia` - export function stripTrailingSlash(path: string): string { return path.endsWith('/') ? path.slice(0, -1) : path } @@ -14,12 +12,8 @@ export function appendToPath(path: string, elem: string): string { return `${left}/${right}` } -export function stripRootPrefix(path: string): string { - return path.startsWith(RootDir) ? path.slice(RootDir.length + 1) : path -} - export function toUtopiaPath(projectID: string, path: string): string { - const result = appendToPath(`${projectID}:/`, stripRootPrefix(path)) + const result = appendToPath(`${projectID}:/`, path) return result } diff --git a/utopia-vscode-common/src/vscode-communication.ts b/utopia-vscode-common/src/vscode-communication.ts deleted file mode 100644 index d836a93b90f6..000000000000 --- a/utopia-vscode-common/src/vscode-communication.ts +++ /dev/null @@ -1,191 +0,0 @@ -// This file exists so that the extension can communicate with the Utopia editor - -import type { FSUser } from './fs/fs-types' -import { - deletePath, - ensureDirectoryExists, - initializeFS, - readFileAsUTF8, - stat, - stopWatchingAll, - watch, - writeFileAsUTF8, -} from './fs/fs-utils' -import { - clearBothMailboxes, - initMailbox, - sendMessage, - stopPollingMailbox, - UtopiaInbox, -} from './mailbox' -import type { FromVSCodeMessage } from './messages' -import { - clearLoadingScreen, - getUtopiaVSCodeConfig, - openFileMessage, - parseFromVSCodeMessage, - utopiaReady, -} from './messages' -import { appendToPath } from './path-utils' -import type { ProjectFile } from './window-messages' -import { - fromVSCodeExtensionMessage, - indexedDBFailure, - isDeletePathChange, - isEnsureDirectoryExistsChange, - isInitProject, - isToVSCodeExtensionMessage, - isWriteProjectFileChange, - messageListenersReady, - vsCodeBridgeReady, - vsCodeFileChange, - vsCodeFileDelete, -} from './window-messages' - -const Scheme = 'utopia' -const RootDir = `/${Scheme}` -const UtopiaFSUser: FSUser = 'UTOPIA' - -function toFSPath(projectPath: string): string { - const fsPath = appendToPath(RootDir, projectPath) - return fsPath -} - -function fromFSPath(fsPath: string): string { - const prefix = RootDir - const prefixIndex = fsPath.indexOf(prefix) - if (prefixIndex === 0) { - const projectPath = fsPath.slice(prefix.length) - return projectPath - } else { - throw new Error(`Invalid FS path: ${fsPath}`) - } -} - -async function writeProjectFile(projectFile: ProjectFile): Promise { - switch (projectFile.type) { - case 'PROJECT_DIRECTORY': { - const { filePath: projectPath } = projectFile - return ensureDirectoryExists(toFSPath(projectPath)) - } - case 'PROJECT_TEXT_FILE': { - const { filePath: projectPath, savedContent, unsavedContent } = projectFile - const filePath = toFSPath(projectPath) - const alreadyExistingFile = await readFileAsUTF8(filePath).catch((_) => null) - const fileDiffers = - alreadyExistingFile == null || - alreadyExistingFile.content !== savedContent || - alreadyExistingFile.unsavedContent !== unsavedContent - if (fileDiffers) { - // Avoid pushing a file to the file system if the content hasn't changed. - return writeFileAsUTF8(filePath, savedContent, unsavedContent) - } - return - } - default: - const _exhaustiveCheck: never = projectFile - throw new Error(`Invalid file projectFile type ${projectFile}`) - } -} - -function watchForChanges(): void { - function onCreated(fsPath: string): void { - void stat(fsPath).then((fsStat) => { - if (fsStat.type === 'FILE' && fsStat.sourceOfLastChange !== UtopiaFSUser) { - void readFileAsUTF8(fsPath).then((fileContent) => { - const filePath = fromFSPath(fsPath) - window.top?.postMessage(vsCodeFileChange(filePath, fileContent), '*') - }) - } - }) - } - function onModified(fsPath: string, modifiedBySelf: boolean): void { - if (!modifiedBySelf) { - onCreated(fsPath) - } - } - function onDeleted(fsPath: string): void { - const filePath = fromFSPath(fsPath) - window.top?.postMessage(vsCodeFileDelete(filePath), '*') - } - function onIndexedDBFailure(): void { - window.top?.postMessage(indexedDBFailure(), '*') - } - - void watch(toFSPath('/'), true, onCreated, onModified, onDeleted, onIndexedDBFailure) -} - -let currentInit: Promise = Promise.resolve() - -async function initIndexedDBBridge( - vsCodeSessionID: string, - projectContents: Array, - openFilePath: string | null, -): Promise { - async function innerInit(): Promise { - stopWatchingAll() - stopPollingMailbox() - await initializeFS(vsCodeSessionID, UtopiaFSUser) - await ensureDirectoryExists(RootDir) - await clearBothMailboxes() - for (const projectFile of projectContents) { - await writeProjectFile(projectFile) - } - await initMailbox(UtopiaInbox, parseFromVSCodeMessage, (message: FromVSCodeMessage) => { - window.top?.postMessage(fromVSCodeExtensionMessage(message), '*') - }) - await sendMessage(utopiaReady()) - await sendMessage(getUtopiaVSCodeConfig()) - watchForChanges() - if (openFilePath != null) { - await sendMessage(openFileMessage(openFilePath, null)) - } else { - window.top?.postMessage(fromVSCodeExtensionMessage(clearLoadingScreen()), '*') - } - - window.top?.postMessage(vsCodeBridgeReady(), '*') - } - - // Prevent multiple initialisations from driving over each other. - currentInit = currentInit.then(innerInit) -} - -// Chain off of the previous one to ensure the ordering of changes is maintained. -let applyProjectChangesCoordinator: Promise = Promise.resolve() - -export function setupVSCodeEventListenersForProject(vsCodeSessionID: string) { - let intervalID: number | null = null - window.addEventListener('message', (messageEvent: MessageEvent) => { - const { data } = messageEvent - if (isInitProject(data)) { - if (intervalID != null) { - window.clearInterval(intervalID) - } - initIndexedDBBridge(vsCodeSessionID, data.projectContents, data.openFilePath) - } else if (isDeletePathChange(data)) { - applyProjectChangesCoordinator = applyProjectChangesCoordinator.then(async () => { - await deletePath(toFSPath(data.fullPath), data.recursive) - }) - } else if (isWriteProjectFileChange(data)) { - applyProjectChangesCoordinator = applyProjectChangesCoordinator.then(async () => { - await writeProjectFile(data.projectFile) - }) - } else if (isEnsureDirectoryExistsChange(data)) { - applyProjectChangesCoordinator = applyProjectChangesCoordinator.then(async () => { - await ensureDirectoryExists(toFSPath(data.fullPath)) - }) - } else if (isToVSCodeExtensionMessage(data)) { - applyProjectChangesCoordinator = applyProjectChangesCoordinator.then(async () => { - await sendMessage(data.message) - }) - } - }) - - intervalID = window.setInterval(() => { - try { - window.top?.postMessage(messageListenersReady(), '*') - } catch (error) { - console.error('Error posting messageListenersReady', error) - } - }, 500) -} diff --git a/utopia-vscode-common/src/window-messages.ts b/utopia-vscode-common/src/window-messages.ts deleted file mode 100644 index a23e0c657f21..000000000000 --- a/utopia-vscode-common/src/window-messages.ts +++ /dev/null @@ -1,252 +0,0 @@ -import type { StoredFile } from './fs/fs-utils' -import type { FromVSCodeMessage, ToVSCodeMessage } from './messages' - -export interface ProjectDirectory { - type: 'PROJECT_DIRECTORY' - filePath: string -} - -export function projectDirectory(filePath: string): ProjectDirectory { - return { - type: 'PROJECT_DIRECTORY', - filePath: filePath, - } -} - -export interface ProjectTextFile { - type: 'PROJECT_TEXT_FILE' - filePath: string - savedContent: string - unsavedContent: string | null -} - -export function projectTextFile( - filePath: string, - savedContent: string, - unsavedContent: string | null, -): ProjectTextFile { - return { - type: 'PROJECT_TEXT_FILE', - filePath: filePath, - savedContent: savedContent, - unsavedContent: unsavedContent, - } -} - -export type ProjectFile = ProjectDirectory | ProjectTextFile - -// Message Types To VS Code -export interface InitProject { - type: 'INIT_PROJECT' - projectContents: Array - openFilePath: string | null -} - -export function initProject( - projectContents: Array, - openFilePath: string | null, -): InitProject { - return { - type: 'INIT_PROJECT', - projectContents: projectContents, - openFilePath: openFilePath, - } -} - -export function isInitProject(messageData: unknown): messageData is InitProject { - return typeof messageData === 'object' && (messageData as any)?.['type'] === 'INIT_PROJECT' -} - -export interface ToVSCodeExtensionMessage { - type: 'TO_VSCODE_EXTENSION_MESSAGE' - message: ToVSCodeMessage -} - -export function toVSCodeExtensionMessage(message: ToVSCodeMessage): ToVSCodeExtensionMessage { - return { - type: 'TO_VSCODE_EXTENSION_MESSAGE', - message: message, - } -} - -export function isToVSCodeExtensionMessage( - messageData: unknown, -): messageData is ToVSCodeExtensionMessage { - return ( - typeof messageData === 'object' && - (messageData as any)?.['type'] === 'TO_VSCODE_EXTENSION_MESSAGE' - ) -} - -export interface WriteProjectFileChange { - type: 'WRITE_PROJECT_FILE' - projectFile: ProjectFile -} - -export function writeProjectFileChange(projectFile: ProjectFile): WriteProjectFileChange { - return { - type: 'WRITE_PROJECT_FILE', - projectFile: projectFile, - } -} - -export function isWriteProjectFileChange( - messageData: unknown, -): messageData is WriteProjectFileChange { - return typeof messageData === 'object' && (messageData as any)?.['type'] === 'WRITE_PROJECT_FILE' -} - -export interface DeletePathChange { - type: 'DELETE_PATH' - fullPath: string - recursive: boolean -} - -export function deletePathChange(fullPath: string, recursive: boolean): DeletePathChange { - return { - type: 'DELETE_PATH', - fullPath: fullPath, - recursive: recursive, - } -} - -export function isDeletePathChange(messageData: unknown): messageData is DeletePathChange { - return typeof messageData === 'object' && (messageData as any)?.['type'] === 'DELETE_PATH' -} - -export interface EnsureDirectoryExistsChange { - type: 'ENSURE_DIRECTORY_EXISTS' - fullPath: string -} - -export function ensureDirectoryExistsChange(fullPath: string): EnsureDirectoryExistsChange { - return { - type: 'ENSURE_DIRECTORY_EXISTS', - fullPath: fullPath, - } -} - -export function isEnsureDirectoryExistsChange( - messageData: unknown, -): messageData is EnsureDirectoryExistsChange { - return ( - typeof messageData === 'object' && (messageData as any)?.['type'] === 'ENSURE_DIRECTORY_EXISTS' - ) -} - -export type FromUtopiaToVSCodeMessage = - | InitProject - | ToVSCodeExtensionMessage - | WriteProjectFileChange - | DeletePathChange - | EnsureDirectoryExistsChange - -// Message Types To Utopia -export interface MessageListenersReady { - type: 'MESSAGE_LISTENERS_READY' -} - -export function messageListenersReady(): MessageListenersReady { - return { - type: 'MESSAGE_LISTENERS_READY', - } -} - -export function isMessageListenersReady( - messageData: unknown, -): messageData is MessageListenersReady { - return ( - typeof messageData === 'object' && (messageData as any)?.['type'] === 'MESSAGE_LISTENERS_READY' - ) -} - -export interface FromVSCodeExtensionMessage { - type: 'FROM_VSCODE_EXTENSION_MESSAGE' - message: FromVSCodeMessage -} - -export function fromVSCodeExtensionMessage(message: FromVSCodeMessage): FromVSCodeExtensionMessage { - return { - type: 'FROM_VSCODE_EXTENSION_MESSAGE', - message: message, - } -} - -export function isFromVSCodeExtensionMessage( - messageData: unknown, -): messageData is FromVSCodeExtensionMessage { - return ( - typeof messageData === 'object' && - (messageData as any)?.['type'] === 'FROM_VSCODE_EXTENSION_MESSAGE' - ) -} - -export interface VSCodeFileChange { - type: 'VSCODE_FILE_CHANGE' - filePath: string - fileContent: StoredFile -} - -export function vsCodeFileChange(filePath: string, fileContent: StoredFile): VSCodeFileChange { - return { - type: 'VSCODE_FILE_CHANGE', - filePath: filePath, - fileContent: fileContent, - } -} - -export function isVSCodeFileChange(messageData: unknown): messageData is VSCodeFileChange { - return typeof messageData === 'object' && (messageData as any)?.['type'] === 'VSCODE_FILE_CHANGE' -} - -export interface VSCodeFileDelete { - type: 'VSCODE_FILE_DELETE' - filePath: string -} - -export function vsCodeFileDelete(filePath: string): VSCodeFileDelete { - return { - type: 'VSCODE_FILE_DELETE', - filePath: filePath, - } -} - -export function isVSCodeFileDelete(messageData: unknown): messageData is VSCodeFileDelete { - return typeof messageData === 'object' && (messageData as any)?.['type'] === 'VSCODE_FILE_DELETE' -} - -export interface IndexedDBFailure { - type: 'INDEXED_DB_FAILURE' -} - -export function indexedDBFailure(): IndexedDBFailure { - return { - type: 'INDEXED_DB_FAILURE', - } -} - -export function isIndexedDBFailure(messageData: unknown): messageData is IndexedDBFailure { - return typeof messageData === 'object' && (messageData as any)?.['type'] === 'INDEXED_DB_FAILURE' -} - -export interface VSCodeBridgeReady { - type: 'VSCODE_BRIDGE_READY' -} - -export function vsCodeBridgeReady(): VSCodeBridgeReady { - return { - type: 'VSCODE_BRIDGE_READY', - } -} - -export function isVSCodeBridgeReady(messageData: unknown): messageData is VSCodeBridgeReady { - return typeof messageData === 'object' && (messageData as any)?.['type'] === 'VSCODE_BRIDGE_READY' -} - -export type FromVSCodeToUtopiaMessage = - | MessageListenersReady - | FromVSCodeExtensionMessage - | VSCodeFileChange - | VSCodeFileDelete - | IndexedDBFailure - | VSCodeBridgeReady diff --git a/utopia-vscode-extension/package.json b/utopia-vscode-extension/package.json index cdf03d3787b4..2c7f077ddfd2 100644 --- a/utopia-vscode-extension/package.json +++ b/utopia-vscode-extension/package.json @@ -5,7 +5,10 @@ "description": "For providing communication between Utopia and VS Code", "version": "0.0.2", "license": "MIT", - "enableProposedApi": true, + "enabledApiProposals": [ + "fileSearchProvider", + "textSearchProvider" + ], "private": true, "activationEvents": [ "onFileSystem:utopia", @@ -14,15 +17,15 @@ ], "browser": "./dist/browser/extension", "engines": { - "vscode": "^1.61.2" + "vscode": "^1.74.0" }, "scripts": { "build": "NODE_OPTIONS=$NODE_OPENSSL_OPTION yarn webpack-cli --config extension-browser.webpack.config", "production": "NODE_OPTIONS=$NODE_OPENSSL_OPTION yarn webpack-cli --config extension-browser.webpack.config --mode production", "watch": "NODE_OPTIONS=$NODE_OPENSSL_OPTION yarn webpack-cli --config extension-browser.webpack.config --mode production --watch --info-verbosity verbose", "watch-dev": "NODE_OPTIONS=$NODE_OPENSSL_OPTION yarn webpack-cli --config extension-browser.webpack.config --watch --info-verbosity verbose", - "download-api": "mkdir -p src/vscode-types && cd src/vscode-types && npx vscode-dts dev 1.61.2", - "postdownload-api": "mkdir -p src/vscode-types && cd src/vscode-types && npx vscode-dts 1.61.2", + "download-api": "mkdir -p src/vscode-types && cd src/vscode-types && npx @vscode/dts dev 1.91.1", + "postdownload-api": "mkdir -p src/vscode-types && cd src/vscode-types && npx @vscode/dts 1.91.1", "preinstall": "npx only-allow pnpm", "postinstall": "npm run download-api" }, @@ -44,6 +47,13 @@ "language": "javascript", "path": "./snippets.json" } + ], + "commands": [ + { + "command": "utopia.toVSCodeMessage", + "title": "Utopia Message to VS Code", + "category": "Utopia" + } ] }, "dependencies": { @@ -54,6 +64,6 @@ "typescript": "4.0.5", "webpack": "4.42.0", "webpack-cli": "3.3.11", - "vscode-dts": "0.3.1" + "@vscode/dts": "0.4.1" } } diff --git a/utopia-vscode-extension/pnpm-lock.yaml b/utopia-vscode-extension/pnpm-lock.yaml index 715fdea02a81..a1d049e4140e 100644 --- a/utopia-vscode-extension/pnpm-lock.yaml +++ b/utopia-vscode-extension/pnpm-lock.yaml @@ -1,10 +1,10 @@ lockfileVersion: 5.4 specifiers: + '@vscode/dts': 0.4.1 ts-loader: 5.3.3 typescript: 4.0.5 utopia-vscode-common: link:../utopia-vscode-common - vscode-dts: 0.3.1 webpack: 4.42.0 webpack-cli: 3.3.11 @@ -12,14 +12,25 @@ dependencies: utopia-vscode-common: link:../utopia-vscode-common devDependencies: + '@vscode/dts': 0.4.1 ts-loader: 5.3.3_typescript@4.0.5 typescript: 4.0.5 - vscode-dts: 0.3.1 webpack: 4.42.0_webpack-cli@3.3.11 webpack-cli: 3.3.11_webpack@4.42.0 packages: + /@vscode/dts/0.4.1: + resolution: {integrity: sha512-o8cI5Vqt6S6Y5mCI7yCkSQdiLQaLG5DMUpciJV3zReZwE+dA5KERxSVX8H3cPEhyKw21XwKGmIrg6YmN6M5uZA==} + hasBin: true + dependencies: + https-proxy-agent: 7.0.5 + minimist: 1.2.8 + prompts: 2.4.2 + transitivePeerDependencies: + - supports-color + dev: true + /@webassemblyjs/ast/1.8.5: resolution: {integrity: sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==} dependencies: @@ -168,6 +179,15 @@ packages: hasBin: true dev: true + /agent-base/7.1.1: + resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} + engines: {node: '>= 14'} + dependencies: + debug: 4.3.6 + transitivePeerDependencies: + - supports-color + dev: true + /ajv-errors/1.0.1_ajv@6.12.6: resolution: {integrity: sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==} peerDependencies: @@ -742,6 +762,18 @@ packages: supports-color: 6.1.0 dev: true + /debug/4.3.6: + resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + /decamelize/1.2.0: resolution: {integrity: sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=} engines: {node: '>=0.10.0'} @@ -1281,6 +1313,16 @@ packages: resolution: {integrity: sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=} dev: true + /https-proxy-agent/7.0.5: + resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.1 + debug: 4.3.6 + transitivePeerDependencies: + - supports-color + dev: true + /ieee754/1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: true @@ -1722,6 +1764,10 @@ packages: resolution: {integrity: sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==} dev: true + /minimist/1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: true + /mississippi/3.0.0: resolution: {integrity: sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==} engines: {node: '>=4.0.0'} @@ -1768,6 +1814,10 @@ packages: resolution: {integrity: sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=} dev: true + /ms/2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + /nan/2.15.0: resolution: {integrity: sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==} dev: true @@ -2059,8 +2109,8 @@ packages: bluebird: 3.7.2 dev: true - /prompts/2.4.1: - resolution: {integrity: sha512-EQyfIuO2hPDsX1L/blblV+H7I0knhgAd82cVneCwcdND9B8AuCDuRcBH6yIcG4dFzlOUqbazQqwGjx5xmsNLuQ==} + /prompts/2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} dependencies: kleur: 3.0.3 @@ -2251,13 +2301,6 @@ packages: glob: 7.2.0 dev: true - /rimraf/3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - hasBin: true - dependencies: - glob: 7.2.0 - dev: true - /ripemd160/2.0.2: resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==} dependencies: @@ -2732,15 +2775,6 @@ packages: resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} dev: true - /vscode-dts/0.3.1: - resolution: {integrity: sha512-8XZ+M7IQV5MnPXEhHLemGOk5FRBfT7HCBEughfDhn2i6wwPXlpv4OuQQdhs6XZVmF3GFdKqt+fXOgfsNBKP+fw==} - hasBin: true - dependencies: - minimist: 1.2.5 - prompts: 2.4.1 - rimraf: 3.0.2 - dev: true - /watchpack-chokidar2/2.0.1: resolution: {integrity: sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==} requiresBuild: true diff --git a/utopia-vscode-extension/src/extension.ts b/utopia-vscode-extension/src/extension.ts index e64fe6b70836..8474ce85146b 100644 --- a/utopia-vscode-extension/src/extension.ts +++ b/utopia-vscode-extension/src/extension.ts @@ -2,40 +2,37 @@ import * as vscode from 'vscode' import type { DecorationRange, DecorationRangeType, - FSError, BoundsInFile, Bounds, - ToVSCodeMessage, UtopiaVSCodeConfig, + FromUtopiaToVSCodeMessage, + FromVSCodeToUtopiaMessage, } from 'utopia-vscode-common' import { - ensureDirectoryExists, - RootDir, - initMailbox, - VSCodeInbox, - setErrorHandler, toUtopiaPath, - initializeFS, - parseToVSCodeMessage, - sendMessage, editorCursorPositionChanged, - readFileAsUTF8, - exists, - writeFileUnsavedContentAsUTF8, - clearFileUnsavedContent, applyPrettier, utopiaVSCodeConfigValues, vsCodeReady, clearLoadingScreen, ProjectIDPlaceholderPrefix, + vsCodeBridgeReady, + vsCodeFileChange, } from 'utopia-vscode-common' import { UtopiaFSExtension } from './utopia-fs' -import { fromUtopiaURI } from './path-utils' import type { TextDocumentChangeEvent, TextDocumentWillSaveEvent, Uri } from 'vscode' +import type { FSError } from './in-mem-fs' +import { + clearFileUnsavedContent, + exists, + readFileAsUTF8, + setErrorHandler, + writeFileUnsavedContentAsUTF8, +} from './in-mem-fs' const FollowSelectionConfigKey = 'utopia.editor.followSelection.enabled' -export async function activate(context: vscode.ExtensionContext): Promise { +export function activate(context: vscode.ExtensionContext) { const workspaceRootUri = vscode.workspace.workspaceFolders[0].uri const projectID = workspaceRootUri.scheme if (projectID.startsWith(ProjectIDPlaceholderPrefix)) { @@ -43,23 +40,23 @@ export async function activate(context: vscode.ExtensionContext): Promise return } - /* eslint-disable-next-line react-hooks/rules-of-hooks */ - useFileSystemProviderErrors(projectID) + setErrorHandler((e) => toFileSystemProviderError(projectID, e)) + + const utopiaFS = new UtopiaFSExtension(projectID) + context.subscriptions.push(utopiaFS) - await initFS(projectID) - const utopiaFS = initUtopiaFSProvider(projectID, context) - initMessaging(context, workspaceRootUri) + initMessaging(context, workspaceRootUri, utopiaFS) - watchForUnsavedContentChangesFromFS(utopiaFS) watchForChangesFromVSCode(context, projectID) // Send a VSCodeReady message on activation as this might be triggered by an iframe reload, // meaning no new UtopiaReady message will have been sent - await sendMessage(vsCodeReady()) + sendMessageToUtopia(vsCodeReady()) watchForFileDeletions() } +// FIXME This isn't actually closing the document function watchForFileDeletions() { let fileWatcherChain: Promise = Promise.resolve() const fileWatcher = vscode.workspace.createFileSystemWatcher('**/*') @@ -85,33 +82,6 @@ async function wait(timeoutms: number): Promise { return new Promise((resolve) => setTimeout(() => resolve(), timeoutms)) } -async function initFS(projectID: string): Promise { - await initializeFS(projectID, 'VSCODE') - await ensureDirectoryExists(RootDir) -} - -function initUtopiaFSProvider( - projectID: string, - context: vscode.ExtensionContext, -): UtopiaFSExtension { - const utopiaFS = new UtopiaFSExtension(projectID) - context.subscriptions.push(utopiaFS) - return utopiaFS -} - -function watchForUnsavedContentChangesFromFS(utopiaFS: UtopiaFSExtension) { - utopiaFS.onUtopiaDidChangeUnsavedContent((uris) => { - uris.forEach((uri) => { - updateDirtyContent(uri) - }) - }) - utopiaFS.onUtopiaDidChangeSavedContent((uris) => { - uris.forEach((uri) => { - clearDirtyFlags(uri) - }) - }) -} - let dirtyFiles: Set = new Set() let incomingFileChanges: Set = new Set() @@ -194,7 +164,7 @@ function minimisePendingWork(): void { pendingWork = newPendingWork } -async function doSubscriptionWork(work: SubscriptionWork): Promise { +function doSubscriptionWork(work: SubscriptionWork) { switch (work.type) { case 'DID_CHANGE_TEXT': { const { path, event } = work @@ -208,7 +178,9 @@ async function doSubscriptionWork(work: SubscriptionWork): Promise { incomingFileChanges.delete(path) } else { const fullText = event.document.getText() - await writeFileUnsavedContentAsUTF8(path, fullText) + writeFileUnsavedContentAsUTF8(path, fullText) + const updatedFile = readFileAsUTF8(path) + sendMessageToUtopia(vsCodeFileChange(path, updatedFile)) } } break @@ -216,12 +188,12 @@ async function doSubscriptionWork(work: SubscriptionWork): Promise { case 'UPDATE_DIRTY_CONTENT': { const { path, uri } = work if (!incomingFileChanges.has(path)) { - await updateDirtyContent(uri) + updateDirtyContent(uri) } break } case 'WILL_SAVE_TEXT': { - const { path, event } = work + const { path } = work dirtyFiles.delete(path) break @@ -232,6 +204,9 @@ async function doSubscriptionWork(work: SubscriptionWork): Promise { // User decided to bin unsaved changes when closing the document clearFileUnsavedContent(path) dirtyFiles.delete(path) + + const updatedFile = readFileAsUTF8(path) + sendMessageToUtopia(vsCodeFileChange(path, updatedFile)) } break @@ -244,13 +219,15 @@ async function doSubscriptionWork(work: SubscriptionWork): Promise { const SUBSCRIPTION_POLLING_TIMEOUT = 100 -async function runPendingSubscriptionChanges(): Promise { +function runPendingSubscriptionChanges() { minimisePendingWork() for (const work of pendingWork) { - await doSubscriptionWork(work) + doSubscriptionWork(work) } pendingWork = [] + + // TODO should we still do it like this, or instead follow the pattern used by queueEvents? setTimeout(runPendingSubscriptionChanges, SUBSCRIPTION_POLLING_TIMEOUT) } @@ -265,16 +242,13 @@ function watchForChangesFromVSCode(context: vscode.ExtensionContext, projectID: vscode.workspace.onDidChangeTextDocument((event) => { if (isUtopiaDocument(event.document)) { const resource = event.document.uri - if (resource.scheme === projectID) { - // Don't act on changes to other documents - const path = fromUtopiaURI(resource) - pendingWork.push(didChangeTextChange(path, event)) - } + const path = resource.path + pendingWork.push(didChangeTextChange(path, event)) } }), vscode.workspace.onWillSaveTextDocument((event) => { if (isUtopiaDocument(event.document)) { - const path = fromUtopiaURI(event.document.uri) + const path = event.document.uri.path pendingWork.push(willSaveText(path, event)) if (event.reason === vscode.TextDocumentSaveReason.Manual) { const formattedCode = applyPrettier(event.document.getText(), false).formatted @@ -284,13 +258,13 @@ function watchForChangesFromVSCode(context: vscode.ExtensionContext, projectID: }), vscode.workspace.onDidCloseTextDocument((document) => { if (isUtopiaDocument(document)) { - const path = fromUtopiaURI(document.uri) + const path = document.uri.path pendingWork.push(didClose(path)) } }), vscode.workspace.onDidOpenTextDocument((document) => { if (isUtopiaDocument(document)) { - const path = fromUtopiaURI(document.uri) + const path = document.uri.path pendingWork.push(updateDirtyContentChange(path, document.uri)) } }), @@ -334,56 +308,110 @@ function getFullConfig(): UtopiaVSCodeConfig { let currentDecorations: Array = [] let currentSelection: BoundsInFile | null = null -function initMessaging(context: vscode.ExtensionContext, workspaceRootUri: vscode.Uri): void { - function handleMessage(message: ToVSCodeMessage): void { - switch (message.type) { - case 'OPEN_FILE': - if (message.bounds != null) { - revealRangeIfPossible(workspaceRootUri, { ...message.bounds, filePath: message.filePath }) - } else { - openFile(vscode.Uri.joinPath(workspaceRootUri, message.filePath)) - } - break - case 'UPDATE_DECORATIONS': - currentDecorations = message.decorations - updateDecorations(currentDecorations) - break - case 'SELECTED_ELEMENT_CHANGED': - const followSelectionEnabled = getFollowSelectionEnabledConfig() - const shouldFollowSelection = - followSelectionEnabled && - (shouldFollowSelectionWithActiveFile() || message.forceNavigation === 'force-navigation') - if (shouldFollowSelection) { - currentSelection = message.boundsInFile - revealRangeIfPossible(workspaceRootUri, message.boundsInFile) - } - break - case 'GET_UTOPIA_VSCODE_CONFIG': - sendFullConfigToUtopia() - break - case 'SET_FOLLOW_SELECTION_CONFIG': - vscode.workspace - .getConfiguration() - .update(FollowSelectionConfigKey, message.enabled, vscode.ConfigurationTarget.Workspace) - break - case 'SET_VSCODE_THEME': - vscode.workspace.getConfiguration().update('workbench.colorTheme', message.theme, true) - break - case 'ACCUMULATED_TO_VSCODE_MESSAGE': - for (const innerMessage of message.messages) { - handleMessage(innerMessage) - } - break - case 'UTOPIA_READY': - sendMessage(vsCodeReady()) - break - default: - const _exhaustiveCheck: never = message - console.error(`Unhandled message type ${JSON.stringify(message)}`) - } - } +// This part is the crux of the communication system. We have this extension and VS Code register +// a pair of new commands `utopia.toUtopiaMessage` and `utopia.toVSCodeMessage` for passing messages +// between themselves, with the VS Code side then forwarding those messages straight onto Utopia via +// a window.postMessage (since that isn't possible from the extension) +function sendMessageToUtopia(message: FromVSCodeToUtopiaMessage): void { + vscode.commands.executeCommand('utopia.toUtopiaMessage', message) +} - initMailbox(VSCodeInbox, parseToVSCodeMessage, handleMessage) +function initMessaging( + context: vscode.ExtensionContext, + workspaceRootUri: vscode.Uri, + utopiaFS: UtopiaFSExtension, +): void { + context.subscriptions.push( + vscode.commands.registerCommand( + 'utopia.toVSCodeMessage', + (message: FromUtopiaToVSCodeMessage) => { + switch (message.type) { + case 'INIT_PROJECT': + const { projectContents, openFilePath } = message + for (const projectFile of projectContents) { + utopiaFS.writeProjectFile(projectFile) + if (projectFile.type === 'PROJECT_TEXT_FILE' && projectFile.unsavedContent != null) { + updateDirtyContent(vscode.Uri.joinPath(workspaceRootUri, projectFile.filePath)) + } + } + if (openFilePath != null) { + openFile(vscode.Uri.joinPath(workspaceRootUri, openFilePath)) + } else { + sendMessageToUtopia(clearLoadingScreen()) + } + + sendMessageToUtopia(vsCodeReady()) // FIXME do we need both? + sendFullConfigToUtopia() + sendMessageToUtopia(vsCodeBridgeReady()) + break + case 'WRITE_PROJECT_FILE': + const { projectFile } = message + utopiaFS.writeProjectFile(projectFile) + if (projectFile.type === 'PROJECT_TEXT_FILE') { + const fileUri = vscode.Uri.joinPath(workspaceRootUri, projectFile.filePath) + if (projectFile.unsavedContent == null) { + clearDirtyFlags(fileUri) + } else { + updateDirtyContent(fileUri) + } + } + break + case 'DELETE_PATH': + utopiaFS.silentDelete(message.fullPath, { recursive: message.recursive }) + break + case 'ENSURE_DIRECTORY_EXISTS': + utopiaFS.ensureDirectoryExists(message.fullPath) + break + case 'OPEN_FILE': + if (message.bounds != null) { + revealRangeIfPossible(workspaceRootUri, { + ...message.bounds, + filePath: message.filePath, + }) + } else { + openFile(vscode.Uri.joinPath(workspaceRootUri, message.filePath)) + } + break + case 'UPDATE_DECORATIONS': + currentDecorations = message.decorations + updateDecorations(currentDecorations) + break + case 'SELECTED_ELEMENT_CHANGED': + const followSelectionEnabled = getFollowSelectionEnabledConfig() + const shouldFollowSelection = + followSelectionEnabled && + (shouldFollowSelectionWithActiveFile() || + message.forceNavigation === 'force-navigation') + if (shouldFollowSelection) { + currentSelection = message.boundsInFile + revealRangeIfPossible(workspaceRootUri, message.boundsInFile) + } + break + case 'GET_UTOPIA_VSCODE_CONFIG': + sendFullConfigToUtopia() + break + case 'SET_FOLLOW_SELECTION_CONFIG': + vscode.workspace + .getConfiguration() + .update( + FollowSelectionConfigKey, + message.enabled, + vscode.ConfigurationTarget.Workspace, + ) + break + case 'SET_VSCODE_THEME': + vscode.workspace.getConfiguration().update('workbench.colorTheme', message.theme, true) + break + case 'UTOPIA_READY': + sendMessageToUtopia(vsCodeReady()) + break + default: + const _exhaustiveCheck: never = message + console.error(`Unhandled message type ${JSON.stringify(message)}`) + } + }, + ), + ) context.subscriptions.push( vscode.window.onDidChangeVisibleTextEditors(() => { @@ -402,9 +430,9 @@ function initMessaging(context: vscode.ExtensionContext, workspaceRootUri: vscod ) } -function sendFullConfigToUtopia(): Promise { +function sendFullConfigToUtopia() { const fullConfig = getFullConfig() - return sendMessage(utopiaVSCodeConfigValues(fullConfig)) + sendMessageToUtopia(utopiaVSCodeConfigValues(fullConfig)) } function entireDocRange() { @@ -421,8 +449,8 @@ async function clearDirtyFlags(resource: vscode.Uri): Promise { } async function updateDirtyContent(resource: vscode.Uri): Promise { - const filePath = fromUtopiaURI(resource) - const { unsavedContent } = await readFileAsUTF8(filePath) + const filePath = resource.path + const { unsavedContent } = readFileAsUTF8(filePath) if (unsavedContent != null) { incomingFileChanges.add(filePath) const workspaceEdit = new vscode.WorkspaceEdit() @@ -442,19 +470,20 @@ async function updateDirtyContent(resource: vscode.Uri): Promise { } async function openFile(fileUri: vscode.Uri, retries: number = 5): Promise { - const filePath = fromUtopiaURI(fileUri) - const fileExists = await exists(filePath) + const filePath = fileUri.path + const fileExists = exists(filePath) if (fileExists) { await vscode.commands.executeCommand('vscode.open', fileUri, { preserveFocus: true }) - sendMessage(clearLoadingScreen()) + sendMessageToUtopia(clearLoadingScreen()) return true } else { + // FIXME We shouldn't need this // Just in case the message is processed before the file has been written to the FS if (retries > 0) { await wait(100) return openFile(fileUri, retries - 1) } else { - sendMessage(clearLoadingScreen()) + sendMessageToUtopia(clearLoadingScreen()) return false } } @@ -465,21 +494,18 @@ function cursorPositionChanged(event: vscode.TextEditorSelectionChangeEvent): vo const editor = event.textEditor const filename = editor.document.uri.path const position = editor.selection.active - sendMessage(editorCursorPositionChanged(filename, position.line, position.character)) + sendMessageToUtopia(editorCursorPositionChanged(filename, position.line, position.character)) } catch (error) { console.error('cursorPositionChanged failure.', error) } } -async function revealRangeIfPossible( - workspaceRootUri: vscode.Uri, - boundsInFile: BoundsInFile, -): Promise { +function revealRangeIfPossible(workspaceRootUri: vscode.Uri, boundsInFile: BoundsInFile) { const visibleEditor = vscode.window.visibleTextEditors.find( (editor) => editor.document.uri.path === boundsInFile.filePath, ) if (visibleEditor == null) { - const opened = await openFile(vscode.Uri.joinPath(workspaceRootUri, boundsInFile.filePath)) + const opened = openFile(vscode.Uri.joinPath(workspaceRootUri, boundsInFile.filePath)) if (opened) { revealRangeIfPossibleInVisibleEditor(boundsInFile) } @@ -588,10 +614,6 @@ function updateDecorations(decorations: Array): void { } } -function useFileSystemProviderErrors(projectID: string): void { - setErrorHandler((e) => toFileSystemProviderError(projectID, e)) -} - function toFileSystemProviderError(projectID: string, error: FSError): vscode.FileSystemError { const { path: unadjustedPath, code } = error const path = toUtopiaPath(projectID, unadjustedPath) @@ -604,8 +626,6 @@ function toFileSystemProviderError(projectID: string, error: FSError): vscode.Fi return vscode.FileSystemError.FileNotADirectory(path) case 'EEXIST': return vscode.FileSystemError.FileExists(path) - case 'FS_UNAVAILABLE': - return vscode.FileSystemError.Unavailable(path) default: const _exhaustiveCheck: never = code throw new Error(`Unhandled FS Error ${JSON.stringify(error)}`) diff --git a/utopia-vscode-extension/src/in-mem-fs.ts b/utopia-vscode-extension/src/in-mem-fs.ts new file mode 100644 index 000000000000..5932dc8cc94d --- /dev/null +++ b/utopia-vscode-extension/src/in-mem-fs.ts @@ -0,0 +1,400 @@ +import { stripTrailingSlash } from 'utopia-vscode-common' +import { getParentPath } from './path-utils' + +type FSNodeType = 'FILE' | 'DIRECTORY' + +interface FSNode { + type: FSNodeType + ctime: number + mtime: number + lastSavedTime: number +} + +interface FSNodeWithPath { + path: string + node: FSNode +} + +interface FSStat extends FSNode { + size: number +} + +interface FileContent { + content: Uint8Array + unsavedContent: Uint8Array | null +} + +interface FSFile extends FSNode, FileContent { + type: 'FILE' +} + +function fsFile( + content: Uint8Array, + unsavedContent: Uint8Array | null, + ctime: number, + mtime: number, + lastSavedTime: number, +): FSFile { + return { + type: 'FILE', + ctime: ctime, + mtime: mtime, + lastSavedTime: lastSavedTime, + content: content, + unsavedContent: unsavedContent, + } +} + +interface FSDirectory extends FSNode { + type: 'DIRECTORY' +} + +function fsDirectory(ctime: number, mtime: number): FSDirectory { + return { + type: 'DIRECTORY', + ctime: ctime, + mtime: mtime, + lastSavedTime: mtime, + } +} + +export function newFSDirectory(): FSDirectory { + const now = Date.now() + return { + type: 'DIRECTORY', + ctime: now, + mtime: now, + lastSavedTime: now, + } +} + +export function isFile(node: FSNode): node is FSFile { + return node.type === 'FILE' +} + +export function isDirectory(node: FSNode): node is FSDirectory { + return node.type === 'DIRECTORY' +} + +type FSErrorCode = 'ENOENT' | 'EEXIST' | 'EISDIR' | 'ENOTDIR' +export interface FSError { + code: FSErrorCode + path: string +} + +type FSErrorHandler = (e: FSError) => Error + +function fsError(code: FSErrorCode, path: string): FSError { + return { + code: code, + path: path, + } +} + +const encoder = new TextEncoder() +const decoder = new TextDecoder() + +const Store = new Map() +Store.set('', newFSDirectory()) + +export function keys(): IterableIterator { + return Store.keys() +} + +export function getItem(path: string): FSNode | undefined { + return Store.get(stripTrailingSlash(path)) +} + +export function setItem(path: string, value: FSNode) { + Store.set(stripTrailingSlash(path), value) +} + +export function removeItem(path: string) { + Store.delete(stripTrailingSlash(path)) +} + +let handleError: FSErrorHandler = (e: FSError) => { + let error = Error(`FS Error: ${JSON.stringify(e)}`) + error.name = e.code + return error +} + +export function setErrorHandler(handler: FSErrorHandler): void { + handleError = handler +} + +const missingFileError = (path: string) => handleError(fsError('ENOENT', path)) +const existingFileError = (path: string) => handleError(fsError('EEXIST', path)) +const isDirectoryError = (path: string) => handleError(fsError('EISDIR', path)) +export const isNotDirectoryError = (path: string) => handleError(fsError('ENOTDIR', path)) + +export function exists(path: string): boolean { + const value = getItem(path) + return value != null +} + +export function pathIsDirectory(path: string): boolean { + const node = getItem(path) + return node != null && isDirectory(node) +} + +export function pathIsFile(path: string): boolean { + const node = getItem(path) + return node != null && isFile(node) +} + +export function pathIsFileWithUnsavedContent(path: string): boolean { + const node = getItem(path) + return node != null && isFile(node) && node.unsavedContent != null +} + +function getNode(path: string): FSNode { + const node = getItem(path) + if (node == null) { + throw missingFileError(path) + } else { + return node + } +} + +function getFile(path: string): FSFile { + const node = getNode(path) + if (isFile(node)) { + return node + } else { + throw isDirectoryError(path) + } +} + +export function readFile(path: string): FileContent { + return getFile(path) +} + +export function readFileSavedContent(path: string): Uint8Array { + const fileNode = getFile(path) + return fileNode.content +} + +export function readFileUnsavedContent(path: string): Uint8Array | null { + const fileNode = getFile(path) + return fileNode.unsavedContent +} + +export interface StoredFile { + content: string + unsavedContent: string | null +} + +export function readFileAsUTF8(path: string): StoredFile { + const { content, unsavedContent } = getFile(path) + return { + content: decoder.decode(content), + unsavedContent: unsavedContent == null ? null : decoder.decode(unsavedContent), + } +} + +export function readFileSavedContentAsUTF8(path: string): string { + const { content } = readFileAsUTF8(path) + return content +} + +export function readFileUnsavedContentAsUTF8(path: string): string | null { + const { unsavedContent } = readFileAsUTF8(path) + return unsavedContent +} + +function fsStatForNode(node: FSNode): FSStat { + return { + type: node.type, + ctime: node.ctime, + mtime: node.mtime, + lastSavedTime: node.lastSavedTime, + size: isFile(node) ? node.content.length : 0, + } +} + +export function stat(path: string): FSStat { + const node = getNode(path) + return fsStatForNode(node) +} + +export function getDescendentPathsWithAllPaths( + path: string, + allPaths: Array, +): Array { + return allPaths.filter((k) => k != path && k.startsWith(path)) +} + +export function getDescendentPaths(path: string): string[] { + const allPaths = keys() + return getDescendentPathsWithAllPaths(path, Array.from(allPaths)) +} + +function targetsForOperation(path: string, recursive: boolean): string[] { + if (recursive) { + const allDescendents = getDescendentPaths(path) + let result = [path, ...allDescendents] + result.sort() + result.reverse() + return result + } else { + return [path] + } +} + +function filenameOfPath(path: string): string { + const target = path.endsWith('/') ? path.slice(0, -1) : path + const lastSlashIndex = target.lastIndexOf('/') + return lastSlashIndex >= 0 ? path.slice(lastSlashIndex + 1) : path +} + +export function childPaths(path: string): Array { + const allDescendents = getDescendentPaths(path) + const pathAsDir = stripTrailingSlash(path) + return allDescendents.filter((k) => getParentPath(k) === pathAsDir) +} + +function getDirectory(path: string): FSDirectory { + const node = getNode(path) + if (isDirectory(node)) { + return node + } else { + throw isNotDirectoryError(path) + } +} + +function getParent(path: string): FSNodeWithPath | null { + // null signifies we're already at the root + const parentPath = getParentPath(path) + if (parentPath == null) { + return null + } else { + const parentDir = getDirectory(parentPath) + return { + path: parentPath, + node: parentDir, + } + } +} + +export function readDirectory(path: string): Array { + getDirectory(path) // Ensure the path exists and is a directory + const children = childPaths(path) + return children.map(filenameOfPath) +} + +export function createDirectory(path: string) { + if (exists(path)) { + throw existingFileError(path) + } + + createDirectoryWithoutError(path) +} + +export function createDirectoryWithoutError(path: string) { + setItem(path, newFSDirectory()) + + const parent = getParent(path) + if (parent != null) { + markModified(parent) + } +} + +export function writeFile(path: string, content: Uint8Array, unsavedContent: Uint8Array | null) { + const parent = getParent(path) + const maybeExistingFile = getItem(path) + if (maybeExistingFile != null && isDirectory(maybeExistingFile)) { + throw isDirectoryError(path) + } + + const now = Date.now() + const fileCTime = maybeExistingFile == null ? now : maybeExistingFile.ctime + const lastSavedTime = + unsavedContent == null || maybeExistingFile == null ? now : maybeExistingFile.lastSavedTime + const fileToWrite = fsFile(content, unsavedContent, fileCTime, now, lastSavedTime) + setItem(path, fileToWrite) + if (parent != null) { + markModified(parent) + } +} + +export function writeFileSavedContent(path: string, content: Uint8Array) { + writeFile(path, content, null) +} + +export function writeFileUnsavedContent(path: string, unsavedContent: Uint8Array) { + const savedContent = readFileSavedContent(path) + writeFile(path, savedContent, unsavedContent) +} + +export function writeFileAsUTF8(path: string, content: string, unsavedContent: string | null) { + writeFile( + path, + encoder.encode(content), + unsavedContent == null ? null : encoder.encode(unsavedContent), + ) +} + +export function writeFileSavedContentAsUTF8(path: string, savedContent: string) { + writeFileAsUTF8(path, savedContent, null) +} + +export function writeFileUnsavedContentAsUTF8(path: string, unsavedContent: string) { + writeFileUnsavedContent(path, encoder.encode(unsavedContent)) +} + +export function clearFileUnsavedContent(path: string) { + const savedContent = readFileSavedContent(path) + writeFileSavedContent(path, savedContent) +} + +function updateMTime(node: FSNode): FSNode { + const now = Date.now() + if (isFile(node)) { + const lastSavedTime = node.unsavedContent == null ? now : node.lastSavedTime + return fsFile(node.content, node.unsavedContent, node.ctime, now, lastSavedTime) + } else { + return fsDirectory(node.ctime, now) + } +} + +function markModified(nodeWithPath: FSNodeWithPath) { + setItem(nodeWithPath.path, updateMTime(nodeWithPath.node)) +} + +function uncheckedMove(oldPath: string, newPath: string) { + const node = getNode(oldPath) + setItem(newPath, updateMTime(node)) + removeItem(oldPath) +} + +export function rename(oldPath: string, newPath: string) { + const oldParent = getParent(oldPath) + const newParent = getParent(newPath) + + const pathsToMove = targetsForOperation(oldPath, true) + const toNewPath = (p: string) => `${newPath}${p.slice(0, oldPath.length)}` + pathsToMove.forEach((pathToMove) => uncheckedMove(pathToMove, toNewPath(pathToMove))) + if (oldParent != null) { + markModified(oldParent) + } + if (newParent != null) { + markModified(newParent) + } +} + +export function deletePath(path: string, recursive: boolean) { + const parent = getParent(path) + const targets = targetsForOperation(path, recursive) + + // Really this should fail if recursive isn't set to true when trying to delete a + // non-empty directory, but for some reason VSCode doesn't provide an error suitable for that + for (const target of targets) { + removeItem(target) + } + + if (parent != null) { + markModified(parent) + } + return targets +} diff --git a/utopia-vscode-extension/src/path-utils.ts b/utopia-vscode-extension/src/path-utils.ts index 961a590af516..0a93330174ac 100644 --- a/utopia-vscode-extension/src/path-utils.ts +++ b/utopia-vscode-extension/src/path-utils.ts @@ -1,10 +1,36 @@ import { Uri } from 'vscode' -import { appendToPath, RootDir, toUtopiaPath } from 'utopia-vscode-common' +import { + appendToPath, + stripLeadingSlash, + stripTrailingSlash, + toUtopiaPath, +} from 'utopia-vscode-common' -export function toUtopiaURI(projectID: string, path: string): Uri { +export function addSchemeToPath(projectID: string, path: string): Uri { return Uri.parse(toUtopiaPath(projectID, path)) } -export function fromUtopiaURI(uri: Uri): string { - return appendToPath(RootDir, uri.path) +export function allPathsUpToPath(path: string): Array { + const directories = path.split('/') + const { paths: allPaths } = directories.reduce( + ({ paths, workingPath }, next) => { + const nextPath = appendToPath(workingPath, next) + return { + paths: paths.concat(nextPath), + workingPath: nextPath, + } + }, + { paths: ['/'], workingPath: '/' }, + ) + return allPaths +} + +export function getParentPath(path: string): string | null { + const withoutLeadingOrTrailingSlash = stripLeadingSlash(stripTrailingSlash(path)) + const pathElems = withoutLeadingOrTrailingSlash.split('/') + if (pathElems.length <= 1) { + return path === '/' || path === '' ? null : '' + } else { + return `/${pathElems.slice(0, -1).join('/')}` + } } diff --git a/utopia-vscode-extension/src/utopia-fs.ts b/utopia-vscode-extension/src/utopia-fs.ts index 54440ab1ee38..c03971029194 100644 --- a/utopia-vscode-extension/src/utopia-fs.ts +++ b/utopia-vscode-extension/src/utopia-fs.ts @@ -24,31 +24,35 @@ import { Position, Range, workspace, + commands, } from 'vscode' import { - watch, - stopWatching, stat, pathIsDirectory, createDirectory, readFile, exists, writeFile, - appendToPath, - dirname, - stripRootPrefix, deletePath, rename, getDescendentPaths, isDirectory, readDirectory, - RootDir, readFileSavedContent, writeFileSavedContent, readFileSavedContentAsUTF8, pathIsFileWithUnsavedContent, -} from 'utopia-vscode-common' -import { fromUtopiaURI, toUtopiaURI } from './path-utils' + readFileAsUTF8, + writeFileAsUTF8, + getItem, + createDirectoryWithoutError, + isFile, + isNotDirectoryError, + pathIsFile, +} from './in-mem-fs' +import { appendToPath, dirname, vsCodeFileDelete } from 'utopia-vscode-common' +import type { ProjectFile } from 'utopia-vscode-common' +import { addSchemeToPath, allPathsUpToPath } from './path-utils' interface EventQueue { queue: T[] @@ -69,9 +73,8 @@ export class UtopiaFSExtension { private disposable: Disposable + // This is the event queue for notifying VS Code of file changes private fileChangeEventQueue = newEventQueue() - private utopiaSavedChangeEventQueue = newEventQueue() - private utopiaUnsavedChangeEventQueue = newEventQueue() private allFilePaths: string[] | null = null @@ -89,13 +92,9 @@ export class UtopiaFSExtension // FileSystemProvider readonly onDidChangeFile: Event = this.fileChangeEventQueue.emitter.event - readonly onUtopiaDidChangeSavedContent: Event = - this.utopiaSavedChangeEventQueue.emitter.event - readonly onUtopiaDidChangeUnsavedContent: Event = - this.utopiaUnsavedChangeEventQueue.emitter.event - private queueEvent(event: T, eventQueue: EventQueue): void { - eventQueue.queue.push(event) + private queueEvents(events: Array, eventQueue: EventQueue): void { + eventQueue.queue.push(...events) if (eventQueue.handle != null) { clearTimeout(eventQueue.handle) @@ -107,82 +106,96 @@ export class UtopiaFSExtension }, 5) } - private queueFileChangeEvent(event: FileChangeEvent): void { + private queueFileChangeEvents(events: Array): void { this.clearCachedFiles() - this.queueEvent(event, this.fileChangeEventQueue) + this.queueEvents(events, this.fileChangeEventQueue) } - private queueUtopiaSavedChangeEvent(resource: Uri): void { - this.queueEvent(resource, this.utopiaSavedChangeEventQueue) - } - - private queueUtopiaUnsavedChangeEvent(resource: Uri): void { - this.queueEvent(resource, this.utopiaUnsavedChangeEventQueue) - } - - private async notifyFileChanged(path: string, modifiedBySelf: boolean): Promise { - const uri = toUtopiaURI(this.projectID, path) - const hasUnsavedContent = await pathIsFileWithUnsavedContent(path) + private notifyFileChanged(path: string) { + const uri = addSchemeToPath(this.projectID, path) + const hasUnsavedContent = pathIsFileWithUnsavedContent(path) const fileWasSaved = !hasUnsavedContent if (fileWasSaved) { // Notify VS Code of updates to the saved content - this.queueFileChangeEvent({ - type: FileChangeType.Changed, - uri: uri, - }) - } - - if (!modifiedBySelf) { - // Notify our extension of changes coming from Utopia only - if (fileWasSaved) { - this.queueUtopiaSavedChangeEvent(uri) - } else { - this.queueUtopiaUnsavedChangeEvent(uri) - } + this.queueFileChangeEvents([ + { + type: FileChangeType.Changed, + uri: uri, + }, + ]) } } - private notifyFileCreated(path: string): void { - this.queueFileChangeEvent({ - type: FileChangeType.Created, - uri: toUtopiaURI(this.projectID, path), - }) + private notifyFileCreated(path: string) { + const parentDirectory = dirname(path) + this.queueFileChangeEvents([ + { + type: FileChangeType.Created, + uri: addSchemeToPath(this.projectID, path), + }, + { + type: FileChangeType.Changed, + uri: addSchemeToPath(this.projectID, parentDirectory), + }, + ]) } - private notifyFileDeleted(path: string): void { - this.queueFileChangeEvent({ - type: FileChangeType.Deleted, - uri: toUtopiaURI(this.projectID, path), - }) + private notifyFileDeleted(path: string) { + const parentDirectory = dirname(path) + this.queueFileChangeEvents([ + { + type: FileChangeType.Deleted, + uri: addSchemeToPath(this.projectID, path), + }, + { + type: FileChangeType.Changed, + uri: addSchemeToPath(this.projectID, parentDirectory), + }, + ]) } - watch(uri: Uri, options: { recursive: boolean; excludes: string[] }): Disposable { - const path = fromUtopiaURI(uri) - watch( - path, - options.recursive, - this.notifyFileCreated.bind(this), - this.notifyFileChanged.bind(this), - this.notifyFileDeleted.bind(this), - () => { - /* no op */ + private notifyFileRenamed(oldPath: string, newPath: string) { + const oldParentDirectory = dirname(oldPath) + const newParentDirectory = dirname(newPath) + const parentChanged = oldParentDirectory !== newParentDirectory + this.queueFileChangeEvents([ + { + type: FileChangeType.Deleted, + uri: addSchemeToPath(this.projectID, oldPath), }, - ) + { + type: FileChangeType.Created, + uri: addSchemeToPath(this.projectID, newPath), + }, + { + type: FileChangeType.Changed, + uri: addSchemeToPath(this.projectID, oldParentDirectory), + }, + ...(parentChanged + ? [ + { + type: FileChangeType.Changed, + uri: addSchemeToPath(this.projectID, newParentDirectory), + }, + ] + : []), + ]) + } - return new Disposable(() => { - stopWatching(path, options.recursive) - }) + watch(): Disposable { + // No need for this since all events are manually fired + return new Disposable(() => {}) } - async exists(uri: Uri): Promise { - const path = fromUtopiaURI(uri) + exists(uri: Uri): boolean { + const path = uri.path return exists(path) } - async stat(uri: Uri): Promise { - const path = fromUtopiaURI(uri) - const stats = await stat(path) + stat(uri: Uri): FileStat { + const path = uri.path + const stats = stat(path) const fileType = isDirectory(stats) ? FileType.Directory : FileType.File return { @@ -193,36 +206,30 @@ export class UtopiaFSExtension } } - async readDirectory(uri: Uri): Promise<[string, FileType][]> { - const path = fromUtopiaURI(uri) - const children = await readDirectory(path) - const result: Promise<[string, FileType]>[] = children.map((childName) => - pathIsDirectory(appendToPath(path, childName)).then((resultIsDirectory) => [ - childName, - resultIsDirectory ? FileType.Directory : FileType.File, - ]), - ) - return Promise.all(result) + readDirectory(uri: Uri): Array<[string, FileType]> { + const path = uri.path + const children = readDirectory(path) + return children.map((childName) => { + const resultIsDirectory = pathIsDirectory(appendToPath(path, childName)) + return [childName, resultIsDirectory ? FileType.Directory : FileType.File] + }) } - async createDirectory(uri: Uri): Promise { - const path = fromUtopiaURI(uri) - await createDirectory(path) + createDirectory(uri: Uri) { + const path = uri.path + createDirectory(path) + this.notifyFileCreated(path) } - async readFile(uri: Uri): Promise { - const path = fromUtopiaURI(uri) + readFile(uri: Uri): Uint8Array { + const path = uri.path return readFileSavedContent(path) } - async writeFile( - uri: Uri, - content: Uint8Array, - options: { create: boolean; overwrite: boolean }, - ): Promise { - const path = fromUtopiaURI(uri) + writeFile(uri: Uri, content: Uint8Array, options: { create: boolean; overwrite: boolean }) { + const path = uri.path + const fileExists = exists(path) if (!options.create || !options.overwrite) { - const fileExists = await exists(path) if (!fileExists && !options.create) { throw FileSystemError.FileNotFound(uri) } else if (fileExists && !options.overwrite) { @@ -230,90 +237,159 @@ export class UtopiaFSExtension } } - await writeFileSavedContent(path, content) + writeFileSavedContent(path, content) + if (fileExists) { + this.notifyFileChanged(path) + } else { + this.notifyFileCreated(path) + } + } + + ensureDirectoryExists(pathToEnsure: string) { + const allPaths = allPathsUpToPath(pathToEnsure) + let createdDirectories: Array = [] + for (const pathToCreate of allPaths) { + const existingNode = getItem(pathToCreate) + if (existingNode == null) { + createDirectoryWithoutError(pathToCreate) + createdDirectories.push(pathToCreate) + } else if (isFile(existingNode)) { + throw isNotDirectoryError(pathToCreate) + } + } + + createdDirectories.forEach((createdDirectory) => this.notifyFileCreated(createdDirectory)) + } + + writeProjectFile(projectFile: ProjectFile) { + switch (projectFile.type) { + case 'PROJECT_DIRECTORY': { + const { filePath } = projectFile + this.ensureDirectoryExists(filePath) + break + } + case 'PROJECT_TEXT_FILE': { + const { filePath, savedContent, unsavedContent } = projectFile + const fileExists = exists(filePath) + const alreadyExistingFile = fileExists ? readFileAsUTF8(filePath) : null + const fileDiffers = + alreadyExistingFile == null || + alreadyExistingFile.content !== savedContent || + alreadyExistingFile.unsavedContent !== unsavedContent + if (fileDiffers) { + // Avoid pushing a file to the file system if the content hasn't changed. + writeFileAsUTF8(filePath, savedContent, unsavedContent) + + if (fileExists) { + this.notifyFileChanged(filePath) + } else { + this.notifyFileCreated(filePath) + } + } + break + } + default: + const _exhaustiveCheck: never = projectFile + throw new Error(`Invalid file projectFile type ${projectFile}`) + } + } + + delete(uri: Uri, options: { recursive: boolean }) { + this.silentDelete(uri.path, options) + commands.executeCommand('utopia.toUtopiaMessage', vsCodeFileDelete(uri.path)) } - async delete(uri: Uri, options: { recursive: boolean }): Promise { - const path = fromUtopiaURI(uri) - await deletePath(path, options.recursive) + // "silent" because it doesn't send the message to Utopia. It still emits the event. + silentDelete(path: string, options: { recursive: boolean }) { + deletePath(path, options.recursive) + this.notifyFileDeleted(path) } - async rename(oldUri: Uri, newUri: Uri, options: { overwrite: boolean }): Promise { - const oldPath = fromUtopiaURI(oldUri) - const newPath = fromUtopiaURI(newUri) + rename(oldUri: Uri, newUri: Uri, options: { overwrite: boolean }) { + const oldPath = oldUri.path + const newPath = newUri.path if (!options.overwrite) { - const fileExists = await exists(newPath) + const fileExists = exists(newPath) if (fileExists) { throw FileSystemError.FileExists(newUri) } } - await rename(oldPath, newPath) + rename(oldPath, newPath) + this.notifyFileRenamed(oldPath, newPath) } - async copy(source: Uri, destination: Uri, options: { overwrite: boolean }): Promise { + copy(source: Uri, destination: Uri, options: { overwrite: boolean }) { // It's not clear where this will ever be called from, but it seems to be from the side bar // that isn't available in Utopia, so this implementation is "just in case" - const sourcePath = fromUtopiaURI(source) - const destinationPath = fromUtopiaURI(destination) + const sourcePath = source.path + const destinationPath = destination.path const destinationParentDir = dirname(destinationPath) - const destinationParentDirExists = await exists(destinationParentDir) + const destinationParentDirExists = exists(destinationParentDir) if (!destinationParentDirExists) { - throw FileSystemError.FileNotFound(toUtopiaURI(this.projectID, destinationParentDir)) + throw FileSystemError.FileNotFound(addSchemeToPath(this.projectID, destinationParentDir)) } if (!options.overwrite) { - const destinationExists = await exists(destinationPath) + const destinationExists = exists(destinationPath) if (destinationExists && !options.overwrite) { throw FileSystemError.FileExists(destination) } } - const { content, unsavedContent } = await readFile(sourcePath) - await writeFile(destinationPath, content, unsavedContent) + const { content, unsavedContent } = readFile(sourcePath) + writeFile(destinationPath, content, unsavedContent) + this.notifyFileCreated(destinationPath) } // FileSearchProvider - async provideFileSearchResults( + provideFileSearchResults( query: FileSearchQuery, options: FileSearchOptions, _token: CancellationToken, - ): Promise { + ): Array { // TODO Support all search options - const { result: foundPaths } = await this.filterFilePaths(query.pattern, options.maxResults) - return foundPaths.map((p) => toUtopiaURI(this.projectID, p)) + const lowerCaseQuery = query.pattern.toLocaleLowerCase() + const filePaths = this.getAllFilePaths() + const foundPaths = filePaths.filter((p) => p.toLocaleLowerCase().includes(lowerCaseQuery)) + return foundPaths.map((p) => addSchemeToPath(this.projectID, p)) } // TextSearchProvider - async provideTextSearchResults( + provideTextSearchResults( query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken, - ): Promise { + ): TextSearchComplete { // This appears to only be callable from the side bar that isn't available in Utopia // TODO Support all search options - const { result: filePaths, limitHit } = await this.filterFilePaths(options.includes[0]) + const filePaths = this.filterFilePaths(options.includes[0]) if (filePaths.length > 0) { + const isCaseSensitive = query.isCaseSensitive ?? false + const lowerCaseQuery = query.pattern.toLocaleLowerCase() + for (const filePath of filePaths) { if (token.isCancellationRequested) { break } - const content = await readFileSavedContentAsUTF8(filePath) + const content = readFileSavedContentAsUTF8(filePath) const lines = splitIntoLines(content) for (let i = 0; i < lines.length; i++) { const line = lines[i] - const index = line.indexOf(query.pattern) + const index = isCaseSensitive + ? line.indexOf(query.pattern) + : line.toLocaleLowerCase().indexOf(lowerCaseQuery) if (index !== -1) { progress.report({ - uri: toUtopiaURI(this.projectID, filePath), + uri: addSchemeToPath(this.projectID, filePath), ranges: new Range( new Position(i, index), new Position(i, index + query.pattern.length), @@ -331,49 +407,35 @@ export class UtopiaFSExtension } } - return { limitHit: limitHit } + return { limitHit: false } } // Common - private async filterFilePaths( - query: string | undefined, - maxResults?: number, - ): Promise<{ result: string[]; limitHit: boolean }> { - const filePaths = await this.getAllPaths() - let result: string[] = [] - let limitHit = false - let remainingCount = maxResults == null ? Infinity : maxResults + private filterFilePaths(query: string | undefined): Array { + const filePaths = this.getAllFilePaths() + if (query == null) { + return filePaths + } - const pattern = query ? new RegExp(convertSimple2RegExpPattern(query)) : null + let result: string[] = [] + const pattern = new RegExp(convertSimple2RegExpPattern(query)) for (const path of filePaths) { - if (remainingCount < 0) { - break - } - - if (!pattern || pattern.exec(stripRootPrefix(path))) { - if (remainingCount === 0) { - // We've already found the max number of results, but we want to flag that there are more - limitHit = true - } else { - result.push(path) - } - remainingCount-- + if (!pattern || pattern.exec(path)) { + result.push(path) } } - return { - result: result, - limitHit: limitHit, - } + return result } - async getAllPaths(): Promise { + getAllFilePaths(): Array { if (this.allFilePaths == null) { - const result = await getDescendentPaths(RootDir) - this.allFilePaths = result - return result + const allPaths = getDescendentPaths('') + const allFilePaths = allPaths.filter((p) => pathIsFile(p)) + this.allFilePaths = allFilePaths + return allFilePaths } else { return this.allFilePaths } diff --git a/vscode-build/build.js b/vscode-build/build.js index 48c15cc5fb40..e0224f11e95e 100644 --- a/vscode-build/build.js +++ b/vscode-build/build.js @@ -5,7 +5,7 @@ const fse = require('fs-extra') const glob = require('glob') const rmdir = require('rimraf') -const vscodeVersion = '1.61.2' +const vscodeVersion = '1.91.1' if (fs.existsSync('vscode')) { process.chdir('vscode') @@ -35,9 +35,7 @@ child_process.execSync(`git apply ../vscode.patch`, { child_process.execSync('yarn', { stdio: 'inherit' }) // Compile -child_process.execSync('yarn gulp compile-build', { stdio: 'inherit' }) -child_process.execSync('yarn gulp minify-vscode', { stdio: 'inherit' }) -child_process.execSync('yarn compile-web', { stdio: 'inherit' }) +child_process.execSync('yarn gulp vscode-web-min', { stdio: 'inherit' }) // Remove maps const mapFiles = glob.sync('out-vscode-min/**/*.js.map', {}) @@ -50,13 +48,12 @@ if (fs.existsSync('../dist')) { fs.rmdirSync('../dist', { recursive: true }) } -fs.mkdirSync('../dist') +fse.moveSync('../vscode-web', '../dist') fs.mkdirSync('../dist/lib') -fse.copySync('out-vscode-min', '../dist/vscode') +fse.copySync('resources', '../dist/vscode/resources') fse.copySync('product.json', '../dist/product.json') -fse.copySync('../node_modules/semver-umd', '../dist/lib/semver-umd') -fse.copySync('../node_modules/vscode-oniguruma', '../dist/lib/vscode-oniguruma') -fse.copySync('../node_modules/vscode-textmate', '../dist/lib/vscode-textmate') +fse.copySync('node_modules/vscode-oniguruma', '../dist/lib/vscode-oniguruma') +fse.copySync('node_modules/vscode-textmate', '../dist/lib/vscode-textmate') fse.copySync('../node_modules/utopia-vscode-common', '../dist/lib/utopia-vscode-common') const extensionNM = glob.sync('extensions/**/node_modules', {}) diff --git a/vscode-build/package.json b/vscode-build/package.json index bb6eaff8b244..61ac2bb57d8a 100644 --- a/vscode-build/package.json +++ b/vscode-build/package.json @@ -16,10 +16,7 @@ "fs-extra": "9.0.1", "glob": "7.1.6", "rimraf": "3.0.2", - "semver-umd": "5.5.7", - "utopia-vscode-common": "file:../utopia-vscode-common", - "vscode-oniguruma": "1.4.0", - "vscode-textmate": "5.2.0" + "utopia-vscode-common": "file:../utopia-vscode-common" }, "dependencies": {} } diff --git a/vscode-build/pull-utopia-extension.js b/vscode-build/pull-utopia-extension.js index a5f0b8e026b9..7535a0bcb0d9 100644 --- a/vscode-build/pull-utopia-extension.js +++ b/vscode-build/pull-utopia-extension.js @@ -36,7 +36,7 @@ for (const extension of extensionsContent) { extensions.push({ packageJSON, extensionPath: extension, - packageNLS, + ...(packageNLS == null ? {} : { packageNLS }), }) } } diff --git a/vscode-build/shell.nix b/vscode-build/shell.nix index 8acb32e59ef8..0f904326cabc 100644 --- a/vscode-build/shell.nix +++ b/vscode-build/shell.nix @@ -1,7 +1,7 @@ let release = (import ../release.nix {}); - pkgs = release.pkgs; - node = pkgs.nodejs-16_x; + pkgs = release.recentPkgs; + node = pkgs.nodejs_20; stdenv = pkgs.stdenv; pnpm = node.pkgs.pnpm; yarn = pkgs.yarn.override { nodejs = node; }; diff --git a/vscode-build/vscode.patch b/vscode-build/vscode.patch index b1508679304c..0d851e68d257 100644 --- a/vscode-build/vscode.patch +++ b/vscode-build/vscode.patch @@ -1,35 +1,31 @@ -diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js -index 13c20bed989..2a8452a08ab 100644 ---- a/build/gulpfile.vscode.js -+++ b/build/gulpfile.vscode.js -@@ -35,13 +35,14 @@ const { compileExtensionsBuildTask } = require('./gulpfile.extensions'); +diff --git a/build/gulpfile.compile.js b/build/gulpfile.compile.js +index c4947e76cbf..c2dd4189229 100644 +--- a/build/gulpfile.compile.js ++++ b/build/gulpfile.compile.js +@@ -22,7 +22,9 @@ function makeCompileBuildTask(disableMangle) { + } + + // Full compile, including nls and inline sources in sourcemaps, mangling, minification, for build +-const compileBuildTask = task.define('compile-build', makeCompileBuildTask(false)); ++// The `true` here is to disable mangling. Minification still happens. ++// For some reason the mangling was causing the build to completely hang on my machine. ++const compileBuildTask = task.define('compile-build', makeCompileBuildTask(true)); + gulp.task(compileBuildTask); + exports.compileBuildTask = compileBuildTask; + +diff --git a/build/gulpfile.vscode.web.js b/build/gulpfile.vscode.web.js +index 50c7e6fb631..629a119108b 100644 +--- a/build/gulpfile.vscode.web.js ++++ b/build/gulpfile.vscode.web.js +@@ -186,7 +186,7 @@ function packageTask(sourceFolderName, destinationFolderName) { + const json = require('gulp-json-editor'); - // Build - const vscodeEntryPoints = _.flatten([ -- buildfile.entrypoint('vs/workbench/workbench.desktop.main'), -+ buildfile.entrypoint('vs/workbench/workbench.web.api'), - buildfile.base, - buildfile.workerExtensionHost, - buildfile.workerNotebook, - buildfile.workerLanguageDetection, - buildfile.workerLocalFileSearch, -- buildfile.workbenchDesktop, -+ buildfile.workbenchWeb, -+ buildfile.keyboardMaps, - buildfile.code - ]); + const src = gulp.src(sourceFolderName + '/**', { base: '.' }) +- .pipe(rename(function (path) { path.dirname = path.dirname.replace(new RegExp('^' + sourceFolderName), 'out'); })); ++ .pipe(rename(function (path) { path.dirname = path.dirname.replace(new RegExp('^' + sourceFolderName), 'vscode'); })); -@@ -157,8 +158,8 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op + const extensions = gulp.src('.build/web/extensions/**', { base: '.build/web', dot: true }); - const checksums = computeChecksums(out, [ - 'vs/base/parts/sandbox/electron-browser/preload.js', -- 'vs/workbench/workbench.desktop.main.js', -- 'vs/workbench/workbench.desktop.main.css', -+ 'vs/workbench/workbench.web.api.js', -+ 'vs/workbench/workbench.web.api.css', - 'vs/workbench/services/extensions/node/extensionHostProcess.js', - 'vs/code/electron-browser/workbench/workbench.html', - 'vs/code/electron-browser/workbench/workbench.js' diff --git a/extensions/css-language-features/server/test/linksTestFixtures/node_modules/foo/package.json b/extensions/css-language-features/server/test/linksTestFixtures/node_modules/foo/package.json deleted file mode 100644 index 9e26dfeeb6e..00000000000 @@ -38,59 +34,27 @@ index 9e26dfeeb6e..00000000000 @@ -1 +0,0 @@ -{} \ No newline at end of file -diff --git a/extensions/simple-browser/src/simpleBrowserView.ts b/extensions/simple-browser/src/simpleBrowserView.ts -index 2d7da1aecf8..052f52383f0 100644 ---- a/extensions/simple-browser/src/simpleBrowserView.ts -+++ b/extensions/simple-browser/src/simpleBrowserView.ts -@@ -139,7 +139,7 @@ export class SimpleBrowserView extends Disposable { - -
-
${localize('view.iframe-focused', "Focus Lock")}
-- -+ -
- - diff --git a/package.json b/package.json -index 2bca5115cac..53884a9bb17 100644 +index 2103fe1fe1a..92954953615 100644 --- a/package.json +++ b/package.json -@@ -44,15 +44,15 @@ - "valid-layers-check": "node build/lib/layersChecker.js", - "update-distro": "node build/npm/update-distro.js", - "web": "node resources/web/code-web.js", -- "compile-web": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js compile-web", -- "watch-web": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js watch-web", -+ "compile-web": "node --max_old_space_size=4095 $NODE_OPENSSL_OPTION ./node_modules/gulp/bin/gulp.js compile-web", -+ "watch-web": "node --max_old_space_size=4095 $NODE_OPENSSL_OPTION ./node_modules/gulp/bin/gulp.js watch-web", - "eslint": "node build/eslint", - "playwright-install": "node build/azure-pipelines/common/installPlaywright.js", -- "compile-build": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js compile-build", -- "compile-extensions-build": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js compile-extensions-build", -- "minify-vscode": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js minify-vscode", -- "minify-vscode-reh": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js minify-vscode-reh", -- "minify-vscode-reh-web": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js minify-vscode-reh-web", -+ "compile-build": "node --max_old_space_size=4095 $NODE_OPENSSL_OPTION ./node_modules/gulp/bin/gulp.js compile-build", -+ "compile-extensions-build": "node --max_old_space_size=4095 $NODE_OPENSSL_OPTION ./node_modules/gulp/bin/gulp.js compile-extensions-build", -+ "minify-vscode": "node --max_old_space_size=4095 $NODE_OPENSSL_OPTION ./node_modules/gulp/bin/gulp.js minify-vscode", -+ "minify-vscode-reh": "node --max_old_space_size=4095 $NODE_OPENSSL_OPTION ./node_modules/gulp/bin/gulp.js minify-vscode-reh", -+ "minify-vscode-reh-web": "node --max_old_space_size=4095 $NODE_OPENSSL_OPTION ./node_modules/gulp/bin/gulp.js minify-vscode-reh-web", - "hygiene": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js hygiene", - "core-ci": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js core-ci", - "extensions-ci": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js extensions-ci" -@@ -77,6 +77,7 @@ - "spdlog": "^0.13.0", - "sudo-prompt": "9.2.1", - "tas-client-umd": "0.1.4", +@@ -96,10 +96,10 @@ + "kerberos": "^2.0.1", + "minimist": "^1.2.6", + "native-is-elevated": "0.7.0", +- "native-keymap": "^3.3.5", + "native-watchdog": "^1.4.1", + "node-pty": "1.1.0-beta11", + "tas-client-umd": "0.2.0", + "utopia-vscode-common": "file:../../utopia-vscode-common", - "v8-inspect-profiler": "^0.0.22", - "vscode-nsfw": "2.1.8", - "vscode-oniguruma": "1.5.1", + "v8-inspect-profiler": "^0.1.1", + "vscode-oniguruma": "1.7.0", + "vscode-regexpp": "^3.1.0", diff --git a/product.json b/product.json -index 7b60eac641d..b37ebcd9f73 100644 +index 27ae53fe16b..83637043bdc 100644 --- a/product.json +++ b/product.json -@@ -1,96 +1,9 @@ +@@ -1,84 +1,9 @@ { - "nameShort": "Code - OSS", - "nameLong": "Code - OSS", @@ -99,50 +63,36 @@ index 7b60eac641d..b37ebcd9f73 100644 - "win32MutexName": "vscodeoss", - "licenseName": "MIT", - "licenseUrl": "https://github.com/microsoft/vscode/blob/main/LICENSE.txt", +- "serverLicenseUrl": "https://github.com/microsoft/vscode/blob/main/LICENSE.txt", +- "serverGreeting": [], +- "serverLicense": [], +- "serverLicensePrompt": "", +- "serverApplicationName": "code-server-oss", +- "serverDataFolderName": ".vscode-server-oss", +- "tunnelApplicationName": "code-tunnel-oss", - "win32DirName": "Microsoft Code OSS", - "win32NameVersion": "Microsoft Code OSS", - "win32RegValueName": "CodeOSS", -- "win32AppId": "{{E34003BB-9E10-4501-8C11-BE3FAA83F23F}", - "win32x64AppId": "{{D77B7E06-80BA-4137-BCF4-654B95CCEBC5}", - "win32arm64AppId": "{{D1ACE434-89C5-48D1-88D3-E2991DF85475}", -- "win32UserAppId": "{{C6065F05-9603-4FC4-8101-B9781A25D88E}", - "win32x64UserAppId": "{{CC6B787D-37A0-49E8-AE24-8559A032BE0C}", - "win32arm64UserAppId": "{{3AEBF0C8-F733-4AD4-BADE-FDB816D53D7B}", - "win32AppUserModelId": "Microsoft.CodeOSS", - "win32ShellNameShort": "C&ode - OSS", +- "win32TunnelServiceMutex": "vscodeoss-tunnelservice", +- "win32TunnelMutex": "vscodeoss-tunnel", - "darwinBundleIdentifier": "com.visualstudio.code.oss", -- "linuxIconName": "com.visualstudio.code.oss", +- "linuxIconName": "code-oss", - "licenseFileName": "LICENSE.txt", - "reportIssueUrl": "https://github.com/microsoft/vscode/issues/new", +- "nodejsRepository": "https://nodejs.org", - "urlProtocol": "code-oss", -- "webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-webview.net/{{quality}}/{{commit}}/out/vs/workbench/contrib/webview/browser/pre/", -- "extensionAllowedProposedApi": [ -- "ms-vscode.vscode-js-profile-flame", -- "ms-vscode.vscode-js-profile-table", -- "ms-vscode.remotehub", -- "ms-vscode.remotehub-insiders", -- "GitHub.remotehub", -- "GitHub.remotehub-insiders" -- ], +- "webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-cdn.net/insider/ef65ac1ba57f57f2a3961bfe94aa20481caca4c6/out/vs/workbench/contrib/webview/browser/pre/", - "builtInExtensions": [ - { -- "name": "ms-vscode.references-view", -- "version": "0.0.81", -- "repo": "https://github.com/microsoft/vscode-references-view", -- "metadata": { -- "id": "dc489f46-520d-4556-ae85-1f9eab3c412d", -- "publisherId": { -- "publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee", -- "publisherName": "ms-vscode", -- "displayName": "Microsoft", -- "flags": "verified" -- }, -- "publisherDisplayName": "Microsoft" -- } -- }, -- { - "name": "ms-vscode.js-debug-companion", -- "version": "1.0.15", +- "version": "1.1.2", +- "sha256": "e034b8b41beb4e97e02c70f7175bd88abe66048374c2bd629f54bb33354bc2aa", - "repo": "https://github.com/microsoft/vscode-js-debug-companion", - "metadata": { - "id": "99cb0b7f-7354-4278-b8da-6cc79972169d", @@ -157,7 +107,8 @@ index 7b60eac641d..b37ebcd9f73 100644 - }, - { - "name": "ms-vscode.js-debug", -- "version": "1.61.0", +- "version": "1.91.0", +- "sha256": "53b99146c7fa280f00c74414e09721530c622bf3e5eac2c967ddfb9906b51c80", - "repo": "https://github.com/microsoft/vscode-js-debug", - "metadata": { - "id": "25629058-ddac-4e17-abba-74678e126c5d", @@ -172,7 +123,8 @@ index 7b60eac641d..b37ebcd9f73 100644 - }, - { - "name": "ms-vscode.vscode-js-profile-table", -- "version": "0.0.18", +- "version": "1.0.9", +- "sha256": "3b62ee4276a2bbea3fe230f94b1d5edd915b05966090ea56f882e1e0ab53e1a6", - "repo": "https://github.com/microsoft/vscode-js-profile-visualizer", - "metadata": { - "id": "7e52b41b-71ad-457b-ab7e-0620f1fc4feb", @@ -186,349 +138,529 @@ index 7b60eac641d..b37ebcd9f73 100644 - } - } - ] +-} + "productConfiguration": { + "nameShort": "Code Web", + "nameLong": "Code Web", + "applicationName": "code-web", + "dataFolderName": ".vscode-web", -+ "version": "1.62.0" ++ "version": "1.91.1" + } - } ++} +\ No newline at end of file diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts -index 6148eb30092..6948ededa4a 100644 +index 66d30c3aca3..25f12f879e4 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts -@@ -365,7 +365,11 @@ export function getClientArea(element: HTMLElement): Dimension { - return new Dimension(document.documentElement.clientWidth, document.documentElement.clientHeight); +@@ -495,8 +495,11 @@ export function getClientArea(element: HTMLElement, fallback?: HTMLElement): Dim + if (fallback) { + return getClientArea(fallback); } - +- - throw new Error('Unable to figure out browser width and height'); + // this Error would prevent VSCode from loading inside Utopia if the browser tab is not in the foreground + // throw new Error('Unable to figure out browser width and height'); + -+ // Instead, we just return 0 x 0, it seems to be fine -+ return new Dimension(0, 0); ++ // Instead, we just return 1 x 1, as a non-zero value is required for laying out the editor correctly ++ return new Dimension(1, 1); } class SizeUtils { diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts -index 534fe232636..b41446430f0 100644 +index f8875029a8a..3bdfd1bfa64 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts -@@ -1,536 +1,101 @@ --/*--------------------------------------------------------------------------------------------- -- * Copyright (c) Microsoft Corporation. All rights reserved. -- * Licensed under the MIT License. See License.txt in the project root for license information. -- *--------------------------------------------------------------------------------------------*/ -- +@@ -3,586 +3,41 @@ + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + -import { isStandalone } from 'vs/base/browser/browser'; --import { streamToBuffer } from 'vs/base/common/buffer'; --import { CancellationToken } from 'vs/base/common/cancellation'; --import { Emitter, Event } from 'vs/base/common/event'; --import { Disposable } from 'vs/base/common/lifecycle'; + import { mainWindow } from 'vs/base/browser/window'; +-import { VSBuffer, decodeBase64, encodeBase64 } from 'vs/base/common/buffer'; +-import { Emitter } from 'vs/base/common/event'; +-import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +-import { parse } from 'vs/base/common/marshalling'; -import { Schemas } from 'vs/base/common/network'; +-import { posix } from 'vs/base/common/path'; -import { isEqual } from 'vs/base/common/resources'; +-import { ltrim } from 'vs/base/common/strings'; -import { URI, UriComponents } from 'vs/base/common/uri'; --import { generateUuid } from 'vs/base/common/uuid'; --import { request } from 'vs/base/parts/request/browser/request'; --import { localize } from 'vs/nls'; --import { parseLogLevel } from 'vs/platform/log/common/log'; -import product from 'vs/platform/product/common/product'; --import { isFolderToOpen, isWorkspaceToOpen } from 'vs/platform/windows/common/windows'; --import { create, ICredentialsProvider, IHomeIndicator, IProductQualityChangeHandler, ISettingsSyncOptions, IURLCallbackProvider, IWelcomeBanner, IWindowIndicator, IWorkbenchConstructionOptions, IWorkspace, IWorkspaceProvider } from 'vs/workbench/workbench.web.api'; -- --function doCreateUri(path: string, queryValues: Map): URI { -- let query: string | undefined = undefined; -- -- if (queryValues) { -- let index = 0; -- queryValues.forEach((value, key) => { -- if (!query) { -- query = ''; -- } -- -- const prefix = (index++ === 0) ? '' : '&'; -- query += `${prefix}${key}=${encodeURIComponent(value)}`; -- }); +-import { ISecretStorageProvider } from 'vs/platform/secrets/common/secrets'; +-import { isFolderToOpen, isWorkspaceToOpen } from 'vs/platform/window/common/window'; ++import { URI } from 'vs/base/common/uri'; + import type { IWorkbenchConstructionOptions, IWorkspace, IWorkspaceProvider } from 'vs/workbench/browser/web.api'; +-import { AuthenticationSessionInfo } from 'vs/workbench/services/authentication/browser/authenticationService'; +-import type { IURLCallbackProvider } from 'vs/workbench/services/url/browser/urlService'; + import { create } from 'vs/workbench/workbench.web.main'; + +-interface ISecretStorageCrypto { +- seal(data: string): Promise; +- unseal(data: string): Promise; +-} ++(async function () { ++ // create workbench ++ const result = await fetch('/vscode/product.json') ++ const loadedConfig: IWorkbenchConstructionOptions = await result.json() + +-class TransparentCrypto implements ISecretStorageCrypto { +- async seal(data: string): Promise { +- return data; - } - -- return URI.parse(window.location.href).with({ path, query }); +- async unseal(data: string): Promise { +- return data; +- } -} - --interface ICredential { -- service: string; -- account: string; -- password: string; +-const enum AESConstants { +- ALGORITHM = 'AES-GCM', +- KEY_LENGTH = 256, +- IV_LENGTH = 12, -} - --class LocalStorageCredentialsProvider implements ICredentialsProvider { +-class ServerKeyedAESCrypto implements ISecretStorageCrypto { +- private _serverKey: Uint8Array | undefined; +- +- /** Gets whether the algorithm is supported; requires a secure context */ +- public static supported() { +- return !!crypto.subtle; +- } - -- static readonly CREDENTIALS_OPENED_KEY = 'credentials.provider'; +- constructor(private readonly authEndpoint: string) { } +- +- async seal(data: string): Promise { +- // Get a new key and IV on every change, to avoid the risk of reusing the same key and IV pair with AES-GCM +- // (see also: https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams#properties) +- const iv = mainWindow.crypto.getRandomValues(new Uint8Array(AESConstants.IV_LENGTH)); +- // crypto.getRandomValues isn't a good-enough PRNG to generate crypto keys, so we need to use crypto.subtle.generateKey and export the key instead +- const clientKeyObj = await mainWindow.crypto.subtle.generateKey( +- { name: AESConstants.ALGORITHM as const, length: AESConstants.KEY_LENGTH as const }, +- true, +- ['encrypt', 'decrypt'] +- ); +- +- const clientKey = new Uint8Array(await mainWindow.crypto.subtle.exportKey('raw', clientKeyObj)); +- const key = await this.getKey(clientKey); +- const dataUint8Array = new TextEncoder().encode(data); +- const cipherText: ArrayBuffer = await mainWindow.crypto.subtle.encrypt( +- { name: AESConstants.ALGORITHM as const, iv }, +- key, +- dataUint8Array +- ); +- +- // Base64 encode the result and store the ciphertext, the key, and the IV in localStorage +- // Note that the clientKey and IV don't need to be secret +- const result = new Uint8Array([...clientKey, ...iv, ...new Uint8Array(cipherText)]); +- return encodeBase64(VSBuffer.wrap(result)); +- } - -- private readonly authService: string | undefined; +- async unseal(data: string): Promise { +- // encrypted should contain, in order: the key (32-byte), the IV for AES-GCM (12-byte) and the ciphertext (which has the GCM auth tag at the end) +- // Minimum length must be 44 (key+IV length) + 16 bytes (1 block encrypted with AES - regardless of key size) +- const dataUint8Array = decodeBase64(data); - -- constructor() { -- let authSessionInfo: { readonly id: string, readonly accessToken: string, readonly providerId: string, readonly canSignOut?: boolean, readonly scopes: string[][] } | undefined; -- const authSessionElement = document.getElementById('vscode-workbench-auth-session'); -- const authSessionElementAttribute = authSessionElement ? authSessionElement.getAttribute('data-settings') : undefined; -- if (authSessionElementAttribute) { -- try { -- authSessionInfo = JSON.parse(authSessionElementAttribute); -- } catch (error) { /* Invalid session is passed. Ignore. */ } +- if (dataUint8Array.byteLength < 60) { +- throw Error('Invalid length for the value for credentials.crypto'); - } - -- if (authSessionInfo) { -- // Settings Sync Entry -- this.setPassword(`${product.urlProtocol}.login`, 'account', JSON.stringify(authSessionInfo)); -- -- // Auth extension Entry -- this.authService = `${product.urlProtocol}-${authSessionInfo.providerId}.login`; -- this.setPassword(this.authService, 'account', JSON.stringify(authSessionInfo.scopes.map(scopes => ({ -- id: authSessionInfo!.id, -- scopes, -- accessToken: authSessionInfo!.accessToken -- })))); -- } +- const keyLength = AESConstants.KEY_LENGTH / 8; +- const clientKey = dataUint8Array.slice(0, keyLength); +- const iv = dataUint8Array.slice(keyLength, keyLength + AESConstants.IV_LENGTH); +- const cipherText = dataUint8Array.slice(keyLength + AESConstants.IV_LENGTH); +- +- // Do the decryption and parse the result as JSON +- const key = await this.getKey(clientKey.buffer); +- const decrypted = await mainWindow.crypto.subtle.decrypt( +- { name: AESConstants.ALGORITHM as const, iv: iv.buffer }, +- key, +- cipherText.buffer +- ); +- +- return new TextDecoder().decode(new Uint8Array(decrypted)); - } - -- private _credentials: ICredential[] | undefined; -- private get credentials(): ICredential[] { -- if (!this._credentials) { -- try { -- const serializedCredentials = window.localStorage.getItem(LocalStorageCredentialsProvider.CREDENTIALS_OPENED_KEY); -- if (serializedCredentials) { -- this._credentials = JSON.parse(serializedCredentials); -- } -- } catch (error) { -- // ignore -- } +- /** +- * Given a clientKey, returns the CryptoKey object that is used to encrypt/decrypt the data. +- * The actual key is (clientKey XOR serverKey) +- */ +- private async getKey(clientKey: Uint8Array): Promise { +- if (!clientKey || clientKey.byteLength !== AESConstants.KEY_LENGTH / 8) { +- throw Error('Invalid length for clientKey'); +- } - -- if (!Array.isArray(this._credentials)) { -- this._credentials = []; -- } +- const serverKey = await this.getServerKeyPart(); +- const keyData = new Uint8Array(AESConstants.KEY_LENGTH / 8); +- +- for (let i = 0; i < keyData.byteLength; i++) { +- keyData[i] = clientKey[i]! ^ serverKey[i]!; - } - -- return this._credentials; +- return mainWindow.crypto.subtle.importKey( +- 'raw', +- keyData, +- { +- name: AESConstants.ALGORITHM as const, +- length: AESConstants.KEY_LENGTH as const, +- }, +- true, +- ['encrypt', 'decrypt'] +- ); - } - -- private save(): void { -- window.localStorage.setItem(LocalStorageCredentialsProvider.CREDENTIALS_OPENED_KEY, JSON.stringify(this.credentials)); -- } +- private async getServerKeyPart(): Promise { +- if (this._serverKey) { +- return this._serverKey; +- } - -- async getPassword(service: string, account: string): Promise { -- return this.doGetPassword(service, account); -- } +- let attempt = 0; +- let lastError: unknown | undefined; - -- private async doGetPassword(service: string, account?: string): Promise { -- for (const credential of this.credentials) { -- if (credential.service === service) { -- if (typeof account !== 'string' || account === credential.account) { -- return credential.password; +- while (attempt <= 3) { +- try { +- const res = await fetch(this.authEndpoint, { credentials: 'include', method: 'POST' }); +- if (!res.ok) { +- throw new Error(res.statusText); - } +- const serverKey = new Uint8Array(await await res.arrayBuffer()); +- if (serverKey.byteLength !== AESConstants.KEY_LENGTH / 8) { +- throw Error(`The key retrieved by the server is not ${AESConstants.KEY_LENGTH} bit long.`); +- } +- this._serverKey = serverKey; +- return this._serverKey; +- } catch (e) { +- lastError = e; +- attempt++; +- +- // exponential backoff +- await new Promise(resolve => setTimeout(resolve, attempt * attempt * 100)); - } - } - -- return null; +- throw lastError; - } +-} - -- async setPassword(service: string, account: string, password: string): Promise { -- this.doDeletePassword(service, account); -- -- this.credentials.push({ service, account, password }); +-export class LocalStorageSecretStorageProvider implements ISecretStorageProvider { +- private readonly _storageKey = 'secrets.provider'; - -- this.save(); +- private _secretsPromise: Promise> = this.load(); - -- try { -- if (password && service === this.authService) { -- const value = JSON.parse(password); -- if (Array.isArray(value) && value.length === 0) { -- await this.logout(service); -- } -- } -- } catch (error) { -- console.log(error); -- } -- } +- type: 'in-memory' | 'persisted' | 'unknown' = 'persisted'; - -- async deletePassword(service: string, account: string): Promise { -- const result = await this.doDeletePassword(service, account); +- constructor( +- private readonly crypto: ISecretStorageCrypto, +- ) { } - -- if (result && service === this.authService) { +- private async load(): Promise> { +- const record = this.loadAuthSessionFromElement(); +- // Get the secrets from localStorage +- const encrypted = localStorage.getItem(this._storageKey); +- if (encrypted) { - try { -- await this.logout(service); -- } catch (error) { -- console.log(error); +- const decrypted = JSON.parse(await this.crypto.unseal(encrypted)); +- return { ...record, ...decrypted }; +- } catch (err) { +- // TODO: send telemetry +- console.error('Failed to decrypt secrets from localStorage', err); +- localStorage.removeItem(this._storageKey); - } - } - -- return result; +- return record; - } - -- private async doDeletePassword(service: string, account: string): Promise { -- let found = false; +- private loadAuthSessionFromElement(): Record { +- let authSessionInfo: (AuthenticationSessionInfo & { scopes: string[][] }) | undefined; +- const authSessionElement = mainWindow.document.getElementById('vscode-workbench-auth-session'); +- const authSessionElementAttribute = authSessionElement ? authSessionElement.getAttribute('data-settings') : undefined; +- if (authSessionElementAttribute) { +- try { +- authSessionInfo = JSON.parse(authSessionElementAttribute); +- } catch (error) { /* Invalid session is passed. Ignore. */ } +- } - -- this._credentials = this.credentials.filter(credential => { -- if (credential.service === service && credential.account === account) { -- found = true; +- if (!authSessionInfo) { +- return {}; +- } - -- return false; -- } +- const record: Record = {}; - -- return true; -- }); +- // Settings Sync Entry +- record[`${product.urlProtocol}.loginAccount`] = JSON.stringify(authSessionInfo); - -- if (found) { -- this.save(); +- // Auth extension Entry +- if (authSessionInfo.providerId !== 'github') { +- console.error(`Unexpected auth provider: ${authSessionInfo.providerId}. Expected 'github'.`); +- return record; - } - -- return found; -- } +- const authAccount = JSON.stringify({ extensionId: 'vscode.github-authentication', key: 'github.auth' }); +- record[authAccount] = JSON.stringify(authSessionInfo.scopes.map(scopes => ({ +- id: authSessionInfo.id, +- scopes, +- accessToken: authSessionInfo.accessToken +- }))); - -- async findPassword(service: string): Promise { -- return this.doGetPassword(service); +- return record; - } - -- async findCredentials(service: string): Promise> { -- return this.credentials -- .filter(credential => credential.service === service) -- .map(({ account, password }) => ({ account, password })); +- async get(key: string): Promise { +- const secrets = await this._secretsPromise; +- return secrets[key]; - } -- -- private async logout(service: string): Promise { -- const queryValues: Map = new Map(); -- queryValues.set('logout', String(true)); -- queryValues.set('service', service); -- -- await request({ -- url: doCreateUri('/auth/logout', queryValues).toString(true) -- }, CancellationToken.None); +- async set(key: string, value: string): Promise { +- const secrets = await this._secretsPromise; +- secrets[key] = value; +- this._secretsPromise = Promise.resolve(secrets); +- this.save(); +- } +- async delete(key: string): Promise { +- const secrets = await this._secretsPromise; +- delete secrets[key]; +- this._secretsPromise = Promise.resolve(secrets); +- this.save(); - } - -- async clear(): Promise { -- window.localStorage.removeItem(LocalStorageCredentialsProvider.CREDENTIALS_OPENED_KEY); +- private async save(): Promise { +- try { +- const encrypted = await this.crypto.seal(JSON.stringify(await this._secretsPromise)); +- localStorage.setItem(this._storageKey, encrypted); +- } catch (err) { +- console.error(err); +- } - } -} - --class PollingURLCallbackProvider extends Disposable implements IURLCallbackProvider { - -- static readonly FETCH_INTERVAL = 500; // fetch every 500ms -- static readonly FETCH_TIMEOUT = 5 * 60 * 1000; // ...but stop after 5min +-class LocalStorageURLCallbackProvider extends Disposable implements IURLCallbackProvider { +- +- private static REQUEST_ID = 0; - -- static readonly QUERY_KEYS = { -- REQUEST_ID: 'vscode-requestId', -- SCHEME: 'vscode-scheme', -- AUTHORITY: 'vscode-authority', -- PATH: 'vscode-path', -- QUERY: 'vscode-query', -- FRAGMENT: 'vscode-fragment' -- }; +- private static QUERY_KEYS: ('scheme' | 'authority' | 'path' | 'query' | 'fragment')[] = [ +- 'scheme', +- 'authority', +- 'path', +- 'query', +- 'fragment' +- ]; - - private readonly _onCallback = this._register(new Emitter()); - readonly onCallback = this._onCallback.event; - -- create(options?: Partial): URI { -- const queryValues: Map = new Map(); +- private pendingCallbacks = new Set(); +- private lastTimeChecked = Date.now(); +- private checkCallbacksTimeout: unknown | undefined = undefined; +- private onDidChangeLocalStorageDisposable: IDisposable | undefined; - -- const requestId = generateUuid(); -- queryValues.set(PollingURLCallbackProvider.QUERY_KEYS.REQUEST_ID, requestId); +- constructor(private readonly _callbackRoute: string) { +- super(); +- } - -- const { scheme, authority, path, query, fragment } = options ? options : { scheme: undefined, authority: undefined, path: undefined, query: undefined, fragment: undefined }; +- create(options: Partial = {}): URI { +- const id = ++LocalStorageURLCallbackProvider.REQUEST_ID; +- const queryParams: string[] = [`vscode-reqid=${id}`]; - -- if (scheme) { -- queryValues.set(PollingURLCallbackProvider.QUERY_KEYS.SCHEME, scheme); -- } +- for (const key of LocalStorageURLCallbackProvider.QUERY_KEYS) { +- const value = options[key]; - -- if (authority) { -- queryValues.set(PollingURLCallbackProvider.QUERY_KEYS.AUTHORITY, authority); +- if (value) { +- queryParams.push(`vscode-${key}=${encodeURIComponent(value)}`); +- } - } - -- if (path) { -- queryValues.set(PollingURLCallbackProvider.QUERY_KEYS.PATH, path); -- } +- // TODO@joao remove eventually +- // https://github.com/microsoft/vscode-dev/issues/62 +- // https://github.com/microsoft/vscode/blob/159479eb5ae451a66b5dac3c12d564f32f454796/extensions/github-authentication/src/githubServer.ts#L50-L50 +- if (!(options.authority === 'vscode.github-authentication' && options.path === '/dummy')) { +- const key = `vscode-web.url-callbacks[${id}]`; +- localStorage.removeItem(key); - -- if (query) { -- queryValues.set(PollingURLCallbackProvider.QUERY_KEYS.QUERY, query); +- this.pendingCallbacks.add(id); +- this.startListening(); - } - -- if (fragment) { -- queryValues.set(PollingURLCallbackProvider.QUERY_KEYS.FRAGMENT, fragment); +- return URI.parse(mainWindow.location.href).with({ path: this._callbackRoute, query: queryParams.join('&') }); +- } +- +- private startListening(): void { +- if (this.onDidChangeLocalStorageDisposable) { +- return; - } - -- // Start to poll on the callback being fired -- this.periodicFetchCallback(requestId, Date.now()); +- const fn = () => this.onDidChangeLocalStorage(); +- mainWindow.addEventListener('storage', fn); +- this.onDidChangeLocalStorageDisposable = { dispose: () => mainWindow.removeEventListener('storage', fn) }; +- } - -- return doCreateUri('/callback', queryValues); +- private stopListening(): void { +- this.onDidChangeLocalStorageDisposable?.dispose(); +- this.onDidChangeLocalStorageDisposable = undefined; - } - -- private async periodicFetchCallback(requestId: string, startTime: number): Promise { +- // this fires every time local storage changes, but we +- // don't want to check more often than once a second +- private async onDidChangeLocalStorage(): Promise { +- const ellapsed = Date.now() - this.lastTimeChecked; +- +- if (ellapsed > 1000) { +- this.checkCallbacks(); +- } else if (this.checkCallbacksTimeout === undefined) { +- this.checkCallbacksTimeout = setTimeout(() => { +- this.checkCallbacksTimeout = undefined; +- this.checkCallbacks(); +- }, 1000 - ellapsed); +- } +- } - -- // Ask server for callback results -- const queryValues: Map = new Map(); -- queryValues.set(PollingURLCallbackProvider.QUERY_KEYS.REQUEST_ID, requestId); +- private checkCallbacks(): void { +- let pendingCallbacks: Set | undefined; - -- const result = await request({ -- url: doCreateUri('/fetch-callback', queryValues).toString(true) -- }, CancellationToken.None); +- for (const id of this.pendingCallbacks) { +- const key = `vscode-web.url-callbacks[${id}]`; +- const result = localStorage.getItem(key); - -- // Check for callback results -- const content = await streamToBuffer(result.stream); -- if (content.byteLength > 0) { -- try { -- this._onCallback.fire(URI.revive(JSON.parse(content.toString()))); -- } catch (error) { -- console.error(error); -- } +- if (result !== null) { +- try { +- this._onCallback.fire(URI.revive(JSON.parse(result))); +- } catch (error) { +- console.error(error); +- } - -- return; // done +- pendingCallbacks = pendingCallbacks ?? new Set(this.pendingCallbacks); +- pendingCallbacks.delete(id); +- localStorage.removeItem(key); +- } - } - -- // Continue fetching unless we hit the timeout -- if (Date.now() - startTime < PollingURLCallbackProvider.FETCH_TIMEOUT) { -- setTimeout(() => this.periodicFetchCallback(requestId, startTime), PollingURLCallbackProvider.FETCH_INTERVAL); +- if (pendingCallbacks) { +- this.pendingCallbacks = pendingCallbacks; +- +- if (this.pendingCallbacks.size === 0) { +- this.stopListening(); +- } - } +- +- this.lastTimeChecked = Date.now(); - } -} - -class WorkspaceProvider implements IWorkspaceProvider { - -- static QUERY_PARAM_EMPTY_WINDOW = 'ew'; -- static QUERY_PARAM_FOLDER = 'folder'; -- static QUERY_PARAM_WORKSPACE = 'workspace'; +- private static QUERY_PARAM_EMPTY_WINDOW = 'ew'; +- private static QUERY_PARAM_FOLDER = 'folder'; +- private static QUERY_PARAM_WORKSPACE = 'workspace'; - -- static QUERY_PARAM_PAYLOAD = 'payload'; +- private static QUERY_PARAM_PAYLOAD = 'payload'; - -- readonly trusted = true; +- static create(config: IWorkbenchConstructionOptions & { folderUri?: UriComponents; workspaceUri?: UriComponents }) { +- let foundWorkspace = false; +- let workspace: IWorkspace; +- let payload = Object.create(null); - -- constructor( +- const query = new URL(document.location.href).searchParams; +- query.forEach((value, key) => { +- switch (key) { +- +- // Folder +- case WorkspaceProvider.QUERY_PARAM_FOLDER: +- if (config.remoteAuthority && value.startsWith(posix.sep)) { +- // when connected to a remote and having a value +- // that is a path (begins with a `/`), assume this +- // is a vscode-remote resource as simplified URL. +- workspace = { folderUri: URI.from({ scheme: Schemas.vscodeRemote, path: value, authority: config.remoteAuthority }) }; +- } else { +- workspace = { folderUri: URI.parse(value) }; +- } +- foundWorkspace = true; +- break; +- +- // Workspace +- case WorkspaceProvider.QUERY_PARAM_WORKSPACE: +- if (config.remoteAuthority && value.startsWith(posix.sep)) { +- // when connected to a remote and having a value +- // that is a path (begins with a `/`), assume this +- // is a vscode-remote resource as simplified URL. +- workspace = { workspaceUri: URI.from({ scheme: Schemas.vscodeRemote, path: value, authority: config.remoteAuthority }) }; +- } else { +- workspace = { workspaceUri: URI.parse(value) }; +- } +- foundWorkspace = true; +- break; +- +- // Empty +- case WorkspaceProvider.QUERY_PARAM_EMPTY_WINDOW: +- workspace = undefined; +- foundWorkspace = true; +- break; +- +- // Payload +- case WorkspaceProvider.QUERY_PARAM_PAYLOAD: +- try { +- payload = parse(value); // use marshalling#parse() to revive potential URIs +- } catch (error) { +- console.error(error); // possible invalid JSON +- } +- break; +- } +- }); +- +- // If no workspace is provided through the URL, check for config +- // attribute from server +- if (!foundWorkspace) { +- if (config.folderUri) { +- workspace = { folderUri: URI.revive(config.folderUri) }; +- } else if (config.workspaceUri) { +- workspace = { workspaceUri: URI.revive(config.workspaceUri) }; +- } +- } +- +- return new WorkspaceProvider(workspace, payload, config); +- } +- +- readonly trusted = true; ++ // Inject project specific utopia config into the product.json ++ const urlParams = new URLSearchParams(window.location.search) ++ const vsCodeSessionID = urlParams.get('vs_code_session_id')! + +- private constructor( - readonly workspace: IWorkspace, -- readonly payload: object -- ) { } +- readonly payload: object, +- private readonly config: IWorkbenchConstructionOptions +- ) { +- } - -- async open(workspace: IWorkspace, options?: { reuse?: boolean, payload?: object }): Promise { +- async open(workspace: IWorkspace, options?: { reuse?: boolean; payload?: object }): Promise { - if (options?.reuse && !options.payload && this.isSame(this.workspace, workspace)) { - return true; // return early if workspace and environment is not changing and we are reusing window - } -- ++ // Use this instance as the webview provider rather than hitting MS servers ++ const webviewEndpoint = `${window.location.origin}/vscode/vscode/vs/workbench/contrib/webview/browser/pre` + - const targetHref = this.createTargetUrl(workspace, options); - if (targetHref) { - if (options?.reuse) { -- window.location.href = targetHref; +- mainWindow.location.href = targetHref; - return true; - } else { - let result; -- if (isStandalone) { -- result = window.open(targetHref, '_blank', 'toolbar=no'); // ensures to open another 'standalone' window! +- if (isStandalone()) { +- result = mainWindow.open(targetHref, '_blank', 'toolbar=no'); // ensures to open another 'standalone' window! - } else { -- result = window.open(targetHref); +- result = mainWindow.open(targetHref); - } - - return !!result; - } - } - return false; -- } -- -- private createTargetUrl(workspace: IWorkspace, options?: { reuse?: boolean, payload?: object }): string | undefined { ++ let config = { ++ ...loadedConfig, ++ webviewEndpoint: webviewEndpoint, ++ editSessionId: vsCodeSessionID + } + +- private createTargetUrl(workspace: IWorkspace, options?: { reuse?: boolean; payload?: object }): string | undefined { - - // Empty - let targetHref: string | undefined = undefined; @@ -538,12 +670,14 @@ index 534fe232636..b41446430f0 100644 - - // Folder - else if (isFolderToOpen(workspace)) { -- targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_FOLDER}=${encodeURIComponent(workspace.folderUri.toString())}`; +- const queryParamFolder = this.encodeWorkspacePath(workspace.folderUri); +- targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_FOLDER}=${queryParamFolder}`; - } - - // Workspace - else if (isWorkspaceToOpen(workspace)) { -- targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_WORKSPACE}=${encodeURIComponent(workspace.workspaceUri.toString())}`; +- const queryParamWorkspace = this.encodeWorkspacePath(workspace.workspaceUri); +- targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_WORKSPACE}=${queryParamWorkspace}`; - } - - // Append payload if any @@ -552,6 +686,29 @@ index 534fe232636..b41446430f0 100644 - } - - return targetHref; ++ const workspace = { ++ folderUri: URI.parse(`${vsCodeSessionID}:/`) + } + +- private encodeWorkspacePath(uri: URI): string { +- if (this.config.remoteAuthority && uri.scheme === Schemas.vscodeRemote) { +- +- // when connected to a remote and having a folder +- // or workspace for that remote, only use the path +- // as query value to form shorter, nicer URLs. +- // however, we still need to `encodeURIComponent` +- // to ensure to preserve special characters, such +- // as `+` in the path. +- +- return encodeURIComponent(`${posix.sep}${ltrim(uri.path, posix.sep)}`).replaceAll('%2F', '/'); ++ if (workspace) { ++ const workspaceProvider: IWorkspaceProvider = { ++ workspace, ++ open: async (_workspace: IWorkspace, _options?: { reuse?: boolean, payload?: object }) => true, ++ trusted: true + } +- +- return encodeURIComponent(uri.toString(true)); - } - - private isSame(workspaceA: IWorkspace, workspaceB: IWorkspace): boolean { @@ -585,280 +742,49 @@ index 534fe232636..b41446430f0 100644 - } -} - --class WindowIndicator implements IWindowIndicator { -- -- readonly onDidChange = Event.None; -- -- readonly label: string; -- readonly tooltip: string; -- readonly command: string | undefined; -- -- constructor(workspace: IWorkspace) { -- let repositoryOwner: string | undefined = undefined; -- let repositoryName: string | undefined = undefined; -- -- if (workspace) { -- let uri: URI | undefined = undefined; -- if (isFolderToOpen(workspace)) { -- uri = workspace.folderUri; -- } else if (isWorkspaceToOpen(workspace)) { -- uri = workspace.workspaceUri; -- } -- -- if (uri?.scheme === 'github' || uri?.scheme === 'codespace') { -- [repositoryOwner, repositoryName] = uri.authority.split('+'); -- } -- } -- -- // Repo -- if (repositoryName && repositoryOwner) { -- this.label = localize('playgroundLabelRepository', "$(remote) Visual Studio Code Playground: {0}/{1}", repositoryOwner, repositoryName); -- this.tooltip = localize('playgroundRepositoryTooltip', "Visual Studio Code Playground: {0}/{1}", repositoryOwner, repositoryName); -- } -- -- // No Repo -- else { -- this.label = localize('playgroundLabel', "$(remote) Visual Studio Code Playground"); -- this.tooltip = localize('playgroundTooltip', "Visual Studio Code Playground"); +-function readCookie(name: string): string | undefined { +- const cookies = document.cookie.split('; '); +- for (const cookie of cookies) { +- if (cookie.startsWith(name + '=')) { +- return cookie.substring(name.length + 1); - } - } +- +- return undefined; -} - -(function () { - - // Find config by checking for DOM -- const configElement = document.getElementById('vscode-workbench-web-configuration'); +- const configElement = mainWindow.document.getElementById('vscode-workbench-web-configuration'); - const configElementAttribute = configElement ? configElement.getAttribute('data-settings') : undefined; - if (!configElement || !configElementAttribute) { - throw new Error('Missing web configuration element'); -- } -- -- const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents, workspaceUri?: UriComponents } = JSON.parse(configElementAttribute); -- -- // Find workspace to open and payload -- let foundWorkspace = false; -- let workspace: IWorkspace; -- let payload = Object.create(null); -- let logLevel: string | undefined = undefined; -- -- const query = new URL(document.location.href).searchParams; -- query.forEach((value, key) => { -- switch (key) { -- -- // Folder -- case WorkspaceProvider.QUERY_PARAM_FOLDER: -- workspace = { folderUri: URI.parse(value) }; -- foundWorkspace = true; -- break; -- -- // Workspace -- case WorkspaceProvider.QUERY_PARAM_WORKSPACE: -- workspace = { workspaceUri: URI.parse(value) }; -- foundWorkspace = true; -- break; -- -- // Empty -- case WorkspaceProvider.QUERY_PARAM_EMPTY_WINDOW: -- workspace = undefined; -- foundWorkspace = true; -- break; -- -- // Payload -- case WorkspaceProvider.QUERY_PARAM_PAYLOAD: -- try { -- payload = JSON.parse(value); -- } catch (error) { -- console.error(error); // possible invalid JSON -- } -- break; -- -- // Log level -- case 'logLevel': -- logLevel = value; -- break; -- } -- }); -- -- // If no workspace is provided through the URL, check for config attribute from server -- if (!foundWorkspace) { -- if (config.folderUri) { -- workspace = { folderUri: URI.revive(config.folderUri) }; -- } else if (config.workspaceUri) { -- workspace = { workspaceUri: URI.revive(config.workspaceUri) }; -- } else { -- workspace = undefined; -- } -- } -- -- // Workspace Provider -- const workspaceProvider = new WorkspaceProvider(workspace, payload); -- -- // Home Indicator -- const homeIndicator: IHomeIndicator = { -- href: 'https://github.com/microsoft/vscode', -- icon: 'code', -- title: localize('home', "Home") -- }; -- -- // Welcome Banner -- const welcomeBanner: IWelcomeBanner = { -- message: localize('welcomeBannerMessage', "{0} Web. Browser based playground for testing.", product.nameShort), -- actions: [{ -- href: 'https://github.com/microsoft/vscode', -- label: localize('learnMore', "Learn More") -- }] -- }; -- -- // Window indicator (unless connected to a remote) -- let windowIndicator: WindowIndicator | undefined = undefined; -- if (!workspaceProvider.hasRemote()) { -- windowIndicator = new WindowIndicator(workspace); -- } -- -- // Product Quality Change Handler -- const productQualityChangeHandler: IProductQualityChangeHandler = (quality) => { -- let queryString = `quality=${quality}`; -- -- // Save all other query params we might have -- const query = new URL(document.location.href).searchParams; -- query.forEach((value, key) => { -- if (key !== 'quality') { -- queryString += `&${key}=${value}`; -- } -- }); -- -- window.location.href = `${window.location.origin}?${queryString}`; -- }; -- -- // settings sync options -- const settingsSyncOptions: ISettingsSyncOptions | undefined = config.settingsSyncOptions ? { -- enabled: config.settingsSyncOptions.enabled, -- } : undefined; -- -- // Finally create workbench -- create(document.body, { ++ config = { ...config, workspaceProvider } + } +- const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents; workspaceUri?: UriComponents; callbackRoute: string } = JSON.parse(configElementAttribute); +- const secretStorageKeyPath = readCookie('vscode-secret-key-path'); +- const secretStorageCrypto = secretStorageKeyPath && ServerKeyedAESCrypto.supported() +- ? new ServerKeyedAESCrypto(secretStorageKeyPath) : new TransparentCrypto(); + +- // Create workbench +- create(mainWindow.document.body, { - ...config, -- developmentOptions: { -- logLevel: logLevel ? parseLogLevel(logLevel) : undefined, -- ...config.developmentOptions -- }, -- settingsSyncOptions, -- homeIndicator, -- windowIndicator, -- welcomeBanner, -- productQualityChangeHandler, -- workspaceProvider, -- urlCallbackProvider: new PollingURLCallbackProvider(), -- credentialsProvider: new LocalStorageCredentialsProvider() +- windowIndicator: config.windowIndicator ?? { label: '$(remote)', tooltip: `${product.nameShort} Web` }, +- settingsSyncOptions: config.settingsSyncOptions ? { enabled: config.settingsSyncOptions.enabled, } : undefined, +- workspaceProvider: WorkspaceProvider.create(config), +- urlCallbackProvider: new LocalStorageURLCallbackProvider(config.callbackRoute), +- secretStorageProvider: config.remoteAuthority && !secretStorageKeyPath +- ? undefined /* with a remote without embedder-preferred storage, store on the remote */ +- : new LocalStorageSecretStorageProvider(secretStorageCrypto), - }); --})(); -+import { -+ create, -+ IWorkspace, -+ IWorkbenchConstructionOptions, -+ IWorkspaceProvider, -+} from 'vs/workbench/workbench.web.api' -+import { URI } from 'vs/base/common/uri' -+import { setupVSCodeEventListenersForProject } from 'utopia-vscode-common' -+ -+// TODO revisit these dummy parts so that they can return something rather than nothing -+// import { -+// getSingletonServiceDescriptors, -+// registerSingleton, -+// } from 'vs/platform/instantiation/common/extensions' -+// import { BrandedService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation' -+// import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors' -+ -+// const _registry = getSingletonServiceDescriptors() -+ -+// function replaceRegisteredSingleton( -+// id: ServiceIdentifier, -+// ctor: new (...services: Services) => T, -+// supportsDelayedInstantiation?: boolean, -+// ): void { -+// const index = _registry.findIndex((tuple) => tuple[0] === id) -+// if (index > 0) { -+// _registry[index] = [ -+// id, -+// new SyncDescriptor(ctor as new (...args: any[]) => T, [], supportsDelayedInstantiation), -+// ] -+// } else { -+// registerSingleton(id, ctor) -+// } -+// } -+ -+// // Replace services for the parts we don't want to use - -+// // We have to import the original part first to ensure it isn't registered later -+// import 'vs/workbench/browser/parts/panel/panelPart' -+// import { PanelPart } from 'vs/workbench/browser/parts/dummies/panelPart' -+// import { IPanelService } from 'vs/workbench/services/panel/common/panelService' -+// replaceRegisteredSingleton(IPanelService, PanelPart) -+ -+// import 'vs/workbench/browser/parts/sidebar/sidebarPart' -+// import { SidebarPart } from 'vs/workbench/browser/parts/dummies/sidebarPart' -+// import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet' -+// replaceRegisteredSingleton(IViewletService, SidebarPart) -+ -+// import 'vs/workbench/browser/parts/activitybar/activitybarPart' -+// import { ActivitybarPart } from 'vs/workbench/browser/parts/dummies/activitybarPart' -+// import { IActivityBarService } from 'vs/workbench/services/activityBar/browser/activityBarService' -+// replaceRegisteredSingleton(IActivityBarService, ActivitybarPart) -+ -+// import 'vs/workbench/browser/parts/titlebar/titlebarPart' -+// import { TitlebarPart } from 'vs/workbench/browser/parts/dummies/titlebarPart' -+// import { ITitleService } from 'vs/workbench/services/title/common/titleService' -+// replaceRegisteredSingleton(ITitleService, TitlebarPart) -+ -+// import 'vs/workbench/browser/parts/statusbar/statusbarPart' -+// import { StatusbarPart } from 'vs/workbench/browser/parts/dummies/statusbarPart' -+// import { IStatusbarService } from 'vs/workbench/services/statusbar/common/statusbar' -+// replaceRegisteredSingleton(IStatusbarService, StatusbarPart) -+ -+;(async function () { -+ // create workbench -+ const result = await fetch('/vscode/product.json') -+ const loadedConfig: IWorkbenchConstructionOptions = await result.json() -+ -+ // Inject project specific utopia config into the product.json -+ const urlParams = new URLSearchParams(window.location.search) -+ const vsCodeSessionID = urlParams.get('vs_code_session_id')! -+ -+ // Use this instance as the webview provider rather than hitting MS servers -+ const webviewEndpoint = `${window.location.origin}/vscode/vscode/vs/workbench/contrib/webview/browser/pre` -+ -+ let config = { -+ ...loadedConfig, -+ folderUri: { -+ scheme: vsCodeSessionID, -+ authority: '', -+ path: `/`, -+ query: '', -+ fragment: '', -+ }, -+ webviewEndpoint: webviewEndpoint, -+ } -+ -+ const workspace = { folderUri: URI.revive(config.folderUri) } -+ -+ if (workspace) { -+ const workspaceProvider: IWorkspaceProvider = { -+ workspace, -+ open: async (workspace: IWorkspace, options?: { reuse?: boolean, payload?: object }) => true, -+ trusted: true -+ } -+ config = { ...config, workspaceProvider } -+ } -+ -+ setupVSCodeEventListenersForProject(vsCodeSessionID) -+ -+ create(document.body, config) -+})() -\ No newline at end of file ++ create(mainWindow.document.body, config) + })(); diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts -index 7d8189c50be..125b6c89d0b 100644 +index 294e7030695..46730d6d518 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts -@@ -2560,7 +2560,7 @@ class EditorMinimap extends BaseEditorOption { + assert.ok(typeof result === 'boolean', testErrorMessage('native-is-elevated')); + }); - views: { -@@ -513,13 +514,13 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi - this.state.fullscreen = isFullscreen(); - - // Menubar visibility -- this.state.menuBar.visibility = getMenuBarVisibility(this.configurationService); -+ this.state.menuBar.visibility = 'hidden'; - - // Activity bar visibility -- this.state.activityBar.hidden = !this.configurationService.getValue(Settings.ACTIVITYBAR_VISIBLE); -+ this.state.activityBar.hidden = true; - - // Sidebar visibility -- this.state.sideBar.hidden = this.storageService.getBoolean(Storage.SIDEBAR_HIDDEN, StorageScope.WORKSPACE, this.contextService.getWorkbenchState() === WorkbenchState.EMPTY); -+ this.state.sideBar.hidden = true; - - // Sidebar position - this.state.sideBar.position = (this.configurationService.getValue(Settings.SIDEBAR_POSITION) === 'right') ? Position.RIGHT : Position.LEFT; -@@ -552,7 +553,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi - this.state.editor.editorsToOpen = this.resolveEditorsToOpen(fileService, this.contextService); - - // Panel visibility -- this.state.panel.hidden = this.storageService.getBoolean(Storage.PANEL_HIDDEN, StorageScope.WORKSPACE, true); -+ this.state.panel.hidden = true; - - // Whether or not the panel was last maximized - this.state.panel.wasLastMaximized = this.storageService.getBoolean(Storage.PANEL_LAST_IS_MAXIMIZED, StorageScope.WORKSPACE, false); -@@ -590,7 +591,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi - this.state.panel.lastNonMaximizedWidth = this.storageService.getNumber(Storage.PANEL_LAST_NON_MAXIMIZED_WIDTH, StorageScope.GLOBAL, 300); - - // Statusbar visibility -- this.state.statusBar.hidden = !this.configurationService.getValue(Settings.STATUSBAR_VISIBLE); -+ this.state.statusBar.hidden = true; - - // Zen mode enablement - this.state.zenMode.restore = this.storageService.getBoolean(Storage.ZEN_MODE_ENABLED, StorageScope.WORKSPACE, false) && this.configurationService.getValue(Settings.ZEN_MODE_RESTORE); -@@ -1227,7 +1228,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi - } - } +- test('native-keymap', async () => { +- const keyMap = await import('native-keymap'); +- assert.ok(typeof keyMap.getCurrentKeyboardLayout === 'function', testErrorMessage('native-keymap')); +- +- const result = keyMap.getCurrentKeyboardLayout(); +- assert.ok(result, testErrorMessage('native-keymap')); +- }); ++ // I removed everything involving native-keymap as that was causing issues with the build involving Electron (which we obviously don't care about) -- private setStatusBarHidden(hidden: boolean, skipLayout?: boolean): void { -+ private setStatusBarHidden(_hidden: boolean, skipLayout?: boolean): void { -+ const hidden = true; - this.state.statusBar.hidden = hidden; + test('native-watchdog', async () => { + const watchDog = await import('native-watchdog'); +diff --git a/src/vs/platform/keyboardLayout/electron-main/keyboardLayoutMainService.ts b/src/vs/platform/keyboardLayout/electron-main/keyboardLayoutMainService.ts +index 1d17e4c709e..00109563f5c 100644 +--- a/src/vs/platform/keyboardLayout/electron-main/keyboardLayoutMainService.ts ++++ b/src/vs/platform/keyboardLayout/electron-main/keyboardLayoutMainService.ts +@@ -3,8 +3,6 @@ + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ - // Adjust CSS -@@ -1453,7 +1455,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi - } +-import type * as nativeKeymap from 'native-keymap'; +-import * as platform from 'vs/base/common/platform'; + import { Emitter } from 'vs/base/common/event'; + import { Disposable } from 'vs/base/common/lifecycle'; + import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +@@ -45,29 +43,10 @@ export class KeyboardLayoutMainService extends Disposable implements INativeKeyb + return this._initPromise; } -- private setActivityBarHidden(hidden: boolean, skipLayout?: boolean): void { -+ private setActivityBarHidden(_hidden: boolean, skipLayout?: boolean): void { -+ const hidden = true; - this.state.activityBar.hidden = hidden; +- private async _doInitialize(): Promise { +- const nativeKeymapMod = await import('native-keymap'); +- +- this._keyboardLayoutData = readKeyboardLayoutData(nativeKeymapMod); +- if (!platform.isCI) { +- // See https://github.com/microsoft/vscode/issues/152840 +- // Do not register the keyboard layout change listener in CI because it doesn't work +- // on the build machines and it just adds noise to the build logs. +- nativeKeymapMod.onDidChangeKeyboardLayout(() => { +- this._keyboardLayoutData = readKeyboardLayoutData(nativeKeymapMod); +- this._onDidChangeKeyboardLayout.fire(this._keyboardLayoutData); +- }); +- } +- } ++ private async _doInitialize(): Promise {} - // Propagate to grid -@@ -1500,7 +1503,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi - ]); + public async getKeyboardLayoutData(): Promise { + await this._initialize(); + return this._keyboardLayoutData!; } + } +- +-function readKeyboardLayoutData(nativeKeymapMod: typeof nativeKeymap): IKeyboardLayoutData { +- const keyboardMapping = nativeKeymapMod.getKeyMap(); +- const keyboardLayoutInfo = nativeKeymapMod.getCurrentKeyboardLayout(); +- return { keyboardMapping, keyboardLayoutInfo }; +-} +diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts +index b8232cb8490..0ce6a8e7002 100644 +--- a/src/vs/workbench/browser/layout.ts ++++ b/src/vs/workbench/browser/layout.ts +@@ -1203,40 +1203,40 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi -- private setSideBarHidden(hidden: boolean, skipLayout?: boolean): void { -+ private setSideBarHidden(_hidden: boolean, skipLayout?: boolean): void { -+ const hidden = true - this.state.sideBar.hidden = hidden; + if (this.initialized) { + switch (part) { +- case Parts.TITLEBAR_PART: +- return this.workbenchGrid.isViewVisible(this.titleBarPartView); +- case Parts.SIDEBAR_PART: +- return !this.stateModel.getRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN); +- case Parts.PANEL_PART: +- return !this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_HIDDEN); +- case Parts.AUXILIARYBAR_PART: +- return !this.stateModel.getRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN); +- case Parts.STATUSBAR_PART: +- return !this.stateModel.getRuntimeValue(LayoutStateKeys.STATUSBAR_HIDDEN); +- case Parts.ACTIVITYBAR_PART: +- return !this.stateModel.getRuntimeValue(LayoutStateKeys.ACTIVITYBAR_HIDDEN); ++ // case Parts.TITLEBAR_PART: ++ // return this.workbenchGrid.isViewVisible(this.titleBarPartView); ++ // case Parts.SIDEBAR_PART: ++ // return !this.stateModel.getRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN); ++ // case Parts.PANEL_PART: ++ // return !this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_HIDDEN); ++ // case Parts.AUXILIARYBAR_PART: ++ // return !this.stateModel.getRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN); ++ // case Parts.STATUSBAR_PART: ++ // return !this.stateModel.getRuntimeValue(LayoutStateKeys.STATUSBAR_HIDDEN); ++ // case Parts.ACTIVITYBAR_PART: ++ // return !this.stateModel.getRuntimeValue(LayoutStateKeys.ACTIVITYBAR_HIDDEN); + case Parts.EDITOR_PART: + return !this.stateModel.getRuntimeValue(LayoutStateKeys.EDITOR_HIDDEN); +- case Parts.BANNER_PART: +- return this.workbenchGrid.isViewVisible(this.bannerPartView); ++ // case Parts.BANNER_PART: ++ // return this.workbenchGrid.isViewVisible(this.bannerPartView); + default: + return false; // any other part cannot be hidden + } + } - // Adjust CSS -@@ -1560,7 +1564,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi - return viewContainerModel.activeViewDescriptors.length >= 1; + switch (part) { +- case Parts.TITLEBAR_PART: +- return shouldShowCustomTitleBar(this.configurationService, mainWindow, this.state.runtime.menuBar.toggled, this.isZenModeActive()); +- case Parts.SIDEBAR_PART: +- return !this.stateModel.getRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN); +- case Parts.PANEL_PART: +- return !this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_HIDDEN); +- case Parts.AUXILIARYBAR_PART: +- return !this.stateModel.getRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN); +- case Parts.STATUSBAR_PART: +- return !this.stateModel.getRuntimeValue(LayoutStateKeys.STATUSBAR_HIDDEN); +- case Parts.ACTIVITYBAR_PART: +- return !this.stateModel.getRuntimeValue(LayoutStateKeys.ACTIVITYBAR_HIDDEN); ++ // case Parts.TITLEBAR_PART: ++ // return shouldShowCustomTitleBar(this.configurationService, mainWindow, this.state.runtime.menuBar.toggled, this.isZenModeActive()); ++ // case Parts.SIDEBAR_PART: ++ // return !this.stateModel.getRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN); ++ // case Parts.PANEL_PART: ++ // return !this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_HIDDEN); ++ // case Parts.AUXILIARYBAR_PART: ++ // return !this.stateModel.getRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN); ++ // case Parts.STATUSBAR_PART: ++ // return !this.stateModel.getRuntimeValue(LayoutStateKeys.STATUSBAR_HIDDEN); ++ // case Parts.ACTIVITYBAR_PART: ++ // return !this.stateModel.getRuntimeValue(LayoutStateKeys.ACTIVITYBAR_HIDDEN); + case Parts.EDITOR_PART: + return !this.stateModel.getRuntimeValue(LayoutStateKeys.EDITOR_HIDDEN); + default: +@@ -1293,7 +1293,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } -- private setPanelHidden(hidden: boolean, skipLayout?: boolean): void { -+ private setPanelHidden(_hidden: boolean, skipLayout?: boolean): void { -+ const hidden = true; - const wasHidden = this.state.panel.hidden; - this.state.panel.hidden = hidden; - -@@ -1771,7 +1776,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi - return this.state.sideBar.position; + private isZenModeActive(): boolean { +- return this.stateModel.getRuntimeValue(LayoutStateKeys.ZEN_MODE_ACTIVE); ++ return true; // Always true to remove all of the other UI } -- setMenubarVisibility(visibility: MenuBarVisibility, skipLayout: boolean): void { -+ setMenubarVisibility(_visibility: MenuBarVisibility, skipLayout: boolean): void { -+ const visibility = 'hidden'; - if (this.state.menuBar.visibility !== visibility) { - this.state.menuBar.visibility = visibility; - + private setZenModeActive(active: boolean) { diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts -index 000fd151ee2..d21a981e220 100644 +index ae4d22c9eaa..e342fb223d8 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts -@@ -7,7 +7,7 @@ import product from 'vs/platform/product/common/product'; - import { Registry } from 'vs/platform/registry/common/platform'; - import { localize } from 'vs/nls'; - import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; --import { isMacintosh, isWindows, isLinux, isWeb, isNative } from 'vs/base/common/platform'; -+import { isMacintosh, isWeb, isNative } from 'vs/base/common/platform'; - import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; - import { isStandalone } from 'vs/base/browser/browser'; - -@@ -278,6 +278,12 @@ const registry = Registry.as(ConfigurationExtensions.Con - 'default': 'left', - 'description': localize('sideBarLocation', "Controls the location of the sidebar and activity bar. They can either show on the left or right of the workbench.") - }, -+ 'workbench.sideBar.visible': { -+ 'type': 'boolean', -+ 'default': false, -+ 'description': localize('sideBarVisibility', "Controls the visibility of the side bar at the side of the workbench."), -+ 'included': false -+ }, - 'workbench.panel.defaultLocation': { - 'type': 'string', - 'enum': ['left', 'bottom', 'right'], -@@ -297,13 +303,15 @@ const registry = Registry.as(ConfigurationExtensions.Con - }, - 'workbench.statusBar.visible': { +@@ -784,12 +784,12 @@ const registry = Registry.as(ConfigurationExtensions.Con + 'properties': { + 'zenMode.fullScreen': { 'type': 'boolean', - 'default': true, -- 'description': localize('statusBarVisibility', "Controls the visibility of the status bar at the bottom of the workbench.") + 'default': false, -+ 'description': localize('statusBarVisibility', "Controls the visibility of the status bar at the bottom of the workbench."), -+ 'included': false + 'description': localize('zenMode.fullScreen', "Controls whether turning on Zen Mode also puts the workbench into full screen mode.") }, - 'workbench.activityBar.visible': { + 'zenMode.centerLayout': { 'type': 'boolean', - 'default': true, -- 'description': localize('activityBarVisibility', "Controls the visibility of the activity bar in the workbench.") + 'default': false, -+ 'description': localize('activityBarVisibility', "Controls the visibility of the activity bar in the workbench."), -+ 'included': false - }, - 'workbench.activityBar.iconClickBehavior': { - 'type': 'string', -@@ -420,26 +428,26 @@ const registry = Registry.as(ConfigurationExtensions.Con - localize('window.menuBarVisibility.hidden', "Menu is always hidden."), - localize('window.menuBarVisibility.compact', "Menu is displayed as a compact button in the sidebar. This value is ignored when `#window.titleBarStyle#` is `native`.") - ], -- 'default': isWeb ? 'compact' : 'classic', -+ 'default': 'hidden', - 'scope': ConfigurationScope.APPLICATION, - 'markdownDescription': isMacintosh ? - localize('menuBarVisibility.mac', "Control the visibility of the menu bar. A setting of 'toggle' means that the menu bar is hidden and executing `Focus Application Menu` will show it. A setting of 'compact' will move the menu into the sidebar.") : - localize('menuBarVisibility', "Control the visibility of the menu bar. A setting of 'toggle' means that the menu bar is hidden and a single press of the Alt key will show it. A setting of 'compact' will move the menu into the sidebar."), -- 'included': isWindows || isLinux || isWeb -+ 'included': false + 'description': localize('zenMode.centerLayout', "Controls whether turning on Zen Mode also centers the layout.") }, - 'window.enableMenuBarMnemonics': { - 'type': 'boolean', -- 'default': true, -+ 'default': false, - 'scope': ConfigurationScope.APPLICATION, - 'description': localize('enableMenuBarMnemonics', "Controls whether the main menus can be opened via Alt-key shortcuts. Disabling mnemonics allows to bind these Alt-key shortcuts to editor commands instead."), -- 'included': isWindows || isLinux -+ 'included': false + 'zenMode.showTabs': { +@@ -815,7 +815,7 @@ const registry = Registry.as(ConfigurationExtensions.Con }, - 'window.customMenuBarAltFocus': { + 'zenMode.hideLineNumbers': { 'type': 'boolean', - 'default': true, + 'default': false, - 'scope': ConfigurationScope.APPLICATION, - 'markdownDescription': localize('customMenuBarAltFocus', "Controls whether the menu bar will be focused by pressing the Alt-key. This setting has no effect on toggling the menu bar with the Alt-key."), -- 'included': isWindows || isLinux -+ 'included': false + 'description': localize('zenMode.hideLineNumbers', "Controls whether turning on Zen Mode also hides the editor line numbers.") }, - 'window.openFilesInNewWindow': { - 'type': 'string', + 'zenMode.restore': { +diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts +index b0688133537..4353093fd5b 100644 +--- a/src/vs/workbench/browser/workbench.ts ++++ b/src/vs/workbench/browser/workbench.ts +@@ -50,6 +50,8 @@ import { AccessibilityProgressSignalScheduler } from 'vs/platform/accessibilityS + import { setProgressAcccessibilitySignalScheduler } from 'vs/base/browser/ui/progressbar/progressAccessibilitySignal'; + import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry'; + import { NotificationAccessibleView } from 'vs/workbench/browser/parts/notifications/notificationAccessibleView'; ++import { ICommandService } from '../../platform/commands/common/commands'; ++import { isFromUtopiaToVSCodeMessage, messageListenersReady } from 'utopia-vscode-common'; + + export interface IWorkbenchOptions { + +@@ -193,6 +195,37 @@ export class Workbench extends Layout { + this.restore(lifecycleService); + }); + ++ // Chain off of the previous one to ensure the ordering of changes is maintained. ++ // FIXME Do we still need this? ++ let applyProjectChangesCoordinator: Promise = Promise.resolve() ++ ++ let intervalID: number | null = null ++ ++ mainWindow.addEventListener('message', (messageEvent: MessageEvent) => { ++ const { data } = messageEvent; ++ if (isFromUtopiaToVSCodeMessage(data)) { ++ const commandService = this.serviceCollection.get(ICommandService); ++ if (commandService == null) { ++ console.error(`There is no command service`); ++ } else { ++ if (intervalID != null) { ++ window.clearInterval(intervalID) ++ } ++ applyProjectChangesCoordinator = applyProjectChangesCoordinator.then(async () => { ++ (commandService as ICommandService).executeCommand('utopia.toVSCodeMessage', data); ++ }) ++ } ++ } ++ }); ++ ++ intervalID = window.setInterval(() => { ++ try { ++ window.top?.postMessage(messageListenersReady(), '*') ++ } catch (error) { ++ console.error('Error posting messageListenersReady', error) ++ } ++ }, 500) ++ + return instantiationService; + } catch (error) { + onUnexpectedError(error); diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts -index b9beba774a5..40627803528 100644 +index 81bf68c3e0b..1075398b4b8 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts -@@ -567,6 +567,38 @@ CommandsRegistry.registerCommand({ +@@ -53,6 +53,7 @@ import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; + import { RemoveRootFolderAction } from 'vs/workbench/browser/actions/workspaceActions'; + import { OpenEditorsView } from 'vs/workbench/contrib/files/browser/views/openEditorsView'; + import { ExplorerView } from 'vs/workbench/contrib/files/browser/views/explorerView'; ++import { mainWindow } from '../../../../base/browser/window'; + + export const openWindowCommand = (accessor: ServicesAccessor, toOpen: IWindowOpenable[], options?: IOpenWindowOptions) => { + if (Array.isArray(toOpen)) { +@@ -734,3 +735,43 @@ CommandsRegistry.registerCommand({ + }); } }); - ++ +CommandsRegistry.registerCommand({ + id: 'workbench.action.files.revertResource', + handler: async (accessor, resource: URI) => { @@ -1121,148 +1064,76 @@ index b9beba774a5..40627803528 100644 + } +}); + - CommandsRegistry.registerCommand({ - id: REMOVE_ROOT_FOLDER_COMMAND_ID, - handler: (accessor, resource: URI | object) => { ++ ++CommandsRegistry.registerCommand({ ++ id: 'utopia.toUtopiaMessage', ++ handler: async (_accessor, message: any) => { ++ mainWindow.top?.postMessage(message); ++ } ++}); +\ No newline at end of file diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts -index 114f1357403..bec56e38917 100644 +index d525ba5860c..56ae5cfbc70 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts -@@ -234,12 +234,12 @@ configurationRegistry.registerConfiguration({ - nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.onFocusChange' }, "A dirty editor is automatically saved when the editor loses focus."), - nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.onWindowChange' }, "A dirty editor is automatically saved when the window loses focus.") +@@ -267,7 +267,7 @@ configurationRegistry.registerConfiguration({ + nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.onFocusChange' }, "An editor with changes is automatically saved when the editor loses focus."), + nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'files.autoSave.onWindowChange' }, "An editor with changes is automatically saved when the window loses focus.") ], - 'default': isWeb ? AutoSaveConfiguration.AFTER_DELAY : AutoSaveConfiguration.OFF, + 'default': AutoSaveConfiguration.OFF, - 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSave' }, "Controls auto save of dirty editors. Read more about autosave [here](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save).", AutoSaveConfiguration.OFF, AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE, AutoSaveConfiguration.AFTER_DELAY) - }, - 'files.autoSaveDelay': { - 'type': 'number', -- 'default': 1000, -+ 'default': 100, - 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSaveDelay' }, "Controls the delay in ms after which a dirty editor is saved automatically. Only applies when `#files.autoSave#` is set to `{0}`.", AutoSaveConfiguration.AFTER_DELAY) + 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'autoSave' }, "Controls [auto save](https://code.visualstudio.com/docs/editor/codebasics#_save-auto-save) of editors that have unsaved changes.", AutoSaveConfiguration.OFF, AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE, AutoSaveConfiguration.AFTER_DELAY), + scope: ConfigurationScope.LANGUAGE_OVERRIDABLE }, - 'files.watcherExclude': { -diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts -index 79e5defa112..318ab13d6fd 100644 ---- a/src/vs/workbench/contrib/search/browser/search.contribution.ts -+++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts -@@ -889,7 +889,7 @@ configurationRegistry.registerConfiguration({ - 'search.quickOpen.includeHistory': { - type: 'boolean', - description: nls.localize('search.quickOpen.includeHistory', "Whether to include results from recently opened files in the file results for Quick Open."), -- default: true -+ default: false // FIXME We should be filtering history based on project rather than disabling it - }, - 'search.quickOpen.history.filterSortOrder': { - 'type': 'string', -diff --git a/src/vs/workbench/contrib/url/browser/trustedDomains.ts b/src/vs/workbench/contrib/url/browser/trustedDomains.ts -index 265a5dc43e3..8f1db546879 100644 ---- a/src/vs/workbench/contrib/url/browser/trustedDomains.ts -+++ b/src/vs/workbench/contrib/url/browser/trustedDomains.ts -@@ -217,7 +217,11 @@ export function readStaticTrustedDomains(accessor: ServicesAccessor): IStaticTru +diff --git a/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts +index e1913359976..3834f2dde41 100644 +--- a/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts ++++ b/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts +@@ -51,10 +51,11 @@ export class BuiltinExtensionsScannerService implements IBuiltinExtensionsScanne + if (builtinExtensionsServiceUrl) { + let bundledExtensions: IBundledExtension[] = []; - const defaultTrustedDomains = [ - ...productService.linkProtectionTrustedDomains ?? [], -- ...environmentService.options?.additionalTrustedDomains ?? [] -+ ...environmentService.options?.additionalTrustedDomains ?? [], -+ "https://utopia.app", -+ "https://utopia.fm", -+ "https://utopia.pizza", -+ "https://utopia95.com" - ]; +- if (environmentService.isBuilt) { +- // Built time configuration (do NOT modify) +- bundledExtensions = [/*BUILD->INSERT_BUILTIN_EXTENSIONS*/]; +- } else { ++ // This code prevents us from slipping our extension into the list of built in extensions ++ // if (environmentService.isBuilt) { ++ // // Built time configuration (do NOT modify) ++ // bundledExtensions = [/*BUILD->INSERT_BUILTIN_EXTENSIONS*/]; ++ // } else { + // Find builtin extensions by checking for DOM + const builtinExtensionsElement = mainWindow.document.getElementById('vscode-workbench-builtin-extensions'); + const builtinExtensionsElementAttribute = builtinExtensionsElement ? builtinExtensionsElement.getAttribute('data-settings') : undefined; +@@ -63,7 +64,7 @@ export class BuiltinExtensionsScannerService implements IBuiltinExtensionsScanne + bundledExtensions = JSON.parse(builtinExtensionsElementAttribute); + } catch (error) { /* ignore error*/ } + } +- } ++ // } - let trustedDomains: string[] = []; -diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js -index 618837df8e2..29b5ba8d333 100644 ---- a/src/vs/workbench/contrib/webview/browser/pre/main.js -+++ b/src/vs/workbench/contrib/webview/browser/pre/main.js -@@ -810,6 +810,7 @@ onDomReady(() => { - if (options.allowScripts) { - sandboxRules.add('allow-scripts'); - sandboxRules.add('allow-downloads'); -+ sandboxRules.add('allow-popups'); - } - if (options.allowForms) { - sandboxRules.add('allow-forms'); -diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts -index b23847b8bf1..9c2a6875fe0 100644 ---- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts -+++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts -@@ -370,7 +370,7 @@ export class IFrameWebview extends Disposable implements Webview { - const element = document.createElement('iframe'); - element.name = this.id; - element.className = `webview ${options.customClasses || ''}`; -- element.sandbox.add('allow-scripts', 'allow-same-origin', 'allow-forms', 'allow-pointer-lock', 'allow-downloads'); -+ element.sandbox.add('allow-scripts', 'allow-same-origin', 'allow-forms', 'allow-pointer-lock', 'allow-downloads', 'allow-popups'); - if (!isFirefox) { - element.setAttribute('allow', 'clipboard-read; clipboard-write;'); - } -diff --git a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.contribution.ts b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.contribution.ts -index 674eade0db1..f0b7198440f 100644 ---- a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.contribution.ts -+++ b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.contribution.ts -@@ -26,7 +26,7 @@ Registry.as(ConfigurationExtensions.Configuration) - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.newUntitledFile' }, "Open a new untitled file (only applies when opening an empty window)."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePageInEmptyWorkbench' }, "Open the Welcome page when opening an empty workbench."), - ], -- 'default': 'welcomePage', -+ 'default': 'none', - 'description': localize('workbench.startupEditor', "Controls which editor is shown at startup, if none are restored from the previous session.") - }, - } + this.builtinExtensionsPromises = bundledExtensions.map(async e => { + const id = getGalleryExtensionId(e.packageJSON.publisher, e.packageJSON.name); diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts -index aedda47bd66..1e685812b0e 100644 +index 731a48bf5b0..b77097ce7c2 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts -@@ -190,7 +190,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx - +@@ -121,7 +121,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx // help the file service to activate providers by activating extensions by file system event this._register(this._fileService.onWillActivateFileSystemProvider(e => { -- e.join(this.activateByEvent(`onFileSystem:${e.scheme}`)); -+ e.join(this.activateByEvent(`onFileSystem:utopia`)); + if (e.scheme !== Schemas.vscodeRemote) { +- e.join(this.activateByEvent(`onFileSystem:${e.scheme}`)); ++ e.join(this.activateByEvent(`onFileSystem:utopia`)); + } })); - this._registry = new ExtensionDescriptionRegistry([]); -diff --git a/src/vs/workbench/services/workspaces/browser/workspaces.ts b/src/vs/workbench/services/workspaces/browser/workspaces.ts -index 3b90080dc3d..cf9ac44fca9 100644 ---- a/src/vs/workbench/services/workspaces/browser/workspaces.ts -+++ b/src/vs/workbench/services/workspaces/browser/workspaces.ts -@@ -5,7 +5,6 @@ - - import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; - import { URI } from 'vs/base/common/uri'; --import { hash } from 'vs/base/common/hash'; - - // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - // NOTE: DO NOT CHANGE. IDENTIFIERS HAVE TO REMAIN STABLE -@@ -30,5 +29,6 @@ export function getSingleFolderWorkspaceIdentifier(folderPath: URI): ISingleFold - } - - function getWorkspaceId(uri: URI): string { -- return hash(uri.toString()).toString(16); -+ const urlParams = new URLSearchParams(window.location.search); -+ return urlParams.get('vs_code_session_id')!; - } diff --git a/yarn.lock b/yarn.lock -index 2f4f87570dd..cdeffa74f26 100644 +index 71aef4295fa..3cfe4dade65 100644 --- a/yarn.lock +++ b/yarn.lock -@@ -5248,6 +5248,11 @@ ignore@^5.1.4: - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" - integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== - -+immediate@~3.0.5: -+ version "3.0.6" -+ resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" -+ integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== -+ - import-cwd@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" -@@ -6138,6 +6143,13 @@ levn@^0.3.0, levn@~0.3.0: - prelude-ls "~1.1.2" - type-check "~0.3.2" +@@ -6508,6 +6508,13 @@ levn@^0.4.1: + prelude-ls "^1.2.1" + type-check "~0.4.0" +lie@3.1.1: + version "3.1.1" @@ -1271,10 +1142,10 @@ index 2f4f87570dd..cdeffa74f26 100644 + dependencies: + immediate "~3.0.5" + - liftoff@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-3.1.0.tgz#c9ba6081f908670607ee79062d700df062c52ed3" -@@ -6201,6 +6213,13 @@ loader-utils@^2.0.0: + lie@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" +@@ -6576,6 +6583,13 @@ loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" @@ -1288,9 +1159,21 @@ index 2f4f87570dd..cdeffa74f26 100644 locate-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" -@@ -8079,6 +8098,11 @@ prepend-http@^2.0.0: - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" - integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= +@@ -7200,11 +7214,6 @@ native-is-elevated@0.7.0: + resolved "https://registry.yarnpkg.com/native-is-elevated/-/native-is-elevated-0.7.0.tgz#77499639e232edad1886403969e2bf236294e7af" + integrity sha512-tp8hUqK7vexBiyIWKMvmRxdG6kqUtO+3eay9iB0i16NYgvCqE5wMe1Y0guHilpkmRgvVXEWNW4et1+qqcwpLBA== + +-native-keymap@^3.3.5: +- version "3.3.5" +- resolved "https://registry.yarnpkg.com/native-keymap/-/native-keymap-3.3.5.tgz#b1da65d32e42bf65e3ff9db05bed319927dc2b01" +- integrity sha512-7XDOLPNX1FnUFC/cX3cioBz2M+dO212ai9DuwpfKFzkPu3xTmEzOm5xewOMLXE4V9YoRhNPxvq1H2YpPWDgSsg== +- + native-watchdog@^1.4.1: + version "1.4.2" + resolved "https://registry.yarnpkg.com/native-watchdog/-/native-watchdog-1.4.2.tgz#cf9f913157ee992723aa372b6137293c663be9b7" +@@ -8287,6 +8296,11 @@ prelude-ls@~1.1.2: + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +prettier@2.8.8: + version "2.8.8" @@ -1300,16 +1183,16 @@ index 2f4f87570dd..cdeffa74f26 100644 pretty-hrtime@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" -@@ -10213,6 +10237,12 @@ util@^0.12.4: - safe-buffer "^5.1.2" +@@ -10235,6 +10249,12 @@ util@^0.12.4: + is-typed-array "^1.1.3" which-typed-array "^1.1.2" +"utopia-vscode-common@file:../../utopia-vscode-common": -+ version "0.1.4" ++ version "0.1.6" + dependencies: + localforage "1.9.0" + prettier "2.8.8" + - uuid@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" + uuid@^8.3.0: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" diff --git a/vscode-build/yarn.lock b/vscode-build/yarn.lock index 256a78553839..0f700f0ca86b 100644 --- a/vscode-build/yarn.lock +++ b/vscode-build/yarn.lock @@ -422,11 +422,6 @@ safe-buffer@5.1.2: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -semver-umd@5.5.7: - version "5.5.7" - resolved "https://registry.yarnpkg.com/semver-umd/-/semver-umd-5.5.7.tgz#966beb5e96c7da6fbf09c3da14c2872d6836c528" - integrity sha512-XgjPNlD0J6aIc8xoTN6GQGwWc2Xg0kq8NzrqMVuKG/4Arl6ab1F8+Am5Y/XKKCR+FceFr2yN/Uv5ZJBhRyRqKg== - send@0.17.1: version "0.17.1" resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" @@ -495,7 +490,7 @@ utils-merge@1.0.1: integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= "utopia-vscode-common@file:../utopia-vscode-common": - version "0.1.4" + version "0.1.6" dependencies: localforage "1.9.0" prettier "2.8.8" @@ -505,16 +500,6 @@ vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= -vscode-oniguruma@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.4.0.tgz#3795fd1ef9633a4a33f208bce92c008e64a6fc8f" - integrity sha512-VvTl/jIAADEqWWpEYRsOI1sXiYOTDA8KYNgK60+Mb3T+an9zPz3Cqc6RVJeYgOx/P5G+4M4jygB3X5xLLfYD0g== - -vscode-textmate@5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.2.0.tgz#01f01760a391e8222fe4f33fbccbd1ad71aed74e" - integrity sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ== - wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" diff --git a/website-next/components/common/env-vars.ts b/website-next/components/common/env-vars.ts index 269a7f3f4cf3..4415118af3b1 100644 --- a/website-next/components/common/env-vars.ts +++ b/website-next/components/common/env-vars.ts @@ -76,7 +76,6 @@ export const STATIC_BASE_URL: string = export const FLOATING_PREVIEW_BASE_URL: string = SECONDARY_BASE_URL export const PROPERTY_CONTROLS_INFO_BASE_URL: string = SECONDARY_BASE_URL -export const MONACO_EDITOR_IFRAME_BASE_URL: string = SECONDARY_BASE_URL export const ASSET_ENDPOINT = UTOPIA_BACKEND + 'asset/' export const THUMBNAIL_ENDPOINT = UTOPIA_BACKEND + 'thumbnail/'