diff --git a/.github/classifier.json b/.github/classifier.json new file mode 100644 index 0000000000..9607d5f137 --- /dev/null +++ b/.github/classifier.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/vscode-github-triage-actions/master/classifier/apply/apply-labels/classifier-config.schema.json", + "assignees": { + "JacksonKearl": { + "assign": true + } + }, + "labels": { + "search-editor": { + "applyLabel": true, + "assign": [ + "JacksonKearl" + ] + } + } +} diff --git a/.github/classifier.yml b/.github/classifier.yml deleted file mode 100644 index 754726f1a1..0000000000 --- a/.github/classifier.yml +++ /dev/null @@ -1,36 +0,0 @@ -{ - perform: true, - alwaysRequireAssignee: false, - labelsRequiringAssignee: [], - defaultLabel: 'Triage: Needed', - defaultAssignee: '', - autoAssignees: { - Area - Acquisition: [], - Area - Azure: [], - Area - Backup\Restore: [], - Area - Charting\Insights: [], - Area - Connection: [ charles-gagnon ], - Area - DacFX: [], - Area - Dashboard: [], - Area - Data Explorer: [], - Area - Edit Data: [], - Area - Extensibility: [], - Area - External Table: [], - Area - Fundamentals: [], - Area - Language Service: [ charles-gagnon ], - Area - Localization: [], - Area - Notebooks: [ chlafreniere ], - Area - Performance: [], - Area - Query Editor: [ anthonydresser ], - Area - Query Plan: [], - Area - Reliability: [], - Area - Resource Deployment: [], - Area - Schema Compare: [], - Area - Shell: [], - Area - SQL Agent: [], - Area - SQL Import: [], - Area - SQL Profiler: [], - Area - SQL 2019: [], - Area - SSMS Integration: [] - } -} diff --git a/.github/workflows/classifier-apply.yml b/.github/workflows/classifier-apply.yml new file mode 100644 index 0000000000..80b61b5d31 --- /dev/null +++ b/.github/workflows/classifier-apply.yml @@ -0,0 +1,43 @@ +name: "Classifier: Apply" +on: + schedule: + - cron: 0,30 * * * * + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Checkout Actions + uses: actions/checkout@v2 + with: + repository: 'microsoft/vscode-github-triage-actions' + ref: v15 + path: ./actions + - name: Install Actions + run: npm install --production --prefix ./actions + - name: Install Additional Dependencies + # Pulls in a bunch of other packages that arent needed for the rest of the actions + run: npm install @azure/storage-blob@12 + - name: "Run Classifier: Scraper" + uses: ./actions/classifier/apply/fetch-issues + with: + # slightly overlapping to protect against issues slipping through the cracks if a run is delayed + from: 45 + until: 5 + blobContainerName: classifier-models + blobStorageKey: ${{secrets.AZURE_BLOB_STORAGE_CONNECTION_STRING}} + - name: Set up Python 3.7 + uses: actions/setup-python@v1 + with: + python-version: 3.7 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install --upgrade numpy scipy scikit-learn joblib nltk + - name: "Run Classifier: Generator" + run: python ./actions/classifier/apply/generate-labels/main.py + - name: "Run Classifier: Labeler" + uses: ./actions/classifier/apply/apply-labels + with: + config-path: classifier + token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} diff --git a/.vscode/launch.json b/.vscode/launch.json index a631078f52..1b823d9c03 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -145,6 +145,25 @@ "order": 2 } }, + { + "type": "node", + "request": "launch", + "name": "Main Process", + "runtimeExecutable": "${workspaceFolder}/scripts/code.sh", + "windows": { + "runtimeExecutable": "${workspaceFolder}/scripts/code.bat", + }, + "runtimeArgs": [ + "--no-cached-data" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "presentation": { + "group": "1_vscode", + "order": 1 + } + }, { "type": "chrome", "request": "launch", diff --git a/build/azure-pipelines/darwin/continuous-build-darwin.yml b/build/azure-pipelines/darwin/continuous-build-darwin.yml index 221ad7c199..553e683255 100644 --- a/build/azure-pipelines/darwin/continuous-build-darwin.yml +++ b/build/azure-pipelines/darwin/continuous-build-darwin.yml @@ -2,54 +2,76 @@ steps: - task: NodeTool@0 inputs: versionSpec: "12.13.0" + - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@3 # {{SQL CARBON EDIT}} update version inputs: versionSpec: "1.x" + - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 inputs: keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock' targetfolder: '**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules' vstsFeed: 'npm-cache' # {{SQL CARBON EDIT}} update build cache + - script: | CHILD_CONCURRENCY=1 yarn --frozen-lockfile displayName: Install Dependencies condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) + - task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1 inputs: keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock' targetfolder: '**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules' vstsFeed: 'npm-cache' # {{SQL CARBON EDIT}} update build cache condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) + - script: | yarn electron x64 displayName: Download Electron + - script: | yarn gulp hygiene displayName: Run Hygiene Checks + - script: | # {{SQL CARBON EDIT}} add step yarn strict-vscode displayName: Run Strict Null Check. + # - script: | {{SQL CARBON EDIT}} remove step # yarn monaco-compile-check # displayName: Run Monaco Editor Checks + - script: | yarn valid-layers-check displayName: Run Valid Layers Checks + - script: | yarn compile displayName: Compile Sources + # - script: | {{SQL CARBON EDIT}} remove step # yarn download-builtin-extensions # displayName: Download Built-in Extensions + - script: | ./scripts/test.sh --tfs "Unit Tests" displayName: Run Unit Tests (Electron) + # - script: | {{SQL CARBON EDIT}} disable # yarn test-browser --browser chromium --browser webkit --browser firefox # displayName: Run Unit Tests (Browser) + # - script: | {{SQL CARBON EDIT}} disable # ./scripts/test-integration.sh --tfs "Integration Tests" # displayName: Run Integration Tests (Electron) + +- task: PublishPipelineArtifact@0 + inputs: + artifactName: crash-dump-macos + targetPath: .build/crashes + displayName: 'Publish Crash Reports' + condition: succeededOrFailed() + - task: PublishTestResults@2 displayName: Publish Tests Results inputs: diff --git a/build/azure-pipelines/darwin/entitlements.plist b/build/azure-pipelines/darwin/entitlements.plist index be8b7163da..6631ffa6f2 100644 --- a/build/azure-pipelines/darwin/entitlements.plist +++ b/build/azure-pipelines/darwin/entitlements.plist @@ -2,13 +2,5 @@ - com.apple.security.cs.allow-jit - - com.apple.security.cs.allow-unsigned-executable-memory - - com.apple.security.cs.disable-library-validation - - com.apple.security.cs.allow-dyld-environment-variables - diff --git a/build/azure-pipelines/darwin/helper-gpu-entitlements.plist b/build/azure-pipelines/darwin/helper-gpu-entitlements.plist new file mode 100644 index 0000000000..4efe1ce508 --- /dev/null +++ b/build/azure-pipelines/darwin/helper-gpu-entitlements.plist @@ -0,0 +1,8 @@ + + + + + com.apple.security.cs.allow-jit + + + diff --git a/build/azure-pipelines/darwin/helper-plugin-entitlements.plist b/build/azure-pipelines/darwin/helper-plugin-entitlements.plist new file mode 100644 index 0000000000..7cd9df032b --- /dev/null +++ b/build/azure-pipelines/darwin/helper-plugin-entitlements.plist @@ -0,0 +1,10 @@ + + + + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + + diff --git a/build/azure-pipelines/darwin/helper-renderer-entitlements.plist b/build/azure-pipelines/darwin/helper-renderer-entitlements.plist new file mode 100644 index 0000000000..be8b7163da --- /dev/null +++ b/build/azure-pipelines/darwin/helper-renderer-entitlements.plist @@ -0,0 +1,14 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + com.apple.security.cs.allow-dyld-environment-variables + + + diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index e354e28dc3..175ec6c111 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -152,15 +152,29 @@ steps: displayName: Run smoke tests (Browser) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) +- task: PublishPipelineArtifact@0 + inputs: + artifactName: crash-dump-macos + targetPath: .build/crashes + displayName: 'Publish Crash Reports' + condition: succeededOrFailed() + - script: | set -e + APP_ROOT=$(agent.builddirectory)/VSCode-darwin + APP_NAME="`ls $APP_ROOT | head -n 1`" + HELPER_APP_NAME="`echo $APP_NAME | sed -e 's/^Visual Studio //;s/\.app$//'`" + APP_FRAMEWORK_PATH="$APP_ROOT/$APP_NAME/Contents/Frameworks" security create-keychain -p pwd $(agent.tempdirectory)/buildagent.keychain security default-keychain -s $(agent.tempdirectory)/buildagent.keychain security unlock-keychain -p pwd $(agent.tempdirectory)/buildagent.keychain echo "$(macos-developer-certificate)" | base64 -D > $(agent.tempdirectory)/cert.p12 security import $(agent.tempdirectory)/cert.p12 -k $(agent.tempdirectory)/buildagent.keychain -P "$(macos-developer-certificate-key)" -T /usr/bin/codesign security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k pwd $(agent.tempdirectory)/buildagent.keychain - codesign -s 99FM488X57 --deep --force --options runtime --entitlements build/azure-pipelines/darwin/entitlements.plist $(agent.builddirectory)/VSCode-darwin/*.app + codesign -s 99FM488X57 --deep --force --options runtime --entitlements build/azure-pipelines/darwin/entitlements.plist "$APP_ROOT"/*.app + codesign -s 99FM488X57 --force --options runtime --entitlements build/azure-pipelines/darwin/helper-gpu-entitlements.plist "$APP_FRAMEWORK_PATH/$HELPER_APP_NAME Helper (GPU).app" + codesign -s 99FM488X57 --force --options runtime --entitlements build/azure-pipelines/darwin/helper-plugin-entitlements.plist "$APP_FRAMEWORK_PATH/$HELPER_APP_NAME Helper (Plugin).app" + codesign -s 99FM488X57 --force --options runtime --entitlements build/azure-pipelines/darwin/helper-renderer-entitlements.plist "$APP_FRAMEWORK_PATH/$HELPER_APP_NAME Helper (Renderer).app" displayName: Set Hardened Entitlements - script: | diff --git a/build/azure-pipelines/linux/continuous-build-linux.yml b/build/azure-pipelines/linux/continuous-build-linux.yml index 2e898ac73e..0a692dffdb 100644 --- a/build/azure-pipelines/linux/continuous-build-linux.yml +++ b/build/azure-pipelines/linux/continuous-build-linux.yml @@ -7,57 +7,80 @@ steps: sudo chmod +x /etc/init.d/xvfb sudo update-rc.d xvfb defaults sudo service xvfb start + - task: NodeTool@0 inputs: versionSpec: "12.13.0" + - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@3 inputs: versionSpec: "1.x" + - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 inputs: keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock' targetfolder: '**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules' vstsFeed: 'npm-cache' # {{SQL CARBON EDIT}} update build cache + - script: | CHILD_CONCURRENCY=1 yarn --frozen-lockfile displayName: Install Dependencies condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) + - task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1 inputs: keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock' targetfolder: '**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules' vstsFeed: 'npm-cache' # {{SQL CARBON EDIT}} update build cache condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) + - script: | yarn electron x64 displayName: Download Electron + - script: | yarn gulp hygiene displayName: Run Hygiene Checks + - script: | # {{SQL CARBON EDIT}} add strict null check yarn strict-vscode + displayName: Run Strict Null Check # - script: | {{SQL CARBON EDIT}} remove monaco editor checks # yarn monaco-compile-check # displayName: Run Monaco Editor Checks + - script: | yarn valid-layers-check displayName: Run Valid Layers Checks + - script: | yarn compile displayName: Compile Sources + # - script: | {{SQL CARBON EDIT}} remove step # yarn download-builtin-extensions # displayName: Download Built-in Extensions + - script: | DISPLAY=:10 ./scripts/test.sh --tfs "Unit Tests" displayName: Run Unit Tests (Electron) + # - script: | {{SQL CARBON EDIT}} disable # DISPLAY=:10 yarn test-browser --browser chromium # displayName: Run Unit Tests (Browser) + # - script: | {{SQL CARBON EDIT}} disable # DISPLAY=:10 ./scripts/test-integration.sh --tfs "Integration Tests" # displayName: Run Integration Tests (Electron) + +- task: PublishPipelineArtifact@0 + inputs: + artifactName: crash-dump-linux + targetPath: .build/crashes + displayName: 'Publish Crash Reports' + condition: succeededOrFailed() + - task: PublishTestResults@2 displayName: Publish Tests Results inputs: diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index cbe3bf051e..76428b860f 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -140,6 +140,13 @@ steps: displayName: Run integration tests (Browser) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) +- task: PublishPipelineArtifact@0 + inputs: + artifactName: crash-dump-linux + targetPath: .build/crashes + displayName: 'Publish Crash Reports' + condition: succeededOrFailed() + - script: | set -e yarn gulp "vscode-linux-x64-build-deb" diff --git a/build/azure-pipelines/win32/continuous-build-win32.yml b/build/azure-pipelines/win32/continuous-build-win32.yml index d0f7037962..27826fdd1f 100644 --- a/build/azure-pipelines/win32/continuous-build-win32.yml +++ b/build/azure-pipelines/win32/continuous-build-win32.yml @@ -2,59 +2,83 @@ steps: - task: NodeTool@0 inputs: versionSpec: "12.13.0" + - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@3 # {{SQL CARBON EDIT}} update version inputs: versionSpec: "1.x" + - task: UsePythonVersion@0 inputs: versionSpec: '2.x' addToPath: true + - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 inputs: keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock' targetfolder: '**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules' vstsFeed: 'npm-cache' # {{SQL CARBON EDIT}} update build cache + - powershell: | yarn --frozen-lockfile env: CHILD_CONCURRENCY: "1" displayName: Install Dependencies condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) + - task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1 inputs: keyfile: 'build/.cachesalt, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock, !samples/**/yarn.lock' targetfolder: '**/node_modules, !**/node_modules/**/node_modules, !samples/**/node_modules' vstsFeed: 'npm-cache' # {{SQL CARBON EDIT}} update build cache condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) + - powershell: | yarn electron + displayName: Download Electron + - script: | yarn gulp hygiene displayName: Run Hygiene Checks + - script: | # {{SQL CARBON EDIT}} add step yarn strict-vscode displayName: Run Strict Null Check + # - powershell: | {{SQL CARBON EDIT}} remove step # yarn monaco-compile-check # displayName: Run Monaco Editor Checks + - script: | yarn valid-layers-check displayName: Run Valid Layers Checks + - powershell: | yarn compile displayName: Compile Sources + # - powershell: | {{SQL CARBON EDIT}} remove step # yarn download-builtin-extensions # displayName: Download Built-in Extensions + - powershell: | .\scripts\test.bat --tfs "Unit Tests" displayName: Run Unit Tests (Electron) + # - powershell: | {{SQL CARBON EDIT}} disable # yarn test-browser --browser chromium --browser firefox # displayName: Run Unit Tests (Browser) + # - powershell: | {{SQL CARBON EDIT}} disable # .\scripts\test-integration.bat --tfs "Integration Tests" # displayName: Run Integration Tests (Electron) + +- task: PublishPipelineArtifact@0 + displayName: 'Publish Crash Reports' + inputs: + artifactName: crash-dump-windows + targetPath: .build\crashes + condition: succeededOrFailed() + - task: PublishTestResults@2 displayName: Publish Tests Results inputs: diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index c428642454..75dc54e359 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -149,6 +149,13 @@ steps: displayName: Run integration tests (Browser) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) +- task: PublishPipelineArtifact@0 + inputs: + artifactName: crash-dump-windows-$(VSCODE_ARCH) + targetPath: .build\crashes + displayName: 'Publish Crash Reports' + condition: succeededOrFailed() + - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 inputs: ConnectedServiceName: 'ESRP CodeSign' diff --git a/build/lib/treeshaking.js b/build/lib/treeshaking.js index 0c8b5dea26..34b517f76c 100644 --- a/build/lib/treeshaking.js +++ b/build/lib/treeshaking.js @@ -430,6 +430,7 @@ function markNodes(languageService, options) { || ts.isIndexSignatureDeclaration(member) || ts.isCallSignatureDeclaration(member) || memberName === '[Symbol.iterator]' + || memberName === '[Symbol.toStringTag]' || memberName === 'toJSON' || memberName === 'toString' || memberName === 'dispose' // TODO: keeping all `dispose` methods diff --git a/build/lib/treeshaking.ts b/build/lib/treeshaking.ts index b2959ee1eb..80822c4c87 100644 --- a/build/lib/treeshaking.ts +++ b/build/lib/treeshaking.ts @@ -548,6 +548,7 @@ function markNodes(languageService: ts.LanguageService, options: ITreeShakingOpt || ts.isIndexSignatureDeclaration(member) || ts.isCallSignatureDeclaration(member) || memberName === '[Symbol.iterator]' + || memberName === '[Symbol.toStringTag]' || memberName === 'toJSON' || memberName === 'toString' || memberName === 'dispose'// TODO: keeping all `dispose` methods @@ -795,8 +796,8 @@ function getRealNodeSymbol(checker: ts.TypeChecker, node: ts.Node): [ts.Symbol | let symbol = ( ts.isShorthandPropertyAssignment(node) - ? checker.getShorthandAssignmentValueSymbol(node) - : checker.getSymbolAtLocation(node) + ? checker.getShorthandAssignmentValueSymbol(node) + : checker.getSymbolAtLocation(node) ); let importNode: ts.Declaration | null = null; diff --git a/extensions/git/package.json b/extensions/git/package.json index 7c28c7e4b0..ea99da5140 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -409,6 +409,11 @@ "command": "git.timeline.copyCommitMessage", "title": "%command.timelineCopyCommitMessage%", "category": "Git" + }, + { + "command": "git.rebaseAbort", + "title": "%command.rebaseAbort%", + "category": "Git" } ], "keybindings": [ diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 3f5269dd51..a735b58aa7 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -63,6 +63,7 @@ "command.showOutput": "Show Git Output", "command.ignore": "Add to .gitignore", "command.revealInExplorer": "Reveal in Side Bar", + "command.rebaseAbort": "Abort Rebase", "command.stashIncludeUntracked": "Stash (Include Untracked)", "command.stash": "Stash", "command.stashPop": "Pop Stash...", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index e31124e15d..4a2ecfa7ab 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -2494,6 +2494,10 @@ export class CommandCenter { env.clipboard.writeText(item.message); } + @command('git.rebaseAbort', { repository: true }) + async rebaseAbort(repository: Repository): Promise { + await repository.rebaseAbort(); + } private createCommand(id: string, key: string, method: Function, options: CommandOptions): (...args: any[]) => any { const result = (...args: any[]) => { diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 0dabbcf951..ed8b89ea98 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -1333,6 +1333,10 @@ export class Repository { } } + async rebaseAbort(): Promise { + await this.run(['rebase', '--abort']); + } + async rebaseContinue(): Promise { const args = ['rebase', '--continue']; diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 3d1f49f78c..ad2cf89802 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -303,6 +303,7 @@ export const enum Operation { CheckIgnore = 'CheckIgnore', GetObjectDetails = 'GetObjectDetails', SubmoduleUpdate = 'SubmoduleUpdate', + RebaseAbort = 'RebaseAbort', RebaseContinue = 'RebaseContinue', FindTrackingBranches = 'GetTracking', Apply = 'Apply', @@ -1331,6 +1332,10 @@ export class Repository implements Disposable { }); } + async rebaseAbort(): Promise { + await this.run(Operation.RebaseAbort, async () => await this.repository.rebaseAbort()); + } + checkIgnore(filePaths: string[]): Promise> { return this.run(Operation.CheckIgnore, () => { return new Promise>((resolve, reject) => { diff --git a/extensions/json-language-features/client/src/jsonMain.ts b/extensions/json-language-features/client/src/jsonMain.ts index b7a874b2d9..ebdbf7e661 100644 --- a/extensions/json-language-features/client/src/jsonMain.ts +++ b/extensions/json-language-features/client/src/jsonMain.ts @@ -77,6 +77,12 @@ interface JSONSchemaSettings { schema?: any; } +namespace SettingIds { + export const enableFormatter = 'json.format.enable'; + export const enableSchemaDownload = 'json.schemaDownload.enable'; + export const maxItemsComputed = 'json.maxItemsComputed'; +} + let telemetryReporter: TelemetryReporter | undefined; export function activate(context: ExtensionContext) { @@ -107,10 +113,8 @@ export function activate(context: ExtensionContext) { id: 'status.json.resolveError', name: localize('json.resolveError', "JSON: Schema Resolution Error"), alignment: StatusBarAlignment.Right, - priority: 0 + priority: 0, }); - schemaResolutionErrorStatusBarItem.command = '_json.retryResolveSchema'; - schemaResolutionErrorStatusBarItem.tooltip = localize('json.schemaResolutionErrorMessage', 'Unable to resolve schema.') + ' ' + localize('json.clickToRetry', 'Click to retry.'); schemaResolutionErrorStatusBarItem.text = '$(alert)'; toDispose.push(schemaResolutionErrorStatusBarItem); @@ -200,6 +204,7 @@ export function activate(context: ExtensionContext) { toDispose.push(disposable); client.onReady().then(() => { const schemaDocuments: { [uri: string]: boolean } = {}; + let schemaDownloadEnabled = true; // handle content request client.onRequest(VSCodeContentRequest.type, (uriPath: string) => { @@ -208,12 +213,16 @@ export function activate(context: ExtensionContext) { return Promise.reject(new Error(localize('untitled.schema', 'Unable to load {0}', uri.toString()))); } if (uri.scheme !== 'http' && uri.scheme !== 'https') { - return workspace.openTextDocument(uri).then(doc => { - schemaDocuments[uri.toString()] = true; - return doc.getText(); - }, error => { - return Promise.reject(error); - }); + if (schemaDownloadEnabled) { + return workspace.openTextDocument(uri).then(doc => { + schemaDocuments[uri.toString()] = true; + return doc.getText(); + }, error => { + return Promise.reject(error); + }); + } else { + return Promise.reject(localize('schemaDownloadDisabled', 'Downloading schemas is disabled through setting \'{0}\'', SettingIds.enableSchemaDownload)); + } } else { if (telemetryReporter && uri.authority === 'schema.management.azure.com') { /* __GDPR__ @@ -294,16 +303,61 @@ export function activate(context: ExtensionContext) { client.sendNotification(SchemaAssociationNotification.type, getSchemaAssociations(context)); }); - // manually register / deregister format provider based on the `html.format.enable` setting avoiding issues with late registration. See #71652. + // manually register / deregister format provider based on the `json.format.enable` setting avoiding issues with late registration. See #71652. updateFormatterRegistration(); toDispose.push({ dispose: () => rangeFormatting && rangeFormatting.dispose() }); - toDispose.push(workspace.onDidChangeConfiguration(e => e.affectsConfiguration('html.format.enable') && updateFormatterRegistration())); + updateSchemaDownloadSetting(); + + toDispose.push(workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(SettingIds.enableFormatter)) { + updateFormatterRegistration(); + } else if (e.affectsConfiguration(SettingIds.enableSchemaDownload)) { + updateSchemaDownloadSetting(); + } + })); client.onNotification(ResultLimitReachedNotification.type, message => { - window.showInformationMessage(`${message}\nUse setting 'json.maxItemsComputed' to configure the limit.`); + window.showInformationMessage(`${message}\n${localize('configureLimit', 'Use setting \'{0}\' to configure the limit.', SettingIds.maxItemsComputed)}`); }); + function updateFormatterRegistration() { + const formatEnabled = workspace.getConfiguration().get(SettingIds.enableFormatter); + if (!formatEnabled && rangeFormatting) { + rangeFormatting.dispose(); + rangeFormatting = undefined; + } else if (formatEnabled && !rangeFormatting) { + rangeFormatting = languages.registerDocumentRangeFormattingEditProvider(documentSelector, { + provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult { + const params: DocumentRangeFormattingParams = { + textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), + range: client.code2ProtocolConverter.asRange(range), + options: client.code2ProtocolConverter.asFormattingOptions(options) + }; + return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then( + client.protocol2CodeConverter.asTextEdits, + (error) => { + client.logFailedRequest(DocumentRangeFormattingRequest.type, error); + return Promise.resolve([]); + } + ); + } + }); + } + } + + function updateSchemaDownloadSetting() { + schemaDownloadEnabled = workspace.getConfiguration().get(SettingIds.enableSchemaDownload) !== false; + if (schemaDownloadEnabled) { + schemaResolutionErrorStatusBarItem.tooltip = localize('json.schemaResolutionErrorMessage', 'Unable to resolve schema. Click to retry.'); + schemaResolutionErrorStatusBarItem.command = '_json.retryResolveSchema'; + handleRetryResolveSchemaCommand(); + } else { + schemaResolutionErrorStatusBarItem.tooltip = localize('json.schemaResolutionDisabledMessage', 'Downloading schemas is disabled. Click to configure.'); + schemaResolutionErrorStatusBarItem.command = { command: 'workbench.action.openSettings', arguments: [SettingIds.enableSchemaDownload], title: '' }; + } + } + }); const languageConfiguration: LanguageConfiguration = { @@ -316,30 +370,6 @@ export function activate(context: ExtensionContext) { languages.setLanguageConfiguration('json', languageConfiguration); languages.setLanguageConfiguration('jsonc', languageConfiguration); - function updateFormatterRegistration() { - const formatEnabled = workspace.getConfiguration().get('json.format.enable'); - if (!formatEnabled && rangeFormatting) { - rangeFormatting.dispose(); - rangeFormatting = undefined; - } else if (formatEnabled && !rangeFormatting) { - rangeFormatting = languages.registerDocumentRangeFormattingEditProvider(documentSelector, { - provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult { - const params: DocumentRangeFormattingParams = { - textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), - range: client.code2ProtocolConverter.asRange(range), - options: client.code2ProtocolConverter.asFormattingOptions(options) - }; - return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then( - client.protocol2CodeConverter.asTextEdits, - (error) => { - client.logFailedRequest(DocumentRangeFormattingRequest.type, error); - return Promise.resolve([]); - } - ); - } - }); - } - } } @@ -386,7 +416,7 @@ function getSchemaAssociations(_context: ExtensionContext): ISchemaAssociation[] function getSettings(): Settings { const httpSettings = workspace.getConfiguration('http'); - const resultLimit: number = Math.trunc(Math.max(0, Number(workspace.getConfiguration().get('json.maxItemsComputed')))) || 5000; + const resultLimit: number = Math.trunc(Math.max(0, Number(workspace.getConfiguration().get(SettingIds.maxItemsComputed)))) || 5000; const settings: Settings = { http: { diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index d2edb7d54a..2868c0c01a 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -95,7 +95,13 @@ "type": "number", "default": 5000, "description": "%json.maxItemsComputed.desc%" - } + }, + "json.schemaDownload.enable": { + "type": "boolean", + "default": true, + "description": "%json.enableSchemaDownload.desc%", + "tags": ["usesOnlineServices"] + } } }, "configurationDefaults": { diff --git a/extensions/json-language-features/package.nls.json b/extensions/json-language-features/package.nls.json index 35cf05e2d0..69a8c0eea8 100644 --- a/extensions/json-language-features/package.nls.json +++ b/extensions/json-language-features/package.nls.json @@ -12,5 +12,6 @@ "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.maxItemsComputed.desc": "The maximum number of outline symbols and folding regions computed (limited for performance reasons)." + "json.maxItemsComputed.desc": "The maximum number of outline symbols and folding regions computed (limited for performance reasons).", + "json.enableSchemaDownload.desc": "When enabled, JSON schemas can be fetched from http and https locations." } diff --git a/extensions/machine-learning/src/test/mainController.test.ts b/extensions/machine-learning/src/test/mainController.test.ts index 6b7f2b9132..88abbe7cfd 100644 --- a/extensions/machine-learning/src/test/mainController.test.ts +++ b/extensions/machine-learning/src/test/mainController.test.ts @@ -90,7 +90,8 @@ function createContext(): TestContext { storagePath: '', globalStoragePath: '', logPath: '', - extensionUri: vscode.Uri.parse('') + extensionUri: vscode.Uri.parse(''), + environmentVariableCollection: { } as any }, outputChannel: { name: '', diff --git a/extensions/notebook/src/test/common/stubs.ts b/extensions/notebook/src/test/common/stubs.ts index 8f577d7f2d..f541416276 100644 --- a/extensions/notebook/src/test/common/stubs.ts +++ b/extensions/notebook/src/test/common/stubs.ts @@ -22,6 +22,7 @@ export class MockExtensionContext implements vscode.ExtensionContext { constructor() { this.subscriptions = []; } + environmentVariableCollection: vscode.EnvironmentVariableCollection; } export class MockOutputChannel implements vscode.OutputChannel { diff --git a/extensions/sql-database-projects/src/test/testContext.ts b/extensions/sql-database-projects/src/test/testContext.ts index fd637eef4f..5936784e8d 100644 --- a/extensions/sql-database-projects/src/test/testContext.ts +++ b/extensions/sql-database-projects/src/test/testContext.ts @@ -33,7 +33,8 @@ export function createContext(): TestContext { storagePath: '', globalStoragePath: '', logPath: '', - extensionUri: vscode.Uri.parse('') + extensionUri: vscode.Uri.parse(''), + environmentVariableCollection: undefined as any }, }; } diff --git a/extensions/theme-seti/build/update-icon-theme.js b/extensions/theme-seti/build/update-icon-theme.js index 4bd77f37f3..67e33fb9a6 100644 --- a/extensions/theme-seti/build/update-icon-theme.js +++ b/extensions/theme-seti/build/update-icon-theme.js @@ -10,8 +10,8 @@ let fs = require('fs'); let https = require('https'); let url = require('url'); -// list of languagesIs not shipped with VSCode. The information is used to associate an icon with a language association -let nonBuiltInLanguages = { // { fileNames, extensions } +// list of languagesId not shipped with VSCode. The information is used to associate an icon with a language association +let nonBuiltInLanguages = { // { fileNames, extensions } "r": { extensions: ['r', 'rhistory', 'rprofile', 'rt'] }, "argdown": { extensions: ['ad', 'adown', 'argdown', 'argdn'] }, "elm": { extensions: ['elm'] }, @@ -32,10 +32,16 @@ let nonBuiltInLanguages = { // { fileNames, extensions } "haml": { extensions: ['haml'] }, "stylus": { extensions: ['styl'] }, "vala": { extensions: ['vala'] }, - "todo": { fileNames: ['todo'] }, - "jsonc": { extensions: ['json'] } + "todo": { fileNames: ['todo'] } }; +// list of languagesId that inherit the icon from another language +let inheritIconFromLanguage = { + "jsonc": 'json', + "postcss": 'css', + "django-html": 'html' +} + let FROM_DISK = true; // set to true to take content from a repo checked out next to the vscode repo let font, fontMappingsFile, fileAssociationFile, colorsFile; @@ -358,6 +364,16 @@ exports.update = function () { } } } + for (let lang in inheritIconFromLanguage) { + let superLang = inheritIconFromLanguage[lang]; + let def = lang2Def[superLang]; + if (def) { + lang2Def[lang] = def; + } else { + console.log('skipping icon def for ' + lang + ': no icon for ' + superLang + ' defined'); + } + + } return download(colorsFile).then(function (content) { diff --git a/extensions/theme-seti/cgmanifest.json b/extensions/theme-seti/cgmanifest.json index 5ee7c0f5d5..a3df8c6b32 100644 --- a/extensions/theme-seti/cgmanifest.json +++ b/extensions/theme-seti/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "seti-ui", "repositoryUrl": "https://github.com/jesseweed/seti-ui", - "commitHash": "4b3e0a3d0ca8999430bc3aa9f2c8324e6922b3de" + "commitHash": "8f22764c37feb7f706465f5186132111a2401b6b" } }, "version": "0.1.0" diff --git a/extensions/theme-seti/icons/seti.woff b/extensions/theme-seti/icons/seti.woff index 85f6ee76a5..b4b8366f16 100644 Binary files a/extensions/theme-seti/icons/seti.woff and b/extensions/theme-seti/icons/seti.woff differ diff --git a/extensions/theme-seti/icons/vs-seti-icon-theme.json b/extensions/theme-seti/icons/vs-seti-icon-theme.json index eed503fcc0..16de3aceb1 100644 --- a/extensions/theme-seti/icons/vs-seti-icon-theme.json +++ b/extensions/theme-seti/icons/vs-seti-icon-theme.json @@ -966,399 +966,413 @@ "fontCharacter": "\\E06C", "fontColor": "#519aba" }, - "_pug_light": { + "_prolog_light": { "fontCharacter": "\\E06E", + "fontColor": "#cc6d2e" + }, + "_prolog": { + "fontCharacter": "\\E06E", + "fontColor": "#e37933" + }, + "_pug_light": { + "fontCharacter": "\\E06F", "fontColor": "#b8383d" }, "_pug": { - "fontCharacter": "\\E06E", + "fontCharacter": "\\E06F", "fontColor": "#cc3e44" }, "_puppet_light": { - "fontCharacter": "\\E06F", + "fontCharacter": "\\E070", "fontColor": "#b7b73b" }, "_puppet": { - "fontCharacter": "\\E06F", + "fontCharacter": "\\E070", "fontColor": "#cbcb41" }, "_python_light": { - "fontCharacter": "\\E070", + "fontCharacter": "\\E071", "fontColor": "#498ba7" }, "_python": { - "fontCharacter": "\\E070", + "fontCharacter": "\\E071", "fontColor": "#519aba" }, "_react_light": { - "fontCharacter": "\\E072", + "fontCharacter": "\\E073", "fontColor": "#498ba7" }, "_react": { - "fontCharacter": "\\E072", + "fontCharacter": "\\E073", "fontColor": "#519aba" }, "_react_1_light": { - "fontCharacter": "\\E072", + "fontCharacter": "\\E073", "fontColor": "#cc6d2e" }, "_react_1": { - "fontCharacter": "\\E072", + "fontCharacter": "\\E073", "fontColor": "#e37933" }, "_react_2_light": { - "fontCharacter": "\\E072", + "fontCharacter": "\\E073", "fontColor": "#b7b73b" }, "_react_2": { - "fontCharacter": "\\E072", + "fontCharacter": "\\E073", "fontColor": "#cbcb41" }, "_reasonml_light": { - "fontCharacter": "\\E073", + "fontCharacter": "\\E074", "fontColor": "#b8383d" }, "_reasonml": { - "fontCharacter": "\\E073", + "fontCharacter": "\\E074", "fontColor": "#cc3e44" }, + "_rmd_light": { + "fontColor": "#498ba7" + }, + "_rmd": { + "fontColor": "#519aba" + }, "_rollup_light": { - "fontCharacter": "\\E074", + "fontCharacter": "\\E075", "fontColor": "#b8383d" }, "_rollup": { - "fontCharacter": "\\E074", + "fontCharacter": "\\E075", "fontColor": "#cc3e44" }, "_ruby_light": { - "fontCharacter": "\\E075", + "fontCharacter": "\\E076", "fontColor": "#b8383d" }, "_ruby": { - "fontCharacter": "\\E075", + "fontCharacter": "\\E076", "fontColor": "#cc3e44" }, "_rust_light": { - "fontCharacter": "\\E076", + "fontCharacter": "\\E077", "fontColor": "#627379" }, "_rust": { - "fontCharacter": "\\E076", + "fontCharacter": "\\E077", "fontColor": "#6d8086" }, "_salesforce_light": { - "fontCharacter": "\\E077", + "fontCharacter": "\\E078", "fontColor": "#498ba7" }, "_salesforce": { - "fontCharacter": "\\E077", + "fontCharacter": "\\E078", "fontColor": "#519aba" }, "_sass_light": { - "fontCharacter": "\\E078", + "fontCharacter": "\\E079", "fontColor": "#dd4b78" }, "_sass": { - "fontCharacter": "\\E078", + "fontCharacter": "\\E079", "fontColor": "#f55385" }, "_sbt_light": { - "fontCharacter": "\\E079", + "fontCharacter": "\\E07A", "fontColor": "#498ba7" }, "_sbt": { - "fontCharacter": "\\E079", + "fontCharacter": "\\E07A", "fontColor": "#519aba" }, "_scala_light": { - "fontCharacter": "\\E07A", + "fontCharacter": "\\E07B", "fontColor": "#b8383d" }, "_scala": { - "fontCharacter": "\\E07A", + "fontCharacter": "\\E07B", "fontColor": "#cc3e44" }, "_shell_light": { - "fontCharacter": "\\E07D", + "fontCharacter": "\\E07E", "fontColor": "#455155" }, "_shell": { - "fontCharacter": "\\E07D", + "fontCharacter": "\\E07E", "fontColor": "#4d5a5e" }, "_slim_light": { - "fontCharacter": "\\E07E", + "fontCharacter": "\\E07F", "fontColor": "#cc6d2e" }, "_slim": { - "fontCharacter": "\\E07E", + "fontCharacter": "\\E07F", "fontColor": "#e37933" }, "_smarty_light": { - "fontCharacter": "\\E07F", + "fontCharacter": "\\E080", "fontColor": "#b7b73b" }, "_smarty": { - "fontCharacter": "\\E07F", + "fontCharacter": "\\E080", "fontColor": "#cbcb41" }, "_spring_light": { - "fontCharacter": "\\E080", + "fontCharacter": "\\E081", "fontColor": "#7fae42" }, "_spring": { - "fontCharacter": "\\E080", + "fontCharacter": "\\E081", "fontColor": "#8dc149" }, "_stylelint_light": { - "fontCharacter": "\\E081", + "fontCharacter": "\\E082", "fontColor": "#bfc2c1" }, "_stylelint": { - "fontCharacter": "\\E081", + "fontCharacter": "\\E082", "fontColor": "#d4d7d6" }, "_stylelint_1_light": { - "fontCharacter": "\\E081", + "fontCharacter": "\\E082", "fontColor": "#455155" }, "_stylelint_1": { - "fontCharacter": "\\E081", + "fontCharacter": "\\E082", "fontColor": "#4d5a5e" }, "_stylus_light": { - "fontCharacter": "\\E082", + "fontCharacter": "\\E083", "fontColor": "#7fae42" }, "_stylus": { - "fontCharacter": "\\E082", + "fontCharacter": "\\E083", "fontColor": "#8dc149" }, "_sublime_light": { - "fontCharacter": "\\E083", + "fontCharacter": "\\E084", "fontColor": "#cc6d2e" }, "_sublime": { - "fontCharacter": "\\E083", + "fontCharacter": "\\E084", "fontColor": "#e37933" }, "_svg_light": { - "fontCharacter": "\\E084", + "fontCharacter": "\\E085", "fontColor": "#9068b0" }, "_svg": { - "fontCharacter": "\\E084", + "fontCharacter": "\\E085", "fontColor": "#a074c4" }, "_svg_1_light": { - "fontCharacter": "\\E084", + "fontCharacter": "\\E085", "fontColor": "#498ba7" }, "_svg_1": { - "fontCharacter": "\\E084", + "fontCharacter": "\\E085", "fontColor": "#519aba" }, "_swift_light": { - "fontCharacter": "\\E085", + "fontCharacter": "\\E086", "fontColor": "#cc6d2e" }, "_swift": { - "fontCharacter": "\\E085", + "fontCharacter": "\\E086", "fontColor": "#e37933" }, "_terraform_light": { - "fontCharacter": "\\E086", + "fontCharacter": "\\E087", "fontColor": "#9068b0" }, "_terraform": { - "fontCharacter": "\\E086", + "fontCharacter": "\\E087", "fontColor": "#a074c4" }, "_tex_light": { - "fontCharacter": "\\E087", + "fontCharacter": "\\E088", "fontColor": "#498ba7" }, "_tex": { - "fontCharacter": "\\E087", + "fontCharacter": "\\E088", "fontColor": "#519aba" }, "_tex_1_light": { - "fontCharacter": "\\E087", + "fontCharacter": "\\E088", "fontColor": "#b7b73b" }, "_tex_1": { - "fontCharacter": "\\E087", + "fontCharacter": "\\E088", "fontColor": "#cbcb41" }, "_tex_2_light": { - "fontCharacter": "\\E087", + "fontCharacter": "\\E088", "fontColor": "#cc6d2e" }, "_tex_2": { - "fontCharacter": "\\E087", + "fontCharacter": "\\E088", "fontColor": "#e37933" }, "_tex_3_light": { - "fontCharacter": "\\E087", + "fontCharacter": "\\E088", "fontColor": "#bfc2c1" }, "_tex_3": { - "fontCharacter": "\\E087", + "fontCharacter": "\\E088", "fontColor": "#d4d7d6" }, "_todo": { - "fontCharacter": "\\E089" + "fontCharacter": "\\E08A" }, "_tsconfig_light": { - "fontCharacter": "\\E08A", + "fontCharacter": "\\E08B", "fontColor": "#498ba7" }, "_tsconfig": { - "fontCharacter": "\\E08A", + "fontCharacter": "\\E08B", "fontColor": "#519aba" }, "_twig_light": { - "fontCharacter": "\\E08B", + "fontCharacter": "\\E08C", "fontColor": "#7fae42" }, "_twig": { - "fontCharacter": "\\E08B", + "fontCharacter": "\\E08C", "fontColor": "#8dc149" }, "_typescript_light": { - "fontCharacter": "\\E08C", + "fontCharacter": "\\E08D", "fontColor": "#498ba7" }, "_typescript": { - "fontCharacter": "\\E08C", + "fontCharacter": "\\E08D", "fontColor": "#519aba" }, "_typescript_1_light": { - "fontCharacter": "\\E08C", + "fontCharacter": "\\E08D", "fontColor": "#b7b73b" }, "_typescript_1": { - "fontCharacter": "\\E08C", + "fontCharacter": "\\E08D", "fontColor": "#cbcb41" }, "_vala_light": { - "fontCharacter": "\\E08D", + "fontCharacter": "\\E08E", "fontColor": "#627379" }, "_vala": { - "fontCharacter": "\\E08D", + "fontCharacter": "\\E08E", "fontColor": "#6d8086" }, "_video_light": { - "fontCharacter": "\\E08E", + "fontCharacter": "\\E08F", "fontColor": "#dd4b78" }, "_video": { - "fontCharacter": "\\E08E", + "fontCharacter": "\\E08F", "fontColor": "#f55385" }, "_vue_light": { - "fontCharacter": "\\E08F", + "fontCharacter": "\\E090", "fontColor": "#7fae42" }, "_vue": { - "fontCharacter": "\\E08F", + "fontCharacter": "\\E090", "fontColor": "#8dc149" }, "_wasm_light": { - "fontCharacter": "\\E090", + "fontCharacter": "\\E091", "fontColor": "#9068b0" }, "_wasm": { - "fontCharacter": "\\E090", + "fontCharacter": "\\E091", "fontColor": "#a074c4" }, "_wat_light": { - "fontCharacter": "\\E091", + "fontCharacter": "\\E092", "fontColor": "#9068b0" }, "_wat": { - "fontCharacter": "\\E091", + "fontCharacter": "\\E092", "fontColor": "#a074c4" }, "_webpack_light": { - "fontCharacter": "\\E092", + "fontCharacter": "\\E093", "fontColor": "#498ba7" }, "_webpack": { - "fontCharacter": "\\E092", + "fontCharacter": "\\E093", "fontColor": "#519aba" }, "_wgt_light": { - "fontCharacter": "\\E093", + "fontCharacter": "\\E094", "fontColor": "#498ba7" }, "_wgt": { - "fontCharacter": "\\E093", + "fontCharacter": "\\E094", "fontColor": "#519aba" }, "_windows_light": { - "fontCharacter": "\\E094", + "fontCharacter": "\\E095", "fontColor": "#498ba7" }, "_windows": { - "fontCharacter": "\\E094", + "fontCharacter": "\\E095", "fontColor": "#519aba" }, "_word_light": { - "fontCharacter": "\\E095", + "fontCharacter": "\\E096", "fontColor": "#498ba7" }, "_word": { - "fontCharacter": "\\E095", + "fontCharacter": "\\E096", "fontColor": "#519aba" }, "_xls_light": { - "fontCharacter": "\\E096", + "fontCharacter": "\\E097", "fontColor": "#7fae42" }, "_xls": { - "fontCharacter": "\\E096", + "fontCharacter": "\\E097", "fontColor": "#8dc149" }, "_xml_light": { - "fontCharacter": "\\E097", + "fontCharacter": "\\E098", "fontColor": "#cc6d2e" }, "_xml": { - "fontCharacter": "\\E097", + "fontCharacter": "\\E098", "fontColor": "#e37933" }, "_yarn_light": { - "fontCharacter": "\\E098", + "fontCharacter": "\\E099", "fontColor": "#498ba7" }, "_yarn": { - "fontCharacter": "\\E098", + "fontCharacter": "\\E099", "fontColor": "#519aba" }, "_yml_light": { - "fontCharacter": "\\E099", + "fontCharacter": "\\E09A", "fontColor": "#9068b0" }, "_yml": { - "fontCharacter": "\\E099", + "fontCharacter": "\\E09A", "fontColor": "#a074c4" }, "_zip_light": { - "fontCharacter": "\\E09A", + "fontCharacter": "\\E09B", "fontColor": "#b8383d" }, "_zip": { - "fontCharacter": "\\E09A", + "fontCharacter": "\\E09B", "fontColor": "#cc3e44" }, "_zip_1_light": { - "fontCharacter": "\\E09A", + "fontCharacter": "\\E09B", "fontColor": "#627379" }, "_zip_1": { - "fontCharacter": "\\E09A", + "fontCharacter": "\\E09B", "fontColor": "#6d8086" }, // {{SQL CARBON EDIT}} @@ -1392,6 +1406,7 @@ "edn": "_clojure_1", "cfc": "_coldfusion", "cfm": "_coldfusion", + "litcoffee": "_coffee", "config": "_config", "cfg": "_config", "conf": "_config", @@ -1434,6 +1449,7 @@ "hxml": "_haxe_3", "class": "_java", "classpath": "_java", + "properties": "_java", "js.map": "_javascript", "spec.js": "_javascript_1", "test.js": "_javascript_1", @@ -1480,6 +1496,7 @@ "test.tsx": "_react_2", "re": "_reasonml", "r": "_R", + "rmd": "_rmd", "erb": "_html_erb", "erb.html": "_html_erb", "html.erb": "_html_erb", @@ -1507,6 +1524,7 @@ "vue": "_vue", "wasm": "_wasm", "wat": "_wat", + "pro": "_prolog", "jar": "_zip", "zip": "_zip_1", "wgt": "_wgt", @@ -1545,6 +1563,8 @@ "obj": "_svg_1", "dae": "_svg_1", "babelrc": "_babel", + "babelrc.js": "_babel", + "babelrc.cjs": "_babel", "bowerrc": "_bower", "dockerignore": "_docker_1", "codeclimate.yml": "_code-climate", @@ -1588,12 +1608,14 @@ "version.md": "_clock", "version": "_clock", "mvnw": "_maven", - "tsconfig.json": "_tsconfig", "swagger.json": "_json_1", "swagger.yml": "_json_1", "swagger.yaml": "_json_1", "mime.types": "_config", "jenkinsfile": "_jenkins", + "babel.config.js": "_babel", + "babel.config.json": "_babel", + "babel.config.cjs": "_babel", "bower.json": "_bower", "docker-healthcheck": "_docker_2", "docker-compose.yml": "_docker_3", @@ -1606,6 +1628,7 @@ "gruntfile.babel.js": "_grunt", "gruntfile.coffee": "_grunt", "gulpfile": "_gulp", + "gulpfile.js": "_gulp", "ionic.config.json": "_ionic", "ionic.project": "_ionic", "platformio.ini": "_platformio", @@ -1649,7 +1672,6 @@ "groovy": "_grails", "handlebars": "_mustache", "html": "_html_3", - "properties": "_java", "java": "_java", "javascriptreact": "_react", "javascript": "_javascript", @@ -1699,7 +1721,9 @@ "todo": "_todo", // {{SQL CARBON EDIT}} "notebook": "notebook_dark", - "scmp": "scmp_dark" + "scmp": "scmp_dark", + "postcss": "_css", + "django-html": "_html_3" }, "light": { "file": "_default_light", @@ -1719,6 +1743,7 @@ "edn": "_clojure_1_light", "cfc": "_coldfusion_light", "cfm": "_coldfusion_light", + "litcoffee": "_coffee_light", "config": "_config_light", "cfg": "_config_light", "conf": "_config_light", @@ -1761,6 +1786,7 @@ "hxml": "_haxe_3_light", "class": "_java_light", "classpath": "_java_light", + "properties": "_java_light", "js.map": "_javascript_light", "spec.js": "_javascript_1_light", "test.js": "_javascript_1_light", @@ -1807,6 +1833,7 @@ "test.tsx": "_react_2_light", "re": "_reasonml_light", "r": "_R_light", + "rmd": "_rmd_light", "erb": "_html_erb_light", "erb.html": "_html_erb_light", "html.erb": "_html_erb_light", @@ -1834,6 +1861,7 @@ "vue": "_vue_light", "wasm": "_wasm_light", "wat": "_wat_light", + "pro": "_prolog_light", "jar": "_zip_light", "zip": "_zip_1_light", "wgt": "_wgt_light", @@ -1872,6 +1900,8 @@ "obj": "_svg_1_light", "dae": "_svg_1_light", "babelrc": "_babel_light", + "babelrc.js": "_babel_light", + "babelrc.cjs": "_babel_light", "bowerrc": "_bower_light", "dockerignore": "_docker_1_light", "codeclimate.yml": "_code-climate_light", @@ -1919,7 +1949,6 @@ "groovy": "_grails_light", "handlebars": "_mustache_light", "html": "_html_3_light", - "properties": "_java_light", "java": "_java_light", "javascriptreact": "_react_light", "javascript": "_javascript_light", @@ -1968,7 +1997,9 @@ "vala": "_vala_light", // {{SQL CARBON EDIT}} "notebook": "notebook", - "scmp": "scmp" + "scmp": "scmp", + "postcss": "_css_light", + "django-html": "_html_3_light" }, "fileNames": { "mix": "_hex_light", @@ -1981,12 +2012,14 @@ "version.md": "_clock_light", "version": "_clock_light", "mvnw": "_maven_light", - "tsconfig.json": "_tsconfig_light", "swagger.json": "_json_1_light", "swagger.yml": "_json_1_light", "swagger.yaml": "_json_1_light", "mime.types": "_config_light", "jenkinsfile": "_jenkins_light", + "babel.config.js": "_babel_light", + "babel.config.json": "_babel_light", + "babel.config.cjs": "_babel_light", "bower.json": "_bower_light", "docker-healthcheck": "_docker_2_light", "docker-compose.yml": "_docker_3_light", @@ -1999,6 +2032,7 @@ "gruntfile.babel.js": "_grunt_light", "gruntfile.coffee": "_grunt_light", "gulpfile": "_gulp_light", + "gulpfile.js": "_gulp_light", "ionic.config.json": "_ionic_light", "ionic.project": "_ionic_light", "platformio.ini": "_platformio_light", @@ -2027,5 +2061,5 @@ "Schema Compare": "scmp" } }, - "version": "https://github.com/jesseweed/seti-ui/commit/4b3e0a3d0ca8999430bc3aa9f2c8324e6922b3de" -} \ No newline at end of file + "version": "https://github.com/jesseweed/seti-ui/commit/8f22764c37feb7f706465f5186132111a2401b6b" +} diff --git a/extensions/vscode-account/src/AADHelper.ts b/extensions/vscode-account/src/AADHelper.ts index 623c4926d8..c5b4046a97 100644 --- a/extensions/vscode-account/src/AADHelper.ts +++ b/extensions/vscode-account/src/AADHelper.ts @@ -317,6 +317,10 @@ export class AzureActiveDirectoryService { } private getCallbackEnvironment(callbackUri: vscode.Uri): string { + if (callbackUri.authority.endsWith('.workspaces.github.com')) { + return `${callbackUri.authority},`; + } + switch (callbackUri.authority) { case 'online.visualstudio.com': return 'vso,'; diff --git a/package.json b/package.json index 9668eff395..4a00aef3d6 100644 --- a/package.json +++ b/package.json @@ -76,15 +76,15 @@ "sudo-prompt": "9.1.1", "v8-inspect-profiler": "^0.0.20", "vscode-nsfw": "1.2.8", - "vscode-oniguruma": "1.3.0", + "vscode-oniguruma": "1.3.1", "vscode-proxy-agent": "^0.5.2", "vscode-ripgrep": "^1.5.8", "vscode-sqlite3": "4.0.10", "vscode-textmate": "5.1.1", - "xterm": "4.6.0-beta.38", + "xterm": "4.6.0-beta.44", "xterm-addon-search": "0.7.0-beta.2", "xterm-addon-unicode11": "0.2.0-beta.5", - "xterm-addon-web-links": "0.4.0-beta.5", + "xterm-addon-web-links": "0.4.0-beta.6", "xterm-addon-webgl": "0.7.0-beta.10", "yauzl": "^2.9.2", "yazl": "^2.4.3", diff --git a/remote/package.json b/remote/package.json index 26faf6ed76..3e8943349a 100644 --- a/remote/package.json +++ b/remote/package.json @@ -34,14 +34,14 @@ "slickgrid": "github:anthonydresser/SlickGrid#2.3.33", "spdlog": "^0.11.1", "vscode-nsfw": "1.2.8", - "vscode-oniguruma": "1.3.0", + "vscode-oniguruma": "1.3.1", "vscode-proxy-agent": "^0.5.2", "vscode-ripgrep": "^1.5.8", "vscode-textmate": "5.1.1", - "xterm": "4.6.0-beta.38", + "xterm": "4.6.0-beta.44", "xterm-addon-search": "0.7.0-beta.2", "xterm-addon-unicode11": "0.2.0-beta.5", - "xterm-addon-web-links": "0.4.0-beta.5", + "xterm-addon-web-links": "0.4.0-beta.6", "xterm-addon-webgl": "0.7.0-beta.10", "yauzl": "^2.9.2", "yazl": "^2.4.3", diff --git a/remote/web/package.json b/remote/web/package.json index 0f2da3630b..c5ab0ad2d0 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -21,12 +21,12 @@ "sanitize-html": "^1.19.1", "semver-umd": "^5.5.6", "slickgrid": "github:anthonydresser/SlickGrid#2.3.33", - "vscode-oniguruma": "1.3.0", + "vscode-oniguruma": "1.3.1", "vscode-textmate": "5.1.1", - "xterm": "4.6.0-beta.38", + "xterm": "4.6.0-beta.44", "xterm-addon-search": "0.7.0-beta.2", "xterm-addon-unicode11": "0.2.0-beta.5", - "xterm-addon-web-links": "0.4.0-beta.5", + "xterm-addon-web-links": "0.4.0-beta.6", "xterm-addon-webgl": "0.7.0-beta.10", "zone.js": "^0.8.4" } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index d11c030bb4..ba75b1f9cd 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -338,10 +338,10 @@ util-deprecate@^1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= -vscode-oniguruma@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.3.0.tgz#6788a9db2f8b0781243b4eb8c7a1dd25f6c0e2c8" - integrity sha512-m4Br19v6XD4MRbVrgsLNSZgQrBzk1BCMCleL8+GrcoGxKEJJd62zOFcTaoQR3hCrSlLqoxWmJ7Cc0VieVV3iTQ== +vscode-oniguruma@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.3.1.tgz#e2383879c3485b19f533ec34efea9d7a2b14be8f" + integrity sha512-gz6ZBofA7UXafVA+m2Yt2zHKgXC2qedArprIsHAPKByTkwq9l5y/izAGckqxYml7mSbYxTRTfdRwsFq3cwF4LQ== vscode-textmate@5.1.1: version "5.1.1" @@ -363,20 +363,20 @@ xterm-addon-unicode11@0.2.0-beta.5: resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.2.0-beta.5.tgz#5961850162df20b5e966166423cd6957ac2db298" integrity sha512-IjnbBcyfS5JgJDXPO0W2nk/VBtGwx6GWE2snMC676z4DmAABUqPXfTzJKfUoWqoT6UcbxB0oIjDzykCfoRJp6Q== -xterm-addon-web-links@0.4.0-beta.5: - version "0.4.0-beta.5" - resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.4.0-beta.5.tgz#523fd0a1c5668370d73e05019ed16eaf596894c8" - integrity sha512-Qe0idPpSokCNvGrthSBjdrOZrsgXwnLYbzuv0JoEec/A9HVcxKmZ+ktw7fOA2gT/zbcwtrA5FWrir3GlRHglCQ== +xterm-addon-web-links@0.4.0-beta.6: + version "0.4.0-beta.6" + resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.4.0-beta.6.tgz#d159d4542eb9a02d57977fe7eb5f42f8ef2f27fa" + integrity sha512-dsQVD/EyVq8PtAYGh2PGQTCt009UipIfX6Q2SBDlz+W9x7IkXjhRxRaryMmLsBCca20qeVKwmbQ+ANhLi+nTaQ== xterm-addon-webgl@0.7.0-beta.10: version "0.7.0-beta.10" resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.7.0-beta.10.tgz#39fdb96351e97a1bf15f4c4c8944ba3d05cacee4" integrity sha512-nQl/ASk+ck11aSrBZXb2a0tu+SNDnm89owBk/sAZeZzi5MHNo6bB8y2VTKNNC6D3i3aFouTz4VorYB25LUgNFg== -xterm@4.6.0-beta.38: - version "4.6.0-beta.38" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.6.0-beta.38.tgz#8472b168941500c3071aba482c2b5c6040951ec7" - integrity sha512-Q+nOalMD1MDGOqXdtkGZmOQqbSBU+71vhlX2RBwQoSpJa1QBrKDAhSlN/J+/XvouvVEtCiEFDeacF4EufMEIMg== +xterm@4.6.0-beta.44: + version "4.6.0-beta.44" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.6.0-beta.44.tgz#76b2a6b8e147595ab44aa752c0e721d935464615" + integrity sha512-vYtfz4spFcSKLEUpC6anH7TwDams71+k2wAtUzCJ47dNL2IrwYafcFsvGPm46QLTtq4M2Bp9rQo3R3V746yxNg== zone.js@^0.8.4: version "0.8.29" diff --git a/remote/yarn.lock b/remote/yarn.lock index 0d63dcfbbc..f5da6262f7 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -698,10 +698,10 @@ vscode-nsfw@1.2.8: lodash.isundefined "^3.0.1" nan "^2.10.0" -vscode-oniguruma@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.3.0.tgz#6788a9db2f8b0781243b4eb8c7a1dd25f6c0e2c8" - integrity sha512-m4Br19v6XD4MRbVrgsLNSZgQrBzk1BCMCleL8+GrcoGxKEJJd62zOFcTaoQR3hCrSlLqoxWmJ7Cc0VieVV3iTQ== +vscode-oniguruma@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.3.1.tgz#e2383879c3485b19f533ec34efea9d7a2b14be8f" + integrity sha512-gz6ZBofA7UXafVA+m2Yt2zHKgXC2qedArprIsHAPKByTkwq9l5y/izAGckqxYml7mSbYxTRTfdRwsFq3cwF4LQ== vscode-proxy-agent@^0.5.2: version "0.5.2" @@ -750,20 +750,20 @@ xterm-addon-unicode11@0.2.0-beta.5: resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.2.0-beta.5.tgz#5961850162df20b5e966166423cd6957ac2db298" integrity sha512-IjnbBcyfS5JgJDXPO0W2nk/VBtGwx6GWE2snMC676z4DmAABUqPXfTzJKfUoWqoT6UcbxB0oIjDzykCfoRJp6Q== -xterm-addon-web-links@0.4.0-beta.5: - version "0.4.0-beta.5" - resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.4.0-beta.5.tgz#523fd0a1c5668370d73e05019ed16eaf596894c8" - integrity sha512-Qe0idPpSokCNvGrthSBjdrOZrsgXwnLYbzuv0JoEec/A9HVcxKmZ+ktw7fOA2gT/zbcwtrA5FWrir3GlRHglCQ== +xterm-addon-web-links@0.4.0-beta.6: + version "0.4.0-beta.6" + resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.4.0-beta.6.tgz#d159d4542eb9a02d57977fe7eb5f42f8ef2f27fa" + integrity sha512-dsQVD/EyVq8PtAYGh2PGQTCt009UipIfX6Q2SBDlz+W9x7IkXjhRxRaryMmLsBCca20qeVKwmbQ+ANhLi+nTaQ== xterm-addon-webgl@0.7.0-beta.10: version "0.7.0-beta.10" resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.7.0-beta.10.tgz#39fdb96351e97a1bf15f4c4c8944ba3d05cacee4" integrity sha512-nQl/ASk+ck11aSrBZXb2a0tu+SNDnm89owBk/sAZeZzi5MHNo6bB8y2VTKNNC6D3i3aFouTz4VorYB25LUgNFg== -xterm@4.6.0-beta.38: - version "4.6.0-beta.38" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.6.0-beta.38.tgz#8472b168941500c3071aba482c2b5c6040951ec7" - integrity sha512-Q+nOalMD1MDGOqXdtkGZmOQqbSBU+71vhlX2RBwQoSpJa1QBrKDAhSlN/J+/XvouvVEtCiEFDeacF4EufMEIMg== +xterm@4.6.0-beta.44: + version "4.6.0-beta.44" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.6.0-beta.44.tgz#76b2a6b8e147595ab44aa752c0e721d935464615" + integrity sha512-vYtfz4spFcSKLEUpC6anH7TwDams71+k2wAtUzCJ47dNL2IrwYafcFsvGPm46QLTtq4M2Bp9rQo3R3V746yxNg== yauzl@^2.9.2: version "2.10.0" diff --git a/resources/linux/bin/code.sh b/resources/linux/bin/code.sh index 96aa59e3d8..91e4d79d27 100755 --- a/resources/linux/bin/code.sh +++ b/resources/linux/bin/code.sh @@ -9,6 +9,7 @@ if grep -qi Microsoft /proc/version && [ -z "$DONT_PROMPT_WSL_INSTALL" ]; then read -e -p "Do you want to continue anyways ? [y/N] " YN [[ $YN == "n" || $YN == "N" || $YN == "" ]] && exit 1 + echo "To no longer see this prompt, start @@PRODNAME@@ with the environment variable DONT_PROMPT_WSL_INSTALL defined." fi diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index ae9d1c08d3..c07b93569c 100755 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -4,14 +4,16 @@ setlocal pushd %~dp0\.. set VSCODEUSERDATADIR=%TEMP%\vscodeuserfolder-%RANDOM%-%TIME:~6,2% +set VSCODECRASHDIR=%~dp0\..\.build\crashes :: Figure out which Electron to use for running tests if "%INTEGRATION_TEST_ELECTRON_PATH%"=="" ( - :: Run out of sources: no need to compile as code.sh takes care of it + :: Run out of sources: no need to compile as code.bat takes care of it chcp 65001 set INTEGRATION_TEST_ELECTRON_PATH=.\scripts\code.bat set VSCODE_BUILD_BUILTIN_EXTENSIONS_SILENCE_PLEASE=1 + echo Storing crash reports into '%VSCODECRASHDIR%'. echo Running integration tests out of sources. ) else ( :: Run from a built: need to compile all test extensions @@ -29,8 +31,8 @@ if "%INTEGRATION_TEST_ELECTRON_PATH%"=="" ( :: Configuration for more verbose output set VSCODE_CLI=1 set ELECTRON_ENABLE_LOGGING=1 - set ELECTRON_ENABLE_STACK_DUMPING=1 + echo Storing crash reports into '%VSCODECRASHDIR%'. echo Running integration tests with '%INTEGRATION_TEST_ELECTRON_PATH%' as build. ) @@ -41,19 +43,19 @@ if "%INTEGRATION_TEST_ELECTRON_PATH%"=="" ( :: Tests in the extension host -REM call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-api-tests\testWorkspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=%~dp0\..\extensions\vscode-api-tests --extensionTestsPath=%~dp0\..\extensions\vscode-api-tests\out\singlefolder-tests --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% +REM call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-api-tests\testWorkspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=%~dp0\..\extensions\vscode-api-tests --extensionTestsPath=%~dp0\..\extensions\vscode-api-tests\out\singlefolder-tests --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% REM if %errorlevel% neq 0 exit /b %errorlevel% -REM call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-api-tests\testworkspace.code-workspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=%~dp0\..\extensions\vscode-api-tests --extensionTestsPath=%~dp0\..\extensions\vscode-api-tests\out\workspace-tests --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% +REM call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-api-tests\testworkspace.code-workspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=%~dp0\..\extensions\vscode-api-tests --extensionTestsPath=%~dp0\..\extensions\vscode-api-tests\out\workspace-tests --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% REM if %errorlevel% neq 0 exit /b %errorlevel% -REM call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-colorize-tests\test --extensionDevelopmentPath=%~dp0\..\extensions\vscode-colorize-tests --extensionTestsPath=%~dp0\..\extensions\vscode-colorize-tests\out --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% +REM call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-colorize-tests\test --extensionDevelopmentPath=%~dp0\..\extensions\vscode-colorize-tests --extensionTestsPath=%~dp0\..\extensions\vscode-colorize-tests\out --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% REM if %errorlevel% neq 0 exit /b %errorlevel% -REM call "%INTEGRATION_TEST_ELECTRON_PATH%" $%~dp0\..\extensions\markdown-language-features\out\test\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\markdown-language-features --extensionTestsPath=%~dp0\..\extensions\markdown-language-features\out\test --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% . +REM call "%INTEGRATION_TEST_ELECTRON_PATH%" $%~dp0\..\extensions\markdown-language-features\out\test\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\markdown-language-features --extensionTestsPath=%~dp0\..\extensions\markdown-language-features\out\test --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% . REM if %errorlevel% neq 0 exit /b %errorlevel% -REM call "%INTEGRATION_TEST_ELECTRON_PATH%" $%~dp0\..\extensions\emmet\out\test\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\emmet --extensionTestsPath=%~dp0\..\extensions\emmet\out\test --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% . +REM call "%INTEGRATION_TEST_ELECTRON_PATH%" $%~dp0\..\extensions\emmet\out\test\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\emmet --extensionTestsPath=%~dp0\..\extensions\emmet\out\test --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% . REM if %errorlevel% neq 0 exit /b %errorlevel% call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\azurecore\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\azurecore --extensionTestsPath=%~dp0\..\extensions\azurecore\out\test --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% @@ -62,7 +64,7 @@ if %errorlevel% neq 0 exit /b %errorlevel% for /f "delims=" %%i in ('node -p "require('fs').realpathSync.native(require('os').tmpdir())"') do set TEMPDIR=%%i set GITWORKSPACE=%TEMPDIR%\git-%RANDOM% mkdir %GITWORKSPACE% -call "%INTEGRATION_TEST_ELECTRON_PATH%" %GITWORKSPACE% --extensionDevelopmentPath=%~dp0\..\extensions\git --extensionTestsPath=%~dp0\..\extensions\git\out\test --enable-proposed-api=vscode.git --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% +call "%INTEGRATION_TEST_ELECTRON_PATH%" %GITWORKSPACE% --extensionDevelopmentPath=%~dp0\..\extensions\git --extensionTestsPath=%~dp0\..\extensions\git\out\test --enable-proposed-api=vscode.git --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% if %errorlevel% neq 0 exit /b %errorlevel% :: Tests in commonJS (HTML, CSS, JSON language server tests...) diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index 2a1b62a28a..20e9d20d2c 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -10,6 +10,7 @@ else fi VSCODEUSERDATADIR=`mktemp -d 2>/dev/null` +VSCODECRASHDIR=$ROOT/.build/crashes cd $ROOT # Figure out which Electron to use for running tests @@ -18,6 +19,7 @@ then # Run out of sources: no need to compile as code.sh takes care of it INTEGRATION_TEST_ELECTRON_PATH="./scripts/code.sh" + echo "Storing crash reports into '$VSCODECRASHDIR'." echo "Running integration tests out of sources." else # Run from a built: need to compile all test extensions @@ -38,6 +40,7 @@ else export ELECTRON_ENABLE_STACK_DUMPING=1 export ELECTRON_ENABLE_LOGGING=1 + echo "Storing crash reports into '$VSCODECRASHDIR'." echo "Running integration tests with '$INTEGRATION_TEST_ELECTRON_PATH' as build." fi @@ -45,13 +48,13 @@ fi ./scripts/test.sh --runGlob **/*.integrationTest.js "$@" # Tests in the extension host -# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/vscode-notebook-tests/test --enable-proposed-api=vscode.vscode-notebook-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-notebook-tests --extensionTestsPath=$ROOT/extensions/vscode-notebook-tests/out/ --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR -# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/vscode-api-tests/testWorkspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/singlefolder-tests --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR -# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/vscode-api-tests/testworkspace.code-workspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/workspace-tests --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR -# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/vscode-colorize-tests/test --extensionDevelopmentPath=$ROOT/extensions/vscode-colorize-tests --extensionTestsPath=$ROOT/extensions/vscode-colorize-tests/out --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR -# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/markdown-language-features/out/test/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/markdown-language-features --extensionTestsPath=$ROOT/extensions/markdown-language-features/out/test --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR -# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/emmet/out/test/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/emmet --extensionTestsPath=$ROOT/extensions/emmet/out/test --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR -# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $(mktemp -d 2>/dev/null) --enable-proposed-api=vscode.git --extensionDevelopmentPath=$ROOT/extensions/git --extensionTestsPath=$ROOT/extensions/git/out/test --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR +# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/vscode-notebook-tests/test --enable-proposed-api=vscode.vscode-notebook-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-notebook-tests --extensionTestsPath=$ROOT/extensions/vscode-notebook-tests/out/ --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR +# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/vscode-api-tests/testWorkspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/singlefolder-tests --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR +# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/vscode-api-tests/testworkspace.code-workspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/workspace-tests --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR +# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/vscode-colorize-tests/test --extensionDevelopmentPath=$ROOT/extensions/vscode-colorize-tests --extensionTestsPath=$ROOT/extensions/vscode-colorize-tests/out --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR +# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/markdown-language-features/out/test/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/markdown-language-features --extensionTestsPath=$ROOT/extensions/markdown-language-features/out/test --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR +# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/emmet/out/test/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/emmet --extensionTestsPath=$ROOT/extensions/emmet/out/test --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR +# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $(mktemp -d 2>/dev/null) --enable-proposed-api=vscode.git --extensionDevelopmentPath=$ROOT/extensions/git --extensionTestsPath=$ROOT/extensions/git/out/test --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/azurecore/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/azurecore --extensionTestsPath=$ROOT/extensions/azurecore/out/test --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR diff --git a/src/main.js b/src/main.js index f02b10901e..4f31e22afb 100644 --- a/src/main.js +++ b/src/main.js @@ -39,6 +39,25 @@ if (args['nogpu']) { // {{SQL CARBON EDIT}} const userDataPath = getUserDataPath(args); app.setPath('userData', userDataPath); +// Set temp directory based on crash-reporter-directory CLI argument +// The crash reporter will store crashes in temp folder so we need +// to change that location accordingly. +let crashReporterDirectory = args['crash-reporter-directory']; +if (crashReporterDirectory) { + crashReporterDirectory = path.normalize(crashReporterDirectory); + + if (!fs.existsSync(crashReporterDirectory)) { + try { + fs.mkdirSync(crashReporterDirectory); + } catch (error) { + console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory does not seem to exist or cannot be created.`); + app.exit(1); + } + } + console.log(`Found --crash-reporter-directory argument. Setting temp directory to be '${crashReporterDirectory}'`); + app.setPath('temp', crashReporterDirectory); +} + // Set logs path before app 'ready' event if running portable // to ensure that no 'logs' folder is created on disk at a // location outside of the portable directory @@ -336,7 +355,8 @@ function parseCLIArgs() { 'user-data-dir', 'locale', 'js-flags', - 'max-memory' + 'max-memory', + 'crash-reporter-directory' ] }); } diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index 38412c1358..da7b37b0e0 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -477,27 +477,27 @@ export class ActionBar extends Disposable implements IActionRunner { DOM.addClass(this.domNode, 'animated'); } - let previousKey: KeyCode; - let nextKey: KeyCode; + let previousKeys: KeyCode[]; + let nextKeys: KeyCode[]; switch (this.options.orientation) { case ActionsOrientation.HORIZONTAL: - previousKey = KeyCode.LeftArrow; - nextKey = KeyCode.RightArrow; + previousKeys = [KeyCode.LeftArrow, KeyCode.UpArrow]; + nextKeys = [KeyCode.RightArrow, KeyCode.DownArrow]; break; case ActionsOrientation.HORIZONTAL_REVERSE: - previousKey = KeyCode.RightArrow; - nextKey = KeyCode.LeftArrow; + previousKeys = [KeyCode.RightArrow, KeyCode.DownArrow]; + nextKeys = [KeyCode.LeftArrow, KeyCode.UpArrow]; this.domNode.className += ' reverse'; break; case ActionsOrientation.VERTICAL: - previousKey = KeyCode.UpArrow; - nextKey = KeyCode.DownArrow; + previousKeys = [KeyCode.LeftArrow, KeyCode.UpArrow]; + nextKeys = [KeyCode.RightArrow, KeyCode.DownArrow]; this.domNode.className += ' vertical'; break; case ActionsOrientation.VERTICAL_REVERSE: - previousKey = KeyCode.DownArrow; - nextKey = KeyCode.UpArrow; + previousKeys = [KeyCode.RightArrow, KeyCode.DownArrow]; + nextKeys = [KeyCode.LeftArrow, KeyCode.UpArrow]; this.domNode.className += ' vertical reverse'; break; } @@ -506,9 +506,9 @@ export class ActionBar extends Disposable implements IActionRunner { const event = new StandardKeyboardEvent(e); let eventHandled = true; - if (event.equals(previousKey)) { + if (previousKeys && (event.equals(previousKeys[0]) || event.equals(previousKeys[1]))) { this.focusPrevious(); - } else if (event.equals(nextKey)) { + } else if (nextKeys && (event.equals(nextKeys[0]) || event.equals(nextKeys[1]))) { this.focusNext(); } else if (event.equals(KeyCode.Escape)) { this._onDidCancel.fire(); diff --git a/src/vs/base/browser/ui/countBadge/countBadge.css b/src/vs/base/browser/ui/countBadge/countBadge.css index f7bab5b08e..3d3915b604 100644 --- a/src/vs/base/browser/ui/countBadge/countBadge.css +++ b/src/vs/base/browser/ui/countBadge/countBadge.css @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ .monaco-count-badge { - padding: 3px 5px; + padding: 3px 6px; border-radius: 11px; font-size: 11px; min-width: 18px; @@ -14,4 +14,9 @@ text-align: center; display: inline-block; box-sizing: border-box; -} \ No newline at end of file +} + +.monaco-count-badge.long { + padding: 2px 3px; + border-radius: 2px; +} diff --git a/src/vs/base/browser/ui/tree/indexTreeModel.ts b/src/vs/base/browser/ui/tree/indexTreeModel.ts index d901efae8b..2af4512869 100644 --- a/src/vs/base/browser/ui/tree/indexTreeModel.ts +++ b/src/vs/base/browser/ui/tree/indexTreeModel.ts @@ -18,6 +18,7 @@ export interface IIndexTreeNode extends ITreeNode, TFilterData = voi collapsible: false, collapsed: false, renderNodeCount: 0, + visibility: TreeVisibility.Visible, visible: true, filterData: undefined }; @@ -185,6 +187,17 @@ export class IndexTreeModel, TFilterData = voi } this._onDidSplice.fire({ insertedNodes: nodesToInsert, deletedNodes }); + + let node: IIndexTreeNode | undefined = parentNode; + + while (node) { + if (node.visibility === TreeVisibility.Recurse) { + this.refilter(); + break; + } + + node = node.parent; + } } rerender(location: number[]): void { @@ -194,7 +207,7 @@ export class IndexTreeModel, TFilterData = voi const { node, listIndex, revealed } = this.getTreeNodeWithListIndex(location); - if (revealed) { + if (node.visible && revealed) { this.list.splice(listIndex, 1, [node]); } } @@ -355,11 +368,13 @@ export class IndexTreeModel, TFilterData = voi collapsible: typeof treeElement.collapsible === 'boolean' ? treeElement.collapsible : (typeof treeElement.collapsed !== 'undefined'), collapsed: typeof treeElement.collapsed === 'undefined' ? this.collapseByDefault : treeElement.collapsed, renderNodeCount: 1, + visibility: TreeVisibility.Visible, visible: true, filterData: undefined }; const visibility = this._filterNode(node, parentVisibility); + node.visibility = visibility; if (revealed) { treeListElements.push(node); diff --git a/src/vs/base/common/codicons.ts b/src/vs/base/common/codicons.ts index 95a64ade15..9c7c88cd86 100644 --- a/src/vs/base/common/codicons.ts +++ b/src/vs/base/common/codicons.ts @@ -43,12 +43,12 @@ const _registry = new Registry(); export const iconRegistry: IIconRegistry = _registry; -export function registerIcon(id: string, def: Codicon) { +export function registerIcon(id: string, def: Codicon, description?: string) { return new Codicon(id, def); } export class Codicon { - constructor(public readonly id: string, public readonly definition: Codicon | IconDefinition) { + constructor(public readonly id: string, public readonly definition: Codicon | IconDefinition, public description?: string) { _registry.add(this); } public get classNames() { return 'codicon codicon-' + this.id; } diff --git a/src/vs/base/common/jsonSchema.ts b/src/vs/base/common/jsonSchema.ts index 9f2c8519b8..8834dc3807 100644 --- a/src/vs/base/common/jsonSchema.ts +++ b/src/vs/base/common/jsonSchema.ts @@ -63,6 +63,7 @@ export interface IJSONSchema { markdownEnumDescriptions?: string[]; markdownDescription?: string; doNotSuggest?: boolean; + suggestSortText?: string; allowComments?: boolean; allowTrailingCommas?: boolean; } diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index e5813e996b..eb56c470b0 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -580,9 +580,7 @@ export const enum Touch { AsNew = 2 } -export class LinkedMap implements Map{ - - readonly [Symbol.toStringTag] = 'LinkedMap'; +export class LinkedMap { private _map: Map>; private _head: Item | undefined; @@ -634,7 +632,7 @@ export class LinkedMap implements Map{ return item.value; } - set(key: K, value: V, touch: Touch = Touch.None): this { + set(key: K, value: V, touch: Touch = Touch.None): void { let item = this._map.get(key); if (item) { item.value = value; @@ -660,7 +658,6 @@ export class LinkedMap implements Map{ this._map.set(key, item); this._size++; } - return this; } delete(key: K): boolean { @@ -704,13 +701,34 @@ export class LinkedMap implements Map{ } } - keys(): IterableIterator { + values(): V[] { + const result: V[] = []; let current = this._head; + while (current) { + result.push(current.value); + current = current.next; + } + return result; + } + + keys(): K[] { + const result: K[] = []; + let current = this._head; + while (current) { + result.push(current.key); + current = current.next; + } + return result; + } + + /* VS Code / Monaco editor runs on es5 which has no Symbol.iterator + keys(): IterableIterator { + const current = this._head; const iterator: IterableIterator = { [Symbol.iterator]() { return iterator; }, - next(): IteratorResult { + next():IteratorResult { if (current) { const result = { value: current.key, done: false }; current = current.next; @@ -724,12 +742,12 @@ export class LinkedMap implements Map{ } values(): IterableIterator { - let current = this._head; + const current = this._head; const iterator: IterableIterator = { [Symbol.iterator]() { return iterator; }, - next(): IteratorResult { + next():IteratorResult { if (current) { const result = { value: current.value, done: false }; current = current.next; @@ -741,29 +759,7 @@ export class LinkedMap implements Map{ }; return iterator; } - - entries(): IterableIterator<[K, V]> { - let current = this._head; - const iterator: IterableIterator<[K, V]> = { - [Symbol.iterator]() { - return iterator; - }, - next(): IteratorResult<[K, V]> { - if (current) { - const result: IteratorResult<[K, V]> = { value: [current.key, current.value], done: false }; - current = current.next; - return result; - } else { - return { value: undefined, done: true }; - } - } - }; - return iterator; - } - - [Symbol.iterator](): IterableIterator<[K, V]> { - return this.entries(); - } + */ protected trimOld(newSize: number) { if (newSize >= this.size) { @@ -965,10 +961,9 @@ export class LRUCache extends LinkedMap { return super.get(key, Touch.None); } - set(key: K, value: V): this { + set(key: K, value: V): void { super.set(key, value, Touch.AsNew); this.checkTrim(); - return this; } private checkTrim() { diff --git a/src/vs/base/parts/quickinput/browser/media/quickInput.css b/src/vs/base/parts/quickinput/browser/media/quickInput.css index aa2cb9758d..5a1e959ec4 100644 --- a/src/vs/base/parts/quickinput/browser/media/quickInput.css +++ b/src/vs/base/parts/quickinput/browser/media/quickInput.css @@ -97,10 +97,14 @@ align-self: center; position: absolute; right: 4px; + display: flex; + align-items: center; } .quick-input-count .monaco-count-badge { vertical-align: middle; + padding: 2px 4px; + border-radius: 2px; } .quick-input-action { @@ -108,9 +112,8 @@ } .quick-input-action .monaco-text-button { - font-size: 85%; + font-size: 11px; padding: 0 6px; - line-height: initial; display: flex; height: 100%; align-items: center; diff --git a/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts b/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts index cd6c9fc389..f099f53d07 100644 --- a/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts +++ b/src/vs/base/test/browser/ui/tree/objectTreeModel.test.ts @@ -4,13 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { ITreeNode } from 'vs/base/browser/ui/tree/tree'; +import { ITreeNode, ITreeFilter, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { ISpliceable } from 'vs/base/common/sequence'; import { ObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; function toSpliceable(arr: T[]): ISpliceable { return { splice(start: number, deleteCount: number, elements: T[]): void { + // console.log(`splice (${start}, ${deleteCount}, ${elements.length} [${elements.join(', ')}] )`); // debugging arr.splice(start, deleteCount, ...elements); } }; @@ -240,4 +241,35 @@ suite('ObjectTreeModel', function () { model.expandTo(1000); assert.deepEqual(toArray(list), [0, 10, 100, 1000, 11, 12, 1, 2]); }); + + test('issue #95641', () => { + const list: ITreeNode[] = []; + let fn = (_: string) => true; + const filter = new class implements ITreeFilter { + filter(element: string, parentVisibility: TreeVisibility): TreeVisibility { + if (element === 'file') { + return TreeVisibility.Recurse; + } + + return fn(element) ? TreeVisibility.Visible : parentVisibility; + } + }; + const model = new ObjectTreeModel('test', toSpliceable(list), { filter }); + + model.setChildren(null, [{ element: 'file', children: [{ element: 'hello' }] }]); + assert.deepEqual(toArray(list), ['file', 'hello']); + + fn = (el: string) => el === 'world'; + model.refilter(); + assert.deepEqual(toArray(list), []); + + model.setChildren('file', [{ element: 'world' }]); + assert.deepEqual(toArray(list), ['file', 'world']); + + model.setChildren('file', [{ element: 'hello' }]); + assert.deepEqual(toArray(list), []); + + model.setChildren('file', [{ element: 'world' }]); + assert.deepEqual(toArray(list), ['file', 'world']); + }); }); diff --git a/src/vs/base/test/common/map.test.ts b/src/vs/base/test/common/map.test.ts index 2ca1eb2aa7..3650bff666 100644 --- a/src/vs/base/test/common/map.test.ts +++ b/src/vs/base/test/common/map.test.ts @@ -13,8 +13,8 @@ suite('Map', () => { let map = new LinkedMap(); map.set('ak', 'av'); map.set('bk', 'bv'); - assert.deepStrictEqual([...map.keys()], ['ak', 'bk']); - assert.deepStrictEqual([...map.values()], ['av', 'bv']); + assert.deepStrictEqual(map.keys(), ['ak', 'bk']); + assert.deepStrictEqual(map.values(), ['av', 'bv']); assert.equal(map.first, 'av'); assert.equal(map.last, 'bv'); }); @@ -23,16 +23,16 @@ suite('Map', () => { let map = new LinkedMap(); map.set('ak', 'av'); map.set('ak', 'av', Touch.AsOld); - assert.deepStrictEqual([...map.keys()], ['ak']); - assert.deepStrictEqual([...map.values()], ['av']); + assert.deepStrictEqual(map.keys(), ['ak']); + assert.deepStrictEqual(map.values(), ['av']); }); test('LinkedMap - Touch New one', () => { let map = new LinkedMap(); map.set('ak', 'av'); map.set('ak', 'av', Touch.AsNew); - assert.deepStrictEqual([...map.keys()], ['ak']); - assert.deepStrictEqual([...map.values()], ['av']); + assert.deepStrictEqual(map.keys(), ['ak']); + assert.deepStrictEqual(map.values(), ['av']); }); test('LinkedMap - Touch Old two', () => { @@ -40,8 +40,8 @@ suite('Map', () => { map.set('ak', 'av'); map.set('bk', 'bv'); map.set('bk', 'bv', Touch.AsOld); - assert.deepStrictEqual([...map.keys()], ['bk', 'ak']); - assert.deepStrictEqual([...map.values()], ['bv', 'av']); + assert.deepStrictEqual(map.keys(), ['bk', 'ak']); + assert.deepStrictEqual(map.values(), ['bv', 'av']); }); test('LinkedMap - Touch New two', () => { @@ -49,8 +49,8 @@ suite('Map', () => { map.set('ak', 'av'); map.set('bk', 'bv'); map.set('ak', 'av', Touch.AsNew); - assert.deepStrictEqual([...map.keys()], ['bk', 'ak']); - assert.deepStrictEqual([...map.values()], ['bv', 'av']); + assert.deepStrictEqual(map.keys(), ['bk', 'ak']); + assert.deepStrictEqual(map.values(), ['bv', 'av']); }); test('LinkedMap - Touch Old from middle', () => { @@ -59,8 +59,8 @@ suite('Map', () => { map.set('bk', 'bv'); map.set('ck', 'cv'); map.set('bk', 'bv', Touch.AsOld); - assert.deepStrictEqual([...map.keys()], ['bk', 'ak', 'ck']); - assert.deepStrictEqual([...map.values()], ['bv', 'av', 'cv']); + assert.deepStrictEqual(map.keys(), ['bk', 'ak', 'ck']); + assert.deepStrictEqual(map.values(), ['bv', 'av', 'cv']); }); test('LinkedMap - Touch New from middle', () => { @@ -69,8 +69,8 @@ suite('Map', () => { map.set('bk', 'bv'); map.set('ck', 'cv'); map.set('bk', 'bv', Touch.AsNew); - assert.deepStrictEqual([...map.keys()], ['ak', 'ck', 'bk']); - assert.deepStrictEqual([...map.values()], ['av', 'cv', 'bv']); + assert.deepStrictEqual(map.keys(), ['ak', 'ck', 'bk']); + assert.deepStrictEqual(map.values(), ['av', 'cv', 'bv']); }); test('LinkedMap - basics', function () { @@ -136,15 +136,13 @@ suite('Map', () => { assert.strictEqual(cache.size, 5); cache.set(6, 6); assert.strictEqual(cache.size, 5); - assert.deepStrictEqual([...cache.keys()], [2, 3, 4, 5, 6]); + assert.deepStrictEqual(cache.keys(), [2, 3, 4, 5, 6]); cache.set(7, 7); assert.strictEqual(cache.size, 5); - assert.deepStrictEqual([...cache.keys()], [3, 4, 5, 6, 7]); + assert.deepStrictEqual(cache.keys(), [3, 4, 5, 6, 7]); let values: number[] = []; [3, 4, 5, 6, 7].forEach(key => values.push(cache.get(key)!)); assert.deepStrictEqual(values, [3, 4, 5, 6, 7]); - - assert.deepEqual([...cache.entries()], [[3, 3], [4, 4], [5, 5], [6, 6], [7, 7]]); }); test('LinkedMap - LRU Cache get', () => { @@ -152,11 +150,11 @@ suite('Map', () => { [1, 2, 3, 4, 5].forEach(value => cache.set(value, value)); assert.strictEqual(cache.size, 5); - assert.deepStrictEqual([...cache.keys()], [1, 2, 3, 4, 5]); + assert.deepStrictEqual(cache.keys(), [1, 2, 3, 4, 5]); cache.get(3); - assert.deepStrictEqual([...cache.keys()], [1, 2, 4, 5, 3]); + assert.deepStrictEqual(cache.keys(), [1, 2, 4, 5, 3]); cache.peek(4); - assert.deepStrictEqual([...cache.keys()], [1, 2, 4, 5, 3]); + assert.deepStrictEqual(cache.keys(), [1, 2, 4, 5, 3]); let values: number[] = []; [1, 2, 3, 4, 5].forEach(key => values.push(cache.get(key)!)); assert.deepStrictEqual(values, [1, 2, 3, 4, 5]); @@ -171,7 +169,7 @@ suite('Map', () => { assert.strictEqual(cache.size, 10); cache.limit = 5; assert.strictEqual(cache.size, 5); - assert.deepStrictEqual([...cache.keys()], [6, 7, 8, 9, 10]); + assert.deepStrictEqual(cache.keys(), [6, 7, 8, 9, 10]); cache.limit = 20; assert.strictEqual(cache.size, 5); for (let i = 11; i <= 20; i++) { @@ -183,7 +181,7 @@ suite('Map', () => { values.push(cache.get(i)!); assert.strictEqual(cache.get(i), i); } - assert.deepStrictEqual([...cache.values()], values); + assert.deepStrictEqual(cache.values(), values); }); test('LinkedMap - LRU Cache limit with ratio', () => { @@ -195,11 +193,11 @@ suite('Map', () => { assert.strictEqual(cache.size, 10); cache.set(11, 11); assert.strictEqual(cache.size, 5); - assert.deepStrictEqual([...cache.keys()], [7, 8, 9, 10, 11]); + assert.deepStrictEqual(cache.keys(), [7, 8, 9, 10, 11]); let values: number[] = []; - [...cache.keys()].forEach(key => values.push(cache.get(key)!)); + cache.keys().forEach(key => values.push(cache.get(key)!)); assert.deepStrictEqual(values, [7, 8, 9, 10, 11]); - assert.deepStrictEqual([...cache.values()], values); + assert.deepStrictEqual(cache.values(), values); }); test('LinkedMap - toJSON / fromJSON', () => { @@ -239,7 +237,7 @@ suite('Map', () => { map.delete('1'); assert.equal(map.get('1'), undefined); assert.equal(map.size, 0); - assert.equal([...map.keys()].length, 0); + assert.equal(map.keys().length, 0); }); test('LinkedMap - delete Head', function () { @@ -253,8 +251,8 @@ suite('Map', () => { map.delete('1'); assert.equal(map.get('2'), 2); assert.equal(map.size, 1); - assert.equal([...map.keys()].length, 1); - assert.equal([...map.keys()][0], 2); + assert.equal(map.keys().length, 1); + assert.equal(map.keys()[0], 2); }); test('LinkedMap - delete Tail', function () { @@ -268,8 +266,8 @@ suite('Map', () => { map.delete('2'); assert.equal(map.get('1'), 1); assert.equal(map.size, 1); - assert.equal([...map.keys()].length, 1); - assert.equal([...map.keys()][0], 1); + assert.equal(map.keys().length, 1); + assert.equal(map.keys()[0], 1); }); diff --git a/src/vs/editor/browser/controller/pointerHandler.ts b/src/vs/editor/browser/controller/pointerHandler.ts index 9686283e1a..4b438f6e1a 100644 --- a/src/vs/editor/browser/controller/pointerHandler.ts +++ b/src/vs/editor/browser/controller/pointerHandler.ts @@ -31,84 +31,6 @@ function gestureChangeEventMerger(lastEvent: IThrottledGestureEvent | null, curr return r; } -/** - * Basically IE10 and IE11 - */ -class MsPointerHandler extends MouseHandler implements IDisposable { - - private _lastPointerType: string; - private _installGestureHandlerTimeout: number; - - constructor(context: ViewContext, viewController: ViewController, viewHelper: IPointerHandlerHelper) { - super(context, viewController, viewHelper); - - (this.viewHelper.linesContentDomNode.style as any).msTouchAction = 'none'; - (this.viewHelper.linesContentDomNode.style as any).msContentZooming = 'none'; - - // TODO@Alex -> this expects that the view is added in 100 ms, might not be the case - // This handler should be added when the dom node is in the dom tree - this._installGestureHandlerTimeout = window.setTimeout(() => { - this._installGestureHandlerTimeout = -1; - if ((window).MSGesture) { - const touchGesture = new MSGesture(); - const penGesture = new MSGesture(); - touchGesture.target = this.viewHelper.linesContentDomNode; - penGesture.target = this.viewHelper.linesContentDomNode; - this.viewHelper.linesContentDomNode.addEventListener('MSPointerDown', (e: MSPointerEvent) => { - // Circumvent IE11 breaking change in e.pointerType & TypeScript's stale definitions - const pointerType = e.pointerType; - if (pointerType === ((e).MSPOINTER_TYPE_MOUSE || 'mouse')) { - this._lastPointerType = 'mouse'; - return; - } else if (pointerType === ((e).MSPOINTER_TYPE_TOUCH || 'touch')) { - this._lastPointerType = 'touch'; - touchGesture.addPointer(e.pointerId); - } else { - this._lastPointerType = 'pen'; - penGesture.addPointer(e.pointerId); - } - }); - this._register(dom.addDisposableThrottledListener(this.viewHelper.linesContentDomNode, 'MSGestureChange', (e) => this._onGestureChange(e), gestureChangeEventMerger)); - this._register(dom.addDisposableListener(this.viewHelper.linesContentDomNode, 'MSGestureTap', (e) => this._onCaptureGestureTap(e), true)); - } - }, 100); - this._lastPointerType = 'mouse'; - } - - public _onMouseDown(e: EditorMouseEvent): void { - if (this._lastPointerType === 'mouse') { - super._onMouseDown(e); - } - } - - private _onCaptureGestureTap(rawEvent: MSGestureEvent): void { - const e = new EditorMouseEvent(rawEvent, this.viewHelper.viewDomNode); - const t = this._createMouseTarget(e, false); - if (t.position) { - this.viewController.moveTo(t.position); - } - // IE does not want to focus when coming in from the browser's address bar - if ((e.browserEvent).fromElement) { - e.preventDefault(); - this.viewHelper.focusTextArea(); - } else { - // TODO@Alex -> cancel this is focus is lost - setTimeout(() => { - this.viewHelper.focusTextArea(); - }); - } - } - - private _onGestureChange(e: IThrottledGestureEvent): void { - this._context.viewLayout.deltaScrollNow(-e.translationX, -e.translationY); - } - - public dispose(): void { - window.clearTimeout(this._installGestureHandlerTimeout); - super.dispose(); - } -} - /** * Basically Edge but should be modified to handle any pointerEnabled, even without support of MSGesture */ @@ -302,9 +224,7 @@ export class PointerHandler extends Disposable { constructor(context: ViewContext, viewController: ViewController, viewHelper: IPointerHandlerHelper) { super(); - if (window.navigator.msPointerEnabled) { - this.handler = this._register(new MsPointerHandler(context, viewController, viewHelper)); - } else if ((platform.isIOS && BrowserFeatures.pointerEvents)) { + if ((platform.isIOS && BrowserFeatures.pointerEvents)) { this.handler = this._register(new PointerEventHandler(context, viewController, viewHelper)); } else if ((window).TouchEvent) { this.handler = this._register(new TouchHandler(context, viewController, viewHelper)); diff --git a/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts b/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts index 356d93a11a..261e63e8b9 100644 --- a/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts +++ b/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts @@ -111,7 +111,7 @@ export class EditorScrollbar extends ViewPart { const minimap = options.get(EditorOption.minimap); const side = minimap.side; if (side === 'right') { - this.scrollbarDomNode.setWidth(layoutInfo.contentWidth + layoutInfo.minimapWidth); + this.scrollbarDomNode.setWidth(layoutInfo.contentWidth + layoutInfo.minimap.minimapWidth); } else { this.scrollbarDomNode.setWidth(layoutInfo.contentWidth); } diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index 00fb54efae..bc5aebe196 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -104,30 +104,31 @@ class MinimapOptions { const options = configuration.options; const pixelRatio = options.get(EditorOption.pixelRatio); const layoutInfo = options.get(EditorOption.layoutInfo); + const minimapLayout = layoutInfo.minimap; const fontInfo = options.get(EditorOption.fontInfo); const minimapOpts = options.get(EditorOption.minimap); - this.renderMinimap = layoutInfo.renderMinimap | 0; + this.renderMinimap = minimapLayout.renderMinimap; this.size = minimapOpts.size; - this.minimapHeightIsEditorHeight = layoutInfo.minimapHeightIsEditorHeight; + this.minimapHeightIsEditorHeight = minimapLayout.minimapHeightIsEditorHeight; this.scrollBeyondLastLine = options.get(EditorOption.scrollBeyondLastLine); this.showSlider = minimapOpts.showSlider; this.pixelRatio = pixelRatio; this.typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth; this.lineHeight = options.get(EditorOption.lineHeight); - this.minimapLeft = layoutInfo.minimapLeft; - this.minimapWidth = layoutInfo.minimapWidth; + this.minimapLeft = minimapLayout.minimapLeft; + this.minimapWidth = minimapLayout.minimapWidth; this.minimapHeight = layoutInfo.height; - this.canvasInnerWidth = layoutInfo.minimapCanvasInnerWidth; - this.canvasInnerHeight = layoutInfo.minimapCanvasInnerHeight; - this.canvasOuterWidth = layoutInfo.minimapCanvasOuterWidth; - this.canvasOuterHeight = layoutInfo.minimapCanvasOuterHeight; + this.canvasInnerWidth = minimapLayout.minimapCanvasInnerWidth; + this.canvasInnerHeight = minimapLayout.minimapCanvasInnerHeight; + this.canvasOuterWidth = minimapLayout.minimapCanvasOuterWidth; + this.canvasOuterHeight = minimapLayout.minimapCanvasOuterHeight; - this.isSampling = layoutInfo.minimapIsSampling; + this.isSampling = minimapLayout.minimapIsSampling; this.editorHeight = layoutInfo.height; - this.fontScale = layoutInfo.minimapScale; - this.minimapLineHeight = layoutInfo.minimapLineHeight; + this.fontScale = minimapLayout.minimapScale; + this.minimapLineHeight = minimapLayout.minimapLineHeight; this.minimapCharWidth = Constants.BASE_CHAR_WIDTH * this.fontScale; this.charRenderer = once(() => MinimapCharRendererFactory.create(this.fontScale, fontInfo.fontFamily)); @@ -262,7 +263,8 @@ class MinimapLayout { const computedSliderRatio = (maxMinimapSliderTop) / (scrollHeight - viewportHeight); const sliderTop = (scrollTop * computedSliderRatio); const sliderNeeded = (maxMinimapSliderTop > 0); - return new MinimapLayout(scrollTop, scrollHeight, sliderNeeded, computedSliderRatio, sliderTop, sliderHeight, 1, lineCount); + const maxLinesFitting = Math.floor(options.canvasInnerHeight / options.minimapLineHeight); + return new MinimapLayout(scrollTop, scrollHeight, sliderNeeded, computedSliderRatio, sliderTop, sliderHeight, 1, Math.min(lineCount, maxLinesFitting)); } // The visible line count in a viewport can change due to a number of reasons: diff --git a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts index 241763520f..ced91db145 100644 --- a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts +++ b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts @@ -42,7 +42,7 @@ export class ViewOverlayWidgets extends ViewPart { this._widgets = {}; this._verticalScrollbarWidth = layoutInfo.verticalScrollbarWidth; - this._minimapWidth = layoutInfo.minimapWidth; + this._minimapWidth = layoutInfo.minimap.minimapWidth; this._horizontalScrollbarHeight = layoutInfo.horizontalScrollbarHeight; this._editorHeight = layoutInfo.height; this._editorWidth = layoutInfo.width; @@ -68,7 +68,7 @@ export class ViewOverlayWidgets extends ViewPart { const layoutInfo = options.get(EditorOption.layoutInfo); this._verticalScrollbarWidth = layoutInfo.verticalScrollbarWidth; - this._minimapWidth = layoutInfo.minimapWidth; + this._minimapWidth = layoutInfo.minimap.minimapWidth; this._horizontalScrollbarHeight = layoutInfo.horizontalScrollbarHeight; this._editorHeight = layoutInfo.height; this._editorWidth = layoutInfo.width; diff --git a/src/vs/editor/browser/viewParts/scrollDecoration/scrollDecoration.ts b/src/vs/editor/browser/viewParts/scrollDecoration/scrollDecoration.ts index 3fbcda8b19..04d47c779a 100644 --- a/src/vs/editor/browser/viewParts/scrollDecoration/scrollDecoration.ts +++ b/src/vs/editor/browser/viewParts/scrollDecoration/scrollDecoration.ts @@ -58,10 +58,10 @@ export class ScrollDecorationViewPart extends ViewPart { const options = this._context.configuration.options; const layoutInfo = options.get(EditorOption.layoutInfo); - if (layoutInfo.renderMinimap === 0 || (layoutInfo.minimapWidth > 0 && layoutInfo.minimapLeft === 0)) { + if (layoutInfo.minimap.renderMinimap === 0 || (layoutInfo.minimap.minimapWidth > 0 && layoutInfo.minimap.minimapLeft === 0)) { this._width = layoutInfo.width; } else { - this._width = layoutInfo.width - layoutInfo.minimapWidth - layoutInfo.verticalScrollbarWidth; + this._width = layoutInfo.width - layoutInfo.minimap.minimapWidth - layoutInfo.verticalScrollbarWidth; } } diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index aaf2a0e3ff..f652d90aa5 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -8,7 +8,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import * as objects from 'vs/base/common/objects'; import * as arrays from 'vs/base/common/arrays'; -import { IEditorOptions, editorOptionsRegistry, ValidatedEditorOptions, IEnvironmentalOptions, IComputedEditorOptions, ConfigurationChangedEvent, EDITOR_MODEL_DEFAULTS, EditorOption, FindComputedEditorOptionValueById } from 'vs/editor/common/config/editorOptions'; +import { IEditorOptions, editorOptionsRegistry, ValidatedEditorOptions, IEnvironmentalOptions, IComputedEditorOptions, ConfigurationChangedEvent, EDITOR_MODEL_DEFAULTS, EditorOption, FindComputedEditorOptionValueById, ComputeOptionsMemory } from 'vs/editor/common/config/editorOptions'; import { EditorZoom } from 'vs/editor/common/config/editorZoom'; import { BareFontInfo, FontInfo } from 'vs/editor/common/config/fontInfo'; import { IConfiguration, IDimension } from 'vs/editor/common/editorCommon'; @@ -284,6 +284,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC public readonly onDidChange: Event = this._onDidChange.event; public readonly isSimpleWidget: boolean; + private _computeOptionsMemory: ComputeOptionsMemory; public options!: ComputedEditorOptions; private _isDominatedByLongLines: boolean; @@ -299,6 +300,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC this.isSimpleWidget = isSimpleWidget; this._isDominatedByLongLines = false; + this._computeOptionsMemory = new ComputeOptionsMemory(); this._viewLineCount = 1; this._lineNumbersDigitCount = 1; @@ -344,6 +346,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC const partialEnv = this._getEnvConfiguration(); const bareFontInfo = BareFontInfo.createFromValidatedSettings(this._validatedOptions, partialEnv.zoomLevel, this.isSimpleWidget); const env: IEnvironmentalOptions = { + memory: this._computeOptionsMemory, outerWidth: partialEnv.outerWidth, outerHeight: partialEnv.outerHeight, fontInfo: this.readConfiguration(bareFontInfo), diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index e479cd603e..4713d29f70 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -693,6 +693,7 @@ export interface IComputedEditorOptions { * @internal */ export interface IEnvironmentalOptions { + readonly memory: ComputeOptionsMemory | null; readonly outerWidth: number; readonly outerHeight: number; readonly fontInfo: FontInfo; @@ -706,6 +707,22 @@ export interface IEnvironmentalOptions { readonly accessibilitySupport: AccessibilitySupport; } +/** + * @internal + */ +export class ComputeOptionsMemory { + + public stableMinimapLayoutInput: IMinimapLayoutInput | null; + public stableFitMaxMinimapScale: number; + public stableFitRemainingWidth: number; + + constructor() { + this.stableMinimapLayoutInput = null; + this.stableFitMaxMinimapScale = 0; + this.stableFitRemainingWidth = 0; + } +} + export interface IEditorOption { readonly id: K1; readonly name: string; @@ -1717,32 +1734,19 @@ export interface EditorLayoutInfo { readonly contentWidth: number; /** - * The position for the minimap + * Layout information for the minimap */ - readonly minimapLeft: number; - /** - * The width of the minimap - */ - readonly minimapWidth: number; - readonly minimapHeightIsEditorHeight: boolean; - readonly minimapIsSampling: boolean; - readonly minimapScale: number; - readonly minimapLineHeight: number; - readonly minimapCanvasInnerWidth: number; - readonly minimapCanvasInnerHeight: number; - readonly minimapCanvasOuterWidth: number; - readonly minimapCanvasOuterHeight: number; - - /** - * Minimap render type - */ - readonly renderMinimap: RenderMinimap; + readonly minimap: EditorMinimapLayoutInfo; /** * The number of columns (of typical characters) fitting on a viewport line. */ readonly viewportColumn: number; + readonly isWordWrapMinified: boolean; + readonly isViewportWrapping: boolean; + readonly wrappingColumn: number; + /** * The width of the vertical scrollbar. */ @@ -1758,18 +1762,80 @@ export interface EditorLayoutInfo { readonly overviewRuler: OverviewRulerPosition; } +/** + * The internal layout details of the editor. + */ +export interface EditorMinimapLayoutInfo { + readonly renderMinimap: RenderMinimap; + readonly minimapLeft: number; + readonly minimapWidth: number; + readonly minimapHeightIsEditorHeight: boolean; + readonly minimapIsSampling: boolean; + readonly minimapScale: number; + readonly minimapLineHeight: number; + readonly minimapCanvasInnerWidth: number; + readonly minimapCanvasInnerHeight: number; + readonly minimapCanvasOuterWidth: number; + readonly minimapCanvasOuterHeight: number; +} + /** * @internal */ export interface EditorLayoutInfoComputerEnv { - outerWidth: number; - outerHeight: number; - lineHeight: number; - viewLineCount: number; - lineNumbersDigitCount: number; - typicalHalfwidthCharacterWidth: number; - maxDigitWidth: number; - pixelRatio: number; + readonly memory: ComputeOptionsMemory | null; + readonly outerWidth: number; + readonly outerHeight: number; + readonly isDominatedByLongLines: boolean + readonly lineHeight: number; + readonly viewLineCount: number; + readonly lineNumbersDigitCount: number; + readonly typicalHalfwidthCharacterWidth: number; + readonly maxDigitWidth: number; + readonly pixelRatio: number; +} + +/** + * @internal + */ +export interface IEditorLayoutComputerInput { + readonly outerWidth: number; + readonly outerHeight: number; + readonly isDominatedByLongLines: boolean + readonly lineHeight: number; + readonly lineNumbersDigitCount: number; + readonly typicalHalfwidthCharacterWidth: number; + readonly maxDigitWidth: number; + readonly pixelRatio: number; + readonly glyphMargin: boolean; + readonly lineDecorationsWidth: string | number; + readonly folding: boolean; + readonly minimap: Readonly>; + readonly scrollbar: InternalEditorScrollbarOptions; + readonly lineNumbers: InternalEditorRenderLineNumbersOptions; + readonly lineNumbersMinChars: number; + readonly scrollBeyondLastLine: boolean; + readonly wordWrap: 'wordWrapColumn' | 'on' | 'off' | 'bounded'; + readonly wordWrapColumn: number; + readonly wordWrapMinified: boolean; + readonly accessibilitySupport: AccessibilitySupport; +} + +/** + * @internal + */ +export interface IMinimapLayoutInput { + readonly outerWidth: number; + readonly outerHeight: number; + readonly lineHeight: number; + readonly typicalHalfwidthCharacterWidth: number; + readonly pixelRatio: number; + readonly scrollBeyondLastLine: boolean; + readonly minimap: Readonly>; + readonly verticalScrollbarWidth: number; + readonly viewLineCount: number; + readonly remainingWidth: number; + readonly isViewportWrapping: boolean; } /** @@ -1780,14 +1846,22 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption= 2 ? Math.round(input.minimap.scale * 2) : input.minimap.scale); + const minimapMaxColumn = input.minimap.maxColumn; + const minimapSize = input.minimap.size; + const minimapSide = input.minimap.side; + const verticalScrollbarWidth = input.verticalScrollbarWidth; + const viewLineCount = input.viewLineCount; + const remainingWidth = input.remainingWidth; + const isViewportWrapping = input.isViewportWrapping; + + const baseCharHeight = minimapRenderCharacters ? 2 : 3; + let minimapCanvasInnerHeight = Math.floor(pixelRatio * outerHeight); + const minimapCanvasOuterHeight = minimapCanvasInnerHeight / pixelRatio; + let minimapHeightIsEditorHeight = false; + let minimapIsSampling = false; + let minimapLineHeight = baseCharHeight * minimapScale; + let minimapCharWidth = minimapScale / pixelRatio; + let minimapWidthMultiplier: number = 1; + + if (minimapSize === 'fill' || minimapSize === 'fit') { + const { typicalViewportLineCount, extraLinesBeyondLastLine, desiredRatio, minimapLineCount } = EditorLayoutInfoComputer.computeContainedMinimapLineCount({ + viewLineCount: viewLineCount, + scrollBeyondLastLine: scrollBeyondLastLine, + height: outerHeight, + lineHeight: lineHeight, + pixelRatio: pixelRatio + }); + // ratio is intentionally not part of the layout to avoid the layout changing all the time + // when doing sampling + const ratio = viewLineCount / minimapLineCount; + + if (ratio > 1) { + minimapHeightIsEditorHeight = true; + minimapIsSampling = true; + minimapScale = 1; + minimapLineHeight = 1; + minimapCharWidth = minimapScale / pixelRatio; + } else { + let fitBecomesFill = false; + let maxMinimapScale = minimapScale + 1; + + if (minimapSize === 'fit') { + const effectiveMinimapHeight = Math.ceil((viewLineCount + extraLinesBeyondLastLine) * minimapLineHeight); + if (isViewportWrapping && couldUseMemory && remainingWidth <= memory.stableFitRemainingWidth) { + // There is a loop when using `fit` and viewport wrapping: + // - view line count impacts minimap layout + // - minimap layout impacts viewport width + // - viewport width impacts view line count + // To break the loop, once we go to a smaller minimap scale, we try to stick with it. + fitBecomesFill = true; + maxMinimapScale = memory.stableFitMaxMinimapScale; + } else { + fitBecomesFill = (effectiveMinimapHeight > minimapCanvasInnerHeight); + if (isViewportWrapping && fitBecomesFill) { + // remember for next time + memory.stableMinimapLayoutInput = input; + memory.stableFitRemainingWidth = remainingWidth; + } else { + memory.stableMinimapLayoutInput = null; + memory.stableFitRemainingWidth = 0; + } + } + } + + if (minimapSize === 'fill' || fitBecomesFill) { + minimapHeightIsEditorHeight = true; + const configuredMinimapScale = minimapScale; + minimapLineHeight = Math.min(lineHeight * pixelRatio, Math.max(1, Math.floor(1 / desiredRatio))); + minimapScale = Math.min(maxMinimapScale, Math.max(1, Math.floor(minimapLineHeight / baseCharHeight))); + if (minimapScale > configuredMinimapScale) { + minimapWidthMultiplier = Math.min(2, minimapScale / configuredMinimapScale); + } + minimapCharWidth = minimapScale / pixelRatio / minimapWidthMultiplier; + minimapCanvasInnerHeight = Math.ceil((Math.max(typicalViewportLineCount, viewLineCount + extraLinesBeyondLastLine)) * minimapLineHeight); + if (isViewportWrapping && fitBecomesFill) { + memory.stableFitMaxMinimapScale = minimapScale; + } + } + } + } + + // Given: + // (leaving 2px for the cursor to have space after the last character) + // viewportColumn = (contentWidth - verticalScrollbarWidth - 2) / typicalHalfwidthCharacterWidth + // minimapWidth = viewportColumn * minimapCharWidth + // contentWidth = remainingWidth - minimapWidth + // What are good values for contentWidth and minimapWidth ? + + // minimapWidth = ((contentWidth - verticalScrollbarWidth - 2) / typicalHalfwidthCharacterWidth) * minimapCharWidth + // typicalHalfwidthCharacterWidth * minimapWidth = (contentWidth - verticalScrollbarWidth - 2) * minimapCharWidth + // typicalHalfwidthCharacterWidth * minimapWidth = (remainingWidth - minimapWidth - verticalScrollbarWidth - 2) * minimapCharWidth + // (typicalHalfwidthCharacterWidth + minimapCharWidth) * minimapWidth = (remainingWidth - verticalScrollbarWidth - 2) * minimapCharWidth + // minimapWidth = ((remainingWidth - verticalScrollbarWidth - 2) * minimapCharWidth) / (typicalHalfwidthCharacterWidth + minimapCharWidth) + + const minimapMaxWidth = Math.floor(minimapMaxColumn * minimapCharWidth); + const minimapWidth = Math.min(minimapMaxWidth, Math.max(0, Math.floor(((remainingWidth - verticalScrollbarWidth - 2) * minimapCharWidth) / (typicalHalfwidthCharacterWidth + minimapCharWidth))) + MINIMAP_GUTTER_WIDTH); + + let minimapCanvasInnerWidth = Math.floor(pixelRatio * minimapWidth); + const minimapCanvasOuterWidth = minimapCanvasInnerWidth / pixelRatio; + minimapCanvasInnerWidth = Math.floor(minimapCanvasInnerWidth * minimapWidthMultiplier); + + const renderMinimap = (minimapRenderCharacters ? RenderMinimap.Text : RenderMinimap.Blocks); + const minimapLeft = (minimapSide === 'left' ? 0 : (outerWidth - minimapWidth - verticalScrollbarWidth)); + + return { + renderMinimap, + minimapLeft, + minimapWidth, + minimapHeightIsEditorHeight, + minimapIsSampling, + minimapScale, + minimapLineHeight, + minimapCanvasInnerWidth, + minimapCanvasInnerHeight, + minimapCanvasOuterWidth, + minimapCanvasOuterHeight, + }; + } + public static computeLayout(options: IComputedEditorOptions, env: EditorLayoutInfoComputerEnv): EditorLayoutInfo { const outerWidth = env.outerWidth | 0; const outerHeight = env.outerHeight | 0; @@ -1819,24 +2059,25 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption= 2 ? Math.round(minimap.scale * 2) : minimap.scale); - const minimapMaxColumn = minimap.maxColumn | 0; - const minimapSize = minimap.size; const scrollbar = options.get(EditorOption.scrollbar); - const verticalScrollbarWidth = scrollbar.verticalScrollbarSize | 0; + const verticalScrollbarWidth = scrollbar.verticalScrollbarSize; const verticalScrollbarHasArrows = scrollbar.verticalHasArrows; - const scrollbarArrowSize = scrollbar.arrowSize | 0; - const horizontalScrollbarHeight = scrollbar.horizontalScrollbarSize | 0; + const scrollbarArrowSize = scrollbar.arrowSize; + const horizontalScrollbarHeight = scrollbar.horizontalScrollbarSize; const rawLineDecorationsWidth = options.get(EditorOption.lineDecorationsWidth); const folding = options.get(EditorOption.folding); @@ -1870,107 +2111,62 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption 1) { - minimapHeightIsEditorHeight = true; - minimapIsSampling = true; - minimapScale = 1; - minimapLineHeight = 1; - minimapCharWidth = minimapScale / pixelRatio; - } else { - const effectiveMinimapHeight = Math.ceil((viewLineCount + extraLinesBeyondLastLine) * minimapLineHeight); - if (minimapSize === 'fill' || effectiveMinimapHeight > minimapCanvasInnerHeight) { - minimapHeightIsEditorHeight = true; - const configuredFontScale = minimapScale; - minimapLineHeight = Math.min(lineHeight * pixelRatio, Math.max(1, Math.floor(1 / desiredRatio))); - minimapScale = Math.min(configuredFontScale + 1, Math.max(1, Math.floor(minimapLineHeight / baseCharHeight))); - if (minimapScale > configuredFontScale) { - minimapWidthMultiplier = Math.min(2, minimapScale / configuredFontScale); - } - minimapCharWidth = minimapScale / pixelRatio / minimapWidthMultiplier; - minimapCanvasInnerHeight = Math.ceil((Math.max(typicalViewportLineCount, viewLineCount + extraLinesBeyondLastLine)) * minimapLineHeight); - } - } + if (accessibilitySupport !== AccessibilitySupport.Enabled) { + // See https://github.com/Microsoft/vscode/issues/27766 + // Never enable wrapping when a screen reader is attached + // because arrow down etc. will not move the cursor in the way + // a screen reader expects. + if (wordWrapMinified && isDominatedByLongLines) { + // Force viewport width wrapping if model is dominated by long lines + isWordWrapMinified = true; + isViewportWrapping = true; + } else if (wordWrap === 'on' || wordWrap === 'bounded') { + isViewportWrapping = true; + } else if (wordWrap === 'wordWrapColumn') { + wrappingColumn = wordWrapColumn; } - - renderMinimap = minimapRenderCharacters ? RenderMinimap.Text : RenderMinimap.Blocks; - - // Given: - // (leaving 2px for the cursor to have space after the last character) - // viewportColumn = (contentWidth - verticalScrollbarWidth - 2) / typicalHalfwidthCharacterWidth - // minimapWidth = viewportColumn * minimapCharWidth - // contentWidth = remainingWidth - minimapWidth - // What are good values for contentWidth and minimapWidth ? - - // minimapWidth = ((contentWidth - verticalScrollbarWidth - 2) / typicalHalfwidthCharacterWidth) * minimapCharWidth - // typicalHalfwidthCharacterWidth * minimapWidth = (contentWidth - verticalScrollbarWidth - 2) * minimapCharWidth - // typicalHalfwidthCharacterWidth * minimapWidth = (remainingWidth - minimapWidth - verticalScrollbarWidth - 2) * minimapCharWidth - // (typicalHalfwidthCharacterWidth + minimapCharWidth) * minimapWidth = (remainingWidth - verticalScrollbarWidth - 2) * minimapCharWidth - // minimapWidth = ((remainingWidth - verticalScrollbarWidth - 2) * minimapCharWidth) / (typicalHalfwidthCharacterWidth + minimapCharWidth) - - minimapWidth = Math.max(0, Math.floor(((remainingWidth - verticalScrollbarWidth - 2) * minimapCharWidth) / (typicalHalfwidthCharacterWidth + minimapCharWidth))) + MINIMAP_GUTTER_WIDTH; - let minimapColumns = minimapWidth / minimapCharWidth; - if (minimapColumns > minimapMaxColumn) { - minimapWidth = Math.floor(minimapMaxColumn * minimapCharWidth); - } - contentWidth = remainingWidth - minimapWidth; - - if (minimapSide === 'left') { - minimapLeft = 0; - glyphMarginLeft += minimapWidth; - lineNumbersLeft += minimapWidth; - decorationsLeft += minimapWidth; - contentLeft += minimapWidth; - } else { - minimapLeft = outerWidth - minimapWidth - verticalScrollbarWidth; - } - - minimapCanvasInnerWidth = Math.floor(pixelRatio * minimapWidth); - minimapCanvasOuterWidth = minimapCanvasInnerWidth / pixelRatio; - minimapCanvasInnerWidth = Math.floor(minimapCanvasInnerWidth * minimapWidthMultiplier); } + const minimapLayout = EditorLayoutInfoComputer._computeMinimapLayout({ + outerWidth: outerWidth, + outerHeight: outerHeight, + lineHeight: lineHeight, + typicalHalfwidthCharacterWidth: typicalHalfwidthCharacterWidth, + pixelRatio: pixelRatio, + scrollBeyondLastLine: scrollBeyondLastLine, + minimap: minimap, + verticalScrollbarWidth: verticalScrollbarWidth, + viewLineCount: viewLineCount, + remainingWidth: remainingWidth, + isViewportWrapping: isViewportWrapping, + }, env.memory || new ComputeOptionsMemory()); + + if (minimapLayout.renderMinimap !== RenderMinimap.None && minimapLayout.minimapLeft === 0) { + // the minimap is rendered to the left, so move everything to the right + glyphMarginLeft += minimapLayout.minimapWidth; + lineNumbersLeft += minimapLayout.minimapWidth; + decorationsLeft += minimapLayout.minimapWidth; + contentLeft += minimapLayout.minimapWidth; + } + const contentWidth = remainingWidth - minimapLayout.minimapWidth; + // (leaving 2px for the cursor to have space after the last character) const viewportColumn = Math.max(1, Math.floor((contentWidth - verticalScrollbarWidth - 2) / typicalHalfwidthCharacterWidth)); const verticalArrowSize = (verticalScrollbarHasArrows ? scrollbarArrowSize : 0); + if (isViewportWrapping) { + // compute the actual wrappingColumn + wrappingColumn = Math.max(1, viewportColumn); + if (wordWrap === 'bounded') { + wrappingColumn = Math.min(wrappingColumn, wordWrapColumn); + } + } + return { width: outerWidth, height: outerHeight, @@ -1987,20 +2183,14 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption { constructor() { - super(EditorOption.wrappingInfo, [EditorOption.wordWrap, EditorOption.wordWrapColumn, EditorOption.wordWrapMinified, EditorOption.layoutInfo, EditorOption.accessibilitySupport]); + super(EditorOption.wrappingInfo, [EditorOption.layoutInfo]); } public compute(env: IEnvironmentalOptions, options: IComputedEditorOptions, _: EditorWrappingInfo): EditorWrappingInfo { - const wordWrap = options.get(EditorOption.wordWrap); - const wordWrapColumn = options.get(EditorOption.wordWrapColumn); - const wordWrapMinified = options.get(EditorOption.wordWrapMinified); const layoutInfo = options.get(EditorOption.layoutInfo); - const accessibilitySupport = options.get(EditorOption.accessibilitySupport); - - let bareWrappingInfo: { isWordWrapMinified: boolean; isViewportWrapping: boolean; wrappingColumn: number; } | null = null; - { - if (accessibilitySupport === AccessibilitySupport.Enabled) { - // See https://github.com/Microsoft/vscode/issues/27766 - // Never enable wrapping when a screen reader is attached - // because arrow down etc. will not move the cursor in the way - // a screen reader expects. - bareWrappingInfo = { - isWordWrapMinified: false, - isViewportWrapping: false, - wrappingColumn: -1 - }; - } else if (wordWrapMinified && env.isDominatedByLongLines) { - // Force viewport width wrapping if model is dominated by long lines - bareWrappingInfo = { - isWordWrapMinified: true, - isViewportWrapping: true, - wrappingColumn: Math.max(1, layoutInfo.viewportColumn) - }; - } else if (wordWrap === 'on') { - bareWrappingInfo = { - isWordWrapMinified: false, - isViewportWrapping: true, - wrappingColumn: Math.max(1, layoutInfo.viewportColumn) - }; - } else if (wordWrap === 'bounded') { - bareWrappingInfo = { - isWordWrapMinified: false, - isViewportWrapping: true, - wrappingColumn: Math.min(Math.max(1, layoutInfo.viewportColumn), wordWrapColumn) - }; - } else if (wordWrap === 'wordWrapColumn') { - bareWrappingInfo = { - isWordWrapMinified: false, - isViewportWrapping: false, - wrappingColumn: wordWrapColumn - }; - } else { - bareWrappingInfo = { - isWordWrapMinified: false, - isViewportWrapping: false, - wrappingColumn: -1 - }; - } - } return { isDominatedByLongLines: env.isDominatedByLongLines, - isWordWrapMinified: bareWrappingInfo.isWordWrapMinified, - isViewportWrapping: bareWrappingInfo.isViewportWrapping, - wrappingColumn: bareWrappingInfo.wrappingColumn, + isWordWrapMinified: layoutInfo.isWordWrapMinified, + isViewportWrapping: layoutInfo.isViewportWrapping, + wrappingColumn: layoutInfo.wrappingColumn, }; } } diff --git a/src/vs/editor/contrib/find/findWidget.ts b/src/vs/editor/contrib/find/findWidget.ts index 923a489115..c2d31236b0 100644 --- a/src/vs/editor/contrib/find/findWidget.ts +++ b/src/vs/editor/contrib/find/findWidget.ts @@ -701,7 +701,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } const editorWidth = layoutInfo.width; - const minimapWidth = layoutInfo.minimapWidth; + const minimapWidth = layoutInfo.minimap.minimapWidth; let collapsedFindWidget = false; let reducedFindWidget = false; let narrowFindWidget = false; @@ -1218,7 +1218,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas // 1. never resized before, double click should maximizes it // 2. users resized it already but its width is the same as default const layoutInfo = this._codeEditor.getLayoutInfo(); - width = layoutInfo.width - 28 - layoutInfo.minimapWidth - 15; + width = layoutInfo.width - 28 - layoutInfo.minimap.minimapWidth - 15; this._resized = true; } else { /** diff --git a/src/vs/editor/contrib/zoneWidget/zoneWidget.ts b/src/vs/editor/contrib/zoneWidget/zoneWidget.ts index 6b163949e0..e008b9e311 100644 --- a/src/vs/editor/contrib/zoneWidget/zoneWidget.ts +++ b/src/vs/editor/contrib/zoneWidget/zoneWidget.ts @@ -254,13 +254,13 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider { } private _getWidth(info: EditorLayoutInfo): number { - return info.width - info.minimapWidth - info.verticalScrollbarWidth; + return info.width - info.minimap.minimapWidth - info.verticalScrollbarWidth; } private _getLeft(info: EditorLayoutInfo): number { // If minimap is to the left, we move beyond it - if (info.minimapWidth > 0 && info.minimapLeft === 0) { - return info.minimapWidth; + if (info.minimap.minimapWidth > 0 && info.minimap.minimapLeft === 0) { + return info.minimap.minimapWidth; } return 0; } @@ -526,6 +526,6 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider { getHorizontalSashWidth() { const layoutInfo = this.editor.getLayoutInfo(); - return layoutInfo.width - layoutInfo.minimapWidth; + return layoutInfo.width - layoutInfo.minimap.minimapWidth; } } diff --git a/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts b/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts index cd362822bf..0ced0a4798 100644 --- a/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts +++ b/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts @@ -76,9 +76,16 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { }; options._write(EditorOption.lineNumbers, lineNumbersOptions); + options._write(EditorOption.wordWrap, 'off'); + options._write(EditorOption.wordWrapColumn, 80); + options._write(EditorOption.wordWrapMinified, true); + options._write(EditorOption.accessibilitySupport, 'auto'); + const actual = EditorLayoutInfoComputer.computeLayout(options, { + memory: null, outerWidth: input.outerWidth, outerHeight: input.outerHeight, + isDominatedByLongLines: false, lineHeight: input.lineHeight, viewLineCount: input.maxLineNumber || Math.pow(10, input.lineNumbersDigitCount) - 1, lineNumbersDigitCount: input.lineNumbersDigitCount, @@ -126,18 +133,24 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { contentLeft: 10, contentWidth: 990, - renderMinimap: RenderMinimap.None, - minimapLeft: 0, - minimapWidth: 0, - minimapHeightIsEditorHeight: false, - minimapIsSampling: false, - minimapScale: 1, - minimapLineHeight: 1, - minimapCanvasInnerWidth: 0, - minimapCanvasInnerHeight: 800, - minimapCanvasOuterWidth: 0, - minimapCanvasOuterHeight: 800, + minimap: { + renderMinimap: RenderMinimap.None, + minimapLeft: 0, + minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 800, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 800, + }, + viewportColumn: 98, + isWordWrapMinified: false, + isViewportWrapping: false, + wrappingColumn: -1, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -188,18 +201,24 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { contentLeft: 10, contentWidth: 990, - renderMinimap: RenderMinimap.None, - minimapLeft: 0, - minimapWidth: 0, - minimapHeightIsEditorHeight: false, - minimapIsSampling: false, - minimapScale: 1, - minimapLineHeight: 1, - minimapCanvasInnerWidth: 0, - minimapCanvasInnerHeight: 800, - minimapCanvasOuterWidth: 0, - minimapCanvasOuterHeight: 800, + minimap: { + renderMinimap: RenderMinimap.None, + minimapLeft: 0, + minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 800, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 800, + }, + viewportColumn: 97, + isWordWrapMinified: false, + isViewportWrapping: false, + wrappingColumn: -1, verticalScrollbarWidth: 11, horizontalScrollbarHeight: 12, @@ -250,18 +269,24 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { contentLeft: 10, contentWidth: 890, - renderMinimap: RenderMinimap.None, - minimapLeft: 0, - minimapWidth: 0, - minimapHeightIsEditorHeight: false, - minimapIsSampling: false, - minimapScale: 1, - minimapLineHeight: 1, - minimapCanvasInnerWidth: 0, - minimapCanvasInnerHeight: 800, - minimapCanvasOuterWidth: 0, - minimapCanvasOuterHeight: 800, + minimap: { + renderMinimap: RenderMinimap.None, + minimapLeft: 0, + minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 800, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 800, + }, + viewportColumn: 88, + isWordWrapMinified: false, + isViewportWrapping: false, + wrappingColumn: -1, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -312,18 +337,24 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { contentLeft: 10, contentWidth: 890, - renderMinimap: RenderMinimap.None, - minimapLeft: 0, - minimapWidth: 0, - minimapHeightIsEditorHeight: false, - minimapIsSampling: false, - minimapScale: 1, - minimapLineHeight: 1, - minimapCanvasInnerWidth: 0, - minimapCanvasInnerHeight: 900, - minimapCanvasOuterWidth: 0, - minimapCanvasOuterHeight: 900, + minimap: { + renderMinimap: RenderMinimap.None, + minimapLeft: 0, + minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 900, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 900, + }, + viewportColumn: 88, + isWordWrapMinified: false, + isViewportWrapping: false, + wrappingColumn: -1, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -374,18 +405,24 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { contentLeft: 10, contentWidth: 890, - renderMinimap: RenderMinimap.None, - minimapLeft: 0, - minimapWidth: 0, - minimapHeightIsEditorHeight: false, - minimapIsSampling: false, - minimapScale: 1, - minimapLineHeight: 1, - minimapCanvasInnerWidth: 0, - minimapCanvasInnerHeight: 900, - minimapCanvasOuterWidth: 0, - minimapCanvasOuterHeight: 900, + minimap: { + renderMinimap: RenderMinimap.None, + minimapLeft: 0, + minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 900, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 900, + }, + viewportColumn: 88, + isWordWrapMinified: false, + isViewportWrapping: false, + wrappingColumn: -1, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -436,18 +473,24 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { contentLeft: 60, contentWidth: 840, - renderMinimap: RenderMinimap.None, - minimapLeft: 0, - minimapWidth: 0, - minimapHeightIsEditorHeight: false, - minimapIsSampling: false, - minimapScale: 1, - minimapLineHeight: 1, - minimapCanvasInnerWidth: 0, - minimapCanvasInnerHeight: 900, - minimapCanvasOuterWidth: 0, - minimapCanvasOuterHeight: 900, + minimap: { + renderMinimap: RenderMinimap.None, + minimapLeft: 0, + minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 900, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 900, + }, + viewportColumn: 83, + isWordWrapMinified: false, + isViewportWrapping: false, + wrappingColumn: -1, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -498,18 +541,24 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { contentLeft: 60, contentWidth: 840, - renderMinimap: RenderMinimap.None, - minimapLeft: 0, - minimapWidth: 0, - minimapHeightIsEditorHeight: false, - minimapIsSampling: false, - minimapScale: 1, - minimapLineHeight: 1, - minimapCanvasInnerWidth: 0, - minimapCanvasInnerHeight: 900, - minimapCanvasOuterWidth: 0, - minimapCanvasOuterHeight: 900, + minimap: { + renderMinimap: RenderMinimap.None, + minimapLeft: 0, + minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 900, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 900, + }, + viewportColumn: 83, + isWordWrapMinified: false, + isViewportWrapping: false, + wrappingColumn: -1, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -560,18 +609,24 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { contentLeft: 70, contentWidth: 830, - renderMinimap: RenderMinimap.None, - minimapLeft: 0, - minimapWidth: 0, - minimapHeightIsEditorHeight: false, - minimapIsSampling: false, - minimapScale: 1, - minimapLineHeight: 1, - minimapCanvasInnerWidth: 0, - minimapCanvasInnerHeight: 900, - minimapCanvasOuterWidth: 0, - minimapCanvasOuterHeight: 900, + minimap: { + renderMinimap: RenderMinimap.None, + minimapLeft: 0, + minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 900, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 900, + }, + viewportColumn: 82, + isWordWrapMinified: false, + isViewportWrapping: false, + wrappingColumn: -1, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -622,18 +677,24 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { contentLeft: 40, contentWidth: 860, - renderMinimap: RenderMinimap.None, - minimapLeft: 0, - minimapWidth: 0, - minimapHeightIsEditorHeight: false, - minimapIsSampling: false, - minimapScale: 1, - minimapLineHeight: 1, - minimapCanvasInnerWidth: 0, - minimapCanvasInnerHeight: 900, - minimapCanvasOuterWidth: 0, - minimapCanvasOuterHeight: 900, + minimap: { + renderMinimap: RenderMinimap.None, + minimapLeft: 0, + minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 900, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 900, + }, + viewportColumn: 171, + isWordWrapMinified: false, + isViewportWrapping: false, + wrappingColumn: -1, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -684,18 +745,24 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { contentLeft: 40, contentWidth: 860, - renderMinimap: RenderMinimap.None, - minimapLeft: 0, - minimapWidth: 0, - minimapHeightIsEditorHeight: false, - minimapIsSampling: false, - minimapScale: 1, - minimapLineHeight: 1, - minimapCanvasInnerWidth: 0, - minimapCanvasInnerHeight: 900, - minimapCanvasOuterWidth: 0, - minimapCanvasOuterHeight: 900, + minimap: { + renderMinimap: RenderMinimap.None, + minimapLeft: 0, + minimapWidth: 0, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 0, + minimapCanvasInnerHeight: 900, + minimapCanvasOuterWidth: 0, + minimapCanvasOuterHeight: 900, + }, + viewportColumn: 169, + isWordWrapMinified: false, + isViewportWrapping: false, + wrappingColumn: -1, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -746,18 +813,24 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { contentLeft: 10, contentWidth: 893, - renderMinimap: RenderMinimap.Text, - minimapLeft: 903, - minimapWidth: 97, - minimapHeightIsEditorHeight: false, - minimapIsSampling: false, - minimapScale: 1, - minimapLineHeight: 2, - minimapCanvasInnerWidth: 97, - minimapCanvasInnerHeight: 800, - minimapCanvasOuterWidth: 97, - minimapCanvasOuterHeight: 800, + minimap: { + renderMinimap: RenderMinimap.Text, + minimapLeft: 903, + minimapWidth: 97, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 1, + minimapLineHeight: 2, + minimapCanvasInnerWidth: 97, + minimapCanvasInnerHeight: 800, + minimapCanvasOuterWidth: 97, + minimapCanvasOuterHeight: 800, + }, + viewportColumn: 89, + isWordWrapMinified: false, + isViewportWrapping: false, + wrappingColumn: -1, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -808,18 +881,24 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { contentLeft: 10, contentWidth: 893, - renderMinimap: RenderMinimap.Text, - minimapLeft: 903, - minimapWidth: 97, - minimapHeightIsEditorHeight: false, - minimapIsSampling: false, - minimapScale: 2, - minimapLineHeight: 4, - minimapCanvasInnerWidth: 194, - minimapCanvasInnerHeight: 1600, - minimapCanvasOuterWidth: 97, - minimapCanvasOuterHeight: 800, + minimap: { + renderMinimap: RenderMinimap.Text, + minimapLeft: 903, + minimapWidth: 97, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 2, + minimapLineHeight: 4, + minimapCanvasInnerWidth: 194, + minimapCanvasInnerHeight: 1600, + minimapCanvasOuterWidth: 97, + minimapCanvasOuterHeight: 800, + }, + viewportColumn: 89, + isWordWrapMinified: false, + isViewportWrapping: false, + wrappingColumn: -1, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -870,18 +949,24 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { contentLeft: 10, contentWidth: 935, - renderMinimap: RenderMinimap.Text, - minimapLeft: 945, - minimapWidth: 55, - minimapHeightIsEditorHeight: false, - minimapIsSampling: false, - minimapScale: 2, - minimapLineHeight: 4, - minimapCanvasInnerWidth: 220, - minimapCanvasInnerHeight: 3200, - minimapCanvasOuterWidth: 55, - minimapCanvasOuterHeight: 800, + minimap: { + renderMinimap: RenderMinimap.Text, + minimapLeft: 945, + minimapWidth: 55, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 2, + minimapLineHeight: 4, + minimapCanvasInnerWidth: 220, + minimapCanvasInnerHeight: 3200, + minimapCanvasOuterWidth: 55, + minimapCanvasOuterHeight: 800, + }, + viewportColumn: 93, + isWordWrapMinified: false, + isViewportWrapping: false, + wrappingColumn: -1, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -932,18 +1017,24 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { contentLeft: 65, contentWidth: 935, - renderMinimap: RenderMinimap.Text, - minimapLeft: 0, - minimapWidth: 55, - minimapHeightIsEditorHeight: false, - minimapIsSampling: false, - minimapScale: 2, - minimapLineHeight: 4, - minimapCanvasInnerWidth: 220, - minimapCanvasInnerHeight: 3200, - minimapCanvasOuterWidth: 55, - minimapCanvasOuterHeight: 800, + minimap: { + renderMinimap: RenderMinimap.Text, + minimapLeft: 0, + minimapWidth: 55, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 2, + minimapLineHeight: 4, + minimapCanvasInnerWidth: 220, + minimapCanvasInnerHeight: 3200, + minimapCanvasOuterWidth: 55, + minimapCanvasOuterHeight: 800, + }, + viewportColumn: 93, + isWordWrapMinified: false, + isViewportWrapping: false, + wrappingColumn: -1, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -996,18 +1087,24 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { contentLeft: 10, contentWidth: 893, - renderMinimap: RenderMinimap.Text, - minimapLeft: 903, - minimapWidth: 97, - minimapHeightIsEditorHeight: true, - minimapIsSampling: false, - minimapScale: 3, - minimapLineHeight: 13, - minimapCanvasInnerWidth: 291, - minimapCanvasInnerHeight: 1560, - minimapCanvasOuterWidth: 97, - minimapCanvasOuterHeight: 800, + minimap: { + renderMinimap: RenderMinimap.Text, + minimapLeft: 903, + minimapWidth: 97, + minimapHeightIsEditorHeight: true, + minimapIsSampling: false, + minimapScale: 3, + minimapLineHeight: 13, + minimapCanvasInnerWidth: 291, + minimapCanvasInnerHeight: 1560, + minimapCanvasOuterWidth: 97, + minimapCanvasOuterHeight: 800, + }, + viewportColumn: 89, + isWordWrapMinified: false, + isViewportWrapping: false, + wrappingColumn: -1, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -1060,18 +1157,24 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { contentLeft: 10, contentWidth: 935, - renderMinimap: RenderMinimap.Text, - minimapLeft: 945, - minimapWidth: 55, - minimapHeightIsEditorHeight: true, - minimapIsSampling: true, - minimapScale: 1, - minimapLineHeight: 1, - minimapCanvasInnerWidth: 110, - minimapCanvasInnerHeight: 1600, - minimapCanvasOuterWidth: 55, - minimapCanvasOuterHeight: 800, + minimap: { + renderMinimap: RenderMinimap.Text, + minimapLeft: 945, + minimapWidth: 55, + minimapHeightIsEditorHeight: true, + minimapIsSampling: true, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 110, + minimapCanvasInnerHeight: 1600, + minimapCanvasOuterWidth: 55, + minimapCanvasOuterHeight: 800, + }, + viewportColumn: 93, + isWordWrapMinified: false, + isViewportWrapping: false, + wrappingColumn: -1, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -1124,18 +1227,24 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { contentLeft: 10, contentWidth: 893, - renderMinimap: RenderMinimap.Text, - minimapLeft: 903, - minimapWidth: 97, - minimapHeightIsEditorHeight: false, - minimapIsSampling: false, - minimapScale: 2, - minimapLineHeight: 4, - minimapCanvasInnerWidth: 194, - minimapCanvasInnerHeight: 1600, - minimapCanvasOuterWidth: 97, - minimapCanvasOuterHeight: 800, + minimap: { + renderMinimap: RenderMinimap.Text, + minimapLeft: 903, + minimapWidth: 97, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 2, + minimapLineHeight: 4, + minimapCanvasInnerWidth: 194, + minimapCanvasInnerHeight: 1600, + minimapCanvasOuterWidth: 97, + minimapCanvasOuterHeight: 800, + }, + viewportColumn: 89, + isWordWrapMinified: false, + isViewportWrapping: false, + wrappingColumn: -1, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -1188,18 +1297,24 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { contentLeft: 10, contentWidth: 935, - renderMinimap: RenderMinimap.Text, - minimapLeft: 945, - minimapWidth: 55, - minimapHeightIsEditorHeight: true, - minimapIsSampling: true, - minimapScale: 1, - minimapLineHeight: 1, - minimapCanvasInnerWidth: 110, - minimapCanvasInnerHeight: 1600, - minimapCanvasOuterWidth: 55, - minimapCanvasOuterHeight: 800, + minimap: { + renderMinimap: RenderMinimap.Text, + minimapLeft: 945, + minimapWidth: 55, + minimapHeightIsEditorHeight: true, + minimapIsSampling: true, + minimapScale: 1, + minimapLineHeight: 1, + minimapCanvasInnerWidth: 110, + minimapCanvasInnerHeight: 1600, + minimapCanvasOuterWidth: 55, + minimapCanvasOuterHeight: 800, + }, + viewportColumn: 93, + isWordWrapMinified: false, + isViewportWrapping: false, + wrappingColumn: -1, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, @@ -1250,18 +1365,24 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { contentLeft: 92, contentWidth: 1018, - renderMinimap: RenderMinimap.Text, - minimapLeft: 1096, - minimapWidth: 91, - minimapHeightIsEditorHeight: false, - minimapIsSampling: false, - minimapScale: 2, - minimapLineHeight: 4, - minimapCanvasInnerWidth: 182, - minimapCanvasInnerHeight: 844, - minimapCanvasOuterWidth: 91, - minimapCanvasOuterHeight: 422, + minimap: { + renderMinimap: RenderMinimap.Text, + minimapLeft: 1096, + minimapWidth: 91, + minimapHeightIsEditorHeight: false, + minimapIsSampling: false, + minimapScale: 2, + minimapLineHeight: 4, + minimapCanvasInnerWidth: 182, + minimapCanvasInnerHeight: 844, + minimapCanvasOuterWidth: 91, + minimapCanvasOuterHeight: 422, + }, + viewportColumn: 83, + isWordWrapMinified: false, + isViewportWrapping: false, + wrappingColumn: -1, verticalScrollbarWidth: 14, horizontalScrollbarHeight: 10, diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index a251a6b99b..f9258fce82 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3404,29 +3404,16 @@ declare namespace monaco.editor { */ readonly contentWidth: number; /** - * The position for the minimap + * Layout information for the minimap */ - readonly minimapLeft: number; - /** - * The width of the minimap - */ - readonly minimapWidth: number; - readonly minimapHeightIsEditorHeight: boolean; - readonly minimapIsSampling: boolean; - readonly minimapScale: number; - readonly minimapLineHeight: number; - readonly minimapCanvasInnerWidth: number; - readonly minimapCanvasInnerHeight: number; - readonly minimapCanvasOuterWidth: number; - readonly minimapCanvasOuterHeight: number; - /** - * Minimap render type - */ - readonly renderMinimap: RenderMinimap; + readonly minimap: EditorMinimapLayoutInfo; /** * The number of columns (of typical characters) fitting on a viewport line. */ readonly viewportColumn: number; + readonly isWordWrapMinified: boolean; + readonly isViewportWrapping: boolean; + readonly wrappingColumn: number; /** * The width of the vertical scrollbar. */ @@ -3441,6 +3428,23 @@ declare namespace monaco.editor { readonly overviewRuler: OverviewRulerPosition; } + /** + * The internal layout details of the editor. + */ + export interface EditorMinimapLayoutInfo { + readonly renderMinimap: RenderMinimap; + readonly minimapLeft: number; + readonly minimapWidth: number; + readonly minimapHeightIsEditorHeight: boolean; + readonly minimapIsSampling: boolean; + readonly minimapScale: number; + readonly minimapLineHeight: number; + readonly minimapCanvasInnerWidth: number; + readonly minimapCanvasInnerHeight: number; + readonly minimapCanvasOuterWidth: number; + readonly minimapCanvasOuterHeight: number; + } + /** * Configuration options for editor lightbulb */ diff --git a/src/vs/platform/configuration/common/configurationModels.ts b/src/vs/platform/configuration/common/configurationModels.ts index cbd78aeae0..f7fc33dc05 100644 --- a/src/vs/platform/configuration/common/configurationModels.ts +++ b/src/vs/platform/configuration/common/configurationModels.ts @@ -324,7 +324,8 @@ export class ConfigurationModelParser { result[key] = this.filterByScope(properties[key], configurationProperties, false, scopes); } else { const scope = this.getScope(key, configurationProperties); - if (scopes.indexOf(scope) !== -1) { + // Load unregistered configurations always. + if (scope === undefined || scopes.indexOf(scope) !== -1) { result[key] = properties[key]; } } @@ -332,9 +333,9 @@ export class ConfigurationModelParser { return result; } - private getScope(key: string, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema }): ConfigurationScope { + private getScope(key: string, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema }): ConfigurationScope | undefined { const propertySchema = configurationProperties[key]; - return propertySchema && typeof propertySchema.scope !== 'undefined' ? propertySchema.scope : ConfigurationScope.WINDOW; + return propertySchema ? typeof propertySchema.scope !== 'undefined' ? propertySchema.scope : ConfigurationScope.WINDOW : undefined; } } diff --git a/src/vs/platform/diagnostics/node/diagnosticsService.ts b/src/vs/platform/diagnostics/node/diagnosticsService.ts index 4e19409def..5e742ed518 100644 --- a/src/vs/platform/diagnostics/node/diagnosticsService.ts +++ b/src/vs/platform/diagnostics/node/diagnosticsService.ts @@ -5,7 +5,7 @@ import * as osLib from 'os'; import { virtualMachineHint } from 'vs/base/node/id'; import { IMachineInfo, WorkspaceStats, WorkspaceStatItem, PerformanceInfo, SystemInfo, IRemoteDiagnosticInfo, IRemoteDiagnosticError, isRemoteDiagnosticError, IWorkspaceInformation } from 'vs/platform/diagnostics/common/diagnostics'; -import { readdir, stat, exists, readFile } from 'fs'; +import { readdir, exists, readFile } from 'fs'; import { join, basename } from 'vs/base/common/path'; import { parse, ParseError, getNodeType } from 'vs/base/common/json'; import { listProcesses } from 'vs/base/node/ps'; @@ -17,6 +17,7 @@ import { ProcessItem } from 'vs/base/common/processes'; import { IMainProcessInfo } from 'vs/platform/launch/common/launch'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { Iterable } from 'vs/base/common/iterator'; export const ID = 'diagnosticsService'; export const IDiagnosticsService = createDecorator(ID); @@ -42,24 +43,31 @@ export interface ProcessInfo { name: string; } -export function collectWorkspaceStats(folder: string, filter: string[]): Promise { - const configFilePatterns = [ - { 'tag': 'grunt.js', 'pattern': /^gruntfile\.js$/i }, - { 'tag': 'gulp.js', 'pattern': /^gulpfile\.js$/i }, - { 'tag': 'tsconfig.json', 'pattern': /^tsconfig\.json$/i }, - { 'tag': 'package.json', 'pattern': /^package\.json$/i }, - { 'tag': 'jsconfig.json', 'pattern': /^jsconfig\.json$/i }, - { 'tag': 'tslint.json', 'pattern': /^tslint\.json$/i }, - { 'tag': 'eslint.json', 'pattern': /^eslint\.json$/i }, - { 'tag': 'tasks.json', 'pattern': /^tasks\.json$/i }, - { 'tag': 'launch.json', 'pattern': /^launch\.json$/i }, - { 'tag': 'settings.json', 'pattern': /^settings\.json$/i }, - { 'tag': 'webpack.config.js', 'pattern': /^webpack\.config\.js$/i }, - { 'tag': 'project.json', 'pattern': /^project\.json$/i }, - { 'tag': 'makefile', 'pattern': /^makefile$/i }, - { 'tag': 'sln', 'pattern': /^.+\.sln$/i }, - { 'tag': 'csproj', 'pattern': /^.+\.csproj$/i }, - { 'tag': 'cmake', 'pattern': /^.+\.cmake$/i } +interface ConfigFilePatterns { + tag: string; + filePattern: RegExp; + relativePathPattern?: RegExp; +} + +export async function collectWorkspaceStats(folder: string, filter: string[]): Promise { + const configFilePatterns: ConfigFilePatterns[] = [ + { tag: 'grunt.js', filePattern: /^gruntfile\.js$/i }, + { tag: 'gulp.js', filePattern: /^gulpfile\.js$/i }, + { tag: 'tsconfig.json', filePattern: /^tsconfig\.json$/i }, + { tag: 'package.json', filePattern: /^package\.json$/i }, + { tag: 'jsconfig.json', filePattern: /^jsconfig\.json$/i }, + { tag: 'tslint.json', filePattern: /^tslint\.json$/i }, + { tag: 'eslint.json', filePattern: /^eslint\.json$/i }, + { tag: 'tasks.json', filePattern: /^tasks\.json$/i }, + { tag: 'launch.json', filePattern: /^launch\.json$/i }, + { tag: 'settings.json', filePattern: /^settings\.json$/i }, + { tag: 'webpack.config.js', filePattern: /^webpack\.config\.js$/i }, + { tag: 'project.json', filePattern: /^project\.json$/i }, + { tag: 'makefile', filePattern: /^makefile$/i }, + { tag: 'sln', filePattern: /^.+\.sln$/i }, + { tag: 'csproj', filePattern: /^.+\.csproj$/i }, + { tag: 'cmake', filePattern: /^.+\.cmake$/i }, + { tag: 'github-actions', filePattern: /^.+\.yml$/i, relativePathPattern: /^\.github(?:\/|\\)workflows$/i } ]; const fileTypes = new Map(); @@ -67,123 +75,91 @@ export function collectWorkspaceStats(folder: string, filter: string[]): Promise const MAX_FILES = 20000; - function walk(dir: string, filter: string[], token: { count: number, maxReached: boolean }, done: (allFiles: string[]) => void): void { - let results: string[] = []; - readdir(dir, async (err, files) => { - // Ignore folders that can't be read - if (err) { - return done(results); - } + function collect(root: string, dir: string, filter: string[], token: { count: number, maxReached: boolean }): Promise { + const relativePath = dir.substring(root.length + 1); + + return new Promise(resolve => { + readdir(dir, { withFileTypes: true }, async (err, files) => { + // Ignore folders that can't be read + if (err) { + resolve(); + return; + } + + if (token.count > MAX_FILES) { + token.count += files.length; + token.maxReached = true; + resolve(); + return; + } + + let pending = files.length; + if (pending === 0) { + resolve(); + return; + } + + let filesToRead = files; + if (token.count + files.length > MAX_FILES) { + token.maxReached = true; + pending = MAX_FILES - token.count; + filesToRead = files.slice(0, pending); + } - if (token.count > MAX_FILES) { token.count += files.length; - token.maxReached = true; - return done(results); - } - let pending = files.length; - if (pending === 0) { - return done(results); - } + for (const file of filesToRead) { + if (file.isDirectory()) { + if (!filter.includes(file.name)) { + await collect(root, join(dir, file.name), filter, token); + } - let filesToRead = files; - if (token.count + files.length > MAX_FILES) { - token.maxReached = true; - pending = MAX_FILES - token.count; - filesToRead = files.slice(0, pending); - } - - token.count += files.length; - - for (const file of filesToRead) { - stat(join(dir, file), (err, stats) => { - // Ignore files that can't be read - if (err) { if (--pending === 0) { - return done(results); + resolve(); + return; } } else { - if (stats.isDirectory()) { - if (filter.indexOf(file) === -1) { - walk(join(dir, file), filter, token, (res: string[]) => { - results = results.concat(res); - - if (--pending === 0) { - return done(results); - } - }); - } else { - if (--pending === 0) { - done(results); - } - } - } else { - results.push(file); - - if (--pending === 0) { - done(results); + const index = file.name.lastIndexOf('.'); + if (index >= 0) { + const fileType = file.name.substring(index + 1); + if (fileType) { + fileTypes.set(fileType, (fileTypes.get(fileType) ?? 0) + 1); } } + + for (const configFile of configFilePatterns) { + if (configFile.relativePathPattern?.test(relativePath) !== false && configFile.filePattern.test(file.name)) { + configFiles.set(configFile.tag, (configFiles.get(configFile.tag) ?? 0) + 1); + } + } + + if (--pending === 0) { + resolve(); + return; + } } - }); - } + } + }); }); } - const addFileType = (fileType: string) => { - if (fileTypes.has(fileType)) { - fileTypes.set(fileType, fileTypes.get(fileType)! + 1); - } - else { - fileTypes.set(fileType, 1); - } - }; - - const addConfigFiles = (fileName: string) => { - for (const each of configFilePatterns) { - if (each.pattern.test(fileName)) { - if (configFiles.has(each.tag)) { - configFiles.set(each.tag, configFiles.get(each.tag)! + 1); - } else { - configFiles.set(each.tag, 1); - } - } - } - }; - - const acceptFile = (name: string) => { - if (name.lastIndexOf('.') >= 0) { - const suffix: string | undefined = name.split('.').pop(); - if (suffix) { - addFileType(suffix); - } - } - addConfigFiles(name); - }; - const token: { count: number, maxReached: boolean } = { count: 0, maxReached: false }; - return new Promise((resolve, reject) => { - walk(folder, filter, token, async (files) => { - files.forEach(acceptFile); - - const launchConfigs = await collectLaunchConfigs(folder); - - resolve({ - configFiles: asSortedItems(configFiles), - fileTypes: asSortedItems(fileTypes), - fileCount: token.count, - maxFilesReached: token.maxReached, - launchConfigFiles: launchConfigs - }); - }); - }); + await collect(folder, folder, filter, token); + const launchConfigs = await collectLaunchConfigs(folder); + return { + configFiles: asSortedItems(configFiles), + fileTypes: asSortedItems(fileTypes), + fileCount: token.count, + maxFilesReached: token.maxReached, + launchConfigFiles: launchConfigs + }; } -function asSortedItems(map: Map): WorkspaceStatItem[] { - const a: WorkspaceStatItem[] = []; - map.forEach((value, index) => a.push({ name: index, count: value })); - return a.sort((a, b) => b.count - a.count); +function asSortedItems(items: Map): WorkspaceStatItem[] { + return [ + ...Iterable.map(items.entries(), ([name, count]) => ({ name: name, count: count })) + ].sort((a, b) => b.count - a.count); } export function getMachineInfo(): IMachineInfo { @@ -540,58 +516,61 @@ export class DiagnosticsService implements IDiagnosticsService { } public async reportWorkspaceStats(workspace: IWorkspaceInformation): Promise { - workspace.folders.forEach(folder => { - const folderUri = URI.revive(folder.uri); - if (folderUri.scheme === 'file') { - const folder = folderUri.fsPath; - collectWorkspaceStats(folder, ['node_modules', '.git']).then(stats => { - type WorkspaceStatsClassification = { - 'workspace.id': { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - rendererSessionId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - }; - type WorkspaceStatsEvent = { - 'workspace.id': string | undefined; - rendererSessionId: string; - }; - this.telemetryService.publicLog2('workspace.stats', { - 'workspace.id': workspace.telemetryId, - rendererSessionId: workspace.rendererSessionId - }); - type WorkspaceStatsFileClassification = { - rendererSessionId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - type: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; - count: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; - }; - type WorkspaceStatsFileEvent = { - rendererSessionId: string; - type: string; - count: number; - }; - stats.fileTypes.forEach(e => { - this.telemetryService.publicLog2('workspace.stats.file', { - rendererSessionId: workspace.rendererSessionId, - type: e.name, - count: e.count - }); - }); - stats.launchConfigFiles.forEach(e => { - this.telemetryService.publicLog2('workspace.stats.launchConfigFile', { - rendererSessionId: workspace.rendererSessionId, - type: e.name, - count: e.count - }); - }); - stats.configFiles.forEach(e => { - this.telemetryService.publicLog2('workspace.stats.configFiles', { - rendererSessionId: workspace.rendererSessionId, - type: e.name, - count: e.count - }); - }); - }).catch(_ => { - // Report nothing if collecting metadata fails. - }); + for (const { uri } of workspace.folders) { + const folderUri = URI.revive(uri); + if (folderUri.scheme !== 'file') { + continue; } - }); + + const folder = folderUri.fsPath; + try { + const stats = await collectWorkspaceStats(folder, ['node_modules', '.git']); + type WorkspaceStatsClassification = { + 'workspace.id': { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + rendererSessionId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + }; + type WorkspaceStatsEvent = { + 'workspace.id': string | undefined; + rendererSessionId: string; + }; + this.telemetryService.publicLog2('workspace.stats', { + 'workspace.id': workspace.telemetryId, + rendererSessionId: workspace.rendererSessionId + }); + type WorkspaceStatsFileClassification = { + rendererSessionId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + type: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; + count: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; + }; + type WorkspaceStatsFileEvent = { + rendererSessionId: string; + type: string; + count: number; + }; + stats.fileTypes.forEach(e => { + this.telemetryService.publicLog2('workspace.stats.file', { + rendererSessionId: workspace.rendererSessionId, + type: e.name, + count: e.count + }); + }); + stats.launchConfigFiles.forEach(e => { + this.telemetryService.publicLog2('workspace.stats.launchConfigFile', { + rendererSessionId: workspace.rendererSessionId, + type: e.name, + count: e.count + }); + }); + stats.configFiles.forEach(e => { + this.telemetryService.publicLog2('workspace.stats.configFiles', { + rendererSessionId: workspace.rendererSessionId, + type: e.name, + count: e.count + }); + }); + } catch { + // Report nothing if collecting metadata fails. + } + } } } diff --git a/src/vs/platform/editor/common/editor.ts b/src/vs/platform/editor/common/editor.ts index 389aad6482..5fc94a7d6c 100644 --- a/src/vs/platform/editor/common/editor.ts +++ b/src/vs/platform/editor/common/editor.ts @@ -170,6 +170,12 @@ export interface IEditorOptions { */ readonly pinned?: boolean; + /** + * An editor that is sticky moves to the beginning of the editors list within the group and will remain + * there unless explicitly closed. Operations such as "Close All" will not close sticky editors. + */ + readonly sticky?: boolean; + /** * The index in the document stack where to insert the editor into when opening. */ diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 4f9f165b9a..56e6d3306e 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -62,6 +62,7 @@ export interface ParsedArgs { 'install-source'?: string; 'disable-updates'?: boolean; 'disable-crash-reporter'?: boolean; + 'crash-reporter-directory'?: string; 'skip-add-to-recently-opened'?: boolean; 'max-memory'?: string; 'file-write'?: boolean; @@ -186,6 +187,7 @@ export const OPTIONS: OptionDescriptions> = { 'disable-telemetry': { type: 'boolean' }, 'disable-updates': { type: 'boolean' }, 'disable-crash-reporter': { type: 'boolean' }, + 'crash-reporter-directory': { type: 'string' }, 'disable-user-env-probe': { type: 'boolean' }, 'skip-add-to-recently-opened': { type: 'boolean' }, 'unity-launch': { type: 'boolean' }, diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index 0a28995113..f3bd1b75b9 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -253,6 +253,7 @@ export class EnvironmentService implements INativeEnvironmentService { get disableUpdates(): boolean { return !!this._args['disable-updates']; } get disableCrashReporter(): boolean { return !!this._args['disable-crash-reporter']; } + get crashReporterDirectory(): string | undefined { return this._args['crash-reporter-directory']; } get driverHandle(): string | undefined { return this._args['driver']; } get driverVerbose(): boolean { return !!this._args['driver-verbose']; } diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 34e5f00b0a..df39033372 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -5452,6 +5452,12 @@ declare module 'vscode' { */ readonly extensionPath: string; + /** + * Gets the extension's environment variable collection for this workspace, enabling changes + * to be applied to terminal environment variables. + */ + readonly environmentVariableCollection: EnvironmentVariableCollection; + /** * Get the absolute path of a resource contained in the extension. * @@ -8257,6 +8263,113 @@ declare module 'vscode' { readonly code: number | undefined; } + /** + * A type of mutation that can be applied to an environment variable. + */ + export enum EnvironmentVariableMutatorType { + /** + * Replace the variable's existing value. + */ + Replace = 1, + /** + * Append to the end of the variable's existing value. + */ + Append = 2, + /** + * Prepend to the start of the variable's existing value. + */ + Prepend = 3 + } + + /** + * A type of mutation and its value to be applied to an environment variable. + */ + export interface EnvironmentVariableMutator { + /** + * The type of mutation that will occur to the variable. + */ + readonly type: EnvironmentVariableMutatorType; + + /** + * The value to use for the variable. + */ + readonly value: string; + } + + /** + * A collection of mutations that an extension can apply to a process environment. + */ + export interface EnvironmentVariableCollection { + /** + * Whether the collection should be cached for the workspace and applied to the terminal + * across window reloads. When true the collection will be active immediately such when the + * window reloads. Additionally, this API will return the cached version if it exists. The + * collection will be invalidated when the extension is uninstalled or when the collection + * is cleared. Defaults to true. + */ + persistent: boolean; + + /** + * Replace an environment variable with a value. + * + * Note that an extension can only make a single change to any one variable, so this will + * overwrite any previous calls to replace, append or prepend. + * + * @param variable The variable to replace. + * @param value The value to replace the variable with. + */ + replace(variable: string, value: string): void; + + /** + * Append a value to an environment variable. + * + * Note that an extension can only make a single change to any one variable, so this will + * overwrite any previous calls to replace, append or prepend. + * + * @param variable The variable to append to. + * @param value The value to append to the variable. + */ + append(variable: string, value: string): void; + + /** + * Prepend a value to an environment variable. + * + * Note that an extension can only make a single change to any one variable, so this will + * overwrite any previous calls to replace, append or prepend. + * + * @param variable The variable to prepend. + * @param value The value to prepend to the variable. + */ + prepend(variable: string, value: string): void; + + /** + * Gets the mutator that this collection applies to a variable, if any. + * + * @param variable The variable to get the mutator for. + */ + get(variable: string): EnvironmentVariableMutator | undefined; + + /** + * Iterate over each mutator in this collection. + * + * @param callback Function to execute for each entry. + * @param thisArg The `this` context used when invoking the handler function. + */ + forEach(callback: (variable: string, mutator: EnvironmentVariableMutator, collection: EnvironmentVariableCollection) => any, thisArg?: any): void; + + /** + * Deletes this collection's mutator for a variable. + * + * @param variable The variable to delete the mutator for. + */ + delete(variable: string): void; + + /** + * Clears all mutators from this collection. + */ + clear(): void; + } + /** * A location in the editor at which progress information can be shown. It depends on the * location how progress is visually represented. diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index df34dc8da7..ff2e947766 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -930,103 +930,6 @@ declare module 'vscode' { //#endregion - //#region Contribute to terminal environment https://github.com/microsoft/vscode/issues/46696 - - export enum EnvironmentVariableMutatorType { - /** - * Replace the variable's existing value. - */ - Replace = 1, - /** - * Append to the end of the variable's existing value. - */ - Append = 2, - /** - * Prepend to the start of the variable's existing value. - */ - Prepend = 3 - } - - export interface EnvironmentVariableMutator { - /** - * The type of mutation that will occur to the variable. - */ - readonly type: EnvironmentVariableMutatorType; - - /** - * The value to use for the variable. - */ - readonly value: string; - } - - /** - * A collection of mutations that an extension can apply to a process environment. - */ - export interface EnvironmentVariableCollection { - /** - * Whether the collection should be cached for the workspace and applied to the terminal - * across window reloads. When true the collection will be active immediately such when the - * window reloads. Additionally, this API will return the cached version if it exists. The - * collection will be invalidated when the extension is uninstalled or when the collection - * is cleared. Defaults to true. - */ - persistent: boolean; - - /** - * Replace an environment variable with a value. - * - * Note that an extension can only make a single change to any one variable, so this will - * overwrite any previous calls to replace, append or prepend. - */ - replace(variable: string, value: string): void; - - /** - * Append a value to an environment variable. - * - * Note that an extension can only make a single change to any one variable, so this will - * overwrite any previous calls to replace, append or prepend. - */ - append(variable: string, value: string): void; - - /** - * Prepend a value to an environment variable. - * - * Note that an extension can only make a single change to any one variable, so this will - * overwrite any previous calls to replace, append or prepend. - */ - prepend(variable: string, value: string): void; - - /** - * Gets the mutator that this collection applies to a variable, if any. - */ - get(variable: string): EnvironmentVariableMutator | undefined; - - /** - * Iterate over each mutator in this collection. - */ - forEach(callback: (variable: string, mutator: EnvironmentVariableMutator, collection: EnvironmentVariableCollection) => any, thisArg?: any): void; - - /** - * Deletes this collection's mutator for a variable. - */ - delete(variable: string): void; - - /** - * Clears all mutators from this collection. - */ - clear(): void; - } - - export interface ExtensionContext { - /** - * Gets the extension's environment variable collection for this workspace, enabling changes - * to be applied to terminal environment variables. - */ - readonly environmentVariableCollection: EnvironmentVariableCollection; - } - - //#endregion - //#region @jrieken -> exclusive document filters export interface DocumentFilter { @@ -1737,12 +1640,6 @@ declare module 'vscode' { edit(callback: (editBuilder: NotebookEditorCellEdit) => void): Thenable; } - export interface NotebookProvider { - resolveNotebook(editor: NotebookEditor): Promise; - executeCell(document: NotebookDocument, cell: NotebookCell | undefined, token: CancellationToken): Promise; - save(document: NotebookDocument): Promise; - } - export interface NotebookOutputSelector { type: string; subTypes?: string[]; @@ -1805,11 +1702,6 @@ declare module 'vscode' { provider: NotebookContentProvider ): Disposable; - export function registerNotebookProvider( - notebookType: string, - provider: NotebookProvider - ): Disposable; - export function registerNotebookOutputRenderer( type: string, outputSelector: NotebookOutputSelector, @@ -1839,6 +1731,43 @@ declare module 'vscode' { //#endregion + //#region @connor4312 extension mode: https://github.com/microsoft/vscode/issues/95926 + + /** + * The ExtensionMode is provided on the `ExtensionContext` and indicates the + * mode the specific extension is running in. + */ + export enum ExtensionMode { + /** + * The extension is installed normally (for example, from the marketplace + * or VSIX) in VS Code. + */ + Release = 1, + + /** + * The extension is running from an `--extensionDevelopmentPath` provided + * when launching VS Code. + */ + Development = 2, + + /** + * The extension is running from an `--extensionDevelopmentPath` and + * the extension host is running unit tests. + */ + Test = 3, + } + + export interface ExtensionContext { + /** + * The mode the extension is running in. This is specific to the current + * extension. One extension may be in `ExtensionMode.Development` while + * other extensions in the host run in `ExtensionMode.Release`. + */ + readonly extensionMode: ExtensionMode; + } + + //#endregion + //#region https://github.com/microsoft/vscode/issues/39441 export interface CompletionItem { diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 7f658546f0..fc4fbf4fcd 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -8,7 +8,7 @@ import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IEx import { Disposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellOutputsSplice, CellKind, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellOutputsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -84,7 +84,9 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo registerListeners() { this._register(this._notebookService.onDidChangeActiveEditor(e => { - this._proxy.$updateActiveEditor(e.viewType, e.uri); + this._proxy.$acceptDocumentAndEditorsDelta({ + newActiveEditor: e.uri + }); })); const updateOrder = () => { @@ -129,16 +131,6 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo return; } - async $createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise { - let controller = this._notebookProviders.get(viewType); - - if (controller) { - controller.createNotebookDocument(handle, viewType, resource); - } - - return; - } - async $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise { let controller = this._notebookProviders.get(viewType); @@ -163,11 +155,6 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo } } - async resolveNotebook(viewType: string, uri: URI): Promise { - let handle = await this._proxy.$resolveNotebook(viewType, uri); - return handle; - } - async $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise { let controller = this._notebookProviders.get(viewType); controller?.spliceNotebookCellOutputs(resource, cellHandle, splices, renderers); @@ -195,6 +182,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo export class MainThreadNotebookController implements IMainNotebookController { private _mapping: Map = new Map(); + static documentHandle: number = 0; constructor( private readonly _proxy: ExtHostNotebookShape, @@ -203,26 +191,44 @@ export class MainThreadNotebookController implements IMainNotebookController { ) { } - async resolveNotebook(viewType: string, uri: URI): Promise { - // TODO: resolve notebook should wait for all notebook document destory operations to finish. + async createNotebook(viewType: string, uri: URI, forBackup: boolean, forceReload: boolean): Promise { let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); if (mainthreadNotebook) { + if (forceReload) { + const data = await this._proxy.$resolveNotebookData(viewType, uri); + if (!data) { + return undefined; // {{SQL CARBON EDIT}} + } + + mainthreadNotebook.textModel.languages = data.languages; + mainthreadNotebook.textModel.metadata = data.metadata; + mainthreadNotebook.textModel.applyEdit(mainthreadNotebook.textModel.versionId, [ + { editType: CellEditType.Delete, count: mainthreadNotebook.textModel.cells.length, index: 0 }, + { editType: CellEditType.Insert, index: 0, cells: data.cells } + ]); + } return mainthreadNotebook.textModel; } - let notebookHandle = await this._mainThreadNotebook.resolveNotebook(viewType, uri); - if (notebookHandle !== undefined) { - mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); - if (mainthreadNotebook && mainthreadNotebook.textModel.cells.length === 0) { - // it's empty, we should create an empty template one - const mainCell = mainthreadNotebook.textModel.createCellTextModel([''], mainthreadNotebook.textModel.languages.length ? mainthreadNotebook.textModel.languages[0] : '', CellKind.Code, [], undefined); - mainthreadNotebook.textModel.insertTemplateCell(mainCell); - } - return mainthreadNotebook?.textModel; + let document = new MainThreadNotebookDocument(this._proxy, MainThreadNotebookController.documentHandle++, viewType, uri); + await this.createNotebookDocument(document); + + if (forBackup) { + return document.textModel; } - return undefined; + // open notebook document + const data = await this._proxy.$resolveNotebookData(viewType, uri); + if (!data) { + return undefined; // {{SQL CARBON EDIT}} + } + + document.textModel.languages = data.languages; + document.textModel.metadata = data.metadata; + document.textModel.initialize(data!.cells); + + return document.textModel; } async tryApplyEdits(resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[], renderers: number[]): Promise { @@ -250,12 +256,32 @@ export class MainThreadNotebookController implements IMainNotebookController { this._proxy.$onDidReceiveMessage(uri, message); } - // Methods for ExtHost - async createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise { - let document = new MainThreadNotebookDocument(this._proxy, handle, viewType, URI.revive(resource)); - this._mapping.set(URI.revive(resource).toString(), document); + async createNotebookDocument(document: MainThreadNotebookDocument): Promise { + this._mapping.set(document.uri.toString(), document); + + await this._proxy.$acceptDocumentAndEditorsDelta({ + addedDocuments: [{ + viewType: document.viewType, + handle: document.handle, + uri: document.uri + }] + }); } + async removeNotebookDocument(notebook: INotebookTextModel): Promise { + let document = this._mapping.get(URI.from(notebook.uri).toString()); + + if (!document) { + return; + } + + await this._proxy.$acceptDocumentAndEditorsDelta({ removedDocuments: [notebook.uri] }); + document.dispose(); + this._mapping.delete(URI.from(notebook.uri).toString()); + } + + // Methods for ExtHost + updateLanguages(resource: UriComponents, languages: string[]) { let document = this._mapping.get(URI.from(resource).toString()); document?.textModel.updateLanguages(languages); @@ -280,21 +306,12 @@ export class MainThreadNotebookController implements IMainNotebookController { return this._proxy.$executeNotebook(this._viewType, uri, handle, token); } - async destoryNotebookDocument(notebook: INotebookTextModel): Promise { - let document = this._mapping.get(URI.from(notebook.uri).toString()); - - if (!document) { - return; - } - - let removeFromExtHost = await this._proxy.$destoryNotebookDocument(this._viewType, notebook.uri); - if (removeFromExtHost) { - document.dispose(); - this._mapping.delete(URI.from(notebook.uri).toString()); - } - } - async save(uri: URI, token: CancellationToken): Promise { return this._proxy.$saveNotebook(this._viewType, uri, token); } + + async saveAs(uri: URI, target: URI, token: CancellationToken): Promise { + return this._proxy.$saveNotebookAs(this._viewType, uri, target, token); + + } } diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index a4385b7929..f5cf39b33c 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -368,6 +368,9 @@ class ViewsExtensionHandler implements IWorkbenchContribution { } private addViews(extensions: readonly IExtensionPointUser[]): void { + const viewIds: Set = new Set(); + const allViewDescriptors: { views: IViewDescriptor[], viewContainer: ViewContainer }[] = []; + for (const extension of extensions) { const { value, collector } = extension; @@ -386,10 +389,9 @@ class ViewsExtensionHandler implements IWorkbenchContribution { collector.warn(localize('ViewContainerDoesnotExist', "View container '{0}' does not exist and all views registered to it will be added to 'Explorer'.", entry.key)); } const container = viewContainer || this.getDefaultViewContainer(); - const viewIds: string[] = []; const viewDescriptors = coalesce(entry.value.map((item, index) => { // validate - if (viewIds.indexOf(item.id) !== -1) { + if (viewIds.has(item.id)) { collector.error(localize('duplicateView1', "Cannot register multiple views with same id `{0}`", item.id)); return null; } @@ -421,12 +423,16 @@ class ViewsExtensionHandler implements IWorkbenchContribution { remoteAuthority: item.remoteName || (item).remoteAuthority // TODO@roblou - delete after remote extensions are updated }; - viewIds.push(viewDescriptor.id); + viewIds.add(viewDescriptor.id); return viewDescriptor; })); - this.viewsRegistry.registerViews(viewDescriptors, container); + + allViewDescriptors.push({ viewContainer: container, views: viewDescriptors }); + }); } + + this.viewsRegistry.registerViews2(allViewDescriptors); } private getDefaultViewContainer(): ViewContainer { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 89af44294d..9fdfd2fc92 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -931,10 +931,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostNotebook.onDidCloseNotebookDocument; }, - registerNotebookProvider: (viewType: string, provider: vscode.NotebookProvider) => { - checkProposedApiEnabled(extension); - return extHostNotebook.registerNotebookProvider(extension, viewType, provider); - }, registerNotebookContentProvider: (viewType: string, provider: vscode.NotebookContentProvider) => { checkProposedApiEnabled(extension); return extHostNotebook.registerNotebookContentProvider(extension, viewType, provider); @@ -1015,6 +1011,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I EvaluatableExpression: extHostTypes.EvaluatableExpression, EventEmitter: Emitter, ExtensionKind: extHostTypes.ExtensionKind, + ExtensionMode: extHostTypes.ExtensionMode, CustomExecution: extHostTypes.CustomExecution, CustomExecution2: extHostTypes.CustomExecution, FileChangeType: extHostTypes.FileChangeType, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index b8487f6d5d..8d8db3c9f0 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -51,7 +51,7 @@ import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { TunnelOptions } from 'vs/platform/remote/common/tunnel'; import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; import { revive } from 'vs/base/common/marshalling'; -import { INotebookMimeTypeSelector, IOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEvent } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookMimeTypeSelector, IOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEvent, NotebookDataDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { Dto } from 'vs/base/common/types'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; @@ -695,7 +695,6 @@ export interface MainThreadNotebookShape extends IDisposable { $unregisterNotebookProvider(viewType: string): Promise; $registerNotebookRenderer(extension: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, handle: number, preloads: UriComponents[]): Promise; $unregisterNotebookRenderer(handle: number): Promise; - $createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise; $tryApplyEdits(viewType: string, resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[], renderers: number[]): Promise; $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise; $updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata): Promise; @@ -1542,16 +1541,31 @@ export interface INotebookEditorPropertiesChangeData { selections: INotebookSelectionChangeEvent | null; } +export interface INotebookModelAddedData { + uri: UriComponents; + handle: number; + // versionId: number; + viewType: string; +} + +export interface INotebookDocumentsAndEditorsDelta { + removedDocuments?: UriComponents[]; + addedDocuments?: INotebookModelAddedData[]; + // removedEditors?: string[]; + // addedEditors?: ITextEditorAddData[]; + newActiveEditor?: UriComponents | null; +} + export interface ExtHostNotebookShape { - $resolveNotebook(viewType: string, uri: UriComponents): Promise; + $resolveNotebookData(viewType: string, uri: UriComponents): Promise; $executeNotebook(viewType: string, uri: UriComponents, cellHandle: number | undefined, token: CancellationToken): Promise; $saveNotebook(viewType: string, uri: UriComponents, token: CancellationToken): Promise; - $updateActiveEditor(viewType: string, uri: UriComponents): Promise; - $destoryNotebookDocument(viewType: string, uri: UriComponents): Promise; + $saveNotebookAs(viewType: string, uri: UriComponents, target: UriComponents, token: CancellationToken): Promise; $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void; $onDidReceiveMessage(uri: UriComponents, message: any): void; $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent): void; $acceptEditorPropertiesChanged(uriComponents: UriComponents, data: INotebookEditorPropertiesChangeData): void; + $acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): Promise; } export interface ExtHostStorageShape { diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 055fefef06..dc71c421a7 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -25,7 +25,7 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio import { Schemas } from 'vs/base/common/network'; import { VSBuffer } from 'vs/base/common/buffer'; import { ExtensionMemento } from 'vs/workbench/api/common/extHostMemento'; -import { RemoteAuthorityResolverError } from 'vs/workbench/api/common/extHostTypes'; +import { RemoteAuthorityResolverError, ExtensionMode } from 'vs/workbench/api/common/extHostTypes'; import { ResolvedAuthority, ResolvedOptions, RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; @@ -358,8 +358,12 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio const globalState = new ExtensionMemento(extensionDescription.identifier.value, true, this._storage); const workspaceState = new ExtensionMemento(extensionDescription.identifier.value, false, this._storage); + const extensionMode = extensionDescription.isUnderDevelopment + ? (this._initData.environment.extensionTestsLocationURI ? ExtensionMode.Test : ExtensionMode.Development) + : ExtensionMode.Release; this._logService.trace(`ExtensionService#loadExtensionContext ${extensionDescription.identifier.value}`); + return Promise.all([ globalState.whenReady, workspaceState.whenReady, @@ -376,10 +380,11 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio get globalStoragePath() { return that._storagePath.globalValue(extensionDescription); }, asAbsolutePath(relativePath: string) { return path.join(extensionDescription.extensionLocation.fsPath, relativePath); }, get logPath() { return path.join(that._initData.logsLocation.fsPath, extensionDescription.identifier.value); }, - get environmentVariableCollection() { + get extensionMode() { checkProposedApiEnabled(extensionDescription); - return that._extHostTerminalService.getEnvironmentVariableCollection(extensionDescription); - } + return extensionMode; + }, + get environmentVariableCollection() { return that._extHostTerminalService.getEnvironmentVariableCollection(extensionDescription); } }); }); } diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 7d6945ad8a..1f01961b11 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -10,10 +10,10 @@ import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecyc import { ISplice } from 'vs/base/common/sequence'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { CellKind, CellOutputKind, ExtHostNotebookShape, IMainContext, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice, MainThreadDocumentsShape, INotebookEditorPropertiesChangeData } from 'vs/workbench/api/common/extHost.protocol'; +import { CellKind, CellOutputKind, ExtHostNotebookShape, IMainContext, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice, MainThreadDocumentsShape, INotebookEditorPropertiesChangeData, INotebookDocumentsAndEditorsDelta } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { CellEditType, CellUri, diff, ICellEditOperation, ICellInsertEdit, IErrorOutput, INotebookDisplayOrder, INotebookEditData, IOrderedMimeType, IStreamOutput, ITransformedDisplayOutputDto, mimeTypeSupportedByCore, NotebookCellsChangedEvent, NotebookCellsSplice2, sortMimeTypes, ICellDeleteEdit, notebookDocumentMetadataDefaults, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, CellUri, diff, ICellEditOperation, ICellInsertEdit, IErrorOutput, INotebookDisplayOrder, INotebookEditData, IOrderedMimeType, IStreamOutput, ITransformedDisplayOutputDto, mimeTypeSupportedByCore, NotebookCellsChangedEvent, NotebookCellsSplice2, sortMimeTypes, ICellDeleteEdit, notebookDocumentMetadataDefaults, NotebookCellsChangeType, NotebookDataDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Disposable as VSCodeDisposable } from './extHostTypes'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; @@ -345,8 +345,6 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo transformMimeTypes(output: vscode.CellDisplayOutput): ITransformedDisplayOutputDto { let mimeTypes = Object.keys(output.data); - - // TODO@rebornix, the document display order might be assigned a bit later. We need to postpone sending the outputs to the core side. let coreDisplayOrder = this.renderingHandler.outputDisplayOrder; const sorted = sortMimeTypes(mimeTypes, coreDisplayOrder?.userOrder || [], this._displayOrder, coreDisplayOrder?.defaultOrder || []); @@ -415,7 +413,7 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo } } -export class NotebookEditorCellEdit { +export class NotebookEditorCellEditBuilder implements vscode.NotebookEditorCellEdit { private _finalized: boolean = false; private readonly _documentVersionId: number; private _collectedEdits: ICellEditOperation[] = []; @@ -526,13 +524,13 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook })); } - edit(callback: (editBuilder: NotebookEditorCellEdit) => void): Thenable { - const edit = new NotebookEditorCellEdit(this); + edit(callback: (editBuilder: NotebookEditorCellEditBuilder) => void): Thenable { + const edit = new NotebookEditorCellEditBuilder(this); callback(edit); return this._applyEdit(edit); } - private _applyEdit(editBuilder: NotebookEditorCellEdit): Promise { + private _applyEdit(editBuilder: NotebookEditorCellEditBuilder): Promise { const editData = editBuilder.finalize(); // return when there is nothing to do @@ -625,7 +623,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN private static _handlePool: number = 0; private readonly _proxy: MainThreadNotebookShape; - private readonly _notebookProviders = new Map(); private readonly _notebookContentProviders = new Map(); private readonly _documents = new Map(); private readonly _editors = new Map; }>(); @@ -706,31 +703,13 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return matches; } - registerNotebookProvider( - extension: IExtensionDescription, - viewType: string, - provider: vscode.NotebookProvider, - ): vscode.Disposable { - - if (this._notebookProviders.has(viewType)) { - throw new Error(`Notebook provider for '${viewType}' already registered`); - } - - this._notebookProviders.set(viewType, { extension, provider }); - this._proxy.$registerNotebookProvider({ id: extension.identifier, location: extension.extensionLocation }, viewType); - return new VSCodeDisposable(() => { - this._notebookProviders.delete(viewType); - this._proxy.$unregisterNotebookProvider(viewType); - }); - } - registerNotebookContentProvider( extension: IExtensionDescription, viewType: string, provider: vscode.NotebookContentProvider, ): vscode.Disposable { - if (this._notebookProviders.has(viewType)) { + if (this._notebookContentProviders.has(viewType)) { throw new Error(`Notebook provider for '${viewType}' already registered`); } @@ -742,97 +721,99 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN }); } - async _resolveNotebookFromContentProvider(viewType: string, uri: UriComponents): Promise { + async $resolveNotebookData(viewType: string, uri: UriComponents): Promise { let provider = this._notebookContentProviders.get(viewType); + let document = this._documents.get(URI.revive(uri).toString()); - if (provider) { - const revivedUri = URI.revive(uri); - if (!this._documents.has(revivedUri.toString())) { - let document = new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, viewType, revivedUri, this); - await this._proxy.$createNotebookDocument( - document.handle, - viewType, - uri - ); + if (provider && document) { + const rawCells = await provider.provider.openNotebook(URI.revive(uri)); + const renderers = new Set(); + const dto = { + metadata: { + ...notebookDocumentMetadataDefaults, + ...rawCells.metadata + }, + languages: rawCells.languages, + cells: rawCells.cells.map(cell => { + let transformedOutputs = cell.outputs.map(output => { + if (output.outputKind === CellOutputKind.Rich) { + // TODO display string[] + const ret = this._transformMimeTypes(document!, (rawCells.metadata.displayOrder as string[]) || [], output); - this._documents.set(revivedUri.toString(), document); - } + if (ret.orderedMimeTypes[ret.pickedMimeTypeIndex].isResolved) { + renderers.add(ret.orderedMimeTypes[ret.pickedMimeTypeIndex].rendererId!); + } + return ret; + } else { + return output as IStreamOutput | IErrorOutput; + } + }); - const onDidReceiveMessage = new Emitter(); - - let editor = new ExtHostNotebookEditor( - viewType, - `${ExtHostNotebookController._handlePool++}`, - revivedUri, - this._proxy, - onDidReceiveMessage, - this._documents.get(revivedUri.toString())!, - this._documentsAndEditors - ); - - this._editors.set(revivedUri.toString(), { editor, onDidReceiveMessage }); - - const data = await provider.provider.openNotebook(revivedUri); - editor.document.languages = data.languages; - editor.document.metadata = { - ...notebookDocumentMetadataDefaults, - ...data.metadata + return { + language: cell.language, + cellKind: cell.cellKind, + metadata: cell.metadata, + source: cell.source, + outputs: transformedOutputs + }; + }) }; - await editor.edit(editBuilder => { - for (let i = 0; i < data.cells.length; i++) { - const cell = data.cells[i]; - editBuilder.insert(0, cell.source, cell.language, cell.cellKind, cell.outputs, cell.metadata); - } - }); - - this._onDidOpenNotebookDocument.fire(editor.document); - return editor.document.handle; - } else { - return Promise.resolve(undefined); + return dto; } + + return undefined; // {{SQL CARBON EDIT}} } - async $resolveNotebook(viewType: string, uri: UriComponents): Promise { - let notebookFromNotebookContentProvider = await this._resolveNotebookFromContentProvider(viewType, uri); + private _transformMimeTypes(document: ExtHostNotebookDocument, displayOrder: string[], output: vscode.CellDisplayOutput): ITransformedDisplayOutputDto { + let mimeTypes = Object.keys(output.data); + let coreDisplayOrder = this.outputDisplayOrder; + const sorted = sortMimeTypes(mimeTypes, coreDisplayOrder?.userOrder || [], displayOrder, coreDisplayOrder?.defaultOrder || []); - if (notebookFromNotebookContentProvider !== undefined) { - return notebookFromNotebookContentProvider; - } + let orderMimeTypes: IOrderedMimeType[] = []; - let provider = this._notebookProviders.get(viewType); + sorted.forEach(mimeType => { + let handlers = this.findBestMatchedRenderer(mimeType); - if (provider) { - if (!this._documents.has(URI.revive(uri).toString())) { - let document = new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, viewType, URI.revive(uri), this); - await this._proxy.$createNotebookDocument( - document.handle, - viewType, - uri - ); + if (handlers.length) { + let renderedOutput = handlers[0].render(document, output, mimeType); - this._documents.set(URI.revive(uri).toString(), document); + orderMimeTypes.push({ + mimeType: mimeType, + isResolved: true, + rendererId: handlers[0].handle, + output: renderedOutput + }); + + for (let i = 1; i < handlers.length; i++) { + orderMimeTypes.push({ + mimeType: mimeType, + isResolved: false, + rendererId: handlers[i].handle + }); + } + + if (mimeTypeSupportedByCore(mimeType)) { + orderMimeTypes.push({ + mimeType: mimeType, + isResolved: false, + rendererId: -1 + }); + } + } else { + orderMimeTypes.push({ + mimeType: mimeType, + isResolved: false + }); } + }); - const onDidReceiveMessage = new Emitter(); - - let editor = new ExtHostNotebookEditor( - viewType, - `${ExtHostNotebookController._handlePool++}`, - URI.revive(uri), - this._proxy, - onDidReceiveMessage, - this._documents.get(URI.revive(uri).toString())!, - this._documentsAndEditors - ); - - this._editors.set(URI.revive(uri).toString(), { editor, onDidReceiveMessage }); - await provider.provider.resolveNotebook(editor); - // await editor.document.$updateCells(); - return editor.document.handle; - } - - return Promise.resolve(undefined); + return { + outputKind: output.outputKind, + data: output.data, + orderedMimeTypes: orderMimeTypes, + pickedMimeTypeIndex: 0 + }; } async $executeNotebook(viewType: string, uri: UriComponents, cellHandle: number | undefined, token: CancellationToken): Promise { @@ -847,15 +828,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return this._notebookContentProviders.get(viewType)!.provider.executeCell(document, cell, token); } - - let provider = this._notebookProviders.get(viewType); - - if (!provider) { - return; - } - - let cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; - return provider.provider.executeCell(document!, cell, token); } async $saveNotebook(viewType: string, uri: UriComponents, token: CancellationToken): Promise { @@ -874,44 +846,26 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return true; } - let provider = this._notebookProviders.get(viewType); - - if (provider && document) { - return await provider.provider.save(document); - } - return false; } - async $updateActiveEditor(viewType: string, uri: UriComponents): Promise { - this._activeNotebookDocument = this._documents.get(URI.revive(uri).toString()); - this._activeNotebookEditor = this._editors.get(URI.revive(uri).toString())?.editor; - } - - async $destoryNotebookDocument(viewType: string, uri: UriComponents): Promise { - let provider = this._notebookProviders.get(viewType); - - if (!provider) { + async $saveNotebookAs(viewType: string, uri: UriComponents, target: UriComponents, token: CancellationToken): Promise { + let document = this._documents.get(URI.revive(uri).toString()); + if (!document) { return false; } - let document = this._documents.get(URI.revive(uri).toString()); + if (this._notebookContentProviders.has(viewType)) { + try { + await this._notebookContentProviders.get(viewType)!.provider.saveNotebookAs(URI.revive(target), document, token); + } catch (e) { + return false; + } - if (document) { - document.dispose(); - this._documents.delete(URI.revive(uri).toString()); - this._onDidCloseNotebookDocument.fire(document); + return true; } - let editor = this._editors.get(URI.revive(uri).toString()); - - if (editor) { - editor.editor.dispose(); - editor.onDidReceiveMessage.dispose(); - this._editors.delete(URI.revive(uri).toString()); - } - - return true; + return false; } $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void { @@ -957,4 +911,60 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } } } + + async $acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta) { + if (delta.removedDocuments) { + delta.removedDocuments.forEach((uri) => { + let document = this._documents.get(URI.revive(uri).toString()); + + if (document) { + document.dispose(); + this._documents.delete(URI.revive(uri).toString()); + this._onDidCloseNotebookDocument.fire(document); + } + + let editor = this._editors.get(URI.revive(uri).toString()); + + if (editor) { + editor.editor.dispose(); + editor.onDidReceiveMessage.dispose(); + this._editors.delete(URI.revive(uri).toString()); + } + }); + } + + if (delta.addedDocuments) { + delta.addedDocuments.forEach(modelData => { + const revivedUri = URI.revive(modelData.uri); + const viewType = modelData.viewType; + if (!this._documents.has(revivedUri.toString())) { + let document = new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, viewType, revivedUri, this); + this._documents.set(revivedUri.toString(), document); + } + + const onDidReceiveMessage = new Emitter(); + const document = this._documents.get(revivedUri.toString())!; + + let editor = new ExtHostNotebookEditor( + viewType, + `${ExtHostNotebookController._handlePool++}`, + revivedUri, + this._proxy, + onDidReceiveMessage, + document, + this._documentsAndEditors + ); + + this._onDidOpenNotebookDocument.fire(document); + + // TODO, does it already exist? + this._editors.set(revivedUri.toString(), { editor, onDidReceiveMessage }); + }); + } + + if (delta.newActiveEditor) { + this._activeNotebookDocument = this._documents.get(URI.revive(delta.newActiveEditor).toString()); + this._activeNotebookEditor = this._editors.get(URI.revive(delta.newActiveEditor).toString())?.editor; + } + } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 5e27ac4eff..0e734ba902 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2742,3 +2742,27 @@ export class TimelineItem implements vscode.TimelineItem { } //#endregion Timeline + +//#region ExtensionContext + +export enum ExtensionMode { + /** + * The extension is installed normally (for example, from the marketplace + * or VSIX) in VS Code. + */ + Release = 1, + + /** + * The extension is running from an `--extensionDevelopmentPath` provided + * when launching VS Code. + */ + Development = 2, + + /** + * The extension is running from an `--extensionDevelopmentPath` and + * the extension host is running unit tests. + */ + Test = 3, +} + +//#endregion ExtensionContext diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index 4077020a39..9e8f5aacdb 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -37,7 +37,6 @@ namespace schema { case 'debug/callstack/context': return MenuId.DebugCallStackContext; case 'debug/toolbar': return MenuId.DebugToolBar; case 'debug/toolBar': return MenuId.DebugToolBar; - case 'menuBar/file': return MenuId.MenubarFileMenu; case 'menuBar/webNavigation': return MenuId.MenubarWebNavigationMenu; case 'scm/title': return MenuId.SCMTitle; case 'scm/sourceControl': return MenuId.SCMSourceControl; @@ -70,7 +69,6 @@ namespace schema { export function isProposedAPI(menuId: MenuId): boolean { switch (menuId) { case MenuId.StatusBarWindowIndicatorMenu: - case MenuId.MenubarFileMenu: case MenuId.MenubarWebNavigationMenu: return true; } diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index 6feda76860..1ab607e76c 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -230,13 +230,10 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { public getEnvironmentVariableCollection(extension: IExtensionDescription): vscode.EnvironmentVariableCollection { let collection = this._environmentVariableCollections.get(extension.identifier.value); - if (!collection) { - // TODO: Disable dispose collection = new EnvironmentVariableCollection(); this._setEnvironmentVariableCollection(extension.identifier.value, collection); } - return collection; } diff --git a/src/vs/workbench/browser/actions/navigationActions.ts b/src/vs/workbench/browser/actions/navigationActions.ts index a45cbc3b33..5889c7a30d 100644 --- a/src/vs/workbench/browser/actions/navigationActions.ts +++ b/src/vs/workbench/browser/actions/navigationActions.ts @@ -202,8 +202,9 @@ class NavigateDownAction extends BaseNavigationAction { } function findVisibleNeighbour(layoutService: IWorkbenchLayoutService, part: Parts, next: boolean): Parts { - const neighbour = part === Parts.EDITOR_PART ? (next ? Parts.STATUSBAR_PART : Parts.PANEL_PART) : part === Parts.STATUSBAR_PART ? (next ? Parts.SIDEBAR_PART : Parts.EDITOR_PART) : - part === Parts.SIDEBAR_PART ? (next ? Parts.PANEL_PART : Parts.STATUSBAR_PART) : part === Parts.PANEL_PART ? (next ? Parts.EDITOR_PART : Parts.SIDEBAR_PART) : Parts.EDITOR_PART; + const neighbour = part === Parts.EDITOR_PART ? (next ? Parts.STATUSBAR_PART : Parts.PANEL_PART) : part === Parts.STATUSBAR_PART ? (next ? Parts.ACTIVITYBAR_PART : Parts.EDITOR_PART) : + part === Parts.ACTIVITYBAR_PART ? (next ? Parts.SIDEBAR_PART : Parts.STATUSBAR_PART) : part === Parts.SIDEBAR_PART ? (next ? Parts.PANEL_PART : Parts.ACTIVITYBAR_PART) : + part === Parts.PANEL_PART ? (next ? Parts.EDITOR_PART : Parts.SIDEBAR_PART) : Parts.EDITOR_PART; if (layoutService.isVisible(neighbour) || neighbour === Parts.EDITOR_PART) { return neighbour; } @@ -212,8 +213,8 @@ function findVisibleNeighbour(layoutService: IWorkbenchLayoutService, part: Part } function focusNextOrPreviousPart(layoutService: IWorkbenchLayoutService, next: boolean): void { - const currentlyFocusedPart = layoutService.hasFocus(Parts.EDITOR_PART) ? Parts.EDITOR_PART : layoutService.hasFocus(Parts.STATUSBAR_PART) ? Parts.STATUSBAR_PART : - layoutService.hasFocus(Parts.SIDEBAR_PART) ? Parts.SIDEBAR_PART : layoutService.hasFocus(Parts.PANEL_PART) ? Parts.PANEL_PART : undefined; + const currentlyFocusedPart = layoutService.hasFocus(Parts.EDITOR_PART) ? Parts.EDITOR_PART : layoutService.hasFocus(Parts.ACTIVITYBAR_PART) ? Parts.ACTIVITYBAR_PART : + layoutService.hasFocus(Parts.STATUSBAR_PART) ? Parts.STATUSBAR_PART : layoutService.hasFocus(Parts.SIDEBAR_PART) ? Parts.SIDEBAR_PART : layoutService.hasFocus(Parts.PANEL_PART) ? Parts.PANEL_PART : undefined; let partToFocus = Parts.EDITOR_PART; if (currentlyFocusedPart) { partToFocus = findVisibleNeighbour(layoutService, currentlyFocusedPart, next); diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index b9c31c0e36..61e3973c11 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -11,7 +11,7 @@ import { IWindowOpenable } from 'vs/platform/windows/common/windows'; import { URI } from 'vs/base/common/uri'; import { ITextFileService, stringToSnapshot } from 'vs/workbench/services/textfile/common/textfiles'; import { Schemas } from 'vs/base/common/network'; -import { IEditorViewState } from 'vs/editor/common/editorCommon'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { DataTransfers, IDragAndDropData } from 'vs/base/browser/dnd'; import { DragMouseEvent } from 'vs/base/browser/mouseEvent'; import { normalizeDriveLetter } from 'vs/base/common/labels'; @@ -58,7 +58,7 @@ export interface IDraggedEditor extends IDraggedResource { content?: string; encoding?: string; mode?: string; - viewState?: IEditorViewState; + options?: ITextEditorOptions; } export interface ISerializedDraggedEditor { @@ -66,7 +66,7 @@ export interface ISerializedDraggedEditor { content?: string; encoding?: string; mode?: string; - viewState?: IEditorViewState; + options?: ITextEditorOptions; } export const CodeDataTransfers = { @@ -90,7 +90,7 @@ export function extractResources(e: DragEvent, externalOnly?: boolean): Array ITextEditorOptions) | undefined, event: DragMouseEvent | DragEvent): void { if (resources.length === 0 || !event.dataTransfer) { return; } @@ -346,18 +346,30 @@ export function fillResourceDataTransfers(accessor: ServicesAccessor, resources: const draggedEditors: ISerializedDraggedEditor[] = []; files.forEach(file => { + let options: ITextEditorOptions | undefined = undefined; - // Try to find editor view state from the visible editors that match given resource - let viewState: IEditorViewState | undefined = undefined; - const textEditorControls = editorService.visibleTextEditorControls; - for (const textEditorControl of textEditorControls) { - if (isCodeEditor(textEditorControl)) { - const model = textEditorControl.getModel(); - if (model?.uri?.toString() === file.resource.toString()) { - viewState = withNullAsUndefined(textEditorControl.saveViewState()); - break; - } - } + // Use provided callback for editor options + if (typeof optionsCallback === 'function') { + options = optionsCallback(file.resource); + } + + // Otherwise try to figure out the view state from opened editors that match + else { + options = { + viewState: (() => { + const textEditorControls = editorService.visibleTextEditorControls; + for (const textEditorControl of textEditorControls) { + if (isCodeEditor(textEditorControl)) { + const model = textEditorControl.getModel(); + if (model?.uri?.toString() === file.resource.toString()) { + return withNullAsUndefined(textEditorControl.saveViewState()); + } + } + } + + return undefined; + })() + }; } // Try to find encoding and mode from text model @@ -378,7 +390,7 @@ export function fillResourceDataTransfers(accessor: ServicesAccessor, resources: } // Add as dragged editor - draggedEditors.push({ resource: file.resource.toString(), content, viewState, encoding, mode }); + draggedEditors.push({ resource: file.resource.toString(), content, options, encoding, mode }); }); if (draggedEditors.length) { diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 701ff0bc29..3e551bac6b 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -42,8 +42,21 @@ function toResource(props: IResourceLabelProps | undefined): URI | undefined { } export interface IResourceLabelOptions extends IIconLabelValueOptions { + + /** + * A hint to the file kind of the resource. + */ fileKind?: FileKind; + + /** + * File decorations to use for the label. + */ fileDecorations?: { colors: boolean, badges: boolean }; + + /** + * Will take the provided label as is and e.g. not override it for untitled files. + */ + forceLabel?: boolean; } export interface IFileLabelOptions extends IResourceLabelOptions { @@ -368,7 +381,7 @@ class ResourceLabelWidget extends IconLabel { /*const resource = toResource(label); {{SQL CARBON EDIT}} we don't want to special case untitled files const isMasterDetail = label?.resource && !URI.isUri(label.resource); - if (!isMasterDetail && resource?.scheme === Schemas.untitled) { + if (!options.forceLabel && !isMasterDetail && resource?.scheme === Schemas.untitled) { // Untitled labels are very dynamic because they may change // whenever the content changes (unless a path is associated). // As such we always ask the actual editor for it's name and @@ -528,7 +541,6 @@ class ResourceLabelWidget extends IconLabel { ); if (deco) { - this.renderDisposables.add(deco); if (deco.tooltip) { diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 593e3371e2..318e96ed07 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -177,6 +177,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private backupFileService!: IBackupFileService; private notificationService!: INotificationService; private themeService!: IThemeService; + private activityBarService!: IActivityBarService; protected readonly state = { fullscreen: false, @@ -260,8 +261,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.viewDescriptorService = accessor.get(IViewDescriptorService); this.titleService = accessor.get(ITitleService); this.notificationService = accessor.get(INotificationService); + this.activityBarService = accessor.get(IActivityBarService); accessor.get(IStatusbarService); // not used, but called to ensure instantiated - accessor.get(IActivityBarService); // not used, but called to ensure instantiated // Listeners this.registerLayoutListeners(); @@ -846,6 +847,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi activeViewlet.focus(); } break; + case Parts.ACTIVITYBAR_PART: + this.activityBarService.focusActivityBar(); + break; default: // Status Bar, Activity Bar and Title Bar simply pass focus to container const container = this.getContainer(part); diff --git a/src/vs/workbench/browser/media/part.css b/src/vs/workbench/browser/media/part.css index 389b283c40..32889c8a07 100644 --- a/src/vs/workbench/browser/media/part.css +++ b/src/vs/workbench/browser/media/part.css @@ -10,7 +10,6 @@ .monaco-workbench .part > .drop-block-overlay.visible { visibility: visible; - opacity: 1; } .monaco-workbench .part > .drop-block-overlay { @@ -18,10 +17,8 @@ top: 0; width: 100%; height: 100%; - backdrop-filter: brightness(97%) blur(2px); visibility: hidden; opacity: 0; - transition: opacity .5s, visibility .5s; z-index: 10; } diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 3f72b4249a..a046a2b2a5 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -168,6 +168,10 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.registerListeners(); } + focusActivityBar(): void { + this.compositeBar.focus(); + } + private registerListeners(): void { // View Container Changes diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index 4fcb0e24d6..e144f9914f 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -66,8 +66,6 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { if (targetCompositeId) { this.moveComposite(currentContainer.id, targetCompositeId, before); } - - this.openComposite(currentContainer.id, true); } } @@ -255,6 +253,12 @@ export class CompositeBar extends Widget implements ICompositeBar { return actionBarDiv; } + focus(): void { + if (this.compositeSwitcherBar) { + this.compositeSwitcherBar.focus(); + } + } + layout(dimension: Dimension): void { this.dimension = dimension; if (dimension.height === 0 || dimension.width === 0) { @@ -275,8 +279,7 @@ export class CompositeBar extends Widget implements ICompositeBar { // Add to the model if (this.model.add(id, name, order)) { this.computeSizes([this.model.findItem(id)]); - // Set timeout helps prevent flicker - setTimeout(() => this.updateCompositeSwitcher(), 0); + this.updateCompositeSwitcher(); } } diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index 14a5c6438d..ae5a50c44e 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -548,11 +548,8 @@ export class CompositeActionViewItem extends ActivityActionViewItem { if (e.eventData.dataTransfer) { e.eventData.dataTransfer.effectAllowed = 'move'; } - - // Trigger the action even on drag start to prevent clicks from failing that started a drag - if (!this.getAction().checked) { - this.getAction().run(); - } + // Remove focus indicator when dragging + this.blur(); } })); diff --git a/src/vs/workbench/browser/parts/editor/baseEditor.ts b/src/vs/workbench/browser/parts/editor/baseEditor.ts index 11a916565e..42e827ebbf 100644 --- a/src/vs/workbench/browser/parts/editor/baseEditor.ts +++ b/src/vs/workbench/browser/parts/editor/baseEditor.ts @@ -19,6 +19,7 @@ import { MementoObject } from 'vs/workbench/common/memento'; import { isEqualOrParent, joinPath } from 'vs/base/common/resources'; import { isLinux } from 'vs/base/common/platform'; import { indexOfPath } from 'vs/base/common/extpath'; +import { IDisposable } from 'vs/base/common/lifecycle'; /** * The base class of editors in the workbench. Editors register themselves for specific editor inputs. @@ -171,6 +172,7 @@ interface MapGroupToMemento { export class EditorMemento implements IEditorMemento { private cache: LRUCache> | undefined; private cleanedUp = false; + private editorDisposables: Map | undefined; constructor( public readonly id: string, @@ -200,9 +202,18 @@ export class EditorMemento implements IEditorMemento { // Automatically clear when editor input gets disposed if any if (resourceOrEditor instanceof EditorInput) { - Event.once(resourceOrEditor.onDispose)(() => { - this.clearEditorState(resource); - }); + const editor = resourceOrEditor; + + if (!this.editorDisposables) { + this.editorDisposables = new Map(); + } + + if (!this.editorDisposables.has(editor)) { + this.editorDisposables.set(editor, Event.once(resourceOrEditor.onDispose)(() => { + this.clearEditorState(resource); + this.editorDisposables?.delete(editor); + })); + } } } diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index ad1139571c..8200bdc682 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -7,7 +7,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import * as nls from 'vs/nls'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; -import { EditorInput, IEditorInputFactory, SideBySideEditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, TextCompareEditorActiveContext, EditorPinnedContext, EditorGroupEditorsCountContext } from 'vs/workbench/common/editor'; +import { EditorInput, IEditorInputFactory, SideBySideEditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, TextCompareEditorActiveContext, EditorPinnedContext, EditorGroupEditorsCountContext, EditorStickyContext } from 'vs/workbench/common/editor'; import { TextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor'; import { SideBySideEditor } from 'vs/workbench/browser/parts/editor/sideBySideEditor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; @@ -433,6 +433,8 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: editorCo MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: editorCommands.CLOSE_SAVED_EDITORS_COMMAND_ID, title: nls.localize('closeAllSaved', "Close Saved") }, group: '1_close', order: 40 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: editorCommands.CLOSE_EDITORS_IN_GROUP_COMMAND_ID, title: nls.localize('closeAll', "Close All") }, group: '1_close', order: 50 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: editorCommands.KEEP_EDITOR_COMMAND_ID, title: nls.localize('keepOpen', "Keep Open"), precondition: EditorPinnedContext.toNegated() }, group: '3_preview', order: 10, when: ContextKeyExpr.has('config.workbench.editor.enablePreview') }); +MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: editorCommands.PIN_EDITOR_COMMAND_ID, title: nls.localize('pin', "Pin") }, group: '3_preview', order: 20, when: ContextKeyExpr.and(EditorStickyContext.toNegated(), ContextKeyExpr.has('config.workbench.editor.showTabs')) }); +MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: editorCommands.UNPIN_EDITOR_COMMAND_ID, title: nls.localize('unpin', "Unpin") }, group: '3_preview', order: 20, when: ContextKeyExpr.and(EditorStickyContext, ContextKeyExpr.has('config.workbench.editor.showTabs')) }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: editorCommands.SPLIT_EDITOR_UP, title: nls.localize('splitUp', "Split Up") }, group: '5_split', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: editorCommands.SPLIT_EDITOR_DOWN, title: nls.localize('splitDown', "Split Down") }, group: '5_split', order: 20 }); MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { command: { id: editorCommands.SPLIT_EDITOR_LEFT, title: nls.localize('splitLeft', "Split Left") }, group: '5_split', order: 30 }); @@ -579,6 +581,8 @@ appendEditorToolItem( // Editor Commands for Command Palette const viewCategory = { value: nls.localize('view', "View"), original: 'View' }; MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.KEEP_EDITOR_COMMAND_ID, title: { value: nls.localize('keepEditor', "Keep Editor"), original: 'Keep Editor' }, category: viewCategory }, when: ContextKeyExpr.has('config.workbench.editor.enablePreview') }); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.PIN_EDITOR_COMMAND_ID, title: { value: nls.localize('pinEditor', "Pin Editor"), original: 'Pin Editor' }, category: viewCategory }, when: ContextKeyExpr.has('config.workbench.editor.showTabs') }); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.UNPIN_EDITOR_COMMAND_ID, title: { value: nls.localize('unpinEditor', "Unpin Editor"), original: 'Unpin Editor' }, category: viewCategory }, when: ContextKeyExpr.has('config.workbench.editor.showTabs') }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.CLOSE_EDITORS_IN_GROUP_COMMAND_ID, title: { value: nls.localize('closeEditorsInGroup', "Close All Editors in Group"), original: 'Close All Editors in Group' }, category: viewCategory } }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.CLOSE_SAVED_EDITORS_COMMAND_ID, title: { value: nls.localize('closeSavedEditors', "Close Saved Editors in Group"), original: 'Close Saved Editors in Group' }, category: viewCategory } }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: editorCommands.CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID, title: { value: nls.localize('closeOtherEditors', "Close Other Editors in Group"), original: 'Close Other Editors in Group' }, category: viewCategory } }); diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index eae878cfcc..f46d470920 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -500,7 +500,7 @@ export class CloseLeftEditorsInGroupAction extends Action { async run(context?: IEditorIdentifier): Promise { const { group, editor } = getTarget(this.editorService, this.editorGroupService, context); if (group && editor) { - return group.closeEditors({ direction: CloseDirection.LEFT, except: editor }); + return group.closeEditors({ direction: CloseDirection.LEFT, except: editor, excludeSticky: true }); } } } @@ -514,7 +514,7 @@ function getTarget(editorService: IEditorService, editorGroupService: IEditorGro return { group: editorGroupService.activeGroup, editor: editorGroupService.activeGroup.activeEditor }; } -export abstract class BaseCloseAllAction extends Action { +abstract class BaseCloseAllAction extends Action { constructor( id: string, @@ -554,7 +554,7 @@ export abstract class BaseCloseAllAction extends Action { // to bring each dirty editor to the front so that the user // can review if the files should be changed or not. await Promise.all(this.groupsToClose.map(async groupToClose => { - for (const editor of groupToClose.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) { + for (const editor of groupToClose.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, { excludeSticky: this.excludeSticky })) { if (editor.isDirty() && !editor.isSaving() /* ignore editors that are being saved */) { return groupToClose.openEditor(editor); } @@ -566,7 +566,7 @@ export abstract class BaseCloseAllAction extends Action { const dirtyEditorsToConfirm = new Set(); const dirtyEditorsToAutoSave = new Set(); - for (const editor of this.editorService.editors) { + for (const editor of this.editorService.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: this.excludeSticky }).map(({ editor }) => editor)) { if (!editor.isDirty() || editor.isSaving()) { continue; // only interested in dirty editors (unless in the process of saving) } @@ -601,21 +601,29 @@ export abstract class BaseCloseAllAction extends Action { confirmation = ConfirmResult.DONT_SAVE; } - if (confirmation === ConfirmResult.CANCEL) { - return; + // Handle result from asking user + let result: boolean | undefined = undefined; + switch (confirmation) { + case ConfirmResult.CANCEL: + return; + case ConfirmResult.DONT_SAVE: + result = await this.editorService.revertAll({ soft: true, includeUntitled: true, excludeSticky: this.excludeSticky }); + break; + case ConfirmResult.SAVE: + result = await this.editorService.saveAll({ reason: saveReason, includeUntitled: true, excludeSticky: this.excludeSticky }); + break; } - if (confirmation === ConfirmResult.DONT_SAVE) { - await this.editorService.revertAll({ soft: true, includeUntitled: true }); - } else { - await this.editorService.saveAll({ reason: saveReason, includeUntitled: true }); - } - if (!this.workingCopyService.hasDirty) { + // Only continue to close editors if we either have no more dirty + // editors or the result from the save/revert was successful + if (!this.workingCopyService.hasDirty || result) { return this.doCloseAll(); } } + protected abstract get excludeSticky(): boolean; + protected abstract doCloseAll(): Promise; } @@ -636,8 +644,12 @@ export class CloseAllEditorsAction extends BaseCloseAllAction { super(id, label, Codicon.closeAll.classNames, workingCopyService, fileDialogService, editorGroupService, editorService, filesConfigurationService); } + protected get excludeSticky(): boolean { + return true; + } + protected async doCloseAll(): Promise { - await Promise.all(this.groupsToClose.map(g => g.closeAllEditors())); + await Promise.all(this.groupsToClose.map(group => group.closeAllEditors({ excludeSticky: true }))); } } @@ -658,6 +670,10 @@ export class CloseAllEditorGroupsAction extends BaseCloseAllAction { super(id, label, undefined, workingCopyService, fileDialogService, editorGroupService, editorService, filesConfigurationService); } + protected get excludeSticky(): boolean { + return false; + } + protected async doCloseAll(): Promise { await Promise.all(this.groupsToClose.map(group => group.closeAllEditors())); @@ -680,12 +696,12 @@ export class CloseEditorsInOtherGroupsAction extends Action { async run(context?: IEditorIdentifier): Promise { const groupToSkip = context ? this.editorGroupService.getGroup(context.groupId) : this.editorGroupService.activeGroup; - await Promise.all(this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).map(async g => { - if (groupToSkip && g.id === groupToSkip.id) { + await Promise.all(this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).map(async group => { + if (groupToSkip && group.id === groupToSkip.id) { return; } - return g.closeAllEditors(); + return group.closeAllEditors({ excludeSticky: true }); })); } } @@ -707,7 +723,7 @@ export class CloseEditorInAllGroupsAction extends Action { async run(): Promise { const activeEditor = this.editorService.activeEditor; if (activeEditor) { - await Promise.all(this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).map(g => g.closeEditor(activeEditor))); + await Promise.all(this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).map(group => group.closeEditor(activeEditor))); } } } diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 0894e73047..07c0e4b26d 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { TextCompareEditorVisibleContext, EditorInput, IEditorIdentifier, IEditorCommandsContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, CloseDirection, IEditorInput, IVisibleEditorPane } from 'vs/workbench/common/editor'; +import { TextCompareEditorVisibleContext, EditorInput, IEditorIdentifier, IEditorCommandsContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, CloseDirection, IEditorInput, IVisibleEditorPane, EditorStickyContext, EditorsOrder } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor'; @@ -37,6 +37,9 @@ export const LAYOUT_EDITOR_GROUPS_COMMAND_ID = 'layoutEditorGroups'; export const KEEP_EDITOR_COMMAND_ID = 'workbench.action.keepEditor'; export const SHOW_EDITORS_IN_GROUP = 'workbench.action.showEditorsInGroup'; +export const PIN_EDITOR_COMMAND_ID = 'workbench.action.pinEditor'; +export const UNPIN_EDITOR_COMMAND_ID = 'workbench.action.unpinEditor'; + export const TOGGLE_DIFF_SIDE_BY_SIDE = 'toggle.diff.renderSideBySide'; export const GOTO_NEXT_CHANGE = 'workbench.action.compareEditor.nextChange'; export const GOTO_PREVIOUS_CHANGE = 'workbench.action.compareEditor.previousChange'; @@ -258,7 +261,7 @@ function registerDiffEditorCommands(): void { function navigateInDiffEditor(accessor: ServicesAccessor, next: boolean): void { const editorService = accessor.get(IEditorService); - const candidates = [editorService.activeEditorPane, ...editorService.visibleEditorPanes].filter(e => e instanceof TextDiffEditor); + const candidates = [editorService.activeEditorPane, ...editorService.visibleEditorPanes].filter(editor => editor instanceof TextDiffEditor); if (candidates.length > 0) { const navigator = (candidates[0]).getDiffNavigator(); @@ -491,7 +494,7 @@ function registerCloseEditorCommands() { return Promise.all(distinct(contexts.map(c => c.groupId)).map(async groupId => { const group = editorGroupService.getGroup(groupId); if (group) { - return group.closeEditors({ savedOnly: true }); + return group.closeEditors({ savedOnly: true, excludeSticky: true }); } })); } @@ -514,7 +517,7 @@ function registerCloseEditorCommands() { return Promise.all(distinctGroupIds.map(async groupId => { const group = editorGroupService.getGroup(groupId); if (group) { - return group.closeAllEditors(); + return group.closeAllEditors({ excludeSticky: true }); } })); } @@ -596,7 +599,8 @@ function registerCloseEditorCommands() { const editors = contexts .filter(context => context.groupId === groupId) .map(context => typeof context.editorIndex === 'number' ? group.getEditorByIndex(context.editorIndex) : group.activeEditor); - const editorsToClose = group.editors.filter(e => editors.indexOf(e) === -1); + + const editorsToClose = group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).filter(editor => editors.indexOf(editor) === -1); if (group.activeEditor) { group.pinEditor(group.activeEditor); @@ -622,7 +626,7 @@ function registerCloseEditorCommands() { group.pinEditor(group.activeEditor); } - return group.closeEditors({ direction: CloseDirection.RIGHT, except: editor }); + return group.closeEditors({ direction: CloseDirection.RIGHT, except: editor, excludeSticky: true }); } } }); @@ -642,6 +646,36 @@ function registerCloseEditorCommands() { } }); + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: PIN_EDITOR_COMMAND_ID, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(EditorStickyContext.toNegated(), ContextKeyExpr.has('config.workbench.editor.showTabs')), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.Enter), + handler: async (accessor, resourceOrContext: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { + const editorGroupService = accessor.get(IEditorGroupsService); + + const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); + if (group && editor) { + return group.stickEditor(editor); + } + } + }); + + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: UNPIN_EDITOR_COMMAND_ID, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(EditorStickyContext, ContextKeyExpr.has('config.workbench.editor.showTabs')), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.Enter), + handler: async (accessor, resourceOrContext: URI | IEditorCommandsContext, context?: IEditorCommandsContext) => { + const editorGroupService = accessor.get(IEditorGroupsService); + + const { group, editor } = resolveCommandsContext(editorGroupService, getCommandsContext(resourceOrContext, context)); + if (group && editor) { + return group.unstickEditor(editor); + } + } + }); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: SHOW_EDITORS_IN_GROUP, weight: KeybindingWeight.WorkbenchContrib, diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index 988cf5a1ee..1e32861d54 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -266,7 +266,10 @@ class DropOverlay extends Themable { } // Open in target group - const options = getActiveTextEditorOptions(sourceGroup, draggedEditor.editor, EditorOptions.create({ pinned: true })); + const options = getActiveTextEditorOptions(sourceGroup, draggedEditor.editor, EditorOptions.create({ + pinned: true, // always pin dropped editor + sticky: sourceGroup.isSticky(draggedEditor.editor) // preserve sticky state + })); targetGroup.openEditor(draggedEditor.editor, options); // Ensure target has focus diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 368c19a337..3757a16a9c 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -17,7 +17,7 @@ import { attachProgressBarStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, registerThemingParticipant, Themable } from 'vs/platform/theme/common/themeService'; import { editorBackground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { EDITOR_GROUP_HEADER_TABS_BACKGROUND, EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND, EDITOR_GROUP_EMPTY_BACKGROUND, EDITOR_GROUP_FOCUSED_EMPTY_BORDER, EDITOR_GROUP_HEADER_BORDER } from 'vs/workbench/common/theme'; -import { IMoveEditorOptions, ICopyEditorOptions, ICloseEditorsFilter, IGroupChangeEvent, GroupChangeKind, GroupsOrder, ICloseEditorOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IMoveEditorOptions, ICopyEditorOptions, ICloseEditorsFilter, IGroupChangeEvent, GroupChangeKind, GroupsOrder, ICloseEditorOptions, ICloseAllEditorsOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; import { TabsTitleControl } from 'vs/workbench/browser/parts/editor/tabsTitleControl'; import { EditorControl } from 'vs/workbench/browser/parts/editor/editorControl'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; @@ -441,6 +441,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } options.pinned = this._group.isPinned(activeEditor); // preserve pinned state + options.sticky = this._group.isSticky(activeEditor); // preserve sticky state options.preserveFocus = true; // handle focus after editor is opened const activeElement = document.activeElement; @@ -732,6 +733,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this._group.count; } + get stickyCount(): number { + return this._group.stickyCount; + } + get activeEditorPane(): IVisibleEditorPane | undefined { return this.editorControl ? withNullAsUndefined(this.editorControl.activeEditorPane) : undefined; } @@ -748,12 +753,16 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this._group.isPinned(editor); } + isSticky(editorOrIndex: EditorInput | number): boolean { + return this._group.isSticky(editorOrIndex); + } + isActive(editor: EditorInput): boolean { return this._group.isActive(editor); } - getEditors(order: EditorsOrder): EditorInput[] { - return this._group.getEditors(order); + getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): EditorInput[] { + return this._group.getEditors(order, options); } getEditorByIndex(index: number): EditorInput | undefined { @@ -794,6 +803,43 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } } + stickEditor(candidate: EditorInput | undefined = this.activeEditor || undefined): void { + this.doStickEditor(candidate, true); + } + + unstickEditor(candidate: EditorInput | undefined = this.activeEditor || undefined): void { + this.doStickEditor(candidate, false); + } + + private doStickEditor(candidate: EditorInput | undefined, sticky: boolean): void { + if (candidate && this._group.isSticky(candidate) !== sticky) { + const oldIndexOfEditor = this.getIndexOfEditor(candidate); + + // Update model + const editor = sticky ? this._group.stick(candidate) : this._group.unstick(candidate); + if (!editor) { + return; + } + + // If the index of the editor changed, we need to forward this to + // title control and also make sure to emit this as an event + const newIndexOfEditor = this.getIndexOfEditor(editor); + if (newIndexOfEditor !== oldIndexOfEditor) { + this.titleAreaControl.moveEditor(editor, oldIndexOfEditor, newIndexOfEditor); + + // Event + this._onDidGroupChange.fire({ kind: GroupChangeKind.EDITOR_MOVE, editor }); + } + + // Forward sticky state to title control + if (sticky) { + this.titleAreaControl.stickEditor(editor); + } else { + this.titleAreaControl.unstickEditor(editor); + } + } + } + invokeWithinContext(fn: (accessor: ServicesAccessor) => T): T { return this.scopedInstantiationService.invokeFunction(fn); } @@ -833,11 +879,19 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Determine options const openEditorOptions: IEditorOpenOptions = { index: options ? options.index : undefined, - pinned: !this.accessor.partOptions.enablePreview || editor.isDirty() || (options?.pinned ?? typeof options?.index === 'number'), // unless specified, prefer to pin when opening with index + pinned: options?.sticky || !this.accessor.partOptions.enablePreview || editor.isDirty() || (options?.pinned ?? typeof options?.index === 'number' /* unless specified, prefer to pin when opening with index */) || (typeof options?.index === 'number' && this._group.isSticky(options.index)), + sticky: options?.sticky || (typeof options?.index === 'number' && this._group.isSticky(options.index)), active: this._group.count === 0 || !options || !options.inactive }; - if (!openEditorOptions.active && !openEditorOptions.pinned && this._group.activeEditor && this._group.isPreview(this._group.activeEditor)) { + if (options?.sticky && typeof options?.index === 'number' && !this._group.isSticky(options.index)) { + // Special case: we are to open an editor sticky but at an index that is not sticky + // In that case we prefer to open the editor at the index but not sticky. This enables + // to drag a sticky editor to an index that is not sticky to unstick it. + openEditorOptions.sticky = false; + } + + if (!openEditorOptions.active && !openEditorOptions.pinned && this._group.activeEditor && !this._group.isPinned(this._group.activeEditor)) { // Special case: we are to open an editor inactive and not pinned, but the current active // editor is also not pinned, which means it will get replaced with this one. As such, // the editor can only be active. @@ -1095,8 +1149,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // When moving an editor, try to preserve as much view state as possible by checking // for the editor to be a text editor and creating the options accordingly if so - const options = getActiveTextEditorOptions(this, editor, EditorOptions.create(moveOptions)); - options.pinned = true; // always pin moved editor + const options = getActiveTextEditorOptions(this, editor, EditorOptions.create({ + ...moveOptions, + pinned: true, // always pin moved editor + sticky: this._group.isSticky(editor) // preserve sticky state + })); // A move to another group is an open first... target.openEditor(editor, options); @@ -1377,7 +1434,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return; } - const editors = this.getEditorsToClose(args); + const editors = this.doGetEditorsToClose(args); // Check for dirty and veto const veto = await this.handleDirtyClosing(editors.slice(0)); @@ -1389,31 +1446,31 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.doCloseEditors(editors, options); } - private getEditorsToClose(editors: EditorInput[] | ICloseEditorsFilter): EditorInput[] { - if (Array.isArray(editors)) { - return editors; + private doGetEditorsToClose(args: EditorInput[] | ICloseEditorsFilter): EditorInput[] { + if (Array.isArray(args)) { + return args; } - const filter = editors; + const filter = args; const hasDirection = typeof filter.direction === 'number'; - let editorsToClose = this._group.getEditors(hasDirection ? EditorsOrder.SEQUENTIAL : EditorsOrder.MOST_RECENTLY_ACTIVE); // in MRU order only if direction is not specified + let editorsToClose = this._group.getEditors(hasDirection ? EditorsOrder.SEQUENTIAL : EditorsOrder.MOST_RECENTLY_ACTIVE, filter); // in MRU order only if direction is not specified // Filter: saved or saving only if (filter.savedOnly) { - editorsToClose = editorsToClose.filter(e => !e.isDirty() || e.isSaving()); + editorsToClose = editorsToClose.filter(editor => !editor.isDirty() || editor.isSaving()); } // Filter: direction (left / right) else if (hasDirection && filter.except) { editorsToClose = (filter.direction === CloseDirection.LEFT) ? - editorsToClose.slice(0, this._group.indexOf(filter.except)) : - editorsToClose.slice(this._group.indexOf(filter.except) + 1); + editorsToClose.slice(0, this._group.indexOf(filter.except, editorsToClose)) : + editorsToClose.slice(this._group.indexOf(filter.except, editorsToClose) + 1); } // Filter: except else if (filter.except) { - editorsToClose = editorsToClose.filter(e => !e.matches(filter.except)); + editorsToClose = editorsToClose.filter(editor => !editor.matches(filter.except)); } return editorsToClose; @@ -1444,7 +1501,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region closeAllEditors() - async closeAllEditors(): Promise { + async closeAllEditors(options?: ICloseAllEditorsOptions): Promise { if (this.isEmpty) { // If the group is empty and the request is to close all editors, we still close @@ -1458,30 +1515,34 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } // Check for dirty and veto - const editors = this._group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); - const veto = await this.handleDirtyClosing(editors.slice(0)); + const veto = await this.handleDirtyClosing(this._group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, options)); if (veto) { return; } // Do close - this.doCloseAllEditors(); + this.doCloseAllEditors(options); } - private doCloseAllEditors(): void { + private doCloseAllEditors(options?: ICloseAllEditorsOptions): void { // Close all inactive editors first - this.editors.forEach(editor => { + const editorsToClose: EditorInput[] = []; + this._group.getEditors(EditorsOrder.SEQUENTIAL, options).forEach(editor => { if (!this.isActive(editor)) { this.doCloseInactiveEditor(editor); } + + editorsToClose.push(editor); }); - // Close active editor last - this.doCloseActiveEditor(); + // Close active editor last (unless we skip it, e.g. because it is sticky) + if (this.activeEditor && editorsToClose.includes(this.activeEditor)) { + this.doCloseActiveEditor(); + } // Forward to title control - this.titleAreaControl.closeAllEditors(); + this.titleAreaControl.closeEditors(editorsToClose); } //#endregion diff --git a/src/vs/workbench/browser/parts/editor/editorsObserver.ts b/src/vs/workbench/browser/parts/editor/editorsObserver.ts index 7948dc3d5a..3631bff815 100644 --- a/src/vs/workbench/browser/parts/editor/editorsObserver.ts +++ b/src/vs/workbench/browser/parts/editor/editorsObserver.ts @@ -48,7 +48,7 @@ export class EditorsObserver extends Disposable { } get editors(): IEditorIdentifier[] { - return [...this.mostRecentEditorsMap.values()]; + return this.mostRecentEditorsMap.values(); } hasEditor(resource: URI): boolean { @@ -283,7 +283,7 @@ export class EditorsObserver extends Disposable { // Across all editor groups else { - await this.doEnsureOpenedEditorsLimit(limit, [...this.mostRecentEditorsMap.values()], exclude); + await this.doEnsureOpenedEditorsLimit(limit, this.mostRecentEditorsMap.values(), exclude); } } @@ -302,6 +302,10 @@ export class EditorsObserver extends Disposable { return false; // never the editor that should be excluded } + if (this.editorGroupsService.getGroup(groupId)?.isSticky(editor)) { + return false; // never sticky editors + } + return true; }); @@ -342,7 +346,7 @@ export class EditorsObserver extends Disposable { private serialize(): ISerializedEditorsList { const registry = Registry.as(Extensions.EditorInputFactories); - const entries = [...this.mostRecentEditorsMap.values()]; + const entries = this.mostRecentEditorsMap.values(); const mapGroupToSerializableEditorsOfGroup = new Map(); return { diff --git a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css index 8e7cf6329e..3d6cd72b8b 100644 --- a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css @@ -63,8 +63,8 @@ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon-theme.close-button-right, -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon-theme.close-button-off { - padding-left: 5px; /* reduce padding when we show icons and are in shrinking mode and tab close button is not left */ +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon-theme.close-button-off:not(.sticky) { + padding-left: 5px; /* reduce padding when we show icons and are in shrinking mode and tab close button is not left (unless sticky) */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit { @@ -82,6 +82,30 @@ max-width: -moz-fit-content; } +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit.sticky, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.sticky { + + /** Sticky tabs do not scroll in case of overflow and are always above unsticky tabs which scroll under */ + position: sticky; + z-index: 1; + + /** Sticky tabs are even and never grow */ + flex-basis: 0; + flex-grow: 0; + + /** Sticky tabs have a fixed width of 38px */ + width: 38px; + min-width: 38px; + max-width: 38px; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-fit.sticky, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-shrink.sticky { + + /** Disable sticky positions for sticky tabs if the available space is too little */ + position: static; +} + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-left .action-label { margin-right: 4px !important; } @@ -174,6 +198,10 @@ opacity: 0; /* when tab has the focus this shade breaks the tab border (fixes https://github.com/Microsoft/vscode/issues/57819) */ } +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sticky:not(.has-icon-theme) .monaco-icon-label { + text-align: center; /* ensure that sticky tabs without icon have label centered */ +} + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit .monaco-icon-label, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-fit .monaco-icon-label > .monaco-icon-label-container { overflow: visible; /* fixes https://github.com/Microsoft/vscode/issues/20182 */ @@ -210,14 +238,15 @@ overflow: visible; /* ...but still show the close button on hover, focus and when dirty */ } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off:not(.dirty) > .tab-close { - display: none; /* hide the close action bar when we are configured to hide it */ +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off:not(.dirty) > .tab-close, +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.sticky > .tab-close { + display: none; /* hide the close action bar when we are configured to hide it (unless dirty, but always when sticky) */ } .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active > .tab-close .action-label, /* always show it for active tab */ .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab > .tab-close .action-label:focus, /* always show it on focus */ .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab:hover > .tab-close .action-label, /* always show it on hover */ -.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active:hover > .tab-close .action-label, /* always show it on hover */ +.monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active:hover > .tab-close .action-label, /* always show it on hover */ .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.dirty > .tab-close .action-label { /* always show it for dirty tabs */ opacity: 1; } @@ -233,10 +262,10 @@ content: "\ea71"; /* use `circle-filled` icon unicode */ } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active > .tab-close .action-label, /* show dimmed for inactive group */ +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active > .tab-close .action-label, /* show dimmed for inactive group */ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active:hover > .tab-close .action-label, /* show dimmed for inactive group */ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty > .tab-close .action-label, /* show dimmed for inactive group */ -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover > .tab-close .action-label { /* show dimmed for inactive group */ +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover > .tab-close .action-label { /* show dimmed for inactive group */ opacity: 0.5; } @@ -257,8 +286,8 @@ padding-right: 10px; /* give a little bit more room if close button is off */ } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.close-button-off { - padding-right: 5px; /* we need less room when sizing is shrink */ +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.close-button-off:not(.sticky) { + padding-right: 5px; /* we need less room when sizing is shrink (unless tab is sticky) */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.dirty-border-top > .tab-close, diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index 2a348f1ef1..e2c1943dce 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -136,10 +136,6 @@ export class NoTabsTitleControl extends TitleControl { this.ifActiveEditorChanged(() => this.redraw()); } - closeAllEditors(): void { - this.redraw(); - } - moveEditor(editor: IEditorInput, fromIndex: number, targetIndex: number): void { this.ifActiveEditorChanged(() => this.redraw()); } @@ -148,6 +144,14 @@ export class NoTabsTitleControl extends TitleControl { this.ifEditorIsActive(editor, () => this.redraw()); } + stickEditor(editor: IEditorInput): void { + // Sticky editors are not presented any different with tabs disabled + } + + unstickEditor(editor: IEditorInput): void { + // Sticky editors are not presented any different with tabs disabled + } + setActive(isActive: boolean): void { this.redraw(); } @@ -219,7 +223,6 @@ export class NoTabsTitleControl extends TitleControl { } } - private ifEditorIsActive(editor: IEditorInput, fn: () => void): void { if (this.group.isActive(editor)) { fn(); // only run if editor is current active diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 331494c005..cfe1295d2a 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -27,7 +27,7 @@ import { getOrSet } from 'vs/base/common/map'; import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector, HIGH_CONTRAST } from 'vs/platform/theme/common/themeService'; import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_ACTIVE_FOREGROUND, TAB_INACTIVE_FOREGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND, TAB_UNFOCUSED_INACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, WORKBENCH_BACKGROUND, TAB_ACTIVE_BORDER_TOP, TAB_UNFOCUSED_ACTIVE_BORDER_TOP, TAB_ACTIVE_MODIFIED_BORDER, TAB_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_ACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_BACKGROUND, TAB_HOVER_FOREGROUND, TAB_UNFOCUSED_HOVER_FOREGROUND, EDITOR_GROUP_HEADER_TABS_BORDER } from 'vs/workbench/common/theme'; import { activeContrastBorder, contrastBorder, editorBackground, breadcrumbsBackground } from 'vs/platform/theme/common/colorRegistry'; -import { ResourcesDropHandler, fillResourceDataTransfers, DraggedEditorIdentifier, DraggedEditorGroupIdentifier, DragAndDropObserver } from 'vs/workbench/browser/dnd'; +import { ResourcesDropHandler, DraggedEditorIdentifier, DraggedEditorGroupIdentifier, DragAndDropObserver } from 'vs/workbench/browser/dnd'; import { Color } from 'vs/base/common/color'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -67,6 +67,11 @@ export class TabsTitleControl extends TitleControl { large: 10 }; + private static readonly TAB_SIZES = { + sticky: 38, + fit: 120 + }; + private titleContainer: HTMLElement | undefined; private tabsAndActionsContainer: HTMLElement | undefined; private tabsContainer: HTMLElement | undefined; @@ -392,10 +397,6 @@ export class TabsTitleControl extends TitleControl { this.handleClosedEditors(); } - closeAllEditors(): void { - this.handleClosedEditors(); - } - private handleClosedEditors(): void { // There are tabs to show @@ -453,7 +454,24 @@ export class TabsTitleControl extends TitleControl { } pinEditor(editor: IEditorInput): void { - this.withTab(editor, (editor, index, tabContainer, tabLabelWidget, tabLabel) => this.redrawLabel(editor, tabContainer, tabLabelWidget, tabLabel)); + this.withTab(editor, (editor, index, tabContainer, tabLabelWidget, tabLabel) => this.redrawLabel(editor, index, tabContainer, tabLabelWidget, tabLabel)); + } + + stickEditor(editor: IEditorInput): void { + this.doHandleStickyEditorChange(editor); + } + + unstickEditor(editor: IEditorInput): void { + this.doHandleStickyEditorChange(editor); + } + + private doHandleStickyEditorChange(editor: IEditorInput): void { + + // Update tab + this.withTab(editor, (editor, index, tabContainer, tabLabelWidget, tabLabel) => this.redrawTab(editor, index, tabContainer, tabLabelWidget, tabLabel)); + + // A change to the sticky state requires a layout to keep the active editor visible + this.layout(this.dimension); } setActive(isGroupActive: boolean): void { @@ -487,7 +505,7 @@ export class TabsTitleControl extends TitleControl { // As such we need to redraw each label this.forEachTab((editor, index, tabContainer, tabLabelWidget, tabLabel) => { - this.redrawLabel(editor, tabContainer, tabLabelWidget, tabLabel); + this.redrawLabel(editor, index, tabContainer, tabLabelWidget, tabLabel); }); // A change to a label requires a layout to keep the active editor visible @@ -745,10 +763,7 @@ export class TabsTitleControl extends TitleControl { } // Apply some datatransfer types to allow for dragging the element outside of the application - const resource = toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }); - if (resource) { - this.instantiationService.invokeFunction(fillResourceDataTransfers, [resource], e); - } + this.doFillResourceDataTransfers(editor, e); // Fixes https://github.com/Microsoft/vscode/issues/18733 addClass(tab, 'dragged'); @@ -1010,7 +1025,7 @@ export class TabsTitleControl extends TitleControl { private redrawTab(editor: IEditorInput, index: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel): void { // Label - this.redrawLabel(editor, tabContainer, tabLabelWidget, tabLabel); + this.redrawLabel(editor, index, tabContainer, tabLabelWidget, tabLabel); // Borders / Outline const borderRightColor = (this.getColor(TAB_BORDER) || this.getColor(contrastBorder)); @@ -1018,10 +1033,12 @@ export class TabsTitleControl extends TitleControl { tabContainer.style.outlineColor = this.getColor(activeContrastBorder) || ''; // Settings + const isTabSticky = this.group.isSticky(index); const options = this.accessor.partOptions; + const tabCloseButton = isTabSticky ? 'off' /* treat sticky tabs as tabCloseButton: 'off' */ : options.tabCloseButton; ['off', 'left', 'right'].forEach(option => { - const domAction = options.tabCloseButton === option ? addClass : removeClass; + const domAction = tabCloseButton === option ? addClass : removeClass; domAction(tabContainer, `close-button-${option}`); }); @@ -1036,13 +1053,37 @@ export class TabsTitleControl extends TitleControl { removeClass(tabContainer, 'has-icon-theme'); } + // Sticky Tabs need a position to remain at their location + // when scrolling to stay in view (requirement for position: sticky) + if (isTabSticky) { + addClass(tabContainer, 'sticky'); + tabContainer.style.left = `${index * TabsTitleControl.TAB_SIZES.sticky}px`; + } else { + removeClass(tabContainer, 'sticky'); + tabContainer.style.left = 'auto'; + } + // Active / dirty state this.redrawEditorActiveAndDirty(this.accessor.activeGroup === this.group, editor, tabContainer, tabLabelWidget); } - private redrawLabel(editor: IEditorInput, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel): void { - const name = tabLabel.name; - const description = tabLabel.description || ''; + private redrawLabel(editor: IEditorInput, index: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel): void { + const isTabSticky = this.group.isSticky(index); + + // Unless tabs are sticky, show the full label and description + // Sticky tabs will only show an icon if icons are enabled + // or their first character of the name otherwise + let name: string | undefined; + let description: string; + if (isTabSticky) { + const isShowingIcons = this.accessor.partOptions.showIcons && !!this.accessor.partOptions.iconTheme; + name = isShowingIcons ? '' : tabLabel.name?.charAt(0).toUpperCase(); + description = ''; + } else { + name = tabLabel.name; + description = tabLabel.description || ''; + } + const title = tabLabel.title || ''; if (tabLabel.ariaLabel) { @@ -1056,7 +1097,7 @@ export class TabsTitleControl extends TitleControl { // Label tabLabelWidget.setResource( { name, description, resource: toResource(editor, { supportSideBySide: SideBySideEditor.BOTH }) }, - { title, extraClasses: ['tab-label'], italic: !this.group.isPinned(editor) } + { title, extraClasses: ['tab-label'], italic: !this.group.isPinned(editor), forceLabel: isTabSticky } ); this.setEditorTabColor(editor, tabContainer, this.group.isActive(editor)); // {{SQL CARBON EDIT}} -- Display the editor's tab color @@ -1173,8 +1214,8 @@ export class TabsTitleControl extends TitleControl { layout(dimension: Dimension | undefined): void { this.dimension = dimension; - const activeTab = this.group.activeEditor ? this.getTab(this.group.activeEditor) : undefined; - if (!activeTab || !this.dimension) { + const activeTabAndIndex = this.group.activeEditor ? this.getTabAndIndex(this.group.activeEditor) : undefined; + if (!activeTabAndIndex || !this.dimension) { return; } @@ -1192,20 +1233,66 @@ export class TabsTitleControl extends TitleControl { } private doLayout(dimension: Dimension): void { - const activeTab = this.group.activeEditor ? this.getTab(this.group.activeEditor) : undefined; - if (!activeTab) { - return; + const activeTabAndIndex = this.group.activeEditor ? this.getTabAndIndex(this.group.activeEditor) : undefined; + if (!activeTabAndIndex) { + return; // nothing to do if not editor opened } - const [tabsContainer, tabsScrollbar] = assertAllDefined(this.tabsContainer, this.tabsScrollbar); + // Breadcrumbs + this.doLayoutBreadcrumbs(dimension); + // Tabs + const [activeTab, activeIndex] = activeTabAndIndex; + this.doLayoutTabs(activeTab, activeIndex); + } + + private doLayoutBreadcrumbs(dimension: Dimension): void { if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) { + const tabsScrollbar = assertIsDefined(this.tabsScrollbar); + this.breadcrumbsControl.layout({ width: dimension.width, height: BreadcrumbsControl.HEIGHT }); tabsScrollbar.getDomNode().style.height = `${dimension.height - BreadcrumbsControl.HEIGHT}px`; } + } - const visibleContainerWidth = tabsContainer.offsetWidth; - const totalContainerWidth = tabsContainer.scrollWidth; + private doLayoutTabs(activeTab: HTMLElement, activeIndex: number): void { + const [tabsContainer, tabsScrollbar] = assertAllDefined(this.tabsContainer, this.tabsScrollbar); + + // + // Synopsis + // - allTabsWidth: sum of all tab widths + // - stickyTabsWidth: sum of all sticky tab widths + // - visibleContainerWidth: size of tab container + // - availableContainerWidth: size of tab container minus size of sticky tabs + // + // [------------------------------ All tabs width ---------------------------------------] + // [------------------- Visible container width -------------------] + // [------ Available container width ------] + // [ Sticky A ][ Sticky B ][ Tab C ][ Tab D ][ Tab E ][ Tab F ][ Tab G ][ Tab H ][ Tab I ] + // Active Tab Width [-------] + // [------- Active Tab Pos X -------] + // [-- Sticky Tabs Width --] + // + + const visibleTabsContainerWidth = tabsContainer.offsetWidth; + const allTabsWidth = tabsContainer.scrollWidth; + + let stickyTabsWidth = this.group.stickyCount * TabsTitleControl.TAB_SIZES.sticky; + let activeTabSticky = this.group.isSticky(activeIndex); + let availableTabsContainerWidth = visibleTabsContainerWidth - stickyTabsWidth; + + // Special case: we have sticky tabs but the available space for showing tabs + // is little enough that we need to disable sticky tabs sticky positioning + // so that tabs can be scrolled at naturally. + if (this.group.stickyCount > 0 && availableTabsContainerWidth < TabsTitleControl.TAB_SIZES.fit) { + addClass(tabsContainer, 'disable-sticky-tabs'); + + availableTabsContainerWidth = visibleTabsContainerWidth; + stickyTabsWidth = 0; + activeTabSticky = false; + } else { + removeClass(tabsContainer, 'disable-sticky-tabs'); + } let activeTabPosX: number | undefined; let activeTabWidth: number | undefined; @@ -1217,42 +1304,78 @@ export class TabsTitleControl extends TitleControl { // Update scrollbar tabsScrollbar.setScrollDimensions({ - width: visibleContainerWidth, - scrollWidth: totalContainerWidth + width: visibleTabsContainerWidth, + scrollWidth: allTabsWidth }); // Return now if we are blocked to reveal the active tab and clear flag - if (this.blockRevealActiveTab || typeof activeTabPosX !== 'number' || typeof activeTabWidth !== 'number') { + // We also return if the active tab is sticky because this means it is + // always visible anyway. + if (this.blockRevealActiveTab || typeof activeTabPosX !== 'number' || typeof activeTabWidth !== 'number' || activeTabSticky) { this.blockRevealActiveTab = false; return; } // Reveal the active one - const containerScrollPosX = tabsScrollbar.getScrollPosition().scrollLeft; - const activeTabFits = activeTabWidth <= visibleContainerWidth; + const tabsContainerScrollPosX = tabsScrollbar.getScrollPosition().scrollLeft; + const activeTabFits = activeTabWidth <= availableTabsContainerWidth; + const adjustedActiveTabPosX = activeTabPosX - stickyTabsWidth; + // + // Synopsis + // - adjustedActiveTabPosX: the adjusted tabPosX takes the width of sticky tabs into account + // conceptually the scrolling only begins after sticky tabs so in order to reveal a tab fully + // the actual position needs to be adjusted for sticky tabs. + // // Tab is overflowing to the right: Scroll minimally until the element is fully visible to the right // Note: only try to do this if we actually have enough width to give to show the tab fully! - if (activeTabFits && containerScrollPosX + visibleContainerWidth < activeTabPosX + activeTabWidth) { + // + // Example: Tab G should be made active and needs to be fully revealed as such. + // + // [-------------------------------- All tabs width -----------------------------------------] + // [-------------------- Visible container width --------------------] + // [----- Available container width -------] + // [ Sticky A ][ Sticky B ][ Tab C ][ Tab D ][ Tab E ][ Tab F ][ Tab G ][ Tab H ][ Tab I ] + // Active Tab Width [-------] + // [------- Active Tab Pos X -------] + // [-------- Adjusted Tab Pos X -------] + // [-- Sticky Tabs Width --] + // + // + if (activeTabFits && tabsContainerScrollPosX + availableTabsContainerWidth < adjustedActiveTabPosX + activeTabWidth) { tabsScrollbar.setScrollPosition({ - scrollLeft: containerScrollPosX + ((activeTabPosX + activeTabWidth) /* right corner of tab */ - (containerScrollPosX + visibleContainerWidth) /* right corner of view port */) + scrollLeft: tabsContainerScrollPosX + ((adjustedActiveTabPosX + activeTabWidth) /* right corner of tab */ - (tabsContainerScrollPosX + availableTabsContainerWidth) /* right corner of view port */) }); } - // Tab is overlflowng to the left or does not fit: Scroll it into view to the left - else if (containerScrollPosX > activeTabPosX || !activeTabFits) { + // + // Tab is overlflowing to the left or does not fit: Scroll it into view to the left + // + // Example: Tab C should be made active and needs to be fully revealed as such. + // + // [----------------------------- All tabs width ----------------------------------------] + // [------------------ Visible container width ------------------] + // [----- Available container width -------] + // [ Sticky A ][ Sticky B ][ Tab C ][ Tab D ][ Tab E ][ Tab F ][ Tab G ][ Tab H ][ Tab I ] + // Active Tab Width [-------] + // [------- Active Tab Pos X -------] + // Adjusted Tab Pos X [] + // [-- Sticky Tabs Width --] + // + // + else if (tabsContainerScrollPosX > adjustedActiveTabPosX || !activeTabFits) { tabsScrollbar.setScrollPosition({ - scrollLeft: activeTabPosX + scrollLeft: adjustedActiveTabPosX }); } } - private getTab(editor: IEditorInput): HTMLElement | undefined { + private getTabAndIndex(editor: IEditorInput): [HTMLElement, number /* index */] | undefined { const editorIndex = this.group.getIndexOfEditor(editor); if (editorIndex >= 0) { const tabsContainer = assertIsDefined(this.tabsContainer); - return tabsContainer.children[editorIndex] as HTMLElement; + return [tabsContainer.children[editorIndex] as HTMLElement, editorIndex]; } return undefined; @@ -1494,11 +1617,11 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = // Adjust gradient for focused and unfocused hover background const makeTabHoverBackgroundRule = (color: Color, colorDrag: Color, hasFocus = false) => ` - .monaco-workbench .part.editor > .content:not(.dragged-over) .editor-group-container${hasFocus ? '.active' : ''} > .title .tabs-container > .tab.sizing-shrink:not(.dragged):hover > .tab-label::after { + .monaco-workbench .part.editor > .content:not(.dragged-over) .editor-group-container${hasFocus ? '.active' : ''} > .title .tabs-container > .tab.sizing-shrink:not(.dragged):not(.sticky):hover > .tab-label::after { background: linear-gradient(to left, ${color}, transparent) !important; } - .monaco-workbench .part.editor > .content.dragged-over .editor-group-container${hasFocus ? '.active' : ''} > .title .tabs-container > .tab.sizing-shrink:not(.dragged):hover > .tab-label::after { + .monaco-workbench .part.editor > .content.dragged-over .editor-group-container${hasFocus ? '.active' : ''} > .title .tabs-container > .tab.sizing-shrink:not(.dragged):not(.sticky):hover > .tab-label::after { background: linear-gradient(to left, ${colorDrag}, transparent) !important; } `; @@ -1521,19 +1644,19 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = if (editorDragAndDropBackground && adjustedTabDragBackground) { const adjustedColorDrag = editorDragAndDropBackground.flatten(adjustedTabDragBackground); collector.addRule(` - .monaco-workbench .part.editor > .content.dragged-over .editor-group-container.active > .title .tabs-container > .tab.sizing-shrink.dragged-over:not(.active):not(.dragged) > .tab-label::after, - .monaco-workbench .part.editor > .content.dragged-over .editor-group-container:not(.active) > .title .tabs-container > .tab.sizing-shrink.dragged-over:not(.dragged) > .tab-label::after { + .monaco-workbench .part.editor > .content.dragged-over .editor-group-container.active > .title .tabs-container > .tab.sizing-shrink.dragged-over:not(.active):not(.dragged):not(.sticky) > .tab-label::after, + .monaco-workbench .part.editor > .content.dragged-over .editor-group-container:not(.active) > .title .tabs-container > .tab.sizing-shrink.dragged-over:not(.dragged):not(.sticky) > .tab-label::after { background: linear-gradient(to left, ${adjustedColorDrag}, transparent) !important; } `); } const makeTabBackgroundRule = (color: Color, colorDrag: Color, focused: boolean, active: boolean) => ` - .monaco-workbench .part.editor > .content:not(.dragged-over) .editor-group-container${focused ? '.active' : ':not(.active)'} > .title .tabs-container > .tab.sizing-shrink${active ? '.active' : ''}:not(.dragged) > .tab-label::after { + .monaco-workbench .part.editor > .content:not(.dragged-over) .editor-group-container${focused ? '.active' : ':not(.active)'} > .title .tabs-container > .tab.sizing-shrink${active ? '.active' : ''}:not(.dragged):not(.sticky) > .tab-label::after { background: linear-gradient(to left, ${color}, transparent); } - .monaco-workbench .part.editor > .content.dragged-over .editor-group-container${focused ? '.active' : ':not(.active)'} > .title .tabs-container > .tab.sizing-shrink${active ? '.active' : ''}:not(.dragged) > .tab-label::after { + .monaco-workbench .part.editor > .content.dragged-over .editor-group-container${focused ? '.active' : ':not(.active)'} > .title .tabs-container > .tab.sizing-shrink${active ? '.active' : ''}:not(.dragged):not(.sticky) > .tab-label::after { background: linear-gradient(to left, ${colorDrag}, transparent); } `; diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index b44ecbd5bc..4c554db927 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -181,7 +181,12 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan // because we are triggering another openEditor() call // and do not control the initial intent that resulted // in us now opening as binary. - const preservingOptions: IEditorOptions = { activation: EditorActivation.PRESERVE, pinned: this.group?.isPinned(input) }; + const preservingOptions: IEditorOptions = { + activation: EditorActivation.PRESERVE, + pinned: this.group?.isPinned(input), + sticky: this.group?.isSticky(input) + }; + if (options) { options.overwrite(preservingOptions); } else { @@ -237,7 +242,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan if (isArray(error)) { const errors = error; - return errors.some(e => this.isFileBinaryError(e)); + return errors.some(error => this.isFileBinaryError(error)); } return (error).textFileOperationResult === TextFileOperationResult.FILE_IS_BINARY; diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index 4a74150f1e..38c91390b1 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./media/titlecontrol'; import { applyDragImage, DataTransfers } from 'vs/base/browser/dnd'; import { addDisposableListener, Dimension, EventType } from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; @@ -12,8 +13,7 @@ import { IAction, IRunEvent, WorkbenchActionExecutedEvent, WorkbenchActionExecut import * as arrays from 'vs/base/common/arrays'; import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; import { dispose, DisposableStore } from 'vs/base/common/lifecycle'; -import 'vs/css!./media/titlecontrol'; -import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { getCodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { localize } from 'vs/nls'; import { createActionViewItem, createAndFillInActionBarActions, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { ExecuteCommandAction, IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions'; @@ -32,13 +32,14 @@ import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs'; import { BreadcrumbsControl, IBreadcrumbsControlOptions } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; import { EDITOR_TITLE_HEIGHT, IEditorGroupsAccessor, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; -import { EditorCommandsContextActionRunner, IEditorCommandsContext, IEditorInput, toResource, IEditorPartOptions, SideBySideEditor, EditorPinnedContext } from 'vs/workbench/common/editor'; +import { EditorCommandsContextActionRunner, IEditorCommandsContext, IEditorInput, toResource, IEditorPartOptions, SideBySideEditor, EditorPinnedContext, EditorStickyContext } from 'vs/workbench/common/editor'; import { ResourceContextKey } from 'vs/workbench/common/resources'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { IFileService } from 'vs/platform/files/common/files'; import { withNullAsUndefined, withUndefinedAsNull, assertIsDefined } from 'vs/base/common/types'; import { isFirefox } from 'vs/base/browser/browser'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; export interface IToolbarActions { primary: IAction[]; @@ -59,6 +60,7 @@ export abstract class TitleControl extends Themable { private resourceContext: ResourceContextKey; private editorPinnedContext: IContextKey; + private editorStickyContext: IContextKey; private readonly editorToolBarMenuDisposables = this._register(new DisposableStore()); @@ -86,6 +88,7 @@ export abstract class TitleControl extends Themable { this.resourceContext = this._register(instantiationService.createInstance(ResourceContextKey)); this.editorPinnedContext = EditorPinnedContext.bindTo(contextKeyService); + this.editorStickyContext = EditorStickyContext.bindTo(contextKeyService); this.contextMenu = this._register(this.menuService.createMenu(MenuId.EditorTitleContext, this.contextKeyService)); @@ -222,6 +225,7 @@ export abstract class TitleControl extends Themable { // Update contexts this.resourceContext.set(this.group.activeEditor ? withUndefinedAsNull(toResource(this.group.activeEditor, { supportSideBySide: SideBySideEditor.MASTER })) : null); this.editorPinnedContext.set(this.group.activeEditor ? this.group.isPinned(this.group.activeEditor) : false); + this.editorStickyContext.set(this.group.activeEditor ? this.group.isSticky(this.group.activeEditor) : false); // Editor actions require the editor control to be there, so we retrieve it via service const activeEditorPane = this.group.activeEditorPane; @@ -266,10 +270,8 @@ export abstract class TitleControl extends Themable { // If tabs are disabled, treat dragging as if an editor tab was dragged let hasDataTransfer = false; if (!this.accessor.partOptions.showTabs) { - const resource = this.group.activeEditor ? toResource(this.group.activeEditor, { supportSideBySide: SideBySideEditor.MASTER }) : null; - if (resource) { - this.instantiationService.invokeFunction(fillResourceDataTransfers, [resource], e); - hasDataTransfer = true; + if (this.group.activeEditor) { + hasDataTransfer = this.doFillResourceDataTransfers(this.group.activeEditor, e); } } @@ -295,6 +297,31 @@ export abstract class TitleControl extends Themable { })); } + protected doFillResourceDataTransfers(editor: IEditorInput, e: DragEvent): boolean { + const resource = toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }); + if (!resource) { + return false; + } + + const editorOptions: ITextEditorOptions = { + viewState: (() => { + if (this.group.activeEditor === editor) { + const activeControl = this.group.activeEditorPane?.getControl(); + if (isCodeEditor(activeControl)) { + return withNullAsUndefined(activeControl.saveViewState()); + } + } + + return undefined; + })(), + sticky: this.group.isSticky(editor) + }; + + this.instantiationService.invokeFunction(fillResourceDataTransfers, [resource], () => editorOptions, e); + + return true; + } + protected onContextMenu(editor: IEditorInput, e: Event, node: HTMLElement): void { // Update contexts based on editor picked and remember previous to restore @@ -302,6 +329,8 @@ export abstract class TitleControl extends Themable { this.resourceContext.set(withUndefinedAsNull(toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }))); const currentPinnedContext = !!this.editorPinnedContext.get(); this.editorPinnedContext.set(this.group.isPinned(editor)); + const currentStickyContext = !!this.editorStickyContext.get(); + this.editorStickyContext.set(this.group.isSticky(editor)); // Find target anchor let anchor: HTMLElement | { x: number, y: number } = node; @@ -325,6 +354,7 @@ export abstract class TitleControl extends Themable { // restore previous contexts this.resourceContext.set(currentResourceContext || null); this.editorPinnedContext.set(currentPinnedContext); + this.editorStickyContext.set(currentStickyContext); // restore focus to active group this.accessor.activeGroup.focus(); @@ -351,12 +381,14 @@ export abstract class TitleControl extends Themable { abstract closeEditors(editors: IEditorInput[]): void; - abstract closeAllEditors(): void; - abstract moveEditor(editor: IEditorInput, fromIndex: number, targetIndex: number): void; abstract pinEditor(editor: IEditorInput): void; + abstract stickEditor(editor: IEditorInput): void; + + abstract unstickEditor(editor: IEditorInput): void; + abstract setActive(isActive: boolean): void; abstract updateEditorLabel(editor: IEditorInput): void; diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index 4d80bd76d8..329f21a63d 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -207,7 +207,8 @@ export class PanelPart extends CompositePart implements IPanelService { } if (isActive) { - if (!activePanel) { + // Only try to open the panel if it has been created and visible + if (!activePanel && this.element && this.layoutService.isVisible(Parts.PANEL_PART)) { this.doOpenPanel(panel.id); } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index ed63e150e6..c7dfb191a0 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -22,7 +22,7 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/ import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { isThemeColor } from 'vs/editor/common/editorCommon'; import { Color } from 'vs/base/common/color'; -import { addClass, EventHelper, createStyleSheet, addDisposableListener, addClasses, removeClass, EventType, hide, show, removeClasses } from 'vs/base/browser/dom'; +import { addClass, EventHelper, createStyleSheet, addDisposableListener, addClasses, removeClass, EventType, hide, show, removeClasses, isAncestor } from 'vs/base/browser/dom'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage'; import { Parts, IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; @@ -35,6 +35,11 @@ import { assertIsDefined } from 'vs/base/common/types'; import { Emitter } from 'vs/base/common/event'; import { Command } from 'vs/editor/common/modes'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; interface IPendingStatusbarEntry { id: string; @@ -51,8 +56,11 @@ interface IStatusbarViewModelEntry { alignment: StatusbarAlignment; priority: number; container: HTMLElement; + labelContainer: HTMLElement; } +const CONTEXT_STATUS_BAR_FOCUSED = new RawContextKey('statusBarFocused', false); + class StatusbarViewModel extends Disposable { static readonly HIDDEN_ENTRIES_KEY = 'workbench.statusbar.hidden'; @@ -188,6 +196,40 @@ class StatusbarViewModel extends Disposable { return this._entries.filter(entry => entry.alignment === alignment); } + focusNextEntry(): void { + this.focusEntry(+1, 0); + } + + focusPreviousEntry(): void { + this.focusEntry(-1, this.entries.length - 1); + } + + private focusEntry(delta: number, restartPosition: number): void { + const getVisibleEntry = (start: number) => { + let indexToFocus = start; + let entry = (indexToFocus >= 0 && indexToFocus < this._entries.length) ? this._entries[indexToFocus] : undefined; + while (entry && this.isHidden(entry.id)) { + indexToFocus += delta; + entry = (indexToFocus >= 0 && indexToFocus < this._entries.length) ? this._entries[indexToFocus] : undefined; + } + return entry; + }; + + const focused = this._entries.find(entry => isAncestor(document.activeElement, entry.container)); + if (focused) { + const entry = getVisibleEntry(this._entries.indexOf(focused) + delta); + if (entry) { + entry.labelContainer.focus(); + return; + } + } + + const entry = getVisibleEntry(restartPosition); + if (entry) { + entry.labelContainer.focus(); + } + } + private updateVisibility(id: string, trigger: boolean): void; private updateVisibility(entry: IStatusbarViewModelEntry, trigger: boolean): void; private updateVisibility(arg1: string | IStatusbarViewModelEntry, trigger: boolean): void { @@ -355,6 +397,7 @@ export class StatusbarPart extends Part implements IStatusbarService { @IStorageService private readonly storageService: IStorageService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @IContextMenuService private contextMenuService: IContextMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { super(Parts.STATUSBAR_PART, { hasTitle: false }, themeService, storageService, layoutService); @@ -415,7 +458,7 @@ export class StatusbarPart extends Part implements IStatusbarService { this.appendOneStatusbarEntry(itemContainer, alignment, priority); // Add to view model - const viewModelEntry: IStatusbarViewModelEntry = { id, name, alignment, priority, container: itemContainer }; + const viewModelEntry: IStatusbarViewModelEntry = { id, name, alignment, priority, container: itemContainer, labelContainer: item.labelContainer }; const viewModelEntryDispose = this.viewModel.add(viewModelEntry); return { @@ -442,9 +485,21 @@ export class StatusbarPart extends Part implements IStatusbarService { } } + focusNextEntry(): void { + this.viewModel.focusNextEntry(); + } + + focusPreviousEntry(): void { + this.viewModel.focusPreviousEntry(); + } + createContentArea(parent: HTMLElement): HTMLElement { this.element = parent; + // Track focus within container + const scopedContextKeyService = this.contextKeyService.createScoped(this.element); + CONTEXT_STATUS_BAR_FOCUSED.bindTo(scopedContextKeyService).set(true); + // Left items container this.leftItemsContainer = document.createElement('div'); addClasses(this.leftItemsContainer, 'left-items', 'items-container'); @@ -645,13 +700,14 @@ class StatusbarEntryItem extends Disposable { private entry!: IStatusbarEntry; - private labelContainer!: HTMLElement; + labelContainer!: HTMLElement; private label!: CodiconLabel; private readonly foregroundListener = this._register(new MutableDisposable()); private readonly backgroundListener = this._register(new MutableDisposable()); - private readonly commandListener = this._register(new MutableDisposable()); + private readonly commandMouseListener = this._register(new MutableDisposable()); + private readonly commandKeyboardListener = this._register(new MutableDisposable()); constructor( private container: HTMLElement, @@ -711,11 +767,18 @@ class StatusbarEntryItem extends Disposable { // Update: Command if (!this.entry || entry.command !== this.entry.command) { - this.commandListener.clear(); + this.commandMouseListener.clear(); + this.commandKeyboardListener.clear(); const command = entry.command; if (command) { - this.commandListener.value = addDisposableListener(this.labelContainer, EventType.CLICK, () => this.executeCommand(command)); + this.commandMouseListener.value = addDisposableListener(this.labelContainer, EventType.CLICK, () => this.executeCommand(command)); + this.commandKeyboardListener.value = addDisposableListener(this.labelContainer, EventType.KEY_UP, e => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) { + this.executeCommand(command); + } + }); removeClass(this.labelContainer, 'disabled'); } else { @@ -814,7 +877,8 @@ class StatusbarEntryItem extends Disposable { dispose(this.foregroundListener); dispose(this.backgroundListener); - dispose(this.commandListener); + dispose(this.commandMouseListener); + dispose(this.commandKeyboardListener); } } @@ -822,6 +886,7 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = const statusBarItemHoverBackground = theme.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND); if (statusBarItemHoverBackground) { collector.addRule(`.monaco-workbench .part.statusbar > .items-container > .statusbar-item a:hover { background-color: ${statusBarItemHoverBackground}; }`); + collector.addRule(`.monaco-workbench .part.statusbar > .items-container > .statusbar-item a:focus { background-color: ${statusBarItemHoverBackground}; }`); } const statusBarItemActiveBackground = theme.getColor(STATUS_BAR_ITEM_ACTIVE_BACKGROUND); @@ -846,3 +911,27 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = }); registerSingleton(IStatusbarService, StatusbarPart); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'workbench.statusBar.focusPrevious', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.LeftArrow, + secondary: [KeyCode.UpArrow], + when: CONTEXT_STATUS_BAR_FOCUSED, + handler: (accessor: ServicesAccessor) => { + const statusBarService = accessor.get(IStatusbarService); + statusBarService.focusPreviousEntry(); + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'workbench.statusBar.focusNext', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.RightArrow, + secondary: [KeyCode.DownArrow], + when: CONTEXT_STATUS_BAR_FOCUSED, + handler: (accessor: ServicesAccessor) => { + const statusBarService = accessor.get(IStatusbarService); + statusBarService.focusNextEntry(); + } +}); diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index fc3c835c4e..a4da520a55 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -934,17 +934,21 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { getContextMenuActions(viewDescriptor?: IViewDescriptor): IAction[] { const result: IAction[] = []; + let showHide = true; if (!viewDescriptor && this.isViewMergedWithContainer()) { viewDescriptor = this.viewDescriptorService.getViewDescriptorById(this.panes[0].id) || undefined; + showHide = false; } if (viewDescriptor) { - result.push({ - id: `${viewDescriptor.id}.removeView`, - label: nls.localize('hideView', "Hide"), - enabled: viewDescriptor.canToggleVisibility, - run: () => this.toggleViewVisibility(viewDescriptor!.id) - }); + if (showHide) { + result.push({ + id: `${viewDescriptor.id}.removeView`, + label: nls.localize('hideView', "Hide"), + enabled: viewDescriptor.canToggleVisibility, + run: () => this.toggleViewVisibility(viewDescriptor!.id) + }); + } const view = this.getView(viewDescriptor.id); if (view) { result.push(...view.getContextMenuActions()); @@ -955,7 +959,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { id: `${viewDescriptor.id}.toggleVisibility`, label: viewDescriptor.name, checked: this.viewContainerModel.isVisible(viewDescriptor.id), - enabled: viewDescriptor.canToggleVisibility, + enabled: viewDescriptor.canToggleVisibility && (!this.viewContainerModel.isVisible(viewDescriptor.id) || this.viewContainerModel.visibleViewDescriptors.length > 1), run: () => this.toggleViewVisibility(viewDescriptor.id) })); diff --git a/src/vs/workbench/browser/parts/views/viewsService.ts b/src/vs/workbench/browser/parts/views/viewsService.ts index 1745e10c6d..591346aa9d 100644 --- a/src/vs/workbench/browser/parts/views/viewsService.ts +++ b/src/vs/workbench/browser/parts/views/viewsService.ts @@ -127,15 +127,10 @@ export class ViewsService extends Disposable implements IViewsService { this.deregisterViewletOrPanel(container, location); } for (const { container, location } of added) { - this.registerViewletOrPanel(container, location); + this.onDidRegisterViewContainer(container, location); } } - private onDidChangeContainerLocation(viewContainer: ViewContainer, from: ViewContainerLocation, to: ViewContainerLocation): void { - this.deregisterViewletOrPanel(viewContainer, from); - this.registerViewletOrPanel(viewContainer, to); - } - private onDidRegisterViewContainer(viewContainer: ViewContainer, viewContainerLocation: ViewContainerLocation): void { this.registerViewletOrPanel(viewContainer, viewContainerLocation); const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); @@ -146,6 +141,11 @@ export class ViewsService extends Disposable implements IViewsService { })); } + private onDidChangeContainerLocation(viewContainer: ViewContainer, from: ViewContainerLocation, to: ViewContainerLocation): void { + this.deregisterViewletOrPanel(viewContainer, from); + this.registerViewletOrPanel(viewContainer, to); + } + private onViewDescriptorsAdded(views: ReadonlyArray, container: ViewContainer): void { const location = this.viewDescriptorService.getViewContainerLocation(container); if (location === null) { diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 93d647fb34..7ef541546c 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -71,7 +71,7 @@ class BrowserMain extends Disposable { services.storageService.store(Settings.WORKSPACE_FIRST_OPEN, !(firstOpen ?? false), StorageScope.WORKSPACE); } - { await domContentLoaded(); } + await domContentLoaded(); mark('willStartWorkbench'); // Base Theme @@ -272,7 +272,7 @@ class BrowserMain extends Disposable { if (!this.configuration.userDataProvider) { const remoteUserDataUri = this.getRemoteUserDataUri(); if (remoteUserDataUri) { - this.configuration.userDataProvider = this._register(new FileUserDataProvider(remoteUserDataUri, joinPath(remoteUserDataUri, BACKUPS), remoteFileSystemProvider, environmentService)); + this.configuration.userDataProvider = this._register(new FileUserDataProvider(remoteUserDataUri, joinPath(remoteUserDataUri, BACKUPS), remoteFileSystemProvider, environmentService, logService)); } } } diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 612469dcfa..07605aae37 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -108,12 +108,12 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio }, 'workbench.editor.enablePreview': { 'type': 'boolean', - 'description': nls.localize('enablePreview', "Controls whether opened editors show as preview. Preview editors are reused until they are pinned (e.g. via double click or editing) and show up with an italic font style."), + 'description': nls.localize('enablePreview', "Controls whether opened editors show as preview. Preview editors are reused until they are explicitly set to be kept open (e.g. via double click or editing) and show up with an italic font style."), 'default': true }, 'workbench.editor.enablePreviewFromQuickOpen': { 'type': 'boolean', - 'description': nls.localize('enablePreviewFromQuickOpen', "Controls whether editors opened from Quick Open show as preview. Preview editors are reused until they are pinned (e.g. via double click or editing)."), + 'description': nls.localize('enablePreviewFromQuickOpen', "Controls whether editors opened from Quick Open show as preview. Preview editors are reused until they are explicitly set to be kept open (e.g. via double click or editing)."), 'default': true }, 'workbench.editor.closeOnFileDelete': { diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 88746d0046..d91b4c626d 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -35,6 +35,7 @@ export const ActiveEditorIsReadonlyContext = new RawContextKey('activeE export const ActiveEditorAvailableEditorIdsContext = new RawContextKey('activeEditorAvailableEditorIds', ''); export const EditorsVisibleContext = new RawContextKey('editorIsOpen', false); export const EditorPinnedContext = new RawContextKey('editorPinned', false); +export const EditorStickyContext = new RawContextKey('editorSticky', false); export const EditorGroupActiveEditorDirtyContext = new RawContextKey('groupActiveEditorDirty', false); export const EditorGroupEditorsCountContext = new RawContextKey('groupEditorsCount', 0); export const NoEditorsVisibleContext = EditorsVisibleContext.toNegated(); @@ -1046,6 +1047,12 @@ export class EditorOptions implements IEditorOptions { */ pinned: boolean | undefined; + /** + * An editor that is sticky moves to the beginning of the editors list within the group and will remain + * there unless explicitly closed. Operations such as "Close All" will not close sticky editors. + */ + sticky: boolean | undefined; + /** * The index in the document stack where to insert the editor into when opening. */ @@ -1111,6 +1118,10 @@ export class EditorOptions implements IEditorOptions { this.pinned = options.pinned; } + if (typeof options.sticky === 'boolean') { + this.sticky = options.sticky; + } + if (typeof options.inactive === 'boolean') { this.inactive = options.inactive; } @@ -1291,6 +1302,7 @@ export class EditorCommandsContextActionRunner extends ActionRunner { export interface IEditorCloseEvent extends IEditorIdentifier { replaced: boolean; index: number; + sticky: boolean; } export type GroupIdentifier = number; diff --git a/src/vs/workbench/common/editor/editorGroup.ts b/src/vs/workbench/common/editor/editorGroup.ts index faa94f777b..3982bb00a7 100644 --- a/src/vs/workbench/common/editor/editorGroup.ts +++ b/src/vs/workbench/common/editor/editorGroup.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Emitter } from 'vs/base/common/event'; -import { Extensions, IEditorInputFactoryRegistry, EditorInput, IEditorIdentifier, IEditorCloseEvent, GroupIdentifier, CloseDirection, SideBySideEditorInput, IEditorInput, EditorsOrder } from 'vs/workbench/common/editor'; +import { Extensions, IEditorInputFactoryRegistry, EditorInput, IEditorIdentifier, IEditorCloseEvent, GroupIdentifier, SideBySideEditorInput, IEditorInput, EditorsOrder } from 'vs/workbench/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { coalesce } from 'vs/base/common/arrays'; @@ -30,6 +30,7 @@ export interface EditorIdentifier extends IEditorIdentifier { export interface IEditorOpenOptions { pinned?: boolean; + sticky?: boolean; active?: boolean; index?: number; } @@ -44,6 +45,7 @@ export interface ISerializedEditorGroup { editors: ISerializedEditorInput[]; mru: number[]; preview?: number; + sticky?: number; } export function isSerializedEditorGroup(obj?: unknown): obj is ISerializedEditorGroup { @@ -92,6 +94,7 @@ export class EditorGroup extends Disposable { private preview: EditorInput | null = null; // editor in preview state private active: EditorInput | null = null; // editor in active state + private sticky: number = -1; // index of first editor in sticky state private editorOpenPositioning: ('left' | 'right' | 'first' | 'last') | undefined; private focusRecentEditorAfterClose: boolean | undefined; @@ -114,10 +117,10 @@ export class EditorGroup extends Disposable { } private registerListeners(): void { - this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e))); + this._register(this.configurationService.onDidChangeConfiguration(() => this.onConfigurationUpdated())); } - private onConfigurationUpdated(event?: IConfigurationChangeEvent): void { + private onConfigurationUpdated(): void { this.editorOpenPositioning = this.configurationService.getValue('workbench.editor.openPositioning'); this.focusRecentEditorAfterClose = this.configurationService.getValue('workbench.editor.focusRecentEditorAfterClose'); } @@ -126,8 +129,25 @@ export class EditorGroup extends Disposable { return this.editors.length; } - getEditors(order: EditorsOrder): EditorInput[] { - return order === EditorsOrder.MOST_RECENTLY_ACTIVE ? this.mru.slice(0) : this.editors.slice(0); + get stickyCount(): number { + return this.sticky + 1; + } + + getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): EditorInput[] { + const editors = order === EditorsOrder.MOST_RECENTLY_ACTIVE ? this.mru.slice(0) : this.editors.slice(0); + + if (options?.excludeSticky) { + + // MRU: need to check for index on each + if (order === EditorsOrder.MOST_RECENTLY_ACTIVE) { + return editors.filter(editor => !this.isSticky(editor)); + } + + // Sequential: simply start after sticky index + return editors.slice(this.sticky + 1); + } + + return editors; } getEditorByIndex(index: number): EditorInput | undefined { @@ -146,18 +166,15 @@ export class EditorGroup extends Disposable { return this.preview; } - isPreview(editor: EditorInput): boolean { - return this.matches(this.preview, editor); - } - openEditor(candidate: EditorInput, options?: IEditorOpenOptions): EditorInput { - const makePinned = options?.pinned; + const makeSticky = options?.sticky || (typeof options?.index === 'number' && this.isSticky(options.index)); + const makePinned = options?.pinned || options?.sticky; const makeActive = options?.active || !this.activeEditor || (!makePinned && this.matches(this.preview, this.activeEditor)); - const existingEditor = this.findEditor(candidate); + const existingEditorAndIndex = this.findEditor(candidate); // New editor - if (!existingEditor) { + if (!existingEditorAndIndex) { const newEditor = candidate; const indexOfActive = this.indexOf(this.active); @@ -170,6 +187,12 @@ export class EditorGroup extends Disposable { // Insert to the BEGINNING else if (this.editorOpenPositioning === EditorOpenPositioning.FIRST) { targetIndex = 0; + + // Always make sure targetIndex is after sticky editors + // unless we are explicitly told to make the editor sticky + if (!makeSticky && this.isSticky(targetIndex)) { + targetIndex = this.sticky + 1; + } } // Insert to the END @@ -177,18 +200,38 @@ export class EditorGroup extends Disposable { targetIndex = this.editors.length; } - // Insert to the LEFT of active editor - else if (this.editorOpenPositioning === EditorOpenPositioning.LEFT) { - if (indexOfActive === 0 || !this.editors.length) { - targetIndex = 0; // to the left becoming first editor in list - } else { - targetIndex = indexOfActive; // to the left of active editor + // Insert to LEFT or RIGHT of active editor + else { + + // Insert to the LEFT of active editor + if (this.editorOpenPositioning === EditorOpenPositioning.LEFT) { + if (indexOfActive === 0 || !this.editors.length) { + targetIndex = 0; // to the left becoming first editor in list + } else { + targetIndex = indexOfActive; // to the left of active editor + } + } + + // Insert to the RIGHT of active editor + else { + targetIndex = indexOfActive + 1; + } + + // Always make sure targetIndex is after sticky editors + // unless we are explicitly told to make the editor sticky + if (!makeSticky && this.isSticky(targetIndex)) { + targetIndex = this.sticky + 1; } } - // Insert to the RIGHT of active editor - else { - targetIndex = indexOfActive + 1; + // If the editor becomes sticky, increment the sticky index and adjust + // the targetIndex to be at the end of sticky editors unless already. + if (makeSticky) { + this.sticky++; + + if (!this.isSticky(targetIndex)) { + targetIndex = this.sticky; + } } // Insert into our list of editors if pinned or we have no preview editor @@ -228,6 +271,7 @@ export class EditorGroup extends Disposable { // Existing editor else { + const [existingEditor] = existingEditorAndIndex; // Pin it if (makePinned) { @@ -244,6 +288,12 @@ export class EditorGroup extends Disposable { this.moveEditor(existingEditor, options.index); } + // Stick it (intentionally after the moveEditor call in case + // the editor was already moved into the sticky range) + if (makeSticky) { + this.doStick(existingEditor, this.indexOf(existingEditor)); + } + return existingEditor; } } @@ -252,8 +302,7 @@ export class EditorGroup extends Disposable { const listeners = new DisposableStore(); // Re-emit disposal of editor input as our own event - const onceDispose = Event.once(editor.onDispose); - listeners.add(onceDispose(() => { + listeners.add(Event.once(editor.onDispose)(() => { if (this.indexOf(editor) >= 0) { this._onDidDisposeEditor.fire(editor); } @@ -309,6 +358,7 @@ export class EditorGroup extends Disposable { } const editor = this.editors[index]; + const sticky = this.isSticky(index); // Active Editor closed if (openNext && this.matches(this.active, editor)) { @@ -344,45 +394,18 @@ export class EditorGroup extends Disposable { this.splice(index, true); // Event - return { editor, replaced, index, groupId: this.id }; - } - - closeEditors(except: EditorInput, direction?: CloseDirection): void { - const index = this.indexOf(except); - if (index === -1) { - return; // not found - } - - // Close to the left - if (direction === CloseDirection.LEFT) { - for (let i = index - 1; i >= 0; i--) { - this.closeEditor(this.editors[i]); - } - } - - // Close to the right - else if (direction === CloseDirection.RIGHT) { - for (let i = this.editors.length - 1; i > index; i--) { - this.closeEditor(this.editors[i]); - } - } - - // Both directions - else { - this.mru.filter(e => !this.matches(e, except)).forEach(e => this.closeEditor(e)); - } - } - - closeAllEditors(): void { - - // Optimize: close all non active editors first to produce less upstream work - this.mru.filter(e => !this.matches(e, this.active)).forEach(e => this.closeEditor(e)); - if (this.active) { - this.closeEditor(this.active); - } + return { editor, replaced, sticky, index, groupId: this.id }; } moveEditor(candidate: EditorInput, toIndex: number): EditorInput | undefined { + + // Ensure toIndex is in bounds of our model + if (toIndex >= this.editors.length) { + toIndex = this.editors.length - 1; + } else if (toIndex < 0) { + toIndex = 0; + } + const index = this.indexOf(candidate); if (index < 0 || toIndex === index) { return undefined; // {{SQL CARBON EDIT}} strict-null-check @@ -390,6 +413,16 @@ export class EditorGroup extends Disposable { const editor = this.editors[index]; + // Adjust sticky index: editor moved out of sticky state into unsticky state + if (this.isSticky(index) && toIndex > this.sticky) { + this.sticky--; + } + + // ...or editor moved into sticky state from unsticky state + else if (!this.isSticky(index) && toIndex <= this.sticky) { + this.sticky++; + } + // Move this.editors.splice(index, 1); this.editors.splice(toIndex, 0, editor); @@ -401,11 +434,13 @@ export class EditorGroup extends Disposable { } setActive(candidate: EditorInput): EditorInput | undefined { - const editor = this.findEditor(candidate); - if (!editor) { + const res = this.findEditor(candidate); + if (!res) { return undefined; // not found {{SQL CARBON EDIT}} strict-null-check } + const [editor] = res; + this.doSetActive(editor); return editor; @@ -428,18 +463,20 @@ export class EditorGroup extends Disposable { } pin(candidate: EditorInput): EditorInput | undefined { - const editor = this.findEditor(candidate); - if (!editor) { + const res = this.findEditor(candidate); + if (!res) { return undefined; // not found {{SQL CARBON EDIT}} strict-null-check } + const [editor] = res; + this.doPin(editor); return editor; } private doPin(editor: EditorInput): void { - if (!this.isPreview(editor)) { + if (this.isPinned(editor)) { return; // can only pin a preview editor } @@ -451,11 +488,13 @@ export class EditorGroup extends Disposable { } unpin(candidate: EditorInput): EditorInput | undefined { - const editor = this.findEditor(candidate); - if (!editor) { + const res = this.findEditor(candidate); + if (!res) { return undefined; // not found {{SQL CARBON EDIT}} strict-null-check } + const [editor] = res; + this.doUnpin(editor); return editor; @@ -479,33 +518,97 @@ export class EditorGroup extends Disposable { } } - isPinned(editor: EditorInput): boolean; - isPinned(index: number): boolean; - isPinned(arg1: EditorInput | number): boolean { - if (!this.preview) { - return true; // no preview editor - } - + isPinned(editorOrIndex: EditorInput | number): boolean { let editor: EditorInput; - let index: number; - if (typeof arg1 === 'number') { - editor = this.editors[arg1]; - index = arg1; + if (typeof editorOrIndex === 'number') { + editor = this.editors[editorOrIndex]; } else { - editor = arg1; - index = this.indexOf(editor); - } - - if (index === -1 || !editor) { - return false; // editor not found + editor = editorOrIndex; } return !this.matches(this.preview, editor); } + stick(candidate: EditorInput): EditorInput | undefined { + const res = this.findEditor(candidate); + if (!res) { + return undefined; // not found {{SQL CARBON EDIT}} strict-null-check + } + + const [editor, index] = res; + + this.doStick(editor, index); + + return editor; + } + + private doStick(editor: EditorInput, index: number): void { + if (this.isSticky(index)) { + return; // can only stick a non-sticky editor + } + + // Pin editor + this.pin(editor); + + // Move editor to be the last sticky editor + this.moveEditor(editor, this.sticky + 1); + + // Adjust sticky index + this.sticky++; + } + + unstick(candidate: EditorInput): EditorInput | undefined { + const res = this.findEditor(candidate); + if (!res) { + return undefined; // not found {{SQL CARBON EDIT}} strict-null-check + } + + const [editor, index] = res; + + this.doUnstick(editor, index); + + return editor; + } + + private doUnstick(editor: EditorInput, index: number): void { + if (!this.isSticky(index)) { + return; // can only unstick a sticky editor + } + + // Move editor to be the first non-sticky editor + this.moveEditor(editor, this.sticky); + + // Adjust sticky index + this.sticky--; + } + + isSticky(candidateOrIndex: EditorInput | number): boolean { + if (this.sticky < 0) { + return false; // no sticky editor + } + + let index: number; + if (typeof candidateOrIndex === 'number') { + index = candidateOrIndex; + } else { + index = this.indexOf(candidateOrIndex); + } + + if (index < 0) { + return false; + } + + return index <= this.sticky; + } + private splice(index: number, del: boolean, editor?: EditorInput): void { const editorToDeleteOrReplace = this.editors[index]; + // Perform on sticky index + if (del && this.isSticky(index)) { + this.sticky--; + } + // Perform on editors array if (editor) { this.editors.splice(index, del ? 1 : 0, editor); @@ -513,35 +616,38 @@ export class EditorGroup extends Disposable { this.editors.splice(index, del ? 1 : 0); } - // Add - if (!del && editor) { - if (this.mru.length === 0) { - // the list of most recent editors is empty - // so this editor can only be the most recent - this.mru.push(editor); - } else { - // we have most recent editors. as such we - // put this newly opened editor right after - // the current most recent one because it cannot - // be the most recently active one unless - // it becomes active. but it is still more - // active then any other editor in the list. - this.mru.splice(1, 0, editor); - } - } - - // Remove / Replace - else { - const indexInMRU = this.indexOf(editorToDeleteOrReplace, this.mru); - - // Remove - if (del && !editor) { - this.mru.splice(indexInMRU, 1); // remove from MRU + // Perform on MRU + { + // Add + if (!del && editor) { + if (this.mru.length === 0) { + // the list of most recent editors is empty + // so this editor can only be the most recent + this.mru.push(editor); + } else { + // we have most recent editors. as such we + // put this newly opened editor right after + // the current most recent one because it cannot + // be the most recently active one unless + // it becomes active. but it is still more + // active then any other editor in the list. + this.mru.splice(1, 0, editor); + } } - // Replace - else if (del && editor) { - this.mru.splice(indexInMRU, 1, editor); // replace MRU at location + // Remove / Replace + else { + const indexInMRU = this.indexOf(editorToDeleteOrReplace, this.mru); + + // Remove + if (del && !editor) { + this.mru.splice(indexInMRU, 1); // remove from MRU + } + + // Replace + else if (del && editor) { + this.mru.splice(indexInMRU, 1, editor); // replace MRU at location + } } } } @@ -560,13 +666,13 @@ export class EditorGroup extends Disposable { return -1; } - private findEditor(candidate: EditorInput | null): EditorInput | undefined { + private findEditor(candidate: EditorInput | null): [EditorInput, number /* index */] | undefined { const index = this.indexOf(candidate, this.editors); if (index === -1) { return undefined; } - return this.editors[index]; + return [this.editors[index], index]; } contains(candidate: EditorInput, searchInSideBySideEditors?: boolean): boolean { @@ -599,7 +705,7 @@ export class EditorGroup extends Disposable { group.mru = this.mru.slice(0); group.preview = this.preview; group.active = this.active; - group.editorOpenPositioning = this.editorOpenPositioning; + group.sticky = this.sticky; return group; } @@ -609,32 +715,52 @@ export class EditorGroup extends Disposable { // Serialize all editor inputs so that we can store them. // Editors that cannot be serialized need to be ignored - // from mru, active and preview if any. + // from mru, active, preview and sticky if any. let serializableEditors: EditorInput[] = []; let serializedEditors: ISerializedEditorInput[] = []; let serializablePreviewIndex: number | undefined; - this.editors.forEach(e => { - const factory = registry.getEditorInputFactory(e.getTypeId()); - if (factory) { - const value = factory.serialize(e); - if (typeof value === 'string') { - serializedEditors.push({ id: e.getTypeId(), value }); - serializableEditors.push(e); + let serializableSticky = this.sticky; - if (this.preview === e) { + for (let i = 0; i < this.editors.length; i++) { + const editor = this.editors[i]; + let canSerializeEditor = false; + + const factory = registry.getEditorInputFactory(editor.getTypeId()); + if (factory) { + const value = factory.serialize(editor); + + // Editor can be serialized + if (typeof value === 'string') { + canSerializeEditor = true; + + serializedEditors.push({ id: editor.getTypeId(), value }); + serializableEditors.push(editor); + + if (this.preview === editor) { serializablePreviewIndex = serializableEditors.length - 1; } } - } - }); - const serializableMru = this.mru.map(e => this.indexOf(e, serializableEditors)).filter(i => i >= 0); + // Editor cannot be serialized + else { + canSerializeEditor = false; + } + } + + // Adjust index of sticky editors if the editor cannot be serialized and is pinned + if (!canSerializeEditor && this.isSticky(i)) { + serializableSticky--; + } + } + + const serializableMru = this.mru.map(editor => this.indexOf(editor, serializableEditors)).filter(i => i >= 0); return { id: this.id, editors: serializedEditors, mru: serializableMru, preview: serializablePreviewIndex, + sticky: serializableSticky >= 0 ? serializableSticky : undefined }; } @@ -649,18 +775,22 @@ export class EditorGroup extends Disposable { this._id = EditorGroup.IDS++; // backwards compatibility } - this.editors = coalesce(data.editors.map(e => { + this.editors = coalesce(data.editors.map((e, index) => { + let editor: EditorInput | undefined = undefined; + const factory = registry.getEditorInputFactory(e.id); if (factory) { - const editor = doHandleUpgrade(factory.deserialize(this.instantiationService, e.value)); // {{SQL CARBON EDIT}} handle upgrade path to new serialization + editor = doHandleUpgrade(factory.deserialize(this.instantiationService, e.value)); // {{SQL CARBON EDIT}} handle upgrade path to new serialization if (editor) { this.registerEditorListeners(editor); } - - return editor; } - return null; + if (!editor && typeof data.sticky === 'number' && index <= data.sticky) { + data.sticky--; // if editor cannot be deserialized but was sticky, we need to decrease sticky index + } + + return editor; })); this.mru = coalesce(data.mru.map(i => this.editors[i])); @@ -671,6 +801,10 @@ export class EditorGroup extends Disposable { this.preview = this.editors[data.preview]; } + if (typeof data.sticky === 'number') { + this.sticky = data.sticky; + } + return this._id; } } diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index dfd947e301..b9cf1c3d63 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -282,7 +282,7 @@ export interface IViewContentDescriptor { export interface IViewsRegistry { - readonly onViewsRegistered: Event<{ views: IViewDescriptor[], viewContainer: ViewContainer }>; + readonly onViewsRegistered: Event<{ views: IViewDescriptor[], viewContainer: ViewContainer }[]>; readonly onViewsDeregistered: Event<{ views: IViewDescriptor[], viewContainer: ViewContainer }>; @@ -290,6 +290,8 @@ export interface IViewsRegistry { registerViews(views: IViewDescriptor[], viewContainer: ViewContainer): void; + registerViews2(views: { views: IViewDescriptor[], viewContainer: ViewContainer }[]): void; + deregisterViews(views: IViewDescriptor[], viewContainer: ViewContainer): void; moveViews(views: IViewDescriptor[], viewContainer: ViewContainer): void; @@ -319,8 +321,8 @@ function compareViewContentDescriptors(a: IViewContentDescriptor, b: IViewConten class ViewsRegistry extends Disposable implements IViewsRegistry { - private readonly _onViewsRegistered: Emitter<{ views: IViewDescriptor[], viewContainer: ViewContainer }> = this._register(new Emitter<{ views: IViewDescriptor[], viewContainer: ViewContainer }>()); - readonly onViewsRegistered: Event<{ views: IViewDescriptor[], viewContainer: ViewContainer }> = this._onViewsRegistered.event; + private readonly _onViewsRegistered = this._register(new Emitter<{ views: IViewDescriptor[], viewContainer: ViewContainer }[]>()); + readonly onViewsRegistered = this._onViewsRegistered.event; private readonly _onViewsDeregistered: Emitter<{ views: IViewDescriptor[], viewContainer: ViewContainer }> = this._register(new Emitter<{ views: IViewDescriptor[], viewContainer: ViewContainer }>()); readonly onViewsDeregistered: Event<{ views: IViewDescriptor[], viewContainer: ViewContainer }> = this._onViewsDeregistered.event; @@ -336,8 +338,12 @@ class ViewsRegistry extends Disposable implements IViewsRegistry { private _viewWelcomeContents = new SetMap(); registerViews(views: IViewDescriptor[], viewContainer: ViewContainer): void { - this.addViews(views, viewContainer); - this._onViewsRegistered.fire({ views: views, viewContainer }); + this.registerViews2([{ views, viewContainer }]); + } + + registerViews2(views: { views: IViewDescriptor[], viewContainer: ViewContainer }[]): void { + views.forEach(({ views, viewContainer }) => this.addViews(views, viewContainer)); + this._onViewsRegistered.fire(views); } deregisterViews(viewDescriptors: IViewDescriptor[], viewContainer: ViewContainer): void { diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index b9d449596e..528d027fae 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -172,6 +172,20 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess updatePickerItems(); disposables.add(picker.onDidChangeValue(updatePickerItems)); + let ignoreFirstActiveEvent = true; + disposables.add(picker.onDidChangeActive(() => { + const [entry] = picker.activeItems; + + if (entry && entries[entry.index]) { + if (ignoreFirstActiveEvent) { + ignoreFirstActiveEvent = false; + return; + } + + entries[entry.index]?.reveal(); + } + })); + }).catch(err => { onUnexpectedError(err); picker.hide(); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 357c839a4e..0202571c2a 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -350,7 +350,7 @@ class BreakpointsRenderer implements IListRendererstackFrame.thread).getStaleCallStack(); + callStack = callStack.length > 0 ? callStack : stackFrame.thread.getCallStack(); + const otherSources = callStack.map(sf => sf.source).filter(s => s !== stackFrame.source); + let suffixLength = 0; + otherSources.forEach(s => { + if (s.name === stackFrame.source.name) { + suffixLength = Math.max(suffixLength, commonSuffixLength(stackFrame.source.uri.path, s.uri.path)); + } + }); + if (suffixLength === 0) { + return stackFrame.source.name; + } + + const from = Math.max(0, stackFrame.source.uri.path.lastIndexOf(posix.sep, stackFrame.source.uri.path.length - suffixLength - 1)); + return (from > 0 ? '...' : '') + stackFrame.source.uri.path.substr(from); +} + export class CallStackView extends ViewPane { private pauseMessage!: HTMLSpanElement; private pauseMessageLabel!: HTMLSpanElement; @@ -453,7 +475,7 @@ class SessionsRenderer implements ITreeRenderer { @@ -562,7 +584,7 @@ class StackFramesRenderer implements ITreeRendererelement).name); } if (element instanceof StackFrame) { - return nls.localize('stackFrameAriaLabel', "Stack Frame {0}, line {1}, {2}, callstack, debug", element.name, element.range.startLineNumber, element.getSpecificSourceName()); + return nls.localize('stackFrameAriaLabel', "Stack Frame {0}, line {1}, {2}, callstack, debug", element.name, element.range.startLineNumber, getSpecificSourceName(element)); } if (isDebugSession(element)) { return nls.localize('sessionLabel', "Debug Session {0}", element.getLabel()); diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 086f205c03..08570508fd 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -229,11 +229,6 @@ configurationRegistry.registerConfiguration({ default: 'openOnSessionStart', description: nls.localize('openDebug', "Controls when the debug view should open.") }, - 'debug.enableAllHovers': { - type: 'boolean', - description: nls.localize({ comment: ['This is the description for a setting'], key: 'enableAllHovers' }, "Controls whether the non-debug hovers should be enabled while debugging. When enabled the hover providers will be called to provide a hover. Regular hovers will not be shown even if this setting is enabled."), - default: false - }, 'debug.showSubSessionsInToolBar': { type: 'boolean', description: nls.localize({ comment: ['This is the description for a setting'], key: 'showSubSessionsInToolBar' }, "Controls whether the debug sub-sessions are shown in the debug tool bar. When this setting is false the stop command on a sub-session will also stop the parent session."), diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 9858d17d70..939b78c497 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -24,7 +24,6 @@ import { InputFocusedContext } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { PanelFocusContext } from 'vs/workbench/common/panel'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { onUnexpectedError } from 'vs/base/common/errors'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -206,7 +205,7 @@ export function registerCommands(): void { weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.F5, when: CONTEXT_IN_DEBUG_MODE, - handler: (accessor: ServicesAccessor, _: string, context: CallStackContext | unknown) => { + handler: async (accessor: ServicesAccessor, _: string, context: CallStackContext | unknown) => { const debugService = accessor.get(IDebugService); let session: IDebugSession | undefined; if (isSessionContext(context)) { @@ -217,10 +216,10 @@ export function registerCommands(): void { if (!session) { const { launch, name } = debugService.getConfigurationManager().selectedConfiguration; - debugService.startDebugging(launch, name, { noDebug: false }); + await debugService.startDebugging(launch, name, { noDebug: false }); } else { session.removeReplExpressions(); - debugService.restartSession(session).then(undefined, onUnexpectedError); + await debugService.restartSession(session); } } }); @@ -268,10 +267,15 @@ export function registerCommands(): void { CommandsRegistry.registerCommand({ id: DISCONNECT_ID, - handler: (accessor: ServicesAccessor, sessionId: string | undefined) => { + handler: async (accessor: ServicesAccessor, _: string, context: CallStackContext | unknown) => { const debugService = accessor.get(IDebugService); - const session = debugService.getModel().getSession(sessionId) || debugService.getViewModel().focusedSession; - debugService.stopSession(session).then(undefined, onUnexpectedError); + let session: IDebugSession | undefined; + if (isSessionContext(context)) { + session = debugService.getModel().getSession(context.sessionId); + } else { + session = debugService.getViewModel().focusedSession; + } + await debugService.stopSession(session); } }); @@ -280,7 +284,7 @@ export function registerCommands(): void { weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.Shift | KeyCode.F5, when: CONTEXT_IN_DEBUG_MODE, - handler: (accessor: ServicesAccessor, _: string, context: CallStackContext | unknown) => { + handler: async (accessor: ServicesAccessor, _: string, context: CallStackContext | unknown) => { const debugService = accessor.get(IDebugService); let session: IDebugSession | undefined; if (isSessionContext(context)) { @@ -296,7 +300,7 @@ export function registerCommands(): void { session = session.parentSession; } - debugService.stopSession(session).then(undefined, onUnexpectedError); + await debugService.stopSession(session); } }); @@ -336,9 +340,9 @@ export function registerCommands(): void { CommandsRegistry.registerCommand({ id: 'debug.startFromConfig', - handler: (accessor, config: IConfig) => { + handler: async (accessor, config: IConfig) => { const debugService = accessor.get(IDebugService); - debugService.startDebugging(undefined, config).then(undefined, onUnexpectedError); + await debugService.startDebugging(undefined, config); } }); diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index af8b4c6e8b..1d2e8b4643 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -30,10 +30,8 @@ import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; import { first } from 'vs/base/common/arrays'; import { memoize, createMemoizer } from 'vs/base/common/decorators'; import { IEditorHoverOptions, EditorOption } from 'vs/editor/common/config/editorOptions'; -import { CancellationToken } from 'vs/base/common/cancellation'; import { DebugHoverWidget } from 'vs/workbench/contrib/debug/browser/debugHover'; import { ITextModel } from 'vs/editor/common/model'; -import { getHover } from 'vs/editor/contrib/hover/getHover'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { basename } from 'vs/base/common/path'; @@ -170,7 +168,6 @@ class DebugEditorContribution implements IDebugEditorContribution { private toDispose: IDisposable[]; private hoverWidget: DebugHoverWidget; - private nonDebugHoverPosition: Position | undefined; private hoverRange: Range | null = null; private mouseDown = false; private static readonly MEMOIZER = createMemoizer(); @@ -204,7 +201,6 @@ class DebugEditorContribution implements IDebugEditorContribution { this.toDispose.push(this.editor.onMouseUp(() => this.mouseDown = false)); this.toDispose.push(this.editor.onMouseMove((e: IEditorMouseEvent) => this.onEditorMouseMove(e))); this.toDispose.push(this.editor.onMouseLeave((e: IPartialEditorMouseEvent) => { - this.provideNonDebugHoverScheduler.cancel(); const hoverDomNode = this.hoverWidget.getDomNode(); if (!hoverDomNode) { return; @@ -315,24 +311,11 @@ class DebugEditorContribution implements IDebugEditorContribution { return scheduler; } - @memoize - private get provideNonDebugHoverScheduler(): RunOnceScheduler { - const scheduler = new RunOnceScheduler(() => { - if (this.editor.hasModel() && this.nonDebugHoverPosition) { - getHover(this.editor.getModel(), this.nonDebugHoverPosition, CancellationToken.None); - } - }, HOVER_DELAY); - this.toDispose.push(scheduler); - - return scheduler; - } - private hideHoverWidget(): void { if (!this.hideHoverScheduler.isScheduled() && this.hoverWidget.isVisible()) { this.hideHoverScheduler.schedule(); } this.showHoverScheduler.cancel(); - this.provideNonDebugHoverScheduler.cancel(); } // hover business @@ -351,10 +334,6 @@ class DebugEditorContribution implements IDebugEditorContribution { return; } - if (this.configurationService.getValue('debug').enableAllHovers && mouseEvent.target.position) { - this.nonDebugHoverPosition = mouseEvent.target.position; - this.provideNonDebugHoverScheduler.schedule(); - } const targetType = mouseEvent.target.type; const stopKey = env.isMacintosh ? 'metaKey' : 'ctrlKey'; diff --git a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index a62ee921e9..39e4b01889 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -66,10 +66,8 @@ /* Debug viewlet trees */ .debug-pane .line-number { - border-radius: 2px; - font-size: 0.9em; - padding: 0 3px; - line-height: 20px; + padding-left: 4px; + padding-right: 4px; } .debug-pane .disabled { @@ -118,6 +116,8 @@ .debug-pane .debug-call-stack .thread > .state, .debug-pane .debug-call-stack .session > .state { + display: flex; + align-items: center; text-align: right; overflow: hidden; text-overflow: ellipsis; @@ -159,9 +159,8 @@ .debug-pane .debug-call-stack .thread > .state > .label, .debug-pane .debug-call-stack .session > .state > .label { - border-radius: 2px; font-size: 0.8em; - padding: 0 3px; + min-height: auto; } .debug-pane .debug-call-stack .stack-frame { diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index c1ce98ff28..1f7289523f 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -318,7 +318,6 @@ export interface IStackFrame extends ITreeElement { readonly source: Source; getScopes(): Promise; getMostSpecificScopes(range: IRange): Promise>; - getSpecificSourceName(): string; forgetScopes(): void; restart(): Promise; toString(): string; diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 31f3a9552b..b95ce2296e 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -17,8 +17,6 @@ import { IThread, IRawModelUpdate, IScope, IRawStoppedDetails, IEnablement, IBreakpointData, IExceptionInfo, IBreakpointsChangeEvent, IBreakpointUpdateData, IBaseBreakpoint, State, IDataBreakpoint } from 'vs/workbench/contrib/debug/common/debug'; import { Source, UNKNOWN_SOURCE_LABEL, getUriFromSource } from 'vs/workbench/contrib/debug/common/debugSource'; -import { commonSuffixLength } from 'vs/base/common/strings'; -import { posix } from 'vs/base/common/path'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ITextEditorPane } from 'vs/workbench/common/editor'; @@ -325,26 +323,6 @@ export class StackFrame implements IStackFrame { return this.scopes; } - getSpecificSourceName(): string { - // To reduce flashing of the path name and the way we fetch stack frames - // We need to compute the source name based on the other frames in the stale call stack - let callStack = (this.thread).getStaleCallStack(); - callStack = callStack.length > 0 ? callStack : this.thread.getCallStack(); - const otherSources = callStack.map(sf => sf.source).filter(s => s !== this.source); - let suffixLength = 0; - otherSources.forEach(s => { - if (s.name === this.source.name) { - suffixLength = Math.max(suffixLength, commonSuffixLength(this.source.uri.path, s.uri.path)); - } - }); - if (suffixLength === 0) { - return this.source.name; - } - - const from = Math.max(0, this.source.uri.path.lastIndexOf(posix.sep, this.source.uri.path.length - suffixLength - 1)); - return (from > 0 ? '...' : '') + this.source.uri.path.substr(from); - } - async getMostSpecificScopes(range: IRange): Promise { const scopes = await this.getScopes(); const nonExpensiveScopes = scopes.filter(s => !s.expensive); diff --git a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts index 75bb423d5e..a32e1a8af7 100644 --- a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts @@ -14,7 +14,7 @@ import { IDebugSessionOptions, State } from 'vs/workbench/contrib/debug/common/d import { NullOpenerService } from 'vs/platform/opener/common/opener'; import { createDecorationsForStackFrame } from 'vs/workbench/contrib/debug/browser/callStackEditorContribution'; import { Constants } from 'vs/base/common/uint'; -import { getContext, getContextForContributedActions } from 'vs/workbench/contrib/debug/browser/callStackView'; +import { getContext, getContextForContributedActions, getSpecificSourceName } from 'vs/workbench/contrib/debug/browser/callStackView'; import { getStackFrameThreadAndSessionToFocus } from 'vs/workbench/contrib/debug/browser/debugService'; import { generateUuid } from 'vs/base/common/uuid'; @@ -250,8 +250,8 @@ suite('Debug - CallStack', () => { model.addSession(session); const { firstStackFrame, secondStackFrame } = createTwoStackFrames(session); - assert.equal(firstStackFrame.getSpecificSourceName(), '.../b/c/d/internalModule.js'); - assert.equal(secondStackFrame.getSpecificSourceName(), '.../x/c/d/internalModule.js'); + assert.equal(getSpecificSourceName(firstStackFrame), '.../b/c/d/internalModule.js'); + assert.equal(getSpecificSourceName(secondStackFrame), '.../x/c/d/internalModule.js'); }); test('stack frame toString()', () => { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 80994a24ac..1287fd6051 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -1629,11 +1629,11 @@ export class ShowInstalledExtensionsAction extends Action { super(id, label, undefined, true); } - run(): Promise { + run(refresh?: boolean): Promise { return this.viewletService.openViewlet(VIEWLET_ID, true) .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) .then(viewlet => { - viewlet.search('@installed '); + viewlet.search('@installed ', refresh); viewlet.focus(); }); } @@ -2983,90 +2983,84 @@ export class InstallVSIXAction extends Action { @IHostService private readonly hostService: IHostService, @IFileDialogService private readonly fileDialogService: IFileDialogService, @IExtensionService private readonly extensionService: IExtensionService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @IConfigurationService private readonly configurationService: IConfigurationService, // {{SQL CARBON EDIT}} @IStorageService private storageService: IStorageService ) { super(id, label, 'extension-action install-vsix', true); } - run(): Promise { + async run(vsixPaths?: URI[]): Promise { // {{SQL CARBON EDIT}} - Replace run body let extensionPolicy = this.configurationService.getValue(ExtensionsPolicyKey); if (extensionPolicy === ExtensionsPolicy.allowAll) { - return Promise.resolve(this.fileDialogService.showOpenDialog({ - title: localize('installFromVSIX', "Install from VSIX"), - filters: [{ name: 'VSIX Extensions', extensions: ['vsix'] }], - canSelectFiles: true, - openLabel: mnemonicButtonLabel(localize({ key: 'installButton', comment: ['&& denotes a mnemonic'] }, "&&Install")) - }).then(result => { - if (!result) { - return Promise.resolve(); - } - return Promise.all(result.map(vsix => { - if (!this.storageService.getBoolean(vsix.fsPath, StorageScope.GLOBAL)) { + if (!vsixPaths) { + vsixPaths = await this.fileDialogService.showOpenDialog({ + title: localize('installFromVSIX', "Install from VSIX"), + filters: [{ name: 'VSIX Extensions', extensions: ['vsix'] }], + canSelectFiles: true, + openLabel: mnemonicButtonLabel(localize({ key: 'installButton', comment: ['&& denotes a mnemonic'] }, "&&Install")) + }); + } + + await Promise.all(vsixPaths.map(async vsix => { + if (!this.storageService.getBoolean(vsix.fsPath, StorageScope.GLOBAL)) { + const accept = await new Promise(resolve => { this.notificationService.prompt( Severity.Warning, localize('thirdPartyExtension.vsix', 'This is a third party extension and might involve security risks. Are you sure you want to install this extension?'), [ { label: localize('thirdPartExt.yes', 'Yes'), - run: () => { - this.extensionsWorkbenchService.install(vsix).then(extension => { - const requireReload = !(extension.local && this.extensionService.canAddExtension(toExtensionDescription(extension.local))); - const message = requireReload ? localize('InstallVSIXAction.successReload', "Please reload Azure Data Studio to complete installing the extension {0}.", extension.identifier.id) - : localize('InstallVSIXAction.success', "Completed installing the extension {0}.", extension.identifier.id); - const actions = requireReload ? [{ - label: localize('InstallVSIXAction.reloadNow', "Reload Now"), - run: () => this.hostService.reload() - }] : []; - this.notificationService.prompt( - Severity.Info, - message, - actions, - { sticky: true } - ); - }); - } + run: () => resolve(true) }, { label: localize('thirdPartyExt.no', 'No'), - run: () => { return Promise.resolve(); } + run: () => resolve(false) }, { label: localize('thirdPartyExt.dontShowAgain', 'Don\'t Show Again'), isSecondary: true, run: () => { this.storageService.store(vsix.fsPath, true, StorageScope.GLOBAL); - return Promise.resolve(); + resolve(true); } } ], { sticky: true } ); - } else { - this.extensionsWorkbenchService.install(vsix).then(extension => { - const requireReload = !(extension.local && this.extensionService.canAddExtension(toExtensionDescription(extension.local))); - const message = requireReload ? localize('InstallVSIXAction.successReload', "Please reload Azure Data Studio to complete installing the extension {0}.", extension.identifier.id) - : localize('InstallVSIXAction.success', "Completed installing the extension {0}.", extension.identifier.id); - const actions = requireReload ? [{ - label: localize('InstallVSIXAction.reloadNow', "Reload Now"), - run: () => this.hostService.reload() - }] : []; - this.notificationService.prompt( - Severity.Info, - message, - actions, - { sticky: true } - ); - }); + }); + + if (!accept) { + return undefined; } - })).then(() => Promise.resolve()); - })); + } + + return this.extensionsWorkbenchService.install(vsix); + })).then(async (extensions) => { + for (const extension of extensions) { + if (!extension) { + return; + } + const requireReload = !(extension.local && this.extensionService.canAddExtension(toExtensionDescription(extension.local))); + const message = requireReload ? localize('InstallVSIXAction.successReload', "Please reload Visual Studio Code to complete installing the extension {0}.", extension.displayName || extension.name) + : localize('InstallVSIXAction.success', "Completed installing the extension {0}.", extension.displayName || extension.name); + const actions = requireReload ? [{ + label: localize('InstallVSIXAction.reloadNow', "Reload Now"), + run: () => this.hostService.reload() + }] : []; + this.notificationService.prompt( + Severity.Info, + message, + actions, + { sticky: true } + ); + } + await this.instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, ShowInstalledExtensionsAction.LABEL).run(true); + }); } else { this.notificationService.error(localize('InstallVSIXAction.allowNone', 'Your extension policy does not allow downloading extensions. Please change your extension policy and try again.')); - return Promise.resolve(); } - // {{SQL CARBON EDIT}} - End } get enabled(): boolean { // {{SQL CARBON EDIT}} add enabled logic diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index df6ef5b797..fb38c6f116 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -14,7 +14,7 @@ import { IAction, Action } from 'vs/base/common/actions'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { append, $, addClass, toggleClass, Dimension } from 'vs/base/browser/dom'; +import { append, $, addClass, toggleClass, Dimension, hide, show } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -57,6 +57,9 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { MementoObject } from 'vs/workbench/common/memento'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { DragAndDropObserver } from 'vs/workbench/browser/dnd'; +import { URI } from 'vs/base/common/uri'; +import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; const NonEmptyWorkspaceContext = new RawContextKey('nonEmptyWorkspace', false); const DefaultViewsContext = new RawContextKey('defaultExtensionViews', true); @@ -400,8 +403,12 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE addClass(parent, 'extensions-viewlet'); this.root = parent; - const header = append(this.root, $('.header')); + const overlay = append(this.root, $('.overlay')); + const overlayBackgroundColor = this.getColor(SIDE_BAR_DRAG_AND_DROP_BACKGROUND) ?? ''; + overlay.style.backgroundColor = overlayBackgroundColor; + hide(overlay); + const header = append(this.root, $('.header')); const placeholder = localize('searchExtensions', "Search Extensions in Marketplace"); const searchValue = this.searchViewletState['query.value'] ? this.searchViewletState['query.value'] : ''; @@ -435,6 +442,49 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE } })); + // Register DragAndDrop support + this._register(new DragAndDropObserver(this.root, { + onDragEnd: (e: DragEvent) => undefined, + onDragEnter: (e: DragEvent) => { + if (this.isSupportedDragElement(e)) { + show(overlay); + } + }, + onDragLeave: (e: DragEvent) => { + if (this.isSupportedDragElement(e)) { + hide(overlay); + } + }, + onDragOver: (e: DragEvent) => { + if (e.dataTransfer) { + e.dataTransfer.dropEffect = this.isSupportedDragElement(e) ? 'copy' : 'none'; + } + }, + onDrop: async (e: DragEvent) => { + if (this.isSupportedDragElement(e)) { + hide(overlay); + + if (e.dataTransfer && e.dataTransfer.files.length > 0) { + let vsixPaths: URI[] = []; + for (let index = 0; index < e.dataTransfer.files.length; index++) { + const path = e.dataTransfer.files.item(index)!.path; + if (path.indexOf('.vsix') !== -1) { + vsixPaths.push(URI.parse(path)); + } + } + + try { + // Attempt to install the extension(s) + await this.instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL).run(vsixPaths); + } + catch (err) { + this.notificationService.error(err); + } + } + } + }, + })); + super.create(append(this.root, $('.extensions'))); } @@ -622,6 +672,15 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.notificationService.error(err); } + + private isSupportedDragElement(e: DragEvent): boolean { + if (e.dataTransfer) { + const typesLowerCase = e.dataTransfer.types.map(t => t.toLocaleLowerCase()); + return typesLowerCase.indexOf('files') !== -1; + } + + return false; + } } export class StatusUpdater extends Disposable implements IWorkbenchContribution { diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css index 2f9d902154..dfe87d3d3d 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css @@ -4,9 +4,19 @@ *--------------------------------------------------------------------------------------------*/ .extensions-viewlet { + position: relative; height: 100%; } +.extensions-viewlet > .overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 2; +} + .extensions-viewlet > .header { height: 41px; box-sizing: border-box; diff --git a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css index 06a3588294..1f7baf8a48 100644 --- a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css +++ b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css @@ -60,10 +60,9 @@ } .dirty-count.monaco-count-badge { - padding: 1px 6px 2px; + padding: 2px 4px; margin-left: 6px; min-height: auto; - border-radius: 0; /* goes better when ellipsis shows up on narrow sidebar */ } .explorer-viewlet .explorer-item.nonexistent-root { diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 6318bef652..32d2699f7f 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -890,7 +890,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { const items = FileDragAndDrop.getStatsFromDragAndDropData(data as ElementsDragAndDropData, originalEvent); if (items && items.length && originalEvent.dataTransfer) { // Apply some datatransfer types to allow for dragging the element outside of the application - this.instantiationService.invokeFunction(fillResourceDataTransfers, items, originalEvent); + this.instantiationService.invokeFunction(fillResourceDataTransfers, items, undefined, originalEvent); // The only custom data transfer we set from the explorer is a file transfer // to be able to DND between multiple code file explorers across windows @@ -965,7 +965,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { private async handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { const droppedResources = extractResources(originalEvent, true); // Check for dropped external files to be folders - const result = await this.fileService.resolveAll(droppedResources); + const result = await this.fileService.resolveAll(droppedResources.map(droppedResource => ({ resource: droppedResource.resource }))); // Pass focus to window this.hostService.focus(); diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 36f69e6987..518bee68b5 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -184,7 +184,7 @@ export class OpenEditorsView extends ViewPane { super.renderHeaderTitle(container, this.title); const count = dom.append(container, $('.count')); - this.dirtyCountElement = dom.append(count, $('.dirty-count.monaco-count-badge')); + this.dirtyCountElement = dom.append(count, $('.dirty-count.monaco-count-badge.long')); this._register((attachStylerCallback(this.themeService, { badgeBackground, badgeForeground, contrastBorder }, colors => { const background = colors.badgeBackground ? colors.badgeBackground.toString() : ''; @@ -660,7 +660,7 @@ class OpenEditorsDragAndDrop implements IListDragAndDrop { if (resources.length) { // Apply some datatransfer types to allow for dragging the element outside of the application - this.instantiationService.invokeFunction(fillResourceDataTransfers, resources, originalEvent); + this.instantiationService.invokeFunction(fillResourceDataTransfers, resources, undefined, originalEvent); } } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index 47dad8eb1f..def77010a0 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -540,7 +540,12 @@ registerAction2(class extends InsertCellCommand { id: INSERT_CODE_CELL_ABOVE_COMMAND_ID, title: localize('notebookActions.insertCodeCellAbove', "Insert Code Cell Above"), category: NOTEBOOK_ACTIONS_CATEGORY, - f1: true + f1: true, + keybinding: { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter, + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), + weight: KeybindingWeight.WorkbenchContrib + } }, CellKind.Code, 'above'); @@ -572,7 +577,12 @@ registerAction2(class extends InsertCellCommand { title: localize('notebookActions.insertCodeCellBelow', "Insert Code Cell Below"), category: NOTEBOOK_ACTIONS_CATEGORY, icon: { id: 'codicon/add' }, - f1: true + f1: true, + keybinding: { + primary: KeyMod.CtrlCmd | KeyCode.Enter, + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), + weight: KeybindingWeight.WorkbenchContrib + } }, CellKind.Code, 'below'); @@ -770,7 +780,12 @@ registerAction2(class extends Action2 { title: localize('notebookActions.moveCellUp', "Move Cell Up"), category: NOTEBOOK_ACTIONS_CATEGORY, icon: { id: 'codicon/arrow-up' }, - f1: true + f1: true, + keybinding: { + primary: KeyMod.Alt | KeyCode.UpArrow, + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), + weight: KeybindingWeight.WorkbenchContrib + } }); } @@ -794,7 +809,12 @@ registerAction2(class extends Action2 { title: localize('notebookActions.moveCellDown', "Move Cell Down"), category: NOTEBOOK_ACTIONS_CATEGORY, icon: { id: 'codicon/arrow-down' }, - f1: true + f1: true, + keybinding: { + primary: KeyMod.Alt | KeyCode.DownArrow, + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), + weight: KeybindingWeight.WorkbenchContrib + } }); } @@ -969,7 +989,12 @@ registerAction2(class extends Action2 { id: COPY_CELL_UP_COMMAND_ID, title: localize('notebookActions.copyCellUp', "Copy Cell Up"), category: NOTEBOOK_ACTIONS_CATEGORY, - f1: true + f1: true, + keybinding: { + primary: KeyMod.Alt | KeyMod.Shift | KeyCode.UpArrow, + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), + weight: KeybindingWeight.WorkbenchContrib + } }); } @@ -992,7 +1017,12 @@ registerAction2(class extends Action2 { id: COPY_CELL_DOWN_COMMAND_ID, title: localize('notebookActions.copyCellDown', "Copy Cell Down"), category: NOTEBOOK_ACTIONS_CATEGORY, - f1: true + f1: true, + keybinding: { + primary: KeyMod.Alt | KeyMod.Shift | KeyCode.DownArrow, + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), + weight: KeybindingWeight.WorkbenchContrib + } }); } diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 43c25648c3..5896d6add1 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -21,6 +21,10 @@ overflow: visible !important; } */ +.monaco-workbench .part.editor > .content .notebook-editor .simple-fr-find-part-wrapper.visible { + z-index: 100; +} + .monaco-workbench .part.editor > .content .notebook-editor .cell-list-container .overflowingContentWidgets > div { z-index: 600 !important; /* @rebornix: larger than the editor title bar */ diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 72209e7926..8ceb73551b 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -110,7 +110,6 @@ export interface ICellViewModel { resolveTextModel(): Promise; getEvaluatedMetadata(documentMetadata: NotebookDocumentMetadata | undefined): NotebookCellMetadata; getSelectionsStartPosition(): IPosition[] | undefined; - getLinesContent(): string[]; } export interface IEditableCellViewModel extends ICellViewModel { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index a13846a6a7..c2030ad417 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -47,6 +47,7 @@ import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IPosition, Position } from 'vs/editor/common/core/position'; +import { IReadonlyTextBuffer } from 'vs/editor/common/model'; const $ = DOM.$; const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState'; @@ -170,19 +171,20 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { return true; } + private updateEditorFocus() { + // Note - focus going to the webview will fire 'blur', but the webview element will be + // a descendent of the notebook editor root. + this.editorFocus?.set(DOM.isAncestor(document.activeElement, this.getDomNode())); + } + protected createEditor(parent: HTMLElement): void { this._rootElement = DOM.append(parent, $('.notebook-editor')); this.createBody(this._rootElement); this.generateFontInfo(); this.editorFocus = NOTEBOOK_EDITOR_FOCUSED.bindTo(this.contextKeyService); this.editorFocus.set(true); - this._register(this.onDidFocus(() => { - this.editorFocus?.set(true); - })); - - this._register(this.onDidBlur(() => { - this.editorFocus?.set(false); - })); + this._register(this.onDidFocus(() => this.updateEditorFocus())); + this._register(this.onDidBlur(() => this.updateEditorFocus())); this.editorEditable = NOTEBOOK_EDITOR_EDITABLE.bindTo(this.contextKeyService); this.editorEditable.set(true); @@ -307,6 +309,8 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { this.control = new NotebookCodeEditors(this.list, this.renderedEditors); this.webview = this.instantiationService.createInstance(BackLayerWebView, this); + this.webview.webview.onDidBlur(() => this.updateEditorFocus()); + this.webview.webview.onDidFocus(() => this.updateEditorFocus()); this._register(this.webview.onMessage(message => { if (this.viewModel) { this.notebookService.onDidReceiveMessage(this.viewModel.viewType, this.viewModel.uri, message); @@ -778,11 +782,6 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { return newCell; } - private isAtEOL(p: IPosition, lines: string[]) { - const line = lines[p.lineNumber - 1]; - return line.length + 1 === p.column; - } - private pushIfAbsent(positions: IPosition[], p: IPosition) { const last = positions.length > 0 ? positions[positions.length - 1] : undefined; if (!last || last.lineNumber !== p.lineNumber || last.column !== p.column) { @@ -795,8 +794,12 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { * Move end of line split points to the beginning of the next line; * Avoid duplicate split points */ - private splitPointsToBoundaries(splitPoints: IPosition[], lines: string[]): IPosition[] | null { + private splitPointsToBoundaries(splitPoints: IPosition[], textBuffer: IReadonlyTextBuffer): IPosition[] | null { const boundaries: IPosition[] = []; + const lineCnt = textBuffer.getLineCount(); + const getLineLen = (lineNumber: number) => { + return textBuffer.getLineLength(lineNumber); + }; // split points need to be sorted splitPoints = splitPoints.sort((l, r) => { @@ -809,22 +812,21 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { this.pushIfAbsent(boundaries, new Position(1, 1)); for (let sp of splitPoints) { - if (this.isAtEOL(sp, lines) && sp.lineNumber < lines.length) { + if (getLineLen(sp.lineNumber) + 1 === sp.column && sp.lineNumber < lineCnt) { sp = new Position(sp.lineNumber + 1, 1); } this.pushIfAbsent(boundaries, sp); } // eat-up any split point at the beginning, i.e. we ignore the split point at the very end - this.pushIfAbsent(boundaries, new Position(lines.length, lines[lines.length - 1].length + 1)); + this.pushIfAbsent(boundaries, new Position(lineCnt, getLineLen(lineCnt) + 1)); // if we only have two then they describe the whole range and nothing needs to be split return boundaries.length > 2 ? boundaries : null; } private computeCellLinesContents(cell: IEditableCellViewModel, splitPoints: IPosition[]): string[] | null { - const lines = cell.getLinesContent(); - const rangeBoundaries = this.splitPointsToBoundaries(splitPoints, lines); + const rangeBoundaries = this.splitPointsToBoundaries(splitPoints, cell.textBuffer); if (!rangeBoundaries) { return null; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts index 8b1ac9e024..2880d46b8f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts @@ -3,13 +3,14 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorInput, IEditorInput, GroupIdentifier, ISaveOptions } from 'vs/workbench/common/editor'; +import { EditorInput, IEditorInput, GroupIdentifier, ISaveOptions, IMoveResult, IRevertOptions } from 'vs/workbench/common/editor'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { URI } from 'vs/base/common/uri'; -import { isEqual } from 'vs/base/common/resources'; +import { isEqual, basename } from 'vs/base/common/resources'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; export class NotebookEditorInput extends EditorInput { @@ -39,7 +40,10 @@ export class NotebookEditorInput extends EditorInput { public name: string, public readonly viewType: string | undefined, @INotebookService private readonly notebookService: INotebookService, - @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, + @IFileDialogService private readonly fileDialogService: IFileDialogService, + // @IEditorService private readonly editorService: IEditorService, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(); } @@ -85,6 +89,48 @@ export class NotebookEditorInput extends EditorInput { return undefined; } + async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { + if (!this.textModel) { + return undefined; + } + + const dialogPath = this.textModel.resource; + const target = await this.fileDialogService.pickFileToSave(dialogPath, options?.availableFileSystems); + if (!target) { + return undefined; // save cancelled + } + + if (!await this.textModel.saveAs(target)) { + return undefined; + } + + return this._move(group, target)?.editor; + } + + move(group: GroupIdentifier, target: URI): IMoveResult | undefined { + if (this.textModel) { + const contributedNotebookProviders = this.notebookService.getContributedNotebookProviders(target); + + if (contributedNotebookProviders.find(provider => provider.id === this.textModel!.viewType)) { + return this._move(group, target); + } + } + return undefined; + } + + _move(group: GroupIdentifier, newResource: URI): { editor: IEditorInput } | undefined { + const editorInput = NotebookEditorInput.getOrCreate(this.instantiationService, newResource, basename(newResource), this.viewType); + return { editor: editorInput }; + } + + async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { + if (this.textModel) { + await this.textModel.revert(options); + } + + return; + } + async resolve(): Promise { if (!await this.notebookService.canResolve(this.viewType!)) { throw new Error(`Cannot open notebook of type '${this.viewType}'`); @@ -96,6 +142,10 @@ export class NotebookEditorInput extends EditorInput { this._onDidChangeDirty.fire(); })); + if (this.textModel.isDirty()) { + this._onDidChangeDirty.fire(); + } + return this.textModel; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index a4200d6e2f..f807ed8626 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -11,7 +11,7 @@ import { notebookProviderExtensionPoint, notebookRendererExtensionPoint } from ' import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol'; import { Emitter, Event } from 'vs/base/common/event'; -import { INotebookTextModel, INotebookMimeTypeSelector, INotebookRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, INotebookMimeTypeSelector, INotebookRendererInfo, NotebookDocumentMetadata, CellEditType, ICellDto2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer'; import { Iterable } from 'vs/base/common/iterator'; @@ -202,13 +202,13 @@ export class NotebookService extends Disposable implements INotebookService, ICu return undefined; // {{SQL CARBON EDIT}} strict-null-check } - async resolveNotebook(viewType: string, uri: URI): Promise { + async createNotebookFromBackup(viewType: string, uri: URI, metadata: NotebookDocumentMetadata, languages: string[], cells: ICellDto2[]): Promise { const provider = this._notebookProviders.get(viewType); if (!provider) { return undefined; } - const notebookModel = await provider.controller.resolveNotebook(viewType, uri); + const notebookModel = await provider.controller.createNotebook(viewType, uri, true, false); if (!notebookModel) { return undefined; } @@ -219,6 +219,39 @@ export class NotebookService extends Disposable implements INotebookService, ICu notebookModel, (model) => this._onWillDispose(model), ); + this._models[modelId] = modelData; + + notebookModel.metadata = metadata; + notebookModel.languages = languages; + + notebookModel.applyEdit(notebookModel.versionId, [ + { + editType: CellEditType.Insert, + index: 0, + cells: cells + } + ]); + + return modelData.model; + } + + async resolveNotebook(viewType: string, uri: URI, forceReload: boolean): Promise { + const provider = this._notebookProviders.get(viewType); + if (!provider) { + return undefined; + } + + let notebookModel: NotebookTextModel | undefined; + + notebookModel = await provider.controller.createNotebook(viewType, uri, false, forceReload); + + // new notebook model created + const modelId = MODEL_ID(uri); + const modelData = new ModelData( + notebookModel!, + (model) => this._onWillDispose(model), + ); + this._models[modelId] = modelData; return modelData.model; } @@ -265,7 +298,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu let provider = this._notebookProviders.get(viewType); if (provider) { - provider.controller.destoryNotebookDocument(notebook); + provider.controller.removeNotebookDocument(notebook); } } @@ -291,6 +324,16 @@ export class NotebookService extends Disposable implements INotebookService, ICu return false; } + async saveAs(viewType: string, resource: URI, target: URI, token: CancellationToken): Promise { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + return provider.controller.saveAs(resource, target, token); + } + + return false; + } + onDidReceiveMessage(viewType: string, uri: URI, message: any): void { let provider = this._notebookProviders.get(viewType); diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index a3a309c441..e4d2abbe18 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -157,7 +157,7 @@ export class NotebookCellList extends WorkbenchList implements ID for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { const cell = this.element(i); - if (this._viewModel!.hasCell(cell)) { + if (this._viewModel!.hasCell(cell.handle)) { hideOutputs.push(...cell?.model.outputs); } else { deletedOutputs.push(...cell?.model.outputs); @@ -177,7 +177,7 @@ export class NotebookCellList extends WorkbenchList implements ID for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { const cell = this.element(i); - if (this._viewModel!.hasCell(cell)) { + if (this._viewModel!.hasCell(cell.handle)) { hideOutputs.push(...cell?.model.outputs); } else { deletedOutputs.push(...cell?.model.outputs); @@ -299,7 +299,7 @@ export class NotebookCellList extends WorkbenchList implements ID for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { const cell = this.element(i); - if (this._viewModel!.hasCell(cell)) { + if (this._viewModel!.hasCell(cell.handle)) { hideOutputs.push(...cell?.model.outputs); } else { deletedOutputs.push(...cell?.model.outputs); @@ -316,6 +316,18 @@ export class NotebookCellList extends WorkbenchList implements ID splice2(start: number, deleteCount: number, elements: CellViewModel[] = []): void { // we need to convert start and delete count based on hidden ranges super.splice(start, deleteCount, elements); + + const selectionsLeft = []; + this._viewModel!.selectionHandles.forEach(handle => { + if (this._viewModel!.hasCell(handle)) { + selectionsLeft.push(handle); + } + }); + + if (!selectionsLeft.length && this._viewModel!.viewCells) { + // after splice, the selected cells are deleted + this._viewModel!.selectionHandles = [this._viewModel!.viewCells[0].handle]; + } } getViewIndex(cell: ICellViewModel) { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts index 04f8faf627..378936ad6d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts @@ -250,7 +250,6 @@ export class StatefullMarkdownCell extends Disposable { bindEditorListeners(model: ITextModel, dimension?: IDimension) { this.localDisposables.add(model.onDidChangeContent(() => { // we don't need to update view cell text anymore as the textbuffer is shared - // this.viewCell.setText(model.getLinesContent()); this.viewCell.clearHTML(); let clientHeight = this.markdownContainer.clientHeight; this.markdownContainer.innerHTML = ''; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 23bc15e9f8..316978712c 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -205,25 +205,6 @@ export abstract class BaseCellViewModel extends Disposable { return this.model.getValue(); } - getLinesContent(): string[] { - if (this._textModel) { - return this._textModel.getLinesContent(); - } - - return this.model.textBuffer.getLinesContent(); - } - - // setLinesContent(value: string[]) { - // if (this._textModel) { - // // TODO @rebornix we should avoid creating a new string here - // return this._textModel.setValue(value.join('\n')); - // } else { - // const range = this.model.getFullModelRange(); - // this.model.textBuffer. - // this.model.source = value; - // } - // } - private saveViewState(): void { if (!this._textEditor) { return; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index 3246891787..6ef1d5b5fd 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -268,7 +268,12 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD }); diffs.reverse().forEach(diff => { - this._viewCells.splice(diff[0], diff[1], ...diff[2]); + const deletedCells = this._viewCells.splice(diff[0], diff[1], ...diff[2]); + + deletedCells.forEach(cell => { + this._handleToViewCellMapping.delete(cell.handle); + }); + diff[2].forEach(cell => { this._handleToViewCellMapping.set(cell.handle, cell); this._localStore.add(cell); @@ -456,8 +461,8 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return index + 1; } - hasCell(cell: ICellViewModel) { - return this._handleToViewCellMapping.has(cell.handle); + hasCell(handle: number) { + return this._handleToViewCellMapping.has(handle); } getVersionId() { @@ -586,7 +591,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this._viewCells.splice(deleteIndex, 1); this._handleToViewCellMapping.delete(deleteCell.handle); - this._notebook.removeCell(deleteIndex); + this._notebook.removeCell(deleteIndex, 1); this._onDidChangeViewCells.fire({ synchronous: true, splices: [[deleteIndex, 1, []]] }); } @@ -638,7 +643,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this._viewCells.splice(index, 1); this._handleToViewCellMapping.delete(viewCell.handle); - this._notebook.removeCell(index); + this._notebook.removeCell(index, 1); let endSelections: number[] = []; if (this.selectionHandles.length) { diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index 997f9727a4..07b780fc95 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -7,7 +7,8 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellTextModelSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, ICellInsertEdit, NotebookCellsChangedEvent, CellKind, IOutput, notebookDocumentMetadataDefaults, diff, ICellDeleteEdit, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellTextModelSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, ICellInsertEdit, NotebookCellsChangedEvent, CellKind, IOutput, notebookDocumentMetadataDefaults, diff, ICellDeleteEdit, NotebookCellsChangeType, ICellDto2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ITextSnapshot } from 'vs/editor/common/model'; function compareRangesUsingEnds(a: [number, number], b: [number, number]): number { if (a[1] === b[1]) { @@ -17,6 +18,49 @@ function compareRangesUsingEnds(a: [number, number], b: [number, number]): numbe return a[1] - b[1]; } +export class NotebookTextModelSnapshot implements ITextSnapshot { + // private readonly _pieces: Ce[] = []; + private _index: number = -1; + + constructor(private _model: NotebookTextModel) { + // for (let i = 0; i < this._model.cells.length; i++) { + // const cell = this._model.cells[i]; + // this._pieces.push(this._model.cells[i].textBuffer.createSnapshot(true)); + // } + } + + read(): string | null { + + if (this._index === -1) { + this._index++; + return `{ "metadata": ${JSON.stringify(this._model.metadata)}, "languages": ${JSON.stringify(this._model.languages)}, "cells": [`; + } + + if (this._index < this._model.cells.length) { + const cell = this._model.cells[this._index]; + + const data = { + source: cell.getValue(), + metadata: cell.metadata, + cellKind: cell.cellKind, + language: cell.language + }; + + const rawStr = JSON.stringify(data); + const isLastCell = this._index === this._model.cells.length - 1; + + this._index++; + return isLastCell ? rawStr : (rawStr + ','); + } else if (this._index === this._model.cells.length) { + this._index++; + return `]}`; + } else { + return null; + } + } + +} + export class NotebookTextModel extends Disposable implements INotebookTextModel { private static _cellhandlePool: number = 0; @@ -77,6 +121,18 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return new NotebookCellTextModel(cellUri, cellHandle, source, language, cellKind, outputs || [], metadata); } + initialize(cells: ICellDto2[]) { + this.cells = []; + this._versionId = 0; + + const mainCells = cells.map(cell => { + const cellHandle = NotebookTextModel._cellhandlePool++; + const cellUri = CellUri.generate(this.uri, cellHandle); + return new NotebookCellTextModel(cellUri, cellHandle, cell.source, cell.language, cell.cellKind, cell.outputs || [], cell.metadata); + }); + this.insertNewCell(0, mainCells); + } + applyEdit(modelVersionId: number, rawEdits: ICellEditOperation[]): boolean { if (modelVersionId !== this._versionId) { return false; @@ -127,7 +183,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this.insertNewCell(insertEdit.index, mainCells); break; case CellEditType.Delete: - this.removeCell(operations[i].index); + this.removeCell(operations[i].index, operations[i].end - operations[i].start); break; } } @@ -142,6 +198,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return true; } + createSnapshot(preserveBOM?: boolean): ITextSnapshot { + return new NotebookTextModelSnapshot(this); + } + private _increaseVersionId(): void { this._versionId = this._versionId + 1; } @@ -250,17 +310,19 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return; } - removeCell(index: number) { + removeCell(index: number, count: number) { this._isUntitled = false; - let cell = this.cells[index]; - this._cellListeners.get(cell.handle)?.dispose(); - this._cellListeners.delete(cell.handle); - this.cells.splice(index, 1); + for (let i = index; i < index + count; i++) { + let cell = this.cells[i]; + this._cellListeners.get(cell.handle)?.dispose(); + this._cellListeners.delete(cell.handle); + } + this.cells.splice(index, count); this._onDidChangeContent.fire(); this._increaseVersionId(); - this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.ModelChange, versionId: this._versionId, changes: [[index, 1, []]] }); + this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.ModelChange, versionId: this._versionId, changes: [[index, count, []]] }); } moveCellToIdx(index: number, newIdx: number) { diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index f83a16980a..9959ce0b81 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -273,7 +273,7 @@ export enum CellEditType { } export interface ICellDto2 { - source: string[]; + source: string | string[]; language: string; cellKind: CellKind; outputs: IOutput[]; @@ -300,6 +300,13 @@ export interface INotebookEditData { renderers: number[]; } +export interface NotebookDataDto { + readonly cells: ICellDto2[]; + readonly languages: string[]; + readonly metadata: NotebookDocumentMetadata; +} + + export namespace CellUri { export const scheme = 'vscode-notebook'; diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 8594f80c79..87811fa8d4 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -15,6 +15,8 @@ import { URI } from 'vs/base/common/uri'; import { IWorkingCopyService, IWorkingCopy, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { basename } from 'vs/base/common/resources'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { DefaultEndOfLine, ITextBuffer, EndOfLinePreference } from 'vs/editor/common/model'; export interface INotebookEditorModelManager { models: NotebookEditorModel[]; @@ -24,6 +26,13 @@ export interface INotebookEditorModelManager { get(resource: URI): NotebookEditorModel | undefined; } +export interface INotebookRevertOptions { + /** + * Go to disk bypassing any cache of the model if any. + */ + forceReadFromDisk?: boolean; +} + export class NotebookEditorModel extends EditorModel implements IWorkingCopy, INotebookEditorModel { private _dirty = false; @@ -47,7 +56,8 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN public readonly resource: URI, public readonly viewType: string, @INotebookService private readonly notebookService: INotebookService, - @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService + @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, + @IBackupFileService private readonly backupFileService: IBackupFileService ) { super(); this._register(this.workingCopyService.registerWorkingCopy(this)); @@ -56,28 +66,91 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN capabilities = 0; async backup(): Promise { - return {}; + return { content: this._notebook.createSnapshot(true) }; } async revert(options?: IRevertOptions | undefined): Promise { + if (options?.soft) { + await this.backupFileService.discardBackup(this.resource); + return; + } + + await this.load({ forceReadFromDisk: true }); + this._dirty = false; + this._onDidChangeDirty.fire(); return; } - async load(): Promise { - const notebook = await this.notebookService.resolveNotebook(this.viewType!, this.resource); + async load(options?: INotebookRevertOptions): Promise { + if (options?.forceReadFromDisk) { + return this.loadFromProvider(true); + } + if (this.isResolved()) { + return this; + } + + const backup = await this.backupFileService.resolve(this.resource); + + if (this.isResolved()) { + return this; // Make sure meanwhile someone else did not succeed in loading + } + + if (backup) { + try { + return await this.loadFromBackup(backup.value.create(DefaultEndOfLine.LF)); + } catch (error) { + // this.logService.error('[text file model] load() from backup', error); // ignore error and continue to load as file below + } + } + + return this.loadFromProvider(false); + } + + private async loadFromBackup(content: ITextBuffer): Promise { + const fullRange = content.getRangeAt(0, content.getLength()); + const data = JSON.parse(content.getValueInRange(fullRange, EndOfLinePreference.LF)); + + const notebook = await this.notebookService.createNotebookFromBackup(this.viewType!, this.resource, data.metadata, data.languages, data.cells); this._notebook = notebook!; this._name = basename(this._notebook!.uri); this._register(this._notebook.onDidChangeContent(() => { - this._dirty = true; - this._onDidChangeDirty.fire(); + this.setDirty(true); + this._onDidChangeContent.fire(); + })); + + await this.backupFileService.discardBackup(this.resource); + this.setDirty(true); + + return this; + } + + private async loadFromProvider(forceReloadFromDisk: boolean) { + const notebook = await this.notebookService.resolveNotebook(this.viewType!, this.resource, forceReloadFromDisk); + this._notebook = notebook!; + + this._name = basename(this._notebook!.uri); + + this._register(this._notebook.onDidChangeContent(() => { + this.setDirty(true); this._onDidChangeContent.fire(); })); return this; } + isResolved(): boolean { + return !!this._notebook; + } + + setDirty(newState: boolean) { + if (this._dirty !== newState) { + this._dirty = newState; + this._onDidChangeDirty.fire(); + } + } + isDirty() { return this._dirty; } @@ -89,6 +162,14 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN this._onDidChangeDirty.fire(); return true; } + + async saveAs(targetResource: URI): Promise { + const tokenSource = new CancellationTokenSource(); + await this.notebookService.saveAs(this.notebook.viewType, this.notebook.uri, targetResource, tokenSource.token); + this._dirty = false; + this._onDidChangeDirty.fire(); + return true; + } } export class NotebookEditorModelManager extends Disposable implements INotebookEditorModelManager { diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index 2a3b8d5c0a..5ead08c981 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri'; import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol'; import { Event } from 'vs/base/common/event'; -import { INotebookTextModel, INotebookMimeTypeSelector, INotebookRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, INotebookMimeTypeSelector, INotebookRendererInfo, NotebookDocumentMetadata, ICellDto2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CancellationToken } from 'vs/base/common/cancellation'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; @@ -17,12 +17,13 @@ import { INotebookEditorModelManager } from 'vs/workbench/contrib/notebook/commo export const INotebookService = createDecorator('notebookService'); export interface IMainNotebookController { - resolveNotebook(viewType: string, uri: URI): Promise; + createNotebook(viewType: string, uri: URI, forBackup: boolean, forceReload: boolean): Promise; executeNotebook(viewType: string, uri: URI, token: CancellationToken): Promise; onDidReceiveMessage(uri: URI, message: any): void; executeNotebookCell(uri: URI, handle: number, token: CancellationToken): Promise; - destoryNotebookDocument(notebook: INotebookTextModel): Promise; + removeNotebookDocument(notebook: INotebookTextModel): Promise; save(uri: URI, token: CancellationToken): Promise; + saveAs(uri: URI, target: URI, token: CancellationToken): Promise; } export interface INotebookService { @@ -35,7 +36,8 @@ export interface INotebookService { registerNotebookRenderer(handle: number, extensionData: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, preloads: URI[]): void; unregisterNotebookRenderer(handle: number): void; getRendererInfo(handle: number): INotebookRendererInfo | undefined; - resolveNotebook(viewType: string, uri: URI): Promise; + resolveNotebook(viewType: string, uri: URI, forceReload: boolean): Promise; + createNotebookFromBackup(viewType: string, uri: URI, metadata: NotebookDocumentMetadata, languages: string[], cells: ICellDto2[]): Promise; executeNotebook(viewType: string, uri: URI): Promise; executeNotebookCell(viewType: string, uri: URI, handle: number, token: CancellationToken): Promise; @@ -45,6 +47,7 @@ export interface INotebookService { destoryNotebookDocument(viewType: string, notebook: INotebookTextModel): void; updateActiveNotebookDocument(viewType: string, resource: URI): void; save(viewType: string, resource: URI, token: CancellationToken): Promise; + saveAs(viewType: string, resource: URI, target: URI, token: CancellationToken): Promise; onDidReceiveMessage(viewType: string, uri: URI, message: any): void; setToCopy(items: NotebookCellTextModel[]): void; getToCopy(): NotebookCellTextModel[] | undefined; diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index 98ad007a26..9e0ba4e995 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -47,6 +47,7 @@ import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { Emitter, Event } from 'vs/base/common/event'; import { MenuRegistry, MenuId, isIMenuItem } from 'vs/platform/actions/common/actions'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { preferencesEditIcon } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets'; const $ = DOM.$; @@ -400,7 +401,7 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditorP } private createRecordingBadge(container: HTMLElement): HTMLElement { - const recordingBadge = DOM.append(container, DOM.$('.recording-badge.disabled')); + const recordingBadge = DOM.append(container, DOM.$('.recording-badge.monaco-count-badge.long.disabled')); recordingBadge.textContent = localize('recording', "Recording Keys"); this._register(attachStylerCallback(this.themeService, { badgeBackground, contrastBorder, badgeForeground }, colors => { const background = colors.badgeBackground ? colors.badgeBackground.toString() : ''; @@ -891,7 +892,7 @@ class ActionsColumn extends Column { private createEditAction(keybindingItemEntry: IKeybindingItemEntry): IAction { const keybinding = this.keybindingsService.lookupKeybinding(KEYBINDINGS_EDITOR_COMMAND_DEFINE); return { - class: 'codicon-edit', + class: preferencesEditIcon.classNames, enabled: true, id: 'editKeybinding', tooltip: keybinding ? localize('editKeybindingLabelWithKey', "Change Keybinding {0}", `(${keybinding.getLabel()})`) : localize('editKeybindingLabel', "Change Keybinding"), diff --git a/src/vs/workbench/contrib/preferences/browser/media/keybindingsEditor.css b/src/vs/workbench/contrib/preferences/browser/media/keybindingsEditor.css index 0eb6f25665..705e5af46c 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/keybindingsEditor.css +++ b/src/vs/workbench/contrib/preferences/browser/media/keybindingsEditor.css @@ -27,8 +27,6 @@ .keybindings-editor > .keybindings-header > .search-container > .keybindings-search-actions-container > .recording-badge { margin-right: 8px; - padding: 0px 8px; - border-radius: 2px; } .keybindings-editor > .keybindings-header.small > .search-container > .keybindings-search-actions-container > .recording-badge, diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index 5a80f14c1d..77afaeb9ae 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -40,12 +40,9 @@ } .settings-editor > .settings-header > .search-container > .settings-count-widget { - margin: 6px 0px; - padding: 0px 8px; - border-radius: 2px; position: absolute; right: 35px; - top: 0; + top: 6px; } .settings-editor > .settings-header > .search-container > .settings-clear-widget { diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts index db54f30caa..10d73a8f20 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts @@ -26,7 +26,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { RangeHighlightDecorations } from 'vs/workbench/browser/parts/editor/rangeDecorations'; -import { DefaultSettingsHeaderWidget, EditPreferenceWidget, SettingsGroupTitleWidget, SettingsHeaderWidget } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets'; +import { DefaultSettingsHeaderWidget, EditPreferenceWidget, SettingsGroupTitleWidget, SettingsHeaderWidget, preferencesEditIcon } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets'; import { IFilterResult, IPreferencesEditorModel, IPreferencesService, ISetting, ISettingsEditorModel, ISettingsGroup } from 'vs/workbench/services/preferences/common/preferences'; import { DefaultSettingsEditorModel, SettingsEditorModel, WorkspaceConfigurationEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; import { IMarkerService, IMarkerData, MarkerSeverity, MarkerTag } from 'vs/platform/markers/common/markers'; @@ -748,7 +748,7 @@ class EditSettingRenderer extends Disposable { const decorations = this.editor.getLineDecorations(line); if (decorations) { for (const { options } of decorations) { - if (options.glyphMarginClassName && options.glyphMarginClassName.indexOf(EditPreferenceWidget.GLYPH_MARGIN_CLASS_NAME) === -1) { + if (options.glyphMarginClassName && options.glyphMarginClassName.indexOf(preferencesEditIcon.classNames) === -1) { return false; } } diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index fd93f800a8..5f9828e972 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -35,6 +35,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { ISettingsGroup } from 'vs/workbench/services/preferences/common/preferences'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { isEqual } from 'vs/base/common/resources'; +import { registerIcon, Codicon } from 'vs/base/common/codicons'; export class SettingsHeaderWidget extends Widget implements IViewZone { @@ -736,9 +737,9 @@ export class SearchWidget extends Widget { } } -export class EditPreferenceWidget extends Disposable { +export const preferencesEditIcon = registerIcon('preferences-edit', Codicon.edit, localize('preferencesEditIcon', 'Icon for the edit action in preferences.')); - static readonly GLYPH_MARGIN_CLASS_NAME = 'codicon codicon-edit'; +export class EditPreferenceWidget extends Disposable { private _line: number = -1; private _preferences: T[] = []; @@ -775,7 +776,7 @@ export class EditPreferenceWidget extends Disposable { this._line = line; newDecoration.push({ options: { - glyphMarginClassName: EditPreferenceWidget.GLYPH_MARGIN_CLASS_NAME, + glyphMarginClassName: preferencesEditIcon.classNames, glyphMarginHoverMessage: new MarkdownString().appendText(hoverMessage), stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, }, diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index c5f41febe5..3104faf821 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -450,7 +450,7 @@ export class SettingsEditor2 extends BaseEditor { inputBorder: settingsTextInputBorder })); - this.countElement = DOM.append(searchContainer, DOM.$('.settings-count-widget')); + this.countElement = DOM.append(searchContainer, DOM.$('.settings-count-widget.monaco-count-badge.long')); this._register(attachStylerCallback(this.themeService, { badgeBackground, contrastBorder, badgeForeground }, colors => { const background = colors.badgeBackground ? colors.badgeBackground.toString() : ''; const border = colors.contrastBorder ? colors.contrastBorder.toString() : ''; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index e675e2599c..2836308c7b 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -21,6 +21,7 @@ import { attachButtonStyler, attachInputBoxStyler } from 'vs/platform/theme/comm import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { disposableTimeout } from 'vs/base/common/async'; import { isUndefinedOrNull } from 'vs/base/common/types'; +import { preferencesEditIcon } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets'; const $ = DOM.$; export const settingsHeaderForeground = registerColor('settings.headerForeground', { light: '#444444', dark: '#e7e7e7', hc: '#ffffff' }, localize('headerForeground', "The foreground color for a section header or active title.")); @@ -367,7 +368,7 @@ export class ListSettingWidget extends Disposable { private createEditAction(idx: number): IAction { return { - class: 'codicon-edit', + class: preferencesEditIcon.classNames, enabled: true, id: 'workbench.action.editListItem', tooltip: this.getLocalizedStrings().editActionTooltip, diff --git a/src/vs/workbench/contrib/search/browser/media/searchview.css b/src/vs/workbench/contrib/search/browser/media/searchview.css index 4d75fa8379..834c1743b2 100644 --- a/src/vs/workbench/contrib/search/browser/media/searchview.css +++ b/src/vs/workbench/contrib/search/browser/media/searchview.css @@ -152,9 +152,7 @@ } .search-view .message { - padding-left: 22px; - padding-right: 22px; - padding-top: 0px; + padding: 0 22px 8px; } .search-view .message p:first-child { diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index f7468c4a32..9c22e28728 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -826,6 +826,11 @@ configurationRegistry.registerConfiguration({ ], markdownDescription: nls.localize('search.searchEditor.doubleClickBehaviour', "Configure effect of double clicking a result in a search editor.") }, + 'search.searchEditor.reusePriorSearchConfiguration': { + type: 'boolean', + default: false, + markdownDescription: nls.localize('search.searchEditor.reusePriorSearchConfiguration', "When enabled, new Search Editors will reuse the includes, excludes, and flags of the previously opened Search Editor") + }, 'search.sortOrder': { 'type': 'string', 'enum': [SearchSortOrder.Default, SearchSortOrder.FileNames, SearchSortOrder.Type, SearchSortOrder.Modified, SearchSortOrder.CountDescending, SearchSortOrder.CountAscending], diff --git a/src/vs/workbench/contrib/search/browser/searchResultsView.ts b/src/vs/workbench/contrib/search/browser/searchResultsView.ts index 42ab75bfbd..e5d8038e6d 100644 --- a/src/vs/workbench/contrib/search/browser/searchResultsView.ts +++ b/src/vs/workbench/contrib/search/browser/searchResultsView.ts @@ -378,7 +378,7 @@ export class SearchDND implements ITreeDragAndDrop { if (resources.length) { // Apply some datatransfer types to allow for dragging the element outside of the application - this.instantiationService.invokeFunction(fillResourceDataTransfers, resources, originalEvent); + this.instantiationService.invokeFunction(fillResourceDataTransfers, resources, undefined, originalEvent); } } diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 35874a5a51..5c784d0957 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -1022,6 +1022,7 @@ export class SearchView extends ViewPane { } this.viewModel.cancelSearch(); this.updateActions(); + this.tree.ariaLabel = nls.localize('emptySearch', "Empty Search"); aria.status(nls.localize('ariaSearchResultsClearStatus', "The search results have been cleared")); } @@ -1557,9 +1558,12 @@ export class SearchView extends ViewPane { this.hasSearchResultsKey.set(fileCount > 0); const msgWasHidden = this.messagesElement.style.display === 'none'; + + const messageEl = this.clearMessage(); + let resultMsg = this.buildResultCountMessage(this.viewModel.searchResult.count(), fileCount); + this.tree.ariaLabel = resultMsg + nls.localize('forTerm', " - Search: {0}", this.searchResult.query?.contentPattern.pattern ?? ''); + if (fileCount > 0) { - const messageEl = this.clearMessage(); - let resultMsg = this.buildResultCountMessage(this.viewModel.searchResult.count(), fileCount); if (disregardExcludesAndIgnores) { resultMsg += nls.localize('useIgnoresAndExcludesDisabled', " - exclude settings and ignore files are disabled"); } diff --git a/src/vs/workbench/contrib/search/common/search.ts b/src/vs/workbench/contrib/search/common/search.ts index f8fae6dba0..de8c03422e 100644 --- a/src/vs/workbench/contrib/search/common/search.ts +++ b/src/vs/workbench/contrib/search/common/search.ts @@ -103,7 +103,7 @@ export function getOutOfWorkspaceEditorResources(accessor: ServicesAccessor): UR } // Supports patterns of <#|:|(><#|:|,> -const LINE_COLON_PATTERN = /\s?[#:\(](\d*)([#:,](\d*))?\)?\s*$/; +const LINE_COLON_PATTERN = /\s?[#:\(](?:line )?(\d*)(?:[#:,](\d*))?\)?\s*$/; export interface IFilterAndRange { filter: string; @@ -119,8 +119,9 @@ export function extractRangeFromFilter(filter: string, unless?: string[]): IFilt // Find Line/Column number from search value using RegExp const patternMatch = LINE_COLON_PATTERN.exec(filter); - if (patternMatch && patternMatch.length > 1) { - const startLineNumber = parseInt(patternMatch[1], 10); + + if (patternMatch) { + const startLineNumber = parseInt(patternMatch[1] ?? '', 10); // Line Number if (isNumber(startLineNumber)) { @@ -132,16 +133,14 @@ export function extractRangeFromFilter(filter: string, unless?: string[]): IFilt }; // Column Number - if (patternMatch.length > 3) { - const startColumn = parseInt(patternMatch[3], 10); - if (isNumber(startColumn)) { - range = { - startLineNumber: range.startLineNumber, - startColumn: startColumn, - endLineNumber: range.endLineNumber, - endColumn: startColumn - }; - } + const startColumn = parseInt(patternMatch[2] ?? '', 10); + if (isNumber(startColumn)) { + range = { + startLineNumber: range.startLineNumber, + startColumn: startColumn, + endLineNumber: range.endLineNumber, + endColumn: startColumn + }; } } diff --git a/src/vs/workbench/contrib/search/test/common/extractRange.test.ts b/src/vs/workbench/contrib/search/test/common/extractRange.test.ts index d97f9fccf1..4bc48e4d8c 100644 --- a/src/vs/workbench/contrib/search/test/common/extractRange.test.ts +++ b/src/vs/workbench/contrib/search/test/common/extractRange.test.ts @@ -13,7 +13,7 @@ suite('extractRangeFromFilter', () => { assert.ok(!extractRangeFromFilter('/some/path')); assert.ok(!extractRangeFromFilter('/some/path/file.txt')); - for (const lineSep of [':', '#', '(']) { + for (const lineSep of [':', '#', '(', ':line ']) { for (const colSep of [':', '#', ',']) { const base = '/some/path/file.txt'; diff --git a/src/vs/workbench/contrib/searchEditor/browser/constants.ts b/src/vs/workbench/contrib/searchEditor/browser/constants.ts index 419a2de8d1..17fbce33bd 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/constants.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/constants.ts @@ -5,27 +5,12 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -export const OpenInEditorCommandId = 'search.action.openInEditor'; -export const OpenNewEditorCommandId = 'search.action.openNewEditor'; -export const OpenNewEditorToSideCommandId = 'search.action.openNewEditorToSide'; -export const FocusQueryEditorWidgetCommandId = 'search.action.focusQueryEditorWidget'; - -export const ToggleSearchEditorCaseSensitiveCommandId = 'toggleSearchEditorCaseSensitive'; -export const ToggleSearchEditorWholeWordCommandId = 'toggleSearchEditorWholeWord'; -export const ToggleSearchEditorRegexCommandId = 'toggleSearchEditorRegex'; -export const ToggleSearchEditorContextLinesCommandId = 'toggleSearchEditorContextLines'; -export const IncreaseSearchEditorContextLinesCommandId = 'increaseSearchEditorContextLines'; -export const DecreaseSearchEditorContextLinesCommandId = 'decreaseSearchEditorContextLines'; - -export const RerunSearchEditorSearchCommandId = 'rerunSearchEditorSearch'; -export const CleanSearchEditorStateCommandId = 'cleanSearchEditorState'; -export const SelectAllSearchEditorMatchesCommandId = 'selectAllSearchEditorMatches'; - export const InSearchEditor = new RawContextKey('inSearchEditor', false); export const SearchEditorScheme = 'search-editor'; -export const SearchEditorBodyScheme = 'search-editor-body'; export const SearchEditorFindMatchClass = 'seaarchEditorFindMatch'; export const SearchEditorID = 'workbench.editor.searchEditor'; + +export const OpenNewEditorCommandId = 'search.action.openNewEditor'; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index c03135b085..cd12a4369f 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts @@ -8,9 +8,10 @@ import * as objects from 'vs/base/common/objects'; import { endsWith } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { Range } from 'vs/editor/common/core/range'; import { ToggleCaseSensitiveKeybinding, ToggleRegexKeybinding, ToggleWholeWordKeybinding } from 'vs/editor/contrib/find/findModel'; import { localize } from 'vs/nls'; -import { MenuId, SyncActionDescriptor, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -20,20 +21,36 @@ import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; -import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { Extensions as EditorInputExtensions, IEditorInputFactory, IEditorInputFactoryRegistry, ActiveEditorContext } from 'vs/workbench/common/editor'; +import { ActiveEditorContext, Extensions as EditorInputExtensions, IEditorInputFactory, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; +import { IViewsService } from 'vs/workbench/common/views'; +import { getSearchView } from 'vs/workbench/contrib/search/browser/searchActions'; +import { searchRefreshIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; import * as SearchConstants from 'vs/workbench/contrib/search/common/constants'; import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants'; import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; -import { modifySearchEditorContextLinesCommand, OpenSearchEditorAction, selectAllSearchEditorMatchesCommand, toggleSearchEditorCaseSensitiveCommand, toggleSearchEditorContextLinesCommand, toggleSearchEditorRegexCommand, toggleSearchEditorWholeWordCommand, createEditorFromSearchResult, openNewSearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions'; -import { getOrMakeSearchEditorInput, SearchEditorInput, SearchConfiguration } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { createEditorFromSearchResult, modifySearchEditorContextLinesCommand, openNewSearchEditor, selectAllSearchEditorMatchesCommand, toggleSearchEditorCaseSensitiveCommand, toggleSearchEditorContextLinesCommand, toggleSearchEditorRegexCommand, toggleSearchEditorWholeWordCommand } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions'; +import { getOrMakeSearchEditorInput, SearchConfiguration, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { parseSavedSearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; -import { Range } from 'vs/editor/common/core/range'; -import { searchRefreshIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; -import { IViewsService } from 'vs/workbench/common/views'; -import { getSearchView } from 'vs/workbench/contrib/search/browser/searchActions'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + + +const OpenInEditorCommandId = 'search.action.openInEditor'; +const OpenNewEditorToSideCommandId = 'search.action.openNewEditorToSide'; +const FocusQueryEditorWidgetCommandId = 'search.action.focusQueryEditorWidget'; + +const ToggleSearchEditorCaseSensitiveCommandId = 'toggleSearchEditorCaseSensitive'; +const ToggleSearchEditorWholeWordCommandId = 'toggleSearchEditorWholeWord'; +const ToggleSearchEditorRegexCommandId = 'toggleSearchEditorRegex'; +const ToggleSearchEditorContextLinesCommandId = 'toggleSearchEditorContextLines'; +const IncreaseSearchEditorContextLinesCommandId = 'increaseSearchEditorContextLines'; +const DecreaseSearchEditorContextLinesCommandId = 'decreaseSearchEditorContextLines'; + +const RerunSearchEditorSearchCommandId = 'rerunSearchEditorSearch'; +const CleanSearchEditorStateCommandId = 'cleanSearchEditorState'; +const SelectAllSearchEditorMatchesCommandId = 'selectAllSearchEditorMatches'; + + //#region Editor Descriptior Registry.as(EditorExtensions.Editors).registerEditor( @@ -128,28 +145,28 @@ Registry.as(EditorInputExtensions.EditorInputFactor //#region Commands KeybindingsRegistry.registerCommandAndKeybindingRule(objects.assign({ - id: SearchEditorConstants.ToggleSearchEditorCaseSensitiveCommandId, + id: ToggleSearchEditorCaseSensitiveCommandId, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor, SearchConstants.SearchInputBoxFocusedKey), handler: toggleSearchEditorCaseSensitiveCommand }, ToggleCaseSensitiveKeybinding)); KeybindingsRegistry.registerCommandAndKeybindingRule(objects.assign({ - id: SearchEditorConstants.ToggleSearchEditorWholeWordCommandId, + id: ToggleSearchEditorWholeWordCommandId, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor, SearchConstants.SearchInputBoxFocusedKey), handler: toggleSearchEditorWholeWordCommand }, ToggleWholeWordKeybinding)); KeybindingsRegistry.registerCommandAndKeybindingRule(objects.assign({ - id: SearchEditorConstants.ToggleSearchEditorRegexCommandId, + id: ToggleSearchEditorRegexCommandId, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor, SearchConstants.SearchInputBoxFocusedKey), handler: toggleSearchEditorRegexCommand }, ToggleRegexKeybinding)); KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: SearchEditorConstants.ToggleSearchEditorContextLinesCommandId, + id: ToggleSearchEditorContextLinesCommandId, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor), handler: toggleSearchEditorContextLinesCommand, @@ -158,7 +175,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ }); KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: SearchEditorConstants.IncreaseSearchEditorContextLinesCommandId, + id: IncreaseSearchEditorContextLinesCommandId, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor), handler: (accessor: ServicesAccessor) => modifySearchEditorContextLinesCommand(accessor, true), @@ -166,7 +183,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ }); KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: SearchEditorConstants.DecreaseSearchEditorContextLinesCommandId, + id: DecreaseSearchEditorContextLinesCommandId, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor), handler: (accessor: ServicesAccessor) => modifySearchEditorContextLinesCommand(accessor, false), @@ -174,7 +191,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ }); KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: SearchEditorConstants.SelectAllSearchEditorMatchesCommandId, + id: SelectAllSearchEditorMatchesCommandId, weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor), primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_L, @@ -182,7 +199,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ }); CommandsRegistry.registerCommand( - SearchEditorConstants.CleanSearchEditorStateCommandId, + CleanSearchEditorStateCommandId, (accessor: ServicesAccessor) => { const activeEditorPane = accessor.get(IEditorService).activeEditorPane; if (activeEditorPane instanceof SearchEditor) { @@ -192,18 +209,40 @@ CommandsRegistry.registerCommand( //#endregion //#region Actions -const registry = Registry.as(ActionExtensions.WorkbenchActions); const category = localize('search', "Search Editor"); -// TODO: Not an action2 becuase used in view pane container action bar, which uses actions -registry.registerWorkbenchAction( - SyncActionDescriptor.from(OpenSearchEditorAction), - 'Search Editor: Open New Search Editor', category); - registerAction2(class extends Action2 { constructor() { super({ - id: SearchEditorConstants.OpenInEditorCommandId, + id: SearchEditorConstants.OpenNewEditorCommandId, + title: localize('search.openNewSearchEditor', "Open new Search Editor"), + category, + f1: true, + }); + } + async run(accessor: ServicesAccessor, args: Partial) { + await accessor.get(IInstantiationService).invokeFunction(openNewSearchEditor, args); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: OpenNewEditorToSideCommandId, + title: localize('search.openNewEditorToSide', "Open new Search Editor to the Side"), + category, + f1: true, + }); + } + async run(accessor: ServicesAccessor, args: Partial) { + await accessor.get(IInstantiationService).invokeFunction(openNewSearchEditor, args, true); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: OpenInEditorCommandId, title: localize('search.openResultsInEditor', "Open Results in Editor"), category, f1: true, @@ -227,22 +266,7 @@ registerAction2(class extends Action2 { registerAction2(class extends Action2 { constructor() { super({ - id: SearchEditorConstants.OpenNewEditorToSideCommandId, - title: localize('search.openNewEditorToSide', "Open New Search Editor to Side"), - category, - f1: true, - }); - } - async run(accessor: ServicesAccessor) { - const instantiationService = accessor.get(IInstantiationService); - await instantiationService.invokeFunction(openNewSearchEditor, true); - } -}); - -registerAction2(class extends Action2 { - constructor() { - super({ - id: SearchEditorConstants.RerunSearchEditorSearchCommandId, + id: RerunSearchEditorSearchCommandId, title: localize('search.rerunSearchInEditor', "Search Again"), category, keybinding: { @@ -274,7 +298,7 @@ registerAction2(class extends Action2 { registerAction2(class extends Action2 { constructor() { super({ - id: SearchEditorConstants.FocusQueryEditorWidgetCommandId, + id: FocusQueryEditorWidgetCommandId, title: localize('search.action.focusQueryEditorWidget', "Focus Search Editor Input"), category, menu: { diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts index 71ae96c9b5..b0679dc802 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts @@ -13,13 +13,18 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { ILabelService } from 'vs/platform/label/common/label'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; -import * as Constants from 'vs/workbench/contrib/searchEditor/browser/constants'; import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; -import { getOrMakeSearchEditorInput, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; +import { getOrMakeSearchEditorInput, SearchEditorInput, SearchConfiguration } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { serializeSearchResultForEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; import { searchNewEditorIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IHistoryService } from 'vs/workbench/services/history/common/history'; +import { Schemas } from 'vs/base/common/network'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { OpenNewEditorCommandId } from 'vs/workbench/contrib/searchEditor/browser/constants'; export const toggleSearchEditorCaseSensitiveCommand = (accessor: ServicesAccessor) => { const editorService = accessor.get(IEditorService); @@ -71,7 +76,7 @@ export const selectAllSearchEditorMatchesCommand = (accessor: ServicesAccessor) export class OpenSearchEditorAction extends Action { - static readonly ID: string = Constants.OpenNewEditorCommandId; + static readonly ID: string = OpenNewEditorCommandId; static readonly LABEL = localize('search.openNewEditor', "Open New Search Editor"); constructor(id: string, label: string, @@ -94,12 +99,23 @@ export class OpenSearchEditorAction extends Action { } export const openNewSearchEditor = - async (accessor: ServicesAccessor, toSide = false) => { + async (accessor: ServicesAccessor, args: Partial = {}, toSide = false) => { const editorService = accessor.get(IEditorService); const telemetryService = accessor.get(ITelemetryService); const instantiationService = accessor.get(IInstantiationService); const configurationService = accessor.get(IConfigurationService); + const configurationResolverService = accessor.get(IConfigurationResolverService); + const workspaceContextService = accessor.get(IWorkspaceContextService); + const historyService = accessor.get(IHistoryService); + const activeWorkspaceRootUri = historyService.getLastActiveWorkspaceRoot(Schemas.file); + const lastActiveWorkspaceRoot = activeWorkspaceRootUri ? withNullAsUndefined(workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri)) : undefined; + + const resolvedArgs: Record = {}; + Object.entries(args).forEach(([name, value]) => { + resolvedArgs[name as any] = (typeof value === 'string') ? configurationResolverService.resolve(lastActiveWorkspaceRoot, value) : value; + }); + const activeEditorControl = editorService.activeTextEditorControl; let activeModel: ICodeEditor | undefined; let selected = ''; @@ -124,7 +140,7 @@ export const openNewSearchEditor = telemetryService.publicLog2('searchEditor/openNewSearchEditor'); - const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { config: { query: selected }, text: '' }); + const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { config: { query: selected, ...resolvedArgs }, text: '' }); const editor = await editorService.openEditor(input, { pinned: true }, toSide ? SIDE_GROUP : ACTIVE_GROUP) as SearchEditor; if (selected && configurationService.getValue('search').searchOnType) { diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts index f7b4b7936f..44297c4b90 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts @@ -36,7 +36,7 @@ import { IWorkingCopy, IWorkingCopyBackup, IWorkingCopyService, WorkingCopyCapab export type SearchConfiguration = { query: string, includes: string, - excludes: string + excludes: string, contextLines: number, wholeWord: boolean, caseSensitive: boolean, @@ -304,7 +304,7 @@ export const getOrMakeSearchEditorInput = ( const storageService = accessor.get(IStorageService); const configurationService = accessor.get(IConfigurationService); - const reuseOldSettings = configurationService.getValue('search').searchEditor?.experimental?.reusePriorSearchConfiguration; + const reuseOldSettings = configurationService.getValue('search').searchEditor?.reusePriorSearchConfiguration; const priorConfig: SearchConfiguration = reuseOldSettings ? new Memento(SearchEditorInput.ID, storageService).getMemento(StorageScope.WORKSPACE).searchConfig : {}; const defaultConfig = defaultSearchConfig(); let config = { ...defaultConfig, ...priorConfig, ...existingData.config }; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts index 22210db82e..fc1c81b1c8 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts @@ -114,7 +114,7 @@ const contentPatternToSearchConfiguration = (pattern: ITextQuery, includes: stri wholeWord: !!pattern.contentPattern.isWordMatch, excludes, includes, showIncludesExcludes: !!(includes || excludes || pattern?.userDisabledExcludesAndIgnoreFiles), - useIgnores: !!(pattern?.userDisabledExcludesAndIgnoreFiles === undefined ? undefined : !pattern.userDisabledExcludesAndIgnoreFiles), + useIgnores: (pattern?.userDisabledExcludesAndIgnoreFiles === undefined ? true : !pattern.userDisabledExcludesAndIgnoreFiles), contextLines, }; }; diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index f548e1145b..e02fd7e4e6 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -791,7 +791,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (quickOpenHistoryLimit === 0) { return; } - let keys = [...this._recentlyUsedTasks.keys()]; + let keys = this._recentlyUsedTasks.keys(); if (keys.length > quickOpenHistoryLimit) { keys = keys.slice(0, quickOpenHistoryLimit); } @@ -1579,7 +1579,9 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer for (const task of taskSet.tasks) { if (task.type !== this._providerTypes.get(handle)) { this._outputChannel.append(nls.localize('unexpectedTaskType', "The task provider for \"{0}\" tasks unexpectedly provided a task of type \"{1}\".\n", this._providerTypes.get(handle), task.type)); - this.showOutput(); + if ((task.type !== 'shell') && (task.type !== 'process')) { + this.showOutput(); + } break; } } @@ -2323,7 +2325,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer taskMap[key] = task; } }); - const reversed = [...recentlyUsedTasks.keys()].reverse(); + const reversed = recentlyUsedTasks.keys().reverse(); for (const key in reversed) { let task = taskMap[key]; if (task) { diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalBaseLinkProvider.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalBaseLinkProvider.ts new file mode 100644 index 0000000000..d27e704572 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalBaseLinkProvider.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ILinkProvider, ILink } from 'xterm'; +import { TerminalLink } from 'vs/workbench/contrib/terminal/browser/links/terminalLink'; + +export abstract class TerminalBaseLinkProvider implements ILinkProvider { + private _activeLinks: TerminalLink[] | undefined; + + async provideLinks(bufferLineNumber: number, callback: (links: ILink[] | undefined) => void): Promise { + this._activeLinks?.forEach(l => l.dispose); + this._activeLinks = await this._provideLinks(bufferLineNumber); + callback(this._activeLinks); + } + + protected abstract _provideLinks(bufferLineNumber: number): Promise | TerminalLink[]; +} diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLink.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLink.ts index d30b0187d9..ca5d743791 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLink.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLink.ts @@ -20,6 +20,9 @@ export const FOLDER_NOT_IN_WORKSPACE_LABEL = localize('openFolder', 'Open folder export class TerminalLink extends DisposableStore implements ILink { decorations: ILinkDecorations; + private _tooltipScheduler: RunOnceScheduler | undefined; + private _hoverListeners: DisposableStore | undefined; + private readonly _onLeave = new Emitter(); public get onLeave(): Event { return this._onLeave.event; } @@ -40,6 +43,14 @@ export class TerminalLink extends DisposableStore implements ILink { }; } + dispose(): void { + super.dispose(); + this._hoverListeners?.dispose(); + this._hoverListeners = undefined; + this._tooltipScheduler?.dispose(); + this._tooltipScheduler = undefined; + } + activate(event: MouseEvent | undefined, text: string): void { this._activateCallback(event, text); } @@ -58,20 +69,23 @@ export class TerminalLink extends DisposableStore implements ILink { })); const timeout = this._configurationService.getValue('editor.hover.delay'); - const scheduler = new RunOnceScheduler(() => { + this._tooltipScheduler = new RunOnceScheduler(() => { this._tooltipCallback( this, convertBufferRangeToViewport(this.range, this._viewportY), this._isHighConfidenceLink ? () => this._enableDecorations() : undefined, this._isHighConfidenceLink ? () => this._disableDecorations() : undefined ); - this.dispose(); + // Clear out scheduler until next hover event + this._tooltipScheduler?.dispose(); + this._tooltipScheduler = undefined; }, timeout); - this.add(scheduler); - scheduler.schedule(); + this.add(this._tooltipScheduler); + this._tooltipScheduler.schedule(); const origin = { x: event.pageX, y: event.pageY }; - this.add(dom.addDisposableListener(document, dom.EventType.MOUSE_MOVE, e => { + this._hoverListeners = new DisposableStore(); + this._hoverListeners.add(dom.addDisposableListener(document, dom.EventType.MOUSE_MOVE, e => { // Update decorations if (this._isModifierDown(e)) { this._enableDecorations(); @@ -83,14 +97,17 @@ export class TerminalLink extends DisposableStore implements ILink { if (Math.abs(e.pageX - origin.x) > window.devicePixelRatio * 2 || Math.abs(e.pageY - origin.y) > window.devicePixelRatio * 2) { origin.x = e.pageX; origin.y = e.pageY; - scheduler.schedule(); + this._tooltipScheduler?.schedule(); } })); } leave(): void { + this._hoverListeners?.dispose(); + this._hoverListeners = undefined; + this._tooltipScheduler?.dispose(); + this._tooltipScheduler = undefined; this._onLeave.fire(); - this.dispose(); } private _enableDecorations(): void { diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalProtocolLinkProvider.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalProtocolLinkProvider.ts index 5a0efcc353..8c3c038315 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalProtocolLinkProvider.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalProtocolLinkProvider.ts @@ -3,14 +3,15 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Terminal, IViewportRange, ILinkProvider, IBufferCellPosition, ILink, IBufferLine } from 'xterm'; +import { Terminal, IViewportRange, IBufferLine } from 'xterm'; import { ILinkComputerTarget, LinkComputer } from 'vs/editor/common/modes/linkComputer'; -import { getXtermLineContent, convertLinkRangeToBuffer, positionIsInRange } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers'; +import { getXtermLineContent, convertLinkRangeToBuffer } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers'; import { TerminalLink, OPEN_FILE_LABEL } from 'vs/workbench/contrib/terminal/browser/links/terminalLink'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; +import { TerminalBaseLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalBaseLinkProvider'; -export class TerminalProtocolLinkProvider implements ILinkProvider { +export class TerminalProtocolLinkProvider extends TerminalBaseLinkProvider { private _linkComputerTarget: ILinkComputerTarget | undefined; constructor( @@ -19,10 +20,11 @@ export class TerminalProtocolLinkProvider implements ILinkProvider { private readonly _tooltipCallback: (link: TerminalLink, viewportRange: IViewportRange, modifierDownCallback?: () => void, modifierUpCallback?: () => void) => void, @IInstantiationService private readonly _instantiationService: IInstantiationService ) { + super(); } - public provideLink(position: IBufferCellPosition, callback: (link: ILink | undefined) => void): void { - let startLine = position.y - 1; + protected _provideLinks(y: number): TerminalLink[] { + let startLine = y - 1; let endLine = startLine; const lines: IBufferLine[] = [ @@ -42,24 +44,16 @@ export class TerminalProtocolLinkProvider implements ILinkProvider { this._linkComputerTarget = new TerminalLinkAdapter(this._xterm, startLine, endLine); const links = LinkComputer.computeLinks(this._linkComputerTarget); - let found = false; - links.forEach(link => { + return links.map(link => { const range = convertLinkRangeToBuffer(lines, this._xterm.cols, link.range, startLine); // Check if the link if within the mouse position - if (positionIsInRange(position, range)) { - found = true; - const uri = link.url - ? (typeof link.url === 'string' ? URI.parse(link.url) : link.url) - : undefined; - const label = (uri?.scheme === 'file') ? OPEN_FILE_LABEL : undefined; - callback(this._instantiationService.createInstance(TerminalLink, range, link.url?.toString() || '', this._xterm.buffer.active.viewportY, this._activateCallback, this._tooltipCallback, true, label)); - } + const uri = link.url + ? (typeof link.url === 'string' ? URI.parse(link.url) : link.url) + : undefined; + const label = (uri?.scheme === 'file') ? OPEN_FILE_LABEL : undefined; + return this._instantiationService.createInstance(TerminalLink, range, link.url?.toString() || '', this._xterm.buffer.active.viewportY, this._activateCallback, this._tooltipCallback, true, label); }); - - if (!found) { - callback(undefined); - } } } diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts index 0bf1e769f1..738d036096 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts @@ -3,8 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Terminal, ILinkProvider, IViewportRange, IBufferCellPosition, ILink, IBufferLine } from 'xterm'; -import { getXtermLineContent, convertLinkRangeToBuffer, positionIsInRange } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers'; +import { Terminal, IViewportRange, IBufferLine } from 'xterm'; +import { getXtermLineContent, convertLinkRangeToBuffer } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers'; import { OperatingSystem } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { TerminalLink, OPEN_FILE_LABEL, FOLDER_IN_WORKSPACE_LABEL, FOLDER_NOT_IN_WORKSPACE_LABEL } from 'vs/workbench/contrib/terminal/browser/links/terminalLink'; @@ -14,6 +14,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { XtermLinkMatcherHandler } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager'; +import { TerminalBaseLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalBaseLinkProvider'; const pathPrefix = '(\\.\\.?|\\~)'; const pathSeparatorClause = '\\/'; @@ -41,7 +42,7 @@ const lineAndColumnClause = [ '(([^:\\s\\(\\)<>\'\"\\[\\]]*)(:(\\d+))?(:(\\d+))?)' // (file path):336, (file path):336:9 ].join('|').replace(/ /g, `[${'\u00A0'} ]`); -export class TerminalValidatedLocalLinkProvider implements ILinkProvider { +export class TerminalValidatedLocalLinkProvider extends TerminalBaseLinkProvider { constructor( private readonly _xterm: Terminal, private readonly _processOperatingSystem: OperatingSystem, @@ -54,10 +55,12 @@ export class TerminalValidatedLocalLinkProvider implements ILinkProvider { @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @IHostService private readonly _hostService: IHostService ) { + super(); } - async provideLink(position: IBufferCellPosition, callback: (link: ILink | undefined) => void) { - let startLine = position.y - 1; + protected async _provideLinks(y: number): Promise { + const result: TerminalLink[] = []; + let startLine = y - 1; let endLine = startLine; const lines: IBufferLine[] = [ @@ -121,34 +124,31 @@ export class TerminalValidatedLocalLinkProvider implements ILinkProvider { endLineNumber: 1 }, startLine); - if (positionIsInRange(position, bufferRange)) { - const validatedLink = await new Promise(r => { - this._validationCallback(link, (result) => { - if (result) { - const label = result.isDirectory - ? (this._isDirectoryInsideWorkspace(result.uri) ? FOLDER_IN_WORKSPACE_LABEL : FOLDER_NOT_IN_WORKSPACE_LABEL) - : OPEN_FILE_LABEL; - const activateCallback = this._wrapLinkHandler((event: MouseEvent | undefined, text: string) => { - if (result.isDirectory) { - this._handleLocalFolderLink(result.uri); - } else { - this._activateFileCallback(event, text); - } - }); - r(this._instantiationService.createInstance(TerminalLink, bufferRange, link, this._xterm.buffer.active.viewportY, activateCallback, this._tooltipCallback, true, label)); - } else { - r(undefined); - } - }); + const validatedLink = await new Promise(r => { + this._validationCallback(link, (result) => { + if (result) { + const label = result.isDirectory + ? (this._isDirectoryInsideWorkspace(result.uri) ? FOLDER_IN_WORKSPACE_LABEL : FOLDER_NOT_IN_WORKSPACE_LABEL) + : OPEN_FILE_LABEL; + const activateCallback = this._wrapLinkHandler((event: MouseEvent | undefined, text: string) => { + if (result.isDirectory) { + this._handleLocalFolderLink(result.uri); + } else { + this._activateFileCallback(event, text); + } + }); + r(this._instantiationService.createInstance(TerminalLink, bufferRange, link, this._xterm.buffer.active.viewportY, activateCallback, this._tooltipCallback, true, label)); + } else { + r(undefined); + } }); - if (validatedLink) { - callback(validatedLink); - return; - } + }); + if (validatedLink) { + result.push(validatedLink); } } - callback(undefined); + return result; } protected get _localLinkRegex(): RegExp { diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts index 0b22b6cc28..5af514b181 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Terminal, ILinkProvider, IViewportRange, IBufferCellPosition, ILink } from 'xterm'; +import { Terminal, IViewportRange } from 'xterm'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITerminalConfiguration, TERMINAL_CONFIG_SECTION } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalLink } from 'vs/workbench/contrib/terminal/browser/links/terminalLink'; @@ -15,8 +15,9 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { XtermLinkMatcherHandler } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager'; +import { TerminalBaseLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalBaseLinkProvider'; -export class TerminalWordLinkProvider implements ILinkProvider { +export class TerminalWordLinkProvider extends TerminalBaseLinkProvider { private readonly _fileQueryBuilder = this._instantiationService.createInstance(QueryBuilder); constructor( @@ -30,54 +31,49 @@ export class TerminalWordLinkProvider implements ILinkProvider { @ISearchService private readonly _searchService: ISearchService, @IEditorService private readonly _editorService: IEditorService ) { + super(); } - public provideLink(position: IBufferCellPosition, callback: (link: ILink | undefined) => void): void { - const start: IBufferCellPosition = { x: position.x, y: position.y }; - const end: IBufferCellPosition = { x: position.x, y: position.y }; - + protected _provideLinks(y: number): TerminalLink[] { // TODO: Support wrapping - // Expand to the left until a word separator is hit - const line = this._xterm.buffer.active.getLine(position.y - 1)!; - let text = ''; - start.x++; // The hovered cell is considered first - for (let x = position.x; x > 0; x--) { - const cell = line.getCell(x - 1); - if (!cell) { - break; - } - const char = cell.getChars(); - const config = this._configurationService.getValue(TERMINAL_CONFIG_SECTION); - if (cell.getWidth() !== 0 && config.wordSeparators.indexOf(char) >= 0) { - break; - } - start.x = x; - text = char + text; - } - - // No links were found (the hovered cell is whitespace) - if (text.length === 0) { - callback(undefined); - return; - } - - // Expand to the right until a word separator is hit - for (let x = position.x + 1; x <= line.length; x++) { - const cell = line.getCell(x - 1); - if (!cell) { - break; - } - const char = cell.getChars(); - const config = this._configurationService.getValue(TERMINAL_CONFIG_SECTION); - if (cell.getWidth() !== 0 && config.wordSeparators.indexOf(char) >= 0) { - break; - } - end.x = x; - text += char; - } - + // Dispose of all old links if new links are provides, links are only cached for the current line + const result: TerminalLink[] = []; + const wordSeparators = this._configurationService.getValue(TERMINAL_CONFIG_SECTION).wordSeparators; const activateCallback = this._wrapLinkHandler((_, link) => this._activate(link)); - callback(new TerminalLink({ start, end }, text, this._xterm.buffer.active.viewportY, activateCallback, this._tooltipCallback, false, localize('searchWorkspace', 'Search workspace'), this._configurationService)); + + const line = this._xterm.buffer.active.getLine(y - 1)!; + let text = ''; + let startX = -1; + const cellData = line.getCell(0)!; + for (let x = 0; x < line.length; x++) { + line.getCell(x, cellData); + const chars = cellData.getChars(); + const width = cellData.getWidth(); + + // Add a link if this is a separator + if (width !== 0 && wordSeparators.indexOf(chars) >= 0) { + if (startX !== -1) { + result.push(new TerminalLink({ start: { x: startX + 1, y }, end: { x, y } }, text, this._xterm.buffer.active.viewportY, activateCallback, this._tooltipCallback, false, localize('searchWorkspace', 'Search workspace'), this._configurationService)); + text = ''; + startX = -1; + } + continue; + } + + // Mark the start of a link if it hasn't started yet + if (startX === -1) { + startX = x; + } + + text += chars; + } + + // Add the final link if there is one + if (startX !== -1) { + result.push(new TerminalLink({ start: { x: startX + 1, y }, end: { x: line.length, y } }, text, this._xterm.buffer.active.viewportY, activateCallback, this._tooltipCallback, false, localize('searchWorkspace', 'Search workspace'), this._configurationService)); + } + + return result; } private async _activate(link: string) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index b294a23488..28d1b45971 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -34,7 +34,7 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; import { TerminalQuickAccessProvider } from 'vs/workbench/contrib/terminal/browser/terminalQuickAccess'; -import { terminalConfiguration, getTerminalShellConfiguration } from 'vs/workbench/contrib/terminal/common/terminalConfiguration'; +import { terminalConfiguration } from 'vs/workbench/contrib/terminal/common/terminalConfiguration'; import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility'; // Register services @@ -58,11 +58,6 @@ CommandsRegistry.registerCommand({ id: quickAccessNavigatePreviousInTerminalPick // Register configurations const configurationRegistry = Registry.as(Extensions.Configuration); configurationRegistry.registerConfiguration(terminalConfiguration); -if (platform.isWeb) { - // Desktop shell configuration are registered in electron-browser as their default values rely - // on process.env - configurationRegistry.registerConfiguration(getTerminalShellConfiguration()); -} // Register views const VIEW_CONTAINER = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ @@ -164,17 +159,6 @@ if (BrowserFeatures.clipboard.readText) { } } -if (platform.isWeb) { - // Register standard external terminal keybinding as integrated terminal when in web as the - // external terminal is not available - KeybindingsRegistry.registerKeybindingRule({ - id: TERMINAL_COMMAND_ID.NEW, - weight: KeybindingWeight.WorkbenchContrib, - when: undefined, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C - }); -} - // Delete word left: ctrl+w registerSendSequenceKeybinding(String.fromCharCode('W'.charCodeAt(0) - 64), { primary: KeyMod.CtrlCmd | KeyCode.Backspace, diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.web.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.web.contribution.ts new file mode 100644 index 0000000000..86374f84c9 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/terminal.web.contribution.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { TERMINAL_COMMAND_ID } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { getTerminalShellConfiguration } from 'vs/workbench/contrib/terminal/common/terminalConfiguration'; + +// Desktop shell configuration are registered in electron-browser as their default values rely +// on process.env +const configurationRegistry = Registry.as(Extensions.Configuration); +configurationRegistry.registerConfiguration(getTerminalShellConfiguration()); + +// Register standard external terminal keybinding as integrated terminal when in web as the +// external terminal is not available +KeybindingsRegistry.registerKeybindingRule({ + id: TERMINAL_COMMAND_ID.NEW, + weight: KeybindingWeight.WorkbenchContrib, + when: undefined, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C +}); diff --git a/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts b/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts index 8e405ca4fb..ebdde18852 100644 --- a/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts +++ b/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts @@ -34,6 +34,10 @@ export class TerminalHover extends Disposable implements ITerminalWidget { super(); } + dispose() { + super.dispose(); + } + attach(container: HTMLElement): void { const target = new CellHoverTarget(container, this._targetOptions); this._register(this._instantiationService.createInstance(HoverWidget, container, target, this._text, this._linkHandler, [])); diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index ad3dbf46cf..311df6f294 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -315,7 +315,7 @@ export const terminalConfiguration: IConfigurationNode = { 'terminal.integrated.experimentalLinkProvider': { description: localize('terminal.integrated.experimentalLinkProvider', "An experimental setting that aims to improve link detection in the terminal by improving when links are detected and by enabling shared link detection with the editor. Currently this only supports web links."), type: 'boolean', - default: false + default: true } } }; diff --git a/src/vs/workbench/contrib/terminal/test/browser/links/terminalProtocolLinkProvider.test.ts b/src/vs/workbench/contrib/terminal/test/browser/links/terminalProtocolLinkProvider.test.ts index 58f7a2229b..50ea0d8ce1 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/links/terminalProtocolLinkProvider.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/links/terminalProtocolLinkProvider.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { TerminalProtocolLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalProtocolLinkProvider'; -import { Terminal, ILink, IBufferRange, IBufferCellPosition } from 'xterm'; +import { Terminal, ILink } from 'xterm'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -18,77 +18,69 @@ suite('Workbench - TerminalWebLinkProvider', () => { instantiationService.stub(IConfigurationService, TestConfigurationService); }); - async function assertLink(text: string, expected: { text: string, range: [number, number][] }) { + async function assertLink(text: string, expected: { text: string, range: [number, number][] }[]) { const xterm = new Terminal(); const provider = instantiationService.createInstance(TerminalProtocolLinkProvider, xterm, () => { }, () => { }); // Write the text and wait for the parser to finish await new Promise(r => xterm.write(text, r)); - // Calculate positions just outside of link boundaries - const noLinkPositions: IBufferCellPosition[] = [ - { x: expected.range[0][0] - 1, y: expected.range[0][1] }, - { x: expected.range[1][0] + 1, y: expected.range[1][1] } - ]; - - // Ensure outside positions do not detect the link - for (let i = 0; i < noLinkPositions.length; i++) { - const link = await new Promise(r => provider.provideLink(noLinkPositions[i], r)); - assert.equal(link, undefined, `Just outside range boundary should not result in link, link found at (${link?.range.start.x}, ${link?.range.start.y}) to (${link?.range.end.x}, ${link?.range.end.y}) while checking (${noLinkPositions[i].x}, ${noLinkPositions[i].y})\nExpected link text=${expected.text}\nActual link text=${link?.text}`); - } - - // Convert range from [[startx, starty], [endx, endy]] to an IBufferRange - const linkRange: IBufferRange = { - start: { x: expected.range[0][0], y: expected.range[0][1] }, - end: { x: expected.range[1][0], y: expected.range[1][1] }, - }; - - // Calculate positions inside the link boundaries - const linkPositions: IBufferCellPosition[] = [ - linkRange.start, - linkRange.end - ]; - - // Ensure inside positions do detect the link - for (let i = 0; i < linkPositions.length; i++) { - const link = await new Promise(r => provider.provideLink(linkPositions[i], r)); - assert.deepEqual(link?.text, expected.text); - assert.deepEqual(link?.range, linkRange); - } + // Ensure all links are provided + const links = (await new Promise(r => provider.provideLinks(1, r)))!; + assert.equal(links.length, expected.length); + const actual = links.map(e => ({ + text: e.text, + range: e.range + })); + const expectedVerbose = expected.map(e => ({ + text: e.text, + range: { + start: { x: e.range[0][0], y: e.range[0][1] }, + end: { x: e.range[1][0], y: e.range[1][1] }, + } + })); + assert.deepEqual(actual, expectedVerbose); } // These tests are based on LinkComputer.test.ts test('LinkComputer cases', async () => { - await assertLink('x = "http://foo.bar";', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' }); - await assertLink('x = (http://foo.bar);', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' }); - await assertLink('x = \'http://foo.bar\';', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' }); - await assertLink('x = http://foo.bar ;', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' }); - await assertLink('x = ;', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' }); - await assertLink('x = {http://foo.bar};', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' }); - await assertLink('(see http://foo.bar)', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' }); - await assertLink('[see http://foo.bar]', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' }); - await assertLink('{see http://foo.bar}', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' }); - await assertLink('', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' }); - await assertLink('http://foo.bar', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' }); - await assertLink('// Click here to learn more. https://go.microsoft.com/fwlink/?LinkID=513275&clcid=0x409', { range: [[30, 1], [7, 2]], text: 'https://go.microsoft.com/fwlink/?LinkID=513275&clcid=0x409' }); - await assertLink('// Click here to learn more. https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx', { range: [[30, 1], [28, 2]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx' }); - await assertLink('// https://github.com/projectkudu/kudu/blob/master/Kudu.Core/Scripts/selectNodeVersion.js', { range: [[4, 1], [9, 2]], text: 'https://github.com/projectkudu/kudu/blob/master/Kudu.Core/Scripts/selectNodeVersion.js' }); - await assertLink('', { range: [[49, 1], [14, 2]], text: 'https://go.microsoft.com/fwlink/?LinkId=166007' }); - await assertLink('For instructions, see https://go.microsoft.com/fwlink/?LinkId=166007.', { range: [[23, 1], [68, 1]], text: 'https://go.microsoft.com/fwlink/?LinkId=166007' }); - await assertLink('For instructions, see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx.', { range: [[23, 1], [21, 2]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx' }); - await assertLink('x = "https://en.wikipedia.org/wiki/Zürich";', { range: [[6, 1], [41, 1]], text: 'https://en.wikipedia.org/wiki/Zürich' }); - await assertLink('請參閱 http://go.microsoft.com/fwlink/?LinkId=761051。', { range: [[8, 1], [53, 1]], text: 'http://go.microsoft.com/fwlink/?LinkId=761051' }); - await assertLink('(請參閱 http://go.microsoft.com/fwlink/?LinkId=761051)', { range: [[10, 1], [55, 1]], text: 'http://go.microsoft.com/fwlink/?LinkId=761051' }); - await assertLink('x = "file:///foo.bar";', { range: [[6, 1], [20, 1]], text: 'file:///foo.bar' }); - await assertLink('x = "file://c:/foo.bar";', { range: [[6, 1], [22, 1]], text: 'file://c:/foo.bar' }); - await assertLink('x = "file://shares/foo.bar";', { range: [[6, 1], [26, 1]], text: 'file://shares/foo.bar' }); - await assertLink('x = "file://shäres/foo.bar";', { range: [[6, 1], [26, 1]], text: 'file://shäres/foo.bar' }); - await assertLink('Some text, then http://www.bing.com.', { range: [[17, 1], [35, 1]], text: 'http://www.bing.com' }); - await assertLink('let url = `http://***/_api/web/lists/GetByTitle(\'Teambuildingaanvragen\')/items`;', { range: [[12, 1], [78, 1]], text: 'http://***/_api/web/lists/GetByTitle(\'Teambuildingaanvragen\')/items' }); - await assertLink('7. At this point, ServiceMain has been called. There is no functionality presently in ServiceMain, but you can consult the [MSDN documentation](https://msdn.microsoft.com/en-us/library/windows/desktop/ms687414(v=vs.85).aspx) to add functionality as desired!', { range: [[66, 2], [64, 3]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/ms687414(v=vs.85).aspx' }); - await assertLink('let x = "http://[::1]:5000/connect/token"', { range: [[10, 1], [40, 1]], text: 'http://[::1]:5000/connect/token' }); - await assertLink('2. Navigate to **https://portal.azure.com**', { range: [[18, 1], [41, 1]], text: 'https://portal.azure.com' }); - await assertLink('POST|https://portal.azure.com|2019-12-05|', { range: [[6, 1], [29, 1]], text: 'https://portal.azure.com' }); - await assertLink('aa https://foo.bar/[this is foo site] aa', { range: [[5, 1], [38, 1]], text: 'https://foo.bar/[this is foo site]' }); + await assertLink('x = "http://foo.bar";', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]); + await assertLink('x = (http://foo.bar);', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]); + await assertLink('x = \'http://foo.bar\';', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]); + await assertLink('x = http://foo.bar ;', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]); + await assertLink('x = ;', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]); + await assertLink('x = {http://foo.bar};', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]); + await assertLink('(see http://foo.bar)', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]); + await assertLink('[see http://foo.bar]', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]); + await assertLink('{see http://foo.bar}', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]); + await assertLink('', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]); + await assertLink('http://foo.bar', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]); + await assertLink('// Click here to learn more. https://go.microsoft.com/fwlink/?LinkID=513275&clcid=0x409', [{ range: [[30, 1], [7, 2]], text: 'https://go.microsoft.com/fwlink/?LinkID=513275&clcid=0x409' }]); + await assertLink('// Click here to learn more. https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx', [{ range: [[30, 1], [28, 2]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx' }]); + await assertLink('// https://github.com/projectkudu/kudu/blob/master/Kudu.Core/Scripts/selectNodeVersion.js', [{ range: [[4, 1], [9, 2]], text: 'https://github.com/projectkudu/kudu/blob/master/Kudu.Core/Scripts/selectNodeVersion.js' }]); + await assertLink('', [{ range: [[49, 1], [14, 2]], text: 'https://go.microsoft.com/fwlink/?LinkId=166007' }]); + await assertLink('For instructions, see https://go.microsoft.com/fwlink/?LinkId=166007.', [{ range: [[23, 1], [68, 1]], text: 'https://go.microsoft.com/fwlink/?LinkId=166007' }]); + await assertLink('For instructions, see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx.', [{ range: [[23, 1], [21, 2]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx' }]); + await assertLink('x = "https://en.wikipedia.org/wiki/Zürich";', [{ range: [[6, 1], [41, 1]], text: 'https://en.wikipedia.org/wiki/Zürich' }]); + await assertLink('請參閱 http://go.microsoft.com/fwlink/?LinkId=761051。', [{ range: [[8, 1], [53, 1]], text: 'http://go.microsoft.com/fwlink/?LinkId=761051' }]); + await assertLink('(請參閱 http://go.microsoft.com/fwlink/?LinkId=761051)', [{ range: [[10, 1], [55, 1]], text: 'http://go.microsoft.com/fwlink/?LinkId=761051' }]); + await assertLink('x = "file:///foo.bar";', [{ range: [[6, 1], [20, 1]], text: 'file:///foo.bar' }]); + await assertLink('x = "file://c:/foo.bar";', [{ range: [[6, 1], [22, 1]], text: 'file://c:/foo.bar' }]); + await assertLink('x = "file://shares/foo.bar";', [{ range: [[6, 1], [26, 1]], text: 'file://shares/foo.bar' }]); + await assertLink('x = "file://shäres/foo.bar";', [{ range: [[6, 1], [26, 1]], text: 'file://shäres/foo.bar' }]); + await assertLink('Some text, then http://www.bing.com.', [{ range: [[17, 1], [35, 1]], text: 'http://www.bing.com' }]); + await assertLink('let url = `http://***/_api/web/lists/GetByTitle(\'Teambuildingaanvragen\')/items`;', [{ range: [[12, 1], [78, 1]], text: 'http://***/_api/web/lists/GetByTitle(\'Teambuildingaanvragen\')/items' }]); + await assertLink('7. At this point, ServiceMain has been called. There is no functionality presently in ServiceMain, but you can consult the [MSDN documentation](https://msdn.microsoft.com/en-us/library/windows/desktop/ms687414(v=vs.85).aspx) to add functionality as desired!', [{ range: [[66, 2], [64, 3]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/ms687414(v=vs.85).aspx' }]); + await assertLink('let x = "http://[::1]:5000/connect/token"', [{ range: [[10, 1], [40, 1]], text: 'http://[::1]:5000/connect/token' }]); + await assertLink('2. Navigate to **https://portal.azure.com**', [{ range: [[18, 1], [41, 1]], text: 'https://portal.azure.com' }]); + await assertLink('POST|https://portal.azure.com|2019-12-05|', [{ range: [[6, 1], [29, 1]], text: 'https://portal.azure.com' }]); + await assertLink('aa https://foo.bar/[this is foo site] aa', [{ range: [[5, 1], [38, 1]], text: 'https://foo.bar/[this is foo site]' }]); + }); + + test('should support multiple link results', async () => { + await assertLink('http://foo.bar http://bar.foo', [ + { range: [[1, 1], [14, 1]], text: 'http://foo.bar' }, + { range: [[16, 1], [29, 1]], text: 'http://bar.foo' } + ]); }); }); diff --git a/src/vs/workbench/contrib/terminal/test/browser/links/terminalValidatedLocalLinkProvider.test.ts b/src/vs/workbench/contrib/terminal/test/browser/links/terminalValidatedLocalLinkProvider.test.ts index ad287a9841..954aa1a0d1 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/links/terminalValidatedLocalLinkProvider.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/links/terminalValidatedLocalLinkProvider.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { TerminalValidatedLocalLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider'; -import { Terminal, ILink, IBufferRange, IBufferCellPosition } from 'xterm'; +import { Terminal, ILink } from 'xterm'; import { OperatingSystem } from 'vs/base/common/platform'; import { format } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; @@ -76,43 +76,28 @@ suite('Workbench - TerminalValidatedLocalLinkProvider', () => { instantiationService.stub(IConfigurationService, TestConfigurationService); }); - async function assertLink(text: string, os: OperatingSystem, expected: { text: string, range: [number, number][] }) { + async function assertLink(text: string, os: OperatingSystem, expected: { text: string, range: [number, number][] }[]) { const xterm = new Terminal(); const provider = instantiationService.createInstance(TerminalValidatedLocalLinkProvider, xterm, os, () => { }, () => { }, () => { }, (_: string, cb: (result: { uri: URI, isDirectory: boolean } | undefined) => void) => { cb({ uri: URI.file('/'), isDirectory: false }); }); // Write the text and wait for the parser to finish await new Promise(r => xterm.write(text, r)); - // Calculate positions just outside of link boundaries - const noLinkPositions: IBufferCellPosition[] = [ - { x: expected.range[0][0] - 1, y: expected.range[0][1] }, - { x: expected.range[1][0] + 1, y: expected.range[1][1] } - ]; - - // Ensure outside positions do not detect the link - for (let i = 0; i < noLinkPositions.length; i++) { - const link = await new Promise(r => provider.provideLink(noLinkPositions[i], r)); - assert.equal(link, undefined, `Just outside range boundary should not result in link, link found at (${link?.range.start.x}, ${link?.range.start.y}) to (${link?.range.end.x}, ${link?.range.end.y}) while checking (${noLinkPositions[i].x}, ${noLinkPositions[i].y})\nExpected link text=${expected.text}\nActual link text=${link?.text}`); - } - - // Convert range from [[startx, starty], [endx, endy]] to an IBufferRange - const linkRange: IBufferRange = { - start: { x: expected.range[0][0], y: expected.range[0][1] }, - end: { x: expected.range[1][0], y: expected.range[1][1] }, - }; - - // Calculate positions inside the link boundaries - const linkPositions: IBufferCellPosition[] = [ - linkRange.start, - linkRange.end - ]; - - // Ensure inside positions do detect the link - for (let i = 0; i < linkPositions.length; i++) { - const link = await new Promise(r => provider.provideLink(linkPositions[i], r)); - assert.deepEqual(link?.text, expected.text); - assert.deepEqual(link?.range, linkRange); - } + // Ensure all links are provided + const links = (await new Promise(r => provider.provideLinks(1, r)))!; + assert.equal(links.length, expected.length); + const actual = links.map(e => ({ + text: e.text, + range: e.range + })); + const expectedVerbose = expected.map(e => ({ + text: e.text, + range: { + start: { x: e.range[0][0], y: e.range[0][1] }, + end: { x: e.range[1][0], y: e.range[1][1] }, + } + })); + assert.deepEqual(actual, expectedVerbose); } suite('Linux/macOS', () => { @@ -122,19 +107,21 @@ suite('Workbench - TerminalValidatedLocalLinkProvider', () => { const linkFormat = supportedLinkFormats[i]; test(`Format: ${linkFormat.urlFormat}`, async () => { const formattedLink = format(linkFormat.urlFormat, baseLink, linkFormat.line, linkFormat.column); - await assertLink(formattedLink, OperatingSystem.Linux, { text: formattedLink, range: [[1, 1], [formattedLink.length, 1]] }); - await assertLink(` ${formattedLink} `, OperatingSystem.Linux, { text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }); - await assertLink(`(${formattedLink})`, OperatingSystem.Linux, { text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }); - await assertLink(`[${formattedLink}]`, OperatingSystem.Linux, { text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }); + await assertLink(formattedLink, OperatingSystem.Linux, [{ text: formattedLink, range: [[1, 1], [formattedLink.length, 1]] }]); + await assertLink(` ${formattedLink} `, OperatingSystem.Linux, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]); + await assertLink(`(${formattedLink})`, OperatingSystem.Linux, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]); + await assertLink(`[${formattedLink}]`, OperatingSystem.Linux, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]); }); } }); }); test('Git diff links', async () => { - await assertLink(`diff --git a/foo/bar b/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[14, 1], [20, 1]] }); - await assertLink(`diff --git a/foo/bar b/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[24, 1], [30, 1]] }); - await assertLink(`--- a/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[7, 1], [13, 1]] }); - await assertLink(`+++ b/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[7, 1], [13, 1]] }); + await assertLink(`diff --git a/foo/bar b/foo/bar`, OperatingSystem.Linux, [ + { text: 'foo/bar', range: [[14, 1], [20, 1]] }, + { text: 'foo/bar', range: [[24, 1], [30, 1]] } + ]); + await assertLink(`--- a/foo/bar`, OperatingSystem.Linux, [{ text: 'foo/bar', range: [[7, 1], [13, 1]] }]); + await assertLink(`+++ b/foo/bar`, OperatingSystem.Linux, [{ text: 'foo/bar', range: [[7, 1], [13, 1]] }]); }); }); @@ -145,19 +132,28 @@ suite('Workbench - TerminalValidatedLocalLinkProvider', () => { const linkFormat = supportedLinkFormats[i]; test(`Format: ${linkFormat.urlFormat}`, async () => { const formattedLink = format(linkFormat.urlFormat, baseLink, linkFormat.line, linkFormat.column); - await assertLink(formattedLink, OperatingSystem.Windows, { text: formattedLink, range: [[1, 1], [formattedLink.length, 1]] }); - await assertLink(` ${formattedLink} `, OperatingSystem.Windows, { text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }); - await assertLink(`(${formattedLink})`, OperatingSystem.Windows, { text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }); - await assertLink(`[${formattedLink}]`, OperatingSystem.Windows, { text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }); + await assertLink(formattedLink, OperatingSystem.Windows, [{ text: formattedLink, range: [[1, 1], [formattedLink.length, 1]] }]); + await assertLink(` ${formattedLink} `, OperatingSystem.Windows, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]); + await assertLink(`(${formattedLink})`, OperatingSystem.Windows, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]); + await assertLink(`[${formattedLink}]`, OperatingSystem.Windows, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]); }); } }); }); test('Git diff links', async () => { - await assertLink(`diff --git a/foo/bar b/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[14, 1], [20, 1]] }); - await assertLink(`diff --git a/foo/bar b/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[24, 1], [30, 1]] }); - await assertLink(`--- a/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[7, 1], [13, 1]] }); - await assertLink(`+++ b/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[7, 1], [13, 1]] }); + await assertLink(`diff --git a/foo/bar b/foo/bar`, OperatingSystem.Linux, [ + { text: 'foo/bar', range: [[14, 1], [20, 1]] }, + { text: 'foo/bar', range: [[24, 1], [30, 1]] } + ]); + await assertLink(`--- a/foo/bar`, OperatingSystem.Linux, [{ text: 'foo/bar', range: [[7, 1], [13, 1]] }]); + await assertLink(`+++ b/foo/bar`, OperatingSystem.Linux, [{ text: 'foo/bar', range: [[7, 1], [13, 1]] }]); }); }); + + test('should support multiple link results', async () => { + await assertLink('./foo ./bar', OperatingSystem.Linux, [ + { range: [[1, 1], [5, 1]], text: './foo' }, + { range: [[7, 1], [11, 1]], text: './bar' } + ]); + }); }); diff --git a/src/vs/workbench/contrib/terminal/test/browser/links/terminalWordLinkProvider.test.ts b/src/vs/workbench/contrib/terminal/test/browser/links/terminalWordLinkProvider.test.ts index 3de22b0b3d..7352110c6a 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/links/terminalWordLinkProvider.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/links/terminalWordLinkProvider.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { Terminal, ILink, IBufferRange, IBufferCellPosition } from 'xterm'; +import { Terminal, ILink } from 'xterm'; import { TerminalWordLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; @@ -21,68 +21,62 @@ suite('Workbench - TerminalWordLinkProvider', () => { instantiationService.stub(IConfigurationService, configurationService); }); - async function assertLink(text: string, expected: { text: string, range: [number, number][] }) { + async function assertLink(text: string, expected: { text: string, range: [number, number][] }[]) { const xterm = new Terminal(); - const provider = instantiationService.createInstance(TerminalWordLinkProvider, xterm, () => { }, () => { }); + const provider: TerminalWordLinkProvider = instantiationService.createInstance(TerminalWordLinkProvider, xterm, () => { }, () => { }); // Write the text and wait for the parser to finish await new Promise(r => xterm.write(text, r)); - // Calculate positions just outside of link boundaries - const noLinkPositions: IBufferCellPosition[] = [ - { x: expected.range[0][0] - 1, y: expected.range[0][1] }, - { x: expected.range[1][0] + 1, y: expected.range[1][1] } - ]; - - // Ensure outside positions do not detect the link - for (let i = 0; i < noLinkPositions.length; i++) { - const link = await new Promise(r => provider.provideLink(noLinkPositions[i], r)); - assert.equal(link, undefined, `Just outside range boundary should not result in link, link found at (${link?.range.start.x}, ${link?.range.start.y}) to (${link?.range.end.x}, ${link?.range.end.y}) while checking (${noLinkPositions[i].x}, ${noLinkPositions[i].y})\nExpected link text=${expected.text}\nActual link text=${link?.text}`); - } - - // Convert range from [[startx, starty], [endx, endy]] to an IBufferRange - const linkRange: IBufferRange = { - start: { x: expected.range[0][0], y: expected.range[0][1] }, - end: { x: expected.range[1][0], y: expected.range[1][1] }, - }; - - // Calculate positions inside the link boundaries - const linkPositions: IBufferCellPosition[] = [ - linkRange.start, - linkRange.end - ]; - - // Ensure inside positions do detect the link - for (let i = 0; i < linkPositions.length; i++) { - const link = await new Promise(r => provider.provideLink(linkPositions[i], r)); - assert.deepEqual(link?.text, expected.text); - assert.deepEqual(link?.range, linkRange); - } + // Ensure all links are provided + const links = (await new Promise(r => provider.provideLinks(1, r)))!; + assert.equal(links.length, expected.length); + const actual = links.map(e => ({ + text: e.text, + range: e.range + })); + const expectedVerbose = expected.map(e => ({ + text: e.text, + range: { + start: { x: e.range[0][0], y: e.range[0][1] }, + end: { x: e.range[1][0], y: e.range[1][1] }, + } + })); + assert.deepEqual(actual, expectedVerbose); } test('should link words as defined by wordSeparators', async () => { await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ()[]' } }); - await assertLink('foo', { range: [[1, 1], [3, 1]], text: 'foo' }); - await assertLink(' foo ', { range: [[2, 1], [4, 1]], text: 'foo' }); - await assertLink('(foo)', { range: [[2, 1], [4, 1]], text: 'foo' }); - await assertLink('[foo]', { range: [[2, 1], [4, 1]], text: 'foo' }); - await assertLink('{foo}', { range: [[1, 1], [5, 1]], text: '{foo}' }); + await assertLink('foo', [{ range: [[1, 1], [3, 1]], text: 'foo' }]); + await assertLink('foo', [{ range: [[1, 1], [3, 1]], text: 'foo' }]); + await assertLink(' foo ', [{ range: [[2, 1], [4, 1]], text: 'foo' }]); + await assertLink('(foo)', [{ range: [[2, 1], [4, 1]], text: 'foo' }]); + await assertLink('[foo]', [{ range: [[2, 1], [4, 1]], text: 'foo' }]); + await assertLink('{foo}', [{ range: [[1, 1], [5, 1]], text: '{foo}' }]); await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ' } }); - await assertLink('foo', { range: [[1, 1], [3, 1]], text: 'foo' }); - await assertLink(' foo ', { range: [[2, 1], [4, 1]], text: 'foo' }); - await assertLink('(foo)', { range: [[1, 1], [5, 1]], text: '(foo)' }); - await assertLink('[foo]', { range: [[1, 1], [5, 1]], text: '[foo]' }); - await assertLink('{foo}', { range: [[1, 1], [5, 1]], text: '{foo}' }); + await assertLink('foo', [{ range: [[1, 1], [3, 1]], text: 'foo' }]); + await assertLink(' foo ', [{ range: [[2, 1], [4, 1]], text: 'foo' }]); + await assertLink('(foo)', [{ range: [[1, 1], [5, 1]], text: '(foo)' }]); + await assertLink('[foo]', [{ range: [[1, 1], [5, 1]], text: '[foo]' }]); + await assertLink('{foo}', [{ range: [[1, 1], [5, 1]], text: '{foo}' }]); }); test('should support wide characters', async () => { await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' []' } }); - await assertLink('aabbccdd.txt ', { range: [[1, 1], [12, 1]], text: 'aabbccdd.txt' }); - await assertLink('我是学生.txt ', { range: [[1, 1], [12, 1]], text: '我是学生.txt' }); - await assertLink(' aabbccdd.txt ', { range: [[2, 1], [13, 1]], text: 'aabbccdd.txt' }); - await assertLink(' 我是学生.txt ', { range: [[2, 1], [13, 1]], text: '我是学生.txt' }); - await assertLink(' [aabbccdd.txt] ', { range: [[3, 1], [14, 1]], text: 'aabbccdd.txt' }); - await assertLink(' [我是学生.txt] ', { range: [[3, 1], [14, 1]], text: '我是学生.txt' }); + await assertLink('aabbccdd.txt ', [{ range: [[1, 1], [12, 1]], text: 'aabbccdd.txt' }]); + await assertLink('我是学生.txt ', [{ range: [[1, 1], [12, 1]], text: '我是学生.txt' }]); + await assertLink(' aabbccdd.txt ', [{ range: [[2, 1], [13, 1]], text: 'aabbccdd.txt' }]); + await assertLink(' 我是学生.txt ', [{ range: [[2, 1], [13, 1]], text: '我是学生.txt' }]); + await assertLink(' [aabbccdd.txt] ', [{ range: [[3, 1], [14, 1]], text: 'aabbccdd.txt' }]); + await assertLink(' [我是学生.txt] ', [{ range: [[3, 1], [14, 1]], text: '我是学生.txt' }]); + }); + + test('should support multiple link results', async () => { + await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ' } }); + await assertLink('foo bar', [ + { range: [[1, 1], [3, 1]], text: 'foo' }, + { range: [[5, 1], [7, 1]], text: 'bar' } + ]); }); }); diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index 1a635634f5..7be2ee4a0e 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -100,7 +100,7 @@ class TimelineAggregate { return this.items[this.items.length - 1]; } - add(timeline: Timeline) { + add(timeline: Timeline, options: TimelineOptions) { let updated = false; if (timeline.items.length !== 0 && this.items.length !== 0) { @@ -139,7 +139,10 @@ class TimelineAggregate { this.items.push(...timeline.items); } - this._cursor = timeline.paging?.cursor; + // If we are not requesting more recent items than we have, then update the cursor + if (options.cursor !== undefined || typeof options.limit !== 'object') { + this._cursor = timeline.paging?.cursor; + } if (updated) { this.items.sort( @@ -626,7 +629,7 @@ export class TimelinePane extends ViewPane { updated = true; } else { - updated = timeline.add(response); + updated = timeline.add(response, request.options); } if (updated) { @@ -923,6 +926,10 @@ export class TimelinePane extends ViewPane { } private loadMore(item: LoadMoreCommand) { + if (item.loading) { + return; + } + item.loading = true; this.tree.rerender(item); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts index 15db362ea2..0caf8d6249 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts @@ -7,10 +7,10 @@ import { IUserDataSyncService, IUserDataSyncLogService, IUserDataSyncEnablementS import { Event } from 'vs/base/common/event'; import { UserDataAutoSyncService as BaseUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; 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'; import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger'; export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 75e9b19c6b..7c4f6086f0 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -8,7 +8,6 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import { canceled, isPromiseCanceledError } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, dispose, MutableDisposable, toDisposable, IDisposable } from 'vs/base/common/lifecycle'; -import { isWeb } from 'vs/base/common/platform'; import { isEqual, basename } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -39,7 +38,6 @@ import { IEditorInput, toResource, SideBySideEditor } from 'vs/workbench/common/ import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; import { IOutputService } from 'vs/workbench/contrib/output/common/output'; -import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger'; import { IActivityService, IBadge, NumberBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -81,9 +79,10 @@ const resolveSettingsConflictsCommand = { id: 'workbench.userData.actions.resolv const resolveKeybindingsConflictsCommand = { id: 'workbench.userData.actions.resolveKeybindingsConflicts', title: localize('showKeybindingsConflicts', "Preferences Sync: Show Keybindings Conflicts") }; const resolveSnippetsConflictsCommand = { id: 'workbench.userData.actions.resolveSnippetsConflicts', title: localize('showSnippetsConflicts', "Preferences Sync: Show User Snippets Conflicts") }; const configureSyncCommand = { id: 'workbench.userData.actions.configureSync', title: localize('configure sync', "Preferences Sync: Configure...") }; -const showSyncActivityCommand = { - id: 'workbench.userData.actions.showSyncActivity', - title: localize('show sync log', "Preferences Sync: Show Log"), +const showSyncActivityCommand = { id: 'workbench.userData.actions.showSyncActivity', title: localize('show sync log', "Preferences Sync: Show Log") }; +const syncNowCommand = { + id: 'workbench.userData.actions.syncNow', + title: localize('sync now', "Preferences Sync: Sync Now"), description(userDataSyncService: IUserDataSyncService): string | undefined { if (userDataSyncService.status === SyncStatus.Syncing) { return localize('sync is on with syncing', "syncing"); @@ -97,7 +96,7 @@ const showSyncActivityCommand = { const showSyncSettingsCommand = { id: 'workbench.userData.actions.syncSettings', title: localize('sync settings', "Preferences Sync: Show Settings"), }; const CONTEXT_TURNING_ON_STATE = new RawContextKey('userDataSyncTurningOn', false); -const CONTEXT_ACCOUNT_STATE = new RawContextKey('userDataSyncAccountStatus', AccountStatus.Uninitialized); +export const CONTEXT_ACCOUNT_STATE = new RawContextKey('userDataSyncAccountStatus', AccountStatus.Uninitialized); export class UserDataSyncWorkbenchContribution extends Disposable implements IWorkbenchContribution { @@ -161,10 +160,6 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo textModelResolverService.registerTextModelContentProvider(USER_DATA_SYNC_SCHEME, instantiationService.createInstance(UserDataRemoteContentProvider)); registerEditorContribution(AcceptChangesContribution.ID, AcceptChangesContribution); - - if (!isWeb) { - this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(source => userDataAutoSyncService.triggerAutoSync([source]))); - } } } @@ -894,6 +889,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private registerSyncStatusAction(): void { const that = this; const when = ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Available), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized)); + this.registerSyncNowAction(); this._register(registerAction2(class SyncStatusAction extends Action2 { constructor() { super({ @@ -946,8 +942,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } items.push({ id: configureSyncCommand.id, label: configureSyncCommand.title }); items.push({ id: showSyncSettingsCommand.id, label: showSyncSettingsCommand.title }); - items.push({ id: showSyncActivityCommand.id, label: showSyncActivityCommand.title, description: showSyncActivityCommand.description(that.userDataSyncService) }); + items.push({ id: showSyncActivityCommand.id, label: showSyncActivityCommand.title }); items.push({ type: 'separator' }); + items.push({ id: syncNowCommand.id, label: syncNowCommand.title, description: syncNowCommand.description(that.userDataSyncService) }); if (that.userDataSyncEnablementService.canToggleEnablement()) { const account = that.userDataSyncAccounts.current; items.push({ id: stopSyncCommand.id, label: stopSyncCommand.title, description: account ? `${account.accountName} (${that.authenticationService.getDisplayName(account.authenticationProviderId)})` : undefined }); @@ -969,6 +966,21 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo })); } + private registerSyncNowAction(): void { + const that = this; + this._register(registerAction2(class SyncNowAction extends Action2 { + constructor() { + super({ + id: syncNowCommand.id, + title: syncNowCommand.title, + }); + } + run(): Promise { + return that.userDataSyncService.sync(); + } + })); + } + private registerTurnOffSyncAction(): void { const that = this; this._register(registerAction2(class StopSyncAction extends Action2 { diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts index bcf6ef7ef1..a19fa782bd 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts @@ -9,10 +9,10 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic 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'; +import { IViewsService } from 'vs/workbench/common/views'; +import { VIEW_CONTAINER_ID } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncView'; export class UserDataSyncTrigger extends Disposable { @@ -22,24 +22,15 @@ export class UserDataSyncTrigger extends Disposable { constructor( @IEditorService editorService: IEditorService, @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, - @IViewletService viewletService: IViewletService, + @IViewsService viewsService: IViewsService, ) { super(); - this._register(Event.any( - Event.map(editorService.onDidActiveEditorChange, () => this.getUserDataEditorInputSource(editorService.activeEditor)), - Event.map(viewletService.onDidViewletOpen, viewlet => this.getUserDataViewletSource(viewlet)) - )(source => { - if (source) { - this._onDidTriggerSync.fire(source); - } - })); - } - - private getUserDataViewletSource(viewlet: IViewlet): string | undefined { - if (viewlet.getId() === VIEWLET_ID) { - return 'extensionsViewlet'; - } - return undefined; + this._register( + Event.filter( + Event.any( + Event.map(editorService.onDidActiveEditorChange, () => this.getUserDataEditorInputSource(editorService.activeEditor)), + Event.map(Event.filter(viewsService.onDidChangeViewContainerVisibility, e => [VIEWLET_ID, VIEW_CONTAINER_ID].includes(e.id) && e.visible), e => e.id) + ), source => source !== undefined)(source => this._onDidTriggerSync.fire(source!))); } private getUserDataEditorInputSource(editorInput: IEditorInput | undefined): string | undefined { diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts index 06dc2cb2fc..d79ea42f0a 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts @@ -10,9 +10,9 @@ import { localize } from 'vs/nls'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { TreeViewPane, TreeView } from 'vs/workbench/browser/parts/views/treeView'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { ALL_SYNC_RESOURCES, CONTEXT_SYNC_ENABLEMENT, SyncResource, IUserDataSyncService, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync'; +import { ALL_SYNC_RESOURCES, SyncResource, IUserDataSyncService, ISyncResourceHandle, CONTEXT_SYNC_STATE, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync'; import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; -import { IContextKeyService, RawContextKey, ContextKeyExpr, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, RawContextKey, ContextKeyExpr, ContextKeyEqualsExpr, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { URI } from 'vs/base/common/uri'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { FolderThemeIcon } from 'vs/platform/theme/common/themeService'; @@ -20,38 +20,45 @@ import { fromNow } from 'vs/base/common/date'; import { pad, uppercaseFirstLetter } from 'vs/base/common/strings'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { Codicon } from 'vs/base/common/codicons'; +import { CONTEXT_ACCOUNT_STATE } from 'vs/workbench/contrib/userDataSync/browser/userDataSync'; +import { AccountStatus } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncAccount'; + +export const VIEW_CONTAINER_ID = 'workbench.view.sync'; +const CONTEXT_ENABLE_VIEWS = new RawContextKey(`showUserDataSyncViews`, false); export class UserDataSyncViewContribution implements IWorkbenchContribution { + private readonly viewsEnablementContext: IContextKey; + constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, ) { const container = this.registerSyncViewContainer(); - this.registerBackupView(container, true); - this.registerBackupView(container, false); + this.viewsEnablementContext = CONTEXT_ENABLE_VIEWS.bindTo(this.contextKeyService); + this.registerView(container, true); + this.registerView(container, false); } private registerSyncViewContainer(): ViewContainer { return Registry.as(Extensions.ViewContainersRegistry).registerViewContainer( { - id: 'workbench.view.sync', + id: VIEW_CONTAINER_ID, name: localize('sync preferences', "Preferences Sync"), ctorDescriptor: new SyncDescriptor( ViewPaneContainer, - ['workbench.view.sync', { mergeViewWithContainerWhenSingleView: true }] + [VIEW_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true }] ), icon: Codicon.sync.classNames, hideIfEmpty: true, }, ViewContainerLocation.Sidebar); } - private registerBackupView(container: ViewContainer, remote: boolean): void { - const id = `workbench.views.sync.${remote ? 'remote' : 'local'}BackupView`; - const name = remote ? localize('remote title', "Remote Backup") : localize('local title', "Local Backup"); - const contextKey = new RawContextKey(`showUserDataSync${remote ? 'Remote' : 'Local'}BackupView`, false); - const viewEnablementContext = contextKey.bindTo(this.contextKeyService); + private registerView(container: ViewContainer, remote: boolean): void { + const that = this; + const id = `workbench.views.sync.${remote ? 'remote' : 'local'}DataView`; + const name = remote ? localize('remote title', "Remote Data") : localize('local title', "Local Backup"); const treeView = this.instantiationService.createInstance(TreeView, id, name); treeView.showCollapseAllAction = true; treeView.showRefreshAction = true; @@ -66,7 +73,7 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution { id, name, ctorDescriptor: new SyncDescriptor(TreeViewPane), - when: ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, contextKey), + when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Available), CONTEXT_ENABLE_VIEWS), canToggleVisibility: true, canMoveView: true, treeView, @@ -77,21 +84,21 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution { registerAction2(class extends Action2 { constructor() { super({ - id: `workbench.actions.showSync${remote ? 'Remote' : 'Local'}BackupView`, + id: `workbench.actions.showSync${remote ? 'Remote' : 'Local'}DataView`, title: remote ? - { value: localize('workbench.action.showSyncRemoteBackup', "Show Remote Backup"), original: `Show Remote Backup` } + { value: localize('workbench.action.showSyncRemoteBackup', "Show Remote Data"), original: `Show Remote Data` } : { value: localize('workbench.action.showSyncLocalBackup', "Show Local Backup"), original: `Show Local Backup` }, category: { value: localize('sync preferences', "Preferences Sync"), original: `Preferences Sync` }, menu: { id: MenuId.CommandPalette, - when: CONTEXT_SYNC_ENABLEMENT + when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Available)), }, }); } async run(accessor: ServicesAccessor): Promise { const viewDescriptorService = accessor.get(IViewDescriptorService); const viewsService = accessor.get(IViewsService); - viewEnablementContext.set(true); + that.viewsEnablementContext.set(true); const viewContainer = viewDescriptorService.getViewContainerByViewId(id); if (viewContainer) { const model = viewDescriptorService.getViewContainerModel(viewContainer); @@ -105,7 +112,6 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution { } }); } - } } }); diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts similarity index 76% rename from src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts rename to src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts index 2ced95292e..4591e6f20d 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts +++ b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts @@ -7,8 +7,9 @@ import { IUserDataAutoSyncService, UserDataSyncError } from 'vs/platform/userDat import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { Disposable } from 'vs/base/common/lifecycle'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Event } from 'vs/base/common/event'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger'; export class UserDataAutoSyncService extends Disposable implements IUserDataAutoSyncService { @@ -18,16 +19,17 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto get onError(): Event { return Event.map(this.channel.listen('onError'), e => UserDataSyncError.toUserDataSyncError(e)); } constructor( + @IInstantiationService instantiationService: IInstantiationService, @ISharedProcessService sharedProcessService: ISharedProcessService ) { super(); this.channel = sharedProcessService.getChannel('userDataAutoSync'); + this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(source => this.triggerAutoSync([source]))); } triggerAutoSync(sources: string[]): Promise { + return this.channel.call('triggerAutoSync', [sources]); } } - -registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService); diff --git a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts index 72824edbc6..4a12443b29 100644 --- a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts @@ -168,6 +168,9 @@ export abstract class BaseWebview extends Disposable { private readonly _onDidFocus = this._register(new Emitter()); public readonly onDidFocus = this._onDidFocus.event; + private readonly _onDidBlur = this._register(new Emitter()); + public readonly onDidBlur = this._onDidBlur.event; + public sendMessage(data: any): void { this._send('message', data); } @@ -267,6 +270,8 @@ export abstract class BaseWebview extends Disposable { this._focused = isFocused; if (isFocused) { this._onDidFocus.fire(); + } else { + this._onDidBlur.fire(); } } diff --git a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts index ddf353d893..ab351c8622 100644 --- a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts +++ b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts @@ -125,6 +125,7 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv // Forward events from inner webview to outer listeners this._webviewEvents.clear(); this._webviewEvents.add(webview.onDidFocus(() => { this._onDidFocus.fire(); })); + this._webviewEvents.add(webview.onDidBlur(() => { this._onDidBlur.fire(); })); this._webviewEvents.add(webview.onDidClickLink(x => { this._onDidClickLink.fire(x); })); this._webviewEvents.add(webview.onMessage(x => { this._onMessage.fire(x); })); this._webviewEvents.add(webview.onMissingCsp(x => { this._onMissingCsp.fire(x); })); @@ -182,6 +183,9 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv private readonly _onDidFocus = this._register(new Emitter()); public readonly onDidFocus: Event = this._onDidFocus.event; + private readonly _onDidBlur = this._register(new Emitter()); + public readonly onDidBlur: Event = this._onDidBlur.event; + private readonly _onDidClickLink = this._register(new Emitter()); public readonly onDidClickLink: Event = this._onDidClickLink.event; diff --git a/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index 4a793bca73..330d8dba3d 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -79,6 +79,7 @@ export interface Webview extends IDisposable { state: string | undefined; readonly onDidFocus: Event; + readonly onDidBlur: Event; readonly onDidClickLink: Event; readonly onDidScroll: Event<{ scrollYPercentage: number }>; readonly onDidWheel: Event; diff --git a/src/vs/workbench/electron-browser/desktop.main.ts b/src/vs/workbench/electron-browser/desktop.main.ts index e3cabe14de..f764ad1970 100644 --- a/src/vs/workbench/electron-browser/desktop.main.ts +++ b/src/vs/workbench/electron-browser/desktop.main.ts @@ -20,9 +20,8 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { KeyboardMapperFactory } from 'vs/workbench/services/keybinding/electron-browser/nativeKeymapService'; import { INativeWindowConfiguration } from 'vs/platform/windows/node/window'; import { ISingleFolderWorkspaceIdentifier, IWorkspaceInitializationPayload, ISingleFolderWorkspaceInitializationPayload, reviveWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; -import { ConsoleLogService, MultiplexLogService, ILogService, ConsoleLogInMainService } from 'vs/platform/log/common/log'; +import { ILogService } from 'vs/platform/log/common/log'; import { NativeStorageService } from 'vs/platform/storage/node/storageService'; -import { LoggerChannelClient, FollowerLogService } from 'vs/platform/log/common/logIpc'; import { Schemas } from 'vs/base/common/network'; import { sanitizeFilePath } from 'vs/base/common/extpath'; import { GlobalStorageDatabaseChannelClient } from 'vs/platform/storage/node/storageIpc'; @@ -41,7 +40,6 @@ import { IFileService } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider } from 'vs/platform/files/electron-browser/diskFileSystemProvider'; import { RemoteFileSystemProvider } from 'vs/workbench/services/remote/common/remoteAgentFileSystemChannel'; import { ConfigurationCache } from 'vs/workbench/services/configuration/node/configurationCache'; -import { SpdLogService } from 'vs/platform/log/node/spdlogService'; import { SignService } from 'vs/platform/sign/node/signService'; import { ISignService } from 'vs/platform/sign/common/sign'; import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider'; @@ -50,6 +48,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import product from 'vs/platform/product/common/product'; import { NativeResourceIdentityService } from 'vs/platform/resource/node/resourceIdentityServiceImpl'; import { IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; +import { DesktopLogService } from 'vs/workbench/services/log/electron-browser/logService'; class DesktopMain extends Disposable { @@ -181,7 +180,7 @@ class DesktopMain extends Disposable { serviceCollection.set(IProductService, { _serviceBrand: undefined, ...product }); // Log - const logService = this._register(this.createLogService(mainProcessService, this.environmentService)); + const logService = this._register(new DesktopLogService(this.configuration.windowId, mainProcessService, this.environmentService)); serviceCollection.set(ILogService, logService); // Remote @@ -203,7 +202,7 @@ class DesktopMain extends Disposable { fileService.registerProvider(Schemas.file, diskFileSystemProvider); // User Data Provider - fileService.registerProvider(Schemas.userData, new FileUserDataProvider(this.environmentService.appSettingsHome, this.environmentService.backupHome, diskFileSystemProvider, this.environmentService)); + fileService.registerProvider(Schemas.userData, new FileUserDataProvider(this.environmentService.appSettingsHome, this.environmentService.backupHome, diskFileSystemProvider, this.environmentService, logService)); const connection = remoteAgentService.getConnection(); if (connection) { @@ -314,27 +313,6 @@ class DesktopMain extends Disposable { } } - private createLogService(mainProcessService: IMainProcessService, environmentService: IWorkbenchEnvironmentService): ILogService { - const loggerClient = new LoggerChannelClient(mainProcessService.getChannel('logger')); - - // Extension development test CLI: forward everything to main side - const loggers: ILogService[] = []; - if (environmentService.isExtensionDevelopment && !!environmentService.extensionTestsLocationURI) { - loggers.push( - new ConsoleLogInMainService(loggerClient, this.environmentService.configuration.logLevel) - ); - } - - // Normal logger: spdylog and console - else { - loggers.push( - new ConsoleLogService(this.environmentService.configuration.logLevel), - new SpdLogService(`renderer${this.configuration.windowId}`, environmentService.logsPath, this.environmentService.configuration.logLevel) - ); - } - - return new FollowerLogService(loggerClient, new MultiplexLogService(loggers)); - } } export function main(configuration: INativeWindowConfiguration): Promise { diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index fe3957ea64..c7fb5fb2a4 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -426,8 +426,20 @@ export class NativeWindow extends Disposable { this.updateTouchbarMenu(); // Crash reporter (if enabled) - if (!this.environmentService.disableCrashReporter && product.crashReporter && product.appCenter && this.configurationService.getValue('telemetry.enableCrashReporter')) { - this.setupCrashReporter(product.crashReporter.companyName, product.crashReporter.productName, product.appCenter); + if (!this.environmentService.disableCrashReporter && this.configurationService.getValue('telemetry.enableCrashReporter')) { + const companyName = product.crashReporter?.companyName || 'Microsoft'; + const productName = product.crashReporter?.productName || product.nameShort; + + // With appCenter enabled, crashes will be uploaded + if (product.appCenter) { + this.setupCrashReporter(companyName, productName, product.appCenter, undefined); + } + + // With a provided crash reporter directory, crashes + // will be stored only locally in that folder + else if (this.environmentService.crashReporterDirectory) { + this.setupCrashReporter(companyName, productName, undefined, this.environmentService.crashReporterDirectory); + } } } @@ -540,13 +552,14 @@ export class NativeWindow extends Disposable { } } - private async setupCrashReporter(companyName: string, productName: string, appCenterConfig: typeof product.appCenter): Promise { - if (!appCenterConfig) { - return; + private async setupCrashReporter(companyName: string, productName: string, appCenter: typeof product.appCenter, crashesDirectory: undefined): Promise; + private async setupCrashReporter(companyName: string, productName: string, appCenter: undefined, crashesDirectory: string): Promise; + private async setupCrashReporter(companyName: string, productName: string, appCenter: typeof product.appCenter | undefined, crashesDirectory: string | undefined): Promise { + let submitURL: string | undefined = undefined; + if (appCenter) { + submitURL = isWindows ? appCenter[process.arch === 'ia32' ? 'win32-ia32' : 'win32-x64'] : isLinux ? appCenter[`linux-x64`] : appCenter.darwin; } - const appCenterURL = isWindows ? appCenterConfig[process.arch === 'ia32' ? 'win32-ia32' : 'win32-x64'] - : isLinux ? appCenterConfig[`linux-x64`] : appCenterConfig.darwin; const info = await this.telemetryService.getTelemetryInfo(); const crashReporterId = this.storageService.get(crashReporterIdStorageKey, StorageScope.GLOBAL)!; @@ -554,11 +567,14 @@ export class NativeWindow extends Disposable { const options: CrashReporterStartOptions = { companyName, productName, - submitURL: appCenterURL.concat('&uid=', crashReporterId, '&iid=', crashReporterId, '&sid=', info.sessionId), + submitURL: (submitURL?.concat('&uid=', crashReporterId, '&iid=', crashReporterId, '&sid=', info.sessionId)) || '', extra: { vscode_version: product.version, vscode_commit: product.commit || '' - } + }, + + // If `crashesDirectory` is specified, we do not upload + uploadToServer: !crashesDirectory, }; // start crash reporter in the main process first. diff --git a/src/vs/workbench/services/activityBar/browser/activityBarService.ts b/src/vs/workbench/services/activityBar/browser/activityBarService.ts index 3079d4e2dd..89cd1e7515 100644 --- a/src/vs/workbench/services/activityBar/browser/activityBarService.ts +++ b/src/vs/workbench/services/activityBar/browser/activityBarService.ts @@ -26,4 +26,9 @@ export interface IActivityBarService { * Returns id of visible viewlets following the visual order. */ getVisibleViewContainerIds(): string[]; + + /** + * Focuses the activity bar. + */ + focusActivityBar(): void; } diff --git a/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts b/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts index 9f9b2361d5..cda6964a8d 100644 --- a/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts +++ b/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts @@ -67,7 +67,7 @@ export class NodeTestBackupFileService extends BackupFileService { const fileService = new FileService(logService); const diskFileSystemProvider = new DiskFileSystemProvider(logService); fileService.registerProvider(Schemas.file, diskFileSystemProvider); - fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService)); + fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService, logService)); super(environmentService, fileService, logService); diff --git a/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts index e15a2b6c08..d9bd665578 100644 --- a/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts @@ -110,7 +110,7 @@ suite('ConfigurationEditingService', () => { const fileService = new FileService(new NullLogService()); const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService()); fileService.registerProvider(Schemas.file, diskFileSystemProvider); - fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService)); + fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService, new NullLogService())); instantiationService.stub(IFileService, fileService); instantiationService.stub(IRemoteAgentService, remoteAgentService); const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService); diff --git a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts index c53f687530..eaac001e54 100644 --- a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts @@ -110,7 +110,7 @@ suite.skip('WorkspaceContextService - Folder', () => { // {{SQL CARBON EDIT}} sk const fileService = new FileService(new NullLogService()); const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService()); fileService.registerProvider(Schemas.file, diskFileSystemProvider); - fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, new DiskFileSystemProvider(new NullLogService()), environmentService)); + fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, new DiskFileSystemProvider(new NullLogService()), environmentService, new NullLogService())); workspaceContextService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, new RemoteAgentService(environmentService, new RemoteAuthorityResolverService(), new SignService(undefined), new NullLogService())); return (workspaceContextService).initialize(convertToWorkspacePayload(URI.file(folderDir))); }); @@ -176,7 +176,7 @@ suite.skip('WorkspaceContextService - Workspace', () => { // {{SQL CARBON EDIT}} const fileService = new FileService(new NullLogService()); const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService()); fileService.registerProvider(Schemas.file, diskFileSystemProvider); - fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService)); + fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService, new NullLogService())); const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService); instantiationService.stub(IWorkspaceContextService, workspaceService); @@ -236,7 +236,7 @@ suite.skip('WorkspaceContextService - Workspace Editing', () => { // {{SQL CARBO const fileService = new FileService(new NullLogService()); const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService()); fileService.registerProvider(Schemas.file, diskFileSystemProvider); - fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService)); + fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService, new NullLogService())); const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService); instantiationService.stub(IWorkspaceContextService, workspaceService); @@ -497,7 +497,7 @@ suite.skip('WorkspaceService - Initialization', () => { // {{SQL CARBON EDIT}} s const fileService = new FileService(new NullLogService()); const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService()); fileService.registerProvider(Schemas.file, diskFileSystemProvider); - fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService)); + fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService, new NullLogService())); const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService); instantiationService.stub(IWorkspaceContextService, workspaceService); instantiationService.stub(IConfigurationService, workspaceService); @@ -774,7 +774,7 @@ suite.skip('WorkspaceConfigurationService - Folder', () => { // {{SQL CARBON EDI fileService = new FileService(new NullLogService()); const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService()); fileService.registerProvider(Schemas.file, diskFileSystemProvider); - fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService)); + fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService, new NullLogService())); workspaceService = disposableStore.add(new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService)); instantiationService.stub(IWorkspaceContextService, workspaceService); instantiationService.stub(IConfigurationService, workspaceService); @@ -1199,7 +1199,7 @@ suite.skip('WorkspaceConfigurationService-Multiroot', () => { // {{SQL CARBON ED const fileService = new FileService(new NullLogService()); const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService()); fileService.registerProvider(Schemas.file, diskFileSystemProvider); - fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService)); + fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService, new NullLogService())); const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService); instantiationService.stub(IWorkspaceContextService, workspaceService); @@ -1709,7 +1709,7 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { const remoteAgentService = instantiationService.stub(IRemoteAgentService, >{ getEnvironment: () => remoteEnvironmentPromise }); const fileService = new FileService(new NullLogService()); fileService.registerProvider(Schemas.file, diskFileSystemProvider); - fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService)); + fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService, new NullLogService())); const configurationCache: IConfigurationCache = { read: () => Promise.resolve(''), write: () => Promise.resolve(), remove: () => Promise.resolve() }; testObject = new WorkspaceService({ configurationCache, remoteAuthority }, environmentService, fileService, remoteAgentService); instantiationService.stub(IWorkspaceContextService, testObject); diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 9df2c25ed0..d327b50f89 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -258,6 +258,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { const optionOverrides = { preserveFocus: true, pinned: group.isPinned(editor), + sticky: group.isSticky(editor), index: group.getIndexOfEditor(editor), inactive: !group.isActive(editor) }; @@ -428,7 +429,9 @@ export class EditorService extends Disposable implements EditorServiceImpl { return this.getEditors(EditorsOrder.SEQUENTIAL).map(({ editor }) => editor); } - getEditors(order: EditorsOrder): ReadonlyArray { + getEditors(order: EditorsOrder.MOST_RECENTLY_ACTIVE): ReadonlyArray; + getEditors(order: EditorsOrder.SEQUENTIAL, options?: { excludeSticky?: boolean }): ReadonlyArray; + getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): ReadonlyArray { if (order === EditorsOrder.MOST_RECENTLY_ACTIVE) { return this.editorsObserver.editors; } @@ -436,7 +439,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { const editors: IEditorIdentifier[] = []; this.editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE).forEach(group => { - editors.push(...group.getEditors(EditorsOrder.SEQUENTIAL).map(editor => ({ editor, groupId: group.id }))); + editors.push(...group.getEditors(EditorsOrder.SEQUENTIAL, options).map(editor => ({ editor, groupId: group.id }))); }); return editors; @@ -1038,7 +1041,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { return this.save(this.getAllDirtyEditors(options), options); } - async revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise { + async revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise { // Convert to array if (!Array.isArray(editors)) { @@ -1056,9 +1059,11 @@ export class EditorService extends Disposable implements EditorServiceImpl { return editor.revert(groupId, options); })); + + return !uniqueEditors.some(({ editor }) => editor.isDirty()); } - async revertAll(options?: IRevertAllEditorsOptions): Promise { + async revertAll(options?: IRevertAllEditorsOptions): Promise { return this.revert(this.getAllDirtyEditors(options), options); } @@ -1067,9 +1072,19 @@ export class EditorService extends Disposable implements EditorServiceImpl { for (const group of this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) { for (const editor of group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) { - if (editor.isDirty() && (!editor.isUntitled() || !!options?.includeUntitled)) { - editors.push({ groupId: group.id, editor }); + if (!editor.isDirty()) { + continue; } + + if (!options?.includeUntitled && editor.isUntitled()) { + continue; + } + + if (options?.excludeSticky && group.isSticky(editor)) { + continue; + } + + editors.push({ groupId: group.id, editor }); } } @@ -1210,7 +1225,15 @@ export class DelegatingEditorService implements IEditorService { get editors(): ReadonlyArray { return this.editorService.editors; } get count(): number { return this.editorService.count; } - getEditors(order: EditorsOrder): ReadonlyArray { return this.editorService.getEditors(order); } + getEditors(order: EditorsOrder.MOST_RECENTLY_ACTIVE): ReadonlyArray; + getEditors(order: EditorsOrder.SEQUENTIAL, options?: { excludeSticky?: boolean }): ReadonlyArray; + getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): ReadonlyArray { + if (order === EditorsOrder.MOST_RECENTLY_ACTIVE) { + return this.editorService.getEditors(order); + } + + return this.editorService.getEditors(order, options); + } openEditors(editors: IEditorInputWithOptions[], group?: OpenInEditorGroup): Promise; openEditors(editors: IResourceEditorInputType[], group?: OpenInEditorGroup): Promise; @@ -1237,8 +1260,8 @@ export class DelegatingEditorService implements IEditorService { save(editors: IEditorIdentifier | IEditorIdentifier[], options?: ISaveEditorsOptions): Promise { return this.editorService.save(editors, options); } saveAll(options?: ISaveAllEditorsOptions): Promise { return this.editorService.saveAll(options); } - revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise { return this.editorService.revert(editors, options); } - revertAll(options?: IRevertAllEditorsOptions): Promise { return this.editorService.revertAll(options); } + revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise { return this.editorService.revert(editors, options); } + revertAll(options?: IRevertAllEditorsOptions): Promise { return this.editorService.revertAll(options); } registerCustomEditorViewTypesHandler(source: string, handler: ICustomEditorViewTypesHandler): IDisposable { throw new Error('Method not implemented.'); diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index a0e0e6129f..c0d9391a1b 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -77,10 +77,6 @@ export interface EditorGroupLayout { groups: GroupLayoutArgument[]; } -export interface ICloseEditorOptions { - preserveFocus?: boolean; -} - export interface IMoveEditorOptions { index?: number; inactive?: boolean; @@ -103,12 +99,21 @@ export interface IMergeGroupOptions { index?: number; } +export interface ICloseEditorOptions { + preserveFocus?: boolean; +} + export type ICloseEditorsFilter = { except?: IEditorInput, direction?: CloseDirection, - savedOnly?: boolean + savedOnly?: boolean, + excludeSticky?: boolean }; +export interface ICloseAllEditorsOptions { + excludeSticky?: boolean; +} + export interface IEditorReplacement { editor: IEditorInput; replacement: IEditorInput; @@ -419,10 +424,15 @@ export interface IEditorGroup { readonly previewEditor: IEditorInput | null; /** - * The number of opend editors in this group. + * The number of opened editors in this group. */ readonly count: number; + /** + * The number of sticky editors in this group. + */ + readonly stickyCount: number; + /** * All opened editors in the group in sequential order of their appearance. */ @@ -432,8 +442,9 @@ export interface IEditorGroup { * Get all editors that are currently opened in the group. * * @param order the order of the editors to use + * @param options options to select only specific editors as instructed */ - getEditors(order: EditorsOrder): ReadonlyArray; + getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): ReadonlyArray; /** * Returns the editor at a specific index of the group. @@ -475,6 +486,11 @@ export interface IEditorGroup { */ isPinned(editor: IEditorInput): boolean; + /** + * Find out if the provided editor or index of editor is sticky in the group. + */ + isSticky(editorOrIndex: IEditorInput | number): boolean; + /** * Find out if the provided editor is active in the group. */ @@ -517,7 +533,7 @@ export interface IEditorGroup { * * @returns a promise when all editors are closed. */ - closeAllEditors(): Promise; + closeAllEditors(options?: ICloseAllEditorsOptions): Promise; /** * Replaces editors in this group with the provided replacement. @@ -538,6 +554,24 @@ export interface IEditorGroup { */ pinEditor(editor?: IEditorInput): void; + /** + * Set an editor to be sticky. A sticky editor is showing in the beginning + * of the tab stripe and will not be impacted by close operations. + * + * @param editor the editor to make sticky, or the currently active editor + * if unspecified. + */ + stickEditor(editor?: IEditorInput): void; + + /** + * Set an editor to be non-sticky and thus moves back to a location after + * sticky editors and can be closed normally. + * + * @param editor the editor to make unsticky, or the currently active editor + * if unspecified. + */ + unstickEditor(editor?: IEditorInput): void; + /** * Move keyboard focus into the group. */ diff --git a/src/vs/workbench/services/editor/common/editorService.ts b/src/vs/workbench/services/editor/common/editorService.ts index 2597345d94..1c4cee3e10 100644 --- a/src/vs/workbench/services/editor/common/editorService.ts +++ b/src/vs/workbench/services/editor/common/editorService.ts @@ -62,6 +62,11 @@ export interface IBaseSaveRevertAllEditorOptions { * Whether to include untitled editors as well. */ readonly includeUntitled?: boolean; + + /** + * Whether to exclude sticky editors. + */ + readonly excludeSticky?: boolean; } export interface ISaveAllEditorsOptions extends ISaveEditorsOptions, IBaseSaveRevertAllEditorOptions { } @@ -166,7 +171,8 @@ export interface IEditorService { * * @param order the order of the editors to use */ - getEditors(order: EditorsOrder): ReadonlyArray; + getEditors(order: EditorsOrder.MOST_RECENTLY_ACTIVE): ReadonlyArray; + getEditors(order: EditorsOrder.SEQUENTIAL, options?: { excludeSticky?: boolean }): ReadonlyArray; /** * Open an editor in an editor group. @@ -262,11 +268,15 @@ export interface IEditorService { /** * Reverts the provided list of editors. + * + * @returns `true` if all editors reverted and `false` otherwise. */ - revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise; + revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise; /** * Reverts all editors. + * + * @returns `true` if all editors reverted and `false` otherwise. */ - revertAll(options?: IRevertAllEditorsOptions): Promise; + revertAll(options?: IRevertAllEditorsOptions): Promise; } diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index 6cfec270f6..16dd59f675 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -461,7 +461,11 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); - await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]); + await group.openEditors([ + { editor: input, options: { pinned: true } }, + { editor: inputInactive } + ]); + assert.equal(group.count, 2); assert.equal(group.getEditorByIndex(0), input); assert.equal(group.getEditorByIndex(1), inputInactive); @@ -480,7 +484,12 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); - await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]); + await group.openEditors([ + { editor: input1, options: { pinned: true } }, + { editor: input2, options: { pinned: true } }, + { editor: input3 } + ]); + assert.equal(group.count, 3); assert.equal(group.getEditorByIndex(0), input1); assert.equal(group.getEditorByIndex(1), input2); @@ -492,6 +501,42 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite part.dispose(); }); + test('closeEditors (except one, sticky editor)', async () => { + const [part] = createPart(); + const group = part.activeGroup; + assert.equal(group.isEmpty, true); + + const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); + + await group.openEditors([ + { editor: input1, options: { pinned: true, sticky: true } }, + { editor: input2, options: { pinned: true } }, + { editor: input3 } + ]); + + assert.equal(group.count, 3); + assert.equal(group.stickyCount, 1); + assert.equal(group.getEditorByIndex(0), input1); + assert.equal(group.getEditorByIndex(1), input2); + assert.equal(group.getEditorByIndex(2), input3); + + await group.closeEditors({ except: input2, excludeSticky: true }); + + assert.equal(group.count, 2); + assert.equal(group.stickyCount, 1); + assert.equal(group.getEditorByIndex(0), input1); + assert.equal(group.getEditorByIndex(1), input2); + + await group.closeEditors({ except: input2 }); + + assert.equal(group.count, 1); + assert.equal(group.stickyCount, 0); + assert.equal(group.getEditorByIndex(0), input2); + part.dispose(); + }); + test('closeEditors (saved only)', async () => { const [part] = createPart(); const group = part.activeGroup; @@ -501,7 +546,12 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); - await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]); + await group.openEditors([ + { editor: input1, options: { pinned: true } }, + { editor: input2, options: { pinned: true } }, + { editor: input3 } + ]); + assert.equal(group.count, 3); assert.equal(group.getEditorByIndex(0), input1); assert.equal(group.getEditorByIndex(1), input2); @@ -512,6 +562,38 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite part.dispose(); }); + test('closeEditors (saved only, sticky editor)', async () => { + const [part] = createPart(); + const group = part.activeGroup; + assert.equal(group.isEmpty, true); + + const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); + + await group.openEditors([ + { editor: input1, options: { pinned: true, sticky: true } }, + { editor: input2, options: { pinned: true } }, + { editor: input3 } + ]); + + assert.equal(group.count, 3); + assert.equal(group.stickyCount, 1); + assert.equal(group.getEditorByIndex(0), input1); + assert.equal(group.getEditorByIndex(1), input2); + assert.equal(group.getEditorByIndex(2), input3); + + await group.closeEditors({ savedOnly: true, excludeSticky: true }); + + assert.equal(group.count, 1); + assert.equal(group.stickyCount, 1); + assert.equal(group.getEditorByIndex(0), input1); + + await group.closeEditors({ savedOnly: true }); + assert.equal(group.count, 0); + part.dispose(); + }); + test('closeEditors (direction: right)', async () => { const [part] = createPart(); const group = part.activeGroup; @@ -521,7 +603,12 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); - await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]); + await group.openEditors([ + { editor: input1, options: { pinned: true } }, + { editor: input2, options: { pinned: true } }, + { editor: input3 } + ]); + assert.equal(group.count, 3); assert.equal(group.getEditorByIndex(0), input1); assert.equal(group.getEditorByIndex(1), input2); @@ -534,6 +621,40 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite part.dispose(); }); + test('closeEditors (direction: right, sticky editor)', async () => { + const [part] = createPart(); + const group = part.activeGroup; + assert.equal(group.isEmpty, true); + + const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); + + await group.openEditors([ + { editor: input1, options: { pinned: true, sticky: true } }, + { editor: input2, options: { pinned: true } }, + { editor: input3 } + ]); + + assert.equal(group.count, 3); + assert.equal(group.stickyCount, 1); + assert.equal(group.getEditorByIndex(0), input1); + assert.equal(group.getEditorByIndex(1), input2); + assert.equal(group.getEditorByIndex(2), input3); + + await group.closeEditors({ direction: CloseDirection.RIGHT, except: input2, excludeSticky: true }); + assert.equal(group.count, 2); + assert.equal(group.stickyCount, 1); + assert.equal(group.getEditorByIndex(0), input1); + assert.equal(group.getEditorByIndex(1), input2); + + await group.closeEditors({ direction: CloseDirection.RIGHT, except: input2 }); + assert.equal(group.count, 2); + assert.equal(group.getEditorByIndex(0), input1); + assert.equal(group.getEditorByIndex(1), input2); + part.dispose(); + }); + test('closeEditors (direction: left)', async () => { const [part] = createPart(); const group = part.activeGroup; @@ -543,7 +664,12 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); - await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]); + await group.openEditors([ + { editor: input1, options: { pinned: true } }, + { editor: input2, options: { pinned: true } }, + { editor: input3 } + ]); + assert.equal(group.count, 3); assert.equal(group.getEditorByIndex(0), input1); assert.equal(group.getEditorByIndex(1), input2); @@ -556,6 +682,41 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite part.dispose(); }); + test('closeEditors (direction: left, sticky editor)', async () => { + const [part] = createPart(); + const group = part.activeGroup; + assert.equal(group.isEmpty, true); + + const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + const input3 = new TestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID); + + await group.openEditors([ + { editor: input1, options: { pinned: true, sticky: true } }, + { editor: input2, options: { pinned: true } }, + { editor: input3 } + ]); + + assert.equal(group.count, 3); + assert.equal(group.stickyCount, 1); + assert.equal(group.getEditorByIndex(0), input1); + assert.equal(group.getEditorByIndex(1), input2); + assert.equal(group.getEditorByIndex(2), input3); + + await group.closeEditors({ direction: CloseDirection.LEFT, except: input2, excludeSticky: true }); + assert.equal(group.count, 3); + assert.equal(group.stickyCount, 1); + assert.equal(group.getEditorByIndex(0), input1); + assert.equal(group.getEditorByIndex(1), input2); + assert.equal(group.getEditorByIndex(2), input3); + + await group.closeEditors({ direction: CloseDirection.LEFT, except: input2 }); + assert.equal(group.count, 2); + assert.equal(group.getEditorByIndex(0), input2); + assert.equal(group.getEditorByIndex(1), input3); + part.dispose(); + }); + test('closeAllEditors', async () => { const [part] = createPart(); const group = part.activeGroup; @@ -564,7 +725,11 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); - await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]); + await group.openEditors([ + { editor: input, options: { pinned: true } }, + { editor: inputInactive } + ]); + assert.equal(group.count, 2); assert.equal(group.getEditorByIndex(0), input); assert.equal(group.getEditorByIndex(1), inputInactive); @@ -574,6 +739,35 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite part.dispose(); }); + test('closeAllEditors (sticky editor)', async () => { + const [part] = createPart(); + const group = part.activeGroup; + assert.equal(group.isEmpty, true); + + const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); + + await group.openEditors([ + { editor: input, options: { pinned: true, sticky: true } }, + { editor: inputInactive } + ]); + + assert.equal(group.count, 2); + assert.equal(group.stickyCount, 1); + + await group.closeAllEditors({ excludeSticky: true }); + + assert.equal(group.count, 1); + assert.equal(group.stickyCount, 1); + assert.equal(group.getEditorByIndex(0), input); + + await group.closeAllEditors(); + + assert.equal(group.isEmpty, true); + + part.dispose(); + }); + test('moveEditor (same group)', async () => { const [part] = createPart(); const group = part.activeGroup; @@ -724,4 +918,107 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite part.dispose(); }); + + test('sticky editors', async () => { + const [part] = createPart(); + const group = part.activeGroup; + + await part.whenRestored; + + assert.equal(group.stickyCount, 0); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL).length, 0); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 0); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).length, 0); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, { excludeSticky: true }).length, 0); + + const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID); + const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID); + + await group.openEditor(input, EditorOptions.create({ pinned: true })); + await group.openEditor(inputInactive, EditorOptions.create({ inactive: true })); + + assert.equal(group.stickyCount, 0); + assert.equal(group.isSticky(input), false); + assert.equal(group.isSticky(inputInactive), false); + + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL).length, 2); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 2); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).length, 2); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, { excludeSticky: true }).length, 2); + + group.stickEditor(input); + + assert.equal(group.stickyCount, 1); + assert.equal(group.isSticky(input), true); + assert.equal(group.isSticky(inputInactive), false); + + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL).length, 2); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 2); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).length, 1); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, { excludeSticky: true }).length, 1); + + group.unstickEditor(input); + + assert.equal(group.stickyCount, 0); + assert.equal(group.isSticky(input), false); + assert.equal(group.isSticky(inputInactive), false); + + assert.equal(group.getIndexOfEditor(input), 0); + assert.equal(group.getIndexOfEditor(inputInactive), 1); + + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL).length, 2); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 2); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).length, 2); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, { excludeSticky: true }).length, 2); + + let editorMoveCounter = 0; + const editorGroupChangeListener = group.onDidGroupChange(e => { + if (e.kind === GroupChangeKind.EDITOR_MOVE) { + assert.ok(e.editor); + editorMoveCounter++; + } + }); + + group.stickEditor(inputInactive); + + assert.equal(group.stickyCount, 1); + assert.equal(group.isSticky(input), false); + assert.equal(group.isSticky(inputInactive), true); + + assert.equal(group.getIndexOfEditor(input), 1); + assert.equal(group.getIndexOfEditor(inputInactive), 0); + assert.equal(editorMoveCounter, 1); + + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL).length, 2); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 2); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).length, 1); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, { excludeSticky: true }).length, 1); + + const inputSticky = new TestFileEditorInput(URI.file('foo/bar/sticky'), TEST_EDITOR_INPUT_ID); + + await group.openEditor(inputSticky, EditorOptions.create({ sticky: true })); + + assert.equal(group.stickyCount, 2); + assert.equal(group.isSticky(input), false); + assert.equal(group.isSticky(inputInactive), true); + assert.equal(group.isSticky(inputSticky), true); + + assert.equal(group.getIndexOfEditor(inputInactive), 0); + assert.equal(group.getIndexOfEditor(inputSticky), 1); + assert.equal(group.getIndexOfEditor(input), 2); + + await group.openEditor(input, EditorOptions.create({ sticky: true })); + + assert.equal(group.stickyCount, 3); + assert.equal(group.isSticky(input), true); + assert.equal(group.isSticky(inputInactive), true); + assert.equal(group.isSticky(inputSticky), true); + + assert.equal(group.getIndexOfEditor(inputInactive), 0); + assert.equal(group.getIndexOfEditor(inputSticky), 1); + assert.equal(group.getIndexOfEditor(input), 2); + + editorGroupChangeListener.dispose(); + part.dispose(); + }); }); diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index 3d71c7ed4a..a955069d05 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -146,6 +146,22 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite assert.equal(activeEditorChangeEventCounter, 4); assert.equal(visibleEditorChangeEventCounter, 4); + const stickyInput = new TestFileEditorInput(URI.parse('my://resource3-basics'), TEST_EDITOR_INPUT_ID); + await service.openEditor(stickyInput, { sticky: true }); + + assert.equal(3, service.count); + + const allSequentialEditors = service.getEditors(EditorsOrder.SEQUENTIAL); + assert.equal(allSequentialEditors.length, 3); + assert.equal(stickyInput, allSequentialEditors[0].editor); + assert.equal(input, allSequentialEditors[1].editor); + assert.equal(otherInput, allSequentialEditors[2].editor); + + const sequentialEditorsExcludingSticky = service.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }); + assert.equal(sequentialEditorsExcludingSticky.length, 2); + assert.equal(input, sequentialEditorsExcludingSticky[0].editor); + assert.equal(otherInput, sequentialEditorsExcludingSticky[1].editor); + activeEditorChangeListener.dispose(); visibleEditorChangeListener.dispose(); didCloseEditorListener.dispose(); @@ -795,6 +811,10 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite input1.gotSavedAs = false; input1.gotReverted = false; + input1.dirty = true; + input2.dirty = true; + sameInput1.dirty = true; + await service.save({ groupId: rootGroup.id, editor: input1 }, { saveAs: true }); assert.equal(input1.gotSavedAs, true); @@ -802,14 +822,24 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite input1.gotSavedAs = false; input1.gotReverted = false; - await service.revertAll(); + input1.dirty = true; + input2.dirty = true; + sameInput1.dirty = true; + + const revertRes = await service.revertAll(); + assert.equal(revertRes, true); assert.equal(input1.gotReverted, true); input1.gotSaved = false; input1.gotSavedAs = false; input1.gotReverted = false; - await service.saveAll(); + input1.dirty = true; + input2.dirty = true; + sameInput1.dirty = true; + + const saveRes = await service.saveAll(); + assert.equal(saveRes, true); assert.equal(input1.gotSaved, true); assert.equal(input2.gotSaved, true); @@ -820,6 +850,10 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite input2.gotSavedAs = false; input2.gotReverted = false; + input1.dirty = true; + input2.dirty = true; + sameInput1.dirty = true; + await service.saveAll({ saveAs: true }); assert.equal(input1.gotSavedAs, true); @@ -833,6 +867,48 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite part.dispose(); }); + test('saveAll, revertAll (sticky editor)', async function () { + const [part, service] = createEditorService(); + + const input1 = new TestFileEditorInput(URI.parse('my://resource1'), TEST_EDITOR_INPUT_ID); + input1.dirty = true; + const input2 = new TestFileEditorInput(URI.parse('my://resource2'), TEST_EDITOR_INPUT_ID); + input2.dirty = true; + const sameInput1 = new TestFileEditorInput(URI.parse('my://resource1'), TEST_EDITOR_INPUT_ID); + sameInput1.dirty = true; + + await part.whenRestored; + + await service.openEditor(input1, { pinned: true, sticky: true }); + await service.openEditor(input2, { pinned: true }); + await service.openEditor(sameInput1, { pinned: true }, SIDE_GROUP); + + const revertRes = await service.revertAll({ excludeSticky: true }); + assert.equal(revertRes, true); + assert.equal(input1.gotReverted, false); + assert.equal(sameInput1.gotReverted, true); + + input1.gotSaved = false; + input1.gotSavedAs = false; + input1.gotReverted = false; + + sameInput1.gotSaved = false; + sameInput1.gotSavedAs = false; + sameInput1.gotReverted = false; + + input1.dirty = true; + input2.dirty = true; + sameInput1.dirty = true; + + const saveRes = await service.saveAll({ excludeSticky: true }); + assert.equal(saveRes, true); + assert.equal(input1.gotSaved, false); + assert.equal(input2.gotSaved, true); + assert.equal(sameInput1.gotSaved, true); + + part.dispose(); + }); + test('file delete closes editor', async function () { return testFileDeleteEditorClose(false); }); diff --git a/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts b/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts index 4c22b3edc9..c1b920189a 100644 --- a/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts @@ -536,4 +536,37 @@ suite.skip('EditorsObserver', function () { //{{SQL CARBON EDIT}} disable failin observer.dispose(); part.dispose(); }); + + test('observer does not close sticky', async () => { + const part = await createPart(); + part.enforcePartOptions({ limit: { enabled: true, value: 3 } }); + + const storage = new TestStorageService(); + const observer = new EditorsObserver(part, storage); + + const rootGroup = part.activeGroup; + + const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_EDITOR_INPUT_ID); + const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_EDITOR_INPUT_ID); + const input4 = new TestFileEditorInput(URI.parse('foo://bar4'), TEST_EDITOR_INPUT_ID); + + await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true, sticky: true })); + await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input4, EditorOptions.create({ pinned: true })); + + assert.equal(rootGroup.count, 3); + assert.equal(rootGroup.isOpened(input1), true); + assert.equal(rootGroup.isOpened(input2), false); + assert.equal(rootGroup.isOpened(input3), true); + assert.equal(rootGroup.isOpened(input4), true); + assert.equal(observer.hasEditor(input1.resource), true); + assert.equal(observer.hasEditor(input2.resource), false); + assert.equal(observer.hasEditor(input3.resource), true); + assert.equal(observer.hasEditor(input4.resource), true); + + observer.dispose(); + part.dispose(); + }); }); diff --git a/src/vs/workbench/services/environment/electron-browser/environmentService.ts b/src/vs/workbench/services/environment/electron-browser/environmentService.ts index 5f5e0db688..55ca2f6df3 100644 --- a/src/vs/workbench/services/environment/electron-browser/environmentService.ts +++ b/src/vs/workbench/services/environment/electron-browser/environmentService.ts @@ -18,6 +18,7 @@ export interface INativeWorkbenchEnvironmentService extends IWorkbenchEnvironmen readonly configuration: INativeEnvironmentConfiguration; readonly disableCrashReporter: boolean; + readonly crashReporterDirectory?: string; readonly cliPath: string; diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts index 25b33b9fd9..13887c5355 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts @@ -6,6 +6,7 @@ import * as nls from 'vs/nls'; import { ChildProcess, fork } from 'child_process'; import { Server, Socket, createServer } from 'net'; +import { CrashReporterStartOptions } from 'electron'; import { getPathFromAmdModule } from 'vs/base/common/amd'; import { timeout } from 'vs/base/common/async'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -170,8 +171,17 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter { opts.execArgv = ['--inspect-port=0']; } - const crashReporterOptions = undefined; // TODO@electron pass this in as options to the extension host after verifying this actually works - if (crashReporterOptions) { + // Enable the crash reporter depending on environment for local reporting + const crashesDirectory = this._environmentService.crashReporterDirectory; + if (crashesDirectory) { + const crashReporterOptions: CrashReporterStartOptions = { + companyName: product.crashReporter?.companyName || 'Microsoft', + productName: product.crashReporter?.productName || product.nameShort, + submitURL: '', + uploadToServer: false, + crashesDirectory + }; + opts.env.CRASH_REPORTER_START_OPTIONS = JSON.stringify(crashReporterOptions); } diff --git a/src/vs/workbench/services/history/browser/history.ts b/src/vs/workbench/services/history/browser/history.ts index e342f13792..7fe5b273d2 100644 --- a/src/vs/workbench/services/history/browser/history.ts +++ b/src/vs/workbench/services/history/browser/history.ts @@ -87,6 +87,7 @@ interface IStackEntry { interface IRecentlyClosedFile { resource: URI; index: number; + sticky: boolean; } export class HistoryService extends Disposable implements IHistoryService { @@ -616,7 +617,7 @@ export class HistoryService extends Disposable implements IHistoryService { // Remove all inputs matching and add as last recently closed this.removeFromRecentlyClosedFiles(event.editor); - this.recentlyClosedFiles.push({ resource, index: event.index }); + this.recentlyClosedFiles.push({ resource, index: event.index, sticky: event.sticky }); // Bounding if (this.recentlyClosedFiles.length > HistoryService.MAX_RECENTLY_CLOSED_EDITORS) { @@ -637,7 +638,10 @@ export class HistoryService extends Disposable implements IHistoryService { if (lastClosedFile) { (async () => { - const editor = await this.editorService.openEditor({ resource: lastClosedFile.resource, options: { pinned: true, index: lastClosedFile.index } }); + const editor = await this.editorService.openEditor({ + resource: lastClosedFile.resource, + options: { pinned: true, sticky: lastClosedFile.sticky, index: lastClosedFile.index } + }); // Fix for https://github.com/Microsoft/vscode/issues/67882 // If opening of the editor fails, make sure to try the next one diff --git a/src/vs/workbench/services/keybinding/browser/keybindingService.ts b/src/vs/workbench/services/keybinding/browser/keybindingService.ts index de4f131705..d459378097 100644 --- a/src/vs/workbench/services/keybinding/browser/keybindingService.ts +++ b/src/vs/workbench/services/keybinding/browser/keybindingService.ts @@ -47,6 +47,7 @@ import { INavigatorWithKeyboard, IKeyboard } from 'vs/workbench/services/keybind import { ScanCode, ScanCodeUtils, IMMUTABLE_CODE_TO_KEY_CODE } from 'vs/base/common/scanCode'; import { flatten } from 'vs/base/common/arrays'; import { BrowserFeatures, KeyboardSupport } from 'vs/base/browser/canIUse'; +import { ILogService } from 'vs/platform/log/common/log'; interface ContributedKeyBinding { command: string; @@ -190,6 +191,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { @IHostService private readonly hostService: IHostService, @IExtensionService extensionService: IExtensionService, @IFileService fileService: IFileService, + @ILogService logService: ILogService, @IKeymapService private readonly keymapService: IKeymapService ) { super(contextKeyService, commandService, telemetryService, notificationService); @@ -216,13 +218,14 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { this._cachedResolver = null; - this.userKeybindings = this._register(new UserKeybindings(environmentService.keybindingsResource, fileService)); + this.userKeybindings = this._register(new UserKeybindings(environmentService.keybindingsResource, fileService, logService)); this.userKeybindings.initialize().then(() => { if (this.userKeybindings.keybindings.length) { this.updateResolver({ source: KeybindingSource.User }); } }); this._register(this.userKeybindings.onDidChange(() => { + logService.debug('User keybindings changed'); this.updateResolver({ source: KeybindingSource.User, keybindings: this.userKeybindings.keybindings @@ -642,7 +645,8 @@ class UserKeybindings extends Disposable { constructor( private readonly keybindingsResource: URI, - private readonly fileService: IFileService + private readonly fileService: IFileService, + logService: ILogService, ) { super(); @@ -651,7 +655,10 @@ class UserKeybindings extends Disposable { this._onDidChange.fire(); } }), 50)); - this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.keybindingsResource))(() => this.reloadConfigurationScheduler.schedule())); + this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.keybindingsResource))(() => { + logService.debug('Keybindings file changed'); + this.reloadConfigurationScheduler.schedule(); + })); } async initialize(): Promise { diff --git a/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts index 18fc6b91bf..91e58bbae0 100644 --- a/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts +++ b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts @@ -115,7 +115,7 @@ suite('KeybindingsEditing', () => { const fileService = new FileService(new NullLogService()); const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService()); fileService.registerProvider(Schemas.file, diskFileSystemProvider); - fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService)); + fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService, new NullLogService())); instantiationService.stub(IFileService, fileService); instantiationService.stub(IWorkingCopyService, new TestWorkingCopyService()); instantiationService.stub(IWorkingCopyFileService, instantiationService.createInstance(WorkingCopyFileService)); diff --git a/src/vs/workbench/services/log/electron-browser/logService.ts b/src/vs/workbench/services/log/electron-browser/logService.ts new file mode 100644 index 0000000000..c2b499064b --- /dev/null +++ b/src/vs/workbench/services/log/electron-browser/logService.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DelegatedLogService, ILogService, ConsoleLogInMainService, ConsoleLogService, MultiplexLogService } from 'vs/platform/log/common/log'; +import { BufferLogService } from 'vs/platform/log/common/bufferLog'; +import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; +import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; +import { LoggerChannelClient, FollowerLogService } from 'vs/platform/log/common/logIpc'; +import { SpdLogService } from 'vs/platform/log/node/spdlogService'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions } from 'vs/workbench/common/contributions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; + +export class DesktopLogService extends DelegatedLogService { + + private readonly bufferSpdLogService: BufferLogService | undefined; + private readonly windowId: number; + private readonly environmentService: NativeWorkbenchEnvironmentService; + + constructor(windowId: number, mainProcessService: IMainProcessService, environmentService: NativeWorkbenchEnvironmentService) { + + const disposables = new DisposableStore(); + const loggerClient = new LoggerChannelClient(mainProcessService.getChannel('logger')); + let bufferSpdLogService: BufferLogService | undefined; + + // Extension development test CLI: forward everything to main side + const loggers: ILogService[] = []; + if (environmentService.isExtensionDevelopment && !!environmentService.extensionTestsLocationURI) { + loggers.push( + disposables.add(new ConsoleLogInMainService(loggerClient, environmentService.configuration.logLevel)) + ); + } + + // Normal logger: spdylog and console + else { + bufferSpdLogService = disposables.add(new BufferLogService(environmentService.configuration.logLevel)); + loggers.push( + disposables.add(new ConsoleLogService(environmentService.configuration.logLevel)), + bufferSpdLogService, + ); + } + + const multiplexLogger = disposables.add(new MultiplexLogService(loggers)); + const followerLogger = disposables.add(new FollowerLogService(loggerClient, multiplexLogger)); + super(followerLogger); + + this.bufferSpdLogService = bufferSpdLogService; + this.windowId = windowId; + this.environmentService = environmentService; + + this._register(disposables); + } + + init(): void { + if (this.bufferSpdLogService) { + this.bufferSpdLogService.logger = this._register(new SpdLogService(`renderer${this.windowId}`, this.environmentService.logsPath, this.getLevel())); + this.trace('Created Spdlogger'); + } + } +} + +class DesktopLogServiceInitContribution implements IWorkbenchContribution { + constructor(@ILogService logService: ILogService) { + if (logService instanceof DesktopLogService) { + logService.init(); + } + } +} +Registry.as(Extensions.Workbench).registerWorkbenchContribution(DesktopLogServiceInitContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index d1d40c5831..918ee2a63b 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -349,7 +349,8 @@ export interface ISearchConfigurationProperties { searchOnTypeDebouncePeriod: number; searchEditor: { doubleClickBehaviour: 'selectWord' | 'goToLocation' | 'openLocationToSide', - experimental: { reusePriorSearchConfiguration: boolean } + reusePriorSearchConfiguration: boolean, + experimental: {} }; sortOrder: SearchSortOrder; } diff --git a/src/vs/workbench/services/statusbar/common/statusbar.ts b/src/vs/workbench/services/statusbar/common/statusbar.ts index 28b448f26f..1b72c96278 100644 --- a/src/vs/workbench/services/statusbar/common/statusbar.ts +++ b/src/vs/workbench/services/statusbar/common/statusbar.ts @@ -89,6 +89,16 @@ export interface IStatusbarService { * Allows to update an entry's visibility with the provided ID. */ updateEntryVisibility(id: string, visible: boolean): void; + + /** + * Focuses the next status bar entry. If none focused, focuses the first. + */ + focusNextEntry(): void; + + /** + * Focuses the previous status bar entry. If none focused, focuses the last. + */ + focusPreviousEntry(): void; } export interface IStatusbarEntryAccessor extends IDisposable { diff --git a/src/vs/workbench/services/textMate/common/cgmanifest.json b/src/vs/workbench/services/textMate/common/cgmanifest.json index f458b3cc96..79b7a01ad4 100644 --- a/src/vs/workbench/services/textMate/common/cgmanifest.json +++ b/src/vs/workbench/services/textMate/common/cgmanifest.json @@ -6,7 +6,7 @@ "other": { "name": "lib-oniguruma", "downloadUrl": "https://github.com/kkos/oniguruma", - "version": "6.9.5" + "version": "6.9.5_rev1" } }, "licenseDetail": [ @@ -41,7 +41,7 @@ ], "isOnlyProductionDependency": true, "license": "BSD", - "version": "6.9.5" + "version": "6.9.5_rev1" } ], "version": 1 diff --git a/src/vs/workbench/services/themes/common/themeConfiguration.ts b/src/vs/workbench/services/themes/common/themeConfiguration.ts index 9f8604ba30..472bdce8f3 100644 --- a/src/vs/workbench/services/themes/common/themeConfiguration.ts +++ b/src/vs/workbench/services/themes/common/themeConfiguration.ts @@ -151,11 +151,13 @@ const semanticTokenColorSchema: IJSONSchema = { properties: { enabled: { type: 'boolean', - description: nls.localize('editorColors.semanticHighlighting.enabled', 'Whether semantic highlighting is enabled or disabled for this theme') + description: nls.localize('editorColors.semanticHighlighting.enabled', 'Whether semantic highlighting is enabled or disabled for this theme'), + suggestSortText: '0_enabled' }, rules: { $ref: tokenStylingSchemaId, - description: nls.localize('editorColors.semanticHighlighting.rules', 'Semantic token styling rules for this theme.') + description: nls.localize('editorColors.semanticHighlighting.rules', 'Semantic token styling rules for this theme.'), + suggestSortText: '0_rules' } }, additionalProperties: false diff --git a/src/vs/workbench/services/userData/common/fileUserDataProvider.ts b/src/vs/workbench/services/userData/common/fileUserDataProvider.ts index 463d4b5e53..c0e06063d8 100644 --- a/src/vs/workbench/services/userData/common/fileUserDataProvider.ts +++ b/src/vs/workbench/services/userData/common/fileUserDataProvider.ts @@ -13,6 +13,7 @@ import { BACKUPS } from 'vs/platform/environment/common/environment'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ReadableStreamEvents } from 'vs/base/common/stream'; +import { ILogService } from 'vs/platform/log/common/log'; export class FileUserDataProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability, @@ -31,7 +32,8 @@ export class FileUserDataProvider extends Disposable implements private readonly fileSystemUserDataHome: URI, private readonly fileSystemBackupsHome: URI, private readonly fileSystemProvider: IFileSystemProviderWithFileReadWriteCapability | IFileSystemProviderWithOpenReadWriteCloseCapability, - environmentService: IWorkbenchEnvironmentService + environmentService: IWorkbenchEnvironmentService, + private readonly logService: ILogService, ) { super(); @@ -127,6 +129,7 @@ export class FileUserDataProvider extends Disposable implements } } if (userDataChanges.length) { + this.logService.debug('User data changed'); this._onDidChangeFile.fire(userDataChanges); } } diff --git a/src/vs/workbench/services/userData/test/electron-browser/fileUserDataProvider.test.ts b/src/vs/workbench/services/userData/test/electron-browser/fileUserDataProvider.test.ts index b4007a7297..9c6eafe1a5 100644 --- a/src/vs/workbench/services/userData/test/electron-browser/fileUserDataProvider.test.ts +++ b/src/vs/workbench/services/userData/test/electron-browser/fileUserDataProvider.test.ts @@ -59,7 +59,7 @@ suite('FileUserDataProvider', () => { const environmentService = new TestBrowserWorkbenchEnvironmentService({ remoteAuthority: 'remote', workspaceId: 'workspaceId', logsPath: URI.file('logFile') }); environmentService.testUserRoamingDataHome = userDataResource; - const userDataFileSystemProvider = new FileUserDataProvider(URI.file(userDataPath), URI.file(backupsPath), diskFileSystemProvider, environmentService); + const userDataFileSystemProvider = new FileUserDataProvider(URI.file(userDataPath), URI.file(backupsPath), diskFileSystemProvider, environmentService, logService); disposables.add(userDataFileSystemProvider); disposables.add(testObject.registerProvider(Schemas.userData, userDataFileSystemProvider)); }); @@ -333,7 +333,7 @@ suite('FileUserDataProvider - Watching', () => { const environmentService = new TestBrowserWorkbenchEnvironmentService({ remoteAuthority: 'remote', workspaceId: 'workspaceId', logsPath: URI.file('logFile') }); environmentService.testUserRoamingDataHome = userDataResource; - const userDataFileSystemProvider = new FileUserDataProvider(localUserDataResource, localBackupsResource, new TestFileSystemProvider(fileEventEmitter.event), environmentService); + const userDataFileSystemProvider = new FileUserDataProvider(localUserDataResource, localBackupsResource, new TestFileSystemProvider(fileEventEmitter.event), environmentService, new NullLogService()); disposables.add(userDataFileSystemProvider); testObject = new FileService(new NullLogService()); diff --git a/src/vs/workbench/services/views/browser/viewDescriptorService.ts b/src/vs/workbench/services/views/browser/viewDescriptorService.ts index f2cd986498..0fab0a49a0 100644 --- a/src/vs/workbench/services/views/browser/viewDescriptorService.ts +++ b/src/vs/workbench/services/views/browser/viewDescriptorService.ts @@ -119,7 +119,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor // Try generating all generated containers that don't need extensions this.tryGenerateContainers(); - this._register(this.viewsRegistry.onViewsRegistered(({ views, viewContainer }) => this.onDidRegisterViews(views, viewContainer))); + this._register(this.viewsRegistry.onViewsRegistered(views => this.onDidRegisterViews(views))); this._register(this.viewsRegistry.onViewsDeregistered(({ views, viewContainer }) => this.onDidDeregisterViews(views, viewContainer))); this._register(this.viewsRegistry.onDidChangeContainer(({ views, from, to }) => this.moveViews(views, from, to))); @@ -212,16 +212,18 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor this.tryGenerateContainers(true); } - private onDidRegisterViews(views: IViewDescriptor[], viewContainer: ViewContainer): void { - // When views are registered, we need to regroup them based on the cache - const regroupedViews = this.regroupViews(viewContainer.id, views); + private onDidRegisterViews(views: { views: IViewDescriptor[], viewContainer: ViewContainer }[]): void { + views.forEach(({ views, viewContainer }) => { + // When views are registered, we need to regroup them based on the cache + const regroupedViews = this.regroupViews(viewContainer.id, views); - // Once they are grouped, try registering them which occurs - // if the container has already been registered within this service - // or we can generate the container from the source view id - this.registerGroupedViews(regroupedViews); + // Once they are grouped, try registering them which occurs + // if the container has already been registered within this service + // or we can generate the container from the source view id + this.registerGroupedViews(regroupedViews); - views.forEach(viewDescriptor => this.getOrCreateMovableViewContextKey(viewDescriptor).set(!!viewDescriptor.canMoveView)); + views.forEach(viewDescriptor => this.getOrCreateMovableViewContextKey(viewDescriptor).set(!!viewDescriptor.canMoveView)); + }); } private shouldGenerateContainer(containerInfo: ICachedViewContainerInfo): boolean { diff --git a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts index d3ed32cc48..edc98a22b0 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts @@ -41,15 +41,20 @@ suite('NotebookConcatDocument', function () { rpcProtocol.set(MainContext.MainThreadNotebook, new class extends mock() { async $registerNotebookProvider() { } async $unregisterNotebookProvider() { } - async $createNotebookDocument() { } }); extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService()); extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors); - let reg = extHostNotebooks.registerNotebookProvider(nullExtensionDescription, 'test', new class extends mock() { - async resolveNotebook() { } + let reg = extHostNotebooks.registerNotebookContentProvider(nullExtensionDescription, 'test', new class extends mock() { + // async openNotebook() { } + }); + await extHostNotebooks.$acceptDocumentAndEditorsDelta({ + addedDocuments: [{ + handle: 0, + uri: notebookUri, + viewType: 'test' + }] }); - await extHostNotebooks.$resolveNotebook('test', notebookUri); extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, versionId: 0, @@ -62,7 +67,7 @@ suite('NotebookConcatDocument', function () { outputs: [], }]]] }); - await extHostNotebooks.$updateActiveEditor('test', notebookUri); + await extHostNotebooks.$acceptDocumentAndEditorsDelta({ newActiveEditor: notebookUri }); notebook = extHostNotebooks.activeNotebookDocument!; diff --git a/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts b/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts index f8c9533af0..a7e0b8a1bb 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts @@ -41,6 +41,38 @@ function createGroup(serialized?: ISerializedEditorGroup): EditorGroup { return inst().createInstance(EditorGroup, serialized); } +function closeAllEditors(group: EditorGroup): void { + for (const editor of group.getEditors(EditorsOrder.SEQUENTIAL)) { + group.closeEditor(editor, false); + } +} + +function closeEditors(group: EditorGroup, except: EditorInput, direction?: CloseDirection): void { + const index = group.indexOf(except); + if (index === -1) { + return; // not found + } + + // Close to the left + if (direction === CloseDirection.LEFT) { + for (let i = index - 1; i >= 0; i--) { + group.closeEditor(group.getEditorByIndex(i)!); + } + } + + // Close to the right + else if (direction === CloseDirection.RIGHT) { + for (let i = group.getEditors(EditorsOrder.SEQUENTIAL).length - 1; i > index; i--) { + group.closeEditor(group.getEditorByIndex(i)!); + } + } + + // Both directions + else { + group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).filter(editor => !editor.matches(except)).forEach(editor => group.closeEditor(editor)); + } +} + interface GroupEvents { opened: EditorInput[]; activated: EditorInput[]; @@ -206,13 +238,25 @@ suite('Workbench editor groups', () => { group.openEditor(input2, { pinned: true, active: true }); group.openEditor(input3, { pinned: false, active: true }); + // Sticky + group.stick(input2); + assert.ok(group.isSticky(input2)); + const clone = group.clone(); assert.notEqual(group.id, clone.id); assert.equal(clone.count, 3); + assert.equal(clone.isPinned(input1), true); + assert.equal(clone.isActive(input1), false); + assert.equal(clone.isSticky(input1), false); + assert.equal(clone.isPinned(input2), true); + assert.equal(clone.isActive(input2), false); + assert.equal(clone.isSticky(input2), true); + assert.equal(clone.isPinned(input3), false); assert.equal(clone.isActive(input3), true); + assert.equal(clone.isSticky(input3), false); }); test('contains()', function () { @@ -346,6 +390,61 @@ suite('Workbench editor groups', () => { assert.equal(deserialized.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 0); }); + test('group serialization (sticky editor)', function () { + inst().invokeFunction(accessor => Registry.as(EditorExtensions.EditorInputFactories).start(accessor)); + const group = createGroup(); + + const input1 = input(); + const input2 = input(); + const input3 = input(); + + // Case 1: inputs can be serialized and deserialized + + group.openEditor(input1, { pinned: true, active: true }); + group.openEditor(input2, { pinned: true, active: true }); + group.openEditor(input3, { pinned: false, active: true }); + + group.stick(input2); + assert.ok(group.isSticky(input2)); + + let deserialized = createGroup(group.serialize()); + assert.equal(group.id, deserialized.id); + assert.equal(deserialized.count, 3); + + assert.equal(deserialized.isPinned(input1), true); + assert.equal(deserialized.isActive(input1), false); + assert.equal(deserialized.isSticky(input1), false); + + assert.equal(deserialized.isPinned(input2), true); + assert.equal(deserialized.isActive(input2), false); + assert.equal(deserialized.isSticky(input2), true); + + assert.equal(deserialized.isPinned(input3), false); + assert.equal(deserialized.isActive(input3), true); + assert.equal(deserialized.isSticky(input3), false); + + // Case 2: inputs cannot be serialized + TestEditorInputFactory.disableSerialize = true; + + deserialized = createGroup(group.serialize()); + assert.equal(group.id, deserialized.id); + assert.equal(deserialized.count, 0); + assert.equal(deserialized.stickyCount, 0); + assert.equal(deserialized.getEditors(EditorsOrder.SEQUENTIAL).length, 0); + assert.equal(deserialized.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 0); + + // Case 3: inputs cannot be deserialized + TestEditorInputFactory.disableSerialize = false; + TestEditorInputFactory.disableDeserialize = true; + + deserialized = createGroup(group.serialize()); + assert.equal(group.id, deserialized.id); + assert.equal(deserialized.count, 0); + assert.equal(deserialized.stickyCount, 0); + assert.equal(deserialized.getEditors(EditorsOrder.SEQUENTIAL).length, 0); + assert.equal(deserialized.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 0); + }); + test('One Editor', function () { const group = createGroup(); const events = groupListener(group); @@ -362,7 +461,6 @@ suite('Workbench editor groups', () => { assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 1); assert.equal(group.activeEditor, input1); assert.equal(group.isActive(input1), true); - assert.equal(group.isPreview(input1), false); assert.equal(group.isPinned(input1), true); assert.equal(group.isPinned(0), true); @@ -386,7 +484,6 @@ suite('Workbench editor groups', () => { assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 1); assert.equal(group.activeEditor, input2); assert.equal(group.isActive(input2), true); - assert.equal(group.isPreview(input2), true); assert.equal(group.isPinned(input2), false); assert.equal(group.isPinned(0), false); @@ -416,7 +513,6 @@ suite('Workbench editor groups', () => { assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 1); assert.equal(group.activeEditor, input3); assert.equal(group.isActive(input3), true); - assert.equal(group.isPreview(input3), false); assert.equal(group.isPinned(input3), true); assert.equal(group.isPinned(0), true); @@ -446,7 +542,6 @@ suite('Workbench editor groups', () => { assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 1); assert.equal(group.activeEditor, input4); assert.equal(group.isActive(input4), true); - assert.equal(group.isPreview(input4), true); assert.equal(group.isPinned(input4), false); assert.equal(group.isPinned(0), false); @@ -484,13 +579,10 @@ suite('Workbench editor groups', () => { assert.equal(group.activeEditor, input3); assert.equal(group.isActive(input1), false); assert.equal(group.isPinned(input1), true); - assert.equal(group.isPreview(input1), false); assert.equal(group.isActive(input2), false); assert.equal(group.isPinned(input2), true); - assert.equal(group.isPreview(input2), false); assert.equal(group.isActive(input3), true); assert.equal(group.isPinned(input3), true); - assert.equal(group.isPreview(input3), false); assert.equal(events.opened[0], input1); assert.equal(events.opened[1], input2); @@ -523,7 +615,7 @@ suite('Workbench editor groups', () => { group.closeEditor(sameInput1); assert.equal(events.closed[0].editor, input1); - group.closeAllEditors(); + closeAllEditors(group); assert.equal(events.closed.length, 3); assert.equal(group.count, 0); @@ -576,7 +668,7 @@ suite('Workbench editor groups', () => { assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[1], input2); assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[2], input1); - group.closeAllEditors(); + closeAllEditors(group); assert.equal(events.closed.length, 3); assert.equal(group.count, 0); @@ -600,15 +692,13 @@ suite('Workbench editor groups', () => { assert.equal(group.isActive(input1), true); assert.equal(group.isPinned(input1), true); assert.equal(group.isPinned(0), true); - assert.equal(group.isPreview(input1), false); assert.equal(group.isActive(input2), false); assert.equal(group.isPinned(input2), true); assert.equal(group.isPinned(1), true); - assert.equal(group.isPreview(input2), false); assert.equal(group.isActive(input3), false); assert.equal(group.isPinned(input3), true); assert.equal(group.isPinned(2), true); - assert.equal(group.isPreview(input3), false); + assert.equal(group.isPinned(input3), true); const mru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); assert.equal(mru[0], input1); @@ -634,7 +724,7 @@ suite('Workbench editor groups', () => { assert.equal(group.activeEditor, input3); assert.equal(group.isActive(input3), true); assert.equal(group.isPinned(input3), false); - assert.equal(group.isPreview(input3), true); + assert.equal(!group.isPinned(input3), true); assert.equal(events.opened[0], input1); assert.equal(events.opened[1], input2); @@ -703,7 +793,6 @@ suite('Workbench editor groups', () => { assert.equal(group.activeEditor, input3); assert.equal(group.isPinned(input3), true); - assert.equal(group.isPreview(input3), false); assert.equal(group.isActive(input3), true); assert.equal(events.pinned[0], input3); assert.equal(group.count, 3); @@ -712,7 +801,6 @@ suite('Workbench editor groups', () => { assert.equal(group.activeEditor, input3); assert.equal(group.isPinned(input1), false); - assert.equal(group.isPreview(input1), true); assert.equal(group.isActive(input1), false); assert.equal(events.unpinned[0], input1); assert.equal(group.count, 3); @@ -883,6 +971,25 @@ suite('Workbench editor groups', () => { assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[2], input3); assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[3], input4); assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[4], input5); + + assert.equal(events.moved.length, 4); + group.moveEditor(input1, 0); + assert.equal(events.moved.length, 4); + group.moveEditor(input1, -1); + assert.equal(events.moved.length, 4); + + group.moveEditor(input5, 4); + assert.equal(events.moved.length, 4); + group.moveEditor(input5, 100); + assert.equal(events.moved.length, 4); + + group.moveEditor(input5, -1); + assert.equal(events.moved.length, 5); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], input5); + + group.moveEditor(input1, 100); + assert.equal(events.moved.length, 6); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[4], input1); }); test('Multiple Editors - move editor across groups', function () { @@ -978,11 +1085,11 @@ suite('Workbench editor groups', () => { group.openEditor(input5, { active: true, pinned: true }); // Close Others - group.closeEditors(group.activeEditor!); + closeEditors(group, group.activeEditor!); assert.equal(group.activeEditor, input5); assert.equal(group.count, 1); - group.closeAllEditors(); + closeAllEditors(group); group.openEditor(input1, { active: true, pinned: true }); group.openEditor(input2, { active: true, pinned: true }); group.openEditor(input3, { active: true, pinned: true }); @@ -992,14 +1099,14 @@ suite('Workbench editor groups', () => { // Close Left assert.equal(group.activeEditor, input3); - group.closeEditors(group.activeEditor!, CloseDirection.LEFT); + closeEditors(group, group.activeEditor!, CloseDirection.LEFT); assert.equal(group.activeEditor, input3); assert.equal(group.count, 3); assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], input3); assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[1], input4); assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[2], input5); - group.closeAllEditors(); + closeAllEditors(group); group.openEditor(input1, { active: true, pinned: true }); group.openEditor(input2, { active: true, pinned: true }); group.openEditor(input3, { active: true, pinned: true }); @@ -1009,7 +1116,7 @@ suite('Workbench editor groups', () => { // Close Right assert.equal(group.activeEditor, input3); - group.closeEditors(group.activeEditor!, CloseDirection.RIGHT); + closeEditors(group, group.activeEditor!, CloseDirection.RIGHT); assert.equal(group.activeEditor, input3); assert.equal(group.count, 3); assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], input1); @@ -1053,7 +1160,7 @@ suite('Workbench editor groups', () => { assert.equal(openedEditor, testJs); assert.equal(group.previewEditor, styleCss); assert.equal(group.activeEditor, testJs); - assert.equal(group.isPreview(styleCss), true); + assert.equal(group.isPinned(styleCss), false); assert.equal(group.isPinned(testJs), true); assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], styleCss); assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[1], testJs); @@ -1064,7 +1171,7 @@ suite('Workbench editor groups', () => { group.openEditor(indexHtml2, { active: true }); assert.equal(group.activeEditor, indexHtml2); assert.equal(group.previewEditor, indexHtml2); - assert.equal(group.isPreview(indexHtml2), true); + assert.equal(group.isPinned(indexHtml2), false); assert.equal(group.isPinned(testJs), true); assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], testJs); assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[1], indexHtml2); @@ -1081,14 +1188,12 @@ suite('Workbench editor groups', () => { const indexHtml3 = input('index.html'); group.pin(indexHtml3); assert.equal(group.isPinned(indexHtml3), true); - assert.equal(group.isPreview(indexHtml3), false); assert.equal(group.activeEditor, testJs); // [test.js, index.html] -> [test.js, file.ts, index.html] const fileTs = input('file.ts'); group.openEditor(fileTs, { active: true, pinned: true }); assert.equal(group.isPinned(fileTs), true); - assert.equal(group.isPreview(fileTs), false); assert.equal(group.count, 3); assert.equal(group.activeEditor, fileTs); @@ -1096,7 +1201,6 @@ suite('Workbench editor groups', () => { group.unpin(fileTs); assert.equal(group.count, 3); assert.equal(group.isPinned(fileTs), false); - assert.equal(group.isPreview(fileTs), true); assert.equal(group.activeEditor, fileTs); // [test.js, /file.ts/, index.html] -> [test.js, /other.ts/, index.html] @@ -1132,7 +1236,6 @@ suite('Workbench editor groups', () => { assert.equal(group.activeEditor, testJs); assert.ok(group.getEditors(EditorsOrder.SEQUENTIAL)[0].matches(testJs)); assert.equal(group.isPinned(testJs), false); - assert.equal(group.isPreview(testJs), true); // /test.js/ -> [] group.closeEditor(testJs); @@ -1289,6 +1392,41 @@ suite('Workbench editor groups', () => { assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[1].matches(serializableInput1), true); }); + test('Single group, multiple editors - persist (some not persistable, sticky editors)', function () { + let inst = new TestInstantiationService(); + + inst.stub(IStorageService, new TestStorageService()); + inst.stub(IWorkspaceContextService, new TestContextService()); + const lifecycle = new TestLifecycleService(); + inst.stub(ILifecycleService, lifecycle); + inst.stub(ITelemetryService, NullTelemetryService); + + const config = new TestConfigurationService(); + config.setUserConfiguration('workbench', { editor: { openPositioning: 'right' } }); + inst.stub(IConfigurationService, config); + + inst.invokeFunction(accessor => Registry.as(EditorExtensions.EditorInputFactories).start(accessor)); + + let group = createGroup(); + + const serializableInput1 = input(); + const nonSerializableInput2 = input('3', true); + const serializableInput2 = input(); + + group.openEditor(serializableInput1, { active: true, pinned: true }); + group.openEditor(nonSerializableInput2, { active: true, pinned: true, sticky: true }); + group.openEditor(serializableInput2, { active: false, pinned: true }); + + assert.equal(group.count, 3); + assert.equal(group.stickyCount, 1); + + // Create model again - should load from storage + group = inst.createInstance(EditorGroup, group.serialize()); + + assert.equal(group.count, 2); + assert.equal(group.stickyCount, 0); + }); + test('Multiple groups, multiple editors - persist (some not persistable, causes empty group)', function () { let inst = new TestInstantiationService(); @@ -1413,7 +1551,7 @@ suite('Workbench editor groups', () => { assert.equal(dirty2Counter, 1); assert.equal(label2ChangeCounter, 1); - group2.closeAllEditors(); + closeAllEditors(group2); (input2).setDirty(); (input2).setLabel(); @@ -1423,4 +1561,268 @@ suite('Workbench editor groups', () => { assert.equal(dirty1Counter, 1); assert.equal(label1ChangeCounter, 1); }); + + test('Sticky Editors', function () { + const group = createGroup(); + + const input1 = input(); + const input2 = input(); + const input3 = input(); + const input4 = input(); + + group.openEditor(input1, { pinned: true, active: true }); + group.openEditor(input2, { pinned: true, active: true }); + group.openEditor(input3, { pinned: false, active: true }); + + assert.equal(group.stickyCount, 0); + + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL).length, 3); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }).length, 3); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 3); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, { excludeSticky: true }).length, 3); + + // Stick last editor should move it first and pin + group.stick(input3); + assert.equal(group.stickyCount, 1); + assert.equal(group.isSticky(input1), false); + assert.equal(group.isSticky(input2), false); + assert.equal(group.isSticky(input3), true); + assert.equal(group.isPinned(input3), true); + assert.equal(group.indexOf(input1), 1); + assert.equal(group.indexOf(input2), 2); + assert.equal(group.indexOf(input3), 0); + + let sequentialAllEditors = group.getEditors(EditorsOrder.SEQUENTIAL); + assert.equal(sequentialAllEditors.length, 3); + let sequentialEditorsExcludingSticky = group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }); + assert.equal(sequentialEditorsExcludingSticky.length, 2); + assert.ok(sequentialEditorsExcludingSticky.indexOf(input1) >= 0); + assert.ok(sequentialEditorsExcludingSticky.indexOf(input2) >= 0); + let mruAllEditors = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); + assert.equal(mruAllEditors.length, 3); + let mruEditorsExcludingSticky = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, { excludeSticky: true }); + assert.equal(mruEditorsExcludingSticky.length, 2); + assert.ok(mruEditorsExcludingSticky.indexOf(input1) >= 0); + assert.ok(mruEditorsExcludingSticky.indexOf(input2) >= 0); + + // Sticking same editor again is a no-op + group.stick(input3); + assert.equal(group.isSticky(input3), true); + + // Sticking last editor now should move it after sticky one + group.stick(input2); + assert.equal(group.stickyCount, 2); + assert.equal(group.isSticky(input1), false); + assert.equal(group.isSticky(input2), true); + assert.equal(group.isSticky(input3), true); + assert.equal(group.indexOf(input1), 2); + assert.equal(group.indexOf(input2), 1); + assert.equal(group.indexOf(input3), 0); + + sequentialAllEditors = group.getEditors(EditorsOrder.SEQUENTIAL); + assert.equal(sequentialAllEditors.length, 3); + sequentialEditorsExcludingSticky = group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }); + assert.equal(sequentialEditorsExcludingSticky.length, 1); + assert.ok(sequentialEditorsExcludingSticky.indexOf(input1) >= 0); + mruAllEditors = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); + assert.equal(mruAllEditors.length, 3); + mruEditorsExcludingSticky = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, { excludeSticky: true }); + assert.equal(mruEditorsExcludingSticky.length, 1); + assert.ok(mruEditorsExcludingSticky.indexOf(input1) >= 0); + + // Sticking remaining editor also works + group.stick(input1); + assert.equal(group.stickyCount, 3); + assert.equal(group.isSticky(input1), true); + assert.equal(group.isSticky(input2), true); + assert.equal(group.isSticky(input3), true); + assert.equal(group.indexOf(input1), 2); + assert.equal(group.indexOf(input2), 1); + assert.equal(group.indexOf(input3), 0); + + sequentialAllEditors = group.getEditors(EditorsOrder.SEQUENTIAL); + assert.equal(sequentialAllEditors.length, 3); + sequentialEditorsExcludingSticky = group.getEditors(EditorsOrder.SEQUENTIAL, { excludeSticky: true }); + assert.equal(sequentialEditorsExcludingSticky.length, 0); + mruAllEditors = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); + assert.equal(mruAllEditors.length, 3); + mruEditorsExcludingSticky = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE, { excludeSticky: true }); + assert.equal(mruEditorsExcludingSticky.length, 0); + + // Unsticking moves editor after sticky ones + group.unstick(input3); + assert.equal(group.stickyCount, 2); + assert.equal(group.isSticky(input1), true); + assert.equal(group.isSticky(input2), true); + assert.equal(group.isSticky(input3), false); + assert.equal(group.indexOf(input1), 1); + assert.equal(group.indexOf(input2), 0); + assert.equal(group.indexOf(input3), 2); + + // Unsticking all works + group.unstick(input1); + group.unstick(input2); + assert.equal(group.stickyCount, 0); + assert.equal(group.isSticky(input1), false); + assert.equal(group.isSticky(input2), false); + assert.equal(group.isSticky(input3), false); + + group.moveEditor(input1, 0); + group.moveEditor(input2, 1); + group.moveEditor(input3, 2); + + // Opening a new editor always opens after sticky editors + group.stick(input1); + group.stick(input2); + group.setActive(input1); + + const events = groupListener(group); + + group.openEditor(input4, { pinned: true, active: true }); + assert.equal(group.indexOf(input4), 2); + group.closeEditor(input4); + + assert.equal(events.closed[0].sticky, false); + + group.setActive(input2); + + group.openEditor(input4, { pinned: true, active: true }); + assert.equal(group.indexOf(input4), 2); + group.closeEditor(input4); + + assert.equal(events.closed[1].sticky, false); + + // Reset + assert.equal(group.stickyCount, 2); + assert.equal(group.isSticky(input1), true); + assert.equal(group.isSticky(input2), true); + assert.equal(group.isSticky(input3), false); + assert.equal(group.indexOf(input1), 0); + assert.equal(group.indexOf(input2), 1); + assert.equal(group.indexOf(input3), 2); + + // Moving a sticky editor works + group.moveEditor(input1, 1); // still moved within sticky range + assert.equal(group.isSticky(input1), true); + assert.equal(group.isSticky(input2), true); + assert.equal(group.isSticky(input3), false); + assert.equal(group.indexOf(input1), 1); + assert.equal(group.indexOf(input2), 0); + assert.equal(group.indexOf(input3), 2); + + group.moveEditor(input1, 0); // still moved within sticky range + assert.equal(group.isSticky(input1), true); + assert.equal(group.isSticky(input2), true); + assert.equal(group.isSticky(input3), false); + assert.equal(group.indexOf(input1), 0); + assert.equal(group.indexOf(input2), 1); + assert.equal(group.indexOf(input3), 2); + + group.moveEditor(input1, 2); // moved out of sticky range + assert.equal(group.isSticky(input1), false); + assert.equal(group.isSticky(input2), true); + assert.equal(group.isSticky(input3), false); + assert.equal(group.indexOf(input1), 2); + assert.equal(group.indexOf(input2), 0); + assert.equal(group.indexOf(input3), 1); + + group.moveEditor(input2, 2); // moved out of sticky range + assert.equal(group.isSticky(input1), false); + assert.equal(group.isSticky(input2), false); + assert.equal(group.isSticky(input3), false); + assert.equal(group.indexOf(input1), 1); + assert.equal(group.indexOf(input2), 2); + assert.equal(group.indexOf(input3), 0); + + // Reset + group.moveEditor(input1, 0); + group.moveEditor(input2, 1); + group.moveEditor(input3, 2); + group.stick(input1); + group.unstick(input2); + assert.equal(group.stickyCount, 1); + assert.equal(group.isSticky(input1), true); + assert.equal(group.isSticky(input2), false); + assert.equal(group.isSticky(input3), false); + assert.equal(group.indexOf(input1), 0); + assert.equal(group.indexOf(input2), 1); + assert.equal(group.indexOf(input3), 2); + + // Moving a unsticky editor in works + group.moveEditor(input3, 1); // still moved within unsticked range + assert.equal(group.isSticky(input1), true); + assert.equal(group.isSticky(input2), false); + assert.equal(group.isSticky(input3), false); + assert.equal(group.indexOf(input1), 0); + assert.equal(group.indexOf(input2), 2); + assert.equal(group.indexOf(input3), 1); + + group.moveEditor(input3, 2); // still moved within unsticked range + assert.equal(group.isSticky(input1), true); + assert.equal(group.isSticky(input2), false); + assert.equal(group.isSticky(input3), false); + assert.equal(group.indexOf(input1), 0); + assert.equal(group.indexOf(input2), 1); + assert.equal(group.indexOf(input3), 2); + + group.moveEditor(input3, 0); // moved into sticky range + assert.equal(group.isSticky(input1), true); + assert.equal(group.isSticky(input2), false); + assert.equal(group.isSticky(input3), true); + assert.equal(group.indexOf(input1), 1); + assert.equal(group.indexOf(input2), 2); + assert.equal(group.indexOf(input3), 0); + + group.moveEditor(input2, 0); // moved into sticky range + assert.equal(group.isSticky(input1), true); + assert.equal(group.isSticky(input2), true); + assert.equal(group.isSticky(input3), true); + assert.equal(group.indexOf(input1), 2); + assert.equal(group.indexOf(input2), 0); + assert.equal(group.indexOf(input3), 1); + + // Closing a sticky editor updates state properly + group.stick(input1); + group.stick(input2); + group.unstick(input3); + assert.equal(group.stickyCount, 2); + group.closeEditor(input1); + assert.equal(events.closed[2].sticky, true); + assert.equal(group.stickyCount, 1); + group.closeEditor(input2); + assert.equal(events.closed[3].sticky, true); + assert.equal(group.stickyCount, 0); + + closeAllEditors(group); + assert.equal(group.stickyCount, 0); + + // Open sticky + group.openEditor(input1, { sticky: true }); + assert.equal(group.stickyCount, 1); + assert.equal(group.isSticky(input1), true); + + group.openEditor(input2, { pinned: true, active: true }); + assert.equal(group.stickyCount, 1); + assert.equal(group.isSticky(input1), true); + assert.equal(group.isSticky(input2), false); + + group.openEditor(input2, { sticky: true }); + assert.equal(group.stickyCount, 2); + assert.equal(group.isSticky(input1), true); + assert.equal(group.isSticky(input2), true); + + group.openEditor(input3, { pinned: true, active: true }); + group.openEditor(input4, { pinned: false, active: true, sticky: true }); + assert.equal(group.stickyCount, 3); + assert.equal(group.isSticky(input1), true); + assert.equal(group.isSticky(input2), true); + assert.equal(group.isSticky(input3), false); + assert.equal(group.isSticky(input4), true); + assert.equal(group.isPinned(input4), true); + + assert.equal(group.indexOf(input1), 0); + assert.equal(group.indexOf(input2), 1); + assert.equal(group.indexOf(input3), 3); + assert.equal(group.indexOf(input4), 2); + }); }); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 640606f3cc..7e096594e8 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -10,7 +10,7 @@ import * as resources from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { IEditorInputWithOptions, CloseDirection, IEditorIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorInput, IEditorPane, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorInput, EditorOptions, EditorsOrder, IFileEditorInput, IEditorInputFactoryRegistry, IEditorInputFactory, Extensions as EditorExtensions, ISaveOptions, IMoveResult, ITextEditorPane, ITextDiffEditorPane, IVisibleEditorPane } from 'vs/workbench/common/editor'; +import { IEditorInputWithOptions, IEditorIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorInput, IEditorPane, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorInput, EditorOptions, EditorsOrder, IFileEditorInput, IEditorInputFactoryRegistry, IEditorInputFactory, Extensions as EditorExtensions, ISaveOptions, IMoveResult, ITextEditorPane, ITextDiffEditorPane, IVisibleEditorPane } from 'vs/workbench/common/editor'; import { IEditorOpeningEvent, EditorServiceImpl, IEditorGroupView, IEditorGroupsAccessor } from 'vs/workbench/browser/parts/editor/editor'; import { Event, Emitter } from 'vs/base/common/event'; import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup'; @@ -51,7 +51,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IDecorationsService, IResourceDecorationChangeEvent, IDecoration, IDecorationData, IDecorationsProvider } from 'vs/workbench/services/decorations/browser/decorations'; import { IDisposable, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IAddGroupOptions, IMergeGroupOptions, IMoveEditorOptions, ICopyEditorOptions, IEditorReplacement, IGroupChangeEvent, IFindGroupScope, EditorGroupLayout, ICloseEditorOptions, GroupOrientation } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IAddGroupOptions, IMergeGroupOptions, IMoveEditorOptions, ICopyEditorOptions, IEditorReplacement, IGroupChangeEvent, IFindGroupScope, EditorGroupLayout, ICloseEditorOptions, GroupOrientation, ICloseAllEditorsOptions, ICloseEditorsFilter } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService, IOpenEditorOverrideHandler, ISaveEditorsOptions, IRevertAllEditorsOptions, IResourceEditorInputType, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE, IOpenEditorOverrideEntry, ICustomEditorViewTypesHandler } from 'vs/workbench/services/editor/common/editorService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorRegistry, EditorDescriptor, Extensions } from 'vs/workbench/browser/editor'; @@ -340,8 +340,6 @@ export class TestHistoryService implements IHistoryService { openLastEditLocation(): void { } } - - export class TestFileDialogService implements IFileDialogService { _serviceBrand: undefined; @@ -547,6 +545,7 @@ export class TestEditorGroupView implements IEditorGroupView { activeEditor!: IEditorInput; previewEditor!: IEditorInput; count!: number; + stickyCount!: number; disposed!: boolean; editors: ReadonlyArray = []; label!: string; @@ -578,14 +577,17 @@ export class TestEditorGroupView implements IEditorGroupView { openEditors(_editors: IEditorInputWithOptions[]): Promise { throw new Error('not implemented'); } isOpened(_editor: IEditorInput | IResourceEditorInput): boolean { return false; } isPinned(_editor: IEditorInput): boolean { return false; } + isSticky(_editor: IEditorInput): boolean { return false; } isActive(_editor: IEditorInput): boolean { return false; } moveEditor(_editor: IEditorInput, _target: IEditorGroup, _options?: IMoveEditorOptions): void { } copyEditor(_editor: IEditorInput, _target: IEditorGroup, _options?: ICopyEditorOptions): void { } closeEditor(_editor?: IEditorInput, options?: ICloseEditorOptions): Promise { return Promise.resolve(); } - closeEditors(_editors: IEditorInput[] | { except?: IEditorInput; direction?: CloseDirection; savedOnly?: boolean; }, options?: ICloseEditorOptions): Promise { return Promise.resolve(); } - closeAllEditors(): Promise { return Promise.resolve(); } + closeEditors(_editors: IEditorInput[] | ICloseEditorsFilter, options?: ICloseEditorOptions): Promise { return Promise.resolve(); } + closeAllEditors(options?: ICloseAllEditorsOptions): Promise { return Promise.resolve(); } replaceEditors(_editors: IEditorReplacement[]): Promise { return Promise.resolve(); } pinEditor(_editor?: IEditorInput): void { } + stickEditor(editor?: IEditorInput | undefined): void { } + unstickEditor(editor?: IEditorInput | undefined): void { } focus(): void { } invokeWithinContext(fn: (accessor: ServicesAccessor) => T): T { throw new Error('not implemented'); } setActive(_isActive: boolean): void { } @@ -666,8 +668,8 @@ export class TestEditorService implements EditorServiceImpl { createEditorInput(_input: IResourceEditorInput | IUntitledTextResourceEditorInput | IResourceDiffEditorInput): EditorInput { throw new Error('not implemented'); } save(editors: IEditorIdentifier[], options?: ISaveEditorsOptions): Promise { throw new Error('Method not implemented.'); } saveAll(options?: ISaveEditorsOptions): Promise { throw new Error('Method not implemented.'); } - revert(editors: IEditorIdentifier[], options?: IRevertOptions): Promise { throw new Error('Method not implemented.'); } - revertAll(options?: IRevertAllEditorsOptions): Promise { throw new Error('Method not implemented.'); } + revert(editors: IEditorIdentifier[], options?: IRevertOptions): Promise { throw new Error('Method not implemented.'); } + revertAll(options?: IRevertAllEditorsOptions): Promise { throw new Error('Method not implemented.'); } } export class TestFileService implements IFileService { @@ -1061,6 +1063,7 @@ export class TestFileEditorInput extends EditorInput implements IFileEditorInput } async save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { this.gotSaved = true; + this.dirty = false; return this; } async saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { @@ -1071,6 +1074,7 @@ export class TestFileEditorInput extends EditorInput implements IFileEditorInput this.gotReverted = true; this.gotSaved = false; this.gotSavedAs = false; + this.dirty = false; } setDirty(): void { this.dirty = true; } isDirty(): boolean { diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 142fdc24b6..f306eaec8c 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -52,7 +52,6 @@ import 'vs/workbench/services/workspaces/electron-browser/workspacesService'; import 'vs/workbench/services/workspaces/electron-browser/workspaceEditingService'; import 'vs/workbench/services/userDataSync/electron-browser/storageKeysSyncRegistryService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncService'; -import 'vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService'; import 'vs/workbench/services/authentication/electron-browser/authenticationTokenService'; import 'vs/workbench/services/authentication/browser/authenticationService'; import 'vs/workbench/services/host/electron-browser/desktopHostService'; @@ -73,9 +72,12 @@ import { ICredentialsService } from 'vs/platform/credentials/common/credentials' import { KeytarCredentialsService } from 'vs/platform/credentials/node/credentialsService'; import { TitlebarPart } from 'vs/workbench/electron-browser/parts/titlebar/titlebarPart'; import { ITitleService } from 'vs/workbench/services/title/common/titleService'; +import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataAutoSyncService } from 'vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService'; registerSingleton(ICredentialsService, KeytarCredentialsService, true); registerSingleton(ITitleService, TitlebarPart); +registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService); //#endregion diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index fcab29a3ca..a2243c2396 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -124,6 +124,7 @@ import 'vs/workbench/contrib/webview/browser/webviewService'; import 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; // Terminal +import 'vs/workbench/contrib/terminal/browser/terminal.web.contribution'; import 'vs/workbench/contrib/terminal/browser/terminalInstanceService'; // Tasks diff --git a/test/automation/src/search.ts b/test/automation/src/search.ts index 51d1c4ce43..03e0fd49c1 100644 --- a/test/automation/src/search.ts +++ b/test/automation/src/search.ts @@ -114,7 +114,7 @@ export class Search extends Viewlet { } async waitForNoResultText(): Promise { - await this.code.waitForElement(`${VIEWLET} .messages[aria-hidden="true"] .message>span`); + await this.code.waitForTextContent(`${VIEWLET} .messages`, ''); } private async waitForInputFocus(selector: string): Promise { diff --git a/yarn.lock b/yarn.lock index 967eb0ff8c..c88fbaacef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9963,10 +9963,10 @@ vscode-nsfw@1.2.8: lodash.isundefined "^3.0.1" nan "^2.10.0" -vscode-oniguruma@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.3.0.tgz#6788a9db2f8b0781243b4eb8c7a1dd25f6c0e2c8" - integrity sha512-m4Br19v6XD4MRbVrgsLNSZgQrBzk1BCMCleL8+GrcoGxKEJJd62zOFcTaoQR3hCrSlLqoxWmJ7Cc0VieVV3iTQ== +vscode-oniguruma@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.3.1.tgz#e2383879c3485b19f533ec34efea9d7a2b14be8f" + integrity sha512-gz6ZBofA7UXafVA+m2Yt2zHKgXC2qedArprIsHAPKByTkwq9l5y/izAGckqxYml7mSbYxTRTfdRwsFq3cwF4LQ== vscode-proxy-agent@^0.5.2: version "0.5.2" @@ -10304,20 +10304,20 @@ xterm-addon-unicode11@0.2.0-beta.5: resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.2.0-beta.5.tgz#5961850162df20b5e966166423cd6957ac2db298" integrity sha512-IjnbBcyfS5JgJDXPO0W2nk/VBtGwx6GWE2snMC676z4DmAABUqPXfTzJKfUoWqoT6UcbxB0oIjDzykCfoRJp6Q== -xterm-addon-web-links@0.4.0-beta.5: - version "0.4.0-beta.5" - resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.4.0-beta.5.tgz#523fd0a1c5668370d73e05019ed16eaf596894c8" - integrity sha512-Qe0idPpSokCNvGrthSBjdrOZrsgXwnLYbzuv0JoEec/A9HVcxKmZ+ktw7fOA2gT/zbcwtrA5FWrir3GlRHglCQ== +xterm-addon-web-links@0.4.0-beta.6: + version "0.4.0-beta.6" + resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.4.0-beta.6.tgz#d159d4542eb9a02d57977fe7eb5f42f8ef2f27fa" + integrity sha512-dsQVD/EyVq8PtAYGh2PGQTCt009UipIfX6Q2SBDlz+W9x7IkXjhRxRaryMmLsBCca20qeVKwmbQ+ANhLi+nTaQ== xterm-addon-webgl@0.7.0-beta.10: version "0.7.0-beta.10" resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.7.0-beta.10.tgz#39fdb96351e97a1bf15f4c4c8944ba3d05cacee4" integrity sha512-nQl/ASk+ck11aSrBZXb2a0tu+SNDnm89owBk/sAZeZzi5MHNo6bB8y2VTKNNC6D3i3aFouTz4VorYB25LUgNFg== -xterm@4.6.0-beta.38: - version "4.6.0-beta.38" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.6.0-beta.38.tgz#8472b168941500c3071aba482c2b5c6040951ec7" - integrity sha512-Q+nOalMD1MDGOqXdtkGZmOQqbSBU+71vhlX2RBwQoSpJa1QBrKDAhSlN/J+/XvouvVEtCiEFDeacF4EufMEIMg== +xterm@4.6.0-beta.44: + version "4.6.0-beta.44" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.6.0-beta.44.tgz#76b2a6b8e147595ab44aa752c0e721d935464615" + integrity sha512-vYtfz4spFcSKLEUpC6anH7TwDams71+k2wAtUzCJ47dNL2IrwYafcFsvGPm46QLTtq4M2Bp9rQo3R3V746yxNg== y18n@^3.2.1: version "3.2.1"