Merge remote-tracking branch 'origin/master' into tyriar/115053

This commit is contained in:
Daniel Imms 2021-02-05 07:35:58 -08:00
commit 6848fcd7b0
228 changed files with 4077 additions and 2900 deletions

View file

@ -1,8 +1,9 @@
name: "Prevent yarn.lock changes in PRs"
name: Prevent yarn.lock changes in PRs
on: [pull_request]
jobs:
main:
name: Prevent yarn.lock changes in PRs
runs-on: ubuntu-latest
steps:
- id: file_changes

View file

@ -1,3 +1,3 @@
disturl "https://electronjs.org/headers"
target "11.2.1"
target "11.2.2"
runtime "electron"

View file

@ -207,7 +207,7 @@ steps:
VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin" \
./scripts/test-integration.sh --build --tfs "Integration Tests"
displayName: Run integration tests (Electron)
timeoutInMinutes: 5
timeoutInMinutes: 10
condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false'))
- script: |
@ -244,6 +244,16 @@ steps:
displayName: Run smoke tests (Electron)
condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false'))
- script: |
set -e
APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH)
APP_NAME="`ls $APP_ROOT | head -n 1`"
VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin" \
yarn smoketest-no-compile --build "$APP_ROOT/$APP_NAME" --remote
timeoutInMinutes: 5
displayName: Run smoke tests (Remote)
condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false'))
- script: |
set -e
VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin" \

View file

@ -172,7 +172,7 @@ steps:
VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-linux-$(VSCODE_ARCH)" \
DISPLAY=:10 ./scripts/test-integration.sh --build --tfs "Integration Tests"
displayName: Run integration tests (Electron)
timeoutInMinutes: 5
timeoutInMinutes: 10
condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false'))
- script: |

View file

@ -3,7 +3,7 @@ $ErrorActionPreference = "Stop"
$CertBytes = [System.Convert]::FromBase64String($CertBase64)
$CertCollection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
$CertCollection.Import($CertBytes, $null, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)
$CertCollection.Import($CertBytes, $null, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable -bxor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet)
$CertStore = New-Object System.Security.Cryptography.X509Certificates.X509Store("My","LocalMachine")
$CertStore.Open("ReadWrite")

View file

@ -60,12 +60,12 @@
"git": {
"name": "electron",
"repositoryUrl": "https://github.com/electron/electron",
"commitHash": "8805b996e0d8cfb6e3921f9b586366bafb125b59"
"commitHash": "805e442ff873e10735a1ea18021f491597afa885"
}
},
"isOnlyProductionDependency": true,
"license": "MIT",
"version": "11.2.1"
"version": "11.2.2"
},
{
"component": {

View file

@ -61,6 +61,18 @@
"default": true,
"description": "%css.validate.desc%"
},
"css.hover.documentation": {
"type": "boolean",
"scope": "resource",
"default": true,
"description": "%css.hover.documentation%"
},
"css.hover.references": {
"type": "boolean",
"scope": "resource",
"default": true,
"description": "%css.hover.references%"
},
"css.lint.compatibleVendorPrefixes": {
"type": "string",
"scope": "resource",
@ -316,6 +328,18 @@
"default": true,
"description": "%scss.validate.desc%"
},
"scss.hover.documentation": {
"type": "boolean",
"scope": "resource",
"default": true,
"description": "%scss.hover.documentation%"
},
"scss.hover.references": {
"type": "boolean",
"scope": "resource",
"default": true,
"description": "%scss.hover.references%"
},
"scss.lint.compatibleVendorPrefixes": {
"type": "string",
"scope": "resource",
@ -561,6 +585,18 @@
"default": true,
"description": "%less.validate.desc%"
},
"less.hover.documentation": {
"type": "boolean",
"scope": "resource",
"default": true,
"description": "%less.hover.documentation%"
},
"less.hover.references": {
"type": "boolean",
"scope": "resource",
"default": true,
"description": "%less.hover.references%"
},
"less.lint.compatibleVendorPrefixes": {
"type": "string",
"scope": "resource",

View file

@ -28,6 +28,8 @@
"css.trace.server.desc": "Traces the communication between VS Code and the CSS language server.",
"css.validate.title": "Controls CSS validation and problem severities.",
"css.validate.desc": "Enables or disables all validations.",
"css.hover.documentation": "Show tag and attribute documentation in CSS hovers.",
"css.hover.references": "Show references to MDN in CSS hovers.",
"less.title": "LESS",
"less.completion.triggerPropertyValueCompletion.desc": "By default, VS Code triggers property value completion after selecting a CSS property. Use this setting to disable this behavior.",
"less.completion.completePropertyWithSemicolon.desc": "Insert semicolon at end of line when completing CSS properties",
@ -53,6 +55,8 @@
"less.lint.zeroUnits.desc": "No unit for zero needed.",
"less.validate.title": "Controls LESS validation and problem severities.",
"less.validate.desc": "Enables or disables all validations.",
"less.hover.documentation": "Show tag and attribute documentation in LESS hovers.",
"less.hover.references": "Show references to MDN in LESS hovers.",
"scss.title": "SCSS (Sass)",
"scss.completion.triggerPropertyValueCompletion.desc": "By default, VS Code triggers property value completion after selecting a CSS property. Use this setting to disable this behavior.",
"scss.completion.completePropertyWithSemicolon.desc": "Insert semicolon at end of line when completing CSS properties",
@ -78,6 +82,8 @@
"scss.lint.zeroUnits.desc": "No unit for zero needed.",
"scss.validate.title": "Controls SCSS validation and problem severities.",
"scss.validate.desc": "Enables or disables all validations.",
"scss.hover.documentation": "Show tag and attribute documentation in SCSS hovers.",
"scss.hover.references": "Show references to MDN in SCSS hovers.",
"css.colorDecorators.enable.deprecationMessage": "The setting `css.colorDecorators.enable` has been deprecated in favor of `editor.colorDecorators`.",
"scss.colorDecorators.enable.deprecationMessage": "The setting `scss.colorDecorators.enable` has been deprecated in favor of `editor.colorDecorators`.",
"less.colorDecorators.enable.deprecationMessage": "The setting `less.colorDecorators.enable` has been deprecated in favor of `editor.colorDecorators`."

View file

@ -10,7 +10,7 @@
"main": "./out/node/cssServerMain",
"browser": "./dist/browser/cssServerMain",
"dependencies": {
"vscode-css-languageservice": "^5.0.3",
"vscode-css-languageservice": "^5.1.0",
"vscode-languageserver": "^7.0.0",
"vscode-uri": "^3.0.2"
},

View file

@ -206,10 +206,10 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
return runSafeAsync(async () => {
const document = documents.get(textDocumentPosition.textDocument.uri);
if (document) {
await dataProvidersReady;
const [settings,] = await Promise.all([getDocumentSettings(document), dataProvidersReady]);
const styleSheet = stylesheets.get(document);
const documentContext = getDocumentContext(document.uri, workspaceFolders);
return getLanguageService(document).doComplete2(document, textDocumentPosition.position, styleSheet, documentContext);
return getLanguageService(document).doComplete2(document, textDocumentPosition.position, styleSheet, documentContext, settings?.completion);
}
return null;
}, null, `Error while computing completions for ${textDocumentPosition.textDocument.uri}`, token);
@ -219,9 +219,9 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
return runSafeAsync(async () => {
const document = documents.get(textDocumentPosition.textDocument.uri);
if (document) {
await dataProvidersReady;
const [settings,] = await Promise.all([getDocumentSettings(document), dataProvidersReady]);
const styleSheet = stylesheets.get(document);
return getLanguageService(document).doHover(document, textDocumentPosition.position, styleSheet);
return getLanguageService(document).doHover(document, textDocumentPosition.position, styleSheet, settings?.hover);
}
return null;
}, null, `Error while computing hover for ${textDocumentPosition.textDocument.uri}`, token);

View file

@ -12,10 +12,10 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679"
integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q==
vscode-css-languageservice@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-5.0.3.tgz#2d400a47e73d0bfc5bc0d3fdf5be487cfdca341b"
integrity sha512-KJt4jhCxqrgGrC02UsQsKw90dPkFknMHsH5HTInT7gkDRRfGFwEd+e2O1/E75br3TdFhvRmzjljYz5thZ58L3A==
vscode-css-languageservice@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-5.1.0.tgz#cd172d13e9e7ae23ba567c73778aee10475ff716"
integrity sha512-iLHd/WjRKgaZBXMNeUooHG+r0qlhJBkXa+3MpQQR6Rpm928cis/3OV2Mp1R80yAQevIMeDL32RIJfHoJCT/RRg==
dependencies:
vscode-languageserver-textdocument "^1.0.1"
vscode-languageserver-types "^3.16.0"

View file

@ -439,7 +439,7 @@
"@emmetio/html-matcher": "^0.3.3",
"@emmetio/math-expression": "^1.0.4",
"image-size": "^0.5.2",
"vscode-emmet-helper": "2.2.4-2",
"vscode-emmet-helper": "2.2.4-3",
"vscode-languageserver-textdocument": "^1.0.1"
}
}

View file

@ -0,0 +1,3 @@
{
"editor.minimap.enabled": false // see https://github.com/microsoft/vscode/issues/115747
}

View file

@ -54,9 +54,9 @@
integrity sha1-Rs/+oRmgoAMxKiHC2bVijLX81EI=
"@types/node@^12.19.9":
version "12.19.12"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.12.tgz#04793c2afa4ce833a9972e4c476432e30f9df47b"
integrity sha512-UwfL2uIU9arX/+/PRcIkT08/iBadGN2z6ExOROA2Dh5mAuWTBj6iJbQX4nekiV5H8cTrEG569LeX+HRco9Cbxw==
version "12.19.15"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.15.tgz#0de7e978fb43db62da369db18ea088a63673c182"
integrity sha512-lowukE3GUI+VSYSu6VcBXl14d61Rp5hA1D+61r16qnwC0lYNSqdxcvRh0pswejorHfS+HgwBasM8jLXz0/aOsw==
"emmet@https://github.com/rzhao271/emmet.git#1b2df677d8925ef5ea6da9df8845968403979a0a":
version "2.3.0"
@ -75,10 +75,10 @@ jsonc-parser@^2.3.0:
resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.3.1.tgz#59549150b133f2efacca48fe9ce1ec0659af2342"
integrity sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==
vscode-emmet-helper@2.2.4-2:
version "2.2.4-2"
resolved "https://registry.yarnpkg.com/vscode-emmet-helper/-/vscode-emmet-helper-2.2.4-2.tgz#8019188077a91dbe9a8d8c10c0b79369bb5c24d6"
integrity sha512-7UTZXwt9M1xwaV72o2YgSBVoghtDtscTgYTOl1kiPkXN9OKiM4N52hcHFA1LlRtdTvIQd4PEkgaz57F9ZT/4kg==
vscode-emmet-helper@2.2.4-3:
version "2.2.4-3"
resolved "https://registry.yarnpkg.com/vscode-emmet-helper/-/vscode-emmet-helper-2.2.4-3.tgz#b1aed7eaaf0ecc2480c65c819f6e9d42d2d3cf5a"
integrity sha512-M+pAiAro8nRiHb0/9lRoRzBod6DPlSwhOX26ZfM8n9a7/zr9ZJojxohDyGmnsApU5xVPhcrsNVI/fEagSeeO0Q==
dependencies:
emmet "https://github.com/rzhao271/emmet.git#1b2df677d8925ef5ea6da9df8845968403979a0a"
jsonc-parser "^2.3.0"

View file

@ -2140,6 +2140,15 @@
"highContrast": "#c74e39"
}
},
{
"id": "gitDecoration.renamedResourceForeground",
"description": "%colors.renamed%",
"defaults": {
"light": "#007100",
"dark": "#73C991",
"highContrast": "#73C991"
}
},
{
"id": "gitDecoration.untrackedResourceForeground",
"description": "%colors.untracked%",
@ -2180,9 +2189,9 @@
"id": "gitDecoration.conflictingResourceForeground",
"description": "%colors.conflict%",
"defaults": {
"light": "#6c6cc4",
"dark": "#6c6cc4",
"highContrast": "#6c6cc4"
"light": "#ad0707",
"dark": "#e4676b",
"highContrast": "#c74e39"
}
},
{

View file

@ -198,6 +198,7 @@
"colors.stageModified": "Color for modified resources which have been staged.",
"colors.stageDeleted": "Color for deleted resources which have been staged.",
"colors.deleted": "Color for deleted resources.",
"colors.renamed": "Color for renamed or copied resources.",
"colors.untracked": "Color for untracked resources.",
"colors.ignored": "Color for ignored resources.",
"colors.conflict": "Color for resources with conflicts.",

View file

@ -55,13 +55,13 @@ export class Resource implements SourceControlResourceState {
case Status.UNTRACKED: return localize('untracked', "Untracked");
case Status.IGNORED: return localize('ignored', "Ignored");
case Status.INTENT_TO_ADD: return localize('intent to add', "Intent to Add");
case Status.BOTH_DELETED: return localize('both deleted', "Both Deleted");
case Status.ADDED_BY_US: return localize('added by us', "Added By Us");
case Status.DELETED_BY_THEM: return localize('deleted by them', "Deleted By Them");
case Status.ADDED_BY_THEM: return localize('added by them', "Added By Them");
case Status.DELETED_BY_US: return localize('deleted by us', "Deleted By Us");
case Status.BOTH_ADDED: return localize('both added', "Both Added");
case Status.BOTH_MODIFIED: return localize('both modified', "Both Modified");
case Status.BOTH_DELETED: return localize('both deleted', "Conflict: Both Deleted");
case Status.ADDED_BY_US: return localize('added by us', "Conflict: Added By Us");
case Status.DELETED_BY_THEM: return localize('deleted by them', "Conflict: Deleted By Them");
case Status.ADDED_BY_THEM: return localize('added by them', "Conflict: Added By Them");
case Status.DELETED_BY_US: return localize('deleted by us', "Conflict: Deleted By Us");
case Status.BOTH_ADDED: return localize('both added', "Conflict: Both Added");
case Status.BOTH_MODIFIED: return localize('both modified', "Conflict: Both Modified");
default: return '';
}
}
@ -199,12 +199,13 @@ export class Resource implements SourceControlResourceState {
case Status.DELETED_BY_US:
return 'D';
case Status.INDEX_COPIED:
return 'C';
case Status.BOTH_DELETED:
case Status.ADDED_BY_US:
case Status.ADDED_BY_THEM:
case Status.BOTH_ADDED:
case Status.BOTH_MODIFIED:
return 'C';
return '!'; // Using ! instead of ⚠, because the latter looks really bad on windows
default:
throw new Error('Unknown git status: ' + this.type);
}
@ -223,12 +224,13 @@ export class Resource implements SourceControlResourceState {
case Status.INDEX_ADDED:
case Status.INTENT_TO_ADD:
return new ThemeColor('gitDecoration.addedResourceForeground');
case Status.INDEX_COPIED:
case Status.INDEX_RENAMED:
return new ThemeColor('gitDecoration.renamedResourceForeground');
case Status.UNTRACKED:
return new ThemeColor('gitDecoration.untrackedResourceForeground');
case Status.IGNORED:
return new ThemeColor('gitDecoration.ignoredResourceForeground');
case Status.INDEX_COPIED:
case Status.BOTH_DELETED:
case Status.ADDED_BY_US:
case Status.DELETED_BY_THEM:
@ -246,10 +248,10 @@ export class Resource implements SourceControlResourceState {
switch (this.type) {
case Status.INDEX_MODIFIED:
case Status.MODIFIED:
case Status.INDEX_COPIED:
return 2;
case Status.IGNORED:
return 3;
case Status.INDEX_COPIED:
case Status.BOTH_DELETED:
case Status.ADDED_BY_US:
case Status.DELETED_BY_THEM:

View file

@ -9,7 +9,7 @@
},
"main": "./out/node/htmlServerMain",
"dependencies": {
"vscode-css-languageservice": "^5.0.3",
"vscode-css-languageservice": "^5.1.0",
"vscode-html-languageservice": "^4.0.1",
"vscode-languageserver": "^7.0.0",
"vscode-nls": "^5.0.0",

View file

@ -5,7 +5,7 @@
import { LanguageModelCache, getLanguageModelCache } from '../languageModelCache';
import { Stylesheet, LanguageService as CSSLanguageService } from 'vscode-css-languageservice';
import { LanguageMode, Workspace, Color, TextDocument, Position, Range, CompletionList, DocumentContext, Settings } from './languageModes';
import { LanguageMode, Workspace, Color, TextDocument, Position, Range, CompletionList, DocumentContext } from './languageModes';
import { HTMLDocumentRegions, CSS_STYLE_RULE } from './embeddedSupport';
export function getCSSMode(cssLanguageService: CSSLanguageService, documentRegions: LanguageModelCache<HTMLDocumentRegions>, workspace: Workspace): LanguageMode {
@ -23,11 +23,11 @@ export function getCSSMode(cssLanguageService: CSSLanguageService, documentRegio
async doComplete(document: TextDocument, position: Position, documentContext: DocumentContext, _settings = workspace.settings) {
let embedded = embeddedCSSDocuments.get(document);
const stylesheet = cssStylesheets.get(embedded);
return cssLanguageService.doComplete2(embedded, position, stylesheet, documentContext) || CompletionList.create();
return cssLanguageService.doComplete2(embedded, position, stylesheet, documentContext, _settings?.css?.completion) || CompletionList.create();
},
async doHover(document: TextDocument, position: Position, settings?: Settings) {
async doHover(document: TextDocument, position: Position, settings = workspace.settings) {
let embedded = embeddedCSSDocuments.get(document);
return cssLanguageService.doHover(embedded, position, cssStylesheets.get(embedded), settings?.html?.hover);
return cssLanguageService.doHover(embedded, position, cssStylesheets.get(embedded), settings?.css?.hover);
},
async findDocumentHighlight(document: TextDocument, position: Position) {
let embedded = embeddedCSSDocuments.get(document);

View file

@ -12,10 +12,10 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679"
integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q==
vscode-css-languageservice@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-5.0.3.tgz#2d400a47e73d0bfc5bc0d3fdf5be487cfdca341b"
integrity sha512-KJt4jhCxqrgGrC02UsQsKw90dPkFknMHsH5HTInT7gkDRRfGFwEd+e2O1/E75br3TdFhvRmzjljYz5thZ58L3A==
vscode-css-languageservice@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-5.1.0.tgz#cd172d13e9e7ae23ba567c73778aee10475ff716"
integrity sha512-iLHd/WjRKgaZBXMNeUooHG+r0qlhJBkXa+3MpQQR6Rpm928cis/3OV2Mp1R80yAQevIMeDL32RIJfHoJCT/RRg==
dependencies:
vscode-languageserver-textdocument "^1.0.1"
vscode-languageserver-types "^3.16.0"

View file

@ -22,10 +22,7 @@ export function runSelectedScript(context: vscode.ExtensionContext) {
}
let document = editor.document;
let contents = document.getText();
let selection = editor.selection;
let offset = document.offsetAt(selection.anchor);
let script = findScriptAtPosition(contents, offset);
let script = findScriptAtPosition(editor.document, contents, editor.selection.anchor);
if (script) {
runScript(context, script, document);
} else {

View file

@ -10,6 +10,7 @@ import { runSelectedScript, selectAndRunScriptFromFolder } from './commands';
import { NpmScriptsTreeDataProvider } from './npmView';
import { getPackageManager, invalidateTasksCache, NpmTaskProvider } from './tasks';
import { invalidateHoverScriptsCache, NpmScriptHoverProvider } from './scriptHover';
import { NpmScriptLensProvider } from './npmScriptLens';
let treeDataProvider: NpmScriptsTreeDataProvider | undefined;
@ -62,6 +63,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
}
return '';
}));
context.subscriptions.push(new NpmScriptLensProvider());
}
function canRunNpmInCurrentWorkspace() {

View file

@ -0,0 +1,113 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import {
CodeLens,
CodeLensProvider,
Disposable,
EventEmitter,
languages,
TextDocument,
Uri,
workspace
} from 'vscode';
import * as nls from 'vscode-nls';
import { findPreferredPM } from './preferred-pm';
import { readScripts } from './readScripts';
const localize = nls.loadMessageBundle();
const enum Constants {
ConfigKey = 'debug.javascript.codelens.npmScripts',
}
const getFreshLensLocation = () => workspace.getConfiguration().get(Constants.ConfigKey);
/**
* Npm script lens provider implementation. Can show a "Debug" text above any
* npm script, or the npm scripts section.
*/
export class NpmScriptLensProvider implements CodeLensProvider, Disposable {
private lensLocation = getFreshLensLocation();
private changeEmitter = new EventEmitter<void>();
private subscriptions: Disposable[] = [];
/**
* @inheritdoc
*/
public onDidChangeCodeLenses = this.changeEmitter.event;
constructor() {
this.subscriptions.push(
workspace.onDidChangeConfiguration(evt => {
if (evt.affectsConfiguration(Constants.ConfigKey)) {
this.lensLocation = getFreshLensLocation();
this.changeEmitter.fire();
}
}),
languages.registerCodeLensProvider(
{
language: 'json',
pattern: '**/package.json',
},
this,
)
);
}
/**
* @inheritdoc
*/
public async provideCodeLenses(document: TextDocument): Promise<CodeLens[]> {
if (this.lensLocation === 'never') {
return [];
}
const tokens = readScripts(document);
if (!tokens) {
return [];
}
const title = localize('codelens.debug', '{0} Debug', '$(debug-start)');
const cwd = path.dirname(document.uri.fsPath);
if (this.lensLocation === 'top') {
return [
new CodeLens(
tokens.location.range,
{
title,
command: 'extension.js-debug.npmScript',
arguments: [cwd],
},
),
];
}
if (this.lensLocation === 'all') {
const packageManager = await findPreferredPM(Uri.joinPath(document.uri, '..').fsPath);
return tokens.scripts.map(
({ name, nameRange }) =>
new CodeLens(
nameRange,
{
title,
command: 'extension.js-debug.createDebuggerTerminal',
arguments: [`${packageManager.name} run ${name}`, workspace.getWorkspaceFolder(document.uri), { cwd }],
},
),
);
}
return [];
}
/**
* @inheritdoc
*/
public dispose() {
this.subscriptions.forEach(s => s.dispose());
}
}

View file

@ -3,21 +3,20 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { JSONVisitor, visit } from 'jsonc-parser';
import * as path from 'path';
import {
commands, Event, EventEmitter, ExtensionContext,
Range,
Selection, Task,
TaskGroup, tasks, TextDocument, TextDocumentShowOptions, ThemeIcon, TreeDataProvider, TreeItem, TreeItemLabel, TreeItemCollapsibleState, Uri,
window, workspace, WorkspaceFolder
window, workspace, WorkspaceFolder, Position, Location
} from 'vscode';
import * as nls from 'vscode-nls';
import { readScripts } from './readScripts';
import {
createTask, getPackageManager, getTaskName, isAutoDetectionEnabled, isWorkspaceFolder, NpmTaskDefinition,
NpmTaskProvider,
startDebugging,
TaskLocation,
TaskWithLocation
} from './tasks';
@ -78,7 +77,7 @@ class NpmScript extends TreeItem {
task: Task;
package: PackageJSON;
constructor(_context: ExtensionContext, packageJson: PackageJSON, task: Task, public taskLocation?: TaskLocation) {
constructor(_context: ExtensionContext, packageJson: PackageJSON, task: Task, public taskLocation?: Location) {
super(task.name, TreeItemCollapsibleState.None);
const command: ExplorerCommands = workspace.getConfiguration('npm').get<ExplorerCommands>('scriptExplorerAction') || 'open';
@ -87,9 +86,9 @@ class NpmScript extends TreeItem {
title: 'Edit Script',
command: 'vscode.open',
arguments: [
taskLocation?.document,
taskLocation?.uri,
taskLocation ? <TextDocumentShowOptions>{
selection: new Range(taskLocation.line, taskLocation.line)
selection: new Range(taskLocation.range.start, taskLocation.range.start)
} : undefined
]
},
@ -153,37 +152,18 @@ export class NpmScriptsTreeDataProvider implements TreeDataProvider<TreeItem> {
startDebugging(this.extensionContext, script.task.definition.script, path.dirname(script.package.resourceUri!.fsPath), script.getFolder());
}
private findScript(document: TextDocument, script?: NpmScript): number {
let scriptOffset = 0;
let inScripts = false;
private findScriptPosition(document: TextDocument, script?: NpmScript) {
const scripts = readScripts(document);
if (!scripts) {
return undefined;
}
let visitor: JSONVisitor = {
onError() {
return scriptOffset;
},
onObjectEnd() {
if (inScripts) {
inScripts = false;
}
},
onObjectProperty(property: string, offset: number, _length: number) {
if (property === 'scripts') {
inScripts = true;
if (!script) { // select the script section
scriptOffset = offset;
}
}
else if (inScripts && script) {
let label = getTaskName(property, script.task.definition.path);
if (script.task.name === label) {
scriptOffset = offset;
}
}
}
};
visit(document.getText(), visitor);
return scriptOffset;
if (!script) {
return scripts.location.range.start;
}
const found = scripts.scripts.find(s => getTaskName(s.name, script.task.definition.path) === script.task.name);
return found?.nameRange.start;
}
private async runInstall(selection: PackageJSON) {
@ -194,7 +174,7 @@ export class NpmScriptsTreeDataProvider implements TreeDataProvider<TreeItem> {
if (!uri) {
return;
}
let task = await createTask(this.extensionContext, 'install', ['install'], selection.folder.workspaceFolder, uri, true, undefined, []);
let task = await createTask(await getPackageManager(this.context, selection.folder.workspaceFolder.uri, true), 'install', ['install'], selection.folder.workspaceFolder, uri, undefined, []);
tasks.executeTask(task);
}
@ -209,8 +189,7 @@ export class NpmScriptsTreeDataProvider implements TreeDataProvider<TreeItem> {
return;
}
let document: TextDocument = await workspace.openTextDocument(uri);
let offset = this.findScript(document, selection instanceof NpmScript ? selection : undefined);
let position = document.positionAt(offset);
let position = this.findScriptPosition(document, selection instanceof NpmScript ? selection : undefined) || new Position(0, 0);
await window.showTextDocument(document, { preserveFocus: true, selection: new Selection(position, position) });
}

View file

@ -0,0 +1,73 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { JSONVisitor, visit } from 'jsonc-parser';
import { Location, Position, Range, TextDocument } from 'vscode';
export interface INpmScriptReference {
name: string;
value: string;
nameRange: Range;
valueRange: Range;
}
export interface INpmScriptInfo {
location: Location;
scripts: INpmScriptReference[];
}
export const readScripts = (document: TextDocument, buffer = document.getText()): INpmScriptInfo | undefined => {
let start: Position | undefined;
let end: Position | undefined;
let inScripts = false;
let buildingScript: { name: string; nameRange: Range } | void;
let level = 0;
const scripts: INpmScriptReference[] = [];
const visitor: JSONVisitor = {
onError() {
// no-op
},
onObjectBegin() {
level++;
},
onObjectEnd(offset) {
if (inScripts) {
end = document.positionAt(offset);
inScripts = false;
}
level--;
},
onLiteralValue(value: unknown, offset: number, length: number) {
if (buildingScript && typeof value === 'string') {
scripts.push({
...buildingScript,
value,
valueRange: new Range(document.positionAt(offset), document.positionAt(offset + length)),
});
buildingScript = undefined;
}
},
onObjectProperty(property: string, offset: number, length: number) {
if (level === 1 && property === 'scripts') {
inScripts = true;
start = document.positionAt(offset);
} else if (inScripts) {
buildingScript = {
name: property,
nameRange: new Range(document.positionAt(offset), document.positionAt(offset + length))
};
}
},
};
visit(buffer, visitor);
if (start === undefined) {
return undefined;
}
return { location: new Location(document.uri, new Range(start, end ?? start)), scripts };
};

View file

@ -3,20 +3,24 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
ExtensionContext, TextDocument, commands, ProviderResult, CancellationToken,
workspace, tasks, Range, HoverProvider, Hover, Position, MarkdownString, Uri
} from 'vscode';
import {
createTask, startDebugging, findAllScriptRanges
} from './tasks';
import * as nls from 'vscode-nls';
import { dirname } from 'path';
import {
CancellationToken, commands, ExtensionContext,
Hover, HoverProvider, MarkdownString, Position, ProviderResult,
tasks, TextDocument,
Uri, workspace
} from 'vscode';
import * as nls from 'vscode-nls';
import { INpmScriptInfo, readScripts } from './readScripts';
import {
createTask,
getPackageManager, startDebugging
} from './tasks';
const localize = nls.loadMessageBundle();
let cachedDocument: Uri | undefined = undefined;
let cachedScriptsMap: Map<string, [number, number, string]> | undefined = undefined;
let cachedScripts: INpmScriptInfo | undefined = undefined;
export function invalidateHoverScriptsCache(document?: TextDocument) {
if (!document) {
@ -42,20 +46,16 @@ export class NpmScriptHoverProvider implements HoverProvider {
let hover: Hover | undefined = undefined;
if (!cachedDocument || cachedDocument.fsPath !== document.uri.fsPath) {
cachedScriptsMap = findAllScriptRanges(document.getText());
cachedScripts = readScripts(document);
cachedDocument = document.uri;
}
cachedScriptsMap!.forEach((value, key) => {
let start = document.positionAt(value[0]);
let end = document.positionAt(value[0] + value[1]);
let range = new Range(start, end);
if (range.contains(position)) {
cachedScripts?.scripts.forEach(({ name, nameRange }) => {
if (nameRange.contains(position)) {
let contents: MarkdownString = new MarkdownString();
contents.isTrusted = true;
contents.appendMarkdown(this.createRunScriptMarkdown(key, document.uri));
contents.appendMarkdown(this.createDebugScriptMarkdown(key, document.uri));
contents.appendMarkdown(this.createRunScriptMarkdown(name, document.uri));
contents.appendMarkdown(this.createDebugScriptMarkdown(name, document.uri));
hover = new Hover(contents);
}
});
@ -103,7 +103,7 @@ export class NpmScriptHoverProvider implements HoverProvider {
let documentUri = args.documentUri;
let folder = workspace.getWorkspaceFolder(documentUri);
if (folder) {
let task = await createTask(this.context, script, ['run', script], folder, documentUri);
let task = await createTask(await getPackageManager(this.context, folder.uri), script, ['run', script], folder, documentUri);
await tasks.executeTask(task);
}
}

View file

@ -5,15 +5,15 @@
import {
TaskDefinition, Task, TaskGroup, WorkspaceFolder, RelativePattern, ShellExecution, Uri, workspace,
DebugConfiguration, debug, TaskProvider, TextDocument, tasks, TaskScope, QuickPickItem, window, Position, ExtensionContext, env,
ShellQuotedString, ShellQuoting
TaskProvider, TextDocument, tasks, TaskScope, QuickPickItem, window, Position, ExtensionContext, env,
ShellQuotedString, ShellQuoting, commands, Location
} from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import * as minimatch from 'minimatch';
import * as nls from 'vscode-nls';
import { JSONVisitor, visit, ParseErrorCode } from 'jsonc-parser';
import { findPreferredPM } from './preferred-pm';
import { readScripts } from './readScripts';
const localize = nls.loadMessageBundle();
@ -40,7 +40,7 @@ export interface TaskLocation {
export interface TaskWithLocation {
task: Task,
location?: TaskLocation
location?: Location
}
export class NpmTaskProvider implements TaskProvider {
@ -57,7 +57,7 @@ export class NpmTaskProvider implements TaskProvider {
return tasks.map(task => task.task);
}
public resolveTask(_task: Task): Promise<Task> | undefined {
public async resolveTask(_task: Task): Promise<Task | undefined> {
const npmTask = (<any>_task.definition).script;
if (npmTask) {
const kind: NpmTaskDefinition = (<any>_task.definition);
@ -75,7 +75,7 @@ export class NpmTaskProvider implements TaskProvider {
if (kind.script !== INSTALL_SCRIPT) {
cmd.unshift('run');
}
return createTask(this.context, kind, cmd, _task.scope, packageJsonUri);
return createTask(await getPackageManager(this.context, _task.scope.uri), kind, cmd, _task.scope, packageJsonUri);
}
return undefined;
}
@ -278,29 +278,30 @@ async function provideNpmScriptsForFolder(context: ExtensionContext, packageJson
const result: TaskWithLocation[] = [];
const prePostScripts = getPrePostScripts(scripts);
const packageManager = await getPackageManager(context, folder.uri, showWarning);
for (const each of scripts.keys()) {
const scriptValue = scripts.get(each)!;
const task = await createTask(context, each, ['run', each], folder!, packageJsonUri, showWarning, scriptValue.script);
const lowerCaseTaskName = each.toLowerCase();
for (const { name, value, nameRange } of scripts.scripts) {
const task = await createTask(packageManager, name, ['run', name], folder!, packageJsonUri, value);
const lowerCaseTaskName = name.toLowerCase();
if (isBuildTask(lowerCaseTaskName)) {
task.group = TaskGroup.Build;
} else if (isTestTask(lowerCaseTaskName)) {
task.group = TaskGroup.Test;
}
if (prePostScripts.has(each)) {
if (prePostScripts.has(name)) {
task.group = TaskGroup.Clean; // hack: use Clean group to tag pre/post scripts
}
// todo@connor4312: all scripts are now debuggable, what is a 'debug script'?
if (isDebugScript(scriptValue.script)) {
if (isDebugScript(value)) {
task.group = TaskGroup.Rebuild; // hack: use Rebuild group to tag debug scripts
}
result.push({ task, location: scriptValue.location });
result.push({ task, location: new Location(packageJsonUri, nameRange) });
}
// always add npm install (without a problem matcher)
result.push({ task: await createTask(context, INSTALL_SCRIPT, [INSTALL_SCRIPT], folder, packageJsonUri, showWarning, 'install dependencies from package', []) });
result.push({ task: await createTask(packageManager, INSTALL_SCRIPT, [INSTALL_SCRIPT], folder, packageJsonUri, 'install dependencies from package', []) });
return result;
}
@ -311,7 +312,7 @@ export function getTaskName(script: string, relativePath: string | undefined) {
return script;
}
export async function createTask(context: ExtensionContext, script: NpmTaskDefinition | string, cmd: string[], folder: WorkspaceFolder, packageJsonUri: Uri, showWarning: boolean = true, detail?: string, matcher?: any): Promise<Task> {
export async function createTask(packageManager: string, script: NpmTaskDefinition | string, cmd: string[], folder: WorkspaceFolder, packageJsonUri: Uri, detail?: string, matcher?: any): Promise<Task> {
let kind: NpmTaskDefinition;
if (typeof script === 'string') {
kind = { type: 'npm', script: script };
@ -319,12 +320,11 @@ export async function createTask(context: ExtensionContext, script: NpmTaskDefin
kind = script;
}
const packageManager = await getPackageManager(context, folder.uri, showWarning);
function getCommandLine(cmd: string[]): (string | ShellQuotedString)[] {
const result: (string | ShellQuotedString)[] = new Array(cmd.length);
for (let i = 0; i < cmd.length; i++) {
if (/\s/.test(cmd[i])) {
result[i] = { value: `${cmd[i]}`, quoting: ShellQuoting.Strong };
result[i] = { value: cmd[i], quoting: cmd[i].includes('--') ? ShellQuoting.Weak : ShellQuoting.Strong };
} else {
result[i] = cmd[i];
}
@ -392,151 +392,39 @@ export async function runScript(context: ExtensionContext, script: string, docum
let uri = document.uri;
let folder = workspace.getWorkspaceFolder(uri);
if (folder) {
let task = await createTask(context, script, ['run', script], folder, uri);
const task = await createTask(await getPackageManager(context, folder.uri), script, ['run', script], folder, uri);
tasks.executeTask(task);
}
}
export async function startDebugging(context: ExtensionContext, scriptName: string, cwd: string, folder: WorkspaceFolder) {
const config: DebugConfiguration = {
type: 'pwa-node',
request: 'launch',
name: `Debug ${scriptName}`,
cwd,
runtimeExecutable: await getPackageManager(context, folder.uri),
runtimeArgs: [
'run',
scriptName,
],
};
if (folder) {
debug.startDebugging(folder, config);
}
commands.executeCommand(
'extension.js-debug.createDebuggerTerminal',
`${await getPackageManager(context, folder.uri)} run ${scriptName}`,
folder,
{ cwd },
);
}
export type StringMap = { [s: string]: string; };
async function findAllScripts(document: TextDocument, buffer: string): Promise<Map<string, { script: string, location: TaskLocation }>> {
let scripts: Map<string, { script: string, location: TaskLocation }> = new Map();
let script: string | undefined = undefined;
let inScripts = false;
let scriptOffset = 0;
export function findScriptAtPosition(document: TextDocument, buffer: string, position: Position): string | undefined {
const read = readScripts(document, buffer);
if (!read) {
return undefined;
}
let visitor: JSONVisitor = {
onError(_error: ParseErrorCode, _offset: number, _length: number) {
console.log(_error);
},
onObjectEnd() {
if (inScripts) {
inScripts = false;
}
},
onLiteralValue(value: any, _offset: number, _length: number) {
if (script) {
if (typeof value === 'string') {
scripts.set(script, { script: value, location: { document: document.uri, line: document.positionAt(scriptOffset) } });
}
script = undefined;
}
},
onObjectProperty(property: string, offset: number, _length: number) {
if (property === 'scripts') {
inScripts = true;
}
else if (inScripts && !script) {
script = property;
scriptOffset = offset;
} else { // nested object which is invalid, ignore the script
script = undefined;
}
for (const script of read.scripts) {
if (script.nameRange.start.isBeforeOrEqual(position) && script.valueRange.end.isAfterOrEqual(position)) {
return script.name;
}
};
visit(buffer, visitor);
return scripts;
}
return undefined;
}
export function findAllScriptRanges(buffer: string): Map<string, [number, number, string]> {
let scripts: Map<string, [number, number, string]> = new Map();
let script: string | undefined = undefined;
let offset: number;
let length: number;
let inScripts = false;
let visitor: JSONVisitor = {
onError(_error: ParseErrorCode, _offset: number, _length: number) {
},
onObjectEnd() {
if (inScripts) {
inScripts = false;
}
},
onLiteralValue(value: any, _offset: number, _length: number) {
if (script) {
scripts.set(script, [offset, length, value]);
script = undefined;
}
},
onObjectProperty(property: string, off: number, len: number) {
if (property === 'scripts') {
inScripts = true;
}
else if (inScripts) {
script = property;
offset = off;
length = len;
}
}
};
visit(buffer, visitor);
return scripts;
}
export function findScriptAtPosition(buffer: string, offset: number): string | undefined {
let script: string | undefined = undefined;
let foundScript: string | undefined = undefined;
let inScripts = false;
let scriptStart: number | undefined;
let visitor: JSONVisitor = {
onError(_error: ParseErrorCode, _offset: number, _length: number) {
},
onObjectEnd() {
if (inScripts) {
inScripts = false;
scriptStart = undefined;
}
},
onLiteralValue(value: any, nodeOffset: number, nodeLength: number) {
if (inScripts && scriptStart) {
if (typeof value === 'string' && offset >= scriptStart && offset < nodeOffset + nodeLength) {
// found the script
inScripts = false;
foundScript = script;
} else {
script = undefined;
}
}
},
onObjectProperty(property: string, nodeOffset: number) {
if (property === 'scripts') {
inScripts = true;
}
else if (inScripts) {
scriptStart = nodeOffset;
script = property;
} else { // nested object which is invalid, ignore the script
script = undefined;
}
}
};
visit(buffer, visitor);
return foundScript;
}
export async function getScripts(packageJsonUri: Uri): Promise<Map<string, { script: string, location: TaskLocation }> | undefined> {
export async function getScripts(packageJsonUri: Uri) {
if (packageJsonUri.scheme !== 'file') {
return undefined;
}
@ -548,9 +436,7 @@ export async function getScripts(packageJsonUri: Uri): Promise<Map<string, { scr
try {
const document: TextDocument = await workspace.openTextDocument(packageJsonUri);
let contents = document.getText();
let json = findAllScripts(document, contents);//JSON.parse(contents);
return json;
return readScripts(document);
} catch (e) {
let localizedParseError = localize('npm.parseError', 'Npm task detection: failed to parse the file {0}', packageJsonUri.fsPath);
throw new Error(localizedParseError);

View file

@ -7,7 +7,7 @@
"tooltip.run": "Run {0}",
"tooltip.debug": "Debug {0}",
"tooltip.runState": "{0}/{0} Tests Passed",
"tooltip.runState": "{0}/{1} Tests Passed",
"tooltip.runStateWithDuration": "{0}/{1} Tests Passed in {2}",
"state.failed": "Failed",

View file

@ -17,7 +17,6 @@
],
"dependencies": {
"jsonc-parser": "^2.2.1",
"rimraf": "^2.6.3",
"semver": "5.5.1",
"typescript-vscode-sh-plugin": "^0.6.14",
"vscode-extension-telemetry": "0.1.1",

View file

@ -3,9 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as rimraf from 'rimraf';
import * as fs from 'fs';
import * as vscode from 'vscode';
import { Api, getExtensionApi } from './api';
import { CommandManager } from './commands/commandManager';
import { registerBaseCommands } from './commands/index';
import { LanguageConfigurationManager } from './languageFeatures/languageConfiguration';
import { createLazyClientHost, lazilyActivateClient } from './lazyClientHost';
@ -13,7 +14,6 @@ import { nodeRequestCancellerFactory } from './tsServer/cancellation.electron';
import { NodeLogDirectoryProvider } from './tsServer/logDirectoryProvider.electron';
import { ChildServerProcess } from './tsServer/serverProcess.electron';
import { DiskTypeScriptVersionProvider } from './tsServer/versionProvider.electron';
import { CommandManager } from './commands/commandManager';
import { onCaseInsenitiveFileSystem } from './utils/fileSystem.electron';
import { PluginManager } from './utils/plugins';
import * as temp from './utils/temp.electron';
@ -62,5 +62,5 @@ export function activate(
}
export function deactivate() {
rimraf.sync(temp.getInstanceTempDir());
fs.rmdirSync(temp.getInstanceTempDir(), { recursive: true });
}

View file

@ -47,24 +47,6 @@ applicationinsights@1.0.8:
diagnostic-channel-publishers "0.2.1"
zone.js "0.7.6"
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
diagnostic-channel-publishers@0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz#8e2d607a8b6d79fe880b548bc58cc6beb288c4f3"
@ -77,67 +59,11 @@ diagnostic-channel@0.2.0:
dependencies:
semver "^5.3.0"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
glob@^7.1.3:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
jsonc-parser@^2.2.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.3.1.tgz#59549150b133f2efacca48fe9ce1ec0659af2342"
integrity sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
dependencies:
brace-expansion "^1.1.7"
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
dependencies:
wrappy "1"
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
rimraf@^2.6.3:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
dependencies:
glob "^7.1.3"
semver@5.5.1:
version "5.5.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477"
@ -165,11 +91,6 @@ vscode-nls@^4.1.1:
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.2.tgz#ca8bf8bb82a0987b32801f9fddfdd2fb9fd3c167"
integrity sha512-7bOHxPsfyuCqmP+hZXscLhiHwe7CSuFE4hyhbs22xPIhQ4jv99FcR4eBzfYYVLP356HNFpdvz63FFb/xw6T4Iw==
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
zone.js@0.7.6:
version "0.7.6"
resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.7.6.tgz#fbbc39d3e0261d0986f1ba06306eb3aeb0d22009"

View file

@ -37,6 +37,14 @@
}
}
},
"views": {
"remote": [
{
"id": "test.treeId",
"when": "never"
}
]
},
"configurationDefaults": {
"[abcLang]": {
"editor.lineNumbers": "off",

View file

@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as assert from 'assert';
import * as vscode from 'vscode';
suite('vscode server cli', () => {
test('extension is installed and enabled when installed by server cli', function () {
const extension = process.env.TESTRESOLVER_INSTALL_BUILTIN_EXTENSION;
if (!process.env.BUILD_SOURCEVERSION // Skip it when running out of sources
|| !process.env.REMOTE_VSCODE // Skip it when not a remote integration test
|| !extension // Skip it when extension is not provided to server
) {
this.skip();
}
assert.ok(vscode.extensions.getExtension(extension!));
});
});

View file

@ -4,5 +4,6 @@
},
"files.exclude": {
"**/files-exclude/**": true
}
}
},
"editor.minimap.enabled": false // see https://github.com/microsoft/vscode/issues/115747
}

View file

@ -0,0 +1,3 @@
{
"editor.minimap.enabled": false // see https://github.com/microsoft/vscode/issues/115747
}

View file

@ -83,12 +83,6 @@ export function activate(context: vscode.ExtensionContext) {
const env = getNewEnv();
const remoteDataDir = process.env['TESTRESOLVER_DATA_FOLDER'] || path.join(os.homedir(), serverDataFolderName || `${dataFolderName}-testresolver`);
const remoteExtension = process.env['TESTRESOLVER_REMOTE_EXTENSION'];
if (remoteExtension) {
commandArgs.push('--install-extension', remoteExtension);
commandArgs.push('--start-server');
}
env['VSCODE_AGENT_FOLDER'] = remoteDataDir;
outputChannel.appendLine(`Using data folder at ${remoteDataDir}`);
@ -98,6 +92,11 @@ export function activate(context: vscode.ExtensionContext) {
const serverCommandPath = path.join(vscodePath, 'resources', 'server', 'bin-dev', serverCommand);
extHostProcess = cp.spawn(serverCommandPath, commandArgs, { env, cwd: vscodePath });
} else {
const extensionToInstall = process.env['TESTRESOLVER_INSTALL_BUILTIN_EXTENSION'];
if (extensionToInstall) {
commandArgs.push('--install-builtin-extension', extensionToInstall);
commandArgs.push('--start-server');
}
const serverCommand = process.platform === 'win32' ? 'server.cmd' : 'server.sh';
let serverLocation = env['VSCODE_REMOTE_SERVER_PATH']; // support environment variable to specify location of server on disk
if (!serverLocation) {

View file

@ -1,7 +1,7 @@
{
"name": "code-oss-dev",
"version": "1.54.0",
"distro": "ef4e25f8dccec71b96ee8c6d0a770f1e53bcf84c",
"distro": "e6efd9b7780f53a499ee7c8f1728ffbc3391d8fb",
"author": {
"name": "Microsoft Corporation"
},
@ -46,7 +46,7 @@
"compile-web": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js compile-web",
"watch-web": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js watch-web",
"eslint": "node build/eslint",
"electron-rebuild": "electron-rebuild --arch=arm64 --force --version=11.2.1",
"electron-rebuild": "electron-rebuild --arch=arm64 --force --version=11.2.2",
"playwright-install": "node build/azure-pipelines/common/installPlaywright.js",
"compile-build": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js compile-build",
"compile-extensions-build": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js compile-extensions-build",
@ -82,10 +82,10 @@
"vscode-ripgrep": "^1.11.1",
"vscode-sqlite3": "4.0.10",
"vscode-textmate": "5.2.0",
"xterm": "4.10.0-beta.39",
"xterm-addon-search": "0.8.0-beta.3",
"xterm": "4.11.0-beta.2",
"xterm-addon-search": "0.8.0",
"xterm-addon-unicode11": "0.3.0-beta.3",
"xterm-addon-webgl": "0.10.0-beta.1",
"xterm-addon-webgl": "0.10.0-beta.2",
"yauzl": "^2.9.2",
"yazl": "^2.4.3"
},
@ -125,7 +125,7 @@
"cssnano": "^4.1.10",
"debounce": "^1.0.0",
"deemon": "^1.4.0",
"electron": "11.2.1",
"electron": "11.2.2",
"electron-rebuild": "2.0.3",
"eslint": "6.8.0",
"eslint-plugin-jsdoc": "^19.1.0",
@ -222,4 +222,4 @@
"elliptic": "^6.5.3",
"nwmatcher": "^1.4.4"
}
}
}

View file

@ -91,7 +91,7 @@
},
{
"name": "ms-vscode.js-debug",
"version": "1.53.0",
"version": "1.54.0",
"repo": "https://github.com/microsoft/vscode-js-debug",
"metadata": {
"id": "25629058-ddac-4e17-abba-74678e126c5d",

View file

@ -22,10 +22,10 @@
"vscode-regexpp": "^3.1.0",
"vscode-ripgrep": "^1.11.1",
"vscode-textmate": "5.2.0",
"xterm": "4.10.0-beta.39",
"xterm-addon-search": "0.8.0-beta.3",
"xterm": "4.11.0-beta.2",
"xterm-addon-search": "0.8.0",
"xterm-addon-unicode11": "0.3.0-beta.3",
"xterm-addon-webgl": "0.10.0-beta.1",
"xterm-addon-webgl": "0.10.0-beta.2",
"yauzl": "^2.9.2",
"yazl": "^2.4.3"
},

View file

@ -8,9 +8,9 @@
"tas-client-umd": "0.1.2",
"vscode-oniguruma": "1.3.1",
"vscode-textmate": "5.2.0",
"xterm": "4.10.0-beta.39",
"xterm-addon-search": "0.8.0-beta.3",
"xterm": "4.11.0-beta.2",
"xterm-addon-search": "0.8.0",
"xterm-addon-unicode11": "0.3.0-beta.3",
"xterm-addon-webgl": "0.10.0-beta.1"
"xterm-addon-webgl": "0.10.0-beta.2"
}
}

View file

@ -27,22 +27,22 @@ vscode-textmate@5.2.0:
resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.2.0.tgz#01f01760a391e8222fe4f33fbccbd1ad71aed74e"
integrity sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==
xterm-addon-search@0.8.0-beta.3:
version "0.8.0-beta.3"
resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.8.0-beta.3.tgz#c6c7e36a03706bd43d8bba383511acf9e435aed0"
integrity sha512-EZP97KJIJ4KGQaOPYiiOaRRJst6LOgeEFoQL46WcBl5EWH9pH8qfrv0BHAJ8+6nBV2B9u5M6rzxO1GvLLec19w==
xterm-addon-search@0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.8.0.tgz#e33eab918df7eac7e7baf95dd2b3d14133754881"
integrity sha512-MPJGPVPpHRUw9cLIuqQbrVepmENMOybVUSxIALz5h1ryyQBrVqVujq2hL5aroX5/dZJoHx9lGHQTVLQ07SKgKA==
xterm-addon-unicode11@0.3.0-beta.3:
version "0.3.0-beta.3"
resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.3.0-beta.3.tgz#70af2dfb67809258edb62c19e2861f7ce5ccf5cd"
integrity sha512-vaYopnOjn19wCLDCyIWPWLwKR7CvLPxB5YZ3CAxt9qL05o3symxIJJJC0DuCa4GaGKVjNc7EmjRCs5bsJ2O1tw==
xterm-addon-webgl@0.10.0-beta.1:
version "0.10.0-beta.1"
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.10.0-beta.1.tgz#e0bf964945a9aa8fc18318ddbd32e56ec99c219e"
integrity sha512-XNZMrmiyFaz3XiPq+LqF0qn2QHpUEwuk+cG53JwpJHnWo3dd2jxoIgHFQUcrnvHIVPZMbTKySIwLCCC9uQVl7Q==
xterm-addon-webgl@0.10.0-beta.2:
version "0.10.0-beta.2"
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.10.0-beta.2.tgz#520547b2f845b2f9265f1817140b0f4e114c4a55"
integrity sha512-DLfmF5+H1M/0BABEaqJlLUasKck7TjyRWVlzGflFTWVCr7/Kqaf0al4KMlw3yWX76A1ITGXrWj0qbDn2Sixl2Q==
xterm@4.10.0-beta.39:
version "4.10.0-beta.39"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.10.0-beta.39.tgz#29c5ce3ed814d04e0bf2781cf9bd09e354b6e422"
integrity sha512-0qtknUlct48WyVj8tnoVvwn4Bz2+lmVBLIGKjifTvawRccw2QVXjTa5hUgNIbecgZv8PWZhseBREtccRbbqbuA==
xterm@4.11.0-beta.2:
version "4.11.0-beta.2"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.11.0-beta.2.tgz#a95560b61c771f54a336c2eb10e1472e556d9f4b"
integrity sha512-0BUaAfuclnowirdOuB13OGgq6OUGg/8etnRVT6apgnOrLGOLRCE1NiL3KhxotleAf4gVP0m3iCxsIr3csDY40g==

View file

@ -450,25 +450,25 @@ vscode-windows-registry@1.0.2:
resolved "https://registry.yarnpkg.com/vscode-windows-registry/-/vscode-windows-registry-1.0.2.tgz#b863e704a6a69c50b3098a55fbddbe595b0c124a"
integrity sha512-/CLLvuOSM2Vme2z6aNyB+4Omd7hDxpf4Thrt8ImxnXeQtxzel2bClJpFQvQqK/s4oaXlkBKS7LqVLeZM+uSVIA==
xterm-addon-search@0.8.0-beta.3:
version "0.8.0-beta.3"
resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.8.0-beta.3.tgz#c6c7e36a03706bd43d8bba383511acf9e435aed0"
integrity sha512-EZP97KJIJ4KGQaOPYiiOaRRJst6LOgeEFoQL46WcBl5EWH9pH8qfrv0BHAJ8+6nBV2B9u5M6rzxO1GvLLec19w==
xterm-addon-search@0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.8.0.tgz#e33eab918df7eac7e7baf95dd2b3d14133754881"
integrity sha512-MPJGPVPpHRUw9cLIuqQbrVepmENMOybVUSxIALz5h1ryyQBrVqVujq2hL5aroX5/dZJoHx9lGHQTVLQ07SKgKA==
xterm-addon-unicode11@0.3.0-beta.3:
version "0.3.0-beta.3"
resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.3.0-beta.3.tgz#70af2dfb67809258edb62c19e2861f7ce5ccf5cd"
integrity sha512-vaYopnOjn19wCLDCyIWPWLwKR7CvLPxB5YZ3CAxt9qL05o3symxIJJJC0DuCa4GaGKVjNc7EmjRCs5bsJ2O1tw==
xterm-addon-webgl@0.10.0-beta.1:
version "0.10.0-beta.1"
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.10.0-beta.1.tgz#e0bf964945a9aa8fc18318ddbd32e56ec99c219e"
integrity sha512-XNZMrmiyFaz3XiPq+LqF0qn2QHpUEwuk+cG53JwpJHnWo3dd2jxoIgHFQUcrnvHIVPZMbTKySIwLCCC9uQVl7Q==
xterm-addon-webgl@0.10.0-beta.2:
version "0.10.0-beta.2"
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.10.0-beta.2.tgz#520547b2f845b2f9265f1817140b0f4e114c4a55"
integrity sha512-DLfmF5+H1M/0BABEaqJlLUasKck7TjyRWVlzGflFTWVCr7/Kqaf0al4KMlw3yWX76A1ITGXrWj0qbDn2Sixl2Q==
xterm@4.10.0-beta.39:
version "4.10.0-beta.39"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.10.0-beta.39.tgz#29c5ce3ed814d04e0bf2781cf9bd09e354b6e422"
integrity sha512-0qtknUlct48WyVj8tnoVvwn4Bz2+lmVBLIGKjifTvawRccw2QVXjTa5hUgNIbecgZv8PWZhseBREtccRbbqbuA==
xterm@4.11.0-beta.2:
version "4.11.0-beta.2"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.11.0-beta.2.tgz#a95560b61c771f54a336c2eb10e1472e556d9f4b"
integrity sha512-0BUaAfuclnowirdOuB13OGgq6OUGg/8etnRVT6apgnOrLGOLRCE1NiL3KhxotleAf4gVP0m3iCxsIr3csDY40g==
yauzl@^2.9.2:
version "2.10.0"

View file

@ -119,4 +119,5 @@ export const isWebkitWebView = (!isChrome && !isSafari && isWebKit);
export const isIPad = (userAgent.indexOf('iPad') >= 0 || (isSafari && navigator.maxTouchPoints > 0));
export const isEdgeLegacyWebView = isEdgeLegacy && (userAgent.indexOf('WebView/') >= 0);
export const isElectron = (userAgent.indexOf('Electron/') >= 0);
export const isAndroid = (userAgent.indexOf('Android') >= 0);
export const isStandalone = (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches);

View file

@ -1230,6 +1230,10 @@ export function asCSSUrl(uri: URI): string {
return `url('${FileAccess.asBrowserUri(uri).toString(true).replace(/'/g, '%27')}')`;
}
export function asCSSPropertyValue(value: string) {
return `'${value.replace(/'/g, '%27')}'`;
}
export function triggerDownload(dataOrUri: Uint8Array | URI, name: string): void {
// If the data is provided as Buffer, we create a

View file

@ -231,11 +231,13 @@ export class IconLabel extends Disposable {
}
tokenSource = new CancellationTokenSource();
function mouseLeaveOrDown(this: HTMLElement, e: MouseEvent): any {
isHovering = false;
hoverOptions = undefined;
tokenSource.dispose(true);
mouseLeaveDisposable.dispose();
mouseDownDisposable.dispose();
if ((<any>e).fromElement === htmlElement) {
isHovering = false;
hoverOptions = undefined;
tokenSource.dispose(true);
mouseLeaveDisposable.dispose();
mouseDownDisposable.dispose();
}
}
const mouseLeaveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_LEAVE, true)(mouseLeaveOrDown.bind(htmlElement));
const mouseDownDisposable = domEvent(htmlElement, dom.EventType.MOUSE_DOWN, true)(mouseLeaveOrDown.bind(htmlElement));

View file

@ -6,8 +6,7 @@
import * as dom from 'vs/base/browser/dom';
import { CSSIcon } from 'vs/base/common/codicons';
const labelWithIconsRegex = /(\\)?\$\(([a-z\-]+(?:~[a-z\-]+)?)\)/gi;
const labelWithIconsRegex = new RegExp(`(\\\\)?\\$\\((${CSSIcon.iconNameExpression}(?:${CSSIcon.iconModifierExpression})?)\\)`, 'g');
export function renderLabelWithIcons(text: string): Array<HTMLSpanElement | string> {
const elements = new Array<HTMLSpanElement | string>();
let match: RegExpMatchArray | null;

View file

@ -89,7 +89,6 @@
padding: 0.4em;
font-size: 12px;
line-height: 17px;
min-height: 34px;
margin-top: -1px;
word-wrap: break-word;
}

View file

@ -292,6 +292,9 @@ export class InputBox extends Widget {
if (range) {
this.input.setSelectionRange(range.start, range.end);
if (range.end === this.input.value.length) {
this.input.scrollLeft = this.input.scrollWidth;
}
}
}

View file

@ -746,7 +746,7 @@ export class MenuBar extends Disposable {
private setUnfocusedState(): void {
if (this.options.visibility === 'toggle' || this.options.visibility === 'hidden') {
this.focusState = MenubarState.HIDDEN;
} else if (this.options.visibility === 'default' && browser.isFullscreen()) {
} else if (this.options.visibility === 'classic' && browser.isFullscreen()) {
this.focusState = MenubarState.HIDDEN;
} else {
this.focusState = MenubarState.VISIBLE;
@ -838,6 +838,22 @@ export class MenuBar extends Disposable {
this._mnemonicsInUse = value;
}
private get shouldAltKeyFocus(): boolean {
if (isMacintosh) {
return false;
}
if (!this.options.disableAltFocus) {
return true;
}
if (this.options.visibility === 'toggle') {
return true;
}
return false;
}
public get onVisibilityChange(): Event<boolean> {
return this._onVisibilityChange.event;
}
@ -869,7 +885,7 @@ export class MenuBar extends Disposable {
}
// Prevent alt-key default if the menu is not hidden and we use alt to focus
if (modifierKeyStatus.event && !this.options.disableAltFocus) {
if (modifierKeyStatus.event && this.shouldAltKeyFocus) {
if (ScanCodeUtils.toEnum(modifierKeyStatus.event.code) === ScanCode.AltLeft) {
modifierKeyStatus.event.preventDefault();
}
@ -885,7 +901,7 @@ export class MenuBar extends Disposable {
// Clean alt key press and release
if (allModifiersReleased && modifierKeyStatus.lastKeyPressed === 'alt' && modifierKeyStatus.lastKeyReleased === 'alt') {
if (!this.awaitingAltRelease) {
if (!this.isFocused && !(this.options.disableAltFocus && this.options.visibility !== 'toggle')) {
if (!this.isFocused && this.shouldAltKeyFocus) {
this.mnemonicsInUse = true;
this.focusedMenu = { index: this.numMenusShown > 0 ? 0 : MenuBar.OVERFLOW_INDEX };
this.focusState = MenubarState.FOCUSED;

View file

@ -1,16 +0,0 @@
{
"name": "vs/base",
"dependencies": [
{
"name": "vs",
"internal": false
}
],
"libs": [
"lib.core.d.ts"
],
"sources": [
"**/*.ts"
],
"declares": []
}

View file

@ -71,21 +71,25 @@ export interface CSSIcon {
readonly id: string;
}
export namespace CSSIcon {
export const iconIdRegex = /^(codicon\/)?([a-z\-]+)(?:~([a-z\-]+))?$/i;
export const iconNameExpression = '[A-Za-z0-9\\-]+';
export const iconModifierExpression = '~[A-Za-z]+';
const cssIconIdRegex = new RegExp(`^(${iconNameExpression})(${iconModifierExpression})?$`);
export function asClassNameArray(icon: CSSIcon): string[] {
if (icon instanceof Codicon) {
return ['codicon', 'codicon-' + icon.id];
}
const match = iconIdRegex.exec(icon.id);
const match = cssIconIdRegex.exec(icon.id);
if (!match) {
return asClassNameArray(Codicon.error);
}
let [, , id, modifier] = match;
let [, id, modifier] = match;
const classNames = ['codicon', 'codicon-' + id];
if (modifier) {
classNames.push('codicon-modifier-' + modifier);
classNames.push('codicon-modifier-' + modifier.substr(1));
}
return classNames;
}
@ -538,6 +542,7 @@ export namespace Codicon {
export const typeHierarchy = new Codicon('type-hierarchy', { character: '\\ebb9' });
export const typeHierarchySub = new Codicon('type-hierarchy-sub', { character: '\\ebba' });
export const typeHierarchySuper = new Codicon('type-hierarchy-super', { character: '\\ebbb' });
export const gitPullRequestCreate = new Codicon('git-pull-request-create', { character: '\\ebbc' });
export const dropDownButton = new Codicon('drop-down-button', Codicon.chevronDown.definition);
}

View file

@ -3,28 +3,26 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CSSIcon } from 'vs/base/common/codicons';
import { matchesFuzzy, IMatch } from 'vs/base/common/filters';
import { ltrim } from 'vs/base/common/strings';
export const iconStartMarker = '$(';
const escapeIconsRegex = /(\\)?\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)/gi;
const iconsRegex = new RegExp(`\\$\\(${CSSIcon.iconNameExpression}(?:${CSSIcon.iconModifierExpression})?\\)`, 'g'); // no capturing groups
const escapeIconsRegex = new RegExp(`(\\\\)?${iconsRegex.source}`, 'g');
export function escapeIcons(text: string): string {
return text.replace(escapeIconsRegex, (match, escaped) => escaped ? match : `\\${match}`);
}
const markdownEscapedIconsRegex = /\\\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)/gi;
const markdownEscapedIconsRegex = new RegExp(`\\\\${iconsRegex.source}`, 'g');
export function markdownEscapeEscapedIcons(text: string): string {
// Need to add an extra \ for escaping in markdown
return text.replace(markdownEscapedIconsRegex, match => `\\${match}`);
}
const markdownUnescapeIconsRegex = /(\\)?\$\\\(([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?)\\\)/gi;
export function markdownUnescapeIcons(text: string): string {
return text.replace(markdownUnescapeIconsRegex, (match, escaped, iconId) => escaped ? match : `$(${iconId})`);
}
const stripIconsRegex = /(\s)?(\\)?\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)(\s)?/gi;
const stripIconsRegex = new RegExp(`(\\s)?(\\\\)?${iconsRegex.source}(\\s)?`, 'g');
export function stripIcons(text: string): string {
if (text.indexOf(iconStartMarker) === -1) {
return text;

View file

@ -71,6 +71,14 @@ export namespace Iterable {
}
}
export function reduce<T, R>(iterable: Iterable<T>, reducer: (previousValue: R, currentValue: T) => R, initialValue: R): R {
let value = initialValue;
for (const element of iterable) {
value = reducer(value, element);
}
return value;
}
/**
* Returns an iterable slice of the array, with the same semantics as `array.slice()`.
*/

View file

@ -7,7 +7,6 @@ import * as fs from 'fs';
import { rtrim } from 'vs/base/common/strings';
import { sep, join, normalize, dirname, basename } from 'vs/base/common/path';
import { readdirSync } from 'vs/base/node/pfs';
import { promisify } from 'util';
/**
* Copied from: https://github.com/microsoft/vscode-node-debug/blob/master/src/node/pathUtilities.ts#L83
@ -53,7 +52,7 @@ export function realcaseSync(path: string): string | null {
export async function realpath(path: string): Promise<string> {
try {
return await promisify(fs.realpath)(path);
return await fs.promises.realpath(path);
} catch (error) {
// We hit an error calling fs.realpath(). Since fs.realpath() is doing some path normalization
@ -63,7 +62,7 @@ export async function realpath(path: string): Promise<string> {
// to not resolve links but to simply see if the path is read accessible or not.
const normalizedPath = normalizePath(path);
await promisify(fs.access)(normalizedPath, fs.constants.R_OK);
await fs.promises.access(normalizedPath, fs.constants.R_OK);
return normalizedPath;
}

View file

@ -2,9 +2,9 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
//@ts-check
'use strict';
/**
* @param {NodeRequire} nodeRequire
@ -30,22 +30,6 @@ function factory(nodeRequire, path, fs, perf) {
return new Promise((c, e) => { const d = new Date(); fs.utimes(file, d, d, err => err ? e(err) : c()); });
}
/**
* @param {string} file
* @returns {Promise<object>}
*/
function lstat(file) {
return new Promise((c, e) => fs.lstat(file, (err, stats) => err ? e(err) : c(stats)));
}
/**
* @param {string} dir
* @returns {Promise<string[]>}
*/
function readdir(dir) {
return new Promise((c, e) => fs.readdir(dir, (err, files) => err ? e(err) : c(files)));
}
/**
* @param {string} dir
* @returns {Promise<string>}
@ -54,53 +38,20 @@ function factory(nodeRequire, path, fs, perf) {
return new Promise((c, e) => fs.mkdir(dir, { recursive: true }, err => (err && err.code !== 'EEXIST') ? e(err) : c(dir)));
}
/**
* @param {string} dir
* @returns {Promise<void>}
*/
function rmdir(dir) {
return new Promise((c, e) => fs.rmdir(dir, err => err ? e(err) : c(undefined)));
}
/**
* @param {string} file
* @returns {Promise<void>}
*/
function unlink(file) {
return new Promise((c, e) => fs.unlink(file, err => err ? e(err) : c(undefined)));
}
/**
* @param {string} location
* @returns {Promise<void>}
*/
function rimraf(location) {
return lstat(location).then(stat => {
if (stat.isDirectory() && !stat.isSymbolicLink()) {
return readdir(location)
.then(children => Promise.all(children.map(child => rimraf(path.join(location, child)))))
.then(() => rmdir(location));
} else {
return unlink(location);
}
}, err => {
if (err.code === 'ENOENT') {
return undefined;
}
throw err;
});
return new Promise((c, e) => fs.rmdir(location, { recursive: true }, err => (err && err.code !== 'ENOENT') ? e(err) : c()));
}
/**
* @param {string} file
* @returns {Promise<string>}
*/
function readFile(file) {
return new Promise(function (resolve, reject) {
fs.readFile(file, 'utf8', function (err, data) {
if (err) {
reject(err);
return;
}
resolve(data);
});
});
return new Promise((c, e) => fs.readFile(file, 'utf8', (err, data) => err ? e(err) : c(data)));
}
/**
@ -109,18 +60,9 @@ function factory(nodeRequire, path, fs, perf) {
* @returns {Promise<void>}
*/
function writeFile(file, content) {
return new Promise(function (resolve, reject) {
fs.writeFile(file, content, 'utf8', function (err) {
if (err) {
reject(err);
return;
}
resolve();
});
});
return new Promise((c, e) => fs.writeFile(file, content, 'utf8', err => err ? e(err) : c()));
}
/**
* @param {string} userDataPath
* @returns {object}
@ -301,8 +243,10 @@ function factory(nodeRequire, path, fs, perf) {
}
// @ts-ignore
if (typeof define === 'function') {
// amd
// @ts-ignore
define(['path', 'fs', 'vs/base/common/performance'], function (path, fs, perf) { return factory(require.__$__nodeRequire, path, fs, perf); });
} else if (typeof module === 'object' && typeof module.exports === 'object') {
const path = require('path');

View file

@ -6,14 +6,15 @@
import * as fs from 'fs';
import { tmpdir } from 'os';
import { join } from 'vs/base/common/path';
import { Promises, Queue } from 'vs/base/common/async';
import { isMacintosh, isWindows } from 'vs/base/common/platform';
import { Queue } from 'vs/base/common/async';
import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
import { Event } from 'vs/base/common/event';
import { promisify } from 'util';
import { isRootOrDriveLetter } from 'vs/base/common/extpath';
import { isEqualOrParent, isRootOrDriveLetter } from 'vs/base/common/extpath';
import { generateUuid } from 'vs/base/common/uuid';
import { normalizeNFC } from 'vs/base/common/normalization';
//#region Constants
// See https://github.com/microsoft/vscode/issues/30180
const WIN32_MAX_FILE_SIZE = 300 * 1024 * 1024; // 300 MB
const GENERAL_MAX_FILE_SIZE = 16 * 1024 * 1024 * 1024; // 16 GB
@ -25,6 +26,10 @@ const GENERAL_MAX_HEAP_SIZE = 700 * 2 * 1024 * 1024; // 1400 MB
export const MAX_FILE_SIZE = process.arch === 'ia32' ? WIN32_MAX_FILE_SIZE : GENERAL_MAX_FILE_SIZE;
export const MAX_HEAP_SIZE = process.arch === 'ia32' ? WIN32_MAX_HEAP_SIZE : GENERAL_MAX_HEAP_SIZE;
//#endregion
//#region rimraf
export enum RimRafMode {
/**
@ -40,12 +45,19 @@ export enum RimRafMode {
MOVE
}
/**
* Allows to delete the provied path (either file or folder) recursively
* with the options:
* - `UNLINK`: direct removal from disk
* - `MOVE`: faster variant that first moves the target to temp dir and then
* deletes it in the background without waiting for that to finish.
*/
export async function rimraf(path: string, mode = RimRafMode.UNLINK): Promise<void> {
if (isRootOrDriveLetter(path)) {
throw new Error('rimraf - will refuse to recursively delete root');
}
// delete: via unlink
// delete: via rmDir
if (mode === RimRafMode.UNLINK) {
return rimrafUnlink(path);
}
@ -54,32 +66,17 @@ export async function rimraf(path: string, mode = RimRafMode.UNLINK): Promise<vo
return rimrafMove(path);
}
async function rimrafUnlink(path: string): Promise<void> {
async function rimrafMove(path: string): Promise<void> {
try {
const stat = await lstat(path);
// Folder delete (recursive) - NOT for symbolic links though!
if (stat.isDirectory() && !stat.isSymbolicLink()) {
// Children
const children = await readdir(path);
await Promises.settled(children.map(child => rimrafUnlink(join(path, child))));
// Folder
await promisify(fs.rmdir)(path);
const pathInTemp = join(tmpdir(), generateUuid());
try {
await fs.promises.rename(path, pathInTemp);
} catch (error) {
return rimrafUnlink(path); // if rename fails, delete without tmp dir
}
// Single file delete
else {
// chmod as needed to allow for unlink
const mode = stat.mode;
if (!(mode & fs.constants.S_IWUSR)) {
await chmod(path, mode | fs.constants.S_IWUSR);
}
return unlink(path);
}
// Delete but do not return as promise
rimrafUnlink(pathInTemp).catch(error => {/* ignore */ });
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
@ -87,22 +84,8 @@ async function rimrafUnlink(path: string): Promise<void> {
}
}
async function rimrafMove(path: string): Promise<void> {
try {
const pathInTemp = join(tmpdir(), generateUuid());
try {
await rename(path, pathInTemp);
} catch (error) {
return rimrafUnlink(path); // if rename fails, delete without tmp dir
}
// Delete but do not return as promise
rimrafUnlink(pathInTemp);
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
async function rimrafUnlink(path: string): Promise<void> {
return fs.promises.rmdir(path, { recursive: true, maxRetries: 3 });
}
export function rimrafSync(path: string): void {
@ -110,193 +93,281 @@ export function rimrafSync(path: string): void {
throw new Error('rimraf - will refuse to recursively delete root');
}
fs.rmdirSync(path, { recursive: true });
}
//#endregion
//#region readdir with NFC support (macos)
export interface IDirent {
name: string;
isFile(): boolean;
isDirectory(): boolean;
isSymbolicLink(): boolean;
}
/**
* Drop-in replacement of `fs.readdir` with support
* for converting from macOS NFD unicon form to NFC
* (https://github.com/nodejs/node/issues/2165)
*/
export async function readdir(path: string): Promise<string[]>;
export async function readdir(path: string, options: { withFileTypes: true }): Promise<IDirent[]>;
export async function readdir(path: string, options?: { withFileTypes: true }): Promise<(string | IDirent)[]> {
return handleDirectoryChildren(await (options ? safeReaddirWithFileTypes(path) : fs.promises.readdir(path)));
}
async function safeReaddirWithFileTypes(path: string): Promise<IDirent[]> {
try {
const stat = fs.lstatSync(path);
// Folder delete (recursive) - NOT for symbolic links though!
if (stat.isDirectory() && !stat.isSymbolicLink()) {
// Children
const children = readdirSync(path);
children.map(child => rimrafSync(join(path, child)));
// Folder
fs.rmdirSync(path);
}
// Single file delete
else {
// chmod as needed to allow for unlink
const mode = stat.mode;
if (!(mode & fs.constants.S_IWUSR)) {
fs.chmodSync(path, mode | fs.constants.S_IWUSR);
}
return fs.unlinkSync(path);
}
return await fs.promises.readdir(path, { withFileTypes: true });
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
}
export async function readdir(path: string): Promise<string[]> {
return handleDirectoryChildren(await promisify(fs.readdir)(path));
}
export async function readdirWithFileTypes(path: string): Promise<fs.Dirent[]> {
const children = await promisify(fs.readdir)(path, { withFileTypes: true });
// Mac: uses NFD unicode form on disk, but we want NFC
// See also https://github.com/nodejs/node/issues/2165
if (isMacintosh) {
for (const child of children) {
child.name = normalizeNFC(child.name);
}
console.warn('[node.js fs] readdir with filetypes failed with error: ', error);
}
return children;
// Fallback to manually reading and resolving each
// children of the folder in case we hit an error
// previously.
// This can only really happen on exotic file systems
// such as explained in #115645 where we get entries
// from `readdir` that we can later not `lstat`.
const result: IDirent[] = [];
const children = await readdir(path);
for (const child of children) {
let isFile = false;
let isDirectory = false;
let isSymbolicLink = false;
try {
const lstat = await fs.promises.lstat(join(path, child));
isFile = lstat.isFile();
isDirectory = lstat.isDirectory();
isSymbolicLink = lstat.isSymbolicLink();
} catch (error) {
console.warn('[node.js fs] unexpected error from lstat after readdir: ', error);
}
result.push({
name: child,
isFile: () => isFile,
isDirectory: () => isDirectory,
isSymbolicLink: () => isSymbolicLink
});
}
return result;
}
/**
* Drop-in replacement of `fs.readdirSync` with support
* for converting from macOS NFD unicon form to NFC
* (https://github.com/nodejs/node/issues/2165)
*/
export function readdirSync(path: string): string[] {
return handleDirectoryChildren(fs.readdirSync(path));
}
function handleDirectoryChildren(children: string[]): string[] {
// Mac: uses NFD unicode form on disk, but we want NFC
// See also https://github.com/nodejs/node/issues/2165
if (isMacintosh) {
return children.map(child => normalizeNFC(child));
}
function handleDirectoryChildren(children: string[]): string[];
function handleDirectoryChildren(children: IDirent[]): IDirent[];
function handleDirectoryChildren(children: (string | IDirent)[]): (string | IDirent)[];
function handleDirectoryChildren(children: (string | IDirent)[]): (string | IDirent)[] {
return children.map(child => {
return children;
}
// Mac: uses NFD unicode form on disk, but we want NFC
// See also https://github.com/nodejs/node/issues/2165
export function exists(path: string): Promise<boolean> {
return promisify(fs.exists)(path);
}
export function chmod(path: string, mode: number): Promise<void> {
return promisify(fs.chmod)(path, mode);
}
export function stat(path: string): Promise<fs.Stats> {
return promisify(fs.stat)(path);
}
export interface IStatAndLink {
// The stats of the file. If the file is a symbolic
// link, the stats will be of that target file and
// not the link itself.
// If the file is a symbolic link pointing to a non
// existing file, the stat will be of the link and
// the `dangling` flag will indicate this.
stat: fs.Stats;
// Will be provided if the resource is a symbolic link
// on disk. Use the `dangling` flag to find out if it
// points to a resource that does not exist on disk.
symbolicLink?: { dangling: boolean };
}
export async function statLink(path: string): Promise<IStatAndLink> {
// First stat the link
let lstats: fs.Stats | undefined;
try {
lstats = await lstat(path);
// Return early if the stat is not a symbolic link at all
if (!lstats.isSymbolicLink()) {
return { stat: lstats };
}
} catch (error) {
/* ignore - use stat() instead */
}
// If the stat is a symbolic link or failed to stat, use fs.stat()
// which for symbolic links will stat the target they point to
try {
const stats = await stat(path);
return { stat: stats, symbolicLink: lstats?.isSymbolicLink() ? { dangling: false } : undefined };
} catch (error) {
// If the link points to a non-existing 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 } };
if (typeof child === 'string') {
return isMacintosh ? normalizeNFC(child) : child;
}
// Windows: workaround a node.js bug where reparse points
// are not supported (https://github.com/nodejs/node/issues/36790)
if (isWindows && error.code === 'EACCES' && lstats) {
try {
const stats = await stat(await readlink(path));
child.name = isMacintosh ? normalizeNFC(child.name) : child.name;
return { stat: stats, symbolicLink: lstats.isSymbolicLink() ? { dangling: false } : undefined };
} catch (error) {
return child;
});
}
// If the link points to a non-existing file we still want
// to return it as result while setting dangling: true flag
if (error.code === 'ENOENT') {
return { stat: lstats, symbolicLink: { dangling: true } };
}
/**
* A convinience method to read all children of a path that
* are directories.
*/
export async function readDirsInDir(dirPath: string): Promise<string[]> {
const children = await readdir(dirPath);
const directories: string[] = [];
throw error;
for (const child of children) {
if (await SymlinkSupport.existsDirectory(join(dirPath, child))) {
directories.push(child);
}
}
return directories;
}
//#endregion
//#region whenDeleted()
/**
* A `Promise` that resolves when the provided `path`
* is deleted from disk.
*/
export function whenDeleted(path: string, intervalMs = 1000): Promise<void> {
return new Promise<void>(resolve => {
let running = false;
const interval = setInterval(() => {
if (!running) {
running = true;
fs.access(path, err => {
running = false;
if (err) {
clearInterval(interval);
resolve(undefined);
}
});
}
}, intervalMs);
});
}
//#endregion
//#region Methods with symbolic links support
export namespace SymlinkSupport {
export interface IStats {
// The stats of the file. If the file is a symbolic
// link, the stats will be of that target file and
// not the link itself.
// If the file is a symbolic link pointing to a non
// existing file, the stat will be of the link and
// the `dangling` flag will indicate this.
stat: fs.Stats;
// Will be provided if the resource is a symbolic link
// on disk. Use the `dangling` flag to find out if it
// points to a resource that does not exist on disk.
symbolicLink?: { dangling: boolean };
}
/**
* Resolves the `fs.Stats` of the provided path. If the path is a
* symbolic link, the `fs.Stats` will be from the target it points
* to. If the target does not exist, `dangling: true` will be returned
* as `symbolicLink` value.
*/
export async function stat(path: string): Promise<IStats> {
// First stat the link
let lstats: fs.Stats | undefined;
try {
lstats = await fs.promises.lstat(path);
// Return early if the stat is not a symbolic link at all
if (!lstats.isSymbolicLink()) {
return { stat: lstats };
}
} catch (error) {
/* ignore - use stat() instead */
}
throw error;
// If the stat is a symbolic link or failed to stat, use fs.stat()
// which for symbolic links will stat the target they point to
try {
const stats = await fs.promises.stat(path);
return { stat: stats, symbolicLink: lstats?.isSymbolicLink() ? { dangling: false } : undefined };
} catch (error) {
// If the link points to a non-existing 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 } };
}
// Windows: workaround a node.js bug where reparse points
// are not supported (https://github.com/nodejs/node/issues/36790)
if (isWindows && error.code === 'EACCES') {
try {
const stats = await fs.promises.stat(await fs.promises.readlink(path));
return { stat: stats, symbolicLink: { dangling: false } };
} catch (error) {
// If the link points to a non-existing 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 } };
}
throw error;
}
}
throw error;
}
}
/**
* Figures out if the `path` exists and is a file with support
* for symlinks.
*
* Note: this will return `false` for a symlink that exists on
* disk but is dangling (pointing to a non-existing path).
*
* Use `exists` if you only care about the path existing on disk
* or not without support for symbolic links.
*/
export async function existsFile(path: string): Promise<boolean> {
try {
const { stat, symbolicLink } = await SymlinkSupport.stat(path);
return stat.isFile() && symbolicLink?.dangling !== true;
} catch (error) {
// Ignore, path might not exist
}
return false;
}
/**
* Figures out if the `path` exists and is a directory with support for
* symlinks.
*
* Note: this will return `false` for a symlink that exists on
* disk but is dangling (pointing to a non-existing path).
*
* Use `exists` if you only care about the path existing on disk
* or not without support for symbolic links.
*/
export async function existsDirectory(path: string): Promise<boolean> {
try {
const { stat, symbolicLink } = await SymlinkSupport.stat(path);
return stat.isDirectory() && symbolicLink?.dangling !== true;
} catch (error) {
// Ignore, path might not exist
}
return false;
}
}
export function lstat(path: string): Promise<fs.Stats> {
return promisify(fs.lstat)(path);
}
//#endregion
export function rename(oldPath: string, newPath: string): Promise<void> {
return promisify(fs.rename)(oldPath, newPath);
}
export function renameIgnoreError(oldPath: string, newPath: string): Promise<void> {
return new Promise(resolve => fs.rename(oldPath, newPath, () => resolve()));
}
export function readlink(path: string): Promise<string> {
return promisify(fs.readlink)(path);
}
export function unlink(path: string): Promise<void> {
return promisify(fs.unlink)(path);
}
export function symlink(target: string, path: string, type?: string): Promise<void> {
return promisify(fs.symlink)(target, path, type);
}
export function truncate(path: string, len: number): Promise<void> {
return promisify(fs.truncate)(path, len);
}
export function readFile(path: string): Promise<Buffer>;
export function readFile(path: string, encoding: string): Promise<string>;
export function readFile(path: string, encoding?: string): Promise<Buffer | string> {
return promisify(fs.readFile)(path, encoding);
}
export async function mkdirp(path: string, mode?: number): Promise<void> {
return promisify(fs.mkdir)(path, { mode, recursive: true });
}
// According to node.js docs (https://nodejs.org/docs/v6.5.0/api/fs.html#fs_fs_writefile_file_data_options_callback)
// it is not safe to call writeFile() on the same path multiple times without waiting for the callback to return.
// Therefor we use a Queue on the path that is given to us to sequentialize calls to the same path properly.
const writeFilePathQueues: Map<string, Queue<void>> = new Map();
//#region Write File
/**
* Same as `fs.writeFile` but with an additional call to
* `fs.fdatasync` after writing to ensure changes are
* flushed to disk.
*
* In addition, multiple writes to the same path are queued.
*/
export function writeFile(path: string, data: string, options?: IWriteFileOptions): Promise<void>;
export function writeFile(path: string, data: Buffer, options?: IWriteFileOptions): Promise<void>;
export function writeFile(path: string, data: Uint8Array, options?: IWriteFileOptions): Promise<void>;
@ -311,6 +382,11 @@ export function writeFile(path: string, data: string | Buffer | Uint8Array, opti
});
}
// According to node.js docs (https://nodejs.org/docs/v6.5.0/api/fs.html#fs_fs_writefile_file_data_options_callback)
// it is not safe to call writeFile() on the same path multiple times without waiting for the callback to return.
// Therefor we use a Queue on the path that is given to us to sequentialize calls to the same path properly.
const writeFilePathQueues: Map<string, Queue<void>> = new Map();
function toQueueKey(path: string): string {
let queueKey = path;
if (isWindows || isMacintosh) {
@ -388,6 +464,11 @@ function doWriteFileAndFlush(path: string, data: string | Buffer | Uint8Array, o
});
}
/**
* Same as `fs.writeFileSync` but with an additional call to
* `fs.fdatasyncSync` after writing to ensure changes are
* flushed to disk.
*/
export function writeFileSync(path: string, data: string | Buffer, options?: IWriteFileOptions): void {
const ensuredOptions = ensureWriteOptions(options);
@ -426,87 +507,48 @@ function ensureWriteOptions(options?: IWriteFileOptions): IEnsuredWriteFileOptio
};
}
export async function readDirsInDir(dirPath: string): Promise<string[]> {
const children = await readdir(dirPath);
const directories: string[] = [];
//#endregion
for (const child of children) {
if (await dirExists(join(dirPath, child))) {
directories.push(child);
}
}
return directories;
}
export async function dirExists(path: string): Promise<boolean> {
try {
const { stat, symbolicLink } = await statLink(path);
return stat.isDirectory() && symbolicLink?.dangling !== true;
} catch (error) {
// Ignore, path might not exist
}
return false;
}
export async function fileExists(path: string): Promise<boolean> {
try {
const { stat, symbolicLink } = await statLink(path);
return stat.isFile() && symbolicLink?.dangling !== true;
} catch (error) {
// Ignore, path might not exist
}
return false;
}
export function whenDeleted(path: string): Promise<void> {
// Complete when wait marker file is deleted
return new Promise<void>(resolve => {
let running = false;
const interval = setInterval(() => {
if (!running) {
running = true;
fs.exists(path, exists => {
running = false;
if (!exists) {
clearInterval(interval);
resolve(undefined);
}
});
}
}, 1000);
});
}
//#region Move / Copy
/**
* A drop-in replacement for `fs.rename` that:
* - updates the `mtime` of the `source` after the operation
* - allows to move across multiple disks
*/
export async function move(source: string, target: string): Promise<void> {
if (source === target) {
return;
return; // simulate node.js behaviour here and do a no-op if paths match
}
// We have been updating `mtime` for move operations for files since the
// beginning for reasons that are no longer quite clear, but changing
// this could be risky as well. As such, trying to reason about it:
// It is very common as developer to have file watchers enabled that watch
// the current workspace for changes. Updating the `mtime` might make it
// easier for these watchers to recognize an actual change. Since changing
// a source code file also updates the `mtime`, moving a file should do so
// as well because conceptually it is a change of a similar category.
async function updateMtime(path: string): Promise<void> {
const stat = await lstat(path);
if (stat.isDirectory() || stat.isSymbolicLink()) {
return; // only for files
}
const fd = await promisify(fs.open)(path, 'a');
try {
await promisify(fs.futimes)(fd, stat.atime, new Date());
} catch (error) {
//ignore
}
const stat = await fs.promises.lstat(path);
if (stat.isDirectory() || stat.isSymbolicLink()) {
return; // only for files
}
return promisify(fs.close)(fd);
const fh = await fs.promises.open(path, 'a');
try {
await fh.utimes(stat.atime, new Date());
} finally {
await fh.close();
}
} catch (error) {
// Ignore any error
}
}
try {
await rename(source, target);
await fs.promises.rename(source, target);
await updateMtime(target);
} catch (error) {
@ -519,7 +561,7 @@ export async function move(source: string, target: string): Promise<void> {
// 2.) The user tries to rename a file/folder that ends with a dot. This is not
// really possible to move then, at least on UNC devices.
if (source.toLowerCase() !== target.toLowerCase() && error.code === 'EXDEV' || source.endsWith('.')) {
await copy(source, target);
await copy(source, target, { preserveSymlinks: false /* copying to another device */ });
await rimraf(source, RimRafMode.MOVE);
await updateMtime(target);
} else {
@ -528,74 +570,119 @@ export async function move(source: string, target: string): Promise<void> {
}
}
interface ICopyPayload {
readonly root: { source: string, target: string };
readonly options: { preserveSymlinks: boolean };
readonly handledSourcePaths: Set<string>;
}
/**
* Recursively copies all of `source` to `target`.
*
* The options `preserveSymlinks` configures how symbolic
* links should be handled when encountered. Set to
* `false` to not preserve them and `true` otherwise.
*/
export async function copy(source: string, target: string, options: { preserveSymlinks: boolean }): Promise<void> {
return doCopy(source, target, { root: { source, target }, options, handledSourcePaths: new Set<string>() });
}
// When copying a file or folder, we want to preserve the mode
// it had and as such provide it when creating. However, modes
// can go beyond what we expect (see link below), so we mask it.
// (https://github.com/nodejs/node-v0.x-archive/issues/3045#issuecomment-4862588)
//
// The `copy` method is very old so we should probably revisit
// it's implementation and check wether this mask is still needed.
const COPY_MODE_MASK = 0o777;
export async function copy(source: string, target: string, handledSourcesIn?: { [path: string]: boolean }): Promise<void> {
async function doCopy(source: string, target: string, payload: ICopyPayload): Promise<void> {
// Keep track of paths already copied to prevent
// cycles from symbolic links to cause issues
const handledSources = handledSourcesIn ?? Object.create(null);
if (handledSources[source]) {
if (payload.handledSourcePaths.has(source)) {
return;
} else {
handledSources[source] = true;
payload.handledSourcePaths.add(source);
}
const { stat, symbolicLink } = await statLink(source);
if (symbolicLink?.dangling) {
return; // skip over dangling symbolic links (https://github.com/microsoft/vscode/issues/111621)
const { stat, symbolicLink } = await SymlinkSupport.stat(source);
// Symlink
if (symbolicLink) {
if (symbolicLink.dangling) {
return; // do not copy dangling symbolic links (https://github.com/microsoft/vscode/issues/111621)
}
// Try to re-create the symlink unless `preserveSymlinks: false`
if (payload.options.preserveSymlinks) {
try {
return await doCopySymlink(source, target, payload);
} catch (error) {
// in any case of an error fallback to normal copy via dereferencing
console.warn('[node.js fs] copy of symlink failed: ', error);
}
}
}
if (!stat.isDirectory()) {
// Folder
if (stat.isDirectory()) {
return doCopyDirectory(source, target, stat.mode & COPY_MODE_MASK, payload);
}
// File or file-like
else {
return doCopyFile(source, target, stat.mode & COPY_MODE_MASK);
}
}
async function doCopyDirectory(source: string, target: string, mode: number, payload: ICopyPayload): Promise<void> {
// Create folder
await mkdirp(target, stat.mode & COPY_MODE_MASK);
await fs.promises.mkdir(target, { recursive: true, mode });
// Copy each file recursively
const files = await readdir(source);
for (let i = 0; i < files.length; i++) {
const file = files[i];
await copy(join(source, file), join(target, file), handledSources);
for (const file of files) {
await doCopy(join(source, file), join(target, file), payload);
}
}
async function doCopyFile(source: string, target: string, mode: number): Promise<void> {
return new Promise((resolve, reject) => {
const reader = fs.createReadStream(source);
const writer = fs.createWriteStream(target, { mode });
let finished = false;
const finish = (error?: Error) => {
if (!finished) {
finished = true;
// Copy file
await fs.promises.copyFile(source, target);
// in error cases, pass to callback
if (error) {
return reject(error);
}
// we need to explicitly chmod because of https://github.com/nodejs/node/issues/1104
fs.chmod(target, mode, error => error ? reject(error) : resolve());
}
};
// handle errors properly
reader.once('error', error => finish(error));
writer.once('error', error => finish(error));
// we are done (underlying fd has been closed)
writer.once('close', () => finish());
// start piping
reader.pipe(writer);
});
// restore mode (https://github.com/nodejs/node/issues/1104)
await fs.promises.chmod(target, mode);
}
async function doCopySymlink(source: string, target: string, payload: ICopyPayload): Promise<void> {
// Figure out link target
let linkTarget = await fs.promises.readlink(source);
// Special case: the symlink points to a target that is
// actually within the path that is being copied. In that
// case we want the symlink to point to the target and
// not the source
if (isEqualOrParent(linkTarget, payload.root.source, !isLinux)) {
linkTarget = join(payload.root.target, linkTarget.substr(payload.root.source.length + 1));
}
// Create symlink
await fs.promises.symlink(linkTarget, target);
}
//#endregion
//#region Async FS Methods
export async function exists(path: string): Promise<boolean> {
try {
await fs.promises.access(path);
return true;
} catch {
return false;
}
}
//#endregion

View file

@ -38,7 +38,7 @@ class PossiblePowerShellExe implements IPossiblePowerShellExe {
public async exists(): Promise<boolean> {
if (this.knownToExist === undefined) {
this.knownToExist = await pfs.fileExists(this.exePath);
this.knownToExist = await pfs.SymlinkSupport.existsFile(this.exePath);
}
return this.knownToExist;
}
@ -100,7 +100,7 @@ async function findPSCoreWindowsInstallation(
const powerShellInstallBaseDir = path.join(programFilesPath, 'PowerShell');
// Ensure the base directory exists
if (!await pfs.dirExists(powerShellInstallBaseDir)) {
if (!await pfs.SymlinkSupport.existsDirectory(powerShellInstallBaseDir)) {
return null;
}
@ -142,7 +142,7 @@ async function findPSCoreWindowsInstallation(
// Now look for the file
const exePath = path.join(powerShellInstallBaseDir, item, 'pwsh.exe');
if (!await pfs.fileExists(exePath)) {
if (!await pfs.SymlinkSupport.existsFile(exePath)) {
continue;
}
@ -169,7 +169,7 @@ async function findPSCoreMsix({ findPreview }: { findPreview?: boolean } = {}):
// Find the base directory for MSIX application exe shortcuts
const msixAppDir = path.join(env.LOCALAPPDATA, 'Microsoft', 'WindowsApps');
if (!await pfs.dirExists(msixAppDir)) {
if (!await pfs.SymlinkSupport.existsDirectory(msixAppDir)) {
return null;
}

View file

@ -5,7 +5,7 @@
import * as path from 'vs/base/common/path';
import * as fs from 'fs';
import { promisify } from 'util';
import * as pfs from 'vs/base/node/pfs';
import * as cp from 'child_process';
import * as nls from 'vs/nls';
import * as Types from 'vs/base/common/types';
@ -456,8 +456,8 @@ export namespace win32 {
}
async function fileExists(path: string): Promise<boolean> {
if (await promisify(fs.exists)(path)) {
return !((await promisify(fs.stat)(path)).isDirectory());
if (await pfs.exists(path)) {
return !((await fs.promises.stat(path)).isDirectory());
}
return false;
}

View file

@ -5,10 +5,10 @@
import * as nls from 'vs/nls';
import * as path from 'vs/base/common/path';
import { createWriteStream, WriteStream } from 'fs';
import { promises, createWriteStream, WriteStream } from 'fs';
import { Readable } from 'stream';
import { Sequencer, createCancelablePromise } from 'vs/base/common/async';
import { mkdirp, rimraf } from 'vs/base/node/pfs';
import { rimraf } from 'vs/base/node/pfs';
import { open as _openZip, Entry, ZipFile } from 'yauzl';
import * as yazl from 'yazl';
import { CancellationToken } from 'vs/base/common/cancellation';
@ -86,7 +86,7 @@ function extractEntry(stream: Readable, fileName: string, mode: number, targetPa
}
});
return Promise.resolve(mkdirp(targetDirName)).then(() => new Promise<void>((c, e) => {
return Promise.resolve(promises.mkdir(targetDirName, { recursive: true })).then(() => new Promise<void>((c, e) => {
if (token.isCancellationRequested) {
return;
}
@ -149,7 +149,7 @@ function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions, tok
// directory file names end with '/'
if (/\/$/.test(fileName)) {
const targetFileName = path.join(targetPath, fileName);
last = createCancelablePromise(token => mkdirp(targetFileName).then(() => readNextEntry(token)).then(undefined, e));
last = createCancelablePromise(token => promises.mkdir(targetFileName, { recursive: true }).then(() => readNextEntry(token)).then(undefined, e));
return;
}

View file

@ -4,11 +4,12 @@
*--------------------------------------------------------------------------------------------*/
import type { Database, Statement } from 'vscode-sqlite3';
import { promises } from 'fs';
import { Event } from 'vs/base/common/event';
import { timeout } from 'vs/base/common/async';
import { mapToString, setToString } from 'vs/base/common/map';
import { basename } from 'vs/base/common/path';
import { copy, renameIgnoreError, unlink } from 'vs/base/node/pfs';
import { copy } from 'vs/base/node/pfs';
import { IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage';
interface IDatabaseConnection {
@ -186,7 +187,7 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
// Delete the existing DB. If the path does not exist or fails to
// be deleted, we do not try to recover anymore because we assume
// that the path is no longer writeable for us.
return unlink(this.path).then(() => {
return promises.unlink(this.path).then(() => {
// Re-open the DB fresh
return this.doConnect(this.path).then(recoveryConnection => {
@ -216,7 +217,7 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
private backup(): Promise<void> {
const backupPath = this.toBackupPath(this.path);
return copy(this.path, backupPath);
return copy(this.path, backupPath, { preserveSymlinks: false });
}
private toBackupPath(path: string): string {
@ -272,8 +273,12 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
// folder is really not writeable for us.
//
try {
await unlink(path);
await renameIgnoreError(this.toBackupPath(path), path);
await promises.unlink(path);
try {
await promises.rename(this.toBackupPath(path), path);
} catch (error) {
// ignore
}
return await this.doConnect(path);
} catch (error) {

View file

@ -7,8 +7,9 @@ import { SQLiteStorageDatabase, ISQLiteStorageDatabaseOptions } from 'vs/base/pa
import { Storage, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/base/parts/storage/common/storage';
import { join } from 'vs/base/common/path';
import { tmpdir } from 'os';
import { promises } from 'fs';
import { strictEqual, ok } from 'assert';
import { mkdirp, writeFile, exists, unlink, rimraf } from 'vs/base/node/pfs';
import { writeFile, exists, rimraf } from 'vs/base/node/pfs';
import { timeout } from 'vs/base/common/async';
import { Event, Emitter } from 'vs/base/common/event';
import { isWindows } from 'vs/base/common/platform';
@ -22,7 +23,7 @@ flakySuite('Storage Library', function () {
setup(function () {
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storagelibrary');
return mkdirp(testDir);
return promises.mkdir(testDir, { recursive: true });
});
teardown(function () {
@ -294,7 +295,7 @@ flakySuite('SQLite Storage Library', function () {
setup(function () {
testdir = getRandomTestPath(tmpdir(), 'vsctests', 'storagelibrary');
return mkdirp(testdir);
return promises.mkdir(testdir, { recursive: true });
});
teardown(function () {
@ -475,7 +476,7 @@ flakySuite('SQLite Storage Library', function () {
// on shutdown.
await storage.checkIntegrity(true).then(null, error => { } /* error is expected here but we do not want to fail */);
await unlink(backupPath); // also test that the recovery DB is backed up properly
await promises.unlink(backupPath); // also test that the recovery DB is backed up properly
let recoveryCalled = false;
await storage.close(() => {

View file

@ -5,7 +5,7 @@
import * as assert from 'assert';
import { IMatch } from 'vs/base/common/filters';
import { matchesFuzzyIconAware, parseLabelWithIcons, IParsedLabelWithIcons, stripIcons } from 'vs/base/common/iconLabels';
import { matchesFuzzyIconAware, parseLabelWithIcons, IParsedLabelWithIcons, stripIcons, escapeIcons, markdownEscapeEscapedIcons } from 'vs/base/common/iconLabels';
export interface IIconFilter {
// Returns null if word doesn't match.
@ -71,4 +71,18 @@ suite('Icon Labels', () => {
assert.strictEqual(stripIcons('$(Hello) World'), ' World');
assert.strictEqual(stripIcons('$(Hello) W$(oi)rld'), ' Wrld');
});
test('escapeIcons', () => {
assert.strictEqual(escapeIcons('Hello World'), 'Hello World');
assert.strictEqual(escapeIcons('$(Hello World'), '$(Hello World');
assert.strictEqual(escapeIcons('$(Hello) World'), '\\$(Hello) World');
assert.strictEqual(escapeIcons('\\$(Hello) W$(oi)rld'), '\\$(Hello) W\\$(oi)rld');
});
test('markdownEscapeEscapedIcons', () => {
assert.strictEqual(markdownEscapeEscapedIcons('Hello World'), 'Hello World');
assert.strictEqual(markdownEscapeEscapedIcons('$(Hello) World'), '$(Hello) World');
assert.strictEqual(markdownEscapeEscapedIcons('\\$(Hello) World'), '\\\\$(Hello) World');
});
});

View file

@ -6,7 +6,8 @@
import { checksum } from 'vs/base/node/crypto';
import { join } from 'vs/base/common/path';
import { tmpdir } from 'os';
import { mkdirp, rimraf, writeFile } from 'vs/base/node/pfs';
import { promises } from 'fs';
import { rimraf, writeFile } from 'vs/base/node/pfs';
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
suite('Crypto', () => {
@ -16,7 +17,7 @@ suite('Crypto', () => {
setup(function () {
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'crypto');
return mkdirp(testDir);
return promises.mkdir(testDir, { recursive: true });
});
teardown(function () {

View file

@ -5,7 +5,8 @@
import * as assert from 'assert';
import { tmpdir } from 'os';
import { mkdirp, rimraf } from 'vs/base/node/pfs';
import { promises } from 'fs';
import { rimraf } from 'vs/base/node/pfs';
import { realcaseSync, realpath, realpathSync } from 'vs/base/node/extpath';
import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils';
@ -15,7 +16,7 @@ flakySuite('Extpath', () => {
setup(() => {
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'extpath');
return mkdirp(testDir, 493);
return promises.mkdir(testDir, { recursive: true });
});
teardown(() => {

View file

@ -8,12 +8,13 @@ import * as fs from 'fs';
import { tmpdir } from 'os';
import { join, sep } from 'vs/base/common/path';
import { generateUuid } from 'vs/base/common/uuid';
import { copy, exists, mkdirp, move, readdir, readDirsInDir, readdirWithFileTypes, readFile, renameIgnoreError, rimraf, RimRafMode, rimrafSync, statLink, writeFile, writeFileSync } from 'vs/base/node/pfs';
import { copy, exists, move, readdir, readDirsInDir, rimraf, RimRafMode, rimrafSync, SymlinkSupport, writeFile, writeFileSync } from 'vs/base/node/pfs';
import { timeout } from 'vs/base/common/async';
import { getPathFromAmdModule } from 'vs/base/common/amd';
import { canNormalize } from 'vs/base/common/normalization';
import { VSBuffer } from 'vs/base/common/buffer';
import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils';
import { isWindows } from 'vs/base/common/platform';
flakySuite('PFS', function () {
@ -22,7 +23,7 @@ flakySuite('PFS', function () {
setup(() => {
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'pfs');
return mkdirp(testDir, 493);
return fs.promises.mkdir(testDir, { recursive: true });
});
teardown(() => {
@ -36,7 +37,7 @@ flakySuite('PFS', function () {
await writeFile(testFile, 'Hello World', (null!));
assert.strictEqual((await readFile(testFile)).toString(), 'Hello World');
assert.strictEqual((await fs.promises.readFile(testFile)).toString(), 'Hello World');
});
test('writeFile - parallel write on different files works', async () => {
@ -153,10 +154,6 @@ flakySuite('PFS', function () {
assert.ok(!fs.existsSync(testDir));
});
test('moveIgnoreError', () => {
return renameIgnoreError(join(testDir, 'foo'), join(testDir, 'bar'));
});
test('copy, move and delete', async () => {
const id = generateUuid();
const id2 = generateUuid();
@ -165,7 +162,7 @@ flakySuite('PFS', function () {
const targetDir = join(parentDir, id);
const targetDir2 = join(parentDir, id2);
await copy(sourceDir, targetDir);
await copy(sourceDir, targetDir, { preserveSymlinks: true });
assert.ok(fs.existsSync(targetDir));
assert.ok(fs.existsSync(join(targetDir, 'index.html')));
@ -194,33 +191,103 @@ flakySuite('PFS', function () {
assert.ok(!fs.existsSync(parentDir));
});
test('copy skips over dangling symbolic links', async () => {
test('copy handles symbolic links', async () => {
const id1 = generateUuid();
const symbolicLinkTarget = join(testDir, id1);
const id2 = generateUuid();
const symbolicLink = join(testDir, id2);
const symLink = join(testDir, id2);
const id3 = generateUuid();
const copyTarget = join(testDir, id3);
await mkdirp(symbolicLinkTarget, 493);
await fs.promises.mkdir(symbolicLinkTarget, { recursive: true });
fs.symlinkSync(symbolicLinkTarget, symbolicLink, 'junction');
fs.symlinkSync(symbolicLinkTarget, symLink, 'junction');
// Copy preserves symlinks if configured as such
//
// Windows: this test does not work because creating symlinks
// requires priviledged permissions (admin).
if (!isWindows) {
await copy(symLink, copyTarget, { preserveSymlinks: true });
assert.ok(fs.existsSync(copyTarget));
const { symbolicLink } = await SymlinkSupport.stat(copyTarget);
assert.ok(symbolicLink);
assert.ok(!symbolicLink.dangling);
const target = await fs.promises.readlink(copyTarget);
assert.strictEqual(target, symbolicLinkTarget);
// Copy does not preserve symlinks if configured as such
await rimraf(copyTarget);
await copy(symLink, copyTarget, { preserveSymlinks: false });
assert.ok(fs.existsSync(copyTarget));
const { symbolicLink: symbolicLink2 } = await SymlinkSupport.stat(copyTarget);
assert.ok(!symbolicLink2);
}
// Copy ignores dangling symlinks
await rimraf(copyTarget);
await rimraf(symbolicLinkTarget);
await copy(symbolicLink, copyTarget); // this should not throw
await copy(symLink, copyTarget, { preserveSymlinks: true }); // this should not throw
assert.ok(!fs.existsSync(copyTarget));
});
test('mkdirp', async () => {
const newDir = join(testDir, generateUuid());
test('copy handles symbolic links when the reference is inside source', async () => {
await mkdirp(newDir, 493);
// Source Folder
const sourceFolder = join(testDir, generateUuid(), 'copy-test'); // copy-test
const sourceLinkTestFolder = join(sourceFolder, 'link-test'); // copy-test/link-test
const sourceLinkMD5JSFolder = join(sourceLinkTestFolder, 'md5'); // copy-test/link-test/md5
const sourceLinkMD5JSFile = join(sourceLinkMD5JSFolder, 'md5.js'); // copy-test/link-test/md5/md5.js
await fs.promises.mkdir(sourceLinkMD5JSFolder, { recursive: true });
await writeFile(sourceLinkMD5JSFile, 'Hello from MD5');
assert.ok(fs.existsSync(newDir));
const sourceLinkMD5JSFolderLinked = join(sourceLinkTestFolder, 'md5-linked'); // copy-test/link-test/md5-linked
fs.symlinkSync(sourceLinkMD5JSFolder, sourceLinkMD5JSFolderLinked, 'junction');
// Target Folder
const targetLinkTestFolder = join(sourceFolder, 'link-test copy'); // copy-test/link-test copy
const targetLinkMD5JSFolder = join(targetLinkTestFolder, 'md5'); // copy-test/link-test copy/md5
const targetLinkMD5JSFile = join(targetLinkMD5JSFolder, 'md5.js'); // copy-test/link-test copy/md5/md5.js
const targetLinkMD5JSFolderLinked = join(targetLinkTestFolder, 'md5-linked'); // copy-test/link-test copy/md5-linked
// Copy with `preserveSymlinks: true` and verify result
//
// Windows: this test does not work because creating symlinks
// requires priviledged permissions (admin).
if (!isWindows) {
await copy(sourceLinkTestFolder, targetLinkTestFolder, { preserveSymlinks: true });
assert.ok(fs.existsSync(targetLinkTestFolder));
assert.ok(fs.existsSync(targetLinkMD5JSFolder));
assert.ok(fs.existsSync(targetLinkMD5JSFile));
assert.ok(fs.existsSync(targetLinkMD5JSFolderLinked));
assert.ok(fs.lstatSync(targetLinkMD5JSFolderLinked).isSymbolicLink());
const linkTarget = await fs.promises.readlink(targetLinkMD5JSFolderLinked);
assert.strictEqual(linkTarget, targetLinkMD5JSFolder);
await fs.promises.rmdir(targetLinkTestFolder, { recursive: true });
}
// Copy with `preserveSymlinks: false` and verify result
await copy(sourceLinkTestFolder, targetLinkTestFolder, { preserveSymlinks: false });
assert.ok(fs.existsSync(targetLinkTestFolder));
assert.ok(fs.existsSync(targetLinkMD5JSFolder));
assert.ok(fs.existsSync(targetLinkMD5JSFile));
assert.ok(fs.existsSync(targetLinkMD5JSFolderLinked));
assert.ok(fs.lstatSync(targetLinkMD5JSFolderLinked).isDirectory());
});
test('readDirsInDir', async () => {
@ -244,14 +311,14 @@ flakySuite('PFS', function () {
const id2 = generateUuid();
const symbolicLink = join(testDir, id2);
await mkdirp(directory, 493);
await fs.promises.mkdir(directory, { recursive: true });
fs.symlinkSync(directory, symbolicLink, 'junction');
let statAndIsLink = await statLink(directory);
let statAndIsLink = await SymlinkSupport.stat(directory);
assert.ok(!statAndIsLink?.symbolicLink);
statAndIsLink = await statLink(symbolicLink);
statAndIsLink = await SymlinkSupport.stat(symbolicLink);
assert.ok(statAndIsLink?.symbolicLink);
assert.ok(!statAndIsLink?.symbolicLink?.dangling);
});
@ -263,13 +330,13 @@ flakySuite('PFS', function () {
const id2 = generateUuid();
const symbolicLink = join(testDir, id2);
await mkdirp(directory, 493);
await fs.promises.mkdir(directory, { recursive: true });
fs.symlinkSync(directory, symbolicLink, 'junction');
await rimraf(directory);
const statAndIsLink = await statLink(symbolicLink);
const statAndIsLink = await SymlinkSupport.stat(symbolicLink);
assert.ok(statAndIsLink?.symbolicLink);
assert.ok(statAndIsLink?.symbolicLink?.dangling);
});
@ -279,7 +346,7 @@ flakySuite('PFS', function () {
const id = generateUuid();
const newDir = join(testDir, 'pfs', id, 'öäü');
await mkdirp(newDir, 493);
await fs.promises.mkdir(newDir, { recursive: true });
assert.ok(fs.existsSync(newDir));
@ -288,16 +355,16 @@ flakySuite('PFS', function () {
}
});
test('readdirWithFileTypes', async () => {
test('readdir (with file types)', async () => {
if (canNormalize && typeof process.versions['electron'] !== 'undefined' /* needs electron */) {
const newDir = join(testDir, 'öäü');
await mkdirp(newDir, 493);
await fs.promises.mkdir(newDir, { recursive: true });
await writeFile(join(testDir, 'somefile.txt'), 'contents');
assert.ok(fs.existsSync(newDir));
const children = await readdirWithFileTypes(testDir);
const children = await readdir(testDir, { withFileTypes: true });
assert.strictEqual(children.some(n => n.name === 'öäü'), true); // Mac always converts to NFD, so
assert.strictEqual(children.some(n => n.isDirectory()), true);

View file

@ -6,8 +6,9 @@
import * as assert from 'assert';
import * as path from 'vs/base/common/path';
import { tmpdir } from 'os';
import { promises } from 'fs';
import { extract } from 'vs/base/node/zip';
import { rimraf, exists, mkdirp } from 'vs/base/node/pfs';
import { rimraf, exists } from 'vs/base/node/pfs';
import { getPathFromAmdModule } from 'vs/base/common/amd';
import { createCancelablePromise } from 'vs/base/common/async';
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
@ -19,7 +20,7 @@ suite('Zip', () => {
setup(() => {
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'zip');
return mkdirp(testDir);
return promises.mkdir(testDir, { recursive: true });
});
teardown(() => {

View file

@ -5,10 +5,14 @@
(function () {
let MonacoEnvironment = (<any>self).MonacoEnvironment;
let monacoBaseUrl = MonacoEnvironment && MonacoEnvironment.baseUrl ? MonacoEnvironment.baseUrl : '../../../';
const MonacoEnvironment = (<any>self).MonacoEnvironment;
const monacoBaseUrl = MonacoEnvironment && MonacoEnvironment.baseUrl ? MonacoEnvironment.baseUrl : '../../../';
const trustedTypesPolicy = self.trustedTypes?.createPolicy('amdLoader', { createScriptURL: value => value });
const trustedTypesPolicy = (
typeof self.trustedTypes?.createPolicy === 'function'
? self.trustedTypes?.createPolicy('amdLoader', { createScriptURL: value => value })
: undefined
);
if (typeof (<any>self).define !== 'function' || !(<any>self).define.amd) {
let loaderSrc: string | TrustedScriptURL = monacoBaseUrl + 'vs/loader.js';

View file

@ -1,13 +0,0 @@
{
"name": "vs",
"dependencies": [
],
"libs": [
"lib.core.d.ts"
],
"sources": [
],
"declares": [
"vs/nls.d.ts"
]
}

View file

@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as path from 'vs/base/common/path';
import * as pfs from 'vs/base/node/pfs';
import { IStringDictionary } from 'vs/base/common/collections';
@ -52,7 +53,7 @@ export class LanguagePackCachedDataCleaner extends Disposable {
: 1000 * 60 * 60 * 24 * 30 * 3; // roughly 3 months
try {
const installed: IStringDictionary<boolean> = Object.create(null);
const metaData: LanguagePackFile = JSON.parse(await pfs.readFile(path.join(this._environmentService.userDataPath, 'languagepacks.json'), 'utf8'));
const metaData: LanguagePackFile = JSON.parse(await fs.promises.readFile(path.join(this._environmentService.userDataPath, 'languagepacks.json'), 'utf8'));
for (let locale of Object.keys(metaData)) {
const entry = metaData[locale];
installed[`${entry.hash}.${locale}`] = true;
@ -80,7 +81,7 @@ export class LanguagePackCachedDataCleaner extends Disposable {
continue;
}
const candidate = path.join(folder, entry);
const stat = await pfs.stat(candidate);
const stat = await fs.promises.stat(candidate);
if (stat.isDirectory()) {
const diff = now - stat.mtime.getTime();
if (diff > maxAge) {

View file

@ -3,10 +3,11 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { promises } from 'fs';
import { basename, dirname, join } from 'vs/base/common/path';
import { onUnexpectedError } from 'vs/base/common/errors';
import { toDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { readdir, rimraf, stat } from 'vs/base/node/pfs';
import { readdir, rimraf } from 'vs/base/node/pfs';
import product from 'vs/platform/product/common/product';
export class NodeCachedDataCleaner {
@ -54,7 +55,7 @@ export class NodeCachedDataCleaner {
if (entry !== nodeCachedDataCurrent) {
const path = join(nodeCachedDataRootDir, entry);
deletes.push(stat(path).then(stats => {
deletes.push(promises.stat(path).then(stats => {
// stat check
// * only directories
// * only when old enough

View file

@ -3,9 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { promises } from 'fs';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { join } from 'vs/base/common/path';
import { readdir, readFile, rimraf } from 'vs/base/node/pfs';
import { readdir, rimraf } from 'vs/base/node/pfs';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { IBackupWorkspacesFormat } from 'vs/platform/backup/node/backup';
@ -32,7 +33,7 @@ export class StorageDataCleaner extends Disposable {
try {
// Leverage the backup workspace file to find out which empty workspace is currently in use to
// determine which empty workspace storage can safely be deleted
const contents = await readFile(this.backupWorkspacesPath, 'utf8');
const contents = await promises.readFile(this.backupWorkspacesPath, 'utf8');
const workspaces = JSON.parse(contents) as IBackupWorkspacesFormat;
const emptyWorkspaces = workspaces.emptyWorkspaceInfos.map(info => info.backupFolder);

View file

@ -29,7 +29,7 @@ import { TelemetryAppenderChannel } from 'vs/platform/telemetry/common/telemetry
import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService';
import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender';
import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals';
import { ILogService, ILoggerService, MultiplexLogService, ConsoleLogService } from 'vs/platform/log/common/log';
import { ILogService, ILoggerService, MultiplexLogService, ConsoleLogger } from 'vs/platform/log/common/log';
import { LoggerChannelClient, FollowerLogService } from 'vs/platform/log/common/logIpc';
import { LocalizationsService } from 'vs/platform/localizations/node/localizations';
import { ILocalizationsService } from 'vs/platform/localizations/common/localizations';
@ -41,7 +41,7 @@ import { LanguagePackCachedDataCleaner } from 'vs/code/electron-browser/sharedPr
import { StorageDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner';
import { LogsDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner';
import { IMainProcessService, MessagePortMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
import { SpdLogService } from 'vs/platform/log/node/spdlogService';
import { SpdLogLogger } from 'vs/platform/log/node/spdlogLog';
import { DiagnosticsService, IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService';
import { FileService } from 'vs/platform/files/common/fileService';
import { IFileService } from 'vs/platform/files/common/files';
@ -144,8 +144,8 @@ class SharedProcessMain extends Disposable {
const mainRouter = new StaticRouter(ctx => ctx === 'main');
const loggerClient = new LoggerChannelClient(this.server.getChannel('logger', mainRouter)); // we only use this for log levels
const multiplexLogger = this._register(new MultiplexLogService([
this._register(new ConsoleLogService(this.configuration.logLevel)),
this._register(new SpdLogService('sharedprocess', environmentService.logsPath, this.configuration.logLevel))
this._register(new ConsoleLogger(this.configuration.logLevel)),
this._register(new SpdLogLogger('sharedprocess', environmentService.logsPath, this.configuration.logLevel))
]));
const logService = this._register(new FollowerLogService(loggerClient, multiplexLogger));

View file

@ -5,13 +5,12 @@
import 'vs/platform/update/common/update.config.contribution';
import { app, dialog } from 'electron';
import { unlinkSync } from 'fs';
import { promises, unlinkSync } from 'fs';
import { localize } from 'vs/nls';
import { isWindows, IProcessEnvironment, isMacintosh } from 'vs/base/common/platform';
import product from 'vs/platform/product/common/product';
import { parseMainProcessArgv, addArg } from 'vs/platform/environment/node/argvHelper';
import { createWaitMarkerFile } from 'vs/platform/environment/node/waitMarkerFile';
import { mkdirp } from 'vs/base/node/pfs';
import { LifecycleMainService, ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
import { Server as NodeIPCServer, serve as nodeIPCServe, connect as nodeIPCConnect, XDG_RUNTIME_DIR } from 'vs/base/parts/ipc/node/ipc.net';
@ -21,7 +20,7 @@ import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiati
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { ILogService, ConsoleLogMainService, MultiplexLogService, getLogLevel } from 'vs/platform/log/common/log';
import { ILogService, ConsoleMainLogger, MultiplexLogService, getLogLevel } from 'vs/platform/log/common/log';
import { StateService } from 'vs/platform/state/node/stateService';
import { IStateService } from 'vs/platform/state/node/state';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
@ -32,7 +31,7 @@ import { IRequestService } from 'vs/platform/request/common/request';
import { RequestMainService } from 'vs/platform/request/electron-main/requestMainService';
import { CodeApplication } from 'vs/code/electron-main/app';
import { getPathLabel, mnemonicButtonLabel } from 'vs/base/common/labels';
import { SpdLogService } from 'vs/platform/log/node/spdlogService';
import { SpdLogLogger } from 'vs/platform/log/node/spdlogLog';
import { BufferLogService } from 'vs/platform/log/common/bufferLog';
import { setUnexpectedErrorHandler } from 'vs/base/common/errors';
import { IThemeMainService, ThemeMainService } from 'vs/platform/theme/electron-main/themeMainService';
@ -134,7 +133,7 @@ class CodeMain {
const mainIpcServer = await this.doStartup(args, logService, environmentService, lifecycleMainService, instantiationService, true);
bufferLogService.logger = new SpdLogService('main', environmentService.logsPath, bufferLogService.getLevel());
bufferLogService.logger = new SpdLogLogger('main', environmentService.logsPath, bufferLogService.getLevel());
once(lifecycleMainService.onWillShutdown)(() => {
fileService.dispose();
(configurationService as ConfigurationService).dispose();
@ -155,7 +154,7 @@ class CodeMain {
services.set(IEnvironmentService, environmentService);
services.set(IEnvironmentMainService, environmentService);
const logService = new MultiplexLogService([new ConsoleLogMainService(getLogLevel(environmentService)), bufferLogService]);
const logService = new MultiplexLogService([new ConsoleMainLogger(getLogLevel(environmentService)), bufferLogService]);
process.once('exit', () => logService.dispose());
services.set(ILogService, logService);
@ -186,7 +185,7 @@ class CodeMain {
environmentService.globalStorageHome.fsPath,
environmentService.workspaceStorageHome.fsPath,
environmentService.backupHome
].map((path): undefined | Promise<void> => path ? mkdirp(path) : undefined));
].map((path): undefined | Promise<void> => path ? promises.mkdir(path, { recursive: true }) : undefined));
// Configuration service
const configurationServiceInitialization = configurationService.initialize();

View file

@ -1192,7 +1192,7 @@ export class CodeWindow extends Disposable implements ICodeWindow {
private getMenuBarVisibility(): MenuBarVisibility {
let menuBarVisibility = getMenuBarVisibility(this.configurationService);
if (['visible', 'toggle', 'hidden'].indexOf(menuBarVisibility) < 0) {
menuBarVisibility = 'default';
menuBarVisibility = 'classic';
}
return menuBarVisibility;
@ -1227,7 +1227,7 @@ export class CodeWindow extends Disposable implements ICodeWindow {
const isFullscreen = this.isFullScreen;
switch (visibility) {
case ('default'):
case ('classic'):
this._win.setMenuBarVisibility(!isFullscreen);
this._win.autoHideMenuBar = isFullscreen;
break;

View file

@ -28,12 +28,11 @@ import { RequestService } from 'vs/platform/request/node/requestService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ConfigurationService } from 'vs/platform/configuration/common/configurationService';
import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender';
import { mkdirp, writeFile } from 'vs/base/node/pfs';
import { IStateService } from 'vs/platform/state/node/state';
import { StateService } from 'vs/platform/state/node/stateService';
import { ILogService, getLogLevel, LogLevel, ConsoleLogService, MultiplexLogService } from 'vs/platform/log/common/log';
import { ILogService, getLogLevel, LogLevel, ConsoleLogger, MultiplexLogService, ILogger } from 'vs/platform/log/common/log';
import { Schemas } from 'vs/base/common/network';
import { SpdLogService } from 'vs/platform/log/node/spdlogService';
import { SpdLogLogger } from 'vs/platform/log/node/spdlogLog';
import { buildTelemetryMessage } from 'vs/platform/telemetry/node/telemetry';
import { FileService } from 'vs/platform/files/common/fileService';
import { IFileService } from 'vs/platform/files/common/files';
@ -46,6 +45,7 @@ import { LocalizationsService } from 'vs/platform/localizations/node/localizatio
import { ILocalizationsService } from 'vs/platform/localizations/common/localizations';
import { setUnexpectedErrorHandler } from 'vs/base/common/errors';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { VSBuffer } from 'vs/base/common/buffer';
class CliMain extends Disposable {
@ -73,6 +73,7 @@ class CliMain extends Disposable {
return instantiationService.invokeFunction(async accessor => {
const logService = accessor.get(ILogService);
const fileService = accessor.get(IFileService);
const environmentService = accessor.get(INativeEnvironmentService);
const extensionManagementCLIService = accessor.get(IExtensionManagementCLIService);
@ -83,7 +84,7 @@ class CliMain extends Disposable {
this.registerErrorHandler(logService);
// Run based on argv
await this.doRun(environmentService, extensionManagementCLIService);
await this.doRun(environmentService, extensionManagementCLIService, fileService);
// Flush the remaining data in AI adapter (with 1s timeout)
return raceTimeout(combinedAppender(...appenders).flush(), 1000);
@ -99,14 +100,14 @@ class CliMain extends Disposable {
services.set(INativeEnvironmentService, environmentService);
// Init folders
await Promise.all([environmentService.appSettingsHome.fsPath, environmentService.extensionsPath].map(path => path ? mkdirp(path) : undefined));
await Promise.all([environmentService.appSettingsHome.fsPath, environmentService.extensionsPath].map(path => path ? fs.promises.mkdir(path, { recursive: true }) : undefined));
// Log
const logLevel = getLogLevel(environmentService);
const loggers: ILogService[] = [];
loggers.push(new SpdLogService('cli', environmentService.logsPath, logLevel));
const loggers: ILogger[] = [];
loggers.push(new SpdLogLogger('cli', environmentService.logsPath, logLevel));
if (logLevel === LogLevel.Trace) {
loggers.push(new ConsoleLogService(logLevel));
loggers.push(new ConsoleLogger(logLevel));
}
const logService = this._register(new MultiplexLogService(loggers));
@ -182,11 +183,11 @@ class CliMain extends Disposable {
});
}
private async doRun(environmentService: INativeEnvironmentService, extensionManagementCLIService: IExtensionManagementCLIService): Promise<void> {
private async doRun(environmentService: INativeEnvironmentService, extensionManagementCLIService: IExtensionManagementCLIService, fileService: IFileService): Promise<void> {
// Install Source
if (this.argv['install-source']) {
return this.setInstallSource(environmentService, this.argv['install-source']);
return this.setInstallSource(environmentService, fileService, this.argv['install-source']);
}
// List Extensions
@ -219,8 +220,8 @@ class CliMain extends Disposable {
return inputs.map(input => /\.vsix$/i.test(input) ? URI.file(isAbsolute(input) ? input : join(process.cwd(), input)) : input);
}
private setInstallSource(environmentService: INativeEnvironmentService, installSource: string): Promise<void> {
return writeFile(environmentService.installSourcePath, installSource.slice(0, 30));
private async setInstallSource(environmentService: INativeEnvironmentService, fileService: IFileService, installSource: string): Promise<void> {
await fileService.writeFile(URI.file(environmentService.installSourcePath), VSBuffer.fromString(installSource.slice(0, 30)));
}
}

View file

@ -1931,6 +1931,7 @@ registerOverwritableCommand(Handler.Type, {
}]
});
registerOverwritableCommand(Handler.ReplacePreviousChar);
registerOverwritableCommand(Handler.CompositionType);
registerOverwritableCommand(Handler.CompositionStart);
registerOverwritableCommand(Handler.CompositionEnd);
registerOverwritableCommand(Handler.Paste);

View file

@ -42,6 +42,7 @@ export interface IPointerHandlerHelper {
linesContentDomNode: HTMLElement;
focusTextArea(): void;
dispatchTextAreaEvent(event: CustomEvent): void;
/**
* Get the last rendered information for cursors & textarea.

View file

@ -13,6 +13,7 @@ import { EditorMouseEvent, EditorPointerEventFactory } from 'vs/editor/browser/e
import { ViewController } from 'vs/editor/browser/view/viewController';
import { ViewContext } from 'vs/editor/common/view/viewContext';
import { BrowserFeatures } from 'vs/base/browser/canIUse';
import { TextAreaSyntethicEvents } from 'vs/editor/browser/controller/textAreaInput';
interface IThrottledGestureEvent {
translationX: number;
@ -210,6 +211,11 @@ class TouchHandler extends MouseHandler {
const target = this._createMouseTarget(new EditorMouseEvent(event, this.viewHelper.viewDomNode), false);
if (target.position) {
// Send the tap event also to the <textarea> (for input purposes)
const event = document.createEvent('CustomEvent');
event.initEvent(TextAreaSyntethicEvents.Tap, false, true);
this.viewHelper.dispatchTextAreaEvent(event);
this.viewController.moveTo(target.position);
}
}

View file

@ -12,7 +12,7 @@ import * as platform from 'vs/base/common/platform';
import * as strings from 'vs/base/common/strings';
import { Configuration } from 'vs/editor/browser/config/configuration';
import { CopyOptions, ICompositionData, IPasteData, ITextAreaInputHost, TextAreaInput, ClipboardDataToCopy } from 'vs/editor/browser/controller/textAreaInput';
import { ISimpleModel, ITypeData, PagedScreenReaderStrategy, TextAreaState } from 'vs/editor/browser/controller/textAreaState';
import { ISimpleModel, ITypeData, PagedScreenReaderStrategy, TextAreaState, _debugComposition } from 'vs/editor/browser/controller/textAreaState';
import { ViewController } from 'vs/editor/browser/view/viewController';
import { PartFingerprint, PartFingerprints, ViewPart } from 'vs/editor/browser/view/viewPart';
import { LineNumbersOverlay } from 'vs/editor/browser/viewParts/lineNumbers/lineNumbers';
@ -202,6 +202,22 @@ export class TextAreaHandler extends ViewPart {
return TextAreaState.EMPTY;
}
if (browser.isAndroid) {
// when tapping in the editor on a word, Android enters composition mode.
// in the `compositionstart` event we cannot clear the textarea, because
// it then forgets to ever send a `compositionend`.
// we therefore only write the current word in the textarea
const selection = this._selections[0];
if (selection.isEmpty()) {
const position = selection.getStartPosition();
const [wordAtPosition, positionOffsetInWord] = this._getAndroidWordAtPosition(position);
if (wordAtPosition.length > 0) {
return new TextAreaState(wordAtPosition, positionOffsetInWord, positionOffsetInWord, position, position);
}
}
return TextAreaState.EMPTY;
}
return PagedScreenReaderStrategy.fromEditorSelection(currentState, simpleModel, this._selections[0], this._accessibilityPageSize, this._accessibilitySupport === AccessibilitySupport.Unknown);
},
@ -237,9 +253,16 @@ export class TextAreaHandler extends ViewPart {
}));
this._register(this._textAreaInput.onType((e: ITypeData) => {
if (e.replaceCharCnt) {
this._viewController.replacePreviousChar(e.text, e.replaceCharCnt);
if (e.replacePrevCharCnt || e.replaceNextCharCnt || e.positionDelta) {
// must be handled through the new command
if (_debugComposition) {
console.log(` => compositionType: <<${e.text}>>, ${e.replacePrevCharCnt}, ${e.replaceNextCharCnt}, ${e.positionDelta}`);
}
this._viewController.compositionType(e.text, e.replacePrevCharCnt, e.replaceNextCharCnt, e.positionDelta);
} else {
if (_debugComposition) {
console.log(` => type: <<${e.text}>>`);
}
this._viewController.type(e.text);
}
}));
@ -250,7 +273,7 @@ export class TextAreaHandler extends ViewPart {
this._register(this._textAreaInput.onCompositionStart((e) => {
const lineNumber = this._selections[0].startLineNumber;
const column = this._selections[0].startColumn - (e.moveOneCharacterLeft ? 1 : 0);
const column = this._selections[0].startColumn + e.revealDeltaColumns;
this._context.model.revealRange(
'keyboard',
@ -280,8 +303,11 @@ export class TextAreaHandler extends ViewPart {
}));
this._register(this._textAreaInput.onCompositionUpdate((e: ICompositionData) => {
if (!this._visibleTextArea) {
return;
}
// adjust width by its size
this._visibleTextArea = this._visibleTextArea!.setWidth(measureText(e.data, this._fontInfo));
this._visibleTextArea = this._visibleTextArea.setWidth(measureText(e.data, this._fontInfo));
this._render();
}));
@ -308,6 +334,47 @@ export class TextAreaHandler extends ViewPart {
super.dispose();
}
private _getAndroidWordAtPosition(position: Position): [string, number] {
const ANDROID_WORD_SEPARATORS = '`~!@#$%^&*()-=+[{]}\\|;:",.<>/?';
const lineContent = this._context.model.getLineContent(position.lineNumber);
const wordSeparators = getMapForWordSeparators(ANDROID_WORD_SEPARATORS);
let goingLeft = true;
let startColumn = position.column;
let goingRight = true;
let endColumn = position.column;
let distance = 0;
while (distance < 50 && (goingLeft || goingRight)) {
if (goingLeft && startColumn <= 1) {
goingLeft = false;
}
if (goingLeft) {
const charCode = lineContent.charCodeAt(startColumn - 2);
const charClass = wordSeparators.get(charCode);
if (charClass !== WordCharacterClass.Regular) {
goingLeft = false;
} else {
startColumn--;
}
}
if (goingRight && endColumn > lineContent.length) {
goingRight = false;
}
if (goingRight) {
const charCode = lineContent.charCodeAt(endColumn - 1);
const charClass = wordSeparators.get(charCode);
if (charClass !== WordCharacterClass.Regular) {
goingRight = false;
} else {
endColumn++;
}
}
distance++;
}
return [lineContent.substring(startColumn - 1, endColumn - 1), position.column - startColumn];
}
private _getWordBeforePosition(position: Position): string {
const lineContent = this._context.model.getLineContent(position.lineNumber);
const wordSeparators = getMapForWordSeparators(this._context.configuration.options.get(EditorOption.wordSeparators));

View file

@ -18,6 +18,10 @@ import { Position } from 'vs/editor/common/core/position';
import { Selection } from 'vs/editor/common/core/selection';
import { BrowserFeatures } from 'vs/base/browser/canIUse';
export namespace TextAreaSyntethicEvents {
export const Tap = '-monaco-textarea-synthetic-tap';
}
export interface ICompositionData {
data: string;
}
@ -96,7 +100,7 @@ export class InMemoryClipboardMetadataManager {
}
export interface ICompositionStartEvent {
moveOneCharacterLeft: boolean;
revealDeltaColumns: number;
}
/**
@ -204,7 +208,6 @@ export class TextAreaInput extends Disposable {
}
this._isDoingComposition = true;
let moveOneCharacterLeft = false;
if (
platform.isMacintosh
&& lastKeyDown
@ -212,17 +215,12 @@ export class TextAreaInput extends Disposable {
&& this._textAreaState.selectionStart === this._textAreaState.selectionEnd
&& this._textAreaState.selectionStart > 0
&& this._textAreaState.value.substr(this._textAreaState.selectionStart - 1, 1) === e.data
&& (lastKeyDown.code === 'ArrowRight' || lastKeyDown.code === 'ArrowLeft')
) {
// Handling long press case on macOS + arrow key => pretend the character was selected
if (lastKeyDown.code === 'ArrowRight' || lastKeyDown.code === 'ArrowLeft') {
if (_debugComposition) {
console.log(`[compositionstart] Handling long press case on macOS + arrow key`, e);
}
moveOneCharacterLeft = true;
if (_debugComposition) {
console.log(`[compositionstart] Handling long press case on macOS + arrow key`, e);
}
}
if (moveOneCharacterLeft) {
this._textAreaState = new TextAreaState(
this._textAreaState.value,
this._textAreaState.selectionStart - 1,
@ -230,11 +228,19 @@ export class TextAreaInput extends Disposable {
this._textAreaState.selectionStartPosition ? new Position(this._textAreaState.selectionStartPosition.lineNumber, this._textAreaState.selectionStartPosition.column - 1) : null,
this._textAreaState.selectionEndPosition
);
} else {
this._setAndWriteTextAreaState('compositionstart', TextAreaState.EMPTY);
this._onCompositionStart.fire({ revealDeltaColumns: -1 });
return;
}
this._onCompositionStart.fire({ moveOneCharacterLeft });
if (browser.isAndroid) {
// when tapping on the editor, Android enters composition mode to edit the current word
// so we cannot clear the textarea on Android and we must pretend the current word was selected
this._onCompositionStart.fire({ revealDeltaColumns: -this._textAreaState.selectionStart });
return;
}
this._setAndWriteTextAreaState('compositionstart', TextAreaState.EMPTY);
this._onCompositionStart.fire({ revealDeltaColumns: 0 });
}));
/**
@ -246,6 +252,12 @@ export class TextAreaInput extends Disposable {
return [newState, TextAreaState.deduceInput(oldState, newState, couldBeEmojiInput)];
};
const deduceAndroidCompositionInput = (): [TextAreaState, ITypeData] => {
const oldState = this._textAreaState;
const newState = TextAreaState.readFromTextArea(this._textArea);
return [newState, TextAreaState.deduceAndroidCompositionInput(oldState, newState)];
};
/**
* Deduce the composition input from a string.
*/
@ -254,7 +266,9 @@ export class TextAreaInput extends Disposable {
const newState = TextAreaState.selectedText(text);
const typeInput: ITypeData = {
text: newState.value,
replaceCharCnt: oldState.selectionEnd - oldState.selectionStart
replacePrevCharCnt: oldState.selectionEnd - oldState.selectionStart,
replaceNextCharCnt: 0,
positionDelta: 0
};
return [newState, typeInput];
};
@ -263,6 +277,17 @@ export class TextAreaInput extends Disposable {
if (_debugComposition) {
console.log(`[compositionupdate]`, e);
}
if (browser.isAndroid) {
// On Android, the data sent with the composition update event is unusable.
// For example, if the cursor is in the middle of a word like Mic|osoft
// and Microsoft is chosen from the keyboard's suggestions, the e.data will contain "Microsoft".
// This is not really usable because it doesn't tell us where the edit began and where it ended.
const [newState, typeInput] = deduceAndroidCompositionInput();
this._textAreaState = newState;
this._onType.fire(typeInput);
this._onCompositionUpdate.fire(e);
return;
}
const [newState, typeInput] = deduceComposition(e.data || '');
this._textAreaState = newState;
this._onType.fire(typeInput);
@ -278,6 +303,19 @@ export class TextAreaInput extends Disposable {
if (!this._isDoingComposition) {
return;
}
this._isDoingComposition = false;
if (browser.isAndroid) {
// On Android, the data sent with the composition update event is unusable.
// For example, if the cursor is in the middle of a word like Mic|osoft
// and Microsoft is chosen from the keyboard's suggestions, the e.data will contain "Microsoft".
// This is not really usable because it doesn't tell us where the edit began and where it ended.
const [newState, typeInput] = deduceAndroidCompositionInput();
this._textAreaState = newState;
this._onType.fire(typeInput);
this._onCompositionEnd.fire();
return;
}
const [newState, typeInput] = deduceComposition(e.data || '');
this._textAreaState = newState;
@ -290,11 +328,6 @@ export class TextAreaInput extends Disposable {
this._textAreaState = TextAreaState.readFromTextArea(this._textArea);
}
if (!this._isDoingComposition) {
return;
}
this._isDoingComposition = false;
this._onCompositionEnd.fire();
}));
@ -308,18 +341,18 @@ export class TextAreaInput extends Disposable {
}
const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/platform.isMacintosh);
if (typeInput.replaceCharCnt === 0 && typeInput.text.length === 1 && strings.isHighSurrogate(typeInput.text.charCodeAt(0))) {
if (typeInput.replacePrevCharCnt === 0 && typeInput.text.length === 1 && strings.isHighSurrogate(typeInput.text.charCodeAt(0))) {
// Ignore invalid input but keep it around for next time
return;
}
this._textAreaState = newState;
if (this._nextCommand === ReadFromTextArea.Type) {
if (typeInput.text !== '') {
if (typeInput.text !== '' || typeInput.replacePrevCharCnt !== 0) {
this._onType.fire(typeInput);
}
} else {
if (typeInput.text !== '' || typeInput.replaceCharCnt !== 0) {
if (typeInput.text !== '' || typeInput.replacePrevCharCnt !== 0) {
this._firePaste(typeInput.text, null);
}
this._nextCommand = ReadFromTextArea.Type;
@ -388,6 +421,21 @@ export class TextAreaInput extends Disposable {
}
this._setHasFocus(false);
}));
this._register(dom.addDisposableListener(textArea.domNode, TextAreaSyntethicEvents.Tap, () => {
if (browser.isAndroid && this._isDoingComposition) {
// on Android, tapping does not cancel the current composition, so the
// textarea is stuck showing the old composition
// Clear the flag to be able to write to the textarea
this._isDoingComposition = false;
// Clear the textarea to avoid an unwanted cursor type
this.writeScreenReaderContent('tapWithoutCompositionEnd');
// Fire artificial composition end
this._onCompositionEnd.fire();
}
}));
}
private _installSelectionChangeListener(): IDisposable {

View file

@ -27,7 +27,9 @@ export interface ISimpleModel {
export interface ITypeData {
text: string;
replaceCharCnt: number;
replacePrevCharCnt: number;
replaceNextCharCnt: number;
positionDelta: number;
}
export class TextAreaState {
@ -105,7 +107,9 @@ export class TextAreaState {
// This is the EMPTY state
return {
text: '',
replaceCharCnt: 0
replacePrevCharCnt: 0,
replaceNextCharCnt: 0,
positionDelta: 0
};
}
@ -178,7 +182,9 @@ export class TextAreaState {
if (/\uFE0F/.test(potentialEmojiInput) || strings.containsEmoji(potentialEmojiInput)) {
return {
text: potentialEmojiInput,
replaceCharCnt: 0
replacePrevCharCnt: 0,
replaceNextCharCnt: 0,
positionDelta: 0
};
}
}
@ -197,7 +203,9 @@ export class TextAreaState {
if (strings.containsFullWidthCharacter(currentValue)) {
return {
text: '',
replaceCharCnt: 0
replacePrevCharCnt: 0,
replaceNextCharCnt: 0,
positionDelta: 0
};
}
}
@ -210,7 +218,9 @@ export class TextAreaState {
return {
text: currentValue,
replaceCharCnt: replacePreviousCharacters
replacePrevCharCnt: replacePreviousCharacters,
replaceNextCharCnt: 0,
positionDelta: 0
};
}
@ -218,7 +228,57 @@ export class TextAreaState {
const replacePreviousCharacters = previousSelectionEnd - previousSelectionStart;
return {
text: currentValue,
replaceCharCnt: replacePreviousCharacters
replacePrevCharCnt: replacePreviousCharacters,
replaceNextCharCnt: 0,
positionDelta: 0
};
}
public static deduceAndroidCompositionInput(previousState: TextAreaState, currentState: TextAreaState): ITypeData {
if (!previousState) {
// This is the EMPTY state
return {
text: '',
replacePrevCharCnt: 0,
replaceNextCharCnt: 0,
positionDelta: 0
};
}
if (_debugComposition) {
console.log('------------------------deduceAndroidCompositionInput');
console.log('PREVIOUS STATE: ' + previousState.toString());
console.log('CURRENT STATE: ' + currentState.toString());
}
if (previousState.value === currentState.value) {
return {
text: '',
replacePrevCharCnt: 0,
replaceNextCharCnt: 0,
positionDelta: currentState.selectionEnd - previousState.selectionEnd
};
}
const prefixLength = Math.min(strings.commonPrefixLength(previousState.value, currentState.value), previousState.selectionEnd);
const suffixLength = Math.min(strings.commonSuffixLength(previousState.value, currentState.value), previousState.value.length - previousState.selectionEnd);
const previousValue = previousState.value.substring(prefixLength, previousState.value.length - suffixLength);
const currentValue = currentState.value.substring(prefixLength, currentState.value.length - suffixLength);
const previousSelectionStart = previousState.selectionStart - prefixLength;
const previousSelectionEnd = previousState.selectionEnd - prefixLength;
const currentSelectionStart = currentState.selectionStart - prefixLength;
const currentSelectionEnd = currentState.selectionEnd - prefixLength;
if (_debugComposition) {
console.log('AFTER DIFFING PREVIOUS STATE: <' + previousValue + '>, selectionStart: ' + previousSelectionStart + ', selectionEnd: ' + previousSelectionEnd);
console.log('AFTER DIFFING CURRENT STATE: <' + currentValue + '>, selectionStart: ' + currentSelectionStart + ', selectionEnd: ' + currentSelectionEnd);
}
return {
text: currentValue,
replacePrevCharCnt: previousSelectionEnd,
replaceNextCharCnt: previousValue.length - previousSelectionEnd,
positionDelta: currentSelectionEnd - currentValue.length
};
}
}

View file

@ -15,6 +15,7 @@ import { URI } from 'vs/base/common/uri';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { EditorOpenContext } from 'vs/platform/editor/common/editor';
import { ILogService } from 'vs/platform/log/common/log';
import { IExternalOpener, IExternalUriResolver, IOpener, IOpenerService, IResolvedExternalUri, IValidator, matchesScheme, OpenOptions, ResolveExternalUriOptions } from 'vs/platform/opener/common/opener';
class CommandOpener implements IOpener {
@ -106,6 +107,7 @@ export class OpenerService implements IOpenerService {
constructor(
@ICodeEditorService editorService: ICodeEditorService,
@ICommandService commandService: ICommandService,
@ILogService private logService: ILogService
) {
// Default external opener is going through window.open()
this._defaultExternalOpener = {
@ -166,7 +168,8 @@ export class OpenerService implements IOpenerService {
// check with contributed validators
const targetURI = typeof target === 'string' ? URI.parse(target) : target;
// validate against the original URI that this URI resolves to, if one exists
const validationTarget = this._resolvedUriTargets.get(targetURI) ?? target;
const validationTarget = this._resolvedUriTargets.get(targetURI) ?? targetURI;
this.logService.trace(`OpenerService#open: ${targetURI.authority} validating via ${validationTarget.authority}`);
for (const validator of this._validators) {
if (!(await validator.shouldOpen(validationTarget))) {
return false;
@ -188,7 +191,10 @@ export class OpenerService implements IOpenerService {
for (const resolver of this._resolvers) {
const result = await resolver.resolveExternalUri(resource, options);
if (result) {
this._resolvedUriTargets.set(result.resolved, resource);
if (!this._resolvedUriTargets.has(result.resolved)) {
this.logService.trace(`OpenerService#resolveExternalUri: ${resource.authority} resolved to ${result.resolved.authority}`);
this._resolvedUriTargets.set(result.resolved, resource);
}
return result;
}
}

View file

@ -37,7 +37,7 @@ export interface IMouseDispatchData {
export interface ICommandDelegate {
paste(text: string, pasteOnNewLine: boolean, multicursorText: string[] | null, mode: string | null): void;
type(text: string): void;
replacePreviousChar(text: string, replaceCharCnt: number): void;
compositionType(text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number): void;
startComposition(): void;
endComposition(): void;
cut(): void;
@ -70,8 +70,8 @@ export class ViewController {
this.commandDelegate.type(text);
}
public replacePreviousChar(text: string, replaceCharCnt: number): void {
this.commandDelegate.replacePreviousChar(text, replaceCharCnt);
public compositionType(text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number): void {
this.commandDelegate.compositionType(text, replacePrevCharCnt, replaceNextCharCnt, positionDelta);
}
public compositionStart(): void {

View file

@ -239,6 +239,10 @@ export class View extends ViewEventHandler {
this.focus();
},
dispatchTextAreaEvent: (event: CustomEvent) => {
this._textAreaHandler.textArea.domNode.dispatchEvent(event);
},
getLastRenderData: (): PointerHandlerLastRenderData => {
const lastViewCursorsRenderData = this._viewCursors.getLastRenderData() || [];
const lastTextareaPosition = this._textAreaHandler.getLastRenderData();

View file

@ -1002,7 +1002,12 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
}
case editorCommon.Handler.ReplacePreviousChar: {
const args = <Partial<editorCommon.ReplacePreviousCharPayload>>payload;
this._replacePreviousChar(source, args.text || '', args.replaceCharCnt || 0);
this._compositionType(source, args.text || '', args.replaceCharCnt || 0, 0, 0);
return;
}
case editorCommon.Handler.CompositionType: {
const args = <Partial<editorCommon.CompositionTypePayload>>payload;
this._compositionType(source, args.text || '', args.replacePrevCharCnt || 0, args.replaceNextCharCnt || 0, args.positionDelta || 0);
return;
}
case editorCommon.Handler.Paste: {
@ -1061,11 +1066,11 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
}
}
private _replacePreviousChar(source: string | null | undefined, text: string, replaceCharCnt: number): void {
private _compositionType(source: string | null | undefined, text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number): void {
if (!this._modelData) {
return;
}
this._modelData.viewModel.replacePreviousChar(text, replaceCharCnt, source);
this._modelData.viewModel.compositionType(text, replacePrevCharCnt, replaceNextCharCnt, positionDelta, source);
}
private _paste(source: string | null | undefined, text: string, pasteOnNewLine: boolean, multicursorText: string[] | null, mode: string | null): void {
@ -1583,8 +1588,8 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
type: (text: string) => {
this._type('keyboard', text);
},
replacePreviousChar: (text: string, replaceCharCnt: number) => {
this._replacePreviousChar('keyboard', text, replaceCharCnt);
compositionType: (text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number) => {
this._compositionType('keyboard', text, replacePrevCharCnt, replaceNextCharCnt, positionDelta);
},
startComposition: () => {
this._startComposition();
@ -1606,9 +1611,16 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
const payload: editorCommon.TypePayload = { text };
this._commandService.executeCommand(editorCommon.Handler.Type, payload);
},
replacePreviousChar: (text: string, replaceCharCnt: number) => {
const payload: editorCommon.ReplacePreviousCharPayload = { text, replaceCharCnt };
this._commandService.executeCommand(editorCommon.Handler.ReplacePreviousChar, payload);
compositionType: (text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number) => {
// Try if possible to go through the existing `replacePreviousChar` command
if (replaceNextCharCnt || positionDelta) {
// must be handled through the new command
const payload: editorCommon.CompositionTypePayload = { text, replacePrevCharCnt, replaceNextCharCnt, positionDelta };
this._commandService.executeCommand(editorCommon.Handler.CompositionType, payload);
} else {
const payload: editorCommon.ReplacePreviousCharPayload = { text, replaceCharCnt: replacePrevCharCnt };
this._commandService.executeCommand(editorCommon.Handler.ReplacePreviousChar, payload);
}
},
startComposition: () => {
this._commandService.executeCommand(editorCommon.Handler.CompositionStart, {});

View file

@ -509,7 +509,7 @@ const editorConfiguration: IConfigurationNode = {
nls.localize('wordBasedSuggestionsMode.matchingDocuments', 'Suggest words from all open documents of the same language.'),
nls.localize('wordBasedSuggestionsMode.allDocuments', 'Suggest words from all open documents.')
],
description: nls.localize('wordBasedSuggestionsMode', "Controls form what documents word based completions are computed.")
description: nls.localize('wordBasedSuggestionsMode', "Controls from what documents word based completions are computed.")
},
'editor.semanticHighlighting.enabled': {
enum: [true, false, 'configuredByTheme'],

View file

@ -659,9 +659,21 @@ export class Cursor extends Disposable {
}, eventsCollector, source);
}
public replacePreviousChar(eventsCollector: ViewModelEventsCollector, text: string, replaceCharCnt: number, source?: string | null | undefined): void {
public compositionType(eventsCollector: ViewModelEventsCollector, text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number, source?: string | null | undefined): void {
if (text.length === 0 && replacePrevCharCnt === 0 && replaceNextCharCnt === 0) {
// this edit is a no-op
if (positionDelta !== 0) {
// but it still wants to move the cursor
const newSelections = this.getSelections().map(selection => {
const position = selection.getPosition();
return new Selection(position.lineNumber, position.column + positionDelta, position.lineNumber, position.column + positionDelta);
});
this.setSelections(eventsCollector, source, newSelections, CursorChangeReason.NotSet);
}
return;
}
this._executeEdit(() => {
this._executeEditOperation(TypeOperations.replacePreviousChar(this._prevEditOperationType, this.context.cursorConfig, this._model, this.getSelections(), text, replaceCharCnt));
this._executeEditOperation(TypeOperations.compositionType(this._prevEditOperationType, this.context.cursorConfig, this._model, this.getSelections(), text, replacePrevCharCnt, replaceNextCharCnt, positionDelta));
}, eventsCollector, source);
}

View file

@ -258,34 +258,33 @@ export class TypeOperations {
return commands;
}
public static replacePreviousChar(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], txt: string, replaceCharCnt: number): EditOperationResult {
let commands: Array<ICommand | null> = [];
for (let i = 0, len = selections.length; i < len; i++) {
const selection = selections[i];
if (!selection.isEmpty()) {
// looks like https://github.com/microsoft/vscode/issues/2773
// where a cursor operation occurred before a canceled composition
// => ignore composition
commands[i] = null;
continue;
}
const pos = selection.getPosition();
const startColumn = Math.max(1, pos.column - replaceCharCnt);
const range = new Range(pos.lineNumber, startColumn, pos.lineNumber, pos.column);
const oldText = model.getValueInRange(range);
if (oldText === txt) {
// => ignore composition that doesn't do anything
commands[i] = null;
continue;
}
commands[i] = new ReplaceCommand(range, txt);
}
public static compositionType(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number): EditOperationResult {
const commands = selections.map(selection => this._compositionType(model, selection, text, replacePrevCharCnt, replaceNextCharCnt, positionDelta));
return new EditOperationResult(EditOperationType.Typing, commands, {
shouldPushStackElementBefore: (prevEditOperationType !== EditOperationType.Typing),
shouldPushStackElementAfter: false
});
}
private static _compositionType(model: ITextModel, selection: Selection, text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number): ICommand | null {
if (!selection.isEmpty()) {
// looks like https://github.com/microsoft/vscode/issues/2773
// where a cursor operation occurred before a canceled composition
// => ignore composition
return null;
}
const pos = selection.getPosition();
const startColumn = Math.max(1, pos.column - replacePrevCharCnt);
const endColumn = Math.min(model.getLineMaxColumn(pos.lineNumber), pos.column + replaceNextCharCnt);
const range = new Range(pos.lineNumber, startColumn, pos.lineNumber, endColumn);
const oldText = model.getValueInRange(range);
if (oldText === text && positionDelta === 0) {
// => ignore composition that doesn't do anything
return null;
}
return new ReplaceCommandWithOffsetCursorState(range, text, 0, positionDelta);
}
private static _typeCommand(range: Range, text: string, keepPosition: boolean): ICommand {
if (keepPosition) {
return new ReplaceCommandWithoutChangingPosition(range, text, true);

View file

@ -700,6 +700,7 @@ export const enum Handler {
CompositionEnd = 'compositionEnd',
Type = 'type',
ReplacePreviousChar = 'replacePreviousChar',
CompositionType = 'compositionType',
Paste = 'paste',
Cut = 'cut',
}
@ -719,6 +720,16 @@ export interface ReplacePreviousCharPayload {
replaceCharCnt: number;
}
/**
* @internal
*/
export interface CompositionTypePayload {
text: string;
replacePrevCharCnt: number;
replaceNextCharCnt: number;
positionDelta: number;
}
/**
* @internal
*/

View file

@ -948,8 +948,8 @@ export class ViewModel extends Disposable implements IViewModel {
public type(text: string, source?: string | null | undefined): void {
this._executeCursorEdit(eventsCollector => this._cursor.type(eventsCollector, text, source));
}
public replacePreviousChar(text: string, replaceCharCnt: number, source?: string | null | undefined): void {
this._executeCursorEdit(eventsCollector => this._cursor.replacePreviousChar(eventsCollector, text, replaceCharCnt, source));
public compositionType(text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number, source?: string | null | undefined): void {
this._executeCursorEdit(eventsCollector => this._cursor.compositionType(eventsCollector, text, replacePrevCharCnt, replaceNextCharCnt, positionDelta, source));
}
public paste(text: string, pasteOnNewLine: boolean, multicursorText?: string[] | null | undefined, source?: string | null | undefined): void {
this._executeCursorEdit(eventsCollector => this._cursor.paste(eventsCollector, text, pasteOnNewLine, multicursorText, source));

View file

@ -43,6 +43,7 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService
import { StandaloneThemeServiceImpl } from 'vs/editor/standalone/browser/standaloneThemeServiceImpl';
import { splitLines } from 'vs/base/common/strings';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ILogService } from 'vs/platform/log/common/log';
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
@ -56,7 +57,7 @@ function withAllStandaloneServices<T extends IEditor>(domElement: HTMLElement, o
}
if (!services.has(IOpenerService)) {
services.set(IOpenerService, new OpenerService(services.get(ICodeEditorService), services.get(ICommandService)));
services.set(IOpenerService, new OpenerService(services.get(ICodeEditorService), services.get(ICommandService), services.get(ILogService)));
}
let result = callback(services);

View file

@ -32,7 +32,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ILabelService } from 'vs/platform/label/common/label';
import { IListService, ListService } from 'vs/platform/list/browser/listService';
import { ConsoleLogService, ILogService } from 'vs/platform/log/common/log';
import { ConsoleLogger, ILogService, LogService } from 'vs/platform/log/common/log';
import { MarkerService } from 'vs/platform/markers/common/markerService';
import { IMarkerService } from 'vs/platform/markers/common/markers';
import { INotificationService } from 'vs/platform/notification/common/notification';
@ -152,7 +152,7 @@ export module StaticServices {
export const standaloneThemeService = define(IStandaloneThemeService, () => new StandaloneThemeServiceImpl());
export const logService = define(ILogService, () => new ConsoleLogService());
export const logService = define(ILogService, () => new LogService(new ConsoleLogger()));
export const undoRedoService = define(IUndoRedoService, (o) => new UndoRedoService(dialogService.get(o), notificationService.get(o)));

View file

@ -16,7 +16,7 @@ import { ColorIdentifier, Extensions, IColorRegistry } from 'vs/platform/theme/c
import { Extensions as ThemingExtensions, ICssStyleCollector, IFileIconTheme, IThemingRegistry, ITokenStyle } from 'vs/platform/theme/common/themeService';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { ColorScheme } from 'vs/platform/theme/common/theme';
import { getIconRegistry } from 'vs/platform/theme/common/iconRegistry';
import { getIconsStyleSheet } from 'vs/platform/theme/browser/iconsStyleSheet';
const VS_THEME_NAME = 'vs';
const VS_DARK_THEME_NAME = 'vs-dark';
@ -214,9 +214,9 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon
this._knownThemes.set(VS_DARK_THEME_NAME, newBuiltInTheme(VS_DARK_THEME_NAME));
this._knownThemes.set(HC_BLACK_THEME_NAME, newBuiltInTheme(HC_BLACK_THEME_NAME));
const iconRegistry = getIconRegistry();
const iconsStyleSheet = getIconsStyleSheet();
this._codiconCSS = iconRegistry.getCSS();
this._codiconCSS = iconsStyleSheet.getCSS();
this._themeCSS = '';
this._allCSS = `${this._codiconCSS}\n${this._themeCSS}`;
this._globalStyleElement = null;
@ -224,8 +224,8 @@ export class StandaloneThemeServiceImpl extends Disposable implements IStandalon
this._colorMapOverride = null;
this.setTheme(VS_THEME_NAME);
iconRegistry.onDidChange(() => {
this._codiconCSS = iconRegistry.getCSS();
iconsStyleSheet.onDidChange(() => {
this._codiconCSS = iconsStyleSheet.getCSS();
this._updateCSS();
});
}

View file

@ -2079,16 +2079,16 @@ suite('Editor Controller - Regression tests', () => {
// Typing sennsei in Japanese - Hiragana
viewModel.type('', 'keyboard');
viewModel.replacePreviousChar('せ', 1);
viewModel.replacePreviousChar('せn', 1);
viewModel.replacePreviousChar('せん', 2);
viewModel.replacePreviousChar('せんs', 2);
viewModel.replacePreviousChar('せんせ', 3);
viewModel.replacePreviousChar('せんせ', 3);
viewModel.replacePreviousChar('せんせい', 3);
viewModel.replacePreviousChar('せんせい', 4);
viewModel.replacePreviousChar('せんせい', 4);
viewModel.replacePreviousChar('せんせい', 4);
viewModel.compositionType('せ', 1, 0, 0);
viewModel.compositionType('せn', 1, 0, 0);
viewModel.compositionType('せん', 2, 0, 0);
viewModel.compositionType('せんs', 2, 0, 0);
viewModel.compositionType('せんせ', 3, 0, 0);
viewModel.compositionType('せんせ', 3, 0, 0);
viewModel.compositionType('せんせい', 3, 0, 0);
viewModel.compositionType('せんせい', 4, 0, 0);
viewModel.compositionType('せんせい', 4, 0, 0);
viewModel.compositionType('せんせい', 4, 0, 0);
assert.strictEqual(model.getLineContent(1), 'せんせい');
assertCursor(viewModel, new Position(1, 5));
@ -5449,7 +5449,7 @@ suite('autoClosingPairs', () => {
// Typing ` + e on the mac US intl kb layout
viewModel.startComposition();
viewModel.type('`', 'keyboard');
viewModel.replacePreviousChar('è', 1, 'keyboard');
viewModel.compositionType('è', 1, 0, 0, 'keyboard');
viewModel.endComposition('keyboard');
assert.strictEqual(model.getValue(), 'è');
@ -5470,8 +5470,8 @@ suite('autoClosingPairs', () => {
// Typing ` + e on the mac US intl kb layout
viewModel.startComposition();
viewModel.type('\'', 'keyboard');
viewModel.replacePreviousChar('\'', 1, 'keyboard');
viewModel.replacePreviousChar('\'', 1, 'keyboard');
viewModel.compositionType('\'', 1, 0, 0, 'keyboard');
viewModel.compositionType('\'', 1, 0, 0, 'keyboard');
viewModel.endComposition('keyboard');
assert.strictEqual(model.getValue(), '\'test\'');
@ -5550,8 +5550,8 @@ suite('autoClosingPairs', () => {
viewModel.startComposition();
viewModel.type('`', 'keyboard');
moveDown(editor, viewModel, true);
viewModel.replacePreviousChar('`', 1, 'keyboard');
viewModel.replacePreviousChar('`', 1, 'keyboard');
viewModel.compositionType('`', 1, 0, 0, 'keyboard');
viewModel.compositionType('`', 1, 0, 0, 'keyboard');
viewModel.endComposition('keyboard');
assert.strictEqual(model.getValue(), '`hello\nworld');
@ -5575,14 +5575,14 @@ suite('autoClosingPairs', () => {
// Typing ' + space
viewModel.startComposition();
viewModel.type('\'', 'keyboard');
viewModel.replacePreviousChar('\'', 1, 'keyboard');
viewModel.compositionType('\'', 1, 0, 0, 'keyboard');
viewModel.endComposition('keyboard');
assert.strictEqual(model.getValue(), '\'\'');
// Typing one more ' + space
viewModel.startComposition();
viewModel.type('\'', 'keyboard');
viewModel.replacePreviousChar('\'', 1, 'keyboard');
viewModel.compositionType('\'', 1, 0, 0, 'keyboard');
viewModel.endComposition('keyboard');
assert.strictEqual(model.getValue(), '\'\'');
@ -5591,7 +5591,7 @@ suite('autoClosingPairs', () => {
viewModel.setSelections('test', [new Selection(1, 5, 1, 5)]);
viewModel.startComposition();
viewModel.type('\'', 'keyboard');
viewModel.replacePreviousChar('\'', 1, 'keyboard');
viewModel.compositionType('\'', 1, 0, 0, 'keyboard');
viewModel.endComposition('keyboard');
assert.strictEqual(model.getValue(), '\'abc\'');
@ -5601,7 +5601,7 @@ suite('autoClosingPairs', () => {
viewModel.setSelections('test', [new Selection(1, 10, 1, 10)]);
viewModel.startComposition();
viewModel.type('\'', 'keyboard');
viewModel.replacePreviousChar('\'', 1, 'keyboard');
viewModel.compositionType('\'', 1, 0, 0, 'keyboard');
viewModel.endComposition('keyboard');
assert.strictEqual(model.getValue(), '\'abc\'def \'\'');
@ -5611,7 +5611,7 @@ suite('autoClosingPairs', () => {
viewModel.setSelections('test', [new Selection(1, 1, 1, 1)]);
viewModel.startComposition();
viewModel.type('\'', 'keyboard');
viewModel.replacePreviousChar('\'', 1, 'keyboard');
viewModel.compositionType('\'', 1, 0, 0, 'keyboard');
viewModel.endComposition('keyboard');
// No auto closing if it's after a word.
@ -5619,7 +5619,7 @@ suite('autoClosingPairs', () => {
viewModel.setSelections('test', [new Selection(1, 4, 1, 4)]);
viewModel.startComposition();
viewModel.type('\'', 'keyboard');
viewModel.replacePreviousChar('\'', 1, 'keyboard');
viewModel.compositionType('\'', 1, 0, 0, 'keyboard');
viewModel.endComposition('keyboard');
assert.strictEqual(model.getValue(), 'abc\'');
@ -5640,7 +5640,7 @@ suite('autoClosingPairs', () => {
// Typing a + backspace
viewModel.startComposition();
viewModel.type('a', 'keyboard');
viewModel.replacePreviousChar('', 1, 'keyboard');
viewModel.compositionType('', 1, 0, 0, 'keyboard');
viewModel.endComposition('keyboard');
assert.strictEqual(model.getValue(), '{}');
});

View file

@ -151,9 +151,9 @@ function doCreateTest(description: string, inputStr: string, expectedStr: string
};
handler.onType((e) => {
console.log('type text: ' + e.text + ', replaceCharCnt: ' + e.replaceCharCnt);
console.log('type text: ' + e.text + ', replaceCharCnt: ' + e.replacePrevCharCnt);
let text = model.getModelLineContent(1);
let preText = text.substring(0, cursorOffset - e.replaceCharCnt);
let preText = text.substring(0, cursorOffset - e.replacePrevCharCnt);
let postText = text.substring(cursorOffset + cursorLength);
let midText = e.text;

View file

@ -134,8 +134,12 @@ suite('TextAreaState', () => {
let newState = TextAreaState.readFromTextArea(textArea);
let actual = TextAreaState.deduceInput(prevState, newState, couldBeEmojiInput);
assert.strictEqual(actual.text, expected);
assert.strictEqual(actual.replaceCharCnt, expectedCharReplaceCnt);
assert.deepStrictEqual(actual, {
text: expected,
replacePrevCharCnt: expectedCharReplaceCnt,
replaceNextCharCnt: 0,
positionDelta: 0,
});
textArea.dispose();
}
@ -503,6 +507,82 @@ suite('TextAreaState', () => {
);
});
function testDeduceAndroidCompositionInput(
prevState: TextAreaState | null,
value: string, selectionStart: number, selectionEnd: number,
expected: string, expectedReplacePrevCharCnt: number, expectedReplaceNextCharCnt: number, expectedPositionDelta: number): void {
prevState = prevState || TextAreaState.EMPTY;
let textArea = new MockTextAreaWrapper();
textArea._value = value;
textArea._selectionStart = selectionStart;
textArea._selectionEnd = selectionEnd;
let newState = TextAreaState.readFromTextArea(textArea);
let actual = TextAreaState.deduceAndroidCompositionInput(prevState, newState);
assert.deepStrictEqual(actual, {
text: expected,
replacePrevCharCnt: expectedReplacePrevCharCnt,
replaceNextCharCnt: expectedReplaceNextCharCnt,
positionDelta: expectedPositionDelta,
});
textArea.dispose();
}
test('Android composition input 1', () => {
testDeduceAndroidCompositionInput(
new TextAreaState(
'Microsoft',
4, 4,
null, null
),
'Microsoft',
4, 4,
'', 0, 0, 0,
);
});
test('Android composition input 2', () => {
testDeduceAndroidCompositionInput(
new TextAreaState(
'Microsoft',
4, 4,
null, null
),
'Microsoft',
0, 9,
'', 0, 0, 5,
);
});
test('Android composition input 3', () => {
testDeduceAndroidCompositionInput(
new TextAreaState(
'Microsoft',
0, 9,
null, null
),
'Microsoft\'s',
11, 11,
'\'s', 0, 0, 0,
);
});
test('Android backspace', () => {
testDeduceAndroidCompositionInput(
new TextAreaState(
'undefinedVariable',
2, 2,
null, null
),
'udefinedVariable',
1, 1,
'', 1, 0, 0,
);
});
suite('PagedScreenReaderStrategy', () => {
function testPagedScreenReaderStrategy(lines: string[], selection: Selection, expected: TextAreaState): void {

View file

@ -8,6 +8,7 @@ import { URI } from 'vs/base/common/uri';
import { OpenerService } from 'vs/editor/browser/services/openerService';
import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices';
import { CommandsRegistry, ICommandService, NullCommandService } from 'vs/platform/commands/common/commands';
import { NullLogService } from 'vs/platform/log/common/log';
import { matchesScheme } from 'vs/platform/opener/common/opener';
suite('OpenerService', function () {
@ -30,13 +31,13 @@ suite('OpenerService', function () {
});
test('delegate to editorService, scheme:///fff', async function () {
const openerService = new OpenerService(editorService, NullCommandService);
const openerService = new OpenerService(editorService, NullCommandService, new NullLogService());
await openerService.open(URI.parse('another:///somepath'));
assert.equal(editorService.lastInput!.options!.selection, undefined);
});
test('delegate to editorService, scheme:///fff#L123', async function () {
const openerService = new OpenerService(editorService, NullCommandService);
const openerService = new OpenerService(editorService, NullCommandService, new NullLogService());
await openerService.open(URI.parse('file:///somepath#L23'));
assert.equal(editorService.lastInput!.options!.selection!.startLineNumber, 23);
@ -58,7 +59,7 @@ suite('OpenerService', function () {
});
test('delegate to editorService, scheme:///fff#123,123', async function () {
const openerService = new OpenerService(editorService, NullCommandService);
const openerService = new OpenerService(editorService, NullCommandService, new NullLogService());
await openerService.open(URI.parse('file:///somepath#23'));
assert.equal(editorService.lastInput!.options!.selection!.startLineNumber, 23);
@ -76,7 +77,7 @@ suite('OpenerService', function () {
});
test('delegate to commandsService, command:someid', async function () {
const openerService = new OpenerService(editorService, commandService);
const openerService = new OpenerService(editorService, commandService, new NullLogService());
const id = `aCommand${Math.random()}`;
CommandsRegistry.registerCommand(id, function () { });
@ -98,7 +99,7 @@ suite('OpenerService', function () {
});
test('links are protected by validators', async function () {
const openerService = new OpenerService(editorService, commandService);
const openerService = new OpenerService(editorService, commandService, new NullLogService());
openerService.registerValidator({ shouldOpen: () => Promise.resolve(false) });
@ -109,7 +110,7 @@ suite('OpenerService', function () {
});
test('links validated by validators go to openers', async function () {
const openerService = new OpenerService(editorService, commandService);
const openerService = new OpenerService(editorService, commandService, new NullLogService());
openerService.registerValidator({ shouldOpen: () => Promise.resolve(true) });
@ -128,7 +129,7 @@ suite('OpenerService', function () {
});
test('links validated by multiple validators', async function () {
const openerService = new OpenerService(editorService, commandService);
const openerService = new OpenerService(editorService, commandService, new NullLogService());
let v1 = 0;
openerService.registerValidator({
@ -165,7 +166,7 @@ suite('OpenerService', function () {
});
test('links invalidated by first validator do not continue validating', async function () {
const openerService = new OpenerService(editorService, commandService);
const openerService = new OpenerService(editorService, commandService, new NullLogService());
let v1 = 0;
openerService.registerValidator({

View file

@ -35,10 +35,10 @@ export function testCommand(
cursor.executeCommand(commandFactory(cursor.getSelection()), 'tests');
assert.deepEqual(model.getLinesContent(), expectedLines);
assert.deepStrictEqual(model.getLinesContent(), expectedLines);
let actualSelection = cursor.getSelection();
assert.deepEqual(actualSelection.toString(), expectedSelection.toString());
assert.deepStrictEqual(actualSelection.toString(), expectedSelection.toString());
});
model.dispose();

View file

@ -20,7 +20,7 @@ export function testApplyEditsWithSyncedModels(original: string[], edits: IIdent
let inverseEdits = model.applyEdits(edits, true);
// Assert edits produced expected result
assert.deepEqual(model.getValue(EndOfLinePreference.LF), expectedStr);
assert.deepStrictEqual(model.getValue(EndOfLinePreference.LF), expectedStr);
assertMirrorModels();
@ -28,7 +28,7 @@ export function testApplyEditsWithSyncedModels(original: string[], edits: IIdent
let inverseInverseEdits = model.applyEdits(inverseEdits, true);
// Assert the inverse edits brought back model to original state
assert.deepEqual(model.getValue(EndOfLinePreference.LF), originalStr);
assert.deepStrictEqual(model.getValue(EndOfLinePreference.LF), originalStr);
if (!inputEditsAreInvalid) {
const simplifyEdit = (edit: IIdentifiedSingleEditOperation) => {
@ -41,7 +41,7 @@ export function testApplyEditsWithSyncedModels(original: string[], edits: IIdent
};
};
// Assert the inverse of the inverse edits are the original edits
assert.deepEqual(inverseInverseEdits.map(simplifyEdit), edits.map(simplifyEdit));
assert.deepStrictEqual(inverseInverseEdits.map(simplifyEdit), edits.map(simplifyEdit));
}
assertMirrorModels();
@ -59,16 +59,16 @@ function assertOneDirectionLineMapping(model: TextModel, direction: AssertDocume
let line = 1, column = 1, previousIsCarriageReturn = false;
for (let offset = 0; offset <= allText.length; offset++) {
// The position coordinate system cannot express the position between \r and \n
let position = new Position(line, column + (previousIsCarriageReturn ? -1 : 0));
let position: Position = new Position(line, column + (previousIsCarriageReturn ? -1 : 0));
if (direction === AssertDocumentLineMappingDirection.OffsetToPosition) {
let actualPosition = model.getPositionAt(offset);
assert.equal(actualPosition.toString(), position.toString(), msg + ' - getPositionAt mismatch for offset ' + offset);
assert.strictEqual(actualPosition.toString(), position.toString(), msg + ' - getPositionAt mismatch for offset ' + offset);
} else {
// The position coordinate system cannot express the position between \r and \n
let expectedOffset = offset + (previousIsCarriageReturn ? -1 : 0);
let expectedOffset: number = offset + (previousIsCarriageReturn ? -1 : 0);
let actualOffset = model.getOffsetAt(position);
assert.equal(actualOffset, expectedOffset, msg + ' - getOffsetAt mismatch for position ' + position.toString());
assert.strictEqual(actualOffset, expectedOffset, msg + ' - getOffsetAt mismatch for position ' + position.toString());
}
if (allText.charAt(offset) === '\n') {
@ -112,8 +112,8 @@ export function assertSyncedModels(text: string, callback: (model: TextModel, as
let assertMirrorModels = () => {
assertLineMapping(model, 'model');
assert.equal(mirrorModel2.getText(), model.getValue(), 'mirror model 2 text OK');
assert.equal(mirrorModel2.version, model.getVersionId(), 'mirror model 2 version OK');
assert.strictEqual(mirrorModel2.getText(), model.getValue(), 'mirror model 2 text OK');
assert.strictEqual(mirrorModel2.version, model.getVersionId(), 'mirror model 2 version OK');
};
callback(model, assertMirrorModels);

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