diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 440edf6844b..7a3dd5cf7b3 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -8,7 +8,7 @@ This project incorporates components from the projects listed below. The origina 1. atom/language-clojure version 0.22.7 (https://github.com/atom/language-clojure) 2. atom/language-coffee-script version 0.49.3 (https://github.com/atom/language-coffee-script) 3. atom/language-java version 0.31.3 (https://github.com/atom/language-java) -4. atom/language-sass version 0.61.4 (https://github.com/atom/language-sass) +4. atom/language-sass version 0.62.1 (https://github.com/atom/language-sass) 5. atom/language-shellscript version 0.26.0 (https://github.com/atom/language-shellscript) 6. atom/language-xml version 0.35.2 (https://github.com/atom/language-xml) 7. Colorsublime-Themes version 0.1.0 (https://github.com/Colorsublime/Colorsublime-Themes) @@ -27,13 +27,13 @@ This project incorporates components from the projects listed below. The origina 20. Ionic documentation version 1.2.4 (https://github.com/ionic-team/ionic-site) 21. ionide/ionide-fsgrammar (https://github.com/ionide/ionide-fsgrammar) 22. jeff-hykin/cpp-textmate-grammar version 1.12.11 (https://github.com/jeff-hykin/cpp-textmate-grammar) -23. jeff-hykin/cpp-textmate-grammar version 1.14.9 (https://github.com/jeff-hykin/cpp-textmate-grammar) +23. jeff-hykin/cpp-textmate-grammar version 1.14.13 (https://github.com/jeff-hykin/cpp-textmate-grammar) 24. js-beautify version 1.6.8 (https://github.com/beautify-web/js-beautify) 25. Jxck/assert version 1.0.0 (https://github.com/Jxck/assert) 26. language-docker (https://github.com/moby/moby) 27. language-go version 0.44.3 (https://github.com/atom/language-go) 28. language-less version 0.34.2 (https://github.com/atom/language-less) -29. language-php version 0.44.2 (https://github.com/atom/language-php) +29. language-php version 0.44.3 (https://github.com/atom/language-php) 30. language-rust version 0.4.12 (https://github.com/zargony/atom-language-rust) 31. MagicStack/MagicPython version 1.1.1 (https://github.com/MagicStack/MagicPython) 32. marked version 0.6.2 (https://github.com/markedjs/marked) @@ -589,7 +589,7 @@ END OF HTML 5.1 W3C Working Draft NOTICES AND INFORMATION ========================================= MIT License -Copyright (c) 2017 Yuki Ueda +Copyright (c) 2019 Yuki Ueda Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index 6e1f8ec1e53..573d7c7d4c2 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -118,6 +118,32 @@ steps: displayName: Run integration tests condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) +- script: | + set -e + yarn gulp "vscode-linux-x64-build-deb" + yarn gulp "vscode-linux-x64-build-rpm" + yarn gulp "vscode-linux-x64-prepare-snap" + displayName: Build packages + +- task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 + inputs: + ConnectedServiceName: 'ESRP CodeSign' + FolderPath: '.build/linux/rpm/x86_64' + Pattern: '*.rpm' + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "keyCode": "CP-450779-Pgp", + "operationSetCode": "LinuxSign", + "parameters": [ ], + "toolName": "sign", + "toolVersion": "1.0" + } + ] + SessionTimeout: 120 + displayName: Codesign rpm + - script: | set -e AZURE_DOCUMENTDB_MASTERKEY="$(builds-docdb-key-readwrite)" \ diff --git a/build/azure-pipelines/linux/publish.sh b/build/azure-pipelines/linux/publish.sh index 3a5f9683eaa..3da6ea3eed6 100755 --- a/build/azure-pipelines/linux/publish.sh +++ b/build/azure-pipelines/linux/publish.sh @@ -31,7 +31,6 @@ node build/azure-pipelines/common/createAsset.js "server-$PLATFORM_LINUX" archiv node build/azure-pipelines/common/symbols.js "$VSCODE_MIXIN_PASSWORD" "$VSCODE_HOCKEYAPP_TOKEN" "x64" "$VSCODE_HOCKEYAPP_ID_LINUX64" # Publish DEB -yarn gulp "vscode-linux-x64-build-deb" PLATFORM_DEB="linux-deb-x64" DEB_ARCH="amd64" DEB_FILENAME="$(ls $REPO/.build/linux/deb/$DEB_ARCH/deb/)" @@ -40,7 +39,6 @@ DEB_PATH="$REPO/.build/linux/deb/$DEB_ARCH/deb/$DEB_FILENAME" node build/azure-pipelines/common/createAsset.js "$PLATFORM_DEB" package "$DEB_FILENAME" "$DEB_PATH" # Publish RPM -yarn gulp "vscode-linux-x64-build-rpm" PLATFORM_RPM="linux-rpm-x64" RPM_ARCH="x86_64" RPM_FILENAME="$(ls $REPO/.build/linux/rpm/$RPM_ARCH/ | grep .rpm)" @@ -49,8 +47,6 @@ RPM_PATH="$REPO/.build/linux/rpm/$RPM_ARCH/$RPM_FILENAME" node build/azure-pipelines/common/createAsset.js "$PLATFORM_RPM" package "$RPM_FILENAME" "$RPM_PATH" # Publish Snap -yarn gulp "vscode-linux-x64-prepare-snap" - # Pack snap tarball artifact, in order to preserve file perms mkdir -p $REPO/.build/linux/snap-tarball SNAP_TARBALL_PATH="$REPO/.build/linux/snap-tarball/snap-x64.tar.gz" diff --git a/build/builtInExtensions.json b/build/builtInExtensions.json index b75cca4234e..68d891d09d4 100644 --- a/build/builtInExtensions.json +++ b/build/builtInExtensions.json @@ -16,7 +16,7 @@ }, { "name": "ms-vscode.node-debug2", - "version": "1.41.0", + "version": "1.41.3", "repo": "https://github.com/Microsoft/vscode-node-debug2", "metadata": { "id": "36d19e17-7569-4841-a001-947eb18602b2", @@ -31,7 +31,7 @@ }, { "name": "ms-vscode.references-view", - "version": "0.0.38", + "version": "0.0.42", "repo": "https://github.com/Microsoft/vscode-reference-view", "metadata": { "id": "dc489f46-520d-4556-ae85-1f9eab3c412d", diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 6edf7c5b075..f0a01a5cf89 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -50,10 +50,6 @@ "name": "vs/workbench/contrib/comments", "project": "vscode-workbench" }, - { - "name": "vs/workbench/contrib/testCustomEditors", - "project": "vscode-workbench" - }, { "name": "vs/workbench/contrib/debug", "project": "vscode-workbench" diff --git a/build/win32/code.iss b/build/win32/code.iss index 54bde39080a..5c995f26636 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -964,10 +964,10 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBas Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBasename}.exe\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBasename}.exe\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1""" -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu, {#ShellNameShort}}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: addcontextmenufiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu, {#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "Open w&ith {#ShellNameShort}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey diff --git a/extensions/configuration-editing/schemas/attachContainer.schema.json b/extensions/configuration-editing/schemas/attachContainer.schema.json index ba0df4b33dc..cebd0f2bc3e 100644 --- a/extensions/configuration-editing/schemas/attachContainer.schema.json +++ b/extensions/configuration-editing/schemas/attachContainer.schema.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "description": "Configures an attached to container", "allowComments": true, "type": "object", @@ -18,6 +18,10 @@ "type": "integer" } }, + "settings": { + "$ref": "vscode://schemas/settings/machine", + "description": "Machine specific settings that should be copied into the container." + }, "remoteEnv": { "type": "object", "additionalProperties": { diff --git a/extensions/configuration-editing/schemas/devContainer.schema.json b/extensions/configuration-editing/schemas/devContainer.schema.json index 631c58e82b5..aed58455d01 100644 --- a/extensions/configuration-editing/schemas/devContainer.schema.json +++ b/extensions/configuration-editing/schemas/devContainer.schema.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "description": "Defines a dev container", "allowComments": true, "type": "object", @@ -84,6 +84,13 @@ "type": "boolean", "description": "Controls whether on Linux the container's user should be updated with the local user's UID and GID. On by default." }, + "mounts": { + "type": "array", + "description": "Mount points to set up when creating the container. See Docker's documentation for the --mount option for the supported syntax.", + "items": { + "type": "string" + } + }, "runArgs": { "type": "array", "description": "The arguments required when starting in the container.", diff --git a/extensions/cpp/test/colorize-fixtures/test.cpp b/extensions/cpp/test/colorize-fixtures/test.cpp index c3c3c10a953..13ec47a554c 100644 --- a/extensions/cpp/test/colorize-fixtures/test.cpp +++ b/extensions/cpp/test/colorize-fixtures/test.cpp @@ -16,7 +16,13 @@ void Rectangle::set_values (int x, int y) { height = y; } +long double operator "" _w(long double); +#define MY_MACRO(a, b) + int main () { + 1.2_w; // calls operator "" _w(1.2L) + asm("movl %a %b"); + MY_MACRO(1, 2); Rectangle rect; rect.set_values (3,4); cout << "area: " << rect.area(); diff --git a/extensions/cpp/test/colorize-results/test-23630_cpp.json b/extensions/cpp/test/colorize-results/test-23630_cpp.json index 08d81e6afff..4ffc7f01f98 100644 --- a/extensions/cpp/test/colorize-results/test-23630_cpp.json +++ b/extensions/cpp/test/colorize-results/test-23630_cpp.json @@ -36,10 +36,10 @@ "c": "_UCRT", "t": "source.cpp meta.preprocessor.conditional.cpp entity.name.function.preprocessor.cpp", "r": { - "dark_plus": "entity.name.function: #DCDCAA", - "light_plus": "entity.name.function: #795E26", - "dark_vs": "meta.preprocessor: #569CD6", - "light_vs": "meta.preprocessor: #0000FF", + "dark_plus": "entity.name.function.preprocessor: #569CD6", + "light_plus": "entity.name.function.preprocessor: #0000FF", + "dark_vs": "entity.name.function.preprocessor: #569CD6", + "light_vs": "entity.name.function.preprocessor: #0000FF", "hc_black": "entity.name.function: #DCDCAA" } }, @@ -91,10 +91,10 @@ "c": "_UCRT", "t": "source.cpp meta.preprocessor.macro.cpp entity.name.function.preprocessor.cpp", "r": { - "dark_plus": "entity.name.function: #DCDCAA", - "light_plus": "entity.name.function: #795E26", - "dark_vs": "meta.preprocessor: #569CD6", - "light_vs": "meta.preprocessor: #0000FF", + "dark_plus": "entity.name.function.preprocessor: #569CD6", + "light_plus": "entity.name.function.preprocessor: #0000FF", + "dark_vs": "entity.name.function.preprocessor: #569CD6", + "light_vs": "entity.name.function.preprocessor: #0000FF", "hc_black": "entity.name.function: #DCDCAA" } }, diff --git a/extensions/cpp/test/colorize-results/test-23850_cpp.json b/extensions/cpp/test/colorize-results/test-23850_cpp.json index 4ba6b59dc9b..a97f9612665 100644 --- a/extensions/cpp/test/colorize-results/test-23850_cpp.json +++ b/extensions/cpp/test/colorize-results/test-23850_cpp.json @@ -36,10 +36,10 @@ "c": "_UCRT", "t": "source.cpp meta.preprocessor.conditional.cpp entity.name.function.preprocessor.cpp", "r": { - "dark_plus": "entity.name.function: #DCDCAA", - "light_plus": "entity.name.function: #795E26", - "dark_vs": "meta.preprocessor: #569CD6", - "light_vs": "meta.preprocessor: #0000FF", + "dark_plus": "entity.name.function.preprocessor: #569CD6", + "light_plus": "entity.name.function.preprocessor: #0000FF", + "dark_vs": "entity.name.function.preprocessor: #569CD6", + "light_vs": "entity.name.function.preprocessor: #0000FF", "hc_black": "entity.name.function: #DCDCAA" } }, @@ -80,10 +80,10 @@ "c": "_UCRT", "t": "source.cpp meta.preprocessor.macro.cpp entity.name.function.preprocessor.cpp", "r": { - "dark_plus": "entity.name.function: #DCDCAA", - "light_plus": "entity.name.function: #795E26", - "dark_vs": "meta.preprocessor: #569CD6", - "light_vs": "meta.preprocessor: #0000FF", + "dark_plus": "entity.name.function.preprocessor: #569CD6", + "light_plus": "entity.name.function.preprocessor: #0000FF", + "dark_vs": "entity.name.function.preprocessor: #569CD6", + "light_vs": "entity.name.function.preprocessor: #0000FF", "hc_black": "entity.name.function: #DCDCAA" } }, diff --git a/extensions/cpp/test/colorize-results/test-78769_cpp.json b/extensions/cpp/test/colorize-results/test-78769_cpp.json index 1d82d5c4e43..72bb9cdede7 100644 --- a/extensions/cpp/test/colorize-results/test-78769_cpp.json +++ b/extensions/cpp/test/colorize-results/test-78769_cpp.json @@ -36,10 +36,10 @@ "c": "DOCTEST_IMPLEMENT_FIXTURE", "t": "source.cpp meta.preprocessor.macro.cpp entity.name.function.preprocessor.cpp", "r": { - "dark_plus": "entity.name.function: #DCDCAA", - "light_plus": "entity.name.function: #795E26", - "dark_vs": "meta.preprocessor: #569CD6", - "light_vs": "meta.preprocessor: #0000FF", + "dark_plus": "entity.name.function.preprocessor: #569CD6", + "light_plus": "entity.name.function.preprocessor: #0000FF", + "dark_vs": "entity.name.function.preprocessor: #569CD6", + "light_vs": "entity.name.function.preprocessor: #0000FF", "hc_black": "entity.name.function: #DCDCAA" } }, diff --git a/extensions/cpp/test/colorize-results/test_cc.json b/extensions/cpp/test/colorize-results/test_cc.json index 902ef645164..30c22384617 100644 --- a/extensions/cpp/test/colorize-results/test_cc.json +++ b/extensions/cpp/test/colorize-results/test_cc.json @@ -36,10 +36,10 @@ "c": "B4G_DEBUG_CHECK", "t": "source.cpp meta.preprocessor.conditional.cpp entity.name.function.preprocessor.cpp", "r": { - "dark_plus": "entity.name.function: #DCDCAA", - "light_plus": "entity.name.function: #795E26", - "dark_vs": "meta.preprocessor: #569CD6", - "light_vs": "meta.preprocessor: #0000FF", + "dark_plus": "entity.name.function.preprocessor: #569CD6", + "light_plus": "entity.name.function.preprocessor: #0000FF", + "dark_vs": "entity.name.function.preprocessor: #569CD6", + "light_vs": "entity.name.function.preprocessor: #0000FF", "hc_black": "entity.name.function: #DCDCAA" } }, @@ -1807,10 +1807,10 @@ "c": "movw $0x38, %ax; ltr %ax", "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp meta.asm.cpp string.quoted.double.cpp meta.embedded.assembly.cpp", "r": { - "dark_plus": "meta.embedded: #D4D4D4", - "light_plus": "meta.embedded: #000000", - "dark_vs": "meta.embedded: #D4D4D4", - "light_vs": "meta.embedded: #000000", + "dark_plus": "meta.embedded.assembly: #CE9178", + "light_plus": "meta.embedded.assembly: #A31515", + "dark_vs": "meta.embedded.assembly: #CE9178", + "light_vs": "meta.embedded.assembly: #A31515", "hc_black": "meta.embedded: #FFFFFF" } }, diff --git a/extensions/cpp/test/colorize-results/test_cpp.json b/extensions/cpp/test/colorize-results/test_cpp.json index f84d916afa3..4806798686a 100644 --- a/extensions/cpp/test/colorize-results/test_cpp.json +++ b/extensions/cpp/test/colorize-results/test_cpp.json @@ -190,10 +190,10 @@ "c": "EXTERN_C", "t": "source.cpp meta.preprocessor.macro.cpp entity.name.function.preprocessor.cpp", "r": { - "dark_plus": "entity.name.function: #DCDCAA", - "light_plus": "entity.name.function: #795E26", - "dark_vs": "meta.preprocessor: #569CD6", - "light_vs": "meta.preprocessor: #0000FF", + "dark_plus": "entity.name.function.preprocessor: #569CD6", + "light_plus": "entity.name.function.preprocessor: #0000FF", + "dark_vs": "entity.name.function.preprocessor: #569CD6", + "light_vs": "entity.name.function.preprocessor: #0000FF", "hc_black": "entity.name.function: #DCDCAA" } }, @@ -1011,6 +1011,281 @@ "hc_black": "default: #FFFFFF" } }, + { + "c": "long", + "t": "source.cpp meta.function.definition.special.operator-overload.cpp meta.qualified_type.cpp storage.type.primitive.cpp storage.type.built-in.primitive.cpp", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6" + } + }, + { + "c": " ", + "t": "source.cpp meta.function.definition.special.operator-overload.cpp meta.qualified_type.cpp", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "double", + "t": "source.cpp meta.function.definition.special.operator-overload.cpp meta.qualified_type.cpp storage.type.primitive.cpp storage.type.built-in.primitive.cpp", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6" + } + }, + { + "c": " ", + "t": "source.cpp meta.function.definition.special.operator-overload.cpp", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "operator", + "t": "source.cpp meta.function.definition.special.operator-overload.cpp meta.head.function.definition.special.operator-overload.cpp keyword.other.operator.overload.cpp", + "r": { + "dark_plus": "keyword.other.operator: #C586C0", + "light_plus": "keyword.other.operator: #AF00DB", + "dark_vs": "keyword: #569CD6", + "light_vs": "keyword: #0000FF", + "hc_black": "keyword.other.operator: #C586C0" + } + }, + { + "c": " ", + "t": "source.cpp meta.function.definition.special.operator-overload.cpp", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "\"\"", + "t": "source.cpp meta.function.definition.special.operator-overload.cpp meta.head.function.definition.special.operator-overload.cpp entity.name.operator.custom-literal.cpp", + "r": { + "dark_plus": "entity.name.operator.custom-literal: #DCDCAA", + "light_plus": "entity.name.operator.custom-literal: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " ", + "t": "source.cpp meta.function.definition.special.operator-overload.cpp", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "_w", + "t": "source.cpp meta.function.definition.special.operator-overload.cpp meta.head.function.definition.special.operator-overload.cpp entity.name.operator.custom-literal.cpp", + "r": { + "dark_plus": "entity.name.operator.custom-literal: #DCDCAA", + "light_plus": "entity.name.operator.custom-literal: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "(", + "t": "source.cpp meta.function.definition.special.operator-overload.cpp meta.head.function.definition.special.operator-overload.cpp punctuation.section.parameters.begin.bracket.round.special.operator-overload.cpp", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "long", + "t": "source.cpp meta.function.definition.special.operator-overload.cpp meta.head.function.definition.special.operator-overload.cpp meta.function.definition.parameters.special.operator-overload.cpp meta.parameter.cpp storage.type.primitive.cpp storage.type.built-in.primitive.cpp", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6" + } + }, + { + "c": " ", + "t": "source.cpp meta.function.definition.special.operator-overload.cpp meta.head.function.definition.special.operator-overload.cpp meta.function.definition.parameters.special.operator-overload.cpp meta.parameter.cpp", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "double", + "t": "source.cpp meta.function.definition.special.operator-overload.cpp meta.head.function.definition.special.operator-overload.cpp meta.function.definition.parameters.special.operator-overload.cpp meta.parameter.cpp storage.type.primitive.cpp storage.type.built-in.primitive.cpp", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6" + } + }, + { + "c": ")", + "t": "source.cpp meta.function.definition.special.operator-overload.cpp meta.head.function.definition.special.operator-overload.cpp punctuation.section.parameters.end.bracket.round.special.operator-overload.cpp", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ";", + "t": "source.cpp punctuation.terminator.statement.cpp", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "#", + "t": "source.cpp meta.preprocessor.macro.cpp keyword.control.directive.define.cpp punctuation.definition.directive.cpp", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0" + } + }, + { + "c": "define", + "t": "source.cpp meta.preprocessor.macro.cpp keyword.control.directive.define.cpp", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0" + } + }, + { + "c": " ", + "t": "source.cpp meta.preprocessor.macro.cpp", + "r": { + "dark_plus": "meta.preprocessor: #569CD6", + "light_plus": "meta.preprocessor: #0000FF", + "dark_vs": "meta.preprocessor: #569CD6", + "light_vs": "meta.preprocessor: #0000FF", + "hc_black": "meta.preprocessor: #569CD6" + } + }, + { + "c": "MY_MACRO", + "t": "source.cpp meta.preprocessor.macro.cpp entity.name.function.preprocessor.cpp", + "r": { + "dark_plus": "entity.name.function.preprocessor: #569CD6", + "light_plus": "entity.name.function.preprocessor: #0000FF", + "dark_vs": "entity.name.function.preprocessor: #569CD6", + "light_vs": "entity.name.function.preprocessor: #0000FF", + "hc_black": "entity.name.function: #DCDCAA" + } + }, + { + "c": "(", + "t": "source.cpp meta.preprocessor.macro.cpp punctuation.definition.parameters.begin.preprocessor.cpp", + "r": { + "dark_plus": "meta.preprocessor: #569CD6", + "light_plus": "meta.preprocessor: #0000FF", + "dark_vs": "meta.preprocessor: #569CD6", + "light_vs": "meta.preprocessor: #0000FF", + "hc_black": "meta.preprocessor: #569CD6" + } + }, + { + "c": "a", + "t": "source.cpp meta.preprocessor.macro.cpp variable.parameter.preprocessor.cpp", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "meta.preprocessor: #569CD6", + "light_vs": "meta.preprocessor: #0000FF", + "hc_black": "variable: #9CDCFE" + } + }, + { + "c": ",", + "t": "source.cpp meta.preprocessor.macro.cpp punctuation.separator.parameters.cpp", + "r": { + "dark_plus": "meta.preprocessor: #569CD6", + "light_plus": "meta.preprocessor: #0000FF", + "dark_vs": "meta.preprocessor: #569CD6", + "light_vs": "meta.preprocessor: #0000FF", + "hc_black": "meta.preprocessor: #569CD6" + } + }, + { + "c": " ", + "t": "source.cpp meta.preprocessor.macro.cpp", + "r": { + "dark_plus": "meta.preprocessor: #569CD6", + "light_plus": "meta.preprocessor: #0000FF", + "dark_vs": "meta.preprocessor: #569CD6", + "light_vs": "meta.preprocessor: #0000FF", + "hc_black": "meta.preprocessor: #569CD6" + } + }, + { + "c": "b", + "t": "source.cpp meta.preprocessor.macro.cpp variable.parameter.preprocessor.cpp", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "meta.preprocessor: #569CD6", + "light_vs": "meta.preprocessor: #0000FF", + "hc_black": "variable: #9CDCFE" + } + }, + { + "c": ")", + "t": "source.cpp meta.preprocessor.macro.cpp punctuation.definition.parameters.end.preprocessor.cpp", + "r": { + "dark_plus": "meta.preprocessor: #569CD6", + "light_plus": "meta.preprocessor: #0000FF", + "dark_vs": "meta.preprocessor: #569CD6", + "light_vs": "meta.preprocessor: #0000FF", + "hc_black": "meta.preprocessor: #569CD6" + } + }, { "c": "int", "t": "source.cpp meta.function.definition.cpp meta.qualified_type.cpp storage.type.primitive.cpp storage.type.built-in.primitive.cpp", @@ -1099,6 +1374,292 @@ "hc_black": "default: #FFFFFF" } }, + { + "c": " ", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "1", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp constant.numeric.decimal.cpp", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #09885A", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #09885A", + "hc_black": "constant.numeric: #B5CEA8" + } + }, + { + "c": ".", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp constant.numeric.decimal.point.cpp", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #09885A", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #09885A", + "hc_black": "constant.numeric: #B5CEA8" + } + }, + { + "c": "2", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp constant.numeric.decimal.cpp", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #09885A", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #09885A", + "hc_black": "constant.numeric: #B5CEA8" + } + }, + { + "c": "_w", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp keyword.other.unit.user-defined.cpp", + "r": { + "dark_plus": "keyword.other.unit: #B5CEA8", + "light_plus": "keyword.other.unit: #09885A", + "dark_vs": "keyword.other.unit: #B5CEA8", + "light_vs": "keyword.other.unit: #09885A", + "hc_black": "keyword.other.unit: #B5CEA8" + } + }, + { + "c": ";", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp punctuation.terminator.statement.cpp", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " ", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp comment.line.double-slash.cpp", + "r": { + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668" + } + }, + { + "c": "//", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp comment.line.double-slash.cpp punctuation.definition.comment.cpp", + "r": { + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668" + } + }, + { + "c": " calls operator \"\" _w(1.2L)", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp comment.line.double-slash.cpp", + "r": { + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668" + } + }, + { + "c": " ", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "asm", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp meta.asm.cpp storage.type.asm.cpp", + "r": { + "dark_plus": "storage.type: #569CD6", + "light_plus": "storage.type: #0000FF", + "dark_vs": "storage.type: #569CD6", + "light_vs": "storage.type: #0000FF", + "hc_black": "storage.type: #569CD6" + } + }, + { + "c": "(", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp meta.asm.cpp punctuation.section.parens.begin.bracket.round.assembly.cpp", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "\"", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp meta.asm.cpp string.quoted.double.cpp punctuation.definition.string.begin.assembly.cpp", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178" + } + }, + { + "c": "movl %a %b", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp meta.asm.cpp string.quoted.double.cpp meta.embedded.assembly.cpp", + "r": { + "dark_plus": "meta.embedded.assembly: #CE9178", + "light_plus": "meta.embedded.assembly: #A31515", + "dark_vs": "meta.embedded.assembly: #CE9178", + "light_vs": "meta.embedded.assembly: #A31515", + "hc_black": "meta.embedded: #FFFFFF" + } + }, + { + "c": "\"", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp meta.asm.cpp string.quoted.double.cpp punctuation.definition.string.end.assembly.cpp", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178" + } + }, + { + "c": ")", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp meta.asm.cpp punctuation.section.parens.end.bracket.round.assembly.cpp", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ";", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp punctuation.terminator.statement.cpp", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " ", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "MY_MACRO", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp entity.name.function.call.cpp", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "entity.name.function: #DCDCAA" + } + }, + { + "c": "(", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp punctuation.section.arguments.begin.bracket.round.function.call.cpp", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "1", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp constant.numeric.decimal.cpp", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #09885A", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #09885A", + "hc_black": "constant.numeric: #B5CEA8" + } + }, + { + "c": ",", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp punctuation.separator.delimiter.comma.cpp", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": " ", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "2", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp constant.numeric.decimal.cpp", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #09885A", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #09885A", + "hc_black": "constant.numeric: #B5CEA8" + } + }, + { + "c": ")", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp punctuation.section.arguments.end.bracket.round.function.call.cpp", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": ";", + "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp punctuation.terminator.statement.cpp", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, { "c": " Rectangle rect", "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp", diff --git a/extensions/css-language-features/client/src/cssMain.ts b/extensions/css-language-features/client/src/cssMain.ts index 1bc18bf6346..936176e9103 100644 --- a/extensions/css-language-features/client/src/cssMain.ts +++ b/extensions/css-language-features/client/src/cssMain.ts @@ -5,8 +5,8 @@ import * as fs from 'fs'; import * as path from 'path'; -import { commands, CompletionItem, CompletionItemKind, ExtensionContext, languages, Position, Range, SnippetString, TextEdit, window, workspace } from 'vscode'; -import { Disposable, LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient'; +import { commands, CompletionItem, CompletionItemKind, ExtensionContext, languages, Position, Range, SnippetString, TextEdit, window, workspace, TextDocument, CompletionContext, CancellationToken, ProviderResult, CompletionList } from 'vscode'; +import { Disposable, LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, ProvideCompletionItemsSignature } from 'vscode-languageclient'; import * as nls from 'vscode-nls'; import { getCustomDataPathsFromAllExtensions, getCustomDataPathsInAllWorkspaces } from './customData'; @@ -43,6 +43,31 @@ export function activate(context: ExtensionContext) { }, initializationOptions: { dataPaths + }, + middleware: { + // testing the replace / insert mode + provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature): ProviderResult { + function updateRanges(item: CompletionItem) { + const range = item.range; + if (range && range.end.isAfter(position) && range.start.isBeforeOrEqual(position)) { + item.range2 = { inserting: new Range(range.start, position), replacing: range }; + item.range = undefined; + } + } + function updateProposals(r: CompletionItem[] | CompletionList | null | undefined): CompletionItem[] | CompletionList | null | undefined { + if (r) { + (Array.isArray(r) ? r : r.items).forEach(updateRanges); + } + return r; + } + const isThenable = (obj: ProviderResult): obj is Thenable => obj && (obj)['then']; + + const r = next(document, position, context, token); + if (isThenable(r)) { + return r.then(updateProposals); + } + return updateProposals(r); + } } }; diff --git a/extensions/css-language-features/package.json b/extensions/css-language-features/package.json index 702d9d755ca..ba1c6f95918 100644 --- a/extensions/css-language-features/package.json +++ b/extensions/css-language-features/package.json @@ -783,6 +783,17 @@ } } ], + "configurationDefaults": { + "[css]": { + "editor.suggest.insertMode": "replace" + }, + "[scss]": { + "editor.suggest.insertMode": "replace" + }, + "[less]": { + "editor.suggest.insertMode": "replace" + } + }, "jsonValidation": [ { "fileMatch": "*.css-data.json", diff --git a/extensions/css-language-features/schemas/package.schema.json b/extensions/css-language-features/schemas/package.schema.json index 3aeca1e040a..cf4193008ec 100644 --- a/extensions/css-language-features/schemas/package.schema.json +++ b/extensions/css-language-features/schemas/package.schema.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "title": "CSS contributions to package.json", "type": "object", "properties": { diff --git a/extensions/css-language-features/server/package.json b/extensions/css-language-features/server/package.json index c5aacd3d41c..66c7e087dcc 100644 --- a/extensions/css-language-features/server/package.json +++ b/extensions/css-language-features/server/package.json @@ -9,7 +9,7 @@ }, "main": "./out/cssServerMain", "dependencies": { - "vscode-css-languageservice": "^4.0.3-next.22", + "vscode-css-languageservice": "^4.0.3-next.24", "vscode-languageserver": "^6.0.0-next.3" }, "devDependencies": { diff --git a/extensions/css-language-features/server/yarn.lock b/extensions/css-language-features/server/yarn.lock index 00e2de5a14b..9196af4c058 100644 --- a/extensions/css-language-features/server/yarn.lock +++ b/extensions/css-language-features/server/yarn.lock @@ -781,10 +781,10 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" -vscode-css-languageservice@^4.0.3-next.22: - version "4.0.3-next.22" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.0.3-next.22.tgz#ddb23a7086807f0e098f069ad91a8e3b7a696cbb" - integrity sha512-j7+S/0LPn/pTe/CQ8WscDttW5J3aY5Ls6bo/Cz8ZdlBjHl7DbrbZ2rEl5Qd6aKbg0ZdgjWVxGqjjgZFzh+O9/w== +vscode-css-languageservice@^4.0.3-next.24: + version "4.0.3-next.24" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.0.3-next.24.tgz#6190fb5af4621b7efc1a0772c1a178d996ffb42d" + integrity sha512-7CPBmaQnvkgL7MXZK9vt5R5CwFaQ9TCqWt9aCi9dGm1g0uAgFOwk8+K5+fV2pasI+aoOvb/kLcFjxSSRD03KdA== dependencies: vscode-languageserver-textdocument "^1.0.0-next.4" vscode-languageserver-types "^3.15.0-next.6" diff --git a/extensions/html-language-features/client/src/htmlMain.ts b/extensions/html-language-features/client/src/htmlMain.ts index 194935b25fc..e8a9a2fa22b 100644 --- a/extensions/html-language-features/client/src/htmlMain.ts +++ b/extensions/html-language-features/client/src/htmlMain.ts @@ -8,13 +8,13 @@ import * as fs from 'fs'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); -import { languages, ExtensionContext, IndentAction, Position, TextDocument, Range, CompletionItem, CompletionItemKind, SnippetString, workspace, Disposable, FormattingOptions, CancellationToken, ProviderResult, TextEdit } from 'vscode'; -import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, RequestType, TextDocumentPositionParams, DocumentRangeFormattingParams, DocumentRangeFormattingRequest } from 'vscode-languageclient'; +import { languages, ExtensionContext, IndentAction, Position, TextDocument, Range, CompletionItem, CompletionItemKind, SnippetString, workspace, Disposable, FormattingOptions, CancellationToken, ProviderResult, TextEdit, CompletionContext, CompletionList } from 'vscode'; +import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, RequestType, TextDocumentPositionParams, DocumentRangeFormattingParams, DocumentRangeFormattingRequest, ProvideCompletionItemsSignature } from 'vscode-languageclient'; import { EMPTY_ELEMENTS } from './htmlEmptyTagsShared'; import { activateTagClosing } from './tagClosing'; import TelemetryReporter from 'vscode-extension-telemetry'; import { getCustomDataPathsInAllWorkspaces, getCustomDataPathsFromAllExtensions } from './customData'; -import { activateMatchingTagPosition as activateMatchingTagSelection } from './matchingTag'; +import { activateMirrorCursor } from './mirrorCursor'; namespace TagCloseRequest { export const type: RequestType = new RequestType('html/tag'); @@ -72,6 +72,31 @@ export function activate(context: ExtensionContext) { dataPaths, provideFormatter: false, // tell the server to not provide formatting capability and ignore the `html.format.enable` setting. }, + middleware: { + // testing the replace / insert mode + provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature): ProviderResult { + function updateRanges(item: CompletionItem) { + const range = item.range; + if (range && range.end.isAfter(position) && range.start.isBeforeOrEqual(position)) { + item.range2 = { inserting: new Range(range.start, position), replacing: range }; + item.range = undefined; + } + } + function updateProposals(r: CompletionItem[] | CompletionList | null | undefined): CompletionItem[] | CompletionList | null | undefined { + if (r) { + (Array.isArray(r) ? r : r.items).forEach(updateRanges); + } + return r; + } + const isThenable = (obj: ProviderResult): obj is Thenable => obj && (obj)['then']; + + const r = next(document, position, context, token); + if (isThenable(r)) { + return r.then(updateProposals); + } + return updateProposals(r); + } + } }; // Create the language client and start the client. @@ -93,7 +118,7 @@ export function activate(context: ExtensionContext) { return client.sendRequest(MatchingTagPositionRequest.type, param); }; - disposable = activateMatchingTagSelection(matchingTagPositionRequestor, { html: true, handlebars: true }, 'html.autoSelectingMatchingTags'); + disposable = activateMirrorCursor(matchingTagPositionRequestor, { html: true, handlebars: true }, 'html.mirrorCursorOnMatchingTag'); toDispose.push(disposable); disposable = client.onTelemetry(e => { diff --git a/extensions/html-language-features/client/src/matchingTag.ts b/extensions/html-language-features/client/src/matchingTag.ts deleted file mode 100644 index 6816c2c5e32..00000000000 --- a/extensions/html-language-features/client/src/matchingTag.ts +++ /dev/null @@ -1,143 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { - window, - workspace, - Disposable, - TextDocument, - Position, - TextEditorSelectionChangeEvent, - Selection, - Range, - WorkspaceEdit -} from 'vscode'; - -export function activateMatchingTagPosition( - matchingTagPositionProvider: (document: TextDocument, position: Position) => Thenable, - supportedLanguages: { [id: string]: boolean }, - configName: string -): Disposable { - let disposables: Disposable[] = []; - - window.onDidChangeTextEditorSelection(event => onDidChangeTextEditorSelection(event), null, disposables); - - let isEnabled = false; - updateEnabledState(); - - window.onDidChangeActiveTextEditor(updateEnabledState, null, disposables); - - function updateEnabledState() { - isEnabled = false; - let editor = window.activeTextEditor; - if (!editor) { - return; - } - let document = editor.document; - if (!supportedLanguages[document.languageId]) { - return; - } - if (!workspace.getConfiguration(undefined, document.uri).get(configName)) { - return; - } - isEnabled = true; - } - - let prevCursorCount = 0; - let cursorCount = 0; - let inMirrorMode = false; - - function onDidChangeTextEditorSelection(event: TextEditorSelectionChangeEvent) { - if (!isEnabled) { - return; - } - - prevCursorCount = cursorCount; - cursorCount = event.selections.length; - - if (cursorCount === 1) { - if (inMirrorMode && prevCursorCount === 2) { - return; - } - if (event.selections[0].isEmpty) { - matchingTagPositionProvider(event.textEditor.document, event.selections[0].active).then(position => { - if (position && window.activeTextEditor) { - inMirrorMode = true; - const newCursor = new Selection(position.line, position.character, position.line, position.character); - window.activeTextEditor.selections = [...window.activeTextEditor.selections, newCursor]; - } - }); - } - } - - if (cursorCount === 2 && inMirrorMode) { - // Check two cases - if (event.selections[0].isEmpty && event.selections[1].isEmpty) { - const charBeforePrimarySelection = getCharBefore(event.textEditor.document, event.selections[0].anchor); - const charAfterPrimarySelection = getCharAfter(event.textEditor.document, event.selections[0].anchor); - const charBeforeSecondarySelection = getCharBefore(event.textEditor.document, event.selections[1].anchor); - const charAfterSecondarySelection = getCharAfter(event.textEditor.document, event.selections[1].anchor); - - // Exit mirror mode when cursor position no longer mirror - // Unless it's in the case of `<|>` - const charBeforeBothPositionRoughlyEqual = - charBeforePrimarySelection === charBeforeSecondarySelection || - (charBeforePrimarySelection === '/' && charBeforeSecondarySelection === '<') || - (charBeforeSecondarySelection === '/' && charBeforePrimarySelection === '<'); - const charAfterBothPositionRoughlyEqual = - charAfterPrimarySelection === charAfterSecondarySelection || - (charAfterPrimarySelection === ' ' && charAfterSecondarySelection === '>') || - (charAfterSecondarySelection === ' ' && charAfterPrimarySelection === '>'); - - if (!charBeforeBothPositionRoughlyEqual || !charAfterBothPositionRoughlyEqual) { - inMirrorMode = false; - window.activeTextEditor!.selections = [window.activeTextEditor!.selections[0]]; - return; - } else { - // Need to cleanup in the case of
- if ( - charBeforePrimarySelection === ' ' && - charAfterPrimarySelection === '>' && - charBeforeSecondarySelection === ' ' && - charAfterSecondarySelection === '>' - ) { - const primaryBeforeSecondary = - event.textEditor.document.offsetAt(event.selections[0].anchor) < - event.textEditor.document.offsetAt(event.selections[1].anchor); - - if (primaryBeforeSecondary) { - inMirrorMode = false; - const cleanupEdit = new WorkspaceEdit(); - const cleanupRange = new Range(event.selections[1].anchor.translate(0, -1), event.selections[1].anchor); - cleanupEdit.replace(event.textEditor.document.uri, cleanupRange, ''); - window.activeTextEditor!.selections = [window.activeTextEditor!.selections[0]]; - workspace.applyEdit(cleanupEdit); - } - } - } - } - } - } - - return Disposable.from(...disposables); -} - -function getCharBefore(document: TextDocument, position: Position) { - const offset = document.offsetAt(position); - if (offset === 0) { - return ''; - } - - return document.getText(new Range(document.positionAt(offset - 1), position)); -} - -function getCharAfter(document: TextDocument, position: Position) { - const offset = document.offsetAt(position); - if (offset === document.getText().length) { - return ''; - } - - return document.getText(new Range(position, document.positionAt(offset + 1))); -} diff --git a/extensions/html-language-features/client/src/mirrorCursor.ts b/extensions/html-language-features/client/src/mirrorCursor.ts new file mode 100644 index 00000000000..43ea3169c97 --- /dev/null +++ b/extensions/html-language-features/client/src/mirrorCursor.ts @@ -0,0 +1,233 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + window, + workspace, + Disposable, + TextDocument, + Position, + TextEditorSelectionChangeEvent, + Selection, + Range, + WorkspaceEdit +} from 'vscode'; + +export function activateMirrorCursor( + matchingTagPositionProvider: (document: TextDocument, position: Position) => Thenable, + supportedLanguages: { [id: string]: boolean }, + configName: string +): Disposable { + let disposables: Disposable[] = []; + + window.onDidChangeTextEditorSelection(event => onDidChangeTextEditorSelection(event), null, disposables); + + let isEnabled = false; + updateEnabledState(); + + window.onDidChangeActiveTextEditor(updateEnabledState, null, disposables); + + function updateEnabledState() { + isEnabled = false; + let editor = window.activeTextEditor; + if (!editor) { + return; + } + let document = editor.document; + if (!supportedLanguages[document.languageId]) { + return; + } + if (!workspace.getConfiguration(undefined, document.uri).get(configName)) { + return; + } + isEnabled = true; + } + + let prevCursors: readonly Selection[] = []; + let cursors: readonly Selection[] = []; + let inMirrorMode = false; + + function onDidChangeTextEditorSelection(event: TextEditorSelectionChangeEvent) { + if (!isEnabled) { + return; + } + + prevCursors = cursors; + cursors = event.selections; + + if (cursors.length === 1) { + if (inMirrorMode && prevCursors.length === 2) { + if (cursors[0].isEqual(prevCursors[0]) || cursors[0].isEqual(prevCursors[1])) { + return; + } + } + if (event.selections[0].isEmpty) { + matchingTagPositionProvider(event.textEditor.document, event.selections[0].active).then(matchingTagPosition => { + if (matchingTagPosition && window.activeTextEditor) { + const charBeforeAndAfterPositionsRoughtlyEqual = isCharBeforeAndAfterPositionsRoughtlyEqual( + event.textEditor.document, + event.selections[0].anchor, + new Position(matchingTagPosition.line, matchingTagPosition.character) + ); + + if (charBeforeAndAfterPositionsRoughtlyEqual) { + inMirrorMode = true; + const newCursor = new Selection( + matchingTagPosition.line, + matchingTagPosition.character, + matchingTagPosition.line, + matchingTagPosition.character + ); + window.activeTextEditor.selections = [...window.activeTextEditor.selections, newCursor]; + } + } + }); + } + } + + const exitMirrorMode = () => { + inMirrorMode = false; + window.activeTextEditor!.selections = [window.activeTextEditor!.selections[0]]; + }; + + if (cursors.length === 2 && inMirrorMode) { + if (event.selections[0].isEmpty && event.selections[1].isEmpty) { + if ( + prevCursors.length === 2 && + event.selections[0].anchor.line !== prevCursors[0].anchor.line && + event.selections[1].anchor.line !== prevCursors[0].anchor.line + ) { + exitMirrorMode(); + return; + } + + const charBeforeAndAfterPositionsRoughtlyEqual = isCharBeforeAndAfterPositionsRoughtlyEqual( + event.textEditor.document, + event.selections[0].anchor, + event.selections[1].anchor + ); + + if (!charBeforeAndAfterPositionsRoughtlyEqual) { + exitMirrorMode(); + return; + } else { + // Need to cleanup in the case of
+ if ( + shouldDoCleanupForHtmlAttributeInput( + event.textEditor.document, + event.selections[0].anchor, + event.selections[1].anchor + ) + ) { + const cleanupEdit = new WorkspaceEdit(); + const cleanupRange = new Range(event.selections[1].anchor.translate(0, -1), event.selections[1].anchor); + cleanupEdit.replace(event.textEditor.document.uri, cleanupRange, ''); + exitMirrorMode(); + workspace.applyEdit(cleanupEdit); + } + } + } + } + } + + return Disposable.from(...disposables); +} + +function getCharBefore(document: TextDocument, position: Position) { + const offset = document.offsetAt(position); + if (offset === 0) { + return ''; + } + + return document.getText(new Range(document.positionAt(offset - 1), position)); +} + +function getCharAfter(document: TextDocument, position: Position) { + const offset = document.offsetAt(position); + if (offset === document.getText().length) { + return ''; + } + + return document.getText(new Range(position, document.positionAt(offset + 1))); +} + +// Check if chars before and after the two positions are equal +// For the chars before, `<` and `/` are consiered equal to handle the case of `<|>` +function isCharBeforeAndAfterPositionsRoughtlyEqual(document: TextDocument, firstPos: Position, secondPos: Position) { + const charBeforePrimarySelection = getCharBefore(document, firstPos); + const charAfterPrimarySelection = getCharAfter(document, firstPos); + const charBeforeSecondarySelection = getCharBefore(document, secondPos); + const charAfterSecondarySelection = getCharAfter(document, secondPos); + + /** + * Special case for exiting + * |
+ * |
+ */ + if ( + charBeforePrimarySelection === ' ' && + charBeforeSecondarySelection === ' ' && + charAfterPrimarySelection === '<' && + charAfterSecondarySelection === '<' + ) { + return false; + } + /** + * Special case for exiting + * |
+ * |
+ */ + if (charBeforePrimarySelection === '\n' && charBeforeSecondarySelection === '\n') { + return false; + } + /** + * Special case for exiting + *
| + *
| + */ + if (charAfterPrimarySelection === '\n' && charAfterSecondarySelection === '\n') { + return false; + } + + // Exit mirror mode when cursor position no longer mirror + // Unless it's in the case of `<|>` + const charBeforeBothPositionRoughlyEqual = + charBeforePrimarySelection === charBeforeSecondarySelection || + (charBeforePrimarySelection === '/' && charBeforeSecondarySelection === '<') || + (charBeforeSecondarySelection === '/' && charBeforePrimarySelection === '<'); + const charAfterBothPositionRoughlyEqual = + charAfterPrimarySelection === charAfterSecondarySelection || + (charAfterPrimarySelection === ' ' && charAfterSecondarySelection === '>') || + (charAfterSecondarySelection === ' ' && charAfterPrimarySelection === '>'); + + return charBeforeBothPositionRoughlyEqual && charAfterBothPositionRoughlyEqual; +} + +function shouldDoCleanupForHtmlAttributeInput(document: TextDocument, firstPos: Position, secondPos: Position) { + // Need to cleanup in the case of
+ const charBeforePrimarySelection = getCharBefore(document, firstPos); + const charAfterPrimarySelection = getCharAfter(document, firstPos); + const charBeforeSecondarySelection = getCharBefore(document, secondPos); + const charAfterSecondarySelection = getCharAfter(document, secondPos); + + const primaryBeforeSecondary = document.offsetAt(firstPos) < document.offsetAt(secondPos); + + /** + * Check two cases + *
+ *
+ * Before 1st cursor: ` ` + * After 1st cursor: `>` or ` ` + * Before 2nd cursor: ` ` + * After 2nd cursor: `>` + */ + return ( + primaryBeforeSecondary && + charBeforePrimarySelection === ' ' && + (charAfterPrimarySelection === '>' || charAfterPrimarySelection === ' ') && + charBeforeSecondarySelection === ' ' && + charAfterSecondarySelection === '>' + ); +} diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index a641eb4ca46..696c2dda852 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -161,11 +161,11 @@ "default": true, "description": "%html.autoClosingTags%" }, - "html.autoSelectingMatchingTags": { + "html.mirrorCursorOnMatchingTag": { "type": "boolean", "scope": "resource", "default": true, - "description": "%html.autoSelectingMatchingTags%" + "description": "%html.mirrorCursorOnMatchingTag%" }, "html.trace.server": { "type": "string", @@ -180,6 +180,14 @@ } } }, + "configurationDefaults": { + "[html]": { + "editor.suggest.insertMode": "replace" + }, + "[handlebars]": { + "editor.suggest.insertMode": "replace" + } + }, "jsonValidation": [ { "fileMatch": "*.html-data.json", diff --git a/extensions/html-language-features/package.nls.json b/extensions/html-language-features/package.nls.json index 4e4d666b9f7..8fa34d25270 100644 --- a/extensions/html-language-features/package.nls.json +++ b/extensions/html-language-features/package.nls.json @@ -25,5 +25,5 @@ "html.validate.scripts": "Controls whether the built-in HTML language support validates embedded scripts.", "html.validate.styles": "Controls whether the built-in HTML language support validates embedded styles.", "html.autoClosingTags": "Enable/disable autoclosing of HTML tags.", - "html.autoSelectingMatchingTags": "Enable/disable auto selecting matching HTML tags." + "html.mirrorCursorOnMatchingTag": "Enable/disable mirroring cursor on matching HTML tag." } diff --git a/extensions/html-language-features/schemas/package.schema.json b/extensions/html-language-features/schemas/package.schema.json index aaf9588221e..a11810ef090 100644 --- a/extensions/html-language-features/schemas/package.schema.json +++ b/extensions/html-language-features/schemas/package.schema.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "title": "HTML contributions to package.json", "type": "object", "properties": { diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index 2cc1a5b2323..7060461f908 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -9,7 +9,7 @@ }, "main": "./out/htmlServerMain", "dependencies": { - "vscode-css-languageservice": "^4.0.3-next.22", + "vscode-css-languageservice": "^4.0.3-next.24", "vscode-html-languageservice": "^3.0.4-next.11", "vscode-languageserver": "^6.0.0-next.3", "vscode-nls": "^4.1.1", diff --git a/extensions/html-language-features/server/yarn.lock b/extensions/html-language-features/server/yarn.lock index 3185f2943f6..c42d15d833e 100644 --- a/extensions/html-language-features/server/yarn.lock +++ b/extensions/html-language-features/server/yarn.lock @@ -611,10 +611,10 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" -vscode-css-languageservice@^4.0.3-next.22: - version "4.0.3-next.22" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.0.3-next.22.tgz#ddb23a7086807f0e098f069ad91a8e3b7a696cbb" - integrity sha512-j7+S/0LPn/pTe/CQ8WscDttW5J3aY5Ls6bo/Cz8ZdlBjHl7DbrbZ2rEl5Qd6aKbg0ZdgjWVxGqjjgZFzh+O9/w== +vscode-css-languageservice@^4.0.3-next.24: + version "4.0.3-next.24" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.0.3-next.24.tgz#6190fb5af4621b7efc1a0772c1a178d996ffb42d" + integrity sha512-7CPBmaQnvkgL7MXZK9vt5R5CwFaQ9TCqWt9aCi9dGm1g0uAgFOwk8+K5+fV2pasI+aoOvb/kLcFjxSSRD03KdA== dependencies: vscode-languageserver-textdocument "^1.0.0-next.4" vscode-languageserver-types "^3.15.0-next.6" diff --git a/extensions/image-preview/README.md b/extensions/image-preview/README.md index ccb5ac3954a..d3f0bd6cb6c 100644 --- a/extensions/image-preview/README.md +++ b/extensions/image-preview/README.md @@ -13,5 +13,4 @@ Supported image formats: - `*.bmp` - `*.gif` - `*.ico` -- `*.tga` - `*.webp` diff --git a/extensions/image-preview/package.json b/extensions/image-preview/package.json index 8be8486a54b..a0664f98e3d 100644 --- a/extensions/image-preview/package.json +++ b/extensions/image-preview/package.json @@ -2,7 +2,7 @@ "name": "image-preview", "displayName": "%displayName%", "description": "%description%", - "extensionKind": "ui", + "extensionKind": ["ui", "workspace"], "version": "1.0.0", "publisher": "vscode", "icon": "icon.png", diff --git a/extensions/json-language-features/client/src/jsonMain.ts b/extensions/json-language-features/client/src/jsonMain.ts index d7c6684b3f2..32c0d422643 100644 --- a/extensions/json-language-features/client/src/jsonMain.ts +++ b/extensions/json-language-features/client/src/jsonMain.ts @@ -10,8 +10,16 @@ import { xhr, XHRResponse, getErrorStatusDescription } from 'request-light'; const localize = nls.loadMessageBundle(); -import { workspace, window, languages, commands, ExtensionContext, extensions, Uri, LanguageConfiguration, Diagnostic, StatusBarAlignment, TextEditor, TextDocument, FormattingOptions, CancellationToken, ProviderResult, TextEdit, Range, Disposable } from 'vscode'; -import { LanguageClient, LanguageClientOptions, RequestType, ServerOptions, TransportKind, NotificationType, DidChangeConfigurationNotification, HandleDiagnosticsSignature, ResponseError, DocumentRangeFormattingParams, DocumentRangeFormattingRequest } from 'vscode-languageclient'; +import { + workspace, window, languages, commands, ExtensionContext, extensions, Uri, LanguageConfiguration, + Diagnostic, StatusBarAlignment, TextEditor, TextDocument, FormattingOptions, CancellationToken, + ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext +} from 'vscode'; +import { + LanguageClient, LanguageClientOptions, RequestType, ServerOptions, TransportKind, NotificationType, + DidChangeConfigurationNotification, HandleDiagnosticsSignature, ResponseError, DocumentRangeFormattingParams, + DocumentRangeFormattingRequest, ProvideCompletionItemsSignature +} from 'vscode-languageclient'; import TelemetryReporter from 'vscode-extension-telemetry'; import { hash } from './utils/hash'; @@ -36,6 +44,10 @@ namespace SchemaAssociationNotification { export const type: NotificationType = new NotificationType('json/schemaAssociations'); } +namespace ResultLimitReachedNotification { + export const type: NotificationType = new NotificationType('json/resultLimitReached'); +} + interface IPackageInfo { name: string; version: string; @@ -132,6 +144,29 @@ export function activate(context: ExtensionContext) { } next(uri, diagnostics); + }, + // testing the replace / insert mode + provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature): ProviderResult { + function updateRanges(item: CompletionItem) { + const range = item.range; + if (range && range.end.isAfter(position) && range.start.isBeforeOrEqual(position)) { + item.range2 = { inserting: new Range(range.start, position), replacing: range }; + item.range = undefined; + } + } + function updateProposals(r: CompletionItem[] | CompletionList | null | undefined): CompletionItem[] | CompletionList | null | undefined { + if (r) { + (Array.isArray(r) ? r : r.items).forEach(updateRanges); + } + return r; + } + const isThenable = (obj: ProviderResult): obj is Thenable => obj && (obj)['then']; + + const r = next(document, position, context, token); + if (isThenable(r)) { + return r.then(updateProposals); + } + return updateProposals(r); } } }; @@ -239,6 +274,12 @@ export function activate(context: ExtensionContext) { updateFormatterRegistration(); toDispose.push({ dispose: () => rangeFormatting && rangeFormatting.dispose() }); toDispose.push(workspace.onDidChangeConfiguration(e => e.affectsConfiguration('html.format.enable') && updateFormatterRegistration())); + + + client.onNotification(ResultLimitReachedNotification.type, message => { + window.showInformationMessage(`${message}\nUse setting 'json.maxItemsComputed' to configure the limit.`); + }); + }); let languageConfiguration: LanguageConfiguration = { @@ -320,6 +361,8 @@ function getSchemaAssociation(_context: ExtensionContext): ISchemaAssociations { function getSettings(): Settings { let httpSettings = workspace.getConfiguration('http'); + let resultLimit: number = Math.trunc(Math.max(0, Number(workspace.getConfiguration().get('json.maxItemsComputed')))) || 5000; + let settings: Settings = { http: { proxy: httpSettings.get('proxy'), @@ -327,7 +370,7 @@ function getSettings(): Settings { }, json: { schemas: [], - resultLimit: 5000 + resultLimit } }; let schemaSettingsById: { [schemaId: string]: JSONSchemaSettings } = Object.create(null); @@ -422,5 +465,4 @@ function readJSONFile(location: string) { console.log(`Problems reading ${location}: ${e}`); return {}; } - } diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index 833059f3c83..09f7c6c1643 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -1,113 +1,125 @@ { - "name": "json-language-features", - "displayName": "%displayName%", - "description": "%description%", - "version": "1.0.0", - "publisher": "vscode", - "license": "MIT", - "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", - "engines": { - "vscode": "0.10.x" - }, - "icon": "icons/json.png", - "activationEvents": [ - "onLanguage:json", - "onLanguage:jsonc" - ], - "main": "./client/out/jsonMain", - "enableProposedApi": true, - "scripts": { - "compile": "gulp compile-extension:json-language-features-client compile-extension:json-language-features-server", - "watch": "gulp watch-extension:json-language-features-client watch-extension:json-language-features-server", - "postinstall": "cd server && yarn install", - "install-client-next": "yarn add vscode-languageclient@next" - }, - "categories": [ - "Programming Languages" - ], - "contributes": { - "configuration": { - "id": "json", - "order": 20, - "type": "object", - "title": "JSON", - "properties": { - "json.schemas": { - "type": "array", - "scope": "resource", - "description": "%json.schemas.desc%", - "items": { - "type": "object", - "default": { - "fileMatch": [ - "/myfile" - ], - "url": "schemaURL" - }, - "properties": { - "url": { - "type": "string", - "default": "/user.schema.json", - "description": "%json.schemas.url.desc%" - }, - "fileMatch": { - "type": "array", - "items": { - "type": "string", - "default": "MyFile.json", - "description": "%json.schemas.fileMatch.item.desc%" - }, - "minItems": 1, - "description": "%json.schemas.fileMatch.desc%" - }, - "schema": { - "$ref": "http://json-schema.org/draft-07/schema#", - "description": "%json.schemas.schema.desc%" - } - } - } - }, - "json.format.enable": { - "type": "boolean", - "scope": "window", - "default": true, - "description": "%json.format.enable.desc%" - }, - "json.trace.server": { - "type": "string", - "scope": "window", - "enum": [ - "off", - "messages", - "verbose" - ], - "default": "off", - "description": "%json.tracing.desc%" - }, - "json.colorDecorators.enable": { - "type": "boolean", - "scope": "window", - "default": true, - "description": "%json.colorDecorators.enable.desc%", - "deprecationMessage": "%json.colorDecorators.enable.deprecationMessage%" - } - } + "name": "json-language-features", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "license": "MIT", + "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", + "engines": { + "vscode": "0.10.x" }, - "configurationDefaults": { - "[json]": { - "editor.quickSuggestions": { - "strings": true + "icon": "icons/json.png", + "activationEvents": [ + "onLanguage:json", + "onLanguage:jsonc" + ], + "main": "./client/out/jsonMain", + "enableProposedApi": true, + "scripts": { + "compile": "gulp compile-extension:json-language-features-client compile-extension:json-language-features-server", + "watch": "gulp watch-extension:json-language-features-client watch-extension:json-language-features-server", + "postinstall": "cd server && yarn install", + "install-client-next": "yarn add vscode-languageclient@next" + }, + "categories": [ + "Programming Languages" + ], + "contributes": { + "configuration": { + "id": "json", + "order": 20, + "type": "object", + "title": "JSON", + "properties": { + "json.schemas": { + "type": "array", + "scope": "resource", + "description": "%json.schemas.desc%", + "items": { + "type": "object", + "default": { + "fileMatch": [ + "/myfile" + ], + "url": "schemaURL" + }, + "properties": { + "url": { + "type": "string", + "default": "/user.schema.json", + "description": "%json.schemas.url.desc%" + }, + "fileMatch": { + "type": "array", + "items": { + "type": "string", + "default": "MyFile.json", + "description": "%json.schemas.fileMatch.item.desc%" + }, + "minItems": 1, + "description": "%json.schemas.fileMatch.desc%" + }, + "schema": { + "$ref": "http://json-schema.org/draft-07/schema#", + "description": "%json.schemas.schema.desc%" + } + } + } + }, + "json.format.enable": { + "type": "boolean", + "scope": "window", + "default": true, + "description": "%json.format.enable.desc%" + }, + "json.trace.server": { + "type": "string", + "scope": "window", + "enum": [ + "off", + "messages", + "verbose" + ], + "default": "off", + "description": "%json.tracing.desc%" + }, + "json.colorDecorators.enable": { + "type": "boolean", + "scope": "window", + "default": true, + "description": "%json.colorDecorators.enable.desc%", + "deprecationMessage": "%json.colorDecorators.enable.deprecationMessage%" + }, + "json.maxItemsComputed": { + "type": "number", + "default": 5000, + "description": "%json.maxItemsComputed.desc%" + } + } + }, + "configurationDefaults": { + "[json]": { + "editor.quickSuggestions": { + "strings": true + }, + "editor.suggest.insertMode": "replace" + }, + "[jsonc]": { + "editor.quickSuggestions": { + "strings": true + }, + "editor.suggest.insertMode": "replace" + } } - } + }, + "dependencies": { + "request-light": "^0.2.5", + "vscode-extension-telemetry": "0.1.1", + "vscode-languageclient": "^6.0.0-next.3", + "vscode-nls": "^4.1.1" + }, + "devDependencies": { + "@types/node": "^12.11.7" } - }, - "dependencies": { - "request-light": "^0.2.5", - "vscode-extension-telemetry": "0.1.1", - "vscode-languageclient": "^6.0.0-next.3", - "vscode-nls": "^4.1.1" - }, - "devDependencies": { - "@types/node": "^12.11.7" - } } diff --git a/extensions/json-language-features/package.nls.json b/extensions/json-language-features/package.nls.json index c61e7b70e8f..5d132ccd776 100644 --- a/extensions/json-language-features/package.nls.json +++ b/extensions/json-language-features/package.nls.json @@ -11,5 +11,6 @@ "json.colorDecorators.enable.desc": "Enables or disables color decorators", "json.colorDecorators.enable.deprecationMessage": "The setting `json.colorDecorators.enable` has been deprecated in favor of `editor.colorDecorators`.", "json.schemaResolutionErrorMessage": "Unable to resolve schema.", - "json.clickToRetry": "Click to retry." + "json.clickToRetry": "Click to retry.", + "json.maxItemsComputed.desc": "The maximum number of outline symbols and folding regions computed (limited for performance reasons)." } diff --git a/extensions/json-language-features/server/README.md b/extensions/json-language-features/server/README.md index 71356e3c441..66fd8437d6a 100644 --- a/extensions/json-language-features/server/README.md +++ b/extensions/json-language-features/server/README.md @@ -63,6 +63,7 @@ The server supports the following settings: - `fileMatch`: an array of file names or paths (separated by `/`). `*` can be used as a wildcard. - `url`: The URL of the schema, optional when also a schema is provided. - `schema`: The schema content. + - `resultLimit`: The max number foldig ranges and otline symbols to be computed (for performance reasons) ```json { @@ -153,6 +154,16 @@ Notification: - method: 'json/schemaContent' - params: `string` the URL of the schema that has changed. +### Item Limit + +If the setting `resultLimit` is set, the JSON language server will limit the number of folding ranges and document symbols computed. +When the limit is reached, a notification `json/resultLimitReached` is sent that can be shown that camn be shown to the user. + +Notification: +- method: 'json/resultLimitReached' +- params: a human readable string to show to the user. + + ## Try The JSON language server is shipped with [Visual Studio Code](https://code.visualstudio.com/) as part of the built-in VSCode extension `json-language-features`. The server is started when the first JSON file is opened. The [VSCode JSON documentation](https://code.visualstudio.com/docs/languages/json) for detailed information on the user experience and has more information on how to configure the language support. diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index 6dc132cc742..64a9c5f5eba 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -14,7 +14,7 @@ "dependencies": { "jsonc-parser": "^2.2.0", "request-light": "^0.2.5", - "vscode-json-languageservice": "^3.4.7", + "vscode-json-languageservice": "^3.4.9", "vscode-languageserver": "^6.0.0-next.3", "vscode-uri": "^2.1.1" }, diff --git a/extensions/json-language-features/server/src/jsonServerMain.ts b/extensions/json-language-features/server/src/jsonServerMain.ts index 975f27b481d..4b058d12350 100644 --- a/extensions/json-language-features/server/src/jsonServerMain.ts +++ b/extensions/json-language-features/server/src/jsonServerMain.ts @@ -35,6 +35,10 @@ namespace SchemaContentChangeNotification { export const type: NotificationType = new NotificationType('json/schemaContent'); } +namespace ResultLimitReachedNotification { + export const type: NotificationType = new NotificationType('json/resultLimitReached'); +} + namespace ForceValidateRequest { export const type: RequestType = new RequestType('json/validate'); } @@ -211,7 +215,7 @@ namespace LimitExceededWarnings { } else { warning = { features: { [name]: name } }; warning.timeout = setTimeout(() => { - connection.window.showInformationMessage(`${posix.basename(uri)}: For performance reasons, ${Object.keys(warning.features).join(' and ')} have been limited to ${resultLimit} items.`); + connection.sendNotification(ResultLimitReachedNotification.type, `${posix.basename(uri)}: For performance reasons, ${Object.keys(warning.features).join(' and ')} have been limited to ${resultLimit} items.`); warning.timeout = undefined; }, 2000); pendingWarnings[uri] = warning; diff --git a/extensions/json-language-features/server/yarn.lock b/extensions/json-language-features/server/yarn.lock index f0fde79ab0c..7ebc93341c6 100644 --- a/extensions/json-language-features/server/yarn.lock +++ b/extensions/json-language-features/server/yarn.lock @@ -80,10 +80,10 @@ request-light@^0.2.5: https-proxy-agent "^2.2.3" vscode-nls "^4.1.1" -vscode-json-languageservice@^3.4.7: - version "3.4.7" - resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-3.4.7.tgz#8d85f3c1d46a1e58e9867d747552fb8c83d934fd" - integrity sha512-y3MN2+/yph3yoIHGmHu4ScYpm285L58XVvfGkd49xTQzLja4apxSbwzsYcP9QsqS0W7KuvoyiPhqksiudoMwjg== +vscode-json-languageservice@^3.4.9: + version "3.4.9" + resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-3.4.9.tgz#7ce485bb0f9a07b4d879c988baac9be2222909ad" + integrity sha512-4VCpZ9ooea/Zc/MTnj1ccc9C7rqcoinKVQLhLoi6jw6yueSf4y4tg/YIUiPPVMlEAG7ZCPS+NVmqxisQ+mOsSw== dependencies: jsonc-parser "^2.2.0" vscode-languageserver-textdocument "^1.0.0-next.4" diff --git a/extensions/markdown-language-features/schemas/package.schema.json b/extensions/markdown-language-features/schemas/package.schema.json index e76ce046c27..5591d0b0032 100644 --- a/extensions/markdown-language-features/schemas/package.schema.json +++ b/extensions/markdown-language-features/schemas/package.schema.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "title": "Markdown contributions to package.json", "type": "object", "properties": { @@ -29,4 +29,4 @@ } } } -} \ No newline at end of file +} diff --git a/extensions/markdown-language-features/src/markdownEngine.ts b/extensions/markdown-language-features/src/markdownEngine.ts index d3b88f06c42..33cb220b449 100644 --- a/extensions/markdown-language-features/src/markdownEngine.ts +++ b/extensions/markdown-language-features/src/markdownEngine.ts @@ -242,8 +242,11 @@ export class MarkdownEngine { if (uri.path[0] === '/') { const root = vscode.workspace.getWorkspaceFolder(this.currentDocument!); if (root) { - uri = uri.with({ - path: path.join(root.uri.fsPath, uri.path), + const fileUri = vscode.Uri.file(path.join(root.uri.fsPath, uri.fsPath)); + uri = fileUri.with({ + scheme: uri.scheme, + fragment: uri.fragment, + query: uri.query, }); } } diff --git a/extensions/npm/src/features/jsonContributions.ts b/extensions/npm/src/features/jsonContributions.ts index e59b7866ec3..af91f401201 100644 --- a/extensions/npm/src/features/jsonContributions.ts +++ b/extensions/npm/src/features/jsonContributions.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Location, getLocation, createScanner, SyntaxKind, ScanError } from 'jsonc-parser'; +import { Location, getLocation, createScanner, SyntaxKind, ScanError, JSONScanner } from 'jsonc-parser'; import { basename } from 'path'; import { BowerJSONContribution } from './bowerJSONContribution'; import { PackageJSONContribution } from './packageJSONContribution'; @@ -111,7 +111,7 @@ export class JSONCompletionItemProvider implements CompletionItemProvider { add: (suggestion: CompletionItem) => { if (!proposed[suggestion.label]) { proposed[suggestion.label] = true; - suggestion.range = overwriteRange; + suggestion.range2 = { replacing: overwriteRange, inserting: new Range(overwriteRange.start, overwriteRange.start) }; items.push(suggestion); } }, @@ -123,8 +123,9 @@ export class JSONCompletionItemProvider implements CompletionItemProvider { let collectPromise: Thenable | null = null; if (location.isAtPropertyKey) { - const addValue = !location.previousNode || !location.previousNode.colonOffset; - const isLast = this.isLast(document, position); + const scanner = createScanner(document.getText(), true); + const addValue = !location.previousNode || !this.hasColonAfter(scanner, location.previousNode.offset + location.previousNode.length); + const isLast = this.isLast(scanner, document.offsetAt(position)); collectPromise = this.jsonContribution.collectPropertySuggestions(fileName, location, currentWord, addValue, isLast, collector); } else { if (location.path.length === 0) { @@ -153,15 +154,19 @@ export class JSONCompletionItemProvider implements CompletionItemProvider { return text.substring(i + 1, position.character); } - private isLast(document: TextDocument, position: Position): boolean { - const scanner = createScanner(document.getText(), true); - scanner.setPosition(document.offsetAt(position)); + private isLast(scanner: JSONScanner, offset: number): boolean { + scanner.setPosition(offset); let nextToken = scanner.scan(); if (nextToken === SyntaxKind.StringLiteral && scanner.getTokenError() === ScanError.UnexpectedEndOfString) { nextToken = scanner.scan(); } return nextToken === SyntaxKind.CloseBraceToken || nextToken === SyntaxKind.EOF; } + private hasColonAfter(scanner: JSONScanner, offset: number): boolean { + scanner.setPosition(offset); + return scanner.scan() === SyntaxKind.ColonToken; + } + } export const xhrDisabled = () => Promise.reject({ responseText: 'Use of online resources is disabled.' }); diff --git a/extensions/package.json b/extensions/package.json index d1030bbe6dd..65d512cb8a5 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "3.7.3-insiders.20191123" + "typescript": "3.7.3" }, "scripts": { "postinstall": "node ./postinstall" diff --git a/extensions/search-result/package.json b/extensions/search-result/package.json index bc7c6cf951c..5e7482b6033 100644 --- a/extensions/search-result/package.json +++ b/extensions/search-result/package.json @@ -19,6 +19,11 @@ "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:search-result ./tsconfig.json" }, "contributes": { + "configurationDefaults": { + "[search-result]": { + "editor.lineNumbers": "off" + } + }, "commands": [ { "command": "searchResult.rerunSearch", @@ -28,6 +33,15 @@ "light": "./src/media/refresh-light.svg", "dark": "./src/media/refresh-dark.svg" } + }, + { + "command": "searchResult.rerunSearchWithContext", + "title": "%searchResult.rerunSearchWithContext.title%", + "category": "Search Result", + "icon": { + "light": "./src/media/refresh-light.svg", + "dark": "./src/media/refresh-dark.svg" + } } ], "menus": { @@ -35,6 +49,7 @@ { "command": "searchResult.rerunSearch", "when": "editorLangId == search-result", + "alt": "searchResult.rerunSearchWithContext", "group": "navigation" } ] diff --git a/extensions/search-result/package.nls.json b/extensions/search-result/package.nls.json index 694f6b61d80..ce90d23c09c 100644 --- a/extensions/search-result/package.nls.json +++ b/extensions/search-result/package.nls.json @@ -1,5 +1,6 @@ { "displayName": "Search Result", "description": "Provides syntax highlighting and language features for tabbed search results.", - "searchResult.rerunSearch.title": "Search Again" + "searchResult.rerunSearch.title": "Search Again", + "searchResult.rerunSearchWithContext.title": "Search Again (With Context)" } diff --git a/extensions/search-result/src/extension.ts b/extensions/search-result/src/extension.ts index ec90cd28fbd..6956a63d329 100644 --- a/extensions/search-result/src/extension.ts +++ b/extensions/search-result/src/extension.ts @@ -7,81 +7,93 @@ import * as vscode from 'vscode'; import * as pathUtils from 'path'; const FILE_LINE_REGEX = /^(\S.*):$/; -const RESULT_LINE_REGEX = /^(\s+)(\d+):(\s+)(.*)$/; +const RESULT_LINE_REGEX = /^(\s+)(\d+)(:| )(\s+)(.*)$/; const SEARCH_RESULT_SELECTOR = { language: 'search-result' }; +const DIRECTIVES = ['# Query:', '# Flags:', '# Including:', '# Excluding:', '# ContextLines:']; +const FLAGS = ['RegExp', 'CaseSensitive', 'IgnoreExcludeSettings', 'WordMatch']; let cachedLastParse: { version: number, parse: ParsedSearchResults } | undefined; -export function activate() { +export function activate(context: vscode.ExtensionContext) { + context.subscriptions.push( + vscode.commands.registerCommand('searchResult.rerunSearch', () => vscode.commands.executeCommand('search.action.rerunEditorSearch')), + vscode.commands.registerCommand('searchResult.rerunSearchWithContext', () => vscode.commands.executeCommand('search.action.rerunEditorSearchWithContext')), - vscode.commands.registerCommand('searchResult.rerunSearch', () => vscode.commands.executeCommand('search.action.rerunEditorSearch')); + vscode.languages.registerDocumentSymbolProvider(SEARCH_RESULT_SELECTOR, { + provideDocumentSymbols(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.DocumentSymbol[] { + const results = parseSearchResults(document, token) + .filter(isFileLine) + .map(line => new vscode.DocumentSymbol( + line.path, + '', + vscode.SymbolKind.File, + line.allLocations.map(({ originSelectionRange }) => originSelectionRange!).reduce((p, c) => p.union(c), line.location.originSelectionRange!), + line.location.originSelectionRange!, + )); - vscode.languages.registerDocumentSymbolProvider(SEARCH_RESULT_SELECTOR, { - provideDocumentSymbols(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.DocumentSymbol[] { - const results = parseSearchResults(document, token) - .filter(isFileLine) - .map(line => new vscode.DocumentSymbol( - line.path, - '', - vscode.SymbolKind.File, - line.allLocations.map(({ originSelectionRange }) => originSelectionRange!).reduce((p, c) => p.union(c), line.location.originSelectionRange!), - line.location.originSelectionRange!, - )); - - return results; - } - }); - - vscode.languages.registerCompletionItemProvider(SEARCH_RESULT_SELECTOR, { - provideCompletionItems(document: vscode.TextDocument, position: vscode.Position): vscode.CompletionItem[] { - - const line = document.lineAt(position.line); - if (position.line > 3) { return []; } - if (position.character === 0 || (position.character === 1 && line.text === '#')) { - const header = Array.from({ length: 4 }).map((_, i) => document.lineAt(i).text); - - return ['# Query:', '# Flags:', '# Including:', '# Excluding:'] - .filter(suggestion => header.every(line => line.indexOf(suggestion) === -1)) - .map(flag => ({ label: flag, insertText: (flag.slice(position.character)) + ' ' })); + return results; } + }), - if (line.text.indexOf('# Flags:') === -1) { return []; } + vscode.languages.registerCompletionItemProvider(SEARCH_RESULT_SELECTOR, { + provideCompletionItems(document: vscode.TextDocument, position: vscode.Position): vscode.CompletionItem[] { - return ['RegExp', 'CaseSensitive', 'IgnoreExcludeSettings', 'WordMatch'] - .filter(flag => line.text.indexOf(flag) === -1) - .map(flag => ({ label: flag, insertText: flag + ' ' })); - } - }, '#'); + const line = document.lineAt(position.line); + if (position.line > 3) { return []; } + if (position.character === 0 || (position.character === 1 && line.text === '#')) { + const header = Array.from({ length: DIRECTIVES.length }).map((_, i) => document.lineAt(i).text); - vscode.languages.registerDefinitionProvider(SEARCH_RESULT_SELECTOR, { - provideDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.DefinitionLink[] { - const lineResult = parseSearchResults(document, token)[position.line]; - if (!lineResult) { return []; } - if (lineResult.type === 'file') { - // TODO: The multi-match peek UX isnt very smooth. - // return lineResult.allLocations.length > 1 ? lineResult.allLocations : [lineResult.location]; - return []; + return DIRECTIVES + .filter(suggestion => header.every(line => line.indexOf(suggestion) === -1)) + .map(flag => ({ label: flag, insertText: (flag.slice(position.character)) + ' ' })); + } + + if (line.text.indexOf('# Flags:') === -1) { return []; } + + return FLAGS + .filter(flag => line.text.indexOf(flag) === -1) + .map(flag => ({ label: flag, insertText: flag + ' ' })); } + }, '#'), - return [lineResult.location]; - } - }); + vscode.languages.registerDefinitionProvider(SEARCH_RESULT_SELECTOR, { + provideDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.DefinitionLink[] { + const lineResult = parseSearchResults(document, token)[position.line]; + if (!lineResult) { return []; } + if (lineResult.type === 'file') { + // TODO: The multi-match peek UX isnt very smooth. + // return lineResult.allLocations.length > 1 ? lineResult.allLocations : [lineResult.location]; + return []; + } - vscode.languages.registerDocumentLinkProvider(SEARCH_RESULT_SELECTOR, { - async provideDocumentLinks(document: vscode.TextDocument, token: vscode.CancellationToken): Promise { - return parseSearchResults(document, token) - .filter(({ type }) => type === 'file') - .map(({ location }) => ({ range: location.originSelectionRange!, target: location.targetUri })); - } - }); + const translateRangeSidewaysBy = (r: vscode.Range, n: number) => + r.with({ start: new vscode.Position(r.start.line, Math.max(0, n - r.start.character)), end: new vscode.Position(r.end.line, Math.max(0, n - r.end.character)) }); - vscode.window.onDidChangeActiveTextEditor(e => { - if (e?.document.languageId === 'search-result') { - // Clear the parse whenever we open a new editor. - // Conservative because things like the URI might remain constant even if the contents change, and re-parsing even large files is relatively fast. - cachedLastParse = undefined; - } - }); + return [{ + ...lineResult.location, + targetSelectionRange: translateRangeSidewaysBy(lineResult.location.targetSelectionRange!, position.character - 1) + }]; + } + }), + + vscode.languages.registerDocumentLinkProvider(SEARCH_RESULT_SELECTOR, { + async provideDocumentLinks(document: vscode.TextDocument, token: vscode.CancellationToken): Promise { + return parseSearchResults(document, token) + .filter(({ type }) => type === 'file') + .map(({ location }) => ({ range: location.originSelectionRange!, target: location.targetUri })); + } + }), + + vscode.window.onDidChangeActiveTextEditor(e => { + if (e?.document.languageId === 'search-result') { + // Clear the parse whenever we open a new editor. + // Conservative because things like the URI might remain constant even if the contents change, and re-parsing even large files is relatively fast. + cachedLastParse = undefined; + } + }), + + { dispose() { cachedLastParse = undefined; } } + ); } @@ -160,13 +172,14 @@ function parseSearchResults(document: vscode.TextDocument, token: vscode.Cancell const resultLine = RESULT_LINE_REGEX.exec(line); if (resultLine) { - const [, indentation, _lineNumber, resultIndentation] = resultLine; + const [, indentation, _lineNumber, seperator, resultIndentation] = resultLine; const lineNumber = +_lineNumber - 1; - const resultStart = (indentation + _lineNumber + ':' + resultIndentation).length; + const resultStart = (indentation + _lineNumber + seperator + resultIndentation).length; + const metadataOffset = (indentation + _lineNumber + seperator).length; const location: vscode.LocationLink = { targetRange: new vscode.Range(Math.max(lineNumber - 3, 0), 0, lineNumber + 3, line.length), - targetSelectionRange: new vscode.Range(lineNumber, 0, lineNumber, line.length), + targetSelectionRange: new vscode.Range(lineNumber, metadataOffset, lineNumber, metadataOffset), targetUri: currentTarget, originSelectionRange: new vscode.Range(i, resultStart, i, line.length), }; diff --git a/extensions/search-result/syntaxes/searchResult.tmLanguage.json b/extensions/search-result/syntaxes/searchResult.tmLanguage.json index 4de2a40ba40..d16ecb8c97c 100644 --- a/extensions/search-result/syntaxes/searchResult.tmLanguage.json +++ b/extensions/search-result/syntaxes/searchResult.tmLanguage.json @@ -3,7 +3,7 @@ "scopeName": "text.searchResult", "patterns": [ { - "match": "^# (Query|Flags|Including|Excluding): .*$", + "match": "^# (Query|Flags|Including|Excluding|ContextLines): .*$", "name": "comment" }, { diff --git a/extensions/theme-defaults/themes/dark_plus.json b/extensions/theme-defaults/themes/dark_plus.json index f8a0ff5b7c5..03e62612d62 100644 --- a/extensions/theme-defaults/themes/dark_plus.json +++ b/extensions/theme-defaults/themes/dark_plus.json @@ -9,7 +9,8 @@ "entity.name.function", "support.function", "support.constant.handlebars", - "source.powershell variable.other.member" + "source.powershell variable.other.member", + "entity.name.operator.custom-literal" // See https://en.cppreference.com/w/cpp/language/user_literal ], "settings": { "foreground": "#DCDCAA" diff --git a/extensions/theme-defaults/themes/dark_vs.json b/extensions/theme-defaults/themes/dark_vs.json index 479b5fd8988..6e5f4c25f80 100644 --- a/extensions/theme-defaults/themes/dark_vs.json +++ b/extensions/theme-defaults/themes/dark_vs.json @@ -171,7 +171,8 @@ }, { "scope": [ - "meta.preprocessor" + "meta.preprocessor", + "entity.name.function.preprocessor" ], "settings": { "foreground": "#569cd6" @@ -226,6 +227,7 @@ "scope": [ "string", "entity.name.operator.custom-literal.string", + "meta.embedded.assembly" ], "settings": { "foreground": "#ce9178" diff --git a/extensions/theme-defaults/themes/light_plus.json b/extensions/theme-defaults/themes/light_plus.json index c7599d60d57..faa2b836c2d 100644 --- a/extensions/theme-defaults/themes/light_plus.json +++ b/extensions/theme-defaults/themes/light_plus.json @@ -9,7 +9,8 @@ "entity.name.function", "support.function", "support.constant.handlebars", - "source.powershell variable.other.member" + "source.powershell variable.other.member", + "entity.name.operator.custom-literal" // See https://en.cppreference.com/w/cpp/language/user_literal ], "settings": { "foreground": "#795E26" diff --git a/extensions/theme-defaults/themes/light_vs.json b/extensions/theme-defaults/themes/light_vs.json index 886d1175c8b..5453787c4ed 100644 --- a/extensions/theme-defaults/themes/light_vs.json +++ b/extensions/theme-defaults/themes/light_vs.json @@ -169,7 +169,8 @@ }, { "scope": [ - "meta.preprocessor" + "meta.preprocessor", + "entity.name.function.preprocessor" ], "settings": { "foreground": "#0000ff" @@ -218,6 +219,7 @@ "scope": [ "string", "entity.name.operator.custom-literal.string", + "meta.embedded.assembly" ], "settings": { "foreground": "#a31515" diff --git a/extensions/typescript-language-features/schemas/package.schema.json b/extensions/typescript-language-features/schemas/package.schema.json index c26af63c93b..c135ea39ec1 100644 --- a/extensions/typescript-language-features/schemas/package.schema.json +++ b/extensions/typescript-language-features/schemas/package.schema.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "title": "TypeScript contributions to package.json", "type": "object", "properties": { @@ -28,4 +28,4 @@ } } } -} \ No newline at end of file +} diff --git a/extensions/typescript-language-features/src/tsServer/server.ts b/extensions/typescript-language-features/src/tsServer/server.ts index e4f9acd1975..782c1c56f05 100644 --- a/extensions/typescript-language-features/src/tsServer/server.ts +++ b/extensions/typescript-language-features/src/tsServer/server.ts @@ -60,7 +60,7 @@ export interface ITypeScriptServer { } export interface TsServerDelegate { - onFatalError(command: string): void; + onFatalError(command: string, error: Error): void; } export interface TsServerProcess { @@ -222,21 +222,13 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe if (!executeInfo.token || !executeInfo.token.isCancellationRequested) { /* __GDPR__ "languageServiceErrorResponse" : { - "command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "message" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "stack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, - "errortext" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, "${include}": [ - "${TypeScriptCommonProperties}" + "${TypeScriptCommonProperties}", + "${TypeScriptRequestErrorProperties}" ] } */ - this._telemetryReporter.logTelemetry('languageServiceErrorResponse', { - command: err.serverCommand, - message: err.serverMessage || '', - stack: err.serverStack || '', - errortext: err.serverErrorText || '', - }); + this._telemetryReporter.logTelemetry('languageServiceErrorResponse', err.telemetry); } } @@ -367,9 +359,8 @@ export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServ } else if (SyntaxRoutingTsServer.sharedCommands.has(command)) { // Dispatch to both server but only return from syntax one - const enum RequestState { Unresolved, Resolved, Errored } - let syntaxRequestState = RequestState.Unresolved; - let semanticRequestState = RequestState.Unresolved; + let syntaxRequestState: RequestState.State = RequestState.Unresolved; + let semanticRequestState: RequestState.State = RequestState.Unresolved; // Also make sure we never cancel requests to just one server let token: vscode.CancellationToken | undefined = undefined; @@ -394,16 +385,16 @@ export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServ semanticRequest .then(result => { semanticRequestState = RequestState.Resolved; - if (syntaxRequestState === RequestState.Errored) { + if (syntaxRequestState.type === RequestState.Type.Errored) { // We've gone out of sync - this._delegate.onFatalError(command); + this._delegate.onFatalError(command, syntaxRequestState.err); } return result; }, err => { - semanticRequestState = RequestState.Errored; + semanticRequestState = new RequestState.Errored(err); if (syntaxRequestState === RequestState.Resolved) { // We've gone out of sync - this._delegate.onFatalError(command); + this._delegate.onFatalError(command, err); } throw err; }); @@ -413,16 +404,16 @@ export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServ syntaxRequest .then(result => { syntaxRequestState = RequestState.Resolved; - if (semanticRequestState === RequestState.Errored) { + if (semanticRequestState.type === RequestState.Type.Errored) { // We've gone out of sync - this._delegate.onFatalError(command); + this._delegate.onFatalError(command, semanticRequestState.err); } return result; }, err => { - syntaxRequestState = RequestState.Errored; + syntaxRequestState = new RequestState.Errored(err); if (semanticRequestState === RequestState.Resolved) { // We've gone out of sync - this._delegate.onFatalError(command); + this._delegate.onFatalError(command, err); } throw err; }); @@ -433,3 +424,21 @@ export class SyntaxRoutingTsServer extends Disposable implements ITypeScriptServ } } } + +namespace RequestState { + export const enum Type { Unresolved, Resolved, Errored } + + export const Unresolved = { type: Type.Unresolved } as const; + + export const Resolved = { type: Type.Resolved } as const; + + export class Errored { + readonly type = Type.Errored; + + constructor( + public readonly err: Error + ) { } + } + + export type State = typeof Unresolved | typeof Resolved | Errored; +} diff --git a/extensions/typescript-language-features/src/tsServer/serverError.ts b/extensions/typescript-language-features/src/tsServer/serverError.ts index 7f58aa6b0f5..5c844b5f4da 100644 --- a/extensions/typescript-language-features/src/tsServer/serverError.ts +++ b/extensions/typescript-language-features/src/tsServer/serverError.ts @@ -7,6 +7,7 @@ import * as Proto from '../protocol'; import { escapeRegExp } from '../utils/regexp'; import { TypeScriptVersion } from '../utils/versionProvider'; + export class TypeScriptServerError extends Error { public static create( serverId: string, @@ -31,6 +32,23 @@ export class TypeScriptServerError extends Error { public get serverCommand() { return this.response.command; } + public get telemetry() { + /* __GDPR__FRAGMENT__ + "TypeScriptRequestErrorProperties" : { + "command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "message" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "stack" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }, + "errortext" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" } + } + */ + return { + command: this.serverCommand, + message: this.serverMessage || '', + stack: this.serverStack || '', + errortext: this.serverErrorText || '', + } as const; + } + /** * Given a `errorText` from a tsserver request indicating failure in handling a request, * prepares a payload for telemetry-logging. diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index f45cb960dca..b3385c4c393 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -11,7 +11,9 @@ import BufferSyncSupport from './features/bufferSyncSupport'; import { DiagnosticKind, DiagnosticsManager } from './features/diagnostics'; import * as Proto from './protocol'; import { ITypeScriptServer } from './tsServer/server'; -import { ITypeScriptServiceClient, ServerResponse, TypeScriptRequests, ExecConfig } from './typescriptService'; +import { TypeScriptServerError } from './tsServer/serverError'; +import { TypeScriptServerSpawner } from './tsServer/spawner'; +import { ExecConfig, ITypeScriptServiceClient, ServerResponse, TypeScriptRequests } from './typescriptService'; import API from './utils/api'; import { TsServerLogLevel, TypeScriptServiceConfiguration } from './utils/configuration'; import { Disposable } from './utils/dispose'; @@ -25,7 +27,6 @@ import Tracer from './utils/tracer'; import { inferredProjectConfig } from './utils/tsconfig'; import { TypeScriptVersionPicker } from './utils/versionPicker'; import { TypeScriptVersion, TypeScriptVersionProvider } from './utils/versionProvider'; -import { TypeScriptServerSpawner } from './tsServer/spawner'; const localize = nls.loadMessageBundle(); @@ -314,7 +315,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType this.onDidChangeTypeScriptVersion(currentVersion); let mytoken = ++this.token; const handle = this.typescriptServerSpawner.spawn(currentVersion, this.configuration, this.pluginManager, { - onFatalError: (command) => this.fatalError(command), + onFatalError: (command, err) => this.fatalError(command, err), }); this.serverState = new ServerState.Running(handle, apiVersion, undefined, true); this.lastStart = Date.now(); @@ -670,7 +671,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType } if (config?.nonRecoverable) { - execution.catch(() => this.fatalError(command)); + execution.catch(err => this.fatalError(command, err)); } return execution; @@ -704,14 +705,16 @@ export default class TypeScriptServiceClient extends Disposable implements IType return this.bufferSyncSupport.interuptGetErr(f); } - private fatalError(command: string): void { + private fatalError(command: string, error: Error): void { /* __GDPR__ "fatalError" : { - "${include}": [ "${TypeScriptCommonProperties}" ], - "command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "${include}": [ + "${TypeScriptCommonProperties}", + "${TypeScriptRequestErrorProperties}" + ] } */ - this.logTelemetry('fatalError', { command }); + this.logTelemetry('fatalError', { command, ...(error instanceof TypeScriptServerError ? error.telemetry : {}) }); console.error(`A non-recoverable error occured while executing tsserver command: ${command}`); if (this.serverState.type === ServerState.Type.Running) { diff --git a/extensions/typescript-language-features/src/utils/resourceMap.ts b/extensions/typescript-language-features/src/utils/resourceMap.ts index a10fbd91636..e942fae4583 100644 --- a/extensions/typescript-language-features/src/utils/resourceMap.ts +++ b/extensions/typescript-language-features/src/utils/resourceMap.ts @@ -101,5 +101,5 @@ export class ResourceMap { } export function isWindowsPath(path: string): boolean { - return /^[a-zA-Z]:\\/.test(path); + return /^[a-zA-Z]:[\/\\]/.test(path); } diff --git a/extensions/vscode-api-tests/src/extension.ts b/extensions/vscode-api-tests/src/extension.ts index fd713b5065c..162528cecea 100644 --- a/extensions/vscode-api-tests/src/extension.ts +++ b/extensions/vscode-api-tests/src/extension.ts @@ -2824,14 +2824,12 @@ export class ProtocolServer implements vscode.DebugAdapter { private _sequence: number = 1; private _pendingRequests = new Map void>(); - constructor() { - } public handleMessage(message: DebugProtocol.ProtocolMessage): void { this.dispatch(message); } - public stop(): void { + public dispose() { } public sendEvent(event: DebugProtocol.Event): void { @@ -3817,7 +3815,7 @@ export class MockDebugAdapterDescriptorFactory implements vscode.DebugAdapterDes } createDebugAdapterDescriptor(_session: vscode.DebugSession, _executable: vscode.DebugAdapterExecutable | undefined): vscode.ProviderResult { - return new vscode.DebugAdapterInlineImplementation(new MockDebugSession(this.memfs)); + return new vscode.DebugAdapterInlineImplementation(new MockDebugSession(this.memfs)); } } diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index 175ef8522d1..6e409489af6 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -916,4 +916,23 @@ suite('workspace-namespace', () => { const expected2 = 'import2;import1;'; assert.equal(document.getText(), expected2); }); + + test('The api workspace.applyEdit failed for some case of mixing resourceChange and textEdit #80688', async function () { + const file1 = await createRandomFile(); + const file2 = await createRandomFile(); + let we = new vscode.WorkspaceEdit(); + we.insert(file1, new vscode.Position(0, 0), 'import1;'); + we.insert(file1, new vscode.Position(0, 0), 'import2;'); + + const file2Name = basename(file2.fsPath); + const file2NewUri = vscode.Uri.parse(file2.toString().replace(file2Name, `new/${file2Name}`)); + we.renameFile(file2, file2NewUri); + + await vscode.workspace.applyEdit(we); + + const document = await vscode.workspace.openTextDocument(file1); + const expected = 'import1;import2;'; + // const expected2 = 'import2;import1;'; + assert.equal(document.getText(), expected); + }); }); diff --git a/extensions/vscode-api-tests/src/workspace-tests/workspace.test.ts b/extensions/vscode-api-tests/src/workspace-tests/workspace.test.ts index f018f581c42..729dd8f14d5 100644 --- a/extensions/vscode-api-tests/src/workspace-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/workspace-tests/workspace.test.ts @@ -13,7 +13,7 @@ suite('workspace-namespace', () => { teardown(closeAllEditors); test('rootPath', () => { - assert.ok(pathEquals(vscode.workspace.rootPath!, join(__dirname, '../../testWorkspace'))); + assert.equal(vscode.workspace.rootPath, undefined); }); test('workspaceFile', () => { diff --git a/extensions/vscode-colorize-tests/src/colorizerTestMain.ts b/extensions/vscode-colorize-tests/src/colorizerTestMain.ts index b8d0a5c146f..7f30a330ffb 100644 --- a/extensions/vscode-colorize-tests/src/colorizerTestMain.ts +++ b/extensions/vscode-colorize-tests/src/colorizerTestMain.ts @@ -8,53 +8,52 @@ import * as jsoncParser from 'jsonc-parser'; export function activate(context: vscode.ExtensionContext): any { - const tokenModifiers = ['static', 'abstract', 'deprecated']; - const tokenTypes = ['strings', 'types', 'structs', 'classes', 'functions', 'variables']; - const legend = new vscode.SemanticColoringLegend(tokenTypes, tokenModifiers); + const tokenTypes = ['type', 'struct', 'class', 'interface', 'enum', 'parameterType', 'function', 'variable']; + const tokenModifiers = ['static', 'abstract', 'deprecated', 'declaration', 'documentation', 'member', 'async']; - /* - * A certain token (at index `i` is encoded using 5 uint32 integers): - * - at index `5*i` - `deltaLine`: token line number, relative to `SemanticColoringArea.line` - * - at index `5*i+1` - `startCharacter`: token start character offset inside the line (inclusive) - * - at index `5*i+2` - `endCharacter`: token end character offset inside the line (exclusive) - * - at index `5*i+3` - `tokenType`: will be looked up in `SemanticColoringLegend.tokenTypes` - * - at index `5*i+4` - `tokenModifiers`: each set bit will be looked up in `SemanticColoringLegend.tokenModifiers` - */ + const legend = new vscode.SemanticTokensLegend(tokenTypes, tokenModifiers); - const semanticHighlightProvider: vscode.SemanticColoringProvider = { - provideSemanticColoring(document: vscode.TextDocument): vscode.ProviderResult { - const result: number[] = []; + const semanticHighlightProvider: vscode.SemanticTokensProvider = { + provideSemanticTokens(document: vscode.TextDocument): vscode.ProviderResult { + const builder = new vscode.SemanticTokensBuilder(); + + function addToken(value: string, startLine: number, startCharacter: number, length: number) { + const [type, ...modifiers] = value.split('.'); + + let tokenType = legend.tokenTypes.indexOf(type); + if (tokenType === -1) { + return; + } + + let tokenModifiers = 0; + for (let i = 0; i < modifiers.length; i++) { + const index = legend.tokenModifiers.indexOf(modifiers[i]); + if (index !== -1) { + tokenModifiers = tokenModifiers | 1 << index; + } + } + + + builder.push(startLine, startCharacter, length, tokenType, tokenModifiers); + } const visitor: jsoncParser.JSONVisitor = { - onObjectProperty: (property: string, _offset: number, length: number, startLine: number, startCharacter: number) => { - result.push(startLine); - result.push(startCharacter); - result.push(startCharacter + length); - - - const [type, ...modifiers] = property.split('.'); - let tokenType = legend.tokenTypes.indexOf(type); - if (tokenType === -1) { - tokenType = 0; + onObjectProperty: (property: string, _offset: number, _length: number, startLine: number, startCharacter: number) => { + addToken(property, startLine, startCharacter, property.length + 2); + }, + onLiteralValue: (value: any, _offset: number, length: number, startLine: number, startCharacter: number) => { + if (typeof value === 'string') { + addToken(value, startLine, startCharacter, length); } - result.push(tokenType); - - let tokenModifiers = 0; - for (let i = 0; i < modifiers.length; i++) { - const index = legend.tokenModifiers.indexOf(modifiers[i]); - if (index !== -1) { - tokenModifiers = tokenModifiers | 1 << index; - } - } - result.push(tokenModifiers); } }; jsoncParser.visit(document.getText(), visitor); - return new vscode.SemanticColoring([new vscode.SemanticColoringArea(0, new Uint32Array(result))]); + + return new vscode.SemanticTokens(builder.build()); } }; - context.subscriptions.push(vscode.languages.registerSemanticColoringProvider({ pattern: '**/color-test.json' }, semanticHighlightProvider, legend)); + context.subscriptions.push(vscode.languages.registerSemanticTokensProvider({ pattern: '**/*semantic-test.json' }, semanticHighlightProvider, legend)); } diff --git a/extensions/vscode-test-resolver/package.json b/extensions/vscode-test-resolver/package.json index a458e14185d..90e3e0e87ee 100644 --- a/extensions/vscode-test-resolver/package.json +++ b/extensions/vscode-test-resolver/package.json @@ -8,7 +8,7 @@ "engines": { "vscode": "^1.25.0" }, - "extensionKind": "ui", + "extensionKind": [ "ui" ], "scripts": { "compile": "node ./node_modules/vscode/bin/compile -watch -p ./", "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:vscode-test-resolver ./tsconfig.json" diff --git a/extensions/yarn.lock b/extensions/yarn.lock index f2e7ae95079..a8eb51df657 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -typescript@3.7.3-insiders.20191123: - version "3.7.3-insiders.20191123" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.3-insiders.20191123.tgz#f3bef33a2a3f6e02f11bcc0c20b6f0de526f17fd" - integrity sha512-b+tLx4D0a6SeuaCa7iehdgkRKHsS67FkioQWw+0REjVNOYZ+AqJ0NjlnomK1hEUvSzSNrH9Du+m+Yiv7JlVpSg== +typescript@3.7.3: + version "3.7.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.3.tgz#b36840668a16458a7025b9eabfad11b66ab85c69" + integrity sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw== diff --git a/package.json b/package.json index 5ee09edfd02..0fcecaa745a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", - "version": "1.41.0", - "distro": "30dcc7436405dfa68898d0f3843551c589fc9008", + "version": "1.42.0", + "distro": "17f1b806c349d58f96b4aef97ae59d836e2c5605", "author": { "name": "Microsoft Corporation" }, @@ -53,7 +53,7 @@ "vscode-ripgrep": "^1.5.7", "vscode-sqlite3": "4.0.9", "vscode-textmate": "4.4.0", - "xterm": "4.3.0-beta.28", + "xterm": "4.3.0-beta.28.vscode.1", "xterm-addon-search": "0.4.0-beta4", "xterm-addon-web-links": "0.2.1", "xterm-addon-webgl": "0.4.0-beta.11", diff --git a/remote/package.json b/remote/package.json index 216a7c37015..891ae131f63 100644 --- a/remote/package.json +++ b/remote/package.json @@ -20,7 +20,7 @@ "vscode-proxy-agent": "^0.5.2", "vscode-ripgrep": "^1.5.7", "vscode-textmate": "4.4.0", - "xterm": "4.3.0-beta.28", + "xterm": "4.3.0-beta.28.vscode.1", "xterm-addon-search": "0.4.0-beta4", "xterm-addon-web-links": "0.2.1", "xterm-addon-webgl": "0.4.0-beta.11", diff --git a/remote/web/package.json b/remote/web/package.json index 2a0519d282d..0aed404aaba 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -5,7 +5,7 @@ "onigasm-umd": "2.2.5", "semver-umd": "^5.5.3", "vscode-textmate": "4.4.0", - "xterm": "4.3.0-beta.28", + "xterm": "4.3.0-beta.28.vscode.1", "xterm-addon-search": "0.4.0-beta4", "xterm-addon-web-links": "0.2.1", "xterm-addon-webgl": "0.4.0-beta.11" diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 1e2d3f0de21..18078da8d19 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -46,7 +46,7 @@ xterm-addon-webgl@0.4.0-beta.11: resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.4.0-beta.11.tgz#0e4a7242e2353cf74aba55e5a5bdc0b4ec87ad10" integrity sha512-AteDxm1RFy1WnjY9r5iJSETozLebvUkR+jextdZk/ASsK21vsYK0DuVWwRI8afgiN2hUVhxcxuHEJUOV+CJDQA== -xterm@4.3.0-beta.28: - version "4.3.0-beta.28" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.3.0-beta.28.tgz#80f7c4ba8f6ee3c953e6f33f8ce5aef08d5a8354" - integrity sha512-WWZ4XCvce5h+klL6ObwtMauJff/n2KGGOwJJkDbJhrAjVy2a77GKgAedJTDDFGgKJ6ix1d7puHtVSSKflIVaDQ== +xterm@4.3.0-beta.28.vscode.1: + version "4.3.0-beta.28.vscode.1" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.3.0-beta.28.vscode.1.tgz#89b85398b5801708e833d08bf92b2124fa128943" + integrity sha512-JNHNZyDtAWnybJTrenPzD6g/yXpHOvPqmjau91Up4onRbjQYMSNlth17SqaES68DKn/+4kcIl2c/RG5SXJjvGw== diff --git a/remote/yarn.lock b/remote/yarn.lock index 6b661b17e47..a45c4d94ff5 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -433,10 +433,10 @@ xterm-addon-webgl@0.4.0-beta.11: resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.4.0-beta.11.tgz#0e4a7242e2353cf74aba55e5a5bdc0b4ec87ad10" integrity sha512-AteDxm1RFy1WnjY9r5iJSETozLebvUkR+jextdZk/ASsK21vsYK0DuVWwRI8afgiN2hUVhxcxuHEJUOV+CJDQA== -xterm@4.3.0-beta.28: - version "4.3.0-beta.28" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.3.0-beta.28.tgz#80f7c4ba8f6ee3c953e6f33f8ce5aef08d5a8354" - integrity sha512-WWZ4XCvce5h+klL6ObwtMauJff/n2KGGOwJJkDbJhrAjVy2a77GKgAedJTDDFGgKJ6ix1d7puHtVSSKflIVaDQ== +xterm@4.3.0-beta.28.vscode.1: + version "4.3.0-beta.28.vscode.1" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.3.0-beta.28.vscode.1.tgz#89b85398b5801708e833d08bf92b2124fa128943" + integrity sha512-JNHNZyDtAWnybJTrenPzD6g/yXpHOvPqmjau91Up4onRbjQYMSNlth17SqaES68DKn/+4kcIl2c/RG5SXJjvGw== yauzl@^2.9.2: version "2.10.0" diff --git a/src/typings/vscode-ripgrep.d.ts b/src/typings/vscode-ripgrep.d.ts deleted file mode 100644 index 4c5c89c3ca8..00000000000 --- a/src/typings/vscode-ripgrep.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare module 'vscode-ripgrep' { - export const rgPath: string; -} diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index e8a190d2a7d..a377409075c 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -972,6 +972,7 @@ export const EventHelper = { export interface IFocusTracker extends Disposable { onDidFocus: Event; onDidBlur: Event; + refreshState?(): void; } export function saveParentsScrollTop(node: Element): number[] { @@ -1000,6 +1001,8 @@ class FocusTracker extends Disposable implements IFocusTracker { private readonly _onDidBlur = this._register(new Emitter()); public readonly onDidBlur: Event = this._onDidBlur.event; + private _refreshStateHandler: () => void; + constructor(element: HTMLElement | Window) { super(); let hasFocus = isAncestor(document.activeElement, element); @@ -1026,9 +1029,24 @@ class FocusTracker extends Disposable implements IFocusTracker { } }; + this._refreshStateHandler = () => { + let currentNodeHasFocus = isAncestor(document.activeElement, element); + if (currentNodeHasFocus !== hasFocus) { + if (hasFocus) { + onBlur(); + } else { + onFocus(); + } + } + }; + this._register(domEvent(element, EventType.FOCUS, true)(onFocus)); this._register(domEvent(element, EventType.BLUR, true)(onBlur)); } + + refreshState() { + this._refreshStateHandler(); + } } export function trackFocus(element: HTMLElement | Window): IFocusTracker { @@ -1079,6 +1097,11 @@ function _$(namespace: Namespace, description: string, attrs? Object.keys(attrs).forEach(name => { const value = attrs![name]; + + if (typeof value === 'undefined') { + return; + } + if (/^on\w+$/.test(name)) { (result)[name] = value; } else if (name === 'selected') { diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 11e53eb44ba..58da2e0be68 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -15,7 +15,6 @@ import { cloneAndChange } from 'vs/base/common/objects'; import { escape } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; -import { renderCodicons } from 'vs/base/browser/ui/codiconLabel/codiconLabel'; export interface MarkdownRenderOptions extends FormattedTextRenderOptions { codeBlockRenderer?: (modeId: string, value: string) => Promise; @@ -63,7 +62,7 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende if (uri.query) { uri = uri.with({ query: _uriMassage(uri.query) }); } - return uri.toString(); + return uri.toString(true); }; // signal to code-block render that the @@ -73,10 +72,6 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende const renderer = new marked.Renderer(); renderer.image = (href: string, title: string, text: string) => { - if (href && href.indexOf('vscode-icon://codicon/') === 0) { - return renderCodicons(`$(${URI.parse(href).path.substr(1)})`); - } - let dimensions: string[] = []; let attributes: string[] = []; if (href) { @@ -178,7 +173,7 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende renderer }; - const allowedSchemes = [Schemas.http, Schemas.https, Schemas.mailto, Schemas.data, Schemas.file, Schemas.vscodeRemote]; + const allowedSchemes = [Schemas.http, Schemas.https, Schemas.mailto, Schemas.data, Schemas.file, Schemas.vscodeRemote, Schemas.vscodeRemoteResource]; if (markdown.isTrusted) { allowedSchemes.push(Schemas.command); } diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css index 2e259eda4f2..162038bd8e5 100644 --- a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css +++ b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css @@ -396,7 +396,6 @@ .codicon-debug-breakpoint-function:before { content: "\eb88" } .codicon-debug-breakpoint-function-disabled:before { content: "\eb88" } .codicon-debug-breakpoint-stackframe-active:before { content: "\eb89" } -.codicon-debug-breakpoint-stackframe-dot:before { content: "\eb8a" } .codicon-debug-breakpoint-stackframe:before { content: "\eb8b" } .codicon-debug-breakpoint-stackframe-focused:before { content: "\eb8b" } .codicon-debug-breakpoint-unsupported:before { content: "\eb8c" } diff --git a/src/vs/base/browser/ui/grid/grid.ts b/src/vs/base/browser/ui/grid/grid.ts index 14bda5075db..336f2ff4a21 100644 --- a/src/vs/base/browser/ui/grid/grid.ts +++ b/src/vs/base/browser/ui/grid/grid.ts @@ -604,8 +604,8 @@ export class SerializableGrid extends Grid { export type GridNodeDescriptor = { size?: number, groups?: GridNodeDescriptor[] }; export type GridDescriptor = { orientation: Orientation, groups?: GridNodeDescriptor[] }; -export function sanitizeGridNodeDescriptor(nodeDescriptor: GridNodeDescriptor): void { - if (nodeDescriptor.groups && nodeDescriptor.groups.length <= 1) { +export function sanitizeGridNodeDescriptor(nodeDescriptor: GridNodeDescriptor, rootNode: boolean): void { + if (!rootNode && nodeDescriptor.groups && nodeDescriptor.groups.length <= 1) { nodeDescriptor.groups = undefined; } @@ -617,7 +617,7 @@ export function sanitizeGridNodeDescriptor(nodeDescriptor: GridNodeDescriptor): let totalDefinedSizeCount = 0; for (const child of nodeDescriptor.groups) { - sanitizeGridNodeDescriptor(child); + sanitizeGridNodeDescriptor(child, false); if (child.size) { totalDefinedSize += child.size; @@ -665,7 +665,7 @@ function getDimensions(node: ISerializedNode, orientation: Orientation): { width } export function createSerializedGrid(gridDescriptor: GridDescriptor): ISerializedGrid { - sanitizeGridNodeDescriptor(gridDescriptor); + sanitizeGridNodeDescriptor(gridDescriptor, true); const root = createSerializedNode(gridDescriptor); const { width, height } = getDimensions(root, gridDescriptor.orientation); diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index 873979ce4b4..88716fd28d3 100644 --- a/src/vs/base/browser/ui/grid/gridview.ts +++ b/src/vs/base/browser/ui/grid/gridview.ts @@ -1086,6 +1086,8 @@ export class GridView implements IDisposable { throw new Error('Invalid JSON: \'width\' property must be a number.'); } else if (typeof json.height !== 'number') { throw new Error('Invalid JSON: \'height\' property must be a number.'); + } else if (json.root?.type !== 'branch') { + throw new Error('Invalid JSON: \'root\' property must have \'type\' value of branch.'); } const orientation = json.orientation; diff --git a/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index 40e7ae8790f..254aed56902 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -9,6 +9,7 @@ import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlighte import { IMatch } from 'vs/base/common/filters'; import { Disposable } from 'vs/base/common/lifecycle'; import { Range } from 'vs/base/common/range'; +import { equals } from 'vs/base/common/objects'; export interface IIconLabelCreationOptions { supportHighlights?: boolean; @@ -26,6 +27,7 @@ export interface IIconLabelValueOptions { labelEscapeNewLines?: boolean; descriptionMatches?: IMatch[]; readonly separator?: string; + readonly domId?: string; } class FastLabelNode { @@ -164,21 +166,23 @@ class Label { private label: string | string[] | undefined = undefined; private singleLabel: HTMLElement | undefined = undefined; + private options: IIconLabelValueOptions | undefined; constructor(private container: HTMLElement) { } setLabel(label: string | string[], options?: IIconLabelValueOptions): void { - if (this.label === label) { + if (this.label === label && equals(this.options, options)) { return; } this.label = label; + this.options = options; if (typeof label === 'string') { if (!this.singleLabel) { this.container.innerHTML = ''; dom.removeClass(this.container, 'multiple'); - this.singleLabel = dom.append(this.container, dom.$('a.label-name')); + this.singleLabel = dom.append(this.container, dom.$('a.label-name', { id: options?.domId })); } this.singleLabel.textContent = label; @@ -189,8 +193,9 @@ class Label { for (let i = 0; i < label.length; i++) { const l = label[i]; + const id = options?.domId && `${options?.domId}_${i}`; - dom.append(this.container, dom.$('a.label-name', { 'data-icon-label-count': label.length, 'data-icon-label-index': i }, l)); + dom.append(this.container, dom.$('a.label-name', { id, 'data-icon-label-count': label.length, 'data-icon-label-index': i }, l)); if (i < label.length - 1) { dom.append(this.container, dom.$('span.label-separator', undefined, options?.separator || '/')); @@ -224,21 +229,23 @@ class LabelWithHighlights { private label: string | string[] | undefined = undefined; private singleLabel: HighlightedLabel | undefined = undefined; + private options: IIconLabelValueOptions | undefined; constructor(private container: HTMLElement, private supportCodicons: boolean) { } setLabel(label: string | string[], options?: IIconLabelValueOptions): void { - if (this.label === label) { + if (this.label === label && equals(this.options, options)) { return; } this.label = label; + this.options = options; if (typeof label === 'string') { if (!this.singleLabel) { this.container.innerHTML = ''; dom.removeClass(this.container, 'multiple'); - this.singleLabel = new HighlightedLabel(dom.append(this.container, dom.$('a.label-name')), this.supportCodicons); + this.singleLabel = new HighlightedLabel(dom.append(this.container, dom.$('a.label-name', { id: options?.domId })), this.supportCodicons); } this.singleLabel.set(label, options?.matches, options?.title, options?.labelEscapeNewLines); @@ -254,8 +261,9 @@ class LabelWithHighlights { for (let i = 0; i < label.length; i++) { const l = label[i]; const m = matches ? matches[i] : undefined; + const id = options?.domId && `${options?.domId}_${i}`; - const name = dom.$('a.label-name', { 'data-icon-label-count': label.length, 'data-icon-label-index': i }); + const name = dom.$('a.label-name', { id, 'data-icon-label-count': label.length, 'data-icon-label-index': i }); const highlightedLabel = new HighlightedLabel(dom.append(this.container, name), this.supportCodicons); highlightedLabel.set(l, m, options?.title, options?.labelEscapeNewLines); diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 417998fd379..026344c5e1b 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -702,6 +702,9 @@ export interface IAccessibilityProvider { * https://www.w3.org/TR/wai-aria/#aria-level */ getAriaLevel?(element: T): number | undefined; + + onDidChangeActiveDescendant?: Event; + getActiveDescendantId?(element: T): string | undefined; } export class DefaultStyleController implements IStyleController { @@ -1115,6 +1118,7 @@ export class List implements ISpliceable, IDisposable { private spliceable: ISpliceable; private styleController: IStyleController; private typeLabelController?: TypeLabelController; + private accessibilityProvider?: IAccessibilityProvider; protected readonly disposables = new DisposableStore(); @@ -1200,8 +1204,14 @@ export class List implements ISpliceable, IDisposable { const baseRenderers: IListRenderer[] = [this.focus.renderer, this.selection.renderer]; - if (_options.accessibilityProvider) { - baseRenderers.push(new AccessibiltyRenderer(_options.accessibilityProvider)); + this.accessibilityProvider = _options.accessibilityProvider; + + if (this.accessibilityProvider) { + baseRenderers.push(new AccessibiltyRenderer(this.accessibilityProvider)); + + if (this.accessibilityProvider.onDidChangeActiveDescendant) { + this.accessibilityProvider.onDidChangeActiveDescendant(this.onDidChangeActiveDescendant, this, this.disposables); + } } renderers = renderers.map(r => new PipelineRenderer(r.templateId, [...baseRenderers, r])); @@ -1623,14 +1633,24 @@ export class List implements ISpliceable, IDisposable { private _onFocusChange(): void { const focus = this.focus.get(); + DOM.toggleClass(this.view.domNode, 'element-focused', focus.length > 0); + this.onDidChangeActiveDescendant(); + } + + private onDidChangeActiveDescendant(): void { + const focus = this.focus.get(); if (focus.length > 0) { - this.view.domNode.setAttribute('aria-activedescendant', this.view.getElementDomId(focus[0])); + let id: string | undefined; + + if (this.accessibilityProvider?.getActiveDescendantId) { + id = this.accessibilityProvider.getActiveDescendantId(this.view.element(focus[0])); + } + + this.view.domNode.setAttribute('aria-activedescendant', id || this.view.getElementDomId(focus[0])); } else { this.view.domNode.removeAttribute('aria-activedescendant'); } - - DOM.toggleClass(this.view.domNode, 'element-focused', focus.length > 0); } private _onSelectionChange(): void { diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index f735bebf0df..5f40e3d8987 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -161,12 +161,16 @@ function asListOptions(modelProvider: () => ITreeModel { + return options.accessibilityProvider!.getActiveDescendantId!(node.element); + }) }, keyboardNavigationLabelProvider: options.keyboardNavigationLabelProvider && { ...options.keyboardNavigationLabelProvider, diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 06c2a70a9cf..91a1f9ddf9d 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -6,7 +6,7 @@ import { ComposedTreeDelegate, IAbstractTreeOptions, IAbstractTreeOptionsUpdate } from 'vs/base/browser/ui/tree/abstractTree'; import { ObjectTree, IObjectTreeOptions, CompressibleObjectTree, ICompressibleTreeRenderer, ICompressibleKeyboardNavigationLabelProvider, ICompressibleObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTree'; import { IListVirtualDelegate, IIdentityProvider, IListDragAndDrop, IListDragOverReaction } from 'vs/base/browser/ui/list/list'; -import { ITreeElement, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeSorter, ICollapseStateChangeEvent, IAsyncDataSource, ITreeDragAndDrop, TreeError, WeakMapper } from 'vs/base/browser/ui/tree/tree'; +import { ITreeElement, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeSorter, ICollapseStateChangeEvent, IAsyncDataSource, ITreeDragAndDrop, TreeError, WeakMapper, ITreeFilter, TreeVisibility, TreeFilterResult } from 'vs/base/browser/ui/tree/tree'; import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; import { timeout, CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; @@ -20,6 +20,7 @@ import { values } from 'vs/base/common/map'; import { ScrollEvent } from 'vs/base/common/scrollable'; import { ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { IThemable } from 'vs/base/common/styler'; +import { isFilterResult, getVisibleState } from 'vs/base/browser/ui/tree/indexTreeModel'; interface IAsyncDataTreeNode { element: TInput | T; @@ -229,9 +230,16 @@ function asObjectTreeOptions(options?: IAsyncDataTreeOpt } }, accessibilityProvider: options.accessibilityProvider && { + ...options.accessibilityProvider, getAriaLabel(e) { return options.accessibilityProvider!.getAriaLabel(e.element as T); - } + }, + getAriaLevel: options.accessibilityProvider!.getAriaLevel && (node => { + return options.accessibilityProvider!.getAriaLevel!(node.element as T); + }), + getActiveDescendantId: options.accessibilityProvider.getActiveDescendantId && (node => { + return options.accessibilityProvider!.getActiveDescendantId!(node.element as T); + }) }, filter: options.filter && { filter(e, parentVisibility) { @@ -760,12 +768,7 @@ export class AsyncDataTree implements IDisposable result = createCancelablePromise(async () => { const children = await this.dataSource.getChildren(node.element!); - - if (this.sorter) { - children.sort(this.sorter.compare.bind(this.sorter)); - } - - return children; + return this.processChildren(children); }); this.refreshPromises.set(node, result); @@ -924,6 +927,14 @@ export class AsyncDataTree implements IDisposable }; } + protected processChildren(children: T[]): T[] { + if (this.sorter) { + children.sort(this.sorter.compare.bind(this.sorter)); + } + + return children; + } + // view state getViewState(): IAsyncDataTreeViewState { @@ -1066,6 +1077,7 @@ export class CompressibleAsyncDataTree extends As protected readonly tree!: CompressibleObjectTree, TFilterData>; protected readonly compressibleNodeMapper: CompressibleAsyncDataTreeNodeMapper = new WeakMapper(node => new CompressibleAsyncDataTreeNodeWrapper(node)); + private filter?: ITreeFilter; constructor( user: string, @@ -1077,6 +1089,7 @@ export class CompressibleAsyncDataTree extends As options: ICompressibleAsyncDataTreeOptions = {} ) { super(user, container, virtualDelegate, renderers, dataSource, options); + this.filter = options.filter; } protected createTree( @@ -1176,14 +1189,16 @@ export class CompressibleAsyncDataTree extends As if (compressedNode) { for (let i = 0; i < compressedNode.elements.length; i++) { const id = getId(compressedNode.elements[i].element as T); + const element = compressedNode.elements[compressedNode.elements.length - 1].element as T; - if (oldSelection.has(id)) { - selection.push(compressedNode.elements[compressedNode.elements.length - 1].element as T); + // github.com/microsoft/vscode/issues/85938 + if (oldSelection.has(id) && selection.indexOf(element) === -1) { + selection.push(element); didChangeSelection = true; } - if (oldFocus.has(id)) { - focus.push(compressedNode.elements[compressedNode.elements.length - 1].element as T); + if (oldFocus.has(id) && focus.indexOf(element) === -1) { + focus.push(element); didChangeFocus = true; } } @@ -1202,4 +1217,34 @@ export class CompressibleAsyncDataTree extends As this.setFocus(focus); } } + + // For compressed async data trees, `TreeVisibility.Recurse` doesn't currently work + // and we have to filter everything beforehand + // Related to #85193 and #85835 + protected processChildren(children: T[]): T[] { + if (this.filter) { + children = children.filter(e => { + const result = this.filter!.filter(e, TreeVisibility.Visible); + const visibility = getVisibility(result); + + if (visibility === TreeVisibility.Recurse) { + throw new Error('Recursive tree visibility not supported in async data compressed trees'); + } + + return visibility === TreeVisibility.Visible; + }); + } + + return super.processChildren(children); + } +} + +function getVisibility(filterResult: TreeFilterResult): TreeVisibility { + if (typeof filterResult === 'boolean') { + return filterResult ? TreeVisibility.Visible : TreeVisibility.Hidden; + } else if (isFilterResult(filterResult)) { + return getVisibleState(filterResult.visibility); + } else { + return getVisibleState(filterResult); + } } diff --git a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts index 293aae91c77..f1da4355047 100644 --- a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts @@ -11,7 +11,7 @@ import { IObjectTreeModelOptions, ObjectTreeModel, IObjectTreeModel } from 'vs/b // Exported only for test reasons, do not use directly export interface ICompressedTreeElement extends ITreeElement { - readonly children?: Iterator> | ICompressedTreeElement[]; + readonly children?: ISequence>; readonly incompressible?: boolean; } diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts index 3ea4bc94e32..9287a6eaebf 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -172,13 +172,28 @@ export module Iterator { } }; } + + export function chain(iterator: Iterator): ChainableIterator { + return new ChainableIterator(iterator); + } +} + +export class ChainableIterator implements Iterator { + + constructor(private it: Iterator) { } + + next(): IteratorResult { return this.it.next(); } + map(fn: (t: T) => R): ChainableIterator { return new ChainableIterator(Iterator.map(this.it, fn)); } + filter(fn: (t: T) => boolean): ChainableIterator { return new ChainableIterator(Iterator.filter(this.it, fn)); } } export type ISequence = Iterator | T[]; -export function getSequenceIterator(arg: Iterator | T[]): Iterator { +export function getSequenceIterator(arg: ISequence | undefined): Iterator { if (Array.isArray(arg)) { return Iterator.fromArray(arg); + } else if (!arg) { + return Iterator.empty(); } else { return arg; } diff --git a/src/vs/base/node/encoding.ts b/src/vs/base/node/encoding.ts index 3fbae109f00..8da85d8ce39 100644 --- a/src/vs/base/node/encoding.ts +++ b/src/vs/base/node/encoding.ts @@ -199,6 +199,13 @@ export function detectEncodingByBOMFromBuffer(buffer: Buffer | VSBuffer | null, return null; } +// we explicitly ignore a specific set of encodings from auto guessing +// - ASCII: we never want this encoding (most UTF-8 files would happily detect as +// ASCII files and then you could not type non-ASCII characters anymore) +// - UTF-16: we have our own detection logic for UTF-16 +// - UTF-32: we do not support this encoding in VSCode +const IGNORE_ENCODINGS = ['ascii', 'utf-16', 'utf-32']; + /** * Guesses the encoding from buffer. */ @@ -210,15 +217,9 @@ async function guessEncodingByBuffer(buffer: Buffer): Promise { return null; } - // Ignore 'ascii' as guessed encoding because that - // is almost never what we want, rather fallback - // to the configured encoding then. Otherwise, - // opening a ascii-only file with auto guessing - // enabled will put the file into 'ascii' mode - // and thus typing any special characters is - // not possible anymore. - if (guessed.encoding.toLowerCase() === 'ascii') { - return null; + const enc = guessed.encoding.toLowerCase(); + if (0 <= IGNORE_ENCODINGS.indexOf(enc)) { + return null; // see comment above why we ignore some encodings } return toIconvLiteEncoding(guessed.encoding); diff --git a/src/vs/base/test/browser/ui/grid/grid.test.ts b/src/vs/base/test/browser/ui/grid/grid.test.ts index ae7093bcadc..7956239d44d 100644 --- a/src/vs/base/test/browser/ui/grid/grid.test.ts +++ b/src/vs/base/test/browser/ui/grid/grid.test.ts @@ -787,7 +787,7 @@ suite('SerializableGrid', function () { test('sanitizeGridNodeDescriptor', () => { const nodeDescriptor = { groups: [{ size: 0.2 }, { size: 0.2 }, { size: 0.6, groups: [{}, {}] }] }; const nodeDescriptorCopy = deepClone(nodeDescriptor); - sanitizeGridNodeDescriptor(nodeDescriptorCopy); + sanitizeGridNodeDescriptor(nodeDescriptorCopy, true); assert.deepEqual(nodeDescriptorCopy, { groups: [{ size: 0.2 }, { size: 0.2 }, { size: 0.6, groups: [{ size: 0.5 }, { size: 0.5 }] }] }); }); diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 1492e7710c1..96ce984b782 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -50,8 +50,8 @@ import { IFileService } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider } from 'vs/platform/files/electron-browser/diskFileSystemProvider'; import { Schemas } from 'vs/base/common/network'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IUserDataSyncService, IUserDataSyncStoreService, ISettingsMergeService, registerConfiguration, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync'; -import { UserDataSyncService, UserDataAutoSync } from 'vs/platform/userDataSync/common/userDataSyncService'; +import { IUserDataSyncService, IUserDataSyncStoreService, ISettingsMergeService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { UserDataSyncChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; import { SettingsMergeChannelClient } from 'vs/platform/userDataSync/common/settingsSyncIpc'; @@ -63,6 +63,8 @@ import { AuthTokenService } from 'vs/platform/auth/electron-browser/authTokenSer import { AuthTokenChannel } from 'vs/platform/auth/common/authTokenIpc'; import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; import { KeytarCredentialsService } from 'vs/platform/credentials/node/credentialsService'; +import { UserDataSyncUtilServiceClient } from 'vs/platform/userDataSync/common/keybindingsSyncIpc'; +import { UserDataAutoSync } from 'vs/platform/userDataSync/electron-browser/userDataAutoSync'; export interface ISharedProcessConfiguration { readonly machineId: string; @@ -186,6 +188,7 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat services.set(IUserDataSyncLogService, new SyncDescriptor(UserDataSyncLogService)); const settingsMergeChannel = server.getChannel('settingsMerge', activeWindowRouter); services.set(ISettingsMergeService, new SettingsMergeChannelClient(settingsMergeChannel)); + services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(server.getChannel('userDataSyncUtil', activeWindowRouter))); services.set(IUserDataSyncStoreService, new SyncDescriptor(UserDataSyncStoreService)); services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService)); registerConfiguration(); diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index d235e6ba791..6b5b7d372c7 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -9,11 +9,10 @@ import { StandardWheelEvent, IMouseWheelEvent } from 'vs/base/browser/mouseEvent import { RunOnceScheduler, TimeoutTimer } from 'vs/base/common/async'; import { Disposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; -import { HitTestContext, IViewZoneData, MouseTarget, MouseTargetFactory } from 'vs/editor/browser/controller/mouseTarget'; +import { HitTestContext, IViewZoneData, MouseTarget, MouseTargetFactory, PointerHandlerLastRenderData } from 'vs/editor/browser/controller/mouseTarget'; import * as editorBrowser from 'vs/editor/browser/editorBrowser'; import { ClientCoordinates, EditorMouseEvent, EditorMouseEventFactory, GlobalEditorMouseMoveMonitor, createEditorPagePosition } from 'vs/editor/browser/editorDom'; import { ViewController } from 'vs/editor/browser/view/viewController'; -import { IViewCursorRenderData } from 'vs/editor/browser/viewParts/viewCursors/viewCursor'; import { EditorZoom } from 'vs/editor/common/config/editorZoom'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; @@ -46,9 +45,9 @@ export interface IPointerHandlerHelper { focusTextArea(): void; /** - * Get the last rendered information of the cursors. + * Get the last rendered information for cursors & textarea. */ - getLastViewCursorsRenderData(): IViewCursorRenderData[]; + getLastRenderData(): PointerHandlerLastRenderData; shouldSuppressMouseDownOnViewZone(viewZoneId: string): boolean; shouldSuppressMouseDownOnWidget(widgetId: string): boolean; @@ -158,13 +157,11 @@ export class MouseHandler extends ViewEventHandler { return null; } - const lastViewCursorsRenderData = this.viewHelper.getLastViewCursorsRenderData(); - return this.mouseTargetFactory.createMouseTarget(lastViewCursorsRenderData, editorPos, pos, null); + return this.mouseTargetFactory.createMouseTarget(this.viewHelper.getLastRenderData(), editorPos, pos, null); } protected _createMouseTarget(e: EditorMouseEvent, testEventTarget: boolean): editorBrowser.IMouseTarget { - const lastViewCursorsRenderData = this.viewHelper.getLastViewCursorsRenderData(); - return this.mouseTargetFactory.createMouseTarget(lastViewCursorsRenderData, e.editorPos, e.pos, testEventTarget ? e.target : null); + return this.mouseTargetFactory.createMouseTarget(this.viewHelper.getLastRenderData(), e.editorPos, e.pos, testEventTarget ? e.target : null); } private _getMouseColumn(e: EditorMouseEvent): number { diff --git a/src/vs/editor/browser/controller/mouseTarget.ts b/src/vs/editor/browser/controller/mouseTarget.ts index 1f737774248..384224093e6 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -89,6 +89,13 @@ interface IHitTestResult { hitTarget: Element | null; } +export class PointerHandlerLastRenderData { + constructor( + public readonly lastViewCursorsRenderData: IViewCursorRenderData[], + public readonly lastTextareaPosition: Position | null + ) { } +} + export class MouseTarget implements IMouseTarget { public readonly element: Element | null; @@ -232,19 +239,19 @@ export class HitTestContext { public readonly viewDomNode: HTMLElement; public readonly lineHeight: number; public readonly typicalHalfwidthCharacterWidth: number; - public readonly lastViewCursorsRenderData: IViewCursorRenderData[]; + public readonly lastRenderData: PointerHandlerLastRenderData; private readonly _context: ViewContext; private readonly _viewHelper: IPointerHandlerHelper; - constructor(context: ViewContext, viewHelper: IPointerHandlerHelper, lastViewCursorsRenderData: IViewCursorRenderData[]) { + constructor(context: ViewContext, viewHelper: IPointerHandlerHelper, lastRenderData: PointerHandlerLastRenderData) { this.model = context.model; const options = context.configuration.options; this.layoutInfo = options.get(EditorOption.layoutInfo); this.viewDomNode = viewHelper.viewDomNode; this.lineHeight = options.get(EditorOption.lineHeight); this.typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; - this.lastViewCursorsRenderData = lastViewCursorsRenderData; + this.lastRenderData = lastRenderData; this._context = context; this._viewHelper = viewHelper; } @@ -462,8 +469,8 @@ export class MouseTargetFactory { return false; } - public createMouseTarget(lastViewCursorsRenderData: IViewCursorRenderData[], editorPos: EditorPagePosition, pos: PageCoordinates, target: HTMLElement | null): IMouseTarget { - const ctx = new HitTestContext(this._context, this._viewHelper, lastViewCursorsRenderData); + public createMouseTarget(lastRenderData: PointerHandlerLastRenderData, editorPos: EditorPagePosition, pos: PageCoordinates, target: HTMLElement | null): IMouseTarget { + const ctx = new HitTestContext(this._context, this._viewHelper, lastRenderData); const request = new HitTestRequest(ctx, editorPos, pos, target); try { const r = MouseTargetFactory._createMouseTarget(ctx, request, false); @@ -544,7 +551,7 @@ export class MouseTargetFactory { if (request.target) { // Check if we've hit a painted cursor - const lastViewCursorsRenderData = ctx.lastViewCursorsRenderData; + const lastViewCursorsRenderData = ctx.lastRenderData.lastViewCursorsRenderData; for (const d of lastViewCursorsRenderData) { @@ -560,7 +567,7 @@ export class MouseTargetFactory { // first or last rendered view line dom node, therefore help it out // and first check if we are on top of a cursor - const lastViewCursorsRenderData = ctx.lastViewCursorsRenderData; + const lastViewCursorsRenderData = ctx.lastRenderData.lastViewCursorsRenderData; const mouseContentHorizontalOffset = request.mouseContentHorizontalOffset; const mouseVerticalOffset = request.mouseVerticalOffset; @@ -602,7 +609,10 @@ export class MouseTargetFactory { private static _hitTestTextArea(ctx: HitTestContext, request: ResolvedHitTestRequest): MouseTarget | null { // Is it the textarea? if (ElementPath.isTextArea(request.targetPath)) { - return request.fulfill(MouseTargetType.TEXTAREA); + if (ctx.lastRenderData.lastTextareaPosition) { + return request.fulfill(MouseTargetType.CONTENT_TEXT, ctx.lastRenderData.lastTextareaPosition); + } + return request.fulfill(MouseTargetType.TEXTAREA, ctx.lastRenderData.lastTextareaPosition); } return null; } diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index 4886c2e1e72..49ed53c81df 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -77,6 +77,12 @@ export class TextAreaHandler extends ViewPart { private _visibleTextArea: VisibleTextAreaData | null; private _selections: Selection[]; + /** + * The position at which the textarea was rendered. + * This is useful for hit-testing and determining the mouse position. + */ + private _lastRenderPosition: Position | null; + public readonly textArea: FastDomNode; public readonly textAreaCover: FastDomNode; private readonly _textAreaInput: TextAreaInput; @@ -104,6 +110,7 @@ export class TextAreaHandler extends ViewPart { this._visibleTextArea = null; this._selections = [new Selection(1, 1, 1, 1)]; + this._lastRenderPosition = null; // Text Area (The focus will always be in the textarea when the cursor is blinking) this.textArea = createFastDomNode(document.createElement('textarea')); @@ -413,13 +420,18 @@ export class TextAreaHandler extends ViewPart { this._textAreaInput.refreshFocusState(); } + public getLastRenderData(): Position | null { + return this._lastRenderPosition; + } + // --- end view API + private _primaryCursorPosition: Position = new Position(1, 1); private _primaryCursorVisibleRange: HorizontalPosition | null = null; public prepareRender(ctx: RenderingContext): void { - const primaryCursorPosition = new Position(this._selections[0].positionLineNumber, this._selections[0].positionColumn); - this._primaryCursorVisibleRange = ctx.visibleRangeForPosition(primaryCursorPosition); + this._primaryCursorPosition = new Position(this._selections[0].positionLineNumber, this._selections[0].positionColumn); + this._primaryCursorVisibleRange = ctx.visibleRangeForPosition(this._primaryCursorPosition); } public render(ctx: RestrictedRenderingContext): void { @@ -431,6 +443,7 @@ export class TextAreaHandler extends ViewPart { if (this._visibleTextArea) { // The text area is visible for composition reasons this._renderInsideEditor( + null, this._visibleTextArea.top - this._scrollTop, this._contentLeft + this._visibleTextArea.left - this._scrollLeft, this._visibleTextArea.width, @@ -465,6 +478,7 @@ export class TextAreaHandler extends ViewPart { // For the popup emoji input, we will make the text area as high as the line height // We will also make the fontSize and lineHeight the correct dimensions to help with the placement of these pickers this._renderInsideEditor( + this._primaryCursorPosition, top, left, canUseZeroSizeTextarea ? 0 : 1, this._lineHeight ); @@ -472,12 +486,14 @@ export class TextAreaHandler extends ViewPart { } this._renderInsideEditor( + this._primaryCursorPosition, top, left, canUseZeroSizeTextarea ? 0 : 1, canUseZeroSizeTextarea ? 0 : 1 ); } - private _renderInsideEditor(top: number, left: number, width: number, height: number): void { + private _renderInsideEditor(renderedPosition: Position | null, top: number, left: number, width: number, height: number): void { + this._lastRenderPosition = renderedPosition; const ta = this.textArea; const tac = this.textAreaCover; @@ -495,6 +511,7 @@ export class TextAreaHandler extends ViewPart { } private _renderAtTopLeft(): void { + this._lastRenderPosition = null; const ta = this.textArea; const tac = this.textAreaCover; diff --git a/src/vs/editor/browser/controller/textAreaInput.ts b/src/vs/editor/browser/controller/textAreaInput.ts index daecfbc4f68..2e64c93ba59 100644 --- a/src/vs/editor/browser/controller/textAreaInput.ts +++ b/src/vs/editor/browser/controller/textAreaInput.ts @@ -274,7 +274,11 @@ export class TextAreaInput extends Disposable { this._register(dom.addDisposableListener(textArea.domNode, 'compositionend', (e: CompositionEvent) => { this._lastTextAreaEvent = TextAreaInputEventType.compositionend; - + // https://github.com/microsoft/monaco-editor/issues/1663 + // On iOS 13.2, Chinese system IME randomly trigger an additional compositionend event with empty data + if (!this._isDoingComposition) { + return; + } if (compositionDataInValid(e.locale)) { // https://github.com/Microsoft/monaco-editor/issues/339 const [newState, typeInput] = deduceInputFromTextAreaValue(/*couldBeEmojiInput*/false, /*couldBeTypingAtOffset0*/false); diff --git a/src/vs/editor/browser/view/viewImpl.ts b/src/vs/editor/browser/view/viewImpl.ts index 02b337315c8..a79acee4e84 100644 --- a/src/vs/editor/browser/view/viewImpl.ts +++ b/src/vs/editor/browser/view/viewImpl.ts @@ -48,6 +48,7 @@ import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler'; import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; import { IThemeService, getThemeTypeSelector } from 'vs/platform/theme/common/themeService'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { PointerHandlerLastRenderData } from 'vs/editor/browser/controller/mouseTarget'; export interface IContentWidgetData { @@ -244,8 +245,10 @@ export class View extends ViewEventHandler { this.focus(); }, - getLastViewCursorsRenderData: () => { - return this.viewCursors.getLastRenderData() || []; + getLastRenderData: (): PointerHandlerLastRenderData => { + const lastViewCursorsRenderData = this.viewCursors.getLastRenderData() || []; + const lastTextareaPosition = this._textAreaHandler.getLastRenderData(); + return new PointerHandlerLastRenderData(lastViewCursorsRenderData, lastTextareaPosition); }, shouldSuppressMouseDownOnViewZone: (viewZoneId: string) => { return this.viewZones.shouldSuppressMouseDownOnViewZone(viewZoneId); diff --git a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts index cb3e355d8c6..59fdaf045a9 100644 --- a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts +++ b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts @@ -57,7 +57,7 @@ export abstract class AbstractLineHighlightOverlay extends DynamicViewOverlay { const renderSelections = isRenderedUsingBorder ? this._selections.slice(0, 1) : this._selections; const cursorsLineNumbers = renderSelections.map(s => s.positionLineNumber); - cursorsLineNumbers.sort(); + cursorsLineNumbers.sort((a, b) => a - b); if (!arrays.equals(this._cursorLineNumbers, cursorsLineNumbers)) { this._cursorLineNumbers = cursorsLineNumbers; hasChanged = true; diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index 4703c176847..b2b451023ee 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -33,6 +33,7 @@ import { Color } from 'vs/base/common/color'; import { GestureEvent, EventType, Gesture } from 'vs/base/browser/touch'; import { MinimapCharRendererFactory } from 'vs/editor/browser/viewParts/minimap/minimapCharRendererFactory'; import { MinimapPosition } from 'vs/editor/common/model'; +import { once } from 'vs/base/common/functional'; function getMinimapLineHeight(renderMinimap: RenderMinimap, scale: number): number { if (renderMinimap === RenderMinimap.Text) { @@ -73,7 +74,7 @@ class MinimapOptions { public readonly fontScale: number; - public readonly charRenderer: MinimapCharRenderer; + public readonly charRenderer: () => MinimapCharRenderer; /** * container dom node left position (in CSS px) @@ -117,7 +118,7 @@ class MinimapOptions { const minimapOpts = options.get(EditorOption.minimap); this.showSlider = minimapOpts.showSlider; this.fontScale = Math.round(minimapOpts.scale * pixelRatio); - this.charRenderer = MinimapCharRendererFactory.create(this.fontScale, fontInfo.fontFamily); + this.charRenderer = once(() => MinimapCharRendererFactory.create(this.fontScale, fontInfo.fontFamily)); this.pixelRatio = pixelRatio; this.typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth; this.lineHeight = options.get(EditorOption.lineHeight); @@ -850,6 +851,11 @@ export class Minimap extends ViewPart { charWidth: number): void { const y = (lineNumber - layout.startLineNumber) * lineHeight; + // Skip rendering the line if it's vertically outside our viewport + if (y + height < 0 || y > this._options.canvasOuterHeight) { + return; + } + // Cache line offset data so that it is only read once per line let lineIndexToXOffset = lineOffsetMap.get(lineNumber); const isFirstDecorationForLine = !lineIndexToXOffset; @@ -900,6 +906,7 @@ export class Minimap extends ViewPart { private renderLines(layout: MinimapLayout): RenderData { const renderMinimap = this._options.renderMinimap; + const charRenderer = this._options.charRenderer(); const startLineNumber = layout.startLineNumber; const endLineNumber = layout.endLineNumber; const minimapLineHeight = getMinimapLineHeight(renderMinimap, this._options.fontScale); @@ -941,7 +948,7 @@ export class Minimap extends ViewPart { useLighterFont, renderMinimap, this._tokensColorTracker, - this._options.charRenderer, + charRenderer, dy, tabSize, lineInfo.data[lineIndex]!, diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 357eeab2a9f..4bfde519d76 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -874,6 +874,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE public onHide(): void { this._modelData?.view.refreshFocusState(); + this._focusTracker.refreshState(); } public getContribution(id: string): T { @@ -1806,6 +1807,12 @@ class CodeEditorWidgetFocusTracker extends Disposable { public hasFocus(): boolean { return this._hasFocus; } + + public refreshState(): void { + if (this._domFocusTracker.refreshState) { + this._domFocusTracker.refreshState(); + } + } } const squigglyStart = encodeURIComponent(`matchBrackets === true) { + options.matchBrackets = 'always'; + } else if (matchBrackets === false) { + options.matchBrackets = 'never'; + } } function deepCloneAndMigrateOptions(_options: IEditorOptions): IEditorOptions { diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 7ad5563ff4a..a5c35ef4b60 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -481,9 +481,9 @@ export interface IEditorOptions { showFoldingControls?: 'always' | 'mouseover'; /** * Enable highlighting of matching brackets. - * Defaults to true. + * Defaults to 'always'. */ - matchBrackets?: boolean; + matchBrackets?: 'never' | 'near' | 'always'; /** * Enable rendering of whitespace. * Defaults to none. @@ -2271,7 +2271,7 @@ class EditorRulers extends SimpleEditorOption { for (let value of input) { rulers.push(EditorIntOption.clampedInt(value, 0, 0, 10000)); } - rulers.sort(); + rulers.sort((a, b) => a - b); return rulers; } return this.defaultValue; @@ -3356,9 +3356,11 @@ export const EditorOptions = { EditorOption.links, 'links', true, { description: nls.localize('links', "Controls whether the editor should detect links and make them clickable.") } )), - matchBrackets: register(new EditorBooleanOption( - EditorOption.matchBrackets, 'matchBrackets', true, - { description: nls.localize('matchBrackets', "Highlight matching brackets when one of them is selected.") } + matchBrackets: register(new EditorStringEnumOption( + EditorOption.matchBrackets, 'matchBrackets', + 'always' as 'never' | 'near' | 'always', + ['always', 'near', 'never'] as const, + { description: nls.localize('matchBrackets', "Highlight matching brackets.") } )), minimap: register(new EditorMinimap()), mouseStyle: register(new EditorStringEnumOption( diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 0c6a851ec53..e26df375fa9 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -898,7 +898,7 @@ export interface ITextModel { * @param position The position at which to start the search. * @internal */ - findEnclosingBrackets(position: IPosition): [Range, Range] | null; + findEnclosingBrackets(position: IPosition, maxDuration?: number): [Range, Range] | null; /** * Given a `position`, if the position is on top or near a bracket, diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index adea9b60c7d..9bc6dc21a62 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -34,6 +34,7 @@ import { withUndefinedAsNull } from 'vs/base/common/types'; import { VSBufferReadableStream, VSBuffer } from 'vs/base/common/buffer'; import { TokensStore, MultilineTokens, countEOL, MultilineTokens2, TokensStore2 } from 'vs/editor/common/model/tokensStore'; import { Color } from 'vs/base/common/color'; +import { Constants } from 'vs/base/common/uint'; function createTextBufferBuilder() { return new PieceTreeTextBufferBuilder(); @@ -2341,16 +2342,21 @@ export class TextModel extends Disposable implements model.ITextModel { return null; } - public findEnclosingBrackets(_position: IPosition): [Range, Range] | null { + public findEnclosingBrackets(_position: IPosition, maxDuration = Constants.MAX_SAFE_SMALL_INTEGER): [Range, Range] | null { const position = this.validatePosition(_position); const lineCount = this.getLineCount(); + const savedCounts = new Map(); let counts: number[] = []; - const resetCounts = (modeBrackets: RichEditBrackets | null) => { - counts = []; - for (let i = 0, len = modeBrackets ? modeBrackets.brackets.length : 0; i < len; i++) { - counts[i] = 0; + const resetCounts = (languageId: number, modeBrackets: RichEditBrackets | null) => { + if (!savedCounts.has(languageId)) { + let tmp = []; + for (let i = 0, len = modeBrackets ? modeBrackets.brackets.length : 0; i < len; i++) { + tmp[i] = 0; + } + savedCounts.set(languageId, tmp); } + counts = savedCounts.get(languageId)!; }; const searchInRange = (modeBrackets: RichEditBrackets, lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): [Range, Range] | null => { while (true) { @@ -2380,7 +2386,12 @@ export class TextModel extends Disposable implements model.ITextModel { let languageId: LanguageId = -1; let modeBrackets: RichEditBrackets | null = null; + const startTime = Date.now(); for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) { + const elapsedTime = Date.now() - startTime; + if (elapsedTime > maxDuration) { + return null; + } const lineTokens = this._getLineTokens(lineNumber); const tokenCount = lineTokens.getCount(); const lineText = this._buffer.getLineContent(lineNumber); @@ -2396,7 +2407,7 @@ export class TextModel extends Disposable implements model.ITextModel { if (languageId !== tokenLanguageId) { languageId = tokenLanguageId; modeBrackets = LanguageConfigurationRegistry.getBracketsSupport(languageId); - resetCounts(modeBrackets); + resetCounts(languageId, modeBrackets); } } @@ -2415,7 +2426,7 @@ export class TextModel extends Disposable implements model.ITextModel { } languageId = tokenLanguageId; modeBrackets = LanguageConfigurationRegistry.getBracketsSupport(languageId); - resetCounts(modeBrackets); + resetCounts(languageId, modeBrackets); } const searchInToken = (!!modeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); diff --git a/src/vs/editor/common/model/tokensStore.ts b/src/vs/editor/common/model/tokensStore.ts index aa6953853ca..359c755e057 100644 --- a/src/vs/editor/common/model/tokensStore.ts +++ b/src/vs/editor/common/model/tokensStore.ts @@ -121,7 +121,7 @@ export interface IEncodedTokens { getMetadata(tokenIndex: number): number; clear(): void; - acceptDeleteRange(startDeltaLine: number, startCharacter: number, endDeltaLine: number, endCharacter: number): void; + acceptDeleteRange(horizontalShiftForFirstLineTokens: number, startDeltaLine: number, startCharacter: number, endDeltaLine: number, endCharacter: number): void; acceptInsertText(deltaLine: number, character: number, eolCount: number, firstLineLength: number, lastLineLength: number, firstCharCode: number): void; } @@ -173,7 +173,7 @@ export class SparseEncodedTokens implements IEncodedTokens { this._tokenCount = 0; } - public acceptDeleteRange(startDeltaLine: number, startCharacter: number, endDeltaLine: number, endCharacter: number): void { + public acceptDeleteRange(horizontalShiftForFirstLineTokens: number, startDeltaLine: number, startCharacter: number, endDeltaLine: number, endCharacter: number): void { // This is a bit complex, here are the cases I used to think about this: // // 1. The token starts before the deletion range @@ -292,9 +292,13 @@ export class SparseEncodedTokens implements IEncodedTokens { tokenDeltaLine -= deletedLineCount; } else if (tokenDeltaLine === endDeltaLine && tokenStartCharacter >= endCharacter) { // 4. (continued) The token starts after the deletion range, on the last line where a deletion occurs + if (horizontalShiftForFirstLineTokens && tokenDeltaLine === 0) { + tokenStartCharacter += horizontalShiftForFirstLineTokens; + tokenEndCharacter += horizontalShiftForFirstLineTokens; + } tokenDeltaLine -= deletedLineCount; - tokenStartCharacter -= endCharacter; - tokenEndCharacter -= endCharacter; + tokenStartCharacter -= (endCharacter - startCharacter); + tokenEndCharacter -= (endCharacter - startCharacter); } else { throw new Error(`Not possible!`); } @@ -373,8 +377,8 @@ export class SparseEncodedTokens implements IEncodedTokens { } } // => the token must move and keep its size constant - tokenDeltaLine += eolCount; if (tokenDeltaLine === deltaLine) { + tokenDeltaLine += eolCount; // this token is on the line where the insertion is taking place if (eolCount === 0) { tokenStartCharacter += firstLineLength; @@ -384,6 +388,8 @@ export class SparseEncodedTokens implements IEncodedTokens { tokenStartCharacter = lastLineLength + (tokenStartCharacter - character); tokenEndCharacter = tokenStartCharacter + tokenLength; } + } else { + tokenDeltaLine += eolCount; } } @@ -527,9 +533,9 @@ export class MultilineTokens2 { const deletedBefore = -firstLineIndex; this.startLineNumber -= deletedBefore; - this.tokens.acceptDeleteRange(0, 0, lastLineIndex, range.endColumn - 1); + this.tokens.acceptDeleteRange(range.startColumn - 1, 0, 0, lastLineIndex, range.endColumn - 1); } else { - this.tokens.acceptDeleteRange(firstLineIndex, range.startColumn - 1, lastLineIndex, range.endColumn - 1); + this.tokens.acceptDeleteRange(0, firstLineIndex, range.startColumn - 1, lastLineIndex, range.endColumn - 1); } } diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 5d3f0109194..3a1399ee682 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -1465,31 +1465,31 @@ export interface CodeLensProvider { resolveCodeLens?(model: model.ITextModel, codeLens: CodeLens, token: CancellationToken): ProviderResult; } -export interface SemanticColoringLegend { +export interface SemanticTokensLegend { readonly tokenTypes: string[]; readonly tokenModifiers: string[]; } -export interface SemanticColoringArea { - /** - * The zero-based line value where this token block begins. - */ - readonly line: number; - /** - * The actual token block encoded data. - */ +export interface SemanticTokens { + readonly resultId?: string; readonly data: Uint32Array; - } -export interface SemanticColoring { - readonly areas: SemanticColoringArea[]; - dispose(): void; +export interface SemanticTokensEdit { + readonly start: number; + readonly deleteCount: number; + readonly data?: Uint32Array; } -export interface SemanticColoringProvider { - getLegend(): SemanticColoringLegend; - provideSemanticColoring(model: model.ITextModel, token: CancellationToken): ProviderResult; +export interface SemanticTokensEdits { + readonly resultId?: string; + readonly edits: SemanticTokensEdit[]; +} + +export interface SemanticTokensProvider { + getLegend(): SemanticTokensLegend; + provideSemanticTokens(model: model.ITextModel, lastResultId: string | null, ranges: Range[] | null, token: CancellationToken): ProviderResult; + releaseSemanticTokens(resultId: string | undefined): void; } // --- feature registries ------ @@ -1597,7 +1597,7 @@ export const FoldingRangeProviderRegistry = new LanguageFeatureRegistry(); +export const SemanticTokensProviderRegistry = new LanguageFeatureRegistry(); /** * @internal diff --git a/src/vs/editor/common/services/markerDecorationsServiceImpl.ts b/src/vs/editor/common/services/markerDecorationsServiceImpl.ts index fbf9037fa91..44fd54c6c6d 100644 --- a/src/vs/editor/common/services/markerDecorationsServiceImpl.ts +++ b/src/vs/editor/common/services/markerDecorationsServiceImpl.ts @@ -17,6 +17,7 @@ import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDeco import { Schemas } from 'vs/base/common/network'; import { Emitter, Event } from 'vs/base/common/event'; import { withUndefinedAsNull } from 'vs/base/common/types'; +import { minimapWarning, minimapError } from 'vs/platform/theme/common/colorRegistry'; function MODEL_ID(resource: URI): string { return resource.toString(); @@ -205,7 +206,7 @@ export class MarkerDecorationsService extends Disposable implements IMarkerDecor color = themeColorFromId(overviewRulerWarning); zIndex = 20; minimap = { - color, + color: themeColorFromId(minimapWarning), position: MinimapPosition.Inline }; break; @@ -220,7 +221,7 @@ export class MarkerDecorationsService extends Disposable implements IMarkerDecor color = themeColorFromId(overviewRulerError); zIndex = 30; minimap = { - color, + color: themeColorFromId(minimapError), position: MinimapPosition.Inline }; break; diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index 3b9734e0a16..4ba21e84bf6 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -14,7 +14,7 @@ import { Range } from 'vs/editor/common/core/range'; import { DefaultEndOfLine, EndOfLinePreference, EndOfLineSequence, IIdentifiedSingleEditOperation, ITextBuffer, ITextBufferFactory, ITextModel, ITextModelCreationOptions } from 'vs/editor/common/model'; import { TextModel, createTextBuffer } from 'vs/editor/common/model/textModel'; import { IModelLanguageChangedEvent, IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; -import { LanguageIdentifier, SemanticColoringProviderRegistry, SemanticColoringProvider, SemanticColoring, SemanticColoringLegend } from 'vs/editor/common/modes'; +import { LanguageIdentifier, SemanticTokensProviderRegistry, SemanticTokensProvider, SemanticTokensLegend, SemanticTokens, SemanticTokensEdits } from 'vs/editor/common/modes'; import { PLAINTEXT_LANGUAGE_IDENTIFIER } from 'vs/editor/common/modes/modesRegistry'; import { ILanguageSelection } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -459,22 +459,22 @@ class SemanticColoringFeature extends Disposable { class SemanticStyling extends Disposable { - private _caches: WeakMap; + private _caches: WeakMap; constructor( private readonly _themeService: IThemeService ) { super(); - this._caches = new WeakMap(); + this._caches = new WeakMap(); if (this._themeService) { // workaround for tests which use undefined... :/ this._register(this._themeService.onThemeChange(() => { - this._caches = new WeakMap(); + this._caches = new WeakMap(); })); } } - public get(provider: SemanticColoringProvider): SemanticColoringProviderStyling { + public get(provider: SemanticTokensProvider): SemanticColoringProviderStyling { if (!this._caches.has(provider)) { this._caches.set(provider, new SemanticColoringProviderStyling(provider.getLegend(), this._themeService)); } @@ -580,7 +580,7 @@ class SemanticColoringProviderStyling { private readonly _hashTable: HashTable; constructor( - private readonly _legend: SemanticColoringLegend, + private readonly _legend: SemanticTokensLegend, private readonly _themeService: IThemeService ) { this._hashTable = new HashTable(); @@ -611,13 +611,39 @@ class SemanticColoringProviderStyling { } } +const enum SemanticColoringConstants { + /** + * Let's aim at having 8KB buffers if possible... + * So that would be 8192 / (5 * 4) = 409.6 tokens per area + */ + DesiredTokensPerArea = 400, + + /** + * Try to keep the total number of areas under 1024 if possible, + * simply compensate by having more tokens per area... + */ + DesiredMaxAreas = 1024, +} + +class SemanticTokensResponse { + constructor( + private readonly _provider: SemanticTokensProvider, + public readonly resultId: string | undefined, + public readonly data: Uint32Array + ) { } + + public dispose(): void { + this._provider.releaseSemanticTokens(this.resultId); + } +} + class ModelSemanticColoring extends Disposable { private _isDisposed: boolean; private readonly _model: ITextModel; private readonly _semanticStyling: SemanticStyling; private readonly _fetchSemanticTokens: RunOnceScheduler; - private _currentResponse: SemanticColoring | null; + private _currentResponse: SemanticTokensResponse | null; private _currentRequestCancellationTokenSource: CancellationTokenSource | null; constructor(model: ITextModel, themeService: IThemeService, stylingProvider: SemanticStyling) { @@ -626,17 +652,21 @@ class ModelSemanticColoring extends Disposable { this._isDisposed = false; this._model = model; this._semanticStyling = stylingProvider; - this._fetchSemanticTokens = this._register(new RunOnceScheduler(() => this._fetchSemanticTokensNow(), 500)); + this._fetchSemanticTokens = this._register(new RunOnceScheduler(() => this._fetchSemanticTokensNow(), 300)); this._currentResponse = null; this._currentRequestCancellationTokenSource = null; - this._register(this._model.onDidChangeContent(e => this._fetchSemanticTokens.schedule())); - this._register(SemanticColoringProviderRegistry.onDidChange(e => this._fetchSemanticTokens.schedule())); + this._register(this._model.onDidChangeContent(e => { + if (!this._fetchSemanticTokens.isScheduled()) { + this._fetchSemanticTokens.schedule(); + } + })); + this._register(SemanticTokensProviderRegistry.onDidChange(e => this._fetchSemanticTokens.schedule())); if (themeService) { // workaround for tests which use undefined... :/ this._register(themeService.onThemeChange(_ => { // clear out existing tokens - this._setSemanticTokens(null, null, []); + this._setSemanticTokens(null, null, null, []); this._fetchSemanticTokens.schedule(); })); } @@ -673,90 +703,208 @@ class ModelSemanticColoring extends Disposable { }); const styling = this._semanticStyling.get(provider); - const request = Promise.resolve(provider.provideSemanticColoring(this._model, this._currentRequestCancellationTokenSource.token)); + + const lastResultId = this._currentResponse ? this._currentResponse.resultId || null : null; + const request = Promise.resolve(provider.provideSemanticTokens(this._model, lastResultId, null, this._currentRequestCancellationTokenSource.token)); request.then((res) => { this._currentRequestCancellationTokenSource = null; contentChangeListener.dispose(); - this._setSemanticTokens(res || null, styling, pendingChanges); + this._setSemanticTokens(provider, res || null, styling, pendingChanges); }, (err) => { errors.onUnexpectedError(err); this._currentRequestCancellationTokenSource = null; contentChangeListener.dispose(); - this._setSemanticTokens(null, styling, pendingChanges); + this._setSemanticTokens(provider, null, styling, pendingChanges); }); } - private _setSemanticTokens(tokens: SemanticColoring | null, styling: SemanticColoringProviderStyling | null, pendingChanges: IModelContentChangedEvent[]): void { + private static _isSemanticTokens(v: SemanticTokens | SemanticTokensEdits): v is SemanticTokens { + return v && !!((v).data); + } + + private static _isSemanticTokensEdits(v: SemanticTokens | SemanticTokensEdits): v is SemanticTokensEdits { + return v && Array.isArray((v).edits); + } + + private static _copy(src: Uint32Array, srcOffset: number, dest: Uint32Array, destOffset: number, length: number): void { + for (let i = 0; i < length; i++) { + dest[destOffset + i] = src[srcOffset + i]; + } + } + + private _setSemanticTokens(provider: SemanticTokensProvider | null, tokens: SemanticTokens | SemanticTokensEdits | null, styling: SemanticColoringProviderStyling | null, pendingChanges: IModelContentChangedEvent[]): void { + const currentResponse = this._currentResponse; if (this._currentResponse) { this._currentResponse.dispose(); this._currentResponse = null; } if (this._isDisposed) { // disposed! - if (tokens) { - tokens.dispose(); + if (provider && tokens) { + provider.releaseSemanticTokens(tokens.resultId); } return; } - this._currentResponse = tokens; - if (!this._currentResponse || !styling) { + if (!provider || !tokens || !styling) { this._model.setSemanticTokens(null); return; } - const result: MultilineTokens2[] = []; - for (const area of this._currentResponse.areas) { - const srcTokens = area.data; - const tokenCount = srcTokens.length / 5; - let destTokens = new Uint32Array(4 * tokenCount); - let destOffset = 0; - for (let i = 0; i < tokenCount; i++) { - const srcOffset = 5 * i; - const deltaLine = srcTokens[srcOffset]; - const startCharacter = srcTokens[srcOffset + 1]; - const endCharacter = srcTokens[srcOffset + 2]; - const tokenTypeIndex = srcTokens[srcOffset + 3]; - const tokenModifierSet = srcTokens[srcOffset + 4]; - const metadata = styling.getMetadata(tokenTypeIndex, tokenModifierSet); - if (metadata !== Constants.NO_STYLING) { - destTokens[destOffset] = deltaLine; - destTokens[destOffset + 1] = startCharacter; - destTokens[destOffset + 2] = endCharacter; - destTokens[destOffset + 3] = metadata; - destOffset += 4; + if (ModelSemanticColoring._isSemanticTokensEdits(tokens)) { + if (!currentResponse) { + // not possible! + this._model.setSemanticTokens(null); + return; + } + if (tokens.edits.length === 0) { + // nothing to do! + tokens = { + resultId: tokens.resultId, + data: currentResponse.data + }; + } else { + let deltaLength = 0; + for (const edit of tokens.edits) { + deltaLength += (edit.data ? edit.data.length : 0) - edit.deleteCount; } - } - if (destOffset !== destTokens.length) { - destTokens = destTokens.subarray(0, destOffset); + const srcData = currentResponse.data; + const destData = new Uint32Array(srcData.length + deltaLength); + + let srcLastStart = srcData.length; + let destLastStart = destData.length; + for (let i = tokens.edits.length - 1; i >= 0; i--) { + const edit = tokens.edits[i]; + + const copyCount = srcLastStart - (edit.start + edit.deleteCount); + if (copyCount > 0) { + ModelSemanticColoring._copy(srcData, srcLastStart - copyCount, destData, destLastStart - copyCount, copyCount); + destLastStart -= copyCount; + } + + if (edit.data) { + ModelSemanticColoring._copy(edit.data, 0, destData, destLastStart - edit.data.length, edit.data.length); + destLastStart -= edit.data.length; + } + + srcLastStart = edit.start; + } + + if (srcLastStart > 0) { + ModelSemanticColoring._copy(srcData, 0, destData, 0, srcLastStart); + } + + tokens = { + resultId: tokens.resultId, + data: destData + }; } - const tokens = new MultilineTokens2(area.line, new SparseEncodedTokens(destTokens)); - result.push(tokens); } - // Adjust incoming semantic tokens - if (pendingChanges.length > 0) { - // More changes occurred while the request was running - // We need to: - // 1. Adjust incoming semantic tokens - // 2. Request them again - for (const change of pendingChanges) { - for (const area of result) { - for (const singleChange of change.changes) { - area.applyEdit(singleChange.range, singleChange.text); + if (ModelSemanticColoring._isSemanticTokens(tokens)) { + + this._currentResponse = new SemanticTokensResponse(provider, tokens.resultId, tokens.data); + + const srcData = tokens.data; + const tokenCount = (tokens.data.length / 5) | 0; + const tokensPerArea = Math.max(Math.ceil(tokenCount / SemanticColoringConstants.DesiredMaxAreas), SemanticColoringConstants.DesiredTokensPerArea); + + const result: MultilineTokens2[] = []; + + let tokenIndex = 0; + let lastLineNumber = 1; + let lastStartCharacter = 0; + while (tokenIndex < tokenCount) { + const tokenStartIndex = tokenIndex; + let tokenEndIndex = Math.min(tokenStartIndex + tokensPerArea, tokenCount); + + // Keep tokens on the same line in the same area... + if (tokenEndIndex < tokenCount) { + + let smallTokenEndIndex = tokenEndIndex; + while (smallTokenEndIndex - 1 > tokenStartIndex && srcData[5 * smallTokenEndIndex] === 0) { + smallTokenEndIndex--; + } + + if (smallTokenEndIndex - 1 === tokenStartIndex) { + // there are so many tokens on this line that our area would be empty, we must now go right + let bigTokenEndIndex = tokenEndIndex; + while (bigTokenEndIndex + 1 < tokenCount && srcData[5 * bigTokenEndIndex] === 0) { + bigTokenEndIndex++; + } + tokenEndIndex = bigTokenEndIndex; + } else { + tokenEndIndex = smallTokenEndIndex; } } + + let destData = new Uint32Array((tokenEndIndex - tokenStartIndex) * 4); + let destOffset = 0; + let areaLine = 0; + while (tokenIndex < tokenEndIndex) { + const srcOffset = 5 * tokenIndex; + const deltaLine = srcData[srcOffset]; + const deltaCharacter = srcData[srcOffset + 1]; + const lineNumber = lastLineNumber + deltaLine; + const startCharacter = (deltaLine === 0 ? lastStartCharacter + deltaCharacter : deltaCharacter); + const length = srcData[srcOffset + 2]; + const tokenTypeIndex = srcData[srcOffset + 3]; + const tokenModifierSet = srcData[srcOffset + 4]; + const metadata = styling.getMetadata(tokenTypeIndex, tokenModifierSet); + + if (metadata !== Constants.NO_STYLING) { + if (areaLine === 0) { + areaLine = lineNumber; + } + destData[destOffset] = lineNumber - areaLine; + destData[destOffset + 1] = startCharacter; + destData[destOffset + 2] = startCharacter + length; + destData[destOffset + 3] = metadata; + destOffset += 4; + } + + lastLineNumber = lineNumber; + lastStartCharacter = startCharacter; + tokenIndex++; + } + + if (destOffset !== destData.length) { + destData = destData.subarray(0, destOffset); + } + + const tokens = new MultilineTokens2(areaLine, new SparseEncodedTokens(destData)); + result.push(tokens); } - this._fetchSemanticTokens.schedule(); + // Adjust incoming semantic tokens + if (pendingChanges.length > 0) { + // More changes occurred while the request was running + // We need to: + // 1. Adjust incoming semantic tokens + // 2. Request them again + for (const change of pendingChanges) { + for (const area of result) { + for (const singleChange of change.changes) { + area.applyEdit(singleChange.range, singleChange.text); + } + } + } + + if (!this._fetchSemanticTokens.isScheduled()) { + this._fetchSemanticTokens.schedule(); + } + } + + this._model.setSemanticTokens(result); + return; } - this._model.setSemanticTokens(result); + this._model.setSemanticTokens(null); } - private _getSemanticColoringProvider(): SemanticColoringProvider | null { - const result = SemanticColoringProviderRegistry.ordered(this._model); + private _getSemanticColoringProvider(): SemanticTokensProvider | null { + const result = SemanticTokensProviderRegistry.ordered(this._model); return (result.length > 0 ? result[0] : null); } } diff --git a/src/vs/editor/common/view/editorColorRegistry.ts b/src/vs/editor/common/view/editorColorRegistry.ts index 2901c23cc75..fbdb785c49b 100644 --- a/src/vs/editor/common/view/editorColorRegistry.ts +++ b/src/vs/editor/common/view/editorColorRegistry.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { Color, RGBA } from 'vs/base/common/color'; -import { activeContrastBorder, editorBackground, editorForeground, registerColor, editorWarningForeground, editorInfoForeground, editorWarningBorder, editorInfoBorder, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; +import { activeContrastBorder, editorBackground, editorForeground, registerColor, editorWarningForeground, editorInfoForeground, editorWarningBorder, editorInfoBorder, contrastBorder, editorFindMatchHighlight } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; /** @@ -15,7 +15,7 @@ export const editorLineHighlight = registerColor('editor.lineHighlightBackground export const editorLineHighlightBorder = registerColor('editor.lineHighlightBorder', { dark: '#282828', light: '#eeeeee', hc: '#f38518' }, nls.localize('lineHighlightBorderBox', 'Background color for the border around the line at the cursor position.')); export const editorRangeHighlight = registerColor('editor.rangeHighlightBackground', { dark: '#ffffff0b', light: '#fdff0033', hc: null }, nls.localize('rangeHighlight', 'Background color of highlighted ranges, like by quick open and find features. The color must not be opaque so as not to hide underlying decorations.'), true); export const editorRangeHighlightBorder = registerColor('editor.rangeHighlightBorder', { dark: null, light: null, hc: activeContrastBorder }, nls.localize('rangeHighlightBorder', 'Background color of the border around highlighted ranges.'), true); -export const editorSymbolHighlight = registerColor('editor.symbolHighlightBackground', { dark: editorRangeHighlight, light: editorRangeHighlight, hc: null }, nls.localize('symbolHighlight', 'Background color of highlighted symbol, like for go to definition or go next/previous symbol. The color must not be opaque so as not to hide underlying decorations.'), true); +export const editorSymbolHighlight = registerColor('editor.symbolHighlightBackground', { dark: editorFindMatchHighlight, light: editorFindMatchHighlight, hc: null }, nls.localize('symbolHighlight', 'Background color of highlighted symbol, like for go to definition or go next/previous symbol. The color must not be opaque so as not to hide underlying decorations.'), true); export const editorSymbolHighlightBorder = registerColor('editor.symbolHighlightBorder', { dark: null, light: null, hc: activeContrastBorder }, nls.localize('symbolHighlightBorder', 'Background color of the border around highlighted symbols.'), true); export const editorCursorForeground = registerColor('editorCursor.foreground', { dark: '#AEAFAD', light: Color.black, hc: Color.white }, nls.localize('caret', 'Color of the editor cursor.')); diff --git a/src/vs/editor/contrib/bracketMatching/bracketMatching.ts b/src/vs/editor/contrib/bracketMatching/bracketMatching.ts index ac8db2f7325..a669b572023 100644 --- a/src/vs/editor/contrib/bracketMatching/bracketMatching.ts +++ b/src/vs/editor/contrib/bracketMatching/bracketMatching.ts @@ -116,7 +116,7 @@ export class BracketMatchingController extends Disposable implements editorCommo private _lastVersionId: number; private _decorations: string[]; private readonly _updateBracketsSoon: RunOnceScheduler; - private _matchBrackets: boolean; + private _matchBrackets: 'never' | 'near' | 'always'; constructor( editor: ICodeEditor @@ -132,7 +132,7 @@ export class BracketMatchingController extends Disposable implements editorCommo this._updateBracketsSoon.schedule(); this._register(editor.onDidChangeCursorPosition((e) => { - if (!this._matchBrackets) { + if (this._matchBrackets === 'never') { // Early exit if nothing needs to be done! // Leave some form of early exit check here if you wish to continue being a cursor position change listener ;) return; @@ -153,12 +153,13 @@ export class BracketMatchingController extends Disposable implements editorCommo this._updateBracketsSoon.schedule(); })); this._register(editor.onDidChangeConfiguration((e) => { - this._matchBrackets = this._editor.getOption(EditorOption.matchBrackets); - if (!this._matchBrackets && this._decorations.length > 0) { - // Remove existing decorations if bracket matching is off + if (e.hasChanged(EditorOption.matchBrackets)) { + this._matchBrackets = this._editor.getOption(EditorOption.matchBrackets); this._decorations = this._editor.deltaDecorations(this._decorations, []); + this._lastBracketsData = []; + this._lastVersionId = 0; + this._updateBracketsSoon.schedule(); } - this._updateBracketsSoon.schedule(); })); } @@ -262,7 +263,7 @@ export class BracketMatchingController extends Disposable implements editorCommo }); private _updateBrackets(): void { - if (!this._matchBrackets) { + if (this._matchBrackets === 'never') { return; } this._recomputeBrackets(); @@ -332,8 +333,8 @@ export class BracketMatchingController extends Disposable implements editorCommo } else { let brackets = model.matchBracket(position); let options = BracketMatchingController._DECORATION_OPTIONS_WITH_OVERVIEW_RULER; - if (!brackets) { - brackets = model.findEnclosingBrackets(position); + if (!brackets && this._matchBrackets === 'always') { + brackets = model.findEnclosingBrackets(position, 20 /* give at most 20ms to compute */); options = BracketMatchingController._DECORATION_OPTIONS_WITHOUT_OVERVIEW_RULER; } newData[newDataLen++] = new BracketsData(position, brackets, options); diff --git a/src/vs/editor/contrib/contextmenu/contextmenu.ts b/src/vs/editor/contrib/contextmenu/contextmenu.ts index 6a301ba93e4..6114c170cce 100644 --- a/src/vs/editor/contrib/contextmenu/contextmenu.ts +++ b/src/vs/editor/contrib/contextmenu/contextmenu.ts @@ -148,19 +148,29 @@ export class ContextMenuController implements IEditorContribution { // translate them into other actions for (let group of groups) { const [, actions] = group; + let addedItems = 0; for (const action of actions) { if (action instanceof SubmenuItemAction) { const subActions = this._getMenuActions(model, action.item.submenu); if (subActions.length > 0) { result.push(new ContextSubMenu(action.label, subActions)); + addedItems++; } } else { result.push(action); + addedItems++; } } - result.push(new Separator()); + + if (addedItems) { + result.push(new Separator()); + } } - result.pop(); // remove last separator + + if (result.length) { + result.pop(); // remove last separator + } + return result; } diff --git a/src/vs/editor/contrib/documentSymbols/outlineTree.ts b/src/vs/editor/contrib/documentSymbols/outlineTree.ts index 970453bc12b..b45f6f13e6c 100644 --- a/src/vs/editor/contrib/documentSymbols/outlineTree.ts +++ b/src/vs/editor/contrib/documentSymbols/outlineTree.ts @@ -7,7 +7,7 @@ import * as dom from 'vs/base/browser/dom'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IDataSource, ITreeNode, ITreeRenderer, ITreeSorter, ITreeFilter } from 'vs/base/browser/ui/tree/tree'; -import { values, forEach } from 'vs/base/common/collections'; +import { values } from 'vs/base/common/collections'; import { createMatches, FuzzyScore } from 'vs/base/common/filters'; import 'vs/css!./media/outlineTree'; import 'vs/css!./media/symbol-icons'; @@ -22,6 +22,8 @@ import { MarkerSeverity } from 'vs/platform/markers/common/markers'; import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { registerColor, listErrorForeground, listWarningForeground, foreground } from 'vs/platform/theme/common/colorRegistry'; import { IdleValue } from 'vs/base/common/async'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; +import { URI } from 'vs/base/common/uri'; export type OutlineItem = OutlineGroup | OutlineElement; @@ -278,27 +280,26 @@ export class OutlineFilter implements ITreeFilter { [SymbolKind.TypeParameter]: 'showTypeParameters', }); - private readonly _filteredTypes = new Set(); - constructor( private readonly _prefix: string, - @IConfigurationService private readonly _configService: IConfigurationService, - ) { - this.update(); - } - - update() { - this._filteredTypes.clear(); - forEach(OutlineFilter.configNameToKind, entry => { - const key = `${this._prefix}.${entry.key}`; - if (this._configService.getValue(key) === false) { - this._filteredTypes.add(entry.value); - } - }); - } + @ITextResourceConfigurationService private readonly _textResourceConfigService: ITextResourceConfigurationService, + ) { } filter(element: OutlineItem): boolean { - return !(element instanceof OutlineElement) || !this._filteredTypes.has(element.symbol.kind); + const outline = OutlineModel.get(element); + let uri: URI | undefined; + + if (outline) { + uri = outline.textModel.uri; + } + + if (!(element instanceof OutlineElement)) { + return true; + } + + const configName = OutlineFilter.kindToConfigName[element.symbol.kind]; + const configKey = `${this._prefix}.${configName}`; + return this._textResourceConfigService.getValue(uri, configKey); } } diff --git a/src/vs/editor/contrib/find/findWidget.ts b/src/vs/editor/contrib/find/findWidget.ts index 5c9aee4a2ed..eef499b1a0e 100644 --- a/src/vs/editor/contrib/find/findWidget.ts +++ b/src/vs/editor/contrib/find/findWidget.ts @@ -63,7 +63,7 @@ const PART_WIDTH = 275; const FIND_INPUT_AREA_WIDTH = PART_WIDTH - 54; let MAX_MATCHES_COUNT_WIDTH = 69; -let FIND_ALL_CONTROLS_WIDTH = 17/** Find Input margin-left */ + (MAX_MATCHES_COUNT_WIDTH + 3 + 1) /** Match Results */ + 23 /** Button */ * 4 + 2/** sash */; +// let FIND_ALL_CONTROLS_WIDTH = 17/** Find Input margin-left */ + (MAX_MATCHES_COUNT_WIDTH + 3 + 1) /** Match Results */ + 23 /** Button */ * 4 + 2/** sash */; const FIND_INPUT_AREA_HEIGHT = 33; // The height of Find Widget when Replace Input is not visible. const ctrlEnterReplaceAllWarningPromptedKey = 'ctrlEnterReplaceAll.windows.donotask'; @@ -706,10 +706,12 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas if (this._resized) { this._findInput.inputBox.layout(); - let findInputWidth = this._findInput.inputBox.width; + let findInputWidth = this._findInput.inputBox.element.clientWidth; if (findInputWidth > 0) { this._replaceInput.width = findInputWidth; } + } else if (this._isReplaceVisible) { + this._replaceInput.width = dom.getTotalWidth(this._findInput.domNode); } } @@ -1159,13 +1161,11 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas return; } - const inputBoxWidth = width - FIND_ALL_CONTROLS_WIDTH; const maxWidth = parseFloat(dom.getComputedStyle(this._domNode).maxWidth!) || 0; if (width > maxWidth) { return; } this._domNode.style.width = `${width}px`; - this._findInput.inputBox.width = inputBoxWidth; if (this._isReplaceVisible) { this._replaceInput.width = dom.getTotalWidth(this._findInput.domNode); } @@ -1197,10 +1197,8 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas */ } - const inputBoxWidth = width - FIND_ALL_CONTROLS_WIDTH; this._domNode.style.width = `${width}px`; - this._findInput.inputBox.width = inputBoxWidth; if (this._isReplaceVisible) { this._replaceInput.width = dom.getTotalWidth(this._findInput.domNode); } diff --git a/src/vs/editor/contrib/gotoSymbol/symbolNavigation.ts b/src/vs/editor/contrib/gotoSymbol/symbolNavigation.ts index 9d9bc68a558..879f3b9aaf9 100644 --- a/src/vs/editor/contrib/gotoSymbol/symbolNavigation.ts +++ b/src/vs/editor/contrib/gotoSymbol/symbolNavigation.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ReferencesModel, OneReference } from 'vs/editor/contrib/gotoSymbol/referencesModel'; -import { RawContextKey, IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -155,10 +155,7 @@ registerEditorCommand(new class extends EditorCommand { constructor() { super({ id: 'editor.gotoNextSymbolFromResult', - precondition: ContextKeyExpr.and( - ctxHasSymbols, - ContextKeyExpr.equals('config.editor.gotoLocation.multiple', 'goto') - ), + precondition: ctxHasSymbols, kbOpts: { weight: KeybindingWeight.EditorContrib, primary: KeyCode.F12 diff --git a/src/vs/editor/contrib/parameterHints/parameterHints.css b/src/vs/editor/contrib/parameterHints/parameterHints.css index e77cb357003..c764f5e9581 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHints.css +++ b/src/vs/editor/contrib/parameterHints/parameterHints.css @@ -34,6 +34,7 @@ .monaco-editor .parameter-hints-widget .body { display: flex; flex-direction: column; + min-height: 100%; } .monaco-editor .parameter-hints-widget .signature { @@ -72,6 +73,7 @@ .monaco-editor .parameter-hints-widget.multiple .controls { display: flex; + padding: 0 2px; } .monaco-editor .parameter-hints-widget.multiple .button { @@ -95,6 +97,7 @@ height: 12px; line-height: 12px; opacity: 0.5; + font-family: var(--monaco-monospace-font); } .monaco-editor .parameter-hints-widget .signature .parameter.active { diff --git a/src/vs/editor/test/common/model/tokensStore.test.ts b/src/vs/editor/test/common/model/tokensStore.test.ts new file mode 100644 index 00000000000..4413caeebc6 --- /dev/null +++ b/src/vs/editor/test/common/model/tokensStore.test.ts @@ -0,0 +1,168 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { MultilineTokens2, SparseEncodedTokens } from 'vs/editor/common/model/tokensStore'; +import { Range } from 'vs/editor/common/core/range'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; +import { MetadataConsts, TokenMetadata } from 'vs/editor/common/modes'; + +suite('TokensStore', () => { + + const SEMANTIC_COLOR = 5; + + function parseTokensState(state: string[]): { text: string; tokens: MultilineTokens2; } { + let text: string[] = []; + let tokens: number[] = []; + let baseLine = 1; + for (let i = 0; i < state.length; i++) { + const line = state[i]; + + let startOffset = 0; + let lineText = ''; + while (true) { + const firstPipeOffset = line.indexOf('|', startOffset); + if (firstPipeOffset === -1) { + break; + } + const secondPipeOffset = line.indexOf('|', firstPipeOffset + 1); + if (secondPipeOffset === -1) { + break; + } + if (firstPipeOffset + 1 === secondPipeOffset) { + // skip || + lineText += line.substring(startOffset, secondPipeOffset + 1); + startOffset = secondPipeOffset + 1; + continue; + } + + lineText += line.substring(startOffset, firstPipeOffset); + const tokenStartCharacter = lineText.length; + const tokenLength = secondPipeOffset - firstPipeOffset - 1; + const metadata = (SEMANTIC_COLOR << MetadataConsts.FOREGROUND_OFFSET); + + if (tokens.length === 0) { + baseLine = i + 1; + } + tokens.push(i + 1 - baseLine, tokenStartCharacter, tokenStartCharacter + tokenLength, metadata); + + lineText += line.substr(firstPipeOffset + 1, tokenLength); + startOffset = secondPipeOffset + 1; + } + + lineText += line.substring(startOffset); + + text.push(lineText); + } + + return { + text: text.join('\n'), + tokens: new MultilineTokens2(baseLine, new SparseEncodedTokens(new Uint32Array(tokens))) + }; + } + + function extractState(model: TextModel): string[] { + let result: string[] = []; + for (let lineNumber = 1; lineNumber <= model.getLineCount(); lineNumber++) { + const lineTokens = model.getLineTokens(lineNumber); + const lineContent = model.getLineContent(lineNumber); + + let lineText = ''; + for (let i = 0; i < lineTokens.getCount(); i++) { + const tokenStartCharacter = lineTokens.getStartOffset(i); + const tokenEndCharacter = lineTokens.getEndOffset(i); + const metadata = lineTokens.getMetadata(i); + const color = TokenMetadata.getForeground(metadata); + const tokenText = lineContent.substring(tokenStartCharacter, tokenEndCharacter); + if (color === SEMANTIC_COLOR) { + lineText += `|${tokenText}|`; + } else { + lineText += tokenText; + } + } + + result.push(lineText); + } + return result; + } + + // function extractState + + function testTokensAdjustment(rawInitialState: string[], edits: IIdentifiedSingleEditOperation[], rawFinalState: string[]) { + const initialState = parseTokensState(rawInitialState); + const model = TextModel.createFromString(initialState.text); + model.setSemanticTokens([initialState.tokens]); + + model.applyEdits(edits); + + const actualState = extractState(model); + assert.deepEqual(actualState, rawFinalState); + + model.dispose(); + } + + test('issue #86303 - color shifting between different tokens', () => { + testTokensAdjustment( + [ + `import { |URI| } from 'vs/base/common/uri';`, + `const foo = |URI|.parse('hey');` + ], + [ + { range: new Range(2, 9, 2, 10), text: '' } + ], + [ + `import { |URI| } from 'vs/base/common/uri';`, + `const fo = |URI|.parse('hey');` + ] + ); + }); + + test('deleting a newline', () => { + testTokensAdjustment( + [ + `import { |URI| } from 'vs/base/common/uri';`, + `const foo = |URI|.parse('hey');` + ], + [ + { range: new Range(1, 42, 2, 1), text: '' } + ], + [ + `import { |URI| } from 'vs/base/common/uri';const foo = |URI|.parse('hey');` + ] + ); + }); + + test('inserting a newline', () => { + testTokensAdjustment( + [ + `import { |URI| } from 'vs/base/common/uri';const foo = |URI|.parse('hey');` + ], + [ + { range: new Range(1, 42, 1, 42), text: '\n' } + ], + [ + `import { |URI| } from 'vs/base/common/uri';`, + `const foo = |URI|.parse('hey');` + ] + ); + }); + + test('deleting a newline 2', () => { + testTokensAdjustment( + [ + `import { `, + ` |URI| } from 'vs/base/common/uri';const foo = |URI|.parse('hey');` + ], + [ + { range: new Range(1, 10, 2, 5), text: '' } + ], + [ + `import { |URI| } from 'vs/base/common/uri';const foo = |URI|.parse('hey');` + ] + ); + }); + +}); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index b5e04b5c7ec..100626ba2f1 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2880,9 +2880,9 @@ declare namespace monaco.editor { showFoldingControls?: 'always' | 'mouseover'; /** * Enable highlighting of matching brackets. - * Defaults to true. + * Defaults to 'always'. */ - matchBrackets?: boolean; + matchBrackets?: 'never' | 'near' | 'always'; /** * Enable rendering of whitespace. * Defaults to none. @@ -5575,30 +5575,31 @@ declare namespace monaco.languages { resolveCodeLens?(model: editor.ITextModel, codeLens: CodeLens, token: CancellationToken): ProviderResult; } - export interface SemanticColoringLegend { + export interface SemanticTokensLegend { readonly tokenTypes: string[]; readonly tokenModifiers: string[]; } - export interface SemanticColoringArea { - /** - * The zero-based line value where this token block begins. - */ - readonly line: number; - /** - * The actual token block encoded data. - */ + export interface SemanticTokens { + readonly resultId?: string; readonly data: Uint32Array; } - export interface SemanticColoring { - readonly areas: SemanticColoringArea[]; - dispose(): void; + export interface SemanticTokensEdit { + readonly start: number; + readonly deleteCount: number; + readonly data?: Uint32Array; } - export interface SemanticColoringProvider { - getLegend(): SemanticColoringLegend; - provideSemanticColoring(model: editor.ITextModel, token: CancellationToken): ProviderResult; + export interface SemanticTokensEdits { + readonly resultId?: string; + readonly edits: SemanticTokensEdit[]; + } + + export interface SemanticTokensProvider { + getLegend(): SemanticTokensLegend; + provideSemanticTokens(model: editor.ITextModel, lastResultId: string | null, ranges: Range[] | null, token: CancellationToken): ProviderResult; + releaseSemanticTokens(resultId: string | undefined): void; } export interface ILanguageExtensionPoint { diff --git a/src/vs/platform/electron/electron-main/electronMainService.ts b/src/vs/platform/electron/electron-main/electronMainService.ts index 7c2a6840f9e..24ef0a029ab 100644 --- a/src/vs/platform/electron/electron-main/electronMainService.ts +++ b/src/vs/platform/electron/electron-main/electronMainService.ts @@ -99,6 +99,7 @@ export class ElectronMainService implements IElectronMainService { cli: this.environmentService.args, forceNewWindow: options.forceNewWindow, forceReuseWindow: options.forceReuseWindow, + preferNewWindow: options.preferNewWindow, diffMode: options.diffMode, addMode: options.addMode, gotoLineMode: options.gotoLineMode, diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 96031eb4ec7..033cdc575f4 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -127,6 +127,7 @@ export interface IEnvironmentService extends IUserHomeProvider { // sync resources userDataSyncLogResource: URI; settingsSyncPreviewResource: URI; + keybindingsSyncPreviewResource: URI; machineSettingsHome: URI; machineSettingsResource: URI; diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index cd86c866c06..99cab4bba27 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -114,6 +114,9 @@ export class EnvironmentService implements IEnvironmentService { @memoize get settingsSyncPreviewResource(): URI { return resources.joinPath(this.userRoamingDataHome, '.settings.json'); } + @memoize + get keybindingsSyncPreviewResource(): URI { return resources.joinPath(this.userRoamingDataHome, '.keybindings.json'); } + @memoize get userDataSyncLogResource(): URI { return URI.file(path.join(this.logsPath, 'userDataSync.log')); } diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 658998bd7c2..83ed4656a1b 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -819,3 +819,18 @@ export function etag(stat: { mtime: number | undefined, size: number | undefined return stat.mtime.toString(29) + stat.size.toString(31); } + + +export function whenProviderRegistered(file: URI, fileService: IFileService): Promise { + if (fileService.canHandleResource(URI.from({ scheme: file.scheme }))) { + return Promise.resolve(); + } + return new Promise((c, e) => { + const disposable = fileService.onDidChangeFileSystemProviderRegistrations(e => { + if (e.scheme === file.scheme && e.added) { + disposable.dispose(); + c(); + } + }); + }); +} diff --git a/src/vs/platform/log/common/fileLogService.ts b/src/vs/platform/log/common/fileLogService.ts index e8bc1ae9005..32493332bc1 100644 --- a/src/vs/platform/log/common/fileLogService.ts +++ b/src/vs/platform/log/common/fileLogService.ts @@ -5,12 +5,13 @@ import { ILogService, LogLevel, AbstractLogService, ILoggerService, ILogger } from 'vs/platform/log/common/log'; import { URI } from 'vs/base/common/uri'; -import { IFileService } from 'vs/platform/files/common/files'; +import { IFileService, whenProviderRegistered } from 'vs/platform/files/common/files'; import { Queue } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { dirname, joinPath, basename } from 'vs/base/common/resources'; import { Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { BufferLogService } from 'vs/platform/log/common/bufferLog'; const MAX_FILE_SIZE = 1024 * 1024 * 5; @@ -163,6 +164,7 @@ export class FileLoggerService extends Disposable implements ILoggerService { constructor( @ILogService private logService: ILogService, @IInstantiationService private instantiationService: IInstantiationService, + @IFileService private fileService: IFileService, ) { super(); this._register(logService.onDidChangeLogLevel(level => this.loggers.forEach(logger => logger.setLevel(level)))); @@ -171,8 +173,9 @@ export class FileLoggerService extends Disposable implements ILoggerService { getLogger(resource: URI): ILogger { let logger = this.loggers.get(resource.toString()); if (!logger) { - logger = this.instantiationService.createInstance(FileLogService, basename(resource), resource, this.logService.getLevel()); + logger = new BufferLogService, this.logService.getLevel(); this.loggers.set(resource.toString(), logger); + whenProviderRegistered(resource, this.fileService).then(() => (logger).logger = this.instantiationService.createInstance(FileLogService, basename(resource), resource, this.logService.getLevel())); } return logger; } diff --git a/src/vs/platform/remote/common/tunnel.ts b/src/vs/platform/remote/common/tunnel.ts index ebbef722b59..78187325d07 100644 --- a/src/vs/platform/remote/common/tunnel.ts +++ b/src/vs/platform/remote/common/tunnel.ts @@ -12,7 +12,7 @@ export const ITunnelService = createDecorator('tunnelService'); export interface RemoteTunnel { readonly tunnelRemotePort: number; readonly tunnelLocalPort: number; - readonly localAddress?: URI; + readonly localAddress?: string; dispose(): void; } diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index 864db693fd1..1fb0eb51c41 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -304,8 +304,8 @@ export const editorFindRangeHighlightBorder = registerColor('editor.findRangeHig * * Distinct from normal editor find match to allow for better differentiation */ -export const searchEditorFindMatch = registerColor('searchEditor.findMatchBackground', { light: transparent(editorFindMatchHighlight, 0.5), dark: transparent(editorFindMatchHighlight, 0.5), hc: editorFindMatchHighlight }, nls.localize('searchEditor.queryMatch', "Color of the Search Editor query matches.")); -export const searchEditorFindMatchBorder = registerColor('searchEditor.findMatchBorder', { light: transparent(editorFindMatchHighlightBorder, 0.5), dark: transparent(editorFindMatchHighlightBorder, 0.5), hc: editorFindMatchHighlightBorder }, nls.localize('searchEditor.editorFindMatchBorder', "Border color of the Search Editor query matches.")); +export const searchEditorFindMatch = registerColor('searchEditor.findMatchBackground', { light: transparent(editorFindMatchHighlight, 0.66), dark: transparent(editorFindMatchHighlight, 0.66), hc: editorFindMatchHighlight }, nls.localize('searchEditor.queryMatch', "Color of the Search Editor query matches.")); +export const searchEditorFindMatchBorder = registerColor('searchEditor.findMatchBorder', { light: transparent(editorFindMatchHighlightBorder, 0.66), dark: transparent(editorFindMatchHighlightBorder, 0.66), hc: editorFindMatchHighlightBorder }, nls.localize('searchEditor.editorFindMatchBorder', "Border color of the Search Editor query matches.")); /** * Editor hover @@ -422,6 +422,8 @@ export const overviewRulerSelectionHighlightForeground = registerColor('editorOv export const minimapFindMatch = registerColor('minimap.findMatchHighlight', { light: '#d18616', dark: '#d18616', hc: '#AB5A00' }, nls.localize('minimapFindMatchHighlight', 'Minimap marker color for find matches.'), true); export const minimapSelection = registerColor('minimap.selectionHighlight', { light: '#ADD6FF', dark: '#264F78', hc: '#ffffff' }, nls.localize('minimapSelectionHighlight', 'Minimap marker color for the editor selection.'), true); +export const minimapError = registerColor('minimap.errorHighlight', { dark: new Color(new RGBA(255, 18, 18, 0.7)), light: new Color(new RGBA(255, 18, 18, 0.7)), hc: new Color(new RGBA(255, 50, 50, 1)) }, nls.localize('minimapError', 'Minimap marker color for errors.')); +export const minimapWarning = registerColor('minimap.warningHighlight', { dark: editorWarningForeground, light: editorWarningForeground, hc: editorWarningBorder }, nls.localize('overviewRuleWarning', 'Minimap marker color for warnings.')); export const problemsErrorIconForeground = registerColor('problemsErrorIcon.foreground', { dark: editorErrorForeground, light: editorErrorForeground, hc: editorErrorForeground }, nls.localize('problemsErrorIconForeground', "The color used for the problems error icon.")); export const problemsWarningIconForeground = registerColor('problemsWarningIcon.foreground', { dark: editorWarningForeground, light: editorWarningForeground, hc: editorWarningForeground }, nls.localize('problemsWarningIconForeground', "The color used for the problems warning icon.")); diff --git a/src/vs/platform/theme/common/tokenClassificationRegistry.ts b/src/vs/platform/theme/common/tokenClassificationRegistry.ts index 3fa3e0c9c46..fa4303f57a9 100644 --- a/src/vs/platform/theme/common/tokenClassificationRegistry.ts +++ b/src/vs/platform/theme/common/tokenClassificationRegistry.ts @@ -320,40 +320,45 @@ export function getTokenClassificationRegistry(): ITokenClassificationRegistry { return tokenClassificationRegistry; } -export const comments = registerTokenType('comments', nls.localize('comments', "Style for comments."), [['comment']]); -export const strings = registerTokenType('strings', nls.localize('strings', "Style for strings."), [['string']]); -export const keywords = registerTokenType('keywords', nls.localize('keywords', "Style for keywords."), [['keyword.control']]); -export const numbers = registerTokenType('numbers', nls.localize('numbers', "Style for numbers."), [['constant.numeric']]); -export const regexp = registerTokenType('regexp', nls.localize('regexp', "Style for expressions."), [['constant.regexp']]); -export const operators = registerTokenType('operators', nls.localize('operator', "Style for operators."), [['keyword.operator']]); +// default token types -export const namespaces = registerTokenType('namespaces', nls.localize('namespace', "Style for namespaces."), [['entity.name.namespace']]); +registerTokenType('comment', nls.localize('comment', "Style for comments."), [['comment']]); +registerTokenType('string', nls.localize('string', "Style for strings."), [['string']]); +registerTokenType('keyword', nls.localize('keyword', "Style for keywords."), [['keyword.control']]); +registerTokenType('number', nls.localize('number', "Style for numbers."), [['constant.numeric']]); +registerTokenType('regexp', nls.localize('regexp', "Style for expressions."), [['constant.regexp']]); +registerTokenType('operator', nls.localize('operator', "Style for operators."), [['keyword.operator']]); -export const types = registerTokenType('types', nls.localize('types', "Style for types."), [['entity.name.type'], ['entity.name.class'], ['support.type'], ['support.class']]); -export const structs = registerTokenType('structs', nls.localize('struct', "Style for structs."), [['storage.type.struct']], types); -export const classes = registerTokenType('classes', nls.localize('class', "Style for classes."), [['entity.name.class']], types); -export const interfaces = registerTokenType('interfaces', nls.localize('interface', "Style for interfaces."), undefined, types); -export const enums = registerTokenType('enums', nls.localize('enum', "Style for enums."), undefined, types); -export const parameterTypes = registerTokenType('parameterTypes', nls.localize('parameterType', "Style for parameter types."), undefined, types); +registerTokenType('namespace', nls.localize('namespace', "Style for namespaces."), [['entity.name.namespace']]); -export const functions = registerTokenType('functions', nls.localize('functions', "Style for functions"), [['entity.name.function'], ['support.function']]); -export const macros = registerTokenType('macros', nls.localize('macro', "Style for macros."), undefined, functions); +registerTokenType('type', nls.localize('type', "Style for types."), [['entity.name.type'], ['entity.name.class'], ['support.type'], ['support.class']]); +registerTokenType('struct', nls.localize('struct', "Style for structs."), [['storage.type.struct']], 'type'); +registerTokenType('class', nls.localize('class', "Style for classes."), [['entity.name.class']], 'type'); +registerTokenType('interface', nls.localize('interface', "Style for interfaces."), undefined, 'type'); +registerTokenType('enum', nls.localize('enum', "Style for enums."), undefined, 'type'); +registerTokenType('parameterType', nls.localize('parameterType', "Style for parameter types."), undefined, 'type'); -export const variables = registerTokenType('variables', nls.localize('variables', "Style for variables."), [['variable'], ['entity.name.variable']]); -export const constants = registerTokenType('constants', nls.localize('constants', "Style for constants."), undefined, variables); -export const parameters = registerTokenType('parameters', nls.localize('parameters', "Style for parameters."), undefined, variables); -export const property = registerTokenType('properties', nls.localize('properties', "Style for properties."), undefined, variables); +registerTokenType('function', nls.localize('function', "Style for functions"), [['entity.name.function'], ['support.function']]); +registerTokenType('macro', nls.localize('macro', "Style for macros."), undefined, 'function'); -export const labels = registerTokenType('labels', nls.localize('labels', "Style for labels. "), undefined); +registerTokenType('variable', nls.localize('variable', "Style for variables."), [['variable'], ['entity.name.variable']]); +registerTokenType('constant', nls.localize('constant', "Style for constants."), undefined, 'variable'); +registerTokenType('parameter', nls.localize('parameter', "Style for parameters."), undefined, 'variable'); +registerTokenType('property', nls.localize('propertie', "Style for properties."), undefined, 'variable'); + +registerTokenType('label', nls.localize('labels', "Style for labels. "), undefined); + +// default token modifiers + +registerTokenModifier('declaration', nls.localize('declaration', "Style for all symbol declarations."), undefined); +registerTokenModifier('documentation', nls.localize('documentation', "Style to use for references in documentation."), undefined); +registerTokenModifier('member', nls.localize('member', "Style to use for member functions, variables (fields) and types."), undefined); +registerTokenModifier('static', nls.localize('static', "Style to use for symbols that are static."), undefined); +registerTokenModifier('abstract', nls.localize('abstract', "Style to use for symbols that are abstract."), undefined); +registerTokenModifier('deprecated', nls.localize('deprecated', "Style to use for symbols that are deprecated."), undefined); +registerTokenModifier('modification', nls.localize('modification', "Style to use for write accesses."), undefined); +registerTokenModifier('async', nls.localize('async', "Style to use for symbols that are async."), undefined); -export const m_declaration = registerTokenModifier('declaration', nls.localize('declaration', "Style for all symbol declarations."), undefined); -export const m_documentation = registerTokenModifier('documentation', nls.localize('documentation', "Style to use for references in documentation."), undefined); -export const m_member = registerTokenModifier('member', nls.localize('member', "Style to use for member functions, variables (fields) and types."), undefined); -export const m_static = registerTokenModifier('static', nls.localize('static', "Style to use for symbols that are static."), undefined); -export const m_abstract = registerTokenModifier('abstract', nls.localize('abstract', "Style to use for symbols that are abstract."), undefined); -export const m_deprecated = registerTokenModifier('deprecated', nls.localize('deprecated', "Style to use for symbols that are deprecated."), undefined); -export const m_modification = registerTokenModifier('modification', nls.localize('modification', "Style to use for write accesses."), undefined); -export const m_async = registerTokenModifier('async', nls.localize('async', "Style to use for symbols that are async."), undefined); function bitCount(u: number) { // https://blogs.msdn.microsoft.com/jeuge/2005/06/08/bit-fiddling-3/ diff --git a/src/vs/platform/userDataSync/common/content.ts b/src/vs/platform/userDataSync/common/content.ts new file mode 100644 index 00000000000..5dd3d97a427 --- /dev/null +++ b/src/vs/platform/userDataSync/common/content.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { JSONPath } from 'vs/base/common/json'; +import { setProperty } from 'vs/base/common/jsonEdit'; +import { FormattingOptions } from 'vs/base/common/jsonFormatter'; + + +export function edit(content: string, originalPath: JSONPath, value: any, formattingOptions: FormattingOptions): string { + const edit = setProperty(content, originalPath, value, formattingOptions)[0]; + if (edit) { + content = content.substring(0, edit.offset) + edit.content + content.substring(edit.offset + edit.length); + } + return content; +} + +export function getLineStartOffset(content: string, eol: string, atOffset: number): number { + let lineStartingOffset = atOffset; + while (lineStartingOffset >= 0) { + if (content.charAt(lineStartingOffset) === eol.charAt(eol.length - 1)) { + if (eol.length === 1) { + return lineStartingOffset + 1; + } + } + lineStartingOffset--; + if (eol.length === 2) { + if (lineStartingOffset >= 0 && content.charAt(lineStartingOffset) === eol.charAt(0)) { + return lineStartingOffset + 2; + } + } + } + return 0; +} + +export function getLineEndOffset(content: string, eol: string, atOffset: number): number { + let lineEndOffset = atOffset; + while (lineEndOffset >= 0) { + if (content.charAt(lineEndOffset) === eol.charAt(eol.length - 1)) { + if (eol.length === 1) { + return lineEndOffset; + } + } + lineEndOffset++; + if (eol.length === 2) { + if (lineEndOffset >= 0 && content.charAt(lineEndOffset) === eol.charAt(1)) { + return lineEndOffset; + } + } + } + return content.length - 1; +} diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index 720d0d86ccc..2a0a9d7d603 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -18,6 +18,7 @@ import { startsWith } from 'vs/base/common/strings'; import { IFileService } from 'vs/platform/files/common/files'; import { Queue } from 'vs/base/common/async'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { localize } from 'vs/nls'; export interface ISyncPreviewResult { readonly added: ISyncExtension[]; @@ -26,6 +27,10 @@ export interface ISyncPreviewResult { readonly remote: ISyncExtension[] | null; } +interface ILastSyncUserData extends IUserData { + skippedExtensions: ISyncExtension[] | undefined; +} + export class ExtensionsSynchroniser extends Disposable implements ISynchroniser { private static EXTERNAL_USER_DATA_EXTENSIONS_KEY: string = 'extensions'; @@ -122,14 +127,16 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser private async doSync(): Promise { const lastSyncData = await this.getLastSyncUserData(); - let remoteData = await this.userDataSyncStoreService.read(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, lastSyncData); + const lastSyncExtensions: ISyncExtension[] | null = lastSyncData ? JSON.parse(lastSyncData.content!) : null; + let skippedExtensions: ISyncExtension[] = lastSyncData ? lastSyncData.skippedExtensions || [] : []; - const lastSyncExtensions: ISyncExtension[] = lastSyncData ? JSON.parse(lastSyncData.content!) : null; + let remoteData = await this.userDataSyncStoreService.read(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, lastSyncData); const remoteExtensions: ISyncExtension[] = remoteData.content ? JSON.parse(remoteData.content) : null; + const localExtensions = await this.getLocalExtensions(); this.logService.trace('Extensions: Merging remote extensions with local extensions...'); - const { added, removed, updated, remote } = this.merge(localExtensions, remoteExtensions, lastSyncExtensions); + const { added, removed, updated, remote } = this.merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions); if (!added.length && !removed.length && !updated.length && !remote) { this.logService.trace('Extensions: No changes found during synchronizing extensions.'); @@ -137,7 +144,7 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser if (added.length || removed.length || updated.length) { this.logService.info('Extensions: Updating local extensions...'); - await this.updateLocalExtensions(added, removed, updated); + skippedExtensions = await this.updateLocalExtensions(added, removed, updated, skippedExtensions); } if (remote) { @@ -151,7 +158,7 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser ) { // update last sync this.logService.info('Extensions: Updating last synchronised extensions...'); - await this.updateLastSyncValue(remoteData); + await this.updateLastSyncValue({ ...remoteData, skippedExtensions }); } } @@ -161,12 +168,12 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser * - Overwrite local with remote changes. Removed, Added, Updated. * - Update remote with those local extension which are newly added or updated or removed and untouched in remote. */ - private merge(localExtensions: ISyncExtension[], remoteExtensions: ISyncExtension[] | null, lastSyncExtensions: ISyncExtension[] | null): { added: ISyncExtension[], removed: IExtensionIdentifier[], updated: ISyncExtension[], remote: ISyncExtension[] | null } { + private merge(localExtensions: ISyncExtension[], remoteExtensions: ISyncExtension[] | null, lastSyncExtensions: ISyncExtension[] | null, skippedExtensions: ISyncExtension[]): { added: ISyncExtension[], removed: IExtensionIdentifier[], updated: ISyncExtension[], remote: ISyncExtension[] | null } { const ignoredExtensions = this.configurationService.getValue('sync.ignoredExtensions') || []; // First time sync if (!remoteExtensions) { this.logService.info('Extensions: Remote extensions does not exist. Synchronizing extensions for the first time.'); - return { added: [], removed: [], updated: [], remote: localExtensions.filter(({ identifier }) => ignoredExtensions.some(id => id.toLowerCase() === identifier.id.toLowerCase())) }; + return { added: [], removed: [], updated: [], remote: localExtensions.filter(({ identifier }) => ignoredExtensions.every(id => id.toLowerCase() !== identifier.id.toLowerCase())) }; } const uuids: Map = new Map(); @@ -187,6 +194,7 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser const remoteExtensionsMap = remoteExtensions.reduce(addExtensionToMap, new Map()); const newRemoteExtensionsMap = remoteExtensions.reduce(addExtensionToMap, new Map()); const lastSyncExtensionsMap = lastSyncExtensions ? lastSyncExtensions.reduce(addExtensionToMap, new Map()) : null; + const skippedExtensionsMap = skippedExtensions.reduce(addExtensionToMap, new Map()); const ignoredExtensionsSet = ignoredExtensions.reduce((set, id) => { const uuid = uuids.get(id.toLowerCase()); return set.add(uuid ? `uuid:${uuid}` : `id:${id.toLowerCase()}`); @@ -273,8 +281,8 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser // Locally removed extensions for (const key of values(baseToLocal.removed)) { - // If not updated in remote - if (!baseToRemote.updated.has(key)) { + // If not skipped and not updated in remote + if (!skippedExtensionsMap.has(key) && !baseToRemote.updated.has(key)) { newRemoteExtensionsMap.delete(key); } } @@ -308,13 +316,17 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser return { added, removed, updated }; } - private async updateLocalExtensions(added: ISyncExtension[], removed: IExtensionIdentifier[], updated: ISyncExtension[]): Promise { + private async updateLocalExtensions(added: ISyncExtension[], removed: IExtensionIdentifier[], updated: ISyncExtension[], skippedExtensions: ISyncExtension[]): Promise { + const removeFromSkipped: IExtensionIdentifier[] = []; + const addToSkipped: ISyncExtension[] = []; + if (removed.length) { const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User); const extensionsToRemove = installedExtensions.filter(({ identifier }) => removed.some(r => areSameExtensions(identifier, r))); - await Promise.all(extensionsToRemove.map(e => { - this.logService.info('Extensions: Removing local extension.', e.identifier.id); - return this.extensionManagementService.uninstall(e); + await Promise.all(extensionsToRemove.map(async extensionToRemove => { + this.logService.info('Extensions: Removing local extension.', extensionToRemove.identifier.id); + await this.extensionManagementService.uninstall(extensionToRemove); + removeFromSkipped.push(extensionToRemove.identifier); })); } @@ -323,18 +335,41 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser const extension = await this.extensionGalleryService.getCompatibleExtension(e.identifier, e.version); if (extension) { this.logService.info('Extensions: Installing local extension.', e.identifier.id, extension.version); - await this.extensionManagementService.installFromGallery(extension); + try { + await this.extensionManagementService.installFromGallery(extension); + removeFromSkipped.push(extension.identifier); + } catch (error) { + addToSkipped.push(e); + this.logService.error(error); + this.logService.info(localize('skip extension', "Skipping synchronising extension {0}", extension.displayName || extension.identifier.id)); + } + } else { + addToSkipped.push(e); } })); } + + const newSkippedExtensions: ISyncExtension[] = []; + for (const skippedExtension of skippedExtensions) { + if (!removeFromSkipped.some(e => areSameExtensions(e, skippedExtension.identifier))) { + newSkippedExtensions.push(skippedExtension); + } + } + for (const skippedExtension of addToSkipped) { + if (!newSkippedExtensions.some(e => areSameExtensions(e.identifier, skippedExtension.identifier))) { + newSkippedExtensions.push(skippedExtension); + } + } + return newSkippedExtensions; } private async getLocalExtensions(): Promise { const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User); - return installedExtensions.map(({ identifier }) => ({ identifier, enabled: true })); + return installedExtensions + .map(({ identifier }) => ({ identifier, enabled: true })); } - private async getLastSyncUserData(): Promise { + private async getLastSyncUserData(): Promise { try { const content = await this.fileService.readFile(this.lastSyncExtensionsResource); return JSON.parse(content.value.toString()); @@ -343,14 +378,14 @@ export class ExtensionsSynchroniser extends Disposable implements ISynchroniser } } + private async updateLastSyncValue(lastSyncUserData: ILastSyncUserData): Promise { + await this.fileService.writeFile(this.lastSyncExtensionsResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData))); + } + private async writeToRemote(extensions: ISyncExtension[], ref: string | null): Promise { const content = JSON.stringify(extensions); ref = await this.userDataSyncStoreService.write(ExtensionsSynchroniser.EXTERNAL_USER_DATA_EXTENSIONS_KEY, content, ref); return { content, ref }; } - private async updateLastSyncValue(remoteUserData: IUserData): Promise { - await this.fileService.writeFile(this.lastSyncExtensionsResource, VSBuffer.fromString(JSON.stringify(remoteUserData))); - } - } diff --git a/src/vs/platform/userDataSync/common/keybindingsMerge.ts b/src/vs/platform/userDataSync/common/keybindingsMerge.ts new file mode 100644 index 00000000000..3c9910a3095 --- /dev/null +++ b/src/vs/platform/userDataSync/common/keybindingsMerge.ts @@ -0,0 +1,367 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as objects from 'vs/base/common/objects'; +import { parse } from 'vs/base/common/json'; +import { values, keys } from 'vs/base/common/map'; +import { IUserFriendlyKeybinding } from 'vs/platform/keybinding/common/keybinding'; +import { firstIndex as findFirstIndex, equals } from 'vs/base/common/arrays'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import * as contentUtil from 'vs/platform/userDataSync/common/content'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { FormattingOptions } from 'vs/base/common/jsonFormatter'; +import { IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; + +interface ICompareResult { + added: Set; + removed: Set; + updated: Set; +} + +interface IMergeResult { + hasLocalForwarded: boolean; + hasRemoteForwarded: boolean; + added: Set; + removed: Set; + updated: Set; + conflicts: Set; +} + +export async function merge(localContent: string, remoteContent: string, baseContent: string | null, formattingOptions: FormattingOptions, userDataSyncUtilService: IUserDataSyncUtilService): Promise<{ mergeContent: string, hasChanges: boolean, hasConflicts: boolean }> { + const local = parse(localContent); + const remote = parse(remoteContent); + const base = baseContent ? parse(baseContent) : null; + + const userbindings: string[] = [...local, ...remote, ...(base || [])].map(keybinding => keybinding.key); + const normalizedKeys = await userDataSyncUtilService.resolveUserBindings(userbindings); + let keybindingsMergeResult = computeMergeResultByKeybinding(local, remote, base, normalizedKeys); + + if (!keybindingsMergeResult.hasLocalForwarded && !keybindingsMergeResult.hasRemoteForwarded) { + // No changes found between local and remote. + return { mergeContent: localContent, hasChanges: false, hasConflicts: false }; + } + + if (!keybindingsMergeResult.hasLocalForwarded && keybindingsMergeResult.hasRemoteForwarded) { + return { mergeContent: remoteContent, hasChanges: true, hasConflicts: false }; + } + + if (keybindingsMergeResult.hasLocalForwarded && !keybindingsMergeResult.hasRemoteForwarded) { + // Local has moved forward and remote has not. Return local. + return { mergeContent: localContent, hasChanges: true, hasConflicts: false }; + } + + // Both local and remote has moved forward. + const localByCommand = byCommand(local); + const remoteByCommand = byCommand(remote); + const baseByCommand = base ? byCommand(base) : null; + const localToRemoteByCommand = compareByCommand(localByCommand, remoteByCommand, normalizedKeys); + const baseToLocalByCommand = baseByCommand ? compareByCommand(baseByCommand, localByCommand, normalizedKeys) : { added: keys(localByCommand).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; + const baseToRemoteByCommand = baseByCommand ? compareByCommand(baseByCommand, remoteByCommand, normalizedKeys) : { added: keys(remoteByCommand).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; + + const commandsMergeResult = computeMergeResult(localToRemoteByCommand, baseToLocalByCommand, baseToRemoteByCommand); + let mergeContent = localContent; + + // Removed commands in Remote + for (const command of values(commandsMergeResult.removed)) { + if (commandsMergeResult.conflicts.has(command)) { + continue; + } + mergeContent = removeKeybindings(mergeContent, command, formattingOptions); + } + + // Added commands in remote + for (const command of values(commandsMergeResult.added)) { + if (commandsMergeResult.conflicts.has(command)) { + continue; + } + const keybindings = remoteByCommand.get(command)!; + // Ignore negated commands + if (keybindings.some(keybinding => keybinding.command !== `-${command}` && keybindingsMergeResult.conflicts.has(normalizedKeys[keybinding.key]))) { + commandsMergeResult.conflicts.add(command); + continue; + } + mergeContent = addKeybindings(mergeContent, keybindings, formattingOptions); + } + + // Updated commands in Remote + for (const command of values(commandsMergeResult.updated)) { + if (commandsMergeResult.conflicts.has(command)) { + continue; + } + const keybindings = remoteByCommand.get(command)!; + // Ignore negated commands + if (keybindings.some(keybinding => keybinding.command !== `-${command}` && keybindingsMergeResult.conflicts.has(normalizedKeys[keybinding.key]))) { + commandsMergeResult.conflicts.add(command); + continue; + } + mergeContent = updateKeybindings(mergeContent, command, keybindings, formattingOptions); + } + + const hasConflicts = commandsMergeResult.conflicts.size > 0; + if (hasConflicts) { + mergeContent = `<<<<<<< local${formattingOptions.eol}` + + mergeContent + + `${formattingOptions.eol}=======${formattingOptions.eol}` + + remoteContent + + `${formattingOptions.eol}>>>>>>> remote`; + } + + return { mergeContent, hasChanges: true, hasConflicts }; +} + +function computeMergeResult(localToRemote: ICompareResult, baseToLocal: ICompareResult, baseToRemote: ICompareResult): { added: Set, removed: Set, updated: Set, conflicts: Set } { + const added: Set = new Set(); + const removed: Set = new Set(); + const updated: Set = new Set(); + const conflicts: Set = new Set(); + + // Removed keys in Local + for (const key of values(baseToLocal.removed)) { + // Got updated in remote + if (baseToRemote.updated.has(key)) { + conflicts.add(key); + } + } + + // Removed keys in Remote + for (const key of values(baseToRemote.removed)) { + if (conflicts.has(key)) { + continue; + } + // Got updated in local + if (baseToLocal.updated.has(key)) { + conflicts.add(key); + } else { + // remove the key + removed.add(key); + } + } + + // Added keys in Local + for (const key of values(baseToLocal.added)) { + if (conflicts.has(key)) { + continue; + } + // Got added in remote + if (baseToRemote.added.has(key)) { + // Has different value + if (localToRemote.updated.has(key)) { + conflicts.add(key); + } + } + } + + // Added keys in remote + for (const key of values(baseToRemote.added)) { + if (conflicts.has(key)) { + continue; + } + // Got added in local + if (baseToLocal.added.has(key)) { + // Has different value + if (localToRemote.updated.has(key)) { + conflicts.add(key); + } + } else { + added.add(key); + } + } + + // Updated keys in Local + for (const key of values(baseToLocal.updated)) { + if (conflicts.has(key)) { + continue; + } + // Got updated in remote + if (baseToRemote.updated.has(key)) { + // Has different value + if (localToRemote.updated.has(key)) { + conflicts.add(key); + } + } + } + + // Updated keys in Remote + for (const key of values(baseToRemote.updated)) { + if (conflicts.has(key)) { + continue; + } + // Got updated in local + if (baseToLocal.updated.has(key)) { + // Has different value + if (localToRemote.updated.has(key)) { + conflicts.add(key); + } + } else { + // updated key + updated.add(key); + } + } + return { added, removed, updated, conflicts }; +} + +function computeMergeResultByKeybinding(local: IUserFriendlyKeybinding[], remote: IUserFriendlyKeybinding[], base: IUserFriendlyKeybinding[] | null, normalizedKeys: IStringDictionary): IMergeResult { + const empty = new Set(); + const localByKeybinding = byKeybinding(local, normalizedKeys); + const remoteByKeybinding = byKeybinding(remote, normalizedKeys); + const baseByKeybinding = base ? byKeybinding(base, normalizedKeys) : null; + + const localToRemoteByKeybinding = compareByKeybinding(localByKeybinding, remoteByKeybinding); + if (localToRemoteByKeybinding.added.size === 0 && localToRemoteByKeybinding.removed.size === 0 && localToRemoteByKeybinding.updated.size === 0) { + return { hasLocalForwarded: false, hasRemoteForwarded: false, added: empty, removed: empty, updated: empty, conflicts: empty }; + } + + const baseToLocalByKeybinding = baseByKeybinding ? compareByKeybinding(baseByKeybinding, localByKeybinding) : { added: keys(localByKeybinding).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; + if (baseToLocalByKeybinding.added.size === 0 && baseToLocalByKeybinding.removed.size === 0 && baseToLocalByKeybinding.updated.size === 0) { + // Remote has moved forward and local has not. + return { hasLocalForwarded: false, hasRemoteForwarded: true, added: empty, removed: empty, updated: empty, conflicts: empty }; + } + + const baseToRemoteByKeybinding = baseByKeybinding ? compareByKeybinding(baseByKeybinding, remoteByKeybinding) : { added: keys(remoteByKeybinding).reduce((r, k) => { r.add(k); return r; }, new Set()), removed: new Set(), updated: new Set() }; + if (baseToRemoteByKeybinding.added.size === 0 && baseToRemoteByKeybinding.removed.size === 0 && baseToRemoteByKeybinding.updated.size === 0) { + return { hasLocalForwarded: true, hasRemoteForwarded: false, added: empty, removed: empty, updated: empty, conflicts: empty }; + } + + const { added, removed, updated, conflicts } = computeMergeResult(localToRemoteByKeybinding, baseToLocalByKeybinding, baseToRemoteByKeybinding); + return { hasLocalForwarded: true, hasRemoteForwarded: true, added, removed, updated, conflicts }; +} + +function byKeybinding(keybindings: IUserFriendlyKeybinding[], keys: IStringDictionary) { + const map: Map = new Map(); + for (const keybinding of keybindings) { + const key = keys[keybinding.key]; + let value = map.get(key); + if (!value) { + value = []; + map.set(key, value); + } + value.push(keybinding); + + } + return map; +} + +function byCommand(keybindings: IUserFriendlyKeybinding[]): Map { + const map: Map = new Map(); + for (const keybinding of keybindings) { + const command = keybinding.command[0] === '-' ? keybinding.command.substring(1) : keybinding.command; + let value = map.get(command); + if (!value) { + value = []; + map.set(command, value); + } + value.push(keybinding); + } + return map; +} + + +function compareByKeybinding(from: Map, to: Map): ICompareResult { + const fromKeys = keys(from); + const toKeys = keys(to); + const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set()); + const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set()); + const updated: Set = new Set(); + + for (const key of fromKeys) { + if (removed.has(key)) { + continue; + } + const value1: IUserFriendlyKeybinding[] = from.get(key)!.map(keybinding => ({ ...keybinding, ...{ key } })); + const value2: IUserFriendlyKeybinding[] = to.get(key)!.map(keybinding => ({ ...keybinding, ...{ key } })); + if (!equals(value1, value2, (a, b) => isSameKeybinding(a, b))) { + updated.add(key); + } + } + + return { added, removed, updated }; +} + +function compareByCommand(from: Map, to: Map, normalizedKeys: IStringDictionary): ICompareResult { + const fromKeys = keys(from); + const toKeys = keys(to); + const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set()); + const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set()); + const updated: Set = new Set(); + + for (const key of fromKeys) { + if (removed.has(key)) { + continue; + } + const value1: IUserFriendlyKeybinding[] = from.get(key)!.map(keybinding => ({ ...keybinding, ...{ key: normalizedKeys[keybinding.key] } })); + const value2: IUserFriendlyKeybinding[] = to.get(key)!.map(keybinding => ({ ...keybinding, ...{ key: normalizedKeys[keybinding.key] } })); + if (!areSameKeybindingsWithSameCommand(value1, value2)) { + updated.add(key); + } + } + + return { added, removed, updated }; +} + +function areSameKeybindingsWithSameCommand(value1: IUserFriendlyKeybinding[], value2: IUserFriendlyKeybinding[]): boolean { + // Compare entries adding keybindings + if (!equals(value1.filter(({ command }) => command[0] !== '-'), value2.filter(({ command }) => command[0] !== '-'), (a, b) => isSameKeybinding(a, b))) { + return false; + } + // Compare entries removing keybindings + if (!equals(value1.filter(({ command }) => command[0] === '-'), value2.filter(({ command }) => command[0] === '-'), (a, b) => isSameKeybinding(a, b))) { + return false; + } + return true; +} + +function isSameKeybinding(a: IUserFriendlyKeybinding, b: IUserFriendlyKeybinding): boolean { + if (a.command !== b.command) { + return false; + } + if (a.key !== b.key) { + return false; + } + const whenA = ContextKeyExpr.deserialize(a.when); + const whenB = ContextKeyExpr.deserialize(b.when); + if ((whenA && !whenB) || (!whenA && whenB)) { + return false; + } + if (whenA && whenB && !whenA.equals(whenB)) { + return false; + } + if (!objects.equals(a.args, b.args)) { + return false; + } + return true; +} + +function addKeybindings(content: string, keybindings: IUserFriendlyKeybinding[], formattingOptions: FormattingOptions): string { + for (const keybinding of keybindings) { + content = contentUtil.edit(content, [-1], keybinding, formattingOptions); + } + return content; +} + +function removeKeybindings(content: string, command: string, formattingOptions: FormattingOptions): string { + const keybindings = parse(content); + for (let index = keybindings.length - 1; index >= 0; index--) { + if (keybindings[index].command === command || keybindings[index].command === `-${command}`) { + content = contentUtil.edit(content, [index], undefined, formattingOptions); + } + } + return content; +} + +function updateKeybindings(content: string, command: string, keybindings: IUserFriendlyKeybinding[], formattingOptions: FormattingOptions): string { + const allKeybindings = parse(content); + const location = findFirstIndex(allKeybindings, keybinding => keybinding.command === command || keybinding.command === `-${command}`); + // Remove all entries with this command + for (let index = allKeybindings.length - 1; index >= 0; index--) { + if (allKeybindings[index].command === command || allKeybindings[index].command === `-${command}`) { + content = contentUtil.edit(content, [index], undefined, formattingOptions); + } + } + // add all entries at the same location where the entry with this command was located. + for (let index = keybindings.length - 1; index >= 0; index--) { + content = contentUtil.edit(content, [location], keybindings[index], formattingOptions); + } + return content; +} diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts new file mode 100644 index 00000000000..269065cd9a0 --- /dev/null +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -0,0 +1,331 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { IFileService, FileSystemProviderErrorCode, FileSystemProviderError, IFileContent } from 'vs/platform/files/common/files'; +import { IUserData, UserDataSyncStoreError, UserDataSyncStoreErrorCode, ISynchroniser, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; +import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { parse, ParseError } from 'vs/base/common/json'; +import { localize } from 'vs/nls'; +import { Emitter, Event } from 'vs/base/common/event'; +import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { URI } from 'vs/base/common/uri'; +import { joinPath, dirname } from 'vs/base/common/resources'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { OS, OperatingSystem } from 'vs/base/common/platform'; +import { isUndefined } from 'vs/base/common/types'; + +interface ISyncContent { + mac?: string; + linux?: string; + windows?: string; + all?: string; +} + +interface ISyncPreviewResult { + readonly fileContent: IFileContent | null; + readonly remoteUserData: IUserData; + readonly hasLocalChanged: boolean; + readonly hasRemoteChanged: boolean; + readonly hasConflicts: boolean; +} + +export class KeybindingsSynchroniser extends Disposable implements ISynchroniser { + + private static EXTERNAL_USER_DATA_KEYBINDINGS_KEY: string = 'keybindings'; + + private syncPreviewResultPromise: CancelablePromise | null = null; + + private _status: SyncStatus = SyncStatus.Idle; + get status(): SyncStatus { return this._status; } + private _onDidChangStatus: Emitter = this._register(new Emitter()); + readonly onDidChangeStatus: Event = this._onDidChangStatus.event; + + private _onDidChangeLocal: Emitter = this._register(new Emitter()); + readonly onDidChangeLocal: Event = this._onDidChangeLocal.event; + + private readonly lastSyncKeybindingsResource: URI; + + constructor( + @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, + @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IFileService private readonly fileService: IFileService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IUserDataSyncUtilService private readonly userDataSyncUtilService: IUserDataSyncUtilService, + ) { + super(); + this.lastSyncKeybindingsResource = joinPath(this.environmentService.userRoamingDataHome, '.lastSyncKeybindings.json'); + this._register(this.fileService.watch(dirname(this.environmentService.keybindingsResource))); + this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.environmentService.keybindingsResource))(() => this._onDidChangeLocal.fire())); + } + + private setStatus(status: SyncStatus): void { + if (this._status !== status) { + this._status = status; + this._onDidChangStatus.fire(status); + } + } + + async sync(_continue?: boolean): Promise { + if (!this.configurationService.getValue('sync.enableKeybindings')) { + this.logService.trace('Keybindings: Skipping synchronizing keybindings as it is disabled.'); + return false; + } + + if (_continue) { + this.logService.info('Keybindings: Resumed synchronizing keybindings'); + return this.continueSync(); + } + + if (this.status !== SyncStatus.Idle) { + this.logService.trace('Keybindings: Skipping synchronizing keybindings as it is running already.'); + return false; + } + + this.logService.trace('Keybindings: Started synchronizing keybindings...'); + this.setStatus(SyncStatus.Syncing); + + try { + const result = await this.getPreview(); + if (result.hasConflicts) { + this.logService.info('Keybindings: Detected conflicts while synchronizing keybindings.'); + this.setStatus(SyncStatus.HasConflicts); + return false; + } + await this.apply(); + return true; + } catch (e) { + this.syncPreviewResultPromise = null; + this.setStatus(SyncStatus.Idle); + if (e instanceof UserDataSyncStoreError && e.code === UserDataSyncStoreErrorCode.Rejected) { + // Rejected as there is a new remote version. Syncing again, + this.logService.info('Keybindings: Failed to synchronise keybindings as there is a new remote version available. Synchronizing again...'); + return this.sync(); + } + if (e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) { + // Rejected as there is a new local version. Syncing again. + this.logService.info('Keybindings: Failed to synchronise keybindings as there is a new local version available. Synchronizing again...'); + return this.sync(); + } + throw e; + } + } + + stop(): void { + if (this.syncPreviewResultPromise) { + this.syncPreviewResultPromise.cancel(); + this.syncPreviewResultPromise = null; + this.logService.info('Keybindings: Stopped synchronizing keybindings.'); + } + this.fileService.del(this.environmentService.keybindingsSyncPreviewResource); + this.setStatus(SyncStatus.Idle); + } + + private async continueSync(): Promise { + if (this.status !== SyncStatus.HasConflicts) { + return false; + } + await this.apply(); + return true; + } + + private async apply(): Promise { + if (!this.syncPreviewResultPromise) { + return; + } + + if (await this.fileService.exists(this.environmentService.keybindingsSyncPreviewResource)) { + const keybindingsPreivew = await this.fileService.readFile(this.environmentService.keybindingsSyncPreviewResource); + const content = keybindingsPreivew.value.toString(); + if (this.hasErrors(content)) { + const error = new Error(localize('errorInvalidKeybindings', "Unable to sync keybindings. Please resolve conflicts without any errors/warnings and try again.")); + this.logService.error(error); + throw error; + } + + let { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged } = await this.syncPreviewResultPromise; + if (!hasLocalChanged && !hasRemoteChanged) { + this.logService.trace('Keybindings: No changes found during synchronizing keybindings.'); + } + if (hasLocalChanged) { + this.logService.info('Keybindings: Updating local keybindings'); + await this.updateLocalContent(content, fileContent); + } + if (hasRemoteChanged) { + this.logService.info('Keybindings: Updating remote keybindings'); + const remoteContents = this.updateSyncContent(content, remoteUserData.content); + const ref = await this.updateRemoteUserData(remoteContents, remoteUserData.ref); + remoteUserData = { ref, content: remoteContents }; + } + if (remoteUserData.content) { + this.logService.info('Keybindings: Updating last synchronised keybindings'); + const lastSyncContent = this.updateSyncContent(content, null); + await this.updateLastSyncUserData({ ref: remoteUserData.ref, content: lastSyncContent }); + } + + // Delete the preview + await this.fileService.del(this.environmentService.keybindingsSyncPreviewResource); + } else { + this.logService.trace('Keybindings: No changes found during synchronizing keybindings.'); + } + + this.logService.trace('Keybindings: Finised synchronizing keybindings.'); + this.syncPreviewResultPromise = null; + this.setStatus(SyncStatus.Idle); + } + + private hasErrors(content: string): boolean { + const parseErrors: ParseError[] = []; + parse(content, parseErrors, { allowEmptyContent: true, allowTrailingComma: true }); + return parseErrors.length > 0; + } + + private getPreview(): Promise { + if (!this.syncPreviewResultPromise) { + this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(token)); + } + return this.syncPreviewResultPromise; + } + + private async generatePreview(token: CancellationToken): Promise { + const lastSyncData = await this.getLastSyncUserData(); + const lastSyncContent = lastSyncData && lastSyncData.content ? this.getKeybindingsContentFromSyncContent(lastSyncData.content) : null; + const remoteUserData = await this.getRemoteUserData(lastSyncData); + const remoteContent = remoteUserData.content ? this.getKeybindingsContentFromSyncContent(remoteUserData.content) : null; + // Get file content last to get the latest + const fileContent = await this.getLocalContent(); + let hasLocalChanged: boolean = false; + let hasRemoteChanged: boolean = false; + let hasConflicts: boolean = false; + let previewContent = null; + + if (remoteContent) { + const localContent: string = fileContent ? fileContent.value.toString() : '[]'; + if (this.hasErrors(localContent)) { + this.logService.error('Keybindings: Unable to sync keybindings as there are errors/warning in keybindings file.'); + return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts }; + } + + if (!lastSyncContent // First time sync + || lastSyncContent !== localContent // Local has forwarded + || lastSyncContent !== remoteContent // Remote has forwarded + ) { + this.logService.trace('Keybindings: Merging remote keybindings with local keybindings...'); + const formattingOptions = await this.userDataSyncUtilService.resolveFormattingOptions(this.environmentService.keybindingsResource); + const result = await merge(localContent, remoteContent, lastSyncContent, formattingOptions, this.userDataSyncUtilService); + // Sync only if there are changes + if (result.hasChanges) { + hasLocalChanged = result.mergeContent !== localContent; + hasRemoteChanged = result.mergeContent !== remoteContent; + hasConflicts = result.hasConflicts; + previewContent = result.mergeContent; + } + } + } + + // First time syncing to remote + else if (fileContent) { + this.logService.info('Keybindings: Remote keybindings does not exist. Synchronizing keybindings for the first time.'); + hasRemoteChanged = true; + previewContent = fileContent.value.toString(); + } + + if (previewContent && !token.isCancellationRequested) { + await this.fileService.writeFile(this.environmentService.keybindingsSyncPreviewResource, VSBuffer.fromString(previewContent)); + } + + return { fileContent, remoteUserData, hasLocalChanged, hasRemoteChanged, hasConflicts }; + } + + private async getLocalContent(): Promise { + try { + return await this.fileService.readFile(this.environmentService.keybindingsResource); + } catch (error) { + return null; + } + } + + private async updateLocalContent(newContent: string, oldContent: IFileContent | null): Promise { + if (oldContent) { + // file exists already + await this.fileService.writeFile(this.environmentService.keybindingsResource, VSBuffer.fromString(newContent), oldContent); + } else { + // file does not exist + await this.fileService.createFile(this.environmentService.keybindingsResource, VSBuffer.fromString(newContent), { overwrite: false }); + } + } + + private async getLastSyncUserData(): Promise { + try { + const content = await this.fileService.readFile(this.lastSyncKeybindingsResource); + return JSON.parse(content.value.toString()); + } catch (error) { + return null; + } + } + + private async updateLastSyncUserData(remoteUserData: IUserData): Promise { + await this.fileService.writeFile(this.lastSyncKeybindingsResource, VSBuffer.fromString(JSON.stringify(remoteUserData))); + } + + private async getRemoteUserData(lastSyncData: IUserData | null): Promise { + return this.userDataSyncStoreService.read(KeybindingsSynchroniser.EXTERNAL_USER_DATA_KEYBINDINGS_KEY, lastSyncData); + } + + private async updateRemoteUserData(content: string, ref: string | null): Promise { + return this.userDataSyncStoreService.write(KeybindingsSynchroniser.EXTERNAL_USER_DATA_KEYBINDINGS_KEY, content, ref); + } + + private getKeybindingsContentFromSyncContent(syncContent: string): string | null { + try { + const parsed = JSON.parse(syncContent); + if (!this.configurationService.getValue('sync.keybindingsPerPlatform')) { + return isUndefined(parsed.all) ? null : parsed.all; + } + switch (OS) { + case OperatingSystem.Macintosh: + return isUndefined(parsed.mac) ? null : parsed.mac; + case OperatingSystem.Linux: + return isUndefined(parsed.linux) ? null : parsed.linux; + case OperatingSystem.Windows: + return isUndefined(parsed.windows) ? null : parsed.windows; + } + } catch (e) { + this.logService.error(e); + return null; + } + } + + private updateSyncContent(keybindingsContent: string, syncContent: string | null): string { + let parsed: ISyncContent = {}; + try { + parsed = JSON.parse(syncContent || '{}'); + } catch (e) { + this.logService.error(e); + } + if (!this.configurationService.getValue('sync.keybindingsPerPlatform')) { + parsed.all = keybindingsContent; + } else { + delete parsed.all; + } + switch (OS) { + case OperatingSystem.Macintosh: + parsed.mac = keybindingsContent; + break; + case OperatingSystem.Linux: + parsed.linux = keybindingsContent; + break; + case OperatingSystem.Windows: + parsed.windows = keybindingsContent; + break; + } + return JSON.stringify(parsed); + } + +} diff --git a/src/vs/platform/userDataSync/common/keybindingsSyncIpc.ts b/src/vs/platform/userDataSync/common/keybindingsSyncIpc.ts new file mode 100644 index 00000000000..f23b3c90c0b --- /dev/null +++ b/src/vs/platform/userDataSync/common/keybindingsSyncIpc.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { Event } from 'vs/base/common/event'; +import { IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { URI } from 'vs/base/common/uri'; +import { FormattingOptions } from 'vs/base/common/jsonFormatter'; + +export class UserDataSycnUtilServiceChannel implements IServerChannel { + + constructor(private readonly service: IUserDataSyncUtilService) { } + + listen(_: unknown, event: string): Event { + throw new Error(`Event not found: ${event}`); + } + + call(context: any, command: string, args?: any): Promise { + switch (command) { + case 'resolveUserKeybindings': return this.service.resolveUserBindings(args[0]); + case 'resolveFormattingOptions': return this.service.resolveFormattingOptions(URI.revive(args[0])); + } + throw new Error('Invalid call'); + } +} + +export class UserDataSyncUtilServiceClient implements IUserDataSyncUtilService { + + _serviceBrand: undefined; + + constructor(private readonly channel: IChannel) { + } + + async resolveUserBindings(userbindings: string[]): Promise> { + return this.channel.call('resolveUserKeybindings', [userbindings]); + } + + async resolveFormattingOptions(file: URI): Promise { + return this.channel.call('resolveFormattingOptions', [file]); + } + +} diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index 1a757524dd6..4532f40b9dd 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -10,10 +10,10 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { parse, ParseError } from 'vs/base/common/json'; import { localize } from 'vs/nls'; import { Emitter, Event } from 'vs/base/common/event'; -import { CancelablePromise, createCancelablePromise, ThrottledDelayer } from 'vs/base/common/async'; +import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { URI } from 'vs/base/common/uri'; -import { joinPath } from 'vs/base/common/resources'; +import { joinPath, dirname } from 'vs/base/common/resources'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { startsWith } from 'vs/base/common/strings'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -37,7 +37,6 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { private _onDidChangStatus: Emitter = this._register(new Emitter()); readonly onDidChangeStatus: Event = this._onDidChangStatus.event; - private readonly throttledDelayer: ThrottledDelayer; private _onDidChangeLocal: Emitter = this._register(new Emitter()); readonly onDidChangeLocal: Event = this._onDidChangeLocal.event; @@ -53,23 +52,8 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { ) { super(); this.lastSyncSettingsResource = joinPath(this.environmentService.userRoamingDataHome, '.lastSyncSettings.json'); - this.throttledDelayer = this._register(new ThrottledDelayer(500)); - this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.environmentService.settingsResource))(() => this.throttledDelayer.trigger(() => this.onDidChangeSettings()))); - } - - private async onDidChangeSettings(): Promise { - const localFileContent = await this.getLocalFileContent(); - const lastSyncData = await this.getLastSyncUserData(); - if (localFileContent && lastSyncData) { - if (localFileContent.value.toString() !== lastSyncData.content) { - this._onDidChangeLocal.fire(); - return; - } - } - if (!localFileContent || !lastSyncData) { - this._onDidChangeLocal.fire(); - return; - } + this._register(this.fileService.watch(dirname(this.environmentService.settingsResource))); + this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.environmentService.settingsResource))(() => this._onDidChangeLocal.fire())); } private setStatus(status: SyncStatus): void { @@ -135,10 +119,9 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { } private async continueSync(): Promise { - if (this.status !== SyncStatus.HasConflicts) { - return false; + if (this.status === SyncStatus.HasConflicts) { + await this.apply(); } - await this.apply(); return true; } @@ -218,8 +201,8 @@ export class SettingsSynchroniser extends Disposable implements ISynchroniser { } if (!lastSyncData // First time sync - || lastSyncData.content !== localContent // Local has moved forwarded - || lastSyncData.content !== remoteContent // Remote has moved forwarded + || lastSyncData.content !== localContent // Local has forwarded + || lastSyncData.content !== remoteContent // Remote has forwarded ) { this.logService.trace('Settings: Merging remote settings with local settings...'); const result = await this.settingsMergeService.merge(localContent, remoteContent, lastSyncData ? lastSyncData.content : null, this.getIgnoredSettings()); diff --git a/src/vs/platform/userDataSync/common/userDataAutoSync.ts b/src/vs/platform/userDataSync/common/userDataAutoSync.ts new file mode 100644 index 00000000000..75123459141 --- /dev/null +++ b/src/vs/platform/userDataSync/common/userDataAutoSync.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IUserDataSyncService, SyncStatus, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Event } from 'vs/base/common/event'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { timeout } from 'vs/base/common/async'; +import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth'; + +export class UserDataAutoSync extends Disposable { + + private enabled: boolean = false; + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService, + @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, + @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, + @IAuthTokenService private readonly authTokenService: IAuthTokenService, + ) { + super(); + this.updateEnablement(false); + this._register(Event.any(authTokenService.onDidChangeStatus, userDataSyncService.onDidChangeStatus)(() => this.updateEnablement(true))); + this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('sync.enable'))(() => this.updateEnablement(true))); + } + + private updateEnablement(stopIfDisabled: boolean): void { + const enabled = this.isSyncEnabled(); + if (this.enabled === enabled) { + return; + } + + this.enabled = enabled; + if (this.enabled) { + this.logService.info('Syncing configuration started'); + this.sync(true); + return; + } else { + if (stopIfDisabled) { + this.userDataSyncService.stop(); + this.logService.info('Syncing configuration stopped.'); + } + } + + } + + protected async sync(loop: boolean): Promise { + if (this.enabled) { + try { + await this.userDataSyncService.sync(); + } catch (e) { + this.logService.error(e); + } + if (loop) { + await timeout(1000 * 60 * 5); // Loop sync for every 5 min. + this.sync(loop); + } + } + } + + private isSyncEnabled(): boolean { + return this.configurationService.getValue('sync.enable') + && this.userDataSyncService.status !== SyncStatus.Uninitialized + && this.authTokenService.status === AuthTokenStatus.SignedIn; + } + +} diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 5771808a1c9..bc76b41f9b0 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -15,6 +15,9 @@ import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/plat import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { ILogService } from 'vs/platform/log/common/log'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { FormattingOptions } from 'vs/base/common/jsonFormatter'; +import { URI } from 'vs/base/common/uri'; const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store'; @@ -52,6 +55,18 @@ export function registerConfiguration(): IDisposable { default: true, scope: ConfigurationScope.APPLICATION, }, + 'sync.enableKeybindings': { + type: 'boolean', + description: localize('sync.enableKeybindings', "Enable synchronizing keybindings."), + default: true, + scope: ConfigurationScope.APPLICATION, + }, + 'sync.keybindingsPerPlatform': { + type: 'boolean', + description: localize('sync.keybindingsPerPlatform', "Synchronize keybindings per platform."), + default: true, + scope: ConfigurationScope.APPLICATION, + }, 'sync.ignoredExtensions': { 'type': 'array', description: localize('sync.ignoredExtensions', "Configure extensions to be ignored while synchronizing."), @@ -132,6 +147,7 @@ export interface ISyncExtension { export const enum SyncSource { Settings = 1, + Keybindings, Extensions } @@ -173,6 +189,18 @@ export interface ISettingsMergeService { } +export const IUserDataSyncUtilService = createDecorator('IUserDataSyncUtilService'); + +export interface IUserDataSyncUtilService { + + _serviceBrand: undefined; + + resolveUserBindings(userbindings: string[]): Promise>; + + resolveFormattingOptions(resource: URI): Promise; + +} + export const IUserDataSyncLogService = createDecorator('IUserDataSyncLogService'); export interface IUserDataSyncLogService extends ILogService { diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 68935078c44..db118026b58 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -3,16 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, SyncStatus, ISynchroniser, IUserDataSyncStoreService, SyncSource, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, SyncStatus, ISynchroniser, IUserDataSyncStoreService, SyncSource } from 'vs/platform/userDataSync/common/userDataSync'; import { Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; import { Emitter, Event } from 'vs/base/common/event'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { timeout } from 'vs/base/common/async'; import { ExtensionsSynchroniser } from 'vs/platform/userDataSync/common/extensionsSync'; import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth'; +import { KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync'; export class UserDataSyncService extends Disposable implements IUserDataSyncService { @@ -31,6 +30,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ get conflictsSource(): SyncSource | null { return this._conflictsSource; } private readonly settingsSynchroniser: SettingsSynchroniser; + private readonly keybindingsSynchroniser: KeybindingsSynchroniser; private readonly extensionsSynchroniser: ExtensionsSynchroniser; constructor( @@ -40,8 +40,9 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ ) { super(); this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser)); + this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser)); this.extensionsSynchroniser = this._register(this.instantiationService.createInstance(ExtensionsSynchroniser)); - this.synchronisers = [this.settingsSynchroniser, this.extensionsSynchroniser]; + this.synchronisers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.extensionsSynchroniser]; this.updateStatus(); if (this.userDataSyncStoreService.userDataSyncStore) { @@ -111,6 +112,9 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ if (source instanceof SettingsSynchroniser) { return SyncSource.Settings; } + if (source instanceof KeybindingsSynchroniser) { + return SyncSource.Keybindings; + } } return null; } @@ -122,65 +126,3 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } } - -export class UserDataAutoSync extends Disposable { - - private enabled: boolean = false; - - constructor( - @IConfigurationService private readonly configurationService: IConfigurationService, - @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, - @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, - @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, - @IAuthTokenService private readonly authTokenService: IAuthTokenService, - ) { - super(); - this.updateEnablement(false); - this._register(Event.any(authTokenService.onDidChangeStatus, userDataSyncService.onDidChangeStatus)(() => this.updateEnablement(true))); - this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('sync.enable'))(() => this.updateEnablement(true))); - - // Sync immediately if there is a local change. - this._register(Event.debounce(this.userDataSyncService.onDidChangeLocal, () => undefined, 500)(() => this.sync(false))); - } - - private updateEnablement(stopIfDisabled: boolean): void { - const enabled = this.isSyncEnabled(); - if (this.enabled === enabled) { - return; - } - - this.enabled = enabled; - if (this.enabled) { - this.logService.info('Syncing configuration started'); - this.sync(true); - return; - } else { - if (stopIfDisabled) { - this.userDataSyncService.stop(); - this.logService.info('Syncing configuration stopped.'); - } - } - - } - - private async sync(loop: boolean): Promise { - if (this.enabled) { - try { - await this.userDataSyncService.sync(); - } catch (e) { - this.logService.error(e); - } - if (loop) { - await timeout(1000 * 30); // Loop sync for every 30s. - this.sync(loop); - } - } - } - - private isSyncEnabled(): boolean { - return this.configurationService.getValue('sync.enable') - && this.userDataSyncService.status !== SyncStatus.Uninitialized - && this.authTokenService.status === AuthTokenStatus.SignedIn; - } - -} diff --git a/src/vs/platform/userDataSync/electron-browser/userDataAutoSync.ts b/src/vs/platform/userDataSync/electron-browser/userDataAutoSync.ts new file mode 100644 index 00000000000..90ec9533ecd --- /dev/null +++ b/src/vs/platform/userDataSync/electron-browser/userDataAutoSync.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IUserDataSyncService, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync'; +import { Event } from 'vs/base/common/event'; +import { IElectronService } from 'vs/platform/electron/node/electron'; +import { UserDataAutoSync as BaseUserDataAutoSync } from 'vs/platform/userDataSync/common/userDataAutoSync'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IAuthTokenService } from 'vs/platform/auth/common/auth'; + +export class UserDataAutoSync extends BaseUserDataAutoSync { + + constructor( + @IUserDataSyncService userDataSyncService: IUserDataSyncService, + @IElectronService electronService: IElectronService, + @IConfigurationService configurationService: IConfigurationService, + @IUserDataSyncLogService logService: IUserDataSyncLogService, + @IAuthTokenService authTokenService: IAuthTokenService, + ) { + super(configurationService, userDataSyncService, logService, authTokenService); + + // Sync immediately if there is a local change. + this._register(Event.debounce(Event.any( + electronService.onWindowFocus, + electronService.onWindowOpen, + userDataSyncService.onDidChangeLocal + ), () => undefined, 500)(() => this.sync(false))); + } + +} diff --git a/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts b/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts new file mode 100644 index 00000000000..85ad7a36b13 --- /dev/null +++ b/src/vs/platform/userDataSync/test/common/keybindingsMerge.test.ts @@ -0,0 +1,735 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { OperatingSystem, OS } from 'vs/base/common/platform'; +import { IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; +import { FormattingOptions } from 'vs/base/common/jsonFormatter'; +import { URI } from 'vs/base/common/uri'; + +suite('KeybindingsMerge - No Conflicts', () => { + + test('merge when local and remote are same with one entry', async () => { + const localContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]); + const remoteContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]); + const actual = await mergeKeybindings(localContent, remoteContent, null); + assert.ok(!actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, localContent); + }); + + test('merge when local and remote are same with similar when contexts', async () => { + const localContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]); + const remoteContent = stringify([{ key: 'alt+c', command: 'a', when: '!editorReadonly && editorTextFocus' }]); + const actual = await mergeKeybindings(localContent, remoteContent, null); + assert.ok(!actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, localContent); + }); + + test('merge when local and remote has entries in different order', async () => { + const localContent = stringify([ + { key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+a', command: 'a', when: 'editorTextFocus' } + ]); + const remoteContent = stringify([ + { key: 'alt+a', command: 'a', when: 'editorTextFocus' }, + { key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' } + ]); + const actual = await mergeKeybindings(localContent, remoteContent, null); + assert.ok(!actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, localContent); + }); + + test('merge when local and remote are same with multiple entries', async () => { + const localContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + { key: 'cmd+c', command: 'b', args: { text: '`' } } + ]); + const remoteContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + { key: 'cmd+c', command: 'b', args: { text: '`' } } + ]); + const actual = await mergeKeybindings(localContent, remoteContent, null); + assert.ok(!actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, localContent); + }); + + test('merge when local and remote are same with different base content', async () => { + const localContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + { key: 'cmd+c', command: 'b', args: { text: '`' } } + ]); + const baseContent = stringify([ + { key: 'ctrl+c', command: 'e' }, + { key: 'shift+d', command: 'd', args: { text: '`' } } + ]); + const remoteContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + { key: 'cmd+c', command: 'b', args: { text: '`' } } + ]); + const actual = await mergeKeybindings(localContent, remoteContent, baseContent); + assert.ok(!actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, localContent); + }); + + test('merge when local and remote are same with multiple entries in different order', async () => { + const localContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + { key: 'cmd+c', command: 'b', args: { text: '`' } } + ]); + const remoteContent = stringify([ + { key: 'cmd+c', command: 'b', args: { text: '`' } }, + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + ]); + const actual = await mergeKeybindings(localContent, remoteContent, null); + assert.ok(!actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, localContent); + }); + + test('merge when local and remote are same when remove entry is in different order', async () => { + const localContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + { key: 'cmd+c', command: 'b', args: { text: '`' } } + ]); + const remoteContent = stringify([ + { key: 'alt+d', command: '-a' }, + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'cmd+c', command: 'b', args: { text: '`' } }, + ]); + const actual = await mergeKeybindings(localContent, remoteContent, null); + assert.ok(!actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, localContent); + }); + + test('merge when a new entry is added to remote', async () => { + const localContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + ]); + const remoteContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + { key: 'cmd+c', command: 'b', args: { text: '`' } }, + ]); + const actual = await mergeKeybindings(localContent, remoteContent, null); + assert.ok(actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, remoteContent); + }); + + test('merge when multiple new entries are added to remote', async () => { + const localContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + ]); + const remoteContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + { key: 'cmd+c', command: 'b', args: { text: '`' } }, + { key: 'cmd+d', command: 'c' }, + ]); + const actual = await mergeKeybindings(localContent, remoteContent, null); + assert.ok(actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, remoteContent); + }); + + test('merge when multiple new entries are added to remote from base and local has not changed', async () => { + const localContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + ]); + const remoteContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + { key: 'cmd+c', command: 'b', args: { text: '`' } }, + { key: 'cmd+d', command: 'c' }, + ]); + const actual = await mergeKeybindings(localContent, remoteContent, localContent); + assert.ok(actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, remoteContent); + }); + + test('merge when an entry is removed from remote from base and local has not changed', async () => { + const localContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + { key: 'cmd+c', command: 'b', args: { text: '`' } }, + ]); + const remoteContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + ]); + const actual = await mergeKeybindings(localContent, remoteContent, localContent); + assert.ok(actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, remoteContent); + }); + + test('merge when an entry (same command) is removed from remote from base and local has not changed', async () => { + const localContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + ]); + const remoteContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + ]); + const actual = await mergeKeybindings(localContent, remoteContent, localContent); + assert.ok(actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, remoteContent); + }); + + test('merge when an entry is updated in remote from base and local has not changed', async () => { + const localContent = stringify([ + { key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + ]); + const remoteContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + ]); + const actual = await mergeKeybindings(localContent, remoteContent, localContent); + assert.ok(actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, remoteContent); + }); + + test('merge when a command with multiple entries is updated from remote from base and local has not changed', async () => { + const localContent = stringify([ + { key: 'shift+c', command: 'c' }, + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: 'b' }, + { key: 'cmd+c', command: 'a' }, + ]); + const remoteContent = stringify([ + { key: 'shift+c', command: 'c' }, + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: 'b' }, + { key: 'cmd+d', command: 'a' }, + ]); + const actual = await mergeKeybindings(localContent, remoteContent, localContent); + assert.ok(actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, remoteContent); + }); + + test('merge when remote has moved forwareded with multiple changes and local stays with base', async () => { + const localContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'cmd+c', command: 'b', args: { text: '`' } }, + { key: 'alt+d', command: '-a' }, + { key: 'cmd+e', command: 'd' }, + { key: 'cmd+d', command: 'c', when: 'context1' }, + ]); + const remoteContent = stringify([ + { key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'cmd+e', command: 'd' }, + { key: 'alt+d', command: '-a' }, + { key: 'alt+f', command: 'f' }, + { key: 'alt+d', command: '-f' }, + { key: 'cmd+d', command: 'c', when: 'context1' }, + { key: 'cmd+c', command: '-c' }, + ]); + const actual = await mergeKeybindings(localContent, remoteContent, localContent); + assert.ok(actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, remoteContent); + }); + + test('merge when a new entry is added to local', async () => { + const localContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + { key: 'cmd+c', command: 'b', args: { text: '`' } }, + ]); + const remoteContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + ]); + const actual = await mergeKeybindings(localContent, remoteContent, null); + assert.ok(actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, localContent); + }); + + test('merge when multiple new entries are added to local', async () => { + const localContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + { key: 'cmd+c', command: 'b', args: { text: '`' } }, + { key: 'cmd+d', command: 'c' }, + ]); + const remoteContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + ]); + const actual = await mergeKeybindings(localContent, remoteContent, null); + assert.ok(actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, localContent); + }); + + test('merge when multiple new entries are added to local from base and remote is not changed', async () => { + const localContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + { key: 'cmd+c', command: 'b', args: { text: '`' } }, + { key: 'cmd+d', command: 'c' }, + ]); + const remoteContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + ]); + const actual = await mergeKeybindings(localContent, remoteContent, remoteContent); + assert.ok(actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, localContent); + }); + + test('merge when an entry is removed from local from base and remote has not changed', async () => { + const localContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + ]); + const remoteContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + { key: 'cmd+c', command: 'b', args: { text: '`' } }, + ]); + const actual = await mergeKeybindings(localContent, remoteContent, remoteContent); + assert.ok(actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, localContent); + }); + + test('merge when an entry (with same command) is removed from local from base and remote has not changed', async () => { + const localContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + ]); + const remoteContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: '-a' }, + ]); + const actual = await mergeKeybindings(localContent, remoteContent, remoteContent); + assert.ok(actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, localContent); + }); + + test('merge when an entry is updated in local from base and remote has not changed', async () => { + const localContent = stringify([ + { key: 'alt+d', command: 'a', when: 'editorTextFocus' }, + ]); + const remoteContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + ]); + const actual = await mergeKeybindings(localContent, remoteContent, remoteContent); + assert.ok(actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, localContent); + }); + + test('merge when a command with multiple entries is updated from local from base and remote has not changed', async () => { + const localContent = stringify([ + { key: 'shift+c', command: 'c' }, + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: 'b' }, + { key: 'cmd+c', command: 'a' }, + ]); + const remoteContent = stringify([ + { key: 'shift+c', command: 'c' }, + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+d', command: 'b' }, + { key: 'cmd+d', command: 'a' }, + ]); + const actual = await mergeKeybindings(localContent, remoteContent, remoteContent); + assert.ok(actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, localContent); + }); + + test('merge when local has moved forwareded with multiple changes and remote stays with base', async () => { + const localContent = stringify([ + { key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'cmd+e', command: 'd' }, + { key: 'alt+d', command: '-a' }, + { key: 'alt+f', command: 'f' }, + { key: 'alt+d', command: '-f' }, + { key: 'cmd+d', command: 'c', when: 'context1' }, + { key: 'cmd+c', command: '-c' }, + ]); + const remoteContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'cmd+c', command: 'b', args: { text: '`' } }, + { key: 'alt+d', command: '-a' }, + { key: 'cmd+e', command: 'd' }, + { key: 'cmd+d', command: 'c', when: 'context1' }, + ]); + const expected = stringify([ + { key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'cmd+e', command: 'd' }, + { key: 'alt+d', command: '-a' }, + { key: 'alt+f', command: 'f' }, + { key: 'alt+d', command: '-f' }, + { key: 'cmd+d', command: 'c', when: 'context1' }, + { key: 'cmd+c', command: '-c' }, + ]); + const actual = await mergeKeybindings(localContent, remoteContent, remoteContent); + assert.ok(actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, expected); + }); + + test('merge when local and remote has moved forwareded with conflicts', async () => { + const baseContent = stringify([ + { key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'ctrl+c', command: '-a' }, + { key: 'cmd+e', command: 'd' }, + { key: 'alt+a', command: 'f' }, + { key: 'alt+d', command: '-f' }, + { key: 'cmd+d', command: 'c', when: 'context1' }, + { key: 'cmd+c', command: '-c' }, + ]); + const localContent = stringify([ + { key: 'alt+d', command: '-f' }, + { key: 'cmd+e', command: 'd' }, + { key: 'cmd+c', command: '-c' }, + { key: 'cmd+d', command: 'c', when: 'context1' }, + { key: 'alt+a', command: 'f' }, + { key: 'alt+e', command: 'e' }, + ]); + const remoteContent = stringify([ + { key: 'alt+a', command: 'f' }, + { key: 'cmd+c', command: '-c' }, + { key: 'cmd+d', command: 'd' }, + { key: 'alt+d', command: '-f' }, + { key: 'alt+c', command: 'c', when: 'context1' }, + { key: 'alt+g', command: 'g', when: 'context2' }, + ]); + const expected = stringify([ + { key: 'alt+d', command: '-f' }, + { key: 'cmd+d', command: 'd' }, + { key: 'cmd+c', command: '-c' }, + { key: 'alt+c', command: 'c', when: 'context1' }, + { key: 'alt+a', command: 'f' }, + { key: 'alt+e', command: 'e' }, + { key: 'alt+g', command: 'g', when: 'context2' }, + ]); + const actual = await mergeKeybindings(localContent, remoteContent, baseContent); + assert.ok(actual.hasChanges); + assert.ok(!actual.hasConflicts); + assert.equal(actual.mergeContent, expected); + }); + +}); + + +suite.skip('KeybindingsMerge - Conflicts', () => { + + test('merge when local and remote with one entry but different value', async () => { + const localContent = stringify([{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }]); + const remoteContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]); + const actual = await mergeKeybindings(localContent, remoteContent, null); + assert.ok(actual.hasChanges); + assert.ok(actual.hasConflicts); + assert.equal(actual.mergeContent, + `<<<<<<< local +[ + { + "key": "alt+d", + "command": "a", + "when": "editorTextFocus && !editorReadonly" + } +] +======= +[ + { + "key": "alt+c", + "command": "a", + "when": "editorTextFocus && !editorReadonly" + } +] +>>>>>>> remote`); + }); + + test('merge when local and remote with different keybinding', async () => { + const localContent = stringify([ + { key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+a', command: '-a', when: 'editorTextFocus && !editorReadonly' } + ]); + const remoteContent = stringify([ + { key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+a', command: '-a', when: 'editorTextFocus && !editorReadonly' } + ]); + const actual = await mergeKeybindings(localContent, remoteContent, null); + assert.ok(actual.hasChanges); + assert.ok(actual.hasConflicts); + assert.equal(actual.mergeContent, + `<<<<<<< local +[ + { + "key": "alt+d", + "command": "a", + "when": "editorTextFocus && !editorReadonly" + }, + { + "key": "alt+a", + "command": "-a", + "when": "editorTextFocus && !editorReadonly" + } +] +======= +[ + { + "key": "alt+c", + "command": "a", + "when": "editorTextFocus && !editorReadonly" + }, + { + "key": "alt+a", + "command": "-a", + "when": "editorTextFocus && !editorReadonly" + } +] +>>>>>>> remote`); + }); + + test('merge when the entry is removed in local but updated in remote', async () => { + const baseContent = stringify([{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }]); + const localContent = stringify([]); + const remoteContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]); + const actual = await mergeKeybindings(localContent, remoteContent, baseContent); + assert.ok(actual.hasChanges); + assert.ok(actual.hasConflicts); + assert.equal(actual.mergeContent, + `<<<<<<< local +[] +======= +[ + { + "key": "alt+c", + "command": "a", + "when": "editorTextFocus && !editorReadonly" + } +] +>>>>>>> remote`); + }); + + test('merge when the entry is removed in local but updated in remote and a new entry is added in local', async () => { + const baseContent = stringify([{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }]); + const localContent = stringify([{ key: 'alt+b', command: 'b' }]); + const remoteContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]); + const actual = await mergeKeybindings(localContent, remoteContent, baseContent); + assert.ok(actual.hasChanges); + assert.ok(actual.hasConflicts); + assert.equal(actual.mergeContent, + `<<<<<<< local +[ + { + "key": "alt+b", + "command": "b" + } +] +======= +[ + { + "key": "alt+c", + "command": "a", + "when": "editorTextFocus && !editorReadonly" + } +] +>>>>>>> remote`); + }); + + test('merge when the entry is removed in remote but updated in local', async () => { + const baseContent = stringify([{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }]); + const localContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]); + const remoteContent = stringify([]); + const actual = await mergeKeybindings(localContent, remoteContent, baseContent); + assert.ok(actual.hasChanges); + assert.ok(actual.hasConflicts); + assert.equal(actual.mergeContent, + `<<<<<<< local +[ + { + "key": "alt+c", + "command": "a", + "when": "editorTextFocus && !editorReadonly" + } +] +======= +[] +>>>>>>> remote`); + }); + + test('merge when the entry is removed in remote but updated in local and a new entry is added in remote', async () => { + const baseContent = stringify([{ key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }]); + const localContent = stringify([{ key: 'alt+c', command: 'a', when: 'editorTextFocus && !editorReadonly' }]); + const remoteContent = stringify([{ key: 'alt+b', command: 'b' }]); + const actual = await mergeKeybindings(localContent, remoteContent, baseContent); + assert.ok(actual.hasChanges); + assert.ok(actual.hasConflicts); + assert.equal(actual.mergeContent, + `<<<<<<< local +[ + { + "key": "alt+c", + "command": "a", + "when": "editorTextFocus && !editorReadonly" + }, + { + "key": "alt+b", + "command": "b" + } +] +======= +[ + { + "key": "alt+b", + "command": "b" + } +] +>>>>>>> remote`); + }); + + test('merge when local and remote has moved forwareded with conflicts', async () => { + const baseContent = stringify([ + { key: 'alt+d', command: 'a', when: 'editorTextFocus && !editorReadonly' }, + { key: 'alt+c', command: '-a' }, + { key: 'cmd+e', command: 'd' }, + { key: 'alt+a', command: 'f' }, + { key: 'alt+d', command: '-f' }, + { key: 'cmd+d', command: 'c', when: 'context1' }, + { key: 'cmd+c', command: '-c' }, + ]); + const localContent = stringify([ + { key: 'alt+d', command: '-f' }, + { key: 'cmd+e', command: 'd' }, + { key: 'cmd+c', command: '-c' }, + { key: 'cmd+d', command: 'c', when: 'context1' }, + { key: 'alt+a', command: 'f' }, + { key: 'alt+e', command: 'e' }, + ]); + const remoteContent = stringify([ + { key: 'alt+a', command: 'f' }, + { key: 'cmd+c', command: '-c' }, + { key: 'cmd+d', command: 'd' }, + { key: 'alt+d', command: '-f' }, + { key: 'alt+c', command: 'c', when: 'context1' }, + { key: 'alt+g', command: 'g', when: 'context2' }, + ]); + const actual = await mergeKeybindings(localContent, remoteContent, baseContent); + assert.ok(actual.hasChanges); + assert.ok(actual.hasConflicts); + assert.equal(actual.mergeContent, + `<<<<<<< local +[ + { + "key": "alt+d", + "command": "-f" + }, + { + "key": "cmd+d", + "command": "d" + }, + { + "key": "cmd+c", + "command": "-c" + }, + { + "key": "cmd+d", + "command": "c", + "when": "context1" + }, + { + "key": "alt+a", + "command": "f" + }, + { + "key": "alt+e", + "command": "e" + }, + { + "key": "alt+g", + "command": "g", + "when": "context2" + } +] +======= +[ + { + "key": "alt+a", + "command": "f" + }, + { + "key": "cmd+c", + "command": "-c" + }, + { + "key": "cmd+d", + "command": "d" + }, + { + "key": "alt+d", + "command": "-f" + }, + { + "key": "alt+c", + "command": "c", + "when": "context1" + }, + { + "key": "alt+g", + "command": "g", + "when": "context2" + } +] +>>>>>>> remote`); + }); + +}); + + +async function mergeKeybindings(localContent: string, remoteContent: string, baseContent: string | null) { + const userDataSyncUtilService = new MockUserDataSyncUtilService(); + const formattingOptions = await userDataSyncUtilService.resolveFormattingOptions(); + return merge(localContent, remoteContent, baseContent, formattingOptions, userDataSyncUtilService); +} + +function stringify(value: any): string { + return JSON.stringify(value, null, '\t'); +} + +class MockUserDataSyncUtilService implements IUserDataSyncUtilService { + + _serviceBrand: any; + + async resolveUserBindings(userbindings: string[]): Promise> { + const keys: IStringDictionary = {}; + for (const keybinding of userbindings) { + keys[keybinding] = keybinding; + } + return keys; + } + + async resolveFormattingOptions(file?: URI): Promise { + return { eol: OS === OperatingSystem.Windows ? '\r\n' : '\n', insertSpaces: false, tabSize: 4 }; + } +} diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index 442167fb954..6fbb32cbd7a 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -25,6 +25,7 @@ export interface IBaseOpenWindowsOptions { export interface IOpenWindowOptions extends IBaseOpenWindowsOptions { forceNewWindow?: boolean; + preferNewWindow?: boolean; noRecentEntry?: boolean; } diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index fdf7f2e890b..58183df6c73 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; -import { basename, normalize, join, } from 'vs/base/common/path'; +import { basename, normalize, join, posix } from 'vs/base/common/path'; import { localize } from 'vs/nls'; import * as arrays from 'vs/base/common/arrays'; import { assign, mixin } from 'vs/base/common/objects'; @@ -1108,8 +1108,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const remoteAuthority = options.remoteAuthority; if (remoteAuthority) { - // assume it's a folder or workspace file - const first = anyPath.charCodeAt(0); // make absolute if (first !== CharCode.Slash) { @@ -1121,11 +1119,15 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const uri = URI.from({ scheme: Schemas.vscodeRemote, authority: remoteAuthority, path: anyPath }); - if (hasWorkspaceFileExtension(anyPath)) { - if (forceOpenWorkspaceAsFile) { + // guess the file type: If it ends with a slash it's a folder. If it has a file extension, it's a file or a workspace. By defaults it's a folder. + if (anyPath.charCodeAt(anyPath.length - 1) !== CharCode.Slash) { + if (hasWorkspaceFileExtension(anyPath)) { + if (forceOpenWorkspaceAsFile) { + return { fileUri: uri, remoteAuthority }; + } + } else if (posix.extname(anyPath).length > 0) { return { fileUri: uri, remoteAuthority }; } - return { workspace: getWorkspaceIdentifier(uri), remoteAuthority }; } return { folderUri: uri, remoteAuthority }; } diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 32bdd775c31..c07725c609b 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -8084,6 +8084,172 @@ declare module 'vscode' { waitUntil(thenable: Thenable): void; } + /** + * An event that is fired when files are going to be created. + * + * To make modifications to the workspace before the files are created, + * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a + * thenable that resolves to a [workspace edit](#WorkspaceEdit). + */ + export interface FileWillCreateEvent { + + /** + * The files that are going to be created. + */ + readonly files: ReadonlyArray; + + /** + * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). + * + * *Note:* This function can only be called during event dispatch and not + * in an asynchronous manner: + * + * ```ts + * workspace.onWillCreateFiles(event => { + * // async, will *throw* an error + * setTimeout(() => event.waitUntil(promise)); + * + * // sync, OK + * event.waitUntil(promise); + * }) + * ``` + * + * @param thenable A thenable that delays saving. + */ + waitUntil(thenable: Thenable): void; + + /** + * Allows to pause the event until the provided thenable resolves. + * + * *Note:* This function can only be called during event dispatch. + * + * @param thenable A thenable that delays saving. + */ + waitUntil(thenable: Thenable): void; + } + + /** + * An event that is fired after files are created. + */ + export interface FileCreateEvent { + + /** + * The files that got created. + */ + readonly files: ReadonlyArray; + } + + /** + * An event that is fired when files are going to be deleted. + * + * To make modifications to the workspace before the files are deleted, + * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a + * thenable that resolves to a [workspace edit](#WorkspaceEdit). + */ + export interface FileWillDeleteEvent { + + /** + * The files that are going to be deleted. + */ + readonly files: ReadonlyArray; + + /** + * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). + * + * *Note:* This function can only be called during event dispatch and not + * in an asynchronous manner: + * + * ```ts + * workspace.onWillCreateFiles(event => { + * // async, will *throw* an error + * setTimeout(() => event.waitUntil(promise)); + * + * // sync, OK + * event.waitUntil(promise); + * }) + * ``` + * + * @param thenable A thenable that delays saving. + */ + waitUntil(thenable: Thenable): void; + + /** + * Allows to pause the event until the provided thenable resolves. + * + * *Note:* This function can only be called during event dispatch. + * + * @param thenable A thenable that delays saving. + */ + waitUntil(thenable: Thenable): void; + } + + /** + * An event that is fired after files are deleted. + */ + export interface FileDeleteEvent { + + /** + * The files that got deleted. + */ + readonly files: ReadonlyArray; + } + + /** + * An event that is fired when files are going to be renamed. + * + * To make modifications to the workspace before the files are renamed, + * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a + * thenable that resolves to a [workspace edit](#WorkspaceEdit). + */ + export interface FileWillRenameEvent { + + /** + * The files that are going to be renamed. + */ + readonly files: ReadonlyArray<{ oldUri: Uri, newUri: Uri }>; + + /** + * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). + * + * *Note:* This function can only be called during event dispatch and not + * in an asynchronous manner: + * + * ```ts + * workspace.onWillCreateFiles(event => { + * // async, will *throw* an error + * setTimeout(() => event.waitUntil(promise)); + * + * // sync, OK + * event.waitUntil(promise); + * }) + * ``` + * + * @param thenable A thenable that delays saving. + */ + waitUntil(thenable: Thenable): void; + + /** + * Allows to pause the event until the provided thenable resolves. + * + * *Note:* This function can only be called during event dispatch. + * + * @param thenable A thenable that delays saving. + */ + waitUntil(thenable: Thenable): void; + } + + /** + * An event that is fired after files are renamed. + */ + export interface FileRenameEvent { + + /** + * The files that got renamed. + */ + readonly files: ReadonlyArray<{ oldUri: Uri, newUri: Uri }>; + } + + /** * An event describing a change to the set of [workspace folders](#workspace.workspaceFolders). */ @@ -8315,8 +8481,8 @@ declare module 'vscode' { * * All changes of a workspace edit are applied in the same order in which they have been added. If * multiple textual inserts are made at the same position, these strings appear in the resulting text - * in the order the 'inserts' were made. Invalid sequences like 'delete file a' -> 'insert text in file a' - * cause failure of the operation. + * in the order the 'inserts' were made, unless that are interleaved with resource edits. Invalid sequences + * like 'delete file a' -> 'insert text in file a' cause failure of the operation. * * When applying a workspace edit that consists only of text edits an 'all-or-nothing'-strategy is used. * A workspace edit with resource creations or deletions aborts the operation, e.g. consecutive edits will @@ -8433,6 +8599,76 @@ declare module 'vscode' { */ export const onDidSaveTextDocument: Event; + /** + * An event that is emitted when files are being created. + * + * *Note 1:* This event is triggered by user gestures, like creating a file from the + * explorer, or from the [`workspace.applyEdit`](#workspace.applyEdit)-api. This event is *not* fired when + * files change on disk, e.g triggered by another application, or when using the + * [`workspace.fs`](#FileSystem)-api. + * + * *Note 2:* When this event is fired, edits to files thare are being created cannot be applied. + */ + export const onWillCreateFiles: Event; + + /** + * An event that is emitted when files have been created. + * + * *Note:* This event is triggered by user gestures, like creating a file from the + * explorer, or from the [`workspace.applyEdit`](#workspace.applyEdit)-api, but this event is *not* fired when + * files change on disk, e.g triggered by another application, or when using the + * [`workspace.fs`](#FileSystem)-api. + */ + export const onDidCreateFiles: Event; + + /** + * An event that is emitted when files are being deleted. + * + * *Note 1:* This event is triggered by user gestures, like deleting a file from the + * explorer, or from the [`workspace.applyEdit`](#workspace.applyEdit)-api, but this event is *not* fired when + * files change on disk, e.g triggered by another application, or when using the + * [`workspace.fs`](#FileSystem)-api. + * + * *Note 2:* When deleting a folder with children only one event is fired. + */ + export const onWillDeleteFiles: Event; + + /** + * An event that is emitted when files have been deleted. + * + * *Note 1:* This event is triggered by user gestures, like deleting a file from the + * explorer, or from the [`workspace.applyEdit`](#workspace.applyEdit)-api, but this event is *not* fired when + * files change on disk, e.g triggered by another application, or when using the + * [`workspace.fs`](#FileSystem)-api. + * + * *Note 2:* When deleting a folder with children only one event is fired. + */ + export const onDidDeleteFiles: Event; + + /** + * An event that is emitted when files are being renamed. + * + * *Note 1:* This event is triggered by user gestures, like renaming a file from the + * explorer, and from the [`workspace.applyEdit`](#workspace.applyEdit)-api, but this event is *not* fired when + * files change on disk, e.g triggered by another application, or when using the + * [`workspace.fs`](#FileSystem)-api. + * + * *Note 2:* When renaming a folder with children only one event is fired. + */ + export const onWillRenameFiles: Event; + + /** + * An event that is emitted when files have been renamed. + * + * *Note 1:* This event is triggered by user gestures, like renaming a file from the + * explorer, and from the [`workspace.applyEdit`](#workspace.applyEdit)-api, but this event is *not* fired when + * files change on disk, e.g triggered by another application, or when using the + * [`workspace.fs`](#FileSystem)-api. + * + * *Note 2:* When renaming a folder with children only one event is fired. + */ + export const onDidRenameFiles: Event; + /** * Get a workspace configuration object. * @@ -9341,7 +9577,7 @@ declare module 'vscode' { constructor(port: number, host?: string); } - export type DebugAdapterDescriptor = DebugAdapterExecutable | DebugAdapterServer | DebugAdapterInlineImplementation; + export type DebugAdapterDescriptor = DebugAdapterExecutable | DebugAdapterServer; export interface DebugAdapterDescriptorFactory { /** diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index c554c0d3c17..19a434638c0 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -68,64 +68,182 @@ declare module 'vscode' { //#endregion - //#region Alex - semantic coloring + //#region Semantic tokens: https://github.com/microsoft/vscode/issues/86415 - export class SemanticColoringLegend { + export class SemanticTokensLegend { public readonly tokenTypes: string[]; public readonly tokenModifiers: string[]; constructor(tokenTypes: string[], tokenModifiers: string[]); } - export class SemanticColoringArea { - /** - * The zero-based line value where this token block begins. - */ - public readonly line: number; - /** - * The actual token block encoded data. - * A certain token (at index `i` is encoded using 5 uint32 integers): - * - at index `5*i` - `deltaLine`: token line number, relative to `SemanticColoringArea.line` - * - at index `5*i+1` - `startCharacter`: token start character offset inside the line (inclusive) - * - at index `5*i+2` - `endCharacter`: token end character offset inside the line (exclusive) - * - at index `5*i+3` - `tokenType`: will be looked up in `SemanticColoringLegend.tokenTypes` - * - at index `5*i+4` - `tokenModifiers`: each set bit will be looked up in `SemanticColoringLegend.tokenModifiers` - */ - public readonly data: Uint32Array; - - constructor(line: number, data: Uint32Array); + export class SemanticTokensBuilder { + constructor(); + push(line: number, char: number, length: number, tokenType: number, tokenModifiers: number): void; + build(): Uint32Array; } - export class SemanticColoring { - public readonly areas: SemanticColoringArea[]; + export class SemanticTokens { + /** + * The result id of the tokens. + * + * On a next call to `provideSemanticTokens`, if VS Code still holds in memory this result, + * the result id will be passed in as `SemanticTokensRequestOptions.previousResultId`. + */ + readonly resultId?: string; + readonly data: Uint32Array; - constructor(areas: SemanticColoringArea[]); + constructor(data: Uint32Array, resultId?: string); + } + + export class SemanticTokensEdits { + /** + * The result id of the tokens. + * + * On a next call to `provideSemanticTokens`, if VS Code still holds in memory this result, + * the result id will be passed in as `SemanticTokensRequestOptions.previousResultId`. + */ + readonly resultId?: string; + readonly edits: SemanticTokensEdit[]; + + constructor(edits: SemanticTokensEdit[], resultId?: string); + } + + export class SemanticTokensEdit { + readonly start: number; + readonly deleteCount: number; + readonly data?: Uint32Array; + + constructor(start: number, deleteCount: number, data?: Uint32Array); + } + + export interface SemanticTokensRequestOptions { + readonly ranges?: readonly Range[]; + /** + * The previous result id that the editor still holds in memory. + * + * Only when this is set it is safe for a `SemanticTokensProvider` to return `SemanticTokensEdits`. + */ + readonly previousResultId?: string; } /** - * The semantic coloring provider interface defines the contract between extensions and - * semantic coloring. - * - * + * The semantic tokens provider interface defines the contract between extensions and + * semantic tokens. */ - export interface SemanticColoringProvider { - - provideSemanticColoring(document: TextDocument, token: CancellationToken): ProviderResult; + export interface SemanticTokensProvider { + /** + * A file can contain many tokens, perhaps even hundreds of thousands tokens. Therefore, to improve + * the memory consumption around describing semantic tokens, we have decided to avoid allocating objects + * and we have decided to represent tokens from a file as an array of integers. + * + * + * In short, each token takes 5 integers to represent, so a specific token i in the file consists of the following fields: + * - at index `5*i` - `deltaLine`: token line number, relative to the previous token + * - at index `5*i+1` - `deltaStart`: token start character, relative to the previous token (relative to 0 or the previous token's start if they are on the same line) + * - at index `5*i+2` - `length`: the length of the token. A token cannot be multiline. + * - at index `5*i+3` - `tokenType`: will be looked up in `SemanticTokensLegend.tokenTypes` + * - at index `5*i+4` - `tokenModifiers`: each set bit will be looked up in `SemanticTokensLegend.tokenModifiers` + * + * + * Here is an example for encoding a file with 3 tokens: + * ``` + * [ { line: 2, startChar: 5, length: 3, tokenType: "properties", tokenModifiers: ["private", "static"] }, + * { line: 2, startChar: 10, length: 4, tokenType: "types", tokenModifiers: [] }, + * { line: 5, startChar: 2, length: 7, tokenType: "classes", tokenModifiers: [] } ] + * ``` + * + * 1. First of all, a legend must be devised. This legend must be provided up-front and capture all possible token types. + * For this example, we will choose the following legend which is passed in when registering the provider: + * ``` + * { tokenTypes: ['', 'properties', 'types', 'classes'], + * tokenModifiers: ['', 'private', 'static'] } + * ``` + * + * 2. The first transformation is to encode `tokenType` and `tokenModifiers` as integers using the legend. Token types are looked + * up by index, so a `tokenType` value of `1` means `tokenTypes[1]`. Multiple token modifiers can be set by using bit flags, + * so a `tokenModifier` value of `6` is first viewed as binary `0b110`, which means `[tokenModifiers[1], tokenModifiers[2]]` because + * bits 1 and 2 are set. Using this legend, the tokens now are: + * ``` + * [ { line: 2, startChar: 5, length: 3, tokenType: 1, tokenModifiers: 6 }, // 6 is 0b110 + * { line: 2, startChar: 10, length: 4, tokenType: 2, tokenModifiers: 0 }, + * { line: 5, startChar: 2, length: 7, tokenType: 3, tokenModifiers: 0 } ] + * ``` + * + * 3. Then, we will encode each token relative to the previous token in the file: + * ``` + * [ { deltaLine: 2, deltaStartChar: 5, length: 3, tokenType: 1, tokenModifiers: 6 }, + * // this token is on the same line as the first one, so the startChar is made relative + * { deltaLine: 0, deltaStartChar: 5, length: 4, tokenType: 2, tokenModifiers: 0 }, + * // this token is on a different line than the second one, so the startChar remains unchanged + * { deltaLine: 3, deltaStartChar: 2, length: 7, tokenType: 3, tokenModifiers: 0 } ] + * ``` + * + * 4. Finally, the integers are organized in a single array, which is a memory friendly representation: + * ``` + * // 1st token, 2nd token, 3rd token + * [ 2,5,3,1,6, 0,5,4,2,0, 3,2,7,3,0 ] + * ``` + * + * In principle, each call to `provideSemanticTokens` expects a complete representations of the semantic tokens. + * It is possible to simply return all the tokens at each call. + * + * But oftentimes, a small edit in the file will result in a small change to the above delta-based represented tokens. + * (In fact, that is why the above tokens are delta-encoded relative to their corresponding previous tokens). + * In such a case, if VS Code passes in the previous result id, it is possible for an advanced tokenization provider + * to return a delta to the integers array. + * + * To continue with the previous example, suppose a new line has been pressed at the beginning of the file, such that + * all the tokens are now one line lower, and that a new token has appeared since the last result on line 4. + * For example, the tokens might look like: + * ``` + * [ { line: 3, startChar: 5, length: 3, tokenType: "properties", tokenModifiers: ["private", "static"] }, + * { line: 3, startChar: 10, length: 4, tokenType: "types", tokenModifiers: [] }, + * { line: 4, startChar: 3, length: 5, tokenType: "properties", tokenModifiers: ["static"] }, + * { line: 6, startChar: 2, length: 7, tokenType: "classes", tokenModifiers: [] } ] + * ``` + * + * The integer encoding of all new tokens would be: + * ``` + * [ 3,5,3,1,6, 0,5,4,2,0, 1,3,5,1,2, 2,2,7,3,0 ] + * ``` + * + * A smart tokens provider can return a `resultId` to `SemanticTokens`. Then, if the editor still has in memory the previous + * result, the editor will pass in options the previous result id at `SemanticTokensRequestOptions.previousResultId`. Only when + * the editor passes in the previous result id, it is safe and smart for a smart tokens provider can compute a diff from the + * previous result to the new result. + * + * *NOTE*: It is illegal to return `SemanticTokensEdits` if `options.previousResultId` is not set! + * + * ``` + * [ 2,5,3,1,6, 0,5,4,2,0, 3,2,7,3,0 ] + * [ 3,5,3,1,6, 0,5,4,2,0, 1,3,5,1,2, 2,2,7,3,0 ] + * ``` + * and return as simple integer edits the diff: + * ``` + * { edits: [ + * { start: 0, deleteCount: 1, data: [3] } // replace integer at offset 0 with 3 + * { start: 10, deleteCount: 1, data: [1,3,5,1,2,2] } // replace integer at offset 10 with [1,3,5,1,2,2] + * ]} + * ``` + * All indices expressed in the returned diff represent indices in the old result array, so they all refer to the previous result state. + */ + provideSemanticTokens(document: TextDocument, options: SemanticTokensRequestOptions, token: CancellationToken): ProviderResult; } export namespace languages { /** - * Register a semantic coloring provider. + * Register a semantic tokens provider. * * Multiple providers can be registered for a language. In that case providers are sorted * by their [score](#languages.match) and the best-matching provider is used. Failure * of the selected provider will cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. - * @param provider A semantic coloring provider. + * @param provider A semantic tokens provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ - export function registerSemanticColoringProvider(selector: DocumentSelector, provider: SemanticColoringProvider, legend: SemanticColoringLegend): Disposable; + export function registerSemanticTokensProvider(selector: DocumentSelector, provider: SemanticTokensProvider, legend: SemanticTokensLegend): Disposable; } //#endregion @@ -158,7 +276,7 @@ declare module 'vscode' { //#endregion - //#region Rob: search provider + //#region TextSearchProvider: https://github.com/microsoft/vscode/issues/59921 /** * The parameters of a query for text search. @@ -302,32 +420,6 @@ declare module 'vscode' { limitHit?: boolean; } - /** - * The parameters of a query for file search. - */ - export interface FileSearchQuery { - /** - * The search pattern to match against file paths. - */ - pattern: string; - } - - /** - * Options that apply to file search. - */ - export interface FileSearchOptions extends SearchOptions { - /** - * The maximum number of results to be returned. - */ - maxResults?: number; - - /** - * A CancellationToken that represents the session for this search query. If the provider chooses to, this object can be used as the key for a cache, - * and searches with the same session object can search the same cache. When the token is cancelled, the session is complete and the cache can be cleared. - */ - session?: CancellationToken; - } - /** * A preview of the text result. */ @@ -387,6 +479,50 @@ declare module 'vscode' { export type TextSearchResult = TextSearchMatch | TextSearchContext; + /** + * A TextSearchProvider provides search results for text results inside files in the workspace. + */ + export interface TextSearchProvider { + /** + * Provide results that match the given text pattern. + * @param query The parameters for this query. + * @param options A set of options to consider while searching. + * @param progress A progress callback that must be invoked for all results. + * @param token A cancellation token. + */ + provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): ProviderResult; + } + + //#endregion + + //#region FileSearchProvider: https://github.com/microsoft/vscode/issues/73524 + + /** + * The parameters of a query for file search. + */ + export interface FileSearchQuery { + /** + * The search pattern to match against file paths. + */ + pattern: string; + } + + /** + * Options that apply to file search. + */ + export interface FileSearchOptions extends SearchOptions { + /** + * The maximum number of results to be returned. + */ + maxResults?: number; + + /** + * A CancellationToken that represents the session for this search query. If the provider chooses to, this object can be used as the key for a cache, + * and searches with the same session object can search the same cache. When the token is cancelled, the session is complete and the cache can be cleared. + */ + session?: CancellationToken; + } + /** * A FileSearchProvider provides search results for files in the given folder that match a query string. It can be invoked by quickopen or other extensions. * @@ -406,20 +542,34 @@ declare module 'vscode' { provideFileSearchResults(query: FileSearchQuery, options: FileSearchOptions, token: CancellationToken): ProviderResult; } - /** - * A TextSearchProvider provides search results for text results inside files in the workspace. - */ - export interface TextSearchProvider { + export namespace workspace { /** - * Provide results that match the given text pattern. - * @param query The parameters for this query. - * @param options A set of options to consider while searching. - * @param progress A progress callback that must be invoked for all results. - * @param token A cancellation token. + * Register a search provider. + * + * Only one provider can be registered per scheme. + * + * @param scheme The provider will be invoked for workspace folders that have this file scheme. + * @param provider The provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ - provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): ProviderResult; + export function registerFileSearchProvider(scheme: string, provider: FileSearchProvider): Disposable; + + /** + * Register a text search provider. + * + * Only one provider can be registered per scheme. + * + * @param scheme The provider will be invoked for workspace folders that have this file scheme. + * @param provider The provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerTextSearchProvider(scheme: string, provider: TextSearchProvider): Disposable; } + //#endregion + + //#region findTextInFiles: https://github.com/microsoft/vscode/issues/59924 + /** * Options that can be set on a findTextInFiles search. */ @@ -484,28 +634,6 @@ declare module 'vscode' { } export namespace workspace { - /** - * Register a search provider. - * - * Only one provider can be registered per scheme. - * - * @param scheme The provider will be invoked for workspace folders that have this file scheme. - * @param provider The provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. - */ - export function registerFileSearchProvider(scheme: string, provider: FileSearchProvider): Disposable; - - /** - * Register a text search provider. - * - * Only one provider can be registered per scheme. - * - * @param scheme The provider will be invoked for workspace folders that have this file scheme. - * @param provider The provider. - * @return A [disposable](#Disposable) that unregisters this provider when being disposed. - */ - export function registerTextSearchProvider(scheme: string, provider: TextSearchProvider): Disposable; - /** * Search text in files across all [workspace folders](#workspace.workspaceFolders) in the workspace. * @param query The query parameters for the search - the search string, whether it's case-sensitive, or a regex, or matches whole words. @@ -582,7 +710,7 @@ declare module 'vscode' { //#endregion - //#region André: debug + //#region André: debug API for inline debug adapters https://github.com/microsoft/vscode/issues/85544 /** * A DebugProtocolMessage is an opaque stand-in type for the [ProtocolMessage](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_ProtocolMessage) type defined in the Debug Adapter Protocol. @@ -592,13 +720,22 @@ declare module 'vscode' { } /** - * A debug adapter that implements the Debug Adapter Protocol can be registered with VS Code if it implements this interface. + * A debug adapter that implements the Debug Adapter Protocol can be registered with VS Code if it implements the DebugAdapter interface. */ - export interface DebugAdapter { + export interface DebugAdapter extends Disposable { + /** + * An event which fires when the debug adapter sends a Debug Adapter Protocol message to VS Code. + * Messages can be requests, responses, or events. + */ readonly onSendMessage: Event; - readonly onError: Event; + /** + * Handle a Debug Adapter Protocol message. + * Messages can be requests, responses, or events. + * Results or errors are returned via onSendMessage events. + * @param message A Debug Adapter Protocol message + */ handleMessage(message: DebugProtocolMessage): void; } @@ -625,7 +762,7 @@ declare module 'vscode' { //#endregion - //#region Rob, Matt: logging + //#region LogLevel: https://github.com/microsoft/vscode/issues/85992 /** * The severity level of a log message @@ -803,7 +940,7 @@ declare module 'vscode' { readonly creationOptions: Readonly; } - //#endregionn + //#endregion //#region Terminal dimensions property and change event https://github.com/microsoft/vscode/issues/55718 @@ -847,247 +984,6 @@ declare module 'vscode' { //#endregion - //#region mjbvz,joh: https://github.com/Microsoft/vscode/issues/43768 - - /** - * An event that is fired when files are going to be created. - * - * To make modifications to the workspace before the files are created, - * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a - * thenable that resolves to a [workspace edit](#WorkspaceEdit). - */ - export interface FileWillCreateEvent { - - /** - * The files that are going to be created. - */ - readonly files: ReadonlyArray; - - /** - * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). - * - * *Note:* This function can only be called during event dispatch and not - * in an asynchronous manner: - * - * ```ts - * workspace.onWillCreateFiles(event => { - * // async, will *throw* an error - * setTimeout(() => event.waitUntil(promise)); - * - * // sync, OK - * event.waitUntil(promise); - * }) - * ``` - * - * @param thenable A thenable that delays saving. - */ - waitUntil(thenable: Thenable): void; - - /** - * Allows to pause the event until the provided thenable resolves. - * - * *Note:* This function can only be called during event dispatch. - * - * @param thenable A thenable that delays saving. - */ - waitUntil(thenable: Thenable): void; - } - - /** - * An event that is fired after files are created. - */ - export interface FileCreateEvent { - - /** - * The files that got created. - */ - readonly files: ReadonlyArray; - } - - /** - * An event that is fired when files are going to be deleted. - * - * To make modifications to the workspace before the files are deleted, - * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a - * thenable that resolves to a [workspace edit](#WorkspaceEdit). - */ - export interface FileWillDeleteEvent { - - /** - * The files that are going to be deleted. - */ - readonly files: ReadonlyArray; - - /** - * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). - * - * *Note:* This function can only be called during event dispatch and not - * in an asynchronous manner: - * - * ```ts - * workspace.onWillCreateFiles(event => { - * // async, will *throw* an error - * setTimeout(() => event.waitUntil(promise)); - * - * // sync, OK - * event.waitUntil(promise); - * }) - * ``` - * - * @param thenable A thenable that delays saving. - */ - waitUntil(thenable: Thenable): void; - - /** - * Allows to pause the event until the provided thenable resolves. - * - * *Note:* This function can only be called during event dispatch. - * - * @param thenable A thenable that delays saving. - */ - waitUntil(thenable: Thenable): void; - } - - /** - * An event that is fired after files are deleted. - */ - export interface FileDeleteEvent { - - /** - * The files that got deleted. - */ - readonly files: ReadonlyArray; - } - - /** - * An event that is fired when files are going to be renamed. - * - * To make modifications to the workspace before the files are renamed, - * call the [`waitUntil](#FileWillCreateEvent.waitUntil)-function with a - * thenable that resolves to a [workspace edit](#WorkspaceEdit). - */ - export interface FileWillRenameEvent { - - /** - * The files that are going to be renamed. - */ - readonly files: ReadonlyArray<{ oldUri: Uri, newUri: Uri }>; - - /** - * Allows to pause the event and to apply a [workspace edit](#WorkspaceEdit). - * - * *Note:* This function can only be called during event dispatch and not - * in an asynchronous manner: - * - * ```ts - * workspace.onWillCreateFiles(event => { - * // async, will *throw* an error - * setTimeout(() => event.waitUntil(promise)); - * - * // sync, OK - * event.waitUntil(promise); - * }) - * ``` - * - * @param thenable A thenable that delays saving. - */ - waitUntil(thenable: Thenable): void; - - /** - * Allows to pause the event until the provided thenable resolves. - * - * *Note:* This function can only be called during event dispatch. - * - * @param thenable A thenable that delays saving. - */ - waitUntil(thenable: Thenable): void; - } - - /** - * An event that is fired after files are renamed. - */ - export interface FileRenameEvent { - - /** - * The files that got renamed. - */ - readonly files: ReadonlyArray<{ oldUri: Uri, newUri: Uri }>; - } - - export namespace workspace { - - /** - * An event that is emitted when files are being created. - * - * *Note 1:* This event is triggered by user gestures, like creating a file from the - * explorer, or from the [`workspace.applyEdit`](#workspace.applyEdit)-api. This event is *not* fired when - * files change on disk, e.g triggered by another application, or when using the - * [`workspace.fs`](#FileSystem)-api. - * - * *Note 2:* When this event is fired, edits to files thare are being created cannot be applied. - */ - export const onWillCreateFiles: Event; - - /** - * An event that is emitted when files have been created. - * - * *Note:* This event is triggered by user gestures, like creating a file from the - * explorer, or from the [`workspace.applyEdit`](#workspace.applyEdit)-api, but this event is *not* fired when - * files change on disk, e.g triggered by another application, or when using the - * [`workspace.fs`](#FileSystem)-api. - */ - export const onDidCreateFiles: Event; - - /** - * An event that is emitted when files are being deleted. - * - * *Note 1:* This event is triggered by user gestures, like deleting a file from the - * explorer, or from the [`workspace.applyEdit`](#workspace.applyEdit)-api, but this event is *not* fired when - * files change on disk, e.g triggered by another application, or when using the - * [`workspace.fs`](#FileSystem)-api. - * - * *Note 2:* When deleting a folder with children only one event is fired. - */ - export const onWillDeleteFiles: Event; - - /** - * An event that is emitted when files have been deleted. - * - * *Note 1:* This event is triggered by user gestures, like deleting a file from the - * explorer, or from the [`workspace.applyEdit`](#workspace.applyEdit)-api, but this event is *not* fired when - * files change on disk, e.g triggered by another application, or when using the - * [`workspace.fs`](#FileSystem)-api. - * - * *Note 2:* When deleting a folder with children only one event is fired. - */ - export const onDidDeleteFiles: Event; - - /** - * An event that is emitted when files are being renamed. - * - * *Note 1:* This event is triggered by user gestures, like renaming a file from the - * explorer, and from the [`workspace.applyEdit`](#workspace.applyEdit)-api, but this event is *not* fired when - * files change on disk, e.g triggered by another application, or when using the - * [`workspace.fs`](#FileSystem)-api. - * - * *Note 2:* When renaming a folder with children only one event is fired. - */ - export const onWillRenameFiles: Event; - - /** - * An event that is emitted when files have been renamed. - * - * *Note 1:* This event is triggered by user gestures, like renaming a file from the - * explorer, and from the [`workspace.applyEdit`](#workspace.applyEdit)-api, but this event is *not* fired when - * files change on disk, e.g triggered by another application, or when using the - * [`workspace.fs`](#FileSystem)-api. - * - * *Note 2:* When renaming a folder with children only one event is fired. - */ - export const onDidRenameFiles: Event; - } - //#endregion - //#region Alex - OnEnter enhancement export interface OnEnterRule { /** diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index 41005c20e4e..673177f0f01 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -141,7 +141,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb } else if (dto.type === 'function') { this.debugService.addFunctionBreakpoint(dto.functionName, dto.id); } else if (dto.type === 'data') { - this.debugService.addDataBreakpoint(dto.label, dto.dataId, dto.canPersist); + this.debugService.addDataBreakpoint(dto.label, dto.dataId, dto.canPersist, dto.accessTypes); } } return Promise.resolve(); @@ -336,7 +336,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb condition: dbp.condition, hitCondition: dbp.hitCondition, logMessage: dbp.logMessage, - label: dbp.label, + label: dbp.description, canPersist: dbp.canPersist }; } else { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 8b3849988cc..05447ed8a4b 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -21,7 +21,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import * as callh from 'vs/workbench/contrib/callHierarchy/browser/callHierarchy'; import { mixin } from 'vs/base/common/objects'; -import { decodeSemanticTokensDto, ISemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokens'; +import { decodeSemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokens'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesShape { @@ -325,10 +325,10 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha })); } - // --- semantic coloring + // --- semantic tokens - $registerSemanticColoringProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticColoringLegend): void { - this._registrations.set(handle, modes.SemanticColoringProviderRegistry.register(selector, new MainThreadSemanticColoringProvider(this._proxy, handle, legend))); + $registerSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend): void { + this._registrations.set(handle, modes.SemanticTokensProviderRegistry.register(selector, new MainThreadSemanticTokensProvider(this._proxy, handle, legend))); } // --- suggest @@ -602,47 +602,28 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha } -class MainThreadSemanticColoringCacheEntry implements modes.SemanticColoring { - - constructor( - private readonly _parent: MainThreadSemanticColoringProvider, - public readonly uri: URI, - public readonly id: number, - public readonly areas: modes.SemanticColoringArea[], - ) { - } - - dispose(): void { - this._parent.release(this); - } -} - -export class MainThreadSemanticColoringProvider implements modes.SemanticColoringProvider { - - private readonly _cache = new Map(); +export class MainThreadSemanticTokensProvider implements modes.SemanticTokensProvider { constructor( private readonly _proxy: ExtHostLanguageFeaturesShape, private readonly _handle: number, - private readonly _legend: modes.SemanticColoringLegend, + private readonly _legend: modes.SemanticTokensLegend, ) { } - release(entry: MainThreadSemanticColoringCacheEntry): void { - const currentCacheEntry = this._cache.get(entry.uri.toString()) || null; - if (currentCacheEntry && currentCacheEntry.id === entry.id) { - this._cache.delete(entry.uri.toString()); + public releaseSemanticTokens(resultId: string | undefined): void { + if (resultId) { + this._proxy.$releaseSemanticTokens(this._handle, parseInt(resultId, 10)); } - this._proxy.$releaseSemanticColoring(this._handle, entry.id); } - getLegend(): modes.SemanticColoringLegend { + public getLegend(): modes.SemanticTokensLegend { return this._legend; } - async provideSemanticColoring(model: ITextModel, token: CancellationToken): Promise { - const lastResult = this._cache.get(model.uri.toString()) || null; - const encodedDto = await this._proxy.$provideSemanticColoring(this._handle, model.uri, lastResult ? lastResult.id : 0, token); + async provideSemanticTokens(model: ITextModel, lastResultId: string | null, ranges: EditorRange[] | null, token: CancellationToken): Promise { + const nLastResultId = lastResultId ? parseInt(lastResultId, 10) : 0; + const encodedDto = await this._proxy.$provideSemanticTokens(this._handle, model.uri, ranges, nLastResultId, token); if (!encodedDto) { return null; } @@ -650,27 +631,15 @@ export class MainThreadSemanticColoringProvider implements modes.SemanticColorin return null; } const dto = decodeSemanticTokensDto(encodedDto); - const res = this._resolveDeltas(model, lastResult, dto); - this._cache.set(model.uri.toString(), res); - return res; - } - - private _resolveDeltas(model: ITextModel, lastResult: MainThreadSemanticColoringCacheEntry | null, dto: ISemanticTokensDto): MainThreadSemanticColoringCacheEntry { - let areas: modes.SemanticColoringArea[] = []; - for (let i = 0, len = dto.areas.length; i < len; i++) { - const areaDto = dto.areas[i]; - if (areaDto.type === 'full') { - areas[i] = { - line: areaDto.line, - data: areaDto.data - }; - } else { - areas[i] = { - line: areaDto.line, - data: lastResult!.areas[areaDto.oldIndex].data - }; - } + if (dto.type === 'full') { + return { + resultId: String(dto.id), + data: dto.data + }; } - return new MainThreadSemanticColoringCacheEntry(this, model.uri, dto.id, areas); + return { + resultId: String(dto.id), + edits: dto.deltas + }; } } diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index 1bd635659f9..529d9e60f99 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -271,18 +271,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma webviewInput.webview.options = options; webviewInput.webview.extension = extension; - const model = await this._customEditorService.models.loadOrCreate(webviewInput.getResource(), webviewInput.viewType); - - model.onUndo(edits => { this._proxy.$undoEdits(handle, edits.map(x => x.data)); }); - model.onApplyEdit(edits => { - const editsToApply = edits.filter(x => x.source !== webviewInput).map(x => x.data); - if (editsToApply.length) { - this._proxy.$applyEdits(handle, editsToApply); - } - }); - model.onWillSave(e => { e.waitUntil(this._proxy.$onSave(handle)); }); - model.onWillSaveAs(e => { e.waitUntil(this._proxy.$onSaveAs(handle, e.resource.toJSON(), e.targetResource.toJSON())); }); - + const model = await this.getModel(webviewInput); webviewInput.onDisposeWebview(() => { this._customEditorService.models.disposeModel(model); }); @@ -315,6 +304,31 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma this._editorProviders.delete(viewType); } + public async $registerCapabilities(handle: extHostProtocol.WebviewPanelHandle, capabilities: readonly extHostProtocol.WebviewEditorCapabilities[]): Promise { + const webviewInput = this.getWebviewInput(handle); + const model = await this.getModel(webviewInput); + + const capabilitiesSet = new Set(capabilities); + if (capabilitiesSet.has(extHostProtocol.WebviewEditorCapabilities.Editable)) { + model.onUndo(edits => { this._proxy.$undoEdits(handle, edits.map(x => x.data)); }); + + model.onApplyEdit(edits => { + const editsToApply = edits.filter(x => x.source !== webviewInput).map(x => x.data); + if (editsToApply.length) { + this._proxy.$applyEdits(handle, editsToApply); + } + }); + + model.onWillSave(e => { e.waitUntil(this._proxy.$onSave(handle)); }); + + model.onWillSaveAs(e => { e.waitUntil(this._proxy.$onSaveAs(handle, e.resource.toJSON(), e.targetResource.toJSON())); }); + } + } + + private getModel(webviewInput: WebviewInput) { + return this._customEditorService.models.loadOrCreate(webviewInput.getResource(), webviewInput.viewType); + } + public $onEdit(handle: extHostProtocol.WebviewPanelHandle, editData: any): void { const webview = this.getWebviewInput(handle); if (!(webview instanceof CustomFileEditorInput)) { diff --git a/src/vs/workbench/api/common/configurationExtensionPoint.ts b/src/vs/workbench/api/common/configurationExtensionPoint.ts index 65e5d6cf070..797cd6d8f9a 100644 --- a/src/vs/workbench/api/common/configurationExtensionPoint.ts +++ b/src/vs/workbench/api/common/configurationExtensionPoint.ts @@ -309,7 +309,9 @@ jsonRegistry.registerSchema('vscode://schemas/workspaceConfig', { $ref: 'vscode://schemas/extensions' }, 'remoteAuthority': { - type: 'string' + type: 'string', + doNotSuggest: true, + description: nls.localize('workspaceConfig.remoteAuthority', "The remote server where the workspace is located. Only used by unsaved remote workspaces."), } }, additionalProperties: false, diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 84a355928e9..ea5ad7991fa 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -351,9 +351,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerOnTypeFormattingEditProvider(selector: vscode.DocumentSelector, provider: vscode.OnTypeFormattingEditProvider, firstTriggerCharacter: string, ...moreTriggerCharacters: string[]): vscode.Disposable { return extHostLanguageFeatures.registerOnTypeFormattingEditProvider(extension, checkSelector(selector), provider, [firstTriggerCharacter].concat(moreTriggerCharacters)); }, - registerSemanticColoringProvider(selector: vscode.DocumentSelector, provider: vscode.SemanticColoringProvider, legend: vscode.SemanticColoringLegend): vscode.Disposable { + registerSemanticTokensProvider(selector: vscode.DocumentSelector, provider: vscode.SemanticTokensProvider, legend: vscode.SemanticTokensLegend): vscode.Disposable { checkProposedApiEnabled(extension); - return extHostLanguageFeatures.registerSemanticColoringProvider(extension, checkSelector(selector), provider, legend); + return extHostLanguageFeatures.registerSemanticTokensProvider(extension, checkSelector(selector), provider, legend); }, registerSignatureHelpProvider(selector: vscode.DocumentSelector, provider: vscode.SignatureHelpProvider, firstItem?: string | vscode.SignatureHelpProviderMetadata, ...remaining: string[]): vscode.Disposable { if (typeof firstItem === 'object') { @@ -699,27 +699,21 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostLabelService.$registerResourceLabelFormatter(formatter); }, onDidCreateFiles: (listener, thisArg, disposables) => { - checkProposedApiEnabled(extension); return extHostFileSystemEvent.onDidCreateFile(listener, thisArg, disposables); }, onDidDeleteFiles: (listener, thisArg, disposables) => { - checkProposedApiEnabled(extension); return extHostFileSystemEvent.onDidDeleteFile(listener, thisArg, disposables); }, onDidRenameFiles: (listener, thisArg, disposables) => { - checkProposedApiEnabled(extension); return extHostFileSystemEvent.onDidRenameFile(listener, thisArg, disposables); }, onWillCreateFiles: (listener: (e: vscode.FileWillCreateEvent) => any, thisArg?: any, disposables?: vscode.Disposable[]) => { - checkProposedApiEnabled(extension); return extHostFileSystemEvent.getOnWillCreateFileEvent(extension)(listener, thisArg, disposables); }, onWillDeleteFiles: (listener: (e: vscode.FileWillDeleteEvent) => any, thisArg?: any, disposables?: vscode.Disposable[]) => { - checkProposedApiEnabled(extension); return extHostFileSystemEvent.getOnWillDeleteFileEvent(extension)(listener, thisArg, disposables); }, onWillRenameFiles: (listener: (e: vscode.FileWillRenameEvent) => any, thisArg?: any, disposables?: vscode.Disposable[]) => { - checkProposedApiEnabled(extension); return extHostFileSystemEvent.getOnWillRenameFileEvent(extension)(listener, thisArg, disposables); } }; @@ -893,9 +887,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I RelativePattern: extHostTypes.RelativePattern, ResolvedAuthority: extHostTypes.ResolvedAuthority, RemoteAuthorityResolverError: extHostTypes.RemoteAuthorityResolverError, - SemanticColoring: extHostTypes.SemanticColoring, - SemanticColoringArea: extHostTypes.SemanticColoringArea, - SemanticColoringLegend: extHostTypes.SemanticColoringLegend, + SemanticTokensLegend: extHostTypes.SemanticTokensLegend, + SemanticTokensBuilder: extHostTypes.SemanticTokensBuilder, + SemanticTokens: extHostTypes.SemanticTokens, + SemanticTokensEdits: extHostTypes.SemanticTokensEdits, + SemanticTokensEdit: extHostTypes.SemanticTokensEdit, Selection: extHostTypes.Selection, SelectionRange: extHostTypes.SelectionRange, ShellExecution: extHostTypes.ShellExecution, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 290c93a353f..3dab81c9c55 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -354,7 +354,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerOnTypeFormattingSupport(handle: number, selector: IDocumentFilterDto[], autoFormatTriggerCharacters: string[], extensionId: ExtensionIdentifier): void; $registerNavigateTypeSupport(handle: number): void; $registerRenameSupport(handle: number, selector: IDocumentFilterDto[], supportsResolveInitialValues: boolean): void; - $registerSemanticColoringProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticColoringLegend): void; + $registerSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend): void; $registerSuggestSupport(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, extensionId: ExtensionIdentifier): void; $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void; $registerDocumentLinkProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean): void; @@ -556,6 +556,10 @@ export interface WebviewExtensionDescription { readonly location: UriComponents; } +export enum WebviewEditorCapabilities { + Editable, +} + export interface MainThreadWebviewsShape extends IDisposable { $createWebviewPanel(extension: WebviewExtensionDescription, handle: WebviewPanelHandle, viewType: string, title: string, showOptions: WebviewPanelShowOptions, options: modes.IWebviewPanelOptions & modes.IWebviewOptions): void; $disposeWebview(handle: WebviewPanelHandle): void; @@ -573,6 +577,7 @@ export interface MainThreadWebviewsShape extends IDisposable { $registerEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions): void; $unregisterEditorProvider(viewType: string): void; + $registerCapabilities(handle: WebviewPanelHandle, capabilities: readonly WebviewEditorCapabilities[]): void; $onEdit(handle: WebviewPanelHandle, editJson: any): void; } @@ -1167,8 +1172,8 @@ export interface ExtHostLanguageFeaturesShape { $releaseWorkspaceSymbols(handle: number, id: number): void; $provideRenameEdits(handle: number, resource: UriComponents, position: IPosition, newName: string, token: CancellationToken): Promise; $resolveRenameLocation(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; - $provideSemanticColoring(handle: number, resource: UriComponents, previousSemanticColoringResultId: number, token: CancellationToken): Promise; - $releaseSemanticColoring(handle: number, semanticColoringResultId: number): void; + $provideSemanticTokens(handle: number, resource: UriComponents, ranges: IRange[] | null, previousResultId: number, token: CancellationToken): Promise; + $releaseSemanticTokens(handle: number, semanticColoringResultId: number): void; $provideCompletionItems(handle: number, resource: UriComponents, position: IPosition, context: modes.CompletionContext, token: CancellationToken): Promise; $resolveCompletionItem(handle: number, resource: UriComponents, position: IPosition, id: ChainedCacheId, token: CancellationToken): Promise; $releaseCompletionItems(handle: number, id: number): void; @@ -1282,6 +1287,7 @@ export interface IDataBreakpointDto extends IBreakpointDto { dataId: string; canPersist: boolean; label: string; + accessTypes?: DebugProtocol.DataBreakpointAccessType[]; } export interface ISourceBreakpointDto extends IBreakpointDto { diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index eda288c27d4..499d4c257f7 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -1054,12 +1054,6 @@ class DirectDebugAdapter extends AbstractDebugAdapter { this.acceptMessage(message); }); } - - if (this.implementation.onError) { - implementation.onError((error: Error) => { - this._onError.fire(error); - }); - } } startSession(): Promise { @@ -1073,6 +1067,9 @@ class DirectDebugAdapter extends AbstractDebugAdapter { } stopSession(): Promise { + if (this.implementation.dispose) { + this.implementation.dispose(); + } return Promise.resolve(undefined); } } diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 88464ef0ead..76a9b14643b 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -7,7 +7,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { mixin } from 'vs/base/common/objects'; import * as vscode from 'vscode'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; -import { Range, Disposable, CompletionList, SnippetString, CodeActionKind, SymbolInformation, DocumentSymbol, SemanticColoringArea } from 'vs/workbench/api/common/extHostTypes'; +import { Range, Disposable, CompletionList, SnippetString, CodeActionKind, SymbolInformation, DocumentSymbol, SemanticTokensEdits } from 'vs/workbench/api/common/extHostTypes'; import { ISingleEditOperation } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; @@ -27,7 +27,7 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio import { IURITransformer } from 'vs/base/common/uriIpc'; import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { VSBuffer } from 'vs/base/common/buffer'; -import { encodeSemanticTokensDto, ISemanticTokensDto, ISemanticTokensAreaDto } from 'vs/workbench/api/common/shared/semanticTokens'; +import { encodeSemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokens'; import { IdGenerator } from 'vs/base/common/idGenerator'; // --- adapter @@ -616,62 +616,40 @@ class RenameAdapter { } } -export const enum SemanticColoringConstants { - /** - * Let's aim at having 8KB buffers if possible... - * So that would be 8192 / (5 * 4) = 409.6 tokens per area - */ - DesiredTokensPerArea = 400, - - /** - * Try to keep the total number of areas under 1024 if possible, - * simply compensate by having more tokens per area... - */ - DesiredMaxAreas = 1024, - - /** - * Threshold for merging multiple delta areas and sending a full area. - */ - MinTokensPerArea = 50 +class SemanticTokensPreviousResult { + constructor( + public readonly resultId: string | undefined, + public readonly tokens?: Uint32Array, + ) { } } -interface ISemanticColoringAreaPair { - data: Uint32Array; - dto: ISemanticTokensAreaDto; -} +export class SemanticTokensAdapter { -export class SemanticColoringAdapter { - - private readonly _previousResults: Map; - private readonly _splitSingleAreaTokenCountThreshold: number; + private readonly _previousResults: Map; private _nextResultId = 1; constructor( private readonly _documents: ExtHostDocuments, - private readonly _provider: vscode.SemanticColoringProvider, - private readonly _desiredTokensPerArea = SemanticColoringConstants.DesiredTokensPerArea, - private readonly _desiredMaxAreas = SemanticColoringConstants.DesiredMaxAreas, - private readonly _minTokensPerArea = SemanticColoringConstants.MinTokensPerArea + private readonly _provider: vscode.SemanticTokensProvider, ) { - this._previousResults = new Map(); - this._splitSingleAreaTokenCountThreshold = Math.round(1.5 * this._desiredTokensPerArea); + this._previousResults = new Map(); } - provideSemanticColoring(resource: URI, previousSemanticColoringResultId: number, token: CancellationToken): Promise { + provideSemanticTokens(resource: URI, ranges: IRange[] | null, previousResultId: number, token: CancellationToken): Promise { const doc = this._documents.getDocument(resource); - - return asPromise(() => this._provider.provideSemanticColoring(doc, token)).then(value => { + const previousResult = (previousResultId !== 0 ? this._previousResults.get(previousResultId) : null); + const opts: vscode.SemanticTokensRequestOptions = { + ranges: (Array.isArray(ranges) && ranges.length > 0 ? ranges.map(typeConvert.Range.to) : undefined), + previousResultId: (previousResult ? previousResult.resultId : undefined) + }; + return asPromise(() => this._provider.provideSemanticTokens(doc, opts, token)).then(value => { if (!value) { return null; } - - const oldAreas = (previousSemanticColoringResultId !== 0 ? this._previousResults.get(previousSemanticColoringResultId) : null); - if (oldAreas) { - this._previousResults.delete(previousSemanticColoringResultId); - return this._deltaEncodeAreas(oldAreas, value.areas); + if (previousResult) { + this._previousResults.delete(previousResultId); } - - return this._fullEncodeAreas(value.areas); + return this._send(SemanticTokensAdapter._convertToEdits(previousResult, value), value); }); } @@ -679,298 +657,77 @@ export class SemanticColoringAdapter { this._previousResults.delete(semanticColoringResultId); } - private _deltaEncodeAreas(oldAreas: Uint32Array[], newAreas: SemanticColoringArea[]): VSBuffer { - if (newAreas.length > 1) { - // this is a fancy provider which is smart enough to break things into good areas - // we therefore try to match old areas only by object identity - const oldAreasIndexMap = new Map(); - for (let i = 0, len = oldAreas.length; i < len; i++) { - oldAreasIndexMap.set(oldAreas[i], i); - } - - let result: ISemanticColoringAreaPair[] = []; - for (let i = 0, len = newAreas.length; i < len; i++) { - const newArea = newAreas[i]; - if (oldAreasIndexMap.has(newArea.data)) { - // great! we can reuse this area - const oldIndex = oldAreasIndexMap.get(newArea.data)!; - result.push({ - data: newArea.data, - dto: { - type: 'delta', - line: newArea.line, - oldIndex: oldIndex - } - }); - } else { - result.push({ - data: newArea.data, - dto: { - type: 'full', - line: newArea.line, - data: newArea.data - } - }); - } - } - - return this._saveResultAndEncode(result); - } - - return this._deltaEncodeArea(oldAreas, newAreas[0]); + private static _isSemanticTokens(v: vscode.SemanticTokens | vscode.SemanticTokensEdits): v is vscode.SemanticTokens { + return v && !!((v as vscode.SemanticTokens).data); } - private static _oldAreaAppearsInNewArea(oldAreaData: Uint32Array, oldAreaTokenCount: number, newAreaData: Uint32Array, newAreaOffset: number): boolean { - const newTokenStartDeltaLine = newAreaData[5 * newAreaOffset]; - - // check that each and every value from `oldArea` is equal to `area` - for (let j = 0; j < oldAreaTokenCount; j++) { - const oldOffset = 5 * j; - const newOffset = 5 * (j + newAreaOffset); - - if ( - (oldAreaData[oldOffset] !== newAreaData[newOffset] - newTokenStartDeltaLine) - || (oldAreaData[oldOffset + 1] !== newAreaData[newOffset + 1]) - || (oldAreaData[oldOffset + 2] !== newAreaData[newOffset + 2]) - || (oldAreaData[oldOffset + 3] !== newAreaData[newOffset + 3]) - || (oldAreaData[oldOffset + 4] !== newAreaData[newOffset + 4]) - ) { - return false; - } - } - - return true; + private static _isSemanticTokensEdits(v: vscode.SemanticTokens | vscode.SemanticTokensEdits): v is vscode.SemanticTokensEdits { + return v && Array.isArray((v as vscode.SemanticTokensEdits).edits); } - private _deltaEncodeArea(oldAreas: Uint32Array[], newArea: SemanticColoringArea): VSBuffer { - const newAreaData = newArea.data; - const prependAreas: ISemanticColoringAreaPair[] = []; - const appendAreas: ISemanticColoringAreaPair[] = []; + private static _convertToEdits(previousResult: SemanticTokensPreviousResult | null | undefined, newResult: vscode.SemanticTokens | vscode.SemanticTokensEdits): vscode.SemanticTokens | vscode.SemanticTokensEdits { + if (!SemanticTokensAdapter._isSemanticTokens(newResult)) { + return newResult; + } + if (!previousResult || !previousResult.tokens) { + return newResult; + } + const oldData = previousResult.tokens; + const oldLength = oldData.length; + const newData = newResult.data; + const newLength = newData.length; - // Try to find appearences of `oldAreas` inside `area`. - let newTokenStartIndex = 0; - let newTokenEndIndex = (newAreaData.length / 5) | 0; - let oldAreaUsedIndex = -1; - for (let i = 0, len = oldAreas.length; i < len; i++) { - const oldAreaData = oldAreas[i]; - const oldAreaTokenCount = (oldAreaData.length / 5) | 0; - if (oldAreaTokenCount === 0) { - // skip old empty areas - continue; - } - if (newTokenEndIndex - newTokenStartIndex < oldAreaTokenCount) { - // there are too many old tokens, this cannot work - break; - } + let commonPrefixLength = 0; + const maxCommonPrefixLength = Math.min(oldLength, newLength); + while (commonPrefixLength < maxCommonPrefixLength && oldData[commonPrefixLength] === newData[commonPrefixLength]) { + commonPrefixLength++; + } - const newAreaOffset = newTokenStartIndex; - const newTokenStartDeltaLine = newAreaData[5 * newAreaOffset]; - const isEqual = SemanticColoringAdapter._oldAreaAppearsInNewArea(oldAreaData, oldAreaTokenCount, newAreaData, newAreaOffset); - if (!isEqual) { - break; - } - newTokenStartIndex += oldAreaTokenCount; + if (commonPrefixLength === oldLength && commonPrefixLength === newLength) { + // complete overlap! + return new SemanticTokensEdits([], newResult.resultId); + } - oldAreaUsedIndex = i; - prependAreas.push({ - data: oldAreaData, - dto: { - type: 'delta', - line: newArea.line + newTokenStartDeltaLine, - oldIndex: i - } + let commonSuffixLength = 0; + const maxCommonSuffixLength = maxCommonPrefixLength - commonPrefixLength; + while (commonSuffixLength < maxCommonSuffixLength && oldData[oldLength - commonSuffixLength - 1] === newData[newLength - commonSuffixLength - 1]) { + commonSuffixLength++; + } + + return new SemanticTokensEdits([{ + start: commonPrefixLength, + deleteCount: (oldLength - commonPrefixLength - commonSuffixLength), + data: newData.subarray(commonPrefixLength, newLength - commonSuffixLength) + }], newResult.resultId); + } + + private _send(value: vscode.SemanticTokens | vscode.SemanticTokensEdits, original: vscode.SemanticTokens | vscode.SemanticTokensEdits): VSBuffer | null { + if (SemanticTokensAdapter._isSemanticTokens(value)) { + const myId = this._nextResultId++; + this._previousResults.set(myId, new SemanticTokensPreviousResult(value.resultId, value.data)); + return encodeSemanticTokensDto({ + id: myId, + type: 'full', + data: value.data }); } - for (let i = oldAreas.length - 1; i > oldAreaUsedIndex; i--) { - const oldAreaData = oldAreas[i]; - const oldAreaTokenCount = (oldAreaData.length / 5) | 0; - if (oldAreaTokenCount === 0) { - // skip old empty areas - continue; + if (SemanticTokensAdapter._isSemanticTokensEdits(value)) { + const myId = this._nextResultId++; + if (SemanticTokensAdapter._isSemanticTokens(original)) { + // store the original + this._previousResults.set(myId, new SemanticTokensPreviousResult(original.resultId, original.data)); + } else { + this._previousResults.set(myId, new SemanticTokensPreviousResult(value.resultId)); } - if (newTokenEndIndex - newTokenStartIndex < oldAreaTokenCount) { - // there are too many old tokens, this cannot work - break; - } - - const newAreaOffset = (newTokenEndIndex - oldAreaTokenCount); - const newTokenStartDeltaLine = newAreaData[5 * newAreaOffset]; - const isEqual = SemanticColoringAdapter._oldAreaAppearsInNewArea(oldAreaData, oldAreaTokenCount, newAreaData, newAreaOffset); - if (!isEqual) { - break; - } - newTokenEndIndex -= oldAreaTokenCount; - - appendAreas.unshift({ - data: oldAreaData, - dto: { - type: 'delta', - line: newArea.line + newTokenStartDeltaLine, - oldIndex: i - } + return encodeSemanticTokensDto({ + id: myId, + type: 'delta', + deltas: (value.edits || []).map(edit => ({ start: edit.start, deleteCount: edit.deleteCount, data: edit.data })) }); } - if (prependAreas.length === 0 && appendAreas.length === 0) { - // There is no reuse possibility! - return this._fullEncodeAreas([newArea]); - } - - if (newTokenStartIndex === newTokenEndIndex) { - // 100% reuse! - return this._saveResultAndEncode(prependAreas.concat(appendAreas)); - } - - // It is clear at this point that there will be at least one full area. - // Expand the mid area if the areas next to it are too small - while (prependAreas.length > 0) { - const tokenCount = (prependAreas[prependAreas.length - 1].data.length / 5); - if (tokenCount < this._minTokensPerArea) { - newTokenStartIndex -= tokenCount; - prependAreas.pop(); - } else { - break; - } - } - while (appendAreas.length > 0) { - const tokenCount = (appendAreas[0].data.length / 5); - if (tokenCount < this._minTokensPerArea) { - newTokenEndIndex += tokenCount; - appendAreas.shift(); - } else { - break; - } - } - - // Extract the mid area - const newTokenStartDeltaLine = newAreaData[5 * newTokenStartIndex]; - const newMidAreaData = new Uint32Array(5 * (newTokenEndIndex - newTokenStartIndex)); - for (let tokenIndex = newTokenStartIndex; tokenIndex < newTokenEndIndex; tokenIndex++) { - const srcOffset = 5 * tokenIndex; - const deltaLine = newAreaData[srcOffset]; - const startCharacter = newAreaData[srcOffset + 1]; - const endCharacter = newAreaData[srcOffset + 2]; - const tokenType = newAreaData[srcOffset + 3]; - const tokenModifiers = newAreaData[srcOffset + 4]; - - const destOffset = 5 * (tokenIndex - newTokenStartIndex); - newMidAreaData[destOffset] = deltaLine - newTokenStartDeltaLine; - newMidAreaData[destOffset + 1] = startCharacter; - newMidAreaData[destOffset + 2] = endCharacter; - newMidAreaData[destOffset + 3] = tokenType; - newMidAreaData[destOffset + 4] = tokenModifiers; - } - - const newMidArea = new SemanticColoringArea(newArea.line + newTokenStartDeltaLine, newMidAreaData); - const newMidAreas = this._splitAreaIntoMultipleAreasIfNecessary(newMidArea); - const newMidAreasPairs: ISemanticColoringAreaPair[] = newMidAreas.map(a => { - return { - data: a.data, - dto: { - type: 'full', - line: a.line, - data: a.data, - } - }; - }); - - return this._saveResultAndEncode(prependAreas.concat(newMidAreasPairs).concat(appendAreas)); - } - - private _fullEncodeAreas(areas: SemanticColoringArea[]): VSBuffer { - if (areas.length === 1) { - areas = this._splitAreaIntoMultipleAreasIfNecessary(areas[0]); - } - - return this._saveResultAndEncode(areas.map(a => { - return { - data: a.data, - dto: { - type: 'full', - line: a.line, - data: a.data - } - }; - })); - } - - private _saveResultAndEncode(areas: ISemanticColoringAreaPair[]): VSBuffer { - const myId = this._nextResultId++; - this._previousResults.set(myId, areas.map(a => a.data)); - console.log(`_saveResultAndEncode: ${myId} --> ${areas.map(a => `${a.dto.line}-${a.dto.type}(${a.data.length / 5})`).join(', ')}`); - const dto: ISemanticTokensDto = { - id: myId, - areas: areas.map(a => a.dto) - }; - return encodeSemanticTokensDto(dto); - } - - private _splitAreaIntoMultipleAreasIfNecessary(area: vscode.SemanticColoringArea): SemanticColoringArea[] { - const srcAreaLine = area.line; - const srcAreaData = area.data; - const tokenCount = (srcAreaData.length / 5) | 0; - if (tokenCount <= this._splitSingleAreaTokenCountThreshold) { - return [area]; - } - - const tokensPerArea = Math.max(Math.ceil(tokenCount / this._desiredMaxAreas), this._desiredTokensPerArea); - - let result: SemanticColoringArea[] = []; - let tokenIndex = 0; - while (tokenIndex < tokenCount) { - const tokenStartIndex = tokenIndex; - let tokenEndIndex = Math.min(tokenStartIndex + tokensPerArea, tokenCount); - - // Keep tokens on the same line in the same area... - if (tokenEndIndex < tokenCount) { - let smallAvoidDeltaLine = srcAreaData[5 * tokenEndIndex]; - let smallTokenEndIndex = tokenEndIndex; - while (smallTokenEndIndex - 1 > tokenStartIndex && srcAreaData[5 * (smallTokenEndIndex - 1)] === smallAvoidDeltaLine) { - smallTokenEndIndex--; - } - - if (smallTokenEndIndex - 1 === tokenStartIndex) { - // there are so many tokens on this line that our area would be empty, we must now go right - let bigAvoidDeltaLine = srcAreaData[5 * (tokenEndIndex - 1)]; - let bigTokenEndIndex = tokenEndIndex; - while (bigTokenEndIndex + 1 < tokenCount && srcAreaData[5 * (bigTokenEndIndex + 1)] === bigAvoidDeltaLine) { - bigTokenEndIndex++; - } - tokenEndIndex = bigTokenEndIndex; - } else { - tokenEndIndex = smallTokenEndIndex; - } - } - - let destAreaLine = 0; - const destAreaData = new Uint32Array((tokenEndIndex - tokenStartIndex) * 5); - while (tokenIndex < tokenEndIndex) { - const srcOffset = 5 * tokenIndex; - const line = srcAreaLine + srcAreaData[srcOffset]; - const startCharacter = srcAreaData[srcOffset + 1]; - const endCharacter = srcAreaData[srcOffset + 2]; - const tokenType = srcAreaData[srcOffset + 3]; - const tokenModifiers = srcAreaData[srcOffset + 4]; - - if (tokenIndex === tokenStartIndex) { - destAreaLine = line; - } - - const destOffset = 5 * (tokenIndex - tokenStartIndex); - destAreaData[destOffset] = line - destAreaLine; - destAreaData[destOffset + 1] = startCharacter; - destAreaData[destOffset + 2] = endCharacter; - destAreaData[destOffset + 3] = tokenType; - destAreaData[destOffset + 4] = tokenModifiers; - - tokenIndex++; - } - - result.push(new SemanticColoringArea(destAreaLine, destAreaData)); - } - - return result; + return null; } } @@ -1452,11 +1209,10 @@ class CallHierarchyAdapter { } releaseSession(sessionId: string): void { - this._cache.delete(sessionId.charAt(0)); + this._cache.delete(sessionId); } - private _cacheAndConvertItem(itemOrSessionId: string, item: vscode.CallHierarchyItem): extHostProtocol.ICallHierarchyItemDto { - const sessionId = itemOrSessionId.charAt(0); + private _cacheAndConvertItem(sessionId: string, item: vscode.CallHierarchyItem): extHostProtocol.ICallHierarchyItemDto { const map = this._cache.get(sessionId)!; const dto: extHostProtocol.ICallHierarchyItemDto = { _sessionId: sessionId, @@ -1474,14 +1230,14 @@ class CallHierarchyAdapter { private _itemFromCache(sessionId: string, itemId: string): vscode.CallHierarchyItem | undefined { const map = this._cache.get(sessionId); - return map && map.get(itemId); + return map?.get(itemId); } } type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter | DocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentFormattingAdapter | RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter - | SemanticColoringAdapter | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter + | SemanticTokensAdapter | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter | TypeDefinitionAdapter | ColorProviderAdapter | FoldingProviderAdapter | DeclarationAdapter | SelectionRangeAdapter | CallHierarchyAdapter; @@ -1809,18 +1565,18 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF //#region semantic coloring - registerSemanticColoringProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.SemanticColoringProvider, legend: vscode.SemanticColoringLegend): vscode.Disposable { - const handle = this._addNewAdapter(new SemanticColoringAdapter(this._documents, provider), extension); - this._proxy.$registerSemanticColoringProvider(handle, this._transformDocumentSelector(selector), legend); + registerSemanticTokensProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.SemanticTokensProvider, legend: vscode.SemanticTokensLegend): vscode.Disposable { + const handle = this._addNewAdapter(new SemanticTokensAdapter(this._documents, provider), extension); + this._proxy.$registerSemanticTokensProvider(handle, this._transformDocumentSelector(selector), legend); return this._createDisposable(handle); } - $provideSemanticColoring(handle: number, resource: UriComponents, previousSemanticColoringResultId: number, token: CancellationToken): Promise { - return this._withAdapter(handle, SemanticColoringAdapter, adapter => adapter.provideSemanticColoring(URI.revive(resource), previousSemanticColoringResultId, token), null); + $provideSemanticTokens(handle: number, resource: UriComponents, ranges: IRange[] | null, previousResultId: number, token: CancellationToken): Promise { + return this._withAdapter(handle, SemanticTokensAdapter, adapter => adapter.provideSemanticTokens(URI.revive(resource), ranges, previousResultId, token), null); } - $releaseSemanticColoring(handle: number, semanticColoringResultId: number): void { - this._withAdapter(handle, SemanticColoringAdapter, adapter => adapter.releaseSemanticColoring(semanticColoringResultId), undefined); + $releaseSemanticTokens(handle: number, semanticColoringResultId: number): void { + this._withAdapter(handle, SemanticTokensAdapter, adapter => adapter.releaseSemanticColoring(semanticColoringResultId), undefined); } //#endregion diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 4573846bd4d..8f89d246b5a 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2368,7 +2368,7 @@ export enum CommentMode { //#region Semantic Coloring -export class SemanticColoringLegend { +export class SemanticTokensLegend { public readonly tokenTypes: string[]; public readonly tokenModifiers: string[]; @@ -2378,21 +2378,74 @@ export class SemanticColoringLegend { } } -export class SemanticColoringArea { - public readonly line: number; - public readonly data: Uint32Array; +export class SemanticTokensBuilder { - constructor(line: number, data: Uint32Array) { - this.line = line; + private _prevLine: number; + private _prevChar: number; + private _data: number[]; + private _dataLen: number; + + constructor() { + this._prevLine = 0; + this._prevChar = 0; + this._data = []; + this._dataLen = 0; + } + + public push(line: number, char: number, length: number, tokenType: number, tokenModifiers: number): void { + let pushLine = line; + let pushChar = char; + if (this._dataLen > 0) { + pushLine -= this._prevLine; + if (pushLine === 0) { + pushChar -= this._prevChar; + } + } + + this._data[this._dataLen++] = pushLine; + this._data[this._dataLen++] = pushChar; + this._data[this._dataLen++] = length; + this._data[this._dataLen++] = tokenType; + this._data[this._dataLen++] = tokenModifiers; + + this._prevLine = line; + this._prevChar = char; + } + + public build(): Uint32Array { + return new Uint32Array(this._data); + } +} + +export class SemanticTokens { + readonly resultId?: string; + readonly data: Uint32Array; + + constructor(data: Uint32Array, resultId?: string) { + this.resultId = resultId; this.data = data; } } -export class SemanticColoring { - public readonly areas: SemanticColoringArea[]; +export class SemanticTokensEdit { + readonly start: number; + readonly deleteCount: number; + readonly data?: Uint32Array; - constructor(areas: SemanticColoringArea[]) { - this.areas = areas; + constructor(start: number, deleteCount: number, data?: Uint32Array) { + this.start = start; + this.deleteCount = deleteCount; + this.data = data; + } +} + +export class SemanticTokensEdits { + readonly resultId?: string; + readonly edits: SemanticTokensEdit[]; + + constructor(edits: SemanticTokensEdit[], resultId?: string) { + this.resultId = resultId; + this.edits = edits; } } diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index e59a0eebbe7..ef8b03fcd93 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -16,7 +16,7 @@ import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; import * as vscode from 'vscode'; -import { ExtHostWebviewsShape, IMainContext, MainContext, MainThreadWebviewsShape, WebviewPanelHandle, WebviewPanelViewStateData } from './extHost.protocol'; +import { ExtHostWebviewsShape, IMainContext, MainContext, MainThreadWebviewsShape, WebviewPanelHandle, WebviewPanelViewStateData, WebviewEditorCapabilities } from './extHost.protocol'; import { Disposable as VSCodeDisposable } from './extHostTypes'; type IconPath = URI | { light: URI, dark: URI }; @@ -257,12 +257,11 @@ export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPa } async _onSave(): Promise { - await assertIsDefined(this._capabilities).editingCapability?.save(); + await assertIsDefined(this._capabilities?.editingCapability)?.save(); } - async _onSaveAs(resource: vscode.Uri, targetResource: vscode.Uri): Promise { - await assertIsDefined(this._capabilities).editingCapability?.saveAs(resource, targetResource); + await assertIsDefined(this._capabilities?.editingCapability)?.saveAs(resource, targetResource); } private assertNotDisposed() { @@ -450,6 +449,7 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { this._webviewPanels.set(handle, revivedPanel); const capabilities = await provider.resolveWebviewEditor({ resource: URI.revive(input.resource) }, revivedPanel); revivedPanel._setCapabilities(capabilities); + this.registerCapabilites(handle, capabilities); // TODO: the first set of edits should likely be passed when resolving if (input.edits.length) { @@ -480,6 +480,14 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { private getWebviewPanel(handle: WebviewPanelHandle): ExtHostWebviewEditor | undefined { return this._webviewPanels.get(handle); } + + private registerCapabilites(handle: WebviewPanelHandle, capabilities: vscode.WebviewEditorCapabilities) { + const declaredCapabilites: WebviewEditorCapabilities[] = []; + if (capabilities.editingCapability) { + declaredCapabilites.push(WebviewEditorCapabilities.Editable); + } + this._proxy.$registerCapabilities(handle, declaredCapabilites); + } } function convertWebviewOptions( diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index 4b5c0c1301f..99654eded85 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -339,6 +339,11 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac if (folders.length === 0) { return undefined; } + + if (folders.length > 1) { + return undefined; + } + // #54483 @Joh Why are we still using fsPath? return folders[0].uri.fsPath; } diff --git a/src/vs/workbench/api/common/shared/semanticTokens.ts b/src/vs/workbench/api/common/shared/semanticTokens.ts index 0d7073ca65d..adce8e3bf58 100644 --- a/src/vs/workbench/api/common/shared/semanticTokens.ts +++ b/src/vs/workbench/api/common/shared/semanticTokens.ts @@ -5,47 +5,71 @@ import { VSBuffer } from 'vs/base/common/buffer'; -export interface ISemanticTokensFullAreaDto { +export interface IFullSemanticTokensDto { + id: number; type: 'full'; - line: number; data: Uint32Array; } -export interface ISemanticTokensDeltaAreaDto { - type: 'delta'; - line: number; - oldIndex: number; -} - -export type ISemanticTokensAreaDto = ISemanticTokensFullAreaDto | ISemanticTokensDeltaAreaDto; - -export interface ISemanticTokensDto { +export interface IDeltaSemanticTokensDto { id: number; - areas: ISemanticTokensAreaDto[]; + type: 'delta'; + deltas: { start: number; deleteCount: number; data?: Uint32Array; }[]; } -const enum EncodedSemanticTokensAreaType { +export type ISemanticTokensDto = IFullSemanticTokensDto | IDeltaSemanticTokensDto; + +const enum EncodedSemanticTokensType { Full = 1, Delta = 2 } export function encodeSemanticTokensDto(semanticTokens: ISemanticTokensDto): VSBuffer { - const buff = VSBuffer.alloc(encodedSize(semanticTokens)); + const buff = VSBuffer.alloc(encodedSize2(semanticTokens)); let offset = 0; buff.writeUInt32BE(semanticTokens.id, offset); offset += 4; - buff.writeUInt32BE(semanticTokens.areas.length, offset); offset += 4; - for (let i = 0; i < semanticTokens.areas.length; i++) { - offset = encodeArea(semanticTokens.areas[i], buff, offset); + if (semanticTokens.type === 'full') { + buff.writeUInt8(EncodedSemanticTokensType.Full, offset); offset += 1; + buff.writeUInt32BE(semanticTokens.data.length, offset); offset += 4; + for (const uint of semanticTokens.data) { + buff.writeUInt32BE(uint, offset); offset += 4; + } + } else { + buff.writeUInt8(EncodedSemanticTokensType.Delta, offset); offset += 1; + buff.writeUInt32BE(semanticTokens.deltas.length, offset); offset += 4; + for (const delta of semanticTokens.deltas) { + buff.writeUInt32BE(delta.start, offset); offset += 4; + buff.writeUInt32BE(delta.deleteCount, offset); offset += 4; + if (delta.data) { + buff.writeUInt32BE(delta.data.length, offset); offset += 4; + for (const uint of delta.data) { + buff.writeUInt32BE(uint, offset); offset += 4; + } + } else { + buff.writeUInt32BE(0, offset); offset += 4; + } + } } return buff; } -function encodedSize(semanticTokens: ISemanticTokensDto): number { +function encodedSize2(semanticTokens: ISemanticTokensDto): number { let result = 0; - result += 4; // etag - result += 4; // area count - for (let i = 0; i < semanticTokens.areas.length; i++) { - result += encodedAreaSize(semanticTokens.areas[i]); + result += 4; // id + result += 1; // type + if (semanticTokens.type === 'full') { + result += 4; // data length + result += semanticTokens.data.byteLength; + } else { + result += 4; // delta count + for (const delta of semanticTokens.deltas) { + result += 4; // start + result += 4; // deleteCount + result += 4; // data length + if (delta.data) { + result += delta.data.byteLength; + } + } } return result; } @@ -53,84 +77,37 @@ function encodedSize(semanticTokens: ISemanticTokensDto): number { export function decodeSemanticTokensDto(buff: VSBuffer): ISemanticTokensDto { let offset = 0; const id = buff.readUInt32BE(offset); offset += 4; - const areasCount = buff.readUInt32BE(offset); offset += 4; - let areas: ISemanticTokensAreaDto[] = []; - for (let i = 0; i < areasCount; i++) { - offset = decodeArea(buff, offset, areas); + const type: EncodedSemanticTokensType = buff.readUInt8(offset); offset += 1; + if (type === EncodedSemanticTokensType.Full) { + const length = buff.readUInt32BE(offset); offset += 4; + const data = new Uint32Array(length); + for (let j = 0; j < length; j++) { + data[j] = buff.readUInt32BE(offset); offset += 4; + } + return { + id: id, + type: 'full', + data: data + }; + } + const deltaCount = buff.readUInt32BE(offset); offset += 4; + let deltas: { start: number; deleteCount: number; data?: Uint32Array; }[] = []; + for (let i = 0; i < deltaCount; i++) { + const start = buff.readUInt32BE(offset); offset += 4; + const deleteCount = buff.readUInt32BE(offset); offset += 4; + const length = buff.readUInt32BE(offset); offset += 4; + let data: Uint32Array | undefined; + if (length > 0) { + data = new Uint32Array(length); + for (let j = 0; j < length; j++) { + data[j] = buff.readUInt32BE(offset); offset += 4; + } + } + deltas[i] = { start, deleteCount, data }; } return { id: id, - areas: areas + type: 'delta', + deltas: deltas }; } - -function encodeArea(area: ISemanticTokensAreaDto, buff: VSBuffer, offset: number): number { - buff.writeUInt8(area.type === 'full' ? EncodedSemanticTokensAreaType.Full : EncodedSemanticTokensAreaType.Delta, offset); offset += 1; - buff.writeUInt32BE(area.line + 1, offset); offset += 4; - if (area.type === 'full') { - const tokens = area.data; - const tokenCount = (tokens.length / 5) | 0; - buff.writeUInt32BE(tokenCount, offset); offset += 4; - // here we are explicitly iterating an writing the ints again to ensure writing the desired endianness. - for (let i = 0; i < tokenCount; i++) { - const tokenOffset = 5 * i; - buff.writeUInt32BE(tokens[tokenOffset], offset); offset += 4; - buff.writeUInt32BE(tokens[tokenOffset + 1], offset); offset += 4; - buff.writeUInt32BE(tokens[tokenOffset + 2], offset); offset += 4; - buff.writeUInt32BE(tokens[tokenOffset + 3], offset); offset += 4; - buff.writeUInt32BE(tokens[tokenOffset + 4], offset); offset += 4; - } - // buff.set(VSBuffer.wrap(uint8), offset); offset += area.data.byteLength; - } else { - buff.writeUInt32BE(area.oldIndex, offset); offset += 4; - } - return offset; -} - -function encodedAreaSize(area: ISemanticTokensAreaDto): number { - let result = 0; - result += 1; // type - result += 4; // line - if (area.type === 'full') { - const tokens = area.data; - const tokenCount = (tokens.length / 5) | 0; - result += 4; // token count - result += tokenCount * 5 * 4; - return result; - } else { - result += 4; // old index - return result; - } -} - -function decodeArea(buff: VSBuffer, offset: number, areas: ISemanticTokensAreaDto[]): number { - const type: EncodedSemanticTokensAreaType = buff.readUInt8(offset); offset += 1; - const line = buff.readUInt32BE(offset); offset += 4; - if (type === EncodedSemanticTokensAreaType.Full) { - // here we are explicitly iterating and reading the ints again to ensure reading the desired endianness. - const tokenCount = buff.readUInt32BE(offset); offset += 4; - const data = new Uint32Array(5 * tokenCount); - for (let i = 0; i < tokenCount; i++) { - const destOffset = 5 * i; - data[destOffset] = buff.readUInt32BE(offset); offset += 4; - data[destOffset + 1] = buff.readUInt32BE(offset); offset += 4; - data[destOffset + 2] = buff.readUInt32BE(offset); offset += 4; - data[destOffset + 3] = buff.readUInt32BE(offset); offset += 4; - data[destOffset + 4] = buff.readUInt32BE(offset); offset += 4; - } - areas.push({ - type: 'full', - line: line, - data: data - }); - return offset; - } else { - const oldIndex = buff.readUInt32BE(offset); offset += 4; - areas.push({ - type: 'delta', - line: line, - oldIndex: oldIndex - }); - return offset; - } -} diff --git a/src/vs/workbench/api/node/extHostCLIServer.ts b/src/vs/workbench/api/node/extHostCLIServer.ts index e5d3c44e63b..1ccb71e52a9 100644 --- a/src/vs/workbench/api/node/extHostCLIServer.ts +++ b/src/vs/workbench/api/node/extHostCLIServer.ts @@ -101,9 +101,6 @@ export class CLIServer { for (const s of folderURIs) { try { urisToOpen.push({ folderUri: URI.parse(s) }); - if (!addMode && !forceReuseWindow) { - forceNewWindow = true; - } } catch (e) { // ignore } @@ -114,9 +111,6 @@ export class CLIServer { try { if (hasWorkspaceFileExtension(s)) { urisToOpen.push({ workspaceUri: URI.parse(s) }); - if (!forceReuseWindow) { - forceNewWindow = true; - } } else { urisToOpen.push({ fileUri: URI.parse(s) }); } @@ -127,7 +121,8 @@ export class CLIServer { } if (urisToOpen.length) { const waitMarkerFileURI = waitMarkerFilePath ? URI.file(waitMarkerFilePath) : undefined; - const windowOpenArgs: INativeOpenWindowOptions = { forceNewWindow, diffMode, addMode, gotoLineMode, forceReuseWindow, waitMarkerFileURI }; + const preferNewWindow = !forceReuseWindow && !waitMarkerFileURI && !addMode; + const windowOpenArgs: INativeOpenWindowOptions = { forceNewWindow, diffMode, addMode, gotoLineMode, forceReuseWindow, preferNewWindow, waitMarkerFileURI }; this._commands.executeCommand('_files.windowOpen', urisToOpen, windowOpenArgs); } res.writeHead(200); diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 51d6ed18b7c..4052bcf0d8b 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -444,7 +444,8 @@ class ResourceLabelWidget extends IconLabel { italic: this.options && this.options.italic, matches: this.options && this.options.matches, extraClasses: [], - separator: this.options?.separator + separator: this.options?.separator, + domId: this.options?.domId }; const resource = this.label.resource; diff --git a/src/vs/workbench/browser/media/style.css b/src/vs/workbench/browser/media/style.css index f6c7bd7b620..b97ac21dda0 100644 --- a/src/vs/workbench/browser/media/style.css +++ b/src/vs/workbench/browser/media/style.css @@ -180,6 +180,8 @@ body.web { .monaco-workbench select { -webkit-appearance: none; -moz-appearance: none; + /* Hides inner border from FF */ + border: 1px solid; } .monaco-workbench .select-container { diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index b4837c8d638..29ac27a90b6 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -11,7 +11,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { Part } from 'vs/workbench/browser/part'; import { GlobalActivityActionViewItem, ViewletActivityAction, ToggleViewletAction, PlaceHolderToggleCompositePinnedAction, PlaceHolderViewletActivityAction } from 'vs/workbench/browser/parts/activitybar/activitybarActions'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { IBadge } from 'vs/workbench/services/activity/common/activity'; +import { IBadge, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IWorkbenchLayoutService, Parts, Position as SideBarPosition } from 'vs/workbench/services/layout/browser/layoutService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; @@ -24,7 +24,7 @@ import { Dimension, addClass, removeNode } from 'vs/base/browser/dom'; import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { ToggleCompositePinnedAction, ICompositeBarColors, ActivityAction } from 'vs/workbench/browser/parts/compositeBarActions'; +import { ToggleCompositePinnedAction, ICompositeBarColors, ActivityAction, ICompositeActivity } from 'vs/workbench/browser/parts/compositeBarActions'; import { ViewletDescriptor } from 'vs/workbench/browser/viewlet'; import { IViewsService, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainer, TEST_VIEW_CONTAINER_ID, IViewDescriptorCollection } from 'vs/workbench/common/views'; import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -68,6 +68,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { private globalActivityAction: ActivityAction | undefined; private globalActivityActionBar: ActionBar | undefined; + private globalActivity: ICompositeActivity[] = []; private customMenubar: CustomMenubarControl | undefined; private menubar: HTMLElement | undefined; @@ -202,18 +203,68 @@ export class ActivitybarPart extends Part implements IActivityBarService { } if (viewletOrActionId === GLOBAL_ACTIVITY_ID) { - return this.showGlobalActivity(badge, clazz); + return this.showGlobalActivity(badge, clazz, priority); } return Disposable.None; } - private showGlobalActivity(badge: IBadge, clazz?: string): IDisposable { + private showGlobalActivity(badge: IBadge, clazz?: string, priority?: number): IDisposable { + if (typeof priority !== 'number') { + priority = 0; + } + const activity: ICompositeActivity = { badge, clazz, priority }; + + for (let i = 0; i <= this.globalActivity.length; i++) { + if (i === this.globalActivity.length) { + this.globalActivity.push(activity); + break; + } else if (this.globalActivity[i].priority <= priority) { + this.globalActivity.splice(i, 0, activity); + break; + } + } + this.updateGlobalActivity(); + + return toDisposable(() => this.removeGlobalActivity(activity)); + } + + private removeGlobalActivity(activity: ICompositeActivity): void { + const index = this.globalActivity.indexOf(activity); + if (index !== -1) { + this.globalActivity.splice(index, 1); + this.updateGlobalActivity(); + } + } + + private updateGlobalActivity(): void { const globalActivityAction = assertIsDefined(this.globalActivityAction); + if (this.globalActivity.length) { + const [{ badge, clazz, priority }] = this.globalActivity; + if (badge instanceof NumberBadge && this.globalActivity.length > 1) { + const cumulativeNumberBadge = this.getCumulativeNumberBadge(priority); + globalActivityAction.setBadge(cumulativeNumberBadge); + } else { + globalActivityAction.setBadge(badge, clazz); + } + } else { + globalActivityAction.setBadge(undefined); + } + } - globalActivityAction.setBadge(badge, clazz); - - return toDisposable(() => globalActivityAction.setBadge(undefined)); + private getCumulativeNumberBadge(priority: number): NumberBadge { + const numberActivities = this.globalActivity.filter(activity => activity.badge instanceof NumberBadge && activity.priority === priority); + let number = numberActivities.reduce((result, activity) => { return result + (activity.badge).number; }, 0); + let descriptorFn = (): string => { + return numberActivities.reduce((result, activity, index) => { + result = result + (activity.badge).getDescription(); + if (index < numberActivities.length - 1) { + result = result + '\n'; + } + return result; + }, ''); + }; + return new NumberBadge(number, descriptorFn); } private uninstallMenubar() { diff --git a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css index a2ec3e8a74a..cbc3d14705b 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css @@ -7,22 +7,6 @@ width: 48px; } -.monaco-workbench.windows.chromium .part.activitybar, -.monaco-workbench.linux.chromium .part.activitybar { - /* - * Explicitly put the part onto its own layer to help Chrome to - * render the content with LCD-anti-aliasing. By partioning the - * workbench into multiple layers, we can ensure that a bad - * behaving part is not making another part fallback to greyscale - * rendering. - * - * macOS: does not render LCD-anti-aliased. - */ - transform: translate3d(0px, 0px, 0px); - overflow: visible; /* when a new layer is created, we need to set overflow visible to avoid clipping the menubar */ - z-index: 10; /* make sure context menu created from activity bar has higher z index then other grid views */ -} - .monaco-workbench .activitybar > .content { height: 100%; display: flex; diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbs.ts b/src/vs/workbench/browser/parts/editor/breadcrumbs.ts index ac600063f15..a62a5f543b0 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbs.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbs.ts @@ -170,131 +170,157 @@ Registry.as(Extensions.Configuration).registerConfigurat 'breadcrumbs.showFiles': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.file', "When enabled breadcrumbs show `file`-symbols.") }, 'breadcrumbs.showModules': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.module', "When enabled breadcrumbs show `module`-symbols.") }, 'breadcrumbs.showNamespaces': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.namespace', "When enabled breadcrumbs show `namespace`-symbols.") }, 'breadcrumbs.showPackages': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.package', "When enabled breadcrumbs show `package`-symbols.") }, 'breadcrumbs.showClasses': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.class', "When enabled breadcrumbs show `class`-symbols.") }, 'breadcrumbs.showMethods': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.method', "When enabled breadcrumbs show `method`-symbols.") }, 'breadcrumbs.showProperties': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.property', "When enabled breadcrumbs show `property`-symbols.") }, 'breadcrumbs.showFields': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.field', "When enabled breadcrumbs show `field`-symbols.") }, 'breadcrumbs.showConstructors': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.constructor', "When enabled breadcrumbs show `constructor`-symbols.") }, 'breadcrumbs.showEnums': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.enum', "When enabled breadcrumbs show `enum`-symbols.") }, 'breadcrumbs.showInterfaces': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.interface', "When enabled breadcrumbs show `interface`-symbols.") }, 'breadcrumbs.showFunctions': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.function', "When enabled breadcrumbs show `function`-symbols.") }, 'breadcrumbs.showVariables': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.variable', "When enabled breadcrumbs show `variable`-symbols.") }, 'breadcrumbs.showConstants': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.constant', "When enabled breadcrumbs show `constant`-symbols.") }, 'breadcrumbs.showStrings': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.string', "When enabled breadcrumbs show `string`-symbols.") }, 'breadcrumbs.showNumbers': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.number', "When enabled breadcrumbs show `number`-symbols.") }, 'breadcrumbs.showBooleans': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.boolean', "When enabled breadcrumbs show `boolean`-symbols.") }, 'breadcrumbs.showArrays': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.array', "When enabled breadcrumbs show `array`-symbols.") }, 'breadcrumbs.showObjects': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.object', "When enabled breadcrumbs show `object`-symbols.") }, 'breadcrumbs.showKeys': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.key', "When enabled breadcrumbs show `key`-symbols.") }, 'breadcrumbs.showNull': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.null', "When enabled breadcrumbs show `null`-symbols.") }, 'breadcrumbs.showEnumMembers': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.enumMember', "When enabled breadcrumbs show `enumMember`-symbols.") }, 'breadcrumbs.showStructs': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.struct', "When enabled breadcrumbs show `struct`-symbols.") }, 'breadcrumbs.showEvents': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.event', "When enabled breadcrumbs show `event`-symbols.") }, 'breadcrumbs.showOperators': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.operator', "When enabled breadcrumbs show `operator`-symbols.") }, 'breadcrumbs.showTypeParameters': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.typeParameter', "When enabled breadcrumbs show `typeParameter`-symbols.") } } diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index ef154461212..dc488d207bd 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -47,6 +47,7 @@ import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { onDidChangeZoomLevel } from 'vs/base/browser/browser'; import { withNullAsUndefined, withUndefinedAsNull } from 'vs/base/common/types'; import { ILabelService } from 'vs/platform/label/common/label'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; class Item extends BreadcrumbsItem { @@ -168,6 +169,7 @@ export class BreadcrumbsControl { @IThemeService private readonly _themeService: IThemeService, @IQuickOpenService private readonly _quickOpenService: IQuickOpenService, @IConfigurationService private readonly _configurationService: IConfigurationService, + @ITextResourceConfigurationService private readonly _textResourceConfigurationService: ITextResourceConfigurationService, @IFileService private readonly _fileService: IFileService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @ILabelService private readonly _labelService: ILabelService, @@ -246,7 +248,12 @@ export class BreadcrumbsControl { const uri = input.getResource()!; const editor = this._getActiveCodeEditor(); - const model = new EditorBreadcrumbsModel(uri, editor, this._configurationService, this._workspaceService); + const model = new EditorBreadcrumbsModel( + uri, editor, + this._configurationService, + this._textResourceConfigurationService, + this._workspaceService + ); dom.toggleClass(this.domNode, 'relative-path', model.isRelative()); dom.toggleClass(this.domNode, 'backslash-path', this._labelService.getSeparator(uri.scheme, uri.authority) === '\\'); diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts index f05157c5257..565a8ebcb9e 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts @@ -23,6 +23,8 @@ import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs import { FileKind } from 'vs/platform/files/common/files'; import { withNullAsUndefined } from 'vs/base/common/types'; import { OutlineFilter } from 'vs/editor/contrib/documentSymbols/outlineTree'; +import { ITextModel } from 'vs/editor/common/model'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; export class FileElement { constructor( @@ -53,9 +55,9 @@ export class EditorBreadcrumbsModel { private readonly _uri: URI, private readonly _editor: ICodeEditor | undefined, @IConfigurationService private readonly _configurationService: IConfigurationService, + @ITextResourceConfigurationService private readonly _textResourceConfigurationService: ITextResourceConfigurationService, @IWorkspaceContextService workspaceService: IWorkspaceContextService, ) { - this._cfgFilePath = BreadcrumbsConfig.FilePath.bindTo(_configurationService); this._cfgSymbolPath = BreadcrumbsConfig.SymbolPath.bindTo(_configurationService); @@ -142,6 +144,17 @@ export class EditorBreadcrumbsModel { this._disposables.add(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('breadcrumbs')) { this._updateOutline(true); + return; + } + if (this._editor && this._editor.getModel()) { + const editorModel = this._editor.getModel() as ITextModel; + const languageName = editorModel.getLanguageIdentifier().language; + + // Checking for changes in the current language override config. + // We can't be more specific than this because the ConfigurationChangeEvent(e) only includes the first part of the root path + if (e.affectsConfiguration(`[${languageName}]`)) { + this._updateOutline(true); + } } })); @@ -250,7 +263,12 @@ export class EditorBreadcrumbsModel { private _isFiltered(element: TreeElement): boolean { if (element instanceof OutlineElement) { const key = `breadcrumbs.${OutlineFilter.kindToConfigName[element.symbol.kind]}`; - return !this._configurationService.getValue(key); + let uri: URI | undefined; + if (this._editor && this._editor.getModel()) { + const model = this._editor.getModel() as ITextModel; + uri = model.uri; + } + return !this._textResourceConfigurationService.getValue(uri, key); } return false; } diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 71ac2878d4e..848ff70c8d7 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -851,6 +851,7 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable { update(editor: ICodeEditor | undefined): void { this.editor = editor; + this.updateMarkers(); this.updateStatus(); } diff --git a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css index c4c66c12eaa..582eac233e2 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css +++ b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css @@ -48,20 +48,6 @@ overflow: hidden; } -.monaco-workbench.windows.chromium .part.editor > .content .editor-group-container > .title, -.monaco-workbench.linux.chromium .part.editor > .content .editor-group-container > .title { - /* - * Explicitly put the part onto its own layer to help Chrome to - * render the content with LCD-anti-aliasing. By partioning the - * workbench into multiple layers, we can ensure that a bad - * behaving part is not making another part fallback to greyscale - * rendering. - * - * macOS: does not render LCD-anti-aliased. - */ - transform: translate3d(0px, 0px, 0px); -} - .monaco-workbench .part.editor > .content .editor-group-container > .title:not(.tabs) { display: flex; /* when tabs are not shown, use flex layout */ flex-wrap: nowrap; diff --git a/src/vs/workbench/browser/parts/panel/media/panelpart.css b/src/vs/workbench/browser/parts/panel/media/panelpart.css index 5db9c19a1ea..4909d7e9738 100644 --- a/src/vs/workbench/browser/parts/panel/media/panelpart.css +++ b/src/vs/workbench/browser/parts/panel/media/panelpart.css @@ -12,20 +12,6 @@ z-index: initial; } -.monaco-workbench.windows.chromium .part.panel, -.monaco-workbench.linux.chromium .part.panel { - /* - * Explicitly put the part onto its own layer to help Chrome to - * render the content with LCD-anti-aliasing. By partioning the - * workbench into multiple layers, we can ensure that a bad - * behaving part is not making another part fallback to greyscale - * rendering. - * - * macOS: does not render LCD-anti-aliased. - */ - transform: translate3d(0px, 0px, 0px); -} - .monaco-workbench .part.panel .title { height: 35px; display: flex; @@ -114,7 +100,7 @@ text-align: center; display: inline-block; box-sizing: border-box; -} + } /** Actions */ diff --git a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css index 1e2fad096a3..25b9dcd0625 100644 --- a/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css +++ b/src/vs/workbench/browser/parts/sidebar/media/sidebarpart.css @@ -3,21 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench.windows.chromium .part.sidebar, -.monaco-workbench.linux.chromium .part.sidebar { - /* - * Explicitly put the part onto its own layer to help Chrome to - * render the content with LCD-anti-aliasing. By partioning the - * workbench into multiple layers, we can ensure that a bad - * behaving part is not making another part fallback to greyscale - * rendering. - * - * macOS: does not render LCD-anti-aliased. - */ - transform: translate3d(0px, 0px, 0px); -} - -.monaco-workbench .part.sidebar > .content { +.monaco-workbench .sidebar > .content { overflow: hidden; } diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index 8dde95a5b7e..3102b114c52 100644 --- a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -13,20 +13,6 @@ overflow: visible; } -.monaco-workbench.windows.chromium .part.statusbar, -.monaco-workbench.linux.chromium .part.statusbar { - /* - * Explicitly put the part onto its own layer to help Chrome to - * render the content with LCD-anti-aliasing. By partioning the - * workbench into multiple layers, we can ensure that a bad - * behaving part is not making another part fallback to greyscale - * rendering. - * - * macOS: does not render LCD-anti-aliased. - */ - transform: translate3d(0px, 0px, 0px); -} - .monaco-workbench .part.statusbar.status-border-top::after { content: ''; position: absolute; @@ -51,7 +37,6 @@ .monaco-workbench .part.statusbar > .left-items { flex-grow: 1; /* left items push right items to the far right end */ - overflow: hidden; /* Hide the overflowing entries */ } .monaco-workbench .part.statusbar > .items-container > .statusbar-item { diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index 54c67324b61..2725a7d5da5 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -19,22 +19,6 @@ display: flex; } -.monaco-workbench.windows .part.titlebar, -.monaco-workbench.linux .part.titlebar { - /* - * Explicitly put the part onto its own layer to help Chrome to - * render the content with LCD-anti-aliasing. By partioning the - * workbench into multiple layers, we can ensure that a bad - * behaving part is not making another part fallback to greyscale - * rendering. - * - * macOS: does not render LCD-anti-aliased. - */ - transform: translate3d(0px, 0px, 0px); - position: relative; - z-index: 1000; /* move the entire titlebar above the workbench, except modals/dialogs */ -} - .monaco-workbench .part.titlebar > .titlebar-drag-region { top: 0; left: 0; @@ -45,11 +29,6 @@ -webkit-app-region: drag; } -.monaco-workbench .part.titlebar > .menubar { - /* Move above drag region since negative z-index on that element causes AA issues */ - z-index: 1; -} - .monaco-workbench .part.titlebar > .window-title { flex: 0 1 auto; font-size: 12px; @@ -78,6 +57,11 @@ cursor: default; } +.monaco-workbench .part.titlebar > .menubar { + /* move menubar above drag region as negative z-index on drag region cause greyscale AA */ + z-index: 2000; +} + .monaco-workbench.linux .part.titlebar > .window-title { font-size: inherit; } @@ -100,7 +84,7 @@ width: 35px; height: 100%; position: relative; - z-index: 2; /* highest level of titlebar */ + z-index: 3000; background-image: url('code-icon.svg'); background-repeat: no-repeat; background-position: center center; @@ -118,12 +102,11 @@ flex-shrink: 0; text-align: center; position: relative; - z-index: 2; /* highest level of titlebar */ + z-index: 3000; -webkit-app-region: no-drag; height: 100%; width: 138px; margin-left: auto; - transform: translate3d(0px, 0px, 0px); } .monaco-workbench.fullscreen .part.titlebar > .window-controls-container { diff --git a/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index 16e966ce337..fb2b337fc50 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -44,7 +44,7 @@ .monaco-workbench .tree-explorer-viewlet-tree-view .message { display: flex; - padding: 4px 12px 0px 18px; + padding: 4px 12px 4px 18px; user-select: text; -webkit-user-select: text; } diff --git a/src/vs/workbench/browser/parts/views/viewsViewlet.ts b/src/vs/workbench/browser/parts/views/viewsViewlet.ts index 1399797b257..13968eb74c9 100644 --- a/src/vs/workbench/browser/parts/views/viewsViewlet.ts +++ b/src/vs/workbench/browser/parts/views/viewsViewlet.ts @@ -101,8 +101,16 @@ export abstract class ViewContainerViewlet extends PaneViewlet implements IViews this.focus(); } - getContextMenuActions(): IAction[] { + getContextMenuActions(viewDescriptor?: IViewDescriptor): IAction[] { const result: IAction[] = []; + if (viewDescriptor) { + result.push({ + id: `${viewDescriptor.id}.removeView`, + label: localize('hideView', "Hide"), + enabled: viewDescriptor.canToggleVisibility, + run: () => this.toggleViewVisibility(viewDescriptor.id) + }); + } const viewToggleActions = this.viewsModel.viewDescriptors.map(viewDescriptor => ({ id: `${viewDescriptor.id}.toggleVisibility`, label: viewDescriptor.name, @@ -111,13 +119,17 @@ export abstract class ViewContainerViewlet extends PaneViewlet implements IViews run: () => this.toggleViewVisibility(viewDescriptor.id) })); - result.push(...viewToggleActions); - const parentActions = this.getViewletContextMenuActions(); - if (viewToggleActions.length && parentActions.length) { + if (result.length && viewToggleActions.length) { result.push(new Separator()); } + result.push(...viewToggleActions); + const parentActions = this.getViewletContextMenuActions(); + if (result.length && parentActions.length) { + result.push(new Separator()); + } result.push(...parentActions); + return result; } @@ -249,17 +261,7 @@ export abstract class ViewContainerViewlet extends PaneViewlet implements IViews event.stopPropagation(); event.preventDefault(); - const actions: IAction[] = []; - actions.push({ - id: `${viewDescriptor.id}.removeView`, - label: localize('hideView', "Hide"), - enabled: viewDescriptor.canToggleVisibility, - run: () => this.toggleViewVisibility(viewDescriptor.id) - }); - const otherActions = this.getContextMenuActions(); - if (otherActions.length) { - actions.push(...[new Separator(), ...otherActions]); - } + const actions: IAction[] = this.getContextMenuActions(viewDescriptor); let anchor: { x: number, y: number } = { x: event.posx, y: event.posy }; this.contextMenuService.showContextMenu({ @@ -377,6 +379,9 @@ export abstract class FilterViewContainerViewlet extends ViewContainerViewlet { protected abstract getFilterOn(viewDescriptor: IViewDescriptor): string | undefined; private onFilterChanged(newFilterValue: string) { + if (this.allViews.size === 0) { + this.updateAllViews(this.viewsModel.viewDescriptors); + } this.getViewsNotForTarget(newFilterValue).forEach(item => this.viewsModel.setVisible(item.id, false)); this.getViewsForTarget(newFilterValue).forEach(item => this.viewsModel.setVisible(item.id, true)); } diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts index 4f5199b62ed..55b2be2978c 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts @@ -24,7 +24,7 @@ import { Range } from 'vs/editor/common/core/range'; import { IPosition } from 'vs/editor/common/core/position'; import { MenuId } from 'vs/platform/actions/common/actions'; -const _ctxHasCompletionItemProvider = new RawContextKey('editorHasCallHierarchyProvider', false); +const _ctxHasCallHierarchyProvider = new RawContextKey('editorHasCallHierarchyProvider', false); const _ctxCallHierarchyVisible = new RawContextKey('callHierarchyVisible', false); class CallHierarchyController implements IEditorContribution { @@ -52,7 +52,7 @@ class CallHierarchyController implements IEditorContribution { @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { this._ctxIsVisible = _ctxCallHierarchyVisible.bindTo(this._contextKeyService); - this._ctxHasProvider = _ctxHasCompletionItemProvider.bindTo(this._contextKeyService); + this._ctxHasProvider = _ctxHasCallHierarchyProvider.bindTo(this._contextKeyService); this._dispoables.add(Event.any(_editor.onDidChangeModel, _editor.onDidChangeModelLanguage, CallHierarchyProviderRegistry.onDidChange)(() => { this._ctxHasProvider.set(_editor.hasModel() && CallHierarchyProviderRegistry.has(_editor.getModel())); })); @@ -172,8 +172,7 @@ registerEditorAction(class extends EditorAction { primary: KeyMod.Shift + KeyMod.Alt + KeyCode.KEY_H }, precondition: ContextKeyExpr.and( - _ctxCallHierarchyVisible.negate(), - _ctxHasCompletionItemProvider, + _ctxHasCallHierarchyProvider, PeekContext.notInPeekEditor ) }); diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts index df5446fd52b..c85cd79025d 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts @@ -49,10 +49,10 @@ class ChangeHierarchyDirectionAction extends Action { }); const update = () => { if (getDirection() === CallHierarchyDirection.CallsFrom) { - this.label = localize('toggle.from', "Showing Calls"); + this.label = localize('toggle.from', "Show Incoming Calls"); this.class = 'calls-from'; } else { - this.label = localize('toggle.to', "Showing Callers"); + this.label = localize('toggle.to', "Showing Outgoing Calls"); this.class = 'calls-to'; } }; @@ -325,7 +325,11 @@ export class CallHierarchyTreePeekWidget extends peekView.PeekViewWidget { // set decorations for caller ranges (if in the same file) let decorations: IModelDeltaDecoration[] = []; let fullRange: IRange | undefined; - for (const loc of element.locations) { + let locations = element.locations; + if (!locations) { + locations = [{ uri: element.item.uri, range: element.item.selectionRange }]; + } + for (const loc of locations) { if (loc.uri.toString() === previewUri.toString()) { decorations.push({ range: loc.range, options }); fullRange = !fullRange ? loc.range : Range.plusRange(loc.range, fullRange); @@ -424,7 +428,7 @@ export class CallHierarchyTreePeekWidget extends peekView.PeekViewWidget { } protected _doLayoutBody(height: number, width: number): void { - if (this._dim.height !== height || this._dim.width === width) { + if (this._dim.height !== height || this._dim.width !== width) { super._doLayoutBody(height, width); this._dim = { height, width }; this._layoutInfo.height = this._viewZone ? this._viewZone.heightInLines : this._layoutInfo.height; diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts index a00b49e0d01..c412f7b992a 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts @@ -17,7 +17,7 @@ import { Range } from 'vs/editor/common/core/range'; export class Call { constructor( readonly item: CallHierarchyItem, - readonly locations: Location[], + readonly locations: Location[] | undefined, readonly model: CallHierarchyModel, readonly parent: Call | undefined ) { } @@ -43,7 +43,7 @@ export class DataSource implements IAsyncDataSource { async getChildren(element: CallHierarchyModel | Call): Promise { if (element instanceof CallHierarchyModel) { - return [new Call(element.root, [], element, undefined)]; + return [new Call(element.root, undefined, element, undefined)]; } const { model, item } = element; diff --git a/src/vs/workbench/contrib/callHierarchy/browser/media/action-call-from-dark.svg b/src/vs/workbench/contrib/callHierarchy/browser/media/action-call-from-dark.svg new file mode 100644 index 00000000000..66406bfc5dd --- /dev/null +++ b/src/vs/workbench/contrib/callHierarchy/browser/media/action-call-from-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/workbench/contrib/callHierarchy/browser/media/action-call-from.svg b/src/vs/workbench/contrib/callHierarchy/browser/media/action-call-from.svg new file mode 100644 index 00000000000..b65e2d14a4d --- /dev/null +++ b/src/vs/workbench/contrib/callHierarchy/browser/media/action-call-from.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/workbench/contrib/callHierarchy/browser/media/action-call-to-dark.svg b/src/vs/workbench/contrib/callHierarchy/browser/media/action-call-to-dark.svg new file mode 100644 index 00000000000..ff488f1ed4c --- /dev/null +++ b/src/vs/workbench/contrib/callHierarchy/browser/media/action-call-to-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/workbench/contrib/callHierarchy/browser/media/action-call-to.svg b/src/vs/workbench/contrib/callHierarchy/browser/media/action-call-to.svg new file mode 100644 index 00000000000..159e5b92eaa --- /dev/null +++ b/src/vs/workbench/contrib/callHierarchy/browser/media/action-call-to.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/vs/workbench/contrib/callHierarchy/browser/media/callHierarchy.css b/src/vs/workbench/contrib/callHierarchy/browser/media/callHierarchy.css index 19c2e871295..c4e553dbd9f 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/media/callHierarchy.css +++ b/src/vs/workbench/contrib/callHierarchy/browser/media/callHierarchy.css @@ -21,19 +21,27 @@ } .monaco-workbench .action-label.calls-to { - background-image: url(files_CallTo_CallTo_16x.svg); + background-image: url(action-call-to.svg); background-size: 14px 14px; background-repeat: no-repeat; background-position: left center; } +.vs-dark .monaco-workbench .action-label.calls-to { + background-image: url(action-call-to-dark.svg); +} + .monaco-workbench .action-label.calls-from { - background-image: url(files_CallFrom_CallFrom_16x.svg); + background-image: url(action-call-from.svg); background-size: 14px 14px; background-repeat: no-repeat; background-position: left center; } +.vs-dark .monaco-workbench .action-label.calls-from{ + background-image: url(action-call-from-dark.svg); +} + .monaco-workbench .call-hierarchy .editor, .monaco-workbench .call-hierarchy .tree { height: 100%; diff --git a/src/vs/workbench/contrib/callHierarchy/browser/media/files_CallFrom_CallFrom_16x.svg b/src/vs/workbench/contrib/callHierarchy/browser/media/files_CallFrom_CallFrom_16x.svg deleted file mode 100644 index 667f3e7655b..00000000000 --- a/src/vs/workbench/contrib/callHierarchy/browser/media/files_CallFrom_CallFrom_16x.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/contrib/callHierarchy/browser/media/files_CallTo_CallTo_16x.svg b/src/vs/workbench/contrib/callHierarchy/browser/media/files_CallTo_CallTo_16x.svg deleted file mode 100644 index 26c436e77c9..00000000000 --- a/src/vs/workbench/contrib/callHierarchy/browser/media/files_CallTo_CallTo_16x.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/workbench/contrib/customEditor/browser/commands.ts b/src/vs/workbench/contrib/customEditor/browser/commands.ts index cf1118961f2..7b384f6c7d1 100644 --- a/src/vs/workbench/contrib/customEditor/browser/commands.ts +++ b/src/vs/workbench/contrib/customEditor/browser/commands.ts @@ -20,6 +20,7 @@ import { CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CONTEXT_HAS_CUSTOM_EDITORS, import { getMultiSelectedResources } from 'vs/workbench/contrib/files/browser/files'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IExplorerService } from 'vs/workbench/contrib/files/common/files'; const viewCategory = nls.localize('viewCategory', "View"); @@ -34,7 +35,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ when: EditorContextKeys.focus.toNegated(), handler: async (accessor: ServicesAccessor, resource: URI | object) => { const editorService = accessor.get(IEditorService); - const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService); + const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService, accessor.get(IExplorerService)); const targetResource = firstOrDefault(resources); if (!targetResource) { return; diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 78cba9eadda..ab6ddeb66df 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -17,6 +17,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { GroupIdentifier, IEditorInput, IRevertOptions, ISaveOptions, Verbosity } from 'vs/workbench/common/editor'; import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { WebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { IWebviewWorkbenchService, LazilyResolvedWebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -117,26 +118,19 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { return false; } - // Preserve view state by opening the editor first. In addition - // this allows the user to review the contents of the editor. - // let viewState: IEditorViewState | undefined = undefined; - // const editor = await this.editorService.openEditor(this, undefined, group); - // if (isTextEditor(editor)) { - // viewState = editor.getViewState(); - // } - let dialogPath = this._editorResource; - // if (this._editorResource.scheme === Schemas.untitled) { - // dialogPath = this.suggestFileName(resource); - // } - const target = await this.promptForPath(this._editorResource, dialogPath, options?.availableFileSystems); if (!target) { return false; // save cancelled } - await this._model.saveAs(this._editorResource, target, options); + if (!await this._model.saveAs(this._editorResource, target, options)) { + return false; + } + const replacement = this.handleMove(groupId, target) || this.instantiationService.createInstance(FileEditorInput, target, undefined, undefined); + + await this.editorService.replaceEditors([{ editor: this, replacement, options: { pinned: true } }], groupId); return true; } @@ -156,15 +150,24 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { // Help user to find a name for the file by opening it first await this.editorService.openEditor({ resource, options: { revealIfOpened: true, preserveFocus: true } }); - return this.fileDialogService.pickFileToSave({});//this.getSaveDialogOptions(defaultUri, availableFileSystems)); + return this.fileDialogService.pickFileToSave({ + availableFileSystems, + defaultUri + }); } public handleMove(groupId: GroupIdentifier, uri: URI, options?: ITextEditorOptions): IEditorInput | undefined { - const webview = assertIsDefined(this.takeOwnershipOfWebview()); - return this.instantiationService.createInstance(CustomFileEditorInput, - uri, - this.viewType, - generateUuid(), - new Lazy(() => webview)); + const editorInfo = this.customEditorService.getCustomEditor(this.viewType); + if (editorInfo?.matches(uri)) { + const webview = assertIsDefined(this.takeOwnershipOfWebview()); + const newInput = this.instantiationService.createInstance(CustomFileEditorInput, + uri, + this.viewType, + generateUuid(), + new Lazy(() => webview)); + newInput.updateGroup(groupId); + return newInput; + } + return undefined; } } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index 9e87a796443..49e2d2fcb4d 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { coalesce, distinct, find, mergeSort } from 'vs/base/common/arrays'; -import * as glob from 'vs/base/common/glob'; import { Lazy } from 'vs/base/common/lazy'; import { Disposable } from 'vs/base/common/lifecycle'; import { basename, isEqual } from 'vs/base/common/resources'; @@ -32,14 +31,14 @@ import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/wo import { CustomFileEditorInput } from './customEditorInput'; const defaultEditorId = 'default'; -const defaultEditorInfo: CustomEditorInfo = { +const defaultEditorInfo = new CustomEditorInfo({ id: defaultEditorId, displayName: nls.localize('promptOpenWith.defaultEditor', "VS Code's standard text editor"), selector: [ { filenamePattern: '*' } ], priority: CustomEditorPriority.default, -}; +}); export class CustomEditorInfoStore { private readonly contributedEditors = new Map(); @@ -64,7 +63,7 @@ export class CustomEditorInfoStore { public getContributedEditors(resource: URI): readonly CustomEditorInfo[] { return Array.from(this.contributedEditors.values()).filter(customEditor => - customEditor.selector.some(selector => matches(selector, resource))); + customEditor.matches(resource)); } } @@ -97,12 +96,12 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ for (const extension of extensions) { for (const webviewEditorContribution of extension.value) { - this._editorInfoStore.add({ + this._editorInfoStore.add(new CustomEditorInfo({ id: webviewEditorContribution.viewType, displayName: webviewEditorContribution.displayName, selector: webviewEditorContribution.selector || [], priority: webviewEditorContribution.priority || CustomEditorPriority.default, - }); + })); } } this.updateContexts(); @@ -127,6 +126,10 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ return { resource, viewType: activeInput.viewType }; } + public getCustomEditor(viewType: string): CustomEditorInfo | undefined { + return this._editorInfoStore.get(viewType); + } + public getContributedCustomEditors(resource: URI): readonly CustomEditorInfo[] { return this._editorInfoStore.getContributedEditors(resource); } @@ -134,7 +137,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ public getUserConfiguredCustomEditors(resource: URI): readonly CustomEditorInfo[] { const rawAssociations = this.configurationService.getValue(customEditorsAssociationsKey) || []; return coalesce(rawAssociations - .filter(association => matches(association, resource)) + .filter(association => CustomEditorInfo.selectorMatches(association, resource)) .map(association => this._editorInfoStore.get(association.viewType))); } @@ -332,6 +335,9 @@ export class CustomEditorContribution implements IWorkbenchContribution { return { override: (async () => { const standardEditor = await this.editorService.openEditor(editor, { ...options, ignoreOverrides: true }, group); + // Give a moment to make sure the editor is showing. + // Otherwise the focus shift can cause the prompt to be dismissed right away. + await new Promise(resolve => setTimeout(resolve, 20)); const selectedEditor = await this.customEditorService.promptOpenWith(resource, options, group); if (selectedEditor && selectedEditor.input) { await group.replaceEditors([{ @@ -405,16 +411,6 @@ function priorityToRank(priority: CustomEditorPriority): number { } } -function matches(selector: CustomEditorSelector, resource: URI): boolean { - if (selector.filenamePattern) { - if (glob.match(selector.filenamePattern.toLowerCase(), basename(resource).toLowerCase())) { - return true; - } - } - - return false; -} - registerThemingParticipant((theme, collector) => { const shadow = theme.getColor(colorRegistry.scrollbarShadow); if (shadow) { diff --git a/src/vs/workbench/contrib/customEditor/common/customEditor.ts b/src/vs/workbench/contrib/customEditor/common/customEditor.ts index 9162ab0e3d2..135ffc819b7 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditor.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditor.ts @@ -4,11 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; +import * as glob from 'vs/base/common/glob'; +import { basename } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { EditorInput, IEditor, ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; +import { EditorInput, IEditor, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; @@ -29,6 +31,7 @@ export interface ICustomEditorService { readonly activeCustomEditor: ICustomEditor | undefined; + getCustomEditor(viewType: string): CustomEditorInfo | undefined; getContributedCustomEditors(resource: URI): readonly CustomEditorInfo[]; getUserConfiguredCustomEditors(resource: URI): readonly CustomEditorInfo[]; @@ -87,9 +90,35 @@ export interface CustomEditorSelector { readonly filenamePattern?: string; } -export interface CustomEditorInfo { - readonly id: string; - readonly displayName: string; - readonly priority: CustomEditorPriority; - readonly selector: readonly CustomEditorSelector[]; +export class CustomEditorInfo { + + public readonly id: string; + public readonly displayName: string; + public readonly priority: CustomEditorPriority; + public readonly selector: readonly CustomEditorSelector[]; + + constructor(descriptor: { + readonly id: string; + readonly displayName: string; + readonly priority: CustomEditorPriority; + readonly selector: readonly CustomEditorSelector[]; + }) { + this.id = descriptor.id; + this.displayName = descriptor.displayName; + this.priority = descriptor.priority; + this.selector = descriptor.selector; + } + + matches(resource: URI): boolean { + return this.selector.some(selector => CustomEditorInfo.selectorMatches(selector, resource)); + } + + static selectorMatches(selector: CustomEditorSelector, resource: URI): boolean { + if (selector.filenamePattern) { + if (glob.match(selector.filenamePattern.toLowerCase(), basename(resource).toLowerCase())) { + return true; + } + } + return false; + } } diff --git a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index 233a2a43a92..9acef112583 100644 --- a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -17,6 +17,7 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; +import { ReplEvaluationResult } from 'vs/workbench/contrib/debug/common/replModel'; export const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; export const twistiePixels = 20; @@ -58,7 +59,7 @@ export function renderExpressionValue(expressionOrValue: IExpressionContainer | // remove stale classes container.className = 'value'; // when resolving expressions we represent errors from the server as a variable with name === null. - if (value === null || ((expressionOrValue instanceof Expression || expressionOrValue instanceof Variable) && !expressionOrValue.available)) { + if (value === null || ((expressionOrValue instanceof Expression || expressionOrValue instanceof Variable || expressionOrValue instanceof ReplEvaluationResult) && !expressionOrValue.available)) { dom.addClass(container, 'unavailable'); if (value !== Expression.DEFAULT_VALUE) { dom.addClass(container, 'error'); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index d45e0ec4d02..5374107fe82 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -273,7 +273,7 @@ class BreakpointEditorContribution implements IBreakpointEditorContribution { })); this.toDispose.push(this.editor.onDidChangeModelDecorations(() => this.onModelDecorationsChanged())); this.toDispose.push(this.configurationService.onDidChangeConfiguration(async (e) => { - if (e.affectsConfiguration('debug.showBreakpointsInOverviewRuler') || e.affectsConfiguration('debug.inlineBreakpointCandidates')) { + if (e.affectsConfiguration('debug.showBreakpointsInOverviewRuler') || e.affectsConfiguration('debug.showInlineBreakpointCandidates')) { await this.setDecorations(); } })); @@ -425,7 +425,7 @@ class BreakpointEditorContribution implements IBreakpointEditorContribution { } // Set breakpoint candidate decorations - const desiredCandidateDecorations = debugSettings.inlineBreakpointCandidates ? await createCandidateDecorations(this.editor.getModel(), this.breakpointDecorations, this.debugService) : []; + const desiredCandidateDecorations = debugSettings.showInlineBreakpointCandidates ? await createCandidateDecorations(this.editor.getModel(), this.breakpointDecorations, this.debugService) : []; const candidateDecorationIds = this.editor.deltaDecorations(this.candidateDecorations.map(c => c.decorationId), desiredCandidateDecorations); this.candidateDecorations.forEach(candidate => { candidate.inlineWidget.dispose(); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index d9711706037..7f277994bb7 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -500,7 +500,7 @@ class DataBreakpointsRenderer implements IListRenderer(ViewletExtensions.Viewlets).registerViewlet(ViewletDescriptor.create( DebugViewlet, VIEWLET_ID, - nls.localize('debugAndRun', "Debug And Run"), + nls.localize('debugAndRun', "Debug and Run"), 'codicon-debug', 3 )); @@ -272,9 +272,9 @@ configurationRegistry.registerConfiguration({ description: nls.localize({ comment: ['This is the description for a setting'], key: 'showBreakpointsInOverviewRuler' }, "Controls whether breakpoints should be shown in the overview ruler."), default: false }, - 'debug.inlineBreakpointCandidates': { + 'debug.showInlineBreakpointCandidates': { type: 'boolean', - description: nls.localize({ comment: ['This is the description for a setting'], key: 'inlineBreakpointCandidates' }, "Controls whether inline breakpoints candidate decorations should be shown in the editor while debugging."), + description: nls.localize({ comment: ['This is the description for a setting'], key: 'showInlineBreakpointCandidates' }, "Controls whether inline breakpoints candidate decorations should be shown in the editor while debugging."), default: true } } diff --git a/src/vs/workbench/contrib/debug/browser/debugCallStackContribution.ts b/src/vs/workbench/contrib/debug/browser/debugCallStackContribution.ts index ee78a18990e..dc262b161e6 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCallStackContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCallStackContribution.ts @@ -186,9 +186,9 @@ registerThemingParticipant((theme, collector) => { .monaco-workbench .codicon-debug-breakpoint-function, .monaco-workbench .codicon-debug-breakpoint-data, .monaco-workbench .codicon-debug-breakpoint-unsupported, - .monaco-workbench .codicon-debug-hint:not(*[class*='codicon-debug-breakpoint']) , - .monaco-workbench .codicon-debug-breakpoint-stackframe-dot, - .monaco-workbench .codicon-debug-breakpoint.codicon-debug-breakpoint-stackframe-focused::after { + .monaco-workbench .codicon-debug-hint:not([class*='codicon-debug-breakpoint']), + .monaco-workbench .codicon-debug-breakpoint.codicon-debug-breakpoint-stackframe-focused::after, + .monaco-workbench .codicon-debug-breakpoint.codicon-debug-breakpoint-stackframe::after { color: ${debugIconBreakpointColor} !important; } `); @@ -212,17 +212,16 @@ registerThemingParticipant((theme, collector) => { `); } - const debugIconBreakpointStackframeColor = theme.getColor(debugIconBreakpointStackframeForeground); - if (debugIconBreakpointStackframeColor) { + const debugIconBreakpointCurrentStackframeForegroundColor = theme.getColor(debugIconBreakpointCurrentStackframeForeground); + if (debugIconBreakpointCurrentStackframeForegroundColor) { collector.addRule(` - .monaco-workbench .codicon-debug-breakpoint-stackframe, - .monaco-workbench .codicon-debug-breakpoint-stackframe-dot::after { - color: ${debugIconBreakpointStackframeColor} !important; + .monaco-workbench .codicon-debug-breakpoint-stackframe { + color: ${debugIconBreakpointCurrentStackframeForegroundColor} !important; } `); } - const debugIconBreakpointStackframeFocusedColor = theme.getColor(debugIconBreakpointStackframeFocusedForeground); + const debugIconBreakpointStackframeFocusedColor = theme.getColor(debugIconBreakpointStackframeForeground); if (debugIconBreakpointStackframeFocusedColor) { collector.addRule(` .monaco-workbench .codicon-debug-breakpoint-stackframe-focused { @@ -239,5 +238,5 @@ const focusedStackFrameColor = registerColor('editor.focusedStackFrameHighlightB const debugIconBreakpointForeground = registerColor('debugIcon.breakpointForeground', { dark: '#E51400', light: '#E51400', hc: '#E51400' }, localize('debugIcon.breakpointForeground', 'Icon color for breakpoints.')); const debugIconBreakpointDisabledForeground = registerColor('debugIcon.breakpointDisabledForeground', { dark: '#848484', light: '#848484', hc: '#848484' }, localize('debugIcon.breakpointDisabledForeground', 'Icon color for disabled breakpoints.')); const debugIconBreakpointUnverifiedForeground = registerColor('debugIcon.breakpointUnverifiedForeground', { dark: '#848484', light: '#848484', hc: '#848484' }, localize('debugIcon.breakpointUnverifiedForeground', 'Icon color for unverified breakpoints.')); -const debugIconBreakpointStackframeForeground = registerColor('debugIcon.breakpointStackframeForeground', { dark: '#FFCC00', light: '#FFCC00', hc: '#FFCC00' }, localize('debugIcon.breakpointStackframeForeground', 'Icon color for breakpoints.')); -const debugIconBreakpointStackframeFocusedForeground = registerColor('debugIcon.breakpointStackframeFocusedForeground', { dark: '#89D185', light: '#89D185', hc: '#89D185' }, localize('debugIcon.breakpointStackframeFocusedForeground', 'Icon color for breakpoints.')); +const debugIconBreakpointCurrentStackframeForeground = registerColor('debugIcon.breakpointCurrentStackframeForeground', { dark: '#FFCC00', light: '#FFCC00', hc: '#FFCC00' }, localize('debugIcon.breakpointCurrentStackframeForeground', 'Icon color for the current breakpoint stack frame.')); +const debugIconBreakpointStackframeForeground = registerColor('debugIcon.breakpointStackframeForeground', { dark: '#89D185', light: '#89D185', hc: '#89D185' }, localize('debugIcon.breakpointStackframeForeground', 'Icon color for all breakpoint stack frames.')); diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index d13bc6c47f4..ba79a6e94b6 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -18,7 +18,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IFileService } from 'vs/platform/files/common/files'; -import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IDebugConfigurationProvider, ICompound, IDebugConfiguration, IConfig, IGlobalConfig, IConfigurationManager, ILaunch, IDebugAdapterDescriptorFactory, IDebugAdapter, IDebugSession, IAdapterDescriptor, CONTEXT_DEBUG_CONFIGURATION_TYPE, IDebugAdapterFactory } from 'vs/workbench/contrib/debug/common/debug'; @@ -80,7 +80,7 @@ export class ConfigurationManager implements IConfigurationManager { const previousSelectedRoot = this.storageService.get(DEBUG_SELECTED_ROOT, StorageScope.WORKSPACE); const previousSelectedLaunch = this.launches.filter(l => l.uri.toString() === previousSelectedRoot).pop(); this.debugConfigurationTypeContext = CONTEXT_DEBUG_CONFIGURATION_TYPE.bindTo(contextKeyService); - if (previousSelectedLaunch) { + if (previousSelectedLaunch && previousSelectedLaunch.getConfigurationNames().length) { this.selectConfiguration(previousSelectedLaunch, this.storageService.get(DEBUG_SELECTED_CONFIG_NAME_KEY, StorageScope.WORKSPACE)); } else if (this.launches.length > 0) { const rootUri = historyService.getLastActiveWorkspaceRoot(); @@ -284,9 +284,10 @@ export class ConfigurationManager implements IConfigurationManager { }); }); - this.toDispose.push(this.contextService.onDidChangeWorkspaceFolders(() => { + this.toDispose.push(Event.any(this.contextService.onDidChangeWorkspaceFolders, this.contextService.onDidChangeWorkbenchState)(() => { this.initLaunches(); - this.selectConfiguration(this.selectedLaunch); + const toSelect = this.selectedLaunch || (this.launches.length > 0 ? this.launches[0] : undefined); + this.selectConfiguration(toSelect); this.setCompoundSchemaValues(); })); this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => { @@ -495,10 +496,13 @@ abstract class AbstractLaunch { getConfigurationNames(includeCompounds = true): string[] { const config = this.getConfig(); - if (!config || !config.configurations || !Array.isArray(config.configurations)) { + if (!config || (!Array.isArray(config.configurations) && !Array.isArray(config.compounds))) { return []; } else { - const names = config.configurations.filter(cfg => cfg && typeof cfg.name === 'string').map(cfg => cfg.name); + const names: string[] = []; + if (config.configurations) { + names.push(...config.configurations.filter(cfg => cfg && typeof cfg.name === 'string').map(cfg => cfg.name)); + } if (includeCompounds && config.compounds) { if (config.compounds) { names.push(...config.compounds.filter(compound => typeof compound.name === 'string' && compound.configurations && compound.configurations.length) diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 56e0f0c8a42..023552bf0ff 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -41,7 +41,7 @@ import { deepClone, equals } from 'vs/base/common/objects'; import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { IDebugService, State, IDebugSession, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_STATE, CONTEXT_IN_DEBUG_MODE, IThread, IDebugConfiguration, VIEWLET_ID, REPL_ID, IConfig, ILaunch, IViewModel, IConfigurationManager, IDebugModel, IEnablement, IBreakpoint, IBreakpointData, ICompound, IGlobalConfig, IStackFrame, AdapterEndEvent, getStateLabel, IDebugSessionOptions, CONTEXT_DEBUG_UX } from 'vs/workbench/contrib/debug/common/debug'; -import { isExtensionHostDebugging } from 'vs/workbench/contrib/debug/common/debugUtils'; +import { getExtensionHostDebugSession } from 'vs/workbench/contrib/debug/common/debugUtils'; import { isErrorWithActions, createErrorWithActions } from 'vs/base/common/errorsWithActions'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; @@ -232,7 +232,8 @@ export class DebugService implements IDebugService { if (this.previousState !== state) { this.debugState.set(getStateLabel(state)); this.inDebugMode.set(state !== State.Inactive); - this.debugUx.set(!!(state !== State.Inactive || this.configurationManager.selectedConfiguration.name) ? 'default' : 'simple'); + // Only show the simple ux if debug is not yet started and if no launch.json exists + this.debugUx.set(((state !== State.Inactive && state !== State.Initializing) || this.configurationManager.selectedConfiguration.name) ? 'default' : 'simple'); this.previousState = state; this._onDidChangeState.fire(state); } @@ -538,8 +539,9 @@ export class DebugService implements IDebugService { } // 'Run without debugging' mode VSCode must terminate the extension host. More details: #3905 - if (isExtensionHostDebugging(session.configuration) && session.state === State.Running && session.configuration.noDebug) { - this.extensionHostDebugService.close(session.getId()); + const extensionDebugSession = getExtensionHostDebugSession(session); + if (extensionDebugSession && extensionDebugSession.state === State.Running && extensionDebugSession.configuration.noDebug) { + this.extensionHostDebugService.close(extensionDebugSession.getId()); } this.telemetryDebugSessionStop(session, adapterExitEvent); @@ -589,10 +591,11 @@ export class DebugService implements IDebugService { return this.runTaskAndCheckErrors(session.root, session.configuration.preLaunchTask); }; - if (isExtensionHostDebugging(session.configuration)) { + const extensionDebugSession = getExtensionHostDebugSession(session); + if (extensionDebugSession) { const taskResult = await runTasks(); if (taskResult === TaskRunResult.Success) { - this.extensionHostDebugService.reload(session.getId()); + this.extensionHostDebugService.reload(extensionDebugSession.getId()); } return; @@ -984,8 +987,8 @@ export class DebugService implements IDebugService { this.storeBreakpoints(); } - async addDataBreakpoint(label: string, dataId: string, canPersist: boolean): Promise { - this.model.addDataBreakpoint(label, dataId, canPersist); + async addDataBreakpoint(label: string, dataId: string, canPersist: boolean, accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined): Promise { + this.model.addDataBreakpoint(label, dataId, canPersist, accessTypes); await this.sendDataBreakpoints(); this.storeBreakpoints(); @@ -1097,7 +1100,7 @@ export class DebugService implements IDebugService { let result: DataBreakpoint[] | undefined; try { result = JSON.parse(this.storageService.get(DEBUG_DATA_BREAKPOINTS_KEY, StorageScope.WORKSPACE, '[]')).map((dbp: any) => { - return new DataBreakpoint(dbp.label, dbp.dataId, true, dbp.enabled, dbp.hitCondition, dbp.condition, dbp.logMessage); + return new DataBreakpoint(dbp.description, dbp.dataId, true, dbp.enabled, dbp.hitCondition, dbp.condition, dbp.logMessage, dbp.accessTypes); }); } catch (e) { } diff --git a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts index 9a195bec864..d84b2cff137 100644 --- a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts @@ -15,7 +15,7 @@ import { StartDebugActionViewItem, FocusSessionActionViewItem } from 'vs/workben import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IProgressService } from 'vs/platform/progress/common/progress'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -119,22 +119,27 @@ export class DebugViewlet extends ViewContainerViewlet { if (CONTEXT_DEBUG_UX.getValue(this.contextKeyService) === 'simple') { return []; } - if (this.showInitialDebugActions) { - return [this.startAction, this.configureAction, this.toggleReplAction]; + if (!this.showInitialDebugActions) { + + if (!this.debugToolBarMenu) { + this.debugToolBarMenu = this.menuService.createMenu(MenuId.DebugToolBar, this.contextKeyService); + this._register(this.debugToolBarMenu); + } + + const { actions, disposable } = DebugToolBar.getActions(this.debugToolBarMenu, this.debugService, this.instantiationService); + if (this.disposeOnTitleUpdate) { + dispose(this.disposeOnTitleUpdate); + } + this.disposeOnTitleUpdate = disposable; + + return actions; } - if (!this.debugToolBarMenu) { - this.debugToolBarMenu = this.menuService.createMenu(MenuId.DebugToolBar, this.contextKeyService); - this._register(this.debugToolBarMenu); + if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { + return [this.toggleReplAction]; } - const { actions, disposable } = DebugToolBar.getActions(this.debugToolBarMenu, this.debugService, this.instantiationService); - if (this.disposeOnTitleUpdate) { - dispose(this.disposeOnTitleUpdate); - } - this.disposeOnTitleUpdate = disposable; - - return actions; + return [this.startAction, this.configureAction, this.toggleReplAction]; } get showInitialDebugActions(): boolean { diff --git a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css index 47fdbe45c60..858d8cbacd7 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css +++ b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css @@ -7,7 +7,7 @@ cursor: pointer; } -.codicon-debug-hint:not(*[class*='codicon-debug-breakpoint']) { +.codicon-debug-hint:not([class*='codicon-debug-breakpoint']) { opacity: .4 !important; } @@ -16,26 +16,10 @@ align-items: center; } -/* overlapped icons */ -.inline-breakpoint-widget.codicon-debug-breakpoint-stackframe-dot::after { - position: absolute; - top: 0; - left: 0; - bottom: 0; - margin: auto; - display: table; -} - -.codicon-debug-breakpoint.codicon-debug-breakpoint-stackframe-focused::after { - position: absolute; -} - -.inline-breakpoint-widget.codicon-debug-breakpoint-stackframe-dot::after { - content: "\eb8b"; -} - -.codicon-debug-breakpoint.codicon-debug-breakpoint-stackframe-focused::after { +.codicon-debug-breakpoint.codicon-debug-breakpoint-stackframe-focused::after, +.codicon-debug-breakpoint.codicon-debug-breakpoint-stackframe::after { content: "\eb8a"; + position: absolute; } .monaco-editor .inline-breakpoint-widget.line-start { diff --git a/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css b/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css index 56d48297c37..70847c6647a 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugToolBar.css @@ -5,7 +5,7 @@ .monaco-workbench .debug-toolbar { position: absolute; - z-index: 200; + z-index: 1000; height: 32px; display: flex; padding-left: 7px; diff --git a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index e40b50a0900..4ccff4262eb 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -374,10 +374,6 @@ justify-content: center; } -.debug-viewlet .debug-breakpoints .breakpoint > .codicon-debug-breakpoint-stackframe-dot::before { - content: "\ea71"; -} - .debug-viewlet .debug-breakpoints .breakpoint > .file-path { opacity: 0.7; font-size: 0.9em; diff --git a/src/vs/workbench/contrib/debug/browser/media/repl.css b/src/vs/workbench/contrib/debug/browser/media/repl.css index 68c1f96bacc..2b38626d95d 100644 --- a/src/vs/workbench/contrib/debug/browser/media/repl.css +++ b/src/vs/workbench/contrib/debug/browser/media/repl.css @@ -30,6 +30,9 @@ cursor: pointer; } +.monaco-workbench.mac .repl .repl-tree .monaco-tl-twistie { + padding-right: 0px; +} .repl .repl-tree .output.expression.value-and-source { display: flex; @@ -39,6 +42,16 @@ flex: 1; } +.repl .repl-tree .monaco-tl-contents .arrow { + position:absolute; + left: 2px; + opacity: 0.25; +} + +.vs-dark .repl .repl-tree .monaco-tl-contents .arrow { + opacity: 0.4; +} + .repl .repl-tree .output.expression.value-and-source .source { margin-left: 4px; margin-right: 8px; diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 4ff31849ccd..935a160bfa0 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -610,6 +610,7 @@ class ReplEvaluationInputsRenderer implements ITreeRenderer { secondMessageElement.textContent = localize('specifyHowToRun', "To futher configure Debug and Run"); - const clickElement = $('span.click'); - clickElement.textContent = localize('configure', " create a launch.json file."); - clickElement.onclick = () => this.commandService.executeCommand(ConfigureAction.ID); + const clickElement = this.createClickElement(localize('configure', " create a launch.json file."), () => this.commandService.executeCommand(ConfigureAction.ID)); this.secondMessageContainer.appendChild(clickElement); }; const setSecondMessageWithFolder = () => { secondMessageElement.textContent = localize('noLaunchConfiguration', "To futher configure Debug and Run, "); - const clickElement = $('span.click'); - clickElement.textContent = localize('openFolder', " open a folder"); - clickElement.onclick = () => this.dialogService.pickFolderAndOpen({ forceNewWindow: false }); + const clickElement = this.createClickElement(localize('openFolder', " open a folder"), () => this.dialogService.pickFolderAndOpen({ forceNewWindow: false })); this.secondMessageContainer.appendChild(clickElement); const moreText = $('span.moreText'); @@ -106,10 +104,7 @@ export class StartView extends ViewletPane { } if (!enabled && emptyWorkbench) { - const clickElement = $('span.click'); - clickElement.textContent = localize('openFile', "Open a file"); - clickElement.onclick = () => this.dialogService.pickFileAndOpen({ forceNewWindow: false }); - + const clickElement = this.createClickElement(localize('openFile', "Open a file"), () => this.dialogService.pickFileAndOpen({ forceNewWindow: false })); this.firstMessageContainer.appendChild(clickElement); const firstMessageElement = $('span'); this.firstMessageContainer.appendChild(firstMessageElement); @@ -121,6 +116,21 @@ export class StartView extends ViewletPane { } } + private createClickElement(textContent: string, action: () => any): HTMLSpanElement { + const clickElement = $('span.click'); + clickElement.textContent = textContent; + clickElement.onclick = action; + clickElement.tabIndex = 0; + clickElement.onkeyup = (e) => { + const keyboardEvent = new StandardKeyboardEvent(e); + if (keyboardEvent.keyCode === KeyCode.Enter || (keyboardEvent.keyCode === KeyCode.Space)) { + action(); + } + }; + + return clickElement; + } + protected renderBody(container: HTMLElement): void { this.firstMessageContainer = $('.top-section'); container.appendChild(this.firstMessageContainer); diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 81012b401ae..fb82b157b50 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -185,7 +185,7 @@ export class VariablesView extends ViewletPane { if (dataid) { actions.push(new Separator()); actions.push(new Action('debug.breakWhenValueChanges', nls.localize('breakWhenValueChanges', "Break When Value Changes"), undefined, true, () => { - return this.debugService.addDataBreakpoint(response.description, dataid, !!response.canPersist); + return this.debugService.addDataBreakpoint(response.description, dataid, !!response.canPersist, response.accessTypes); })); } } diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 092e1364740..88ac5aa6786 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -215,7 +215,7 @@ export interface IDebugSession extends ITreeElement { sendBreakpoints(modelUri: uri, bpts: IBreakpoint[], sourceModified: boolean): Promise; sendFunctionBreakpoints(fbps: IFunctionBreakpoint[]): Promise; - dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null, description: string, canPersist?: boolean }>; + dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null, description: string, canPersist?: boolean, accessTypes?: DebugProtocol.DataBreakpointAccessType[] }>; sendDataBreakpoints(dbps: IDataBreakpoint[]): Promise; sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): Promise; breakpointsLocations(uri: uri, lineNumber: number): Promise; @@ -377,7 +377,7 @@ export interface IExceptionBreakpoint extends IEnablement { } export interface IDataBreakpoint extends IBaseBreakpoint { - readonly label: string; + readonly description: string; readonly dataId: string; readonly canPersist: boolean; } @@ -470,7 +470,7 @@ export interface IDebugConfiguration { focusWindowOnBreak: boolean; onTaskErrors: 'debugAnyway' | 'showErrors' | 'prompt'; showBreakpointsInOverviewRuler: boolean; - inlineBreakpointCandidates: boolean; + showInlineBreakpointCandidates: boolean; } export interface IGlobalConfig { @@ -547,10 +547,8 @@ export interface IDebugAdapterServer { readonly host?: string; } -export interface IDebugAdapterInlineImpl { +export interface IDebugAdapterInlineImpl extends IDisposable { readonly onSendMessage: Event; - readonly onError: Event; - handleMessage(message: DebugProtocol.Message): void; } @@ -797,7 +795,7 @@ export interface IDebugService { /** * Adds a new data breakpoint. */ - addDataBreakpoint(label: string, dataId: string, canPersist: boolean): Promise; + addDataBreakpoint(label: string, dataId: string, canPersist: boolean, accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined): Promise; /** * Removes all data breakpoints. If id is passed only removes the data breakpoint with the passed id. diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 380ee08754b..5ad9aa7d55d 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -157,6 +157,11 @@ export class ExpressionContainer implements IExpressionContainer { this.session = session; try { const response = await session.evaluate(expression, stackFrame ? stackFrame.frameId : undefined, context); + if (response && response.success === false) { + this.value = response.message || ''; + return false; + } + if (response && response.body) { this.value = response.body.result || ''; this.reference = response.body.variablesReference; @@ -743,13 +748,14 @@ export class FunctionBreakpoint extends BaseBreakpoint implements IFunctionBreak export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { constructor( - public label: string, + public description: string, public dataId: string, public canPersist: boolean, enabled: boolean, hitCondition: string | undefined, condition: string | undefined, logMessage: string | undefined, + private accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined, id = generateUuid() ) { super(enabled, hitCondition, condition, logMessage, id); @@ -757,8 +763,9 @@ export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { toJSON(): any { const result = super.toJSON(); - result.label = this.label; - result.dataid = this.dataId; + result.description = this.description; + result.dataId = this.dataId; + result.accessTypes = this.accessTypes; return result; } @@ -772,7 +779,7 @@ export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { } toString(): string { - return this.label; + return this.description; } } @@ -1154,8 +1161,8 @@ export class DebugModel implements IDebugModel { this._onDidChangeBreakpoints.fire({ removed }); } - addDataBreakpoint(label: string, dataId: string, canPersist: boolean): void { - const newDataBreakpoint = new DataBreakpoint(label, dataId, canPersist, true, undefined, undefined, undefined); + addDataBreakpoint(label: string, dataId: string, canPersist: boolean, accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined): void { + const newDataBreakpoint = new DataBreakpoint(label, dataId, canPersist, true, undefined, undefined, undefined, accessTypes); this.dataBreakopints.push(newDataBreakpoint); this._onDidChangeBreakpoints.fire({ added: [newDataBreakpoint] }); } diff --git a/src/vs/workbench/contrib/debug/common/debugUtils.ts b/src/vs/workbench/contrib/debug/common/debugUtils.ts index 38aebf4b083..89c02df8bb7 100644 --- a/src/vs/workbench/contrib/debug/common/debugUtils.ts +++ b/src/vs/workbench/contrib/debug/common/debugUtils.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { equalsIgnoreCase } from 'vs/base/common/strings'; -import { IConfig, IDebuggerContribution, IDebugSession } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebuggerContribution, IDebugSession } from 'vs/workbench/contrib/debug/common/debug'; import { URI as uri } from 'vs/base/common/uri'; import { isAbsolute } from 'vs/base/common/path'; import { deepClone } from 'vs/base/common/objects'; @@ -24,19 +24,28 @@ export function formatPII(value: string, excludePII: boolean, args: { [key: stri } export function isSessionAttach(session: IDebugSession): boolean { - return !session.parentSession && session.configuration.request === 'attach' && !isExtensionHostDebugging(session.configuration); + return !session.parentSession && session.configuration.request === 'attach' && !getExtensionHostDebugSession(session); } -export function isExtensionHostDebugging(config: IConfig) { - if (!config.type) { - return false; +/** + * Returns the session or any parent which is an extension host debug session. + * Returns undefined if there's none. + */ +export function getExtensionHostDebugSession(session: IDebugSession): IDebugSession | void { + let type = session.configuration.type; + if (!type) { + return; } - const type = config.type === 'vslsShare' - ? (config).adapterProxy.configuration.type - : config.type; + if (type === 'vslsShare') { + type = (session.configuration).adapterProxy.configuration.type; + } - return equalsIgnoreCase(type, 'extensionhost') || equalsIgnoreCase(type, 'pwa-extensionhost'); + if (equalsIgnoreCase(type, 'extensionhost') || equalsIgnoreCase(type, 'pwa-extensionhost')) { + return session; + } + + return session.parentSession ? getExtensionHostDebugSession(session.parentSession) : undefined; } // only a debugger contributions with a label, program, or runtime attribute is considered a "defining" or "main" debugger contribution diff --git a/src/vs/workbench/contrib/debug/common/replModel.ts b/src/vs/workbench/contrib/debug/common/replModel.ts index bc0f77b86d2..f9c8ae7c43b 100644 --- a/src/vs/workbench/contrib/debug/common/replModel.ts +++ b/src/vs/workbench/contrib/debug/common/replModel.ts @@ -98,10 +98,23 @@ export class ReplEvaluationInput implements IReplElement { } export class ReplEvaluationResult extends ExpressionContainer implements IReplElement { + private _available = true; + + get available(): boolean { + return this._available; + } + constructor() { super(undefined, undefined, 0, generateUuid()); } + async evaluateExpression(expression: string, session: IDebugSession | undefined, stackFrame: IStackFrame | undefined, context: string): Promise { + const result = await super.evaluateExpression(expression, session, stackFrame, context); + this._available = result; + + return result; + } + toString(): string { return `${this.value}`; } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/media/runtimeExtensionsEditor.css b/src/vs/workbench/contrib/extensions/electron-browser/media/runtimeExtensionsEditor.css index cedd8541829..47a318e4411 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/media/runtimeExtensionsEditor.css +++ b/src/vs/workbench/contrib/extensions/electron-browser/media/runtimeExtensionsEditor.css @@ -22,6 +22,10 @@ font-weight: bold; } +.runtime-extensions-editor .extension .desc .msg .codicon { + vertical-align: middle; +} + .runtime-extensions-editor .extension .time { padding: 4px; text-align: right; diff --git a/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts b/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts index e4a7454fb10..92c391fad15 100644 --- a/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts +++ b/src/vs/workbench/contrib/externalTerminal/browser/externalTerminal.contribution.ts @@ -24,6 +24,7 @@ import { distinct } from 'vs/base/common/arrays'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { optional } from 'vs/platform/instantiation/common/instantiation'; +import { IExplorerService } from 'vs/workbench/contrib/files/common/files'; const OPEN_IN_TERMINAL_COMMAND_ID = 'openInTerminal'; @@ -37,7 +38,7 @@ CommandsRegistry.registerCommand({ const integratedTerminalService = accessor.get(IIntegratedTerminalService); const remoteAgentService = accessor.get(IRemoteAgentService); - const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService); + const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService, accessor.get(IExplorerService)); return fileService.resolveAll(resources.map(r => ({ resource: r }))).then(async stats => { const targets = distinct(stats.filter(data => data.success)); // Always use integrated terminal when using a remote @@ -162,4 +163,3 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { command: openConsoleCommand, when: ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeRemote) }); - diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 8fc60ca17e9..43beeb606fd 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -980,7 +980,12 @@ const downloadFileHandler = (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); const stats = explorerService.getContext(true); + let canceled = false; stats.forEach(async s => { + if (canceled) { + return; + } + if (isWeb) { if (!s.isDirectory) { triggerDownload(asDomUri(s.resource), s.name); @@ -999,6 +1004,9 @@ const downloadFileHandler = (accessor: ServicesAccessor) => { }); if (destination) { await textFileService.copy(s.resource, destination); + } else { + // User canceled a download. In case there were multiple files selected we should cancel the remainder of the prompts #86100 + canceled = true; } } }); diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index f09033db891..3dcc31af8e9 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -119,7 +119,8 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ const editorService = accessor.get(IEditorService); const listService = accessor.get(IListService); const fileService = accessor.get(IFileService); - const resources = getMultiSelectedResources(resource, listService, editorService); + const explorerService = accessor.get(IExplorerService); + const resources = getMultiSelectedResources(resource, listService, editorService, explorerService); // Set side input if (resources.length) { @@ -201,7 +202,8 @@ CommandsRegistry.registerCommand({ id: COMPARE_SELECTED_COMMAND_ID, handler: (accessor, resource: URI | object) => { const editorService = accessor.get(IEditorService); - const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService); + const explorerService = accessor.get(IExplorerService); + const resources = getMultiSelectedResources(resource, accessor.get(IListService), editorService, explorerService); if (resources.length === 2) { return editorService.openEditor({ @@ -251,7 +253,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ }, id: COPY_PATH_COMMAND_ID, handler: async (accessor, resource: URI | object) => { - const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService)); + const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService), accessor.get(IExplorerService)); await resourcesToClipboard(resources, false, accessor.get(IClipboardService), accessor.get(INotificationService), accessor.get(ILabelService)); } }); @@ -265,7 +267,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ }, id: COPY_RELATIVE_PATH_COMMAND_ID, handler: async (accessor, resource: URI | object) => { - const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService)); + const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService), accessor.get(IExplorerService)); await resourcesToClipboard(resources, true, accessor.get(IClipboardService), accessor.get(INotificationService), accessor.get(ILabelService)); } }); @@ -488,7 +490,7 @@ CommandsRegistry.registerCommand({ const workspaceEditingService = accessor.get(IWorkspaceEditingService); const contextService = accessor.get(IWorkspaceContextService); const workspace = contextService.getWorkspace(); - const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService)).filter(r => + const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService), accessor.get(IExplorerService)).filter(r => // Need to verify resources are workspaces since multi selection can trigger this command on some non workspace resources workspace.folders.some(f => isEqual(f.uri, r)) ); diff --git a/src/vs/workbench/contrib/files/browser/files.ts b/src/vs/workbench/contrib/files/browser/files.ts index 7746773311f..c501bd0f486 100644 --- a/src/vs/workbench/contrib/files/browser/files.ts +++ b/src/vs/workbench/contrib/files/browser/files.ts @@ -5,7 +5,7 @@ import { URI } from 'vs/base/common/uri'; import { IListService } from 'vs/platform/list/browser/listService'; -import { OpenEditor } from 'vs/workbench/contrib/files/common/files'; +import { OpenEditor, IExplorerService } from 'vs/workbench/contrib/files/common/files'; import { toResource, SideBySideEditor, IEditorIdentifier } from 'vs/workbench/common/editor'; import { List } from 'vs/base/browser/ui/list/listWidget'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -53,19 +53,15 @@ export function getResourceForCommand(resource: URI | object | undefined, listSe return editorService.activeEditor ? toResource(editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER }) : undefined; } -export function getMultiSelectedResources(resource: URI | object | undefined, listService: IListService, editorService: IEditorService): Array { +export function getMultiSelectedResources(resource: URI | object | undefined, listService: IListService, editorService: IEditorService, explorerService: IExplorerService): Array { const list = listService.lastFocusedList; if (list?.getHTMLElement() === document.activeElement) { // Explorer if (list instanceof AsyncDataTree) { - const selection = list.getSelection().map((fs: ExplorerItem) => fs.resource); - const focusedElements = list.getFocus(); - const focus = focusedElements.length ? focusedElements[0] : undefined; - const mainUriStr = URI.isUri(resource) ? resource.toString() : focus instanceof ExplorerItem ? focus.resource.toString() : undefined; - // If the resource is passed it has to be a part of the returned context. - // We only respect the selection if it contains the focused element. - if (selection.some(s => URI.isUri(s) && s.toString() === mainUriStr)) { - return selection; + // Explorer + const context = explorerService.getContext(true); + if (context.length) { + return context.map(c => c.resource); } } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 5020df769b7..6a59c39f7d5 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -29,7 +29,7 @@ import { DelayedDragHandler } from 'vs/base/browser/dnd'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IViewletPaneOptions, ViewletPane } from 'vs/workbench/browser/parts/views/paneViewlet'; import { ILabelService } from 'vs/platform/label/common/label'; -import { ExplorerDelegate, ExplorerAccessibilityProvider, ExplorerDataSource, FilesRenderer, ICompressedNavigationController, FilesFilter, FileSorter, FileDragAndDrop, ExplorerCompressionDelegate, isCompressedFolderName } from 'vs/workbench/contrib/files/browser/views/explorerViewer'; +import { ExplorerDelegate, ExplorerDataSource, FilesRenderer, ICompressedNavigationController, FilesFilter, FileSorter, FileDragAndDrop, ExplorerCompressionDelegate, isCompressedFolderName } from 'vs/workbench/contrib/files/browser/views/explorerViewer'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; @@ -297,6 +297,13 @@ export class ExplorerView extends ViewletPane { for (const stat of this.tree.getSelection()) { const controller = this.renderer.getCompressedNavigationController(stat); + if (controller && focusedStat && controller === this.compressedNavigationController) { + if (stat === focusedStat) { + selectedStats.push(stat); + } + // Ignore stats which are selected but are part of the same compact node as the focused stat + continue; + } if (controller) { selectedStats.push(...controller.items); @@ -353,7 +360,7 @@ export class ExplorerView extends ViewletPane { this.tree = this.instantiationService.createInstance>(WorkbenchCompressibleAsyncDataTree, 'FileExplorer', container, new ExplorerDelegate(), new ExplorerCompressionDelegate(), [this.renderer], this.instantiationService.createInstance(ExplorerDataSource), { compressionEnabled: isCompressionEnabled(), - accessibilityProvider: new ExplorerAccessibilityProvider(), + accessibilityProvider: this.renderer, ariaLabel: nls.localize('treeAriaLabel', "Files Explorer"), identityProvider: { getId: (stat: ExplorerItem) => { diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 9029835954b..0e95974a599 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -46,7 +46,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces'; import { findValidPasteFileTarget } from 'vs/workbench/contrib/files/browser/fileActions'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter, Event, EventMultiplexer } from 'vs/base/common/event'; import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; @@ -120,10 +120,12 @@ export class ExplorerDataSource implements IAsyncDataSource; previous(): void; next(): void; first(): void; @@ -131,7 +133,9 @@ export interface ICompressedNavigationController { setIndex(index: number): void; } -export class CompressedNavigationController implements ICompressedNavigationController { +export class CompressedNavigationController implements ICompressedNavigationController, IDisposable { + + static ID = 0; private _index: number; readonly labels: HTMLElement[]; @@ -139,10 +143,19 @@ export class CompressedNavigationController implements ICompressedNavigationCont get index(): number { return this._index; } get count(): number { return this.items.length; } get current(): ExplorerItem { return this.items[this._index]!; } + get currentId(): string { return `${this.id}_${this.index}`; } - constructor(readonly items: ExplorerItem[], templateData: IFileTemplateData) { + private _onDidChange = new Emitter(); + readonly onDidChange = this._onDidChange.event; + + constructor(private id: string, readonly items: ExplorerItem[], templateData: IFileTemplateData) { this._index = items.length - 1; this.labels = Array.from(templateData.container.querySelectorAll('.label-name')) as HTMLElement[]; + + for (let i = 0; i < items.length; i++) { + this.labels[i].setAttribute('aria-label', items[i].name); + } + DOM.addClass(this.labels[this._index], 'active'); } @@ -186,6 +199,12 @@ export class CompressedNavigationController implements ICompressedNavigationCont DOM.removeClass(this.labels[this._index], 'active'); this._index = index; DOM.addClass(this.labels[this._index], 'active'); + + this._onDidChange.fire(); + } + + dispose(): void { + this._onDidChange.dispose(); } } @@ -195,13 +214,16 @@ export interface IFileTemplateData { container: HTMLElement; } -export class FilesRenderer implements ICompressibleTreeRenderer, IDisposable { +export class FilesRenderer implements ICompressibleTreeRenderer, IAccessibilityProvider, IDisposable { static readonly ID = 'file'; private config: IFilesConfiguration; private configListener: IDisposable; private compressedNavigationControllers = new Map(); + private _onDidChangeActiveDescendant = new EventMultiplexer(); + readonly onDidChangeActiveDescendant = this._onDidChangeActiveDescendant.event; + constructor( private labels: ResourceLabels, private updateWidth: (stat: ExplorerItem) => void, @@ -240,7 +262,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer e.name); - disposables.add(this.renderStat(stat, label, node.filterData, templateData)); + const id = `compressed-explorer_${CompressedNavigationController.ID++}`; - const compressedNavigationController = new CompressedNavigationController(node.element.elements, templateData); + const label = node.element.elements.map(e => e.name); + disposables.add(this.renderStat(stat, label, id, node.filterData, templateData)); + + const compressedNavigationController = new CompressedNavigationController(id, node.element.elements, templateData); + disposables.add(compressedNavigationController); this.compressedNavigationControllers.set(stat, compressedNavigationController); + // accessibility + disposables.add(this._onDidChangeActiveDescendant.add(compressedNavigationController.onDidChange)); + domEvent(templateData.container, 'mousedown')(e => { const result = getIconLabelNameFromHTMLElement(e.target); @@ -277,9 +305,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer { - this.compressedNavigationControllers.delete(stat); - })); + disposables.add(toDisposable(() => this.compressedNavigationControllers.delete(stat))); templateData.elementDisposable = disposables; } @@ -292,7 +318,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer { @@ -412,15 +439,20 @@ export class FilesRenderer implements ICompressibleTreeRenderer { getAriaLabel(element: ExplorerItem): string { return element.name; } + + getActiveDescendantId(stat: ExplorerItem): string | undefined { + const compressedNavigationController = this.compressedNavigationControllers.get(stat); + return compressedNavigationController?.currentId; + } + + dispose(): void { + this.configListener.dispose(); + } } interface CachedParsedExpression { diff --git a/src/vs/workbench/contrib/files/common/explorerModel.ts b/src/vs/workbench/contrib/files/common/explorerModel.ts index cd74eea7b4e..dd17f19dfb6 100644 --- a/src/vs/workbench/contrib/files/common/explorerModel.ts +++ b/src/vs/workbench/contrib/files/common/explorerModel.ts @@ -197,7 +197,9 @@ export class ExplorerItem { // Properties local.resource = disk.resource; - local.updateName(disk.name); + if (!local.isRoot) { + local.updateName(disk.name); + } local._isDirectory = disk.isDirectory; local._mtime = disk.mtime; local._isDirectoryResolved = disk._isDirectoryResolved; diff --git a/src/vs/workbench/contrib/files/electron-browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/electron-browser/fileActions.contribution.ts index 9c63d009f8d..fc370f6d577 100644 --- a/src/vs/workbench/contrib/files/electron-browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/electron-browser/fileActions.contribution.ts @@ -24,6 +24,7 @@ import { appendToCommandPalette, appendEditorTitleContextMenuItem } from 'vs/wor import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { ShowOpenedFileInNewWindow } from 'vs/workbench/contrib/files/browser/fileActions'; +import { IExplorerService } from 'vs/workbench/contrib/files/common/files'; const REVEAL_IN_OS_COMMAND_ID = 'revealFileInOS'; const REVEAL_IN_OS_LABEL = isWindows ? nls.localize('revealInWindows', "Reveal in Explorer") : isMacintosh ? nls.localize('revealInMac', "Reveal in Finder") : nls.localize('openContainer', "Open Containing Folder"); @@ -37,7 +38,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_R }, handler: (accessor: ServicesAccessor, resource: URI | object) => { - const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService)); + const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService), accessor.get(IExplorerService)); revealResourcesInOS(resources, accessor.get(IElectronService), accessor.get(INotificationService), accessor.get(IWorkspaceContextService)); } }); diff --git a/src/vs/workbench/contrib/logs/common/logs.contribution.ts b/src/vs/workbench/contrib/logs/common/logs.contribution.ts index 7a1a767f67a..09b25d44c91 100644 --- a/src/vs/workbench/contrib/logs/common/logs.contribution.ts +++ b/src/vs/workbench/contrib/logs/common/logs.contribution.ts @@ -12,7 +12,7 @@ import { SetLogLevelAction, OpenWindowSessionLogFileAction } from 'vs/workbench/ import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IFileService, FileChangeType } from 'vs/platform/files/common/files'; +import { IFileService, FileChangeType, whenProviderRegistered } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { IOutputChannelRegistry, Extensions as OutputExt } from 'vs/workbench/contrib/output/common/output'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -45,7 +45,7 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { } private registerCommonContributions(): void { - this.registerLogChannel(Constants.userDataSyncLogChannelId, nls.localize('userDataSyncLog', "Configuration Sync"), this.environmentService.userDataSyncLogResource); + this.registerLogChannel(Constants.userDataSyncLogChannelId, nls.localize('userDataSyncLog', "Sync"), this.environmentService.userDataSyncLogResource); this.registerLogChannel(Constants.rendererLogChannelId, nls.localize('rendererLog', "Window"), this.environmentService.logFile); } @@ -71,6 +71,7 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { } private async registerLogChannel(id: string, label: string, file: URI): Promise { + await whenProviderRegistered(file, this.fileService); const outputChannelRegistry = Registry.as(OutputExt.OutputChannels); const exists = await this.fileService.exists(file); if (exists) { diff --git a/src/vs/workbench/contrib/markers/browser/markersPanel.ts b/src/vs/workbench/contrib/markers/browser/markersPanel.ts index 321d56b444e..0041bade2ba 100644 --- a/src/vs/workbench/contrib/markers/browser/markersPanel.ts +++ b/src/vs/workbench/contrib/markers/browser/markersPanel.ts @@ -37,7 +37,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { Separator, ActionViewItem, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { domEvent } from 'vs/base/browser/event'; import { ResourceLabels } from 'vs/workbench/browser/labels'; import { IMarker } from 'vs/platform/markers/common/markers'; @@ -46,6 +46,7 @@ import { MementoObject } from 'vs/workbench/common/memento'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { PANEL_BACKGROUND } from 'vs/workbench/common/theme'; +import { KeyCode } from 'vs/base/common/keyCodes'; function createResourceMarkersIterator(resourceMarkers: ResourceMarkers): Iterator> { const markersIt = Iterator.fromArray(resourceMarkers.markers); @@ -439,6 +440,7 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { })); this._register(this.tree.onDidChangeSelection(() => this.onSelected())); this._register(this.filterAction.onDidChange((event: IMarkersFilterActionChangeEvent) => { + this.reportFilteringUsed(); if (event.activeFile) { this.refreshPanel(); } else if (event.filterText || event.excludedFiles || event.showWarnings || event.showErrors || event.showInfos) { @@ -520,10 +522,14 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { if (filtered === 0) { this.messageBoxContainer.style.display = 'block'; this.messageBoxContainer.setAttribute('tabIndex', '0'); - if (total > 0) { - this.renderFilteredByFilterMessage(this.messageBoxContainer); + if (this.filterAction.activeFile) { + this.renderFilterMessageForActiveFile(this.messageBoxContainer); } else { - this.renderNoProblemsMessage(this.messageBoxContainer); + if (total > 0) { + this.renderFilteredByFilterMessage(this.messageBoxContainer); + } else { + this.renderNoProblemsMessage(this.messageBoxContainer); + } } } else { this.messageBoxContainer.style.display = 'none'; @@ -536,18 +542,52 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { } } + private renderFilterMessageForActiveFile(container: HTMLElement): void { + if (this.currentActiveResource && this.markersWorkbenchService.markersModel.getResourceMarkers(this.currentActiveResource)) { + this.renderFilteredByFilterMessage(container); + } else { + this.renderNoProblemsMessageForActiveFile(container); + } + } + private renderFilteredByFilterMessage(container: HTMLElement) { const span1 = dom.append(container, dom.$('span')); span1.textContent = Messages.MARKERS_PANEL_NO_PROBLEMS_FILTERS; + const link = dom.append(container, dom.$('a.messageAction')); + link.textContent = localize('clearFilter', "Clear Filters"); + link.setAttribute('tabIndex', '0'); + const span2 = dom.append(container, dom.$('span')); + span2.textContent = '.'; + dom.addStandardDisposableListener(link, dom.EventType.CLICK, () => this.clearFilters()); + dom.addStandardDisposableListener(link, dom.EventType.KEY_DOWN, (e: IKeyboardEvent) => { + if (e.equals(KeyCode.Enter) || e.equals(KeyCode.Space)) { + this.clearFilters(); + e.stopPropagation(); + } + }); this.ariaLabelElement.setAttribute('aria-label', Messages.MARKERS_PANEL_NO_PROBLEMS_FILTERS); } + private renderNoProblemsMessageForActiveFile(container: HTMLElement) { + const span = dom.append(container, dom.$('span')); + span.textContent = Messages.MARKERS_PANEL_NO_PROBLEMS_ACTIVE_FILE_BUILT; + this.ariaLabelElement.setAttribute('aria-label', Messages.MARKERS_PANEL_NO_PROBLEMS_ACTIVE_FILE_BUILT); + } + private renderNoProblemsMessage(container: HTMLElement) { const span = dom.append(container, dom.$('span')); span.textContent = Messages.MARKERS_PANEL_NO_PROBLEMS_BUILT; this.ariaLabelElement.setAttribute('aria-label', Messages.MARKERS_PANEL_NO_PROBLEMS_BUILT); } + private clearFilters(): void { + this.filterAction.filterText = ''; + this.filterAction.excludedFiles = false; + this.filterAction.showErrors = true; + this.filterAction.showWarnings = true; + this.filterAction.showInfos = true; + } + private autoReveal(focus: boolean = false): void { // No need to auto reveal if active file filter is on if (this.filterAction.activeFile) { @@ -714,6 +754,26 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { return { source, code }; } + private reportFilteringUsed(): void { + const data = { + errors: this.filterAction.showErrors, + warnings: this.filterAction.showWarnings, + infos: this.filterAction.showInfos, + activeFile: this.filterAction.activeFile, + excludedFiles: this.filterAction.excludedFiles, + }; + /* __GDPR__ + "problems.filter" : { + "errors" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "warnings": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "infos": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "activeFile": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "excludedFiles": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } + } + */ + this.telemetryService.publicLog('problems.filter', data); + } + protected saveState(): void { this.panelState['filter'] = this.filterAction.filterText; this.panelState['filterHistory'] = this.filterAction.filterHistory; diff --git a/src/vs/workbench/contrib/markers/browser/markersPanelActions.ts b/src/vs/workbench/contrib/markers/browser/markersPanelActions.ts index 47dca87ec67..39719499d5d 100644 --- a/src/vs/workbench/contrib/markers/browser/markersPanelActions.ts +++ b/src/vs/workbench/contrib/markers/browser/markersPanelActions.ts @@ -27,7 +27,6 @@ import { Marker } from 'vs/workbench/contrib/markers/browser/markersModel'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Event, Emitter } from 'vs/base/common/event'; import { FilterOptions } from 'vs/workbench/contrib/markers/browser/markersFilterOptions'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdown'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; @@ -285,7 +284,6 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextViewService private readonly contextViewService: IContextViewService, @IThemeService private readonly themeService: IThemeService, - @ITelemetryService private readonly telemetryService: ITelemetryService, @IContextKeyService contextKeyService: IContextKeyService ) { super(null, action); @@ -385,7 +383,6 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { inputbox.addToHistory(); this.action.filterText = inputbox.value; this.action.filterHistory = inputbox.getHistory(); - this.reportFilteringUsed(); } private updateBadge(): void { @@ -426,23 +423,6 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { } } - private reportFilteringUsed(): void { - const filterOptions = this.filterController.getFilterOptions(); - const data = { - errors: filterOptions.showErrors, - warnings: filterOptions.showWarnings, - infos: filterOptions.showInfos, - }; - /* __GDPR__ - "problems.filter" : { - "errors" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "warnings": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "infos": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } - } - */ - this.telemetryService.publicLog('problems.filter', data); - } - protected updateClass(): void { if (this.element && this.container) { this.element.className = this.action.class || ''; diff --git a/src/vs/workbench/contrib/markers/browser/messages.ts b/src/vs/workbench/contrib/markers/browser/messages.ts index 438ebcd5588..7275dc27c43 100644 --- a/src/vs/workbench/contrib/markers/browser/messages.ts +++ b/src/vs/workbench/contrib/markers/browser/messages.ts @@ -21,6 +21,7 @@ export default class Messages { public static MARKERS_PANEL_TITLE_PROBLEMS: string = nls.localize('markers.panel.title.problems', "Problems"); public static MARKERS_PANEL_NO_PROBLEMS_BUILT: string = nls.localize('markers.panel.no.problems.build', "No problems have been detected in the workspace so far."); + public static MARKERS_PANEL_NO_PROBLEMS_ACTIVE_FILE_BUILT: string = nls.localize('markers.panel.no.problems.activeFile.build', "No problems have been detected in the current file so far."); public static MARKERS_PANEL_NO_PROBLEMS_FILTERS: string = nls.localize('markers.panel.no.problems.filters', "No results found with provided filter criteria."); public static MARKERS_PANEL_ACTION_TOOLTIP_MORE_FILTERS: string = nls.localize('markers.panel.action.moreFilters', "More Filters..."); diff --git a/src/vs/workbench/contrib/outline/browser/outline.contribution.ts b/src/vs/workbench/contrib/outline/browser/outline.contribution.ts index 0affdee3013..33ab1f4a348 100644 --- a/src/vs/workbench/contrib/outline/browser/outline.contribution.ts +++ b/src/vs/workbench/contrib/outline/browser/outline.contribution.ts @@ -11,7 +11,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { OutlineConfigKeys, OutlineViewId } from 'vs/editor/contrib/documentSymbols/outline'; -import './outlineNavigation'; +// import './outlineNavigation'; const _outlineDesc = { id: OutlineViewId, @@ -55,92 +55,110 @@ Registry.as(ConfigurationExtensions.Configuration).regis }, 'outline.showFiles': { type: 'boolean', + overridable: true, default: true, markdownDescription: localize('filteredTypes.file', "When enabled outline shows `file`-symbols.") }, 'outline.showModules': { type: 'boolean', + overridable: true, default: true, markdownDescription: localize('filteredTypes.module', "When enabled outline shows `module`-symbols.") }, 'outline.showNamespaces': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.namespace', "When enabled outline shows `namespace`-symbols.") }, 'outline.showPackages': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.package', "When enabled outline shows `package`-symbols.") }, 'outline.showClasses': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.class', "When enabled outline shows `class`-symbols.") }, 'outline.showMethods': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.method', "When enabled outline shows `method`-symbols.") }, 'outline.showProperties': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.property', "When enabled outline shows `property`-symbols.") }, 'outline.showFields': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.field', "When enabled outline shows `field`-symbols.") }, 'outline.showConstructors': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.constructor', "When enabled outline shows `constructor`-symbols.") }, 'outline.showEnums': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.enum', "When enabled outline shows `enum`-symbols.") }, 'outline.showInterfaces': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.interface', "When enabled outline shows `interface`-symbols.") }, 'outline.showFunctions': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.function', "When enabled outline shows `function`-symbols.") }, 'outline.showVariables': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.variable', "When enabled outline shows `variable`-symbols.") }, 'outline.showConstants': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.constant', "When enabled outline shows `constant`-symbols.") }, 'outline.showStrings': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.string', "When enabled outline shows `string`-symbols.") }, 'outline.showNumbers': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.number', "When enabled outline shows `number`-symbols.") }, 'outline.showBooleans': { type: 'boolean', + overridable: true, default: true, markdownDescription: localize('filteredTypes.boolean', "When enabled outline shows `boolean`-symbols.") }, 'outline.showArrays': { type: 'boolean', default: true, + overridable: true, markdownDescription: localize('filteredTypes.array', "When enabled outline shows `array`-symbols.") }, 'outline.showObjects': { diff --git a/src/vs/workbench/contrib/outline/browser/outlineNavigation.ts b/src/vs/workbench/contrib/outline/browser/outlineNavigation.ts index 9b6e7c96e1f..c67f900e69e 100644 --- a/src/vs/workbench/contrib/outline/browser/outlineNavigation.ts +++ b/src/vs/workbench/contrib/outline/browser/outlineNavigation.ts @@ -17,9 +17,9 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { forEach } from 'vs/base/common/collections'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { OutlineFilter } from 'vs/editor/contrib/documentSymbols/outlineTree'; import { binarySearch } from 'vs/base/common/arrays'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; class FlatOutline { @@ -78,7 +78,7 @@ export class OutlineNavigation implements IEditorContribution { constructor( editor: ICodeEditor, - @IConfigurationService private readonly _configService: IConfigurationService, + @ITextResourceConfigurationService private readonly _textResourceConfigService: ITextResourceConfigurationService, ) { this._editor = editor; } @@ -104,7 +104,7 @@ export class OutlineNavigation implements IEditorContribution { this._cts = new EditorStateCancellationTokenSource(this._editor, CodeEditorStateFlag.Position | CodeEditorStateFlag.Value | CodeEditorStateFlag.Scroll); - const filter = new OutlineFilter('outline', this._configService); + const filter = new OutlineFilter('outline', this._textResourceConfigService); const outlineModel = await OutlineModel.create(textModel, this._cts.token); if (this._cts.token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/outline/browser/outlinePane.ts b/src/vs/workbench/contrib/outline/browser/outlinePane.ts index 068c3471ed5..9aa98e2743c 100644 --- a/src/vs/workbench/contrib/outline/browser/outlinePane.ts +++ b/src/vs/workbench/contrib/outline/browser/outlinePane.ts @@ -372,8 +372,9 @@ export class OutlinePane extends ViewletPane { if (e.affectsConfiguration(OutlineConfigKeys.icons)) { this._tree.updateChildren(); } - if (e.affectsConfiguration('outline')) { - this._treeFilter.update(); + // This is a temporary solution to try and minimize refilters while + // ConfigurationChangeEvents only provide the first section of the config path. + if (e.affectedKeys.some(key => key.search(/(outline|\[\w+\])/) === 0)) { this._tree.refilter(); } })); diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index 894f925ba71..ec040450cce 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -346,6 +346,11 @@ margin-top: 3px; user-select: text; -webkit-user-select: text; + display: none; +} + +.settings-editor > .settings-body > .settings-tree-container .setting-item-contents.is-deprecated .setting-item-deprecation-message { + display: block; } .settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-description { @@ -354,10 +359,6 @@ -webkit-user-select: text; } -.settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-deprecation-message { - position: absolute; -} - .settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-validation-message { display: none; } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index d52e1403ed8..f953fb205e6 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -467,7 +467,10 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre } const onChange = (value: any) => this._onDidChangeSetting.fire({ key: element.setting.key, value, type: template.context!.valueType }); - template.deprecationWarningElement.innerText = element.setting.deprecationMessage || ''; + const deprecationText = element.setting.deprecationMessage || ''; + template.deprecationWarningElement.innerText = deprecationText; + DOM.toggleClass(template.containerElement, 'is-deprecated', !!deprecationText); + this.renderValue(element, template, onChange); } diff --git a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts index a152b50423d..4ef39c8a040 100644 --- a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts +++ b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts @@ -3,23 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IWorkbenchContributionsRegistry, IWorkbenchContribution, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { isMacintosh, isNative } from 'vs/base/common/platform'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { IProductService } from 'vs/platform/product/common/productService'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWindowsConfiguration } from 'vs/platform/windows/common/windows'; +import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { localize } from 'vs/nls'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { RunOnceScheduler } from 'vs/base/common/async'; -import { URI } from 'vs/base/common/uri'; -import { isEqual } from 'vs/base/common/resources'; -import { isMacintosh, isNative } from 'vs/base/common/platform'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IProductService } from 'vs/platform/product/common/productService'; interface IConfiguration extends IWindowsConfiguration { update: { mode: string; }; @@ -132,82 +126,5 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo } } -export class WorkspaceChangeExtHostRelauncher extends Disposable implements IWorkbenchContribution { - - private firstFolderResource?: URI; - private extensionHostRestarter: RunOnceScheduler; - - private onDidChangeWorkspaceFoldersUnbind: IDisposable | undefined; - - constructor( - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IExtensionService extensionService: IExtensionService, - @IHostService hostService: IHostService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService - ) { - super(); - - this.extensionHostRestarter = this._register(new RunOnceScheduler(() => { - if (!!environmentService.extensionTestsLocationURI) { - return; // no restart when in tests: see https://github.com/Microsoft/vscode/issues/66936 - } - - if (environmentService.configuration.remoteAuthority) { - hostService.reload(); // TODO@aeschli, workaround - } else if (isNative) { - extensionService.restartExtensionHost(); - } - }, 10)); - - this.contextService.getCompleteWorkspace() - .then(workspace => { - this.firstFolderResource = workspace.folders.length > 0 ? workspace.folders[0].uri : undefined; - this.handleWorkbenchState(); - this._register(this.contextService.onDidChangeWorkbenchState(() => setTimeout(() => this.handleWorkbenchState()))); - }); - - this._register(toDisposable(() => { - if (this.onDidChangeWorkspaceFoldersUnbind) { - this.onDidChangeWorkspaceFoldersUnbind.dispose(); - } - })); - } - - private handleWorkbenchState(): void { - - // React to folder changes when we are in workspace state - if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { - - // Update our known first folder path if we entered workspace - const workspace = this.contextService.getWorkspace(); - this.firstFolderResource = workspace.folders.length > 0 ? workspace.folders[0].uri : undefined; - - // Install workspace folder listener - if (!this.onDidChangeWorkspaceFoldersUnbind) { - this.onDidChangeWorkspaceFoldersUnbind = this.contextService.onDidChangeWorkspaceFolders(() => this.onDidChangeWorkspaceFolders()); - } - } - - // Ignore the workspace folder changes in EMPTY or FOLDER state - else { - dispose(this.onDidChangeWorkspaceFoldersUnbind); - this.onDidChangeWorkspaceFoldersUnbind = undefined; - } - } - - private onDidChangeWorkspaceFolders(): void { - const workspace = this.contextService.getWorkspace(); - - // Restart extension host if first root folder changed (impact on deprecated workspace.rootPath API) - const newFirstFolderResource = workspace.folders.length > 0 ? workspace.folders[0].uri : undefined; - if (!isEqual(this.firstFolderResource, newFirstFolderResource)) { - this.firstFolderResource = newFirstFolderResource; - - this.extensionHostRestarter.schedule(); // buffer calls to extension host restart - } - } -} - const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(SettingsChangeRelauncher, LifecyclePhase.Restored); -workbenchRegistry.registerWorkbenchContribution(WorkspaceChangeExtHostRelauncher, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts b/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts index 3cd6c869fca..4026be069da 100644 --- a/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts +++ b/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts @@ -20,6 +20,7 @@ import { startsWith } from 'vs/base/common/strings'; import { isStringArray } from 'vs/base/common/types'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; export interface IRemoteSelectItem extends ISelectOptionItem { authority: string[]; @@ -85,10 +86,10 @@ export class SwitchRemoteViewItem extends SelectActionViewItem { return this.optionsItems[index]; } - static createOptionItems(views: IViewDescriptor[]): IRemoteSelectItem[] { + static createOptionItems(views: IViewDescriptor[], contextKeyService: IContextKeyService): IRemoteSelectItem[] { let options: IRemoteSelectItem[] = []; views.forEach(view => { - if (view.group && startsWith(view.group, 'targets') && view.remoteAuthority) { + if (view.group && startsWith(view.group, 'targets') && view.remoteAuthority && (!view.when || contextKeyService.contextMatchesRules(view.when))) { options.push({ text: view.name, authority: isStringArray(view.remoteAuthority) ? view.remoteAuthority : [view.remoteAuthority] }); } }); diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index 2d7afa657bf..f8995407609 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -72,7 +72,8 @@ class HelpModel { })), quickInputService, environmentService, - openerService + openerService, + remoteExplorerService )); } @@ -89,7 +90,8 @@ class HelpModel { })), quickInputService, environmentService, - openerService + openerService, + remoteExplorerService )); } @@ -106,7 +108,8 @@ class HelpModel { })), quickInputService, environmentService, - openerService + openerService, + remoteExplorerService )); } @@ -123,7 +126,8 @@ class HelpModel { })), quickInputService, environmentService, - openerService + openerService, + remoteExplorerService )); } @@ -137,7 +141,8 @@ class HelpModel { })), quickInputService, environmentService, - commandService + commandService, + remoteExplorerService )); } @@ -158,14 +163,15 @@ abstract class HelpItemBase implements IHelpItem { public label: string, public values: { extensionDescription: IExtensionDescription, url?: string, remoteAuthority: string[] | undefined }[], private quickInputService: IQuickInputService, - private environmentService: IWorkbenchEnvironmentService + private environmentService: IWorkbenchEnvironmentService, + private remoteExplorerService: IRemoteExplorerService ) { iconClasses.push('remote-help-tree-node-item-icon'); } async handleClick() { const remoteAuthority = this.environmentService.configuration.remoteAuthority; - if (remoteAuthority) { + if (remoteAuthority && startsWith(remoteAuthority, this.remoteExplorerService.targetType)) { for (let value of this.values) { if (value.remoteAuthority) { for (let authority of value.remoteAuthority) { @@ -207,9 +213,10 @@ class HelpItem extends HelpItemBase { values: { extensionDescription: IExtensionDescription; url: string, remoteAuthority: string[] | undefined }[], quickInputService: IQuickInputService, environmentService: IWorkbenchEnvironmentService, - private openerService: IOpenerService + private openerService: IOpenerService, + remoteExplorerService: IRemoteExplorerService ) { - super(iconClasses, label, values, quickInputService, environmentService); + super(iconClasses, label, values, quickInputService, environmentService, remoteExplorerService); } protected async takeAction(extensionDescription: IExtensionDescription, url: string): Promise { @@ -225,8 +232,9 @@ class IssueReporterItem extends HelpItemBase { quickInputService: IQuickInputService, environmentService: IWorkbenchEnvironmentService, private commandService: ICommandService, + remoteExplorerService: IRemoteExplorerService ) { - super(iconClasses, label, values, quickInputService, environmentService); + super(iconClasses, label, values, quickInputService, environmentService, remoteExplorerService); } protected async takeAction(extensionDescription: IExtensionDescription): Promise { @@ -236,7 +244,7 @@ class IssueReporterItem extends HelpItemBase { class HelpAction extends Action { static readonly ID = 'remote.explorer.help'; - static readonly LABEL = nls.localize('remote.explorer.help', "Help and Feedback"); + static readonly LABEL = nls.localize('remote.explorer.help', "Help, Documentation, and Feedback"); private helpModel: HelpModel; constructor(id: string, @@ -280,6 +288,7 @@ export class RemoteViewlet extends FilterViewContainerViewlet { @IExtensionService extensionService: IExtensionService, @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IContextKeyService private readonly contextKeyService: IContextKeyService ) { super(VIEWLET_ID, remoteExplorerService.onDidChangeTargetType, configurationService, layoutService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); } @@ -290,7 +299,7 @@ export class RemoteViewlet extends FilterViewContainerViewlet { public getActionViewItem(action: Action): IActionViewItem | undefined { if (action.id === SwitchRemoteAction.ID) { - return this.instantiationService.createInstance(SwitchRemoteViewItem, action, SwitchRemoteViewItem.createOptionItems(Registry.as(Extensions.ViewsRegistry).getViews(VIEW_CONTAINER))); + return this.instantiationService.createInstance(SwitchRemoteViewItem, action, SwitchRemoteViewItem.createOptionItems(Registry.as(Extensions.ViewsRegistry).getViews(VIEW_CONTAINER), this.contextKeyService)); } return super.getActionViewItem(action); diff --git a/src/vs/workbench/contrib/remote/browser/remoteViewlet.css b/src/vs/workbench/contrib/remote/browser/remoteViewlet.css index 39b5cdec500..c8f0d9f2964 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteViewlet.css +++ b/src/vs/workbench/contrib/remote/browser/remoteViewlet.css @@ -29,11 +29,15 @@ margin-top: 7px; } +.monaco-workbench.mac .part > .title > .title-actions .switch-remote { + border-radius: 4px; +} + .switch-remote > .monaco-select-box { border: none; display: block; } .monaco-workbench .part > .title > .title-actions .switch-remote > .monaco-select-box { - padding-left: 3px; + padding: 0 22px 0 6px; } diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 8d01b06b61b..8e194bd815f 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -29,7 +29,6 @@ import { IMenuService, MenuId, IMenu, MenuRegistry, MenuItemAction } from 'vs/pl import { createAndFillInContextMenuActions, createAndFillInActionBarActions, ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IRemoteExplorerService, TunnelModel } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { URI } from 'vs/workbench/workbench.web.api'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; @@ -37,6 +36,7 @@ import { once } from 'vs/base/common/functional'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { URI } from 'vs/base/common/uri'; class TunnelTreeVirtualDelegate implements IListVirtualDelegate { getHeight(element: ITunnelItem): number { @@ -103,13 +103,13 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { get forwarded(): TunnelItem[] { return Array.from(this.model.forwarded.values()).map(tunnel => { - return new TunnelItem(TunnelType.Forwarded, tunnel.remote, tunnel.closeable, tunnel.name, tunnel.description, tunnel.local); + return new TunnelItem(TunnelType.Forwarded, tunnel.remote, tunnel.localAddress, tunnel.closeable, tunnel.name, tunnel.description); }); } get published(): TunnelItem[] { return Array.from(this.model.published.values()).map(tunnel => { - return new TunnelItem(TunnelType.Published, tunnel.remote, false, tunnel.name, tunnel.description, tunnel.local); + return new TunnelItem(TunnelType.Published, tunnel.remote, tunnel.localAddress, false, tunnel.name, tunnel.description); }); } @@ -119,7 +119,7 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { let iterator = values.next(); while (!iterator.done) { if (!this.model.forwarded.has(iterator.value.remote) && !this.model.published.has(iterator.value.remote)) { - candidates.push(new TunnelItem(TunnelType.Candidate, iterator.value.remote, false, undefined, iterator.value.description)); + candidates.push(new TunnelItem(TunnelType.Candidate, iterator.value.remote, iterator.value.localAddress, false, undefined, iterator.value.description)); } iterator = values.next(); } @@ -193,17 +193,21 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer { @@ -576,7 +575,7 @@ namespace NameTunnelAction { remoteExplorerService.setEditable(arg.remote, null); }, validationMessage: () => null, - placeholder: nls.localize('remote.tunnelsView.namePlaceholder', "Name port"), + placeholder: nls.localize('remote.tunnelsView.namePlaceholder', "Port name"), startingValue: arg.name }); } @@ -591,31 +590,27 @@ namespace ForwardPortAction { export function handler(): ICommandHandler { return async (accessor, arg) => { - const quickInputService = accessor.get(IQuickInputService); const remoteExplorerService = accessor.get(IRemoteExplorerService); - let remote: number | undefined = undefined; if (arg instanceof TunnelItem) { - remote = arg.remote; + remoteExplorerService.tunnelModel.forward(arg.remote); } else { - const input = parseInt(await quickInputService.input({ placeHolder: nls.localize('remote.tunnelView.pickRemote', 'Remote port to forward') })); - if (typeof input === 'number') { - remote = input; - } + remoteExplorerService.setEditable(undefined, { + onFinish: (value, success) => { + if (success) { + remoteExplorerService.tunnelModel.forward(Number(value)); + } + remoteExplorerService.setEditable(undefined, null); + }, + validationMessage: (value) => { + const asNumber = Number(value); + if (isNaN(asNumber) || (asNumber < 0) || (asNumber > 65535)) { + return nls.localize('remote.tunnelsView.portNumberValid', "Port number is invalid"); + } + return null; + }, + placeholder: nls.localize('remote.tunnelsView.forwardPortPlaceholder', "Port number") + }); } - - if (!remote) { - return; - } - - const local: string = await quickInputService.input({ placeHolder: nls.localize('remote.tunnelView.pickLocal', 'Local port to forward to, or leave blank for {0}', remote) }); - if (local === undefined) { - return; - } - const name = await quickInputService.input({ placeHolder: nls.localize('remote.tunnelView.pickName', 'Port name, or leave blank for no name') }); - if (name === undefined) { - return; - } - await remoteExplorerService.tunnelModel.forward(remote, (local !== '') ? parseInt(local) : remote, (name !== '') ? name : undefined); }; } } @@ -644,9 +639,9 @@ namespace OpenPortInBrowserAction { const model = accessor.get(IRemoteExplorerService).tunnelModel; const openerService = accessor.get(IOpenerService); const tunnel = model.forwarded.has(arg.remote) ? model.forwarded.get(arg.remote) : model.published.get(arg.remote); - let address: URI | undefined; - if (tunnel && tunnel.localUri && (address = model.address(tunnel.remote))) { - return openerService.open(address); + let address: string | undefined; + if (tunnel && tunnel.localAddress && (address = model.address(tunnel.remote))) { + return openerService.open(URI.parse('http://' + address)); } return Promise.resolve(); } diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index a3cac4fbe01..4da76571bbd 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -809,37 +809,37 @@ export class DirtyDiffController extends Disposable implements IEditorContributi export const editorGutterModifiedBackground = registerColor('editorGutter.modifiedBackground', { dark: new Color(new RGBA(12, 125, 157)), light: new Color(new RGBA(102, 175, 224)), - hc: new Color(new RGBA(0, 73, 122)) + hc: new Color(new RGBA(0, 155, 249)) }, nls.localize('editorGutterModifiedBackground', "Editor gutter background color for lines that are modified.")); export const editorGutterAddedBackground = registerColor('editorGutter.addedBackground', { dark: new Color(new RGBA(88, 124, 12)), light: new Color(new RGBA(129, 184, 139)), - hc: new Color(new RGBA(27, 82, 37)) + hc: new Color(new RGBA(51, 171, 78)) }, nls.localize('editorGutterAddedBackground', "Editor gutter background color for lines that are added.")); export const editorGutterDeletedBackground = registerColor('editorGutter.deletedBackground', { dark: new Color(new RGBA(148, 21, 27)), light: new Color(new RGBA(202, 75, 81)), - hc: new Color(new RGBA(141, 14, 20)) + hc: new Color(new RGBA(252, 93, 109)) }, nls.localize('editorGutterDeletedBackground', "Editor gutter background color for lines that are deleted.")); export const minimapGutterModifiedBackground = registerColor('minimapGutter.modifiedBackground', { dark: new Color(new RGBA(12, 125, 157)), light: new Color(new RGBA(102, 175, 224)), - hc: new Color(new RGBA(0, 73, 122)) + hc: new Color(new RGBA(0, 155, 249)) }, nls.localize('minimapGutterModifiedBackground', "Minimap gutter background color for lines that are modified.")); export const minimapGutterAddedBackground = registerColor('minimapGutter.addedBackground', { dark: new Color(new RGBA(88, 124, 12)), light: new Color(new RGBA(129, 184, 139)), - hc: new Color(new RGBA(27, 82, 37)) + hc: new Color(new RGBA(51, 171, 78)) }, nls.localize('minimapGutterAddedBackground', "Minimap gutter background color for lines that are added.")); export const minimapGutterDeletedBackground = registerColor('minimapGutter.deletedBackground', { dark: new Color(new RGBA(148, 21, 27)), light: new Color(new RGBA(202, 75, 81)), - hc: new Color(new RGBA(141, 14, 20)) + hc: new Color(new RGBA(252, 93, 109)) }, nls.localize('minimapGutterDeletedBackground', "Minimap gutter background color for lines that are deleted.")); const overviewRulerDefault = new Color(new RGBA(0, 122, 204, 0.6)); diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index 0c40988389f..0021ac28e7d 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -81,6 +81,13 @@ Registry.as(ConfigurationExtensions.Configuration).regis 'scm.diffDecorations': { type: 'string', enum: ['all', 'gutter', 'overview', 'minimap', 'none'], + enumDescriptions: [ + localize('scm.diffDecorations.all', "Show the diff decorations in all available locations."), + localize('scm.diffDecorations.gutter', "Show the diff decorations only in the editor gutter."), + localize('scm.diffDecorations.overviewRuler', "Show the diff decorations only in the overview ruler."), + localize('scm.diffDecorations.minimap', "Show the diff decorations only in the minimap."), + localize('scm.diffDecorations.none', "Do not show the diff decorations.") + ], default: 'all', description: localize('diffDecorations', "Controls diff decorations in the editor.") }, diff --git a/src/vs/workbench/contrib/search/browser/media/searchview.css b/src/vs/workbench/contrib/search/browser/media/searchview.css index 524b571529c..36fb83e2602 100644 --- a/src/vs/workbench/contrib/search/browser/media/searchview.css +++ b/src/vs/workbench/contrib/search/browser/media/searchview.css @@ -302,10 +302,6 @@ background-color: rgba(255, 255, 255, 0.1) !important; } -.vs-dark .search-view .message { - opacity: .5; -} - .vs-dark .search-view .foldermatch, .vs-dark .search-view .filematch { padding: 0; diff --git a/src/vs/workbench/contrib/search/browser/patternInputWidget.ts b/src/vs/workbench/contrib/search/browser/patternInputWidget.ts index 0d41239f527..b5502c58cf9 100644 --- a/src/vs/workbench/contrib/search/browser/patternInputWidget.ts +++ b/src/vs/workbench/contrib/search/browser/patternInputWidget.ts @@ -149,12 +149,6 @@ export class PatternInputWidget extends Widget { this._register(attachInputBoxStyler(this.inputBox, this.themeService)); this.inputFocusTracker = dom.trackFocus(this.inputBox.inputElement); this.onkeyup(this.inputBox.inputElement, (keyboardEvent) => this.onInputKeyUp(keyboardEvent)); - this._register(this.inputBox.onDidChange(() => { - if (this.searchConfig.searchOnType) { - this._onCancel.fire(); - this.searchOnTypeDelayer.trigger(() => this._onSubmit.fire(true), this.searchConfig.searchOnTypeDebouncePeriod); - } - })); const controls = document.createElement('div'); controls.className = 'controls'; @@ -176,6 +170,10 @@ export class PatternInputWidget extends Widget { this._onCancel.fire(); return; default: + if (this.searchConfig.searchOnType) { + this._onCancel.fire(); + this.searchOnTypeDelayer.trigger(() => this._onSubmit.fire(true), this.searchConfig.searchOnTypeDebouncePeriod); + } return; } } diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index fea95600589..98a487a1695 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -41,7 +41,7 @@ import { ExplorerFolderContext, ExplorerRootContext, FilesExplorerFocusCondition import { OpenAnythingHandler } from 'vs/workbench/contrib/search/browser/openAnythingHandler'; import { OpenSymbolHandler } from 'vs/workbench/contrib/search/browser/openSymbolHandler'; import { registerContributions as replaceContributions } from 'vs/workbench/contrib/search/browser/replaceContributions'; -import { clearHistoryCommand, ClearSearchResultsAction, CloseReplaceAction, CollapseDeepestExpandedLevelAction, copyAllCommand, copyMatchCommand, copyPathCommand, FocusNextInputAction, FocusNextSearchResultAction, FocusPreviousInputAction, FocusPreviousSearchResultAction, focusSearchListCommand, getSearchView, openSearchView, OpenSearchViewletAction, RefreshAction, RemoveAction, ReplaceAction, ReplaceAllAction, ReplaceAllInFolderAction, ReplaceInFilesAction, toggleCaseSensitiveCommand, toggleRegexCommand, toggleWholeWordCommand, FindInFilesCommand, ToggleSearchOnTypeAction, OpenResultsInEditorAction, RerunEditorSearchAction } from 'vs/workbench/contrib/search/browser/searchActions'; +import { clearHistoryCommand, ClearSearchResultsAction, CloseReplaceAction, CollapseDeepestExpandedLevelAction, copyAllCommand, copyMatchCommand, copyPathCommand, FocusNextInputAction, FocusNextSearchResultAction, FocusPreviousInputAction, FocusPreviousSearchResultAction, focusSearchListCommand, getSearchView, openSearchView, OpenSearchViewletAction, RefreshAction, RemoveAction, ReplaceAction, ReplaceAllAction, ReplaceAllInFolderAction, ReplaceInFilesAction, toggleCaseSensitiveCommand, toggleRegexCommand, toggleWholeWordCommand, FindInFilesCommand, ToggleSearchOnTypeAction, OpenResultsInEditorAction, RerunEditorSearchAction, RerunEditorSearchWithContextAction } from 'vs/workbench/contrib/search/browser/searchActions'; import { SearchPanel } from 'vs/workbench/contrib/search/browser/searchPanel'; import { SearchView, SearchViewPosition } from 'vs/workbench/contrib/search/browser/searchView'; import { SearchViewlet } from 'vs/workbench/contrib/search/browser/searchViewlet'; @@ -396,7 +396,7 @@ const searchInFolderCommand: ICommandHandler = (accessor, resource?: URI) => { const panelService = accessor.get(IPanelService); const fileService = accessor.get(IFileService); const configurationService = accessor.get(IConfigurationService); - const resources = getMultiSelectedResources(resource, listService, accessor.get(IEditorService)); + const resources = getMultiSelectedResources(resource, listService, accessor.get(IEditorService), accessor.get(IExplorerService)); return openSearchView(viewletService, panelService, configurationService, true).then(searchView => { if (resources && resources.length && searchView) { @@ -651,6 +651,11 @@ registry.registerWorkbenchAction( 'Search Editor: Search Again', category, ContextKeyExpr.and(EditorContextKeys.languageId.isEqualTo('search-result'))); +registry.registerWorkbenchAction( + SyncActionDescriptor.create(RerunEditorSearchWithContextAction, RerunEditorSearchWithContextAction.ID, RerunEditorSearchWithContextAction.LABEL), + 'Search Editor: Search Again (With Context)', category, + ContextKeyExpr.and(EditorContextKeys.languageId.isEqualTo('search-result'))); + // Register Quick Open Handler Registry.as(QuickOpenExtensions.Quickopen).registerDefaultQuickOpenHandler( @@ -690,7 +695,7 @@ configurationRegistry.registerConfiguration({ 'search.exclude': { type: 'object', markdownDescription: nls.localize('exclude', "Configure glob patterns for excluding files and folders in searches. Inherits all glob patterns from the `#files.exclude#` setting. Read more about glob patterns [here](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options)."), - default: { '**/node_modules': true, '**/bower_components': true }, + default: { '**/node_modules': true, '**/bower_components': true, '**/*.code-search': true }, additionalProperties: { anyOf: [ { @@ -813,6 +818,11 @@ configurationRegistry.registerConfiguration({ type: 'number', default: 300, markdownDescription: nls.localize('search.searchOnTypeDebouncePeriod', "When `#search.searchOnType#` is enabled, controls the timeout in milliseconds between a character being typed and the search starting. Has no effect when `search.searchOnType` is disabled.") + }, + 'search.enableSearchEditorPreview': { + type: 'boolean', + default: false, + description: nls.localize('search.enableSearchEditorPreview', "Experimental: When enabled, allows opening workspace search results in an editor.") } } }); diff --git a/src/vs/workbench/contrib/search/browser/searchActions.ts b/src/vs/workbench/contrib/search/browser/searchActions.ts index c228d3a1218..b8346873f74 100644 --- a/src/vs/workbench/contrib/search/browser/searchActions.ts +++ b/src/vs/workbench/contrib/search/browser/searchActions.ts @@ -32,6 +32,7 @@ import { ITreeNavigator } from 'vs/base/browser/ui/tree/tree'; import { createEditorFromSearchResult, refreshActiveEditorSearch } from 'vs/workbench/contrib/search/browser/searchEditor'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; export function isSearchViewFocused(viewletService: IViewletService, panelService: IPanelService): boolean { const searchView = getSearchView(viewletService, panelService); @@ -472,7 +473,38 @@ export class RerunEditorSearchAction extends Action { async run() { if (this.configurationService.getValue('search').enableSearchEditorPreview) { await this.progressService.withProgress({ location: ProgressLocation.Window }, - () => refreshActiveEditorSearch(this.editorService, this.instantiationService, this.contextService, this.labelService, this.configurationService)); + () => refreshActiveEditorSearch(undefined, this.editorService, this.instantiationService, this.contextService, this.labelService, this.configurationService)); + } + } +} + +export class RerunEditorSearchWithContextAction extends Action { + + static readonly ID: string = Constants.RerunEditorSearchWithContextCommandId; + static readonly LABEL = nls.localize('search.rerunEditorSearchContext', "Search Again (With Context)"); + + constructor(id: string, label: string, + @IInstantiationService private instantiationService: IInstantiationService, + @IEditorService private editorService: IEditorService, + @IConfigurationService private configurationService: IConfigurationService, + @IWorkspaceContextService private contextService: IWorkspaceContextService, + @ILabelService private labelService: ILabelService, + @IProgressService private progressService: IProgressService, + @IQuickInputService private quickPickService: IQuickInputService + ) { + super(id, label); + } + + async run() { + const lines = await this.quickPickService.input({ + prompt: nls.localize('lines', "Lines of Context"), + value: '2', + validateInput: async (value) => isNaN(parseInt(value)) ? nls.localize('mustBeInteger', "Must enter an integer") : undefined + }); + if (lines === undefined) { return; } + if (this.configurationService.getValue('search').enableSearchEditorPreview) { + await this.progressService.withProgress({ location: ProgressLocation.Window }, + () => refreshActiveEditorSearch(+lines, this.editorService, this.instantiationService, this.contextService, this.labelService, this.configurationService)); } } } diff --git a/src/vs/workbench/contrib/search/browser/searchEditor.ts b/src/vs/workbench/contrib/search/browser/searchEditor.ts index 4f515b013b1..4eee6a2d7fb 100644 --- a/src/vs/workbench/contrib/search/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/search/browser/searchEditor.ts @@ -12,7 +12,7 @@ import { URI } from 'vs/base/common/uri'; import { ITextQuery, IPatternInfo, ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; import * as network from 'vs/base/common/network'; import { Range } from 'vs/editor/common/core/range'; -import { ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { ITextModel, TrackedRangeStickiness, EndOfLinePreference } from 'vs/editor/common/model'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder'; import { getOutOfWorkspaceEditorResources } from 'vs/workbench/contrib/search/common/search'; @@ -20,6 +20,8 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { searchEditorFindMatch, searchEditorFindMatchBorder } from 'vs/platform/theme/common/colorRegistry'; +import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput'; +import { localize } from 'vs/nls'; // Using \r\n on Windows inserts an extra newline between results. const lineDelimiter = '\n'; @@ -45,7 +47,7 @@ const matchToSearchResultFormat = (match: Match): { line: string, ranges: Range[ const prefix = ` ${lineNumber}: ${paddingStr}`; const prefixOffset = prefix.length; - const line = (prefix + sourceLine); + const line = (prefix + sourceLine).replace(/\r?\n?$/, ''); const rangeOnThisLine = ({ start, end }: { start?: number; end?: number; }) => new Range(1, (start ?? 1) + prefixOffset, 1, (end ?? sourceLine.length + 1) + prefixOffset); @@ -65,6 +67,7 @@ const matchToSearchResultFormat = (match: Match): { line: string, ranges: Range[ }; type SearchResultSerialization = { text: string[], matchRanges: Range[] }; + function fileMatchToSearchResultFormat(fileMatch: FileMatch, labelFormatter: (x: URI) => string): SearchResultSerialization { const serializedMatches = flatten(fileMatch.matches() .sort(searchMatchComparer) @@ -76,17 +79,37 @@ function fileMatchToSearchResultFormat(fileMatch: FileMatch, labelFormatter: (x: const targetLineNumberToOffset: Record = {}; + const context: { line: string, lineNumber: number }[] = []; + fileMatch.context.forEach((line, lineNumber) => context.push({ line, lineNumber })); + context.sort((a, b) => a.lineNumber - b.lineNumber); + + let lastLine: number | undefined = undefined; + const seenLines = new Set(); serializedMatches.forEach(match => { if (!seenLines.has(match.line)) { + while (context.length && context[0].lineNumber < +match.lineNumber) { + const { line, lineNumber } = context.shift()!; + if (lastLine !== undefined && lineNumber !== lastLine + 1) { + text.push(''); + } + text.push(` ${lineNumber} ${line}`); + lastLine = lineNumber; + } + targetLineNumberToOffset[match.lineNumber] = text.length; seenLines.add(match.line); text.push(match.line); + lastLine = +match.lineNumber; } matchRanges.push(...match.ranges.map(translateRangeLines(targetLineNumberToOffset[match.lineNumber]))); }); + while (context.length) { + const { line, lineNumber } = context.shift()!; + text.push(` ${lineNumber} ${line}`); + } return { text, matchRanges }; } @@ -104,7 +127,7 @@ const flattenSearchResultSerializations = (serializations: SearchResultSerializa return { text, matchRanges }; }; -const contentPatternToSearchResultHeader = (pattern: ITextQuery | null, includes: string, excludes: string): string[] => { +const contentPatternToSearchResultHeader = (pattern: ITextQuery | null, includes: string, excludes: string, contextLines: number): string[] => { if (!pattern) { return []; } const removeNullFalseAndUndefined = (a: (T | null | false | undefined)[]) => a.filter(a => a !== false && a !== null && a !== undefined) as T[]; @@ -123,19 +146,56 @@ const contentPatternToSearchResultHeader = (pattern: ITextQuery | null, includes ]).join(' ')}`, includes ? `# Including: ${includes}` : undefined, excludes ? `# Excluding: ${excludes}` : undefined, + contextLines ? `# ContextLines: ${contextLines}` : undefined, '' ]); }; -const searchHeaderToContentPattern = (header: string[]): { pattern: string, flags: { regex: boolean, wholeWord: boolean, caseSensitive: boolean, ignoreExcludes: boolean }, includes: string, excludes: string } => { - const query = { + +type SearchHeader = { + pattern: string; + flags: { + regex: boolean; + wholeWord: boolean; + caseSensitive: boolean; + ignoreExcludes: boolean; + }; + includes: string; + excludes: string; + context: number | undefined; +}; + +const searchHeaderToContentPattern = (header: string[]): SearchHeader => { + const query: SearchHeader = { pattern: '', flags: { regex: false, caseSensitive: false, ignoreExcludes: false, wholeWord: false }, includes: '', - excludes: '' + excludes: '', + context: undefined }; - const unescapeNewlines = (str: string) => str.replace(/\\\\/g, '\\').replace(/\\n/g, '\n'); + const unescapeNewlines = (str: string) => { + let out = ''; + for (let i = 0; i < str.length; i++) { + if (str[i] === '\\') { + i++; + const escaped = str[i]; + + if (escaped === 'n') { + out += '\n'; + } + else if (escaped === '\\') { + out += '\\'; + } + else { + throw Error(localize('invalidQueryStringError', "All backslashes in Query string must be escaped (\\\\)")); + } + } else { + out += str[i]; + } + } + return out; + }; const parseYML = /^# ([^:]*): (.*)$/; for (const line of header) { const parsed = parseYML.exec(line); @@ -145,6 +205,7 @@ const searchHeaderToContentPattern = (header: string[]): { pattern: string, flag case 'Query': query.pattern = unescapeNewlines(value); break; case 'Including': query.includes = value; break; case 'Excluding': query.excludes = value; break; + case 'ContextLines': query.context = +value; break; case 'Flags': { query.flags = { regex: value.indexOf('RegExp') !== -1, @@ -159,25 +220,26 @@ const searchHeaderToContentPattern = (header: string[]): { pattern: string, flag return query; }; -const serializeSearchResultForEditor = (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, labelFormatter: (x: URI) => string): SearchResultSerialization => { - const header = contentPatternToSearchResultHeader(searchResult.query, rawIncludePattern, rawExcludePattern); +const serializeSearchResultForEditor = (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, contextLines: number, labelFormatter: (x: URI) => string): SearchResultSerialization => { + const header = contentPatternToSearchResultHeader(searchResult.query, rawIncludePattern, rawExcludePattern, contextLines); const allResults = flattenSearchResultSerializations( - flatten(searchResult.folderMatches().sort(searchMatchComparer) - .map(folderMatch => folderMatch.matches().sort(searchMatchComparer) - .map(fileMatch => fileMatchToSearchResultFormat(fileMatch, labelFormatter))))); + flatten( + searchResult.folderMatches().sort(searchMatchComparer) + .map(folderMatch => folderMatch.matches().sort(searchMatchComparer) + .map(fileMatch => fileMatchToSearchResultFormat(fileMatch, labelFormatter))))); return { matchRanges: allResults.matchRanges.map(translateRangeLines(header.length)), text: header.concat(allResults.text) }; }; export const refreshActiveEditorSearch = - async (editorService: IEditorService, instantiationService: IInstantiationService, contextService: IWorkspaceContextService, labelService: ILabelService, configurationService: IConfigurationService) => { + async (contextLines: number | undefined, editorService: IEditorService, instantiationService: IInstantiationService, contextService: IWorkspaceContextService, labelService: ILabelService, configurationService: IConfigurationService) => { const model = editorService.activeTextEditorWidget?.getModel(); if (!model) { return; } const textModel = model as ITextModel; - const header = textModel.getValueInRange(new Range(1, 1, 5, 1)) + const header = textModel.getValueInRange(new Range(1, 1, 5, 1), EndOfLinePreference.LF) .split(lineDelimiter) .filter(line => line.indexOf('# ') === 0); @@ -190,6 +252,8 @@ export const refreshActiveEditorSearch = isWordMatch: contentPattern.flags.wholeWord }; + contextLines = contextLines ?? contentPattern.context ?? 0; + const options: ITextQueryBuilderOptions = { _reason: 'searchEditor', extraFileResources: instantiationService.invokeFunction(getOutOfWorkspaceEditorResources), @@ -202,6 +266,8 @@ export const refreshActiveEditorSearch = matchLines: 1, charsPerLine: 1000 }, + afterContext: contextLines, + beforeContext: contextLines, isSmartCase: configurationService.getValue('search').smartCase, expandPatterns: true }; @@ -220,7 +286,7 @@ export const refreshActiveEditorSearch = await searchModel.search(query); const labelFormatter = (uri: URI): string => labelService.getUriLabel(uri, { relative: true }); - const results = serializeSearchResultForEditor(searchModel.searchResult, '', '', labelFormatter); + const results = serializeSearchResultForEditor(searchModel.searchResult, contentPattern.includes, contentPattern.excludes, contextLines, labelFormatter); textModel.setValue(results.text.join(lineDelimiter)); textModel.deltaDecorations([], results.matchRanges.map(range => ({ range, options: { className: 'searchEditorFindMatch', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } }))); @@ -233,29 +299,35 @@ export const createEditorFromSearchResult = const labelFormatter = (uri: URI): string => labelService.getUriLabel(uri, { relative: true }); - const results = serializeSearchResultForEditor(searchResult, rawIncludePattern, rawExcludePattern, labelFormatter); - + const results = serializeSearchResultForEditor(searchResult, rawIncludePattern, rawExcludePattern, 0, labelFormatter); + const contents = results.text.join(lineDelimiter); let possible = { - contents: results.text.join(lineDelimiter), + contents, mode: 'search-result', resource: URI.from({ scheme: network.Schemas.untitled, path: searchTerm }) }; let id = 0; - while (editorService.getOpened(possible)) { + let existing = editorService.getOpened(possible); + while (existing) { + if (existing instanceof UntitledTextEditorInput) { + const model = await existing.resolve(); + const existingContents = model.textEditorModel.getValue(EndOfLinePreference.LF); + if (existingContents === contents) { + break; + } + } possible.resource = possible.resource.with({ path: searchTerm + '-' + ++id }); + existing = editorService.getOpened(possible); } const editor = await editorService.openEditor(possible); const control = editor?.getControl()!; - control.updateOptions({ lineNumbers: 'off' }); - const model = control.getModel() as ITextModel; model.deltaDecorations([], results.matchRanges.map(range => ({ range, options: { className: 'searchEditorFindMatch', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } }))); }; -// theming registerThemingParticipant((theme, collector) => { collector.addRule(`.monaco-editor .searchEditorFindMatch { background-color: ${theme.getColor(searchEditorFindMatch)}; }`); diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index c814ae3a86b..d11471d7ff7 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -36,7 +36,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IProgressService, IProgressStep, IProgress } from 'vs/platform/progress/common/progress'; import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ITextQuery, VIEW_ID, VIEWLET_ID } from 'vs/workbench/services/search/common/search'; import { ISearchHistoryService, ISearchHistoryValues } from 'vs/workbench/contrib/search/common/searchHistoryService'; -import { diffInserted, diffInsertedOutline, diffRemoved, diffRemovedOutline, editorFindMatchHighlight, editorFindMatchHighlightBorder, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; +import { diffInserted, diffInsertedOutline, diffRemoved, diffRemovedOutline, editorFindMatchHighlight, editorFindMatchHighlightBorder, listActiveSelectionForeground, foreground } from 'vs/platform/theme/common/colorRegistry'; import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { OpenFileFolderAction, OpenFolderAction } from 'vs/workbench/browser/actions/workspaceActions'; @@ -66,6 +66,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme'; import { createEditorFromSearchResult } from 'vs/workbench/contrib/search/browser/searchEditor'; import { ILabelService } from 'vs/platform/label/common/label'; +import { Color, RGBA } from 'vs/base/common/color'; const $ = dom.$; @@ -205,7 +206,7 @@ export class SearchView extends ViewletPane { this.delayedRefresh = this._register(new Delayer(250)); - this.addToSearchHistoryDelayer = this._register(new Delayer(500)); + this.addToSearchHistoryDelayer = this._register(new Delayer(2000)); this.actions = [ this._register(this.instantiationService.createInstance(ClearSearchResultsAction, ClearSearchResultsAction.ID, ClearSearchResultsAction.LABEL)), @@ -962,13 +963,15 @@ export class SearchView extends ViewletPane { return this.searchWidget && this.searchWidget.searchInput.getValue().length > 0; } - clearSearchResults(): void { + clearSearchResults(clearInput = true): void { this.viewModel.searchResult.clear(); this.showEmptyStage(true); if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { this.showSearchWithoutFolderMessage(); } - this.searchWidget.clear(); + if (clearInput) { + this.searchWidget.clear(); + } this.viewModel.cancelSearch(); this.updateActions(); @@ -1201,7 +1204,7 @@ export class SearchView extends ViewletPane { const useExcludesAndIgnoreFiles = this.inputPatternExcludes.useExcludesAndIgnoreFiles(); if (contentPattern.length === 0) { - this.clearSearchResults(); + this.clearSearchResults(false); this.clearMessage(); return; } @@ -1796,4 +1799,10 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { if (outlineSelectionColor) { collector.addRule(`.monaco-workbench .search-view .monaco-list.element-focused .monaco-list-row.focused.selected:not(.highlighted) .action-label:focus { outline-color: ${outlineSelectionColor} }`); } + + const foregroundColor = theme.getColor(foreground); + if (foregroundColor) { + const fgWithOpacity = new Color(new RGBA(foregroundColor.rgba.r, foregroundColor.rgba.g, foregroundColor.rgba.b, 0.5)); + collector.addRule(`.vs-dark .search-view .message { color: ${fgWithOpacity}; }`); + } }); diff --git a/src/vs/workbench/contrib/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index a7ebbd9cb19..3c9a7a6b1fd 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -466,6 +466,7 @@ export class SearchWidget extends Widget { } if (keyboardEvent.equals(KeyCode.Enter)) { + this.searchInput.onSearchSubmit(); this.submitSearch(); keyboardEvent.preventDefault(); } diff --git a/src/vs/workbench/contrib/search/common/constants.ts b/src/vs/workbench/contrib/search/common/constants.ts index 483190b6498..b7c72d5d5cd 100644 --- a/src/vs/workbench/contrib/search/common/constants.ts +++ b/src/vs/workbench/contrib/search/common/constants.ts @@ -17,6 +17,7 @@ export const CopyMatchCommandId = 'search.action.copyMatch'; export const CopyAllCommandId = 'search.action.copyAll'; export const OpenInEditorCommandId = 'search.action.openInEditor'; export const RerunEditorSearchCommandId = 'search.action.rerunEditorSearch'; +export const RerunEditorSearchWithContextCommandId = 'search.action.rerunEditorSearchWithContext'; export const ClearSearchHistoryCommandId = 'search.action.clearHistory'; export const FocusSearchListCommandID = 'search.action.focusSearchList'; export const ReplaceActionId = 'search.action.replace'; diff --git a/src/vs/workbench/contrib/search/common/searchModel.ts b/src/vs/workbench/contrib/search/common/searchModel.ts index 18ce0750f09..99d8a29887d 100644 --- a/src/vs/workbench/contrib/search/common/searchModel.ts +++ b/src/vs/workbench/contrib/search/common/searchModel.ts @@ -20,7 +20,7 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress'; import { ReplacePattern } from 'vs/workbench/services/search/common/replace'; -import { IFileMatch, IPatternInfo, ISearchComplete, ISearchProgressItem, ISearchConfigurationProperties, ISearchService, ITextQuery, ITextSearchPreviewOptions, ITextSearchMatch, ITextSearchStats, resultIsMatch, ISearchRange, OneLineRange } from 'vs/workbench/services/search/common/search'; +import { IFileMatch, IPatternInfo, ISearchComplete, ISearchProgressItem, ISearchConfigurationProperties, ISearchService, ITextQuery, ITextSearchPreviewOptions, ITextSearchMatch, ITextSearchStats, resultIsMatch, ISearchRange, OneLineRange, ITextSearchContext, ITextSearchResult } from 'vs/workbench/services/search/common/search'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { overviewRulerFindMatchForeground, minimapFindMatch } from 'vs/platform/theme/common/colorRegistry'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; @@ -197,6 +197,11 @@ export class FileMatch extends Disposable implements IFileMatch { private _updateScheduler: RunOnceScheduler; private _modelDecorations: string[] = []; + private _context: Map = new Map(); + public get context(): Map { + return new Map(this._context); + } + constructor(private _query: IPatternInfo, private _previewOptions: ITextSearchPreviewOptions | undefined, private _maxResults: number | undefined, private _parent: FolderMatch, private rawMatch: IFileMatch, @IModelService private readonly modelService: IModelService, @IReplaceService private readonly replaceService: IReplaceService ) { @@ -221,6 +226,8 @@ export class FileMatch extends Disposable implements IFileMatch { textSearchResultToMatches(rawMatch, this) .forEach(m => this.add(m)); }); + + this.addContext(this.rawMatch.results); } } @@ -375,6 +382,14 @@ export class FileMatch extends Disposable implements IFileMatch { return getBaseLabel(this.resource); } + addContext(results: ITextSearchResult[] | undefined) { + if (!results) { return; } + + results + .filter((result => !resultIsMatch(result)) as ((a: any) => a is ITextSearchContext)) + .forEach(context => this._context.set(context.lineNumber, context.text)); + } + add(match: Match, trigger?: boolean) { this._matches.set(match.id(), match); if (trigger) { @@ -479,6 +494,8 @@ export class FolderMatch extends Disposable { .forEach(m => existingFileMatch.add(m)); }); updated.push(existingFileMatch); + + existingFileMatch.addContext(rawFileMatch.results); } else { const fileMatch = this.instantiationService.createInstance(FileMatch, this._query.contentPattern, this._query.previewOptions, this._query.maxResults, this, rawFileMatch); this.doAdd(fileMatch); @@ -965,11 +982,6 @@ export class SearchModel extends Disposable { search(query: ITextQuery, onProgress?: (result: ISearchProgressItem) => void): Promise { this.cancelSearch(); - // Exclude Search Editor results unless explicity included - const searchEditorFilenameGlob = `**/*.code-search`; - if (!query.includePattern || !query.includePattern[searchEditorFilenameGlob]) { - query.excludePattern = { ...(query.excludePattern ?? {}), [searchEditorFilenameGlob]: true }; - } this._searchQuery = query; if (!this.searchConfig.searchOnType) { @@ -982,7 +994,7 @@ export class SearchModel extends Disposable { this._replacePattern = new ReplacePattern(this.replaceString, this._searchQuery.contentPattern); // In search on type case, delay the streaming of results just a bit, so that we don't flash the only "local results" fast path - this._startStreamDelay = new Promise(resolve => setTimeout(resolve, this.searchConfig.searchOnType ? 100 : 0)); + this._startStreamDelay = new Promise(resolve => setTimeout(resolve, this.searchConfig.searchOnType ? 150 : 0)); const tokenSource = this.currentCancelTokenSource = new CancellationTokenSource(); const currentRequest = this.searchService.textSearch(this._searchQuery, this.currentCancelTokenSource.token, p => { diff --git a/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts b/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts index 6a696c1666a..1526a62d12b 100644 --- a/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts +++ b/src/vs/workbench/contrib/tasks/common/taskDefinitionRegistry.ts @@ -33,7 +33,7 @@ const taskDefinitionSchema: IJSONSchema = { type: 'object', description: nls.localize('TaskDefinition.properties', 'Additional properties of the task type'), additionalProperties: { - $ref: 'http://json-schema.org/draft-04/schema#' + $ref: 'http://json-schema.org/draft-07/schema#' } } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index 661e638a8e6..ba65ed8a973 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -170,7 +170,7 @@ configurationRegistry.registerConfiguration({ default: DEFAULT_LINE_HEIGHT }, 'terminal.integrated.minimumContrastRatio': { - description: nls.localize('terminal.integrated.minimumContrastRatio', "When set the foreground color of each cell will change to try meet the contrast ratio specified. Example values:\n\n- 1: The default, do nothing.\n- 4.5: Minimum for WCAG AA compliance.\n- 7: Minimum for WCAG AAA compliance.\n- 21: White on black or black on white."), + markdownDescription: nls.localize('terminal.integrated.minimumContrastRatio', "When set the foreground color of each cell will change to try meet the contrast ratio specified. Example values:\n\n- 1: The default, do nothing.\n- 4.5: Minimum for [WCAG AA compliance](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html).\n- 7: Minimum for [WCAG AAA compliance](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast7.html).\n- 21: White on black or black on white."), type: 'number', default: 1 }, @@ -205,7 +205,7 @@ configurationRegistry.registerConfiguration({ markdownDescription: nls.localize('terminal.integrated.detectLocale', "Controls whether to detect and set the `$LANG` environment variable to a UTF-8 compliant option since VS Code's terminal only supports UTF-8 encoded data coming from the shell."), type: 'string', enum: ['auto', 'off', 'on'], - enumDescriptions: [ + markdownEnumDescriptions: [ nls.localize('terminal.integrated.detectLocale.auto', "Set the `$LANG` environment variable if the existing variable does not exist or it does not end in `'.UTF-8'`."), nls.localize('terminal.integrated.detectLocale.off', "Do not set the `$LANG` environment variable."), nls.localize('terminal.integrated.detectLocale.on', "Always set the `$LANG` environment variable.") @@ -215,7 +215,7 @@ configurationRegistry.registerConfiguration({ 'terminal.integrated.rendererType': { type: 'string', enum: ['auto', 'canvas', 'dom', 'experimentalWebgl'], - enumDescriptions: [ + markdownEnumDescriptions: [ nls.localize('terminal.integrated.rendererType.auto', "Let VS Code guess which renderer to use."), nls.localize('terminal.integrated.rendererType.canvas', "Use the standard GPU/canvas-based renderer."), nls.localize('terminal.integrated.rendererType.dom', "Use the fallback DOM-based renderer."), @@ -252,7 +252,7 @@ configurationRegistry.registerConfiguration({ default: false }, 'terminal.integrated.commandsToSkipShell': { - description: nls.localize('terminal.integrated.commandsToSkipShell', "A set of command IDs whose keybindings will not be sent to the shell and instead always be handled by Code. This allows the use of keybindings that would normally be consumed by the shell to act the same as when the terminal is not focused, for example ctrl+p to launch Quick Open.\nDefault Skipped Commands:\n\n{0}", DEFAULT_COMMANDS_TO_SKIP_SHELL.sort().map(command => `- ${command}`).join('\n')), + markdownDescription: nls.localize('terminal.integrated.commandsToSkipShell', "A set of command IDs whose keybindings will not be sent to the shell and instead always be handled by Code. This allows the use of keybindings that would normally be consumed by the shell to act the same as when the terminal is not focused, for example ctrl+p to launch Quick Open.\nDefault Skipped Commands:\n\n{0}", DEFAULT_COMMANDS_TO_SKIP_SHELL.sort().map(command => `- ${command}`).join('\n')), type: 'array', items: { type: 'string' @@ -260,7 +260,7 @@ configurationRegistry.registerConfiguration({ default: [] }, 'terminal.integrated.allowChords': { - markdownDescription: nls.localize('terminal.integrated.allowChords', "Whether or not to allow chord keybindings in the terminal. Note that when this is true and the keystroke results in a chord it will bypass `terminal.integrated.commandsToSkipShell`, setting this to false is particularly useful when you want ctrl+k to go to your shell (not VS Code)."), + markdownDescription: nls.localize('terminal.integrated.allowChords', "Whether or not to allow chord keybindings in the terminal. Note that when this is true and the keystroke results in a chord it will bypass `#terminal.integrated.commandsToSkipShell#`, setting this to false is particularly useful when you want ctrl+k to go to your shell (not VS Code)."), type: 'boolean', default: true }, diff --git a/src/vs/workbench/contrib/testCustomEditors/browser/testCustomEditors.ts b/src/vs/workbench/contrib/testCustomEditors/browser/testCustomEditors.ts deleted file mode 100644 index 661b8412cad..00000000000 --- a/src/vs/workbench/contrib/testCustomEditors/browser/testCustomEditors.ts +++ /dev/null @@ -1,252 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as nls from 'vs/nls'; -import { Action } from 'vs/base/common/actions'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { IEditorInputFactory, EditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, EditorModel, EditorOptions, GroupIdentifier, ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorModel } from 'vs/platform/editor/common/editor'; -import { Dimension, addDisposableListener, EventType } from 'vs/base/browser/dom'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; -import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { URI } from 'vs/base/common/uri'; -import { isEqual } from 'vs/base/common/resources'; -import { generateUuid } from 'vs/base/common/uuid'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { editorBackground, editorForeground } from 'vs/platform/theme/common/colorRegistry'; -import { IWorkingCopy, IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { env } from 'vs/base/common/process'; - -const CUSTOM_SCHEME = 'testCustomEditor'; -const ENABLE = !!env['VSCODE_DEV']; - -class TestCustomEditorsAction extends Action { - - static readonly ID = 'workbench.action.openCustomEditor'; - static readonly LABEL = nls.localize('openCustomEditor', "Test Open Custom Editor"); - - constructor( - id: string, - label: string, - @IEditorService private readonly editorService: IEditorService, - @IInstantiationService private readonly instantiationService: IInstantiationService - ) { - super(id, label); - } - - async run(): Promise { - const input = this.instantiationService.createInstance(TestCustomEditorInput, URI.parse(`${CUSTOM_SCHEME}:/${generateUuid()}`)); - await this.editorService.openEditor(input); - - return true; - } -} - -class TestCustomEditor extends BaseEditor { - - static ID = 'testCustomEditor'; - - private textArea: HTMLTextAreaElement | undefined = undefined; - - constructor( - @ITelemetryService telemetryService: ITelemetryService, - @IThemeService themeService: IThemeService, - @IStorageService storageService: IStorageService - ) { - super(TestCustomEditor.ID, telemetryService, themeService, storageService); - } - - updateStyles(): void { - super.updateStyles(); - - if (this.textArea) { - this.textArea.style.backgroundColor = this.getColor(editorBackground)!.toString(); - this.textArea.style.color = this.getColor(editorForeground)!.toString(); - } - } - - protected createEditor(parent: HTMLElement): void { - this.textArea = document.createElement('textarea'); - this.textArea.style.width = '100%'; - this.textArea.style.height = '100%'; - - parent.appendChild(this.textArea); - - addDisposableListener(this.textArea, EventType.CHANGE, e => this.onDidType()); - addDisposableListener(this.textArea, EventType.KEY_UP, e => this.onDidType()); - - this.updateStyles(); - } - - private onDidType(): void { - if (this._input instanceof TestCustomEditorInput) { - this._input.setValue(this.textArea!.value); - } - } - - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { - await super.setInput(input, options, token); - - const model = await input.resolve(); - if (model instanceof TestCustomEditorModel) { - this.textArea!.value = model.value; - } - } - - clearInput() { - super.clearInput(); - - this.textArea!.value = ''; - } - - focus(): void { - this.textArea!.focus(); - } - - layout(dimension: Dimension): void { } -} - -class TestCustomEditorInput extends EditorInput implements IWorkingCopy { - private model: TestCustomEditorModel | undefined = undefined; - - private dirty = false; - - readonly capabilities = 0; - - constructor(public readonly resource: URI, @IWorkingCopyService workingCopyService: IWorkingCopyService) { - super(); - - this._register(workingCopyService.registerWorkingCopy(this)); - } - - getResource(): URI { - return this.resource; - } - - getTypeId(): string { - return TestCustomEditor.ID; - } - - getName(): string { - return this.resource.toString(); - } - - setValue(value: string) { - if (this.model) { - if (this.model.value === value) { - return; - } - - this.model.value = value; - } - - this.setDirty(value.length > 0); - } - - private setDirty(dirty: boolean) { - if (this.dirty !== dirty) { - this.dirty = dirty; - this._onDidChangeDirty.fire(); - } - } - - isReadonly(): boolean { - return false; - } - - isDirty(): boolean { - return this.dirty; - } - - async save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { - this.setDirty(false); - - return true; - } - - async saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { - this.setDirty(false); - - return true; - } - - async revert(options?: IRevertOptions): Promise { - this.setDirty(false); - - return true; - } - - async resolve(): Promise { - if (!this.model) { - this.model = new TestCustomEditorModel(this.resource); - } - - return this.model; - } - - matches(other: EditorInput) { - return other instanceof TestCustomEditorInput && isEqual(other.resource, this.resource); - } - - dispose(): void { - this.setDirty(false); - - if (this.model) { - this.model.dispose(); - this.model = undefined; - } - - super.dispose(); - } -} - -class TestCustomEditorModel extends EditorModel { - - public value: string = ''; - - constructor(public readonly resource: URI) { - super(); - } -} - -if (ENABLE) { - Registry.as(EditorExtensions.Editors).registerEditor( - EditorDescriptor.create( - TestCustomEditor, - TestCustomEditor.ID, - nls.localize('testCustomEditor', "Test Custom Editor") - ), - [ - new SyncDescriptor(TestCustomEditorInput), - ] - ); - - const registry = Registry.as(Extensions.WorkbenchActions); - - registry.registerWorkbenchAction(SyncActionDescriptor.create(TestCustomEditorsAction, TestCustomEditorsAction.ID, TestCustomEditorsAction.LABEL), 'Test Open Custom Editor'); - - class TestCustomEditorInputFactory implements IEditorInputFactory { - - serialize(editorInput: TestCustomEditorInput): string { - return JSON.stringify({ - resource: editorInput.resource.toString() - }); - } - - deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): TestCustomEditorInput { - return instantiationService.createInstance(TestCustomEditorInput, URI.parse(JSON.parse(serializedEditorInput).resource)); - } - } - - Registry.as(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory(TestCustomEditor.ID, TestCustomEditorInputFactory); -} diff --git a/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index 4f5f9d2e278..cbadd30426a 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -238,18 +238,20 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu let badge: IBadge | undefined = undefined; let clazz: string | undefined; + let priority: number | undefined = undefined; if (state.type === StateType.AvailableForDownload || state.type === StateType.Downloaded || state.type === StateType.Ready) { badge = new NumberBadge(1, () => nls.localize('updateIsReady', "New {0} update available.", this.productService.nameShort)); } else if (state.type === StateType.CheckingForUpdates || state.type === StateType.Downloading || state.type === StateType.Updating) { badge = new ProgressBadge(() => nls.localize('updateIsReady', "New {0} update available.", this.productService.nameShort)); clazz = 'progress-badge'; + priority = 1; } this.badgeDisposable.clear(); if (badge) { - this.badgeDisposable.value = this.activityService.showActivity(GLOBAL_ACTIVITY_ID, badge, clazz); + this.badgeDisposable.value = this.activityService.showActivity(GLOBAL_ACTIVITY_ID, badge, clazz, priority); } this.state = state; diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSync.ts new file mode 100644 index 00000000000..d0d54c3c403 --- /dev/null +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSync.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IUserDataSyncService, IUserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IAuthTokenService } from 'vs/platform/auth/common/auth'; +import { Event } from 'vs/base/common/event'; +import { UserDataAutoSync as BaseUserDataAutoSync } from 'vs/platform/userDataSync/common/userDataAutoSync'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; + +export class UserDataAutoSync extends BaseUserDataAutoSync { + + constructor( + @IUserDataSyncService userDataSyncService: IUserDataSyncService, + @IConfigurationService configurationService: IConfigurationService, + @IUserDataSyncLogService logService: IUserDataSyncLogService, + @IAuthTokenService authTokenService: IAuthTokenService, + @IInstantiationService instantiationService: IInstantiationService, + @IHostService hostService: IHostService, + ) { + super(configurationService, userDataSyncService, logService, authTokenService); + + // Sync immediately if there is a local change. + this._register(Event.debounce(Event.any( + userDataSyncService.onDidChangeLocal, + instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync, + hostService.onDidChangeFocus + ), () => undefined, 500)(() => this.sync(false))); + } + +} diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 98127ebb8c3..9e702a131d6 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -30,7 +30,9 @@ import { FalseContext } from 'vs/platform/contextkey/common/contextkeys'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { isWeb } from 'vs/base/common/platform'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { UserDataAutoSync } from 'vs/platform/userDataSync/common/userDataSyncService'; +import { UserDataAutoSync } from 'vs/workbench/contrib/userDataSync/browser/userDataAutoSync'; +import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger'; +import { timeout } from 'vs/base/common/async'; const CONTEXT_AUTH_TOKEN_STATE = new RawContextKey('authTokenStatus', AuthTokenStatus.Initializing); const SYNC_PUSH_LIGHT_ICON_URI = URI.parse(registerAndGetAmdImageURL(`vs/workbench/contrib/userDataSync/browser/media/check-light.svg`)); @@ -78,10 +80,20 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo if (isWeb) { this._register(instantiationService.createInstance(UserDataAutoSync)); + } else { + this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(() => this.triggerSync())); } } } + private triggerSync(): void { + if (this.configurationService.getValue('sync.enable') + && this.userDataSyncService.status !== SyncStatus.Uninitialized + && this.authTokenService.status === AuthTokenStatus.SignedIn) { + this.userDataSyncService.sync(); + } + } + private onDidChangeAuthTokenStatus(status: AuthTokenStatus) { this.authTokenContext.set(status); if (status === AuthTokenStatus.SignedIn) { @@ -93,7 +105,12 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private onDidChangeSyncStatus(status: SyncStatus) { this.syncStatusContext.set(status); - this.updateBadge(); + if (status === SyncStatus.Syncing) { + // Show syncing progress if takes more than 1s. + timeout(1000).then(() => this.updateBadge()); + } else { + this.updateBadge(); + } if (this.userDataSyncService.status === SyncStatus.HasConflicts) { if (!this.conflictsWarningDisposable.value) { @@ -141,21 +158,24 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo let badge: IBadge | undefined = undefined; let clazz: string | undefined; + let priority: number | undefined = undefined; if (this.userDataSyncService.status !== SyncStatus.Uninitialized && this.configurationService.getValue(UserDataSyncWorkbenchContribution.ENABLEMENT_SETTING) && this.authTokenService.status === AuthTokenStatus.SignedOut) { badge = new NumberBadge(1, () => localize('sign in', "Sync: Sign in...")); } else if (this.authTokenService.status === AuthTokenStatus.SigningIn) { badge = new ProgressBadge(() => localize('signing in', "Signin in...")); clazz = 'progress-badge'; + priority = 1; } else if (this.userDataSyncService.status === SyncStatus.HasConflicts) { badge = new NumberBadge(1, () => localize('resolve conflicts', "Resolve Conflicts")); } else if (this.userDataSyncService.status === SyncStatus.Syncing) { badge = new ProgressBadge(() => localize('syncing', "Synchronizing User Configuration...")); clazz = 'progress-badge'; + priority = 1; } if (badge) { - this.badgeDisposable.value = this.activityService.showActivity(GLOBAL_ACTIVITY_ID, badge, clazz); + this.badgeDisposable.value = this.activityService.showActivity(GLOBAL_ACTIVITY_ID, badge, clazz, priority); } } @@ -189,6 +209,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo const items = [{ id: 'sync.enableSettings', label: localize('user settings', "User Settings") + }, { + id: 'sync.enableKeybindings', + label: localize('user keybindings', "User Keybindings") }, { id: 'sync.enableExtensions', label: localize('extensions', "Extensions") @@ -251,13 +274,14 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } private getPreviewEditorInput(): IEditorInput | undefined { - return this.editorService.editors.filter(input => isEqual(input.getResource(), this.workbenchEnvironmentService.settingsSyncPreviewResource))[0]; + return this.editorService.editors.filter(input => isEqual(input.getResource(), this.workbenchEnvironmentService.settingsSyncPreviewResource) || isEqual(input.getResource(), this.workbenchEnvironmentService.keybindingsSyncPreviewResource))[0]; } private async handleConflicts(): Promise { - if (this.userDataSyncService.conflictsSource === SyncSource.Settings) { + const conflictsResource = this.getConflictsResource(); + if (conflictsResource) { const resourceInput = { - resource: this.workbenchEnvironmentService.settingsSyncPreviewResource, + resource: conflictsResource, options: { preserveFocus: false, pinned: false, @@ -279,6 +303,16 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } + private getConflictsResource(): URI | null { + if (this.userDataSyncService.conflictsSource === SyncSource.Settings) { + return this.workbenchEnvironmentService.settingsSyncPreviewResource; + } + if (this.userDataSyncService.conflictsSource === SyncSource.Keybindings) { + return this.workbenchEnvironmentService.keybindingsSyncPreviewResource; + } + return null; + } + private registerActions(): void { const startSyncMenuItem: IMenuItem = { @@ -380,6 +414,19 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo order: 1, when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.isEqualTo(SyncStatus.HasConflicts), ResourceContextKey.Resource.isEqualTo(this.workbenchEnvironmentService.settingsSyncPreviewResource.toString())), }); + MenuRegistry.appendMenuItem(MenuId.EditorTitle, { + command: { + id: continueSyncCommandId, + title: localize('continue sync', "Sync: Continue"), + icon: { + light: SYNC_PUSH_LIGHT_ICON_URI, + dark: SYNC_PUSH_DARK_ICON_URI + } + }, + group: 'navigation', + order: 1, + when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.isEqualTo(SyncStatus.HasConflicts), ResourceContextKey.Resource.isEqualTo(this.workbenchEnvironmentService.keybindingsSyncPreviewResource.toString())), + }); const signOutMenuItem: IMenuItem = { group: '5_sync', diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts new file mode 100644 index 00000000000..21dc9c85367 --- /dev/null +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event, Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { SettingsEditor2Input, KeybindingsEditorInput, PreferencesEditorInput } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; +import { isEqual } from 'vs/base/common/resources'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; +import { VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IEditorInput } from 'vs/workbench/common/editor'; +import { IViewlet } from 'vs/workbench/common/viewlet'; + +export class UserDataSyncTrigger extends Disposable { + + private readonly _onDidTriggerSync: Emitter = this._register(new Emitter()); + readonly onDidTriggerSync: Event = this._onDidTriggerSync.event; + + constructor( + @IEditorService editorService: IEditorService, + @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, + @IViewletService viewletService: IViewletService, + ) { + super(); + this._register(Event.debounce(Event.any( + Event.filter(editorService.onDidActiveEditorChange, () => this.isUserDataEditorInput(editorService.activeEditor)), + Event.filter(viewletService.onDidViewletOpen, viewlet => this.isUserDataViewlet(viewlet)) + ), () => undefined, 500)(() => this._onDidTriggerSync.fire())); + } + + private isUserDataViewlet(viewlet: IViewlet): boolean { + return viewlet.getId() === VIEWLET_ID; + } + + private isUserDataEditorInput(editorInput: IEditorInput | undefined): boolean { + if (!editorInput) { + return false; + } + if (editorInput instanceof SettingsEditor2Input) { + return true; + } + if (editorInput instanceof PreferencesEditorInput) { + return true; + } + if (editorInput instanceof KeybindingsEditorInput) { + return true; + } + const resource = editorInput.getResource(); + if (isEqual(resource, this.workbenchEnvironmentService.settingsResource)) { + return true; + } + if (isEqual(resource, this.workbenchEnvironmentService.keybindingsResource)) { + return true; + } + return false; + } +} + diff --git a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSync.contribution.ts b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSync.contribution.ts index 2b8fb68f933..1ff66d24159 100644 --- a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSync.contribution.ts +++ b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSync.contribution.ts @@ -4,19 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { ISettingsMergeService } from 'vs/platform/userDataSync/common/userDataSync'; +import { ISettingsMergeService, IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { SettingsMergeChannel } from 'vs/platform/userDataSync/common/settingsSyncIpc'; +import { UserDataSycnUtilServiceChannel } from 'vs/platform/userDataSync/common/keybindingsSyncIpc'; class UserDataSyncServicesContribution implements IWorkbenchContribution { constructor( @ISettingsMergeService settingsMergeService: ISettingsMergeService, + @IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService, @ISharedProcessService sharedProcessService: ISharedProcessService, ) { sharedProcessService.registerChannel('settingsMerge', new SettingsMergeChannel(settingsMergeService)); + sharedProcessService.registerChannel('userDataSyncUtil', new UserDataSycnUtilServiceChannel(userDataSyncUtilService)); } } diff --git a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts index 372826a2ae7..4690972af8b 100644 --- a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts @@ -226,17 +226,18 @@ class BulkEditModel implements IDisposable { } } -export type Edit = ResourceFileEdit | ResourceTextEdit; +type Edit = ResourceFileEdit | ResourceTextEdit; -export class BulkEdit { +class BulkEdit { - private _edits: Edit[] = []; - private _editor: ICodeEditor | undefined; - private _progress: IProgress; + private readonly _edits: Edit[] = []; + private readonly _editor: ICodeEditor | undefined; + private readonly _progress: IProgress; constructor( editor: ICodeEditor | undefined, progress: IProgress | undefined, + edits: Edit[], @ILogService private readonly _logService: ILogService, @ITextModelService private readonly _textModelService: ITextModelService, @IFileService private readonly _fileService: IFileService, @@ -246,14 +247,7 @@ export class BulkEdit { ) { this._editor = editor; this._progress = progress || emptyProgress; - } - - add(edits: Edit[] | Edit): void { - if (Array.isArray(edits)) { - this._edits.push(...edits); - } else { - this._edits.push(edits); - } + this._edits = edits; } ariaMessage(): string { @@ -419,8 +413,11 @@ export class BulkEditService implements IBulkEditService { // If the code editor is readonly still allow bulk edits to be applied #68549 codeEditor = undefined; } - const bulkEdit = new BulkEdit(codeEditor, options.progress, this._logService, this._textModelService, this._fileService, this._textFileService, this._labelService, this._configurationService); - bulkEdit.add(edits); + const bulkEdit = new BulkEdit( + codeEditor, options.progress, edits, + this._logService, this._textModelService, this._fileService, this._textFileService, this._labelService, this._configurationService + ); + return bulkEdit.perform().then(() => { return { ariaSummary: bulkEdit.ariaMessage() }; diff --git a/src/vs/workbench/services/configuration/browser/configuration.ts b/src/vs/workbench/services/configuration/browser/configuration.ts index d3af9e42aee..d2450a267f7 100644 --- a/src/vs/workbench/services/configuration/browser/configuration.ts +++ b/src/vs/workbench/services/configuration/browser/configuration.ts @@ -9,7 +9,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import * as errors from 'vs/base/common/errors'; import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { FileChangeType, FileChangesEvent, IFileService } from 'vs/platform/files/common/files'; +import { FileChangeType, FileChangesEvent, IFileService, whenProviderRegistered } from 'vs/platform/files/common/files'; import { ConfigurationModel, ConfigurationModelParser } from 'vs/platform/configuration/common/configurationModels'; import { WorkspaceConfigurationModelParser, StandaloneConfigurationModelParser } from 'vs/workbench/services/configuration/common/configurationModels'; import { FOLDER_SETTINGS_PATH, TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY, IConfigurationCache, ConfigurationKey, REMOTE_MACHINE_SCOPES, FOLDER_SCOPES, WORKSPACE_SCOPES } from 'vs/workbench/services/configuration/common/configuration'; @@ -24,20 +24,6 @@ import { IConfigurationModel } from 'vs/platform/configuration/common/configurat import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { hash } from 'vs/base/common/hash'; -function whenProviderRegistered(scheme: string, fileService: IFileService): Promise { - if (fileService.canHandleResource(URI.from({ scheme }))) { - return Promise.resolve(); - } - return new Promise((c, e) => { - const disposable = fileService.onDidChangeFileSystemProviderRegistrations(e => { - if (e.scheme === scheme && e.added) { - disposable.dispose(); - c(); - } - }); - }); -} - export class UserConfiguration extends Disposable { private readonly parser: ConfigurationModelParser; @@ -353,7 +339,7 @@ export class WorkspaceConfiguration extends Disposable { } private async waitAndSwitch(workspaceIdentifier: IWorkspaceIdentifier): Promise { - await whenProviderRegistered(workspaceIdentifier.configPath.scheme, this._fileService); + await whenProviderRegistered(workspaceIdentifier.configPath, this._fileService); if (!(this._workspaceConfiguration instanceof FileServiceBasedWorkspaceConfiguration)) { const fileServiceBasedWorkspaceConfiguration = this._register(new FileServiceBasedWorkspaceConfiguration(this._fileService)); await fileServiceBasedWorkspaceConfiguration.load(workspaceIdentifier); @@ -750,7 +736,7 @@ export class FolderConfiguration extends Disposable implements IFolderConfigurat if (workspaceFolder.uri.scheme === Schemas.file) { this.folderConfiguration = new FileServiceBasedFolderConfiguration(this.configurationFolder, this.workbenchState, fileService); } else { - whenProviderRegistered(workspaceFolder.uri.scheme, fileService) + whenProviderRegistered(workspaceFolder.uri, fileService) .then(() => { this.folderConfiguration.dispose(); this.folderConfigurationDisposable.dispose(); diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index bdbed598a77..d54e68fa70f 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -136,6 +136,9 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment @memoize get settingsSyncPreviewResource(): URI { return joinPath(this.userRoamingDataHome, '.settings.json'); } + @memoize + get keybindingsSyncPreviewResource(): URI { return joinPath(this.userRoamingDataHome, '.keybindings.json'); } + @memoize get userDataSyncLogResource(): URI { return joinPath(this.options.logsPath, 'userDataSync.log'); } diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index 4ed69ee646b..dfb857d8350 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -146,7 +146,7 @@ export class BrowserHostService extends Disposable implements IHostService { const windowConfig = this.configurationService.getValue('window'); const openFolderInNewWindowConfig = windowConfig?.openFoldersInNewWindow || 'default' /* default */; - let openFolderInNewWindow = !!options.forceNewWindow && !options.forceReuseWindow; + let openFolderInNewWindow = (options.preferNewWindow || !!options.forceNewWindow) && !options.forceReuseWindow; if (!options.forceNewWindow && !options.forceReuseWindow && (openFolderInNewWindowConfig === 'on' || openFolderInNewWindowConfig === 'off')) { openFolderInNewWindow = (openFolderInNewWindowConfig === 'on'); } diff --git a/src/vs/workbench/services/keybinding/browser/keybindingService.ts b/src/vs/workbench/services/keybinding/browser/keybindingService.ts index 2c0becf3008..a952f48a57d 100644 --- a/src/vs/workbench/services/keybinding/browser/keybindingService.ts +++ b/src/vs/workbench/services/keybinding/browser/keybindingService.ts @@ -338,7 +338,8 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { } const resolvedKeybindings = this.resolveKeybinding(keybinding); - for (const resolvedKeybinding of resolvedKeybindings) { + for (let i = resolvedKeybindings.length - 1; i >= 0; i--) { + const resolvedKeybinding = resolvedKeybindings[i]; result[resultLen++] = new ResolvedKeybindingItem(resolvedKeybinding, item.command, item.commandArgs, when, isDefault); } } diff --git a/src/vs/workbench/services/keybinding/electron-browser/nativeKeymapService.ts b/src/vs/workbench/services/keybinding/electron-browser/nativeKeymapService.ts index 757f632c3a7..6ef2edda43d 100644 --- a/src/vs/workbench/services/keybinding/electron-browser/nativeKeymapService.ts +++ b/src/vs/workbench/services/keybinding/electron-browser/nativeKeymapService.ts @@ -61,7 +61,7 @@ export class KeyboardMapperFactory { private static _isUSStandard(_kbInfo: nativeKeymap.IKeyboardLayoutInfo): boolean { if (OS === OperatingSystem.Linux) { const kbInfo = _kbInfo; - return (kbInfo && kbInfo.layout === 'us'); + return (kbInfo && (kbInfo.layout === 'us' || /^us,/.test(kbInfo.layout))); } if (OS === OperatingSystem.Macintosh) { diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index 1406adf1750..4fc5403acf3 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -1056,7 +1056,7 @@ export function createValidator(prop: IConfigurationPropertySchema): (value: any } if (prop.maxItems && stringArrayValue.length > prop.maxItems) { - message += nls.localize('validations.stringArrayMaxItem', 'Array must have less than {0} items', prop.maxItems); + message += nls.localize('validations.stringArrayMaxItem', 'Array must have at most {0} items', prop.maxItems); message += '\n'; } diff --git a/src/vs/workbench/services/preferences/test/common/preferencesModel.test.ts b/src/vs/workbench/services/preferences/test/common/preferencesModel.test.ts index 9fbfb467a2b..65673979df4 100644 --- a/src/vs/workbench/services/preferences/test/common/preferencesModel.test.ts +++ b/src/vs/workbench/services/preferences/test/common/preferencesModel.test.ts @@ -291,7 +291,7 @@ suite('Preferences Model test', () => { arr.rejects([]).withMessage('Array must have at least 1 items'); arr.accepts(['a']); arr.accepts(['a', 'a']); - arr.rejects(['a', 'a', 'a']).withMessage('Array must have less than 2 items'); + arr.rejects(['a', 'a', 'a']).withMessage('Array must have at most 2 items'); } }); @@ -312,7 +312,7 @@ suite('Preferences Model test', () => { test('min-max and enum', () => { const arr = new ArrayTester({ type: 'array', items: { type: 'string', enum: ['a', 'b'] }, minItems: 1, maxItems: 2 }); - arr.rejects(['a', 'b', 'c']).withMessage('Array must have less than 2 items'); + arr.rejects(['a', 'b', 'c']).withMessage('Array must have at most 2 items'); arr.rejects(['a', 'b', 'c']).withMessage(`Value 'c' is not one of`); }); diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index 4f2e382f040..631de089101 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -10,7 +10,6 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { URI } from 'vs/base/common/uri'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { Disposable } from 'vs/base/common/lifecycle'; import { IEditableData } from 'vs/workbench/common/views'; @@ -20,7 +19,7 @@ export const REMOTE_EXPLORER_TYPE_KEY: string = 'remote.explorerType'; export interface Tunnel { remote: number; - localUri: URI; + localAddress: string; local?: number; name?: string; description?: string; @@ -47,7 +46,7 @@ export class TunnelModel extends Disposable { if (tunnel.localAddress) { this.forwarded.set(tunnel.tunnelRemotePort, { remote: tunnel.tunnelRemotePort, - localUri: tunnel.localAddress, + localAddress: tunnel.localAddress, local: tunnel.tunnelLocalPort }); } @@ -63,7 +62,7 @@ export class TunnelModel extends Disposable { if (!this.forwarded.has(tunnel.tunnelRemotePort) && tunnel.localAddress) { this.forwarded.set(tunnel.tunnelRemotePort, { remote: tunnel.tunnelRemotePort, - localUri: tunnel.localAddress, + localAddress: tunnel.localAddress, local: tunnel.tunnelLocalPort }); } @@ -86,7 +85,7 @@ export class TunnelModel extends Disposable { local: tunnel.tunnelLocalPort, name: name, closeable: true, - localUri: tunnel.localAddress + localAddress: tunnel.localAddress }; this.forwarded.set(remote, newForward); this._onForwardPort.fire(newForward); @@ -105,8 +104,8 @@ export class TunnelModel extends Disposable { return this.tunnelService.closeTunnel(remote); } - address(remote: number): URI | undefined { - return (this.forwarded.get(remote) || this.published.get(remote))?.localUri; + address(remote: number): string | undefined { + return (this.forwarded.get(remote) || this.published.get(remote))?.localAddress; } } @@ -116,9 +115,9 @@ export interface IRemoteExplorerService { targetType: string; readonly helpInformation: HelpInformation[]; readonly tunnelModel: TunnelModel; - onDidChangeEditable: Event; - setEditable(remote: number, data: IEditableData | null): void; - getEditableData(remote: number): IEditableData | undefined; + onDidChangeEditable: Event; + setEditable(remote: number | undefined, data: IEditableData | null): void; + getEditableData(remote: number | undefined): IEditableData | undefined; } export interface HelpInformation { @@ -163,9 +162,9 @@ class RemoteExplorerService implements IRemoteExplorerService { public readonly onDidChangeTargetType: Event = this._onDidChangeTargetType.event; private _helpInformation: HelpInformation[] = []; private _tunnelModel: TunnelModel; - private editable: { remote: number, data: IEditableData } | undefined; - private readonly _onDidChangeEditable: Emitter = new Emitter(); - public readonly onDidChangeEditable: Event = this._onDidChangeEditable.event; + private editable: { remote: number | undefined, data: IEditableData } | undefined; + private readonly _onDidChangeEditable: Emitter = new Emitter(); + public readonly onDidChangeEditable: Event = this._onDidChangeEditable.event; constructor( @IStorageService private readonly storageService: IStorageService, @@ -220,7 +219,7 @@ class RemoteExplorerService implements IRemoteExplorerService { return this._tunnelModel; } - setEditable(remote: number, data: IEditableData | null): void { + setEditable(remote: number | undefined, data: IEditableData | null): void { if (!data) { this.editable = undefined; } else { @@ -229,7 +228,7 @@ class RemoteExplorerService implements IRemoteExplorerService { this._onDidChangeEditable.fire(remote); } - getEditableData(remote: number): IEditableData | undefined { + getEditableData(remote: number | undefined): IEditableData | undefined { return this.editable && this.editable.remote === remote ? this.editable.data : undefined; } } diff --git a/src/vs/workbench/services/remote/node/tunnelService.ts b/src/vs/workbench/services/remote/node/tunnelService.ts index 30c886758f7..d668ac3d3c6 100644 --- a/src/vs/workbench/services/remote/node/tunnelService.ts +++ b/src/vs/workbench/services/remote/node/tunnelService.ts @@ -17,7 +17,6 @@ import { ISignService } from 'vs/platform/sign/common/sign'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { findFreePort } from 'vs/base/node/ports'; -import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; export async function createRemoteTunnel(options: IConnectionOptions, tunnelRemotePort: number, tunnelLocalPort?: number): Promise { @@ -29,7 +28,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { public readonly tunnelRemotePort: number; public tunnelLocalPort!: number; - public localAddress?: URI; + public localAddress?: string; private readonly _options: IConnectionOptions; private readonly _server: net.Server; @@ -71,7 +70,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { this.tunnelLocalPort = address.port; await this._barrier.wait(); - this.localAddress = URI.from({ scheme: 'http', authority: 'localhost:' + address.port }); + this.localAddress = 'localhost:' + address.port; return this; } diff --git a/src/vs/workbench/services/search/common/searchService.ts b/src/vs/workbench/services/search/common/searchService.ts index ddbe6ecb49e..65d2ff3bd44 100644 --- a/src/vs/workbench/services/search/common/searchService.ts +++ b/src/vs/workbench/services/search/common/searchService.ts @@ -409,6 +409,11 @@ export class SearchService extends Disposable implements ISearchService { return; } + // Exclude files from the git FileSystemProvider, e.g. to prevent open staged files from showing in search results + if (resource.scheme === 'gitfs') { + return; + } + if (!this.matches(resource, query)) { return; // respect user filters } diff --git a/src/vs/workbench/services/search/node/rawSearchService.ts b/src/vs/workbench/services/search/node/rawSearchService.ts index 04fb0cf5e1f..ac159a60c92 100644 --- a/src/vs/workbench/services/search/node/rawSearchService.ts +++ b/src/vs/workbench/services/search/node/rawSearchService.ts @@ -277,7 +277,7 @@ export class SearchService implements IRawSearchService { for (const previousSearch in cache.resultsToSearchCache) { // If we narrow down, we might be able to reuse the cached results if (strings.startsWith(searchValue, previousSearch)) { - if (hasPathSep && previousSearch.indexOf(sep) < 0) { + if (hasPathSep && previousSearch.indexOf(sep) < 0 && previousSearch !== '') { continue; // since a path character widens the search for potential more matches, require it in previous search too } diff --git a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts index 9b748aef08b..7cd4ad05439 100644 --- a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts +++ b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts @@ -350,8 +350,9 @@ export class EncodingOracle extends Disposable implements IResourceEncodings { // Global settings defaultEncodingOverrides.push({ parent: this.environmentService.userRoamingDataHome, encoding: UTF8 }); - // Workspace files + // Workspace files (via extension and via untitled workspaces location) defaultEncodingOverrides.push({ extension: WORKSPACE_EXTENSION, encoding: UTF8 }); + defaultEncodingOverrides.push({ parent: this.environmentService.untitledWorkspacesHome, encoding: UTF8 }); // Folder Settings this.contextService.getWorkspace().folders.forEach(folder => { diff --git a/src/vs/workbench/services/themes/common/colorThemeData.ts b/src/vs/workbench/services/themes/common/colorThemeData.ts index 9639f59e0d1..d093aca70c1 100644 --- a/src/vs/workbench/services/themes/common/colorThemeData.ts +++ b/src/vs/workbench/services/themes/common/colorThemeData.ts @@ -30,7 +30,7 @@ let tokenClassificationRegistry = getTokenClassificationRegistry(); const tokenGroupToScopesMap = { comments: ['comment', 'punctuation.definition.comment'], - strings: ['string'], + strings: ['string', 'meta.embedded.assembly'], keywords: ['keyword - keyword.operator', 'keyword.control', 'storage', 'storage.type'], numbers: ['constant.numeric'], types: ['entity.name.type', 'entity.name.class', 'support.type', 'support.class'], diff --git a/src/vs/workbench/services/themes/test/electron-browser/tokenStyleResolving.test.ts b/src/vs/workbench/services/themes/test/electron-browser/tokenStyleResolving.test.ts index b256164b7b4..0e84f4e22c4 100644 --- a/src/vs/workbench/services/themes/test/electron-browser/tokenStyleResolving.test.ts +++ b/src/vs/workbench/services/themes/test/electron-browser/tokenStyleResolving.test.ts @@ -6,7 +6,7 @@ import { ColorThemeData } from 'vs/workbench/services/themes/common/colorThemeData'; import * as assert from 'assert'; import { ITokenColorCustomizations } from 'vs/workbench/services/themes/common/workbenchThemeService'; -import { TokenStyle, comments, variables, types, functions, keywords, numbers, strings, getTokenClassificationRegistry } from 'vs/platform/theme/common/tokenClassificationRegistry'; +import { TokenStyle, getTokenClassificationRegistry } from 'vs/platform/theme/common/tokenClassificationRegistry'; import { Color } from 'vs/base/common/color'; import { isString } from 'vs/base/common/types'; import { FileService } from 'vs/platform/files/common/fileService'; @@ -107,13 +107,13 @@ suite('Themes - TokenStyleResolving', () => { assert.equal(themeData.isLoaded, true); assertTokenStyles(themeData, { - [comments]: ts('#88846f', undefinedStyle), - [variables]: ts('#F8F8F2', unsetStyle), - [types]: ts('#A6E22E', { underline: true }), - [functions]: ts('#A6E22E', unsetStyle), - [strings]: ts('#E6DB74', undefinedStyle), - [numbers]: ts('#AE81FF', undefinedStyle), - [keywords]: ts('#F92672', undefinedStyle) + 'comment': ts('#88846f', undefinedStyle), + 'variable': ts('#F8F8F2', unsetStyle), + 'type': ts('#A6E22E', { underline: true }), + 'function': ts('#A6E22E', unsetStyle), + 'string': ts('#E6DB74', undefinedStyle), + 'number': ts('#AE81FF', undefinedStyle), + 'keyword': ts('#F92672', undefinedStyle) }); }); @@ -127,13 +127,13 @@ suite('Themes - TokenStyleResolving', () => { assert.equal(themeData.isLoaded, true); assertTokenStyles(themeData, { - [comments]: ts('#6A9955', undefinedStyle), - [variables]: ts('#9CDCFE', undefinedStyle), - [types]: ts('#4EC9B0', undefinedStyle), - [functions]: ts('#DCDCAA', undefinedStyle), - [strings]: ts('#CE9178', undefinedStyle), - [numbers]: ts('#B5CEA8', undefinedStyle), - [keywords]: ts('#C586C0', undefinedStyle) + 'comment': ts('#6A9955', undefinedStyle), + 'variable': ts('#9CDCFE', undefinedStyle), + 'type': ts('#4EC9B0', undefinedStyle), + 'function': ts('#DCDCAA', undefinedStyle), + 'string': ts('#CE9178', undefinedStyle), + 'number': ts('#B5CEA8', undefinedStyle), + 'keyword': ts('#C586C0', undefinedStyle) }); }); @@ -147,13 +147,13 @@ suite('Themes - TokenStyleResolving', () => { assert.equal(themeData.isLoaded, true); assertTokenStyles(themeData, { - [comments]: ts('#008000', undefinedStyle), - [variables]: ts(undefined, undefinedStyle), - [types]: ts(undefined, undefinedStyle), - [functions]: ts(undefined, undefinedStyle), - [strings]: ts('#a31515', undefinedStyle), - [numbers]: ts('#09885a', undefinedStyle), - [keywords]: ts('#0000ff', undefinedStyle) + 'comment': ts('#008000', undefinedStyle), + 'variable': ts(undefined, undefinedStyle), + 'type': ts(undefined, undefinedStyle), + 'function': ts(undefined, undefinedStyle), + 'string': ts('#a31515', undefinedStyle), + 'number': ts('#09885a', undefinedStyle), + 'keyword': ts('#0000ff', undefinedStyle) }); }); @@ -167,13 +167,13 @@ suite('Themes - TokenStyleResolving', () => { assert.equal(themeData.isLoaded, true); assertTokenStyles(themeData, { - [comments]: ts('#7ca668', undefinedStyle), - [variables]: ts('#9CDCFE', undefinedStyle), - [types]: ts('#4EC9B0', undefinedStyle), - [functions]: ts('#DCDCAA', undefinedStyle), - [strings]: ts('#ce9178', undefinedStyle), - [numbers]: ts('#b5cea8', undefinedStyle), - [keywords]: ts('#C586C0', undefinedStyle) + 'comment': ts('#7ca668', undefinedStyle), + 'variable': ts('#9CDCFE', undefinedStyle), + 'type': ts('#4EC9B0', undefinedStyle), + 'function': ts('#DCDCAA', undefinedStyle), + 'string': ts('#ce9178', undefinedStyle), + 'number': ts('#b5cea8', undefinedStyle), + 'keyword': ts('#C586C0', undefinedStyle) }); }); @@ -187,13 +187,13 @@ suite('Themes - TokenStyleResolving', () => { assert.equal(themeData.isLoaded, true); assertTokenStyles(themeData, { - [comments]: ts('#a57a4c', undefinedStyle), - [variables]: ts('#dc3958', undefinedStyle), - [types]: ts('#f06431', undefinedStyle), - [functions]: ts('#8ab1b0', undefinedStyle), - [strings]: ts('#889b4a', undefinedStyle), - [numbers]: ts('#f79a32', undefinedStyle), - [keywords]: ts('#98676a', undefinedStyle) + 'comment': ts('#a57a4c', undefinedStyle), + 'variable': ts('#dc3958', undefinedStyle), + 'type': ts('#f06431', undefinedStyle), + 'function': ts('#8ab1b0', undefinedStyle), + 'string': ts('#889b4a', undefinedStyle), + 'number': ts('#f79a32', undefinedStyle), + 'keyword': ts('#98676a', undefinedStyle) }); }); @@ -207,13 +207,13 @@ suite('Themes - TokenStyleResolving', () => { assert.equal(themeData.isLoaded, true); assertTokenStyles(themeData, { - [comments]: ts('#384887', undefinedStyle), - [variables]: ts(undefined, unsetStyle), - [types]: ts('#ffeebb', { underline: true }), - [functions]: ts('#ddbb88', unsetStyle), - [strings]: ts('#22aa44', undefinedStyle), - [numbers]: ts('#f280d0', undefinedStyle), - [keywords]: ts('#225588', undefinedStyle) + 'comment': ts('#384887', undefinedStyle), + 'variable': ts(undefined, unsetStyle), + 'type': ts('#ffeebb', { underline: true }), + 'function': ts('#ddbb88', unsetStyle), + 'string': ts('#22aa44', undefinedStyle), + 'number': ts('#f280d0', undefinedStyle), + 'keyword': ts('#225588', undefinedStyle) }); }); @@ -301,8 +301,8 @@ suite('Themes - TokenStyleResolving', () => { const themeData = ColorThemeData.createLoadedEmptyTheme('test', 'test'); themeData.setCustomColors({ 'editor.foreground': '#000000' }); themeData.setCustomTokenStyleRules({ - 'types': '#ff0000', - 'classes': { foreground: '#0000ff', fontStyle: 'italic' }, + 'type': '#ff0000', + 'class': { foreground: '#0000ff', fontStyle: 'italic' }, '*.static': { fontStyle: 'bold' }, '*.declaration': { fontStyle: 'italic' }, '*.async.static': { fontStyle: 'italic underline' }, @@ -310,14 +310,14 @@ suite('Themes - TokenStyleResolving', () => { }); assertTokenStyles(themeData, { - 'types': ts('#ff0000', undefinedStyle), - 'types.static': ts('#ff0000', { bold: true }), - 'types.static.declaration': ts('#ff0000', { bold: true, italic: true }), - 'classes': ts('#0000ff', { italic: true }), - 'classes.static.declaration': ts('#0000ff', { bold: true, italic: true }), - 'classes.declaration': ts('#0000ff', { italic: true }), - 'classes.declaration.async': ts('#000fff', { underline: true, italic: false }), - 'classes.declaration.async.static': ts('#000fff', { italic: true, underline: true, bold: true }), + 'type': ts('#ff0000', undefinedStyle), + 'type.static': ts('#ff0000', { bold: true }), + 'type.static.declaration': ts('#ff0000', { bold: true, italic: true }), + 'class': ts('#0000ff', { italic: true }), + 'class.static.declaration': ts('#0000ff', { bold: true, italic: true }), + 'class.declaration': ts('#0000ff', { italic: true }), + 'class.declaration.async': ts('#000fff', { underline: true, italic: false }), + 'class.declaration.async.static': ts('#000fff', { italic: true, underline: true, bold: true }), }); }); diff --git a/src/vs/workbench/services/userDataSync/common/settingsMergeService.ts b/src/vs/workbench/services/userDataSync/common/settingsMergeService.ts index 973a7fd7a68..3e3c652ff1f 100644 --- a/src/vs/workbench/services/userDataSync/common/settingsMergeService.ts +++ b/src/vs/workbench/services/userDataSync/common/settingsMergeService.ts @@ -156,7 +156,7 @@ class SettingsMergeService implements ISettingsMergeService { const remote = parse(remoteContent); const remoteModel = this.modelService.createModel(localContent, this.modeService.create('jsonc')); const ignored = ignoredSettings.reduce((set, key) => { set.add(key); return set; }, new Set()); - for (const key of Object.keys(ignoredSettings)) { + for (const key of ignoredSettings) { if (ignored.has(key)) { this.editSetting(remoteModel, key, undefined); this.editSetting(remoteModel, key, remote[key]); @@ -166,8 +166,8 @@ class SettingsMergeService implements ISettingsMergeService { } private editSetting(model: ITextModel, key: string, value: any | undefined): void { - const insertSpaces = false; - const tabSize = 4; + const insertSpaces = model.getOptions().insertSpaces; + const tabSize = model.getOptions().tabSize; const eol = model.getEOL(); const edit = setProperty(model.getValue(), [key], value, { tabSize, insertSpaces, eol })[0]; if (edit) { diff --git a/src/vs/workbench/services/userDataSync/common/userDataSyncUtil.ts b/src/vs/workbench/services/userDataSync/common/userDataSyncUtil.ts new file mode 100644 index 00000000000..947f5521b8b --- /dev/null +++ b/src/vs/workbench/services/userDataSync/common/userDataSyncUtil.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { FormattingOptions } from 'vs/base/common/jsonFormatter'; +import { URI } from 'vs/base/common/uri'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { ITextResourcePropertiesService, ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; + +class UserDataSyncUtilService implements IUserDataSyncUtilService { + + _serviceBrand: undefined; + + constructor( + @IKeybindingService private readonly keybindingsService: IKeybindingService, + @ITextModelService private readonly textModelService: ITextModelService, + @ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService, + @ITextResourceConfigurationService private readonly textResourceConfigurationService: ITextResourceConfigurationService, + ) { } + + public async resolveUserBindings(userBindings: string[]): Promise> { + const keys: IStringDictionary = {}; + for (const userbinding of userBindings) { + keys[userbinding] = this.keybindingsService.resolveUserBinding(userbinding).map(part => part.getUserSettingsLabel()).join(' '); + } + return keys; + } + + async resolveFormattingOptions(resource: URI): Promise { + try { + const modelReference = await this.textModelService.createModelReference(resource); + const { insertSpaces, tabSize } = modelReference.object.textEditorModel.getOptions(); + const eol = modelReference.object.textEditorModel.getEOL(); + modelReference.dispose(); + return { eol, insertSpaces, tabSize }; + } catch (e) { + } + return { + eol: this.textResourcePropertiesService.getEOL(resource), + insertSpaces: this.textResourceConfigurationService.getValue(resource, 'editor.insertSpaces'), + tabSize: this.textResourceConfigurationService.getValue(resource, 'editor.tabSize') + }; + } +} + +registerSingleton(IUserDataSyncUtilService, UserDataSyncUtilService); diff --git a/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts b/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts index c26f9a10ecf..01af7282519 100644 --- a/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts @@ -29,7 +29,7 @@ suite('Breadcrumb Model', function () { test('only uri, inside workspace', function () { - let model = new EditorBreadcrumbsModel(URI.parse('foo:/bar/baz/ws/some/path/file.ts'), undefined, configService, workspaceService); + let model = new EditorBreadcrumbsModel(URI.parse('foo:/bar/baz/ws/some/path/file.ts'), undefined, configService, configService, workspaceService); let elements = model.getElements(); assert.equal(elements.length, 3); @@ -44,7 +44,7 @@ suite('Breadcrumb Model', function () { test('only uri, outside workspace', function () { - let model = new EditorBreadcrumbsModel(URI.parse('foo:/outside/file.ts'), undefined, configService, workspaceService); + let model = new EditorBreadcrumbsModel(URI.parse('foo:/outside/file.ts'), undefined, configService, configService, workspaceService); let elements = model.getElements(); assert.equal(elements.length, 2); diff --git a/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts b/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts index c05a1d72fd8..d505968d588 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts @@ -120,7 +120,7 @@ suite('ExtHostWorkspace', function () { assert.equal(ws.getPath(), undefined); ws = createExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.file('Folder'), 0), aWorkspaceFolderData(URI.file('Another/Folder'), 1)] }, new NullLogService()); - assert.equal(ws.getPath()!.replace(/\\/g, '/'), '/Folder'); + assert.equal(ws.getPath(), undefined); ws = createExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.file('/Folder'), 0)] }, new NullLogService()); assert.equal(ws.getPath()!.replace(/\\/g, '/'), '/Folder'); diff --git a/src/vs/workbench/test/electron-browser/api/semanticTokens.test.ts b/src/vs/workbench/test/electron-browser/api/semanticTokens.test.ts deleted file mode 100644 index 978888ef532..00000000000 --- a/src/vs/workbench/test/electron-browser/api/semanticTokens.test.ts +++ /dev/null @@ -1,343 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import { URI } from 'vs/base/common/uri'; -import * as types from 'vs/workbench/api/common/extHostTypes'; -import { TestRPCProtocol } from 'vs/workbench/test/electron-browser/api/testRPCProtocol'; -import { SemanticColoringAdapter, SemanticColoringConstants } from 'vs/workbench/api/common/extHostLanguageFeatures'; -import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; -import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { ExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; -import * as vscode from 'vscode'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { decodeSemanticTokensDto, ISemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokens'; -import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; - -suite('SemanticColoringAdapter', () => { - - const resource = URI.parse('foo:bar'); - const rpcProtocol = new TestRPCProtocol(); - - const initialText = [ - 'const enum E01 {}', - 'const enum E02 {}', - 'const enum E03 {}', - 'const enum E04 {}', - 'const enum E05 {}', - 'const enum E06 {}', - 'const enum E07 {}', - 'const enum E08 {}', - 'const enum E09 {}', - 'const enum E10 {}', - 'const enum E11 {}', - 'const enum E12 {}', - 'const enum E13 {}', - 'const enum E14 {}', - 'const enum E15 {}', - 'const enum E16 {}', - 'const enum E17 {}', - 'const enum E18 {}', - 'const enum E19 {}', - 'const enum E20 {}', - 'const enum E21 {}', - 'const enum E22 {}', - 'const enum E23 {}', - ].join('\n'); - - const extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol); - extHostDocumentsAndEditors.$acceptDocumentsAndEditorsDelta({ - addedDocuments: [{ - isDirty: false, - versionId: 1, - modeId: 'javascript', - uri: resource, - lines: initialText.split(/\n/), - EOL: '\n', - }] - }); - const extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); - rpcProtocol.set(ExtHostContext.ExtHostDocuments, extHostDocuments); - - const semanticTokensProvider = new class implements vscode.SemanticColoringProvider { - provideSemanticColoring(document: vscode.TextDocument, token: vscode.CancellationToken): types.SemanticColoring { - const lines = document.getText().split(/\r\n|\r|\n/g); - const tokens: number[] = []; - const pushToken = (line: number, startCharacter: number, endCharacter: number, type: number) => { - tokens.push(line); - tokens.push(startCharacter); - tokens.push(endCharacter); - tokens.push(type); - tokens.push(0); - }; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const m = line.match(/^(const enum )([\w\d]+) \{\}/); - if (m) { - pushToken(i, m[1].length, m[1].length + m[2].length, parseInt(m[2].substr(1))); - } - } - return new types.SemanticColoring([new types.SemanticColoringArea(0, new Uint32Array(tokens))]); - } - }; - - let adapter: SemanticColoringAdapter; - let doc: ExtHostDocumentData; - - setup(() => { - adapter = new SemanticColoringAdapter(extHostDocuments, semanticTokensProvider, 10, SemanticColoringConstants.DesiredMaxAreas, 5); - doc = extHostDocumentsAndEditors.getDocument(resource)!; - const docLineCount = doc.document.lineCount; - const allRange = { startLineNumber: 1, startColumn: 1, endLineNumber: docLineCount, endColumn: doc.document.lineAt(docLineCount - 1).text.length + 1 }; - doc.onEvents({ - versionId: 1, - eol: '\n', - changes: [{ - range: allRange, - rangeOffset: 0, - rangeLength: 0, - text: initialText - }] - }); - }); - - type SimpleTokensDto = { type: 'full'; line: number; tokens: number[]; } | { type: 'delta'; line: number; oldIndex: number }; - - function assertDTO(actual: ISemanticTokensDto, expected: SimpleTokensDto[]): void { - const simpleActual: SimpleTokensDto[] = actual.areas.map((area) => { - if (area.type === 'full') { - const tokenCount = (area.data.length / 5) | 0; - let tokens: number[] = []; - for (let i = 0; i < tokenCount; i++) { - tokens.push(area.data[5 * i]); - } - return { - type: 'full', - line: area.line, - tokens: tokens - }; - } - return { - type: 'delta', - line: area.line, - oldIndex: area.oldIndex - }; - }); - assert.deepEqual(simpleActual, expected); - } - - test('single area - breaks it up', async () => { - const dto = (await adapter.provideSemanticColoring(resource, 0, CancellationToken.None))!; - const result = decodeSemanticTokensDto(dto); - assertDTO(result, [ - { type: 'full', line: 1, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, - { type: 'full', line: 11, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, - { type: 'full', line: 21, tokens: [0, 1, 2] }, - ]); - }); - - test('single area - after a not important change', async () => { - const result1 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, 0, CancellationToken.None))!); - assertDTO(result1, [ - { type: 'full', line: 1, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, - { type: 'full', line: 11, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, - { type: 'full', line: 21, tokens: [0, 1, 2] }, - ]); - - doc.onEvents({ - versionId: 2, - eol: '\n', - changes: [{ - range: { startLineNumber: 2, startColumn: 18, endLineNumber: 2, endColumn: 18 }, - rangeOffset: 0, - rangeLength: 0, - text: '//' - }] - }); - - const result2 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, result1.id, CancellationToken.None))!); - assertDTO(result2, [ - { type: 'delta', line: 1, oldIndex: 0 }, - { type: 'delta', line: 11, oldIndex: 1 }, - { type: 'delta', line: 21, oldIndex: 2 }, - ]); - adapter.releaseSemanticColoring(result1.id); - }); - - test('single area - after a single removal in the first block', async () => { - const result1 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, 0, CancellationToken.None))!); - assertDTO(result1, [ - { type: 'full', line: 1, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, - { type: 'full', line: 11, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, - { type: 'full', line: 21, tokens: [0, 1, 2] }, - ]); - - doc.onEvents({ - versionId: 2, - eol: '\n', - changes: [{ - range: { startLineNumber: 2, startColumn: 1, endLineNumber: 2, endColumn: 1 }, - rangeOffset: 0, - rangeLength: 0, - text: '//' - }] - }); - - const result2 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, result1.id, CancellationToken.None))!); - assertDTO(result2, [ - { type: 'full', line: 1, tokens: [0, 2, 3, 4, 5, 6, 7, 8, 9] }, - { type: 'delta', line: 11, oldIndex: 1 }, - { type: 'delta', line: 21, oldIndex: 2 }, - ]); - adapter.releaseSemanticColoring(result1.id); - }); - - test('single area - after a not important change', async () => { - const result1 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, 0, CancellationToken.None))!); - assertDTO(result1, [ - { type: 'full', line: 1, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, - { type: 'full', line: 11, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, - { type: 'full', line: 21, tokens: [0, 1, 2] }, - ]); - - doc.onEvents({ - versionId: 2, - eol: '\n', - changes: [{ - range: { startLineNumber: 2, startColumn: 18, endLineNumber: 2, endColumn: 18 }, - rangeOffset: 0, - rangeLength: 0, - text: '//' - }] - }); - - const result2 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, result1.id, CancellationToken.None))!); - assertDTO(result2, [ - { type: 'delta', line: 1, oldIndex: 0 }, - { type: 'delta', line: 11, oldIndex: 1 }, - { type: 'delta', line: 21, oldIndex: 2 }, - ]); - adapter.releaseSemanticColoring(result1.id); - }); - - test('single area - after a down shift of all the blocks', async () => { - const result1 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, 0, CancellationToken.None))!); - assertDTO(result1, [ - { type: 'full', line: 1, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, - { type: 'full', line: 11, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, - { type: 'full', line: 21, tokens: [0, 1, 2] }, - ]); - - doc.onEvents({ - versionId: 2, - eol: '\n', - changes: [{ - range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, - rangeOffset: 0, - rangeLength: 0, - text: '\n' - }] - }); - - const result2 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, result1.id, CancellationToken.None))!); - assertDTO(result2, [ - { type: 'delta', line: 2, oldIndex: 0 }, - { type: 'delta', line: 12, oldIndex: 1 }, - { type: 'delta', line: 22, oldIndex: 2 }, - ]); - adapter.releaseSemanticColoring(result1.id); - }); - - test('single area - after a single removal in the last block', async () => { - const result1 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, 0, CancellationToken.None))!); - assertDTO(result1, [ - { type: 'full', line: 1, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, - { type: 'full', line: 11, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, - { type: 'full', line: 21, tokens: [0, 1, 2] }, - ]); - - doc.onEvents({ - versionId: 2, - eol: '\n', - changes: [{ - range: { startLineNumber: 22, startColumn: 1, endLineNumber: 22, endColumn: 1 }, - rangeOffset: 0, - rangeLength: 0, - text: '//' - }] - }); - - const result2 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, result1.id, CancellationToken.None))!); - assertDTO(result2, [ - { type: 'delta', line: 1, oldIndex: 0 }, - { type: 'delta', line: 11, oldIndex: 1 }, - { type: 'full', line: 21, tokens: [0, 2] }, - ]); - adapter.releaseSemanticColoring(result1.id); - }); - - test('single area - after a single addition in the first block', async () => { - const result1 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, 0, CancellationToken.None))!); - assertDTO(result1, [ - { type: 'full', line: 1, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, - { type: 'full', line: 11, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] }, - { type: 'full', line: 21, tokens: [0, 1, 2] }, - ]); - - doc.onEvents({ - versionId: 2, - eol: '\n', - changes: [{ - range: { startLineNumber: 2, startColumn: 1, endLineNumber: 2, endColumn: 1 }, - rangeOffset: 0, - rangeLength: 0, - text: 'const enum E00 {}\n' - }] - }); - - const result2 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, result1.id, CancellationToken.None))!); - assertDTO(result2, [ - { type: 'full', line: 1, tokens: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }, - { type: 'delta', line: 12, oldIndex: 1 }, - { type: 'delta', line: 22, oldIndex: 2 }, - ]); - adapter.releaseSemanticColoring(result1.id); - }); - - test('going from empty to 1 semantic token', async () => { - doc.onEvents({ - versionId: 2, - eol: '\n', - changes: [{ - range: { startLineNumber: 1, startColumn: 1, endLineNumber: 23, endColumn: 18 }, - rangeOffset: 0, - rangeLength: 0, - text: '' - }] - }); - - const result1 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, 0, CancellationToken.None))!); - assertDTO(result1, [ - { type: 'full', line: 1, tokens: [] }, - ]); - - doc.onEvents({ - versionId: 3, - eol: '\n', - changes: [{ - range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, - rangeOffset: 0, - rangeLength: 0, - text: 'const enum E01 {}\n' - }] - }); - - const result2 = decodeSemanticTokensDto((await adapter.provideSemanticColoring(resource, result1.id, CancellationToken.None))!); - assertDTO(result2, [ - { type: 'full', line: 1, tokens: [0] } - ]); - adapter.releaseSemanticColoring(result1.id); - }); -}); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index f229fed7853..b34643ef998 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -80,6 +80,7 @@ import 'vs/workbench/services/extensionManagement/common/extensionEnablementServ import 'vs/workbench/services/notification/common/notificationService'; import 'vs/workbench/services/extensions/common/staticExtensions'; import 'vs/workbench/services/userDataSync/common/settingsMergeService'; +import 'vs/workbench/services/userDataSync/common/userDataSyncUtil'; import 'vs/workbench/services/path/common/remotePathService'; import 'vs/workbench/services/remote/common/remoteExplorerService'; import 'vs/workbench/services/workingCopy/common/workingCopyService'; @@ -262,7 +263,4 @@ import 'vs/workbench/contrib/userDataSync/browser/userDataSync.contribution'; // Code Actions import 'vs/workbench/contrib/codeActions/common/codeActions.contribution'; -// Test Custom Editors -import 'vs/workbench/contrib/testCustomEditors/browser/testCustomEditors'; - //#endregion diff --git a/test/automation/src/debug.ts b/test/automation/src/debug.ts index 4739d8a9917..b4ea77a8f78 100644 --- a/test/automation/src/debug.ts +++ b/test/automation/src/debug.ts @@ -11,7 +11,7 @@ import { Editor } from './editor'; import { IElement } from '../src/driver'; const VIEWLET = 'div[id="workbench.view.debug"]'; -const DEBUG_VIEW = `${VIEWLET} .debug-view-content`; +const DEBUG_VIEW = `${VIEWLET}`; const CONFIGURE = `div[id="workbench.parts.sidebar"] .actions-container .codicon-gear`; const STOP = `.debug-toolbar .action-label[title*="Stop"]`; const STEP_OVER = `.debug-toolbar .action-label[title*="Step Over"]`; diff --git a/test/smoke/src/areas/debug/debug.test.ts b/test/smoke/src/areas/debug/debug.test.ts index 429fc7852d2..ee19ff9fd16 100644 --- a/test/smoke/src/areas/debug/debug.test.ts +++ b/test/smoke/src/areas/debug/debug.test.ts @@ -17,7 +17,7 @@ export function setup() { await app.workbench.debug.openDebugViewlet(); await app.workbench.quickopen.openFile('app.js'); - await app.workbench.debug.configure(); + await app.workbench.quickopen.runCommand('Debug: Open launch.json'); const launchJsonPath = path.join(app.workspacePathOrFolder, '.vscode', 'launch.json'); const content = fs.readFileSync(launchJsonPath, 'utf8'); diff --git a/tslint.json b/tslint.json index a0feadaf2c0..06adddd9c6e 100644 --- a/tslint.json +++ b/tslint.json @@ -75,6 +75,7 @@ "target": "**/vs/base/test/common/**", "restrictions": [ "assert", + "sinon", "vs/nls", "**/vs/base/common/**", "**/vs/base/test/common/**" @@ -101,6 +102,7 @@ "target": "**/vs/base/test/browser/**", "restrictions": [ "assert", + "sinon", "vs/nls", "**/vs/base/{common,browser}/**", "**/vs/base/test/{common,browser}/**" @@ -237,6 +239,7 @@ "target": "**/vs/editor/test/common/**", "restrictions": [ "assert", + "sinon", "vs/nls", "**/vs/base/common/**", "**/vs/platform/*/common/**", @@ -259,6 +262,7 @@ "target": "**/vs/editor/test/browser/**", "restrictions": [ "assert", + "sinon", "vs/nls", "**/vs/base/{common,browser}/**", "**/vs/platform/*/{common,browser}/**", @@ -281,6 +285,7 @@ "target": "**/vs/editor/standalone/test/common/**", "restrictions": [ "assert", + "sinon", "vs/nls", "**/vs/base/common/**", "**/vs/platform/*/common/**", @@ -306,6 +311,7 @@ "target": "**/vs/editor/standalone/test/browser/**", "restrictions": [ "assert", + "sinon", "vs/nls", "**/vs/base/{common,browser}/**", "**/vs/platform/*/{common,browser}/**", @@ -319,6 +325,7 @@ "target": "**/vs/editor/contrib/*/test/**", "restrictions": [ "assert", + "sinon", "vs/nls", "**/vs/base/{common,browser}/**", "**/vs/base/test/{common,browser}/**", diff --git a/yarn.lock b/yarn.lock index a4b936375f7..edbbc9743d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9374,10 +9374,10 @@ xterm-addon-webgl@0.4.0-beta.11: resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.4.0-beta.11.tgz#0e4a7242e2353cf74aba55e5a5bdc0b4ec87ad10" integrity sha512-AteDxm1RFy1WnjY9r5iJSETozLebvUkR+jextdZk/ASsK21vsYK0DuVWwRI8afgiN2hUVhxcxuHEJUOV+CJDQA== -xterm@4.3.0-beta.28: - version "4.3.0-beta.28" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.3.0-beta.28.tgz#80f7c4ba8f6ee3c953e6f33f8ce5aef08d5a8354" - integrity sha512-WWZ4XCvce5h+klL6ObwtMauJff/n2KGGOwJJkDbJhrAjVy2a77GKgAedJTDDFGgKJ6ix1d7puHtVSSKflIVaDQ== +xterm@4.3.0-beta.28.vscode.1: + version "4.3.0-beta.28.vscode.1" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.3.0-beta.28.vscode.1.tgz#89b85398b5801708e833d08bf92b2124fa128943" + integrity sha512-JNHNZyDtAWnybJTrenPzD6g/yXpHOvPqmjau91Up4onRbjQYMSNlth17SqaES68DKn/+4kcIl2c/RG5SXJjvGw== y18n@^3.2.1: version "3.2.1"