Archived
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:
@@ -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
@@ -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
@@ -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 Contributor’s 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 party’s
|
||||
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
|
||||
party’s 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 party’s 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.
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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 } {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => ' '.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 {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
@@ -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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
var unescapes = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
''': "'"
|
||||
};
|
||||
var rescaped = /(&|<|>|"|')/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.
|
||||
+669
-463
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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)[];
|
||||
}
|
||||
@@ -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
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+2941
-2708
File diff suppressed because one or more lines are too long
@@ -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
@@ -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:
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface IProcessEnvironment {
|
||||
*/
|
||||
export interface INodeProcess {
|
||||
platform: string;
|
||||
arch: string;
|
||||
env: IProcessEnvironment;
|
||||
nextTick?: (callback: (...args: any[]) => void) => void;
|
||||
versions?: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,6 +17,8 @@ export interface UriParts {
|
||||
scheme: string;
|
||||
authority?: string;
|
||||
path?: string;
|
||||
query?: string;
|
||||
fragment?: string;
|
||||
}
|
||||
|
||||
export interface IRawURITransformer {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 !== ' ') {
|
||||
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));
|
||||
|
||||
@@ -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: `` };
|
||||
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: `` };
|
||||
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: `` };
|
||||
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: `` };
|
||||
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: `` };
|
||||
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: `` });
|
||||
assert.strictEqual(result.innerHTML, `<p><img src="someimageurl" alt="image" title="caption" width="100"></p>`);
|
||||
const result: HTMLElement = renderMarkdown({ value: `` }).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: `` });
|
||||
assert.strictEqual(result.innerHTML, `<p><img src="someimageurl" alt="image" title="caption" height="100"></p>`);
|
||||
const result: HTMLElement = renderMarkdown({ value: `` }).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: `` });
|
||||
assert.strictEqual(result.innerHTML, `<p><img src="someimageurl" alt="image" title="caption" width="100" height="200"></p>`);
|
||||
const result: HTMLElement = renderMarkdown({ value: `` }).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) $(not a theme icon) $(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) $(not a theme icon) $(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<b>b</b>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
Reference in New Issue
Block a user