Merge vscode source through 1.62 release (#19981)

* Build breaks 1

* Build breaks

* Build breaks

* Build breaks

* More build breaks

* Build breaks (#2512)

* Runtime breaks

* Build breaks

* Fix dialog location break

* Update typescript

* Fix ASAR break issue

* Unit test breaks

* Update distro

* Fix breaks in ADO builds (#2513)

* Bump to node 16

* Fix hygiene errors

* Bump distro

* Remove reference to node type

* Delete vscode specific extension

* Bump to node 16 in CI yaml

* Skip integration tests in CI builds (while fixing)

* yarn.lock update

* Bump moment dependency in remote yarn

* Fix drop-down chevron style

* Bump to node 16

* Remove playwrite from ci.yaml

* Skip building build scripts in hygine check
This commit is contained in:
Karl Burtram
2022-07-11 14:09:32 -07:00
committed by GitHub
parent fa0fcef303
commit 26455e9113
1876 changed files with 72050 additions and 37997 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { AnchorAlignment, AnchorAxisAlignment } from 'vs/base/browser/ui/contextview/contextview';
import { IAction, IActionRunner } from 'vs/base/common/actions';
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { ResolvedKeybinding } from 'vs/base/common/keybindings';
export interface IContextMenuEvent {
readonly shiftKey?: boolean;
+34 -46
View File
@@ -10,10 +10,10 @@ import { IMouseEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { TimeoutTimer } from 'vs/base/common/async';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { insane, InsaneOptions } from 'vs/base/common/insane/insane';
import * as dompurify from 'vs/base/browser/dompurify/dompurify';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { FileAccess, RemoteAuthorities } from 'vs/base/common/network';
import { FileAccess, RemoteAuthorities, Schemas } from 'vs/base/common/network';
import * as platform from 'vs/base/common/platform';
import { withNullAsUndefined } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
@@ -1153,11 +1153,11 @@ export function finalHandler<T extends DOMEvent>(fn: (event: T) => any): (event:
};
}
export function domContentLoaded(): Promise<any> {
return new Promise<any>(resolve => {
export function domContentLoaded(): Promise<unknown> {
return new Promise<unknown>(resolve => {
const readyState = document.readyState;
if (readyState === 'complete' || (document && document.body !== null)) {
platform.setImmediate(resolve);
resolve(undefined);
} else {
window.addEventListener('DOMContentLoaded', resolve, false);
}
@@ -1361,53 +1361,41 @@ export function detectFullscreen(): IDetectedFullscreen | null {
// -- sanitize and trusted html
function _extInsaneOptions(opts: InsaneOptions, allowedAttributesForAll: string[]): InsaneOptions {
let allowedAttributes: Record<string, string[]> = opts.allowedAttributes ?? {};
if (opts.allowedTags) {
for (let tag of opts.allowedTags) {
let array = allowedAttributes[tag];
if (!array) {
array = allowedAttributesForAll;
} else {
array = array.concat(allowedAttributesForAll);
}
allowedAttributes[tag] = array;
}
}
return { ...opts, allowedAttributes };
}
const _ttpSafeInnerHtml = window.trustedTypes?.createPolicy('safeInnerHtml', {
createHTML(value, options: InsaneOptions) {
return insane(value, options);
}
});
/**
* Sanitizes the given `value` and reset the given `node` with it.
*/
export function safeInnerHtml(node: HTMLElement, value: string): void {
const options: dompurify.Config = {
ALLOWED_TAGS: ['a', 'button', 'blockquote', 'code', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'input', 'label', 'li', 'p', 'pre', 'select', 'small', 'span', 'strong', 'textarea', 'ul', 'ol'],
ALLOWED_ATTR: ['href', 'data-href', 'data-command', 'target', 'title', 'name', 'src', 'alt', 'class', 'id', 'role', 'tabindex', 'style', 'data-code', 'width', 'height', 'align', 'x-dispatch', 'required', 'checked', 'placeholder', 'type'],
RETURN_DOM: false,
RETURN_DOM_FRAGMENT: false,
};
const options = _extInsaneOptions({
allowedTags: ['a', 'button', 'blockquote', 'code', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input', 'label', 'li', 'p', 'pre', 'select', 'small', 'span', 'strong', 'textarea', 'ul', 'ol'], // {{SQL CARBON EDIT}} Add i & img tags for welcome page support
allowedAttributes: {
'a': ['href', 'x-dispatch'],
'button': ['data-href', 'x-dispatch'],
'input': ['type', 'placeholder', 'checked', 'required'],
'img': ['src', 'alt', 'title', 'aria-label'], // {{SQL CARBON EDIT}} Add img for welcome page support
'label': ['for'],
'select': ['required'],
'span': ['data-command', 'role'],
'textarea': ['name', 'placeholder', 'required'],
},
allowedSchemes: ['http', 'https', 'command', 'vscode-file'] // {{SQL CARBON EDIT}} Add allowed schema for welcome page support
}, ['class', 'id', 'role', 'tabindex']);
const allowedProtocols = [Schemas.http, Schemas.https, Schemas.command];
const html = _ttpSafeInnerHtml?.createHTML(value, options) ?? insane(value, options);
node.innerHTML = html as string;
// https://github.com/cure53/DOMPurify/blob/main/demos/hooks-scheme-allowlist.html
dompurify.addHook('afterSanitizeAttributes', (node) => {
// build an anchor to map URLs to
const anchor = document.createElement('a');
// check all href/src attributes for validity
for (const attr in ['href', 'src']) {
if (node.hasAttribute(attr)) {
anchor.href = node.getAttribute(attr) as string;
if (!allowedProtocols.includes(anchor.protocol)) {
node.removeAttribute(attr);
}
}
}
});
try {
const html = dompurify.sanitize(value, { ...options, RETURN_TRUSTED_TYPE: true });
node.innerHTML = html as unknown as string;
} finally {
dompurify.removeHook('afterSanitizeAttributes');
}
}
/**
@@ -0,0 +1,17 @@
{
"registrations": [
{
"component": {
"type": "git",
"git": {
"name": "dompurify",
"repositoryUrl": "https://github.com/cure53/DOMPurify",
"commitHash": "6cfcdf56269b892550af80baa7c1fa5b680e5db7"
}
},
"license": "Apache 2.0",
"version": "2.3.1"
}
],
"version": 1
}
+104
View File
@@ -0,0 +1,104 @@
// Type definitions for DOM Purify 2.2
// Project: https://github.com/cure53/DOMPurify
// Definitions by: Dave Taylor https://github.com/davetayls
// Samira Bazuzi <https://github.com/bazuzi>
// FlowCrypt <https://github.com/FlowCrypt>
// Exigerr <https://github.com/Exigerr>
// Piotr Błażejewicz <https://github.com/peterblazejewicz>
// Nicholas Ellul <https://github.com/NicholasEllul>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
export as namespace DOMPurify;
export = DOMPurify;
declare const DOMPurify: createDOMPurifyI;
interface createDOMPurifyI extends DOMPurify.DOMPurifyI {
(window?: Window): DOMPurify.DOMPurifyI;
}
declare namespace DOMPurify {
interface DOMPurifyI {
sanitize(source: string | Node): string;
sanitize(source: string | Node, config: Config & { RETURN_TRUSTED_TYPE: true }): TrustedHTML;
sanitize(source: string | Node, config: Config & { RETURN_DOM_FRAGMENT?: false | undefined; RETURN_DOM?: false | undefined }): string;
sanitize(source: string | Node, config: Config & { RETURN_DOM_FRAGMENT: true }): DocumentFragment;
sanitize(source: string | Node, config: Config & { RETURN_DOM: true }): HTMLElement;
sanitize(source: string | Node, config: Config): string | HTMLElement | DocumentFragment;
addHook(hook: 'uponSanitizeElement', cb: (currentNode: Element, data: SanitizeElementHookEvent, config: Config) => void): void;
addHook(hook: 'uponSanitizeAttribute', cb: (currentNode: Element, data: SanitizeAttributeHookEvent, config: Config) => void): void;
addHook(hook: HookName, cb: (currentNode: Element, data: HookEvent, config: Config) => void): void;
setConfig(cfg: Config): void;
clearConfig(): void;
isValidAttribute(tag: string, attr: string, value: string): boolean;
removeHook(entryPoint: HookName): void;
removeHooks(entryPoint: HookName): void;
removeAllHooks(): void;
version: string;
removed: any[];
isSupported: boolean;
}
interface Config {
ADD_ATTR?: string[] | undefined;
ADD_DATA_URI_TAGS?: string[] | undefined;
ADD_TAGS?: string[] | undefined;
ALLOW_DATA_ATTR?: boolean | undefined;
ALLOWED_ATTR?: string[] | undefined;
ALLOWED_TAGS?: string[] | undefined;
FORBID_ATTR?: string[] | undefined;
FORBID_TAGS?: string[] | undefined;
FORCE_BODY?: boolean | undefined;
KEEP_CONTENT?: boolean | undefined;
/**
* change the default namespace from HTML to something different
*/
NAMESPACE?: string | undefined;
RETURN_DOM?: boolean | undefined;
RETURN_DOM_FRAGMENT?: boolean | undefined;
/**
* This defaults to `true` starting DOMPurify 2.2.0. Note that setting it to `false`
* might cause XSS from attacks hidden in closed shadowroots in case the browser
* supports Declarative Shadow: DOM https://web.dev/declarative-shadow-dom/
*/
RETURN_DOM_IMPORT?: boolean | undefined;
RETURN_TRUSTED_TYPE?: boolean | undefined;
SANITIZE_DOM?: boolean | undefined;
WHOLE_DOCUMENT?: boolean | undefined;
ALLOWED_URI_REGEXP?: RegExp | undefined;
SAFE_FOR_TEMPLATES?: boolean | undefined;
ALLOW_UNKNOWN_PROTOCOLS?: boolean | undefined;
USE_PROFILES?: false | { mathMl?: boolean | undefined; svg?: boolean | undefined; svgFilters?: boolean | undefined; html?: boolean | undefined } | undefined;
IN_PLACE?: boolean | undefined;
}
type HookName =
| 'beforeSanitizeElements'
| 'uponSanitizeElement'
| 'afterSanitizeElements'
| 'beforeSanitizeAttributes'
| 'uponSanitizeAttribute'
| 'afterSanitizeAttributes'
| 'beforeSanitizeShadowDOM'
| 'uponSanitizeShadowNode'
| 'afterSanitizeShadowDOM';
type HookEvent = SanitizeElementHookEvent | SanitizeAttributeHookEvent | null;
interface SanitizeElementHookEvent {
tagName: string;
allowedTags: { [key: string]: boolean };
}
interface SanitizeAttributeHookEvent {
attrName: string;
attrValue: string;
keepAttr: boolean;
allowedAttributes: { [key: string]: boolean };
forceKeepAttr?: boolean | undefined;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,377 @@
DOMPurify
Copyright 2015 Mario Heiderich
DOMPurify is free software; you can redistribute it and/or modify it under the
terms of either:
a) the Apache License Version 2.0, or
b) the Mozilla Public License Version 2.0
-----------------------------------------------------------------------------
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-----------------------------------------------------------------------------
Mozilla Public License, version 2.0
1. Definitions
1.1. “Contributor”
means each individual or legal entity that creates, contributes to the
creation of, or owns Covered Software.
1.2. “Contributor Version”
means the combination of the Contributions of others (if any) used by a
Contributor and that particular Contributors Contribution.
1.3. “Contribution”
means Covered Software of a particular Contributor.
1.4. “Covered Software”
means Source Code Form to which the initial Contributor has attached the
notice in Exhibit A, the Executable Form of such Source Code Form, and
Modifications of such Source Code Form, in each case including portions
thereof.
1.5. “Incompatible With Secondary Licenses”
means
a. that the initial Contributor has attached the notice described in
Exhibit B to the Covered Software; or
b. that the Covered Software was made available under the terms of version
1.1 or earlier of the License, but not also under the terms of a
Secondary License.
1.6. “Executable Form”
means any form of the work other than Source Code Form.
1.7. “Larger Work”
means a work that combines Covered Software with other material, in a separate
file or files, that is not Covered Software.
1.8. “License”
means this document.
1.9. “Licensable”
means having the right to grant, to the maximum extent possible, whether at the
time of the initial grant or subsequently, any and all of the rights conveyed by
this License.
1.10. “Modifications”
means any of the following:
a. any file in Source Code Form that results from an addition to, deletion
from, or modification of the contents of Covered Software; or
b. any new file in Source Code Form that contains any Covered Software.
1.11. “Patent Claims” of a Contributor
means any patent claim(s), including without limitation, method, process,
and apparatus claims, in any patent Licensable by such Contributor that
would be infringed, but for the grant of the License, by the making,
using, selling, offering for sale, having made, import, or transfer of
either its Contributions or its Contributor Version.
1.12. “Secondary License”
means either the GNU General Public License, Version 2.0, the GNU Lesser
General Public License, Version 2.1, the GNU Affero General Public
License, Version 3.0, or any later versions of those licenses.
1.13. “Source Code Form”
means the form of the work preferred for making modifications.
1.14. “You” (or “Your”)
means an individual or a legal entity exercising rights under this
License. For legal entities, “You” includes any entity that controls, is
controlled by, or is under common control with You. For purposes of this
definition, “control” means (a) the power, direct or indirect, to cause
the direction or management of such entity, whether by contract or
otherwise, or (b) ownership of more than fifty percent (50%) of the
outstanding shares or beneficial ownership of such entity.
2. License Grants and Conditions
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
a. under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or as
part of a Larger Work; and
b. under Patent Claims of such Contributor to make, use, sell, offer for
sale, have made, import, and otherwise transfer either its Contributions
or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution become
effective for each Contribution on the date the Contributor first distributes
such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under this
License. No additional rights or licenses will be implied from the distribution
or licensing of Covered Software under this License. Notwithstanding Section
2.1(b) above, no patent license is granted by a Contributor:
a. for any code that a Contributor has removed from Covered Software; or
b. for infringements caused by: (i) Your and any other third partys
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
c. under Patent Claims infringed by Covered Software in the absence of its
Contributions.
This License does not grant any rights in the trademarks, service marks, or
logos of any Contributor (except as may be necessary to comply with the
notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this License
(see Section 10.2) or under the terms of a Secondary License (if permitted
under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its Contributions
are its original creation(s) or it has sufficient rights to grant the
rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under applicable
copyright doctrines of fair use, fair dealing, or other equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
Section 2.1.
3. Responsibilities
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under the
terms of this License. You must inform recipients that the Source Code Form
of the Covered Software is governed by the terms of this License, and how
they can obtain a copy of this License. You may not attempt to alter or
restrict the recipients rights in the Source Code Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
a. such Covered Software must also be made available in Source Code Form,
as described in Section 3.1, and You must inform recipients of the
Executable Form how they can obtain a copy of such Source Code Form by
reasonable means in a timely manner, at a charge no more than the cost
of distribution to the recipient; and
b. You may distribute such Executable Form under the terms of this License,
or sublicense it under different terms, provided that the license for
the Executable Form does not attempt to limit or alter the recipients
rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for the
Covered Software. If the Larger Work is a combination of Covered Software
with a work governed by one or more Secondary Licenses, and the Covered
Software is not Incompatible With Secondary Licenses, this License permits
You to additionally distribute such Covered Software under the terms of
such Secondary License(s), so that the recipient of the Larger Work may, at
their option, further distribute the Covered Software under the terms of
either this License or such Secondary License(s).
3.4. Notices
You may not remove or alter the substance of any license notices (including
copyright notices, patent notices, disclaimers of warranty, or limitations
of liability) contained within the Source Code Form of the Covered
Software, except that You may alter any license notices to the extent
required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on behalf
of any Contributor. You must make it absolutely clear that any such
warranty, support, indemnity, or liability obligation is offered by You
alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
If it is impossible for You to comply with any of the terms of this License
with respect to some or all of the Covered Software due to statute, judicial
order, or regulation then You must: (a) comply with the terms of this License
to the maximum extent possible; and (b) describe the limitations and the code
they affect. Such description must be placed in a text file included with all
distributions of the Covered Software under this License. Except to the
extent prohibited by statute or regulation, such description must be
sufficiently detailed for a recipient of ordinary skill to be able to
understand it.
5. Termination
5.1. The rights granted under this License will terminate automatically if You
fail to comply with any of its terms. However, if You become compliant,
then the rights granted under this License from a particular Contributor
are reinstated (a) provisionally, unless and until such Contributor
explicitly and finally terminates Your grants, and (b) on an ongoing basis,
if such Contributor fails to notify You of the non-compliance by some
reasonable means prior to 60 days after You have come back into compliance.
Moreover, Your grants from a particular Contributor are reinstated on an
ongoing basis if such Contributor notifies You of the non-compliance by
some reasonable means, this is the first time You have received notice of
non-compliance with this License from such Contributor, and You become
compliant prior to 30 days after Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions, counter-claims,
and cross-claims) alleging that a Contributor Version directly or
indirectly infringes any patent, then the rights granted to You by any and
all Contributors for the Covered Software under Section 2.1 of this License
shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
license agreements (excluding distributors and resellers) which have been
validly granted by You or Your distributors under this License prior to
termination shall survive termination.
6. Disclaimer of Warranty
Covered Software is provided under this License on an “as is” basis, without
warranty of any kind, either expressed, implied, or statutory, including,
without limitation, warranties that the Covered Software is free of defects,
merchantable, fit for a particular purpose or non-infringing. The entire
risk as to the quality and performance of the Covered Software is with You.
Should any Covered Software prove defective in any respect, You (not any
Contributor) assume the cost of any necessary servicing, repair, or
correction. This disclaimer of warranty constitutes an essential part of this
License. No use of any Covered Software is authorized under this License
except under this disclaimer.
7. Limitation of Liability
Under no circumstances and under no legal theory, whether tort (including
negligence), contract, or otherwise, shall any Contributor, or anyone who
distributes Covered Software as permitted above, be liable to You for any
direct, indirect, special, incidental, or consequential damages of any
character including, without limitation, damages for lost profits, loss of
goodwill, work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses, even if such party shall have been
informed of the possibility of such damages. This limitation of liability
shall not apply to liability for death or personal injury resulting from such
partys negligence to the extent applicable law prohibits such limitation.
Some jurisdictions do not allow the exclusion or limitation of incidental or
consequential damages, so this exclusion and limitation may not apply to You.
8. Litigation
Any litigation relating to this License may be brought only in the courts of
a jurisdiction where the defendant maintains its principal place of business
and such litigation shall be governed by laws of that jurisdiction, without
reference to its conflict-of-law provisions. Nothing in this Section shall
prevent a partys ability to bring cross-claims or counter-claims.
9. Miscellaneous
This License represents the complete agreement concerning the subject matter
hereof. If any provision of this License is held to be unenforceable, such
provision shall be reformed only to the extent necessary to make it
enforceable. Any law or regulation which provides that the language of a
contract shall be construed against the drafter shall not be used to construe
this License against a Contributor.
10. Versions of the License
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version of
the License under which You originally received the Covered Software, or
under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a modified
version of this License if you rename the license and remove any
references to the name of the license steward (except to note that such
modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular file, then
You may include the notice in a location (such as a LICENSE file in a relevant
directory) where a recipient would be likely to look for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - “Incompatible With Secondary Licenses” Notice
This Source Code Form is “Incompatible
With Secondary Licenses”, as defined by
the Mozilla Public License, v. 2.0.
+3 -3
View File
@@ -87,7 +87,7 @@ interface IFormatParseTree {
children?: IFormatParseTree[];
}
function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionHandler?: IContentActionHandler, renderCodeSegements?: boolean) {
function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionHandler?: IContentActionHandler, renderCodeSegments?: boolean) {
let child: Node | undefined;
if (treeNode.type === FormatType.Text) {
@@ -96,7 +96,7 @@ function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionH
child = document.createElement('b');
} else if (treeNode.type === FormatType.Italics) {
child = document.createElement('i');
} else if (treeNode.type === FormatType.Code && renderCodeSegements) {
} else if (treeNode.type === FormatType.Code && renderCodeSegments) {
child = document.createElement('code');
} else if (treeNode.type === FormatType.Action && actionHandler) {
const a = document.createElement('a');
@@ -118,7 +118,7 @@ function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionH
if (child && Array.isArray(treeNode.children)) {
treeNode.children.forEach((nodeChild) => {
_renderFormattedText(child!, nodeChild, actionHandler, renderCodeSegements);
_renderFormattedText(child!, nodeChild, actionHandler, renderCodeSegments);
});
}
}
+3 -5
View File
@@ -29,11 +29,9 @@ function getParentWindowIfSameOrigin(w: Window): Window | null {
try {
let location = w.location;
let parentLocation = w.parent.location;
if (location.origin !== 'null' && parentLocation.origin !== 'null') {
if (location.protocol !== parentLocation.protocol || location.hostname !== parentLocation.hostname || location.port !== parentLocation.port) {
hasDifferentOriginAncestorFlag = true;
return null;
}
if (location.origin !== 'null' && parentLocation.origin !== 'null' && location.origin !== parentLocation.origin) {
hasDifferentOriginAncestorFlag = true;
return null;
}
} catch (e) {
hasDifferentOriginAncestorFlag = true;
+30 -159
View File
@@ -4,164 +4,11 @@
*--------------------------------------------------------------------------------------------*/
import * as browser from 'vs/base/browser/browser';
import { KeyCode, KeyCodeUtils, KeyMod, SimpleKeybinding } from 'vs/base/common/keyCodes';
import { EVENT_KEY_CODE_MAP, KeyCode, KeyCodeUtils, KeyMod } from 'vs/base/common/keyCodes';
import { SimpleKeybinding } from 'vs/base/common/keybindings';
import * as platform from 'vs/base/common/platform';
let KEY_CODE_MAP: { [keyCode: number]: KeyCode } = new Array(230);
let INVERSE_KEY_CODE_MAP: KeyCode[] = new Array(KeyCode.MAX_VALUE);
(function () {
for (let i = 0; i < INVERSE_KEY_CODE_MAP.length; i++) {
INVERSE_KEY_CODE_MAP[i] = -1;
}
function define(code: number, keyCode: KeyCode): void {
KEY_CODE_MAP[code] = keyCode;
INVERSE_KEY_CODE_MAP[keyCode] = code;
}
define(3, KeyCode.PauseBreak); // VK_CANCEL 0x03 Control-break processing
define(8, KeyCode.Backspace);
define(9, KeyCode.Tab);
define(13, KeyCode.Enter);
define(16, KeyCode.Shift);
define(17, KeyCode.Ctrl);
define(18, KeyCode.Alt);
define(19, KeyCode.PauseBreak);
define(20, KeyCode.CapsLock);
define(27, KeyCode.Escape);
define(32, KeyCode.Space);
define(33, KeyCode.PageUp);
define(34, KeyCode.PageDown);
define(35, KeyCode.End);
define(36, KeyCode.Home);
define(37, KeyCode.LeftArrow);
define(38, KeyCode.UpArrow);
define(39, KeyCode.RightArrow);
define(40, KeyCode.DownArrow);
define(45, KeyCode.Insert);
define(46, KeyCode.Delete);
define(48, KeyCode.KEY_0);
define(49, KeyCode.KEY_1);
define(50, KeyCode.KEY_2);
define(51, KeyCode.KEY_3);
define(52, KeyCode.KEY_4);
define(53, KeyCode.KEY_5);
define(54, KeyCode.KEY_6);
define(55, KeyCode.KEY_7);
define(56, KeyCode.KEY_8);
define(57, KeyCode.KEY_9);
define(65, KeyCode.KEY_A);
define(66, KeyCode.KEY_B);
define(67, KeyCode.KEY_C);
define(68, KeyCode.KEY_D);
define(69, KeyCode.KEY_E);
define(70, KeyCode.KEY_F);
define(71, KeyCode.KEY_G);
define(72, KeyCode.KEY_H);
define(73, KeyCode.KEY_I);
define(74, KeyCode.KEY_J);
define(75, KeyCode.KEY_K);
define(76, KeyCode.KEY_L);
define(77, KeyCode.KEY_M);
define(78, KeyCode.KEY_N);
define(79, KeyCode.KEY_O);
define(80, KeyCode.KEY_P);
define(81, KeyCode.KEY_Q);
define(82, KeyCode.KEY_R);
define(83, KeyCode.KEY_S);
define(84, KeyCode.KEY_T);
define(85, KeyCode.KEY_U);
define(86, KeyCode.KEY_V);
define(87, KeyCode.KEY_W);
define(88, KeyCode.KEY_X);
define(89, KeyCode.KEY_Y);
define(90, KeyCode.KEY_Z);
define(93, KeyCode.ContextMenu);
define(96, KeyCode.NUMPAD_0);
define(97, KeyCode.NUMPAD_1);
define(98, KeyCode.NUMPAD_2);
define(99, KeyCode.NUMPAD_3);
define(100, KeyCode.NUMPAD_4);
define(101, KeyCode.NUMPAD_5);
define(102, KeyCode.NUMPAD_6);
define(103, KeyCode.NUMPAD_7);
define(104, KeyCode.NUMPAD_8);
define(105, KeyCode.NUMPAD_9);
define(106, KeyCode.NUMPAD_MULTIPLY);
define(107, KeyCode.NUMPAD_ADD);
define(108, KeyCode.NUMPAD_SEPARATOR);
define(109, KeyCode.NUMPAD_SUBTRACT);
define(110, KeyCode.NUMPAD_DECIMAL);
define(111, KeyCode.NUMPAD_DIVIDE);
define(112, KeyCode.F1);
define(113, KeyCode.F2);
define(114, KeyCode.F3);
define(115, KeyCode.F4);
define(116, KeyCode.F5);
define(117, KeyCode.F6);
define(118, KeyCode.F7);
define(119, KeyCode.F8);
define(120, KeyCode.F9);
define(121, KeyCode.F10);
define(122, KeyCode.F11);
define(123, KeyCode.F12);
define(124, KeyCode.F13);
define(125, KeyCode.F14);
define(126, KeyCode.F15);
define(127, KeyCode.F16);
define(128, KeyCode.F17);
define(129, KeyCode.F18);
define(130, KeyCode.F19);
define(144, KeyCode.NumLock);
define(145, KeyCode.ScrollLock);
define(186, KeyCode.US_SEMICOLON);
define(187, KeyCode.US_EQUAL);
define(188, KeyCode.US_COMMA);
define(189, KeyCode.US_MINUS);
define(190, KeyCode.US_DOT);
define(191, KeyCode.US_SLASH);
define(192, KeyCode.US_BACKTICK);
define(193, KeyCode.ABNT_C1);
define(194, KeyCode.ABNT_C2);
define(219, KeyCode.US_OPEN_SQUARE_BRACKET);
define(220, KeyCode.US_BACKSLASH);
define(221, KeyCode.US_CLOSE_SQUARE_BRACKET);
define(222, KeyCode.US_QUOTE);
define(223, KeyCode.OEM_8);
define(226, KeyCode.OEM_102);
/**
* https://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html
* If an Input Method Editor is processing key input and the event is keydown, return 229.
*/
define(229, KeyCode.KEY_IN_COMPOSITION);
if (browser.isFirefox) {
define(59, KeyCode.US_SEMICOLON);
define(107, KeyCode.US_EQUAL);
define(109, KeyCode.US_MINUS);
if (platform.isMacintosh) {
define(224, KeyCode.Meta);
}
} else if (browser.isWebKit) {
define(91, KeyCode.Meta);
if (platform.isMacintosh) {
// the two meta keys in the Mac have different key codes (91 and 93)
define(93, KeyCode.Meta);
} else {
define(92, KeyCode.Meta);
}
}
})();
function extractKeyCode(e: KeyboardEvent): KeyCode {
if (e.charCode) {
@@ -169,11 +16,35 @@ function extractKeyCode(e: KeyboardEvent): KeyCode {
let char = String.fromCharCode(e.charCode).toUpperCase();
return KeyCodeUtils.fromString(char);
}
return KEY_CODE_MAP[e.keyCode] || KeyCode.Unknown;
}
export function getCodeForKeyCode(keyCode: KeyCode): number {
return INVERSE_KEY_CODE_MAP[keyCode];
const keyCode = e.keyCode;
// browser quirks
if (keyCode === 3) {
return KeyCode.PauseBreak;
} else if (browser.isFirefox) {
if (keyCode === 59) {
return KeyCode.Semicolon;
} else if (keyCode === 107) {
return KeyCode.Equal;
} else if (keyCode === 109) {
return KeyCode.Minus;
} else if (platform.isMacintosh && keyCode === 224) {
return KeyCode.Meta;
}
} else if (browser.isWebKit) {
if (keyCode === 91) {
return KeyCode.Meta;
} else if (platform.isMacintosh && keyCode === 93) {
// the two meta keys in the Mac have different key codes (91 and 93)
return KeyCode.Meta;
} else if (!platform.isMacintosh && keyCode === 92) {
return KeyCode.Meta;
}
}
// cross browser keycodes:
return EVENT_KEY_CODE_MAP[keyCode] || KeyCode.Unknown;
}
export interface IKeyboardEvent {
+105 -72
View File
@@ -4,16 +4,19 @@
*--------------------------------------------------------------------------------------------*/
import * as DOM from 'vs/base/browser/dom';
import * as dompurify from 'vs/base/browser/dompurify/dompurify';
import { DomEmitter } from 'vs/base/browser/event';
import { createElement, FormattedTextRenderOptions } from 'vs/base/browser/formattedTextRenderer';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
import { raceCancellation } from 'vs/base/common/async';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Event } from 'vs/base/common/event';
import { IMarkdownString, parseHrefAndDimensions, removeMarkdownEscapes } from 'vs/base/common/htmlContent';
import { markdownEscapeEscapedIcons } from 'vs/base/common/iconLabels';
import { defaultGenerator } from 'vs/base/common/idGenerator';
import { insane, InsaneOptions } from 'vs/base/common/insane/insane';
import { DisposableStore } from 'vs/base/common/lifecycle';
import * as marked from 'vs/base/common/marked/marked';
import { parse } from 'vs/base/common/marshalling';
import { FileAccess, Schemas } from 'vs/base/common/network';
@@ -27,24 +30,23 @@ export interface MarkedOptions extends marked.MarkedOptions {
}
export interface MarkdownRenderOptions extends FormattedTextRenderOptions {
codeBlockRenderer?: (modeId: string, value: string) => Promise<HTMLElement>;
codeBlockRenderer?: (languageId: string, value: string) => Promise<HTMLElement>;
asyncRenderCallback?: () => void;
baseUrl?: URI;
}
const _ttpInsane = window.trustedTypes?.createPolicy('insane', {
createHTML(value, options: InsaneOptions): string {
return insane(value, options);
}
});
/**
* Low-level way create a html element from a markdown string.
*
* **Note** that for most cases you should be using [`MarkdownRenderer`](./src/vs/editor/browser/core/markdownRenderer.ts)
* which comes with support for pretty code block rendering and which uses the default way of handling links.
*/
export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRenderOptions = {}, markedOptions: MarkedOptions = {}): HTMLElement {
export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRenderOptions = {}, markedOptions: MarkedOptions = {}): { element: HTMLElement, dispose: () => void } {
const disposables = new DisposableStore();
let isDisposed = false;
const cts = disposables.add(new CancellationTokenSource());
const element = createElement(options);
const _uriMassage = function (part: string): string {
@@ -74,6 +76,9 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
}
let uri = URI.revive(data);
if (isDomUri) {
if (href.startsWith(Schemas.data + ':')) {
return href;
}
// this URI will end up as "src"-attribute of a dom node
// and because of that special rewriting needs to be done
// so that the URI uses a protocol that's understood by
@@ -155,10 +160,6 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
}
};
renderer.paragraph = (text): string => {
if (markdown.supportThemeIcons) {
const elements = renderLabelWithIcons(text);
text = elements.map(e => typeof e === 'string' ? e : e.outerHTML).join('');
}
return `<p>${text}</p>`;
};
@@ -168,26 +169,23 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
// when code-block rendering is async we return sync
// but update the node with the real result later.
const id = defaultGenerator.nextId();
// {{SQL CARBON EDIT}} - Promise.all not returning the strValue properly in original code? @todo anthonydresser 4/12/19 investigate a better way to do this.
const promise = value.then(strValue => {
withInnerHTML.then(e => {
raceCancellation(Promise.all([value, withInnerHTML]), cts.token).then(values => {
if (!isDisposed && values) {
const span = <HTMLDivElement>element.querySelector(`div[data-code="${id}"]`);
if (span) {
DOM.reset(span, strValue);
DOM.reset(span, values[0]);
}
}).catch(err => {
// ignore
});
options.asyncRenderCallback?.();
}
}).catch(() => {
// ignore
});
if (options.asyncRenderCallback) {
promise.then(options.asyncRenderCallback);
}
return `<div class="code" data-code="${id}">${escape(code)}</div>`;
};
}
if (options.actionHandler) {
const onClick = options.actionHandler.disposables.add(new DomEmitter(element, 'click'));
const onAuxClick = options.actionHandler.disposables.add(new DomEmitter(element, 'auxclick'));
@@ -217,17 +215,21 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
}));
}
// Use our own sanitizer so that we can let through only spans.
// Otherwise, we'd be letting all html be rendered.
// If we want to allow markdown permitted tags, then we can delete sanitizer and sanitize.
// We always pass the output through insane after this so that we don't rely on
// marked for sanitization.
markedOptions.sanitizer = (html: string): string => {
const match = markdown.isTrusted ? html.match(/^(<span[^>]+>)|(<\/\s*span>)$/) : undefined;
return match ? html : '';
};
markedOptions.sanitize = true;
markedOptions.silent = true;
if (!markdown.supportHtml) {
// TODO: Can we deprecated this in favor of 'supportHtml'?
// Use our own sanitizer so that we can let through only spans.
// Otherwise, we'd be letting all html be rendered.
// If we want to allow markdown permitted tags, then we can delete sanitizer and sanitize.
// We always pass the output through dompurify after this so that we don't rely on
// marked for sanitization.
markedOptions.sanitizer = (html: string): string => {
const match = markdown.isTrusted ? html.match(/^(<span[^>]+>)|(<\/\s*span>)$/) : undefined;
return match ? html : '';
};
markedOptions.sanitize = true;
markedOptions.silent = true;
}
markedOptions.renderer = renderer;
@@ -241,10 +243,15 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
value = markdownEscapeEscapedIcons(value);
}
const renderedMarkdown = marked.parse(value, markedOptions);
let renderedMarkdown = marked.parse(value, markedOptions);
// sanitize with insane
element.innerHTML = sanitizeRenderedMarkdown(markdown, renderedMarkdown) as string;
// Rewrite theme icons
if (markdown.supportThemeIcons) {
const elements = renderLabelWithIcons(renderedMarkdown);
renderedMarkdown = elements.map(e => typeof e === 'string' ? e : e.outerHTML).join('');
}
element.innerHTML = sanitizeRenderedMarkdown(markdown, renderedMarkdown) as unknown as string;
// signal that async code blocks can be now be inserted
signalInnerHTML!();
@@ -252,26 +259,69 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
// signal size changes for image tags
if (options.asyncRenderCallback) {
for (const img of element.getElementsByTagName('img')) {
const listener = DOM.addDisposableListener(img, 'load', () => {
const listener = disposables.add(DOM.addDisposableListener(img, 'load', () => {
listener.dispose();
options.asyncRenderCallback!();
});
}));
}
}
return element;
return {
element,
dispose: () => {
isDisposed = true;
cts.cancel();
disposables.dispose();
}
};
}
function sanitizeRenderedMarkdown(
options: { isTrusted?: boolean },
renderedMarkdown: string,
): string | TrustedHTML {
const insaneOptions = getInsaneOptions(options);
return _ttpInsane?.createHTML(renderedMarkdown, insaneOptions) ?? insane(renderedMarkdown, insaneOptions);
): TrustedHTML {
const { config, allowedSchemes } = getSanitizerOptions(options);
dompurify.addHook('uponSanitizeAttribute', (element, e) => {
if (e.attrName === 'style' || e.attrName === 'class') {
if (element.tagName === 'SPAN') {
if (e.attrName === 'style') {
e.keepAttr = /^(color\:#[0-9a-fA-F]+;)?(background-color\:#[0-9a-fA-F]+;)?$/.test(e.attrValue);
return;
} else if (e.attrName === 'class') {
e.keepAttr = /^codicon codicon-[a-z\-]+( codicon-modifier-[a-z\-]+)?$/.test(e.attrValue);
return;
}
}
e.keepAttr = false;
return;
}
});
// build an anchor to map URLs to
const anchor = document.createElement('a');
// https://github.com/cure53/DOMPurify/blob/main/demos/hooks-scheme-allowlist.html
dompurify.addHook('afterSanitizeAttributes', (node) => {
// check all href/src attributes for validity
for (const attr of ['href', 'src']) {
if (node.hasAttribute(attr)) {
anchor.href = node.getAttribute(attr) as string;
if (!allowedSchemes.includes(anchor.protocol.replace(/:$/, ''))) {
node.removeAttribute(attr);
}
}
}
});
try {
return dompurify.sanitize(renderedMarkdown, { ...config, RETURN_TRUSTED_TYPE: true });
} finally {
dompurify.removeHook('uponSanitizeAttribute');
dompurify.removeHook('afterSanitizeAttributes');
}
}
function getInsaneOptions(options: { readonly isTrusted?: boolean }): InsaneOptions {
function getSanitizerOptions(options: { readonly isTrusted?: boolean }): { config: dompurify.Config, allowedSchemes: string[] } {
const allowedSchemes = [
Schemas.http,
Schemas.https,
@@ -288,33 +338,16 @@ function getInsaneOptions(options: { readonly isTrusted?: boolean }): InsaneOpti
}
return {
allowedSchemes,
// allowedTags should included everything that markdown renders to.
// Since we have our own sanitize function for marked, it's possible we missed some tag so let insane make sure.
// HTML tags that can result from markdown are from reading https://spec.commonmark.org/0.29/
// HTML table tags that can result from markdown are from https://github.github.com/gfm/#tables-extension-
allowedTags: ['ul', 'li', 'p', 'code', 'blockquote', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'em', 'pre', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'div', 'del', 'a', 'strong', 'br', 'img', 'span'],
allowedAttributes: {
'a': ['href', 'name', 'target', 'data-href'],
'img': ['src', 'title', 'alt', 'width', 'height'],
'div': ['class', 'data-code'],
'span': ['class', 'style'],
// https://github.com/microsoft/vscode/issues/95937
'th': ['align'],
'td': ['align']
config: {
// allowedTags should included everything that markdown renders to.
// Since we have our own sanitize function for marked, it's possible we missed some tag so let dompurify make sure.
// HTML tags that can result from markdown are from reading https://spec.commonmark.org/0.29/
// HTML table tags that can result from markdown are from https://github.github.com/gfm/#tables-extension-
ALLOWED_TAGS: ['ul', 'li', 'p', 'b', 'i', 'code', 'blockquote', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'em', 'pre', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'div', 'del', 'a', 'strong', 'br', 'img', 'span'],
ALLOWED_ATTR: ['href', 'data-href', 'target', 'title', 'src', 'alt', 'class', 'style', 'data-code', 'width', 'height', 'align'],
ALLOW_UNKNOWN_PROTOCOLS: true,
},
filter(token: { tag: string; attrs: { readonly [key: string]: string; }; }): boolean {
if (token.tag === 'span' && options.isTrusted) {
if (token.attrs['style'] && (Object.keys(token.attrs).length === 1)) {
return !!token.attrs['style'].match(/^(color\:#[0-9a-fA-F]+;)?(background-color\:#[0-9a-fA-F]+;)?$/);
} else if (token.attrs['class']) {
// The class should match codicon rendering in src\vs\base\common\codicons.ts
return !!token.attrs['class'].match(/^codicon codicon-[a-z\-]+( codicon-modifier-[a-z\-]+)?$/);
}
return false;
}
return true;
}
allowedSchemes
};
}
@@ -150,7 +150,7 @@ export class BaseActionViewItem extends Disposable implements IActionViewItem {
// menus do not use the click event
if (!(this.options && this.options.isMenu)) {
platform.setImmediate(() => this.onClick(e));
this.onClick(e);
}
}));
@@ -21,7 +21,7 @@ const GOLDEN_RATIO = {
rightMarginRatio: 0.1909
};
function createEmptyView(background: Color | undefined): ISplitViewView {
function createEmptyView(background: Color | undefined): ISplitViewView<{ top: number, left: number }> {
const element = $('.centered-layout-margin');
element.style.height = '100%';
if (background) {
@@ -37,13 +37,13 @@ function createEmptyView(background: Color | undefined): ISplitViewView {
};
}
function toSplitViewView(view: IView, getHeight: () => number): ISplitViewView {
function toSplitViewView(view: IView, getHeight: () => number): ISplitViewView<{ top: number, left: number }> {
return {
element: view.element,
get maximumSize() { return view.maximumWidth; },
get minimumSize() { return view.minimumWidth; },
onDidChange: Event.map(view.onDidChange, e => e && e.width),
layout: (size, offset) => view.layout(size, getHeight(), 0, offset)
layout: (size, offset, ctx) => view.layout(size, getHeight(), ctx?.top ?? 0, (ctx?.left ?? 0) + offset)
};
}
@@ -53,12 +53,12 @@ export interface ICenteredViewStyles extends ISplitViewStyles {
export class CenteredViewLayout implements IDisposable {
private splitView?: SplitView;
private splitView?: SplitView<{ top: number, left: number }>;
private width: number = 0;
private height: number = 0;
private style!: ICenteredViewStyles;
private didLayout = false;
private emptyViews: ISplitViewView[] | undefined;
private emptyViews: ISplitViewView<{ top: number, left: number }>[] | undefined;
private readonly splitViewDisposables = new DisposableStore();
constructor(private container: HTMLElement, private view: IView, public readonly state: CenteredViewState = { leftMarginRatio: GOLDEN_RATIO.leftMarginRatio, rightMarginRatio: GOLDEN_RATIO.rightMarginRatio }) {
@@ -86,7 +86,7 @@ export class CenteredViewLayout implements IDisposable {
this.splitView.orthogonalEndSash = boundarySashes.bottom;
}
layout(width: number, height: number): void {
layout(width: number, height: number, top: number, left: number): void {
this.width = width;
this.height = height;
if (this.splitView) {
@@ -95,7 +95,7 @@ export class CenteredViewLayout implements IDisposable {
this.resizeMargins();
}
} else {
this.view.layout(width, height, 0, 0);
this.view.layout(width, height, top, left);
}
this.didLayout = true;
}
+6 -2
View File
@@ -47,7 +47,7 @@ export class CheckboxActionViewItem extends BaseActionViewItem {
super(context, action, options);
this.checkbox = this._register(new Checkbox({
actionClassName: this._action.class,
isChecked: this._action.checked,
isChecked: !!this._action.checked,
title: (<IActionViewItemOptions>this.options).keybinding ? `${this._action.label} (${(<IActionViewItemOptions>this.options).keybinding})` : this._action.label,
notFocusable: true
}));
@@ -70,7 +70,7 @@ export class CheckboxActionViewItem extends BaseActionViewItem {
}
override updateChecked(): void {
this.checkbox.checked = this._action.checked;
this.checkbox.checked = !!this._action.checked;
}
override focus(): void {
@@ -205,6 +205,10 @@ export class Checkbox extends Widget {
this.domNode.setAttribute('aria-disabled', String(true));
}
setTitle(newTitle: string): void {
this.domNode.title = newTitle;
this.domNode.setAttribute('aria-label', newTitle);
}
}
export class SimpleCheckbox extends Widget {
@@ -5,6 +5,7 @@
@font-face {
font-family: "codicon";
font-display: block;
src: url("./codicon.ttf?5d4d76ab2ce5108968ad644d591a16a6") format("truetype");
}
Binary file not shown.
@@ -366,6 +366,7 @@ let SHADOW_ROOT_CSS = /* css */ `
@font-face {
font-family: "codicon";
font-display: block;
src: url("./codicon.ttf?5d4d76ab2ce5108968ad644d591a16a6") format("truetype");
}
+3 -5
View File
@@ -127,7 +127,6 @@
.monaco-dialog-box > .dialog-buttons-row {
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 1px;
overflow: hidden; /* buttons row should never overflow */
}
@@ -141,11 +140,10 @@
/** Dialog: Buttons */
.monaco-dialog-box > .dialog-buttons-row > .dialog-buttons {
display: flex;
width: 100%;
justify-content: flex-end;
overflow: hidden;
}
.monaco-dialog-box > .dialog-buttons-row > .dialog-buttons.centered {
justify-content: center;
margin-left: 67px; /* for long buttons, force align with text */
}
.monaco-dialog-box > .dialog-buttons-row > .dialog-buttons > .monaco-button {
-1
View File
@@ -196,7 +196,6 @@ export class Dialog extends Disposable {
const buttonBar = this.buttonBar = this._register(new ButtonBar(this.buttonsContainer));
const buttonMap = this.rearrangeButtons(this.buttons, this.options.cancelId);
this.buttonsContainer.classList.toggle('centered');
// Handle button clicks
buttonMap.forEach((entry, index) => {
+1 -1
View File
@@ -31,7 +31,7 @@ export class BaseDropdown extends ActionRunner {
private contents?: HTMLElement;
private visible: boolean | undefined;
private _onDidChangeVisibility = new Emitter<boolean>();
private _onDidChangeVisibility = this._register(new Emitter<boolean>());
readonly onDidChangeVisibility = this._onDidChangeVisibility.event;
constructor(container: HTMLElement, options: IBaseDropdownOptions) {
@@ -13,7 +13,8 @@ import { DropdownMenu, IActionProvider, IDropdownMenuOptions, ILabelRenderer } f
import { Action, IAction, IActionRunner } from 'vs/base/common/actions';
import { Codicon } from 'vs/base/common/codicons';
import { Emitter } from 'vs/base/common/event';
import { KeyCode, ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { KeyCode } from 'vs/base/common/keyCodes';
import { ResolvedKeybinding } from 'vs/base/common/keybindings';
import { IDisposable } from 'vs/base/common/lifecycle';
import 'vs/css!./dropdown';
@@ -31,6 +31,7 @@ export interface IFindInputOptions extends IFindInputStyles {
readonly appendWholeWordsLabel?: string;
readonly appendRegexLabel?: string;
readonly history?: string[];
readonly showHistoryHint?: () => boolean;
}
export interface IFindInputStyles extends IInputBoxStyles {
@@ -150,6 +151,7 @@ export class FindInput extends Widget {
inputValidationErrorForeground: this.inputValidationErrorForeground,
inputValidationErrorBorder: this.inputValidationErrorBorder,
history,
showHistoryHint: options.showHistoryHint,
flexibleHeight,
flexibleWidth,
flexibleMaxHeight
@@ -30,6 +30,7 @@ export interface IReplaceInputOptions extends IReplaceInputStyles {
readonly appendPreserveCaseLabel?: string;
readonly history?: string[];
readonly showHistoryHint?: () => boolean;
}
export interface IReplaceInputStyles extends IInputBoxStyles {
@@ -157,6 +158,7 @@ export class ReplaceInput extends Widget {
inputValidationErrorForeground: this.inputValidationErrorForeground,
inputValidationErrorBorder: this.inputValidationErrorBorder,
history,
showHistoryHint: options.showHistoryHint,
flexibleHeight,
flexibleWidth,
flexibleMaxHeight
+4 -4
View File
@@ -254,8 +254,8 @@ export class Grid<T extends IView = IView> extends Disposable {
this.gridview.style(styles);
}
layout(width: number, height: number): void {
this.gridview.layout(width, height);
layout(width: number, height: number, top: number = 0, left: number = 0): void {
this.gridview.layout(width, height, top, left);
this.didLayout = true;
}
@@ -556,8 +556,8 @@ export class SerializableGrid<T extends ISerializableView> extends Grid<T> {
};
}
override layout(width: number, height: number): void {
super.layout(width, height);
override layout(width: number, height: number, top: number = 0, left: number = 0): void {
super.layout(width, height, top, left);
if (this.initialLayoutContext) {
this.initialLayoutContext = false;
+3 -3
View File
@@ -980,11 +980,11 @@ export class GridView implements IDisposable {
this.root.style(styles);
}
layout(width: number, height: number): void {
layout(width: number, height: number, top: number = 0, left: number = 0): void {
this.firstLayoutController.isLayoutEnabled = true;
const [size, orthogonalSize] = this.root.orientation === Orientation.HORIZONTAL ? [height, width] : [width, height];
this.root.layout(size, 0, { orthogonalSize, absoluteOffset: 0, absoluteOrthogonalOffset: 0, absoluteSize: size, absoluteOrthogonalSize: orthogonalSize });
const [size, orthogonalSize, offset, orthogonalOffset] = this.root.orientation === Orientation.HORIZONTAL ? [height, width, top, left] : [width, height, left, top];
this.root.layout(size, offset, { orthogonalSize, absoluteOffset: offset, absoluteOrthogonalOffset: orthogonalOffset, absoluteSize: size, absoluteOrthogonalSize: orthogonalSize });
}
addView(view: IView, size: number | Sizing, location: number[]): void {
+2 -2
View File
@@ -20,7 +20,7 @@
display: none;
}
.monaco-hover .hover-contents {
.monaco-hover .hover-contents:not(.html-hover-contents) {
padding: 4px 8px;
}
@@ -137,7 +137,7 @@
}
/** Spans in markdown hovers need a margin-bottom to avoid looking cramped: https://github.com/microsoft/vscode/issues/101496 **/
.monaco-hover .markdown-hover .hover-contents:not(.code-hover-contents) span {
.monaco-hover .markdown-hover .hover-contents:not(.code-hover-contents):not(.html-hover-contents) span {
margin-bottom: 4px;
display: inline-block;
}
+1 -1
View File
@@ -63,7 +63,7 @@ export class HoverAction extends Disposable {
const label = dom.append(this.action, $('span'));
label.textContent = keybindingLabel ? `${actionOptions.label} (${keybindingLabel})` : actionOptions.label;
this._register(dom.addDisposableListener(this.actionContainer, dom.EventType.CLICK, e => {
this._register(dom.addDisposableListener(this.actionContainer, dom.EventType.MOUSE_DOWN, e => {
e.stopPropagation();
e.preventDefault();
actionOptions.run(this.actionContainer);
@@ -17,10 +17,16 @@ export interface IHoverDelegateOptions {
target: IHoverDelegateTarget | HTMLElement;
hoverPosition?: HoverPosition;
showPointer?: boolean;
skipFadeInAnimation?: boolean;
}
export interface IHoverDelegate {
showHover(options: IHoverDelegateOptions): IDisposable | undefined;
showHover(options: IHoverDelegateOptions, focus?: boolean): IHoverWidget | undefined;
onDidHideHover?: () => void;
delay: number;
placement?: 'mouse' | 'element';
}
export interface IHoverWidget extends IDisposable {
readonly isDisposed: boolean;
}
@@ -23,7 +23,7 @@ export interface IIconLabelCreationOptions {
}
export interface IIconLabelMarkdownString {
markdown: IMarkdownString | string | undefined | ((token: CancellationToken) => Promise<IMarkdownString | string | undefined>);
markdown: IMarkdownString | string | HTMLElement | undefined | ((token: CancellationToken) => Promise<IMarkdownString | string | undefined>);
markdownNotSupportedFallback: string | undefined;
}
@@ -147,7 +147,7 @@ export class IconLabel extends Disposable {
}
this.domNode.className = classes.join(' ');
this.setupHover(this.labelContainer, options?.title);
this.setupHover(options?.descriptionTitle ? this.labelContainer : this.element, options?.title);
this.nameNode.setLabel(label, options);
@@ -5,16 +5,15 @@
import * as dom from 'vs/base/browser/dom';
import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget';
import { IHoverDelegate, IHoverDelegateTarget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate';
import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget, IHoverWidget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate';
import { IIconLabelMarkdownString } from 'vs/base/browser/ui/iconLabel/iconLabel';
import { RunOnceScheduler } from 'vs/base/common/async';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { TimeoutTimer } from 'vs/base/common/async';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { IMarkdownString, isMarkdownString } from 'vs/base/common/htmlContent';
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { isFunction, isString } from 'vs/base/common/types';
import { localize } from 'vs/nls';
export function setupNativeHover(htmlElement: HTMLElement, tooltip: string | IIconLabelMarkdownString | undefined): void {
if (isString(tooltip)) {
htmlElement.title = tooltip;
@@ -25,107 +24,181 @@ export function setupNativeHover(htmlElement: HTMLElement, tooltip: string | IIc
}
}
export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, markdownTooltip: string | IIconLabelMarkdownString | undefined): IDisposable | undefined {
if (!markdownTooltip) {
return undefined;
export interface ICustomHover extends IDisposable {
/**
* Allows to programmatically open the hover.
*/
show(focus?: boolean): void;
/**
* Allows to programmatically hide the hover.
*/
hide(): void;
/**
* Updates the contents of the hover.
*/
update(tooltip: string | IIconLabelMarkdownString | HTMLElement): void;
}
type MarkdownTooltipContent = string | IIconLabelMarkdownString | HTMLElement | undefined;
type ResolvedMarkdownTooltipContent = IMarkdownString | string | HTMLElement | undefined;
class UpdatableHoverWidget implements IDisposable {
private _hoverWidget: IHoverWidget | undefined;
private _cancellationTokenSource: CancellationTokenSource | undefined;
constructor(private hoverDelegate: IHoverDelegate, private target: IHoverDelegateTarget | HTMLElement, private fadeInAnimation: boolean) {
}
const tooltip = getTooltipForCustom(markdownTooltip);
async update(markdownTooltip: MarkdownTooltipContent, focus?: boolean): Promise<void> {
if (this._cancellationTokenSource) {
// there's an computation ongoing, cancel it
this._cancellationTokenSource.dispose(true);
this._cancellationTokenSource = undefined;
}
if (this.isDisposed) {
return;
}
let resolvedContent;
if (markdownTooltip === undefined || isString(markdownTooltip) || markdownTooltip instanceof HTMLElement) {
resolvedContent = markdownTooltip;
} else if (!isFunction(markdownTooltip.markdown)) {
resolvedContent = markdownTooltip.markdown ?? markdownTooltip.markdownNotSupportedFallback;
} else {
// compute the content, potentially long-running
// show 'Loading' if no hover is up yet
if (!this._hoverWidget) {
this.show(localize('iconLabel.loading', "Loading..."), focus);
}
// compute the content
this._cancellationTokenSource = new CancellationTokenSource();
const token = this._cancellationTokenSource.token;
resolvedContent = await markdownTooltip.markdown(token);
if (this.isDisposed || token.isCancellationRequested) {
// either the widget has been closed in the meantime
// or there has been a new call to `update`
return;
}
}
this.show(resolvedContent, focus);
}
private show(content: ResolvedMarkdownTooltipContent, focus?: boolean): void {
const oldHoverWidget = this._hoverWidget;
if (this.hasContent(content)) {
const hoverOptions: IHoverDelegateOptions = {
content,
target: this.target,
showPointer: this.hoverDelegate.placement === 'element',
hoverPosition: HoverPosition.BELOW,
skipFadeInAnimation: !this.fadeInAnimation || !!oldHoverWidget // do not fade in if the hover is already showing
};
this._hoverWidget = this.hoverDelegate.showHover(hoverOptions, focus);
}
oldHoverWidget?.dispose();
}
private hasContent(content: ResolvedMarkdownTooltipContent): content is NonNullable<ResolvedMarkdownTooltipContent> {
if (!content) {
return false;
}
if (isMarkdownString(content)) {
return !!content.value;
}
return true;
}
get isDisposed() {
return this._hoverWidget?.isDisposed;
}
dispose(): void {
this._hoverWidget?.dispose();
this._cancellationTokenSource?.dispose(true);
this._cancellationTokenSource = undefined;
}
}
export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, markdownTooltip: string | IIconLabelMarkdownString | HTMLElement): ICustomHover {
let hoverPreparation: IDisposable | undefined;
let hoverWidget: IDisposable | undefined;
let hoverWidget: UpdatableHoverWidget | undefined;
const mouseEnter = (e: MouseEvent) => {
const hideHover = (disposeWidget: boolean, disposePreparation: boolean) => {
if (disposeWidget) {
hoverWidget?.dispose();
hoverWidget = undefined;
}
if (disposePreparation) {
hoverPreparation?.dispose();
hoverPreparation = undefined;
}
hoverDelegate.onDidHideHover?.();
};
const triggerShowHover = (delay: number, focus?: boolean, target?: IHoverDelegateTarget) => {
return new TimeoutTimer(async () => {
if (!hoverWidget || hoverWidget.isDisposed) {
hoverWidget = new UpdatableHoverWidget(hoverDelegate, target || htmlElement, delay > 0);
await hoverWidget.update(markdownTooltip, focus);
}
}, delay);
};
const onMouseOver = () => {
if (hoverPreparation) {
return;
}
const tokenSource = new CancellationTokenSource();
const toDispose: DisposableStore = new DisposableStore();
const mouseLeaveOrDown = (e: MouseEvent) => {
const isMouseDown = e.type === dom.EventType.MOUSE_DOWN;
if (isMouseDown) {
hoverWidget?.dispose();
hoverWidget = undefined;
}
if (isMouseDown || (<any>e).fromElement === htmlElement) {
hoverPreparation?.dispose();
hoverPreparation = undefined;
}
};
const mouseLeaveDomListener = dom.addDisposableListener(htmlElement, dom.EventType.MOUSE_LEAVE, mouseLeaveOrDown, true);
const mouseDownDownListener = dom.addDisposableListener(htmlElement, dom.EventType.MOUSE_DOWN, mouseLeaveOrDown, true);
const onMouseLeave = (e: MouseEvent) => hideHover(false, (<any>e).fromElement === htmlElement);
toDispose.add(dom.addDisposableListener(htmlElement, dom.EventType.MOUSE_LEAVE, onMouseLeave, true));
const onMouseDown = () => hideHover(true, true);
toDispose.add(dom.addDisposableListener(htmlElement, dom.EventType.MOUSE_DOWN, onMouseDown, true));
const target: IHoverDelegateTarget = {
targetElements: [htmlElement],
dispose: () => { }
};
let mouseMoveDomListener: IDisposable | undefined;
if (hoverDelegate.placement === undefined || hoverDelegate.placement === 'mouse') {
const mouseMove = (e: MouseEvent) => target.x = e.x + 10;
mouseMoveDomListener = dom.addDisposableListener(htmlElement, dom.EventType.MOUSE_MOVE, mouseMove, true);
// track the mouse position
const onMouseMove = (e: MouseEvent) => target.x = e.x + 10;
toDispose.add(dom.addDisposableListener(htmlElement, dom.EventType.MOUSE_MOVE, onMouseMove, true));
}
toDispose.add(triggerShowHover(hoverDelegate.delay, false, target));
const showHover = async () => {
if (hoverPreparation) {
const hoverOptions = {
content: localize('iconLabel.loading', "Loading..."),
target,
hoverPosition: HoverPosition.BELOW
};
hoverWidget?.dispose();
hoverWidget = hoverDelegate.showHover(hoverOptions);
const resolvedTooltip = (await tooltip(tokenSource.token)) ?? (!isString(markdownTooltip) ? markdownTooltip.markdownNotSupportedFallback : undefined);
hoverWidget?.dispose();
hoverWidget = undefined;
// awaiting the tooltip could take a while. Make sure we're still preparing to hover.
if (resolvedTooltip && hoverPreparation) {
const hoverOptions = {
content: resolvedTooltip,
target,
showPointer: hoverDelegate.placement === 'element',
hoverPosition: HoverPosition.BELOW
};
hoverWidget = hoverDelegate.showHover(hoverOptions);
}
}
mouseMoveDomListener?.dispose();
};
const timeout = new RunOnceScheduler(showHover, hoverDelegate.delay);
timeout.schedule();
hoverPreparation = toDisposable(() => {
timeout.dispose();
mouseMoveDomListener?.dispose();
mouseDownDownListener.dispose();
mouseLeaveDomListener.dispose();
tokenSource.dispose(true);
});
hoverPreparation = toDispose;
};
const mouseOverDomEmitter = dom.addDisposableListener(htmlElement, dom.EventType.MOUSE_OVER, mouseEnter, true);
return toDisposable(() => {
mouseOverDomEmitter.dispose();
hoverPreparation?.dispose();
hoverWidget?.dispose();
});
}
function getTooltipForCustom(markdownTooltip: string | IIconLabelMarkdownString): (token: CancellationToken) => Promise<string | IMarkdownString | undefined> {
if (isString(markdownTooltip)) {
return async () => markdownTooltip;
} else if (isFunction(markdownTooltip.markdown)) {
return markdownTooltip.markdown;
} else {
const markdown = markdownTooltip.markdown;
return async () => markdown;
}
const mouseOverDomEmitter = dom.addDisposableListener(htmlElement, dom.EventType.MOUSE_OVER, onMouseOver, true);
const hover: ICustomHover = {
show: focus => {
hideHover(false, true); // terminate a ongoing mouse over preparation
triggerShowHover(0, focus); // show hover immediately
},
hide: () => {
hideHover(true, true);
},
update: async newTooltip => {
markdownTooltip = newTooltip;
await hoverWidget?.update(markdownTooltip);
},
dispose: () => {
mouseOverDomEmitter.dispose();
hideHover(true, true);
}
};
return hover;
}
@@ -87,7 +87,7 @@
opacity: 0.75;
font-size: 90%;
font-weight: 600;
margin: 0 16px 0 5px;
margin: auto 16px 0 5px; /* https://github.com/microsoft/vscode/issues/113223 */
text-align: center;
}
+62 -2
View File
@@ -99,11 +99,11 @@ export const defaultOpts = {
export class InputBox extends Widget {
private contextViewProvider?: IContextViewProvider;
element: HTMLElement;
private input: HTMLInputElement;
protected input: HTMLInputElement;
private actionbar?: ActionBar;
private options: IInputOptions;
private message: IMessage | null;
private placeholder: string;
protected placeholder: string;
private tooltip: string;
private ariaLabel: string;
private validation?: IInputValidator;
@@ -667,15 +667,75 @@ export class InputBox extends Widget {
export interface IHistoryInputOptions extends IInputOptions {
history: string[];
readonly showHistoryHint?: () => boolean;
}
export class HistoryInputBox extends InputBox implements IHistoryNavigationWidget {
private readonly history: HistoryNavigator<string>;
private observer: MutationObserver | undefined;
constructor(container: HTMLElement, contextViewProvider: IContextViewProvider | undefined, options: IHistoryInputOptions) {
const NLS_PLACEHOLDER_HISTORY_HINT = nls.localize({ key: 'history.inputbox.hint', comment: ['Text will be prefixed with \u21C5 plus a single space, then used as a hint where input field keeps history'] }, "for history");
const NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX = ` or \u21C5 ${NLS_PLACEHOLDER_HISTORY_HINT}`;
const NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS = ` (\u21C5 ${NLS_PLACEHOLDER_HISTORY_HINT})`;
super(container, contextViewProvider, options);
this.history = new HistoryNavigator<string>(options.history, 100);
// Function to append the history suffix to the placeholder if necessary
const addSuffix = () => {
if (options.showHistoryHint && options.showHistoryHint() && !this.placeholder.endsWith(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX) && !this.placeholder.endsWith(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS) && this.history.getHistory().length) {
const suffix = this.placeholder.endsWith(')') ? NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX : NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS;
const suffixedPlaceholder = this.placeholder + suffix;
if (options.showPlaceholderOnFocus && document.activeElement !== this.input) {
this.placeholder = suffixedPlaceholder;
}
else {
this.setPlaceHolder(suffixedPlaceholder);
}
}
};
// Spot the change to the textarea class attribute which occurs when it changes between non-empty and empty,
// and add the history suffix to the placeholder if not yet present
this.observer = new MutationObserver((mutationList: MutationRecord[], observer: MutationObserver) => {
mutationList.forEach((mutation: MutationRecord) => {
if (!mutation.target.textContent) {
addSuffix();
}
});
});
this.observer.observe(this.input, { attributeFilter: ['class'] });
this.onfocus(this.input, () => addSuffix());
this.onblur(this.input, () => {
const resetPlaceholder = (historyHint: string) => {
if (!this.placeholder.endsWith(historyHint)) {
return false;
}
else {
const revertedPlaceholder = this.placeholder.slice(0, this.placeholder.length - historyHint.length);
if (options.showPlaceholderOnFocus) {
this.placeholder = revertedPlaceholder;
}
else {
this.setPlaceHolder(revertedPlaceholder);
}
return true;
}
};
if (!resetPlaceholder(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS)) {
resetPlaceholder(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX);
}
});
}
override dispose() {
super.dispose();
if (this.observer) {
this.observer.disconnect();
this.observer = undefined;
}
}
public addToHistory(): void {
@@ -6,7 +6,7 @@
import * as dom from 'vs/base/browser/dom';
import { Color } from 'vs/base/common/color';
import { UILabelProvider } from 'vs/base/common/keybindingLabels';
import { ResolvedKeybinding, ResolvedKeybindingPart } from 'vs/base/common/keyCodes';
import { ResolvedKeybinding, ResolvedKeybindingPart } from 'vs/base/common/keybindings';
import { equals } from 'vs/base/common/objects';
import { OperatingSystem } from 'vs/base/common/platform';
import { IThemable } from 'vs/base/common/styler';
+1
View File
@@ -11,6 +11,7 @@ export interface IListVirtualDelegate<T> {
getHeight(element: T): number;
getTemplateId(element: T): string;
hasDynamicHeight?(element: T): boolean;
getDynamicHeight?(element: T): number | null;
setDynamicHeight?(element: T, height: number): void;
}
+27 -4
View File
@@ -389,12 +389,24 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
this.scrollableElement.triggerScrollFromMouseWheelEvent(browserEvent);
}
updateElementHeight(index: number, size: number, anchorIndex: number | null): void {
updateElementHeight(index: number, size: number | undefined, anchorIndex: number | null): void {
if (index < 0 || index >= this.items.length) {
return;
}
if (this.items[index].size === size) {
const originalSize = this.items[index].size;
if (typeof size === 'undefined') {
if (!this.supportDynamicHeights) {
console.warn('Dynamic heights not supported');
return;
}
this.items[index].lastDynamicHeightWidth = undefined;
size = originalSize + this.probeDynamicHeight(index);
}
if (originalSize === size) {
return;
}
@@ -404,12 +416,12 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
if (index < lastRenderRange.start) {
// do not scroll the viewport if resized element is out of viewport
heightDiff = size - this.items[index].size;
heightDiff = size - originalSize;
} else {
if (anchorIndex !== null && anchorIndex > index && anchorIndex <= lastRenderRange.end) {
// anchor in viewport
// resized element in viewport and above the anchor
heightDiff = size - this.items[index].size;
heightDiff = size - originalSize;
} else {
heightDiff = 0;
}
@@ -828,6 +840,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
item.row!.domNode.setAttribute('data-index', `${index}`);
item.row!.domNode.setAttribute('data-last-element', index === this.length - 1 ? 'true' : 'false');
item.row!.domNode.setAttribute('data-parity', index % 2 === 0 ? 'even' : 'odd');
item.row!.domNode.setAttribute('aria-setsize', String(this.accessibilityProvider.getSetSize(item.element, index, this.length)));
item.row!.domNode.setAttribute('aria-posinset', String(this.accessibilityProvider.getPosInSet(item.element, index)));
item.row!.domNode.setAttribute('id', this.getElementDomId(index));
@@ -1305,6 +1318,16 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
private probeDynamicHeight(index: number): number {
const item = this.items[index];
if (!!this.virtualDelegate.getDynamicHeight) {
const newSize = this.virtualDelegate.getDynamicHeight(item.element);
if (newSize !== null) {
const size = item.size;
item.size = newSize;
item.lastDynamicHeightWidth = this.renderWidth;
return newSize - size;
}
}
if (!item.hasDynamicHeight || item.lastDynamicHeightWidth === this.renderWidth) {
return 0;
}
+21 -11
View File
@@ -283,7 +283,7 @@ class KeyboardController<T> implements IDisposable {
this.onKeyDown.filter(e => e.keyCode === KeyCode.Escape).on(this.onEscape, this, this.disposables);
if (options.multipleSelectionSupport !== false) {
this.onKeyDown.filter(e => (platform.isMacintosh ? e.metaKey : e.ctrlKey) && e.keyCode === KeyCode.KEY_A).on(this.onCtrlA, this, this.multipleSelectionDisposables);
this.onKeyDown.filter(e => (platform.isMacintosh ? e.metaKey : e.ctrlKey) && e.keyCode === KeyCode.KeyA).on(this.onCtrlA, this, this.multipleSelectionDisposables);
}
}
@@ -292,7 +292,7 @@ class KeyboardController<T> implements IDisposable {
this.multipleSelectionDisposables.clear();
if (optionsUpdate.multipleSelectionSupport) {
this.onKeyDown.filter(e => (platform.isMacintosh ? e.metaKey : e.ctrlKey) && e.keyCode === KeyCode.KEY_A).on(this.onCtrlA, this, this.multipleSelectionDisposables);
this.onKeyDown.filter(e => (platform.isMacintosh ? e.metaKey : e.ctrlKey) && e.keyCode === KeyCode.KeyA).on(this.onCtrlA, this, this.multipleSelectionDisposables);
}
}
}
@@ -307,7 +307,9 @@ class KeyboardController<T> implements IDisposable {
e.preventDefault();
e.stopPropagation();
this.list.focusPrevious(1, false, e.browserEvent);
this.list.reveal(this.list.getFocus()[0]);
const el = this.list.getFocus()[0];
this.list.setAnchor(el);
this.list.reveal(el);
this.view.domNode.focus();
}
@@ -315,7 +317,9 @@ class KeyboardController<T> implements IDisposable {
e.preventDefault();
e.stopPropagation();
this.list.focusNext(1, false, e.browserEvent);
this.list.reveal(this.list.getFocus()[0]);
const el = this.list.getFocus()[0];
this.list.setAnchor(el);
this.list.reveal(el);
this.view.domNode.focus();
}
@@ -323,7 +327,9 @@ class KeyboardController<T> implements IDisposable {
e.preventDefault();
e.stopPropagation();
this.list.focusPreviousPage(e.browserEvent);
this.list.reveal(this.list.getFocus()[0]);
const el = this.list.getFocus()[0];
this.list.setAnchor(el);
this.list.reveal(el);
this.view.domNode.focus();
}
@@ -331,7 +337,9 @@ class KeyboardController<T> implements IDisposable {
e.preventDefault();
e.stopPropagation();
this.list.focusNextPage(e.browserEvent);
this.list.reveal(this.list.getFocus()[0]);
const el = this.list.getFocus()[0];
this.list.setAnchor(el);
this.list.reveal(el);
this.view.domNode.focus();
}
@@ -339,6 +347,7 @@ class KeyboardController<T> implements IDisposable {
e.preventDefault();
e.stopPropagation();
this.list.setSelection(range(this.list.length), e.browserEvent);
this.list.setAnchor(undefined);
this.view.domNode.focus();
}
@@ -347,6 +356,7 @@ class KeyboardController<T> implements IDisposable {
e.preventDefault();
e.stopPropagation();
this.list.setSelection([], e.browserEvent);
this.list.setAnchor(undefined);
this.view.domNode.focus();
}
}
@@ -368,10 +378,10 @@ export const DefaultKeyboardNavigationDelegate = new class implements IKeyboardN
return false;
}
return (event.keyCode >= KeyCode.KEY_A && event.keyCode <= KeyCode.KEY_Z)
|| (event.keyCode >= KeyCode.KEY_0 && event.keyCode <= KeyCode.KEY_9)
|| (event.keyCode >= KeyCode.NUMPAD_0 && event.keyCode <= KeyCode.NUMPAD_9)
|| (event.keyCode >= KeyCode.US_SEMICOLON && event.keyCode <= KeyCode.US_QUOTE);
return (event.keyCode >= KeyCode.KeyA && event.keyCode <= KeyCode.KeyZ)
|| (event.keyCode >= KeyCode.Digit0 && event.keyCode <= KeyCode.Digit9)
|| (event.keyCode >= KeyCode.Numpad0 && event.keyCode <= KeyCode.Numpad9)
|| (event.keyCode >= KeyCode.Semicolon && event.keyCode <= KeyCode.Quote);
}
};
@@ -424,7 +434,7 @@ class TypeLabelController<T> implements IDisposable {
.filter(() => this.automaticKeyboardNavigation || this.triggered)
.map(event => new StandardKeyboardEvent(event))
.filter(e => this.delegate.mightProducePrintableCharacter(e))
.forEach(e => { e.stopPropagation(); e.preventDefault(); })
.forEach(e => e.preventDefault())
.map(event => event.browserEvent.key)
.event;
+44 -8
View File
@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { isFirefox } from 'vs/base/browser/browser';
import { EventType as TouchEventType, Gesture } from 'vs/base/browser/touch';
import { $, addDisposableListener, append, clearNode, createStyleSheet, Dimension, EventHelper, EventLike, EventType, getActiveElement, IDomNodePagePosition, isAncestor, isInShadowDOM } from 'vs/base/browser/dom';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
@@ -18,7 +19,8 @@ import { Codicon, registerCodicon } from 'vs/base/common/codicons';
import { Color } from 'vs/base/common/color';
import { Event } from 'vs/base/common/event';
import { stripIcons } from 'vs/base/common/iconLabels';
import { KeyCode, ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { KeyCode } from 'vs/base/common/keyCodes';
import { ResolvedKeybinding } from 'vs/base/common/keybindings';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { isLinux, isMacintosh } from 'vs/base/common/platform';
import { ScrollbarVisibility, ScrollEvent } from 'vs/base/common/scrollable';
@@ -100,6 +102,8 @@ export class Menu extends ActionBar {
this.initializeStyleSheet(container);
this._register(Gesture.addTarget(menuElement));
addDisposableListener(menuElement, EventType.KEY_DOWN, (e) => {
const event = new StandardKeyboardEvent(e);
@@ -182,6 +186,29 @@ export class Menu extends ActionBar {
}
}));
// Support touch on actions list to focus items (needed for submenus)
this._register(Gesture.addTarget(this.actionsList));
this._register(addDisposableListener(this.actionsList, TouchEventType.Tap, e => {
let target = e.initialTarget as HTMLElement;
if (!target || !isAncestor(target, this.actionsList) || target === this.actionsList) {
return;
}
while (target.parentElement !== this.actionsList && target.parentElement !== null) {
target = target.parentElement;
}
if (target.classList.contains('action-item')) {
const lastFocusedItem = this.focusedItem;
this.setFocusedItem(target);
if (lastFocusedItem !== this.focusedItem) {
this.updateFocus();
}
}
}));
let parentData: ISubMenuData = {
parent: this
};
@@ -201,6 +228,14 @@ export class Menu extends ActionBar {
const scrollElement = this.scrollableElement.getDomNode();
scrollElement.style.position = '';
// Support scroll on menu drag
this._register(addDisposableListener(menuElement, TouchEventType.Change, e => {
EventHelper.stop(e, true);
const scrollTop = this.scrollableElement.getScrollPosition().scrollTop;
this.scrollableElement.setScrollPosition({ scrollTop: scrollTop - e.translationY });
}));
this._register(addDisposableListener(scrollElement, EventType.MOUSE_UP, e => {
// Absorb clicks in menu dead space https://github.com/microsoft/vscode/issues/63575
// We do this on the scroll element so the scroll bar doesn't dismiss the menu either
@@ -644,14 +679,14 @@ class BaseMenuActionViewItem extends BaseActionViewItem {
return;
}
if (this.getAction().checked) {
this.item.classList.add('checked');
const checked = this.getAction().checked;
this.item.classList.toggle('checked', !!checked);
if (checked !== undefined) {
this.item.setAttribute('role', 'menuitemcheckbox');
this.item.setAttribute('aria-checked', 'true');
this.item.setAttribute('aria-checked', checked ? 'true' : 'false');
} else {
this.item.classList.remove('checked');
this.item.setAttribute('role', 'menuitem');
this.item.setAttribute('aria-checked', 'false');
this.item.setAttribute('aria-checked', '');
}
}
@@ -883,8 +918,9 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
const viewBox = this.submenuContainer.getBoundingClientRect();
const { top, left } = this.calculateSubmenuMenuLayout(new Dimension(window.innerWidth, window.innerHeight), Dimension.lift(viewBox), entryBoxUpdated, this.expandDirection);
this.submenuContainer.style.left = `${left}px`;
this.submenuContainer.style.top = `${top}px`;
// subtract offsets caused by transform parent
this.submenuContainer.style.left = `${left - viewBox.left}px`;
this.submenuContainer.style.top = `${top - viewBox.top}px`;
this.submenuDisposables.add(addDisposableListener(this.submenuContainer, EventType.KEY_UP, e => {
let event = new StandardKeyboardEvent(e);
+167 -149
View File
@@ -14,10 +14,10 @@ import { asArray } from 'vs/base/common/arrays';
import { RunOnceScheduler } from 'vs/base/common/async';
import { Codicon, registerCodicon } from 'vs/base/common/codicons';
import { Emitter, Event } from 'vs/base/common/event';
import { KeyCode, KeyMod, ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { KeyCode, KeyMod, ScanCode, ScanCodeUtils } from 'vs/base/common/keyCodes';
import { ResolvedKeybinding } from 'vs/base/common/keybindings';
import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle';
import { isMacintosh } from 'vs/base/common/platform';
import { ScanCode, ScanCodeUtils } from 'vs/base/common/scanCode';
import * as strings from 'vs/base/common/strings';
import { withNullAsUndefined } from 'vs/base/common/types';
import 'vs/css!./menubar';
@@ -42,6 +42,11 @@ export interface MenuBarMenu {
label: string;
}
interface MenuBarMenuWithElements extends MenuBarMenu {
titleElement?: HTMLElement;
buttonElement?: HTMLElement;
}
enum MenubarState {
HIDDEN,
VISIBLE,
@@ -53,19 +58,9 @@ export class MenuBar extends Disposable {
static readonly OVERFLOW_INDEX: number = -1;
private menuCache: {
buttonElement: HTMLElement;
titleElement: HTMLElement;
label: string;
actions?: IAction[];
}[];
private menus: MenuBarMenuWithElements[];
private overflowMenu!: {
buttonElement: HTMLElement;
titleElement: HTMLElement;
label: string;
actions?: IAction[];
};
private overflowMenu!: MenuBarMenuWithElements & { titleElement: HTMLElement; buttonElement: HTMLElement };
private focusedMenu: {
index: number;
@@ -98,11 +93,11 @@ export class MenuBar extends Disposable {
super();
this.container.setAttribute('role', 'menubar');
if (this.options.compactMode !== undefined) {
if (this.isCompact) {
this.container.classList.add('compact');
}
this.menuCache = [];
this.menus = [];
this.mnemonics = new Map<string, number>();
this._focusState = MenubarState.VISIBLE;
@@ -126,7 +121,7 @@ export class MenuBar extends Disposable {
let eventHandled = true;
const key = !!e.key ? e.key.toLocaleLowerCase() : '';
const tabNav = isMacintosh && this.options.compactMode === undefined;
const tabNav = isMacintosh && !this.isCompact;
if (event.equals(KeyCode.LeftArrow) || (tabNav && event.equals(KeyCode.Tab | KeyMod.Shift))) {
this.focusPrevious();
@@ -142,7 +137,7 @@ export class MenuBar extends Disposable {
}
// Never allow default tab behavior when not compact
if (this.options.compactMode === undefined && (event.equals(KeyCode.Tab | KeyMod.Shift) || event.equals(KeyCode.Tab))) {
if (!this.isCompact && (event.equals(KeyCode.Tab | KeyMod.Shift) || event.equals(KeyCode.Tab))) {
event.preventDefault();
}
@@ -207,116 +202,120 @@ export class MenuBar extends Disposable {
const menus: MenuBarMenu[] = asArray(arg);
menus.forEach((menuBarMenu) => {
const menuIndex = this.menuCache.length;
const menuIndex = this.menus.length;
const cleanMenuLabel = cleanMnemonic(menuBarMenu.label);
const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': -1, 'aria-label': cleanMenuLabel, 'aria-haspopup': true });
const titleElement = $('div.menubar-menu-title', { 'role': 'none', 'aria-hidden': true });
buttonElement.appendChild(titleElement);
this.container.insertBefore(buttonElement, this.overflowMenu.buttonElement);
let mnemonicMatches = MENU_MNEMONIC_REGEX.exec(menuBarMenu.label);
// Register mnemonics
if (mnemonicMatches) {
let mnemonic = !!mnemonicMatches[1] ? mnemonicMatches[1] : mnemonicMatches[3];
this.registerMnemonic(this.menuCache.length, mnemonic);
this.registerMnemonic(this.menus.length, mnemonic);
}
this.updateLabels(titleElement, buttonElement, menuBarMenu.label);
if (this.isCompact) {
this.menus.push(menuBarMenu);
} else {
const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': -1, 'aria-label': cleanMenuLabel, 'aria-haspopup': true });
const titleElement = $('div.menubar-menu-title', { 'role': 'none', 'aria-hidden': true });
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.KEY_UP, (e) => {
let event = new StandardKeyboardEvent(e as KeyboardEvent);
let eventHandled = true;
buttonElement.appendChild(titleElement);
this.container.insertBefore(buttonElement, this.overflowMenu.buttonElement);
if ((event.equals(KeyCode.DownArrow) || event.equals(KeyCode.Enter)) && !this.isOpen) {
this.focusedMenu = { index: menuIndex };
this.openedViaKeyboard = true;
this.focusState = MenubarState.OPEN;
} else {
eventHandled = false;
}
this.updateLabels(titleElement, buttonElement, menuBarMenu.label);
if (eventHandled) {
event.preventDefault();
event.stopPropagation();
}
}));
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.KEY_UP, (e) => {
let event = new StandardKeyboardEvent(e as KeyboardEvent);
let eventHandled = true;
this._register(Gesture.addTarget(buttonElement));
this._register(DOM.addDisposableListener(buttonElement, EventType.Tap, (e: GestureEvent) => {
// Ignore this touch if the menu is touched
if (this.isOpen && this.focusedMenu && this.focusedMenu.holder && DOM.isAncestor(e.initialTarget as HTMLElement, this.focusedMenu.holder)) {
return;
}
this.ignoreNextMouseUp = false;
this.onMenuTriggered(menuIndex, true);
e.preventDefault();
e.stopPropagation();
}));
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => {
// Ignore non-left-click
const mouseEvent = new StandardMouseEvent(e);
if (!mouseEvent.leftButton) {
e.preventDefault();
return;
}
if (!this.isOpen) {
// Open the menu with mouse down and ignore the following mouse up event
this.ignoreNextMouseUp = true;
this.onMenuTriggered(menuIndex, true);
} else {
this.ignoreNextMouseUp = false;
}
e.preventDefault();
e.stopPropagation();
}));
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_UP, (e) => {
if (e.defaultPrevented) {
return;
}
if (!this.ignoreNextMouseUp) {
if (this.isFocused) {
this.onMenuTriggered(menuIndex, true);
if ((event.equals(KeyCode.DownArrow) || event.equals(KeyCode.Enter)) && !this.isOpen) {
this.focusedMenu = { index: menuIndex };
this.openedViaKeyboard = true;
this.focusState = MenubarState.OPEN;
} else {
eventHandled = false;
}
} else {
if (eventHandled) {
event.preventDefault();
event.stopPropagation();
}
}));
this._register(Gesture.addTarget(buttonElement));
this._register(DOM.addDisposableListener(buttonElement, EventType.Tap, (e: GestureEvent) => {
// Ignore this touch if the menu is touched
if (this.isOpen && this.focusedMenu && this.focusedMenu.holder && DOM.isAncestor(e.initialTarget as HTMLElement, this.focusedMenu.holder)) {
return;
}
this.ignoreNextMouseUp = false;
}
}));
this.onMenuTriggered(menuIndex, true);
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_ENTER, () => {
if (this.isOpen && !this.isCurrentMenu(menuIndex)) {
this.menuCache[menuIndex].buttonElement.focus();
this.cleanupCustomMenu();
this.showCustomMenu(menuIndex, false);
} else if (this.isFocused && !this.isOpen) {
this.focusedMenu = { index: menuIndex };
buttonElement.focus();
}
}));
e.preventDefault();
e.stopPropagation();
}));
this.menuCache.push({
label: menuBarMenu.label,
actions: menuBarMenu.actions,
buttonElement: buttonElement,
titleElement: titleElement
});
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => {
// Ignore non-left-click
const mouseEvent = new StandardMouseEvent(e);
if (!mouseEvent.leftButton) {
e.preventDefault();
return;
}
if (!this.isOpen) {
// Open the menu with mouse down and ignore the following mouse up event
this.ignoreNextMouseUp = true;
this.onMenuTriggered(menuIndex, true);
} else {
this.ignoreNextMouseUp = false;
}
e.preventDefault();
e.stopPropagation();
}));
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_UP, (e) => {
if (e.defaultPrevented) {
return;
}
if (!this.ignoreNextMouseUp) {
if (this.isFocused) {
this.onMenuTriggered(menuIndex, true);
}
} else {
this.ignoreNextMouseUp = false;
}
}));
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_ENTER, () => {
if (this.isOpen && !this.isCurrentMenu(menuIndex)) {
buttonElement.focus();
this.cleanupCustomMenu();
this.showCustomMenu(menuIndex, false);
} else if (this.isFocused && !this.isOpen) {
this.focusedMenu = { index: menuIndex };
buttonElement.focus();
}
}));
this.menus.push({
label: menuBarMenu.label,
actions: menuBarMenu.actions,
buttonElement: buttonElement,
titleElement: titleElement
});
}
});
}
createOverflowMenu(): void {
const label = this.options.compactMode !== undefined ? nls.localize('mAppMenu', 'Application Menu') : nls.localize('mMore', 'More');
const title = this.options.compactMode !== undefined ? label : undefined;
const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': this.options.compactMode !== undefined ? 0 : -1, 'aria-label': label, 'title': title, 'aria-haspopup': true });
const label = this.isCompact ? nls.localize('mAppMenu', 'Application Menu') : nls.localize('mMore', 'More');
const title = this.isCompact ? label : undefined;
const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': this.isCompact ? 0 : -1, 'aria-label': label, 'title': title, 'aria-haspopup': true });
const titleElement = $('div.menubar-menu-title.toolbar-toggle-more' + menuBarMoreIcon.cssSelector, { 'role': 'none', 'aria-hidden': true });
buttonElement.appendChild(titleElement);
@@ -328,7 +327,7 @@ export class MenuBar extends Disposable {
let eventHandled = true;
const triggerKeys = [KeyCode.Enter];
if (this.options.compactMode === undefined) {
if (!this.isCompact) {
triggerKeys.push(KeyCode.DownArrow);
} else {
triggerKeys.push(KeyCode.Space);
@@ -411,12 +410,13 @@ export class MenuBar extends Disposable {
this.overflowMenu = {
buttonElement: buttonElement,
titleElement: titleElement,
label: 'More'
label: 'More',
actions: []
};
}
updateMenu(menu: MenuBarMenu): void {
const menuToUpdate = this.menuCache.filter(menuBarMenu => menuBarMenu.label === menu.label);
const menuToUpdate = this.menus.filter(menuBarMenu => menuBarMenu.label === menu.label);
if (menuToUpdate && menuToUpdate.length) {
menuToUpdate[0].actions = menu.actions;
}
@@ -425,9 +425,9 @@ export class MenuBar extends Disposable {
override dispose(): void {
super.dispose();
this.menuCache.forEach(menuBarMenu => {
menuBarMenu.titleElement.remove();
menuBarMenu.buttonElement.remove();
this.menus.forEach(menuBarMenu => {
menuBarMenu.titleElement?.remove();
menuBarMenu.buttonElement?.remove();
});
this.overflowMenu.titleElement.remove();
@@ -442,9 +442,9 @@ export class MenuBar extends Disposable {
}
getWidth(): number {
if (this.menuCache) {
const left = this.menuCache[0].buttonElement.getBoundingClientRect().left;
const right = this.hasOverflow ? this.overflowMenu.buttonElement.getBoundingClientRect().right : this.menuCache[this.menuCache.length - 1].buttonElement.getBoundingClientRect().right;
if (!this.isCompact && this.menus) {
const left = this.menus[0].buttonElement!.getBoundingClientRect().left;
const right = this.hasOverflow ? this.overflowMenu.buttonElement.getBoundingClientRect().right : this.menus[this.menus.length - 1].buttonElement!.getBoundingClientRect().right;
return right - left;
}
@@ -466,16 +466,18 @@ export class MenuBar extends Disposable {
}
private updateOverflowAction(): void {
if (!this.menuCache || !this.menuCache.length) {
if (!this.menus || !this.menus.length) {
return;
}
const sizeAvailable = this.container.offsetWidth;
let currentSize = 0;
let full = this.options.compactMode !== undefined;
let full = this.isCompact;
const prevNumMenusShown = this.numMenusShown;
this.numMenusShown = 0;
for (let menuBarMenu of this.menuCache) {
const showableMenus = this.menus.filter(menu => menu.buttonElement !== undefined && menu.titleElement !== undefined) as (MenuBarMenuWithElements & { titleElement: HTMLElement, buttonElement: HTMLElement })[];
for (let menuBarMenu of showableMenus) {
if (!full) {
const size = menuBarMenu.buttonElement.offsetWidth;
if (currentSize + size > sizeAvailable) {
@@ -495,24 +497,10 @@ export class MenuBar extends Disposable {
}
// Overflow
if (full) {
// Can't fit the more button, need to remove more menus
while (currentSize + this.overflowMenu.buttonElement.offsetWidth > sizeAvailable && this.numMenusShown > 0) {
this.numMenusShown--;
const size = this.menuCache[this.numMenusShown].buttonElement.offsetWidth;
this.menuCache[this.numMenusShown].buttonElement.style.visibility = 'hidden';
currentSize -= size;
}
if (this.isCompact) {
this.overflowMenu.actions = [];
for (let idx = this.numMenusShown; idx < this.menuCache.length; idx++) {
this.overflowMenu.actions.push(new SubmenuAction(`menubar.submenu.${this.menuCache[idx].label}`, this.menuCache[idx].label, this.menuCache[idx].actions || []));
}
if (this.overflowMenu.buttonElement.nextElementSibling !== this.menuCache[this.numMenusShown].buttonElement) {
this.overflowMenu.buttonElement.remove();
this.container.insertBefore(this.overflowMenu.buttonElement, this.menuCache[this.numMenusShown].buttonElement);
this.overflowMenu.buttonElement.style.visibility = 'visible';
for (let idx = this.numMenusShown; idx < this.menus.length; idx++) {
this.overflowMenu.actions.push(new SubmenuAction(`menubar.submenu.${this.menus[idx].label}`, this.menus[idx].label, this.menus[idx].actions || []));
}
const compactMenuActions = this.options.getCompactMenuActions?.();
@@ -520,6 +508,28 @@ export class MenuBar extends Disposable {
this.overflowMenu.actions.push(new Separator());
this.overflowMenu.actions.push(...compactMenuActions);
}
this.overflowMenu.buttonElement.style.visibility = 'visible';
} else if (full) {
// Can't fit the more button, need to remove more menus
while (currentSize + this.overflowMenu.buttonElement.offsetWidth > sizeAvailable && this.numMenusShown > 0) {
this.numMenusShown--;
const size = showableMenus[this.numMenusShown].buttonElement.offsetWidth;
showableMenus[this.numMenusShown].buttonElement.style.visibility = 'hidden';
currentSize -= size;
}
this.overflowMenu.actions = [];
for (let idx = this.numMenusShown; idx < showableMenus.length; idx++) {
this.overflowMenu.actions.push(new SubmenuAction(`menubar.submenu.${showableMenus[idx].label}`, showableMenus[idx].label, showableMenus[idx].actions || []));
}
if (this.overflowMenu.buttonElement.nextElementSibling !== showableMenus[this.numMenusShown].buttonElement) {
this.overflowMenu.buttonElement.remove();
this.container.insertBefore(this.overflowMenu.buttonElement, showableMenus[this.numMenusShown].buttonElement);
}
this.overflowMenu.buttonElement.style.visibility = 'visible';
} else {
this.overflowMenu.buttonElement.remove();
this.container.appendChild(this.overflowMenu.buttonElement);
@@ -589,7 +599,11 @@ export class MenuBar extends Disposable {
return;
}
this.menuCache.forEach(menuBarMenu => {
this.menus.forEach(menuBarMenu => {
if (!menuBarMenu.buttonElement || !menuBarMenu.titleElement) {
return;
}
this.updateLabels(menuBarMenu.titleElement, menuBarMenu.buttonElement, menuBarMenu.label);
});
@@ -682,7 +696,7 @@ export class MenuBar extends Disposable {
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
this.overflowMenu.buttonElement.blur();
} else {
this.menuCache[this.focusedMenu.index].buttonElement.blur();
this.menus[this.focusedMenu.index].buttonElement?.blur();
}
}
@@ -708,7 +722,7 @@ export class MenuBar extends Disposable {
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
this.overflowMenu.buttonElement.focus();
} else {
this.menuCache[this.focusedMenu.index].buttonElement.focus();
this.menus[this.focusedMenu.index].buttonElement?.focus();
}
}
break;
@@ -740,7 +754,11 @@ export class MenuBar extends Disposable {
}
private get hasOverflow(): boolean {
return this.numMenusShown < this.menuCache.length;
return this.isCompact || this.numMenusShown < this.menus.length;
}
private get isCompact(): boolean {
return this.options.compactMode !== undefined;
}
private setUnfocusedState(): void {
@@ -783,7 +801,7 @@ export class MenuBar extends Disposable {
if (newFocusedIndex === MenuBar.OVERFLOW_INDEX) {
this.overflowMenu.buttonElement.focus();
} else {
this.menuCache[newFocusedIndex].buttonElement.focus();
this.menus[newFocusedIndex].buttonElement?.focus();
}
}
}
@@ -812,15 +830,15 @@ export class MenuBar extends Disposable {
if (newFocusedIndex === MenuBar.OVERFLOW_INDEX) {
this.overflowMenu.buttonElement.focus();
} else {
this.menuCache[newFocusedIndex].buttonElement.focus();
this.menus[newFocusedIndex].buttonElement?.focus();
}
}
}
private updateMnemonicVisibility(visible: boolean): void {
if (this.menuCache) {
this.menuCache.forEach(menuBarMenu => {
if (menuBarMenu.titleElement.children.length) {
if (this.menus) {
this.menus.forEach(menuBarMenu => {
if (menuBarMenu.titleElement && menuBarMenu.titleElement.children.length) {
let child = menuBarMenu.titleElement.children.item(0) as HTMLElement;
if (child) {
child.style.textDecoration = (this.options.alwaysOnMnemonics || visible) ? 'underline' : '';
@@ -916,7 +934,7 @@ export class MenuBar extends Disposable {
this.awaitingAltRelease = false;
}
if (this.options.enableMnemonics && this.menuCache && !this.isOpen) {
if (this.options.enableMnemonics && this.menus && !this.isOpen) {
this.updateMnemonicVisibility((!this.awaitingAltRelease && modifierKeyStatus.altKey) || this.mnemonicsInUse);
}
}
@@ -935,7 +953,7 @@ export class MenuBar extends Disposable {
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
this.overflowMenu.buttonElement.focus();
} else {
this.menuCache[this.focusedMenu.index].buttonElement.focus();
this.menus[this.focusedMenu.index].buttonElement?.focus();
}
if (this.focusedMenu.holder) {
@@ -956,9 +974,9 @@ export class MenuBar extends Disposable {
private showCustomMenu(menuIndex: number, selectFirst = true): void {
const actualMenuIndex = menuIndex >= this.numMenusShown ? MenuBar.OVERFLOW_INDEX : menuIndex;
const customMenu = actualMenuIndex === MenuBar.OVERFLOW_INDEX ? this.overflowMenu : this.menuCache[actualMenuIndex];
const customMenu = actualMenuIndex === MenuBar.OVERFLOW_INDEX ? this.overflowMenu : this.menus[actualMenuIndex];
if (!customMenu.actions) {
if (!customMenu.actions || !customMenu.buttonElement) {
return;
}
@@ -987,7 +1005,7 @@ export class MenuBar extends Disposable {
actionRunner: this.actionRunner,
enableMnemonics: this.options.alwaysOnMnemonics || (this.mnemonicsInUse && this.options.enableMnemonics),
ariaLabel: withNullAsUndefined(customMenu.buttonElement.getAttribute('aria-label')),
expandDirection: this.options.compactMode !== undefined ? this.options.compactMode : Direction.Right,
expandDirection: this.isCompact ? this.options.compactMode : Direction.Right,
useEventAsContext: true
};
@@ -409,11 +409,15 @@ export abstract class AbstractScrollableElement extends Widget {
let desiredScrollPosition: INewScrollPosition = {};
if (deltaY) {
const desiredScrollTop = futureScrollPosition.scrollTop - SCROLL_WHEEL_SENSITIVITY * deltaY;
const deltaScrollTop = SCROLL_WHEEL_SENSITIVITY * deltaY;
// Here we convert values such as -0.3 to -1 or 0.3 to 1, otherwise low speed scrolling will never scroll
const desiredScrollTop = futureScrollPosition.scrollTop - (deltaScrollTop < 0 ? Math.floor(deltaScrollTop) : Math.ceil(deltaScrollTop));
this._verticalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollTop);
}
if (deltaX) {
const desiredScrollLeft = futureScrollPosition.scrollLeft - SCROLL_WHEEL_SENSITIVITY * deltaX;
const deltaScrollLeft = SCROLL_WHEEL_SENSITIVITY * deltaX;
// Here we convert values such as -0.3 to -1 or 0.3 to 1, otherwise low speed scrolling will never scroll
const desiredScrollLeft = futureScrollPosition.scrollLeft - (deltaScrollLeft < 0 ? Math.floor(deltaScrollLeft) : Math.ceil(deltaScrollLeft));
this._horizontalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollLeft);
}
@@ -180,6 +180,12 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
// Inline stylesheet for themes
this.styleElement = dom.createStyleSheet(this.selectDropDownContainer);
// Prevent dragging of dropdown #114329
this.selectDropDownContainer.setAttribute('draggable', 'true');
this._register(dom.addDisposableListener(this.selectDropDownContainer, dom.EventType.DRAG_START, (e) => {
dom.EventHelper.stop(e, true);
}));
}
private registerListeners() {
@@ -758,6 +764,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
.map(e => new StandardKeyboardEvent(e));
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.Enter).on(e => this.onEnter(e), this));
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.Tab).on(e => this.onEnter(e), this)); // Tab should behave the same as enter, #79339
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.Escape).on(e => this.onEscape(e), this));
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.UpArrow).on(e => this.onUpArrow(e), this));
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.DownArrow).on(e => this.onDownArrow(e), this));
@@ -765,7 +772,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.PageUp).on(this.onPageUp, this));
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.Home).on(this.onHome, this));
this._register(onSelectDropDownKeyDown.filter(e => e.keyCode === KeyCode.End).on(this.onEnd, this));
this._register(onSelectDropDownKeyDown.filter(e => (e.keyCode >= KeyCode.KEY_0 && e.keyCode <= KeyCode.KEY_Z) || (e.keyCode >= KeyCode.US_SEMICOLON && e.keyCode <= KeyCode.NUMPAD_DIVIDE)).on(this.onCharacter, this));
this._register(onSelectDropDownKeyDown.filter(e => (e.keyCode >= KeyCode.Digit0 && e.keyCode <= KeyCode.KeyZ) || (e.keyCode >= KeyCode.Semicolon && e.keyCode <= KeyCode.NumpadDivide)).on(this.onCharacter, this));
// SetUp list mouse controller - control navigation, disabled items, focus
@@ -870,12 +877,12 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
}
};
const renderedMarkdown = renderMarkdown({ value: text }, { actionHandler });
const rendered = renderMarkdown({ value: text }, { actionHandler });
renderedMarkdown.classList.add('select-box-description-markdown');
cleanRenderedMarkdown(renderedMarkdown);
rendered.element.classList.add('select-box-description-markdown');
cleanRenderedMarkdown(rendered.element);
return renderedMarkdown;
return rendered.element;
}
// List Focus Change - passive - update details pane with newly focused element's data
@@ -890,12 +897,13 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
private updateDetail(selectedIndex: number): void {
this.selectionDetailsPane.innerText = '';
const description = this.options[selectedIndex]?.description; // {{SQL CARBON EDIT}} Handle undefined options
const descriptionIsMarkdown = this.options[selectedIndex]?.descriptionIsMarkdown; // {{SQL CARBON EDIT}} Handle undefined options
const option = this.options[selectedIndex];
const description = option?.description ?? '';
const descriptionIsMarkdown = option?.descriptionIsMarkdown ?? false;
if (description) {
if (descriptionIsMarkdown) {
const actionHandler = this.options[selectedIndex].descriptionMarkdownActionHandler;
const actionHandler = option.descriptionMarkdownActionHandler;
this.selectionDetailsPane.appendChild(this.renderDescriptionMarkdown(description, actionHandler));
} else {
this.selectionDetailsPane.innerText = description;
@@ -444,6 +444,7 @@ export class PaneView extends Disposable {
orientation: Orientation;
readonly onDidSashChange: Event<number>;
readonly onDidSashReset: Event<number>;
readonly onDidScroll: Event<ScrollEvent>;
constructor(container: HTMLElement, options: IPaneViewOptions = {}) {
@@ -453,6 +454,7 @@ export class PaneView extends Disposable {
this.orientation = options.orientation ?? Orientation.VERTICAL;
this.element = append(container, $('.monaco-pane-view'));
this.splitview = this._register(new SplitView(this.element, { orientation: this.orientation }));
this.onDidSashReset = this.splitview.onDidSashReset;
this.onDidSashChange = this.splitview.onDidSashChange;
this.onDidScroll = this.splitview.onDidScroll;
}
+1 -1
View File
@@ -10,7 +10,7 @@ import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdown
import { Action, IAction, IActionRunner, SubmenuAction } from 'vs/base/common/actions';
import { Codicon, CSSIcon, registerCodicon } from 'vs/base/common/codicons';
import { EventMultiplexer } from 'vs/base/common/event';
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { ResolvedKeybinding } from 'vs/base/common/keybindings';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { withNullAsUndefined } from 'vs/base/common/types';
import 'vs/css!./toolbar';
+3 -3
View File
@@ -13,7 +13,7 @@ import { DefaultKeyboardNavigationDelegate, IListOptions, IListStyles, isInputEl
import { getVisibleState, isFilterResult } from 'vs/base/browser/ui/tree/indexTreeModel';
import { ICollapseStateChangeEvent, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeEvent, ITreeFilter, ITreeModel, ITreeModelSpliceEvent, ITreeMouseEvent, ITreeNavigator, ITreeNode, ITreeRenderer, TreeDragOverBubble, TreeFilterResult, TreeMouseEventTarget, TreeVisibility } from 'vs/base/browser/ui/tree/tree';
import { treeFilterClearIcon, treeFilterOnTypeOffIcon, treeFilterOnTypeOnIcon, treeItemExpandedIcon } from 'vs/base/browser/ui/tree/treeIcons';
import { distinctES6, equals, firstOrDefault, range } from 'vs/base/common/arrays';
import { distinct, equals, firstOrDefault, range } from 'vs/base/common/arrays';
import { disposableTimeout } from 'vs/base/common/async';
import { SetMap } from 'vs/base/common/collections';
import { Emitter, Event, EventBufferer, Relay } from 'vs/base/common/event';
@@ -1220,11 +1220,11 @@ class TreeNodeList<T, TFilterData, TRef> extends List<ITreeNode<T, TFilterData>>
});
if (additionalFocus.length > 0) {
super.setFocus(distinctES6([...super.getFocus(), ...additionalFocus]));
super.setFocus(distinct([...super.getFocus(), ...additionalFocus]));
}
if (additionalSelection.length > 0) {
super.setSelection(distinctES6([...super.getSelection(), ...additionalSelection]));
super.setSelection(distinct([...super.getSelection(), ...additionalSelection]));
}
if (typeof anchor === 'number') {
+19 -4
View File
@@ -23,6 +23,7 @@ export interface IIndexTreeNode<T, TFilterData = void> extends ITreeNode<T, TFil
visibility: TreeVisibility;
visible: boolean;
filterData: TFilterData | undefined;
lastDiffIds?: string[];
}
export function isFilterResult<T>(obj: any): obj is ITreeFilterDataResult<T> {
@@ -87,7 +88,7 @@ function isCollapsibleStateUpdate(update: CollapseStateUpdate): update is Collap
}
export interface IList<T> extends ISpliceable<T> {
updateElementHeight(index: number, height: number): void;
updateElementHeight(index: number, height: number | undefined): void;
}
export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = void> implements ITreeModel<T, TFilterData, number[]> {
@@ -162,10 +163,14 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
recurseLevels = options.diffDepth ?? 0,
) {
const { parentNode } = this.getParentNodeWithListIndex(location);
if (!parentNode.lastDiffIds) {
return this.spliceSimple(location, deleteCount, toInsertIterable, options);
}
const toInsert = [...toInsertIterable];
const index = location[location.length - 1];
const diff = new LcsDiff(
{ getElements: () => parentNode.children.map(e => identity.getId(e.element).toString()) },
{ getElements: () => parentNode.lastDiffIds! },
{
getElements: () => [
...parentNode.children.slice(0, index),
@@ -177,6 +182,7 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
// if we were given a 'best effort' diff, use default behavior
if (diff.quitEarly) {
parentNode.lastDiffIds = undefined;
return this.spliceSimple(location, deleteCount, toInsert, options);
}
@@ -221,7 +227,7 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
location: number[],
deleteCount: number,
toInsert: Iterable<ITreeElement<T>> = Iterable.empty(),
{ onDidCreateNode, onDidDeleteNode }: IIndexTreeModelSpliceOptions<T, TFilterData>,
{ onDidCreateNode, onDidDeleteNode, diffIdentityProvider }: IIndexTreeModelSpliceOptions<T, TFilterData>,
) {
const { parentNode, listIndex, revealed, visible } = this.getParentNodeWithListIndex(location);
const treeListElementsToInsert: ITreeNode<T, TFilterData>[] = [];
@@ -258,6 +264,14 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
const deletedNodes = splice(parentNode.children, lastIndex, deleteCount, nodesToInsert);
if (!diffIdentityProvider) {
parentNode.lastDiffIds = undefined;
} else if (parentNode.lastDiffIds) {
splice(parentNode.lastDiffIds, lastIndex, deleteCount, nodesToInsert.map(n => diffIdentityProvider.getId(n.element).toString()));
} else {
parentNode.lastDiffIds = parentNode.children.map(n => diffIdentityProvider.getId(n.element).toString());
}
// figure out what is the count of deleted visible children
let deletedVisibleChildrenCount = 0;
@@ -328,7 +342,7 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
}
}
updateElementHeight(location: number[], height: number): void {
updateElementHeight(location: number[], height: number | undefined): void {
if (location.length === 0) {
throw new TreeError(this.user, 'Invalid tree location');
}
@@ -619,6 +633,7 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
if (node !== this.root) {
node.visible = visibility! === TreeVisibility.Recurse ? hasVisibleDescendants : (visibility! === TreeVisibility.Visible);
node.visibility = visibility!;
}
if (!node.visible) {
+1 -1
View File
@@ -65,7 +65,7 @@ export class ObjectTree<T extends NonNullable<any>, TFilterData = void> extends
this.model.rerender(element);
}
updateElementHeight(element: T, height: number): void {
updateElementHeight(element: T, height: number | undefined): void {
this.model.updateElementHeight(element, height);
}
@@ -14,7 +14,7 @@ export type ITreeNodeCallback<T, TFilterData> = (node: ITreeNode<T, TFilterData>
export interface IObjectTreeModel<T extends NonNullable<any>, TFilterData extends NonNullable<any> = void> extends ITreeModel<T | null, TFilterData, T | null> {
setChildren(element: T | null, children: Iterable<ITreeElement<T>> | undefined, options?: IObjectTreeModelSetChildrenOptions<T, TFilterData>): void;
resort(element?: T | null, recursive?: boolean): void;
updateElementHeight(element: T, height: number): void;
updateElementHeight(element: T, height: number | undefined): void;
}
export interface IObjectTreeModelSetChildrenOptions<T, TFilterData> extends IIndexTreeModelSpliceOptions<T, TFilterData> {
@@ -164,7 +164,7 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
this.model.rerender(location);
}
updateElementHeight(element: T, height: number): void {
updateElementHeight(element: T, height: number | undefined): void {
const location = this.getElementLocation(element);
this.model.updateElementHeight(location, height);
}
+5 -5
View File
@@ -29,7 +29,7 @@ export interface IAction extends IDisposable {
tooltip: string;
class: string | undefined;
enabled: boolean;
checked: boolean;
checked?: boolean;
expanded?: boolean | undefined; // {{SQL CARBON EDIT}}
run(event?: unknown): unknown;
}
@@ -60,7 +60,7 @@ export class Action extends Disposable implements IAction {
protected _tooltip: string | undefined;
protected _cssClass: string | undefined;
protected _enabled: boolean = true;
protected _checked: boolean = false;
protected _checked?: boolean;
protected _expanded: boolean = false; // {{SQL CARBON EDIT}}
protected readonly _actionCallback?: (event?: unknown) => unknown;
@@ -137,15 +137,15 @@ export class Action extends Disposable implements IAction {
}
}
get checked(): boolean {
get checked(): boolean | undefined {
return this._checked;
}
set checked(value: boolean) {
set checked(value: boolean | undefined) {
this._setChecked(value);
}
protected _setChecked(value: boolean): void {
protected _setChecked(value: boolean | undefined): void {
if (this._checked !== value) {
this._checked = value;
this._onDidChange.fire({ checked: value });
+18 -25
View File
@@ -324,36 +324,17 @@ export function isNonEmptyArray<T>(obj: T[] | readonly T[] | undefined | null):
/**
* Removes duplicates from the given array. The optional keyFn allows to specify
* how elements are checked for equality by returning a unique string for each.
* how elements are checked for equality by returning an alternate value for each.
*/
export function distinct<T>(array: ReadonlyArray<T>, keyFn?: (t: T) => string): T[] {
if (!keyFn) {
return array.filter((element, position) => {
return array.indexOf(element) === position;
});
}
export function distinct<T>(array: ReadonlyArray<T>, keyFn: (value: T) => any = value => value): T[] {
const seen = new Set<any>();
const seen: { [key: string]: boolean; } = Object.create(null);
return array.filter((elem) => {
const key = keyFn(elem);
if (seen[key]) {
return false;
}
seen[key] = true;
return true;
});
}
export function distinctES6<T>(array: ReadonlyArray<T>): T[] {
const seen = new Set<T>();
return array.filter(element => {
if (seen.has(element)) {
const key = keyFn!(element);
if (seen.has(key)) {
return false;
}
seen.add(element);
seen.add(key);
return true;
});
}
@@ -373,6 +354,14 @@ export function uniqueFilter<T>(keyFn: (t: T) => string): (t: T) => boolean {
};
}
export function findLast<T>(arr: readonly T[], predicate: (item: T) => boolean): T | undefined {
const idx = lastIndex(arr, predicate);
if (idx === -1) {
return undefined;
}
return arr[idx];
}
export function lastIndex<T>(array: ReadonlyArray<T>, fn: (item: T) => boolean): number {
for (let i = array.length - 1; i >= 0; i--) {
const element = array[i];
@@ -683,4 +672,8 @@ export class ArrayQueue<T> {
this.lastIdx = endIdx;
return result;
}
peek(): T | undefined {
return this.items[this.firstIdx];
}
}
+105 -5
View File
@@ -759,6 +759,81 @@ export class RunOnceScheduler {
}
}
/**
* Same as `RunOnceScheduler`, but doesn't count the time spent in sleep mode.
* > **NOTE**: Only offers 1s resolution.
*
* When calling `setTimeout` with 3hrs, and putting the computer immediately to sleep
* for 8hrs, `setTimeout` will fire **as soon as the computer wakes from sleep**. But
* this scheduler will execute 3hrs **after waking the computer from sleep**.
*/
export class ProcessTimeRunOnceScheduler {
private runner: (() => void) | null;
private timeout: number;
private counter: number;
private intervalToken: any;
private intervalHandler: () => void;
constructor(runner: () => void, delay: number) {
if (delay % 1000 !== 0) {
console.warn(`ProcessTimeRunOnceScheduler resolution is 1s, ${delay}ms is not a multiple of 1000ms.`);
}
this.runner = runner;
this.timeout = delay;
this.counter = 0;
this.intervalToken = -1;
this.intervalHandler = this.onInterval.bind(this);
}
dispose(): void {
this.cancel();
this.runner = null;
}
cancel(): void {
if (this.isScheduled()) {
clearInterval(this.intervalToken);
this.intervalToken = -1;
}
}
/**
* Cancel previous runner (if any) & schedule a new runner.
*/
schedule(delay = this.timeout): void {
if (delay % 1000 !== 0) {
console.warn(`ProcessTimeRunOnceScheduler resolution is 1s, ${delay}ms is not a multiple of 1000ms.`);
}
this.cancel();
this.counter = Math.ceil(delay / 1000);
this.intervalToken = setInterval(this.intervalHandler, 1000);
}
/**
* Returns true if scheduled.
*/
isScheduled(): boolean {
return this.intervalToken !== -1;
}
private onInterval() {
this.counter--;
if (this.counter > 0) {
// still need to wait
return;
}
// time elapsed
clearInterval(this.intervalToken);
this.intervalToken = -1;
if (this.runner) {
this.runner();
}
}
}
export class RunOnceWorker<T> extends RunOnceScheduler {
private units: T[] = [];
@@ -903,12 +978,16 @@ declare function cancelIdleCallback(handle: number): void;
(function () {
if (typeof requestIdleCallback !== 'function' || typeof cancelIdleCallback !== 'function') {
const dummyIdle: IdleDeadline = Object.freeze({
didTimeout: true,
timeRemaining() { return 15; }
});
runWhenIdle = (runner) => {
const handle = setTimeout(() => runner(dummyIdle));
const handle = setTimeout(() => {
const end = Date.now() + 15; // one frame at 64fps
runner(Object.freeze({
didTimeout: true,
timeRemaining() {
return Math.max(0, end - Date.now());
}
}));
});
let disposed = false;
return {
dispose() {
@@ -1230,6 +1309,27 @@ export namespace Promises {
return result as unknown as T[]; // cast is needed and protected by the `throw` above
}
/**
* A helper to create a new `Promise<T>` with a body that is a promise
* itself. By default, an error that raises from the async body will
* end up as a unhandled rejection, so this utility properly awaits the
* body and rejects the promise as a normal promise does without async
* body.
*
* This method should only be used in rare cases where otherwise `async`
* cannot be used (e.g. when callbacks are involved that require this).
*/
export function withAsyncBody<T, E = Error>(bodyFn: (resolve: (value: T) => unknown, reject: (error: E) => unknown) => Promise<unknown>): Promise<T> {
// eslint-disable-next-line no-async-promise-executor
return new Promise<T>(async (resolve, reject) => {
try {
await bodyFn(resolve, reject);
} catch (error) {
reject(error);
}
});
}
}
//#endregion
+10 -1
View File
@@ -423,6 +423,12 @@ export const enum CharCode {
U_GREEK_OXIA = 0x1FFD, // U+1FFD GREEK OXIA
U_GREEK_DASIA = 0x1FFE, // U+1FFE GREEK DASIA
U_IDEOGRAPHIC_FULL_STOP = 0x3002, // U+3002 IDEOGRAPHIC FULL STOP
U_LEFT_CORNER_BRACKET = 0x300C, // U+300C LEFT CORNER BRACKET
U_RIGHT_CORNER_BRACKET = 0x300D, // U+300D RIGHT CORNER BRACKET
U_LEFT_BLACK_LENTICULAR_BRACKET = 0x3010, // U+3010 LEFT BLACK LENTICULAR BRACKET
U_RIGHT_BLACK_LENTICULAR_BRACKET = 0x3011, // U+3011 RIGHT BLACK LENTICULAR BRACKET
U_OVERLINE = 0x203E, // Unicode Character 'OVERLINE'
@@ -431,5 +437,8 @@ export const enum CharCode {
* Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF)
* http://www.fileformat.info/info/unicode/char/feff/index.htm
*/
UTF8_BOM = 65279
UTF8_BOM = 65279,
U_FULLWIDTH_SEMICOLON = 0xFF1B, // U+FF1B FULLWIDTH SEMICOLON
U_FULLWIDTH_COMMA = 0xFF0C, // U+FF0C FULLWIDTH COMMA
}
+15 -5
View File
@@ -133,9 +133,9 @@ export namespace Codicon {
// built-in icons, with image name
export const add = new Codicon('add', { fontCharacter: '\\ea60' });
export const plus = new Codicon('plus', { fontCharacter: '\\ea60' });
export const gistNew = new Codicon('gist-new', { fontCharacter: '\\ea60' });
export const repoCreate = new Codicon('repo-create', { fontCharacter: '\\ea60' });
export const plus = new Codicon('plus', Codicon.add.definition);
export const gistNew = new Codicon('gist-new', Codicon.add.definition);
export const repoCreate = new Codicon('repo-create', Codicon.add.definition);
export const lightbulb = new Codicon('lightbulb', { fontCharacter: '\\ea61' });
export const lightBulb = new Codicon('light-bulb', { fontCharacter: '\\ea61' });
export const repo = new Codicon('repo', { fontCharacter: '\\ea62' });
@@ -293,6 +293,7 @@ export namespace Codicon {
export const check = new Codicon('check', { fontCharacter: '\\eab2' });
export const checklist = new Codicon('checklist', { fontCharacter: '\\eab3' });
export const chevronDown = new Codicon('chevron-down', { fontCharacter: '\\eab4' });
export const dropDownButton = new Codicon('drop-down-button', Codicon.chevronDown.definition);
export const chevronLeft = new Codicon('chevron-left', { fontCharacter: '\\eab5' });
export const chevronRight = new Codicon('chevron-right', { fontCharacter: '\\eab6' });
export const chevronUp = new Codicon('chevron-up', { fontCharacter: '\\eab7' });
@@ -598,7 +599,16 @@ export namespace Codicon {
export const debugCoverage = new Codicon('debug-coverage', { fontCharacter: '\\ebdd' });
export const runErrors = new Codicon('run-errors', { fontCharacter: '\\ebde' });
export const folderLibrary = new Codicon('folder-library', { fontCharacter: '\\ebdf' });
export const dropDownButton = new Codicon('drop-down-button', Codicon.chevronDown.definition);
export const debugContinueSmall = new Codicon('debug-continue-small', { fontCharacter: '\\ebe0' });
export const beakerStop = new Codicon('beaker-stop', { fontCharacter: '\\ebe1' });
export const graphLine = new Codicon('graph-line', { fontCharacter: '\\ebe2' });
export const graphScatter = new Codicon('graph-scatter', { fontCharacter: '\\ebe3' });
export const pieChart = new Codicon('pie-chart', { fontCharacter: '\\ebe4' });
export const bracket = new Codicon('bracket', Codicon.json.definition);
export const bracketDot = new Codicon('bracket-dot', { fontCharacter: '\\ebe5' });
export const bracketError = new Codicon('bracket-error', { fontCharacter: '\\ebe6' });
export const lockSmall = new Codicon('lock-small', { fontCharacter: '\\ebe7' });
export const azureDevops = new Codicon('azure-devops', { fontCharacter: '\\ebe8' });
export const verifiedFilled = new Codicon('verified-filled', { fontCharacter: '\\ebe9' });
}
+5 -1
View File
@@ -432,8 +432,12 @@ export class Color {
));
}
private _toString?: string;
toString(): string {
return '' + Color.Format.CSS.format(this);
if (!this._toString) {
this._toString = Color.Format.CSS.format(this);
}
return this._toString;
}
static getLighterColor(of: Color, relative: Color, factor?: number): Color {
+27
View File
@@ -732,6 +732,33 @@ export class DebounceEmitter<T> extends PauseableEmitter<T> {
}
}
/**
* An emitter which queue all events and then process them at the
* end of the event loop.
*/
export class MicrotaskEmitter<T> extends Emitter<T> {
private _queuedEvents: T[] = [];
private _mergeFn?: (input: T[]) => T;
constructor(options?: EmitterOptions & { merge?: (input: T[]) => T }) {
super(options);
this._mergeFn = options?.merge;
}
override fire(event: T): void {
this._queuedEvents.push(event);
if (this._queuedEvents.length === 1) {
queueMicrotask(() => {
if (this._mergeFn) {
super.fire(this._mergeFn(this._queuedEvents));
} else {
this._queuedEvents.forEach(e => super.fire(e));
}
this._queuedEvents = [];
});
}
}
}
export class EventMultiplexer<T> implements IDisposable {
private readonly emitter: Emitter<T>;
+52 -45
View File
@@ -6,6 +6,7 @@
import { CharCode } from 'vs/base/common/charCode';
import { compareAnything } from 'vs/base/common/comparers';
import { createMatches as createFuzzyMatches, fuzzyScore, IMatch, isUpper, matchesPrefix } from 'vs/base/common/filters';
import { hash } from 'vs/base/common/hash';
import { sep } from 'vs/base/common/path';
import { isLinux, isWindows } from 'vs/base/common/platform';
import { equalsIgnoreCase, stripWildcards } from 'vs/base/common/strings';
@@ -21,7 +22,7 @@ const NO_SCORE: FuzzyScore = [NO_MATCH, []];
// const DEBUG = false;
// const DEBUG_MATRIX = false;
export function scoreFuzzy(target: string, query: string, queryLower: string, fuzzy: boolean): FuzzyScore {
export function scoreFuzzy(target: string, query: string, queryLower: string, allowNonContiguousMatches: boolean): FuzzyScore {
if (!target || !query) {
return NO_SCORE; // return early if target or query are undefined
}
@@ -38,20 +39,7 @@ export function scoreFuzzy(target: string, query: string, queryLower: string, fu
// }
const targetLower = target.toLowerCase();
// When not searching fuzzy, we require the query to be contained fully
// in the target string contiguously.
if (!fuzzy) {
if (!targetLower.includes(queryLower)) {
// if (DEBUG) {
// console.log(`Characters not matching consecutively ${queryLower} within ${targetLower}`);
// }
return NO_SCORE;
}
}
const res = doScoreFuzzy(query, queryLower, queryLength, target, targetLower, targetLength);
const res = doScoreFuzzy(query, queryLower, queryLength, target, targetLower, targetLength, allowNonContiguousMatches);
// if (DEBUG) {
// console.log(`%cFinal Score: ${res[0]}`, 'font-weight: bold');
@@ -61,7 +49,7 @@ export function scoreFuzzy(target: string, query: string, queryLower: string, fu
return res;
}
function doScoreFuzzy(query: string, queryLower: string, queryLength: number, target: string, targetLower: string, targetLength: number): FuzzyScore {
function doScoreFuzzy(query: string, queryLower: string, queryLength: number, target: string, targetLower: string, targetLength: number, allowNonContiguousMatches: boolean): FuzzyScore {
const scores: number[] = [];
const matches: number[] = [];
@@ -116,7 +104,17 @@ function doScoreFuzzy(query: string, queryLower: string, queryLength: number, ta
// We have a score and its equal or larger than the left score
// Match: sequence continues growing from previous diag value
// Score: increases by diag score value
if (score && diagScore + score >= leftScore) {
const isValidScore = score && diagScore + score >= leftScore;
if (isValidScore && (
// We don't need to check if it's contiguous if we allow non-contiguous matches
allowNonContiguousMatches ||
// We must be looking for a contiguous match.
// Looking at an index higher than 0 in the query means we must have already
// found out this is contiguous otherwise there wouldn't have been a score
queryIndexGtNull ||
// lastly check if the query is completely contiguous at this index in the target
targetLower.startsWith(queryLower, targetIndex)
)) {
matches[currentIndex] = matchesSequenceLength + 1;
scores[currentIndex] = diagScore + score;
}
@@ -210,8 +208,11 @@ function computeCharScore(queryCharAtIndex: string, queryLowerCharAtIndex: strin
// }
}
// Inside word upper case bonus (camel case)
else if (isUpper(target.charCodeAt(targetIndex))) {
// Inside word upper case bonus (camel case). We only give this bonus if we're not in a contiguous sequence.
// For example:
// NPE => NullPointerException = boost
// HTTP => HTTP = not boost
else if (isUpper(target.charCodeAt(targetIndex)) && matchesSequenceLength === 0) {
score += 2;
// if (DEBUG) {
@@ -372,7 +373,20 @@ const PATH_IDENTITY_SCORE = 1 << 18;
const LABEL_PREFIX_SCORE_THRESHOLD = 1 << 17;
const LABEL_SCORE_THRESHOLD = 1 << 16;
export function scoreItemFuzzy<T>(item: T, query: IPreparedQuery, fuzzy: boolean, accessor: IItemAccessor<T>, cache: FuzzyScorerCache): IItemScore {
function getCacheHash(label: string, description: string | undefined, allowNonContiguousMatches: boolean, query: IPreparedQuery) {
const values = query.values ? query.values : [query];
const cacheHash = hash({
[query.normalized]: {
values: values.map(v => ({ value: v.normalized, expectContiguousMatch: v.expectContiguousMatch })),
label,
description,
allowNonContiguousMatches
}
});
return cacheHash;
}
export function scoreItemFuzzy<T>(item: T, query: IPreparedQuery, allowNonContiguousMatches: boolean, accessor: IItemAccessor<T>, cache: FuzzyScorerCache): IItemScore {
if (!item || !query.normalized) {
return NO_ITEM_SCORE; // we need an item and query to score on at least
}
@@ -387,28 +401,21 @@ export function scoreItemFuzzy<T>(item: T, query: IPreparedQuery, fuzzy: boolean
// in order to speed up scoring, we cache the score with a unique hash based on:
// - label
// - description (if provided)
// - query (normalized)
// - number of query pieces (i.e. 'hello world' and 'helloworld' are different)
// - whether fuzzy matching is enabled or not
let cacheHash: string;
if (description) {
cacheHash = `${label}${description}${query.normalized}${Array.isArray(query.values) ? query.values.length : ''}${fuzzy}${query.expectExactMatch}`;
} else {
cacheHash = `${label}${query.normalized}${Array.isArray(query.values) ? query.values.length : ''}${fuzzy}${query.expectExactMatch}`;
}
// - whether non-contiguous matching is enabled or not
// - hash of the query (normalized) values
const cacheHash = getCacheHash(label, description, allowNonContiguousMatches, query);
const cached = cache[cacheHash];
if (cached) {
return cached;
}
const itemScore = doScoreItemFuzzy(label, description, accessor.getItemPath(item), query, fuzzy);
const itemScore = doScoreItemFuzzy(label, description, accessor.getItemPath(item), query, allowNonContiguousMatches);
cache[cacheHash] = itemScore;
return itemScore;
}
function doScoreItemFuzzy(label: string, description: string | undefined, path: string | undefined, query: IPreparedQuery, fuzzy: boolean): IItemScore {
function doScoreItemFuzzy(label: string, description: string | undefined, path: string | undefined, query: IPreparedQuery, allowNonContiguousMatches: boolean): IItemScore {
const preferLabelMatches = !path || !query.containsPathSeparator;
// Treat identity matches on full path highest
@@ -418,20 +425,20 @@ function doScoreItemFuzzy(label: string, description: string | undefined, path:
// Score: multiple inputs
if (query.values && query.values.length > 1) {
return doScoreItemFuzzyMultiple(label, description, path, query.values, preferLabelMatches, fuzzy);
return doScoreItemFuzzyMultiple(label, description, path, query.values, preferLabelMatches, allowNonContiguousMatches);
}
// Score: single input
return doScoreItemFuzzySingle(label, description, path, query, preferLabelMatches, fuzzy);
return doScoreItemFuzzySingle(label, description, path, query, preferLabelMatches, allowNonContiguousMatches);
}
function doScoreItemFuzzyMultiple(label: string, description: string | undefined, path: string | undefined, query: IPreparedQueryPiece[], preferLabelMatches: boolean, fuzzy: boolean): IItemScore {
function doScoreItemFuzzyMultiple(label: string, description: string | undefined, path: string | undefined, query: IPreparedQueryPiece[], preferLabelMatches: boolean, allowNonContiguousMatches: boolean): IItemScore {
let totalScore = 0;
const totalLabelMatches: IMatch[] = [];
const totalDescriptionMatches: IMatch[] = [];
for (const queryPiece of query) {
const { score, labelMatch, descriptionMatch } = doScoreItemFuzzySingle(label, description, path, queryPiece, preferLabelMatches, fuzzy);
const { score, labelMatch, descriptionMatch } = doScoreItemFuzzySingle(label, description, path, queryPiece, preferLabelMatches, allowNonContiguousMatches);
if (score === NO_MATCH) {
// if a single query value does not match, return with
// no score entirely, we require all queries to match
@@ -457,7 +464,7 @@ function doScoreItemFuzzyMultiple(label: string, description: string | undefined
};
}
function doScoreItemFuzzySingle(label: string, description: string | undefined, path: string | undefined, query: IPreparedQueryPiece, preferLabelMatches: boolean, fuzzy: boolean): IItemScore {
function doScoreItemFuzzySingle(label: string, description: string | undefined, path: string | undefined, query: IPreparedQueryPiece, preferLabelMatches: boolean, allowNonContiguousMatches: boolean): IItemScore {
// Prefer label matches if told so or we have no description
if (preferLabelMatches || !description) {
@@ -465,7 +472,7 @@ function doScoreItemFuzzySingle(label: string, description: string | undefined,
label,
query.normalized,
query.normalizedLowercase,
fuzzy && !query.expectExactMatch);
allowNonContiguousMatches && !query.expectContiguousMatch);
if (labelScore) {
// If we have a prefix match on the label, we give a much
@@ -507,7 +514,7 @@ function doScoreItemFuzzySingle(label: string, description: string | undefined,
descriptionAndLabel,
query.normalized,
query.normalizedLowercase,
fuzzy && !query.expectExactMatch);
allowNonContiguousMatches && !query.expectContiguousMatch);
if (labelDescriptionScore) {
const labelDescriptionMatches = createMatches(labelDescriptionPositions);
const labelMatch: IMatch[] = [];
@@ -606,9 +613,9 @@ function matchOverlaps(matchA: IMatch, matchB: IMatch): boolean {
//#region Comparers
export function compareItemsByFuzzyScore<T>(itemA: T, itemB: T, query: IPreparedQuery, fuzzy: boolean, accessor: IItemAccessor<T>, cache: FuzzyScorerCache): number {
const itemScoreA = scoreItemFuzzy(itemA, query, fuzzy, accessor, cache);
const itemScoreB = scoreItemFuzzy(itemB, query, fuzzy, accessor, cache);
export function compareItemsByFuzzyScore<T>(itemA: T, itemB: T, query: IPreparedQuery, allowNonContiguousMatches: boolean, accessor: IItemAccessor<T>, cache: FuzzyScorerCache): number {
const itemScoreA = scoreItemFuzzy(itemA, query, allowNonContiguousMatches, accessor, cache);
const itemScoreB = scoreItemFuzzy(itemB, query, allowNonContiguousMatches, accessor, cache);
const scoreA = itemScoreA.score;
const scoreB = itemScoreB.score;
@@ -807,7 +814,7 @@ export interface IPreparedQueryPiece {
* this query must be a substring of the input.
* In other words, no fuzzy matching is used.
*/
expectExactMatch: boolean;
expectContiguousMatch: boolean;
}
export interface IPreparedQuery extends IPreparedQueryPiece {
@@ -869,13 +876,13 @@ export function prepareQuery(original: string): IPreparedQuery {
pathNormalized: pathNormalizedPiece,
normalized: normalizedPiece,
normalizedLowercase: normalizedLowercasePiece,
expectExactMatch: expectExactMatchPiece
expectContiguousMatch: expectExactMatchPiece
});
}
}
}
return { original, originalLowercase, pathNormalized, normalized, normalizedLowercase, values, containsPathSeparator, expectExactMatch };
return { original, originalLowercase, pathNormalized, normalized, normalizedLowercase, values, containsPathSeparator, expectContiguousMatch: expectExactMatch };
}
function normalizeQuery(original: string): { pathNormalized: string, normalized: string, normalizedLowercase: string } {
+1 -1
View File
@@ -34,7 +34,7 @@ export function doHash(obj: any, hashVal: number): number {
}
}
function numberHash(val: number, initialHashVal: number): number {
export function numberHash(val: number, initialHashVal: number): number {
return (((initialHashVal << 5) - initialHashVal) + val) | 0; // hashVal * 31 + ch, keep as int32
}
+7 -3
View File
@@ -11,6 +11,7 @@ export interface IMarkdownString {
readonly value: string;
readonly isTrusted?: boolean;
readonly supportThemeIcons?: boolean;
readonly supportHtml?: boolean;
uris?: { [href: string]: UriComponents };
}
@@ -24,10 +25,11 @@ export class MarkdownString implements IMarkdownString {
public value: string;
public isTrusted?: boolean;
public supportThemeIcons?: boolean;
public supportHtml?: boolean;
constructor(
value: string = '',
isTrustedOrOptions: boolean | { isTrusted?: boolean, supportThemeIcons?: boolean } = false,
isTrustedOrOptions: boolean | { isTrusted?: boolean, supportThemeIcons?: boolean, supportHtml?: boolean } = false,
) {
this.value = value;
if (typeof this.value !== 'string') {
@@ -37,17 +39,19 @@ export class MarkdownString implements IMarkdownString {
if (typeof isTrustedOrOptions === 'boolean') {
this.isTrusted = isTrustedOrOptions;
this.supportThemeIcons = false;
this.supportHtml = false;
}
else {
this.isTrusted = isTrustedOrOptions.isTrusted ?? undefined;
this.supportThemeIcons = isTrustedOrOptions.supportThemeIcons ?? false;
this.supportHtml = isTrustedOrOptions.supportHtml ?? false;
}
}
appendText(value: string, newlineStyle: MarkdownStringTextNewlineStyle = MarkdownStringTextNewlineStyle.Paragraph): MarkdownString {
this.value += escapeMarkdownSyntaxTokens(this.supportThemeIcons ? escapeIcons(value) : value)
.replace(/([ \t]+)/g, (_match, g1) => '&nbsp;'.repeat(g1.length))
.replace(/^>/gm, '\\>')
.replace(/\>/gm, '\\>')
.replace(/\n/g, newlineStyle === MarkdownStringTextNewlineStyle.Break ? '\\\n' : '\n\n');
return this;
@@ -101,7 +105,7 @@ export function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boo
export function escapeMarkdownSyntaxTokens(text: string): string {
// escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash
return text.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&');
return text.replace(/[\\`*_{}[\]()#+\-!]/g, '\\$&');
}
export function removeMarkdownEscapes(text: string): string {
-17
View File
@@ -1,17 +0,0 @@
{
"registrations": [
{
"component": {
"type": "git",
"git": {
"name": "insane",
"repositoryUrl": "https://github.com/bevacqua/insane",
"commitHash": "7f5a809f44a37e7d11ee5343b2d8bca4be6ba4ae"
}
},
"license": "MIT",
"version": "2.6.2"
}
],
"version": 1
}
-17
View File
@@ -1,17 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface InsaneOptions {
readonly allowedSchemes?: readonly string[],
readonly allowedTags?: readonly string[],
readonly allowedAttributes?: { readonly [key: string]: string[] },
readonly filter?: (token: { tag: string, attrs: { readonly [key: string]: string } }) => boolean,
}
export function insane(
html: string,
options?: InsaneOptions,
strict?: boolean,
): string;
-474
View File
@@ -1,474 +0,0 @@
/*
The Source EULA (MIT)
Copyright © 2015 Nicolas Bevacqua
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
let __insane_func;
(function () { function r(e, n, t) { function o(i, f) { if (!n[i]) { if (!e[i]) { var c = "function" == typeof require && require; if (!f && c) return c(i, !0); if (u) return u(i, !0); var a = new Error("Cannot find module '" + i + "'"); throw a.code = "MODULE_NOT_FOUND", a } var p = n[i] = { exports: {} }; e[i][0].call(p.exports, function (r) { var n = e[i][1][r]; return o(n || r) }, p, p.exports, r, e, n, t) } return n[i].exports } for (var u = "function" == typeof require && require, i = 0; i < t.length; i++)o(t[i]); return o } return r })()({
1: [function (require, module, exports) {
'use strict';
var toMap = require('./toMap');
var uris = ['background', 'base', 'cite', 'href', 'longdesc', 'src', 'usemap'];
module.exports = {
uris: toMap(uris) // attributes that have an href and hence need to be sanitized
};
}, { "./toMap": 10 }], 2: [function (require, module, exports) {
'use strict';
var defaults = {
allowedAttributes: {
'*': ['title', 'accesskey'],
a: ['href', 'name', 'target', 'aria-label'],
iframe: ['allowfullscreen', 'frameborder', 'src'],
img: ['src', 'alt', 'title', 'aria-label']
},
allowedClasses: {},
allowedSchemes: ['http', 'https', 'mailto'],
allowedTags: [
'a', 'abbr', 'article', 'b', 'blockquote', 'br', 'caption', 'code', 'del', 'details', 'div', 'em',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'main', 'mark',
'ol', 'p', 'pre', 'section', 'span', 'strike', 'strong', 'sub', 'summary', 'sup', 'table',
'tbody', 'td', 'th', 'thead', 'tr', 'u', 'ul'
],
filter: null
};
module.exports = defaults;
}, {}], 3: [function (require, module, exports) {
'use strict';
var toMap = require('./toMap');
var voids = ['area', 'br', 'col', 'hr', 'img', 'wbr', 'input', 'base', 'basefont', 'link', 'meta'];
module.exports = {
voids: toMap(voids)
};
}, { "./toMap": 10 }], 4: [function (require, module, exports) {
'use strict';
var he = require('he');
var assign = require('assignment');
var parser = require('./parser');
var sanitizer = require('./sanitizer');
var defaults = require('./defaults');
function insane(html, options, strict) {
var buffer = [];
var configuration = strict === true ? options : assign({}, defaults, options);
var handler = sanitizer(buffer, configuration);
parser(html, handler);
return buffer.join('');
}
insane.defaults = defaults;
module.exports = insane;
__insane_func = insane;
}, { "./defaults": 2, "./parser": 7, "./sanitizer": 8, "assignment": 6, "he": 9 }], 5: [function (require, module, exports) {
'use strict';
module.exports = function lowercase(string) {
return typeof string === 'string' ? string.toLowerCase() : string;
};
}, {}], 6: [function (require, module, exports) {
'use strict';
function assignment(result) {
var stack = Array.prototype.slice.call(arguments, 1);
var item;
var key;
while (stack.length) {
item = stack.shift();
for (key in item) {
if (item.hasOwnProperty(key)) {
if (Object.prototype.toString.call(result[key]) === '[object Object]') {
result[key] = assignment(result[key], item[key]);
} else {
result[key] = item[key];
}
}
}
}
return result;
}
module.exports = assignment;
}, {}], 7: [function (require, module, exports) {
'use strict';
var he = require('he');
var lowercase = require('./lowercase');
var attributes = require('./attributes');
var elements = require('./elements');
var rstart = /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/;
var rend = /^<\s*\/\s*([\w:-]+)[^>]*>/;
var rattrs = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g;
var rtag = /^</;
var rtagend = /^<\s*\//;
function createStack() {
var stack = [];
stack.lastItem = function lastItem() {
return stack[stack.length - 1];
};
return stack;
}
function parser(html, handler) {
var stack = createStack();
var last = html;
var chars;
while (html) {
parsePart();
}
parseEndTag(); // clean up any remaining tags
function parsePart() {
chars = true;
parseTag();
var same = html === last;
last = html;
if (same) { // discard, because it's invalid
html = '';
}
}
function parseTag() {
if (html.substr(0, 4) === '<!--') { // comments
parseComment();
} else if (rtagend.test(html)) {
parseEdge(rend, parseEndTag);
} else if (rtag.test(html)) {
parseEdge(rstart, parseStartTag);
}
parseTagDecode();
}
function parseEdge(regex, parser) {
var match = html.match(regex);
if (match) {
html = html.substring(match[0].length);
match[0].replace(regex, parser);
chars = false;
}
}
function parseComment() {
var index = html.indexOf('-->');
if (index >= 0) {
if (handler.comment) {
handler.comment(html.substring(4, index));
}
html = html.substring(index + 3);
chars = false;
}
}
function parseTagDecode() {
if (!chars) {
return;
}
var text;
var index = html.indexOf('<');
if (index >= 0) {
text = html.substring(0, index);
html = html.substring(index);
} else {
text = html;
html = '';
}
if (handler.chars) {
handler.chars(text);
}
}
function parseStartTag(tag, tagName, rest, unary) {
var attrs = {};
var low = lowercase(tagName);
var u = elements.voids[low] || !!unary;
rest.replace(rattrs, attrReplacer);
if (!u) {
stack.push(low);
}
if (handler.start) {
handler.start(low, attrs, u);
}
function attrReplacer(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) {
if (doubleQuotedValue === void 0 && singleQuotedValue === void 0 && unquotedValue === void 0) {
attrs[name] = void 0; // attribute is like <button disabled></button>
} else {
attrs[name] = he.decode(doubleQuotedValue || singleQuotedValue || unquotedValue || '');
}
}
}
function parseEndTag(tag, tagName) {
var i;
var pos = 0;
var low = lowercase(tagName);
if (low) {
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos] === low) {
break; // find the closest opened tag of the same type
}
}
}
if (pos >= 0) {
for (i = stack.length - 1; i >= pos; i--) {
if (handler.end) { // close all the open elements, up the stack
handler.end(stack[i]);
}
}
stack.length = pos;
}
}
}
module.exports = parser;
}, { "./attributes": 1, "./elements": 3, "./lowercase": 5, "he": 9 }], 8: [function (require, module, exports) {
'use strict';
var he = require('he');
var lowercase = require('./lowercase');
var attributes = require('./attributes');
var elements = require('./elements');
function sanitizer(buffer, options) {
var last;
var context;
var o = options || {};
reset();
return {
start: start,
end: end,
chars: chars
};
function out(value) {
buffer.push(value);
}
function start(tag, attrs, unary) {
var low = lowercase(tag);
if (context.ignoring) {
ignore(low); return;
}
if ((o.allowedTags || []).indexOf(low) === -1) {
ignore(low); return;
}
if (o.filter && !o.filter({ tag: low, attrs: attrs })) {
ignore(low); return;
}
out('<');
out(low);
Object.keys(attrs).forEach(parse);
out(unary ? '/>' : '>');
function parse(key) {
var value = attrs[key];
var classesOk = (o.allowedClasses || {})[low] || [];
var attrsOk = (o.allowedAttributes || {})[low] || [];
attrsOk = attrsOk.concat((o.allowedAttributes || {})['*'] || []);
var valid;
var lkey = lowercase(key);
if (lkey === 'class' && attrsOk.indexOf(lkey) === -1) {
value = value.split(' ').filter(isValidClass).join(' ').trim();
valid = value.length;
} else {
valid = attrsOk.indexOf(lkey) !== -1 && (attributes.uris[lkey] !== true || testUrl(value));
}
if (valid) {
out(' ');
out(key);
if (typeof value === 'string') {
out('="');
out(he.encode(value));
out('"');
}
}
function isValidClass(className) {
return classesOk && classesOk.indexOf(className) !== -1;
}
}
}
function end(tag) {
var low = lowercase(tag);
var allowed = (o.allowedTags || []).indexOf(low) !== -1;
if (allowed) {
if (context.ignoring === false) {
out('</');
out(low);
out('>');
} else {
unignore(low);
}
} else {
unignore(low);
}
}
function testUrl(text) {
var start = text[0];
if (start === '#' || start === '/') {
return true;
}
var colon = text.indexOf(':');
if (colon === -1) {
return true;
}
var questionmark = text.indexOf('?');
if (questionmark !== -1 && colon > questionmark) {
return true;
}
var hash = text.indexOf('#');
if (hash !== -1 && colon > hash) {
return true;
}
return o.allowedSchemes.some(matches);
function matches(scheme) {
return text.indexOf(scheme + ':') === 0;
}
}
function chars(text) {
if (context.ignoring === false) {
out(o.transformText ? o.transformText(text) : text);
}
}
function ignore(tag) {
if (elements.voids[tag]) {
return;
}
if (context.ignoring === false) {
context = { ignoring: tag, depth: 1 };
} else if (context.ignoring === tag) {
context.depth++;
}
}
function unignore(tag) {
if (context.ignoring === tag) {
if (--context.depth <= 0) {
reset();
}
}
}
function reset() {
context = { ignoring: false, depth: 0 };
}
}
module.exports = sanitizer;
}, { "./attributes": 1, "./elements": 3, "./lowercase": 5, "he": 9 }], 9: [function (require, module, exports) {
'use strict';
var escapes = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
};
var unescapes = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&#39;': "'"
};
var rescaped = /(&amp;|&lt;|&gt;|&quot;|&#39;)/g;
var runescaped = /[&<>"']/g;
function escapeHtmlChar(match) {
return escapes[match];
}
function unescapeHtmlChar(match) {
return unescapes[match];
}
function escapeHtml(text) {
return text == null ? '' : String(text).replace(runescaped, escapeHtmlChar);
}
function unescapeHtml(html) {
return html == null ? '' : String(html).replace(rescaped, unescapeHtmlChar);
}
escapeHtml.options = unescapeHtml.options = {};
module.exports = {
encode: escapeHtml,
escape: escapeHtml,
decode: unescapeHtml,
unescape: unescapeHtml,
version: '1.0.0-browser'
};
}, {}], 10: [function (require, module, exports) {
'use strict';
function toMap(list) {
return list.reduce(asKey, {});
}
function asKey(accumulator, item) {
accumulator[item] = true;
return accumulator;
}
module.exports = toMap;
}, {}]
}, {}, [4]);
// ESM-comment-begin
define(function() { return { insane: __insane_func }; });
// ESM-comment-end
// ESM-uncomment-begin
// export var insane = __insane_func;
// ESM-uncomment-end
@@ -1,20 +0,0 @@
The MIT License (MIT)
Copyright © 2015 Nicolas Bevacqua
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
File diff suppressed because it is too large Load Diff
+1 -7
View File
@@ -3,6 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Modifiers } from 'vs/base/common/keybindings';
import { OperatingSystem } from 'vs/base/common/platform';
import * as nls from 'vs/nls';
@@ -14,13 +15,6 @@ export interface ModifierLabels {
readonly separator: string;
}
export interface Modifiers {
readonly ctrlKey: boolean;
readonly shiftKey: boolean;
readonly altKey: boolean;
readonly metaKey: boolean;
}
export interface KeyLabelProvider<T extends Modifiers> {
(keybinding: T): string | null;
}
+2 -2
View File
@@ -3,9 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ChordKeybinding, Keybinding, KeyCodeUtils, SimpleKeybinding } from 'vs/base/common/keyCodes';
import { KeyCodeUtils, ScanCodeUtils } from 'vs/base/common/keyCodes';
import { ChordKeybinding, Keybinding, SimpleKeybinding, ScanCodeBinding } from 'vs/base/common/keybindings';
import { OperatingSystem } from 'vs/base/common/platform';
import { ScanCodeBinding, ScanCodeUtils } from 'vs/base/common/scanCode';
export class KeybindingParser {
+281
View File
@@ -0,0 +1,281 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { illegalArgument } from 'vs/base/common/errors';
import { KeyCode, ScanCode } from 'vs/base/common/keyCodes';
import { OperatingSystem } from 'vs/base/common/platform';
/**
* Binary encoding strategy:
* ```
* 1111 11
* 5432 1098 7654 3210
* ---- CSAW KKKK KKKK
* C = bit 11 = ctrlCmd flag
* S = bit 10 = shift flag
* A = bit 9 = alt flag
* W = bit 8 = winCtrl flag
* K = bits 0-7 = key code
* ```
*/
const enum BinaryKeybindingsMask {
CtrlCmd = (1 << 11) >>> 0,
Shift = (1 << 10) >>> 0,
Alt = (1 << 9) >>> 0,
WinCtrl = (1 << 8) >>> 0,
KeyCode = 0x000000FF
}
export function createKeybinding(keybinding: number, OS: OperatingSystem): Keybinding | null {
if (keybinding === 0) {
return null;
}
const firstPart = (keybinding & 0x0000FFFF) >>> 0;
const chordPart = (keybinding & 0xFFFF0000) >>> 16;
if (chordPart !== 0) {
return new ChordKeybinding([
createSimpleKeybinding(firstPart, OS),
createSimpleKeybinding(chordPart, OS)
]);
}
return new ChordKeybinding([createSimpleKeybinding(firstPart, OS)]);
}
export function createSimpleKeybinding(keybinding: number, OS: OperatingSystem): SimpleKeybinding {
const ctrlCmd = (keybinding & BinaryKeybindingsMask.CtrlCmd ? true : false);
const winCtrl = (keybinding & BinaryKeybindingsMask.WinCtrl ? true : false);
const ctrlKey = (OS === OperatingSystem.Macintosh ? winCtrl : ctrlCmd);
const shiftKey = (keybinding & BinaryKeybindingsMask.Shift ? true : false);
const altKey = (keybinding & BinaryKeybindingsMask.Alt ? true : false);
const metaKey = (OS === OperatingSystem.Macintosh ? ctrlCmd : winCtrl);
const keyCode = (keybinding & BinaryKeybindingsMask.KeyCode);
return new SimpleKeybinding(ctrlKey, shiftKey, altKey, metaKey, keyCode);
}
export interface Modifiers {
readonly ctrlKey: boolean;
readonly shiftKey: boolean;
readonly altKey: boolean;
readonly metaKey: boolean;
}
export interface IBaseKeybinding extends Modifiers {
isDuplicateModifierCase(): boolean;
}
export class SimpleKeybinding implements IBaseKeybinding {
public readonly ctrlKey: boolean;
public readonly shiftKey: boolean;
public readonly altKey: boolean;
public readonly metaKey: boolean;
public readonly keyCode: KeyCode;
constructor(ctrlKey: boolean, shiftKey: boolean, altKey: boolean, metaKey: boolean, keyCode: KeyCode) {
this.ctrlKey = ctrlKey;
this.shiftKey = shiftKey;
this.altKey = altKey;
this.metaKey = metaKey;
this.keyCode = keyCode;
}
public equals(other: SimpleKeybinding): boolean {
return (
this.ctrlKey === other.ctrlKey
&& this.shiftKey === other.shiftKey
&& this.altKey === other.altKey
&& this.metaKey === other.metaKey
&& this.keyCode === other.keyCode
);
}
public getHashCode(): string {
const ctrl = this.ctrlKey ? '1' : '0';
const shift = this.shiftKey ? '1' : '0';
const alt = this.altKey ? '1' : '0';
const meta = this.metaKey ? '1' : '0';
return `${ctrl}${shift}${alt}${meta}${this.keyCode}`;
}
public isModifierKey(): boolean {
return (
this.keyCode === KeyCode.Unknown
|| this.keyCode === KeyCode.Ctrl
|| this.keyCode === KeyCode.Meta
|| this.keyCode === KeyCode.Alt
|| this.keyCode === KeyCode.Shift
);
}
public toChord(): ChordKeybinding {
return new ChordKeybinding([this]);
}
/**
* Does this keybinding refer to the key code of a modifier and it also has the modifier flag?
*/
public isDuplicateModifierCase(): boolean {
return (
(this.ctrlKey && this.keyCode === KeyCode.Ctrl)
|| (this.shiftKey && this.keyCode === KeyCode.Shift)
|| (this.altKey && this.keyCode === KeyCode.Alt)
|| (this.metaKey && this.keyCode === KeyCode.Meta)
);
}
}
export class ChordKeybinding {
public readonly parts: SimpleKeybinding[];
constructor(parts: SimpleKeybinding[]) {
if (parts.length === 0) {
throw illegalArgument(`parts`);
}
this.parts = parts;
}
public getHashCode(): string {
let result = '';
for (let i = 0, len = this.parts.length; i < len; i++) {
if (i !== 0) {
result += ';';
}
result += this.parts[i].getHashCode();
}
return result;
}
public equals(other: ChordKeybinding | null): boolean {
if (other === null) {
return false;
}
if (this.parts.length !== other.parts.length) {
return false;
}
for (let i = 0; i < this.parts.length; i++) {
if (!this.parts[i].equals(other.parts[i])) {
return false;
}
}
return true;
}
}
export type Keybinding = ChordKeybinding;
export class ScanCodeBinding implements IBaseKeybinding {
public readonly ctrlKey: boolean;
public readonly shiftKey: boolean;
public readonly altKey: boolean;
public readonly metaKey: boolean;
public readonly scanCode: ScanCode;
constructor(ctrlKey: boolean, shiftKey: boolean, altKey: boolean, metaKey: boolean, scanCode: ScanCode) {
this.ctrlKey = ctrlKey;
this.shiftKey = shiftKey;
this.altKey = altKey;
this.metaKey = metaKey;
this.scanCode = scanCode;
}
public equals(other: ScanCodeBinding): boolean {
return (
this.ctrlKey === other.ctrlKey
&& this.shiftKey === other.shiftKey
&& this.altKey === other.altKey
&& this.metaKey === other.metaKey
&& this.scanCode === other.scanCode
);
}
/**
* Does this keybinding refer to the key code of a modifier and it also has the modifier flag?
*/
public isDuplicateModifierCase(): boolean {
return (
(this.ctrlKey && (this.scanCode === ScanCode.ControlLeft || this.scanCode === ScanCode.ControlRight))
|| (this.shiftKey && (this.scanCode === ScanCode.ShiftLeft || this.scanCode === ScanCode.ShiftRight))
|| (this.altKey && (this.scanCode === ScanCode.AltLeft || this.scanCode === ScanCode.AltRight))
|| (this.metaKey && (this.scanCode === ScanCode.MetaLeft || this.scanCode === ScanCode.MetaRight))
);
}
}
export class ResolvedKeybindingPart {
readonly ctrlKey: boolean;
readonly shiftKey: boolean;
readonly altKey: boolean;
readonly metaKey: boolean;
readonly keyLabel: string | null;
readonly keyAriaLabel: string | null;
constructor(ctrlKey: boolean, shiftKey: boolean, altKey: boolean, metaKey: boolean, kbLabel: string | null, kbAriaLabel: string | null) {
this.ctrlKey = ctrlKey;
this.shiftKey = shiftKey;
this.altKey = altKey;
this.metaKey = metaKey;
this.keyLabel = kbLabel;
this.keyAriaLabel = kbAriaLabel;
}
}
export type KeybindingModifier = 'ctrl' | 'shift' | 'alt' | 'meta';
/**
* A resolved keybinding. Can be a simple keybinding or a chord keybinding.
*/
export abstract class ResolvedKeybinding {
/**
* This prints the binding in a format suitable for displaying in the UI.
*/
public abstract getLabel(): string | null;
/**
* This prints the binding in a format suitable for ARIA.
*/
public abstract getAriaLabel(): string | null;
/**
* This prints the binding in a format suitable for electron's accelerators.
* See https://github.com/electron/electron/blob/master/docs/api/accelerator.md
*/
public abstract getElectronAccelerator(): string | null;
/**
* This prints the binding in a format suitable for user settings.
*/
public abstract getUserSettingsLabel(): string | null;
/**
* Is the user settings label reflecting the label?
*/
public abstract isWYSIWYG(): boolean;
/**
* Is the binding a chord?
*/
public abstract isChord(): boolean;
/**
* Returns the parts that comprise of the keybinding.
* Simple keybindings return one element.
*/
public abstract getParts(): ResolvedKeybindingPart[];
/**
* Returns the parts that should be used for dispatching.
* Returns null for parts consisting of only modifier keys
* @example keybinding "Shift" -> null
* @example keybinding ("D" with shift == true) -> "shift+D"
*/
public abstract getDispatchParts(): (string | null)[];
/**
* Returns the parts that should be used for dispatching single modifier keys
* Returns null for parts that contain more than one modifier or a regular key.
* @example keybinding "Shift" -> "shift"
* @example keybinding ("D" with shift == true") -> null
*/
public abstract getSingleModifierDispatchParts(): (KeybindingModifier | null)[];
}
+9
View File
@@ -394,3 +394,12 @@ export class ImmortalReference<T> implements IReference<T> {
constructor(public object: T) { }
dispose(): void { /* noop */ }
}
export function disposeOnReturn(fn: (store: DisposableStore) => void): void {
const store = new DisposableStore();
try {
fn(store);
} finally {
store.dispose();
}
}
+276 -56
View File
@@ -3,6 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { shuffle } from 'vs/base/common/arrays';
import { CharCode } from 'vs/base/common/charCode';
import { compare, compareIgnoreCase, compareSubstring, compareSubstringIgnoreCase } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
@@ -263,9 +264,10 @@ export class UriIterator implements IKeyIterator<URI> {
}
class TernarySearchTreeNode<K, V> {
height: number = 1;
segment!: string;
value: V | undefined;
key!: K;
key: K | undefined;
left: TernarySearchTreeNode<K, V> | undefined;
mid: TernarySearchTreeNode<K, V> | undefined;
right: TernarySearchTreeNode<K, V> | undefined;
@@ -273,6 +275,46 @@ class TernarySearchTreeNode<K, V> {
isEmpty(): boolean {
return !this.left && !this.mid && !this.right && !this.value;
}
rotateLeft() {
const tmp = this.right!;
this.right = tmp.left;
tmp.left = this;
this.updateHeight();
tmp.updateHeight();
return tmp;
}
rotateRight() {
const tmp = this.left!;
this.left = tmp.right;
tmp.right = this;
this.updateHeight();
tmp.updateHeight();
return tmp;
}
updateHeight() {
this.height = 1 + Math.max(this.heightLeft, this.heightRight);
}
balanceFactor() {
return this.heightRight - this.heightLeft;
}
get heightLeft() {
return this.left?.height ?? 0;
}
get heightRight() {
return this.right?.height ?? 0;
}
}
const enum Dir {
Left = -1,
Mid = 0,
Right = 1,
}
export class TernarySearchTree<K, V> {
@@ -304,6 +346,30 @@ export class TernarySearchTree<K, V> {
this._root = undefined;
}
/**
* Fill the tree with the same value of the given keys
*/
fill(element: V, keys: readonly K[]): void;
/**
* Fill the tree with given [key,value]-tuples
*/
fill(values: readonly [K, V][]): void;
fill(values: readonly [K, V][] | V, keys?: readonly K[]): void {
if (keys) {
const arr = keys.slice(0);
shuffle(arr);
for (let k of arr) {
this.set(k, (<V>values));
}
} else {
const arr = (<[K, V][]>values).slice(0);
shuffle(arr);
for (let entry of arr) {
this.set(entry[0], entry[1]);
}
}
}
set(key: K, element: V): V | undefined {
const iter = this._iter.reset(key);
let node: TernarySearchTreeNode<K, V>;
@@ -312,7 +378,9 @@ export class TernarySearchTree<K, V> {
this._root = new TernarySearchTreeNode<K, V>();
this._root.segment = iter.value();
}
const stack: [Dir, TernarySearchTreeNode<K, V>][] = [];
// find insert_node
node = this._root;
while (true) {
const val = iter.cmp(node.segment);
@@ -322,6 +390,7 @@ export class TernarySearchTree<K, V> {
node.left = new TernarySearchTreeNode<K, V>();
node.left.segment = iter.value();
}
stack.push([Dir.Left, node]);
node = node.left;
} else if (val < 0) {
@@ -330,6 +399,7 @@ export class TernarySearchTree<K, V> {
node.right = new TernarySearchTreeNode<K, V>();
node.right.segment = iter.value();
}
stack.push([Dir.Right, node]);
node = node.right;
} else if (iter.hasNext()) {
@@ -339,14 +409,71 @@ export class TernarySearchTree<K, V> {
node.mid = new TernarySearchTreeNode<K, V>();
node.mid.segment = iter.value();
}
stack.push([Dir.Mid, node]);
node = node.mid;
} else {
break;
}
}
// set value
const oldElement = node.value;
node.value = element;
node.key = key;
// balance
for (let i = stack.length - 1; i >= 0; i--) {
const node = stack[i][1];
node.updateHeight();
const bf = node.balanceFactor();
if (bf < -1 || bf > 1) {
// needs rotate
const d1 = stack[i][0];
const d2 = stack[i + 1][0];
if (d1 === Dir.Right && d2 === Dir.Right) {
//right, right -> rotate left
stack[i][1] = node.rotateLeft();
} else if (d1 === Dir.Left && d2 === Dir.Left) {
// left, left -> rotate right
stack[i][1] = node.rotateRight();
} else if (d1 === Dir.Right && d2 === Dir.Left) {
// right, left -> double rotate right, left
node.right = stack[i + 1][1] = stack[i + 1][1].rotateRight();
stack[i][1] = node.rotateLeft();
} else if (d1 === Dir.Left && d2 === Dir.Right) {
// left, right -> double rotate left, right
node.left = stack[i + 1][1] = stack[i + 1][1].rotateLeft();
stack[i][1] = node.rotateRight();
} else {
throw new Error();
}
// patch path to parent
if (i > 0) {
switch (stack[i - 1][0]) {
case Dir.Left:
stack[i - 1][1].left = stack[i][1];
break;
case Dir.Right:
stack[i - 1][1].right = stack[i][1];
break;
case Dir.Mid:
stack[i - 1][1].mid = stack[i][1];
break;
}
} else {
this._root = stack[0][1];
}
}
}
return oldElement;
}
@@ -391,49 +518,127 @@ export class TernarySearchTree<K, V> {
private _delete(key: K, superStr: boolean): void {
const iter = this._iter.reset(key);
const stack: [-1 | 0 | 1, TernarySearchTreeNode<K, V>][] = [];
const stack: [Dir, TernarySearchTreeNode<K, V>][] = [];
let node = this._root;
// find and unset node
// find node
while (node) {
const val = iter.cmp(node.segment);
if (val > 0) {
// left
stack.push([1, node]);
stack.push([Dir.Left, node]);
node = node.left;
} else if (val < 0) {
// right
stack.push([-1, node]);
stack.push([Dir.Right, node]);
node = node.right;
} else if (iter.hasNext()) {
// mid
iter.next();
stack.push([0, node]);
stack.push([Dir.Mid, node]);
node = node.mid;
} else {
if (superStr) {
// remove children
node.left = undefined;
node.mid = undefined;
node.right = undefined;
} else {
// remove element
node.value = undefined;
}
// clean up empty nodes
while (stack.length > 0 && node.isEmpty()) {
let [dir, parent] = stack.pop()!;
switch (dir) {
case 1: parent.left = undefined; break;
case 0: parent.mid = undefined; break;
case -1: parent.right = undefined; break;
}
node = parent;
}
break;
}
}
if (!node) {
// node not found
return;
}
if (superStr) {
// removing children, reset height
node.left = undefined;
node.mid = undefined;
node.right = undefined;
node.height = 1;
} else {
// removing element
node.key = undefined;
node.value = undefined;
}
// BST node removal
if (!node.mid && !node.value) {
if (node.left && node.right) {
// full node
const min = this._min(node.right);
const { key, value, segment } = min;
this._delete(min.key!, false);
node.key = key;
node.value = value;
node.segment = segment;
} else {
// empty or half empty
const newChild = node.left ?? node.right;
if (stack.length > 0) {
const [dir, parent] = stack[stack.length - 1];
switch (dir) {
case Dir.Left: parent.left = newChild; break;
case Dir.Mid: parent.mid = newChild; break;
case Dir.Right: parent.right = newChild; break;
}
} else {
this._root = newChild;
}
}
}
// AVL balance
for (let i = stack.length - 1; i >= 0; i--) {
const node = stack[i][1];
node.updateHeight();
const bf = node.balanceFactor();
if (bf > 1) {
// right heavy
if (node.right!.balanceFactor() >= 0) {
// right, right -> rotate left
stack[i][1] = node.rotateLeft();
} else {
// right, left -> double rotate
node.right = stack[i + 1][1] = stack[i + 1][1].rotateRight();
stack[i][1] = node.rotateLeft();
}
} else if (bf < -1) {
// left heavy
if (node.left!.balanceFactor() <= 0) {
// left, left -> rotate right
stack[i][1] = node.rotateRight();
} else {
// left, right -> double rotate
node.left = stack[i + 1][1] = stack[i + 1][1].rotateLeft();
stack[i][1] = node.rotateRight();
}
}
// patch path to parent
if (i > 0) {
switch (stack[i - 1][0]) {
case Dir.Left:
stack[i - 1][1].left = stack[i][1];
break;
case Dir.Right:
stack[i - 1][1].right = stack[i][1];
break;
case Dir.Mid:
stack[i - 1][1].mid = stack[i][1];
break;
}
} else {
this._root = stack[0][1];
}
}
}
private _min(node: TernarySearchTreeNode<K, V>): TernarySearchTreeNode<K, V> {
while (node.left) {
node = node.left;
}
return node;
}
findSubstr(key: K): V | undefined {
@@ -502,24 +707,33 @@ export class TernarySearchTree<K, V> {
if (!node) {
return;
}
const stack = [node];
while (stack.length > 0) {
const node = stack.pop();
if (node) {
if (node.value) {
yield [node.key, node.value];
}
if (node.left) {
stack.push(node.left);
}
if (node.mid) {
stack.push(node.mid);
}
if (node.right) {
stack.push(node.right);
}
}
if (node.left) {
yield* this._entries(node.left);
}
if (node.value) {
yield [node.key!, node.value];
}
if (node.mid) {
yield* this._entries(node.mid);
}
if (node.right) {
yield* this._entries(node.right);
}
}
// for debug/testing
_isBalanced(): boolean {
const nodeIsBalanced = (node: TernarySearchTreeNode<any, any> | undefined): boolean => {
if (!node) {
return true;
}
const bf = node.balanceFactor();
if (bf < -1 || bf > 1) {
return false;
}
return nodeIsBalanced(node.left) && nodeIsBalanced(node.right);
};
return nodeIsBalanced(this._root);
}
}
@@ -527,13 +741,17 @@ interface ResourceMapKeyFn {
(resource: URI): string;
}
class ResourceMapEntry<T> {
constructor(readonly uri: URI, readonly value: T) { }
}
export class ResourceMap<T> implements Map<URI, T> {
private static readonly defaultToKey = (resource: URI) => resource.toString();
readonly [Symbol.toStringTag] = 'ResourceMap';
private readonly map: Map<string, T>;
private readonly map: Map<string, ResourceMapEntry<T>>;
private readonly toKey: ResourceMapKeyFn;
/**
@@ -560,12 +778,12 @@ export class ResourceMap<T> implements Map<URI, T> {
}
set(resource: URI, value: T): this {
this.map.set(this.toKey(resource), value);
this.map.set(this.toKey(resource), new ResourceMapEntry(resource, value));
return this;
}
get(resource: URI): T | undefined {
return this.map.get(this.toKey(resource));
return this.map.get(this.toKey(resource))?.value;
}
has(resource: URI): boolean {
@@ -588,30 +806,32 @@ export class ResourceMap<T> implements Map<URI, T> {
if (typeof thisArg !== 'undefined') {
clb = clb.bind(thisArg);
}
for (let [index, value] of this.map) {
clb(value, URI.parse(index), <any>this);
for (let [_, entry] of this.map) {
clb(entry.value, entry.uri, <any>this);
}
}
values(): IterableIterator<T> {
return this.map.values();
*values(): IterableIterator<T> {
for (let entry of this.map.values()) {
yield entry.value;
}
}
*keys(): IterableIterator<URI> {
for (let key of this.map.keys()) {
yield URI.parse(key);
for (let entry of this.map.values()) {
yield entry.uri;
}
}
*entries(): IterableIterator<[URI, T]> {
for (let tuple of this.map.entries()) {
yield [URI.parse(tuple[0]), tuple[1]];
for (let entry of this.map.values()) {
yield [entry.uri, entry.value];
}
}
*[Symbol.iterator](): IterableIterator<[URI, T]> {
for (let item of this.map) {
yield [URI.parse(item[0]), item[1]];
for (let [, entry] of this.map) {
yield [entry.uri, entry.value];
}
}
}
+2 -2
View File
@@ -6,11 +6,11 @@
"git": {
"name": "marked",
"repositoryUrl": "https://github.com/markedjs/marked",
"commitHash": "8cfa29ccd2a2759e8e60fe0d8d6df8c022beda4e"
"commitHash": "d1b7d521c41bcf915f81f0218b0e5acd607c1b72"
}
},
"license": "MIT",
"version": "1.1.0"
"version": "3.0.2"
}
],
"version": 1
File diff suppressed because one or more lines are too long
+5 -4
View File
@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { match } from 'vs/base/common/glob';
import { ParsedPattern, parse } from 'vs/base/common/glob';
import { Schemas } from 'vs/base/common/network';
import { basename, extname, posix } from 'vs/base/common/path';
import { DataUri } from 'vs/base/common/resources';
@@ -15,6 +15,7 @@ export namespace Mimes {
export const binary = 'application/octet-stream';
export const unknown = 'application/unknown';
export const markdown = 'text/markdown';
export const latex = 'text/latex';
}
export interface ITextMimeAssociation {
@@ -30,7 +31,7 @@ export interface ITextMimeAssociation {
interface ITextMimeAssociationItem extends ITextMimeAssociation {
readonly filenameLowercase?: string;
readonly extensionLowercase?: string;
readonly filepatternLowercase?: string;
readonly filepatternLowercase?: ParsedPattern;
readonly filepatternOnPath?: boolean;
}
@@ -89,7 +90,7 @@ function toTextMimeAssociationItem(association: ITextMimeAssociation): ITextMime
userConfigured: association.userConfigured,
filenameLowercase: association.filename ? association.filename.toLowerCase() : undefined,
extensionLowercase: association.extension ? association.extension.toLowerCase() : undefined,
filepatternLowercase: association.filepattern ? association.filepattern.toLowerCase() : undefined,
filepatternLowercase: association.filepattern ? parse(association.filepattern.toLowerCase()) : undefined,
filepatternOnPath: association.filepattern ? association.filepattern.indexOf(posix.sep) >= 0 : false
};
}
@@ -178,7 +179,7 @@ function guessMimeTypeByPath(path: string, filename: string, associations: IText
if (association.filepattern) {
if (!patternMatch || association.filepattern.length > patternMatch.filepattern!.length) {
const target = association.filepatternOnPath ? path : filename; // match on full path if pattern contains path separator
if (match(association.filepatternLowercase!, target)) {
if (association.filepatternLowercase?.(target)) {
patternMatch = association;
}
}
+12 -12
View File
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
// NOTE: VSCode's copy of nodejs path library to be usable in common (non-node) namespace
// Copied from: https://github.com/nodejs/node/blob/v12.8.1/lib/path.js
// Copied from: https://github.com/nodejs/node/blob/v14.16.0/lib/path.js
/**
* Copyright Joyent, Inc. and other Node contributors.
@@ -78,8 +78,8 @@ function isPosixPathSeparator(code: number | undefined) {
}
function isWindowsDeviceRoot(code: number) {
return code >= CHAR_UPPERCASE_A && code <= CHAR_UPPERCASE_Z ||
code >= CHAR_LOWERCASE_A && code <= CHAR_LOWERCASE_Z;
return (code >= CHAR_UPPERCASE_A && code <= CHAR_UPPERCASE_Z) ||
(code >= CHAR_LOWERCASE_A && code <= CHAR_LOWERCASE_Z);
}
// Resolves . and .. elements in a path with directory names
@@ -220,8 +220,8 @@ export const win32: IPath = {
// Verify that a cwd was found and that it actually points
// to our drive. If not, default to the drive's root.
if (path === undefined ||
path.slice(0, 2).toLowerCase() !== resolvedDevice.toLowerCase() &&
path.charCodeAt(2) === CHAR_BACKWARD_SLASH) {
(path.slice(0, 2).toLowerCase() !== resolvedDevice.toLowerCase() &&
path.charCodeAt(2) === CHAR_BACKWARD_SLASH)) {
path = `${resolvedDevice}\\`;
}
}
@@ -429,10 +429,10 @@ export const win32: IPath = {
const code = path.charCodeAt(0);
return isPathSeparator(code) ||
// Possible device root
len > 2 &&
isWindowsDeviceRoot(code) &&
path.charCodeAt(1) === CHAR_COLON &&
isPathSeparator(path.charCodeAt(2));
(len > 2 &&
isWindowsDeviceRoot(code) &&
path.charCodeAt(1) === CHAR_COLON &&
isPathSeparator(path.charCodeAt(2)));
},
join(...paths: string[]): string {
@@ -460,14 +460,14 @@ export const win32: IPath = {
}
// Make sure that the joined path doesn't start with two slashes, because
// normalize() will mistake it for an UNC path then.
// normalize() will mistake it for a UNC path then.
//
// This step is skipped when it is very clear that the user actually
// intended to point at an UNC path. This is assumed when the first
// intended to point at a UNC path. This is assumed when the first
// non-empty string arguments starts with exactly two slashes followed by
// at least one more non-slash character.
//
// Note that for normalize() to treat a path as an UNC path it needs to
// Note that for normalize() to treat a path as a UNC path it needs to
// have at least 2 components, so we don't filter for that here.
// This means that the user can use join to construct UNC paths from
// a server name and a share name; for example:
+1
View File
@@ -36,6 +36,7 @@ export interface IProcessEnvironment {
*/
export interface INodeProcess {
platform: string;
arch: string;
env: IProcessEnvironment;
nextTick?: (callback: (...args: any[]) => void) => void;
versions?: {
+11 -1
View File
@@ -5,7 +5,7 @@
import { globals, INodeProcess, isMacintosh, isWindows, setImmediate } from 'vs/base/common/platform';
let safeProcess: INodeProcess & { nextTick: (callback: (...args: any[]) => void) => void; };
let safeProcess: Omit<INodeProcess, 'arch'> & { nextTick: (callback: (...args: any[]) => void) => void; arch: string | undefined; };
declare const process: INodeProcess;
// Native sandbox environment
@@ -13,6 +13,7 @@ if (typeof globals.vscode !== 'undefined' && typeof globals.vscode.process !== '
const sandboxProcess: INodeProcess = globals.vscode.process;
safeProcess = {
get platform() { return sandboxProcess.platform; },
get arch() { return sandboxProcess.arch; },
get env() { return sandboxProcess.env; },
cwd() { return sandboxProcess.cwd(); },
nextTick(callback: (...args: any[]) => void): void { return setImmediate(callback); }
@@ -23,6 +24,7 @@ if (typeof globals.vscode !== 'undefined' && typeof globals.vscode.process !== '
else if (typeof process !== 'undefined') {
safeProcess = {
get platform() { return process.platform; },
get arch() { return process.arch; },
get env() { return process.env; },
cwd() { return process.env['VSCODE_CWD'] || process.cwd(); },
nextTick(callback: (...args: any[]) => void): void { return process.nextTick!(callback); }
@@ -35,6 +37,7 @@ else {
// Supported
get platform() { return isWindows ? 'win32' : isMacintosh ? 'darwin' : 'linux'; },
get arch() { return undefined; /* arch is undefined in web */ },
nextTick(callback: (...args: any[]) => void): void { return setImmediate(callback); },
// Unsupported
@@ -70,3 +73,10 @@ export const platform = safeProcess.platform;
* environments.
*/
export const nextTick = safeProcess.nextTick;
/**
* Provides safe access to the `arch` method in node.js, sandboxed or web
* environments.
* Note: `arch` is `undefined` in web
*/
export const arch = safeProcess.arch;
+8 -3
View File
@@ -53,6 +53,7 @@ export interface IProductConfiguration {
readonly updateUrl?: string;
readonly webEndpointUrl?: string;
readonly webEndpointUrlTemplate?: string;
readonly webviewContentExternalBaseUrlTemplate?: string;
readonly target?: string;
readonly settingsSearchBuildId?: number;
@@ -82,6 +83,7 @@ export interface IProductConfiguration {
readonly remoteExtensionTips?: { [remoteName: string]: IRemoteExtensionTip; };
readonly extensionKeywords?: { [extension: string]: readonly string[]; };
readonly keymapExtensionTips?: readonly string[];
readonly webExtensionTips?: readonly string[];
readonly languageExtensionTips?: readonly string[];
readonly trustedExtensionUrlPublicKeys?: { [id: string]: string[]; };
@@ -97,6 +99,7 @@ export interface IProductConfiguration {
};
readonly enableTelemetry?: boolean;
readonly openToWelcomeMainPage?: boolean;
readonly aiConfig?: {
readonly asimovKey: string;
};
@@ -120,7 +123,10 @@ export interface IProductConfiguration {
readonly reportMarketplaceIssueUrl?: string;
readonly licenseUrl?: string;
readonly privacyStatementUrl?: string;
readonly telemetryOptOutUrl?: string;
readonly telemetryOptOutUrl?: string; // {{SQL CARBON EDIT}} add back
readonly showTelemetryOptOut?: boolean;
readonly serverGreeting: string[];
readonly npsSurveyUrl?: string;
readonly cesSurveyUrl?: string;
@@ -146,8 +152,6 @@ export interface IProductConfiguration {
readonly 'configurationSync.store'?: ConfigurationSyncStore;
readonly darwinUniversalAssetId?: string;
readonly webviewContentExternalBaseUrlTemplate?: string;
}
export type ImportantExtensionTip = { name: string; languages?: string[]; pattern?: string; isExtensionPack?: boolean };
@@ -162,6 +166,7 @@ export interface IAppCenterConfiguration {
export interface IConfigBasedExtensionTip {
configPath: string;
configName: string;
configScheme?: string;
recommendations: IStringDictionary<{ name: string, remotes?: string[], important?: boolean, isExtensionPack?: boolean }>;
}
+1 -3
View File
@@ -195,9 +195,7 @@ export class ExtUri implements IExtUri {
}
extname(resource: URI): string {
const resourceExt = paths.posix.extname(resource.path);
const queryStringLocation = resourceExt.indexOf('?');
return queryStringLocation !== -1 ? resourceExt.substr(0, queryStringLocation) : resourceExt;
return paths.posix.extname(resource.path);
}
dirname(resource: URI): URI {
-690
View File
@@ -1,690 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { KeyCode } from 'vs/base/common/keyCodes';
/**
* keyboardEvent.code
*/
export const enum ScanCode {
DependsOnKbLayout = -1,
None,
Hyper,
Super,
Fn,
FnLock,
Suspend,
Resume,
Turbo,
Sleep,
WakeUp,
KeyA,
KeyB,
KeyC,
KeyD,
KeyE,
KeyF,
KeyG,
KeyH,
KeyI,
KeyJ,
KeyK,
KeyL,
KeyM,
KeyN,
KeyO,
KeyP,
KeyQ,
KeyR,
KeyS,
KeyT,
KeyU,
KeyV,
KeyW,
KeyX,
KeyY,
KeyZ,
Digit1,
Digit2,
Digit3,
Digit4,
Digit5,
Digit6,
Digit7,
Digit8,
Digit9,
Digit0,
Enter,
Escape,
Backspace,
Tab,
Space,
Minus,
Equal,
BracketLeft,
BracketRight,
Backslash,
IntlHash,
Semicolon,
Quote,
Backquote,
Comma,
Period,
Slash,
CapsLock,
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
PrintScreen,
ScrollLock,
Pause,
Insert,
Home,
PageUp,
Delete,
End,
PageDown,
ArrowRight,
ArrowLeft,
ArrowDown,
ArrowUp,
NumLock,
NumpadDivide,
NumpadMultiply,
NumpadSubtract,
NumpadAdd,
NumpadEnter,
Numpad1,
Numpad2,
Numpad3,
Numpad4,
Numpad5,
Numpad6,
Numpad7,
Numpad8,
Numpad9,
Numpad0,
NumpadDecimal,
IntlBackslash,
ContextMenu,
Power,
NumpadEqual,
F13,
F14,
F15,
F16,
F17,
F18,
F19,
F20,
F21,
F22,
F23,
F24,
Open,
Help,
Select,
Again,
Undo,
Cut,
Copy,
Paste,
Find,
AudioVolumeMute,
AudioVolumeUp,
AudioVolumeDown,
NumpadComma,
IntlRo,
KanaMode,
IntlYen,
Convert,
NonConvert,
Lang1,
Lang2,
Lang3,
Lang4,
Lang5,
Abort,
Props,
NumpadParenLeft,
NumpadParenRight,
NumpadBackspace,
NumpadMemoryStore,
NumpadMemoryRecall,
NumpadMemoryClear,
NumpadMemoryAdd,
NumpadMemorySubtract,
NumpadClear,
NumpadClearEntry,
ControlLeft,
ShiftLeft,
AltLeft,
MetaLeft,
ControlRight,
ShiftRight,
AltRight,
MetaRight,
BrightnessUp,
BrightnessDown,
MediaPlay,
MediaRecord,
MediaFastForward,
MediaRewind,
MediaTrackNext,
MediaTrackPrevious,
MediaStop,
Eject,
MediaPlayPause,
MediaSelect,
LaunchMail,
LaunchApp2,
LaunchApp1,
SelectTask,
LaunchScreenSaver,
BrowserSearch,
BrowserHome,
BrowserBack,
BrowserForward,
BrowserStop,
BrowserRefresh,
BrowserFavorites,
ZoomToggle,
MailReply,
MailForward,
MailSend,
MAX_VALUE
}
const scanCodeIntToStr: string[] = [];
const scanCodeStrToInt: { [code: string]: number; } = Object.create(null);
const scanCodeLowerCaseStrToInt: { [code: string]: number; } = Object.create(null);
export const ScanCodeUtils = {
lowerCaseToEnum: (scanCode: string) => scanCodeLowerCaseStrToInt[scanCode] || ScanCode.None,
toEnum: (scanCode: string) => scanCodeStrToInt[scanCode] || ScanCode.None,
toString: (scanCode: ScanCode) => scanCodeIntToStr[scanCode] || 'None'
};
/**
* -1 if a ScanCode => KeyCode mapping depends on kb layout.
*/
export const IMMUTABLE_CODE_TO_KEY_CODE: KeyCode[] = [];
/**
* -1 if a KeyCode => ScanCode mapping depends on kb layout.
*/
export const IMMUTABLE_KEY_CODE_TO_CODE: ScanCode[] = [];
export class ScanCodeBinding {
public readonly ctrlKey: boolean;
public readonly shiftKey: boolean;
public readonly altKey: boolean;
public readonly metaKey: boolean;
public readonly scanCode: ScanCode;
constructor(ctrlKey: boolean, shiftKey: boolean, altKey: boolean, metaKey: boolean, scanCode: ScanCode) {
this.ctrlKey = ctrlKey;
this.shiftKey = shiftKey;
this.altKey = altKey;
this.metaKey = metaKey;
this.scanCode = scanCode;
}
public equals(other: ScanCodeBinding): boolean {
return (
this.ctrlKey === other.ctrlKey
&& this.shiftKey === other.shiftKey
&& this.altKey === other.altKey
&& this.metaKey === other.metaKey
&& this.scanCode === other.scanCode
);
}
/**
* Does this keybinding refer to the key code of a modifier and it also has the modifier flag?
*/
public isDuplicateModifierCase(): boolean {
return (
(this.ctrlKey && (this.scanCode === ScanCode.ControlLeft || this.scanCode === ScanCode.ControlRight))
|| (this.shiftKey && (this.scanCode === ScanCode.ShiftLeft || this.scanCode === ScanCode.ShiftRight))
|| (this.altKey && (this.scanCode === ScanCode.AltLeft || this.scanCode === ScanCode.AltRight))
|| (this.metaKey && (this.scanCode === ScanCode.MetaLeft || this.scanCode === ScanCode.MetaRight))
);
}
}
(function () {
function d(intScanCode: ScanCode, strScanCode: string): void {
scanCodeIntToStr[intScanCode] = strScanCode;
scanCodeStrToInt[strScanCode] = intScanCode;
scanCodeLowerCaseStrToInt[strScanCode.toLowerCase()] = intScanCode;
}
d(ScanCode.None, 'None');
d(ScanCode.Hyper, 'Hyper');
d(ScanCode.Super, 'Super');
d(ScanCode.Fn, 'Fn');
d(ScanCode.FnLock, 'FnLock');
d(ScanCode.Suspend, 'Suspend');
d(ScanCode.Resume, 'Resume');
d(ScanCode.Turbo, 'Turbo');
d(ScanCode.Sleep, 'Sleep');
d(ScanCode.WakeUp, 'WakeUp');
d(ScanCode.KeyA, 'KeyA');
d(ScanCode.KeyB, 'KeyB');
d(ScanCode.KeyC, 'KeyC');
d(ScanCode.KeyD, 'KeyD');
d(ScanCode.KeyE, 'KeyE');
d(ScanCode.KeyF, 'KeyF');
d(ScanCode.KeyG, 'KeyG');
d(ScanCode.KeyH, 'KeyH');
d(ScanCode.KeyI, 'KeyI');
d(ScanCode.KeyJ, 'KeyJ');
d(ScanCode.KeyK, 'KeyK');
d(ScanCode.KeyL, 'KeyL');
d(ScanCode.KeyM, 'KeyM');
d(ScanCode.KeyN, 'KeyN');
d(ScanCode.KeyO, 'KeyO');
d(ScanCode.KeyP, 'KeyP');
d(ScanCode.KeyQ, 'KeyQ');
d(ScanCode.KeyR, 'KeyR');
d(ScanCode.KeyS, 'KeyS');
d(ScanCode.KeyT, 'KeyT');
d(ScanCode.KeyU, 'KeyU');
d(ScanCode.KeyV, 'KeyV');
d(ScanCode.KeyW, 'KeyW');
d(ScanCode.KeyX, 'KeyX');
d(ScanCode.KeyY, 'KeyY');
d(ScanCode.KeyZ, 'KeyZ');
d(ScanCode.Digit1, 'Digit1');
d(ScanCode.Digit2, 'Digit2');
d(ScanCode.Digit3, 'Digit3');
d(ScanCode.Digit4, 'Digit4');
d(ScanCode.Digit5, 'Digit5');
d(ScanCode.Digit6, 'Digit6');
d(ScanCode.Digit7, 'Digit7');
d(ScanCode.Digit8, 'Digit8');
d(ScanCode.Digit9, 'Digit9');
d(ScanCode.Digit0, 'Digit0');
d(ScanCode.Enter, 'Enter');
d(ScanCode.Escape, 'Escape');
d(ScanCode.Backspace, 'Backspace');
d(ScanCode.Tab, 'Tab');
d(ScanCode.Space, 'Space');
d(ScanCode.Minus, 'Minus');
d(ScanCode.Equal, 'Equal');
d(ScanCode.BracketLeft, 'BracketLeft');
d(ScanCode.BracketRight, 'BracketRight');
d(ScanCode.Backslash, 'Backslash');
d(ScanCode.IntlHash, 'IntlHash');
d(ScanCode.Semicolon, 'Semicolon');
d(ScanCode.Quote, 'Quote');
d(ScanCode.Backquote, 'Backquote');
d(ScanCode.Comma, 'Comma');
d(ScanCode.Period, 'Period');
d(ScanCode.Slash, 'Slash');
d(ScanCode.CapsLock, 'CapsLock');
d(ScanCode.F1, 'F1');
d(ScanCode.F2, 'F2');
d(ScanCode.F3, 'F3');
d(ScanCode.F4, 'F4');
d(ScanCode.F5, 'F5');
d(ScanCode.F6, 'F6');
d(ScanCode.F7, 'F7');
d(ScanCode.F8, 'F8');
d(ScanCode.F9, 'F9');
d(ScanCode.F10, 'F10');
d(ScanCode.F11, 'F11');
d(ScanCode.F12, 'F12');
d(ScanCode.PrintScreen, 'PrintScreen');
d(ScanCode.ScrollLock, 'ScrollLock');
d(ScanCode.Pause, 'Pause');
d(ScanCode.Insert, 'Insert');
d(ScanCode.Home, 'Home');
d(ScanCode.PageUp, 'PageUp');
d(ScanCode.Delete, 'Delete');
d(ScanCode.End, 'End');
d(ScanCode.PageDown, 'PageDown');
d(ScanCode.ArrowRight, 'ArrowRight');
d(ScanCode.ArrowLeft, 'ArrowLeft');
d(ScanCode.ArrowDown, 'ArrowDown');
d(ScanCode.ArrowUp, 'ArrowUp');
d(ScanCode.NumLock, 'NumLock');
d(ScanCode.NumpadDivide, 'NumpadDivide');
d(ScanCode.NumpadMultiply, 'NumpadMultiply');
d(ScanCode.NumpadSubtract, 'NumpadSubtract');
d(ScanCode.NumpadAdd, 'NumpadAdd');
d(ScanCode.NumpadEnter, 'NumpadEnter');
d(ScanCode.Numpad1, 'Numpad1');
d(ScanCode.Numpad2, 'Numpad2');
d(ScanCode.Numpad3, 'Numpad3');
d(ScanCode.Numpad4, 'Numpad4');
d(ScanCode.Numpad5, 'Numpad5');
d(ScanCode.Numpad6, 'Numpad6');
d(ScanCode.Numpad7, 'Numpad7');
d(ScanCode.Numpad8, 'Numpad8');
d(ScanCode.Numpad9, 'Numpad9');
d(ScanCode.Numpad0, 'Numpad0');
d(ScanCode.NumpadDecimal, 'NumpadDecimal');
d(ScanCode.IntlBackslash, 'IntlBackslash');
d(ScanCode.ContextMenu, 'ContextMenu');
d(ScanCode.Power, 'Power');
d(ScanCode.NumpadEqual, 'NumpadEqual');
d(ScanCode.F13, 'F13');
d(ScanCode.F14, 'F14');
d(ScanCode.F15, 'F15');
d(ScanCode.F16, 'F16');
d(ScanCode.F17, 'F17');
d(ScanCode.F18, 'F18');
d(ScanCode.F19, 'F19');
d(ScanCode.F20, 'F20');
d(ScanCode.F21, 'F21');
d(ScanCode.F22, 'F22');
d(ScanCode.F23, 'F23');
d(ScanCode.F24, 'F24');
d(ScanCode.Open, 'Open');
d(ScanCode.Help, 'Help');
d(ScanCode.Select, 'Select');
d(ScanCode.Again, 'Again');
d(ScanCode.Undo, 'Undo');
d(ScanCode.Cut, 'Cut');
d(ScanCode.Copy, 'Copy');
d(ScanCode.Paste, 'Paste');
d(ScanCode.Find, 'Find');
d(ScanCode.AudioVolumeMute, 'AudioVolumeMute');
d(ScanCode.AudioVolumeUp, 'AudioVolumeUp');
d(ScanCode.AudioVolumeDown, 'AudioVolumeDown');
d(ScanCode.NumpadComma, 'NumpadComma');
d(ScanCode.IntlRo, 'IntlRo');
d(ScanCode.KanaMode, 'KanaMode');
d(ScanCode.IntlYen, 'IntlYen');
d(ScanCode.Convert, 'Convert');
d(ScanCode.NonConvert, 'NonConvert');
d(ScanCode.Lang1, 'Lang1');
d(ScanCode.Lang2, 'Lang2');
d(ScanCode.Lang3, 'Lang3');
d(ScanCode.Lang4, 'Lang4');
d(ScanCode.Lang5, 'Lang5');
d(ScanCode.Abort, 'Abort');
d(ScanCode.Props, 'Props');
d(ScanCode.NumpadParenLeft, 'NumpadParenLeft');
d(ScanCode.NumpadParenRight, 'NumpadParenRight');
d(ScanCode.NumpadBackspace, 'NumpadBackspace');
d(ScanCode.NumpadMemoryStore, 'NumpadMemoryStore');
d(ScanCode.NumpadMemoryRecall, 'NumpadMemoryRecall');
d(ScanCode.NumpadMemoryClear, 'NumpadMemoryClear');
d(ScanCode.NumpadMemoryAdd, 'NumpadMemoryAdd');
d(ScanCode.NumpadMemorySubtract, 'NumpadMemorySubtract');
d(ScanCode.NumpadClear, 'NumpadClear');
d(ScanCode.NumpadClearEntry, 'NumpadClearEntry');
d(ScanCode.ControlLeft, 'ControlLeft');
d(ScanCode.ShiftLeft, 'ShiftLeft');
d(ScanCode.AltLeft, 'AltLeft');
d(ScanCode.MetaLeft, 'MetaLeft');
d(ScanCode.ControlRight, 'ControlRight');
d(ScanCode.ShiftRight, 'ShiftRight');
d(ScanCode.AltRight, 'AltRight');
d(ScanCode.MetaRight, 'MetaRight');
d(ScanCode.BrightnessUp, 'BrightnessUp');
d(ScanCode.BrightnessDown, 'BrightnessDown');
d(ScanCode.MediaPlay, 'MediaPlay');
d(ScanCode.MediaRecord, 'MediaRecord');
d(ScanCode.MediaFastForward, 'MediaFastForward');
d(ScanCode.MediaRewind, 'MediaRewind');
d(ScanCode.MediaTrackNext, 'MediaTrackNext');
d(ScanCode.MediaTrackPrevious, 'MediaTrackPrevious');
d(ScanCode.MediaStop, 'MediaStop');
d(ScanCode.Eject, 'Eject');
d(ScanCode.MediaPlayPause, 'MediaPlayPause');
d(ScanCode.MediaSelect, 'MediaSelect');
d(ScanCode.LaunchMail, 'LaunchMail');
d(ScanCode.LaunchApp2, 'LaunchApp2');
d(ScanCode.LaunchApp1, 'LaunchApp1');
d(ScanCode.SelectTask, 'SelectTask');
d(ScanCode.LaunchScreenSaver, 'LaunchScreenSaver');
d(ScanCode.BrowserSearch, 'BrowserSearch');
d(ScanCode.BrowserHome, 'BrowserHome');
d(ScanCode.BrowserBack, 'BrowserBack');
d(ScanCode.BrowserForward, 'BrowserForward');
d(ScanCode.BrowserStop, 'BrowserStop');
d(ScanCode.BrowserRefresh, 'BrowserRefresh');
d(ScanCode.BrowserFavorites, 'BrowserFavorites');
d(ScanCode.ZoomToggle, 'ZoomToggle');
d(ScanCode.MailReply, 'MailReply');
d(ScanCode.MailForward, 'MailForward');
d(ScanCode.MailSend, 'MailSend');
})();
(function () {
for (let i = 0; i <= ScanCode.MAX_VALUE; i++) {
IMMUTABLE_CODE_TO_KEY_CODE[i] = KeyCode.DependsOnKbLayout;
}
for (let i = 0; i <= KeyCode.MAX_VALUE; i++) {
IMMUTABLE_KEY_CODE_TO_CODE[i] = ScanCode.DependsOnKbLayout;
}
function define(code: ScanCode, keyCode: KeyCode): void {
IMMUTABLE_CODE_TO_KEY_CODE[code] = keyCode;
if (
(keyCode !== KeyCode.Unknown)
&& (keyCode !== KeyCode.Enter)
&& (keyCode !== KeyCode.Ctrl)
&& (keyCode !== KeyCode.Shift)
&& (keyCode !== KeyCode.Alt)
&& (keyCode !== KeyCode.Meta)
) {
IMMUTABLE_KEY_CODE_TO_CODE[keyCode] = code;
}
}
// Manually added due to the exclusion above (due to duplication with NumpadEnter)
IMMUTABLE_KEY_CODE_TO_CODE[KeyCode.Enter] = ScanCode.Enter;
define(ScanCode.None, KeyCode.Unknown);
define(ScanCode.Hyper, KeyCode.Unknown);
define(ScanCode.Super, KeyCode.Unknown);
define(ScanCode.Fn, KeyCode.Unknown);
define(ScanCode.FnLock, KeyCode.Unknown);
define(ScanCode.Suspend, KeyCode.Unknown);
define(ScanCode.Resume, KeyCode.Unknown);
define(ScanCode.Turbo, KeyCode.Unknown);
define(ScanCode.Sleep, KeyCode.Unknown);
define(ScanCode.WakeUp, KeyCode.Unknown);
// define(ScanCode.KeyA, KeyCode.Unknown);
// define(ScanCode.KeyB, KeyCode.Unknown);
// define(ScanCode.KeyC, KeyCode.Unknown);
// define(ScanCode.KeyD, KeyCode.Unknown);
// define(ScanCode.KeyE, KeyCode.Unknown);
// define(ScanCode.KeyF, KeyCode.Unknown);
// define(ScanCode.KeyG, KeyCode.Unknown);
// define(ScanCode.KeyH, KeyCode.Unknown);
// define(ScanCode.KeyI, KeyCode.Unknown);
// define(ScanCode.KeyJ, KeyCode.Unknown);
// define(ScanCode.KeyK, KeyCode.Unknown);
// define(ScanCode.KeyL, KeyCode.Unknown);
// define(ScanCode.KeyM, KeyCode.Unknown);
// define(ScanCode.KeyN, KeyCode.Unknown);
// define(ScanCode.KeyO, KeyCode.Unknown);
// define(ScanCode.KeyP, KeyCode.Unknown);
// define(ScanCode.KeyQ, KeyCode.Unknown);
// define(ScanCode.KeyR, KeyCode.Unknown);
// define(ScanCode.KeyS, KeyCode.Unknown);
// define(ScanCode.KeyT, KeyCode.Unknown);
// define(ScanCode.KeyU, KeyCode.Unknown);
// define(ScanCode.KeyV, KeyCode.Unknown);
// define(ScanCode.KeyW, KeyCode.Unknown);
// define(ScanCode.KeyX, KeyCode.Unknown);
// define(ScanCode.KeyY, KeyCode.Unknown);
// define(ScanCode.KeyZ, KeyCode.Unknown);
// define(ScanCode.Digit1, KeyCode.Unknown);
// define(ScanCode.Digit2, KeyCode.Unknown);
// define(ScanCode.Digit3, KeyCode.Unknown);
// define(ScanCode.Digit4, KeyCode.Unknown);
// define(ScanCode.Digit5, KeyCode.Unknown);
// define(ScanCode.Digit6, KeyCode.Unknown);
// define(ScanCode.Digit7, KeyCode.Unknown);
// define(ScanCode.Digit8, KeyCode.Unknown);
// define(ScanCode.Digit9, KeyCode.Unknown);
// define(ScanCode.Digit0, KeyCode.Unknown);
define(ScanCode.Enter, KeyCode.Enter);
define(ScanCode.Escape, KeyCode.Escape);
define(ScanCode.Backspace, KeyCode.Backspace);
define(ScanCode.Tab, KeyCode.Tab);
define(ScanCode.Space, KeyCode.Space);
// define(ScanCode.Minus, KeyCode.Unknown);
// define(ScanCode.Equal, KeyCode.Unknown);
// define(ScanCode.BracketLeft, KeyCode.Unknown);
// define(ScanCode.BracketRight, KeyCode.Unknown);
// define(ScanCode.Backslash, KeyCode.Unknown);
// define(ScanCode.IntlHash, KeyCode.Unknown);
// define(ScanCode.Semicolon, KeyCode.Unknown);
// define(ScanCode.Quote, KeyCode.Unknown);
// define(ScanCode.Backquote, KeyCode.Unknown);
// define(ScanCode.Comma, KeyCode.Unknown);
// define(ScanCode.Period, KeyCode.Unknown);
// define(ScanCode.Slash, KeyCode.Unknown);
define(ScanCode.CapsLock, KeyCode.CapsLock);
define(ScanCode.F1, KeyCode.F1);
define(ScanCode.F2, KeyCode.F2);
define(ScanCode.F3, KeyCode.F3);
define(ScanCode.F4, KeyCode.F4);
define(ScanCode.F5, KeyCode.F5);
define(ScanCode.F6, KeyCode.F6);
define(ScanCode.F7, KeyCode.F7);
define(ScanCode.F8, KeyCode.F8);
define(ScanCode.F9, KeyCode.F9);
define(ScanCode.F10, KeyCode.F10);
define(ScanCode.F11, KeyCode.F11);
define(ScanCode.F12, KeyCode.F12);
define(ScanCode.PrintScreen, KeyCode.Unknown);
define(ScanCode.ScrollLock, KeyCode.ScrollLock);
define(ScanCode.Pause, KeyCode.PauseBreak);
define(ScanCode.Insert, KeyCode.Insert);
define(ScanCode.Home, KeyCode.Home);
define(ScanCode.PageUp, KeyCode.PageUp);
define(ScanCode.Delete, KeyCode.Delete);
define(ScanCode.End, KeyCode.End);
define(ScanCode.PageDown, KeyCode.PageDown);
define(ScanCode.ArrowRight, KeyCode.RightArrow);
define(ScanCode.ArrowLeft, KeyCode.LeftArrow);
define(ScanCode.ArrowDown, KeyCode.DownArrow);
define(ScanCode.ArrowUp, KeyCode.UpArrow);
define(ScanCode.NumLock, KeyCode.NumLock);
define(ScanCode.NumpadDivide, KeyCode.NUMPAD_DIVIDE);
define(ScanCode.NumpadMultiply, KeyCode.NUMPAD_MULTIPLY);
define(ScanCode.NumpadSubtract, KeyCode.NUMPAD_SUBTRACT);
define(ScanCode.NumpadAdd, KeyCode.NUMPAD_ADD);
define(ScanCode.NumpadEnter, KeyCode.Enter); // Duplicate
define(ScanCode.Numpad1, KeyCode.NUMPAD_1);
define(ScanCode.Numpad2, KeyCode.NUMPAD_2);
define(ScanCode.Numpad3, KeyCode.NUMPAD_3);
define(ScanCode.Numpad4, KeyCode.NUMPAD_4);
define(ScanCode.Numpad5, KeyCode.NUMPAD_5);
define(ScanCode.Numpad6, KeyCode.NUMPAD_6);
define(ScanCode.Numpad7, KeyCode.NUMPAD_7);
define(ScanCode.Numpad8, KeyCode.NUMPAD_8);
define(ScanCode.Numpad9, KeyCode.NUMPAD_9);
define(ScanCode.Numpad0, KeyCode.NUMPAD_0);
define(ScanCode.NumpadDecimal, KeyCode.NUMPAD_DECIMAL);
// define(ScanCode.IntlBackslash, KeyCode.Unknown);
define(ScanCode.ContextMenu, KeyCode.ContextMenu);
define(ScanCode.Power, KeyCode.Unknown);
define(ScanCode.NumpadEqual, KeyCode.Unknown);
define(ScanCode.F13, KeyCode.F13);
define(ScanCode.F14, KeyCode.F14);
define(ScanCode.F15, KeyCode.F15);
define(ScanCode.F16, KeyCode.F16);
define(ScanCode.F17, KeyCode.F17);
define(ScanCode.F18, KeyCode.F18);
define(ScanCode.F19, KeyCode.F19);
define(ScanCode.F20, KeyCode.Unknown);
define(ScanCode.F21, KeyCode.Unknown);
define(ScanCode.F22, KeyCode.Unknown);
define(ScanCode.F23, KeyCode.Unknown);
define(ScanCode.F24, KeyCode.Unknown);
define(ScanCode.Open, KeyCode.Unknown);
define(ScanCode.Help, KeyCode.Unknown);
define(ScanCode.Select, KeyCode.Unknown);
define(ScanCode.Again, KeyCode.Unknown);
define(ScanCode.Undo, KeyCode.Unknown);
define(ScanCode.Cut, KeyCode.Unknown);
define(ScanCode.Copy, KeyCode.Unknown);
define(ScanCode.Paste, KeyCode.Unknown);
define(ScanCode.Find, KeyCode.Unknown);
define(ScanCode.AudioVolumeMute, KeyCode.Unknown);
define(ScanCode.AudioVolumeUp, KeyCode.Unknown);
define(ScanCode.AudioVolumeDown, KeyCode.Unknown);
define(ScanCode.NumpadComma, KeyCode.NUMPAD_SEPARATOR);
// define(ScanCode.IntlRo, KeyCode.Unknown);
define(ScanCode.KanaMode, KeyCode.Unknown);
// define(ScanCode.IntlYen, KeyCode.Unknown);
define(ScanCode.Convert, KeyCode.Unknown);
define(ScanCode.NonConvert, KeyCode.Unknown);
define(ScanCode.Lang1, KeyCode.Unknown);
define(ScanCode.Lang2, KeyCode.Unknown);
define(ScanCode.Lang3, KeyCode.Unknown);
define(ScanCode.Lang4, KeyCode.Unknown);
define(ScanCode.Lang5, KeyCode.Unknown);
define(ScanCode.Abort, KeyCode.Unknown);
define(ScanCode.Props, KeyCode.Unknown);
define(ScanCode.NumpadParenLeft, KeyCode.Unknown);
define(ScanCode.NumpadParenRight, KeyCode.Unknown);
define(ScanCode.NumpadBackspace, KeyCode.Unknown);
define(ScanCode.NumpadMemoryStore, KeyCode.Unknown);
define(ScanCode.NumpadMemoryRecall, KeyCode.Unknown);
define(ScanCode.NumpadMemoryClear, KeyCode.Unknown);
define(ScanCode.NumpadMemoryAdd, KeyCode.Unknown);
define(ScanCode.NumpadMemorySubtract, KeyCode.Unknown);
define(ScanCode.NumpadClear, KeyCode.Unknown);
define(ScanCode.NumpadClearEntry, KeyCode.Unknown);
define(ScanCode.ControlLeft, KeyCode.Ctrl); // Duplicate
define(ScanCode.ShiftLeft, KeyCode.Shift); // Duplicate
define(ScanCode.AltLeft, KeyCode.Alt); // Duplicate
define(ScanCode.MetaLeft, KeyCode.Meta); // Duplicate
define(ScanCode.ControlRight, KeyCode.Ctrl); // Duplicate
define(ScanCode.ShiftRight, KeyCode.Shift); // Duplicate
define(ScanCode.AltRight, KeyCode.Alt); // Duplicate
define(ScanCode.MetaRight, KeyCode.Meta); // Duplicate
define(ScanCode.BrightnessUp, KeyCode.Unknown);
define(ScanCode.BrightnessDown, KeyCode.Unknown);
define(ScanCode.MediaPlay, KeyCode.Unknown);
define(ScanCode.MediaRecord, KeyCode.Unknown);
define(ScanCode.MediaFastForward, KeyCode.Unknown);
define(ScanCode.MediaRewind, KeyCode.Unknown);
define(ScanCode.MediaTrackNext, KeyCode.Unknown);
define(ScanCode.MediaTrackPrevious, KeyCode.Unknown);
define(ScanCode.MediaStop, KeyCode.Unknown);
define(ScanCode.Eject, KeyCode.Unknown);
define(ScanCode.MediaPlayPause, KeyCode.Unknown);
define(ScanCode.MediaSelect, KeyCode.Unknown);
define(ScanCode.LaunchMail, KeyCode.Unknown);
define(ScanCode.LaunchApp2, KeyCode.Unknown);
define(ScanCode.LaunchApp1, KeyCode.Unknown);
define(ScanCode.SelectTask, KeyCode.Unknown);
define(ScanCode.LaunchScreenSaver, KeyCode.Unknown);
define(ScanCode.BrowserSearch, KeyCode.Unknown);
define(ScanCode.BrowserHome, KeyCode.Unknown);
define(ScanCode.BrowserBack, KeyCode.Unknown);
define(ScanCode.BrowserForward, KeyCode.Unknown);
define(ScanCode.BrowserStop, KeyCode.Unknown);
define(ScanCode.BrowserRefresh, KeyCode.Unknown);
define(ScanCode.BrowserFavorites, KeyCode.Unknown);
define(ScanCode.ZoomToggle, KeyCode.Unknown);
define(ScanCode.MailReply, KeyCode.Unknown);
define(ScanCode.MailForward, KeyCode.Unknown);
define(ScanCode.MailSend, KeyCode.Unknown);
})();
+21 -47
View File
@@ -318,21 +318,27 @@ export function compareSubstringIgnoreCase(a: string, b: string, aStart: number
continue;
}
const diff = codeA - codeB;
if (diff === 32 && isUpperAsciiLetter(codeB)) { //codeB =[65-90] && codeA =[97-122]
continue;
} else if (diff === -32 && isUpperAsciiLetter(codeA)) { //codeB =[97-122] && codeA =[65-90]
continue;
}
if (isLowerAsciiLetter(codeA) && isLowerAsciiLetter(codeB)) {
//
return diff;
} else {
if (codeA >= 128 || codeB >= 128) {
// not ASCII letters -> fallback to lower-casing strings
return compareSubstring(a.toLowerCase(), b.toLowerCase(), aStart, aEnd, bStart, bEnd);
}
// mapper lower-case ascii letter onto upper-case varinats
// [97-122] (lower ascii) --> [65-90] (upper ascii)
if (isLowerAsciiLetter(codeA)) {
codeA -= 32;
}
if (isLowerAsciiLetter(codeB)) {
codeB -= 32;
}
// compare both code points
const diff = codeA - codeB;
if (diff === 0) {
continue;
}
return diff;
}
const aLen = aEnd - aStart;
@@ -355,40 +361,8 @@ export function isUpperAsciiLetter(code: number): boolean {
return code >= CharCode.A && code <= CharCode.Z;
}
function isAsciiLetter(code: number): boolean {
return isLowerAsciiLetter(code) || isUpperAsciiLetter(code);
}
export function equalsIgnoreCase(a: string, b: string): boolean {
return a.length === b.length && doEqualsIgnoreCase(a, b);
}
function doEqualsIgnoreCase(a: string, b: string, stopAt = a.length): boolean {
for (let i = 0; i < stopAt; i++) {
const codeA = a.charCodeAt(i);
const codeB = b.charCodeAt(i);
if (codeA === codeB) {
continue;
}
// a-z A-Z
if (isAsciiLetter(codeA) && isAsciiLetter(codeB)) {
const diff = Math.abs(codeA - codeB);
if (diff !== 0 && diff !== 32) {
return false;
}
}
// Any other charcode
else {
if (String.fromCharCode(codeA).toLowerCase() !== String.fromCharCode(codeB).toLowerCase()) {
return false;
}
}
}
return true;
return a.length === b.length && compareSubstringIgnoreCase(a, b) === 0;
}
export function startsWithIgnoreCase(str: string, candidate: string): boolean {
@@ -397,7 +371,7 @@ export function startsWithIgnoreCase(str: string, candidate: string): boolean {
return false;
}
return doEqualsIgnoreCase(str, candidate, candidateLength);
return compareSubstringIgnoreCase(str, candidate, 0, candidateLength) === 0;
}
/**
+2
View File
@@ -17,6 +17,8 @@ export interface UriParts {
scheme: string;
authority?: string;
path?: string;
query?: string;
fragment?: string;
}
export interface IRawURITransformer {
+255 -83
View File
@@ -4,19 +4,21 @@
*--------------------------------------------------------------------------------------------*/
import { transformErrorForSerialization } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { isWeb } from 'vs/base/common/platform';
import { globals, isWeb } from 'vs/base/common/platform';
import * as types from 'vs/base/common/types';
import * as strings from 'vs/base/common/strings';
const INITIALIZE = '$initialize';
export interface IWorker extends IDisposable {
getId(): number;
postMessage(message: any, transfer: ArrayBuffer[]): void;
postMessage(message: Message, transfer: ArrayBuffer[]): void;
}
export interface IWorkerCallback {
(message: any): void;
(message: Message): void;
}
export interface IWorkerFactory {
@@ -36,23 +38,56 @@ export function logOnceWebWorkerWarning(err: any): void {
console.warn(err.message);
}
interface IMessage {
vsWorker: number;
req?: string;
seq?: string;
const enum MessageType {
Request,
Reply,
SubscribeEvent,
Event,
UnsubscribeEvent
}
interface IRequestMessage extends IMessage {
req: string;
method: string;
args: any[];
class RequestMessage {
public readonly type = MessageType.Request;
constructor(
public readonly vsWorker: number,
public readonly req: string,
public readonly method: string,
public readonly args: any[]
) { }
}
interface IReplyMessage extends IMessage {
seq: string;
err: any;
res: any;
class ReplyMessage {
public readonly type = MessageType.Reply;
constructor(
public readonly vsWorker: number,
public readonly seq: string,
public readonly res: any,
public readonly err: any
) { }
}
class SubscribeEventMessage {
public readonly type = MessageType.SubscribeEvent;
constructor(
public readonly vsWorker: number,
public readonly req: string,
public readonly eventName: string,
public readonly arg: any
) { }
}
class EventMessage {
public readonly type = MessageType.Event;
constructor(
public readonly vsWorker: number,
public readonly req: string,
public readonly event: any
) { }
}
class UnsubscribeEventMessage {
public readonly type = MessageType.UnsubscribeEvent;
constructor(
public readonly vsWorker: number,
public readonly req: string
) { }
}
type Message = RequestMessage | ReplyMessage | SubscribeEventMessage | EventMessage | UnsubscribeEventMessage;
interface IMessageReply {
resolve: (value?: any) => void;
@@ -62,6 +97,7 @@ interface IMessageReply {
interface IMessageHandler {
sendMessage(msg: any, transfer?: ArrayBuffer[]): void;
handleMessage(method: string, args: any[]): Promise<any>;
handleEvent(eventName: string, arg: any): Event<any>;
}
class SimpleWorkerProtocol {
@@ -69,6 +105,8 @@ class SimpleWorkerProtocol {
private _workerId: number;
private _lastSentReq: number;
private _pendingReplies: { [req: string]: IMessageReply; };
private _pendingEmitters: Map<string, Emitter<any>>;
private _pendingEvents: Map<string, IDisposable>;
private _handler: IMessageHandler;
constructor(handler: IMessageHandler) {
@@ -76,6 +114,8 @@ class SimpleWorkerProtocol {
this._handler = handler;
this._lastSentReq = 0;
this._pendingReplies = Object.create(null);
this._pendingEmitters = new Map<string, Emitter<any>>();
this._pendingEvents = new Map<string, IDisposable>();
}
public setWorkerId(workerId: number): void {
@@ -83,22 +123,34 @@ class SimpleWorkerProtocol {
}
public sendMessage(method: string, args: any[]): Promise<any> {
let req = String(++this._lastSentReq);
const req = String(++this._lastSentReq);
return new Promise<any>((resolve, reject) => {
this._pendingReplies[req] = {
resolve: resolve,
reject: reject
};
this._send({
vsWorker: this._workerId,
req: req,
method: method,
args: args
});
this._send(new RequestMessage(this._workerId, req, method, args));
});
}
public handleMessage(message: IMessage): void {
public listen(eventName: string, arg: any): Event<any> {
let req: string | null = null;
const emitter = new Emitter<any>({
onFirstListenerAdd: () => {
req = String(++this._lastSentReq);
this._pendingEmitters.set(req, emitter);
this._send(new SubscribeEventMessage(this._workerId, req, eventName, arg));
},
onLastListenerRemove: () => {
this._pendingEmitters.delete(req!);
this._send(new UnsubscribeEventMessage(this._workerId, req!));
req = null;
}
});
return emitter.event;
}
public handleMessage(message: Message): void {
if (!message || !message.vsWorker) {
return;
}
@@ -108,70 +160,95 @@ class SimpleWorkerProtocol {
this._handleMessage(message);
}
private _handleMessage(msg: IMessage): void {
if (msg.seq) {
let replyMessage = <IReplyMessage>msg;
if (!this._pendingReplies[replyMessage.seq]) {
console.warn('Got reply to unknown seq');
return;
}
private _handleMessage(msg: Message): void {
switch (msg.type) {
case MessageType.Reply:
return this._handleReplyMessage(msg);
case MessageType.Request:
return this._handleRequestMessage(msg);
case MessageType.SubscribeEvent:
return this._handleSubscribeEventMessage(msg);
case MessageType.Event:
return this._handleEventMessage(msg);
case MessageType.UnsubscribeEvent:
return this._handleUnsubscribeEventMessage(msg);
}
}
let reply = this._pendingReplies[replyMessage.seq];
delete this._pendingReplies[replyMessage.seq];
if (replyMessage.err) {
let err = replyMessage.err;
if (replyMessage.err.$isError) {
err = new Error();
err.name = replyMessage.err.name;
err.message = replyMessage.err.message;
err.stack = replyMessage.err.stack;
}
reply.reject(err);
return;
}
reply.resolve(replyMessage.res);
private _handleReplyMessage(replyMessage: ReplyMessage): void {
if (!this._pendingReplies[replyMessage.seq]) {
console.warn('Got reply to unknown seq');
return;
}
let requestMessage = <IRequestMessage>msg;
let reply = this._pendingReplies[replyMessage.seq];
delete this._pendingReplies[replyMessage.seq];
if (replyMessage.err) {
let err = replyMessage.err;
if (replyMessage.err.$isError) {
err = new Error();
err.name = replyMessage.err.name;
err.message = replyMessage.err.message;
err.stack = replyMessage.err.stack;
}
reply.reject(err);
return;
}
reply.resolve(replyMessage.res);
}
private _handleRequestMessage(requestMessage: RequestMessage): void {
let req = requestMessage.req;
let result = this._handler.handleMessage(requestMessage.method, requestMessage.args);
result.then((r) => {
this._send({
vsWorker: this._workerId,
seq: req,
res: r,
err: undefined
});
this._send(new ReplyMessage(this._workerId, req, r, undefined));
}, (e) => {
if (e.detail instanceof Error) {
// Loading errors have a detail property that points to the actual error
e.detail = transformErrorForSerialization(e.detail);
}
this._send({
vsWorker: this._workerId,
seq: req,
res: undefined,
err: transformErrorForSerialization(e)
});
this._send(new ReplyMessage(this._workerId, req, undefined, transformErrorForSerialization(e)));
});
}
private _send(msg: IRequestMessage | IReplyMessage): void {
private _handleSubscribeEventMessage(msg: SubscribeEventMessage): void {
const req = msg.req;
const disposable = this._handler.handleEvent(msg.eventName, msg.arg)((event) => {
this._send(new EventMessage(this._workerId, req, event));
});
this._pendingEvents.set(req, disposable);
}
private _handleEventMessage(msg: EventMessage): void {
if (!this._pendingEmitters.has(msg.req)) {
console.warn('Got event for unknown req');
return;
}
this._pendingEmitters.get(msg.req)!.fire(msg.event);
}
private _handleUnsubscribeEventMessage(msg: UnsubscribeEventMessage): void {
if (!this._pendingEvents.has(msg.req)) {
console.warn('Got unsubscribe for unknown req');
return;
}
this._pendingEvents.get(msg.req)!.dispose();
this._pendingEvents.delete(msg.req);
}
private _send(msg: Message): void {
let transfer: ArrayBuffer[] = [];
if (msg.req) {
const m = <IRequestMessage>msg;
for (let i = 0; i < m.args.length; i++) {
if (m.args[i] instanceof ArrayBuffer) {
transfer.push(m.args[i]);
if (msg.type === MessageType.Request) {
for (let i = 0; i < msg.args.length; i++) {
if (msg.args[i] instanceof ArrayBuffer) {
transfer.push(msg.args[i]);
}
}
} else {
const m = <IReplyMessage>msg;
if (m.res instanceof ArrayBuffer) {
transfer.push(m.res);
} else if (msg.type === MessageType.Reply) {
if (msg.res instanceof ArrayBuffer) {
transfer.push(msg.res);
}
}
this._handler.sendMessage(msg, transfer);
@@ -200,7 +277,7 @@ export class SimpleWorkerClient<W extends object, H extends object> extends Disp
this._worker = this._register(workerFactory.create(
'vs/base/common/worker/simpleWorker',
(msg: any) => {
(msg: Message) => {
this._protocol.handleMessage(msg);
},
(err: any) => {
@@ -226,18 +303,35 @@ export class SimpleWorkerClient<W extends object, H extends object> extends Disp
} catch (e) {
return Promise.reject(e);
}
},
handleEvent: (eventName: string, arg: any): Event<any> => {
if (propertyIsDynamicEvent(eventName)) {
const event = (host as any)[eventName].call(host, arg);
if (typeof event !== 'function') {
throw new Error(`Missing dynamic event ${eventName} on main thread host.`);
}
return event;
}
if (propertyIsEvent(eventName)) {
const event = (host as any)[eventName];
if (typeof event !== 'function') {
throw new Error(`Missing event ${eventName} on main thread host.`);
}
return event;
}
throw new Error(`Malformed event name ${eventName}`);
}
});
this._protocol.setWorkerId(this._worker.getId());
// Gather loader configuration
let loaderConfiguration: any = null;
if (typeof (<any>self).require !== 'undefined' && typeof (<any>self).require.getConfig === 'function') {
if (typeof globals.require !== 'undefined' && typeof globals.require.getConfig === 'function') {
// Get the configuration from the Monaco AMD Loader
loaderConfiguration = (<any>self).require.getConfig();
} else if (typeof (<any>self).requirejs !== 'undefined') {
loaderConfiguration = globals.require.getConfig();
} else if (typeof globals.requirejs !== 'undefined') {
// Get the configuration from requirejs
loaderConfiguration = (<any>self).requirejs.s.contexts._.config;
loaderConfiguration = globals.requirejs.s.contexts._.config;
}
const hostMethods = types.getAllMethodNames(host);
@@ -254,11 +348,14 @@ export class SimpleWorkerClient<W extends object, H extends object> extends Disp
const proxyMethodRequest = (method: string, args: any[]): Promise<any> => {
return this._request(method, args);
};
const proxyListen = (eventName: string, arg: any): Event<any> => {
return this._protocol.listen(eventName, arg);
};
this._lazyProxy = new Promise<W>((resolve, reject) => {
lazyProxyReject = reject;
this._onModuleLoaded.then((availableMethods: string[]) => {
resolve(types.createProxyObject<W>(availableMethods, proxyMethodRequest));
resolve(createProxyObject<W>(availableMethods, proxyMethodRequest, proxyListen));
}, (e) => {
reject(e);
this._onError('Worker failed to load ' + moduleId, e);
@@ -284,6 +381,48 @@ export class SimpleWorkerClient<W extends object, H extends object> extends Disp
}
}
function propertyIsEvent(name: string): boolean {
// Assume a property is an event if it has a form of "onSomething"
return name[0] === 'o' && name[1] === 'n' && strings.isUpperAsciiLetter(name.charCodeAt(2));
}
function propertyIsDynamicEvent(name: string): boolean {
// Assume a property is a dynamic event (a method that returns an event) if it has a form of "onDynamicSomething"
return /^onDynamic/.test(name) && strings.isUpperAsciiLetter(name.charCodeAt(9));
}
function createProxyObject<T extends object>(
methodNames: string[],
invoke: (method: string, args: unknown[]) => unknown,
proxyListen: (eventName: string, arg: any) => Event<any>
): T {
const createProxyMethod = (method: string): () => unknown => {
return function () {
const args = Array.prototype.slice.call(arguments, 0);
return invoke(method, args);
};
};
const createProxyDynamicEvent = (eventName: string): (arg: any) => Event<any> => {
return function (arg) {
return proxyListen(eventName, arg);
};
};
let result = {} as T;
for (const methodName of methodNames) {
if (propertyIsDynamicEvent(methodName)) {
(<any>result)[methodName] = createProxyDynamicEvent(methodName);
continue;
}
if (propertyIsEvent(methodName)) {
(<any>result)[methodName] = proxyListen(methodName, undefined);
continue;
}
(<any>result)[methodName] = createProxyMethod(methodName);
}
return result;
}
export interface IRequestHandler {
_requestHandlerBrand: any;
[prop: string]: any;
@@ -302,14 +441,15 @@ export class SimpleWorkerServer<H extends object> {
private _requestHandler: IRequestHandler | null;
private _protocol: SimpleWorkerProtocol;
constructor(postMessage: (msg: any, transfer?: ArrayBuffer[]) => void, requestHandlerFactory: IRequestHandlerFactory<H> | null) {
constructor(postMessage: (msg: Message, transfer?: ArrayBuffer[]) => void, requestHandlerFactory: IRequestHandlerFactory<H> | null) {
this._requestHandlerFactory = requestHandlerFactory;
this._requestHandler = null;
this._protocol = new SimpleWorkerProtocol({
sendMessage: (msg: any, transfer: ArrayBuffer[]): void => {
postMessage(msg, transfer);
},
handleMessage: (method: string, args: any[]): Promise<any> => this._handleMessage(method, args)
handleMessage: (method: string, args: any[]): Promise<any> => this._handleMessage(method, args),
handleEvent: (eventName: string, arg: any): Event<any> => this._handleEvent(eventName, arg)
});
}
@@ -333,14 +473,38 @@ export class SimpleWorkerServer<H extends object> {
}
}
private _handleEvent(eventName: string, arg: any): Event<any> {
if (!this._requestHandler) {
throw new Error(`Missing requestHandler`);
}
if (propertyIsDynamicEvent(eventName)) {
const event = (this._requestHandler as any)[eventName].call(this._requestHandler, arg);
if (typeof event !== 'function') {
throw new Error(`Missing dynamic event ${eventName} on request handler.`);
}
return event;
}
if (propertyIsEvent(eventName)) {
const event = (this._requestHandler as any)[eventName];
if (typeof event !== 'function') {
throw new Error(`Missing event ${eventName} on request handler.`);
}
return event;
}
throw new Error(`Malformed event name ${eventName}`);
}
private initialize(workerId: number, loaderConfig: any, moduleId: string, hostMethods: string[]): Promise<string[]> {
this._protocol.setWorkerId(workerId);
const proxyMethodRequest = (method: string, args: any[]): Promise<any> => {
return this._protocol.sendMessage(method, args);
};
const proxyListen = (eventName: string, arg: any): Event<any> => {
return this._protocol.listen(eventName, arg);
};
const hostProxy = types.createProxyObject<H>(hostMethods, proxyMethodRequest);
const hostProxy = createProxyObject<H>(hostMethods, proxyMethodRequest, proxyListen);
if (this._requestHandlerFactory) {
// static request handler
@@ -365,12 +529,20 @@ export class SimpleWorkerServer<H extends object> {
// Since this is in a web worker, enable catching errors
loaderConfig.catchError = true;
(<any>self).require.config(loaderConfig);
globals.require.config(loaderConfig);
}
return new Promise<string[]>((resolve, reject) => {
// Use the global require to be sure to get the global config
(<any>self).require([moduleId], (module: { create: IRequestHandlerFactory<H> }) => {
// ESM-comment-begin
const req = (globals.require || require);
// ESM-comment-end
// ESM-uncomment-begin
// const req = globals.require;
// ESM-uncomment-end
req([moduleId], (module: { create: IRequestHandlerFactory<H> }) => {
this._requestHandler = module.create(hostProxy);
if (!this._requestHandler) {
@@ -387,6 +559,6 @@ export class SimpleWorkerServer<H extends object> {
/**
* Called on the worker side
*/
export function create(postMessage: (msg: string) => void): SimpleWorkerServer<any> {
export function create(postMessage: (msg: Message, transfer?: ArrayBuffer[]) => void): SimpleWorkerServer<any> {
return new SimpleWorkerServer(postMessage, null);
}
+1
View File
@@ -17,6 +17,7 @@ function validateMacAddress(candidate: string): boolean {
}
export function getMac(): Promise<string> {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
const timeout = setTimeout(() => reject('Unable to retrieve mac address (timeout after 10s)'), 10000);
+6 -5
View File
@@ -270,7 +270,7 @@ export namespace SymlinkSupport {
return { stat: stats, symbolicLink: lstats?.isSymbolicLink() ? { dangling: false } : undefined };
} catch (error) {
// If the link points to a non-existing file we still want
// If the link points to a nonexistent file we still want
// to return it as result while setting dangling: true flag
if (error.code === 'ENOENT' && lstats) {
return { stat: lstats, symbolicLink: { dangling: true } };
@@ -285,7 +285,7 @@ export namespace SymlinkSupport {
return { stat: stats, symbolicLink: { dangling: false } };
} catch (error) {
// If the link points to a non-existing file we still want
// If the link points to a nonexistent file we still want
// to return it as result while setting dangling: true flag
if (error.code === 'ENOENT' && lstats) {
return { stat: lstats, symbolicLink: { dangling: true } };
@@ -304,7 +304,7 @@ export namespace SymlinkSupport {
* for symlinks.
*
* Note: this will return `false` for a symlink that exists on
* disk but is dangling (pointing to a non-existing path).
* disk but is dangling (pointing to a nonexistent path).
*
* Use `exists` if you only care about the path existing on disk
* or not without support for symbolic links.
@@ -326,7 +326,7 @@ export namespace SymlinkSupport {
* symlinks.
*
* Note: this will return `false` for a symlink that exists on
* disk but is dangling (pointing to a non-existing path).
* disk but is dangling (pointing to a nonexistent path).
*
* Use `exists` if you only care about the path existing on disk
* or not without support for symbolic links.
@@ -407,6 +407,7 @@ function doWriteFileAndFlush(path: string, data: string | Buffer | Uint8Array, o
}
// Flush contents (not metadata) of the file to disk
// https://github.com/microsoft/vscode/issues/9589
fs.fdatasync(fd, (syncError: Error | null) => {
// In some exotic setups it is well possible that node fails to sync
@@ -444,7 +445,7 @@ export function writeFileSync(path: string, data: string | Buffer, options?: IWr
// Flush contents (not metadata) of the file to disk
try {
fs.fdatasyncSync(fd);
fs.fdatasyncSync(fd); // https://github.com/microsoft/vscode/issues/9589
} catch (syncError) {
console.warn('[node.js fs] fdatasyncSync is now disabled for this session because it failed: ', syncError);
canFlush = false;
+5 -5
View File
@@ -9,7 +9,7 @@ import * as net from 'net';
* Given a start point and a max number of retries, will find a port that
* is openable. Will return 0 in case no free port can be found.
*/
export function findFreePort(startPort: number, giveUpAfter: number, timeout: number): Promise<number> {
export function findFreePort(startPort: number, giveUpAfter: number, timeout: number, stride = 1): Promise<number> {
let done = false;
return new Promise(resolve => {
@@ -20,7 +20,7 @@ export function findFreePort(startPort: number, giveUpAfter: number, timeout: nu
}
}, timeout);
doFindFreePort(startPort, giveUpAfter, (port) => {
doFindFreePort(startPort, giveUpAfter, stride, (port) => {
if (!done) {
done = true;
clearTimeout(timeoutHandle);
@@ -30,7 +30,7 @@ export function findFreePort(startPort: number, giveUpAfter: number, timeout: nu
});
}
function doFindFreePort(startPort: number, giveUpAfter: number, clb: (port: number) => void): void {
function doFindFreePort(startPort: number, giveUpAfter: number, stride: number, clb: (port: number) => void): void {
if (giveUpAfter === 0) {
return clb(0);
}
@@ -41,7 +41,7 @@ function doFindFreePort(startPort: number, giveUpAfter: number, clb: (port: numb
client.once('connect', () => {
dispose(client);
return doFindFreePort(startPort + 1, giveUpAfter - 1, clb);
return doFindFreePort(startPort + stride, giveUpAfter - 1, stride, clb);
});
client.once('data', () => {
@@ -53,7 +53,7 @@ function doFindFreePort(startPort: number, giveUpAfter: number, clb: (port: numb
// If we receive any non ECONNREFUSED error, it means the port is used but we cannot connect
if (err.code !== 'ECONNREFUSED') {
return doFindFreePort(startPort + 1, giveUpAfter - 1, clb);
return doFindFreePort(startPort + stride, giveUpAfter - 1, stride, clb);
}
// Otherwise it means the port is free to use!
+29
View File
@@ -87,6 +87,35 @@ function terminateProcess(process: cp.ChildProcess, cwd?: string): Promise<Termi
return Promise.resolve({ success: true });
}
/**
* Remove dangerous environment variables that have caused crashes
* in forked processes (i.e. in ELECTRON_RUN_AS_NODE processes)
*
* @param env The env object to change
*/
export function removeDangerousEnvVariables(env: NodeJS.ProcessEnv | undefined): void {
if (!env) {
return;
}
// Unset `DEBUG`, as an invalid value might lead to process crashes
// See https://github.com/microsoft/vscode/issues/130072
delete env['DEBUG'];
if (Platform.isMacintosh) {
// Unset `DYLD_LIBRARY_PATH`, as it leads to process crashes
// See https://github.com/microsoft/vscode/issues/104525
// See https://github.com/microsoft/vscode/issues/105848
delete env['DYLD_LIBRARY_PATH'];
}
if (Platform.isLinux) {
// Unset `LD_PRELOAD`, as it might lead to process crashes
// See https://github.com/microsoft/vscode/issues/134177
delete env['LD_PRELOAD'];
}
}
export function getWindowsShell(env = process.env as Platform.IProcessEnvironment): string {
return env['comspec'] || 'cmd.exe';
}
+60
View File
@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { watch } from 'fs';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { isEqualOrParent } from 'vs/base/common/extpath';
import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { normalizeNFC } from 'vs/base/common/normalization';
@@ -202,3 +203,62 @@ function doWatchNonRecursive(file: { path: string, isDirectory: boolean }, onCha
watcherDisposables = dispose(watcherDisposables);
});
}
/**
* Watch the provided `path` for changes and return
* the data in chunks of `Uint8Array` for further use.
*/
export async function watchFileContents(path: string, onData: (chunk: Uint8Array) => void, token: CancellationToken, bufferSize = 512): Promise<void> {
const handle = await Promises.open(path, 'r');
const buffer = Buffer.allocUnsafe(bufferSize);
const cts = new CancellationTokenSource(token);
let error: Error | undefined = undefined;
let isReading = false;
const watcher = watchFile(path, async type => {
if (type === 'changed') {
if (isReading) {
return; // return early if we are already reading the output
}
isReading = true;
try {
// Consume the new contents of the file until finished
// everytime there is a change event signalling a change
while (!cts.token.isCancellationRequested) {
const { bytesRead } = await Promises.read(handle, buffer, 0, bufferSize, null);
if (!bytesRead || cts.token.isCancellationRequested) {
break;
}
onData(buffer.slice(0, bytesRead));
}
} catch (err) {
error = new Error(err);
cts.dispose(true);
} finally {
isReading = false;
}
}
}, err => {
error = new Error(err);
cts.dispose(true);
});
return new Promise<void>((resolve, reject) => {
cts.token.onCancellationRequested(async () => {
watcher.dispose();
await Promises.close(handle);
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
+14 -3
View File
@@ -613,7 +613,7 @@ class LoadEstimator {
/**
* returns an estimative number, from 0 (low load) to 1 (high load)
*/
public load(): number {
private load(): number {
const now = Date.now();
const historyLimit = (1 + LoadEstimator._HISTORY_LENGTH) * 1000;
let score = 0;
@@ -630,6 +630,10 @@ class LoadEstimator {
}
}
export interface ILoadEstimator {
hasHighLoad(): boolean;
}
/**
* Same as Protocol, but will actually track messages and acks.
* Moreover, it will ensure no messages are lost if there are no event listeners.
@@ -658,7 +662,7 @@ export class PersistentProtocol implements IMessagePassingProtocol {
private _socketReader: ProtocolReader;
private _socketDisposables: IDisposable[];
private readonly _loadEstimator = LoadEstimator.getInstance();
private readonly _loadEstimator: ILoadEstimator;
private readonly _onControlMessage = new BufferedEmitter<VSBuffer>();
readonly onControlMessage: Event<VSBuffer> = this._onControlMessage.event;
@@ -679,7 +683,8 @@ export class PersistentProtocol implements IMessagePassingProtocol {
return this._outgoingMsgId - this._outgoingAckId;
}
constructor(socket: ISocket, initialChunk: VSBuffer | null = null) {
constructor(socket: ISocket, initialChunk: VSBuffer | null = null, loadEstimator: ILoadEstimator = LoadEstimator.getInstance()) {
this._loadEstimator = loadEstimator;
this._isReconnecting = false;
this._outgoingUnackMsg = new Queue<ProtocolMessage>();
this._outgoingMsgId = 0;
@@ -945,6 +950,12 @@ export class PersistentProtocol implements IMessagePassingProtocol {
return;
}
if (this._isReconnecting) {
// do not cause a timeout during reconnection,
// because messages will not be actually written until `endAcceptReconnection`
return;
}
const oldestUnacknowledgedMsg = this._outgoingUnackMsg.peek()!;
const timeSinceOldestUnacknowledgedMsg = Date.now() - oldestUnacknowledgedMsg.writtenTime;
if (timeSinceOldestUnacknowledgedMsg >= ProtocolConstants.AcknowledgeTimeoutTime) {
+20 -1
View File
@@ -1071,12 +1071,19 @@ export namespace ProxyChannel {
return new class implements IServerChannel {
listen<T>(_: unknown, event: string): Event<T> {
listen<T>(_: unknown, event: string, arg: any): Event<T> {
const eventImpl = mapEventNameToEvent.get(event);
if (eventImpl) {
return eventImpl as Event<T>;
}
if (propertyIsDynamicEvent(event)) {
const target = handler[event];
if (typeof target === 'function') {
return target.call(handler, arg);
}
}
throw new Error(`Event not found: ${event}`);
}
@@ -1126,6 +1133,13 @@ export namespace ProxyChannel {
return options.properties.get(propKey);
}
// Dynamic Event
if (propertyIsDynamicEvent(propKey)) {
return function (arg: any) {
return channel.listen(propKey, arg);
};
}
// Event
if (propertyIsEvent(propKey)) {
return channel.listen(propKey);
@@ -1162,6 +1176,11 @@ export namespace ProxyChannel {
// Assume a property is an event if it has a form of "onSomething"
return name[0] === 'o' && name[1] === 'n' && strings.isUpperAsciiLetter(name.charCodeAt(2));
}
function propertyIsDynamicEvent(name: string): boolean {
// Assume a property is a dynamic event (a method that returns an event) if it has a form of "onDynamicSomething"
return /^onDynamic/.test(name) && strings.isUpperAsciiLetter(name.charCodeAt(9));
}
}
const colorTables = [
@@ -0,0 +1,35 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { generateUuid } from 'vs/base/common/uuid';
import { ipcMessagePort, ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals';
interface IMessageChannelResult {
nonce: string;
port: MessagePort;
source: unknown;
}
export async function acquirePort(requestChannel: string | undefined, responseChannel: string, nonce = generateUuid()): Promise<MessagePort> {
// Get ready to acquire the message port from the
// provided `responseChannel` via preload helper.
ipcMessagePort.acquire(responseChannel, nonce);
// If a `requestChannel` is provided, we are in charge
// to trigger acquisition of the message port from main
if (typeof requestChannel === 'string') {
ipcRenderer.send(requestChannel, nonce);
}
// Wait until the main side has returned the `MessagePort`
// We need to filter by the `nonce` to ensure we listen
// to the right response.
const onMessageChannelResult = Event.fromDOMEventEmitter<IMessageChannelResult>(window, 'message', (e: MessageEvent) => ({ nonce: e.data, port: e.ports[0], source: e.source }));
const { port } = await Event.toPromise(Event.once(Event.filter(onMessageChannelResult, e => e.nonce === nonce && e.source === window)));
return port;
}
+2 -7
View File
@@ -12,8 +12,7 @@ import * as errors from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { deepClone } from 'vs/base/common/objects';
import { isMacintosh } from 'vs/base/common/platform';
import { createQueuedSender } from 'vs/base/node/processes';
import { createQueuedSender, removeDangerousEnvVariables } from 'vs/base/node/processes';
import { ChannelClient as IPCClient, ChannelServer as IPCServer, IChannel, IChannelClient } from 'vs/base/parts/ipc/common/ipc';
/**
@@ -202,11 +201,7 @@ export class Client implements IChannelClient, IDisposable {
forkOpts.execArgv = process.execArgv.filter(a => !/^--inspect(-brk)?=/.test(a)); // remove
}
if (isMacintosh && forkOpts.env) {
// Unset `DYLD_LIBRARY_PATH`, as it leads to process crashes
// See https://github.com/microsoft/vscode/issues/105848
delete forkOpts.env['DYLD_LIBRARY_PATH'];
}
removeDangerousEnvVariables(forkOpts.env);
this.child = fork(this.modulePath, args, forkOpts);
+14 -1
View File
@@ -181,6 +181,8 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
state: ReadState.PeekHeader,
readLen: Constants.MinHeaderByteSize,
fin: 0,
compressed: false,
firstFrameOfMessage: true,
mask: 0
};
@@ -396,6 +398,7 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
const peekHeader = this._incomingData.peek(this._state.readLen);
const firstByte = peekHeader.readUInt8(0);
const finBit = (firstByte & 0b10000000) >>> 7;
const rsv1Bit = (firstByte & 0b01000000) >>> 6;
const secondByte = peekHeader.readUInt8(1);
const hasMask = (secondByte & 0b10000000) >>> 7;
const len = (secondByte & 0b01111111);
@@ -403,6 +406,11 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
this._state.state = ReadState.ReadHeader;
this._state.readLen = Constants.MinHeaderByteSize + (hasMask ? 4 : 0) + (len === 126 ? 2 : 0) + (len === 127 ? 8 : 0);
this._state.fin = finBit;
if (this._state.firstFrameOfMessage) {
// if the frame is compressed, the RSV1 bit is set only for the first frame of the message
this._state.compressed = Boolean(rsv1Bit);
}
this._state.firstFrameOfMessage = Boolean(finBit);
this._state.mask = 0;
} else if (this._state.state === ReadState.ReadHeader) {
@@ -455,7 +463,12 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
this._state.readLen = Constants.MinHeaderByteSize;
this._state.mask = 0;
if (this._zlibInflate) {
if (this._zlibInflate && this._state.compressed) {
// See https://datatracker.ietf.org/doc/html/rfc7692#section-9.2
// Even if permessageDeflate is negotiated, it is possible
// that the other side might decide to send uncompressed messages
// So only decompress messages that have the RSV 1 bit set
//
// See https://tools.ietf.org/html/rfc7692#section-7.2.2
if (this._recordInflateBytes) {
this._recordedInflateBytes.push(Buffer.from(<Buffer>body.buffer));
+259 -12
View File
@@ -7,10 +7,13 @@ import * as assert from 'assert';
import { EventEmitter } from 'events';
import { createServer, Socket } from 'net';
import { tmpdir } from 'os';
import { Barrier, timeout } from 'vs/base/common/async';
import { VSBuffer } from 'vs/base/common/buffer';
import { Disposable } from 'vs/base/common/lifecycle';
import { PersistentProtocol, Protocol } from 'vs/base/parts/ipc/common/ipc.net';
import { createRandomIPCHandle, createStaticIPCHandle, NodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
import { Emitter } from 'vs/base/common/event';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { ILoadEstimator, PersistentProtocol, Protocol, ProtocolConstants, SocketCloseEvent } from 'vs/base/parts/ipc/common/ipc.net';
import { createRandomIPCHandle, createStaticIPCHandle, NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
import product from 'vs/platform/product/common/product';
@@ -66,6 +69,9 @@ class EtherStream extends EventEmitter {
this._ether.write(this._name, data);
return true;
}
destroy(): void {
}
}
class Ether {
@@ -98,7 +104,7 @@ class Ether {
this._ba.push(data);
}
setImmediate(() => this._deliver());
setTimeout(() => this._deliver(), 0);
}
private _deliver(): void {
@@ -107,7 +113,7 @@ class Ether {
const data = Buffer.concat(this._ab);
this._ab.length = 0;
this._b.emit('data', data);
setImmediate(() => this._deliver());
setTimeout(() => this._deliver(), 0);
return;
}
@@ -115,7 +121,7 @@ class Ether {
const data = Buffer.concat(this._ba);
this._ba.length = 0;
this._a.emit('data', data);
setImmediate(() => this._deliver());
setTimeout(() => this._deliver(), 0);
return;
}
@@ -182,13 +188,8 @@ suite('PersistentProtocol reconnection', () => {
ensureNoDisposablesAreLeakedInTestSuite();
let ether: Ether;
setup(() => {
ether = new Ether();
});
test('acks get piggybacked with messages', async () => {
const ether = new Ether();
const a = new PersistentProtocol(new NodeSocket(ether.a));
const aMessages = new MessageStream(a);
const b = new PersistentProtocol(new NodeSocket(ether.b));
@@ -244,6 +245,125 @@ suite('PersistentProtocol reconnection', () => {
a.dispose();
b.dispose();
});
test('ack gets sent after a while', async () => {
await runWithFakedTimers({ useFakeTimers: true, maxTaskCount: 100 }, async () => {
const loadEstimator: ILoadEstimator = {
hasHighLoad: () => false
};
const ether = new Ether();
const aSocket = new NodeSocket(ether.a);
const a = new PersistentProtocol(aSocket, null, loadEstimator);
const aMessages = new MessageStream(a);
const bSocket = new NodeSocket(ether.b);
const b = new PersistentProtocol(bSocket, null, loadEstimator);
const bMessages = new MessageStream(b);
// send one message A -> B
a.send(VSBuffer.fromString('a1'));
assert.strictEqual(a.unacknowledgedCount, 1);
assert.strictEqual(b.unacknowledgedCount, 0);
const a1 = await bMessages.waitForOne();
assert.strictEqual(a1.toString(), 'a1');
assert.strictEqual(a.unacknowledgedCount, 1);
assert.strictEqual(b.unacknowledgedCount, 0);
// wait for ack to arrive B -> A
await timeout(2 * ProtocolConstants.AcknowledgeTime);
assert.strictEqual(a.unacknowledgedCount, 0);
assert.strictEqual(b.unacknowledgedCount, 0);
aMessages.dispose();
bMessages.dispose();
a.dispose();
b.dispose();
});
});
test('messages that are never written to a socket should not cause an ack timeout', async () => {
await runWithFakedTimers(
{
useFakeTimers: true,
useSetImmediate: true,
maxTaskCount: 1000
},
async () => {
// Date.now() in fake timers starts at 0, which is very inconvenient
// since we want to test exactly that a certain field is not initialized with Date.now()
// As a workaround we wait such that Date.now() starts producing more realistic values
await timeout(60 * 60 * 1000);
const loadEstimator: ILoadEstimator = {
hasHighLoad: () => false
};
const ether = new Ether();
const aSocket = new NodeSocket(ether.a);
const a = new PersistentProtocol(aSocket, null, loadEstimator);
const aMessages = new MessageStream(a);
const bSocket = new NodeSocket(ether.b);
const b = new PersistentProtocol(bSocket, null, loadEstimator);
const bMessages = new MessageStream(b);
// send message a1 before reconnection to get _recvAckCheck() scheduled
a.send(VSBuffer.fromString('a1'));
assert.strictEqual(a.unacknowledgedCount, 1);
assert.strictEqual(b.unacknowledgedCount, 0);
// read message a1 at B
const a1 = await bMessages.waitForOne();
assert.strictEqual(a1.toString(), 'a1');
assert.strictEqual(a.unacknowledgedCount, 1);
assert.strictEqual(b.unacknowledgedCount, 0);
// send message b1 to send the ack for a1
b.send(VSBuffer.fromString('b1'));
assert.strictEqual(a.unacknowledgedCount, 1);
assert.strictEqual(b.unacknowledgedCount, 1);
// read message b1 at A to receive the ack for a1
const b1 = await aMessages.waitForOne();
assert.strictEqual(b1.toString(), 'b1');
assert.strictEqual(a.unacknowledgedCount, 0);
assert.strictEqual(b.unacknowledgedCount, 1);
// begin reconnection
aSocket.dispose();
const aSocket2 = new NodeSocket(ether.a);
a.beginAcceptReconnection(aSocket2, null);
let timeoutListenerCalled = false;
const socketTimeoutListener = a.onSocketTimeout(() => {
timeoutListenerCalled = true;
});
// send message 2 during reconnection
a.send(VSBuffer.fromString('a2'));
assert.strictEqual(a.unacknowledgedCount, 1);
assert.strictEqual(b.unacknowledgedCount, 1);
// wait for scheduled _recvAckCheck() to execute
await timeout(2 * ProtocolConstants.AcknowledgeTimeoutTime);
assert.strictEqual(a.unacknowledgedCount, 1);
assert.strictEqual(b.unacknowledgedCount, 1);
assert.strictEqual(timeoutListenerCalled, false);
a.endAcceptReconnection();
assert.strictEqual(timeoutListenerCalled, false);
await timeout(2 * ProtocolConstants.AcknowledgeTimeoutTime);
assert.strictEqual(a.unacknowledgedCount, 0);
assert.strictEqual(b.unacknowledgedCount, 0);
assert.strictEqual(timeoutListenerCalled, false);
socketTimeoutListener.dispose();
aMessages.dispose();
bMessages.dispose();
a.dispose();
b.dispose();
}
);
});
});
suite('IPC, create handle', () => {
@@ -277,3 +397,130 @@ suite('IPC, create handle', () => {
}
});
suite('WebSocketNodeSocket', () => {
function toUint8Array(data: number[]): Uint8Array {
const result = new Uint8Array(data.length);
for (let i = 0; i < data.length; i++) {
result[i] = data[i];
}
return result;
}
function fromUint8Array(data: Uint8Array): number[] {
const result = [];
for (let i = 0; i < data.length; i++) {
result[i] = data[i];
}
return result;
}
function fromCharCodeArray(data: number[]): string {
let result = '';
for (let i = 0; i < data.length; i++) {
result += String.fromCharCode(data[i]);
}
return result;
}
class FakeNodeSocket extends Disposable {
private readonly _onData = new Emitter<VSBuffer>();
public readonly onData = this._onData.event;
private readonly _onClose = new Emitter<SocketCloseEvent>();
public readonly onClose = this._onClose.event;
constructor() {
super();
}
public fireData(data: number[]): void {
this._onData.fire(VSBuffer.wrap(toUint8Array(data)));
}
}
async function testReading(frames: number[][], permessageDeflate: boolean): Promise<string> {
const disposables = new DisposableStore();
const socket = new FakeNodeSocket();
const webSocket = disposables.add(new WebSocketNodeSocket(<any>socket, permessageDeflate, null, false));
const barrier = new Barrier();
let remainingFrameCount = frames.length;
let receivedData: string = '';
disposables.add(webSocket.onData((buff) => {
receivedData += fromCharCodeArray(fromUint8Array(buff.buffer));
remainingFrameCount--;
if (remainingFrameCount === 0) {
barrier.open();
}
}));
for (let i = 0; i < frames.length; i++) {
socket.fireData(frames[i]);
}
await barrier.wait();
disposables.dispose();
return receivedData;
}
test('A single-frame unmasked text message', async () => {
const frames = [
[0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f] // contains "Hello"
];
const actual = await testReading(frames, false);
assert.deepStrictEqual(actual, 'Hello');
});
test('A single-frame masked text message', async () => {
const frames = [
[0x81, 0x85, 0x37, 0xfa, 0x21, 0x3d, 0x7f, 0x9f, 0x4d, 0x51, 0x58] // contains "Hello"
];
const actual = await testReading(frames, false);
assert.deepStrictEqual(actual, 'Hello');
});
test('A fragmented unmasked text message', async () => {
// contains "Hello"
const frames = [
[0x01, 0x03, 0x48, 0x65, 0x6c], // contains "Hel"
[0x80, 0x02, 0x6c, 0x6f], // contains "lo"
];
const actual = await testReading(frames, false);
assert.deepStrictEqual(actual, 'Hello');
});
suite('compression', () => {
test('A single-frame compressed text message', async () => {
// contains "Hello"
const frames = [
[0xc1, 0x07, 0xf2, 0x48, 0xcd, 0xc9, 0xc9, 0x07, 0x00], // contains "Hello"
];
const actual = await testReading(frames, true);
assert.deepStrictEqual(actual, 'Hello');
});
test('A fragmented compressed text message', async () => {
// contains "Hello"
const frames = [ // contains "Hello"
[0x41, 0x03, 0xf2, 0x48, 0xcd],
[0x80, 0x04, 0xc9, 0xc9, 0x07, 0x00]
];
const actual = await testReading(frames, true);
assert.deepStrictEqual(actual, 'Hello');
});
test('A single-frame non-compressed text message', async () => {
const frames = [
[0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f] // contains "Hello"
];
const actual = await testReading(frames, true);
assert.deepStrictEqual(actual, 'Hello');
});
});
});
@@ -130,9 +130,7 @@
.quick-input-message {
margin-top: -1px;
padding: 5px 5px 2px 5px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
overflow-wrap: break-word;
}
.quick-input-message > .codicon {
@@ -285,7 +285,19 @@ class QuickInput extends Disposable implements IQuickInput {
}),
);
this.ui.show(this);
// update properties in the controller that get reset in the ui.show() call
this.visible = true;
// This ensures the message/prompt gets rendered
this._lastValidationMessage = undefined;
// This ensures the input box has the right severity applied
this._lastSeverity = undefined;
if (this.buttons.length) {
// if there are buttons, the ui.show() clears them out of the UI so we should
// rerender them.
this.buttonsUpdated = true;
}
this.update();
}
@@ -312,7 +324,7 @@ class QuickInput extends Disposable implements IQuickInput {
if (title && this.ui.title.textContent !== title) {
this.ui.title.textContent = title;
} else if (!title && this.ui.title.innerHTML !== '&nbsp;') {
this.ui.title.innerText = '\u00a0;';
this.ui.title.innerText = '\u00a0';
}
const description = this.getDescription();
if (this.ui.description1.textContent !== description) {
@@ -443,6 +455,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
private _matchOnLabel = true;
private _sortByLabel = true;
private _autoFocusOnList = true;
private _keepScrollPosition = false;
private _itemActivation = this.ui.isScreenReaderOptimized() ? ItemActivation.NONE /* https://github.com/microsoft/vscode/issues/57501 */ : ItemActivation.FIRST;
private _activeItems: T[] = [];
private activeItemsUpdated = false;
@@ -515,6 +528,14 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
return this._items;
}
private get scrollTop() {
return this.ui.list.scrollTop;
}
private set scrollTop(scrollTop: number) {
this.ui.list.scrollTop = scrollTop;
}
set items(items: Array<T | IQuickPickSeparator>) {
this._items = items;
this.itemsUpdated = true;
@@ -583,6 +604,14 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
this.update();
}
get keepScrollPosition() {
return this._keepScrollPosition;
}
set keepScrollPosition(keepScrollPosition: boolean) {
this._keepScrollPosition = keepScrollPosition;
}
get itemActivation() {
return this._itemActivation;
}
@@ -788,7 +817,16 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
}
}));
this.visibleDisposables.add(this.ui.onDidAccept(() => {
if (!this.canSelectMany && this.activeItems[0]) {
if (this.canSelectMany) {
// if there are no checked elements, it means that an onDidChangeSelection never fired to overwrite
// `_selectedItems`. In that case, we should emit one with an empty array to ensure that
// `.selectedItems` is up to date.
if (!this.ui.list.getCheckedElements().length) {
this._selectedItems = [];
this.onDidChangeSelectionEmitter.fire(this.selectedItems);
}
} else if (this.activeItems[0]) {
// For single-select, we set `selectedItems` to the item that was accepted.
this._selectedItems = [this.activeItems[0]];
this.onDidChangeSelectionEmitter.fire(this.selectedItems);
}
@@ -910,6 +948,8 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
if (!this.visible) {
return;
}
// store the scrollTop before it is reset
const scrollTopBefore = this.keepScrollPosition ? this.scrollTop : 0;
const hideInput = !!this._hideInput && this._items.length > 0;
this.ui.container.classList.toggle('hidden-input', hideInput && !this.description);
const visibilities: Visibilities = {
@@ -1010,6 +1050,11 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
this.ui.list.focus(QuickInputListFocus.First);
}
}
// Set the scroll position to what it was before updating the items
if (this.keepScrollPosition) {
this.scrollTop = scrollTopBefore;
}
}
}
@@ -1398,11 +1443,14 @@ export class QuickInputController extends Disposable {
if (index !== -1) {
const items = input.items.slice();
const removed = items.splice(index, 1);
const activeItems = input.activeItems.filter((ai) => ai !== removed[0]);
const activeItems = input.activeItems.filter(activeItem => activeItem !== removed[0]);
const keepScrollPositionBefore = input.keepScrollPosition;
input.keepScrollPosition = true;
input.items = items;
if (activeItems) {
input.activeItems = activeItems;
}
input.keepScrollPosition = keepScrollPositionBefore;
}
}
})),
@@ -1430,7 +1478,7 @@ export class QuickInputController extends Disposable {
input.quickNavigate = options.quickNavigate;
input.contextKey = options.contextKey;
input.busy = true;
Promise.all<QuickPickInput<T>[], T | undefined>([picks, options.activeItem])
Promise.all([picks, options.activeItem])
.then(([items, _activeItem]) => {
activeItem = _activeItem;
input.busy = false;
@@ -294,7 +294,7 @@ export class QuickInputList {
case KeyCode.Space:
this.toggleCheckbox();
break;
case KeyCode.KEY_A:
case KeyCode.KeyA:
if (platform.isMacintosh ? e.metaKey : e.ctrlKey) {
this.list.setFocus(range(this.list.length));
}
@@ -362,6 +362,14 @@ export class QuickInputList {
return Event.map(this.list.onDidChangeSelection, e => ({ items: e.elements.map(e => e.item), event: e.browserEvent }));
}
get scrollTop() {
return this.list.scrollTop;
}
set scrollTop(scrollTop: number) {
this.list.scrollTop = scrollTop;
}
getAllVisibleChecked() {
return this.allVisibleChecked(this.elements, false);
}
@@ -6,7 +6,7 @@
import { Event } from 'vs/base/common/event';
import { IMatch } from 'vs/base/common/filters';
import { IItemAccessor } from 'vs/base/common/fuzzyScorer';
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { ResolvedKeybinding } from 'vs/base/common/keybindings';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import Severity from 'vs/base/common/severity';
@@ -289,6 +289,8 @@ export interface IQuickPick<T extends IQuickPickItem> extends IQuickInput {
autoFocusOnList: boolean;
keepScrollPosition: boolean;
quickNavigate: IQuickNavigateConfiguration | undefined;
activeItems: ReadonlyArray<T>;
@@ -13,7 +13,7 @@
/**
* @param {string} channel
* @returns {true | never}
* @returns {true | never}
*/
function validateIPC(channel) {
if (!channel || !channel.startsWith('vscode:')) {
@@ -37,7 +37,7 @@
/**
* @param {string} key the name of the process argument to parse
* @returns {string | undefined}
* @returns {string | undefined}
*/
function parseArgv(key) {
for (const arg of process.argv) {
@@ -57,7 +57,7 @@
* @typedef {import('../common/sandboxTypes').ISandboxConfiguration} ISandboxConfiguration
*/
/** @type {ISandboxConfiguration | undefined} */
/** @type {ISandboxConfiguration | undefined} */
let configuration = undefined;
/** @type {Promise<ISandboxConfiguration>} */
@@ -211,26 +211,24 @@
ipcMessagePort: {
/**
* @param {string} channelRequest
* @param {string} channelResponse
* @param {string} requestNonce
* @param {string} responseChannel
* @param {string} nonce
*/
connect(channelRequest, channelResponse, requestNonce) {
if (validateIPC(channelRequest) && validateIPC(channelResponse)) {
acquire(responseChannel, nonce) {
if (validateIPC(responseChannel)) {
const responseListener = (/** @type {IpcRendererEvent} */ e, /** @type {string} */ responseNonce) => {
// validate that the nonce from the response is the same
// as when requested. and if so, use `postMessage` to
// send the `MessagePort` safely over, even when context
// isolation is enabled
if (requestNonce === responseNonce) {
ipcRenderer.off(channelResponse, responseListener);
window.postMessage(requestNonce, '*', e.ports);
if (nonce === responseNonce) {
ipcRenderer.off(responseChannel, responseListener);
window.postMessage(nonce, '*', e.ports);
}
};
// request message port from main and await result
ipcRenderer.on(channelResponse, responseListener);
ipcRenderer.send(channelRequest, requestNonce);
// handle reply from main
ipcRenderer.on(responseChannel, responseListener);
}
}
},
@@ -322,7 +320,7 @@
* actual value will be set after `resolveConfiguration`
* has finished.
*
* @returns {ISandboxConfiguration | undefined}
* @returns {ISandboxConfiguration | undefined}
*/
configuration() {
return configuration;
@@ -94,16 +94,15 @@ export interface ISandboxNodeProcess extends INodeProcess {
export interface IpcMessagePort {
/**
* Establish a connection via `MessagePort` to a target. The main process
* will need to transfer the port over to the `channelResponse` after listening
* to `channelRequest` with a payload of `requestNonce` so that the
* source can correlate the response.
* Acquire a `MessagePort`. The main process will transfer the port over to
* the `responseChannel` with a payload of `requestNonce` so that the source can
* correlate the response.
*
* The source should install a `window.on('message')` listener, ensuring `e.data`
* matches `requestNonce`, `e.source` matches `window` and then receiving the
* `MessagePort` via `e.ports[0]`.
* matches `nonce`, `e.source` matches `window` and then receiving the `MessagePort`
* via `e.ports[0]`.
*/
connect(channelRequest: string, channelResponse: string, requestNonce: string): void;
acquire(responseChannel: string, nonce: string): void;
}
export interface ISandboxContext {
@@ -31,6 +31,12 @@ export interface IStorageItemsChangeEvent {
readonly deleted?: Set<string>;
}
export function isStorageItemsChangeEvent(thing: unknown): thing is IStorageItemsChangeEvent {
const candidate = thing as IStorageItemsChangeEvent | undefined;
return candidate?.changed instanceof Map || candidate?.deleted instanceof Set;
}
export interface IStorageDatabase {
readonly onDidChangeItemsExternal: Event<IStorageItemsChangeEvent>;
@@ -11,8 +11,9 @@ import { join } from 'vs/base/common/path';
import { isWindows } from 'vs/base/common/platform';
import { generateUuid } from 'vs/base/common/uuid';
import { Promises } from 'vs/base/node/pfs';
import { IStorageDatabase, IStorageItemsChangeEvent, Storage } from 'vs/base/parts/storage/common/storage';
import { isStorageItemsChangeEvent, IStorageDatabase, IStorageItemsChangeEvent, Storage } from 'vs/base/parts/storage/common/storage';
import { ISQLiteStorageDatabaseOptions, SQLiteStorageDatabase } from 'vs/base/parts/storage/node/storage';
import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler';
import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils';
flakySuite('Storage Library', function () {
@@ -29,133 +30,142 @@ flakySuite('Storage Library', function () {
return Promises.rm(testDir);
});
test('basics', async () => {
const storage = new Storage(new SQLiteStorageDatabase(join(testDir, 'storage.db')));
test('basics', () => {
return runWithFakedTimers({}, async function () {
const storage = new Storage(new SQLiteStorageDatabase(join(testDir, 'storage.db')));
await storage.init();
await storage.init();
// Empty fallbacks
strictEqual(storage.get('foo', 'bar'), 'bar');
strictEqual(storage.getNumber('foo', 55), 55);
strictEqual(storage.getBoolean('foo', true), true);
// Empty fallbacks
strictEqual(storage.get('foo', 'bar'), 'bar');
strictEqual(storage.getNumber('foo', 55), 55);
strictEqual(storage.getBoolean('foo', true), true);
let changes = new Set<string>();
storage.onDidChangeStorage(key => {
changes.add(key);
let changes = new Set<string>();
storage.onDidChangeStorage(key => {
changes.add(key);
});
await storage.whenFlushed(); // returns immediately when no pending updates
// Simple updates
const set1Promise = storage.set('bar', 'foo');
const set2Promise = storage.set('barNumber', 55);
const set3Promise = storage.set('barBoolean', true);
let flushPromiseResolved = false;
storage.whenFlushed().then(() => flushPromiseResolved = true);
strictEqual(storage.get('bar'), 'foo');
strictEqual(storage.getNumber('barNumber'), 55);
strictEqual(storage.getBoolean('barBoolean'), true);
strictEqual(changes.size, 3);
ok(changes.has('bar'));
ok(changes.has('barNumber'));
ok(changes.has('barBoolean'));
let setPromiseResolved = false;
await Promise.all([set1Promise, set2Promise, set3Promise]).then(() => setPromiseResolved = true);
strictEqual(setPromiseResolved, true);
strictEqual(flushPromiseResolved, true);
changes = new Set<string>();
// Does not trigger events for same update values
storage.set('bar', 'foo');
storage.set('barNumber', 55);
storage.set('barBoolean', true);
strictEqual(changes.size, 0);
// Simple deletes
const delete1Promise = storage.delete('bar');
const delete2Promise = storage.delete('barNumber');
const delete3Promise = storage.delete('barBoolean');
ok(!storage.get('bar'));
ok(!storage.getNumber('barNumber'));
ok(!storage.getBoolean('barBoolean'));
strictEqual(changes.size, 3);
ok(changes.has('bar'));
ok(changes.has('barNumber'));
ok(changes.has('barBoolean'));
changes = new Set<string>();
// Does not trigger events for same delete values
storage.delete('bar');
storage.delete('barNumber');
storage.delete('barBoolean');
strictEqual(changes.size, 0);
let deletePromiseResolved = false;
await Promise.all([delete1Promise, delete2Promise, delete3Promise]).then(() => deletePromiseResolved = true);
strictEqual(deletePromiseResolved, true);
await storage.close();
await storage.close(); // it is ok to call this multiple times
});
await storage.whenFlushed(); // returns immediately when no pending updates
// Simple updates
const set1Promise = storage.set('bar', 'foo');
const set2Promise = storage.set('barNumber', 55);
const set3Promise = storage.set('barBoolean', true);
let flushPromiseResolved = false;
storage.whenFlushed().then(() => flushPromiseResolved = true);
strictEqual(storage.get('bar'), 'foo');
strictEqual(storage.getNumber('barNumber'), 55);
strictEqual(storage.getBoolean('barBoolean'), true);
strictEqual(changes.size, 3);
ok(changes.has('bar'));
ok(changes.has('barNumber'));
ok(changes.has('barBoolean'));
let setPromiseResolved = false;
await Promise.all([set1Promise, set2Promise, set3Promise]).then(() => setPromiseResolved = true);
strictEqual(setPromiseResolved, true);
strictEqual(flushPromiseResolved, true);
changes = new Set<string>();
// Does not trigger events for same update values
storage.set('bar', 'foo');
storage.set('barNumber', 55);
storage.set('barBoolean', true);
strictEqual(changes.size, 0);
// Simple deletes
const delete1Promise = storage.delete('bar');
const delete2Promise = storage.delete('barNumber');
const delete3Promise = storage.delete('barBoolean');
ok(!storage.get('bar'));
ok(!storage.getNumber('barNumber'));
ok(!storage.getBoolean('barBoolean'));
strictEqual(changes.size, 3);
ok(changes.has('bar'));
ok(changes.has('barNumber'));
ok(changes.has('barBoolean'));
changes = new Set<string>();
// Does not trigger events for same delete values
storage.delete('bar');
storage.delete('barNumber');
storage.delete('barBoolean');
strictEqual(changes.size, 0);
let deletePromiseResolved = false;
await Promise.all([delete1Promise, delete2Promise, delete3Promise]).then(() => deletePromiseResolved = true);
strictEqual(deletePromiseResolved, true);
await storage.close();
await storage.close(); // it is ok to call this multiple times
});
test('external changes', async () => {
test('external changes', () => {
return runWithFakedTimers({}, async function () {
class TestSQLiteStorageDatabase extends SQLiteStorageDatabase {
private readonly _onDidChangeItemsExternal = new Emitter<IStorageItemsChangeEvent>();
override get onDidChangeItemsExternal(): Event<IStorageItemsChangeEvent> { return this._onDidChangeItemsExternal.event; }
class TestSQLiteStorageDatabase extends SQLiteStorageDatabase {
private readonly _onDidChangeItemsExternal = new Emitter<IStorageItemsChangeEvent>();
override get onDidChangeItemsExternal(): Event<IStorageItemsChangeEvent> { return this._onDidChangeItemsExternal.event; }
fireDidChangeItemsExternal(event: IStorageItemsChangeEvent): void {
this._onDidChangeItemsExternal.fire(event);
fireDidChangeItemsExternal(event: IStorageItemsChangeEvent): void {
this._onDidChangeItemsExternal.fire(event);
}
}
}
const database = new TestSQLiteStorageDatabase(join(testDir, 'storage.db'));
const storage = new Storage(database);
const database = new TestSQLiteStorageDatabase(join(testDir, 'storage.db'));
const storage = new Storage(database);
let changes = new Set<string>();
storage.onDidChangeStorage(key => {
changes.add(key);
let changes = new Set<string>();
storage.onDidChangeStorage(key => {
changes.add(key);
});
await storage.init();
await storage.set('foo', 'bar');
ok(changes.has('foo'));
changes.clear();
// Nothing happens if changing to same value
const changed = new Map<string, string>();
changed.set('foo', 'bar');
database.fireDidChangeItemsExternal({ changed });
strictEqual(changes.size, 0);
// Change is accepted if valid
changed.set('foo', 'bar1');
database.fireDidChangeItemsExternal({ changed });
ok(changes.has('foo'));
strictEqual(storage.get('foo'), 'bar1');
changes.clear();
// Delete is accepted
const deleted = new Set<string>(['foo']);
database.fireDidChangeItemsExternal({ deleted });
ok(changes.has('foo'));
strictEqual(storage.get('foo', undefined), undefined);
changes.clear();
// Nothing happens if changing to same value
database.fireDidChangeItemsExternal({ deleted });
strictEqual(changes.size, 0);
strictEqual(isStorageItemsChangeEvent({ changed }), true);
strictEqual(isStorageItemsChangeEvent({ deleted }), true);
strictEqual(isStorageItemsChangeEvent({ changed, deleted }), true);
strictEqual(isStorageItemsChangeEvent(undefined), false);
strictEqual(isStorageItemsChangeEvent({ changed: 'yes', deleted: false }), false);
await storage.close();
});
await storage.init();
await storage.set('foo', 'bar');
ok(changes.has('foo'));
changes.clear();
// Nothing happens if changing to same value
const changed = new Map<string, string>();
changed.set('foo', 'bar');
database.fireDidChangeItemsExternal({ changed });
strictEqual(changes.size, 0);
// Change is accepted if valid
changed.set('foo', 'bar1');
database.fireDidChangeItemsExternal({ changed });
ok(changes.has('foo'));
strictEqual(storage.get('foo'), 'bar1');
changes.clear();
// Delete is accepted
const deleted = new Set<string>(['foo']);
database.fireDidChangeItemsExternal({ deleted });
ok(changes.has('foo'));
strictEqual(storage.get('foo', undefined), undefined);
changes.clear();
// Nothing happens if changing to same value
database.fireDidChangeItemsExternal({ deleted });
strictEqual(changes.size, 0);
await storage.close();
});
test('close flushes data', async () => {
@@ -651,27 +661,27 @@ flakySuite('SQLite Storage Library', function () {
storage.set('foo', 'bar');
storage.set('some/foo/path', 'some/bar/path');
await timeout(10);
await timeout(2);
storage.set('foo1', 'bar');
storage.set('some/foo1/path', 'some/bar/path');
await timeout(10);
await timeout(2);
storage.set('foo2', 'bar');
storage.set('some/foo2/path', 'some/bar/path');
await timeout(10);
await timeout(2);
storage.delete('foo1');
storage.delete('some/foo1/path');
await timeout(10);
await timeout(2);
storage.delete('foo4');
storage.delete('some/foo4/path');
await timeout(70);
await timeout(5);
storage.set('foo3', 'bar');
await storage.set('some/foo3/path', 'some/bar/path');
@@ -13,7 +13,8 @@ import * as mouse from 'vs/base/browser/mouseEvent';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import * as _ from 'vs/base/parts/tree/browser/tree';
import { IDragAndDropData } from 'vs/base/browser/dnd';
import { KeyCode, KeyMod, Keybinding, SimpleKeybinding, createKeybinding } from 'vs/base/common/keyCodes';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { createKeybinding, Keybinding, SimpleKeybinding } from 'vs/base/common/keybindings';
export interface IKeyBindingCallback {
(tree: _.ITree, event: IKeyboardEvent): void;
@@ -112,8 +113,8 @@ export class DefaultController implements _.IController {
this.downKeyBindingDispatcher.set(KeyCode.RightArrow, (t, e) => this.onRight(t, e));
if (platform.isMacintosh) {
this.downKeyBindingDispatcher.set(KeyMod.CtrlCmd | KeyCode.UpArrow, (t, e) => this.onLeft(t, e));
this.downKeyBindingDispatcher.set(KeyMod.WinCtrl | KeyCode.KEY_N, (t, e) => this.onDown(t, e));
this.downKeyBindingDispatcher.set(KeyMod.WinCtrl | KeyCode.KEY_P, (t, e) => this.onUp(t, e));
this.downKeyBindingDispatcher.set(KeyMod.WinCtrl | KeyCode.KeyN, (t, e) => this.onDown(t, e));
this.downKeyBindingDispatcher.set(KeyMod.WinCtrl | KeyCode.KeyP, (t, e) => this.onUp(t, e));
}
this.downKeyBindingDispatcher.set(KeyCode.PageUp, (t, e) => this.onPageUp(t, e));
this.downKeyBindingDispatcher.set(KeyCode.PageDown, (t, e) => this.onPageDown(t, e));
+151 -30
View File
@@ -6,48 +6,106 @@
import * as assert from 'assert';
import { renderMarkdown, renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer';
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
import * as marked from 'vs/base/common/marked/marked';
import { parse } from 'vs/base/common/marshalling';
import { URI } from 'vs/base/common/uri';
suite('MarkdownRenderer', () => {
suite('Images', () => {
function strToNode(str: string): HTMLElement {
return new DOMParser().parseFromString(str, 'text/html').body.firstChild as HTMLElement;
}
function assertNodeEquals(actualNode: HTMLElement, expectedHtml: string) {
const expectedNode = strToNode(expectedHtml);
assert.ok(
actualNode.isEqualNode(expectedNode),
`Expected: ${expectedNode.outerHTML}\nActual: ${actualNode.outerHTML}`);
}
suite('MarkdownRenderer', () => {
suite('Sanitization', () => {
test('Should not render images with unknown schemes', () => {
const markdown = { value: `![image](no-such://example.com/cat.gif)` };
const result: HTMLElement = renderMarkdown(markdown).element;
assert.strictEqual(result.innerHTML, '<p><img alt="image"></p>');
});
});
suite('Images', () => {
test('image rendering conforms to default', () => {
const markdown = { value: `![image](someimageurl 'caption')` };
const result: HTMLElement = renderMarkdown(markdown);
const renderer = new marked.Renderer();
const imageFromMarked = marked(markdown.value, {
renderer
}).trim();
assert.strictEqual(result.innerHTML, imageFromMarked);
const markdown = { value: `![image](http://example.com/cat.gif 'caption')` };
const result: HTMLElement = renderMarkdown(markdown).element;
assertNodeEquals(result, '<div><p><img title="caption" alt="image" src="http://example.com/cat.gif"></p></div>');
});
test('image rendering conforms to default without title', () => {
const markdown = { value: `![image](someimageurl)` };
const result: HTMLElement = renderMarkdown(markdown);
const renderer = new marked.Renderer();
const imageFromMarked = marked(markdown.value, {
renderer
}).trim();
assert.strictEqual(result.innerHTML, imageFromMarked);
const markdown = { value: `![image](http://example.com/cat.gif)` };
const result: HTMLElement = renderMarkdown(markdown).element;
assertNodeEquals(result, '<div><p><img alt="image" src="http://example.com/cat.gif"></p></div>');
});
test('image width from title params', () => {
let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|width=100 'caption')` });
assert.strictEqual(result.innerHTML, `<p><img src="someimageurl" alt="image" title="caption" width="100"></p>`);
const result: HTMLElement = renderMarkdown({ value: `![image](http://example.com/cat.gif|width=100px 'caption')` }).element;
assertNodeEquals(result, `<div><p><img width="100" title="caption" alt="image" src="http://example.com/cat.gif"></p></div>`);
});
test('image height from title params', () => {
let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|height=100 'caption')` });
assert.strictEqual(result.innerHTML, `<p><img src="someimageurl" alt="image" title="caption" height="100"></p>`);
const result: HTMLElement = renderMarkdown({ value: `![image](http://example.com/cat.gif|height=100 'caption')` }).element;
assertNodeEquals(result, `<div><p><img height="100" title="caption" alt="image" src="http://example.com/cat.gif"></p></div>`);
});
test('image width and height from title params', () => {
let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|height=200,width=100 'caption')` });
assert.strictEqual(result.innerHTML, `<p><img src="someimageurl" alt="image" title="caption" width="100" height="200"></p>`);
const result: HTMLElement = renderMarkdown({ value: `![image](http://example.com/cat.gif|height=200,width=100 'caption')` }).element;
assertNodeEquals(result, `<div><p><img height="200" width="100" title="caption" alt="image" src="http://example.com/cat.gif"></p></div>`);
});
});
suite('Code block renderer', () => {
const simpleCodeBlockRenderer = (code: string): Promise<HTMLElement> => {
const element = document.createElement('code');
element.textContent = code;
return Promise.resolve(element);
};
test('asyncRenderCallback should be invoked for code blocks', () => {
const markdown = { value: '```js\n1 + 1;\n```' };
return new Promise<void>(resolve => {
renderMarkdown(markdown, {
asyncRenderCallback: resolve,
codeBlockRenderer: simpleCodeBlockRenderer
});
});
});
test('asyncRenderCallback should not be invoked if result is immediately disposed', () => {
const markdown = { value: '```js\n1 + 1;\n```' };
return new Promise<void>((resolve, reject) => {
const result = renderMarkdown(markdown, {
asyncRenderCallback: reject,
codeBlockRenderer: simpleCodeBlockRenderer
});
result.dispose();
setTimeout(resolve, 250);
});
});
test('asyncRenderCallback should not be invoked if dispose is called before code block is rendered', () => {
const markdown = { value: '```js\n1 + 1;\n```' };
return new Promise<void>((resolve, reject) => {
let resolveCodeBlockRendering: (x: HTMLElement) => void;
const result = renderMarkdown(markdown, {
asyncRenderCallback: reject,
codeBlockRenderer: () => {
return new Promise(resolve => {
resolveCodeBlockRendering = resolve;
});
}
});
setTimeout(() => {
result.dispose();
resolveCodeBlockRendering(document.createElement('code'));
setTimeout(resolve, 250);
}, 250);
});
});
});
suite('ThemeIcons Support On', () => {
@@ -56,7 +114,7 @@ suite('MarkdownRenderer', () => {
const mds = new MarkdownString(undefined, { supportThemeIcons: true });
mds.appendText('$(zap) $(not a theme icon) $(add)');
let result: HTMLElement = renderMarkdown(mds);
let result: HTMLElement = renderMarkdown(mds).element;
assert.strictEqual(result.innerHTML, `<p>$(zap)&nbsp;$(not&nbsp;a&nbsp;theme&nbsp;icon)&nbsp;$(add)</p>`);
});
@@ -64,7 +122,7 @@ suite('MarkdownRenderer', () => {
const mds = new MarkdownString(undefined, { supportThemeIcons: true });
mds.appendMarkdown('$(zap) $(not a theme icon) $(add)');
let result: HTMLElement = renderMarkdown(mds);
let result: HTMLElement = renderMarkdown(mds).element;
assert.strictEqual(result.innerHTML, `<p><span class="codicon codicon-zap"></span> $(not a theme icon) <span class="codicon codicon-add"></span></p>`);
});
@@ -72,10 +130,40 @@ suite('MarkdownRenderer', () => {
const mds = new MarkdownString(undefined, { supportThemeIcons: true });
mds.appendMarkdown('\\$(zap) $(not a theme icon) $(add)');
let result: HTMLElement = renderMarkdown(mds);
let result: HTMLElement = renderMarkdown(mds).element;
assert.strictEqual(result.innerHTML, `<p>$(zap) $(not a theme icon) <span class="codicon codicon-add"></span></p>`);
});
test('render icon in link', () => {
const mds = new MarkdownString(undefined, { supportThemeIcons: true });
mds.appendMarkdown(`[$(zap)-link](#link)`);
let result: HTMLElement = renderMarkdown(mds).element;
assert.strictEqual(result.innerHTML, `<p><a title="#link" data-href="#link" href="#"><span class="codicon codicon-zap"></span>-link</a></p>`);
});
test('render icon in table', () => {
const mds = new MarkdownString(undefined, { supportThemeIcons: true });
mds.appendMarkdown(`
| text | text |
|--------|----------------------|
| $(zap) | [$(zap)-link](#link) |`);
let result: HTMLElement = renderMarkdown(mds).element;
assert.strictEqual(result.innerHTML, `<table>
<thead>
<tr>
<th>text</th>
<th>text</th>
</tr>
</thead>
<tbody><tr>
<td><span class="codicon codicon-zap"></span></td>
<td><a title="#link" data-href="#link" href="#"><span class="codicon codicon-zap"></span>-link</a></td>
</tr>
</tbody></table>
`);
});
});
suite('ThemeIcons Support Off', () => {
@@ -84,7 +172,7 @@ suite('MarkdownRenderer', () => {
const mds = new MarkdownString(undefined, { supportThemeIcons: false });
mds.appendText('$(zap) $(not a theme icon) $(add)');
let result: HTMLElement = renderMarkdown(mds);
let result: HTMLElement = renderMarkdown(mds).element;
assert.strictEqual(result.innerHTML, `<p>$(zap)&nbsp;$(not&nbsp;a&nbsp;theme&nbsp;icon)&nbsp;$(add)</p>`);
});
@@ -92,16 +180,15 @@ suite('MarkdownRenderer', () => {
const mds = new MarkdownString(undefined, { supportThemeIcons: false });
mds.appendMarkdown('\\$(zap) $(not a theme icon) $(add)');
let result: HTMLElement = renderMarkdown(mds);
let result: HTMLElement = renderMarkdown(mds).element;
assert.strictEqual(result.innerHTML, `<p>$(zap) $(not a theme icon) $(add)</p>`);
});
});
test('npm Hover Run Script not working #90855', function () {
const md: IMarkdownString = JSON.parse('{"value":"[Run Script](command:npm.runScriptFromHover?%7B%22documentUri%22%3A%7B%22%24mid%22%3A1%2C%22fsPath%22%3A%22c%3A%5C%5CUsers%5C%5Cjrieken%5C%5CCode%5C%5C_sample%5C%5Cfoo%5C%5Cpackage.json%22%2C%22_sep%22%3A1%2C%22external%22%3A%22file%3A%2F%2F%2Fc%253A%2FUsers%2Fjrieken%2FCode%2F_sample%2Ffoo%2Fpackage.json%22%2C%22path%22%3A%22%2Fc%3A%2FUsers%2Fjrieken%2FCode%2F_sample%2Ffoo%2Fpackage.json%22%2C%22scheme%22%3A%22file%22%7D%2C%22script%22%3A%22echo%22%7D \\"Run the script as a task\\")","supportThemeIcons":false,"isTrusted":true,"uris":{"__uri_e49443":{"$mid":1,"fsPath":"c:\\\\Users\\\\jrieken\\\\Code\\\\_sample\\\\foo\\\\package.json","_sep":1,"external":"file:///c%3A/Users/jrieken/Code/_sample/foo/package.json","path":"/c:/Users/jrieken/Code/_sample/foo/package.json","scheme":"file"},"command:npm.runScriptFromHover?%7B%22documentUri%22%3A%7B%22%24mid%22%3A1%2C%22fsPath%22%3A%22c%3A%5C%5CUsers%5C%5Cjrieken%5C%5CCode%5C%5C_sample%5C%5Cfoo%5C%5Cpackage.json%22%2C%22_sep%22%3A1%2C%22external%22%3A%22file%3A%2F%2F%2Fc%253A%2FUsers%2Fjrieken%2FCode%2F_sample%2Ffoo%2Fpackage.json%22%2C%22path%22%3A%22%2Fc%3A%2FUsers%2Fjrieken%2FCode%2F_sample%2Ffoo%2Fpackage.json%22%2C%22scheme%22%3A%22file%22%7D%2C%22script%22%3A%22echo%22%7D":{"$mid":1,"path":"npm.runScriptFromHover","scheme":"command","query":"{\\"documentUri\\":\\"__uri_e49443\\",\\"script\\":\\"echo\\"}"}}}');
const element = renderMarkdown(md);
const element = renderMarkdown(md).element;
const anchor = element.querySelector('a')!;
assert.ok(anchor);
@@ -131,4 +218,38 @@ suite('MarkdownRenderer', () => {
assert.strictEqual(result, expected);
});
});
suite('supportHtml', () => {
test('supportHtml is disabled by default', () => {
const mds = new MarkdownString(undefined, {});
mds.appendMarkdown('a<b>b</b>c');
const result = renderMarkdown(mds).element;
assert.strictEqual(result.innerHTML, `<p>abc</p>`);
});
test('Renders html when supportHtml=true', () => {
const mds = new MarkdownString(undefined, { supportHtml: true });
mds.appendMarkdown('a<b>b</b>c');
const result = renderMarkdown(mds).element;
assert.strictEqual(result.innerHTML, `<p>a<b>b</b>c</p>`);
});
test('Should not include scripts even when supportHtml=true', () => {
const mds = new MarkdownString(undefined, { supportHtml: true });
mds.appendMarkdown('a<b onclick="alert(1)">b</b><script>alert(2)</script>c');
const result = renderMarkdown(mds).element;
assert.strictEqual(result.innerHTML, `<p>a<b>b</b>c</p>`);
});
test('Should not render html appended as text', () => {
const mds = new MarkdownString(undefined, { supportHtml: true });
mds.appendText('a<b>b</b>c');
const result = renderMarkdown(mds).element;
assert.strictEqual(result.innerHTML, `<p>a&lt;b&gt;b&lt;/b&gt;c</p>`);
});
});
});
@@ -659,6 +659,40 @@ suite('IndexTreeModel', () => {
assert.deepStrictEqual(toArray(list), ['vscode', '.build', 'github', 'build.js', 'build']);
});
test('recursive filter updates when children change (#133272)', () => {
const list: ITreeNode<string>[] = [];
let query = '';
const filter = new class implements ITreeFilter<string> {
filter(element: string): TreeVisibility {
return element.includes(query) ? TreeVisibility.Visible : TreeVisibility.Recurse;
}
};
const model = new IndexTreeModel<string>('test', toList(list), 'root', { filter });
model.splice([0], 0, [
{
element: 'a',
children: [
{ element: 'b' },
],
},
]);
assert.deepStrictEqual(toArray(list), ['a', 'b']);
query = 'visible';
model.refilter();
assert.deepStrictEqual(toArray(list), []);
model.splice([0, 0, 0], 0, [
{
element: 'visible', children: []
},
]);
assert.deepStrictEqual(toArray(list), ['a', 'b', 'visible']);
});
test('recursive filter with collapse', () => {
const list: ITreeNode<string>[] = [];
let query = new RegExp('');

Some files were not shown because too many files have changed in this diff Show More