From 69c128b85d49f84a94aa4218450c1cc59164250d Mon Sep 17 00:00:00 2001 From: Teffen Ellis Date: Fri, 29 Oct 2021 15:51:11 -0400 Subject: [PATCH 01/71] Flesh out code-server refactor to upstream's server. --- build/gulpfile.reh.js | 5 +- build/lib/node.ts | 6 +- build/lib/util.ts | 3 + resources/server/code-192.png | Bin 2721 -> 5486 bytes resources/server/code-512.png | Bin 2721 -> 14689 bytes resources/server/code.png | Bin 0 -> 19318 bytes resources/server/favicon-dark-support.svg | 7 + resources/server/favicon.ico | Bin 34494 -> 4868 bytes resources/server/favicon.svg | 1 + resources/server/web.sh | 8 +- src/tsec.exemptions.json | 1 + src/vs/base/common/product.ts | 12 + .../code/browser/workbench/service-worker.ts | 22 ++ .../code/browser/workbench/workbench-dev.html | 11 +- .../browser/workbench/workbench-error.html | 57 +++++ src/vs/code/browser/workbench/workbench.html | 15 +- .../server/@types/code-server-lib/index.d.ts | 130 ++++++++++ src/vs/server/common/net.ts | 42 ++++ src/vs/server/remoteAgentEnvironmentImpl.ts | 2 +- .../server/remoteExtensionHostAgentServer.ts | 57 +++-- src/vs/server/serverEnvironmentService.ts | 18 ++ src/vs/server/serverThemeService.ts | 121 +++++++++ src/vs/server/webClientServer.ts | 147 +++++++++-- src/vs/workbench/browser/client.ts | 231 ++++++++++++++++++ src/vs/workbench/browser/web.main.ts | 12 +- .../extensions/common/extensionsRegistry.ts | 1 + 26 files changed, 855 insertions(+), 54 deletions(-) create mode 100644 resources/server/code.png create mode 100644 resources/server/favicon-dark-support.svg create mode 100644 resources/server/favicon.svg create mode 100644 src/vs/code/browser/workbench/service-worker.ts create mode 100644 src/vs/code/browser/workbench/workbench-error.html create mode 100644 src/vs/server/@types/code-server-lib/index.d.ts create mode 100644 src/vs/server/common/net.ts create mode 100644 src/vs/server/serverThemeService.ts create mode 100644 src/vs/workbench/browser/client.ts diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index 1334cd15d6e01..4f80fbe06985f 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -125,7 +125,10 @@ const serverWithWebEntryPoints = [ ...vscodeWebEntryPoints ]; -function getNodeVersion() { +function getNodeVersion () { + // NOTE@coder: Fix version due to .yarnrc removal. + return process.versions.node; + const yarnrc = fs.readFileSync(path.join(REPO_ROOT, 'remote', '.yarnrc'), 'utf8'); const target = /^target "(.*)"$/m.exec(yarnrc)[1]; return target; diff --git a/build/lib/node.ts b/build/lib/node.ts index 6ac45ebb1f89c..96fa624ad30a2 100644 --- a/build/lib/node.ts +++ b/build/lib/node.ts @@ -4,13 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as path from 'path'; -import * as fs from 'fs'; const root = path.dirname(path.dirname(__dirname)); -const yarnrcPath = path.join(root, 'remote', '.yarnrc'); -const yarnrc = fs.readFileSync(yarnrcPath, 'utf8'); -const version = /^target\s+"([^"]+)"$/m.exec(yarnrc)![1]; +// NOTE@coder: Fix version due to .yarnrc removal. +const version = process.versions.node; const platform = process.platform; const arch = platform === 'darwin' ? 'x64' : process.arch; diff --git a/build/lib/util.ts b/build/lib/util.ts index 01796a59f6739..f91dcadb3d0d1 100644 --- a/build/lib/util.ts +++ b/build/lib/util.ts @@ -336,6 +336,9 @@ export function streamToPromise(stream: NodeJS.ReadWriteStream): Promise { } export function getElectronVersion(): string { + // NOTE@coder: Fix version due to .yarnrc removal. + return process.versions.node; + const yarnrc = fs.readFileSync(path.join(root, '.yarnrc'), 'utf8'); const target = /^target "(.*)"$/m.exec(yarnrc)![1]; return target; diff --git a/resources/server/code-192.png b/resources/server/code-192.png index 8d8646f8282c77316dcfde05b9f339678079a5c5..a6ee503115d8d4824730e125836e90cad72c6f19 100644 GIT binary patch literal 5486 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 diff --git a/resources/server/code-512.png b/resources/server/code-512.png index 8d8646f8282c77316dcfde05b9f339678079a5c5..ff42978ce9a52611d19eb011a43d830efef9647e 100644 GIT binary patch literal 14689 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 diff --git a/resources/server/code.png b/resources/server/code.png new file mode 100644 index 0000000000000000000000000000000000000000..d4f5188ffc0c4c2db014367c6bed74da09f2db34 GIT binary patch literal 19318 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 \ No newline at end of file diff --git a/resources/server/favicon.ico b/resources/server/favicon.ico index f9f0de3eb46f9cf6cab16ce698bef2e6896f2dd8..e721447bd9d175c1e64eb6bf9a14846f8ed5f7e5 100644 GIT binary patch literal 4868 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 diff --git a/resources/server/favicon.svg b/resources/server/favicon.svg new file mode 100644 index 0000000000000..45388729b6b4f --- /dev/null +++ b/resources/server/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/server/web.sh b/resources/server/web.sh index da072e5f2d0ea..38f2a6b310dc6 100755 --- a/resources/server/web.sh +++ b/resources/server/web.sh @@ -10,11 +10,11 @@ fi function code() { cd $ROOT - # Sync built-in extensions - yarn download-builtin-extensions + # # Sync built-in extensions + # yarn download-builtin-extensions - # Load remote node - yarn gulp node + # # Load remote node + # yarn gulp node NODE=$(node build/lib/node.js) diff --git a/src/tsec.exemptions.json b/src/tsec.exemptions.json index fbb798ca0e3a5..03b18dccea615 100644 --- a/src/tsec.exemptions.json +++ b/src/tsec.exemptions.json @@ -10,6 +10,7 @@ "vs/workbench/services/keybinding/test/electron-browser/keyboardMapperTestUtils.ts" ], "ban-trustedtypes-createpolicy": [ + "vs/workbench/browser/client.ts", "vs/base/browser/dom.ts", "vs/base/browser/markdownRenderer.ts", "vs/base/worker/defaultWorkerFactory.ts", diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 657b9c9dbaac9..510c018173bcf 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -31,6 +31,18 @@ export type ExtensionVirtualWorkspaceSupport = { }; export interface IProductConfiguration { + // @coder BEGIN + readonly codeServerVersion?: string; + readonly authed?: boolean; + readonly logoutEndpointUrl: string; + readonly proxyEndpointUrlTemplate?: string; + readonly serviceWorker?: { + readonly url: string; + readonly scope: string; + } + readonly icons: Array<{ src: string; type: string; sizes: string }>; + // @coder END */ + readonly version: string; readonly date?: string; readonly quality?: string; diff --git a/src/vs/code/browser/workbench/service-worker.ts b/src/vs/code/browser/workbench/service-worker.ts new file mode 100644 index 0000000000000..09cb3a3c8054d --- /dev/null +++ b/src/vs/code/browser/workbench/service-worker.ts @@ -0,0 +1,22 @@ +/* eslint-disable header/header */ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Coder Technologies. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// + +const sw = self as unknown as ServiceWorkerGlobalScope; + +sw.addEventListener('install', () => { + console.debug('[Service Worker] installed'); +}); + +sw.addEventListener('activate', (event) => { + event.waitUntil(sw.clients.claim()); + console.debug('[Service Worker] activated'); +}); + +sw.addEventListener('fetch', () => { + // Without this event handler we won't be recognized as a PWA. +}); diff --git a/src/vs/code/browser/workbench/workbench-dev.html b/src/vs/code/browser/workbench/workbench-dev.html index b30e016806ca0..acc5b9e5cca8d 100644 --- a/src/vs/code/browser/workbench/workbench-dev.html +++ b/src/vs/code/browser/workbench/workbench-dev.html @@ -1,6 +1,6 @@ - + - diff --git a/src/vs/code/browser/workbench/workbench.html b/src/vs/code/browser/workbench/workbench.html index 580f333537f90..0dfa09e5751d2 100644 --- a/src/vs/code/browser/workbench/workbench.html +++ b/src/vs/code/browser/workbench/workbench.html @@ -11,7 +11,8 @@ - + + @@ -25,12 +26,9 @@ - - - - - + + @@ -43,11 +41,15 @@ - + + - + + - - - + + + diff --git a/src/vs/server/common/net.ts b/src/vs/server/common/net.ts index 82c2ab077c575..8c4583858cfe5 100644 --- a/src/vs/server/common/net.ts +++ b/src/vs/server/common/net.ts @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { posix } from 'vs/base/common/path'; +import * as http from 'http'; /** * See [Web app manifest on MDN](https://developer.mozilla.org/en-US/docs/Web/Manifest) for additional information. @@ -26,21 +26,6 @@ export interface ClientTheme { export const ICON_SIZES = [192, 512]; -/** - * Returns the relative path prefix for a given URL path. - * @remark This is especially useful when creating URLs which have to remain - * relative to an initial request. - * - * @example - * ```ts - * const url = new URL('https://www.example.com/foo/bar/baz.js') - * getPathPrefix(url.pathname) // '/foo/bar/' - * ``` - */ -export function getPathPrefix(pathname: string) { - return posix.join(posix.dirname(pathname), '/'); -} - class HTTPError extends Error { constructor (message: string, public readonly code: number) { super(message); @@ -52,3 +37,76 @@ export class HTTPNotFoundError extends HTTPError { super(message, 404); } } + +/** + * Remove extra slashes in a URL. + * + * This is meant to fill the job of `path.join` so you can concatenate paths and + * then normalize out any extra slashes. + * + * If you are using `path.join` you do not need this but note that `path` is for + * file system paths, not URLs. + * + * @author coder + */ +export const normalize = (url: string, keepTrailing = false): string => { + return url.replace(/\/\/+/g, "/").replace(/\/+$/, keepTrailing ? "/" : "") +} + +/** + * Get the relative path that will get us to the root of the page. For each + * slash we need to go up a directory. Will not have a trailing slash. + * + * For example: + * + * / => . + * /foo => . + * /foo/ => ./.. + * /foo/bar => ./.. + * /foo/bar/ => ./../.. + * + * All paths must be relative in order to work behind a reverse proxy since we + * we do not know the base path. Anything that needs to be absolute (for + * example cookies) must get the base path from the frontend. + * + * All relative paths must be prefixed with the relative root to ensure they + * work no matter the depth at which they happen to appear. + * + * For Express `req.originalUrl` should be used as they remove the base from the + * standard `url` property making it impossible to get the true depth. + * + * @author coder + */ +export const relativeRoot = (originalUrl: string): string => { + const depth = (originalUrl.split("?", 1)[0].match(/\//g) || []).length + return normalize("./" + (depth > 1 ? "../".repeat(depth - 1) : "")) +} + +/** + * Get the relative path to the current resource. + * + * For example: + * + * / => . + * /foo => ./foo + * /foo/ => ./ + * /foo/bar => ./bar + * /foo/bar/ => ./bar/ + */ +export const relativePath = (originalUrl: string): string => { + const parts = originalUrl.split("?", 1)[0].split("/") + return normalize("./" + parts[parts.length - 1]) +} + +/** + * code-server serves VS Code using Express. Express removes the base from + * the url and puts the original in `originalUrl` so we must use this to get + * the correct depth. VS Code is not aware it is behind Express so the + * types do not match. We may want to continue moving code into VS Code and + * eventually remove the Express wrapper. + * + * @author coder + */ +export const getOriginalUrl = (req: http.IncomingMessage): string => { + return (req as any).originalUrl || req.url +} diff --git a/src/vs/server/webClientServer.ts b/src/vs/server/webClientServer.ts index 87227981faefd..00a04d9140926 100644 --- a/src/vs/server/webClientServer.ts +++ b/src/vs/server/webClientServer.ts @@ -25,7 +25,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; // eslint-disable-next-line code-import-patterns import type { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api'; import { editorBackground, editorForeground } from 'vs/platform/theme/common/colorRegistry'; -import { ClientTheme, getPathPrefix, HTTPNotFoundError, WebManifest } from 'vs/server/common/net'; +import { ClientTheme, getOriginalUrl, HTTPNotFoundError, relativePath, relativeRoot, WebManifest } from 'vs/server/common/net'; import { IServerThemeService } from 'vs/server/serverThemeService'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; @@ -194,17 +194,13 @@ export class WebClientServer { * PWA manifest file. This informs the browser that the app may be installed. */ private async _handleManifest(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise { - const pathPrefix = getPathPrefix(parsedUrl.pathname!); + // The manifest URL is used as the base when resolving URLs so we can just + // use . without having to check the depth since we serve it at the root. const clientTheme = await this.fetchClientTheme(); - const startUrl = pathPrefix.substring( - 0, - pathPrefix.lastIndexOf('/') + 1 - ); - const webManifest: WebManifest = { name: this._productService.nameLong, short_name: this._productService.nameShort, - start_url: normalize(startUrl), + start_url: '.', display: 'fullscreen', 'background-color': clientTheme.backgroundColor, description: 'Run editors on a remote server.', @@ -320,6 +316,8 @@ export class WebClientServer { scopes: [['user:email'], ['repo']] } : undefined; + const base = relativeRoot(getOriginalUrl(req)) + const vscodeBase = relativePath(getOriginalUrl(req)) const data = (await util.promisify(fs.readFile)(filePath)).toString() .replace('{{WORKBENCH_WEB_CONFIGURATION}}', escapeAttribute(JSON.stringify({ productConfiguration: { @@ -330,17 +328,18 @@ export class WebClientServer { // Service Worker serviceWorker: { - scope: './', - url: `./${this._environmentService.serviceWorkerFileName}` + scope: vscodeBase + '/', + url: vscodeBase + '/' + this._environmentService.serviceWorkerFileName, }, // Endpoints - logoutEndpointUrl: './logout', - webEndpointUrl: './static', - webEndpointUrlTemplate: './static', - webviewContentExternalBaseUrlTemplate: './webview/{{uuid}}/', + base, + logoutEndpointUrl: base + '/logout', + webEndpointUrl: vscodeBase + '/static', + webEndpointUrlTemplate: vscodeBase + '/static', + webviewContentExternalBaseUrlTemplate: vscodeBase + '/webview/{{uuid}}/', - updateUrl: './update/check' + updateUrl: base + '/update/check' }, folderUri: (workspacePath && isFolder) ? transformer.transformOutgoing(URI.file(workspacePath)) : undefined, workspaceUri: (workspacePath && !isFolder) ? transformer.transformOutgoing(URI.file(workspacePath)) : undefined, @@ -356,7 +355,8 @@ export class WebClientServer { }))) .replace(/{{CLIENT_BACKGROUND_COLOR}}/g, () => backgroundColor) .replace(/{{CLIENT_FOREGROUND_COLOR}}/g, () => foregroundColor) - .replace('{{WORKBENCH_AUTH_SESSION}}', () => authSessionInfo ? escapeAttribute(JSON.stringify(authSessionInfo)) : ''); + .replace('{{WORKBENCH_AUTH_SESSION}}', () => authSessionInfo ? escapeAttribute(JSON.stringify(authSessionInfo)) : '') + .replace(/{{BASE}}/g, () => vscodeBase); const cspDirectives = [ 'default-src \'self\';', @@ -505,7 +505,7 @@ export class WebClientServer { return res.end(JSON.stringify(knownCallbackUri)); } - serveError = async (_req: http.IncomingMessage, res: http.ServerResponse, code: number, message: string, parsedUrl?: url.UrlWithParsedQuery): Promise => { + serveError = async (req: http.IncomingMessage, res: http.ServerResponse, code: number, message: string, parsedUrl?: url.UrlWithParsedQuery): Promise => { const { applicationName, commit = 'development', version } = this._productService; res.statusCode = code; @@ -533,7 +533,8 @@ export class WebClientServer { .replace(/{{ERROR_MESSAGE}}/g, () => message) .replace(/{{ERROR_FOOTER}}/g, () => `${version} - ${commit}`) .replace(/{{CLIENT_BACKGROUND_COLOR}}/g, () => clientTheme.backgroundColor) - .replace(/{{CLIENT_FOREGROUND_COLOR}}/g, () => clientTheme.foregroundColor); + .replace(/{{CLIENT_FOREGROUND_COLOR}}/g, () => clientTheme.foregroundColor) + .replace(/{{BASE}}/g, () => relativePath(getOriginalUrl(req))); res.end(data); }; diff --git a/src/vs/workbench/browser/client.ts b/src/vs/workbench/browser/client.ts index ffa033abb8895..3c7c8aacfa378 100644 --- a/src/vs/workbench/browser/client.ts +++ b/src/vs/workbench/browser/client.ts @@ -176,7 +176,7 @@ export class CodeServerClientAdditions extends Disposable { } private appendSessionCommands() { - const { auth, logoutEndpointUrl } = this.productConfiguration; + const { auth, base, logoutEndpointUrl } = this.productConfiguration; // Use to show or hide logout commands and menu options. this.contextKeyService.createKey(CodeServerClientAdditions.AUTH_KEY, auth === AuthType.Password); @@ -190,9 +190,10 @@ export class CodeServerClientAdditions extends Disposable { * @file 'code-server/src/node/route/logout.ts' */ const logoutUrl = new URL(logoutEndpointUrl!, window.location.href); - // Add base param as this session may be stored within a nested path. - logoutUrl.searchParams.set('base', window.location.pathname); - + // Inform the backend about the path since the proxy might have rewritten + // it out of the headers and cookies must be set with absolute paths. + logoutUrl.searchParams.set('base', base || "."); + logoutUrl.searchParams.set('href', window.location.href); window.location.assign(logoutUrl); }); From 8c7a3f2373a23b890a03eb4c6db3b6d95346b628 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 14 Dec 2021 11:16:35 -0600 Subject: [PATCH 46/71] Set remote authority on frontend (#25) Trying to determine the remote authority on the backend is brittle because it does not work behind reverse proxies unless they send the right headers containing information about the proxied source. We could require users add the relevant configuration or provide the remote authority via a flag but neither are user-friendly options. We can make it work out of the box by changing the frontend to make requests to its current address (which is what we try to set the remote authority to anyway). This actually already happens for the most part except in some UI and logs although recent issues suggest there might be other problems which should be entirely resolved by setting this on the frontend. In other words, the remote authority we set on the backend should never be used so we set it to something invalid to ensure we notice (the alternative is to rip it out but that is probably a bigger patch thus generating more conflicts). One scenario where we might want to set the remote authority from the backend is if the frontend is served from a different location than the backend but that is not supported behavior at the moment. Even if we did support this we still cannot determine the authority from the backend (even for non-proxy scenarios in this case) and would need to add a flag for it so this change would still be necessary. https://github.com/cdr/code-server/issues/4604 https://github.com/cdr/code-server/issues/4607 https://github.com/cdr/code-server/issues/4608 --- package.json | 1 - src/vs/code/browser/workbench/workbench.ts | 6 ++++ src/vs/server/webClientServer.ts | 40 ++++++---------------- yarn.lock | 5 --- 4 files changed, 16 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index 76dec3210aefd..4b14230e56b12 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,6 @@ "@vscode/vscode-languagedetection": "1.0.21", "applicationinsights": "1.0.8", "cookie": "^0.4.1", - "forwarded-parse": "^2.1.2", "graceful-fs": "4.2.8", "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^2.2.3", diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index 362f3e2e6cc44..35a23054900bf 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -559,6 +559,12 @@ class WindowIndicator implements IWindowIndicator { // Finally create workbench create(document.body, { ...config, + /** + * Ensure the remote authority points to the current address since we cannot + * determine this reliably on the backend. + * @author coder + */ + remoteAuthority: location.host, /** * Override relative URLs in the product configuration against the window * location as necessary. Only paths that must be absolute need to be diff --git a/src/vs/server/webClientServer.ts b/src/vs/server/webClientServer.ts index 00a04d9140926..ea2c73ccc6b5f 100644 --- a/src/vs/server/webClientServer.ts +++ b/src/vs/server/webClientServer.ts @@ -9,7 +9,6 @@ import * as url from 'url'; import * as util from 'util'; import * as cookie from 'cookie'; import * as crypto from 'crypto'; -import parseForwardHeader = require('forwarded-parse'); import { isEqualOrParent, sanitizeFilePath } from 'vs/base/common/extpath'; import { getMediaMime } from 'vs/base/common/mime'; import { isLinux } from 'vs/base/common/platform'; @@ -27,7 +26,6 @@ import type { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.a import { editorBackground, editorForeground } from 'vs/platform/theme/common/colorRegistry'; import { ClientTheme, getOriginalUrl, HTTPNotFoundError, relativePath, relativeRoot, WebManifest } from 'vs/server/common/net'; import { IServerThemeService } from 'vs/server/serverThemeService'; -import { isFalsyOrWhitespace } from 'vs/base/common/strings'; const textMimeType = { '.html': 'text/html', @@ -167,29 +165,6 @@ export class WebClientServer { private _iconSizes = [192, 512]; - private getRemoteAuthority(req: http.IncomingMessage): URL { - if (req.headers.forwarded) { - const [parsedHeader] = parseForwardHeader(req.headers.forwarded); - return new URL(`${parsedHeader.proto}://${parsedHeader.host}`); - } - - /* Return first non-empty header. */ - const parseHeaders = (headerNames: string[]): string | undefined => { - for (const headerName of headerNames) { - const header = req.headers[headerName]?.toString(); - if (!isFalsyOrWhitespace(header)) { - return header; - } - } - return undefined; - }; - - const proto = parseHeaders(['X-Forwarded-Proto']) || 'http'; - const host = parseHeaders(['X-Forwarded-Host', 'host']) || 'localhost'; - - return new URL(`${proto}://${host}`); - } - /** * PWA manifest file. This informs the browser that the app may be installed. */ @@ -292,9 +267,15 @@ export class WebClientServer { // return this.serveError(req, res, 403, `Forbidden.`, parsedUrl); // } - const remoteAuthority = this.getRemoteAuthority(req); - - const transformer = createRemoteURITransformer(remoteAuthority.host); + /** + * It is not possible to reliably detect the remote authority on the server + * in all cases. Set this to something invalid to make sure we catch code + * that is using this when it should not. + * + * @author coder + */ + const remoteAuthority = 'remote'; + const transformer = createRemoteURITransformer(remoteAuthority); const { workspacePath, isFolder } = await this._getWorkspaceFromCLI(); function escapeAttribute(value: string): string { @@ -343,8 +324,7 @@ export class WebClientServer { }, folderUri: (workspacePath && isFolder) ? transformer.transformOutgoing(URI.file(workspacePath)) : undefined, workspaceUri: (workspacePath && !isFolder) ? transformer.transformOutgoing(URI.file(workspacePath)) : undefined, - // Add port to prevent client-side mismatch for reverse proxies. - remoteAuthority: `${remoteAuthority.hostname}:${remoteAuthority.port || (remoteAuthority.protocol === 'https:' ? '443' : '80')}`, + remoteAuthority, _wrapWebWorkerExtHostInIframe, developmentOptions: { enableSmokeTestDriver: this._environmentService.driverHandle === 'web' ? true : undefined, diff --git a/yarn.lock b/yarn.lock index 549ee5c1907e5..30e4a73595c7e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4348,11 +4348,6 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" -forwarded-parse@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/forwarded-parse/-/forwarded-parse-2.1.2.tgz#08511eddaaa2ddfd56ba11138eee7df117a09325" - integrity sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw== - fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" From 41de3417522c9a7d76339530550a712352985f56 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 14 Dec 2021 11:16:45 -0600 Subject: [PATCH 47/71] Use local filesystem instead of browser storage (#26) This is another patch we lost although I have implemented it in a slightly different way (it used to use the payload but I changed it to match the method we currently use to pass data to the frontend although maybe we should have used the payload for all that). https://github.com/cdr/code-server/issues/4609 --- src/vs/server/webClientServer.ts | 2 +- .../environment/browser/environmentService.ts | 20 +++++++++++++++++-- src/vs/workbench/workbench.web.api.ts | 4 ++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/vs/server/webClientServer.ts b/src/vs/server/webClientServer.ts index ea2c73ccc6b5f..2d61d8eb2388b 100644 --- a/src/vs/server/webClientServer.ts +++ b/src/vs/server/webClientServer.ts @@ -319,7 +319,6 @@ export class WebClientServer { webEndpointUrl: vscodeBase + '/static', webEndpointUrlTemplate: vscodeBase + '/static', webviewContentExternalBaseUrlTemplate: vscodeBase + '/webview/{{uuid}}/', - updateUrl: base + '/update/check' }, folderUri: (workspacePath && isFolder) ? transformer.transformOutgoing(URI.file(workspacePath)) : undefined, @@ -331,6 +330,7 @@ export class WebClientServer { logLevel: this._logService.getLevel(), }, ignoreLastOpened: this._environmentService.ignoreLastOpened, + userDataPath: this._environmentService.userDataPath, settingsSyncOptions: !this._environmentService.isBuilt && this._environmentService.args['enable-sync'] ? { enabled: true } : undefined, }))) .replace(/{{CLIENT_BACKGROUND_COLOR}}/g, () => backgroundColor) diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index f846e7f726df8..e92df2ca1144e 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -116,8 +116,19 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment @memoize get logFile(): URI { return joinPath(this.options.logsPath, 'window.log'); } + /** + * Use the local disk. This solves two problems: + * 1. Extensions running in the browser (like Vim) might use these paths + * directly instead of using the file service and most likely can't write + * to `/User` on disk. + * 2. Settings will be stored in the file system instead of in browser + * storage. Using browser storage makes sharing or seeding settings + * between browsers difficult. We may want to revisit this once/if we get + * settings sync. + * @author coder + */ @memoize - get userRoamingDataHome(): URI { return URI.file('/User').with({ scheme: Schemas.userData }); } + get userRoamingDataHome(): URI { return joinPath(URI.file(this.userDataPath).with({ scheme: Schemas.vscodeRemote }), 'User'); } @memoize get settingsResource(): URI { return joinPath(this.userRoamingDataHome, 'settings.json'); } @@ -256,7 +267,12 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment get ignoreLastOpened(): boolean { return !!this.options.ignoreLastOpened; } - + get userDataPath(): string { + if (!this.options.userDataPath) { + throw new Error('userDataPath was not provided to the browser'); + } + return this.options.userDataPath; + } //#endregion private payload: Map | undefined; diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index 1e3014de22cf2..66a05951f1995 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -345,6 +345,10 @@ interface IWorkbenchConstructionOptions { * @see `BrowserMain#initServices` */ readonly ignoreLastOpened?: boolean + /** + * Path to the user data directory. + */ + readonly userDataPath?: string //#endregion From 97ea564e8525332dda2936854e91b6edfa1de5a5 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 14 Dec 2021 14:50:00 -0600 Subject: [PATCH 48/71] Fix images not loading (#27) https://github.com/cdr/code-server/issues/3410 --- src/vs/workbench/api/common/extHostWebview.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index 2eb67a7bf10e6..4d5a5fdd791df 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -85,7 +85,11 @@ export class ExtHostWebview implements vscode.Webview { } return extensionCspRule + ' ' + webviewGenericCspSource; } - return webviewGenericCspSource; + /** + * When not using a CDN content loads from self. + * @author coder + */ + return `'self' ` + webviewGenericCspSource; } public get html(): string { From 35f5f6823bba17c633d6062fa4d59bedd82e1186 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 15 Dec 2021 15:46:33 -0600 Subject: [PATCH 49/71] Remove last opened functionality (#28) We must handle this before we get to the browser because the first thing it does is create a workspace provider based on the provided workspace before any services are created which means we cannot fetch settings. So if we grab the last opened after that is done it has the wrong workspace and this breaks the close folder functionality (and possibly other things). Since it needs to be done on the server I figure I will implement in code-server to minimize the patch. --- .../server/@types/code-server-lib/index.d.ts | 1 - src/vs/server/serverEnvironmentService.ts | 7 -- src/vs/server/webClientServer.ts | 3 +- src/vs/workbench/browser/web.main.ts | 69 +------------------ .../environment/browser/environmentService.ts | 3 - src/vs/workbench/workbench.web.api.ts | 5 -- 6 files changed, 4 insertions(+), 84 deletions(-) diff --git a/src/vs/server/@types/code-server-lib/index.d.ts b/src/vs/server/@types/code-server-lib/index.d.ts index 2af378da13c39..452356912b04b 100644 --- a/src/vs/server/@types/code-server-lib/index.d.ts +++ b/src/vs/server/@types/code-server-lib/index.d.ts @@ -18,7 +18,6 @@ declare global { export interface ServerParsedArgs { //#region auth?: AuthType; - 'ignore-last-opened'?: boolean; //#endregion port?: string; diff --git a/src/vs/server/serverEnvironmentService.ts b/src/vs/server/serverEnvironmentService.ts index 95eaa5f9fbc80..37f4a336f9400 100644 --- a/src/vs/server/serverEnvironmentService.ts +++ b/src/vs/server/serverEnvironmentService.ts @@ -16,7 +16,6 @@ export const serverOptions: OptionDescriptions = { //#region @coder 'auth': { type: 'string' }, 'port': { type: 'string' }, - 'ignore-last-opened': { type: 'boolean' }, //#endregion 'pick-port': { type: 'string' }, @@ -73,7 +72,6 @@ export const serverOptions: OptionDescriptions = { export interface ServerParsedArgs { //#region auth?: AuthType; - 'ignore-last-opened'?: boolean; //#endregion port?: string; @@ -165,7 +163,6 @@ export interface IServerEnvironmentService extends INativeEnvironmentService { readonly serviceWorkerPath: string; readonly proxyUri: string; readonly auth: AuthType; - readonly ignoreLastOpened: boolean; //#endregion } @@ -190,9 +187,5 @@ export class ServerEnvironmentService extends NativeEnvironmentService implement return '/proxy/{port}'; } - public get ignoreLastOpened(): boolean { - return !!this.args['ignore-last-opened']; - } - //#endregion } diff --git a/src/vs/server/webClientServer.ts b/src/vs/server/webClientServer.ts index 2d61d8eb2388b..999642abb64d6 100644 --- a/src/vs/server/webClientServer.ts +++ b/src/vs/server/webClientServer.ts @@ -79,7 +79,7 @@ export class WebClientServer { private readonly _environmentService: IServerEnvironmentService, private readonly _logService: ILogService, private readonly _themeService: IServerThemeService, - private readonly _productService: IProductService + private readonly _productService: IProductService, ) { } async handle(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise { @@ -329,7 +329,6 @@ export class WebClientServer { enableSmokeTestDriver: this._environmentService.driverHandle === 'web' ? true : undefined, logLevel: this._logService.getLevel(), }, - ignoreLastOpened: this._environmentService.ignoreLastOpened, userDataPath: this._environmentService.userDataPath, settingsSyncOptions: !this._environmentService.isBuilt && this._environmentService.args['enable-sync'] ? { enabled: true } : undefined, }))) diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 865d744e17e56..14f649b11af0d 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -22,12 +22,12 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; import { Schemas } from 'vs/base/common/network'; -import { IWorkspaceContextService, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { onUnexpectedError } from 'vs/base/common/errors'; import { setFullscreen } from 'vs/base/browser/browser'; -import { encodePath, URI } from 'vs/base/common/uri'; -import { isRecentFolder, IWorkspaceInitializationPayload, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; +import { URI } from 'vs/base/common/uri'; +import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService'; import { ConfigurationCache } from 'vs/workbench/services/configuration/common/configurationCache'; import { ISignService } from 'vs/platform/sign/common/sign'; @@ -68,7 +68,6 @@ import { safeStringify } from 'vs/base/common/objects'; import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials'; import { IndexedDB } from 'vs/base/browser/indexedDB'; import { CodeServerClientAdditions } from 'vs/workbench/browser/client'; -import { BrowserWorkspacesService } from 'vs/workbench/services/workspaces/browser/workspacesService'; class BrowserMain extends Disposable { @@ -222,68 +221,6 @@ class BrowserMain extends Disposable { }) ]); - /** - * Added to persist recent workspaces in the browser. - * These behaviors may disabled with the `--ignore-last-opened` argument. - * - * @author coder - * @example User specified a directory at startup. - * ```sh - * code-server ./path/to/project/ - * ``` - * - * @example Blank project without CLI arguments, - * using the last opened directory in the browser. - * ```sh - * code-server - * open http://localhost:8000/ - * ``` - * - * @example Query params override CLI arguments. - * ```sh - * code-server ./path/to/project/ - * open http://localhost:8000/?folder=/path/to/different/project - * ``` - */ - const browserWorkspacesService = new BrowserWorkspacesService(storageService, configurationService, logService, fileService, environmentService, uriIdentityService); - serviceCollection.set(IWorkspacesService, browserWorkspacesService); - const workspace = configurationService.getWorkspace(); - - logService.debug('Workspace configuration', { - workspaceFolders: workspace.folders, - ignoreLastOpened: environmentService.ignoreLastOpened, - }); - - if (workspace.folders.length === 0 && !environmentService.ignoreLastOpened) { - logService.debug('Workspace is empty. Checking for recent folders...'); - - const recentlyOpened = await browserWorkspacesService.getRecentlyOpened(); - - for (const recent of recentlyOpened.workspaces) { - if (isRecentFolder(recent)) { - logService.debug('Recent folder found...'); - const folder = toWorkspaceFolder(recent.folderUri); - // Note that the `folders` property should be reassigned instead of pushed into. - // This property has a setter which updates the workspace's file cache. - workspace.folders = [folder]; - - - /** - * Opening a folder from the browser navigates to a URL including the folder param. - * However, since we're overriding the default state of a blank editor, - * we update the URL query param to match this behavior. - * This is especially useful when a user wants to share a link to server with a specific folder. - * - * @see `WorkspaceProvider.createTargetUrl` - * @see `WorkspaceProvider.QUERY_PARAM_FOLDER` - */ - const nextQueryParam = `?folder=${encodePath(folder.uri.path)}`; - window.history.replaceState(null, '', nextQueryParam); - break; - } - } - } - // Workspace Trust Service const workspaceTrustEnablementService = new WorkspaceTrustEnablementService(configurationService, environmentService); serviceCollection.set(IWorkspaceTrustEnablementService, workspaceTrustEnablementService); diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index e92df2ca1144e..e9f9a42de3fd6 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -264,9 +264,6 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment get disableWorkspaceTrust(): boolean { return true; } //#region @coder - get ignoreLastOpened(): boolean { - return !!this.options.ignoreLastOpened; - } get userDataPath(): string { if (!this.options.userDataPath) { throw new Error('userDataPath was not provided to the browser'); diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index 66a05951f1995..42645fcbb41cd 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -340,11 +340,6 @@ interface ISettingsSyncOptions { interface IWorkbenchConstructionOptions { //#region @coder - /** - * Ignore the last opened folder in the browser. - * @see `BrowserMain#initServices` - */ - readonly ignoreLastOpened?: boolean /** * Path to the user data directory. */ From 48fae57fd9adb772fc1b10e4a9a5e1ba6880640a Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Wed, 15 Dec 2021 16:42:25 -0700 Subject: [PATCH 50/71] fix(product.json): use "code-server" for name (#29) We always refer to the project as "code-server". Never "Code Server." Guessing this was accidentally slipped in. --- product.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/product.json b/product.json index b0e8aceeda150..8941cc166f38b 100644 --- a/product.json +++ b/product.json @@ -1,6 +1,6 @@ { - "nameShort": "Code Server", - "nameLong": "Code Server", + "nameShort": "code-server", + "nameLong": "code-server", "applicationName": "code-server", "dataFolderName": ".code-server", "win32MutexName": "codeserver", From 48507d954e738f691ca9adfecc6e2d094c1b63cf Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Fri, 17 Dec 2021 12:41:15 -0700 Subject: [PATCH 51/71] fix: patch isProposedAPIEnabled to always be true (#30) --- .../services/extensions/common/extensions.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index 8f9b89d41b9aa..6688449c692cf 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -134,10 +134,15 @@ export interface IExtensionHost { } export function isProposedApiEnabled(extension: IExtensionDescription, proposal: ApiProposalName): boolean { - if (!extension.enabledApiProposals) { - return false; - } - return extension.enabledApiProposals.includes(proposal); + /** + * The Jupyter extension uses proposed APIs + * but has a version that doesn't enable it correctly. + * + * This patch ensures that we default to enabling + * the proposed API. + * @author coder + */ + return true } export function checkProposedApiEnabled(extension: IExtensionDescription, proposal: ApiProposalName): void { From 0e2dc9d8cffda388b1f8646b4be881d043ad8ed7 Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Fri, 17 Dec 2021 15:35:12 -0700 Subject: [PATCH 52/71] fix: add remoteAuthority for URIs (#31) --- src/vs/code/browser/workbench/workbench.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index 35a23054900bf..289a100e3d492 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -440,12 +440,17 @@ class WindowIndicator implements IWindowIndicator { /** * If the value begins with a slash assume it is a file path and convert it to * use the vscode-remote scheme. + * + * We also add the remote authority in toRemote. It needs to be accurate + * otherwise other URIs won't match it, leading to issues such as this one: + * https://github.com/coder/code-server/issues/4630 * * @author coder */ + const remoteAuthority = location.host const toRemote = (value: string): string => { if (value.startsWith('/')) { - return 'vscode-remote://' + value; + return 'vscode-remote://' + remoteAuthority + value; } return value; }; @@ -564,7 +569,7 @@ class WindowIndicator implements IWindowIndicator { * determine this reliably on the backend. * @author coder */ - remoteAuthority: location.host, + remoteAuthority, /** * Override relative URLs in the product configuration against the window * location as necessary. Only paths that must be absolute need to be From d50b73c41535a688e4bace787a1f5d829d26af40 Mon Sep 17 00:00:00 2001 From: Len Date: Mon, 20 Dec 2021 17:23:45 +0100 Subject: [PATCH 53/71] Support browsers from before 2020 (#23) --- src/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tsconfig.json b/src/tsconfig.json index eaaa3fb52b8bf..d416b45806793 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -5,7 +5,8 @@ "preserveConstEnums": true, "sourceMap": false, "outDir": "../out/vs", - "target": "es2020", + "target": "es6", + "lib": ["es2020", "dom", "dom.iterable"], "types": [ "keytar", "mocha", From 53bc7512d7af7105fc5908e07eed9f9a891e94d6 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 23 Dec 2021 12:15:35 -0600 Subject: [PATCH 54/71] Fix missing service worker (#35) Again. --- build/gulpfile.vscode.web.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build/gulpfile.vscode.web.js b/build/gulpfile.vscode.web.js index bf45fc26cd24c..bbc9a03817b17 100644 --- a/build/gulpfile.vscode.web.js +++ b/build/gulpfile.vscode.web.js @@ -41,6 +41,9 @@ const vscodeWebResourceIncludes = [ 'out-build/vs/workbench/contrib/webview/browser/pre/*.js', 'out-build/vs/workbench/contrib/webview/browser/pre/*.html', + // Service worker + 'out-build/vs/code/browser/workbench/service-worker.js', + // Extension Worker 'out-build/vs/workbench/services/extensions/worker/httpsWebWorkerExtensionHostIframe.html', 'out-build/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html', From 76663e7c7da4f165ee59d085f9111beeec457303 Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 23 Dec 2021 12:16:36 -0600 Subject: [PATCH 55/71] Restore display language support (#34) For https://github.com/coder/code-server/issues/4598. --- src/vs/base/common/platform.ts | 16 ++++++ src/vs/base/parts/ipc/common/ipc.ts | 2 +- .../code/browser/workbench/workbench-dev.html | 3 + src/vs/code/browser/workbench/workbench.html | 30 +++++++++- .../environment/common/environmentService.ts | 2 +- .../server/@types/code-server-lib/index.d.ts | 1 + .../server/remoteExtensionHostAgentServer.ts | 18 +++++- src/vs/server/remoteLanguagePacks.ts | 55 +++++++++++++++++++ src/vs/server/serverEnvironmentService.ts | 4 +- src/vs/server/webClientServer.ts | 5 ++ .../browser/localizationsService.ts | 28 ++++++++++ src/vs/workbench/workbench.web.main.ts | 8 +++ 12 files changed, 164 insertions(+), 8 deletions(-) create mode 100644 src/vs/workbench/services/localizations/browser/localizationsService.ts diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts index 723d1c3ed3168..648d3f4be7e31 100644 --- a/src/vs/base/common/platform.ts +++ b/src/vs/base/common/platform.ts @@ -81,6 +81,22 @@ if (typeof navigator === 'object' && !isElectronRenderer) { _isWeb = true; _locale = navigator.language; _language = _locale; + + /** + * Make languages work. + * + * @author coder + */ + const el = typeof document !== 'undefined' && document.getElementById('vscode-remote-nls-configuration'); + const rawNlsConfig = el && el.getAttribute('data-settings'); + if (rawNlsConfig) { + try { + const nlsConfig: NLSConfig = JSON.parse(rawNlsConfig); + _locale = nlsConfig.locale; + _translationsConfigFile = nlsConfig._translationsConfigFile; + _language = nlsConfig.availableLanguages['*'] || LANGUAGE_DEFAULT; + } catch (error) { /* Oh well. */ } + } } // Native environment diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index fcb5823f0cb87..344d83302240c 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -1056,7 +1056,7 @@ export namespace ProxyChannel { export interface ICreateServiceChannelOptions extends IProxyOptions { } - export function fromService(service: unknown, options?: ICreateServiceChannelOptions): IServerChannel { + export function fromService(service: unknown, options?: ICreateServiceChannelOptions): IServerChannel { const handler = service as { [key: string]: unknown }; const disableMarshalling = options && options.disableMarshalling; diff --git a/src/vs/code/browser/workbench/workbench-dev.html b/src/vs/code/browser/workbench/workbench-dev.html index 7caeae015cfaa..49b5d99d31bc1 100644 --- a/src/vs/code/browser/workbench/workbench-dev.html +++ b/src/vs/code/browser/workbench/workbench-dev.html @@ -23,6 +23,9 @@ + + + diff --git a/src/vs/code/browser/workbench/workbench.html b/src/vs/code/browser/workbench/workbench.html index 238a1eff989d7..4f97f5b635da7 100644 --- a/src/vs/code/browser/workbench/workbench.html +++ b/src/vs/code/browser/workbench/workbench.html @@ -23,6 +23,9 @@ + + + @@ -42,9 +45,31 @@ `; @@ -187,6 +201,29 @@ suite('Tests for Emmet actions on html tags', () => { }); }); }); + + test('remove tag with extra trim', () => { + const expectedContents = ` +

+
  • Hello
  • + +
  • There
  • + +
  • Bye
  • + +
    + `; + return withRandomFileEditor(spacedContents, 'html', (editor, doc) => { + editor.selections = [ + new Selection(2, 4, 2, 4), // cursor inside ul tag + ]; + + return removeTag()!.then(() => { + assert.strictEqual(doc.getText(), expectedContents); + return Promise.resolve(); + }); + }); + }); // #endregion // #region split/join tag diff --git a/extensions/emmet/src/test/testUtils.ts b/extensions/emmet/src/test/testUtils.ts index 30e11ee794b99..01e1e098fead1 100644 --- a/extensions/emmet/src/test/testUtils.ts +++ b/extensions/emmet/src/test/testUtils.ts @@ -8,8 +8,13 @@ import * as fs from 'fs'; import * as os from 'os'; import { join } from 'path'; -function rndName() { - return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10); +export function rndName() { + let name = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < 10; i++) { + name += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return name; } export function createRandomFile(contents = '', fileExtension = 'txt'): Thenable { diff --git a/extensions/emmet/src/test/updateImageSize.test.ts b/extensions/emmet/src/test/updateImageSize.test.ts index 606f26554b96d..37f5653c574b4 100644 --- a/extensions/emmet/src/test/updateImageSize.test.ts +++ b/extensions/emmet/src/test/updateImageSize.test.ts @@ -12,19 +12,23 @@ import { updateImageSize } from '../updateImageSize'; suite('Tests for Emmet actions on html tags', () => { teardown(closeAllEditors); + const imageUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAHYcAAB2HAY/l8WUAAAATSURBVBhXY/jPwADGDP////8PAB/uBfuDMzhuAAAAAElFTkSuQmCC'; + const imageWidth = 2; + const imageHeight = 2; + test('update image css with multiple cursors in css file', () => { const cssContents = ` .one { margin: 10px; padding: 10px; - background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); + background-image: url('https://github.com/$%7BimageUrl%7D'); } .two { - background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); + background-image: url('https://github.com/$%7BimageUrl%7D'); height: 42px; } .three { - background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); + background-image: url('https://github.com/$%7BimageUrl%7D'); width: 42px; } `; @@ -32,19 +36,19 @@ suite('Tests for Emmet actions on html tags', () => { .one { margin: 10px; padding: 10px; - background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); - width: 1024px; - height: 1024px; + background-image: url('https://github.com/$%7BimageUrl%7D'); + width: ${imageWidth}px; + height: ${imageHeight}px; } .two { - background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); - width: 1024px; - height: 1024px; + background-image: url('https://github.com/$%7BimageUrl%7D'); + width: ${imageWidth}px; + height: ${imageHeight}px; } .three { - background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); - height: 1024px; - width: 1024px; + background-image: url('https://github.com/$%7BimageUrl%7D'); + height: ${imageHeight}px; + width: ${imageWidth}px; } `; return withRandomFileEditor(cssContents, 'css', (editor, doc) => { @@ -68,14 +72,14 @@ suite('Tests for Emmet actions on html tags', () => { .one { margin: 10px; padding: 10px; - background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); + background-image: url('https://github.com/$%7BimageUrl%7D'); } .two { - background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); + background-image: url('https://github.com/$%7BimageUrl%7D'); height: 42px; } .three { - background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); + background-image: url('https://github.com/$%7BimageUrl%7D'); width: 42px; } @@ -87,19 +91,19 @@ suite('Tests for Emmet actions on html tags', () => { .one { margin: 10px; padding: 10px; - background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); - width: 1024px; - height: 1024px; + background-image: url('https://github.com/$%7BimageUrl%7D'); + width: ${imageWidth}px; + height: ${imageHeight}px; } .two { - background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); - width: 1024px; - height: 1024px; + background-image: url('https://github.com/$%7BimageUrl%7D'); + width: ${imageWidth}px; + height: ${imageHeight}px; } .three { - background-image: url(https://raw.githubusercontent.com/microsoft/vscode/master/resources/linux/code.png); - height: 1024px; - width: 1024px; + background-image: url('https://github.com/$%7BimageUrl%7D'); + height: ${imageHeight}px; + width: ${imageWidth}px; } @@ -121,16 +125,16 @@ suite('Tests for Emmet actions on html tags', () => { test('update image size in img tag in html file with multiple cursors', () => { const htmlwithimgtag = ` - - - + + + `; const expectedContents = ` - - - + + + `; return withRandomFileEditor(htmlwithimgtag, 'html', (editor, doc) => { diff --git a/extensions/emmet/src/typings/image-size.d.ts b/extensions/emmet/src/typings/image-size.d.ts deleted file mode 100644 index f9a935ea05028..0000000000000 --- a/extensions/emmet/src/typings/image-size.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Type definitions for image-size -// Project: https://github.com/image-size/image-size -// Definitions by: Elisée MAURER -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped - -/// - -declare module 'image-size' { - interface ImageInfo { - width: number; - height: number; - type: string; - } - - function sizeOf(path: string): ImageInfo; - function sizeOf(path: string, callback: (err: Error, dimensions: ImageInfo) => void): void; - - function sizeOf(buffer: Buffer): ImageInfo; - - namespace sizeOf { } - - export = sizeOf; -} diff --git a/extensions/emmet/src/updateImageSize.ts b/extensions/emmet/src/updateImageSize.ts index dadfe49fd893d..3a9204c0580f3 100644 --- a/extensions/emmet/src/updateImageSize.ts +++ b/extensions/emmet/src/updateImageSize.ts @@ -7,7 +7,7 @@ import { TextEditor, Position, window, TextEdit } from 'vscode'; import * as path from 'path'; -import { getImageSize } from './imageSizeHelper'; +import { getImageSize, ImageInfoWithScale } from './imageSizeHelper'; import { getFlatNode, iterateCSSToken, getCssPropertyFromRule, isStyleSheet, validate, offsetRangeToVsRange } from './util'; import { HtmlNode, CssToken, HtmlToken, Attribute, Property } from 'EmmetFlatNode'; import { locateFile } from './locateFile'; @@ -108,11 +108,11 @@ function updateImageSizeCSS(editor: TextEditor, position: Position, fetchNode: ( return locateFile(path.dirname(editor.document.fileName), src) .then(getImageSize) - .then((size: any): TextEdit[] => { + .then((size: ImageInfoWithScale | undefined): TextEdit[] => { // since this action is asynchronous, we have to ensure that editor wasn't // changed and user didn't moved caret outside node const prop = fetchNode(editor, position); - if (prop && getImageSrcCSS(editor, prop, position) === src) { + if (size && prop && getImageSrcCSS(editor, prop, position) === src) { return updateCSSNode(editor, prop, size.width, size.height); } return []; diff --git a/extensions/emmet/yarn.lock b/extensions/emmet/yarn.lock index 020bf7b07775a..84ad50583e824 100644 --- a/extensions/emmet/yarn.lock +++ b/extensions/emmet/yarn.lock @@ -54,14 +54,14 @@ integrity sha1-Rs/+oRmgoAMxKiHC2bVijLX81EI= "@types/node@14.x": - version "14.17.27" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.27.tgz#5054610d37bb5f6e21342d0e6d24c494231f3b85" - integrity sha512-94+Ahf9IcaDuJTle/2b+wzvjmutxXAEXU6O81JHblYXUg2BDG+dnBy7VxIPHKAyEEDHzCMQydTJuWvrE+Aanzw== + version "14.18.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.0.tgz#98df2397f6936bfbff4f089e40e06fa5dd88d32a" + integrity sha512-0GeIl2kmVMXEnx8tg1SlG6Gg8vkqirrW752KqolYo1PHevhhZN3bhJ67qHj+bQaINhX0Ra3TlWwRvMCd9iEfNQ== "@vscode/emmet-helper@^2.3.0": - version "2.8.2" - resolved "https://registry.yarnpkg.com/@vscode/emmet-helper/-/emmet-helper-2.8.2.tgz#9b2ce4fdd62cf3fda45cf8af67c012cfce55edc9" - integrity sha512-A/+pkBYQq2JTow1A2flfTmEOmiF780KpdkoX7VBjQ7wujeA+CFUPd17YdeIa9aim20+J5Jp7SFujPDwVFiQucQ== + version "2.8.3" + resolved "https://registry.yarnpkg.com/@vscode/emmet-helper/-/emmet-helper-2.8.3.tgz#f7c2b4a4751d03bcf2b421f0fce5b521a0f64a18" + integrity sha512-dkTSL+BaBBS8gFgPm/GMOU+XfxaMyI+Fl1IUYxEi8Iv24RfHf9/q2eCpV2hs7sncLcoKWEbMYe5gv4Ppmp2Oxw== dependencies: emmet "^2.3.0" jsonc-parser "^2.3.0" @@ -71,27 +71,41 @@ vscode-uri "^2.1.2" emmet@^2.3.0: - version "2.3.4" - resolved "https://registry.yarnpkg.com/emmet/-/emmet-2.3.4.tgz#5ba0d7a5569a68c7697dfa890c772e4f3179d123" - integrity sha512-3IqSwmO+N2ZGeuhDyhV/TIOJFUbkChi53bcasSNRE7Yd+4eorbbYz4e53TpMECt38NtYkZNupQCZRlwdAYA42A== + version "2.3.5" + resolved "https://registry.yarnpkg.com/emmet/-/emmet-2.3.5.tgz#7f80f9c3db6831d1ee2b458717b9c36a074b1a47" + integrity sha512-LcWfTamJnXIdMfLvJEC5Ld3hY5/KHXgv1L1bp6I7eEvB0ZhacHZ1kX0BYovJ8FroEsreLcq7n7kZhRMsf6jkXQ== dependencies: "@emmetio/abbreviation" "^2.2.2" "@emmetio/css-abbreviation" "^2.1.4" -image-size@^0.5.2: - version "0.5.5" - resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" - integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w= +image-size@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.0.0.tgz#58b31fe4743b1cec0a0ac26f5c914d3c5b2f0750" + integrity sha512-JLJ6OwBfO1KcA+TvJT+v8gbE6iWbj24LyDNFgFEN0lzegn6cC6a/p3NIDaepMsJjQjlUWqIC7wJv8lBFxPNjcw== + dependencies: + queue "6.0.2" + +inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== jsonc-parser@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.3.1.tgz#59549150b133f2efacca48fe9ce1ec0659af2342" integrity sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg== +queue@6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/queue/-/queue-6.0.2.tgz#b91525283e2315c7553d2efa18d83e76432fed65" + integrity sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA== + dependencies: + inherits "~2.0.3" + vscode-languageserver-textdocument@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.2.tgz#2f9f6bd5b5eb3d8e21424c0c367009216f016236" - integrity sha512-T7uPC18+f8mYE4lbVZwb3OSmvwTZm3cuFhrdx9Bn2l11lmp3SvSuSVjy2JtvrghzjAo4G6Trqny2m9XGnFnWVA== + version "1.0.3" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.3.tgz#879f2649bfa5a6e07bc8b392c23ede2dfbf43eff" + integrity sha512-ynEGytvgTb6HVSUwPJIAZgiHQmPCx8bZ8w5um5Lz+q5DjP0Zj8wTFhQpyg8xaMvefDytw2+HH5yzqS+FhsR28A== vscode-languageserver-types@^3.15.1: version "3.16.0" diff --git a/extensions/extension-editing/package.json b/extensions/extension-editing/package.json index c57ee402b36bc..dbc18aa142be8 100644 --- a/extensions/extension-editing/package.json +++ b/extensions/extension-editing/package.json @@ -27,7 +27,7 @@ }, "dependencies": { "jsonc-parser": "^2.2.1", - "markdown-it": "^12.0.4", + "markdown-it": "^12.3.2", "parse5": "^3.0.2", "vscode-nls": "^5.0.0" }, diff --git a/extensions/extension-editing/src/extensionLinter.ts b/extensions/extension-editing/src/extensionLinter.ts index 0a66243c6d7e4..d4067e5265ad8 100644 --- a/extensions/extension-editing/src/extensionLinter.ts +++ b/extensions/extension-editing/src/extensionLinter.ts @@ -9,7 +9,7 @@ import { URL } from 'url'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); -import { parseTree, findNodeAtLocation, Node as JsonNode } from 'jsonc-parser'; +import { parseTree, findNodeAtLocation, Node as JsonNode, getNodeValue } from 'jsonc-parser'; import * as MarkdownItType from 'markdown-it'; import { languages, workspace, Disposable, TextDocument, Uri, Diagnostic, Range, DiagnosticSeverity, Position, env } from 'vscode'; @@ -17,6 +17,7 @@ import { languages, workspace, Disposable, TextDocument, Uri, Diagnostic, Range, const product = JSON.parse(fs.readFileSync(path.join(env.appRoot, 'product.json'), { encoding: 'utf-8' })); const allowedBadgeProviders: string[] = (product.extensionAllowedBadgeProviders || []).map((s: string) => s.toLowerCase()); const allowedBadgeProvidersRegex: RegExp[] = (product.extensionAllowedBadgeProvidersRegex || []).map((r: string) => new RegExp(r)); +const extensionEnabledApiProposals: Record = product.extensionEnabledApiProposals ?? {}; function isTrustedSVGSource(uri: Uri): boolean { return allowedBadgeProviders.includes(uri.authority.toLowerCase()) || allowedBadgeProvidersRegex.some(r => r.test(uri.toString())); @@ -29,6 +30,7 @@ const dataUrlsNotValid = localize('dataUrlsNotValid', "Data URLs are not a valid const relativeUrlRequiresHttpsRepository = localize('relativeUrlRequiresHttpsRepository', "Relative image URLs require a repository with HTTPS protocol to be specified in the package.json."); const relativeIconUrlRequiresHttpsRepository = localize('relativeIconUrlRequiresHttpsRepository', "An icon requires a repository with HTTPS protocol to be specified in this package.json."); const relativeBadgeUrlRequiresHttpsRepository = localize('relativeBadgeUrlRequiresHttpsRepository', "Relative badge URLs require a repository with HTTPS protocol to be specified in this package.json."); +const apiProposalNotListed = localize('apiProposalNotListed', "This proposal cannot be used because for this extension the product defines a fixed set of API proposals. You can test your extension but before publishing you MUST reach out to the VS Code team."); enum Context { ICON, @@ -75,7 +77,7 @@ export class ExtensionLinter { private queue(document: TextDocument) { const p = document.uri.path; - if (document.languageId === 'json' && endsWith(p, '/package.json')) { + if (document.languageId === 'json' && p.endsWith('/package.json')) { this.packageJsonQ.add(document); this.startTimer(); } @@ -84,7 +86,7 @@ export class ExtensionLinter { private queueReadme(document: TextDocument) { const p = document.uri.path; - if (document.languageId === 'markdown' && (endsWith(p.toLowerCase(), '/readme.md') || endsWith(p.toLowerCase(), '/changelog.md'))) { + if (document.languageId === 'markdown' && (p.toLowerCase().endsWith('/readme.md') || p.toLowerCase().endsWith('/changelog.md'))) { this.readmeQ.add(document); this.startTimer(); } @@ -130,6 +132,23 @@ export class ExtensionLinter { .map(url => this.addDiagnostics(diagnostics, document, url!.offset + 1, url!.offset + url!.length - 1, url!.value, Context.BADGE, info)); } + const publisher = findNodeAtLocation(tree, ['publisher']); + const name = findNodeAtLocation(tree, ['name']); + const enabledApiProposals = findNodeAtLocation(tree, ['enabledApiProposals']); + if (publisher?.type === 'string' && name?.type === 'string' && enabledApiProposals?.type === 'array') { + const extensionId = `${getNodeValue(publisher)}.${getNodeValue(name)}`; + const effectiveProposalNames = extensionEnabledApiProposals[extensionId]; + if (Array.isArray(effectiveProposalNames) && enabledApiProposals.children) { + for (const child of enabledApiProposals.children) { + if (child.type === 'string' && !effectiveProposalNames.includes(getNodeValue(child))) { + const start = document.positionAt(child.offset); + const end = document.positionAt(child.offset + child.length); + diagnostics.push(new Diagnostic(new Range(start, end), apiProposalNotListed, DiagnosticSeverity.Error)); + } + } + } + } + } this.diagnosticsCollection.set(document.uri, diagnostics); }); @@ -329,7 +348,7 @@ export class ExtensionLinter { diagnostics.push(new Diagnostic(range, message, DiagnosticSeverity.Warning)); } - if (endsWith(uri.path.toLowerCase(), '.svg') && !isTrustedSVGSource(uri)) { + if (uri.path.toLowerCase().endsWith('.svg') && !isTrustedSVGSource(uri)) { const range = new Range(document.positionAt(begin), document.positionAt(end)); diagnostics.push(new Diagnostic(range, svgsNotValid, DiagnosticSeverity.Warning)); } @@ -346,17 +365,6 @@ export class ExtensionLinter { } } -function endsWith(haystack: string, needle: string): boolean { - let diff = haystack.length - needle.length; - if (diff > 0) { - return haystack.indexOf(needle, diff) === diff; - } else if (diff === 0) { - return haystack === needle; - } else { - return false; - } -} - function parseUri(src: string, base?: string, retry: boolean = true): Uri | null { try { let url = new URL(src, base); diff --git a/extensions/extension-editing/yarn.lock b/extensions/extension-editing/yarn.lock index ecb53673162a1..171c0baaa6f81 100644 --- a/extensions/extension-editing/yarn.lock +++ b/extensions/extension-editing/yarn.lock @@ -39,10 +39,10 @@ linkify-it@^3.0.1: dependencies: uc.micro "^1.0.1" -markdown-it@^12.0.4: - version "12.0.4" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.0.4.tgz#eec8247d296327eac3ba9746bdeec9cfcc751e33" - integrity sha512-34RwOXZT8kyuOJy25oJNJoulO8L0bTHYWXcdZBYZqFnjIy3NgjeoM3FmPXIOFQ26/lSHYMr8oc62B6adxXcb3Q== +markdown-it@^12.3.2: + version "12.3.2" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90" + integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg== dependencies: argparse "^2.0.1" entities "~2.1.0" @@ -62,12 +62,7 @@ parse5@^3.0.2: dependencies: "@types/node" "^6.0.46" -uc.micro@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192" - integrity sha1-ftUNXg+an7ClczeSWfKndFjVAZI= - -uc.micro@^1.0.5: +uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== diff --git a/extensions/fsharp/cgmanifest.json b/extensions/fsharp/cgmanifest.json index b898a38669c21..c6bf0cd413978 100644 --- a/extensions/fsharp/cgmanifest.json +++ b/extensions/fsharp/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "ionide/ionide-fsgrammar", "repositoryUrl": "https://github.com/ionide/ionide-fsgrammar", - "commitHash": "fc4cac6d9bc1787f54ce48bbc77bcbb1de8160ff" + "commitHash": "bba27391e61090035449b5c1e5c4b9d396bc4c9b" } }, "license": "MIT", diff --git a/extensions/fsharp/syntaxes/fsharp.tmLanguage.json b/extensions/fsharp/syntaxes/fsharp.tmLanguage.json index ca06a19c2c26a..c388ab00750a9 100644 --- a/extensions/fsharp/syntaxes/fsharp.tmLanguage.json +++ b/extensions/fsharp/syntaxes/fsharp.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/ionide/ionide-fsgrammar/commit/fc4cac6d9bc1787f54ce48bbc77bcbb1de8160ff", + "version": "https://github.com/ionide/ionide-fsgrammar/commit/bba27391e61090035449b5c1e5c4b9d396bc4c9b", "name": "fsharp", "scopeName": "source.fsharp", "patterns": [ @@ -527,8 +527,8 @@ "patterns": [ { "name": "comment.block.markdown.fsharp", - "begin": "^\\s*(\\(\\*\\*(?!\\)))(?!\\*\\))$", - "while": "^(?!\\s*\\*\\)$)", + "begin": "^\\s*(\\(\\*\\*(?!\\)))((?!\\*\\)).)*$", + "while": "^(?!\\s*(\\*)+\\)$)", "beginCaptures": { "1": { "name": "comment.block.fsharp" @@ -572,7 +572,7 @@ }, { "name": "comment.block.markdown.fsharp.end", - "match": "(\\*\\))", + "match": "((?('vscode.git-base').exports; + const git = gitBaseExtension.getAPI(1); + ``` diff --git a/extensions/git-base/cgmanifest.json b/extensions/git-base/cgmanifest.json index 256966aba2005..a3786f7edce6a 100644 --- a/extensions/git-base/cgmanifest.json +++ b/extensions/git-base/cgmanifest.json @@ -36,4 +36,4 @@ } ], "version": 1 -} +} \ No newline at end of file diff --git a/extensions/git/package.json b/extensions/git/package.json index b9284c78b8c7e..ab6a045ed97d4 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -38,7 +38,7 @@ "capabilities": { "virtualWorkspaces": true, "untrustedWorkspaces": { - "supported": true + "supported": false } }, "contributes": { @@ -84,8 +84,7 @@ "command": "git.openChange", "title": "%command.openChange%", "category": "Git", - "icon": "$(compare-changes)", - "enablement": "scmActiveResourceHasChanges" + "icon": "$(compare-changes)" }, { "command": "git.openAllChanges", @@ -495,6 +494,11 @@ "title": "%command.stashDrop%", "category": "Git" }, + { + "command": "git.stashDropAll", + "title": "%command.stashDropAll%", + "category": "Git" + }, { "command": "git.timeline.openDiff", "title": "%command.timelineOpenDiff%", @@ -594,11 +598,11 @@ }, { "command": "git.openFile", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && resourceScheme == file && scmActiveResourceHasChanges" }, { "command": "git.openHEADFile", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && resourceScheme == file && scmActiveResourceHasChanges" }, { "command": "git.openChange", @@ -674,7 +678,7 @@ }, { "command": "git.rename", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && resourceScheme == file" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && resourceScheme == file && scmActiveResourceRepository" }, { "command": "git.commit", @@ -874,7 +878,7 @@ }, { "command": "git.ignore", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && resourceScheme == file && scmActiveResourceRepository" }, { "command": "git.stashIncludeUntracked", @@ -904,6 +908,10 @@ "command": "git.stashDrop", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, + { + "command": "git.stashDropAll", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" + }, { "command": "git.timeline.openDiff", "when": "false" @@ -1361,7 +1369,7 @@ { "command": "git.openChange", "group": "navigation", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && !isInDiffEditor && resourceScheme == file" + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && !isInDiffEditor && resourceScheme == file && scmActiveResourceHasChanges" }, { "command": "git.stageSelectedRanges", @@ -1641,6 +1649,10 @@ { "command": "git.stashDrop", "group": "stash@7" + }, + { + "command": "git.stashDropAll", + "group": "stash@8" } ], "git.tags": [ @@ -2205,6 +2217,34 @@ "scope": "resource", "default": 10000, "description": "%config.statusLimit%" + }, + "git.experimental.installGuide": { + "type": "string", + "enum": [ + "default", + "download" + ], + "tags": ["experimental"], + "scope": "machine", + "description": "%config.experimental.installGuide%", + "default": "default" + }, + "git.repositoryScanIgnoredFolders": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "node_modules" + ], + "scope": "resource", + "markdownDescription": "%config.repositoryScanIgnoredFolders%" + }, + "git.repositoryScanMaxDepth": { + "type": "number", + "scope": "resource", + "default": 1, + "markdownDescription": "%config.repositoryScanMaxDepth%" } } }, @@ -2317,10 +2357,30 @@ "contents": "%view.workbench.scm.disabled%", "when": "!config.git.enabled" }, + { + "view": "scm", + "contents": "%view.workbench.scm.missing.guide%", + "when": "config.git.enabled && git.missing && config.git.experimental.installGuide == download" + }, + { + "view": "scm", + "contents": "%view.workbench.scm.missing.guide.mac%", + "when": "config.git.enabled && git.missing && config.git.experimental.installGuide == download && isMac" + }, + { + "view": "scm", + "contents": "%view.workbench.scm.missing.guide.windows%", + "when": "config.git.enabled && git.missing && config.git.experimental.installGuide == download && isWindows" + }, + { + "view": "scm", + "contents": "%view.workbench.scm.missing.guide.windows%", + "when": "config.git.enabled && git.missing && config.git.experimental.installGuide == download && isLinux" + }, { "view": "scm", "contents": "%view.workbench.scm.missing%", - "when": "config.git.enabled && git.missing" + "when": "config.git.enabled && git.missing && config.git.experimental.installGuide == default" }, { "view": "scm", @@ -2367,11 +2427,11 @@ ] }, "dependencies": { + "@vscode/iconv-lite-umd": "0.7.0", "byline": "^5.0.0", "file-type": "^7.2.0", - "iconv-lite-umd": "0.6.10", "jschardet": "3.0.0", - "vscode-extension-telemetry": "0.4.3", + "@vscode/extension-telemetry": "0.4.6", "vscode-nls": "^4.0.0", "vscode-uri": "^2.0.0", "which": "^1.3.0" diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 7b3af1f5ca9c2..7221538835089 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -87,6 +87,7 @@ "command.stashApply": "Apply Stash...", "command.stashApplyLatest": "Apply Latest Stash", "command.stashDrop": "Drop Stash...", + "command.stashDropAll": "Drop All Stashes...", "command.timelineOpenDiff": "Open Changes", "command.timelineCopyCommitId": "Copy Commit ID", "command.timelineCopyCommitMessage": "Copy Commit Message", @@ -193,6 +194,9 @@ "config.showUnpublishedCommitsButton.whenEmpty": "Only shows the action button if there are no other changes and there are unpublished commits.", "config.showUnpublishedCommitsButton.never": "Never shows the action button.", "config.statusLimit": "Controls how to limit the number of changes that can be parsed from Git status command. Can be set to 0 for no limit.", + "config.experimental.installGuide": "Experimental improvements for the git setup flow.", + "config.repositoryScanIgnoredFolders": "List of folders that are ignored while scanning for Git repositories when `#git.autoRepositoryDetection#` is set to `true` or `subFolders`.", + "config.repositoryScanMaxDepth": "Controls the depth used when scanning workspace folders for Git repositories when `#git.autoRepositoryDetection#` is set to `true` or `subFolders`. Can be set to `-1` for no limit.", "submenu.explorer": "Git", "submenu.commit": "Commit", "submenu.commit.amend": "Amend", @@ -214,6 +218,10 @@ "colors.conflict": "Color for resources with conflicts.", "colors.submodule": "Color for submodule resources.", "view.workbench.scm.missing": "A valid git installation was not detected, more details can be found in the [git output](command:git.showOutput).\nPlease [install git](https://git-scm.com/), or learn more about how to use git and source control in VS Code in [our docs](https://aka.ms/vscode-scm).\nIf you're using a different version control system, you can [search the Marketplace](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22) for additional extensions.", + "view.workbench.scm.missing.guide.windows": "[Download Git for Windows](https://git-scm.com/download/win)\nAfter installing, please [reload](command:workbench.action.reloadWindow) (or [troubleshoot](command:git.showOutput)). Additional source control providers can be installed [from the Marketplace](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22).", + "view.workbench.scm.missing.guide.mac": "[Download Git for macOS](https://git-scm.com/download/mac)\nAfter installing, please [reload](command:workbench.action.reloadWindow) (or [troubleshoot](command:git.showOutput)). Additional source control providers can be installed [from the Marketplace](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22).", + "view.workbench.scm.missing.guide.linux": "Source control depends on Git being installed.\n[Download Git for Linux](https://git-scm.com/download/mac)\nAfter installing, please [reload](command:workbench.action.reloadWindow) (or [troubleshoot](command:git.showOutput)). Additional source control providers can be installed [from the Marketplace](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22).", + "view.workbench.scm.missing.guide": "Install Git, a popular source control system, to track code changes and collaborate with others. Learn more in our [Git guides](https://aka.ms/vscode-scm).", "view.workbench.scm.disabled": "If you would like to use git features, please enable git in your [settings](command:workbench.action.openSettings?%5B%22git.enabled%22%5D).\nTo learn more about how to use git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", "view.workbench.scm.empty": "In order to use git features, you can open a folder containing a git repository or clone from a URL.\n[Open Folder](command:vscode.openFolder)\n[Clone Repository](command:git.clone)\nTo learn more about how to use git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", "view.workbench.scm.folder": "The folder currently open doesn't have a git repository. You can initialize a repository which will enable source control features powered by git.\n[Initialize Repository](command:git.init?%5Btrue%5D)\nTo learn more about how to use git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index 2117bec4ded84..f6ae052e177ba 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -104,6 +104,10 @@ export class ApiRepository implements Repository { return this._repository.getCommit(ref); } + add(paths: string[]) { + return this._repository.add(paths.map(p => Uri.file(p))); + } + clean(paths: string[]) { return this._repository.clean(paths.map(p => Uri.file(p))); } @@ -174,6 +178,14 @@ export class ApiRepository implements Repository { return this._repository.getMergeBase(ref1, ref2); } + tag(name: string, upstream: string): Promise { + return this._repository.tag(name, upstream); + } + + deleteTag(name: string): Promise { + return this._repository.deleteTag(name); + } + status(): Promise { return this._repository.status(); } diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 6ab8a38e64c54..c105367696924 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -172,6 +172,7 @@ export interface Repository { show(ref: string, path: string): Promise; getCommit(ref: string): Promise; + add(paths: string[]): Promise; clean(paths: string[]): Promise; apply(patch: string, reverse?: boolean): Promise; @@ -198,6 +199,9 @@ export interface Repository { getMergeBase(ref1: string, ref2: string): Promise; + tag(name: string, upstream: string): Promise; + deleteTag(name: string): Promise; + status(): Promise; checkout(treeish: string): Promise; diff --git a/extensions/git/src/askpass.ts b/extensions/git/src/askpass.ts index e6c21efa9cbe5..f4ba1e84573ce 100644 --- a/extensions/git/src/askpass.ts +++ b/extensions/git/src/askpass.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { window, InputBoxOptions, Uri, OutputChannel, Disposable, workspace } from 'vscode'; -import { IDisposable, EmptyDisposable, toDisposable } from './util'; +import { IDisposable, EmptyDisposable, toDisposable, logTimestamp } from './util'; import * as path from 'path'; import { IIPCHandler, IIPCServer, createIPCServer } from './ipc/ipcServer'; import { CredentialsProvider, Credentials } from './api/git'; @@ -19,7 +19,7 @@ export class Askpass implements IIPCHandler { try { return new Askpass(await createIPCServer(context)); } catch (err) { - outputChannel.appendLine(`[error] Failed to create git askpass IPC: ${err}`); + outputChannel.appendLine(`${logTimestamp()} [error] Failed to create git askpass IPC: ${err}`); return new Askpass(); } } diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 1b6f4aaea4355..913627c892257 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -6,7 +6,7 @@ import * as os from 'os'; import * as path from 'path'; import { Command, commands, Disposable, LineChange, MessageOptions, OutputChannel, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider } from 'vscode'; -import TelemetryReporter from 'vscode-extension-telemetry'; +import TelemetryReporter from '@vscode/extension-telemetry'; import * as nls from 'vscode-nls'; import { Branch, ForcePushMode, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourcePublisher } from './api/git'; import { Git, Stash } from './git'; @@ -14,7 +14,7 @@ import { Model } from './model'; import { Repository, Resource, ResourceGroupType } from './repository'; import { applyLineChanges, getModifiedRange, intersectDiffWithRange, invertLineChange, toLineRanges } from './staging'; import { fromGitUri, toGitUri, isGitUri } from './uri'; -import { grep, isDescendant, pathEquals } from './util'; +import { grep, isDescendant, logTimestamp, pathEquals, relativePath } from './util'; import { Log, LogLevel } from './log'; import { GitTimelineItem } from './timelineProvider'; import { ApiRepository } from './api/api1'; @@ -353,7 +353,7 @@ export class CommandCenter { } Log.logLevel = choice.logLevel; - this.outputChannel.appendLine(localize('changed', "Log level changed to: {0}", LogLevel[Log.logLevel])); + this.outputChannel.appendLine(localize('changed', "{0} Log level changed to: {1}", logTimestamp(), LogLevel[Log.logLevel])); } @command('git.refresh', { repository: true }) @@ -686,6 +686,10 @@ export class CommandCenter { } const activeTextEditor = window.activeTextEditor; + // Must extract these now because opening a new document will change the activeTextEditor reference + const previousVisibleRange = activeTextEditor?.visibleRanges[0]; + const previousURI = activeTextEditor?.document.uri; + const previousSelection = activeTextEditor?.selection; for (const uri of uris) { const opts: TextDocumentShowOptions = { @@ -702,18 +706,21 @@ export class CommandCenter { const document = window.activeTextEditor?.document; // If the document doesn't match what we opened then don't attempt to select the range - if (document?.uri.toString() !== uri.toString()) { + // Additioanlly if there was no previous document we don't have information to select a range + if (document?.uri.toString() !== uri.toString() || !activeTextEditor || !previousURI || !previousSelection) { continue; } // Check if active text editor has same path as other editor. we cannot compare via // URI.toString() here because the schemas can be different. Instead we just go by path. - if (activeTextEditor && activeTextEditor.document.uri.path === uri.path && document) { + if (previousURI.path === uri.path && document) { // preserve not only selection but also visible range - opts.selection = activeTextEditor.selection; - const previousVisibleRanges = activeTextEditor.visibleRanges; + opts.selection = previousSelection; const editor = await window.showTextDocument(document, opts); - editor.revealRange(previousVisibleRanges[0]); + // This should always be defined but just in case + if (previousVisibleRange) { + editor.revealRange(previousVisibleRange); + } } } } @@ -796,7 +803,7 @@ export class CommandCenter { return; } - const from = path.relative(repository.root, fromUri.fsPath); + const from = relativePath(repository.root, fromUri.fsPath); let to = await window.showInputBox({ value: from, valueSelection: [from.length - path.basename(from).length, from.length] @@ -813,14 +820,14 @@ export class CommandCenter { @command('git.stage') async stage(...resourceStates: SourceControlResourceState[]): Promise { - this.outputChannel.appendLine(`git.stage ${resourceStates.length}`); + this.outputChannel.appendLine(`${logTimestamp()} git.stage ${resourceStates.length}`); resourceStates = resourceStates.filter(s => !!s); if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) { const resource = this.getSCMResource(); - this.outputChannel.appendLine(`git.stage.getSCMResource ${resource ? resource.resourceUri.toString() : null}`); + this.outputChannel.appendLine(`${logTimestamp()} git.stage.getSCMResource ${resource ? resource.resourceUri.toString() : null}`); if (!resource) { return; @@ -863,7 +870,7 @@ export class CommandCenter { const untracked = selection.filter(s => s.resourceGroupType === ResourceGroupType.Untracked); const scmResources = [...workingTree, ...untracked, ...resolved, ...unresolved]; - this.outputChannel.appendLine(`git.stage.scmResources ${scmResources.length}`); + this.outputChannel.appendLine(`${logTimestamp()} git.stage.scmResources ${scmResources.length}`); if (!scmResources.length) { return; } @@ -2128,7 +2135,7 @@ export class CommandCenter { } const branchName = repository.HEAD.name; - const message = localize('confirm publish branch', "The branch '{0}' has no upstream branch. Would you like to publish this branch?", branchName); + const message = localize('confirm publish branch', "The branch '{0}' has no remote branch. Would you like to publish this branch?", branchName); const yes = localize('ok', "OK"); const pick = await window.showWarningMessage(message, { modal: true }, yes); @@ -2278,7 +2285,7 @@ export class CommandCenter { return; } else if (!HEAD.upstream) { const branchName = HEAD.name; - const message = localize('confirm publish branch', "The branch '{0}' has no upstream branch. Would you like to publish this branch?", branchName); + const message = localize('confirm publish branch', "The branch '{0}' has no remote branch. Would you like to publish this branch?", branchName); const yes = localize('ok', "OK"); const pick = await window.showWarningMessage(message, { modal: true }, yes); @@ -2596,6 +2603,29 @@ export class CommandCenter { await repository.dropStash(stash.index); } + @command('git.stashDropAll', { repository: true }) + async stashDropAll(repository: Repository): Promise { + const stashes = await repository.getStashes(); + + if (stashes.length === 0) { + window.showInformationMessage(localize('no stashes', "There are no stashes in the repository.")); + return; + } + + // request confirmation for the operation + const yes = localize('yes', "Yes"); + const question = stashes.length === 1 ? + localize('drop one stash', "Are you sure you want to drop ALL stashes? There is 1 stash that will be subject to pruning, and MAY BE IMPOSSIBLE TO RECOVER.") : + localize('drop all stashes', "Are you sure you want to drop ALL stashes? There are {0} stashes that will be subject to pruning, and MAY BE IMPOSSIBLE TO RECOVER.", stashes.length); + + const result = await window.showWarningMessage(question, yes); + if (result !== yes) { + return; + } + + await repository.dropStash(); + } + private async pickStash(repository: Repository, placeHolder: string): Promise { const stashes = await repository.getStashes(); @@ -2813,7 +2843,7 @@ export class CommandCenter { type = 'warning'; options.modal = false; break; - case GitErrorCodes.AuthenticationFailed: + case GitErrorCodes.AuthenticationFailed: { const regex = /Authentication failed for '(.*)'/i; const match = regex.exec(err.stderr || String(err)); @@ -2821,12 +2851,13 @@ export class CommandCenter { ? localize('auth failed specific', "Failed to authenticate to git remote:\n\n{0}", match[1]) : localize('auth failed', "Failed to authenticate to git remote."); break; + } case GitErrorCodes.NoUserNameConfigured: case GitErrorCodes.NoUserEmailConfigured: message = localize('missing user info', "Make sure you configure your 'user.name' and 'user.email' in git."); - choices.set(localize('learn more', "Learn More"), () => commands.executeCommand('vscode.open', Uri.parse('https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup'))); + choices.set(localize('learn more', "Learn More"), () => commands.executeCommand('vscode.open', Uri.parse('https://aka.ms/vscode-setup-git'))); break; - default: + default: { const hint = (err.stderr || err.message || String(err)) .replace(/^error: /mi, '') .replace(/^> husky.*$/mi, '') @@ -2839,6 +2870,7 @@ export class CommandCenter { : localize('git error', "Git error"); break; + } } if (!message) { @@ -2870,10 +2902,10 @@ export class CommandCenter { private getSCMResource(uri?: Uri): Resource | undefined { uri = uri ? uri : (window.activeTextEditor && window.activeTextEditor.document.uri); - this.outputChannel.appendLine(`git.getSCMResource.uri ${uri && uri.toString()}`); + this.outputChannel.appendLine(`${logTimestamp()} git.getSCMResource.uri ${uri && uri.toString()}`); for (const r of this.model.repositories.map(r => r.root)) { - this.outputChannel.appendLine(`repo root ${r}`); + this.outputChannel.appendLine(`${logTimestamp()} repo root ${r}`); } if (!uri) { diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 7a73b0aeb6e72..76ded13313dc1 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -9,9 +9,9 @@ import * as os from 'os'; import * as cp from 'child_process'; import * as which from 'which'; import { EventEmitter } from 'events'; -import * as iconv from 'iconv-lite-umd'; +import * as iconv from '@vscode/iconv-lite-umd'; import * as filetype from 'file-type'; -import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter, Versions } from './util'; +import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter, Versions, isWindows } from './util'; import { CancellationToken, Progress, Uri } from 'vscode'; import { detectEncoding } from './encoding'; import { Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, BranchQuery } from './api/git'; @@ -20,7 +20,6 @@ import { StringDecoder } from 'string_decoder'; // https://github.com/microsoft/vscode/issues/65693 const MAX_CLI_LENGTH = 30000; -const isWindows = process.platform === 'win32'; export interface IGit { path: string; @@ -84,7 +83,7 @@ function findGitDarwin(onValidate: (path: string) => boolean): Promise { return e('git not found'); } - const path = gitPathBuffer.toString().replace(/^\s+|\s+$/g, ''); + const path = gitPathBuffer.toString().trim(); function getVersion(path: string) { if (!onValidate(path)) { @@ -531,10 +530,15 @@ export class Git { child.stdin!.end(options.input, 'utf8'); } + const startTime = Date.now(); const bufferResult = await exec(child, options.cancellationToken); - if (options.log !== false && bufferResult.stderr.length > 0) { - this.log(`${bufferResult.stderr}\n`); + if (options.log !== false) { + this.log(`> git ${args.join(' ')} [${Date.now() - startTime}ms]\n`); + + if (bufferResult.stderr.length > 0) { + this.log(`${bufferResult.stderr}\n`); + } } let encoding = options.encoding || 'utf8'; @@ -585,10 +589,6 @@ export class Git { options.cwd = sanitizePath(options.cwd); } - if (options.log !== false) { - this.log(`> git ${args.join(' ')}\n`); - } - return cp.spawn(this.path, args, options); } @@ -1195,7 +1195,7 @@ export class Repository { break; // Rename contains two paths, the second one is what the file is renamed/copied to. - case 'R': + case 'R': { if (index >= entries.length) { break; } @@ -1214,7 +1214,7 @@ export class Repository { }); continue; - + } default: // Unknown status break entriesLoop; @@ -1793,10 +1793,13 @@ export class Repository { } async dropStash(index?: number): Promise { - const args = ['stash', 'drop']; + const args = ['stash']; if (typeof index === 'number') { + args.push('drop'); args.push(`stash@{${index}}`); + } else { + args.push('clear'); } try { @@ -1810,8 +1813,8 @@ export class Repository { } } - getStatus(opts?: { limit?: number, ignoreSubmodules?: boolean }): Promise<{ status: IFileStatus[]; didHitLimit: boolean; }> { - return new Promise<{ status: IFileStatus[]; didHitLimit: boolean; }>((c, e) => { + getStatus(opts?: { limit?: number, ignoreSubmodules?: boolean }): Promise<{ status: IFileStatus[]; statusLength: number; didHitLimit: boolean; }> { + return new Promise<{ status: IFileStatus[]; statusLength: number; didHitLimit: boolean; }>((c, e) => { const parser = new GitStatusParser(); const env = { GIT_OPTIONAL_LOCKS: '0' }; const args = ['status', '-z', '-u']; @@ -1835,10 +1838,10 @@ export class Repository { })); } - c({ status: parser.status, didHitLimit: false }); + c({ status: parser.status, statusLength: parser.status.length, didHitLimit: false }); }; - const limit = opts?.limit ?? 5000; + const limit = opts?.limit ?? 10000; const onStdoutData = (raw: string) => { parser.update(raw); @@ -1847,7 +1850,7 @@ export class Repository { child.stdout!.removeListener('data', onStdoutData); child.kill(); - c({ status: parser.status.slice(0, limit), didHitLimit: true }); + c({ status: parser.status.slice(0, limit), statusLength: parser.status.length, didHitLimit: true }); } }; diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index 0aa3193e39475..f31a64d9c33d9 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -13,8 +13,8 @@ import { CommandCenter } from './commands'; import { GitFileSystemProvider } from './fileSystemProvider'; import { GitDecorations } from './decorationProvider'; import { Askpass } from './askpass'; -import { toDisposable, filterEvent, eventToPromise } from './util'; -import TelemetryReporter from 'vscode-extension-telemetry'; +import { toDisposable, filterEvent, eventToPromise, logTimestamp } from './util'; +import TelemetryReporter from '@vscode/extension-telemetry'; import { GitExtension } from './api/git'; import { GitProtocolHandler } from './protocolHandler'; import { GitExtensionImpl } from './api/extension'; @@ -46,7 +46,7 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann } const info = await findGit(pathHints, gitPath => { - outputChannel.appendLine(localize('validating', "Validating found git in: {0}", gitPath)); + outputChannel.appendLine(localize('validating', "{0} Validating found git in: {1}", logTimestamp(), gitPath)); if (excludes.length === 0) { return true; } @@ -54,7 +54,7 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann const normalized = path.normalize(gitPath).replace(/[\r\n]+$/, ''); const skip = excludes.some(e => normalized.startsWith(e)); if (skip) { - outputChannel.appendLine(localize('skipped', "Skipped found git in: {0}", gitPath)); + outputChannel.appendLine(localize('skipped', "{0} Skipped found git in: {1}", logTimestamp(), gitPath)); } return !skip; }); @@ -73,7 +73,7 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann version: info.version, env: environment, }); - const model = new Model(git, askpass, context.globalState, outputChannel); + const model = new Model(git, askpass, context.globalState, outputChannel, telemetryReporter); disposables.push(model); const onRepository = () => commands.executeCommand('setContext', 'gitOpenRepositoryCount', `${model.repositories.length}`); @@ -81,7 +81,7 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann model.onDidCloseRepository(onRepository, null, disposables); onRepository(); - outputChannel.appendLine(localize('using git', "Using git {0} from {1}", info.version, info.path)); + outputChannel.appendLine(localize('using git', "{0} Using git {1} from {2}", logTimestamp(), info.version, info.path)); const onOutput = (str: string) => { const lines = str.split(/\r?\n/mg); @@ -90,7 +90,7 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann lines.pop(); } - outputChannel.appendLine(lines.join('\n')); + outputChannel.appendLine(`${logTimestamp()} ${lines.join('\n')}`); }; git.onOutput.addListener('log', onOutput); disposables.push(toDisposable(() => git.onOutput.removeListener('log', onOutput))); @@ -151,7 +151,7 @@ async function warnAboutMissingGit(): Promise { ); if (choice === download) { - commands.executeCommand('vscode.open', Uri.parse('https://git-scm.com/')); + commands.executeCommand('vscode.open', Uri.parse('https://aka.ms/vscode-download-git')); } else if (choice === neverShowAgain) { await config.update('ignoreMissingGitWarning', true, true); } @@ -190,7 +190,12 @@ export async function _activate(context: ExtensionContext): Promise { ); if (choice === update) { - commands.executeCommand('vscode.open', Uri.parse('https://git-scm.com/')); + commands.executeCommand('vscode.open', Uri.parse('https://aka.ms/vscode-download-git')); } else if (choice === neverShowAgain) { await config.update('ignoreLegacyWarning', true, true); } @@ -261,7 +266,7 @@ async function checkGitWindows(info: IGit): Promise { ); if (choice === update) { - commands.executeCommand('vscode.open', Uri.parse('https://git-scm.com/')); + commands.executeCommand('vscode.open', Uri.parse('https://aka.ms/vscode-download-git')); } else if (choice === neverShowAgain) { await config.update('ignoreWindowsGit27Warning', true, true); } diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index 9cf79aadf8248..a6e835043a866 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -4,9 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, OutputChannel, commands } from 'vscode'; +import TelemetryReporter from '@vscode/extension-telemetry'; import { Repository, RepositoryState } from './repository'; import { memoize, sequentialize, debounce } from './decorators'; -import { dispose, anyEvent, filterEvent, isDescendant, pathEquals, toDisposable, eventToPromise } from './util'; +import { dispose, anyEvent, filterEvent, isDescendant, pathEquals, toDisposable, eventToPromise, logTimestamp } from './util'; import { Git } from './git'; import * as path from 'path'; import * as fs from 'fs'; @@ -17,6 +18,7 @@ import { Askpass } from './askpass'; import { IPushErrorHandlerRegistry } from './pushError'; import { ApiRepository } from './api/api1'; import { IRemoteSourcePublisherRegistry } from './remotePublisher'; +import { Log, LogLevel } from './log'; const localize = nls.loadMessageBundle(); @@ -107,7 +109,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR private disposables: Disposable[] = []; - constructor(readonly git: Git, private readonly askpass: Askpass, private globalState: Memento, private outputChannel: OutputChannel) { + constructor(readonly git: Git, private readonly askpass: Askpass, private globalState: Memento, private outputChannel: OutputChannel, private telemetryReporter: TelemetryReporter) { workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders, this, this.disposables); window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables); workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables); @@ -133,25 +135,36 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR } /** - * Scans the first level of each workspace folder, looking - * for git repositories. + * Scans each workspace folder, looking for git repositories. By + * default it scans one level deep but that can be changed using + * the git.repositoryScanMaxDepth setting. */ private async scanWorkspaceFolders(): Promise { const config = workspace.getConfiguration('git'); const autoRepositoryDetection = config.get('autoRepositoryDetection'); + // Log repository scan settings + if (Log.logLevel <= LogLevel.Trace) { + this.outputChannel.appendLine(`${logTimestamp()} Trace: autoRepositoryDetection="${autoRepositoryDetection}"`); + } + if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'subFolders') { return; } await Promise.all((workspace.workspaceFolders || []).map(async folder => { const root = folder.uri.fsPath; - const children = await new Promise((c, e) => fs.readdir(root, (err, r) => err ? e(err) : c(r))); - const subfolders = new Set(children.filter(child => child !== '.git').map(child => path.join(root, child))); + // Workspace folder children + const repositoryScanMaxDepth = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get('repositoryScanMaxDepth', 1); + const repositoryScanIgnoredFolders = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get('repositoryScanIgnoredFolders', []); + + const subfolders = new Set(await this.traverseWorkspaceFolder(root, repositoryScanMaxDepth, repositoryScanIgnoredFolders)); + + // Repository scan folders const scanPaths = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get('scanRepositories') || []; for (const scanPath of scanPaths) { - if (scanPath !== '.git') { + if (scanPath === '.git') { continue; } @@ -167,6 +180,31 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR })); } + private async traverseWorkspaceFolder(workspaceFolder: string, maxDepth: number, repositoryScanIgnoredFolders: string[]): Promise { + const result: string[] = []; + const foldersToTravers = [{ path: workspaceFolder, depth: 0 }]; + + while (foldersToTravers.length > 0) { + const currentFolder = foldersToTravers.shift()!; + + if (currentFolder.depth < maxDepth || maxDepth === -1) { + const children = await fs.promises.readdir(currentFolder.path, { withFileTypes: true }); + const childrenFolders = children + .filter(dirent => + dirent.isDirectory() && dirent.name !== '.git' && + !repositoryScanIgnoredFolders.find(f => pathEquals(dirent.name, f))) + .map(dirent => path.join(currentFolder.path, dirent.name)); + + result.push(...childrenFolders); + foldersToTravers.push(...childrenFolders.map(folder => { + return { path: folder, depth: currentFolder.depth + 1 }; + })); + } + } + + return result; + } + private onPossibleGitRepositoryChange(uri: Uri): void { const config = workspace.getConfiguration('git'); const autoRepositoryDetection = config.get('autoRepositoryDetection'); @@ -297,13 +335,15 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR } const dotGit = await this.git.getRepositoryDotGit(repositoryRoot); - const repository = new Repository(this.git.open(repositoryRoot, dotGit), this, this, this.globalState, this.outputChannel); + const repository = new Repository(this.git.open(repositoryRoot, dotGit), this, this, this.globalState, this.outputChannel, this.telemetryReporter); this.open(repository); repository.status(); // do not await this, we want SCM to know about the repo asap } catch (ex) { // noop - this.outputChannel.appendLine(`Opening repository for path='${repoPath}' failed; ex=${ex}`); + if (Log.logLevel <= LogLevel.Trace) { + this.outputChannel.appendLine(`${logTimestamp()} Trace: Opening repository for path='${repoPath}' failed; ex=${ex}`); + } } } @@ -329,7 +369,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR } private open(repository: Repository): void { - this.outputChannel.appendLine(`Open repository: ${repository.root}`); + this.outputChannel.appendLine(`${logTimestamp()} Open repository: ${repository.root}`); const onDidDisappearRepository = filterEvent(repository.onDidChangeState, state => state === RepositoryState.Disposed); const disappearListener = onDidDisappearRepository(() => dispose()); @@ -386,7 +426,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR return; } - this.outputChannel.appendLine(`Close repository: ${repository.root}`); + this.outputChannel.appendLine(`${logTimestamp()} Close repository: ${repository.root}`); openRepository.dispose(); } diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 27d730b1165d8..6b2205ddcb724 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -5,7 +5,8 @@ import * as fs from 'fs'; import * as path from 'path'; -import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, FileDecoration, commands } from 'vscode'; +import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, FileDecoration, commands, SourceControlActionButton } from 'vscode'; +import TelemetryReporter from '@vscode/extension-telemetry'; import * as nls from 'vscode-nls'; import { Branch, Change, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status, CommitOptions, BranchQuery, FetchOptions } from './api/git'; import { AutoFetcher } from './autofetch'; @@ -13,7 +14,7 @@ import { debounce, memoize, throttle } from './decorators'; import { Commit, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions } from './git'; import { StatusBarCommands } from './statusbar'; import { toGitUri } from './uri'; -import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, onceEvent } from './util'; +import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, logTimestamp, onceEvent, relativePath } from './util'; import { IFileWatcher, watch } from './watch'; import { Log, LogLevel } from './log'; import { IPushErrorHandlerRegistry } from './pushError'; @@ -516,8 +517,8 @@ class FileEventLogger { } this.eventDisposable = combinedDisposable([ - this.onWorkspaceWorkingTreeFileChange(uri => this.outputChannel.appendLine(`[debug] [wt] Change: ${uri.fsPath}`)), - this.onDotGitFileChange(uri => this.outputChannel.appendLine(`[debug] [.git] Change: ${uri.fsPath}`)) + this.onWorkspaceWorkingTreeFileChange(uri => this.outputChannel.appendLine(`${logTimestamp()} [debug] [wt] Change: ${uri.fsPath}`)), + this.onDotGitFileChange(uri => this.outputChannel.appendLine(`${logTimestamp()} [debug] [.git] Change: ${uri.fsPath}`)) ]); } @@ -567,7 +568,7 @@ class DotGitWatcher implements IFileWatcher { upstreamWatcher.event(this.emitter.fire, this.emitter, this.transientDisposables); } catch (err) { if (Log.logLevel <= LogLevel.Error) { - this.outputChannel.appendLine(`Warning: Failed to watch ref '${upstreamPath}', is most likely packed.`); + this.outputChannel.appendLine(`${logTimestamp()} Warning: Failed to watch ref '${upstreamPath}', is most likely packed.`); } } } @@ -664,7 +665,7 @@ class ResourceCommandResolver { case Status.MODIFIED: case Status.UNTRACKED: case Status.IGNORED: - case Status.INTENT_TO_ADD: + case Status.INTENT_TO_ADD: { const uriString = resource.resourceUri.toString(); const [indexStatus] = this.repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString); @@ -673,7 +674,7 @@ class ResourceCommandResolver { } return resource.resourceUri; - + } case Status.BOTH_ADDED: case Status.BOTH_MODIFIED: return resource.resourceUri; @@ -853,7 +854,8 @@ export class Repository implements Disposable { private pushErrorHandlerRegistry: IPushErrorHandlerRegistry, remoteSourcePublisherRegistry: IRemoteSourcePublisherRegistry, globalState: Memento, - outputChannel: OutputChannel + outputChannel: OutputChannel, + private telemetryReporter: TelemetryReporter ) { const workspaceWatcher = workspace.createFileSystemWatcher('**'); this.disposables.push(workspaceWatcher); @@ -870,7 +872,7 @@ export class Repository implements Disposable { this.disposables.push(dotGitFileWatcher); } catch (err) { if (Log.logLevel <= LogLevel.Error) { - outputChannel.appendLine(`Failed to watch '${this.dotGit}', reverting to legacy API file watched. Some events might be lost.\n${err.stack || err}`); + outputChannel.appendLine(`${logTimestamp()} Failed to watch '${this.dotGit}', reverting to legacy API file watched. Some events might be lost.\n${err.stack || err}`); } onDotGitFileChange = filterEvent(onWorkspaceRepositoryFileChange, uri => /\/\.git($|\/)/.test(uri.path)); @@ -1159,8 +1161,8 @@ export class Repository implements Disposable { } async stage(resource: Uri, contents: string): Promise { - const relativePath = path.relative(this.repository.root, resource.fsPath).replace(/\\/g, '/'); - await this.run(Operation.Stage, () => this.repository.stage(relativePath, contents)); + const path = relativePath(this.repository.root, resource.fsPath).replace(/\\/g, '/'); + await this.run(Operation.Stage, () => this.repository.stage(path, contents)); this._onDidChangeOriginalResource.fire(resource); } @@ -1543,16 +1545,16 @@ export class Repository implements Disposable { async show(ref: string, filePath: string): Promise { return await this.run(Operation.Show, async () => { - const relativePath = path.relative(this.repository.root, filePath).replace(/\\/g, '/'); + const path = relativePath(this.repository.root, filePath).replace(/\\/g, '/'); const configFiles = workspace.getConfiguration('files', Uri.file(filePath)); const defaultEncoding = configFiles.get('encoding'); const autoGuessEncoding = configFiles.get('autoGuessEncoding'); try { - return await this.repository.bufferString(`${ref}:${relativePath}`, defaultEncoding, autoGuessEncoding); + return await this.repository.bufferString(`${ref}:${path}`, defaultEncoding, autoGuessEncoding); } catch (err) { if (err.gitErrorCode === GitErrorCodes.WrongCase) { - const gitRelativePath = await this.repository.getGitRelativePath(ref, relativePath); + const gitRelativePath = await this.repository.getGitRelativePath(ref, path); return await this.repository.bufferString(`${ref}:${gitRelativePath}`, defaultEncoding, autoGuessEncoding); } @@ -1563,8 +1565,8 @@ export class Repository implements Disposable { async buffer(ref: string, filePath: string): Promise { return this.run(Operation.Show, () => { - const relativePath = path.relative(this.repository.root, filePath).replace(/\\/g, '/'); - return this.repository.buffer(`${ref}:${relativePath}`); + const path = relativePath(this.repository.root, filePath).replace(/\\/g, '/'); + return this.repository.buffer(`${ref}:${path}`); }); } @@ -1608,7 +1610,7 @@ export class Repository implements Disposable { return await this.run(Operation.Ignore, async () => { const ignoreFile = `${this.repository.root}${path.sep}.gitignore`; const textToAppend = files - .map(uri => path.relative(this.repository.root, uri.fsPath).replace(/\\/g, '/')) + .map(uri => relativePath(this.repository.root, uri.fsPath).replace(/\\/g, '/')) .join('\n'); const document = await new Promise(c => fs.exists(ignoreFile, c)) @@ -1800,9 +1802,20 @@ export class Repository implements Disposable { const scopedConfig = workspace.getConfiguration('git', Uri.file(this.repository.root)); const ignoreSubmodules = scopedConfig.get('ignoreSubmodules'); - const limit = scopedConfig.get('statusLimit', 5000); + const limit = scopedConfig.get('statusLimit', 10000); + + const { status, statusLength, didHitLimit } = await this.repository.getStatus({ limit, ignoreSubmodules }); - const { status, didHitLimit } = await this.repository.getStatus({ limit, ignoreSubmodules }); + if (didHitLimit) { + /* __GDPR__ + "statusLimit" : { + "ignoreSubmodules": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "limit": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "statusLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } + } + */ + this.telemetryReporter.sendTelemetryEvent('statusLimit', { ignoreSubmodules: String(ignoreSubmodules) }, { limit, statusLength }); + } const config = workspace.getConfiguration('git'); const shouldIgnore = config.get('ignoreLimitWarning') === true; @@ -1922,7 +1935,7 @@ export class Repository implements Disposable { return undefined; }); - let actionButton: SourceControl['actionButton']; + let actionButton: SourceControlActionButton | undefined; if (HEAD !== undefined) { const config = workspace.getConfiguration('git', Uri.file(this.repository.root)); const showActionButton = config.get('showUnpublishedCommitsButton', 'whenEmpty'); @@ -1935,18 +1948,23 @@ export class Repository implements Disposable { const rebaseWhenSync = config.get('rebaseWhenSync'); actionButton = { - command: rebaseWhenSync ? 'git.syncRebase' : 'git.sync', - title: localize('scm button sync title', '$(sync) Sync Changes {0}{1}', HEAD.behind ? `${HEAD.behind}$(arrow-down) ` : '', `${HEAD.ahead}$(arrow-up)`), - tooltip: this.syncTooltip, - arguments: [this._sourceControl], + command: { + command: rebaseWhenSync ? 'git.syncRebase' : 'git.sync', + title: localize('scm button sync title', "$(sync) {0}{1}", HEAD.behind ? `${HEAD.behind}$(arrow-down) ` : '', `${HEAD.ahead}$(arrow-up)`), + tooltip: this.syncTooltip, + arguments: [this._sourceControl], + }, + description: localize('scm button sync description', "$(sync) Sync Changes {0}{1}", HEAD.behind ? `${HEAD.behind}$(arrow-down) ` : '', `${HEAD.ahead}$(arrow-up)`) }; } } else { actionButton = { - command: 'git.publish', - title: localize('scm button publish title', "$(cloud-upload) Publish Branch"), - tooltip: localize('scm button publish tooltip', "Publish Branch"), - arguments: [this._sourceControl], + command: { + command: 'git.publish', + title: localize('scm button publish title', "$(cloud-upload) Publish Branch"), + tooltip: localize('scm button publish tooltip', "Publish Branch"), + arguments: [this._sourceControl], + } }; } } diff --git a/extensions/git/src/test/smoke.test.ts b/extensions/git/src/test/smoke.test.ts index 9005bb4956325..ed654eae7fb43 100644 --- a/extensions/git/src/test/smoke.test.ts +++ b/extensions/git/src/test/smoke.test.ts @@ -42,13 +42,12 @@ suite('git smoke test', function () { suiteSetup(async function () { fs.writeFileSync(file('app.js'), 'hello', 'utf8'); fs.writeFileSync(file('index.pug'), 'hello', 'utf8'); - cp.execSync('git init', { cwd }); + cp.execSync('git init -b main', { cwd }); cp.execSync('git config user.name testuser', { cwd }); cp.execSync('git config user.email monacotools@microsoft.com', { cwd }); cp.execSync('git config commit.gpgsign false', { cwd }); cp.execSync('git add .', { cwd }); cp.execSync('git commit -m "initial commit"', { cwd }); - cp.execSync('git branch -m main', { cwd }); // make sure git is activated const ext = extensions.getExtension('vscode.git'); diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index b5a46554ca18f..a8ae8fff98ad8 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -4,15 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Disposable, EventEmitter } from 'vscode'; -import { dirname, sep } from 'path'; +import { dirname, sep, relative } from 'path'; import { Readable } from 'stream'; import { promises as fs, createReadStream } from 'fs'; import * as byline from 'byline'; +export const isMacintosh = process.platform === 'darwin'; +export const isWindows = process.platform === 'win32'; + export function log(...args: any[]): void { console.log.apply(console, ['git:', ...args]); } +export function logTimestamp(): string { + return `[${new Date().toISOString()}]`; +} + export interface IDisposable { dispose(): void; } @@ -280,8 +287,14 @@ export function detectUnicodeEncoding(buffer: Buffer): Encoding | null { return null; } -function isWindowsPath(path: string): boolean { - return /^[a-zA-Z]:\\/.test(path); +function normalizePath(path: string): string { + // Windows & Mac are currently being handled + // as case insensitive file systems in VS Code. + if (isWindows || isMacintosh) { + return path.toLowerCase(); + } + + return path; } export function isDescendant(parent: string, descendant: string): boolean { @@ -293,23 +306,26 @@ export function isDescendant(parent: string, descendant: string): boolean { parent += sep; } - // Windows is case insensitive - if (isWindowsPath(parent)) { - parent = parent.toLowerCase(); - descendant = descendant.toLowerCase(); - } - - return descendant.startsWith(parent); + return normalizePath(descendant).startsWith(normalizePath(parent)); } export function pathEquals(a: string, b: string): boolean { - // Windows is case insensitive - if (isWindowsPath(a)) { - a = a.toLowerCase(); - b = b.toLowerCase(); + return normalizePath(a) === normalizePath(b); +} + +/** + * Given the `repository.root` compute the relative path while trying to preserve + * the casing of the resource URI. The `repository.root` segment of the path can + * have a casing mismatch if the folder/workspace is being opened with incorrect + * casing. + */ +export function relativePath(from: string, to: string): string { + if (isDescendant(from, to) && from.length < to.length) { + return to.substring(from.length + 1); } - return a === b; + // Fallback to `path.relative` + return relative(from, to); } export function* splitInChunks(array: string[], maxChunkLength: number): IterableIterator { diff --git a/extensions/git/src/watch.ts b/extensions/git/src/watch.ts index c6670cfa8847b..a9d99e56a2225 100644 --- a/extensions/git/src/watch.ts +++ b/extensions/git/src/watch.ts @@ -3,23 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, EventEmitter, Uri } from 'vscode'; -import { join } from 'path'; -import * as fs from 'fs'; -import { IDisposable } from './util'; +import { Event, RelativePattern, Uri, workspace } from 'vscode'; +import { IDisposable, anyEvent } from './util'; export interface IFileWatcher extends IDisposable { readonly event: Event; } export function watch(location: string): IFileWatcher { - const dotGitWatcher = fs.watch(location); - const onDotGitFileChangeEmitter = new EventEmitter(); - dotGitWatcher.on('change', (_, e) => onDotGitFileChangeEmitter.fire(Uri.file(join(location, e as string)))); - dotGitWatcher.on('error', err => console.error(err)); + const watcher = workspace.createFileSystemWatcher(new RelativePattern(location, '*')); return new class implements IFileWatcher { - event = onDotGitFileChangeEmitter.event; - dispose() { dotGitWatcher.close(); } + event = anyEvent(watcher.onDidCreate, watcher.onDidChange, watcher.onDidDelete); + dispose() { + watcher.dispose(); + } }; } diff --git a/extensions/git/yarn.lock b/extensions/git/yarn.lock index ff535a1ee3ab1..8c832b81988a7 100644 --- a/extensions/git/yarn.lock +++ b/extensions/git/yarn.lock @@ -36,6 +36,16 @@ resolved "https://registry.yarnpkg.com/@types/which/-/which-1.0.28.tgz#016e387629b8817bed653fe32eab5d11279c8df6" integrity sha1-AW44dim4gXvtZT/jLqtdESecjfY= +"@vscode/extension-telemetry@0.4.6": + version "0.4.6" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.4.6.tgz#2f4c5bf81adf6b2e4ddba54759355e1559c5476b" + integrity sha512-bDXwHoNXIR1Rc8xdphJ4B3rWdzAGm+FUPk4mJl6/oyZmfEX+QdlDLxnCwlv/vxHU1p11ThHSB8kRhsWZ1CzOqw== + +"@vscode/iconv-lite-umd@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.0.tgz#d2f1e0664ee6036408f9743fee264ea0699b0e48" + integrity sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg== + byline@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" @@ -46,11 +56,6 @@ file-type@^7.2.0: resolved "https://registry.yarnpkg.com/file-type/-/file-type-7.2.0.tgz#113cfed52e1d6959ab80248906e2f25a8cdccb74" integrity sha1-ETz+1S4daVmrgCSJBuLyWozcy3Q= -iconv-lite-umd@0.6.10: - version "0.6.10" - resolved "https://registry.yarnpkg.com/iconv-lite-umd/-/iconv-lite-umd-0.6.10.tgz#faec47521e095b8e3a7175ae08e1b4ae0359a735" - integrity sha512-8NtgTa/m1jVq7vdywmD5+SqIlZsB59wtsjaylQuExyCojMq1tHVQxmHjeqVSYwKwnmQbH4mZ1Dxx1eqDkPgaqA== - isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -61,11 +66,6 @@ jschardet@3.0.0: resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882" integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ== -vscode-extension-telemetry@0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.4.3.tgz#ea389b3d14b65d4fd5a6cf3760b3155e930193f2" - integrity sha512-opiIFOaAwyfACYMXByDqFMAlJ2iFMJR65/vIogJ960aLZWp9zaMdwY9CsY02EOYjHxPpjI7QeOQM3sYCb3xtJg== - vscode-nls@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002" diff --git a/extensions/github-authentication/package.json b/extensions/github-authentication/package.json index 2dbf469fd2650..a3e43f3c02013 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -58,9 +58,9 @@ "vscode:prepublish": "npm run compile" }, "dependencies": { - "node-fetch": "2.6.1", + "node-fetch": "2.6.7", "uuid": "8.1.0", - "vscode-extension-telemetry": "0.4.3", + "@vscode/extension-telemetry": "0.4.6", "vscode-nls": "^5.0.0", "vscode-tas-client": "^0.1.22" }, diff --git a/extensions/github-authentication/src/common/env.ts b/extensions/github-authentication/src/common/env.ts new file mode 100644 index 0000000000000..b40c249d40a27 --- /dev/null +++ b/extensions/github-authentication/src/common/env.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Uri } from 'vscode'; + +const VALID_DESKTOP_CALLBACK_SCHEMES = [ + 'vscode', + 'vscode-insiders', + 'code-oss', + 'vscode-wsl', + 'vscode-exploration' +]; + +// This comes from the GitHub Authentication server +export function isSupportedEnvironment(url: Uri): boolean { + return VALID_DESKTOP_CALLBACK_SCHEMES.includes(url.scheme) || url.authority.endsWith('vscode.dev') || url.authority.endsWith('github.dev'); +} diff --git a/extensions/github-authentication/src/common/utils.ts b/extensions/github-authentication/src/common/utils.ts index e2af2e2d3c949..9ffbddec0b8c1 100644 --- a/extensions/github-authentication/src/common/utils.ts +++ b/extensions/github-authentication/src/common/utils.ts @@ -54,7 +54,7 @@ export function promiseFromEvent( let cancel = new EventEmitter(); return { promise: new Promise((resolve, reject) => { - cancel.event(_ => reject()); + cancel.event(_ => reject('Cancelled')); subscription = event((value: T) => { try { Promise.resolve(adapter(value, resolve, reject)) diff --git a/extensions/github-authentication/src/experimentationService.ts b/extensions/github-authentication/src/experimentationService.ts index ccfc19424077c..e3297ddd50ebf 100644 --- a/extensions/github-authentication/src/experimentationService.ts +++ b/extensions/github-authentication/src/experimentationService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import TelemetryReporter from 'vscode-extension-telemetry'; +import TelemetryReporter from '@vscode/extension-telemetry'; import { getExperimentationService, IExperimentationService, IExperimentationTelemetry, TargetPopulation } from 'vscode-tas-client'; export class ExperimentationTelemetry implements IExperimentationTelemetry { diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index 7cee96962f42c..90d3803fffa53 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -9,7 +9,7 @@ import { Keychain } from './common/keychain'; import { GitHubEnterpriseServer, GitHubServer, IGitHubServer } from './githubServer'; import { arrayEquals } from './common/utils'; import { ExperimentationTelemetry } from './experimentationService'; -import TelemetryReporter from 'vscode-extension-telemetry'; +import TelemetryReporter from '@vscode/extension-telemetry'; import { Log } from './common/logger'; interface SessionData { @@ -43,7 +43,11 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid this._telemetryReporter = new ExperimentationTelemetry(context, new TelemetryReporter(name, version, aiKey)); if (this.type === AuthProviderType.github) { - this._githubServer = new GitHubServer(this._logger, this._telemetryReporter); + this._githubServer = new GitHubServer( + // We only can use the Device Code flow when we are running with a remote extension host. + context.extension.extensionKind === vscode.ExtensionKind.Workspace, + this._logger, + this._telemetryReporter); } else { this._githubServer = new GitHubEnterpriseServer(this._logger, this._telemetryReporter); } @@ -68,13 +72,15 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid } async getSessions(scopes?: string[]): Promise { - this._logger.info(`Getting sessions for ${scopes?.join(',') || 'all scopes'}...`); + // For GitHub scope list, order doesn't matter so we immediately sort the scopes + const sortedScopes = scopes?.sort() || []; + this._logger.info(`Getting sessions for ${sortedScopes.length ? sortedScopes.join(',') : 'all scopes'}...`); const sessions = await this._sessionsPromise; - const finalSessions = scopes - ? sessions.filter(session => arrayEquals([...session.scopes].sort(), scopes.sort())) + const finalSessions = sortedScopes.length + ? sessions.filter(session => arrayEquals([...session.scopes].sort(), sortedScopes)) : sessions; - this._logger.info(`Got ${finalSessions.length} sessions for ${scopes?.join(',') || 'all scopes'}...`); + this._logger.info(`Got ${finalSessions.length} sessions for ${sortedScopes?.join(',') ?? 'all scopes'}...`); return finalSessions; } @@ -134,12 +140,20 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid return []; } + // TODO: eventually remove this Set because we should only have one session per set of scopes. + const scopesSeen = new Set(); const sessionPromises = sessionData.map(async (session: SessionData) => { + // For GitHub scope list, order doesn't matter so we immediately sort the scopes + const sortedScopes = session.scopes.sort(); + const scopesStr = sortedScopes.join(' '); + if (scopesSeen.has(scopesStr)) { + return undefined; + } let userInfo: { id: string, accountName: string } | undefined; if (!session.account) { try { userInfo = await this._githubServer.getUserInfo(session.accessToken); - this._logger.info(`Verified session with the following scopes: ${session.scopes}`); + this._logger.info(`Verified session with the following scopes: ${scopesStr}`); } catch (e) { // Remove sessions that return unauthorized response if (e.message === 'Unauthorized') { @@ -150,7 +164,8 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid setTimeout(() => this.afterTokenLoad(session.accessToken), 1000); - this._logger.trace(`Read the following session from the keychain with the following scopes: ${session.scopes}`); + this._logger.trace(`Read the following session from the keychain with the following scopes: ${scopesStr}`); + scopesSeen.add(scopesStr); return { id: session.id, account: { @@ -159,7 +174,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid : userInfo?.accountName ?? '', id: session.account?.id ?? userInfo?.id ?? '' }, - scopes: session.scopes, + scopes: sortedScopes, accessToken: session.accessToken }; }); @@ -186,22 +201,26 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid public async createSession(scopes: string[]): Promise { try { + // For GitHub scope list, order doesn't matter so we immediately sort the scopes + const sortedScopes = scopes.sort(); + /* __GDPR__ "login" : { "scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } } */ this._telemetryReporter?.sendTelemetryEvent('login', { - scopes: JSON.stringify(scopes), + scopes: JSON.stringify(sortedScopes), }); - const scopeString = scopes.join(' '); + + const scopeString = sortedScopes.join(' '); const token = await this._githubServer.login(scopeString); this.afterTokenLoad(token); - const session = await this.tokenToSession(token, scopes); + const session = await this.tokenToSession(token, sortedScopes); const sessions = await this._sessionsPromise; - const sessionIndex = sessions.findIndex(s => s.id === session.id || s.scopes.join(' ') === scopeString); + const sessionIndex = sessions.findIndex(s => s.id === session.id || arrayEquals([...s.scopes].sort(), sortedScopes)); if (sessionIndex > -1) { sessions.splice(sessionIndex, 1, session); } else { @@ -216,7 +235,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid return session; } catch (e) { // If login was cancelled, do not notify user. - if (e === 'Cancelled') { + if (e === 'Cancelled' || e.message === 'Cancelled') { /* __GDPR__ "loginCancelled" : { } */ diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index bc7e864d1141c..085e285c690ed 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -11,8 +11,10 @@ import { PromiseAdapter, promiseFromEvent } from './common/utils'; import { ExperimentationTelemetry } from './experimentationService'; import { AuthProviderType } from './github'; import { Log } from './common/logger'; +import { isSupportedEnvironment } from './common/env'; const localize = nls.loadMessageBundle(); +const CLIENT_ID = '01ab8ac9400c4e429b23'; const NETWORK_ERROR = 'network error'; /** @@ -50,6 +52,13 @@ export interface IGitHubServer extends vscode.Disposable { type: AuthProviderType; } +interface IGitHubDeviceCodeResponse { + device_code: string; + user_code: string; + verification_uri: string; + interval: number; +} + async function getScopes(token: string, serverUri: vscode.Uri, logger: Log): Promise { try { logger.info('Getting token scopes...'); @@ -93,8 +102,18 @@ async function getUserInfo(token: string, serverUri: vscode.Uri, logger: Log): P logger.info('Got account info!'); return { id: json.id, accountName: json.login }; } else { - logger.error(`Getting account info failed: ${result.statusText}`); - throw new Error(result.statusText); + // either display the response message or the http status text + let errorMessage = result.statusText; + try { + const json = await result.json(); + if (json.message) { + errorMessage = json.message; + } + } catch (err) { + // noop + } + logger.error(`Getting account info failed: ${errorMessage}`); + throw new Error(errorMessage); } } @@ -110,7 +129,7 @@ export class GitHubServer implements IGitHubServer { private _disposable: vscode.Disposable; private _uriHandler = new UriEventHandler(this._logger); - constructor(private readonly _logger: Log, private readonly _telemetryReporter: ExperimentationTelemetry) { + constructor(private readonly _supportDeviceCodeFlow: boolean, private readonly _logger: Log, private readonly _telemetryReporter: ExperimentationTelemetry) { this._disposable = vscode.Disposable.from( vscode.commands.registerCommand(this._statusBarCommandId, () => this.manuallyProvideUri()), vscode.window.registerUriHandler(this._uriHandler)); @@ -120,10 +139,6 @@ export class GitHubServer implements IGitHubServer { this._disposable.dispose(); } - private isTestEnvironment(url: vscode.Uri): boolean { - return /\.azurewebsites\.net$/.test(url.authority) || url.authority.startsWith('localhost:'); - } - // TODO@joaomoreno TODO@TylerLeonhardt private async isNoCorsEnvironment(): Promise { const uri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/dummy`)); @@ -135,9 +150,12 @@ export class GitHubServer implements IGitHubServer { const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate`)); - if (this.isTestEnvironment(callbackUri)) { - const token = await vscode.window.showInputBox({ prompt: 'GitHub Personal Access Token', ignoreFocusOut: true }); - if (!token) { throw new Error('Sign in failed: No token provided'); } + if (!isSupportedEnvironment(callbackUri)) { + const token = this._supportDeviceCodeFlow + ? await this.doDeviceCodeFlow(scopes) + : await vscode.window.showInputBox({ prompt: 'GitHub Personal Access Token', ignoreFocusOut: true }); + + if (!token) { throw new Error('No token provided'); } const tokenScopes = await getScopes(token, this.getServerUri('/'), this._logger); // Example: ['repo', 'user'] const scopesList = scopes.split(' '); // Example: 'read:user repo user:email' @@ -192,6 +210,97 @@ export class GitHubServer implements IGitHubServer { }); } + private async doDeviceCodeFlow(scopes: string): Promise { + // Get initial device code + const uri = `https://github.com/login/device/code?client_id=${CLIENT_ID}&scope=${scopes}`; + const result = await fetch(uri, { + method: 'POST', + headers: { + Accept: 'application/json' + } + }); + if (!result.ok) { + throw new Error(`Failed to get one-time code: ${await result.text()}`); + } + + const json = await result.json() as IGitHubDeviceCodeResponse; + + + const modalResult = await vscode.window.showInformationMessage( + localize('code.title', "Your Code: {0}", json.user_code), + { + modal: true, + detail: localize('code.detail', "To finish authenticating, navigate to GitHub and paste in the above one-time code.") + }, 'Copy & Continue to GitHub'); + + if (modalResult !== 'Copy & Continue to GitHub') { + throw new Error('Cancelled'); + } + + await vscode.env.clipboard.writeText(json.user_code); + + const uriToOpen = await vscode.env.asExternalUri(vscode.Uri.parse(json.verification_uri)); + await vscode.env.openExternal(uriToOpen); + + return await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + cancellable: true, + title: localize( + 'progress', + "Open [{0}]({0}) in a new tab and paste your one-time code: {1}", + json.verification_uri, + json.user_code) + }, async (_, token) => { + return await this.waitForDeviceCodeAccessToken(json, token); + }); + } + + private async waitForDeviceCodeAccessToken( + json: IGitHubDeviceCodeResponse, + token: vscode.CancellationToken + ): Promise { + + const refreshTokenUri = `https://github.com/login/oauth/access_token?client_id=${CLIENT_ID}&device_code=${json.device_code}&grant_type=urn:ietf:params:oauth:grant-type:device_code`; + + // Try for 2 minutes + const attempts = 120 / json.interval; + for (let i = 0; i < attempts; i++) { + await new Promise(resolve => setTimeout(resolve, json.interval * 1000)); + if (token.isCancellationRequested) { + throw new Error('Cancelled'); + } + let accessTokenResult; + try { + accessTokenResult = await fetch(refreshTokenUri, { + method: 'POST', + headers: { + Accept: 'application/json' + } + }); + } catch { + continue; + } + + if (!accessTokenResult.ok) { + continue; + } + + const accessTokenJson = await accessTokenResult.json(); + + if (accessTokenJson.error === 'authorization_pending') { + continue; + } + + if (accessTokenJson.error) { + throw new Error(accessTokenJson.error_description); + } + + return accessTokenJson.access_token; + } + + throw new Error('Cancelled'); + } + private exchangeCodeForToken: (scopes: string) => PromiseAdapter = (scopes) => async (uri, resolve, reject) => { const query = parseQuery(uri); diff --git a/extensions/github-authentication/yarn.lock b/extensions/github-authentication/yarn.lock index 3628d956fc84e..f9602e4369f2b 100644 --- a/extensions/github-authentication/yarn.lock +++ b/extensions/github-authentication/yarn.lock @@ -25,6 +25,11 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.0.0.tgz#165aae4819ad2174a17476dbe66feebd549556c0" integrity sha512-xSQfNcvOiE5f9dyd4Kzxbof1aTrLobL278pGLKOZI6esGfZ7ts9Ka16CzIN6Y8hFHE1C7jIBZokULhK1bOgjRw== +"@vscode/extension-telemetry@0.4.6": + version "0.4.6" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.4.6.tgz#2f4c5bf81adf6b2e4ddba54759355e1559c5476b" + integrity sha512-bDXwHoNXIR1Rc8xdphJ4B3rWdzAGm+FUPk4mJl6/oyZmfEX+QdlDLxnCwlv/vxHU1p11ThHSB8kRhsWZ1CzOqw== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -50,9 +55,9 @@ delayed-stream@~1.0.0: integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= follow-redirects@^1.14.0: - version "1.14.5" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.5.tgz#f09a5848981d3c772b5392309778523f8d85c381" - integrity sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA== + version "1.14.7" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685" + integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ== form-data@^3.0.0: version "3.0.0" @@ -75,10 +80,12 @@ mime-types@^2.1.12: dependencies: mime-db "1.44.0" -node-fetch@2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" - integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== +node-fetch@2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" tas-client@0.1.21: version "0.1.21" @@ -87,16 +94,16 @@ tas-client@0.1.21: dependencies: axios "^0.21.1" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + uuid@8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d" integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg== -vscode-extension-telemetry@0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.4.3.tgz#ea389b3d14b65d4fd5a6cf3760b3155e930193f2" - integrity sha512-opiIFOaAwyfACYMXByDqFMAlJ2iFMJR65/vIogJ960aLZWp9zaMdwY9CsY02EOYjHxPpjI7QeOQM3sYCb3xtJg== - vscode-nls@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840" @@ -108,3 +115,16 @@ vscode-tas-client@^0.1.22: integrity sha512-1sYH73nhiSRVQgfZkLQNJW7VzhKM9qNbCe8QyXgiKkLhH4GflDXRPAK4yy4P41jUgula+Fc9G7i5imj1dlKfaw== dependencies: tas-client "0.1.21" + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" diff --git a/extensions/github/markdown.css b/extensions/github/markdown.css new file mode 100644 index 0000000000000..e6b118cb8d92f --- /dev/null +++ b/extensions/github/markdown.css @@ -0,0 +1,4 @@ +.vscode-dark img[src$=\#gh-light-mode-only], +.vscode-light img[src$=\#gh-dark-mode-only] { + display: none; +} diff --git a/extensions/github/package.json b/extensions/github/package.json index e21350c32869a..7c0013fd7a8a4 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -64,6 +64,9 @@ "contents": "%welcome.publishWorkspaceFolder%", "when": "config.git.enabled && git.state == initialized && workbenchState == workspace && workspaceFolderCount != 0" } + ], + "markdown.previewStyles": [ + "./markdown.css" ] }, "scripts": { diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index dad2982533c1c..42ed8198287b0 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -6,12 +6,12 @@ import * as vscode from 'vscode'; import { API as GitAPI } from './typings/git'; import { publishRepository } from './publish'; -import { combinedDisposable } from './util'; +import { DisposableStore } from './util'; export function registerCommands(gitAPI: GitAPI): vscode.Disposable { - const disposables: vscode.Disposable[] = []; + const disposables = new DisposableStore(); - disposables.push(vscode.commands.registerCommand('github.publish', async () => { + disposables.add(vscode.commands.registerCommand('github.publish', async () => { try { publishRepository(gitAPI); } catch (err) { @@ -19,5 +19,5 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable { } })); - return combinedDisposable(disposables); + return disposables; } diff --git a/extensions/github/src/extension.ts b/extensions/github/src/extension.ts index 4e25bad1313e8..2fbe1d597fd76 100644 --- a/extensions/github/src/extension.ts +++ b/extensions/github/src/extension.ts @@ -8,7 +8,7 @@ import { GithubRemoteSourceProvider } from './remoteSourceProvider'; import { GitExtension } from './typings/git'; import { registerCommands } from './commands'; import { GithubCredentialProviderManager } from './credentialProvider'; -import { dispose, combinedDisposable } from './util'; +import { DisposableStore } from './util'; import { GithubPushErrorHandler } from './pushErrorHandler'; import { GitBaseExtension } from './typings/git-base'; import { GithubRemoteSourcePublisher } from './remoteSourcePublisher'; @@ -19,7 +19,7 @@ export function activate(context: ExtensionContext): void { } function initializeGitBaseExtension(): Disposable { - const disposables = new Set(); + const disposables = new DisposableStore(); const initialize = () => { try { @@ -35,8 +35,7 @@ function initializeGitBaseExtension(): Disposable { const onDidChangeGitBaseExtensionEnablement = (enabled: boolean) => { if (!enabled) { - dispose(disposables); - disposables.clear(); + disposables.dispose(); } else { initialize(); } @@ -46,11 +45,11 @@ function initializeGitBaseExtension(): Disposable { disposables.add(gitBaseExtension.onDidChangeEnablement(onDidChangeGitBaseExtensionEnablement)); onDidChangeGitBaseExtensionEnablement(gitBaseExtension.enabled); - return combinedDisposable(disposables); + return disposables; } function initializeGitExtension(): Disposable { - const disposables = new Set(); + const disposables = new DisposableStore(); let gitExtension = extensions.getExtension('vscode.git'); @@ -68,8 +67,7 @@ function initializeGitExtension(): Disposable { commands.executeCommand('setContext', 'git-base.gitEnabled', true); } else { - dispose(disposables); - disposables.clear(); + disposables.dispose(); } }; @@ -81,16 +79,15 @@ function initializeGitExtension(): Disposable { if (gitExtension) { initialize(); } else { - const disposable = extensions.onDidChange(() => { + const listener = extensions.onDidChange(() => { if (!gitExtension && extensions.getExtension('vscode.git')) { gitExtension = extensions.getExtension('vscode.git'); initialize(); - - dispose(disposable); + listener.dispose(); } }); - disposables.add(disposable); + disposables.add(listener); } - return combinedDisposable(disposables); + return disposables; } diff --git a/extensions/github/src/util.ts b/extensions/github/src/util.ts index 60badd28c84b1..c7d23a8230108 100644 --- a/extensions/github/src/util.ts +++ b/extensions/github/src/util.ts @@ -5,20 +5,19 @@ import * as vscode from 'vscode'; -export function dispose(arg: vscode.Disposable | Iterable): void { - if (arg instanceof vscode.Disposable) { - arg.dispose(); - } else { - for (const disposable of arg) { - disposable.dispose(); - } +export class DisposableStore { + + private disposables = new Set(); + + add(disposable: vscode.Disposable): void { + this.disposables.add(disposable); } -} -export function combinedDisposable(disposables: Iterable): vscode.Disposable { - return { - dispose() { - dispose(disposables); + dispose(): void { + for (const disposable of this.disposables) { + disposable.dispose(); } - }; + + this.disposables.clear(); + } } diff --git a/extensions/github/yarn.lock b/extensions/github/yarn.lock index 94f7a26da9d9b..fabc2469c44cc 100644 --- a/extensions/github/yarn.lock +++ b/extensions/github/yarn.lock @@ -125,9 +125,11 @@ is-plain-object@^3.0.0: integrity sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g== node-fetch@^2.3.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" - integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" once@^1.4.0: version "1.4.0" @@ -136,6 +138,11 @@ once@^1.4.0: dependencies: wrappy "1" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + tunnel@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" @@ -151,6 +158,19 @@ vscode-nls@^4.1.2: resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.2.tgz#ca8bf8bb82a0987b32801f9fddfdd2fb9fd3c167" integrity sha512-7bOHxPsfyuCqmP+hZXscLhiHwe7CSuFE4hyhbs22xPIhQ4jv99FcR4eBzfYYVLP356HNFpdvz63FFb/xw6T4Iw== +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" diff --git a/extensions/grunt/package.json b/extensions/grunt/package.json index c784b1ed83a11..7e5c605ec00ff 100644 --- a/extensions/grunt/package.json +++ b/extensions/grunt/package.json @@ -24,7 +24,7 @@ }, "main": "./out/main", "activationEvents": [ - "onCommand:workbench.action.tasks.runTask" + "onTaskType:grunt" ], "capabilities": { "virtualWorkspaces": false, diff --git a/extensions/gulp/package.json b/extensions/gulp/package.json index 21800e96cfb60..995da7fbe5d3a 100644 --- a/extensions/gulp/package.json +++ b/extensions/gulp/package.json @@ -24,7 +24,7 @@ }, "main": "./out/main", "activationEvents": [ - "onCommand:workbench.action.tasks.runTask" + "onTaskType:gulp" ], "capabilities": { "virtualWorkspaces": false, diff --git a/extensions/html-language-features/client/src/tagClosing.ts b/extensions/html-language-features/client/src/autoInsertion.ts similarity index 63% rename from extensions/html-language-features/client/src/tagClosing.ts rename to extensions/html-language-features/client/src/autoInsertion.ts index 0d0ef10f80419..170afa46c0280 100644 --- a/extensions/html-language-features/client/src/tagClosing.ts +++ b/extensions/html-language-features/client/src/autoInsertion.ts @@ -3,15 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { window, workspace, Disposable, TextDocument, Position, SnippetString, TextDocumentChangeEvent, TextDocumentChangeReason } from 'vscode'; +import { window, workspace, Disposable, TextDocument, Position, SnippetString, TextDocumentChangeEvent, TextDocumentChangeReason, TextDocumentContentChangeEvent } from 'vscode'; import { Runtime } from './htmlClient'; -export function activateTagClosing(tagProvider: (document: TextDocument, position: Position) => Thenable, supportedLanguages: { [id: string]: boolean }, configName: string, runtime: Runtime): Disposable { - +export function activateAutoInsertion(provider: (kind: 'autoQuote' | 'autoClose', document: TextDocument, position: Position) => Thenable, supportedLanguages: { [id: string]: boolean }, runtime: Runtime): Disposable { const disposables: Disposable[] = []; workspace.onDidChangeTextDocument(onDidChangeTextDocument, null, disposables); - let isEnabled = false; + let anyIsEnabled = false; + const isEnabled = { + 'autoQuote': false, + 'autoClose': false + }; updateEnabledState(); window.onDidChangeActiveTextEditor(updateEnabledState, null, disposables); @@ -24,7 +27,7 @@ export function activateTagClosing(tagProvider: (document: TextDocument, positio }); function updateEnabledState() { - isEnabled = false; + anyIsEnabled = false; const editor = window.activeTextEditor; if (!editor) { return; @@ -33,14 +36,14 @@ export function activateTagClosing(tagProvider: (document: TextDocument, positio if (!supportedLanguages[document.languageId]) { return; } - if (!workspace.getConfiguration(undefined, document.uri).get(configName)) { - return; - } - isEnabled = true; + const configurations = workspace.getConfiguration(undefined, document.uri); + isEnabled['autoQuote'] = configurations.get('html.autoCreateQuotes') ?? false; + isEnabled['autoClose'] = configurations.get('html.autoClosingTags') ?? false; + anyIsEnabled = isEnabled['autoQuote'] || isEnabled['autoClose']; } function onDidChangeTextDocument({ document, contentChanges, reason }: TextDocumentChangeEvent) { - if (!isEnabled || contentChanges.length === 0 || reason === TextDocumentChangeReason.Undo || reason === TextDocumentChangeReason.Redo) { + if (!anyIsEnabled || contentChanges.length === 0 || reason === TextDocumentChangeReason.Undo || reason === TextDocumentChangeReason.Redo) { return; } const activeDocument = window.activeTextEditor && window.activeTextEditor.document; @@ -53,15 +56,20 @@ export function activateTagClosing(tagProvider: (document: TextDocument, positio const lastChange = contentChanges[contentChanges.length - 1]; const lastCharacter = lastChange.text[lastChange.text.length - 1]; - if (lastChange.rangeLength > 0 || lastCharacter !== '>' && lastCharacter !== '/') { - return; + if (isEnabled['autoQuote'] && lastChange.rangeLength === 0 && lastCharacter === '=') { + doAutoInsert('autoQuote', document, lastChange); + } else if (isEnabled['autoClose'] && lastChange.rangeLength === 0 && (lastCharacter === '>' || lastCharacter === '/')) { + doAutoInsert('autoClose', document, lastChange); } + } + + function doAutoInsert(kind: 'autoQuote' | 'autoClose', document: TextDocument, lastChange: TextDocumentContentChangeEvent) { const rangeStart = lastChange.range.start; const version = document.version; timeout = runtime.timer.setTimeout(() => { const position = new Position(rangeStart.line, rangeStart.character + lastChange.text.length); - tagProvider(document, position).then(text => { - if (text && isEnabled) { + provider(kind, document, position).then(text => { + if (text && isEnabled[kind]) { const activeEditor = window.activeTextEditor; if (activeEditor) { const activeDocument = activeEditor.document; diff --git a/extensions/html-language-features/client/src/customData.ts b/extensions/html-language-features/client/src/customData.ts index ecf964056e541..80e5f2f04d981 100644 --- a/extensions/html-language-features/client/src/customData.ts +++ b/extensions/html-language-features/client/src/customData.ts @@ -4,53 +4,103 @@ *--------------------------------------------------------------------------------------------*/ import { workspace, extensions, Uri, EventEmitter, Disposable } from 'vscode'; -import { resolvePath, joinPath } from './requests'; +import { Runtime } from './htmlClient'; +import { Utils } from 'vscode-uri'; -export function getCustomDataSource(toDispose: Disposable[]) { - let pathsInWorkspace = getCustomDataPathsInAllWorkspaces(); - let pathsInExtensions = getCustomDataPathsFromAllExtensions(); + +export function getCustomDataSource(runtime: Runtime, toDispose: Disposable[]) { + let localExtensionUris = new Set(); + let externalExtensionUris = new Set(); + const workspaceUris = new Set(); + + collectInWorkspaces(workspaceUris); + collectInExtensions(localExtensionUris, externalExtensionUris); const onChange = new EventEmitter(); toDispose.push(extensions.onDidChange(_ => { - const newPathsInExtensions = getCustomDataPathsFromAllExtensions(); - if (newPathsInExtensions.length !== pathsInExtensions.length || !newPathsInExtensions.every((val, idx) => val === pathsInExtensions[idx])) { - pathsInExtensions = newPathsInExtensions; + const newLocalExtensionUris = new Set(); + const newExternalExtensionUris = new Set(); + collectInExtensions(newLocalExtensionUris, newExternalExtensionUris); + if (hasChanges(newLocalExtensionUris, localExtensionUris) || hasChanges(newExternalExtensionUris, externalExtensionUris)) { + localExtensionUris = newLocalExtensionUris; + externalExtensionUris = newExternalExtensionUris; onChange.fire(); } })); toDispose.push(workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration('html.customData')) { - pathsInWorkspace = getCustomDataPathsInAllWorkspaces(); + workspaceUris.clear(); + collectInWorkspaces(workspaceUris); + onChange.fire(); + } + })); + + toDispose.push(workspace.onDidChangeTextDocument(e => { + const path = e.document.uri.toString(); + if (externalExtensionUris.has(path) || workspaceUris.has(path)) { onChange.fire(); } })); return { get uris() { - return pathsInWorkspace.concat(pathsInExtensions); + return [...localExtensionUris].concat([...externalExtensionUris], [...workspaceUris]); }, get onDidChange() { return onChange.event; + }, + getContent(uriString: string): Thenable { + const uri = Uri.parse(uriString); + if (localExtensionUris.has(uriString)) { + return workspace.fs.readFile(uri).then(buffer => { + return new runtime.TextDecoder().decode(buffer); + }); + } + return workspace.openTextDocument(uri).then(doc => { + return doc.getText(); + }); } }; } +function hasChanges(s1: Set, s2: Set) { + if (s1.size !== s2.size) { + return true; + } + for (const uri of s1) { + if (!s2.has(uri)) { + return true; + } + } + return false; +} + +function isURI(uriOrPath: string) { + return /^(?\w[\w\d+.-]*):/.test(uriOrPath); +} -function getCustomDataPathsInAllWorkspaces(): string[] { + +function collectInWorkspaces(workspaceUris: Set): Set { const workspaceFolders = workspace.workspaceFolders; - const dataPaths: string[] = []; + const dataPaths = new Set(); if (!workspaceFolders) { return dataPaths; } - const collect = (paths: string[] | undefined, rootFolder: Uri) => { - if (Array.isArray(paths)) { - for (const path of paths) { - if (typeof path === 'string') { - dataPaths.push(resolvePath(rootFolder, path).toString()); + const collect = (uriOrPaths: string[] | undefined, rootFolder: Uri) => { + if (Array.isArray(uriOrPaths)) { + for (const uriOrPath of uriOrPaths) { + if (typeof uriOrPath === 'string') { + if (!isURI(uriOrPath)) { + // path in the workspace + workspaceUris.add(Utils.resolvePath(rootFolder, uriOrPath).toString()); + } else { + // external uri + workspaceUris.add(uriOrPath); + } } } } @@ -74,15 +124,20 @@ function getCustomDataPathsInAllWorkspaces(): string[] { return dataPaths; } -function getCustomDataPathsFromAllExtensions(): string[] { - const dataPaths: string[] = []; +function collectInExtensions(localExtensionUris: Set, externalUris: Set): void { for (const extension of extensions.all) { const customData = extension.packageJSON?.contributes?.html?.customData; if (Array.isArray(customData)) { - for (const rp of customData) { - dataPaths.push(joinPath(extension.extensionUri, rp).toString()); + for (const uriOrPath of customData) { + if (!isURI(uriOrPath)) { + // relative path in an extension + localExtensionUris.add(Uri.joinPath(extension.extensionUri, uriOrPath).toString()); + } else { + // external uri + externalUris.add(uriOrPath); + } + } } } - return dataPaths; } diff --git a/extensions/html-language-features/client/src/htmlClient.ts b/extensions/html-language-features/client/src/htmlClient.ts index 42e75a297e5b9..d24c0ec9205be 100644 --- a/extensions/html-language-features/client/src/htmlClient.ts +++ b/extensions/html-language-features/client/src/htmlClient.ts @@ -12,20 +12,40 @@ import { DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider, SemanticTokens, window, commands } from 'vscode'; import { - LanguageClientOptions, RequestType, TextDocumentPositionParams, DocumentRangeFormattingParams, - DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, TextDocumentIdentifier, RequestType0, Range as LspRange, NotificationType, CommonLanguageClient + LanguageClientOptions, RequestType, DocumentRangeFormattingParams, + DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, TextDocumentIdentifier, RequestType0, Range as LspRange, Position as LspPosition, NotificationType, CommonLanguageClient } from 'vscode-languageclient'; -import { activateTagClosing } from './tagClosing'; -import { RequestService } from './requests'; +import { FileSystemProvider, serveFileSystemRequests } from './requests'; import { getCustomDataSource } from './customData'; +import { activateAutoInsertion } from './autoInsertion'; namespace CustomDataChangedNotification { export const type: NotificationType = new NotificationType('html/customDataChanged'); } -namespace TagCloseRequest { - export const type: RequestType = new RequestType('html/tag'); +namespace CustomDataContent { + export const type: RequestType = new RequestType('html/customDataContent'); } + +interface AutoInsertParams { + /** + * The auto insert kind + */ + kind: 'autoQuote' | 'autoClose'; + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; + /** + * The position inside the text document. + */ + position: LspPosition; +} + +namespace AutoInsertRequest { + export const type: RequestType = new RequestType('html/autoInsert'); +} + // experimental: semantic tokens interface SemanticTokenParams { textDocument: TextDocumentIdentifier; @@ -56,7 +76,7 @@ export type LanguageClientConstructor = (name: string, description: string, clie export interface Runtime { TextDecoder: { new(encoding?: string): { decode(buffer: ArrayBuffer): string; } }; - fs?: RequestService; + fileFs?: FileSystemProvider; telemetry?: TelemetryReporter; readonly timer: { setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable; @@ -73,8 +93,6 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua let rangeFormatting: Disposable | undefined = undefined; - const customDataSource = getCustomDataSource(context.subscriptions); - // Options to control the language client let clientOptions: LanguageClientOptions = { documentSelector, @@ -120,16 +138,26 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua toDispose.push(disposable); client.onReady().then(() => { + toDispose.push(serveFileSystemRequests(client, runtime)); + + const customDataSource = getCustomDataSource(runtime, context.subscriptions); + client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris); customDataSource.onDidChange(() => { client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris); }); + client.onRequest(CustomDataContent.type, customDataSource.getContent); + - let tagRequestor = (document: TextDocument, position: Position) => { - let param = client.code2ProtocolConverter.asTextDocumentPositionParams(document, position); - return client.sendRequest(TagCloseRequest.type, param); + const insertRequestor = (kind: 'autoQuote' | 'autoClose', document: TextDocument, position: Position): Promise => { + let param: AutoInsertParams = { + kind, + textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), + position: client.code2ProtocolConverter.asPosition(position) + }; + return client.sendRequest(AutoInsertRequest.type, param); }; - disposable = activateTagClosing(tagRequestor, { html: true, handlebars: true }, 'html.autoClosingTags', runtime); + let disposable = activateAutoInsertion(insertRequestor, { html: true, handlebars: true }, runtime); toDispose.push(disposable); disposable = client.onTelemetry(e => { diff --git a/extensions/html-language-features/client/src/node/htmlClientMain.ts b/extensions/html-language-features/client/src/node/htmlClientMain.ts index d402ee31e7954..4c7d24e397c2c 100644 --- a/extensions/html-language-features/client/src/node/htmlClientMain.ts +++ b/extensions/html-language-features/client/src/node/htmlClientMain.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getNodeFSRequestService } from './nodeFs'; +import { getNodeFileFS } from './nodeFs'; import { Disposable, ExtensionContext } from 'vscode'; import { startClient, LanguageClientConstructor } from '../htmlClient'; import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node'; import { TextDecoder } from 'util'; import * as fs from 'fs'; -import TelemetryReporter from 'vscode-extension-telemetry'; +import TelemetryReporter from '@vscode/extension-telemetry'; let telemetry: TelemetryReporter | undefined; @@ -44,7 +44,7 @@ export function activate(context: ExtensionContext) { } }; - startClient(context, newLanguageClient, { fs: getNodeFSRequestService(), TextDecoder, telemetry, timer }); + startClient(context, newLanguageClient, { fileFs: getNodeFileFS(), TextDecoder, telemetry, timer }); } interface IPackageInfo { diff --git a/extensions/html-language-features/client/src/node/nodeFs.ts b/extensions/html-language-features/client/src/node/nodeFs.ts index c13ef2e1c08d5..46a3aeb9de459 100644 --- a/extensions/html-language-features/client/src/node/nodeFs.ts +++ b/extensions/html-language-features/client/src/node/nodeFs.ts @@ -5,28 +5,15 @@ import * as fs from 'fs'; import { Uri } from 'vscode'; -import { getScheme, RequestService, FileType } from '../requests'; +import { FileSystemProvider, FileType } from '../requests'; -export function getNodeFSRequestService(): RequestService { +export function getNodeFileFS(): FileSystemProvider { function ensureFileUri(location: string) { - if (getScheme(location) !== 'file') { + if (!location.startsWith('file:')) { throw new Error('fileRequestService can only handle file URLs'); } } return { - getContent(location: string, encoding?: string) { - ensureFileUri(location); - return new Promise((c, e) => { - const uri = Uri.parse(location); - fs.readFile(uri.fsPath, encoding, (err, buf) => { - if (err) { - return e(err); - } - c(buf.toString()); - - }); - }); - }, stat(location: string) { ensureFileUri(location); return new Promise((c, e) => { diff --git a/extensions/html-language-features/client/src/requests.ts b/extensions/html-language-features/client/src/requests.ts index f127c88562ff2..ba124e28cd7cf 100644 --- a/extensions/html-language-features/client/src/requests.ts +++ b/extensions/html-language-features/client/src/requests.ts @@ -3,13 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Uri, workspace } from 'vscode'; +import { Uri, workspace, Disposable } from 'vscode'; import { RequestType, CommonLanguageClient } from 'vscode-languageclient'; import { Runtime } from './htmlClient'; -export namespace FsContentRequest { - export const type: RequestType<{ uri: string; encoding?: string; }, string, any> = new RequestType('fs/content'); -} export namespace FsStatRequest { export const type: RequestType = new RequestType('fs/stat'); } @@ -18,30 +15,23 @@ export namespace FsReadDirRequest { export const type: RequestType = new RequestType('fs/readDir'); } -export function serveFileSystemRequests(client: CommonLanguageClient, runtime: Runtime) { - client.onRequest(FsContentRequest.type, (param: { uri: string; encoding?: string; }) => { - const uri = Uri.parse(param.uri); - if (uri.scheme === 'file' && runtime.fs) { - return runtime.fs.getContent(param.uri); - } - return workspace.fs.readFile(uri).then(buffer => { - return new runtime.TextDecoder(param.encoding).decode(buffer); - }); - }); - client.onRequest(FsReadDirRequest.type, (uriString: string) => { +export function serveFileSystemRequests(client: CommonLanguageClient, runtime: Runtime): Disposable { + const disposables = []; + disposables.push(client.onRequest(FsReadDirRequest.type, (uriString: string) => { const uri = Uri.parse(uriString); - if (uri.scheme === 'file' && runtime.fs) { - return runtime.fs.readDirectory(uriString); + if (uri.scheme === 'file' && runtime.fileFs) { + return runtime.fileFs.readDirectory(uriString); } return workspace.fs.readDirectory(uri); - }); - client.onRequest(FsStatRequest.type, (uriString: string) => { + })); + disposables.push(client.onRequest(FsStatRequest.type, (uriString: string) => { const uri = Uri.parse(uriString); - if (uri.scheme === 'file' && runtime.fs) { - return runtime.fs.stat(uriString); + if (uri.scheme === 'file' && runtime.fileFs) { + return runtime.fileFs.stat(uriString); } return workspace.fs.stat(uri); - }); + })); + return Disposable.from(...disposables); } export enum FileType { @@ -82,67 +72,7 @@ export interface FileStat { size: number; } -export interface RequestService { - getContent(uri: string, encoding?: string): Promise; - +export interface FileSystemProvider { stat(uri: string): Promise; readDirectory(uri: string): Promise<[string, FileType][]>; } - -export function getScheme(uri: string) { - return uri.substr(0, uri.indexOf(':')); -} - -export function dirname(uri: string) { - const lastIndexOfSlash = uri.lastIndexOf('/'); - return lastIndexOfSlash !== -1 ? uri.substr(0, lastIndexOfSlash) : ''; -} - -export function basename(uri: string) { - const lastIndexOfSlash = uri.lastIndexOf('/'); - return uri.substr(lastIndexOfSlash + 1); -} - -const Slash = '/'.charCodeAt(0); -const Dot = '.'.charCodeAt(0); - -export function isAbsolutePath(path: string) { - return path.charCodeAt(0) === Slash; -} - -export function resolvePath(uri: Uri, path: string): Uri { - if (isAbsolutePath(path)) { - return uri.with({ path: normalizePath(path.split('/')) }); - } - return joinPath(uri, path); -} - -export function normalizePath(parts: string[]): string { - const newParts: string[] = []; - for (const part of parts) { - if (part.length === 0 || part.length === 1 && part.charCodeAt(0) === Dot) { - // ignore - } else if (part.length === 2 && part.charCodeAt(0) === Dot && part.charCodeAt(1) === Dot) { - newParts.pop(); - } else { - newParts.push(part); - } - } - if (parts.length > 1 && parts[parts.length - 1].length === 0) { - newParts.push(''); - } - let res = newParts.join('/'); - if (parts[0].length === 0) { - res = '/' + res; - } - return res; -} - - -export function joinPath(uri: Uri, ...paths: string[]): Uri { - const parts = uri.path.split('/'); - for (let path of paths) { - parts.push(...path.split('/')); - } - return uri.with({ path: normalizePath(parts) }); -} diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index 07bbfe29e9abd..5f348e1128661 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -197,6 +197,12 @@ "default": true, "description": "%html.validate.styles%" }, + "html.autoCreateQuotes": { + "type": "boolean", + "scope": "resource", + "default": true, + "description": "%html.autoCreateQuotes%" + }, "html.autoClosingTags": { "type": "boolean", "scope": "resource", @@ -255,9 +261,10 @@ ] }, "dependencies": { - "vscode-extension-telemetry": "0.4.3", + "@vscode/extension-telemetry": "0.4.6", "vscode-languageclient": "^7.0.0", - "vscode-nls": "^5.0.0" + "vscode-nls": "^5.0.0", + "vscode-uri": "^3.0.3" }, "devDependencies": { "@types/node": "14.x" diff --git a/extensions/html-language-features/package.nls.json b/extensions/html-language-features/package.nls.json index 00a218de43ed2..702ba05054148 100644 --- a/extensions/html-language-features/package.nls.json +++ b/extensions/html-language-features/package.nls.json @@ -27,6 +27,7 @@ "html.trace.server.desc": "Traces the communication between VS Code and the HTML language server.", "html.validate.scripts": "Controls whether the built-in HTML language support validates embedded scripts.", "html.validate.styles": "Controls whether the built-in HTML language support validates embedded styles.", + "html.autoCreateQuotes": "Enable/disable auto creation of quotes for HTML attribute assignment.", "html.autoClosingTags": "Enable/disable autoclosing of HTML tags.", "html.completion.attributeDefaultValue": "Controls the default value for attributes when completion is accepted.", "html.completion.attributeDefaultValue.doublequotes": "Attribute value is set to \"\".", diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index 48f4c0f6259d9..35aedc3f1041d 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -9,12 +9,12 @@ }, "main": "./out/node/htmlServerMain", "dependencies": { - "vscode-css-languageservice": "^5.1.8", - "vscode-html-languageservice": "^4.1.1", + "vscode-css-languageservice": "^5.1.12", + "vscode-html-languageservice": "^4.2.1", "vscode-languageserver": "^7.0.0", - "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-textdocument": "^1.0.3", "vscode-nls": "^5.0.0", - "vscode-uri": "^3.0.2" + "vscode-uri": "^3.0.3" }, "devDependencies": { "@types/mocha": "^8.2.0", diff --git a/extensions/html-language-features/server/src/customData.ts b/extensions/html-language-features/server/src/customData.ts index 3ef347a23d2ef..08e40bf2db3ee 100644 --- a/extensions/html-language-features/server/src/customData.ts +++ b/extensions/html-language-features/server/src/customData.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { IHTMLDataProvider, newHTMLDataProvider } from 'vscode-html-languageservice'; -import { RequestService } from './requests'; +import { CustomDataRequestService } from './htmlServer'; -export function fetchHTMLDataProviders(dataPaths: string[], requestService: RequestService): Promise { +export function fetchHTMLDataProviders(dataPaths: string[], requestService: CustomDataRequestService): Promise { const providers = dataPaths.map(async p => { try { const content = await requestService.getContent(p); diff --git a/extensions/html-language-features/server/src/htmlServer.ts b/extensions/html-language-features/server/src/htmlServer.ts index 425e73c806aa2..033ed22c5645f 100644 --- a/extensions/html-language-features/server/src/htmlServer.ts +++ b/extensions/html-language-features/server/src/htmlServer.ts @@ -5,7 +5,7 @@ import { Connection, TextDocuments, InitializeParams, InitializeResult, RequestType, - DocumentRangeFormattingRequest, Disposable, TextDocumentPositionParams, ServerCapabilities, + DocumentRangeFormattingRequest, Disposable, ServerCapabilities, ConfigurationRequest, ConfigurationParams, DidChangeWorkspaceFoldersNotification, DocumentColorRequest, ColorPresentationRequest, TextDocumentSyncKind, NotificationType, RequestType0, DocumentFormattingRequest, FormattingOptions, TextEdit } from 'vscode-languageserver'; @@ -24,14 +24,33 @@ import { getFoldingRanges } from './modes/htmlFolding'; import { fetchHTMLDataProviders } from './customData'; import { getSelectionRanges } from './modes/selectionRanges'; import { SemanticTokenProvider, newSemanticTokenProvider } from './modes/semanticTokens'; -import { RequestService, getRequestService } from './requests'; +import { FileSystemProvider, getFileSystemProvider } from './requests'; namespace CustomDataChangedNotification { export const type: NotificationType = new NotificationType('html/customDataChanged'); } -namespace TagCloseRequest { - export const type: RequestType = new RequestType('html/tag'); +namespace CustomDataContent { + export const type: RequestType = new RequestType('html/customDataContent'); +} + +interface AutoInsertParams { + /** + * The auto insert kind + */ + kind: 'autoQuote' | 'autoClose'; + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; + /** + * The position inside the text document. + */ + position: Position; +} + +namespace AutoInsertRequest { + export const type: RequestType = new RequestType('html/autoInsert'); } // experimental: semantic tokens @@ -47,8 +66,7 @@ namespace SemanticTokenLegendRequest { } export interface RuntimeEnvironment { - file?: RequestService; - http?: RequestService + fileFs?: FileSystemProvider; configureHttpRequests?(proxy: string, strictSSL: boolean): void; readonly timer: { setImmediate(callback: (...args: any[]) => void, ...args: any[]): Disposable; @@ -56,6 +74,12 @@ export interface RuntimeEnvironment { } } + +export interface CustomDataRequestService { + getContent(uri: string): Promise; +} + + export function startServer(connection: Connection, runtime: RuntimeEnvironment) { // Create a text document manager. @@ -74,10 +98,11 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) let workspaceFoldersSupport = false; let foldingRangeLimit = Number.MAX_VALUE; - const notReady = () => Promise.reject('Not Ready'); - let requestService: RequestService = { getContent: notReady, stat: notReady, readDirectory: notReady }; - - + const customDataRequestService: CustomDataRequestService = { + getContent(uri: string) { + return connection.sendRequest(CustomDataContent.type, uri); + } + }; let globalSettings: Settings = {}; let documentSettings: { [key: string]: Thenable } = {}; @@ -113,17 +138,19 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) } } - requestService = getRequestService(initializationOptions?.handledSchemas || ['file'], connection, runtime); + const handledSchemas = initializationOptions?.handledSchemas as string[] ?? ['file']; + + const fileSystemProvider = getFileSystemProvider(handledSchemas, connection, runtime); const workspace = { get settings() { return globalSettings; }, get folders() { return workspaceFolders; } }; - languageModes = getLanguageModes(initializationOptions?.embeddedLanguages || { css: true, javascript: true }, workspace, params.capabilities, requestService); + languageModes = getLanguageModes(initializationOptions?.embeddedLanguages || { css: true, javascript: true }, workspace, params.capabilities, fileSystemProvider); const dataPaths: string[] = initializationOptions?.dataPaths || []; - fetchHTMLDataProviders(dataPaths, requestService).then(dataProviders => { + fetchHTMLDataProviders(dataPaths, customDataRequestService).then(dataProviders => { languageModes.updateDataProviders(dataProviders); }); @@ -471,20 +498,20 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) }, [], `Error while computing color presentations for ${params.textDocument.uri}`, token); }); - connection.onRequest(TagCloseRequest.type, (params, token) => { + connection.onRequest(AutoInsertRequest.type, (params, token) => { return runSafe(runtime, async () => { const document = documents.get(params.textDocument.uri); if (document) { const pos = params.position; if (pos.character > 0) { const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1)); - if (mode && mode.doAutoClose) { - return mode.doAutoClose(document, pos); + if (mode && mode.doAutoInsert) { + return mode.doAutoInsert(document, pos, params.kind); } } } return null; - }, null, `Error while computing tag close actions for ${params.textDocument.uri}`, token); + }, null, `Error while computing auto insert actions for ${params.textDocument.uri}`, token); }); connection.onFoldingRanges((params, token) => { @@ -567,7 +594,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) }); connection.onNotification(CustomDataChangedNotification.type, dataPaths => { - fetchHTMLDataProviders(dataPaths, requestService).then(dataProviders => { + fetchHTMLDataProviders(dataPaths, customDataRequestService).then(dataProviders => { languageModes.updateDataProviders(dataProviders); }); }); diff --git a/extensions/html-language-features/server/src/modes/htmlMode.ts b/extensions/html-language-features/server/src/modes/htmlMode.ts index 9c75531b3d07c..04c37a2b491dc 100644 --- a/extensions/html-language-features/server/src/modes/htmlMode.ts +++ b/extensions/html-language-features/server/src/modes/htmlMode.ts @@ -55,11 +55,21 @@ export function getHTMLMode(htmlLanguageService: HTMLLanguageService, workspace: async getFoldingRanges(document: TextDocument): Promise { return htmlLanguageService.getFoldingRanges(document); }, - async doAutoClose(document: TextDocument, position: Position) { + async doAutoInsert(document: TextDocument, position: Position, kind: 'autoQuote' | 'autoClose', settings = workspace.settings) { const offset = document.offsetAt(position); const text = document.getText(); - if (offset > 0 && text.charAt(offset - 1).match(/[>\/]/g)) { - return htmlLanguageService.doTagComplete(document, position, htmlDocuments.get(document)); + if (kind === 'autoQuote') { + if (offset > 0 && text.charAt(offset - 1) === '=') { + const htmlSettings = settings?.html; + const options = merge(htmlSettings?.suggest, {}); + options.attributeDefaultValue = htmlSettings?.completion?.attributeDefaultValue ?? 'doublequotes'; + + return htmlLanguageService.doQuoteComplete(document, position, htmlDocuments.get(document), options); + } + } else if (kind === 'autoClose') { + if (offset > 0 && text.charAt(offset - 1).match(/[>\/]/g)) { + return htmlLanguageService.doTagComplete(document, position, htmlDocuments.get(document)); + } } return null; }, diff --git a/extensions/html-language-features/server/src/modes/javascriptMode.ts b/extensions/html-language-features/server/src/modes/javascriptMode.ts index e6571c5bac0ae..85b9a8ae2e058 100644 --- a/extensions/html-language-features/server/src/modes/javascriptMode.ts +++ b/extensions/html-language-features/server/src/modes/javascriptMode.ts @@ -387,83 +387,152 @@ function convertRange(document: TextDocument, span: { start: number | undefined, function convertKind(kind: string): CompletionItemKind { switch (kind) { - case 'primitive type': - case 'keyword': + case Kind.primitiveType: + case Kind.keyword: return CompletionItemKind.Keyword; - case 'var': - case 'local var': + + case Kind.const: + case Kind.let: + case Kind.variable: + case Kind.localVariable: + case Kind.alias: + case Kind.parameter: return CompletionItemKind.Variable; - case 'property': - case 'getter': - case 'setter': + + case Kind.memberVariable: + case Kind.memberGetAccessor: + case Kind.memberSetAccessor: return CompletionItemKind.Field; - case 'function': - case 'method': - case 'construct': - case 'call': - case 'index': + + case Kind.function: + case Kind.localFunction: return CompletionItemKind.Function; - case 'enum': + + case Kind.method: + case Kind.constructSignature: + case Kind.callSignature: + case Kind.indexSignature: + return CompletionItemKind.Method; + + case Kind.enum: return CompletionItemKind.Enum; - case 'module': + + case Kind.enumMember: + return CompletionItemKind.EnumMember; + + case Kind.module: + case Kind.externalModuleName: return CompletionItemKind.Module; - case 'class': + + case Kind.class: + case Kind.type: return CompletionItemKind.Class; - case 'interface': + + case Kind.interface: return CompletionItemKind.Interface; - case 'warning': + + case Kind.warning: + return CompletionItemKind.Text; + + case Kind.script: return CompletionItemKind.File; - } - return CompletionItemKind.Property; + case Kind.directory: + return CompletionItemKind.Folder; + + case Kind.string: + return CompletionItemKind.Constant; + + default: + return CompletionItemKind.Property; + } +} +const enum Kind { + alias = 'alias', + callSignature = 'call', + class = 'class', + const = 'const', + constructorImplementation = 'constructor', + constructSignature = 'construct', + directory = 'directory', + enum = 'enum', + enumMember = 'enum member', + externalModuleName = 'external module name', + function = 'function', + indexSignature = 'index', + interface = 'interface', + keyword = 'keyword', + let = 'let', + localFunction = 'local function', + localVariable = 'local var', + method = 'method', + memberGetAccessor = 'getter', + memberSetAccessor = 'setter', + memberVariable = 'property', + module = 'module', + primitiveType = 'primitive type', + script = 'script', + type = 'type', + variable = 'var', + warning = 'warning', + string = 'string', + parameter = 'parameter', + typeParameter = 'type parameter' } function convertSymbolKind(kind: string): SymbolKind { switch (kind) { - case 'var': - case 'local var': - case 'const': - return SymbolKind.Variable; - case 'function': - case 'local function': - return SymbolKind.Function; - case 'enum': - return SymbolKind.Enum; - case 'module': - return SymbolKind.Module; - case 'class': - return SymbolKind.Class; - case 'interface': - return SymbolKind.Interface; - case 'method': - return SymbolKind.Method; - case 'property': - case 'getter': - case 'setter': - return SymbolKind.Property; + case Kind.module: return SymbolKind.Module; + case Kind.class: return SymbolKind.Class; + case Kind.enum: return SymbolKind.Enum; + case Kind.enumMember: return SymbolKind.EnumMember; + case Kind.interface: return SymbolKind.Interface; + case Kind.indexSignature: return SymbolKind.Method; + case Kind.callSignature: return SymbolKind.Method; + case Kind.method: return SymbolKind.Method; + case Kind.memberVariable: return SymbolKind.Property; + case Kind.memberGetAccessor: return SymbolKind.Property; + case Kind.memberSetAccessor: return SymbolKind.Property; + case Kind.variable: return SymbolKind.Variable; + case Kind.let: return SymbolKind.Variable; + case Kind.const: return SymbolKind.Variable; + case Kind.localVariable: return SymbolKind.Variable; + case Kind.alias: return SymbolKind.Variable; + case Kind.function: return SymbolKind.Function; + case Kind.localFunction: return SymbolKind.Function; + case Kind.constructSignature: return SymbolKind.Constructor; + case Kind.constructorImplementation: return SymbolKind.Constructor; + case Kind.typeParameter: return SymbolKind.TypeParameter; + case Kind.string: return SymbolKind.String; + default: return SymbolKind.Variable; } - return SymbolKind.Variable; } -function convertOptions(options: FormattingOptions, formatSettings: any, initialIndentLevel: number): ts.FormatCodeOptions { +function convertOptions(options: FormattingOptions, formatSettings: any, initialIndentLevel: number): ts.FormatCodeSettings { return { - ConvertTabsToSpaces: options.insertSpaces, - TabSize: options.tabSize, - IndentSize: options.tabSize, - IndentStyle: ts.IndentStyle.Smart, - NewLineCharacter: '\n', - BaseIndentSize: options.tabSize * initialIndentLevel, - InsertSpaceAfterCommaDelimiter: Boolean(!formatSettings || formatSettings.insertSpaceAfterCommaDelimiter), - InsertSpaceAfterSemicolonInForStatements: Boolean(!formatSettings || formatSettings.insertSpaceAfterSemicolonInForStatements), - InsertSpaceBeforeAndAfterBinaryOperators: Boolean(!formatSettings || formatSettings.insertSpaceBeforeAndAfterBinaryOperators), - InsertSpaceAfterKeywordsInControlFlowStatements: Boolean(!formatSettings || formatSettings.insertSpaceAfterKeywordsInControlFlowStatements), - InsertSpaceAfterFunctionKeywordForAnonymousFunctions: Boolean(!formatSettings || formatSettings.insertSpaceAfterFunctionKeywordForAnonymousFunctions), - InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis), - InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets), - InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces), - InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces), - PlaceOpenBraceOnNewLineForControlBlocks: Boolean(formatSettings && formatSettings.placeOpenBraceOnNewLineForFunctions), - PlaceOpenBraceOnNewLineForFunctions: Boolean(formatSettings && formatSettings.placeOpenBraceOnNewLineForControlBlocks) + convertTabsToSpaces: options.insertSpaces, + tabSize: options.tabSize, + indentSize: options.tabSize, + indentStyle: ts.IndentStyle.Smart, + newLineCharacter: '\n', + baseIndentSize: options.tabSize * initialIndentLevel, + insertSpaceAfterCommaDelimiter: Boolean(!formatSettings || formatSettings.insertSpaceAfterCommaDelimiter), + insertSpaceAfterConstructor: Boolean(formatSettings && formatSettings.insertSpaceAfterConstructor), + insertSpaceAfterSemicolonInForStatements: Boolean(!formatSettings || formatSettings.insertSpaceAfterSemicolonInForStatements), + insertSpaceBeforeAndAfterBinaryOperators: Boolean(!formatSettings || formatSettings.insertSpaceBeforeAndAfterBinaryOperators), + insertSpaceAfterKeywordsInControlFlowStatements: Boolean(!formatSettings || formatSettings.insertSpaceAfterKeywordsInControlFlowStatements), + insertSpaceAfterFunctionKeywordForAnonymousFunctions: Boolean(!formatSettings || formatSettings.insertSpaceAfterFunctionKeywordForAnonymousFunctions), + insertSpaceBeforeFunctionParenthesis: Boolean(formatSettings && formatSettings.insertSpaceBeforeFunctionParenthesis), + insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis), + insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets), + insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces), + insertSpaceAfterOpeningAndBeforeClosingEmptyBraces: Boolean(!formatSettings || formatSettings.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces), + insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces), + insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces), + insertSpaceAfterTypeAssertion: Boolean(formatSettings && formatSettings.insertSpaceAfterTypeAssertion), + placeOpenBraceOnNewLineForControlBlocks: Boolean(formatSettings && formatSettings.placeOpenBraceOnNewLineForFunctions), + placeOpenBraceOnNewLineForFunctions: Boolean(formatSettings && formatSettings.placeOpenBraceOnNewLineForControlBlocks), + semicolons: formatSettings?.semicolons }; } diff --git a/extensions/html-language-features/server/src/modes/languageModes.ts b/extensions/html-language-features/server/src/modes/languageModes.ts index eda0dd8376081..7709256121ce8 100644 --- a/extensions/html-language-features/server/src/modes/languageModes.ts +++ b/extensions/html-language-features/server/src/modes/languageModes.ts @@ -21,7 +21,7 @@ import { getCSSMode } from './cssMode'; import { getDocumentRegions, HTMLDocumentRegions } from './embeddedSupport'; import { getHTMLMode } from './htmlMode'; import { getJavaScriptMode } from './javascriptMode'; -import { RequestService } from '../requests'; +import { FileSystemProvider } from '../requests'; export { WorkspaceFolder, CompletionItem, CompletionList, CompletionItemKind, Definition, Diagnostic, DocumentHighlight, DocumentHighlightKind, @@ -72,7 +72,7 @@ export interface LanguageMode { format?: (document: TextDocument, range: Range, options: FormattingOptions, settings?: Settings) => Promise; findDocumentColors?: (document: TextDocument) => Promise; getColorPresentations?: (document: TextDocument, color: Color, range: Range) => Promise; - doAutoClose?: (document: TextDocument, position: Position) => Promise; + doAutoInsert?: (document: TextDocument, position: Position, kind: 'autoClose' | 'autoQuote') => Promise; findMatchingTagPosition?: (document: TextDocument, position: Position) => Promise; getFoldingRanges?: (document: TextDocument) => Promise; onDocumentRemoved(document: TextDocument): void; @@ -97,7 +97,7 @@ export interface LanguageModeRange extends Range { attributeValue?: boolean; } -export function getLanguageModes(supportedLanguages: { [languageId: string]: boolean; }, workspace: Workspace, clientCapabilities: ClientCapabilities, requestService: RequestService): LanguageModes { +export function getLanguageModes(supportedLanguages: { [languageId: string]: boolean; }, workspace: Workspace, clientCapabilities: ClientCapabilities, requestService: FileSystemProvider): LanguageModes { const htmlLanguageService = getHTMLLanguageService({ clientCapabilities, fileSystemProvider: requestService }); const cssLanguageService = getCSSLanguageService({ clientCapabilities, fileSystemProvider: requestService }); diff --git a/extensions/html-language-features/server/src/node/htmlServerMain.ts b/extensions/html-language-features/server/src/node/htmlServerMain.ts index faf82b3c40c13..0367e11a2209f 100644 --- a/extensions/html-language-features/server/src/node/htmlServerMain.ts +++ b/extensions/html-language-features/server/src/node/htmlServerMain.ts @@ -6,7 +6,7 @@ import { createConnection, Connection, Disposable } from 'vscode-languageserver/node'; import { formatError } from '../utils/runner'; import { RuntimeEnvironment, startServer } from '../htmlServer'; -import { getNodeFSRequestService } from './nodeFs'; +import { getNodeFileFS } from './nodeFs'; // Create a connection for the server. @@ -30,7 +30,7 @@ const runtime: RuntimeEnvironment = { return { dispose: () => clearTimeout(handle) }; } }, - file: getNodeFSRequestService() + fileFs: getNodeFileFS() }; startServer(connection, runtime); diff --git a/extensions/html-language-features/server/src/node/nodeFs.ts b/extensions/html-language-features/server/src/node/nodeFs.ts index c7b1301296d4a..9bab4cf2913fd 100644 --- a/extensions/html-language-features/server/src/node/nodeFs.ts +++ b/extensions/html-language-features/server/src/node/nodeFs.ts @@ -3,32 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { RequestService, getScheme } from '../requests'; +import { FileSystemProvider, getScheme } from '../requests'; import { URI as Uri } from 'vscode-uri'; import * as fs from 'fs'; import { FileType } from 'vscode-css-languageservice'; -export function getNodeFSRequestService(): RequestService { +export function getNodeFileFS(): FileSystemProvider { function ensureFileUri(location: string) { if (getScheme(location) !== 'file') { - throw new Error('fileRequestService can only handle file URLs'); + throw new Error('fileSystemProvider can only handle file URLs'); } } return { - getContent(location: string, encoding?: string) { - ensureFileUri(location); - return new Promise((c, e) => { - const uri = Uri.parse(location); - fs.readFile(uri.fsPath, encoding, (err, buf) => { - if (err) { - return e(err); - } - c(buf.toString()); - - }); - }); - }, stat(location: string) { ensureFileUri(location); return new Promise((c, e) => { diff --git a/extensions/html-language-features/server/src/requests.ts b/extensions/html-language-features/server/src/requests.ts index 8d741faebf373..9ece22ac66e96 100644 --- a/extensions/html-language-features/server/src/requests.ts +++ b/extensions/html-language-features/server/src/requests.ts @@ -7,9 +7,6 @@ import { URI } from 'vscode-uri'; import { RequestType, Connection } from 'vscode-languageserver'; import { RuntimeEnvironment } from './htmlServer'; -export namespace FsContentRequest { - export const type: RequestType<{ uri: string; encoding?: string; }, string, any> = new RequestType('fs/content'); -} export namespace FsStatRequest { export const type: RequestType = new RequestType('fs/stat'); } @@ -56,45 +53,27 @@ export interface FileStat { size: number; } -export interface RequestService { - getContent(uri: string, encoding?: string): Promise; - +export interface FileSystemProvider { stat(uri: string): Promise; readDirectory(uri: string): Promise<[string, FileType][]>; } -export function getRequestService(handledSchemas: string[], connection: Connection, runtime: RuntimeEnvironment): RequestService { - const builtInHandlers: { [protocol: string]: RequestService | undefined } = {}; - for (let protocol of handledSchemas) { - if (protocol === 'file') { - builtInHandlers[protocol] = runtime.file; - } else if (protocol === 'http' || protocol === 'https') { - builtInHandlers[protocol] = runtime.http; - } - } +export function getFileSystemProvider(handledSchemas: string[], connection: Connection, runtime: RuntimeEnvironment): FileSystemProvider { + const fileFs = runtime.fileFs && handledSchemas.indexOf('file') !== -1 ? runtime.fileFs : undefined; return { async stat(uri: string): Promise { - const handler = builtInHandlers[getScheme(uri)]; - if (handler) { - return handler.stat(uri); + if (fileFs && uri.startsWith('file:')) { + return fileFs.stat(uri); } const res = await connection.sendRequest(FsStatRequest.type, uri.toString()); return res; }, readDirectory(uri: string): Promise<[string, FileType][]> { - const handler = builtInHandlers[getScheme(uri)]; - if (handler) { - return handler.readDirectory(uri); + if (fileFs && uri.startsWith('file:')) { + return fileFs.readDirectory(uri); } return connection.sendRequest(FsReadDirRequest.type, uri.toString()); - }, - getContent(uri: string, encoding?: string): Promise { - const handler = builtInHandlers[getScheme(uri)]; - if (handler) { - return handler.getContent(uri, encoding); - } - return connection.sendRequest(FsContentRequest.type, { uri: uri.toString(), encoding }); } }; } diff --git a/extensions/html-language-features/server/src/test/completions.test.ts b/extensions/html-language-features/server/src/test/completions.test.ts index aaf3cb5ec6b84..b64d7dee8604c 100644 --- a/extensions/html-language-features/server/src/test/completions.test.ts +++ b/extensions/html-language-features/server/src/test/completions.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import * as path from 'path'; import { URI } from 'vscode-uri'; import { getLanguageModes, WorkspaceFolder, TextDocument, CompletionList, CompletionItemKind, ClientCapabilities, TextEdit } from '../modes/languageModes'; -import { getNodeFSRequestService } from '../node/nodeFs'; +import { getNodeFileFS } from '../node/nodeFs'; import { getDocumentContext } from '../utils/documentContext'; export interface ItemDescription { label: string; @@ -59,7 +59,7 @@ export async function testCompletionFor(value: string, expected: { count?: numbe let position = document.positionAt(offset); const context = getDocumentContext(uri, workspace.folders); - const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService()); + const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFileFS()); const mode = languageModes.getModeAtPosition(document, position)!; let list = await mode.doComplete!(document, position, context); diff --git a/extensions/html-language-features/server/src/test/folding.test.ts b/extensions/html-language-features/server/src/test/folding.test.ts index 7bf90bb10d544..44aaea9026cf5 100644 --- a/extensions/html-language-features/server/src/test/folding.test.ts +++ b/extensions/html-language-features/server/src/test/folding.test.ts @@ -8,7 +8,7 @@ import * as assert from 'assert'; import { getFoldingRanges } from '../modes/htmlFolding'; import { TextDocument, getLanguageModes } from '../modes/languageModes'; import { ClientCapabilities } from 'vscode-css-languageservice'; -import { getNodeFSRequestService } from '../node/nodeFs'; +import { getNodeFileFS } from '../node/nodeFs'; interface ExpectedIndentRange { startLine: number; @@ -22,7 +22,7 @@ async function assertRanges(lines: string[], expected: ExpectedIndentRange[], me settings: {}, folders: [{ name: 'foo', uri: 'test://foo' }] }; - const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService()); + const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFileFS()); const actual = await getFoldingRanges(languageModes, document, nRanges, null); let actualRanges = []; diff --git a/extensions/html-language-features/server/src/test/formatting.test.ts b/extensions/html-language-features/server/src/test/formatting.test.ts index 107001c7c007c..140942868e443 100644 --- a/extensions/html-language-features/server/src/test/formatting.test.ts +++ b/extensions/html-language-features/server/src/test/formatting.test.ts @@ -10,7 +10,7 @@ import * as assert from 'assert'; import { getLanguageModes, TextDocument, Range, FormattingOptions, ClientCapabilities } from '../modes/languageModes'; import { format } from '../modes/formatting'; -import { getNodeFSRequestService } from '../node/nodeFs'; +import { getNodeFileFS } from '../node/nodeFs'; suite('HTML Embedded Formatting', () => { @@ -19,7 +19,7 @@ suite('HTML Embedded Formatting', () => { settings: options, folders: [{ name: 'foo', uri: 'test://foo' }] }; - const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService()); + const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFileFS()); let rangeStartOffset = value.indexOf('|'); let rangeEndOffset; @@ -77,7 +77,7 @@ suite('HTML Embedded Formatting', () => { }); test('HTML & Multiple Scripts', async () => { - await assertFormat('\n', '\n\n\n \n \n\n\n'); + await assertFormat('\n', '\n\n\n \n \n\n\n'); }); test('HTML & Styles', async () => { @@ -120,7 +120,7 @@ suite('HTML Embedded Formatting', () => { '', '', ' + - +
    diff --git a/src/vs/code/browser/workbench/workbench.html b/src/vs/code/browser/workbench/workbench.html index 4f97f5b635da7..03dcf66b9f68c 100644 --- a/src/vs/code/browser/workbench/workbench.html +++ b/src/vs/code/browser/workbench/workbench.html @@ -1,6 +1,6 @@ - + - - + + diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index a64d5e36de1e8..4d1fe2c6a0499 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -4,46 +4,21 @@ *--------------------------------------------------------------------------------------------*/ 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 { parse } from 'vs/base/common/marshalling'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { isEqual } from 'vs/base/common/resources'; -import { encodePath, URI, UriComponents } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; +import { URI, UriComponents } from 'vs/base/common/uri'; 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'; +import { posix } from 'vs/base/common/path'; +import { ltrim } from 'vs/base/common/strings'; +import { create, ICredentialsProvider, IURLCallbackProvider, IWorkbenchConstructionOptions, IWorkspace, IWorkspaceProvider } from 'vs/workbench/workbench.web.main'; import { equals as arrayEquals } from 'vs/base/common/arrays'; -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)}`; - }); - } - - /** - * Preserve the current path so it works with reverse proxies serving behind a - * sub-path. - * @author coder - */ - path = (window.location.pathname + "/" + path).replace(/\/\/+/g, "/") - return URI.parse(window.location.href).with({ path, query }); -} - interface ICredential { service: string; account: string; @@ -60,7 +35,7 @@ interface IToken { class LocalStorageCredentialsProvider implements ICredentialsProvider { - static readonly CREDENTIALS_OPENED_KEY = 'credentials.provider'; + private static readonly CREDENTIALS_STORAGE_KEY = 'credentials.provider'; private readonly authService: string | undefined; @@ -147,7 +122,7 @@ class LocalStorageCredentialsProvider implements ICredentialsProvider { private get credentials(): ICredential[] { if (!this._credentials) { try { - const serializedCredentials = window.localStorage.getItem(LocalStorageCredentialsProvider.CREDENTIALS_OPENED_KEY); + const serializedCredentials = window.localStorage.getItem(LocalStorageCredentialsProvider.CREDENTIALS_STORAGE_KEY); if (serializedCredentials) { this._credentials = JSON.parse(serializedCredentials); } @@ -164,7 +139,7 @@ class LocalStorageCredentialsProvider implements ICredentialsProvider { } private save(): void { - window.localStorage.setItem(LocalStorageCredentialsProvider.CREDENTIALS_OPENED_KEY, JSON.stringify(this.credentials)); + window.localStorage.setItem(LocalStorageCredentialsProvider.CREDENTIALS_STORAGE_KEY, JSON.stringify(this.credentials)); } async getPassword(service: string, account: string): Promise { @@ -257,104 +232,230 @@ class LocalStorageCredentialsProvider implements ICredentialsProvider { } async clear(): Promise { - window.localStorage.removeItem(LocalStorageCredentialsProvider.CREDENTIALS_OPENED_KEY); + window.localStorage.removeItem(LocalStorageCredentialsProvider.CREDENTIALS_STORAGE_KEY); } } -class PollingURLCallbackProvider extends Disposable implements IURLCallbackProvider { +class LocalStorageURLCallbackProvider extends Disposable implements IURLCallbackProvider { - static readonly FETCH_INTERVAL = 500; // fetch every 500ms - static readonly FETCH_TIMEOUT = 5 * 60 * 1000; // ...but stop after 5min + 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); + create(options: Partial = {}): URI { + const id = ++LocalStorageURLCallbackProvider.REQUEST_ID; + const queryParams: string[] = [`vscode-reqid=${id}`]; - const { scheme, authority, path, query, fragment } = options ? options : { scheme: undefined, authority: undefined, path: undefined, query: undefined, fragment: undefined }; + for (const key of LocalStorageURLCallbackProvider.QUERY_KEYS) { + const value = options[key]; - if (scheme) { - queryValues.set(PollingURLCallbackProvider.QUERY_KEYS.SCHEME, scheme); + if (value) { + queryParams.push(`vscode-${key}=${encodeURIComponent(value)}`); + } } - if (authority) { - queryValues.set(PollingURLCallbackProvider.QUERY_KEYS.AUTHORITY, authority); - } + // 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}]`; + window.localStorage.removeItem(key); - if (path) { - queryValues.set(PollingURLCallbackProvider.QUERY_KEYS.PATH, path); + this.pendingCallbacks.add(id); + this.startListening(); } - if (query) { - queryValues.set(PollingURLCallbackProvider.QUERY_KEYS.QUERY, query); - } + return URI.parse(window.location.href).with({ path: '/callback', query: queryParams.join('&') }); + } - if (fragment) { - queryValues.set(PollingURLCallbackProvider.QUERY_KEYS.FRAGMENT, fragment); + private startListening(): void { + if (this.onDidChangeLocalStorageDisposable) { + return; } - // Start to poll on the callback being fired - this.periodicFetchCallback(requestId, Date.now()); + const fn = () => this.onDidChangeLocalStorage(); + window.addEventListener('storage', fn); + this.onDidChangeLocalStorageDisposable = { dispose: () => window.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 = window.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); + window.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 readonly LAST_WORKSPACE_STORAGE_KEY = 'workspaces.lastOpened'; + + private static QUERY_PARAM_EMPTY_WINDOW = 'ew'; + private static QUERY_PARAM_FOLDER = 'folder'; + private static QUERY_PARAM_WORKSPACE = 'workspace'; + + private static QUERY_PARAM_PAYLOAD = 'payload'; + + static create(config: IWorkbenchConstructionOptions & { folderUri?: UriComponents, workspaceUri?: UriComponents }) { + let foundWorkspace = false; + let workspace: IWorkspace; + let payload = Object.create(null); + + const query = new URL(document.location.href).searchParams; + query.forEach((value, key) => { + switch (key) { + + // Folder + case WorkspaceProvider.QUERY_PARAM_FOLDER: + console.log("do we have a query param folder?") + if (config.remoteAuthority && value.startsWith(posix.sep)) { + console.log("do we get in the remote authority block here") + // 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; + } + }); + + // NOTE@coder we have our own logic to determine last open workspace + // This code will never run since we have our redirect logic before + // getting here. + // If we do want to use this, we need to add a flag check. + // -jsjoeio + const DISABLE_LAST_OPENED = true + + if (!foundWorkspace) { + if (config.folderUri) { + console.log("no workspace found") + workspace = { folderUri: URI.revive(config.folderUri) }; + console.log("what is workspace", workspace) + } else if (config.workspaceUri) { + workspace = { workspaceUri: URI.revive(config.workspaceUri) }; + } else if (!DISABLE_LAST_OPENED) { + workspace = (() => { + const lastWorkspaceRaw = window.localStorage.getItem(WorkspaceProvider.LAST_WORKSPACE_STORAGE_KEY); + if (lastWorkspaceRaw) { + try { + return parse(lastWorkspaceRaw); // use marshalling#parse() to revive potential URIs + } catch (error) { + // Ignore + } + } - static QUERY_PARAM_PAYLOAD = 'payload'; + return undefined; + })(); + } + } + + // Keep this as last opened workspace in storage + if (workspace) { + window.localStorage.setItem(WorkspaceProvider.LAST_WORKSPACE_STORAGE_KEY, JSON.stringify(workspace)); + } else { + window.localStorage.removeItem(WorkspaceProvider.LAST_WORKSPACE_STORAGE_KEY); + } + + return new WorkspaceProvider(workspace, payload, config); + } readonly trusted = true; - constructor( + 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 { if (options?.reuse && !options.payload && this.isSame(this.workspace, workspace)) { @@ -388,28 +489,36 @@ class WorkspaceProvider implements IWorkspaceProvider { targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_EMPTY_WINDOW}=true`; } - // Folder - /** - * Modified to print as a human-readable string for file paths. - * @author coder - */ else if (isFolderToOpen(workspace)) { - const target = workspace.folderUri.scheme === Schemas.vscodeRemote - ? encodePath(workspace.folderUri.path) - : encodeURIComponent(workspace.folderUri.toString()); - targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_FOLDER}=${target}`; + let queryParamFolder: string; + if (this.config.remoteAuthority && workspace.folderUri.scheme === Schemas.vscodeRemote) { + // when connected to a remote and having a folder + // for that remote, only use the path as query + // value to form shorter, nicer URLs. + // ensure paths are absolute (begin with `/`) + // clipboard: ltrim(workspace.folderUri.path, posix.sep) + queryParamFolder = `${posix.sep}${ltrim(workspace.folderUri.path, posix.sep)}`; + } else { + queryParamFolder = encodeURIComponent(workspace.folderUri.toString(true)); + } + + targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_FOLDER}=${queryParamFolder}`; } // Workspace - /** - * Modified to print as a human-readable string for file paths. - * @author coder - */ else if (isWorkspaceToOpen(workspace)) { - const target = workspace.workspaceUri.scheme === Schemas.vscodeRemote - ? encodePath(workspace.workspaceUri.path) - : encodeURIComponent(workspace.workspaceUri.toString()); - targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_WORKSPACE}=${target}`; + let queryParamWorkspace: string; + if (this.config.remoteAuthority && workspace.workspaceUri.scheme === Schemas.vscodeRemote) { + // when connected to a remote and having a workspace + // for that remote, only use the path as query + // value to form shorter, nicer URLs. + // ensure paths are absolute (begin with `/`) + queryParamWorkspace = `${posix.sep}${ltrim(workspace.workspaceUri.path, posix.sep)}`; + } else { + queryParamWorkspace = encodeURIComponent(workspace.workspaceUri.toString(true)); + } + + targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_WORKSPACE}=${queryParamWorkspace}`; } // Append payload if any @@ -451,43 +560,28 @@ class WorkspaceProvider implements IWorkspaceProvider { } } -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; - } +function doCreateUri(path: string, queryValues: Map): URI { + let query: string | undefined = undefined; - if (uri?.scheme === 'github' || uri?.scheme === 'codespace') { - [repositoryOwner, repositoryName] = uri.authority.split('+'); + if (queryValues) { + let index = 0; + queryValues.forEach((value, key) => { + if (!query) { + query = ''; } - } - // 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"); - } + const prefix = (index++ === 0) ? '' : '&'; + query += `${prefix}${key}=${encodeURIComponent(value)}`; + }); } + + /** + * Preserve the current path so it works with reverse proxies serving behind a + * sub-path. + * @author coder + */ + path = (window.location.pathname + "/" + path).replace(/\/\/+/g, "/") + return URI.parse(window.location.href).with({ path, query }); } (function () { @@ -498,15 +592,6 @@ class WindowIndicator implements IWindowIndicator { 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; - /** * If the value begins with a slash assume it is a file path and convert it to * use the vscode-remote scheme. @@ -517,129 +602,11 @@ class WindowIndicator implements IWindowIndicator { * * @author coder */ - const remoteAuthority = location.host - const toRemote = (value: string): string => { - if (value.startsWith('/')) { - return 'vscode-remote://' + remoteAuthority + value; - } - return value; - }; - - const query = new URL(document.location.href).searchParams; - query.forEach((value, key) => { - switch (key) { - // Folder - case WorkspaceProvider.QUERY_PARAM_FOLDER: - /** - * Handle URIs that we previously left unencoded and de-schemed. - * - * @author coder - */ - value = toRemote(value); - workspace = { folderUri: URI.parse(value) }; - foundWorkspace = true; - break; - - // Workspace - case WorkspaceProvider.QUERY_PARAM_WORKSPACE: - /** - * Handle URIs that we previously left unencoded and de-schemed. - * - * @author coder - */ - value = toRemote(value); - 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: undefined | IWelcomeBanner = undefined; - // 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; + const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents, workspaceUri?: UriComponents } = { ...JSON.parse(configElementAttribute), remoteAuthority: location.host } // Finally create workbench create(document.body, { ...config, - /** - * Ensure the remote authority points to the current address since we cannot - * determine this reliably on the backend. - * @author coder - */ - remoteAuthority, /** * Override relative URLs in the product configuration against the window * location as necessary. Only paths that must be absolute need to be @@ -659,17 +626,11 @@ class WindowIndicator implements IWindowIndicator { ).toString(), ), }, - developmentOptions: { - logLevel: logLevel ? parseLogLevel(logLevel) : undefined, - ...config.developmentOptions - }, - settingsSyncOptions, - homeIndicator, - windowIndicator, - welcomeBanner, - productQualityChangeHandler, - workspaceProvider, - urlCallbackProvider: new PollingURLCallbackProvider(), - credentialsProvider: new LocalStorageCredentialsProvider() + settingsSyncOptions: config.settingsSyncOptions ? { + enabled: config.settingsSyncOptions.enabled, + } : undefined, + workspaceProvider: WorkspaceProvider.create(config), + urlCallbackProvider: new LocalStorageURLCallbackProvider(), + credentialsProvider: config.remoteAuthority ? undefined : new LocalStorageCredentialsProvider() // with a remote, we don't use a local credentials provider }); })(); diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/deprecatedExtensionsCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/deprecatedExtensionsCleaner.ts deleted file mode 100644 index f02aeb652053f..0000000000000 --- a/src/vs/code/electron-browser/sharedProcess/contrib/deprecatedExtensionsCleaner.ts +++ /dev/null @@ -1,25 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable } from 'vs/base/common/lifecycle'; -import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; - -export class DeprecatedExtensionsCleaner extends Disposable { - - constructor( - @IExtensionManagementService private readonly extensionManagementService: ExtensionManagementService - ) { - super(); - - this._register(extensionManagementService); // TODO@sandy081 this seems fishy - - this.cleanUpDeprecatedExtensions(); - } - - private cleanUpDeprecatedExtensions(): void { - this.extensionManagementService.removeDeprecatedExtensions(); - } -} diff --git a/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts b/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts new file mode 100644 index 0000000000000..f64b77fc74920 --- /dev/null +++ b/src/vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; +import { migrateUnsupportedExtensions } from 'vs/platform/extensionManagement/common/unsupportedExtensionsMigration'; +import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { ILogService } from 'vs/platform/log/common/log'; + +export class ExtensionsCleaner extends Disposable { + + constructor( + @IExtensionManagementService extensionManagementService: ExtensionManagementService, + @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, + @IExtensionStorageService extensionStorageService: IExtensionStorageService, + @IGlobalExtensionEnablementService extensionEnablementService: IGlobalExtensionEnablementService, + @ILogService logService: ILogService, + ) { + super(); + extensionManagementService.removeDeprecatedExtensions(); + migrateUnsupportedExtensions(extensionManagementService, extensionGalleryService, extensionStorageService, extensionEnablementService, logService); + } +} diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index e3077d66510c5..7581b0bcdbdcf 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -14,7 +14,7 @@ import { URI } from 'vs/base/common/uri'; import { ProxyChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; import { Server as MessagePortServer } from 'vs/base/parts/ipc/electron-browser/ipc.mp'; import { CodeCacheCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/codeCacheCleaner'; -import { DeprecatedExtensionsCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/deprecatedExtensionsCleaner'; +import { ExtensionsCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/extensionsCleaner'; import { LanguagePackCachedDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner'; import { LocalizationsUpdater } from 'vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater'; import { LogsDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner'; @@ -28,7 +28,7 @@ import { DiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsServ import { IDownloadService } from 'vs/platform/download/common/download'; import { DownloadService } from 'vs/platform/download/common/downloadService'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; +import { SharedProcessEnvironmentService } from 'vs/platform/sharedProcess/node/sharedProcessEnvironmentService'; import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { IExtensionGalleryService, IExtensionManagementService, IExtensionTipsService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -69,16 +69,15 @@ import { CustomEndpointTelemetryService } from 'vs/platform/telemetry/node/custo import { LocalReconnectConstants, TerminalIpcChannels, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { ILocalPtyService } from 'vs/platform/terminal/electron-sandbox/terminal'; import { PtyHostService } from 'vs/platform/terminal/node/ptyHostService'; -import { ExtensionsStorageSyncService, IExtensionsStorageSyncService } from 'vs/platform/userDataSync/common/extensionsStorageSync'; +import { ExtensionStorageService, IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; import { IgnoredExtensionsManagementService, IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; -import { UserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; -import { IUserDataAutoSyncEnablementService, IUserDataSyncBackupStoreService, IUserDataSyncLogService, IUserDataSyncResourceEnablementService, IUserDataSyncService, IUserDataSyncStoreManagementService, IUserDataSyncStoreService, IUserDataSyncUtilService, registerConfiguration as registerUserDataSyncConfiguration } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncBackupStoreService, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncService, IUserDataSyncStoreManagementService, IUserDataSyncStoreService, IUserDataSyncUtilService, registerConfiguration as registerUserDataSyncConfiguration } from 'vs/platform/userDataSync/common/userDataSync'; import { IUserDataSyncAccountService, UserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; import { UserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSyncBackupStoreService'; import { UserDataAutoSyncChannel, UserDataSyncAccountServiceChannel, UserDataSyncMachinesServiceChannel, UserDataSyncStoreManagementServiceChannel, UserDataSyncUtilServiceClient } from 'vs/platform/userDataSync/common/userDataSyncIpc'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; import { IUserDataSyncMachinesService, UserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; -import { UserDataSyncResourceEnablementService } from 'vs/platform/userDataSync/common/userDataSyncResourceEnablementService'; +import { UserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSyncEnablementService'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { UserDataSyncChannel } from 'vs/platform/userDataSync/common/userDataSyncServiceIpc'; import { UserDataSyncStoreManagementService, UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; @@ -86,16 +85,20 @@ import { UserDataAutoSyncService } from 'vs/platform/userDataSync/electron-sandb import { ActiveWindowManager } from 'vs/platform/windows/node/windowTracker'; import { ISignService } from 'vs/platform/sign/common/sign'; import { SignService } from 'vs/platform/sign/node/signService'; -import { ISharedTunnelsService } from 'vs/platform/remote/common/tunnel'; -import { SharedTunnelsService } from 'vs/platform/remote/node/tunnelService'; +import { ISharedTunnelsService } from 'vs/platform/tunnel/common/tunnel'; +import { SharedTunnelsService } from 'vs/platform/tunnel/node/tunnelService'; import { ipcSharedProcessTunnelChannelName, ISharedProcessTunnelService } from 'vs/platform/remote/common/sharedProcessTunnelService'; -import { SharedProcessTunnelService } from 'vs/platform/remote/node/sharedProcessTunnelService'; +import { SharedProcessTunnelService } from 'vs/platform/tunnel/node/sharedProcessTunnelService'; import { ipcSharedProcessWorkerChannelName, ISharedProcessWorkerConfiguration, ISharedProcessWorkerService } from 'vs/platform/sharedProcess/common/sharedProcessWorkerService'; import { SharedProcessWorkerService } from 'vs/platform/sharedProcess/electron-browser/sharedProcessWorkerService'; -import { IUserConfigurationFileService, UserConfigurationFileServiceId } from 'vs/platform/configuration/common/userConfigurationFileService'; import { AssignmentService } from 'vs/platform/assignment/common/assignmentService'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; +import { isLinux } from 'vs/base/common/platform'; +import { FileUserDataProvider } from 'vs/platform/userData/common/fileUserDataProvider'; +import { DiskFileSystemProviderClient, LOCAL_FILE_SYSTEM_CHANNEL_NAME } from 'vs/platform/files/common/diskFileSystemProviderClient'; +import { InspectProfilingService as V8InspectProfilingService } from 'vs/platform/profiling/node/profilingService'; +import { IV8InspectProfilingService } from 'vs/platform/profiling/common/profiling'; class SharedProcessMain extends Disposable { @@ -160,7 +163,7 @@ class SharedProcessMain extends Disposable { instantiationService.createInstance(StorageDataCleaner, this.configuration.backupWorkspacesPath), instantiationService.createInstance(LogsDataCleaner), instantiationService.createInstance(LocalizationsUpdater), - instantiationService.createInstance(DeprecatedExtensionsCleaner) + instantiationService.createInstance(ExtensionsCleaner) )); } @@ -177,7 +180,7 @@ class SharedProcessMain extends Disposable { services.set(IMainProcessService, mainProcessService); // Environment - const environmentService = new NativeEnvironmentService(this.configuration.args, productService); + const environmentService = new SharedProcessEnvironmentService(this.configuration.args, productService); services.set(INativeEnvironmentService, environmentService); // Logger @@ -205,6 +208,18 @@ class SharedProcessMain extends Disposable { const diskFileSystemProvider = this._register(new DiskFileSystemProvider(logService)); fileService.registerProvider(Schemas.file, diskFileSystemProvider); + const userDataFileSystemProvider = this._register(new FileUserDataProvider( + Schemas.file, + // Specifically for user data, use the disk file system provider + // from the main process to enable atomic read/write operations. + // Since user data can change very frequently across multiple + // processes, we want a single process handling these operations. + this._register(new DiskFileSystemProviderClient(mainProcessService.getChannel(LOCAL_FILE_SYSTEM_CHANNEL_NAME), { pathCaseSensitive: isLinux })), + Schemas.userData, + logService + )); + fileService.registerProvider(Schemas.userData, userDataFileSystemProvider); + // Configuration const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService)); services.set(IConfigurationService, configurationService); @@ -220,9 +235,6 @@ class SharedProcessMain extends Disposable { storageService.initialize() ]); - // User Configuration File - services.set(IUserConfigurationFileService, ProxyChannel.toService(mainProcessService.getChannel(UserConfigurationFileServiceId))); - // URI Identity services.set(IUriIdentityService, new UriIdentityService(fileService)); @@ -232,6 +244,9 @@ class SharedProcessMain extends Disposable { // Checksum services.set(IChecksumService, new SyncDescriptor(ChecksumService)); + // V8 Inspect profiler + services.set(IV8InspectProfilingService, new SyncDescriptor(V8InspectProfilingService)); + // Native Host const nativeHostService = ProxyChannel.toService(mainProcessService.getChannel('nativeHost'), { context: this.configuration.windowId }); services.set(INativeHostService, nativeHostService); @@ -311,13 +326,12 @@ class SharedProcessMain extends Disposable { services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(this.server.getChannel('userDataSyncUtil', client => client.ctx !== 'main'))); services.set(IGlobalExtensionEnablementService, new SyncDescriptor(GlobalExtensionEnablementService)); services.set(IIgnoredExtensionsManagementService, new SyncDescriptor(IgnoredExtensionsManagementService)); - services.set(IExtensionsStorageSyncService, new SyncDescriptor(ExtensionsStorageSyncService)); + services.set(IExtensionStorageService, new SyncDescriptor(ExtensionStorageService)); services.set(IUserDataSyncStoreManagementService, new SyncDescriptor(UserDataSyncStoreManagementService)); services.set(IUserDataSyncStoreService, new SyncDescriptor(UserDataSyncStoreService)); services.set(IUserDataSyncMachinesService, new SyncDescriptor(UserDataSyncMachinesService)); services.set(IUserDataSyncBackupStoreService, new SyncDescriptor(UserDataSyncBackupStoreService)); - services.set(IUserDataAutoSyncEnablementService, new SyncDescriptor(UserDataAutoSyncEnablementService)); - services.set(IUserDataSyncResourceEnablementService, new SyncDescriptor(UserDataSyncResourceEnablementService)); + services.set(IUserDataSyncEnablementService, new SyncDescriptor(UserDataSyncEnablementService)); services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService)); const ptyHostService = new PtyHostService({ @@ -367,6 +381,10 @@ class SharedProcessMain extends Disposable { const checksumChannel = ProxyChannel.fromService(accessor.get(IChecksumService)); this.server.registerChannel('checksum', checksumChannel); + // Profiling + const profilingChannel = ProxyChannel.fromService(accessor.get(IV8InspectProfilingService)); + this.server.registerChannel('v8InspectProfiling', profilingChannel); + // Settings Sync const userDataSyncMachineChannel = new UserDataSyncMachinesServiceChannel(accessor.get(IUserDataSyncMachinesService)); this.server.registerChannel('userDataSyncMachines', userDataSyncMachineChannel); diff --git a/src/vs/code/electron-browser/workbench/workbench.html b/src/vs/code/electron-browser/workbench/workbench.html index 006d06ac4a9ea..47066f520be18 100644 --- a/src/vs/code/electron-browser/workbench/workbench.html +++ b/src/vs/code/electron-browser/workbench/workbench.html @@ -3,8 +3,8 @@ - - + + diff --git a/src/vs/code/electron-browser/workbench/workbench.js b/src/vs/code/electron-browser/workbench/workbench.js index f0235cf89a1f9..d83fb9abcd32c 100644 --- a/src/vs/code/electron-browser/workbench/workbench.js +++ b/src/vs/code/electron-browser/workbench/workbench.js @@ -29,7 +29,7 @@ performance.mark('code/didLoadWorkbenchMain'); // @ts-ignore - return require('vs/workbench/electron-browser/desktop.main').main(configuration); + return require('vs/workbench/electron-sandbox/desktop.main').main(configuration); }, { configureDeveloperSettings: function (windowConfig) { diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 4baf8158a5928..efa47fecfbeba 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { app, BrowserWindow, contentTracing, dialog, ipcMain, protocol, session, Session, systemPreferences } from 'electron'; +import { app, BrowserWindow, contentTracing, dialog, ipcMain, protocol, session, Session, systemPreferences, WebFrameMain } from 'electron'; import { statSync } from 'fs'; import { hostname, release } from 'os'; import { VSBuffer } from 'vs/base/common/buffer'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors'; -import { isEqualOrParent } from 'vs/base/common/extpath'; +import { isEqualOrParent, randomPath } from 'vs/base/common/extpath'; import { once } from 'vs/base/common/functional'; import { stripComments } from 'vs/base/common/json'; import { getPathLabel, mnemonicButtonLabel } from 'vs/base/common/labels'; @@ -17,7 +17,6 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { isAbsolute, join, posix } from 'vs/base/common/path'; import { IProcessEnvironment, isLinux, isLinuxSnap, isMacintosh, isWindows } from 'vs/base/common/platform'; -import { joinPath } from 'vs/base/common/resources'; import { assertType, withNullAsUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; @@ -32,24 +31,28 @@ import { localize } from 'vs/nls'; import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { UserConfigurationFileService, UserConfigurationFileServiceId } from 'vs/platform/configuration/common/userConfigurationFileService'; +import { ICredentialsMainService } from 'vs/platform/credentials/common/credentials'; +import { CredentialsMainService } from 'vs/platform/credentials/node/credentialsMainService'; import { ElectronExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/electron-main/extensionHostDebugIpc'; import { IDiagnosticsService } from 'vs/platform/diagnostics/common/diagnostics'; +import { DiagnosticsMainService, IDiagnosticsMainService } from 'vs/platform/diagnostics/electron-main/diagnosticsMainService'; import { DialogMainService, IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { serve as serveDriver } from 'vs/platform/driver/electron-main/driver'; -import { EncryptionMainService, IEncryptionMainService } from 'vs/platform/encryption/electron-main/encryptionMainService'; +import { IEncryptionMainService } from 'vs/platform/encryption/common/encryptionService'; +import { EncryptionMainService } from 'vs/platform/encryption/node/encryptionMainService'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { isLaunchedFromCli } from 'vs/platform/environment/node/argvHelper'; -import { resolveShellEnv } from 'vs/platform/environment/node/shellEnv'; +import { getResolvedShellEnv } from 'vs/platform/environment/node/shellEnv'; import { IExtensionUrlTrustService } from 'vs/platform/extensionManagement/common/extensionUrlTrust'; import { ExtensionUrlTrustService } from 'vs/platform/extensionManagement/node/extensionUrlTrustService'; import { IExtensionHostStarter, ipcExtensionHostStarterChannelName } from 'vs/platform/extensions/common/extensionHostStarter'; import { WorkerMainProcessExtensionHostStarter } from 'vs/platform/extensions/electron-main/workerMainProcessExtensionHostStarter'; import { IExternalTerminalMainService } from 'vs/platform/externalTerminal/common/externalTerminal'; import { LinuxExternalTerminalService, MacExternalTerminalService, WindowsExternalTerminalService } from 'vs/platform/externalTerminal/node/externalTerminalService'; +import { LOCAL_FILE_SYSTEM_CHANNEL_NAME } from 'vs/platform/files/common/diskFileSystemProviderClient'; import { IFileService } from 'vs/platform/files/common/files'; -import { DiskFileSystemProviderChannel } from 'vs/platform/files/electron-main/diskFileSystemProviderIpc'; +import { DiskFileSystemProviderChannel } from 'vs/platform/files/electron-main/diskFileSystemProviderServer'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -68,7 +71,7 @@ import { SharedProcess } from 'vs/platform/sharedProcess/electron-main/sharedPro import { ISignService } from 'vs/platform/sign/common/sign'; import { IStateMainService } from 'vs/platform/state/electron-main/state'; import { StorageDatabaseChannel } from 'vs/platform/storage/electron-main/storageIpc'; -import { IStorageMainService, StorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; +import { GlobalStorageMainService, IGlobalStorageMainService, IStorageMainService, StorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; import { resolveCommonProperties } from 'vs/platform/telemetry/common/commonProperties'; import { ITelemetryService, machineIdKey, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc'; @@ -154,6 +157,90 @@ export class CodeApplication extends Disposable { //#endregion + //#region Request filtering + + // Block all SVG requests from unsupported origins + const supportedSvgSchemes = new Set([Schemas.file, Schemas.vscodeFileResource, Schemas.vscodeRemoteResource, 'devtools']); + + // But allow them if the are made from inside an webview + const isSafeFrame = (requestFrame: WebFrameMain | undefined): boolean => { + for (let frame: WebFrameMain | null | undefined = requestFrame; frame; frame = frame.parent) { + if (frame.url.startsWith(`${Schemas.vscodeWebview}://`)) { + return true; + } + } + return false; + }; + + const isSvgRequestFromSafeContext = (details: Electron.OnBeforeRequestListenerDetails | Electron.OnHeadersReceivedListenerDetails): boolean => { + return details.resourceType === 'xhr' || isSafeFrame(details.frame); + }; + + const isAllowedVsCodeFileRequest = (details: Electron.OnBeforeRequestListenerDetails) => { + const frame = details.frame; + if (!frame || !this.windowsMainService) { + return false; + } + + // Check to see if the request comes from one of the main windows (or shared process) and not from embedded content + const windows = BrowserWindow.getAllWindows(); + for (const window of windows) { + if (frame.processId === window.webContents.mainFrame.processId) { + return true; + } + } + + return false; + }; + + session.defaultSession.webRequest.onBeforeRequest((details, callback) => { + const uri = URI.parse(details.url); + + if (uri.scheme === Schemas.vscodeFileResource) { + if (!isAllowedVsCodeFileRequest(details)) { + this.logService.error('Blocked vscode-file request', details.url); + return callback({ cancel: true }); + } + } + + // Block most svgs + if (uri.path.endsWith('.svg')) { + const isSafeResourceUrl = supportedSvgSchemes.has(uri.scheme); + if (!isSafeResourceUrl) { + return callback({ cancel: !isSvgRequestFromSafeContext(details) }); + } + } + + return callback({ cancel: false }); + }); + + // Configure SVG header content type properly + // https://github.com/microsoft/vscode/issues/97564 + session.defaultSession.webRequest.onHeadersReceived((details, callback) => { + const responseHeaders = details.responseHeaders as Record; + const contentTypes = (responseHeaders['content-type'] || responseHeaders['Content-Type']); + + if (contentTypes && Array.isArray(contentTypes)) { + const uri = URI.parse(details.url); + if (uri.path.endsWith('.svg')) { + if (supportedSvgSchemes.has(uri.scheme)) { + responseHeaders['Content-Type'] = ['image/svg+xml']; + + return callback({ cancel: false, responseHeaders }); + } + } + + // remote extension schemes have the following format + // http://127.0.0.1:/vscode-remote-resource?path= + if (!uri.path.includes(Schemas.vscodeRemoteResource) && contentTypes.some(contentType => contentType.toLowerCase().includes('image/svg'))) { + return callback({ cancel: !isSvgRequestFromSafeContext(details) }); + } + } + + return callback({ cancel: false }); + }); + + //#endregion //#region Code Cache @@ -488,6 +575,7 @@ export class CodeApplication extends Disposable { services.set(ILaunchMainService, new SyncDescriptor(LaunchMainService)); // Diagnostics + services.set(IDiagnosticsMainService, new SyncDescriptor(DiagnosticsMainService)); services.set(IDiagnosticsService, ProxyChannel.toService(getDelayedChannel(sharedProcessReady.then(client => client.getChannel('diagnostics'))))); // Issues @@ -502,6 +590,9 @@ export class CodeApplication extends Disposable { // Native Host services.set(INativeHostMainService, new SyncDescriptor(NativeHostMainService, [sharedProcess])); + // Credentials + services.set(ICredentialsMainService, new SyncDescriptor(CredentialsMainService, [false])); + // Webview Manager services.set(IWebviewManagerService, new SyncDescriptor(WebviewMainService)); @@ -521,6 +612,7 @@ export class CodeApplication extends Disposable { // Storage services.set(IStorageMainService, new SyncDescriptor(StorageMainService)); + services.set(IGlobalStorageMainService, new SyncDescriptor(GlobalStorageMainService)); // External terminal if (isWindows) { @@ -559,23 +651,23 @@ export class CodeApplication extends Disposable { private initChannels(accessor: ServicesAccessor, mainProcessElectronServer: ElectronIPCServer, sharedProcessClient: Promise): void { - // Launch: this one is explicitly registered to the node.js - // server because when a second instance starts up, that is - // the only possible connection between the first and the - // second instance. Electron IPC does not work across apps. + // Channels registered to node.js are exposed to second instances + // launching because that is the only way the second instance + // can talk to the first instance. Electron IPC does not work + // across apps until `requestSingleInstance` APIs are adopted. + const launchChannel = ProxyChannel.fromService(accessor.get(ILaunchMainService), { disableMarshalling: true }); this.mainProcessNodeIpcServer.registerChannel('launch', launchChannel); + const diagnosticsChannel = ProxyChannel.fromService(accessor.get(IDiagnosticsMainService), { disableMarshalling: true }); + this.mainProcessNodeIpcServer.registerChannel('diagnostics', diagnosticsChannel); + // Local Files const diskFileSystemProvider = this.fileService.getProvider(Schemas.file); assertType(diskFileSystemProvider instanceof DiskFileSystemProvider); - const fileSystemProviderChannel = new DiskFileSystemProviderChannel(diskFileSystemProvider, this.logService); - mainProcessElectronServer.registerChannel('localFilesystem', fileSystemProviderChannel); - - // User Configuration File - const userConfigurationFileService = new UserConfigurationFileService(this.environmentMainService, this.fileService, this.logService); - mainProcessElectronServer.registerChannel(UserConfigurationFileServiceId, ProxyChannel.fromService(userConfigurationFileService)); - sharedProcessClient.then(client => client.registerChannel(UserConfigurationFileServiceId, ProxyChannel.fromService(userConfigurationFileService))); + const fileSystemProviderChannel = new DiskFileSystemProviderChannel(diskFileSystemProvider, this.logService, this.environmentMainService); + mainProcessElectronServer.registerChannel(LOCAL_FILE_SYSTEM_CHANNEL_NAME, fileSystemProviderChannel); + sharedProcessClient.then(client => client.registerChannel(LOCAL_FILE_SYSTEM_CHANNEL_NAME, fileSystemProviderChannel)); // Update const updateChannel = new UpdateChannel(accessor.get(IUpdateService)); @@ -589,6 +681,10 @@ export class CodeApplication extends Disposable { const encryptionChannel = ProxyChannel.fromService(accessor.get(IEncryptionMainService)); mainProcessElectronServer.registerChannel('encryption', encryptionChannel); + // Credentials + const credentialsChannel = ProxyChannel.fromService(accessor.get(ICredentialsMainService)); + mainProcessElectronServer.registerChannel('credentials', credentialsChannel); + // Signing const signChannel = ProxyChannel.fromService(accessor.get(ISignService)); mainProcessElectronServer.registerChannel('sign', signChannel); @@ -1033,7 +1129,7 @@ export class CodeApplication extends Disposable { private async resolveShellEnvironment(args: NativeParsedArgs, env: IProcessEnvironment, notifyOnError: boolean): Promise { try { - return await resolveShellEnv(this.logService, args, env); + return await getResolvedShellEnv(this.logService, args, env); } catch (error) { const errorMessage = toErrorMessage(error); if (notifyOnError) { @@ -1047,7 +1143,7 @@ export class CodeApplication extends Disposable { } private stopTracingEventually(accessor: ServicesAccessor, windows: ICodeWindow[]): void { - this.logService.info(`Tracing: waiting for windows to get ready...`); + this.logService.info('Tracing: waiting for windows to get ready...'); const dialogMainService = accessor.get(IDialogMainService); @@ -1059,7 +1155,7 @@ export class CodeApplication extends Disposable { recordingStopped = true; // only once - const path = await contentTracing.stopRecording(joinPath(this.environmentMainService.userHome, `${this.productService.applicationName}-${Math.random().toString(16).slice(-4)}.trace.txt`).fsPath); + const path = await contentTracing.stopRecording(`${randomPath(this.environmentMainService.userHome.fsPath, this.productService.applicationName)}.trace.txt`); if (!timeout) { dialogMainService.showMessageBox({ diff --git a/src/vs/code/electron-main/auth.ts b/src/vs/code/electron-main/auth.ts index 859dc950f8c6c..9e56bcfa902f0 100644 --- a/src/vs/code/electron-main/auth.ts +++ b/src/vs/code/electron-main/auth.ts @@ -9,9 +9,9 @@ import { Event } from 'vs/base/common/event'; import { hash } from 'vs/base/common/hash'; import { Disposable } from 'vs/base/common/lifecycle'; import { generateUuid } from 'vs/base/common/uuid'; -import { IEncryptionMainService } from 'vs/platform/encryption/electron-main/encryptionMainService'; +import { ICredentialsMainService } from 'vs/platform/credentials/common/credentials'; +import { IEncryptionMainService } from 'vs/platform/encryption/common/encryptionService'; import { ILogService } from 'vs/platform/log/common/log'; -import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; import { IProductService } from 'vs/platform/product/common/productService'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; @@ -67,7 +67,7 @@ export class ProxyAuthHandler extends Disposable { constructor( @ILogService private readonly logService: ILogService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, - @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, + @ICredentialsMainService private readonly credentialsService: ICredentialsMainService, @IEncryptionMainService private readonly encryptionMainService: IEncryptionMainService, @IProductService private readonly productService: IProductService ) { @@ -154,7 +154,7 @@ export class ProxyAuthHandler extends Disposable { let storedUsername: string | undefined = undefined; let storedPassword: string | undefined = undefined; try { - const encryptedSerializedProxyCredentials = await this.nativeHostMainService.getPassword(undefined, this.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash); + const encryptedSerializedProxyCredentials = await this.credentialsService.getPassword(this.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash); if (encryptedSerializedProxyCredentials) { const credentials: Credentials = JSON.parse(await this.encryptionMainService.decrypt(encryptedSerializedProxyCredentials)); @@ -212,9 +212,9 @@ export class ProxyAuthHandler extends Disposable { try { if (reply.remember) { const encryptedSerializedCredentials = await this.encryptionMainService.encrypt(JSON.stringify(credentials)); - await this.nativeHostMainService.setPassword(undefined, this.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash, encryptedSerializedCredentials); + await this.credentialsService.setPassword(this.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash, encryptedSerializedCredentials); } else { - await this.nativeHostMainService.deletePassword(undefined, this.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash); + await this.credentialsService.deletePassword(this.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash); } } catch (error) { this.logService.error(error); // handle gracefully diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 6340ed52eabfc..4ba46359de9d0 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -26,6 +26,7 @@ import { CodeApplication } from 'vs/code/electron-main/app'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; +import { IDiagnosticsMainService } from 'vs/platform/diagnostics/electron-main/diagnosticsMainService'; import { DiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { EnvironmentMainService, IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; @@ -48,8 +49,8 @@ import product from 'vs/platform/product/common/product'; import { IProductService } from 'vs/platform/product/common/productService'; import { IProtocolMainService } from 'vs/platform/protocol/electron-main/protocol'; import { ProtocolMainService } from 'vs/platform/protocol/electron-main/protocolMainService'; -import { ITunnelService } from 'vs/platform/remote/common/tunnel'; -import { TunnelService } from 'vs/platform/remote/node/tunnelService'; +import { ITunnelService } from 'vs/platform/tunnel/common/tunnel'; +import { TunnelService } from 'vs/platform/tunnel/node/tunnelService'; import { IRequestService } from 'vs/platform/request/common/request'; import { RequestMainService } from 'vs/platform/request/electron-main/requestMainService'; import { ISignService } from 'vs/platform/sign/common/sign'; @@ -310,14 +311,15 @@ class CodeMain { }, 10000); } - const launchService = ProxyChannel.toService(client.getChannel('launch'), { disableMarshalling: true }); + const otherInstanceLaunchMainService = ProxyChannel.toService(client.getChannel('launch'), { disableMarshalling: true }); + const otherInstanceDiagnosticsMainService = ProxyChannel.toService(client.getChannel('diagnostics'), { disableMarshalling: true }); // Process Info if (environmentMainService.args.status) { return instantiationService.invokeFunction(async () => { const diagnosticsService = new DiagnosticsService(NullTelemetryService, productService); - const mainProcessInfo = await launchService.getMainProcessInfo(); - const remoteDiagnostics = await launchService.getRemoteDiagnostics({ includeProcesses: true, includeWorkspaceMetadata: true }); + const mainProcessInfo = await otherInstanceLaunchMainService.getMainProcessInfo(); + const remoteDiagnostics = await otherInstanceDiagnosticsMainService.getRemoteDiagnostics({ includeProcesses: true, includeWorkspaceMetadata: true }); const diagnostics = await diagnosticsService.getDiagnostics(mainProcessInfo, remoteDiagnostics); console.log(diagnostics); @@ -327,12 +329,12 @@ class CodeMain { // Windows: allow to set foreground if (isWindows) { - await this.windowsAllowSetForegroundWindow(launchService, logService); + await this.windowsAllowSetForegroundWindow(otherInstanceLaunchMainService, logService); } // Send environment over... logService.trace('Sending env to running instance...'); - await launchService.start(environmentMainService.args, process.env as IProcessEnvironment); + await otherInstanceLaunchMainService.start(environmentMainService.args, process.env as IProcessEnvironment); // Cleanup client.dispose(); diff --git a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts index fcd44854ede91..4fb10a1b5f3cf 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts @@ -3,9 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./media/issueReporter'; +import 'vs/base/browser/ui/codicons/codiconStyles'; // make sure codicon css is loaded +import { localize } from 'vs/nls'; import { $, reset, safeInnerHtml, windowOpenNoOpener } from 'vs/base/browser/dom'; import { Button } from 'vs/base/browser/ui/button/button'; -import 'vs/base/browser/ui/codicons/codiconStyles'; // make sure codicon css is loaded import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { Delayer } from 'vs/base/common/async'; import { Codicon } from 'vs/base/common/codicons'; @@ -17,12 +19,9 @@ import { escape } from 'vs/base/common/strings'; import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { IssueReporterData as IssueReporterModelData, IssueReporterModel } from 'vs/code/electron-sandbox/issue/issueReporterModel'; import BaseHtml from 'vs/code/electron-sandbox/issue/issueReporterPage'; -import 'vs/css!./media/issueReporter'; -import { localize } from 'vs/nls'; import { isRemoteDiagnosticError, SystemInfo } from 'vs/platform/diagnostics/common/diagnostics'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ElectronIPCMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; -import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; + import { IssueReporterData, IssueReporterExtensionData, IssueReporterStyles, IssueReporterWindowConfiguration, IssueType } from 'vs/platform/issue/common/issue'; import { normalizeGitHubUrl } from 'vs/platform/issue/common/issueReporterUtil'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; @@ -70,7 +69,8 @@ export class IssueReporter extends Disposable { constructor(private readonly configuration: IssueReporterWindowConfiguration) { super(); - this.initServices(configuration); + const mainProcessService = new ElectronIPCMainProcessService(configuration.windowId); + this.nativeHostService = new NativeHostService(configuration.windowId, mainProcessService) as INativeHostService; const targetExtension = configuration.data.extensionId ? configuration.data.enabledExtensions.find(extension => extension.id === configuration.data.extensionId) : undefined; this.issueReporterModel = new IssueReporterModel({ @@ -255,15 +255,6 @@ export class IssueReporter extends Disposable { this.updateExtensionSelector(installedExtensions); } - private initServices(configuration: IssueReporterWindowConfiguration): void { - const serviceCollection = new ServiceCollection(); - const mainProcessService = new ElectronIPCMainProcessService(configuration.windowId); - serviceCollection.set(IMainProcessService, mainProcessService); - - this.nativeHostService = new NativeHostService(configuration.windowId, mainProcessService) as INativeHostService; - serviceCollection.set(INativeHostService, this.nativeHostService); - } - private setEventHandlers(): void { this.addEventListener('issue-type', 'change', (event: Event) => { const issueType = parseInt((event.target).value); diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts b/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts index 5f2040266cfc2..fad45e924746c 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts @@ -3,8 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, append, createStyleSheet } from 'vs/base/browser/dom'; +import 'vs/css!./media/processExplorer'; import 'vs/base/browser/ui/codicons/codiconStyles'; // make sure codicon css is loaded +import { localize } from 'vs/nls'; +import { $, append, createStyleSheet } from 'vs/base/browser/dom'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { DataTree } from 'vs/base/browser/ui/tree/dataTree'; import { IDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; @@ -13,8 +15,6 @@ import { ProcessItem } from 'vs/base/common/processes'; import { IContextMenuItem } from 'vs/base/parts/contextmenu/common/contextmenu'; import { popup } from 'vs/base/parts/contextmenu/electron-sandbox/contextmenu'; import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; -import 'vs/css!./media/processExplorer'; -import { localize } from 'vs/nls'; import { IRemoteDiagnosticError, isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; import { ByteSize } from 'vs/platform/files/common/files'; import { ElectronIPCMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; @@ -265,8 +265,6 @@ class ProcessExplorer { this.lastRequestTime = Date.now(); ipcRenderer.send('vscode:windowsInfoRequest'); ipcRenderer.send('vscode:listProcesses'); - - } private setEventHandlers(data: ProcessExplorerData): void { @@ -452,9 +450,23 @@ class ProcessExplorer { items.push({ label: localize('copy', "Copy"), click: () => { - const row = document.getElementById(`pid-${pid}`); - if (row) { - this.nativeHostService.writeClipboardText(row.innerText); + // Collect the selected pids + const selectionPids = this.tree?.getSelection()?.map(e => { + if (!e || !('pid' in e)) { + return undefined; + } + return e.pid; + }).filter(e => !!e) as number[]; + // If the selection does not contain the right clicked item, copy the right clicked + // item only. + if (!selectionPids?.includes(pid)) { + selectionPids.length = 0; + selectionPids.push(pid); + } + const rows = selectionPids?.map(e => document.getElementById(`pid-${e}`)).filter(e => !!e) as HTMLElement[]; + if (rows) { + const text = rows.map(e => e.innerText).filter(e => !!e) as string[]; + this.nativeHostService.writeClipboardText(text.join('\n')); } } }); @@ -506,7 +518,7 @@ function createCodiconStyleSheet() { const codiconStyleSheet = createStyleSheet(); codiconStyleSheet.id = 'codiconStyles'; - const iconsStyleSheet = getIconsStyleSheet(); + const iconsStyleSheet = getIconsStyleSheet(undefined); function updateAll() { codiconStyleSheet.textContent = iconsStyleSheet.getCSS(); } diff --git a/src/vs/code/electron-sandbox/workbench/workbench.html b/src/vs/code/electron-sandbox/workbench/workbench.html index f76e077595219..47066f520be18 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.html +++ b/src/vs/code/electron-sandbox/workbench/workbench.html @@ -3,8 +3,8 @@ - - + + diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 6ece06fd6e3cd..8aac55005d43c 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -8,13 +8,13 @@ import { chmodSync, existsSync, readFileSync, statSync, truncateSync, unlinkSync import { homedir, release, tmpdir } from 'os'; import type { ProfilingSession, Target } from 'v8-inspect-profiler'; import { Event } from 'vs/base/common/event'; -import { isAbsolute, join, resolve } from 'vs/base/common/path'; +import { isAbsolute, resolve } from 'vs/base/common/path'; import { IProcessEnvironment, isMacintosh, isWindows } from 'vs/base/common/platform'; import { randomPort } from 'vs/base/common/ports'; import { isString } from 'vs/base/common/types'; import { whenDeleted, writeFileSync } from 'vs/base/node/pfs'; import { findFreePort } from 'vs/base/node/ports'; -import { watchFileContents } from 'vs/base/node/watcher'; +import { watchFileContents } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { buildHelpMessage, buildVersionMessage, OPTIONS } from 'vs/platform/environment/node/argv'; import { addArg, parseCLIProcessArgv } from 'vs/platform/environment/node/argvHelper'; @@ -22,6 +22,8 @@ import { getStdinFilePath, hasStdinWithoutTty, readFromStdin, stdinDataListener import { createWaitMarkerFile } from 'vs/platform/environment/node/wait'; import product from 'vs/platform/product/common/product'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { randomPath } from 'vs/base/common/extpath'; +import { Utils } from 'vs/platform/profiling/common/profiling'; function shouldSpawnCliProcess(argv: NativeParsedArgs): boolean { return !!argv['install-source'] @@ -32,10 +34,6 @@ function shouldSpawnCliProcess(argv: NativeParsedArgs): boolean { || !!argv['telemetry']; } -function createFileName(dir: string, prefix: string): string { - return join(dir, `${prefix}-${Math.random().toString(16).slice(-4)}`); -} - interface IMainCli { main: (argv: NativeParsedArgs) => Promise; } @@ -259,7 +257,7 @@ export async function main(argv: string[]): Promise { throw new Error('Failed to find free ports for profiler. Make sure to shutdown all instances of the editor first.'); } - const filenamePrefix = createFileName(homedir(), 'prof'); + const filenamePrefix = randomPath(homedir(), 'prof'); addArg(argv, `--inspect-brk=${portMain}`); addArg(argv, `--remote-debugging-port=${portRenderer}`); @@ -288,17 +286,17 @@ export async function main(argv: string[]): Promise { return; } let suffix = ''; - let profile = await session.stop(); + let result = await session.stop(); if (!process.env['VSCODE_DEV']) { // when running from a not-development-build we remove // absolute filenames because we don't want to reveal anything // about users. We also append the `.txt` suffix to make it // easier to attach these files to GH issues - profile = profiler.rewriteAbsolutePaths(profile, 'piiRemoved'); + result.profile = Utils.rewriteAbsolutePaths(result.profile, 'piiRemoved'); suffix = '.txt'; } - await profiler.writeProfile(profile, `${filenamePrefix}.${name}.cpuprofile${suffix}`); + writeFileSync(`${filenamePrefix}.${name}.cpuprofile${suffix}`, JSON.stringify(result.profile, undefined, 4)); } }; } @@ -393,7 +391,7 @@ export async function main(argv: string[]): Promise { for (const outputType of ['stdout', 'stderr']) { // Tmp file to target output to - const tmpName = createFileName(tmpdir(), `code-${outputType}`); + const tmpName = randomPath(tmpdir(), `code-${outputType}`); writeFileSync(tmpName, ''); spawnArgs.push(`--${outputType}`, tmpName); @@ -403,8 +401,12 @@ export async function main(argv: string[]): Promise { const stream = outputType === 'stdout' ? process.stdout : process.stderr; const cts = new CancellationTokenSource(); - child.on('close', () => cts.dispose(true)); - await watchFileContents(tmpName, chunk => stream.write(chunk), cts.token); + child.on('close', () => { + // We must dispose the token to stop watching, + // but the watcher might still be reading data. + setTimeout(() => cts.dispose(true), 200); + }); + await watchFileContents(tmpName, chunk => stream.write(chunk), () => { /* ignore */ }, cts.token); } finally { unlinkSync(tmpName); } diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index fdee012d7ea10..f5a699ea72e87 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -22,7 +22,7 @@ import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; -import { IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService'; import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { IFileService } from 'vs/platform/files/common/files'; @@ -217,7 +217,8 @@ class CliMain extends Disposable { // Install Extension else if (this.argv['install-extension'] || this.argv['install-builtin-extension']) { - return extensionManagementCLIService.installExtensions(this.asExtensionIdOrVSIX(this.argv['install-extension'] || []), this.argv['install-builtin-extension'] || [], !!this.argv['do-not-sync'], !!this.argv['force']); + const installOptions: InstallOptions = { isMachineScoped: !!this.argv['do-not-sync'], installPreReleaseVersion: !!this.argv['pre-release'] }; + return extensionManagementCLIService.installExtensions(this.asExtensionIdOrVSIX(this.argv['install-extension'] || []), this.argv['install-builtin-extension'] || [], installOptions, !!this.argv['force']); } // Uninstall Extension diff --git a/src/vs/code/electron-sandbox/issue/test/testReporterModel.test.ts b/src/vs/code/test/electron-sandbox/issue/testReporterModel.test.ts similarity index 100% rename from src/vs/code/electron-sandbox/issue/test/testReporterModel.test.ts rename to src/vs/code/test/electron-sandbox/issue/testReporterModel.test.ts diff --git a/src/vs/css.build.js b/src/vs/css.build.js index 5ad45ba6a0d44..c8875859684aa 100644 --- a/src/vs/css.build.js +++ b/src/vs/css.build.js @@ -18,10 +18,6 @@ var _cssPluginGlobal = this; var CSSBuildLoaderPlugin; (function (CSSBuildLoaderPlugin) { var global = (_cssPluginGlobal || {}); - /** - * Known issue: - * - In IE there is no way to know if the CSS file loaded successfully or not. - */ var BrowserCSSLoader = /** @class */ (function () { function BrowserCSSLoader() { this._pendingLoads = 0; @@ -328,6 +324,7 @@ var CSSBuildLoaderPlugin; // .svg => url encode as explained at https://codepen.io/tigt/post/optimizing-svgs-in-data-uris var newText = fileContents.toString() .replace(/"/g, '\'') + .replace(/%/g, '%25') .replace(//g, '%3E') .replace(/&/g, '%26') diff --git a/src/vs/editor/browser/config/charWidthReader.ts b/src/vs/editor/browser/config/charWidthReader.ts index 0493c5cddb0c0..16b6d7d1c48d4 100644 --- a/src/vs/editor/browser/config/charWidthReader.ts +++ b/src/vs/editor/browser/config/charWidthReader.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { isSafari } from 'vs/base/browser/browser'; -import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; +import { applyFontInfo } from 'vs/editor/browser/config/domFontInfo'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; export const enum CharWidthRequestType { @@ -64,38 +63,22 @@ class DomCharWidthReader { } private _createDomElements(): void { - const fontFamily = this._bareFontInfo.getMassagedFontFamily(isSafari ? EDITOR_FONT_DEFAULTS.fontFamily : null); - const container = document.createElement('div'); container.style.position = 'absolute'; container.style.top = '-50000px'; container.style.width = '50000px'; const regularDomNode = document.createElement('div'); - regularDomNode.style.fontFamily = fontFamily; - regularDomNode.style.fontWeight = this._bareFontInfo.fontWeight; - regularDomNode.style.fontSize = this._bareFontInfo.fontSize + 'px'; - regularDomNode.style.fontFeatureSettings = this._bareFontInfo.fontFeatureSettings; - regularDomNode.style.lineHeight = this._bareFontInfo.lineHeight + 'px'; - regularDomNode.style.letterSpacing = this._bareFontInfo.letterSpacing + 'px'; + applyFontInfo(regularDomNode, this._bareFontInfo); container.appendChild(regularDomNode); const boldDomNode = document.createElement('div'); - boldDomNode.style.fontFamily = fontFamily; + applyFontInfo(boldDomNode, this._bareFontInfo); boldDomNode.style.fontWeight = 'bold'; - boldDomNode.style.fontSize = this._bareFontInfo.fontSize + 'px'; - boldDomNode.style.fontFeatureSettings = this._bareFontInfo.fontFeatureSettings; - boldDomNode.style.lineHeight = this._bareFontInfo.lineHeight + 'px'; - boldDomNode.style.letterSpacing = this._bareFontInfo.letterSpacing + 'px'; container.appendChild(boldDomNode); const italicDomNode = document.createElement('div'); - italicDomNode.style.fontFamily = fontFamily; - italicDomNode.style.fontWeight = this._bareFontInfo.fontWeight; - italicDomNode.style.fontSize = this._bareFontInfo.fontSize + 'px'; - italicDomNode.style.fontFeatureSettings = this._bareFontInfo.fontFeatureSettings; - italicDomNode.style.lineHeight = this._bareFontInfo.lineHeight + 'px'; - italicDomNode.style.letterSpacing = this._bareFontInfo.letterSpacing + 'px'; + applyFontInfo(italicDomNode, this._bareFontInfo); italicDomNode.style.fontStyle = 'italic'; container.appendChild(italicDomNode); diff --git a/src/vs/editor/browser/config/configuration.ts b/src/vs/editor/browser/config/configuration.ts deleted file mode 100644 index f4175c0bf63a5..0000000000000 --- a/src/vs/editor/browser/config/configuration.ts +++ /dev/null @@ -1,383 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as browser from 'vs/base/browser/browser'; -import { FastDomNode } from 'vs/base/browser/fastDomNode'; -import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; -import * as platform from 'vs/base/common/platform'; -import { CharWidthRequest, CharWidthRequestType, readCharWidths } from 'vs/editor/browser/config/charWidthReader'; -import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver'; -import { CommonEditorConfiguration, IEnvConfiguration } from 'vs/editor/common/config/commonEditorConfig'; -import { EditorOption, EditorFontLigatures, EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; -import { BareFontInfo, FontInfo, SERIALIZED_FONT_INFO_VERSION } from 'vs/editor/common/config/fontInfo'; -import { IDimension } from 'vs/editor/common/editorCommon'; -import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; -import { IEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; - -class CSSBasedConfigurationCache { - - private readonly _keys: { [key: string]: BareFontInfo; }; - private readonly _values: { [key: string]: FontInfo; }; - - constructor() { - this._keys = Object.create(null); - this._values = Object.create(null); - } - - public has(item: BareFontInfo): boolean { - const itemId = item.getId(); - return !!this._values[itemId]; - } - - public get(item: BareFontInfo): FontInfo { - const itemId = item.getId(); - return this._values[itemId]; - } - - public put(item: BareFontInfo, value: FontInfo): void { - const itemId = item.getId(); - this._keys[itemId] = item; - this._values[itemId] = value; - } - - public remove(item: BareFontInfo): void { - const itemId = item.getId(); - delete this._keys[itemId]; - delete this._values[itemId]; - } - - public getValues(): FontInfo[] { - return Object.keys(this._keys).map(id => this._values[id]); - } -} - -export function clearAllFontInfos(): void { - CSSBasedConfiguration.INSTANCE.clearCache(); -} - -export function readFontInfo(bareFontInfo: BareFontInfo): FontInfo { - return CSSBasedConfiguration.INSTANCE.readConfiguration(bareFontInfo); -} - -export function restoreFontInfo(fontInfo: ISerializedFontInfo[]): void { - CSSBasedConfiguration.INSTANCE.restoreFontInfo(fontInfo); -} - -export function serializeFontInfo(): ISerializedFontInfo[] | null { - const fontInfo = CSSBasedConfiguration.INSTANCE.saveFontInfo(); - if (fontInfo.length > 0) { - return fontInfo; - } - - return null; -} - -export interface ISerializedFontInfo { - readonly version: number; - readonly zoomLevel: number; - readonly pixelRatio: number; - readonly fontFamily: string; - readonly fontWeight: string; - readonly fontSize: number; - readonly fontFeatureSettings: string; - readonly lineHeight: number; - readonly letterSpacing: number; - readonly isMonospace: boolean; - readonly typicalHalfwidthCharacterWidth: number; - readonly typicalFullwidthCharacterWidth: number; - readonly canUseHalfwidthRightwardsArrow: boolean; - readonly spaceWidth: number; - readonly middotWidth: number; - readonly wsmiddotWidth: number; - readonly maxDigitWidth: number; -} - -class CSSBasedConfiguration extends Disposable { - - public static readonly INSTANCE = new CSSBasedConfiguration(); - - private _cache: CSSBasedConfigurationCache; - private _evictUntrustedReadingsTimeout: any; - - private _onDidChange = this._register(new Emitter()); - public readonly onDidChange: Event = this._onDidChange.event; - - constructor() { - super(); - - this._cache = new CSSBasedConfigurationCache(); - this._evictUntrustedReadingsTimeout = -1; - } - - public override dispose(): void { - if (this._evictUntrustedReadingsTimeout !== -1) { - clearTimeout(this._evictUntrustedReadingsTimeout); - this._evictUntrustedReadingsTimeout = -1; - } - super.dispose(); - } - - public clearCache(): void { - this._cache = new CSSBasedConfigurationCache(); - this._onDidChange.fire(); - } - - private _writeToCache(item: BareFontInfo, value: FontInfo): void { - this._cache.put(item, value); - - if (!value.isTrusted && this._evictUntrustedReadingsTimeout === -1) { - // Try reading again after some time - this._evictUntrustedReadingsTimeout = setTimeout(() => { - this._evictUntrustedReadingsTimeout = -1; - this._evictUntrustedReadings(); - }, 5000); - } - } - - private _evictUntrustedReadings(): void { - const values = this._cache.getValues(); - let somethingRemoved = false; - for (const item of values) { - if (!item.isTrusted) { - somethingRemoved = true; - this._cache.remove(item); - } - } - if (somethingRemoved) { - this._onDidChange.fire(); - } - } - - public saveFontInfo(): ISerializedFontInfo[] { - // Only save trusted font info (that has been measured in this running instance) - return this._cache.getValues().filter(item => item.isTrusted); - } - - public restoreFontInfo(savedFontInfos: ISerializedFontInfo[]): void { - // Take all the saved font info and insert them in the cache without the trusted flag. - // The reason for this is that a font might have been installed on the OS in the meantime. - for (const savedFontInfo of savedFontInfos) { - if (savedFontInfo.version !== SERIALIZED_FONT_INFO_VERSION) { - // cannot use older version - continue; - } - const fontInfo = new FontInfo(savedFontInfo, false); - this._writeToCache(fontInfo, fontInfo); - } - } - - public readConfiguration(bareFontInfo: BareFontInfo): FontInfo { - if (!this._cache.has(bareFontInfo)) { - let readConfig = CSSBasedConfiguration._actualReadConfiguration(bareFontInfo); - - if (readConfig.typicalHalfwidthCharacterWidth <= 2 || readConfig.typicalFullwidthCharacterWidth <= 2 || readConfig.spaceWidth <= 2 || readConfig.maxDigitWidth <= 2) { - // Hey, it's Bug 14341 ... we couldn't read - readConfig = new FontInfo({ - zoomLevel: browser.getZoomLevel(), - pixelRatio: browser.getPixelRatio(), - fontFamily: readConfig.fontFamily, - fontWeight: readConfig.fontWeight, - fontSize: readConfig.fontSize, - fontFeatureSettings: readConfig.fontFeatureSettings, - lineHeight: readConfig.lineHeight, - letterSpacing: readConfig.letterSpacing, - isMonospace: readConfig.isMonospace, - typicalHalfwidthCharacterWidth: Math.max(readConfig.typicalHalfwidthCharacterWidth, 5), - typicalFullwidthCharacterWidth: Math.max(readConfig.typicalFullwidthCharacterWidth, 5), - canUseHalfwidthRightwardsArrow: readConfig.canUseHalfwidthRightwardsArrow, - spaceWidth: Math.max(readConfig.spaceWidth, 5), - middotWidth: Math.max(readConfig.middotWidth, 5), - wsmiddotWidth: Math.max(readConfig.wsmiddotWidth, 5), - maxDigitWidth: Math.max(readConfig.maxDigitWidth, 5), - }, false); - } - - this._writeToCache(bareFontInfo, readConfig); - } - return this._cache.get(bareFontInfo); - } - - private static createRequest(chr: string, type: CharWidthRequestType, all: CharWidthRequest[], monospace: CharWidthRequest[] | null): CharWidthRequest { - const result = new CharWidthRequest(chr, type); - all.push(result); - if (monospace) { - monospace.push(result); - } - return result; - } - - private static _actualReadConfiguration(bareFontInfo: BareFontInfo): FontInfo { - const all: CharWidthRequest[] = []; - const monospace: CharWidthRequest[] = []; - - const typicalHalfwidthCharacter = this.createRequest('n', CharWidthRequestType.Regular, all, monospace); - const typicalFullwidthCharacter = this.createRequest('\uff4d', CharWidthRequestType.Regular, all, null); - const space = this.createRequest(' ', CharWidthRequestType.Regular, all, monospace); - const digit0 = this.createRequest('0', CharWidthRequestType.Regular, all, monospace); - const digit1 = this.createRequest('1', CharWidthRequestType.Regular, all, monospace); - const digit2 = this.createRequest('2', CharWidthRequestType.Regular, all, monospace); - const digit3 = this.createRequest('3', CharWidthRequestType.Regular, all, monospace); - const digit4 = this.createRequest('4', CharWidthRequestType.Regular, all, monospace); - const digit5 = this.createRequest('5', CharWidthRequestType.Regular, all, monospace); - const digit6 = this.createRequest('6', CharWidthRequestType.Regular, all, monospace); - const digit7 = this.createRequest('7', CharWidthRequestType.Regular, all, monospace); - const digit8 = this.createRequest('8', CharWidthRequestType.Regular, all, monospace); - const digit9 = this.createRequest('9', CharWidthRequestType.Regular, all, monospace); - - // monospace test: used for whitespace rendering - const rightwardsArrow = this.createRequest('→', CharWidthRequestType.Regular, all, monospace); - const halfwidthRightwardsArrow = this.createRequest('→', CharWidthRequestType.Regular, all, null); - - // U+00B7 - MIDDLE DOT - const middot = this.createRequest('·', CharWidthRequestType.Regular, all, monospace); - - // U+2E31 - WORD SEPARATOR MIDDLE DOT - const wsmiddotWidth = this.createRequest(String.fromCharCode(0x2E31), CharWidthRequestType.Regular, all, null); - - // monospace test: some characters - const monospaceTestChars = '|/-_ilm%'; - for (let i = 0, len = monospaceTestChars.length; i < len; i++) { - this.createRequest(monospaceTestChars.charAt(i), CharWidthRequestType.Regular, all, monospace); - this.createRequest(monospaceTestChars.charAt(i), CharWidthRequestType.Italic, all, monospace); - this.createRequest(monospaceTestChars.charAt(i), CharWidthRequestType.Bold, all, monospace); - - } - - readCharWidths(bareFontInfo, all); - - const maxDigitWidth = Math.max(digit0.width, digit1.width, digit2.width, digit3.width, digit4.width, digit5.width, digit6.width, digit7.width, digit8.width, digit9.width); - - let isMonospace = (bareFontInfo.fontFeatureSettings === EditorFontLigatures.OFF); - const referenceWidth = monospace[0].width; - for (let i = 1, len = monospace.length; isMonospace && i < len; i++) { - const diff = referenceWidth - monospace[i].width; - if (diff < -0.001 || diff > 0.001) { - isMonospace = false; - break; - } - } - - let canUseHalfwidthRightwardsArrow = true; - if (isMonospace && halfwidthRightwardsArrow.width !== referenceWidth) { - // using a halfwidth rightwards arrow would break monospace... - canUseHalfwidthRightwardsArrow = false; - } - if (halfwidthRightwardsArrow.width > rightwardsArrow.width) { - // using a halfwidth rightwards arrow would paint a larger arrow than a regular rightwards arrow - canUseHalfwidthRightwardsArrow = false; - } - - // let's trust the zoom level only 2s after it was changed. - const canTrustBrowserZoomLevel = (browser.getTimeSinceLastZoomLevelChanged() > 2000); - return new FontInfo({ - zoomLevel: browser.getZoomLevel(), - pixelRatio: browser.getPixelRatio(), - fontFamily: bareFontInfo.fontFamily, - fontWeight: bareFontInfo.fontWeight, - fontSize: bareFontInfo.fontSize, - fontFeatureSettings: bareFontInfo.fontFeatureSettings, - lineHeight: bareFontInfo.lineHeight, - letterSpacing: bareFontInfo.letterSpacing, - isMonospace: isMonospace, - typicalHalfwidthCharacterWidth: typicalHalfwidthCharacter.width, - typicalFullwidthCharacterWidth: typicalFullwidthCharacter.width, - canUseHalfwidthRightwardsArrow: canUseHalfwidthRightwardsArrow, - spaceWidth: space.width, - middotWidth: middot.width, - wsmiddotWidth: wsmiddotWidth.width, - maxDigitWidth: maxDigitWidth - }, canTrustBrowserZoomLevel); - } -} - -export class Configuration extends CommonEditorConfiguration { - - public static applyFontInfoSlow(domNode: HTMLElement, fontInfo: BareFontInfo): void { - domNode.style.fontFamily = fontInfo.getMassagedFontFamily(browser.isSafari ? EDITOR_FONT_DEFAULTS.fontFamily : null); - domNode.style.fontWeight = fontInfo.fontWeight; - domNode.style.fontSize = fontInfo.fontSize + 'px'; - domNode.style.fontFeatureSettings = fontInfo.fontFeatureSettings; - domNode.style.lineHeight = fontInfo.lineHeight + 'px'; - domNode.style.letterSpacing = fontInfo.letterSpacing + 'px'; - } - - public static applyFontInfo(domNode: FastDomNode, fontInfo: BareFontInfo): void { - domNode.setFontFamily(fontInfo.getMassagedFontFamily(browser.isSafari ? EDITOR_FONT_DEFAULTS.fontFamily : null)); - domNode.setFontWeight(fontInfo.fontWeight); - domNode.setFontSize(fontInfo.fontSize); - domNode.setFontFeatureSettings(fontInfo.fontFeatureSettings); - domNode.setLineHeight(fontInfo.lineHeight); - domNode.setLetterSpacing(fontInfo.letterSpacing); - } - - private readonly _elementSizeObserver: ElementSizeObserver; - - constructor( - isSimpleWidget: boolean, - options: Readonly, - referenceDomElement: HTMLElement | null = null, - private readonly accessibilityService: IAccessibilityService - ) { - super(isSimpleWidget, options); - - this._elementSizeObserver = this._register(new ElementSizeObserver(referenceDomElement, options.dimension, () => this._recomputeOptions())); - - this._register(CSSBasedConfiguration.INSTANCE.onDidChange(() => this._recomputeOptions())); - - if (this._validatedOptions.get(EditorOption.automaticLayout)) { - this._elementSizeObserver.startObserving(); - } - - this._register(browser.onDidChangeZoomLevel(_ => this._recomputeOptions())); - this._register(this.accessibilityService.onDidChangeScreenReaderOptimized(() => this._recomputeOptions())); - - this._recomputeOptions(); - } - - public override observeReferenceElement(dimension?: IDimension): void { - this._elementSizeObserver.observe(dimension); - } - - public override updatePixelRatio(): void { - this._recomputeOptions(); - } - - private static _getExtraEditorClassName(): string { - let extra = ''; - if (!browser.isSafari && !browser.isWebkitWebView) { - // Use user-select: none in all browsers except Safari and native macOS WebView - extra += 'no-user-select '; - } - if (browser.isSafari) { - // See https://github.com/microsoft/vscode/issues/108822 - extra += 'no-minimap-shadow '; - } - if (platform.isMacintosh) { - extra += 'mac '; - } - return extra; - } - - protected _getEnvConfiguration(): IEnvConfiguration { - return { - extraEditorClassName: Configuration._getExtraEditorClassName(), - outerWidth: this._elementSizeObserver.getWidth(), - outerHeight: this._elementSizeObserver.getHeight(), - emptySelectionClipboard: browser.isWebKit || browser.isFirefox, - pixelRatio: browser.getPixelRatio(), - zoomLevel: browser.getZoomLevel(), - accessibilitySupport: ( - this.accessibilityService.isScreenReaderOptimized() - ? AccessibilitySupport.Enabled - : this.accessibilityService.getAccessibilitySupport() - ) - }; - } - - protected readConfiguration(bareFontInfo: BareFontInfo): FontInfo { - return CSSBasedConfiguration.INSTANCE.readConfiguration(bareFontInfo); - } -} diff --git a/src/vs/editor/browser/config/domFontInfo.ts b/src/vs/editor/browser/config/domFontInfo.ts new file mode 100644 index 0000000000000..96c7021e53b82 --- /dev/null +++ b/src/vs/editor/browser/config/domFontInfo.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as browser from 'vs/base/browser/browser'; +import { FastDomNode } from 'vs/base/browser/fastDomNode'; +import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; +import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; + +export function applyFontInfo(domNode: FastDomNode | HTMLElement, fontInfo: BareFontInfo): void { + if (domNode instanceof FastDomNode) { + domNode.setFontFamily(fontInfo.getMassagedFontFamily(browser.isSafari ? EDITOR_FONT_DEFAULTS.fontFamily : null)); + domNode.setFontWeight(fontInfo.fontWeight); + domNode.setFontSize(fontInfo.fontSize); + domNode.setFontFeatureSettings(fontInfo.fontFeatureSettings); + domNode.setLineHeight(fontInfo.lineHeight); + domNode.setLetterSpacing(fontInfo.letterSpacing); + } else { + domNode.style.fontFamily = fontInfo.getMassagedFontFamily(browser.isSafari ? EDITOR_FONT_DEFAULTS.fontFamily : null); + domNode.style.fontWeight = fontInfo.fontWeight; + domNode.style.fontSize = fontInfo.fontSize + 'px'; + domNode.style.fontFeatureSettings = fontInfo.fontFeatureSettings; + domNode.style.lineHeight = fontInfo.lineHeight + 'px'; + domNode.style.letterSpacing = fontInfo.letterSpacing + 'px'; + } +} diff --git a/src/vs/editor/browser/config/editorConfiguration.ts b/src/vs/editor/browser/config/editorConfiguration.ts new file mode 100644 index 0000000000000..5af58c15fd4d8 --- /dev/null +++ b/src/vs/editor/browser/config/editorConfiguration.ts @@ -0,0 +1,332 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as browser from 'vs/base/browser/browser'; +import * as arrays from 'vs/base/common/arrays'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import * as objects from 'vs/base/common/objects'; +import * as platform from 'vs/base/common/platform'; +import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver'; +import { FontMeasurements } from 'vs/editor/browser/config/fontMeasurements'; +import { migrateOptions } from 'vs/editor/browser/config/migrateOptions'; +import { TabFocus } from 'vs/editor/browser/config/tabFocus'; +import { ComputeOptionsMemory, ConfigurationChangedEvent, EditorOption, editorOptionsRegistry, FindComputedEditorOptionValueById, IComputedEditorOptions, IEditorOptions, IEnvironmentalOptions } from 'vs/editor/common/config/editorOptions'; +import { EditorZoom } from 'vs/editor/common/config/editorZoom'; +import { BareFontInfo, FontInfo, IValidatedEditorOptions } from 'vs/editor/common/config/fontInfo'; +import { IDimension } from 'vs/editor/common/core/dimension'; +import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration'; +import { AccessibilitySupport, IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; + +export interface IEditorConstructionOptions extends IEditorOptions { + /** + * The initial editor dimension (to avoid measuring the container). + */ + dimension?: IDimension; + /** + * Place overflow widgets inside an external DOM node. + * Defaults to an internal DOM node. + */ + overflowWidgetsDomNode?: HTMLElement; +} + +export class EditorConfiguration extends Disposable implements IEditorConfiguration { + + private _onDidChange = this._register(new Emitter()); + public readonly onDidChange: Event = this._onDidChange.event; + + private _onDidChangeFast = this._register(new Emitter()); + public readonly onDidChangeFast: Event = this._onDidChangeFast.event; + + public readonly isSimpleWidget: boolean; + private readonly _containerObserver: ElementSizeObserver; + + private _isDominatedByLongLines: boolean = false; + private _viewLineCount: number = 1; + private _lineNumbersDigitCount: number = 1; + private _reservedHeight: number = 0; + + private readonly _computeOptionsMemory: ComputeOptionsMemory = new ComputeOptionsMemory(); + /** + * Raw options as they were passed in and merged with all calls to `updateOptions`. + */ + private readonly _rawOptions: IEditorOptions; + /** + * Validated version of `_rawOptions`. + */ + private _validatedOptions: ValidatedEditorOptions; + /** + * Complete options which are a combination of passed in options and env values. + */ + public options: ComputedEditorOptions; + + constructor( + isSimpleWidget: boolean, + options: Readonly, + container: HTMLElement | null, + @IAccessibilityService private readonly _accessibilityService: IAccessibilityService + ) { + super(); + this.isSimpleWidget = isSimpleWidget; + this._containerObserver = this._register(new ElementSizeObserver(container, options.dimension)); + + this._rawOptions = deepCloneAndMigrateOptions(options); + this._validatedOptions = EditorOptionsUtil.validateOptions(this._rawOptions); + this.options = this._computeOptions(); + + if (this.options.get(EditorOption.automaticLayout)) { + this._containerObserver.startObserving(); + } + + this._register(EditorZoom.onDidChangeZoomLevel(() => this._recomputeOptions())); + this._register(TabFocus.onDidChangeTabFocus(() => this._recomputeOptions())); + this._register(this._containerObserver.onDidChange(() => this._recomputeOptions())); + this._register(FontMeasurements.onDidChange(() => this._recomputeOptions())); + this._register(browser.PixelRatio.onDidChange(() => this._recomputeOptions())); + this._register(this._accessibilityService.onDidChangeScreenReaderOptimized(() => this._recomputeOptions())); + } + + private _recomputeOptions(): void { + const newOptions = this._computeOptions(); + const changeEvent = EditorOptionsUtil.checkEquals(this.options, newOptions); + if (changeEvent === null) { + // nothing changed! + return; + } + + this.options = newOptions; + this._onDidChangeFast.fire(changeEvent); + this._onDidChange.fire(changeEvent); + } + + private _computeOptions(): ComputedEditorOptions { + const partialEnv = this._readEnvConfiguration(); + const bareFontInfo = BareFontInfo.createFromValidatedSettings(this._validatedOptions, partialEnv.pixelRatio, this.isSimpleWidget); + const fontInfo = this._readFontInfo(bareFontInfo); + const env: IEnvironmentalOptions = { + memory: this._computeOptionsMemory, + outerWidth: partialEnv.outerWidth, + outerHeight: partialEnv.outerHeight - this._reservedHeight, + fontInfo: fontInfo, + extraEditorClassName: partialEnv.extraEditorClassName, + isDominatedByLongLines: this._isDominatedByLongLines, + viewLineCount: this._viewLineCount, + lineNumbersDigitCount: this._lineNumbersDigitCount, + emptySelectionClipboard: partialEnv.emptySelectionClipboard, + pixelRatio: partialEnv.pixelRatio, + tabFocusMode: TabFocus.getTabFocusMode(), + accessibilitySupport: partialEnv.accessibilitySupport + }; + return EditorOptionsUtil.computeOptions(this._validatedOptions, env); + } + + protected _readEnvConfiguration(): IEnvConfiguration { + return { + extraEditorClassName: getExtraEditorClassName(), + outerWidth: this._containerObserver.getWidth(), + outerHeight: this._containerObserver.getHeight(), + emptySelectionClipboard: browser.isWebKit || browser.isFirefox, + pixelRatio: browser.PixelRatio.value, + accessibilitySupport: ( + this._accessibilityService.isScreenReaderOptimized() + ? AccessibilitySupport.Enabled + : this._accessibilityService.getAccessibilitySupport() + ) + }; + } + + protected _readFontInfo(bareFontInfo: BareFontInfo): FontInfo { + return FontMeasurements.readFontInfo(bareFontInfo); + } + + public getRawOptions(): IEditorOptions { + return this._rawOptions; + } + + public updateOptions(_newOptions: Readonly): void { + const newOptions = deepCloneAndMigrateOptions(_newOptions); + + const didChange = EditorOptionsUtil.applyUpdate(this._rawOptions, newOptions); + if (!didChange) { + return; + } + + this._validatedOptions = EditorOptionsUtil.validateOptions(this._rawOptions); + this._recomputeOptions(); + } + + public observeContainer(dimension?: IDimension): void { + this._containerObserver.observe(dimension); + } + + public setIsDominatedByLongLines(isDominatedByLongLines: boolean): void { + if (this._isDominatedByLongLines === isDominatedByLongLines) { + return; + } + this._isDominatedByLongLines = isDominatedByLongLines; + this._recomputeOptions(); + } + + public setModelLineCount(modelLineCount: number): void { + const lineNumbersDigitCount = digitCount(modelLineCount); + if (this._lineNumbersDigitCount === lineNumbersDigitCount) { + return; + } + this._lineNumbersDigitCount = lineNumbersDigitCount; + this._recomputeOptions(); + } + + public setViewLineCount(viewLineCount: number): void { + if (this._viewLineCount === viewLineCount) { + return; + } + this._viewLineCount = viewLineCount; + this._recomputeOptions(); + } + + public setReservedHeight(reservedHeight: number) { + if (this._reservedHeight === reservedHeight) { + return; + } + this._reservedHeight = reservedHeight; + this._recomputeOptions(); + } +} + +function digitCount(n: number): number { + let r = 0; + while (n) { + n = Math.floor(n / 10); + r++; + } + return r ? r : 1; +} + +function getExtraEditorClassName(): string { + let extra = ''; + if (!browser.isSafari && !browser.isWebkitWebView) { + // Use user-select: none in all browsers except Safari and native macOS WebView + extra += 'no-user-select '; + } + if (browser.isSafari) { + // See https://github.com/microsoft/vscode/issues/108822 + extra += 'no-minimap-shadow '; + } + if (platform.isMacintosh) { + extra += 'mac '; + } + return extra; +} + +export interface IEnvConfiguration { + extraEditorClassName: string; + outerWidth: number; + outerHeight: number; + emptySelectionClipboard: boolean; + pixelRatio: number; + accessibilitySupport: AccessibilitySupport; +} + +class ValidatedEditorOptions implements IValidatedEditorOptions { + private readonly _values: any[] = []; + public _read(option: EditorOption): T { + return this._values[option]; + } + public get(id: T): FindComputedEditorOptionValueById { + return this._values[id]; + } + public _write(option: EditorOption, value: T): void { + this._values[option] = value; + } +} + +export class ComputedEditorOptions implements IComputedEditorOptions { + private readonly _values: any[] = []; + public _read(id: EditorOption): T { + if (id >= this._values.length) { + throw new Error('Cannot read uninitialized value'); + } + return this._values[id]; + } + public get(id: T): FindComputedEditorOptionValueById { + return this._read(id); + } + public _write(id: EditorOption, value: T): void { + this._values[id] = value; + } +} + +class EditorOptionsUtil { + + public static validateOptions(options: IEditorOptions): ValidatedEditorOptions { + const result = new ValidatedEditorOptions(); + for (const editorOption of editorOptionsRegistry) { + const value = (editorOption.name === '_never_' ? undefined : (options as any)[editorOption.name]); + result._write(editorOption.id, editorOption.validate(value)); + } + return result; + } + + public static computeOptions(options: ValidatedEditorOptions, env: IEnvironmentalOptions): ComputedEditorOptions { + const result = new ComputedEditorOptions(); + for (const editorOption of editorOptionsRegistry) { + result._write(editorOption.id, editorOption.compute(env, result, options._read(editorOption.id))); + } + return result; + } + + private static _deepEquals(a: T, b: T): boolean { + if (typeof a !== 'object' || typeof b !== 'object' || !a || !b) { + return a === b; + } + if (Array.isArray(a) || Array.isArray(b)) { + return (Array.isArray(a) && Array.isArray(b) ? arrays.equals(a, b) : false); + } + if (Object.keys(a).length !== Object.keys(b).length) { + return false; + } + for (const key in a) { + if (!EditorOptionsUtil._deepEquals(a[key], b[key])) { + return false; + } + } + return true; + } + + public static checkEquals(a: ComputedEditorOptions, b: ComputedEditorOptions): ConfigurationChangedEvent | null { + const result: boolean[] = []; + let somethingChanged = false; + for (const editorOption of editorOptionsRegistry) { + const changed = !EditorOptionsUtil._deepEquals(a._read(editorOption.id), b._read(editorOption.id)); + result[editorOption.id] = changed; + if (changed) { + somethingChanged = true; + } + } + return (somethingChanged ? new ConfigurationChangedEvent(result) : null); + } + + /** + * Returns true if something changed. + * Modifies `options`. + */ + public static applyUpdate(options: IEditorOptions, update: Readonly): boolean { + let changed = false; + for (const editorOption of editorOptionsRegistry) { + if (update.hasOwnProperty(editorOption.name)) { + const result = editorOption.applyUpdate((options as any)[editorOption.name], (update as any)[editorOption.name]); + (options as any)[editorOption.name] = result.newValue; + changed = changed || result.didChange; + } + } + return changed; + } +} + +function deepCloneAndMigrateOptions(_options: Readonly): IEditorOptions { + const options = objects.deepClone(_options); + migrateOptions(options); + return options; +} diff --git a/src/vs/editor/browser/config/elementSizeObserver.ts b/src/vs/editor/browser/config/elementSizeObserver.ts index cf42a075793b7..3529822e209c2 100644 --- a/src/vs/editor/browser/config/elementSizeObserver.ts +++ b/src/vs/editor/browser/config/elementSizeObserver.ts @@ -4,51 +4,25 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; -import { IDimension } from 'vs/editor/common/editorCommon'; - -interface ResizeObserver { - observe(target: Element): void; - unobserve(target: Element): void; - disconnect(): void; -} - -interface ResizeObserverSize { - inlineSize: number; - blockSize: number; -} - -interface ResizeObserverEntry { - readonly target: Element; - readonly contentRect: DOMRectReadOnly; - readonly borderBoxSize: ResizeObserverSize; - readonly contentBoxSize: ResizeObserverSize; -} - -type ResizeObserverCallback = (entries: ReadonlyArray, observer: ResizeObserver) => void; - -declare const ResizeObserver: { - prototype: ResizeObserver; - new(callback: ResizeObserverCallback): ResizeObserver; -}; - +import { IDimension } from 'vs/editor/common/core/dimension'; +import { Emitter, Event } from 'vs/base/common/event'; export class ElementSizeObserver extends Disposable { - private readonly referenceDomElement: HTMLElement | null; - private readonly changeCallback: () => void; - private width: number; - private height: number; - private resizeObserver: ResizeObserver | null; - private measureReferenceDomElementToken: number; + private _onDidChange = this._register(new Emitter()); + public readonly onDidChange: Event = this._onDidChange.event; - constructor(referenceDomElement: HTMLElement | null, dimension: IDimension | undefined, changeCallback: () => void) { + private readonly _referenceDomElement: HTMLElement | null; + private _width: number; + private _height: number; + private _resizeObserver: ResizeObserver | null; + + constructor(referenceDomElement: HTMLElement | null, dimension: IDimension | undefined) { super(); - this.referenceDomElement = referenceDomElement; - this.changeCallback = changeCallback; - this.width = -1; - this.height = -1; - this.resizeObserver = null; - this.measureReferenceDomElementToken = -1; + this._referenceDomElement = referenceDomElement; + this._width = -1; + this._height = -1; + this._resizeObserver = null; this.measureReferenceDomElement(false, dimension); } @@ -58,41 +32,30 @@ export class ElementSizeObserver extends Disposable { } public getWidth(): number { - return this.width; + return this._width; } public getHeight(): number { - return this.height; + return this._height; } public startObserving(): void { - if (typeof ResizeObserver !== 'undefined') { - if (!this.resizeObserver && this.referenceDomElement) { - this.resizeObserver = new ResizeObserver((entries) => { - if (entries && entries[0] && entries[0].contentRect) { - this.observe({ width: entries[0].contentRect.width, height: entries[0].contentRect.height }); - } else { - this.observe(); - } - }); - this.resizeObserver.observe(this.referenceDomElement); - } - } else { - if (this.measureReferenceDomElementToken === -1) { - // setInterval type defaults to NodeJS.Timeout instead of number, so specify it as a number - this.measureReferenceDomElementToken = setInterval(() => this.observe(), 100); - } + if (!this._resizeObserver && this._referenceDomElement) { + this._resizeObserver = new ResizeObserver((entries) => { + if (entries && entries[0] && entries[0].contentRect) { + this.observe({ width: entries[0].contentRect.width, height: entries[0].contentRect.height }); + } else { + this.observe(); + } + }); + this._resizeObserver.observe(this._referenceDomElement); } } public stopObserving(): void { - if (this.resizeObserver) { - this.resizeObserver.disconnect(); - this.resizeObserver = null; - } - if (this.measureReferenceDomElementToken !== -1) { - clearInterval(this.measureReferenceDomElementToken); - this.measureReferenceDomElementToken = -1; + if (this._resizeObserver) { + this._resizeObserver.disconnect(); + this._resizeObserver = null; } } @@ -100,25 +63,24 @@ export class ElementSizeObserver extends Disposable { this.measureReferenceDomElement(true, dimension); } - private measureReferenceDomElement(callChangeCallback: boolean, dimension?: IDimension): void { + private measureReferenceDomElement(emitEvent: boolean, dimension?: IDimension): void { let observedWidth = 0; let observedHeight = 0; if (dimension) { observedWidth = dimension.width; observedHeight = dimension.height; - } else if (this.referenceDomElement) { - observedWidth = this.referenceDomElement.clientWidth; - observedHeight = this.referenceDomElement.clientHeight; + } else if (this._referenceDomElement) { + observedWidth = this._referenceDomElement.clientWidth; + observedHeight = this._referenceDomElement.clientHeight; } observedWidth = Math.max(5, observedWidth); observedHeight = Math.max(5, observedHeight); - if (this.width !== observedWidth || this.height !== observedHeight) { - this.width = observedWidth; - this.height = observedHeight; - if (callChangeCallback) { - this.changeCallback(); + if (this._width !== observedWidth || this._height !== observedHeight) { + this._width = observedWidth; + this._height = observedHeight; + if (emitEvent) { + this._onDidChange.fire(); } } } - } diff --git a/src/vs/editor/browser/config/fontMeasurements.ts b/src/vs/editor/browser/config/fontMeasurements.ts new file mode 100644 index 0000000000000..61fc14448a76d --- /dev/null +++ b/src/vs/editor/browser/config/fontMeasurements.ts @@ -0,0 +1,275 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as browser from 'vs/base/browser/browser'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { CharWidthRequest, CharWidthRequestType, readCharWidths } from 'vs/editor/browser/config/charWidthReader'; +import { EditorFontLigatures } from 'vs/editor/common/config/editorOptions'; +import { BareFontInfo, FontInfo, SERIALIZED_FONT_INFO_VERSION } from 'vs/editor/common/config/fontInfo'; + +/** + * Serializable font information. + */ +export interface ISerializedFontInfo { + readonly version: number; + readonly pixelRatio: number; + readonly fontFamily: string; + readonly fontWeight: string; + readonly fontSize: number; + readonly fontFeatureSettings: string; + readonly lineHeight: number; + readonly letterSpacing: number; + readonly isMonospace: boolean; + readonly typicalHalfwidthCharacterWidth: number; + readonly typicalFullwidthCharacterWidth: number; + readonly canUseHalfwidthRightwardsArrow: boolean; + readonly spaceWidth: number; + readonly middotWidth: number; + readonly wsmiddotWidth: number; + readonly maxDigitWidth: number; +} + +class FontMeasurementsImpl extends Disposable { + + private _cache: FontMeasurementsCache; + private _evictUntrustedReadingsTimeout: number; + + private readonly _onDidChange = this._register(new Emitter()); + public readonly onDidChange: Event = this._onDidChange.event; + + constructor() { + super(); + + this._cache = new FontMeasurementsCache(); + this._evictUntrustedReadingsTimeout = -1; + } + + public override dispose(): void { + if (this._evictUntrustedReadingsTimeout !== -1) { + window.clearTimeout(this._evictUntrustedReadingsTimeout); + this._evictUntrustedReadingsTimeout = -1; + } + super.dispose(); + } + + /** + * Clear all cached font information and trigger a change event. + */ + public clearAllFontInfos(): void { + this._cache = new FontMeasurementsCache(); + this._onDidChange.fire(); + } + + private _writeToCache(item: BareFontInfo, value: FontInfo): void { + this._cache.put(item, value); + + if (!value.isTrusted && this._evictUntrustedReadingsTimeout === -1) { + // Try reading again after some time + this._evictUntrustedReadingsTimeout = window.setTimeout(() => { + this._evictUntrustedReadingsTimeout = -1; + this._evictUntrustedReadings(); + }, 5000); + } + } + + private _evictUntrustedReadings(): void { + const values = this._cache.getValues(); + let somethingRemoved = false; + for (const item of values) { + if (!item.isTrusted) { + somethingRemoved = true; + this._cache.remove(item); + } + } + if (somethingRemoved) { + this._onDidChange.fire(); + } + } + + /** + * Serialized currently cached font information. + */ + public serializeFontInfo(): ISerializedFontInfo[] { + // Only save trusted font info (that has been measured in this running instance) + return this._cache.getValues().filter(item => item.isTrusted); + } + + /** + * Restore previously serialized font informations. + */ + public restoreFontInfo(savedFontInfos: ISerializedFontInfo[]): void { + // Take all the saved font info and insert them in the cache without the trusted flag. + // The reason for this is that a font might have been installed on the OS in the meantime. + for (const savedFontInfo of savedFontInfos) { + if (savedFontInfo.version !== SERIALIZED_FONT_INFO_VERSION) { + // cannot use older version + continue; + } + const fontInfo = new FontInfo(savedFontInfo, false); + this._writeToCache(fontInfo, fontInfo); + } + } + + /** + * Read font information. + */ + public readFontInfo(bareFontInfo: BareFontInfo): FontInfo { + if (!this._cache.has(bareFontInfo)) { + let readConfig = this._actualReadFontInfo(bareFontInfo); + + if (readConfig.typicalHalfwidthCharacterWidth <= 2 || readConfig.typicalFullwidthCharacterWidth <= 2 || readConfig.spaceWidth <= 2 || readConfig.maxDigitWidth <= 2) { + // Hey, it's Bug 14341 ... we couldn't read + readConfig = new FontInfo({ + pixelRatio: browser.PixelRatio.value, + fontFamily: readConfig.fontFamily, + fontWeight: readConfig.fontWeight, + fontSize: readConfig.fontSize, + fontFeatureSettings: readConfig.fontFeatureSettings, + lineHeight: readConfig.lineHeight, + letterSpacing: readConfig.letterSpacing, + isMonospace: readConfig.isMonospace, + typicalHalfwidthCharacterWidth: Math.max(readConfig.typicalHalfwidthCharacterWidth, 5), + typicalFullwidthCharacterWidth: Math.max(readConfig.typicalFullwidthCharacterWidth, 5), + canUseHalfwidthRightwardsArrow: readConfig.canUseHalfwidthRightwardsArrow, + spaceWidth: Math.max(readConfig.spaceWidth, 5), + middotWidth: Math.max(readConfig.middotWidth, 5), + wsmiddotWidth: Math.max(readConfig.wsmiddotWidth, 5), + maxDigitWidth: Math.max(readConfig.maxDigitWidth, 5), + }, false); + } + + this._writeToCache(bareFontInfo, readConfig); + } + return this._cache.get(bareFontInfo); + } + + private _createRequest(chr: string, type: CharWidthRequestType, all: CharWidthRequest[], monospace: CharWidthRequest[] | null): CharWidthRequest { + const result = new CharWidthRequest(chr, type); + all.push(result); + if (monospace) { + monospace.push(result); + } + return result; + } + + private _actualReadFontInfo(bareFontInfo: BareFontInfo): FontInfo { + const all: CharWidthRequest[] = []; + const monospace: CharWidthRequest[] = []; + + const typicalHalfwidthCharacter = this._createRequest('n', CharWidthRequestType.Regular, all, monospace); + const typicalFullwidthCharacter = this._createRequest('\uff4d', CharWidthRequestType.Regular, all, null); + const space = this._createRequest(' ', CharWidthRequestType.Regular, all, monospace); + const digit0 = this._createRequest('0', CharWidthRequestType.Regular, all, monospace); + const digit1 = this._createRequest('1', CharWidthRequestType.Regular, all, monospace); + const digit2 = this._createRequest('2', CharWidthRequestType.Regular, all, monospace); + const digit3 = this._createRequest('3', CharWidthRequestType.Regular, all, monospace); + const digit4 = this._createRequest('4', CharWidthRequestType.Regular, all, monospace); + const digit5 = this._createRequest('5', CharWidthRequestType.Regular, all, monospace); + const digit6 = this._createRequest('6', CharWidthRequestType.Regular, all, monospace); + const digit7 = this._createRequest('7', CharWidthRequestType.Regular, all, monospace); + const digit8 = this._createRequest('8', CharWidthRequestType.Regular, all, monospace); + const digit9 = this._createRequest('9', CharWidthRequestType.Regular, all, monospace); + + // monospace test: used for whitespace rendering + const rightwardsArrow = this._createRequest('→', CharWidthRequestType.Regular, all, monospace); + const halfwidthRightwardsArrow = this._createRequest('→', CharWidthRequestType.Regular, all, null); + + // U+00B7 - MIDDLE DOT + const middot = this._createRequest('·', CharWidthRequestType.Regular, all, monospace); + + // U+2E31 - WORD SEPARATOR MIDDLE DOT + const wsmiddotWidth = this._createRequest(String.fromCharCode(0x2E31), CharWidthRequestType.Regular, all, null); + + // monospace test: some characters + const monospaceTestChars = '|/-_ilm%'; + for (let i = 0, len = monospaceTestChars.length; i < len; i++) { + this._createRequest(monospaceTestChars.charAt(i), CharWidthRequestType.Regular, all, monospace); + this._createRequest(monospaceTestChars.charAt(i), CharWidthRequestType.Italic, all, monospace); + this._createRequest(monospaceTestChars.charAt(i), CharWidthRequestType.Bold, all, monospace); + } + + readCharWidths(bareFontInfo, all); + + const maxDigitWidth = Math.max(digit0.width, digit1.width, digit2.width, digit3.width, digit4.width, digit5.width, digit6.width, digit7.width, digit8.width, digit9.width); + + let isMonospace = (bareFontInfo.fontFeatureSettings === EditorFontLigatures.OFF); + const referenceWidth = monospace[0].width; + for (let i = 1, len = monospace.length; isMonospace && i < len; i++) { + const diff = referenceWidth - monospace[i].width; + if (diff < -0.001 || diff > 0.001) { + isMonospace = false; + break; + } + } + + let canUseHalfwidthRightwardsArrow = true; + if (isMonospace && halfwidthRightwardsArrow.width !== referenceWidth) { + // using a halfwidth rightwards arrow would break monospace... + canUseHalfwidthRightwardsArrow = false; + } + if (halfwidthRightwardsArrow.width > rightwardsArrow.width) { + // using a halfwidth rightwards arrow would paint a larger arrow than a regular rightwards arrow + canUseHalfwidthRightwardsArrow = false; + } + + return new FontInfo({ + pixelRatio: browser.PixelRatio.value, + fontFamily: bareFontInfo.fontFamily, + fontWeight: bareFontInfo.fontWeight, + fontSize: bareFontInfo.fontSize, + fontFeatureSettings: bareFontInfo.fontFeatureSettings, + lineHeight: bareFontInfo.lineHeight, + letterSpacing: bareFontInfo.letterSpacing, + isMonospace: isMonospace, + typicalHalfwidthCharacterWidth: typicalHalfwidthCharacter.width, + typicalFullwidthCharacterWidth: typicalFullwidthCharacter.width, + canUseHalfwidthRightwardsArrow: canUseHalfwidthRightwardsArrow, + spaceWidth: space.width, + middotWidth: middot.width, + wsmiddotWidth: wsmiddotWidth.width, + maxDigitWidth: maxDigitWidth + }, true); + } +} + +class FontMeasurementsCache { + + private readonly _keys: { [key: string]: BareFontInfo; }; + private readonly _values: { [key: string]: FontInfo; }; + + constructor() { + this._keys = Object.create(null); + this._values = Object.create(null); + } + + public has(item: BareFontInfo): boolean { + const itemId = item.getId(); + return !!this._values[itemId]; + } + + public get(item: BareFontInfo): FontInfo { + const itemId = item.getId(); + return this._values[itemId]; + } + + public put(item: BareFontInfo, value: FontInfo): void { + const itemId = item.getId(); + this._keys[itemId] = item; + this._values[itemId] = value; + } + + public remove(item: BareFontInfo): void { + const itemId = item.getId(); + delete this._keys[itemId]; + delete this._values[itemId]; + } + + public getValues(): FontInfo[] { + return Object.keys(this._keys).map(id => this._values[id]); + } +} + +export const FontMeasurements = new FontMeasurementsImpl(); diff --git a/src/vs/editor/browser/config/migrateOptions.ts b/src/vs/editor/browser/config/migrateOptions.ts new file mode 100644 index 0000000000000..713fd69f4bdda --- /dev/null +++ b/src/vs/editor/browser/config/migrateOptions.ts @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { forEach } from 'vs/base/common/collections'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; + +/** + * Compatibility with old options + */ +export function migrateOptions(options: IEditorOptions): void { + const wordWrap = options.wordWrap; + if (wordWrap === true) { + options.wordWrap = 'on'; + } else if (wordWrap === false) { + options.wordWrap = 'off'; + } + + const lineNumbers = options.lineNumbers; + if (lineNumbers === true) { + options.lineNumbers = 'on'; + } else if (lineNumbers === false) { + options.lineNumbers = 'off'; + } + + const autoClosingBrackets = options.autoClosingBrackets; + if (autoClosingBrackets === false) { + options.autoClosingBrackets = 'never'; + options.autoClosingQuotes = 'never'; + options.autoSurround = 'never'; + } + + const cursorBlinking = options.cursorBlinking; + if (cursorBlinking === 'visible') { + options.cursorBlinking = 'solid'; + } + + const renderWhitespace = options.renderWhitespace; + if (renderWhitespace === true) { + options.renderWhitespace = 'boundary'; + } else if (renderWhitespace === false) { + options.renderWhitespace = 'none'; + } + + const renderLineHighlight = options.renderLineHighlight; + if (renderLineHighlight === true) { + options.renderLineHighlight = 'line'; + } else if (renderLineHighlight === false) { + options.renderLineHighlight = 'none'; + } + + const acceptSuggestionOnEnter = options.acceptSuggestionOnEnter; + if (acceptSuggestionOnEnter === true) { + options.acceptSuggestionOnEnter = 'on'; + } else if (acceptSuggestionOnEnter === false) { + options.acceptSuggestionOnEnter = 'off'; + } + + const tabCompletion = options.tabCompletion; + if (tabCompletion === false) { + options.tabCompletion = 'off'; + } else if (tabCompletion === true) { + options.tabCompletion = 'onlySnippets'; + } + + const suggest = options.suggest; + if (suggest && typeof (suggest).filteredTypes === 'object' && (suggest).filteredTypes) { + const mapping: Record = {}; + mapping['method'] = 'showMethods'; + mapping['function'] = 'showFunctions'; + mapping['constructor'] = 'showConstructors'; + mapping['deprecated'] = 'showDeprecated'; + mapping['field'] = 'showFields'; + mapping['variable'] = 'showVariables'; + mapping['class'] = 'showClasses'; + mapping['struct'] = 'showStructs'; + mapping['interface'] = 'showInterfaces'; + mapping['module'] = 'showModules'; + mapping['property'] = 'showProperties'; + mapping['event'] = 'showEvents'; + mapping['operator'] = 'showOperators'; + mapping['unit'] = 'showUnits'; + mapping['value'] = 'showValues'; + mapping['constant'] = 'showConstants'; + mapping['enum'] = 'showEnums'; + mapping['enumMember'] = 'showEnumMembers'; + mapping['keyword'] = 'showKeywords'; + mapping['text'] = 'showWords'; + mapping['color'] = 'showColors'; + mapping['file'] = 'showFiles'; + mapping['reference'] = 'showReferences'; + mapping['folder'] = 'showFolders'; + mapping['typeParameter'] = 'showTypeParameters'; + mapping['snippet'] = 'showSnippets'; + forEach(mapping, entry => { + const value = (suggest).filteredTypes[entry.key]; + if (value === false) { + (suggest)[entry.value] = value; + } + }); + // delete (suggest).filteredTypes; + } + + const hover = options.hover; + if (hover === true) { + options.hover = { + enabled: true + }; + } else if (hover === false) { + options.hover = { + enabled: false + }; + } + + const parameterHints = options.parameterHints; + if (parameterHints === true) { + options.parameterHints = { + enabled: true + }; + } else if (parameterHints === false) { + options.parameterHints = { + enabled: false + }; + } + + const autoIndent = options.autoIndent; + if (autoIndent === true) { + options.autoIndent = 'full'; + } else if (autoIndent === false) { + options.autoIndent = 'advanced'; + } + + const matchBrackets = options.matchBrackets; + if (matchBrackets === true) { + options.matchBrackets = 'always'; + } else if (matchBrackets === false) { + options.matchBrackets = 'never'; + } + + const { renderIndentGuides, highlightActiveIndentGuide } = options as any as { + renderIndentGuides: boolean; + highlightActiveIndentGuide: boolean; + }; + if (!options.guides) { + options.guides = {}; + } + + if (renderIndentGuides !== undefined) { + options.guides.indentation = !!renderIndentGuides; + } + if (highlightActiveIndentGuide !== undefined) { + options.guides.highlightActiveIndentation = !!highlightActiveIndentGuide; + } +} diff --git a/src/vs/editor/browser/config/tabFocus.ts b/src/vs/editor/browser/config/tabFocus.ts new file mode 100644 index 0000000000000..70f8bb9205643 --- /dev/null +++ b/src/vs/editor/browser/config/tabFocus.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; + +class TabFocusImpl { + private _tabFocus: boolean = false; + + private readonly _onDidChangeTabFocus = new Emitter(); + public readonly onDidChangeTabFocus: Event = this._onDidChangeTabFocus.event; + + public getTabFocusMode(): boolean { + return this._tabFocus; + } + + public setTabFocusMode(tabFocusMode: boolean): void { + if (this._tabFocus === tabFocusMode) { + return; + } + + this._tabFocus = tabFocusMode; + this._onDidChangeTabFocus.fire(this._tabFocus); + } +} + +/** + * Control what pressing Tab does. + * If it is false, pressing Tab or Shift-Tab will be handled by the editor. + * If it is true, pressing Tab or Shift-Tab will move the browser focus. + * Defaults to false. + */ +export const TabFocus = new TabFocusImpl(); diff --git a/src/vs/editor/browser/controller/coreCommands.ts b/src/vs/editor/browser/controller/coreCommands.ts index b5a9ad8ad9ad6..04f0b7359ee4b 100644 --- a/src/vs/editor/browser/controller/coreCommands.ts +++ b/src/vs/editor/browser/controller/coreCommands.ts @@ -11,17 +11,17 @@ import { status } from 'vs/base/browser/ui/aria/aria'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Command, EditorCommand, ICommandOptions, registerEditorCommand, MultiCommand, UndoCommand, RedoCommand, SelectAllCommand } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { ColumnSelection, IColumnSelectResult } from 'vs/editor/common/controller/cursorColumnSelection'; -import { CursorState, EditOperationType, IColumnSelectData, PartialCursorState } from 'vs/editor/common/controller/cursorCommon'; -import { DeleteOperations } from 'vs/editor/common/controller/cursorDeleteOperations'; -import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; -import { CursorMove as CursorMove_, CursorMoveCommands } from 'vs/editor/common/controller/cursorMoveCommands'; -import { TypeOperations } from 'vs/editor/common/controller/cursorTypeOperations'; +import { ColumnSelection, IColumnSelectResult } from 'vs/editor/common/cursor/cursorColumnSelection'; +import { CursorState, EditOperationType, IColumnSelectData, PartialCursorState } from 'vs/editor/common/cursor/cursorCommon'; +import { DeleteOperations } from 'vs/editor/common/cursor/cursorDeleteOperations'; +import { CursorChangeReason } from 'vs/editor/common/cursor/cursorEvents'; +import { CursorMove as CursorMove_, CursorMoveCommands } from 'vs/editor/common/cursor/cursorMoveCommands'; +import { TypeOperations } from 'vs/editor/common/cursor/cursorTypeOperations'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Handler, ScrollType } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { VerticalRevealType } from 'vs/editor/common/view/viewEvents'; +import { VerticalRevealType } from 'vs/editor/common/viewModel/viewEvents'; import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -329,34 +329,40 @@ export namespace CoreNavigationCommands { class BaseMoveToCommand extends CoreEditorCommand { + private readonly _minimalReveal: boolean; private readonly _inSelectionMode: boolean; - constructor(opts: ICommandOptions & { inSelectionMode: boolean; }) { + constructor(opts: ICommandOptions & { minimalReveal: boolean; inSelectionMode: boolean; }) { super(opts); + this._minimalReveal = opts.minimalReveal; this._inSelectionMode = opts.inSelectionMode; } public runCoreEditorCommand(viewModel: IViewModel, args: any): void { viewModel.model.pushStackElement(); - viewModel.setCursorStates( + const cursorStateChanged = viewModel.setCursorStates( args.source, CursorChangeReason.Explicit, [ CursorMoveCommands.moveTo(viewModel, viewModel.getPrimaryCursorState(), this._inSelectionMode, args.position, args.viewPosition) ] ); - viewModel.revealPrimaryCursor(args.source, true); + if (cursorStateChanged) { + viewModel.revealPrimaryCursor(args.source, true, this._minimalReveal); + } } } export const MoveTo: CoreEditorCommand = registerEditorCommand(new BaseMoveToCommand({ id: '_moveTo', + minimalReveal: true, inSelectionMode: false, precondition: undefined })); export const MoveToSelect: CoreEditorCommand = registerEditorCommand(new BaseMoveToCommand({ id: '_moveToSelect', + minimalReveal: false, inSelectionMode: true, precondition: undefined })); @@ -398,8 +404,8 @@ export namespace CoreNavigationCommands { const validatedPosition = viewModel.model.validatePosition(args.position); const validatedViewPosition = viewModel.coordinatesConverter.validateViewPosition(new Position(args.viewPosition.lineNumber, args.viewPosition.column), validatedPosition); - let fromViewLineNumber = args.doColumnSelect ? prevColumnSelectData.fromViewLineNumber : validatedViewPosition.lineNumber; - let fromViewVisualColumn = args.doColumnSelect ? prevColumnSelectData.fromViewVisualColumn : args.mouseColumn - 1; + const fromViewLineNumber = args.doColumnSelect ? prevColumnSelectData.fromViewLineNumber : validatedViewPosition.lineNumber; + const fromViewVisualColumn = args.doColumnSelect ? prevColumnSelectData.fromViewVisualColumn : args.mouseColumn - 1; return ColumnSelection.columnSelect(viewModel.cursorConfig, viewModel, fromViewLineNumber, fromViewVisualColumn, validatedViewPosition.lineNumber, args.mouseColumn - 1); } }); @@ -598,7 +604,7 @@ export namespace CoreNavigationCommands { direction: this._staticArgs.direction, unit: this._staticArgs.unit, select: this._staticArgs.select, - value: viewModel.cursorConfig.pageSize + value: dynamicArgs.pageSize || viewModel.cursorConfig.pageSize }; } diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index 6743bb2cba23d..8a2279b483072 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -8,16 +8,16 @@ import { StandardWheelEvent, IMouseWheelEvent } from 'vs/base/browser/mouseEvent import { TimeoutTimer } from 'vs/base/common/async'; import { Disposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; -import { HitTestContext, IViewZoneData, MouseTarget, MouseTargetFactory, PointerHandlerLastRenderData } from 'vs/editor/browser/controller/mouseTarget'; -import { IMouseTarget, MouseTargetType } from 'vs/editor/browser/editorBrowser'; -import { ClientCoordinates, EditorMouseEvent, EditorMouseEventFactory, GlobalEditorMouseMoveMonitor, createEditorPagePosition } from 'vs/editor/browser/editorDom'; +import { HitTestContext, MouseTarget, MouseTargetFactory, PointerHandlerLastRenderData } from 'vs/editor/browser/controller/mouseTarget'; +import { IMouseTarget, IMouseTargetViewZoneData, MouseTargetType } from 'vs/editor/browser/editorBrowser'; +import { ClientCoordinates, EditorMouseEvent, EditorMouseEventFactory, GlobalEditorMouseMoveMonitor, createEditorPagePosition, createCoordinatesRelativeToEditor } from 'vs/editor/browser/editorDom'; import { ViewController } from 'vs/editor/browser/view/viewController'; import { EditorZoom } from 'vs/editor/common/config/editorZoom'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; -import { HorizontalPosition } from 'vs/editor/common/view/renderingContext'; -import { ViewContext } from 'vs/editor/common/view/viewContext'; -import * as viewEvents from 'vs/editor/common/view/viewEvents'; +import { HorizontalPosition } from 'vs/editor/browser/view/renderingContext'; +import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; +import * as viewEvents from 'vs/editor/common/viewModel/viewEvents'; import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -172,7 +172,8 @@ export class MouseHandler extends ViewEventHandler { return null; } - return this.mouseTargetFactory.createMouseTarget(this.viewHelper.getLastRenderData(), editorPos, pos, null); + const relativePos = createCoordinatesRelativeToEditor(this.viewHelper.viewDomNode, editorPos, pos); + return this.mouseTargetFactory.createMouseTarget(this.viewHelper.getLastRenderData(), editorPos, pos, relativePos, null); } protected _createMouseTarget(e: EditorMouseEvent, testEventTarget: boolean): IMouseTarget { @@ -185,11 +186,11 @@ export class MouseHandler extends ViewEventHandler { ); } } - return this.mouseTargetFactory.createMouseTarget(this.viewHelper.getLastRenderData(), e.editorPos, e.pos, testEventTarget ? target : null); + return this.mouseTargetFactory.createMouseTarget(this.viewHelper.getLastRenderData(), e.editorPos, e.pos, e.relativePos, testEventTarget ? target : null); } private _getMouseColumn(e: EditorMouseEvent): number { - return this.mouseTargetFactory.getMouseColumn(e.editorPos, e.pos); + return this.mouseTargetFactory.getMouseColumn(e.relativePos); } protected _onContextMenu(e: EditorMouseEvent, testEventTarget: boolean): void { @@ -259,7 +260,7 @@ export class MouseHandler extends ViewEventHandler { // Do not steal focus e.preventDefault(); } else if (targetIsViewZone) { - const viewZoneData = t.detail; + const viewZoneData = t.detail; if (this.viewHelper.shouldSuppressMouseDownOnViewZone(viewZoneData.viewZoneId)) { focus(); this._mouseDownOperation.start(t.type, e); @@ -454,7 +455,7 @@ class MouseDownOperation extends Disposable { this._currentSelection = e.selections[0]; } - private _getPositionOutsideEditor(e: EditorMouseEvent): MouseTarget | null { + private _getPositionOutsideEditor(e: EditorMouseEvent): IMouseTarget | null { const editorContent = e.editorPos; const model = this._context.model; const viewLayout = this._context.viewLayout; @@ -467,42 +468,42 @@ class MouseDownOperation extends Disposable { if (viewZoneData) { const newPosition = this._helpPositionJumpOverViewZone(viewZoneData); if (newPosition) { - return new MouseTarget(null, MouseTargetType.OUTSIDE_EDITOR, mouseColumn, newPosition); + return MouseTarget.createOutsideEditor(mouseColumn, newPosition); } } const aboveLineNumber = viewLayout.getLineNumberAtVerticalOffset(verticalOffset); - return new MouseTarget(null, MouseTargetType.OUTSIDE_EDITOR, mouseColumn, new Position(aboveLineNumber, 1)); + return MouseTarget.createOutsideEditor(mouseColumn, new Position(aboveLineNumber, 1)); } if (e.posy > editorContent.y + editorContent.height) { - const verticalOffset = viewLayout.getCurrentScrollTop() + (e.posy - editorContent.y); + const verticalOffset = viewLayout.getCurrentScrollTop() + e.relativePos.y; const viewZoneData = HitTestContext.getZoneAtCoord(this._context, verticalOffset); if (viewZoneData) { const newPosition = this._helpPositionJumpOverViewZone(viewZoneData); if (newPosition) { - return new MouseTarget(null, MouseTargetType.OUTSIDE_EDITOR, mouseColumn, newPosition); + return MouseTarget.createOutsideEditor(mouseColumn, newPosition); } } const belowLineNumber = viewLayout.getLineNumberAtVerticalOffset(verticalOffset); - return new MouseTarget(null, MouseTargetType.OUTSIDE_EDITOR, mouseColumn, new Position(belowLineNumber, model.getLineMaxColumn(belowLineNumber))); + return MouseTarget.createOutsideEditor(mouseColumn, new Position(belowLineNumber, model.getLineMaxColumn(belowLineNumber))); } - const possibleLineNumber = viewLayout.getLineNumberAtVerticalOffset(viewLayout.getCurrentScrollTop() + (e.posy - editorContent.y)); + const possibleLineNumber = viewLayout.getLineNumberAtVerticalOffset(viewLayout.getCurrentScrollTop() + e.relativePos.y); if (e.posx < editorContent.x) { - return new MouseTarget(null, MouseTargetType.OUTSIDE_EDITOR, mouseColumn, new Position(possibleLineNumber, 1)); + return MouseTarget.createOutsideEditor(mouseColumn, new Position(possibleLineNumber, 1)); } if (e.posx > editorContent.x + editorContent.width) { - return new MouseTarget(null, MouseTargetType.OUTSIDE_EDITOR, mouseColumn, new Position(possibleLineNumber, model.getLineMaxColumn(possibleLineNumber))); + return MouseTarget.createOutsideEditor(mouseColumn, new Position(possibleLineNumber, model.getLineMaxColumn(possibleLineNumber))); } return null; } - private _findMousePosition(e: EditorMouseEvent, testEventTarget: boolean): MouseTarget | null { + private _findMousePosition(e: EditorMouseEvent, testEventTarget: boolean): IMouseTarget | null { const positionOutsideEditor = this._getPositionOutsideEditor(e); if (positionOutsideEditor) { return positionOutsideEditor; @@ -515,16 +516,16 @@ class MouseDownOperation extends Disposable { } if (t.type === MouseTargetType.CONTENT_VIEW_ZONE || t.type === MouseTargetType.GUTTER_VIEW_ZONE) { - const newPosition = this._helpPositionJumpOverViewZone(t.detail); + const newPosition = this._helpPositionJumpOverViewZone(t.detail); if (newPosition) { - return new MouseTarget(t.element, t.type, t.mouseColumn, newPosition, null, t.detail); + return MouseTarget.createViewZone(t.type, t.element, t.mouseColumn, newPosition, t.detail); } } return t; } - private _helpPositionJumpOverViewZone(viewZoneData: IViewZoneData): Position | null { + private _helpPositionJumpOverViewZone(viewZoneData: IMouseTargetViewZoneData): Position | null { // Force position on view zones to go above or below depending on where selection started from const selectionStart = new Position(this._currentSelection.selectionStartLineNumber, this._currentSelection.selectionStartColumn); const positionBefore = viewZoneData.positionBefore; @@ -540,7 +541,7 @@ class MouseDownOperation extends Disposable { return null; } - private _dispatchMouse(position: MouseTarget, inSelectionMode: boolean): void { + private _dispatchMouse(position: IMouseTarget, inSelectionMode: boolean): void { if (!position.position) { return; } diff --git a/src/vs/editor/browser/controller/mouseTarget.ts b/src/vs/editor/browser/controller/mouseTarget.ts index e6dc5ca034a30..fe7424ed899d4 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -4,51 +4,26 @@ *--------------------------------------------------------------------------------------------*/ import { IPointerHandlerHelper } from 'vs/editor/browser/controller/mouseHandler'; -import { IMouseTarget, MouseTargetType } from 'vs/editor/browser/editorBrowser'; -import { ClientCoordinates, EditorMouseEvent, EditorPagePosition, PageCoordinates } from 'vs/editor/browser/editorDom'; +import { IMouseTargetContentEmptyData, IMouseTargetMarginData, IMouseTarget, IMouseTargetContentEmpty, IMouseTargetContentText, IMouseTargetContentWidget, IMouseTargetMargin, IMouseTargetOutsideEditor, IMouseTargetOverlayWidget, IMouseTargetScrollbar, IMouseTargetTextarea, IMouseTargetUnknown, IMouseTargetViewZone, IMouseTargetContentTextData, IMouseTargetViewZoneData, MouseTargetType } from 'vs/editor/browser/editorBrowser'; +import { ClientCoordinates, EditorMouseEvent, EditorPagePosition, PageCoordinates, CoordinatesRelativeToEditor } from 'vs/editor/browser/editorDom'; import { PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart'; import { ViewLine } from 'vs/editor/browser/viewParts/lines/viewLine'; import { IViewCursorRenderData } from 'vs/editor/browser/viewParts/viewCursors/viewCursor'; import { EditorLayoutInfo, EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { Range as EditorRange } from 'vs/editor/common/core/range'; -import { HorizontalPosition } from 'vs/editor/common/view/renderingContext'; -import { ViewContext } from 'vs/editor/common/view/viewContext'; +import { HorizontalPosition } from 'vs/editor/browser/view/renderingContext'; +import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; -import { CursorColumns } from 'vs/editor/common/controller/cursorCommon'; +import { CursorColumns } from 'vs/editor/common/core/cursorColumns'; import * as dom from 'vs/base/browser/dom'; -import { AtomicTabMoveOperations, Direction } from 'vs/editor/common/controller/cursorAtomicMoveOperations'; +import { AtomicTabMoveOperations, Direction } from 'vs/editor/common/cursor/cursorAtomicMoveOperations'; import { PositionAffinity } from 'vs/editor/common/model'; import { InjectedText } from 'vs/editor/common/viewModel/modelLineProjectionData'; -export interface IViewZoneData { - viewZoneId: string; - positionBefore: Position | null; - positionAfter: Position | null; - position: Position; - afterLineNumber: number; -} - -export interface IMarginData { - isAfterLines: boolean; - glyphMarginLeft: number; - glyphMarginWidth: number; - lineNumbersWidth: number; - offsetX: number; -} - -export interface IEmptyContentData { - isAfterLines: boolean; - horizontalDistanceToText?: number; -} - -export interface ITextContentData { - mightBeForeignElement: boolean; -} - const enum HitTestResultType { - Unknown = 0, - Content = 1, + Unknown, + Content, } class UnknownHitTestResult { @@ -86,25 +61,46 @@ export class PointerHandlerLastRenderData { ) { } } -export class MouseTarget implements IMouseTarget { - - public readonly element: Element | null; - public readonly type: MouseTargetType; - public readonly mouseColumn: number; - public readonly position: Position | null; - public readonly range: EditorRange | null; - public readonly detail: any; +export class MouseTarget { - constructor(element: Element | null, type: MouseTargetType, mouseColumn: number = 0, position: Position | null = null, range: EditorRange | null = null, detail: any = null) { - this.element = element; - this.type = type; - this.mouseColumn = mouseColumn; - this.position = position; + private static _deduceRage(position: Position): EditorRange; + private static _deduceRage(position: Position, range: EditorRange | null): EditorRange; + private static _deduceRage(position: Position | null): EditorRange | null; + private static _deduceRage(position: Position | null, range: EditorRange | null = null): EditorRange | null { if (!range && position) { - range = new EditorRange(position.lineNumber, position.column, position.lineNumber, position.column); + return new EditorRange(position.lineNumber, position.column, position.lineNumber, position.column); } - this.range = range; - this.detail = detail; + return range ?? null; + } + public static createUnknown(element: Element | null, mouseColumn: number, position: Position | null): IMouseTargetUnknown { + return { type: MouseTargetType.UNKNOWN, element, mouseColumn, position, range: this._deduceRage(position) }; + } + public static createTextarea(element: Element | null, mouseColumn: number): IMouseTargetTextarea { + return { type: MouseTargetType.TEXTAREA, element, mouseColumn, position: null, range: null }; + } + public static createMargin(type: MouseTargetType.GUTTER_GLYPH_MARGIN | MouseTargetType.GUTTER_LINE_NUMBERS | MouseTargetType.GUTTER_LINE_DECORATIONS, element: Element | null, mouseColumn: number, position: Position, range: EditorRange, detail: IMouseTargetMarginData): IMouseTargetMargin { + return { type, element, mouseColumn, position, range, detail }; + } + public static createViewZone(type: MouseTargetType.GUTTER_VIEW_ZONE | MouseTargetType.CONTENT_VIEW_ZONE, element: Element | null, mouseColumn: number, position: Position, detail: IMouseTargetViewZoneData): IMouseTargetViewZone { + return { type, element, mouseColumn, position, range: this._deduceRage(position), detail }; + } + public static createContentText(element: Element | null, mouseColumn: number, position: Position, range: EditorRange | null, detail: IMouseTargetContentTextData): IMouseTargetContentText { + return { type: MouseTargetType.CONTENT_TEXT, element, mouseColumn, position, range: this._deduceRage(position, range), detail }; + } + public static createContentEmpty(element: Element | null, mouseColumn: number, position: Position, detail: IMouseTargetContentEmptyData): IMouseTargetContentEmpty { + return { type: MouseTargetType.CONTENT_EMPTY, element, mouseColumn, position, range: this._deduceRage(position), detail }; + } + public static createContentWidget(element: Element | null, mouseColumn: number, detail: string): IMouseTargetContentWidget { + return { type: MouseTargetType.CONTENT_WIDGET, element, mouseColumn, position: null, range: null, detail }; + } + public static createScrollbar(element: Element | null, mouseColumn: number, position: Position): IMouseTargetScrollbar { + return { type: MouseTargetType.SCROLLBAR, element, mouseColumn, position, range: this._deduceRage(position) }; + } + public static createOverlayWidget(element: Element | null, mouseColumn: number, detail: string): IMouseTargetOverlayWidget { + return { type: MouseTargetType.OVERLAY_WIDGET, element, mouseColumn, position: null, range: null, detail }; + } + public static createOutsideEditor(mouseColumn: number, position: Position): IMouseTargetOutsideEditor { + return { type: MouseTargetType.OUTSIDE_EDITOR, element: null, mouseColumn, position, range: this._deduceRage(position) }; } private static _typeToString(type: MouseTargetType): string { @@ -148,11 +144,7 @@ export class MouseTarget implements IMouseTarget { } public static toString(target: IMouseTarget): string { - return this._typeToString(target.type) + ': ' + target.position + ' - ' + target.range + ' - ' + target.detail; - } - - public toString(): string { - return MouseTarget.toString(this); + return this._typeToString(target.type) + ': ' + target.position + ' - ' + target.range + ' - ' + JSON.stringify((target).detail); } } @@ -248,11 +240,11 @@ export class HitTestContext { this._viewHelper = viewHelper; } - public getZoneAtCoord(mouseVerticalOffset: number): IViewZoneData | null { + public getZoneAtCoord(mouseVerticalOffset: number): IMouseTargetViewZoneData | null { return HitTestContext.getZoneAtCoord(this._context, mouseVerticalOffset); } - public static getZoneAtCoord(context: ViewContext, mouseVerticalOffset: number): IViewZoneData | null { + public static getZoneAtCoord(context: ViewContext, mouseVerticalOffset: number): IMouseTargetViewZoneData | null { // The target is either a view zone or the empty space after the last view-line const viewZoneWhitespace = context.viewLayout.getWhitespaceAtVerticalOffset(mouseVerticalOffset); @@ -374,6 +366,7 @@ abstract class BareHitTestRequest { public readonly editorPos: EditorPagePosition; public readonly pos: PageCoordinates; + public readonly relativePos: CoordinatesRelativeToEditor; public readonly mouseVerticalOffset: number; public readonly isInMarginArea: boolean; public readonly isInContentArea: boolean; @@ -381,13 +374,14 @@ abstract class BareHitTestRequest { protected readonly mouseColumn: number; - constructor(ctx: HitTestContext, editorPos: EditorPagePosition, pos: PageCoordinates) { + constructor(ctx: HitTestContext, editorPos: EditorPagePosition, pos: PageCoordinates, relativePos: CoordinatesRelativeToEditor) { this.editorPos = editorPos; this.pos = pos; + this.relativePos = relativePos; - this.mouseVerticalOffset = Math.max(0, ctx.getCurrentScrollTop() + pos.y - editorPos.y); - this.mouseContentHorizontalOffset = ctx.getCurrentScrollLeft() + pos.x - editorPos.x - ctx.layoutInfo.contentLeft; - this.isInMarginArea = (pos.x - editorPos.x < ctx.layoutInfo.contentLeft && pos.x - editorPos.x >= ctx.layoutInfo.glyphMarginLeft); + this.mouseVerticalOffset = Math.max(0, ctx.getCurrentScrollTop() + this.relativePos.y); + this.mouseContentHorizontalOffset = ctx.getCurrentScrollLeft() + this.relativePos.x - ctx.layoutInfo.contentLeft; + this.isInMarginArea = (this.relativePos.x < ctx.layoutInfo.contentLeft && this.relativePos.x >= ctx.layoutInfo.glyphMarginLeft); this.isInContentArea = !this.isInMarginArea; this.mouseColumn = Math.max(0, MouseTargetFactory._getMouseColumn(this.mouseContentHorizontalOffset, ctx.typicalHalfwidthCharacterWidth)); } @@ -398,8 +392,8 @@ class HitTestRequest extends BareHitTestRequest { public readonly target: Element | null; public readonly targetPath: Uint8Array; - constructor(ctx: HitTestContext, editorPos: EditorPagePosition, pos: PageCoordinates, target: Element | null) { - super(ctx, editorPos, pos); + constructor(ctx: HitTestContext, editorPos: EditorPagePosition, pos: PageCoordinates, relativePos: CoordinatesRelativeToEditor, target: Element | null) { + super(ctx, editorPos, pos, relativePos); this._ctx = ctx; if (target) { @@ -412,31 +406,47 @@ class HitTestRequest extends BareHitTestRequest { } public override toString(): string { - return `pos(${this.pos.x},${this.pos.y}), editorPos(${this.editorPos.x},${this.editorPos.y}), mouseVerticalOffset: ${this.mouseVerticalOffset}, mouseContentHorizontalOffset: ${this.mouseContentHorizontalOffset}\n\ttarget: ${this.target ? (this.target).outerHTML : null}`; - } - - public fulfill(type: MouseTargetType.UNKNOWN, position?: Position | null, range?: EditorRange | null): MouseTarget; - public fulfill(type: MouseTargetType.TEXTAREA, position: Position | null): MouseTarget; - public fulfill(type: MouseTargetType.GUTTER_GLYPH_MARGIN | MouseTargetType.GUTTER_LINE_NUMBERS | MouseTargetType.GUTTER_LINE_DECORATIONS, position: Position, range: EditorRange, detail: IMarginData): MouseTarget; - public fulfill(type: MouseTargetType.GUTTER_VIEW_ZONE | MouseTargetType.CONTENT_VIEW_ZONE, position: Position, range: null, detail: IViewZoneData): MouseTarget; - public fulfill(type: MouseTargetType.CONTENT_TEXT, position: Position | null, range: EditorRange | null, detail: ITextContentData): MouseTarget; - public fulfill(type: MouseTargetType.CONTENT_EMPTY, position: Position | null, range: EditorRange | null, detail: IEmptyContentData): MouseTarget; - public fulfill(type: MouseTargetType.CONTENT_WIDGET, position: null, range: null, detail: string): MouseTarget; - public fulfill(type: MouseTargetType.SCROLLBAR, position: Position): MouseTarget; - public fulfill(type: MouseTargetType.OVERLAY_WIDGET, position: null, range: null, detail: string): MouseTarget; - // public fulfill(type: MouseTargetType.OVERVIEW_RULER, position?: Position | null, range?: EditorRange | null, detail?: any): MouseTarget; - // public fulfill(type: MouseTargetType.OUTSIDE_EDITOR, position?: Position | null, range?: EditorRange | null, detail?: any): MouseTarget; - public fulfill(type: MouseTargetType, position: Position | null = null, range: EditorRange | null = null, detail: any = null): MouseTarget { - let mouseColumn = this.mouseColumn; + return `pos(${this.pos.x},${this.pos.y}), editorPos(${this.editorPos.x},${this.editorPos.y}), relativePos(${this.relativePos.x},${this.relativePos.y}), mouseVerticalOffset: ${this.mouseVerticalOffset}, mouseContentHorizontalOffset: ${this.mouseContentHorizontalOffset}\n\ttarget: ${this.target ? (this.target).outerHTML : null}`; + } + + private _getMouseColumn(position: Position | null = null): number { if (position && position.column < this._ctx.model.getLineMaxColumn(position.lineNumber)) { // Most likely, the line contains foreign decorations... - mouseColumn = CursorColumns.visibleColumnFromColumn(this._ctx.model.getLineContent(position.lineNumber), position.column, this._ctx.model.getTextModelOptions().tabSize) + 1; + return CursorColumns.visibleColumnFromColumn(this._ctx.model.getLineContent(position.lineNumber), position.column, this._ctx.model.getTextModelOptions().tabSize) + 1; } - return new MouseTarget(this.target, type, mouseColumn, position, range, detail); + return this.mouseColumn; + } + + public fulfillUnknown(position: Position | null = null): IMouseTargetUnknown { + return MouseTarget.createUnknown(this.target, this._getMouseColumn(position), position); + } + public fulfillTextarea(): IMouseTargetTextarea { + return MouseTarget.createTextarea(this.target, this._getMouseColumn()); + } + public fulfillMargin(type: MouseTargetType.GUTTER_GLYPH_MARGIN | MouseTargetType.GUTTER_LINE_NUMBERS | MouseTargetType.GUTTER_LINE_DECORATIONS, position: Position, range: EditorRange, detail: IMouseTargetMarginData): IMouseTargetMargin { + return MouseTarget.createMargin(type, this.target, this._getMouseColumn(position), position, range, detail); + } + public fulfillViewZone(type: MouseTargetType.GUTTER_VIEW_ZONE | MouseTargetType.CONTENT_VIEW_ZONE, position: Position, detail: IMouseTargetViewZoneData): IMouseTargetViewZone { + return MouseTarget.createViewZone(type, this.target, this._getMouseColumn(position), position, detail); + } + public fulfillContentText(position: Position, range: EditorRange | null, detail: IMouseTargetContentTextData): IMouseTargetContentText { + return MouseTarget.createContentText(this.target, this._getMouseColumn(position), position, range, detail); + } + public fulfillContentEmpty(position: Position, detail: IMouseTargetContentEmptyData): IMouseTargetContentEmpty { + return MouseTarget.createContentEmpty(this.target, this._getMouseColumn(position), position, detail); + } + public fulfillContentWidget(detail: string): IMouseTargetContentWidget { + return MouseTarget.createContentWidget(this.target, this._getMouseColumn(), detail); + } + public fulfillScrollbar(position: Position): IMouseTargetScrollbar { + return MouseTarget.createScrollbar(this.target, this._getMouseColumn(position), position); + } + public fulfillOverlayWidget(detail: string): IMouseTargetOverlayWidget { + return MouseTarget.createOverlayWidget(this.target, this._getMouseColumn(), detail); } public withTarget(target: Element | null): HitTestRequest { - return new HitTestRequest(this._ctx, this.editorPos, this.pos, target); + return new HitTestRequest(this._ctx, this.editorPos, this.pos, this.relativePos, target); } } @@ -444,9 +454,9 @@ interface ResolvedHitTestRequest extends HitTestRequest { readonly target: Element; } -const EMPTY_CONTENT_AFTER_LINES: IEmptyContentData = { isAfterLines: true }; +const EMPTY_CONTENT_AFTER_LINES: IMouseTargetContentEmptyData = { isAfterLines: true }; -function createEmptyContentDataInLines(horizontalDistanceToText: number): IEmptyContentData { +function createEmptyContentDataInLines(horizontalDistanceToText: number): IMouseTargetContentEmptyData { return { isAfterLines: false, horizontalDistanceToText: horizontalDistanceToText @@ -480,20 +490,20 @@ export class MouseTargetFactory { return false; } - public createMouseTarget(lastRenderData: PointerHandlerLastRenderData, editorPos: EditorPagePosition, pos: PageCoordinates, target: HTMLElement | null): IMouseTarget { + public createMouseTarget(lastRenderData: PointerHandlerLastRenderData, editorPos: EditorPagePosition, pos: PageCoordinates, relativePos: CoordinatesRelativeToEditor, target: HTMLElement | null): IMouseTarget { const ctx = new HitTestContext(this._context, this._viewHelper, lastRenderData); - const request = new HitTestRequest(ctx, editorPos, pos, target); + const request = new HitTestRequest(ctx, editorPos, pos, relativePos, target); try { const r = MouseTargetFactory._createMouseTarget(ctx, request, false); - // console.log(r.toString()); + // console.log(MouseTarget.toString(r)); return r; } catch (err) { // console.log(err); - return request.fulfill(MouseTargetType.UNKNOWN); + return request.fulfillUnknown(); } } - private static _createMouseTarget(ctx: HitTestContext, request: HitTestRequest, domHitTestExecuted: boolean): MouseTarget { + private static _createMouseTarget(ctx: HitTestContext, request: HitTestRequest, domHitTestExecuted: boolean): IMouseTarget { // console.log(`${domHitTestExecuted ? '=>' : ''}CAME IN REQUEST: ${request}`); @@ -501,7 +511,7 @@ export class MouseTargetFactory { if (request.target === null) { if (domHitTestExecuted) { // Still no target... and we have already executed hit test... - return request.fulfill(MouseTargetType.UNKNOWN); + return request.fulfillUnknown(); } const hitTestResult = MouseTargetFactory._doHitTest(ctx, request); @@ -516,7 +526,7 @@ export class MouseTargetFactory { // we know for a fact that request.target is not null const resolvedRequest = request; - let result: MouseTarget | null = null; + let result: IMouseTarget | null = null; result = result || MouseTargetFactory._hitTestContentWidget(ctx, resolvedRequest); result = result || MouseTargetFactory._hitTestOverlayWidget(ctx, resolvedRequest); @@ -529,36 +539,36 @@ export class MouseTargetFactory { result = result || MouseTargetFactory._hitTestViewLines(ctx, resolvedRequest, domHitTestExecuted); result = result || MouseTargetFactory._hitTestScrollbar(ctx, resolvedRequest); - return (result || request.fulfill(MouseTargetType.UNKNOWN)); + return (result || request.fulfillUnknown()); } - private static _hitTestContentWidget(ctx: HitTestContext, request: ResolvedHitTestRequest): MouseTarget | null { + private static _hitTestContentWidget(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null { // Is it a content widget? if (ElementPath.isChildOfContentWidgets(request.targetPath) || ElementPath.isChildOfOverflowingContentWidgets(request.targetPath)) { const widgetId = ctx.findAttribute(request.target, 'widgetId'); if (widgetId) { - return request.fulfill(MouseTargetType.CONTENT_WIDGET, null, null, widgetId); + return request.fulfillContentWidget(widgetId); } else { - return request.fulfill(MouseTargetType.UNKNOWN); + return request.fulfillUnknown(); } } return null; } - private static _hitTestOverlayWidget(ctx: HitTestContext, request: ResolvedHitTestRequest): MouseTarget | null { + private static _hitTestOverlayWidget(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null { // Is it an overlay widget? if (ElementPath.isChildOfOverlayWidgets(request.targetPath)) { const widgetId = ctx.findAttribute(request.target, 'widgetId'); if (widgetId) { - return request.fulfill(MouseTargetType.OVERLAY_WIDGET, null, null, widgetId); + return request.fulfillOverlayWidget(widgetId); } else { - return request.fulfill(MouseTargetType.UNKNOWN); + return request.fulfillUnknown(); } } return null; } - private static _hitTestViewCursor(ctx: HitTestContext, request: ResolvedHitTestRequest): MouseTarget | null { + private static _hitTestViewCursor(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null { if (request.target) { // Check if we've hit a painted cursor @@ -567,7 +577,7 @@ export class MouseTargetFactory { for (const d of lastViewCursorsRenderData) { if (request.target === d.domNode) { - return request.fulfill(MouseTargetType.CONTENT_TEXT, d.position, null, { mightBeForeignElement: false }); + return request.fulfillContentText(d.position, null, { mightBeForeignElement: false, injectedText: null }); } } } @@ -599,7 +609,7 @@ export class MouseTargetFactory { cursorVerticalOffset <= mouseVerticalOffset && mouseVerticalOffset <= cursorVerticalOffset + d.height ) { - return request.fulfill(MouseTargetType.CONTENT_TEXT, d.position, null, { mightBeForeignElement: false }); + return request.fulfillContentText(d.position, null, { mightBeForeignElement: false, injectedText: null }); } } } @@ -607,33 +617,33 @@ export class MouseTargetFactory { return null; } - private static _hitTestViewZone(ctx: HitTestContext, request: ResolvedHitTestRequest): MouseTarget | null { + private static _hitTestViewZone(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null { const viewZoneData = ctx.getZoneAtCoord(request.mouseVerticalOffset); if (viewZoneData) { const mouseTargetType = (request.isInContentArea ? MouseTargetType.CONTENT_VIEW_ZONE : MouseTargetType.GUTTER_VIEW_ZONE); - return request.fulfill(mouseTargetType, viewZoneData.position, null, viewZoneData); + return request.fulfillViewZone(mouseTargetType, viewZoneData.position, viewZoneData); } return null; } - private static _hitTestTextArea(ctx: HitTestContext, request: ResolvedHitTestRequest): MouseTarget | null { + private static _hitTestTextArea(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null { // Is it the textarea? if (ElementPath.isTextArea(request.targetPath)) { if (ctx.lastRenderData.lastTextareaPosition) { - return request.fulfill(MouseTargetType.CONTENT_TEXT, ctx.lastRenderData.lastTextareaPosition, null, { mightBeForeignElement: false }); + return request.fulfillContentText(ctx.lastRenderData.lastTextareaPosition, null, { mightBeForeignElement: false, injectedText: null }); } - return request.fulfill(MouseTargetType.TEXTAREA, ctx.lastRenderData.lastTextareaPosition); + return request.fulfillTextarea(); } return null; } - private static _hitTestMargin(ctx: HitTestContext, request: ResolvedHitTestRequest): MouseTarget | null { + private static _hitTestMargin(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null { if (request.isInMarginArea) { const res = ctx.getFullLineRangeAtCoord(request.mouseVerticalOffset); const pos = res.range.getStartPosition(); - let offset = Math.abs(request.pos.x - request.editorPos.x); - const detail: IMarginData = { + let offset = Math.abs(request.relativePos.x); + const detail: IMouseTargetMarginData = { isAfterLines: res.isAfterLines, glyphMarginLeft: ctx.layoutInfo.glyphMarginLeft, glyphMarginWidth: ctx.layoutInfo.glyphMarginWidth, @@ -645,29 +655,29 @@ export class MouseTargetFactory { if (offset <= ctx.layoutInfo.glyphMarginWidth) { // On the glyph margin - return request.fulfill(MouseTargetType.GUTTER_GLYPH_MARGIN, pos, res.range, detail); + return request.fulfillMargin(MouseTargetType.GUTTER_GLYPH_MARGIN, pos, res.range, detail); } offset -= ctx.layoutInfo.glyphMarginWidth; if (offset <= ctx.layoutInfo.lineNumbersWidth) { // On the line numbers - return request.fulfill(MouseTargetType.GUTTER_LINE_NUMBERS, pos, res.range, detail); + return request.fulfillMargin(MouseTargetType.GUTTER_LINE_NUMBERS, pos, res.range, detail); } offset -= ctx.layoutInfo.lineNumbersWidth; // On the line decorations - return request.fulfill(MouseTargetType.GUTTER_LINE_DECORATIONS, pos, res.range, detail); + return request.fulfillMargin(MouseTargetType.GUTTER_LINE_DECORATIONS, pos, res.range, detail); } return null; } - private static _hitTestViewLines(ctx: HitTestContext, request: ResolvedHitTestRequest, domHitTestExecuted: boolean): MouseTarget | null { + private static _hitTestViewLines(ctx: HitTestContext, request: ResolvedHitTestRequest, domHitTestExecuted: boolean): IMouseTarget | null { if (!ElementPath.isChildOfViewLines(request.targetPath)) { return null; } if (ctx.isInTopPadding(request.mouseVerticalOffset)) { - return request.fulfill(MouseTargetType.CONTENT_EMPTY, new Position(1, 1), null, EMPTY_CONTENT_AFTER_LINES); + return request.fulfillContentEmpty(new Position(1, 1), EMPTY_CONTENT_AFTER_LINES); } // Check if it is below any lines and any view zones @@ -675,7 +685,7 @@ export class MouseTargetFactory { // This most likely indicates it happened after the last view-line const lineCount = ctx.model.getLineCount(); const maxLineColumn = ctx.model.getLineMaxColumn(lineCount); - return request.fulfill(MouseTargetType.CONTENT_EMPTY, new Position(lineCount, maxLineColumn), null, EMPTY_CONTENT_AFTER_LINES); + return request.fulfillContentEmpty(new Position(lineCount, maxLineColumn), EMPTY_CONTENT_AFTER_LINES); } if (domHitTestExecuted) { @@ -686,19 +696,19 @@ export class MouseTargetFactory { if (ctx.model.getLineLength(lineNumber) === 0) { const lineWidth = ctx.getLineWidth(lineNumber); const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth); - return request.fulfill(MouseTargetType.CONTENT_EMPTY, new Position(lineNumber, 1), null, detail); + return request.fulfillContentEmpty(new Position(lineNumber, 1), detail); } const lineWidth = ctx.getLineWidth(lineNumber); if (request.mouseContentHorizontalOffset >= lineWidth) { const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth); const pos = new Position(lineNumber, ctx.model.getLineMaxColumn(lineNumber)); - return request.fulfill(MouseTargetType.CONTENT_EMPTY, pos, null, detail); + return request.fulfillContentEmpty(pos, detail); } } // We have already executed hit test... - return request.fulfill(MouseTargetType.UNKNOWN); + return request.fulfillUnknown(); } const hitTestResult = MouseTargetFactory._doHitTest(ctx, request); @@ -710,45 +720,45 @@ export class MouseTargetFactory { return this._createMouseTarget(ctx, request.withTarget(hitTestResult.hitTarget), true); } - private static _hitTestMinimap(ctx: HitTestContext, request: ResolvedHitTestRequest): MouseTarget | null { + private static _hitTestMinimap(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null { if (ElementPath.isChildOfMinimap(request.targetPath)) { const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset); const maxColumn = ctx.model.getLineMaxColumn(possibleLineNumber); - return request.fulfill(MouseTargetType.SCROLLBAR, new Position(possibleLineNumber, maxColumn)); + return request.fulfillScrollbar(new Position(possibleLineNumber, maxColumn)); } return null; } - private static _hitTestScrollbarSlider(ctx: HitTestContext, request: ResolvedHitTestRequest): MouseTarget | null { + private static _hitTestScrollbarSlider(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null { if (ElementPath.isChildOfScrollableElement(request.targetPath)) { if (request.target && request.target.nodeType === 1) { const className = request.target.className; if (className && /\b(slider|scrollbar)\b/.test(className)) { const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset); const maxColumn = ctx.model.getLineMaxColumn(possibleLineNumber); - return request.fulfill(MouseTargetType.SCROLLBAR, new Position(possibleLineNumber, maxColumn)); + return request.fulfillScrollbar(new Position(possibleLineNumber, maxColumn)); } } } return null; } - private static _hitTestScrollbar(ctx: HitTestContext, request: ResolvedHitTestRequest): MouseTarget | null { + private static _hitTestScrollbar(ctx: HitTestContext, request: ResolvedHitTestRequest): IMouseTarget | null { // Is it the overview ruler? // Is it a child of the scrollable element? if (ElementPath.isChildOfScrollableElement(request.targetPath)) { const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset); const maxColumn = ctx.model.getLineMaxColumn(possibleLineNumber); - return request.fulfill(MouseTargetType.SCROLLBAR, new Position(possibleLineNumber, maxColumn)); + return request.fulfillScrollbar(new Position(possibleLineNumber, maxColumn)); } return null; } - public getMouseColumn(editorPos: EditorPagePosition, pos: PageCoordinates): number { + public getMouseColumn(relativePos: CoordinatesRelativeToEditor): number { const options = this._context.configuration.options; const layoutInfo = options.get(EditorOption.layoutInfo); - const mouseContentHorizontalOffset = this._context.viewLayout.getCurrentScrollLeft() + pos.x - editorPos.x - layoutInfo.contentLeft; + const mouseContentHorizontalOffset = this._context.viewLayout.getCurrentScrollLeft() + relativePos.x - layoutInfo.contentLeft; return MouseTargetFactory._getMouseColumn(mouseContentHorizontalOffset, options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth); } @@ -760,7 +770,7 @@ export class MouseTargetFactory { return (chars + 1); } - private static createMouseTargetFromHitTestPosition(ctx: HitTestContext, request: HitTestRequest, spanNode: HTMLElement, pos: Position, injectedText: InjectedText | null): MouseTarget { + private static createMouseTargetFromHitTestPosition(ctx: HitTestContext, request: HitTestRequest, spanNode: HTMLElement, pos: Position, injectedText: InjectedText | null): IMouseTarget { const lineNumber = pos.lineNumber; const column = pos.column; @@ -768,19 +778,19 @@ export class MouseTargetFactory { if (request.mouseContentHorizontalOffset > lineWidth) { const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth); - return request.fulfill(MouseTargetType.CONTENT_EMPTY, pos, null, detail); + return request.fulfillContentEmpty(pos, detail); } const visibleRange = ctx.visibleRangeForPosition(lineNumber, column); if (!visibleRange) { - return request.fulfill(MouseTargetType.UNKNOWN, pos); + return request.fulfillUnknown(pos); } const columnHorizontalOffset = visibleRange.left; if (request.mouseContentHorizontalOffset === columnHorizontalOffset) { - return request.fulfill(MouseTargetType.CONTENT_TEXT, pos, null, { mightBeForeignElement: !!injectedText }); + return request.fulfillContentText(pos, null, { mightBeForeignElement: !!injectedText, injectedText }); } // Let's define a, b, c and check if the offset is in between them... @@ -813,10 +823,10 @@ export class MouseTargetFactory { const curr = points[i]; if (prev.offset <= request.mouseContentHorizontalOffset && request.mouseContentHorizontalOffset <= curr.offset) { const rng = new EditorRange(lineNumber, prev.column, lineNumber, curr.column); - return request.fulfill(MouseTargetType.CONTENT_TEXT, pos, rng, { mightBeForeignElement: !mouseIsOverSpanNode || !!injectedText }); + return request.fulfillContentText(pos, rng, { mightBeForeignElement: !mouseIsOverSpanNode || !!injectedText, injectedText }); } } - return request.fulfill(MouseTargetType.CONTENT_TEXT, pos, null, { mightBeForeignElement: !mouseIsOverSpanNode || !!injectedText }); + return request.fulfillContentText(pos, null, { mightBeForeignElement: !mouseIsOverSpanNode || !!injectedText, injectedText }); } /** @@ -834,8 +844,8 @@ export class MouseTargetFactory { if (adjustedPageY <= request.editorPos.y) { adjustedPageY = request.editorPos.y + 1; } - if (adjustedPageY >= request.editorPos.y + ctx.layoutInfo.height) { - adjustedPageY = request.editorPos.y + ctx.layoutInfo.height - 1; + if (adjustedPageY >= request.editorPos.y + request.editorPos.height) { + adjustedPageY = request.editorPos.y + request.editorPos.height - 1; } const adjustedPage = new PageCoordinates(request.pos.x, adjustedPageY); diff --git a/src/vs/editor/browser/controller/pointerHandler.ts b/src/vs/editor/browser/controller/pointerHandler.ts index 5c4df88ff3b17..0fb7a0d6788d4 100644 --- a/src/vs/editor/browser/controller/pointerHandler.ts +++ b/src/vs/editor/browser/controller/pointerHandler.ts @@ -11,7 +11,7 @@ import { IPointerHandlerHelper, MouseHandler, createMouseMoveEventMerger } from import { IMouseTarget } from 'vs/editor/browser/editorBrowser'; import { EditorMouseEvent, EditorPointerEventFactory } from 'vs/editor/browser/editorDom'; import { ViewController } from 'vs/editor/browser/view/viewController'; -import { ViewContext } from 'vs/editor/common/view/viewContext'; +import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; import { TextAreaSyntethicEvents } from 'vs/editor/browser/controller/textAreaInput'; diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index 323517cac2d05..beee2daef1ba4 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -10,8 +10,8 @@ import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import * as platform from 'vs/base/common/platform'; import * as strings from 'vs/base/common/strings'; -import { Configuration } from 'vs/editor/browser/config/configuration'; -import { CopyOptions, ICompositionData, IPasteData, ITextAreaInputHost, TextAreaInput, ClipboardDataToCopy } from 'vs/editor/browser/controller/textAreaInput'; +import { applyFontInfo } from 'vs/editor/browser/config/domFontInfo'; +import { CopyOptions, ICompositionData, IPasteData, ITextAreaInputHost, TextAreaInput, ClipboardDataToCopy, TextAreaWrapper } from 'vs/editor/browser/controller/textAreaInput'; import { ISimpleModel, ITypeData, PagedScreenReaderStrategy, TextAreaState, _debugComposition } from 'vs/editor/browser/controller/textAreaState'; import { ViewController } from 'vs/editor/browser/view/viewController'; import { PartFingerprint, PartFingerprints, ViewPart } from 'vs/editor/browser/view/viewPart'; @@ -19,38 +19,58 @@ import { LineNumbersOverlay } from 'vs/editor/browser/viewParts/lineNumbers/line import { Margin } from 'vs/editor/browser/viewParts/margin/margin'; import { RenderLineNumbersType, EditorOption, IComputedEditorOptions, EditorOptions } from 'vs/editor/common/config/editorOptions'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; -import { WordCharacterClass, getMapForWordSeparators } from 'vs/editor/common/controller/wordCharacterClassifier'; +import { WordCharacterClass, getMapForWordSeparators } from 'vs/editor/common/core/wordCharacterClassifier'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { EndOfLinePreference } from 'vs/editor/common/model'; -import { RenderingContext, RestrictedRenderingContext, HorizontalPosition } from 'vs/editor/common/view/renderingContext'; -import { ViewContext } from 'vs/editor/common/view/viewContext'; -import * as viewEvents from 'vs/editor/common/view/viewEvents'; +import { RenderingContext, RestrictedRenderingContext, HorizontalPosition } from 'vs/editor/browser/view/renderingContext'; +import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; +import * as viewEvents from 'vs/editor/common/viewModel/viewEvents'; import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; import { IEditorAriaOptions } from 'vs/editor/browser/editorBrowser'; import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; +import { ColorId, ITokenPresentation, TokenizationRegistry } from 'vs/editor/common/languages'; +import { Color } from 'vs/base/common/color'; -export interface ITextAreaHandlerHelper { - visibleRangeForPositionRelativeToEditor(lineNumber: number, column: number): HorizontalPosition | null; +export interface IVisibleRangeProvider { + visibleRangeForPosition(position: Position): HorizontalPosition | null; } class VisibleTextAreaData { _visibleTextAreaBrand: void = undefined; - public readonly top: number; - public readonly left: number; - public readonly width: number; + public startPosition: Position | null = null; + public endPosition: Position | null = null; - constructor(top: number, left: number, width: number) { - this.top = top; - this.left = left; - this.width = width; + public visibleTextareaStart: HorizontalPosition | null = null; + public visibleTextareaEnd: HorizontalPosition | null = null; + + constructor( + private readonly _context: ViewContext, + public readonly modelLineNumber: number, + public readonly distanceToModelLineStart: number, + public readonly widthOfHiddenLineTextBefore: number, + public readonly distanceToModelLineEnd: number, + ) { } - public setWidth(width: number): VisibleTextAreaData { - return new VisibleTextAreaData(this.top, this.left, width); + prepareRender(visibleRangeProvider: IVisibleRangeProvider): void { + const startModelPosition = new Position(this.modelLineNumber, this.distanceToModelLineStart + 1); + const endModelPosition = new Position(this.modelLineNumber, this._context.model.getModelLineMaxColumn(this.modelLineNumber) - this.distanceToModelLineEnd); + + this.startPosition = this._context.model.coordinatesConverter.convertModelPositionToViewPosition(startModelPosition); + this.endPosition = this._context.model.coordinatesConverter.convertModelPositionToViewPosition(endModelPosition); + + if (this.startPosition.lineNumber === this.endPosition.lineNumber) { + this.visibleTextareaStart = visibleRangeProvider.visibleRangeForPosition(this.startPosition); + this.visibleTextareaEnd = visibleRangeProvider.visibleRangeForPosition(this.endPosition); + } else { + // TODO: what if the view positions are not on the same line? + this.visibleTextareaStart = null; + this.visibleTextareaEnd = null; + } } } @@ -59,7 +79,7 @@ const canUseZeroSizeTextarea = (browser.isFirefox); export class TextAreaHandler extends ViewPart { private readonly _viewController: ViewController; - private readonly _viewHelper: ITextAreaHandlerHelper; + private readonly _visibleRangeProvider: IVisibleRangeProvider; private _scrollLeft: number; private _scrollTop: number; @@ -90,11 +110,11 @@ export class TextAreaHandler extends ViewPart { public readonly textAreaCover: FastDomNode; private readonly _textAreaInput: TextAreaInput; - constructor(context: ViewContext, viewController: ViewController, viewHelper: ITextAreaHandlerHelper) { + constructor(context: ViewContext, viewController: ViewController, visibleRangeProvider: IVisibleRangeProvider) { super(context); this._viewController = viewController; - this._viewHelper = viewHelper; + this._visibleRangeProvider = visibleRangeProvider; this._scrollLeft = 0; this._scrollTop = 0; @@ -152,7 +172,7 @@ export class TextAreaHandler extends ViewPart { }; const textAreaInputHost: ITextAreaInputHost = { - getDataToCopy: (generateHTML: boolean): ClipboardDataToCopy => { + getDataToCopy: (): ClipboardDataToCopy => { const rawTextToCopy = this._context.model.getPlainTextToCopy(this._modelSelections, this._emptySelectionClipboard, platform.isWindows); const newLineCharacter = this._context.model.getEOL(); @@ -162,13 +182,11 @@ export class TextAreaHandler extends ViewPart { let html: string | null | undefined = undefined; let mode: string | null = null; - if (generateHTML) { - if (CopyOptions.forceCopyWithSyntaxHighlighting || (this._copyWithSyntaxHighlighting && text.length < 65536)) { - const richText = this._context.model.getRichTextToCopy(this._modelSelections, this._emptySelectionClipboard); - if (richText) { - html = richText.html; - mode = richText.mode; - } + if (CopyOptions.forceCopyWithSyntaxHighlighting || (this._copyWithSyntaxHighlighting && text.length < 65536)) { + const richText = this._context.model.getRichTextToCopy(this._modelSelections, this._emptySelectionClipboard); + if (richText) { + html = richText.html; + mode = richText.mode; } } return { @@ -226,7 +244,8 @@ export class TextAreaHandler extends ViewPart { } }; - this._textAreaInput = this._register(new TextAreaInput(textAreaInputHost, this.textArea)); + const textAreaWrapper = this._register(new TextAreaWrapper(this.textArea.domNode)); + this._textAreaInput = this._register(new TextAreaInput(textAreaInputHost, textAreaWrapper, platform.OS, browser)); this._register(this._textAreaInput.onKeyDown((e: IKeyboardEvent) => { this._viewController.emitKeyDown(e); @@ -272,28 +291,80 @@ export class TextAreaHandler extends ViewPart { })); this._register(this._textAreaInput.onCompositionStart((e) => { - const lineNumber = this._selections[0].startLineNumber; - const column = this._selections[0].startColumn + e.revealDeltaColumns; + // The textarea might contain some content when composition starts. + // + // When we make the textarea visible, it always has a height of 1 line, + // so we don't need to worry too much about content on lines above or below + // the selection. + // + // However, the text on the current line needs to be made visible because + // some IME methods allow to glyphs on the current line (by pressing arrow keys). + // + // (1) The textarea might contain only some parts of the current line, + // like the word before the selection. Also, the content inside the textarea + // can grow or shrink as composition occurs. We therefore anchor the textarea + // in terms of distance to a certain line start and line end. + // + // (2) Also, we should not make \t characters visible, because their rendering + // inside the