diff --git a/build/.moduleignore b/build/.moduleignore index 9d25f3553b5..56e2fb15718 100644 --- a/build/.moduleignore +++ b/build/.moduleignore @@ -102,6 +102,7 @@ vsda/src/** vsda/.gitignore vsda/binding.gyp vsda/README.md +vsda/SECURITY.md vsda/targets !vsda/build/Release/vsda.node diff --git a/build/.webignore b/build/.webignore index 37154964b80..bc8fe659566 100644 --- a/build/.webignore +++ b/build/.webignore @@ -49,6 +49,5 @@ xterm-addon-webgl/out/** !@microsoft/applicationinsights-core-js/browser/applicationinsights-core-js.min.js !@microsoft/applicationinsights-shims/dist/umd/applicationinsights-shims.min.js - - - +vsda/** +!vsda/rust/web/** diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index 4630eaad3cc..2a0c236eaf0 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -72,14 +72,13 @@ const serverResources = [ 'out-build/vs/base/node/ps.sh', // Terminal shell integration - 'out-build/vs/workbench/contrib/terminal/browser/media/shellIntegration.fish', 'out-build/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1', 'out-build/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh', 'out-build/vs/workbench/contrib/terminal/browser/media/shellIntegration-env.zsh', 'out-build/vs/workbench/contrib/terminal/browser/media/shellIntegration-profile.zsh', 'out-build/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh', 'out-build/vs/workbench/contrib/terminal/browser/media/shellIntegration-login.zsh', - 'out-build/vs/workbench/contrib/terminal/browser/media/shellIntegration.fish', + 'out-build/vs/workbench/contrib/terminal/browser/media/fish_xdg_data/fish/vendor_conf.d/shellIntegration.fish', '!**/test/**' ]; diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index aa8de556bef..0ca2cfd60a9 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -68,7 +68,7 @@ const vscodeResources = [ 'out-build/vs/workbench/browser/media/*-theme.css', 'out-build/vs/workbench/contrib/debug/**/*.json', 'out-build/vs/workbench/contrib/externalTerminal/**/*.scpt', - 'out-build/vs/workbench/contrib/terminal/browser/media/*.fish', + 'out-build/vs/workbench/contrib/terminal/browser/media/fish_xdg_data/fish/vendor_conf.d/*.fish', 'out-build/vs/workbench/contrib/terminal/browser/media/*.ps1', 'out-build/vs/workbench/contrib/terminal/browser/media/*.sh', 'out-build/vs/workbench/contrib/terminal/browser/media/*.zsh', diff --git a/build/gulpfile.vscode.win32.js b/build/gulpfile.vscode.win32.js index 0d3abdae01b..6e9a6f331ba 100644 --- a/build/gulpfile.vscode.win32.js +++ b/build/gulpfile.vscode.win32.js @@ -99,6 +99,9 @@ function buildWin32Setup(arch, target) { RegValueName: product.win32RegValueName, ShellNameShort: product.win32ShellNameShort, AppMutex: product.win32MutexName, + TunnelMutex: product.win32TunnelMutex, + TunnelServiceMutex: product.win32TunnelServiceMutex, + ApplicationName: product.applicationName, Arch: arch, AppId: { 'ia32': ia32AppId, 'x64': x64AppId, 'arm64': arm64AppId }[arch], IncompatibleTargetAppId: { 'ia32': product.win32AppId, 'x64': product.win32x64AppId, 'arm64': product.win32arm64AppId }[arch], diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 13cf8a1be2a..0a13a59a1b1 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -36,6 +36,8 @@ "--vscode-charts-purple", "--vscode-charts-red", "--vscode-charts-yellow", + "--vscode-chat-requestBackground", + "--vscode-chat-requestBorder", "--vscode-checkbox-background", "--vscode-checkbox-border", "--vscode-checkbox-foreground", @@ -304,8 +306,6 @@ "--vscode-inputValidation-warningBackground", "--vscode-inputValidation-warningBorder", "--vscode-inputValidation-warningForeground", - "--vscode-interactive-requestBackground", - "--vscode-interactive-requestBorder", "--vscode-interactiveEditor-border", "--vscode-interactiveEditor-regionHighlight", "--vscode-interactiveEditor-shadow", diff --git a/build/lib/util.js b/build/lib/util.js index 9ac562f4640..e683874090f 100644 --- a/build/lib/util.js +++ b/build/lib/util.js @@ -322,6 +322,11 @@ function acquireWebNodePaths() { const root = path.join(__dirname, '..', '..'); const webPackageJSON = path.join(root, '/remote/web', 'package.json'); const webPackages = JSON.parse(fs.readFileSync(webPackageJSON, 'utf8')).dependencies; + const distroWebPackageJson = path.join(root, '.build/distro/npm/remote/web/package.json'); + if (fs.existsSync(distroWebPackageJson)) { + const distroWebPackages = JSON.parse(fs.readFileSync(distroWebPackageJson, 'utf8')).dependencies; + Object.assign(webPackages, distroWebPackages); + } const nodePaths = {}; for (const key of Object.keys(webPackages)) { const packageJSON = path.join(root, 'node_modules', key, 'package.json'); @@ -400,4 +405,4 @@ function buildWebNodePaths(outDir) { return result; } exports.buildWebNodePaths = buildWebNodePaths; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXRpbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInV0aWwudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBOzs7Z0dBR2dHOzs7QUFFaEcsbUNBQW1DO0FBQ25DLHNDQUF1QztBQUN2Qyx1Q0FBdUM7QUFDdkMsc0NBQXNDO0FBQ3RDLDZCQUE2QjtBQUM3Qix5QkFBeUI7QUFDekIsa0NBQWtDO0FBQ2xDLG1DQUFtQztBQUduQyw2QkFBb0M7QUFDcEMsZ0RBQWdEO0FBRWhELE1BQU0sSUFBSSxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDO0FBTW5ELE1BQU0sbUJBQW1CLEdBQXVCLEVBQUUsdUJBQXVCLEVBQUUsR0FBRyxFQUFFLENBQUMsS0FBSyxFQUFFLENBQUM7QUFNekYsU0FBZ0IsV0FBVyxDQUFDLGNBQStCLEVBQUUsT0FBK0IsRUFBRSxvQkFBOEI7SUFDM0gsTUFBTSxLQUFLLEdBQUcsRUFBRSxDQUFDLE9BQU8sRUFBRSxDQUFDO0lBQzNCLE1BQU0sTUFBTSxHQUFHLEVBQUUsQ0FBQyxPQUFPLEVBQUUsQ0FBQztJQUM1QixJQUFJLEtBQUssR0FBRyxNQUFNLENBQUM7SUFDbkIsSUFBSSxNQUFNLEdBQUcsTUFBTSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUVqQyxNQUFNLEtBQUssR0FBbUMsQ0FBQyxvQkFBb0IsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxFQUFFLHVCQUF1QixFQUFFLEdBQUcsRUFBRSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRSxDQUFDO0lBRXBKLE1BQU0sR0FBRyxHQUFHLENBQUMsS0FBNkIsRUFBRSxhQUFzQixFQUFFLEVBQUU7UUFDckUsS0FBSyxHQUFHLFNBQVMsQ0FBQztRQUVsQixNQUFNLE1BQU0sR0FBRyxDQUFDLG9CQUFvQixDQUFDLENB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 \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXRpbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInV0aWwudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBOzs7Z0dBR2dHOzs7QUFFaEcsbUNBQW1DO0FBQ25DLHNDQUF1QztBQUN2Qyx1Q0FBdUM7QUFDdkMsc0NBQXNDO0FBQ3RDLDZCQUE2QjtBQUM3Qix5QkFBeUI7QUFDekIsa0NBQWtDO0FBQ2xDLG1DQUFtQztBQUduQyw2QkFBb0M7QUFDcEMsZ0RBQWdEO0FBRWhELE1BQU0sSUFBSSxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDO0FBTW5ELE1BQU0sbUJBQW1CLEdBQXVCLEVBQUUsdUJBQXVCLEVBQUUsR0FBRyxFQUFFLENBQUMsS0FBSyxFQUFFLENBQUM7QUFNekYsU0FBZ0IsV0FBVyxDQUFDLGNBQStCLEVBQUUsT0FBK0IsRUFBRSxvQkFBOEI7SUFDM0gsTUFBTSxLQUFLLEdBQUcsRUFBRSxDQUFDLE9BQU8sRUFBRSxDQUFDO0lBQzNCLE1BQU0sTUFBTSxHQUFHLEVBQUUsQ0FBQyxPQUFPLEVBQUUsQ0FBQztJQUM1QixJQUFJLEtBQUssR0FBRyxNQUFNLENBQUM7SUFDbkIsSUFBSSxNQUFNLEdBQUcsTUFBTSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUVqQyxNQUFNLEtBQUssR0FBbUMsQ0FBQyxvQkFBb0IsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxFQUFFLHVCQUF1QixFQUFFLEdBQUcsRUFBRSxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRSxDQUFDO0lBRXBKLE1BQU0sR0FBRyxHQUFHLENBQUMsS0FBNkIsRUFBRSxhQUFzQixFQUFFLEVBQUU7UUFDckUsS0FBSyxHQUFHLFNBQVMsQ0FBQztRQUVsQixNQUFNLE1BQU0sR0FBRyxDQUFDLG9CQUFvQixDQUFDLENB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 \ No newline at end of file diff --git a/build/lib/util.ts b/build/lib/util.ts index 004f131e2fb..5ee92a7f9d3 100644 --- a/build/lib/util.ts +++ b/build/lib/util.ts @@ -394,6 +394,13 @@ export function acquireWebNodePaths() { const root = path.join(__dirname, '..', '..'); const webPackageJSON = path.join(root, '/remote/web', 'package.json'); const webPackages = JSON.parse(fs.readFileSync(webPackageJSON, 'utf8')).dependencies; + + const distroWebPackageJson = path.join(root, '.build/distro/npm/remote/web/package.json'); + if (fs.existsSync(distroWebPackageJson)) { + const distroWebPackages = JSON.parse(fs.readFileSync(distroWebPackageJson, 'utf8')).dependencies; + Object.assign(webPackages, distroWebPackages); + } + const nodePaths: { [key: string]: string } = {}; for (const key of Object.keys(webPackages)) { const packageJSON = path.join(root, 'node_modules', key, 'package.json'); diff --git a/build/win32/code.iss b/build/win32/code.iss index d365ab1cbda..7754562225b 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -62,13 +62,13 @@ Name: "hungarian"; MessagesFile: "{#RepoDir}\build\win32\i18n\Default.hu.isl,{#R Name: "turkish"; MessagesFile: "compiler:Languages\Turkish.isl,{#RepoDir}\build\win32\i18n\messages.tr.isl" {#LocalizedLanguageFile("trk")} [InstallDelete] -Type: filesandordirs; Name: "{app}\resources\app\out"; Check: IsNotUpdate -Type: filesandordirs; Name: "{app}\resources\app\plugins"; Check: IsNotUpdate -Type: filesandordirs; Name: "{app}\resources\app\extensions"; Check: IsNotUpdate -Type: filesandordirs; Name: "{app}\resources\app\node_modules"; Check: IsNotUpdate -Type: filesandordirs; Name: "{app}\resources\app\node_modules.asar.unpacked"; Check: IsNotUpdate -Type: files; Name: "{app}\resources\app\node_modules.asar"; Check: IsNotUpdate -Type: files; Name: "{app}\resources\app\Credits_45.0.2454.85.html"; Check: IsNotUpdate +Type: filesandordirs; Name: "{app}\resources\app\out"; Check: IsNotBackgroundUpdate +Type: filesandordirs; Name: "{app}\resources\app\plugins"; Check: IsNotBackgroundUpdate +Type: filesandordirs; Name: "{app}\resources\app\extensions"; Check: IsNotBackgroundUpdate +Type: filesandordirs; Name: "{app}\resources\app\node_modules"; Check: IsNotBackgroundUpdate +Type: filesandordirs; Name: "{app}\resources\app\node_modules.asar.unpacked"; Check: IsNotBackgroundUpdate +Type: files; Name: "{app}\resources\app\node_modules.asar"; Check: IsNotBackgroundUpdate +Type: files; Name: "{app}\resources\app\Credits_45.0.2454.85.html"; Check: IsNotBackgroundUpdate [UninstallDelete] Type: filesandordirs; Name: "{app}\_" @@ -1299,6 +1299,16 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValu Root: {#EnvironmentRootKey}; Subkey: "{#EnvironmentKey}"; ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}\bin"; Tasks: addtopath; Check: NeedsAddPath(ExpandConstant('{app}\bin')) [Code] +function IsBackgroundUpdate(): Boolean; +begin + Result := ExpandConstant('{param:update|false}') <> 'false'; +end; + +function IsNotBackgroundUpdate(): Boolean; +begin + Result := not IsBackgroundUpdate(); +end; + // Don't allow installing conflicting architectures function InitializeSetup(): Boolean; var @@ -1351,6 +1361,13 @@ begin MsgBox('Please uninstall the ' + AltArch + '-bit version of {#NameShort} before installing this ' + ThisArch + '-bit version.', mbInformation, MB_OK); end; end; + + if IsNotBackgroundUpdate() and CheckForMutexes('{#TunnelMutex}') then + begin + MsgBox('{#NameShort} is still running a tunnel. Please stop the tunnel before installing.', mbInformation, MB_OK); + Result := false + end; + end; function WizardNotSilent(): Boolean; @@ -1359,14 +1376,44 @@ begin end; // Updates -function IsBackgroundUpdate(): Boolean; + +var + ShouldRestartTunnelService: Boolean; + +procedure StopTunnelServiceIfNeeded(); +var + StopServiceResultCode: Integer; + WaitCounter: Integer; begin - Result := ExpandConstant('{param:update|false}') <> 'false'; + ShouldRestartTunnelService := False; + if CheckForMutexes('{#TunnelServiceMutex}') then begin + // stop the tunnel service + Log('Stopping the tunnel service using ' + ExpandConstant('"{app}\bin\{#ApplicationName}.cmd"')); + ShellExec('', ExpandConstant('"{app}\bin\{#ApplicationName}.cmd"'), 'tunnel service uninstall', '', SW_HIDE, ewWaitUntilTerminated, StopServiceResultCode); + + Log('Stopping the tunnel service completed with result code ' + IntToStr(StopServiceResultCode)); + + WaitCounter := 10; + while (WaitCounter > 0) and CheckForMutexes('{#TunnelServiceMutex}') do + begin + Log('Tunnel service is still running, waiting'); + Sleep(500); + WaitCounter := WaitCounter - 1 + end; + if CheckForMutexes('{#TunnelServiceMutex}') then + Log('Unable to stop tunnel service') + else + ShouldRestartTunnelService := True; + end end; -function IsNotUpdate(): Boolean; + +// called before the wizard checks for running application +function PrepareToInstall(var NeedsRestart: Boolean): String; begin - Result := not IsBackgroundUpdate(); + if IsNotBackgroundUpdate() then + StopTunnelServiceIfNeeded(); + Result := '' end; // VS Code will create a flag file before the update starts (/update=C:\foo\bar) @@ -1450,18 +1497,33 @@ end; procedure CurStepChanged(CurStep: TSetupStep); var UpdateResultCode: Integer; + StartServiceResultCode: Integer; begin - if IsBackgroundUpdate() and (CurStep = ssPostInstall) then + if CurStep = ssPostInstall then begin - CreateMutex('{#AppMutex}-ready'); - - while (CheckForMutexes('{#AppMutex}')) do + if IsBackgroundUpdate() then begin - Log('Application is still running, waiting'); - Sleep(1000); + CreateMutex('{#AppMutex}-ready'); + + while (CheckForMutexes('{#AppMutex}')) do + begin + Log('Application is still running, waiting'); + Sleep(1000) + end; + + StopTunnelServiceIfNeeded(); + + Exec(ExpandConstant('{app}\tools\inno_updater.exe'), ExpandConstant('"{app}\{#ExeBasename}.exe" ' + BoolToStr(LockFileExists())), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); end; - Exec(ExpandConstant('{app}\tools\inno_updater.exe'), ExpandConstant('"{app}\{#ExeBasename}.exe" ' + BoolToStr(LockFileExists())), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); + if ShouldRestartTunnelService then + begin + // start the tunnel service + Log('Restarting the tunnel service...'); + ShellExec('', ExpandConstant('"{app}\bin\{#ApplicationName}.cmd"'), 'tunnel service install', '', SW_HIDE, ewWaitUntilTerminated, StartServiceResultCode); + Log('Starting the tunnel service completed with result code ' + IntToStr(StartServiceResultCode)); + ShouldRestartTunnelService := False + end; end; end; @@ -1545,4 +1607,4 @@ begin #endif Exec(ExpandConstant('{sys}\icacls.exe'), ExpandConstant('"{app}" /inheritancelevel:r ') + Permissions, '', SW_HIDE, ewWaitUntilTerminated, ResultCode); -end; +end; \ No newline at end of file diff --git a/cli/Cargo.lock b/cli/Cargo.lock index a4b541970d0..bd207ab1373 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -373,30 +373,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "crossbeam-deque" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" -dependencies = [ - "cfg-if", - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f916dfc5d356b0ed9dae65f1db9fc9770aa2851d2662b988ccf4fe3516e86348" -dependencies = [ - "autocfg", - "cfg-if", - "crossbeam-utils", - "memoffset", - "scopeguard", -] - [[package]] name = "crossbeam-utils" version = "0.8.12" @@ -529,12 +505,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "either" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" - [[package]] name = "encode_unicode" version = "0.3.6" @@ -1729,30 +1699,6 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "rayon" -version = "1.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" -dependencies = [ - "autocfg", - "crossbeam-deque", - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-utils", - "num_cpus", -] - [[package]] name = "redox_syscall" version = "0.2.16" @@ -2183,7 +2129,6 @@ dependencies = [ "libc", "ntapi", "once_cell", - "rayon", "winapi", ] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 6b5c8d07c3f..ac05391f654 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -22,7 +22,7 @@ flate2 = { version = "1.0.22" } zip = { version = "0.5.13", default-features = false, features = ["time", "deflate"] } regex = { version = "1.5.5" } lazy_static = { version = "1.4.0" } -sysinfo = { version = "0.27.7" } +sysinfo = { version = "0.27.7", default-features = false } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0" } rmp-serde = "1.0" diff --git a/cli/src/bin/code/main.rs b/cli/src/bin/code/main.rs index 54660c935f6..2dd31707466 100644 --- a/cli/src/bin/code/main.rs +++ b/cli/src/bin/code/main.rs @@ -8,7 +8,7 @@ use std::process::Command; use clap::Parser; use cli::{ - commands::{args, internal_wsl, tunnels, update, version, CommandContext}, + commands::{args, tunnels, update, version, CommandContext}, constants::get_default_user_agent, desktop, log, state::LauncherPaths, @@ -65,9 +65,6 @@ async fn main() -> Result<(), std::convert::Infallible> { .. }) => match cmd { args::StandaloneCommands::Update(args) => update::update(context!(), args).await, - args::StandaloneCommands::Wsl(args) => match args.command { - args::WslCommands::Serve => internal_wsl::serve(context!()).await, - }, }, args::AnyCli::Standalone(args::StandaloneCli { core: c, .. }) | args::AnyCli::Integrated(args::IntegratedCli { core: c, .. }) => match c.subcommand { @@ -98,6 +95,8 @@ async fn main() -> Result<(), std::convert::Infallible> { args::VersionSubcommand::Show => version::show(context!()).await, }, + Some(args::Commands::CommandShell) => tunnels::command_shell(context!()).await, + Some(args::Commands::Tunnel(tunnel_args)) => match tunnel_args.subcommand { Some(args::TunnelSubcommand::Prune) => tunnels::prune(context!()).await, Some(args::TunnelSubcommand::Unregister) => tunnels::unregister(context!()).await, diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 082031af201..754729f2c04 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -6,7 +6,6 @@ mod context; pub mod args; -pub mod internal_wsl; pub mod tunnels; pub mod update; pub mod version; diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index 8687819f893..1cc557af3d7 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -146,22 +146,6 @@ impl<'a> From<&'a CliCore> for CodeServerArgs { pub enum StandaloneCommands { /// Updates the CLI. Update(StandaloneUpdateArgs), - - /// Internal commands for WSL serving. - #[clap(hide = true)] - Wsl(WslArgs), -} - -#[derive(Args, Debug, Clone)] -pub struct WslArgs { - #[clap(subcommand)] - pub command: WslCommands, -} - -#[derive(Subcommand, Debug, Clone)] -pub enum WslCommands { - /// Runs the WSL server on stdin/out - Serve, } #[derive(Args, Debug, Clone)] @@ -187,6 +171,10 @@ pub enum Commands { /// Changes the version of the editor you're using. Version(VersionArgs), + + /// Runs the control server on process stdin/stdout + #[clap(hide = true)] + CommandShell, } #[derive(Args, Debug, Clone)] diff --git a/cli/src/commands/internal_wsl.rs b/cli/src/commands/internal_wsl.rs deleted file mode 100644 index 483ee52c6aa..00000000000 --- a/cli/src/commands/internal_wsl.rs +++ /dev/null @@ -1,32 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -use crate::{ - tunnels::{serve_wsl, shutdown_signal::ShutdownRequest}, - util::{errors::AnyError, prereqs::PreReqChecker}, -}; - -use super::CommandContext; - -pub async fn serve(ctx: CommandContext) -> Result { - let signal = ShutdownRequest::create_rx([ShutdownRequest::CtrlC]); - let platform = spanf!( - ctx.log, - ctx.log.span("prereq"), - PreReqChecker::new().verify() - )?; - - serve_wsl( - ctx.log, - ctx.paths, - (&ctx.args).into(), - platform, - ctx.http, - signal, - ) - .await?; - - Ok(0) -} diff --git a/cli/src/commands/tunnels.rs b/cli/src/commands/tunnels.rs index 56db02c58ed..cc0249224be 100644 --- a/cli/src/commands/tunnels.rs +++ b/cli/src/commands/tunnels.rs @@ -25,13 +25,13 @@ use crate::{ code_server::CodeServerArgs, create_service_manager, dev_tunnels, legal, paths::get_all_servers, - protocol, + protocol, serve_stream, shutdown_signal::ShutdownRequest, singleton_client::do_single_rpc_call, singleton_server::{ make_singleton_server, start_singleton_server, BroadcastLogSink, SingletonServerArgs, }, - Next, ServiceContainer, ServiceManager, + Next, ServeStreamParams, ServiceContainer, ServiceManager, }, util::{ app_lock::AppMutex, @@ -107,6 +107,25 @@ impl ServiceContainer for TunnelServiceContainer { } } +pub async fn command_shell(ctx: CommandContext) -> Result { + let platform = PreReqChecker::new().verify().await?; + serve_stream( + tokio::io::stdin(), + tokio::io::stderr(), + ServeStreamParams { + log: ctx.log, + launcher_paths: ctx.paths, + platform, + requires_auth: true, + exit_barrier: ShutdownRequest::create_rx([ShutdownRequest::CtrlC]), + code_server_args: (&ctx.args).into(), + }, + ) + .await; + + Ok(0) +} + pub async fn service( ctx: CommandContext, service_args: TunnelServiceSubCommands, diff --git a/cli/src/constants.rs b/cli/src/constants.rs index fa419e11568..2dac5d43563 100644 --- a/cli/src/constants.rs +++ b/cli/src/constants.rs @@ -18,7 +18,8 @@ pub const CONTROL_PORT: u16 = 31545; /// 2 - Addition of `serve.compressed` property to control whether servermsg's /// are compressed bidirectionally. /// 3 - The server's connection token is set to a SHA256 hash of the tunnel ID -pub const PROTOCOL_VERSION: u32 = 3; +/// 4 - The server's msgpack messages are no longer length-prefixed +pub const PROTOCOL_VERSION: u32 = 4; /// Prefix for the tunnel tag that includes the version. pub const PROTOCOL_VERSION_TAG_PREFIX: &str = "protocolv"; diff --git a/cli/src/msgpack_rpc.rs b/cli/src/msgpack_rpc.rs index 0350c1bfd64..219c923cdf2 100644 --- a/cli/src/msgpack_rpc.rs +++ b/cli/src/msgpack_rpc.rs @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ use bytes::Buf; +use serde::de::DeserializeOwned; use tokio::{ - io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader}, + io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, pin, sync::mpsc, }; @@ -18,7 +19,7 @@ use crate::{ sync::{Barrier, Receivable}, }, }; -use std::io; +use std::io::{self, Cursor, ErrorKind}; #[derive(Copy, Clone)] pub struct MsgPackSerializer {} @@ -35,21 +36,28 @@ impl Serialization for MsgPackSerializer { pub type MsgPackCaller = rpc::RpcCaller; -/// Creates a new RPC Builder that serializes to JSON. +/// Creates a new RPC Builder that serializes to msgpack. pub fn new_msgpack_rpc() -> rpc::RpcBuilder { rpc::RpcBuilder::new(MsgPackSerializer {}) } -pub async fn start_msgpack_rpc( - dispatcher: rpc::RpcDispatcher, - read: impl AsyncRead + Unpin, - mut write: impl AsyncWrite + Unpin, +/// Starting processing msgpack rpc over the given i/o. It's recommended that +/// the reader be passed in as a BufReader for efficiency. +pub async fn start_msgpack_rpc< + C: Send + Sync + 'static, + X: Clone, + S: Send + Sync + Serialization, + Read: AsyncRead + Unpin, + Write: AsyncWrite + Unpin, +>( + dispatcher: rpc::RpcDispatcher, + mut read: Read, + mut write: Write, mut msg_rx: impl Receivable>, - mut shutdown_rx: Barrier, -) -> io::Result> { + mut shutdown_rx: Barrier, +) -> io::Result<(Option, Read, Write)> { let (write_tx, mut write_rx) = mpsc::channel::>(8); - let mut read = BufReader::new(read); - let mut decoder = U32PrefixedCodec {}; + let mut decoder = MsgPackCodec::new(); let mut decoder_buf = bytes::BytesMut::new(); let shutdown_fut = shutdown_rx.wait(); @@ -61,7 +69,7 @@ pub async fn start_msgpack_rpc( r?; while let Some(frame) = decoder.decode(&mut decoder_buf)? { - match dispatcher.dispatch(&frame) { + match dispatcher.dispatch_with_partial(&frame.vec, frame.obj) { MaybeSync::Sync(Some(v)) => { let _ = write_tx.send(v).await; }, @@ -94,39 +102,94 @@ pub async fn start_msgpack_rpc( Some(m) = msg_rx.recv_msg() => { write.write_all(&m).await?; }, - r = &mut shutdown_fut => return Ok(r.ok()), + r = &mut shutdown_fut => return Ok((r.ok(), read, write)), } write.flush().await?; } } -/// Reader that reads length-prefixed msgpack messages in a cancellation-safe -/// way using Tokio's codecs. -pub struct U32PrefixedCodec {} +/// Reader that reads msgpack object messages in a cancellation-safe way using Tokio's codecs. +/// +/// rmp_serde does not support async reads, and does not plan to. But we know every +/// type in protocol is some kind of object, so by asking to deserialize the +/// requested object from a reader (repeatedly, if incomplete) we can +/// accomplish streaming. +pub struct MsgPackCodec { + _marker: std::marker::PhantomData, +} -const U32_SIZE: usize = 4; +impl MsgPackCodec { + pub fn new() -> Self { + Self { + _marker: std::marker::PhantomData::default(), + } + } +} -impl tokio_util::codec::Decoder for U32PrefixedCodec { - type Item = Vec; +pub struct MsgPackDecoded { + pub obj: T, + pub vec: Vec, +} + +impl tokio_util::codec::Decoder for MsgPackCodec { + type Item = MsgPackDecoded; type Error = io::Error; fn decode(&mut self, src: &mut bytes::BytesMut) -> Result, Self::Error> { - if src.len() < 4 { - src.reserve(U32_SIZE - src.len()); - return Ok(None); - } + let bytes_ref = src.as_ref(); + let mut cursor = Cursor::new(bytes_ref); - let mut be_bytes = [0; U32_SIZE]; - be_bytes.copy_from_slice(&src[..U32_SIZE]); - let required_len = U32_SIZE + (u32::from_be_bytes(be_bytes) as usize); - if src.len() < required_len { - src.reserve(required_len - src.len()); - return Ok(None); + match rmp_serde::decode::from_read::<_, T>(&mut cursor) { + Err( + rmp_serde::decode::Error::InvalidDataRead(e) + | rmp_serde::decode::Error::InvalidMarkerRead(e), + ) if e.kind() == ErrorKind::UnexpectedEof => { + src.reserve(1024); + Ok(None) + } + Err(e) => Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + e.to_string(), + )), + Ok(obj) => { + let len = cursor.position() as usize; + let vec = src[..len].to_vec(); + src.advance(len); + Ok(Some(MsgPackDecoded { obj, vec })) + } } - - let msg = src[U32_SIZE..required_len].to_vec(); - src.advance(required_len); - Ok(Some(msg)) + } +} + +#[cfg(test)] +mod tests { + use serde::{Deserialize, Serialize}; + + use super::*; + + #[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] + pub struct Msg { + pub x: i32, + } + + #[test] + fn test_protocol() { + let mut c = MsgPackCodec::::new(); + let mut buf = bytes::BytesMut::new(); + + assert!(c.decode(&mut buf).unwrap().is_none()); + + buf.extend_from_slice(rmp_serde::to_vec_named(&Msg { x: 1 }).unwrap().as_slice()); + buf.extend_from_slice(rmp_serde::to_vec_named(&Msg { x: 2 }).unwrap().as_slice()); + + assert_eq!( + c.decode(&mut buf).unwrap().expect("expected msg1").obj, + Msg { x: 1 } + ); + assert_eq!( + c.decode(&mut buf).unwrap().expect("expected msg1").obj, + Msg { x: 2 } + ); } } diff --git a/cli/src/rpc.rs b/cli/src/rpc.rs index 28dfc0efb47..02cbac61bee 100644 --- a/cli/src/rpc.rs +++ b/cli/src/rpc.rs @@ -104,6 +104,10 @@ impl RpcMethodBuilder { R: Serialize, F: Fn(P, &C) -> Result + Send + Sync + 'static, { + if self.methods.contains_key(method_name) { + panic!("Method already registered: {}", method_name); + } + let serial = self.serializer.clone(); let context = self.context.clone(); self.methods.insert( @@ -276,7 +280,9 @@ impl RpcMethodBuilder { self.register_async(METHOD_STREAM_ENDED, move |m: StreamEndedParams, _| { let s1 = s1.clone(); async move { - s1.lock().await.remove(&m.stream); + if let Some(mut s) = s1.lock().await.remove(&m.stream) { + let _ = s.shutdown().await; + } Ok(()) } }); @@ -410,13 +416,17 @@ impl RpcDispatcher { /// The future or return result will be optional bytes that should be sent /// back to the socket. pub fn dispatch(&self, body: &[u8]) -> MaybeSync { - let partial = match self.serializer.deserialize::(body) { - Ok(b) => b, + match self.serializer.deserialize::(body) { + Ok(partial) => self.dispatch_with_partial(body, partial), Err(_err) => { warning!(self.log, "Failed to deserialize request, hex: {:X?}", body); - return MaybeSync::Sync(None); + MaybeSync::Sync(None) } - }; + } + } + + /// Like dispatch, but allows passing an existing PartialIncoming. + pub fn dispatch_with_partial(&self, body: &[u8], partial: PartialIncoming) -> MaybeSync { let id = partial.id; if let Some(method_name) = partial.method { @@ -536,8 +546,8 @@ trait AssertIsSync: Sync {} impl AssertIsSync for RpcDispatcher {} /// Approximate shape that is used to determine what kind of data is incoming. -#[derive(Deserialize)] -struct PartialIncoming { +#[derive(Deserialize, Debug)] +pub struct PartialIncoming { pub id: Option, pub method: Option, pub error: Option, diff --git a/cli/src/tunnels.rs b/cli/src/tunnels.rs index ebab9475988..801b6545e51 100644 --- a/cli/src/tunnels.rs +++ b/cli/src/tunnels.rs @@ -7,11 +7,12 @@ pub mod code_server; pub mod dev_tunnels; pub mod legal; pub mod paths; +pub mod protocol; pub mod shutdown_signal; pub mod singleton_client; pub mod singleton_server; -pub mod protocol; +mod challenge; mod control_server; mod nosleep; #[cfg(target_os = "linux")] @@ -31,11 +32,9 @@ mod service_macos; #[cfg(target_os = "windows")] mod service_windows; mod socket_signal; -mod wsl_server; -pub use control_server::{serve, Next}; +pub use control_server::{serve, serve_stream, Next, ServeStreamParams}; pub use nosleep::SleepInhibitor; pub use service::{ create_service_manager, ServiceContainer, ServiceManager, SERVICE_LOG_FILE_NAME, }; -pub use wsl_server::serve_wsl; diff --git a/cli/src/tunnels/challenge.rs b/cli/src/tunnels/challenge.rs new file mode 100644 index 00000000000..1c4abc651ba --- /dev/null +++ b/cli/src/tunnels/challenge.rs @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +#[cfg(not(feature = "vsda"))] +pub fn create_challenge() -> String { + use rand::distributions::{Alphanumeric, DistString}; + Alphanumeric.sample_string(&mut rand::thread_rng(), 16) +} + +#[cfg(not(feature = "vsda"))] +pub fn sign_challenge(challenge: &str) -> String { + use sha2::{Digest, Sha256}; + let mut hash = Sha256::new(); + hash.update(challenge.as_bytes()); + let result = hash.finalize(); + base64::encode_config(result, base64::URL_SAFE_NO_PAD) +} + +#[cfg(not(feature = "vsda"))] +pub fn verify_challenge(challenge: &str, response: &str) -> bool { + sign_challenge(challenge) == response +} + +#[cfg(feature = "vsda")] +pub fn create_challenge() -> String { + use rand::distributions::{Alphanumeric, DistString}; + let str = Alphanumeric.sample_string(&mut rand::thread_rng(), 16); + vsda::create_new_message(&str) +} + +#[cfg(feature = "vsda")] +pub fn sign_challenge(challenge: &str) -> String { + vsda::sign(challenge) +} + +#[cfg(feature = "vsda")] +pub fn verify_challenge(challenge: &str, response: &str) -> bool { + vsda::validate(challenge, response) +} diff --git a/cli/src/tunnels/control_server.rs b/cli/src/tunnels/control_server.rs index 67a0bcf64ac..bf85f1b28bb 100644 --- a/cli/src/tunnels/control_server.rs +++ b/cli/src/tunnels/control_server.rs @@ -5,16 +5,15 @@ use crate::async_pipe::get_socket_rw_stream; use crate::constants::{CONTROL_PORT, PRODUCT_NAME_LONG}; use crate::log; -use crate::msgpack_rpc::U32PrefixedCodec; -use crate::rpc::{MaybeSync, RpcBuilder, RpcDispatcher, Serialization}; +use crate::msgpack_rpc::{new_msgpack_rpc, start_msgpack_rpc, MsgPackCodec, MsgPackSerializer}; +use crate::rpc::{MaybeSync, RpcBuilder, RpcCaller, RpcDispatcher}; use crate::self_update::SelfUpdate; use crate::state::LauncherPaths; -use crate::tunnels::protocol::HttpRequestParams; +use crate::tunnels::protocol::{HttpRequestParams, METHOD_CHALLENGE_ISSUE}; use crate::tunnels::socket_signal::CloseReason; use crate::update_service::{Platform, Release, TargetKind, UpdateService}; use crate::util::errors::{ - wrap, AnyError, CodeError, InvalidRpcDataError, MismatchedLaunchModeError, - NoAttachedServerError, + wrap, AnyError, CodeError, MismatchedLaunchModeError, NoAttachedServerError, }; use crate::util::http::{ DelegatedHttpRequest, DelegatedSimpleHttp, FallbackSimpleHttp, ReqwestSimpleHttp, @@ -22,7 +21,7 @@ use crate::util::http::{ use crate::util::io::SilentCopyProgress; use crate::util::is_integrated_cli; use crate::util::os::os_release; -use crate::util::sync::{new_barrier, Barrier}; +use crate::util::sync::{new_barrier, Barrier, BarrierOpener}; use futures::stream::FuturesUnordered; use futures::FutureExt; @@ -31,6 +30,7 @@ use opentelemetry::KeyValue; use std::collections::HashMap; use std::process::Stdio; use tokio::pin; +use tokio::process::{ChildStderr, ChildStdin}; use tokio_util::codec::Decoder; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; @@ -39,6 +39,7 @@ use std::time::Instant; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader, DuplexStream}; use tokio::sync::{mpsc, Mutex}; +use super::challenge::{create_challenge, sign_challenge, verify_challenge}; use super::code_server::{ download_cli_into_cache, AnyCodeServer, CodeServerArgs, ServerBuilder, ServerParamsRaw, SocketCodeServer, @@ -47,11 +48,12 @@ use super::dev_tunnels::ActiveTunnel; use super::paths::prune_stopped_servers; use super::port_forwarder::{PortForwarding, PortForwardingProcessor}; use super::protocol::{ - AcquireCliParams, CallServerHttpParams, CallServerHttpResult, ClientRequestMethod, EmptyObject, - ForwardParams, ForwardResult, FsStatRequest, FsStatResponse, GetEnvResponse, - GetHostnameResponse, HttpBodyParams, HttpHeadersParams, ServeParams, ServerLog, - ServerMessageParams, SpawnParams, SpawnResult, ToClientRequest, UnforwardParams, UpdateParams, - UpdateResult, VersionParams, + AcquireCliParams, CallServerHttpParams, CallServerHttpResult, ChallengeIssueResponse, + ChallengeVerifyParams, ClientRequestMethod, EmptyObject, ForwardParams, ForwardResult, + FsStatRequest, FsStatResponse, GetEnvResponse, GetHostnameResponse, HttpBodyParams, + HttpHeadersParams, ServeParams, ServerLog, ServerMessageParams, SpawnParams, SpawnResult, + ToClientRequest, UnforwardParams, UpdateParams, UpdateResult, VersionResponse, + METHOD_CHALLENGE_VERIFY, }; use super::server_bridge::ServerBridge; use super::server_multiplexer::ServerMultiplexer; @@ -68,6 +70,8 @@ struct HandlerContext { log: log::Logger, /// Whether the server update during the handler session. did_update: Arc, + /// Whether authentication is still required on the socket. + auth_state: Arc>, /// A loopback channel to talk to the socket server task. socket_tx: mpsc::Sender, /// Configured launcher paths. @@ -79,7 +83,7 @@ struct HandlerContext { // the cli arguments used to start the code server code_server_args: CodeServerArgs, /// port forwarding functionality - port_forwarding: PortForwarding, + port_forwarding: Option, /// install platform for the VS Code server platform: Platform, /// http client to make download/update requests @@ -88,6 +92,16 @@ struct HandlerContext { http_requests: HttpRequestsMap, } +/// Handler auth state. +enum AuthState { + /// Auth is required, we're waiting for the client to send its challenge. + WaitingForChallenge, + /// A challenge has been issued. Waiting for a verification. + ChallengeIssued(String), + /// Auth is no longer required. + Authenticated, +} + static MESSAGE_ID_COUNTER: AtomicU32 = AtomicU32::new(0); // Gets a next incrementing number that can be used in logs @@ -195,7 +209,14 @@ pub async fn serve( debug!(own_log, "Serving new connection"); let (writehalf, readhalf) = socket.into_split(); - let stats = process_socket(own_exit, readhalf, writehalf, own_log, own_tx, own_paths, own_code_server_args, own_forwarding, platform).with_context(cx.clone()).await; + let stats = process_socket(readhalf, writehalf, own_tx, Some(own_forwarding), ServeStreamParams { + log: own_log, + launcher_paths: own_paths, + code_server_args: own_code_server_args, + platform, + exit_barrier: own_exit, + requires_auth: false, + }).with_context(cx.clone()).await; cx.span().add_event( "socket.bandwidth", @@ -206,69 +227,91 @@ pub async fn serve( ], ); cx.span().end(); - }); + }); } } } } -struct SocketStats { +pub struct ServeStreamParams { + pub log: log::Logger, + pub launcher_paths: LauncherPaths, + pub code_server_args: CodeServerArgs, + pub platform: Platform, + pub requires_auth: bool, + pub exit_barrier: Barrier, +} + +pub async fn serve_stream( + readhalf: impl AsyncRead + Send + Unpin + 'static, + writehalf: impl AsyncWrite + Unpin, + params: ServeStreamParams, +) -> SocketStats { + // Currently the only server signal is respawn, that doesn't have much meaning + // when serving a stream, so make an ignored channel. + let (server_rx, server_tx) = mpsc::channel(1); + drop(server_tx); + + process_socket(readhalf, writehalf, server_rx, None, params).await +} + +pub struct SocketStats { rx: usize, tx: usize, } -#[derive(Copy, Clone)] -struct MsgPackSerializer {} - -impl Serialization for MsgPackSerializer { - fn serialize(&self, value: impl serde::Serialize) -> Vec { - rmp_serde::to_vec_named(&value).expect("expected to serialize") - } - - fn deserialize(&self, b: &[u8]) -> Result { - rmp_serde::from_slice(b).map_err(|e| InvalidRpcDataError(e.to_string()).into()) - } -} - -#[allow(clippy::too_many_arguments)] // necessary here -async fn process_socket( - mut exit_barrier: Barrier<()>, - readhalf: impl AsyncRead + Send + Unpin + 'static, - mut writehalf: impl AsyncWrite + Unpin, +#[allow(clippy::too_many_arguments)] +fn make_socket_rpc( log: log::Logger, - server_tx: mpsc::Sender, + socket_tx: mpsc::Sender, + http_delegated: DelegatedSimpleHttp, launcher_paths: LauncherPaths, code_server_args: CodeServerArgs, - port_forwarding: PortForwarding, + port_forwarding: Option, + requires_auth: bool, platform: Platform, -) -> SocketStats { - let (socket_tx, mut socket_rx) = mpsc::channel(4); - let rx_counter = Arc::new(AtomicUsize::new(0)); +) -> RpcDispatcher { let http_requests = Arc::new(std::sync::Mutex::new(HashMap::new())); let server_bridges = ServerMultiplexer::new(); - let (http_delegated, mut http_rx) = DelegatedSimpleHttp::new(log.clone()); let mut rpc = RpcBuilder::new(MsgPackSerializer {}).methods(HandlerContext { did_update: Arc::new(AtomicBool::new(false)), - socket_tx: socket_tx.clone(), + auth_state: Arc::new(std::sync::Mutex::new(match requires_auth { + true => AuthState::WaitingForChallenge, + false => AuthState::Authenticated, + })), + socket_tx, log: log.clone(), launcher_paths, code_server_args, code_server: Arc::new(Mutex::new(None)), - server_bridges: server_bridges.clone(), + server_bridges, port_forwarding, platform, http: Arc::new(FallbackSimpleHttp::new( ReqwestSimpleHttp::new(), http_delegated, )), - http_requests: http_requests.clone(), + http_requests, }); rpc.register_sync("ping", |_: EmptyObject, _| Ok(EmptyObject {})); rpc.register_sync("gethostname", |_: EmptyObject, _| handle_get_hostname()); - rpc.register_sync("fs_stat", |p: FsStatRequest, _| handle_stat(p.path)); - rpc.register_sync("get_env", |_: EmptyObject, _| handle_get_env()); + rpc.register_sync("fs_stat", |p: FsStatRequest, c| { + ensure_auth(&c.auth_state)?; + handle_stat(p.path) + }); + rpc.register_sync("get_env", |_: EmptyObject, c| { + ensure_auth(&c.auth_state)?; + handle_get_env() + }); + rpc.register_sync(METHOD_CHALLENGE_ISSUE, |_: EmptyObject, c| { + handle_challenge_issue(&c.auth_state) + }); + rpc.register_sync(METHOD_CHALLENGE_VERIFY, |p: ChallengeVerifyParams, c| { + handle_challenge_verify(p.response, &c.auth_state) + }); rpc.register_async("serve", move |params: ServeParams, c| async move { + ensure_auth(&c.auth_state)?; handle_serve(c, params).await }); rpc.register_async("update", |p: UpdateParams, c| async move { @@ -286,15 +329,19 @@ async fn process_socket( handle_call_server_http(code_server, p).await }); rpc.register_async("forward", |p: ForwardParams, c| async move { + ensure_auth(&c.auth_state)?; handle_forward(&c.log, &c.port_forwarding, p).await }); rpc.register_async("unforward", |p: UnforwardParams, c| async move { + ensure_auth(&c.auth_state)?; handle_unforward(&c.log, &c.port_forwarding, p).await }); rpc.register_async("acquire_cli", |p: AcquireCliParams, c| async move { + ensure_auth(&c.auth_state)?; handle_acquire_cli(&c.launcher_paths, &c.http, &c.log, p).await }); rpc.register_duplex("spawn", 3, |mut streams, p: SpawnParams, c| async move { + ensure_auth(&c.auth_state)?; handle_spawn( &c.log, p, @@ -304,13 +351,28 @@ async fn process_socket( ) .await }); + rpc.register_duplex( + "spawn_cli", + 3, + |mut streams, p: SpawnParams, c| async move { + ensure_auth(&c.auth_state)?; + handle_spawn_cli( + &c.log, + p, + streams.remove(0), + streams.remove(0), + streams.remove(0), + ) + .await + }, + ); rpc.register_sync("httpheaders", |p: HttpHeadersParams, c| { if let Some(req) = c.http_requests.lock().unwrap().get(&p.req_id) { req.initial_response(p.status_code, p.headers); } Ok(EmptyObject {}) }); - rpc.register_sync("unforward", move |p: HttpBodyParams, c| { + rpc.register_sync("httpbody", move |p: HttpBodyParams, c| { let mut reqs = c.http_requests.lock().unwrap(); if let Some(req) = reqs.get(&p.req_id) { if !p.segment.is_empty() { @@ -322,15 +384,64 @@ async fn process_socket( } Ok(EmptyObject {}) }); + rpc.register_sync( + "version", + |_: EmptyObject, _| Ok(VersionResponse::default()), + ); + + rpc.build(log) +} + +fn ensure_auth(is_authed: &Arc>) -> Result<(), AnyError> { + if let AuthState::Authenticated = &*is_authed.lock().unwrap() { + Ok(()) + } else { + Err(CodeError::ServerAuthRequired.into()) + } +} + +#[allow(clippy::too_many_arguments)] // necessary here +async fn process_socket( + readhalf: impl AsyncRead + Send + Unpin + 'static, + mut writehalf: impl AsyncWrite + Unpin, + server_tx: mpsc::Sender, + port_forwarding: Option, + params: ServeStreamParams, +) -> SocketStats { + let ServeStreamParams { + mut exit_barrier, + log, + launcher_paths, + code_server_args, + platform, + requires_auth, + } = params; + + let (http_delegated, mut http_rx) = DelegatedSimpleHttp::new(log.clone()); + let (socket_tx, mut socket_rx) = mpsc::channel(4); + let rx_counter = Arc::new(AtomicUsize::new(0)); + let http_requests = Arc::new(std::sync::Mutex::new(HashMap::new())); + + let rpc = make_socket_rpc( + log.clone(), + socket_tx.clone(), + http_delegated, + launcher_paths, + code_server_args, + port_forwarding, + requires_auth, + platform, + ); { let log = log.clone(); let rx_counter = rx_counter.clone(); let socket_tx = socket_tx.clone(); let exit_barrier = exit_barrier.clone(); - let rpc = rpc.build(log.clone()); tokio::spawn(async move { - send_version(&socket_tx).await; + if !requires_auth { + send_version(&socket_tx).await; + } if let Err(e) = handle_socket_read(&log, readhalf, exit_barrier, &socket_tx, rx_counter, &rpc).await @@ -350,6 +461,10 @@ async fn process_socket( } ctx.dispose().await; + + let _ = socket_tx + .send(SocketSignal::CloseWith(CloseReason("eof".to_string()))) + .await; }); } @@ -408,7 +523,7 @@ async fn process_socket( async fn send_version(tx: &mpsc::Sender) { tx.send(SocketSignal::from_message(&ToClientRequest { id: None, - params: ClientRequestMethod::version(VersionParams::default()), + params: ClientRequestMethod::version(VersionResponse::default()), })) .await .ok(); @@ -416,13 +531,13 @@ async fn send_version(tx: &mpsc::Sender) { async fn handle_socket_read( _log: &log::Logger, readhalf: impl AsyncRead + Unpin, - mut closer: Barrier<()>, + mut closer: Barrier, socket_tx: &mpsc::Sender, rx_counter: Arc, rpc: &RpcDispatcher, ) -> Result<(), std::io::Error> { let mut readhalf = BufReader::new(readhalf); - let mut decoder = U32PrefixedCodec {}; + let mut decoder = MsgPackCodec::new(); let mut decoder_buf = bytes::BytesMut::new(); loop { @@ -431,10 +546,14 @@ async fn handle_socket_read( _ = closer.wait() => Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "eof")), }?; + if read_len == 0 { + return Ok(()); + } + rx_counter.fetch_add(read_len, Ordering::Relaxed); while let Some(frame) = decoder.decode(&mut decoder_buf)? { - match rpc.dispatch(&frame) { + match rpc.dispatch_with_partial(&frame.vec, frame.obj) { MaybeSync::Sync(Some(v)) => { if socket_tx.send(SocketSignal::Send(v)).await.is_err() { return Ok(()); @@ -704,11 +823,44 @@ fn handle_get_env() -> Result { }) } +fn handle_challenge_issue( + auth_state: &Arc>, +) -> Result { + let challenge = create_challenge(); + + let mut auth_state = auth_state.lock().unwrap(); + *auth_state = AuthState::ChallengeIssued(challenge.clone()); + + Ok(ChallengeIssueResponse { challenge }) +} + +fn handle_challenge_verify( + response: String, + auth_state: &Arc>, +) -> Result { + let mut auth_state = auth_state.lock().unwrap(); + + match &*auth_state { + AuthState::Authenticated => Ok(EmptyObject {}), + AuthState::WaitingForChallenge => Err(CodeError::AuthChallengeNotIssued.into()), + AuthState::ChallengeIssued(c) => match verify_challenge(c, &response) { + false => Err(CodeError::AuthChallengeNotIssued.into()), + true => { + *auth_state = AuthState::Authenticated; + Ok(EmptyObject {}) + } + }, + } +} + async fn handle_forward( log: &log::Logger, - port_forwarding: &PortForwarding, + port_forwarding: &Option, params: ForwardParams, ) -> Result { + let port_forwarding = port_forwarding + .as_ref() + .ok_or(CodeError::PortForwardingNotAvailable)?; info!(log, "Forwarding port {}", params.port); let uri = port_forwarding.forward(params.port).await?; Ok(ForwardResult { uri }) @@ -716,9 +868,12 @@ async fn handle_forward( async fn handle_unforward( log: &log::Logger, - port_forwarding: &PortForwarding, + port_forwarding: &Option, params: UnforwardParams, ) -> Result { + let port_forwarding = port_forwarding + .as_ref() + .ok_or(CodeError::PortForwardingNotAvailable)?; info!(log, "Unforwarding port {}", params.port); port_forwarding.unforward(params.port).await?; Ok(EmptyObject {}) @@ -818,17 +973,17 @@ async fn handle_spawn( stderr: Option, ) -> Result where - Stdin: AsyncRead + Unpin + Send, - StdoutAndErr: AsyncWrite + Unpin + Send, + Stdin: AsyncRead + Unpin + Send + 'static, + StdoutAndErr: AsyncWrite + Unpin + Send + 'static, { debug!( log, "requested to spawn {} with args {:?}", params.command, params.args ); - macro_rules! pipe_if_some { + macro_rules! pipe_if { ($e: expr) => { - if $e.is_some() { + if $e { Stdio::piped() } else { Stdio::null() @@ -839,9 +994,9 @@ where let mut p = tokio::process::Command::new(¶ms.command); p.args(¶ms.args); p.envs(¶ms.env); - p.stdin(pipe_if_some!(stdin)); - p.stdout(pipe_if_some!(stdout)); - p.stderr(pipe_if_some!(stderr)); + p.stdin(pipe_if!(stdin.is_some())); + p.stdout(pipe_if!(stdin.is_some())); + p.stderr(pipe_if!(stderr.is_some())); if let Some(cwd) = ¶ms.cwd { p.current_dir(cwd); } @@ -859,7 +1014,72 @@ where futs.push(async move { tokio::io::copy(&mut a, &mut b).await }.boxed()); } - let closed = p.wait(); + wait_for_process_exit(log, ¶ms.command, p, futs).await +} + +async fn handle_spawn_cli( + log: &log::Logger, + params: SpawnParams, + mut protocol_in: DuplexStream, + mut protocol_out: DuplexStream, + mut log_out: DuplexStream, +) -> Result { + debug!( + log, + "requested to spawn cli {} with args {:?}", params.command, params.args + ); + + let mut p = tokio::process::Command::new(¶ms.command); + p.args(¶ms.args); + + // CLI args to spawn a server; contracted with clients that they should _not_ provide these. + p.arg("--verbose"); + p.arg("tunnel"); + p.arg("stdio"); + + p.envs(¶ms.env); + p.stdin(Stdio::piped()); + p.stdout(Stdio::piped()); + p.stderr(Stdio::piped()); + if let Some(cwd) = ¶ms.cwd { + p.current_dir(cwd); + } + + let mut p = p.spawn().map_err(CodeError::ProcessSpawnFailed)?; + + let mut stdin = p.stdin.take().unwrap(); + let mut stdout = p.stdout.take().unwrap(); + let mut stderr = p.stderr.take().unwrap(); + + // Start handling logs while doing the handshake in case there's some kind of error + let log_pump = tokio::spawn(async move { tokio::io::copy(&mut stdout, &mut log_out).await }); + + // note: intentionally do not wrap stdin in a bufreader, since we don't + // want to read anything other than our handshake messages. + if let Err(e) = spawn_do_child_authentication(log, &mut stdin, &mut stderr).await { + warning!(log, "failed to authenticate with child process {}", e); + let _ = p.kill().await; + return Err(e.into()); + } + + debug!(log, "cli authenticated, attaching stdio"); + let futs = FuturesUnordered::new(); + futs.push(async move { tokio::io::copy(&mut protocol_in, &mut stdin).await }.boxed()); + futs.push(async move { tokio::io::copy(&mut stderr, &mut protocol_out).await }.boxed()); + futs.push(async move { log_pump.await.unwrap() }.boxed()); + + wait_for_process_exit(log, ¶ms.command, p, futs).await +} + +type TokioCopyFuture = dyn futures::Future> + Send; + +async fn wait_for_process_exit( + log: &log::Logger, + command: &str, + mut process: tokio::process::Child, + futs: FuturesUnordered>>, +) -> Result { + let closed = process.wait(); pin!(closed); let r = tokio::select! { @@ -880,8 +1100,69 @@ where debug!( log, - "spawned command {} exited with code {}", params.command, r.exit_code + "spawned cli {} exited with code {}", command, r.exit_code ); Ok(r) } + +async fn spawn_do_child_authentication( + log: &log::Logger, + stdin: &mut ChildStdin, + stdout: &mut ChildStderr, +) -> Result<(), CodeError> { + let (msg_tx, msg_rx) = mpsc::unbounded_channel(); + let (shutdown_rx, shutdown) = new_barrier(); + let mut rpc = new_msgpack_rpc(); + let caller = rpc.get_caller(msg_tx); + + let challenge_response = do_challenge_response_flow(caller, shutdown); + let rpc = start_msgpack_rpc( + rpc.methods(()).build(log.prefixed("client-auth")), + stdout, + stdin, + msg_rx, + shutdown_rx, + ); + pin!(rpc); + + tokio::select! { + r = &mut rpc => { + match r { + // means shutdown happened cleanly already, we're good + Ok(_) => Ok(()), + Err(e) => Err(CodeError::ProcessSpawnHandshakeFailed(e)) + } + }, + r = challenge_response => { + r?; + rpc.await.map(|_| ()).map_err(CodeError::ProcessSpawnFailed) + } + } +} + +async fn do_challenge_response_flow( + caller: RpcCaller, + shutdown: BarrierOpener<()>, +) -> Result<(), CodeError> { + let challenge: ChallengeIssueResponse = caller + .call(METHOD_CHALLENGE_ISSUE, EmptyObject {}) + .await + .unwrap() + .map_err(CodeError::TunnelRpcCallFailed)?; + + let _: EmptyObject = caller + .call( + METHOD_CHALLENGE_VERIFY, + ChallengeVerifyParams { + response: sign_challenge(&challenge.challenge), + }, + ) + .await + .unwrap() + .map_err(CodeError::TunnelRpcCallFailed)?; + + shutdown.open(()); + + Ok(()) +} diff --git a/cli/src/tunnels/protocol.rs b/cli/src/tunnels/protocol.rs index 17282381c55..eb20afe0ce5 100644 --- a/cli/src/tunnels/protocol.rs +++ b/cli/src/tunnels/protocol.rs @@ -18,7 +18,7 @@ pub enum ClientRequestMethod<'a> { servermsg(RefServerMessageParams<'a>), serverlog(ServerLog<'a>), makehttpreq(HttpRequestParams<'a>), - version(VersionParams), + version(VersionResponse), } #[derive(Deserialize, Debug)] @@ -58,14 +58,6 @@ pub struct ForwardResult { pub uri: String, } -/// The `install_local` method in the wsl control server -#[derive(Deserialize, Debug)] -pub struct InstallFromLocalFolderParams { - pub archive_path: String, - #[serde(flatten)] - pub inner: ServeParams, -} - #[derive(Deserialize, Debug)] pub struct ServeParams { pub socket_id: u16, @@ -165,12 +157,12 @@ pub struct CallServerHttpResult { } #[derive(Serialize, Debug)] -pub struct VersionParams { +pub struct VersionResponse { pub version: &'static str, pub protocol_version: u32, } -impl Default for VersionParams { +impl Default for VersionResponse { fn default() -> Self { Self { version: VSCODE_CLI_VERSION.unwrap_or("dev"), @@ -204,6 +196,19 @@ pub struct SpawnResult { pub exit_code: i32, } +pub const METHOD_CHALLENGE_ISSUE: &str = "challenge_issue"; +pub const METHOD_CHALLENGE_VERIFY: &str = "challenge_verify"; + +#[derive(Serialize, Deserialize)] +pub struct ChallengeIssueResponse { + pub challenge: String, +} + +#[derive(Deserialize, Serialize)] +pub struct ChallengeVerifyParams { + pub response: String, +} + pub mod singleton { use crate::log; use serde::{Deserialize, Serialize}; diff --git a/cli/src/tunnels/singleton_client.rs b/cli/src/tunnels/singleton_client.rs index b67a53306d1..ef9fdf85cc0 100644 --- a/cli/src/tunnels/singleton_client.rs +++ b/cli/src/tunnels/singleton_client.rs @@ -74,7 +74,6 @@ pub async fn start_singleton_client(args: SingletonClientArgs) -> bool { let mut input = String::new(); loop { input.truncate(0); - println!("reading line"); match std::io::stdin().read_line(&mut input) { Err(_) | Ok(0) => return, // EOF or not a tty _ => {} diff --git a/cli/src/tunnels/socket_signal.rs b/cli/src/tunnels/socket_signal.rs index a3d3b08a5d4..2a2df6607ea 100644 --- a/cli/src/tunnels/socket_signal.rs +++ b/cli/src/tunnels/socket_signal.rs @@ -38,6 +38,7 @@ impl SocketSignal { } /// todo@connor4312: cleanup once everything is moved to rpc standard interfaces +#[allow(dead_code)] pub enum ServerMessageDestination { Channel(mpsc::Sender), Rpc(MsgPackCaller), diff --git a/cli/src/tunnels/wsl_server.rs b/cli/src/tunnels/wsl_server.rs deleted file mode 100644 index 3eafae92f3a..00000000000 --- a/cli/src/tunnels/wsl_server.rs +++ /dev/null @@ -1,173 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -use std::sync::Arc; - -use tokio::sync::mpsc; - -use crate::{ - log, - msgpack_rpc::{new_msgpack_rpc, start_msgpack_rpc, MsgPackCaller}, - state::LauncherPaths, - tunnels::code_server::ServerBuilder, - update_service::{Platform, Release, TargetKind}, - util::{ - errors::{ - wrap, AnyError, InvalidRpcDataError, MismatchedLaunchModeError, NoAttachedServerError, - }, - http::ReqwestSimpleHttp, - sync::Barrier, - }, -}; - -use super::{ - code_server::{AnyCodeServer, CodeServerArgs, ResolvedServerParams}, - protocol::{EmptyObject, InstallFromLocalFolderParams, ServerMessageParams, VersionParams}, - server_bridge::ServerBridge, - server_multiplexer::ServerMultiplexer, - shutdown_signal::ShutdownSignal, - socket_signal::{ClientMessageDecoder, ServerMessageDestination, ServerMessageSink}, -}; - -struct HandlerContext { - log: log::Logger, - code_server_args: CodeServerArgs, - launcher_paths: LauncherPaths, - platform: Platform, - http: ReqwestSimpleHttp, - caller: MsgPackCaller, - multiplexer: ServerMultiplexer, -} - -#[derive(Clone)] -struct RpcLogSink(MsgPackCaller); - -impl RpcLogSink { - fn write_json(&self, level: String, message: &str) { - self.0.notify( - "log", - serde_json::json!({ - "level": level, - "message": message, - }), - ); - } -} - -impl log::LogSink for RpcLogSink { - fn write_log(&self, level: log::Level, _prefix: &str, message: &str) { - self.write_json(level.to_string(), message); - } - - fn write_result(&self, message: &str) { - self.write_json("result".to_string(), message); - } -} - -pub async fn serve_wsl( - log: log::Logger, - launcher_paths: LauncherPaths, - code_server_args: CodeServerArgs, - platform: Platform, - http: reqwest::Client, - shutdown_rx: Barrier, -) -> Result { - let (caller_tx, caller_rx) = mpsc::unbounded_channel(); - let mut rpc = new_msgpack_rpc(); - let caller = rpc.get_caller(caller_tx); - - // notify the incoming client about the server version - caller.notify("version", VersionParams::default()); - - let log = log.with_sink(RpcLogSink(caller.clone())); - let mut rpc = rpc.methods(HandlerContext { - log: log.clone(), - caller, - code_server_args, - launcher_paths, - platform, - multiplexer: ServerMultiplexer::new(), - http: ReqwestSimpleHttp::with_client(http), - }); - - rpc.register_async( - "serve", - move |m: InstallFromLocalFolderParams, c| async move { handle_serve(&c, m).await }, - ); - rpc.register_sync("servermsg", move |m: ServerMessageParams, c| { - if c.multiplexer.write_message(&c.log, m.i, m.body) { - Ok(EmptyObject {}) - } else { - Err(NoAttachedServerError().into()) - } - }); - - start_msgpack_rpc( - rpc.build(log), - tokio::io::stdin(), - tokio::io::stderr(), - caller_rx, - shutdown_rx, - ) - .await - .map_err(|e| wrap(e, "error handling server stdio"))?; - - Ok(0) -} - -async fn handle_serve( - c: &HandlerContext, - params: InstallFromLocalFolderParams, -) -> Result { - // fill params.extensions into code_server_args.install_extensions - let mut csa = c.code_server_args.clone(); - csa.connection_token = params.inner.connection_token.or(csa.connection_token); - csa.install_extensions - .extend(params.inner.extensions.into_iter()); - - let resolved = ResolvedServerParams { - code_server_args: csa, - release: Release { - name: String::new(), - commit: params - .inner - .commit_id - .ok_or_else(|| InvalidRpcDataError("commit_id is required".to_string()))?, - platform: c.platform, - target: TargetKind::Server, - quality: params.inner.quality, - }, - }; - - let sb = ServerBuilder::new( - &c.log, - &resolved, - &c.launcher_paths, - Arc::new(c.http.clone()), - ); - let code_server = match sb.get_running().await? { - Some(AnyCodeServer::Socket(s)) => s, - Some(_) => return Err(MismatchedLaunchModeError().into()), - None => { - sb.setup().await?; - sb.listen_on_default_socket().await? - } - }; - - let bridge = ServerBridge::new( - &code_server.socket, - ServerMessageSink::new_plain( - c.multiplexer.clone(), - params.inner.socket_id, - ServerMessageDestination::Rpc(c.caller.clone()), - ), - ClientMessageDecoder::new_plain(), - ) - .await?; - - c.multiplexer.register(params.inner.socket_id, bridge); - trace!(c.log, "Attached to server"); - Ok(EmptyObject {}) -} diff --git a/cli/src/util/errors.rs b/cli/src/util/errors.rs index 9ab421d3301..f1c4cbf5c22 100644 --- a/cli/src/util/errors.rs +++ b/cli/src/util/errors.rs @@ -479,9 +479,18 @@ pub enum CodeError { PrerequisitesFailed { name: &'static str, bullets: String }, #[error("failed to spawn process: {0:?}")] ProcessSpawnFailed(std::io::Error), - + #[error("failed to handshake spawned process: {0:?}")] + ProcessSpawnHandshakeFailed(std::io::Error), #[error("download appears corrupted, please retry ({0})")] CorruptDownload(&'static str), + #[error("port forwarding is not available in this context")] + PortForwardingNotAvailable, + #[error("'auth' call required")] + ServerAuthRequired, + #[error("challenge not yet issued")] + AuthChallengeNotIssued, + #[error("unauthorized client refused")] + AuthMismatch, } makeAnyError!( diff --git a/extensions/git/package.json b/extensions/git/package.json index e13864027b8..5e00d215463 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -13,7 +13,7 @@ "diffCommand", "contribEditorContentMenu", "contribEditSessions", - "canonicalUriIdentityProvider", + "canonicalUriProvider", "contribViewsWelcome", "editSessionIdentityProvider", "quickDiffProvider", diff --git a/extensions/git/src/editSessionIdentityProvider.ts b/extensions/git/src/editSessionIdentityProvider.ts index 10ebbf349e4..6a0a31774a1 100644 --- a/extensions/git/src/editSessionIdentityProvider.ts +++ b/extensions/git/src/editSessionIdentityProvider.ts @@ -35,7 +35,7 @@ export class GitEditSessionIdentityProvider implements vscode.EditSessionIdentit } const remoteUrl = repository.remotes.find((remote) => remote.name === repository.HEAD?.upstream?.remote)?.pushUrl?.replace(/^(git@[^\/:]+)(:)/i, 'ssh://$1/'); - const remote = remoteUrl ? await vscode.workspace.provideCanonicalUriIdentity(vscode.Uri.parse(remoteUrl), token) : null; + const remote = remoteUrl ? await vscode.workspace.getCanonicalUri(vscode.Uri.parse(remoteUrl), { targetScheme: 'https' }, token) : null; return JSON.stringify({ remote: remote?.toString() ?? remoteUrl, diff --git a/extensions/git/src/typings/vscode.proposed.canonicalUriIdentityProvider.d.ts b/extensions/git/src/typings/vscode.proposed.canonicalUriIdentityProvider.d.ts deleted file mode 100644 index 3a61ca15798..00000000000 --- a/extensions/git/src/typings/vscode.proposed.canonicalUriIdentityProvider.d.ts +++ /dev/null @@ -1,38 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - - // https://github.com/microsoft/vscode/issues/180582 - - export namespace workspace { - /** - * - * @param scheme The URI scheme that this provider can provide canonical URI identities for. - * A canonical URI represents the conversion of a resource's alias into a source of truth URI. - * Multiple aliases may convert to the same source of truth URI. - * @param provider A provider which can convert URIs for workspace folders of scheme @param scheme to - * a canonical URI identifier which is stable across machines. - */ - export function registerCanonicalUriIdentityProvider(scheme: string, provider: CanonicalUriIdentityProvider): Disposable; - - /** - * - * @param uri The URI to provide a canonical URI identity for. - * @param token A cancellation token for the request. - */ - export function provideCanonicalUriIdentity(uri: Uri, token: CancellationToken): ProviderResult; - } - - export interface CanonicalUriIdentityProvider { - /** - * - * @param uri The URI to provide a canonical URI identity for. - * @param token A cancellation token for the request. - * @returns The canonical URI identity for the requested URI. - */ - provideCanonicalUriIdentity(uri: Uri, token: CancellationToken): ProviderResult; - } -} diff --git a/extensions/git/src/typings/vscode.proposed.canonicalUriProvider.d.ts b/extensions/git/src/typings/vscode.proposed.canonicalUriProvider.d.ts new file mode 100644 index 00000000000..84ee599797d --- /dev/null +++ b/extensions/git/src/typings/vscode.proposed.canonicalUriProvider.d.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/180582 + + export namespace workspace { + /** + * + * @param scheme The URI scheme that this provider can provide canonical URIs for. + * A canonical URI represents the conversion of a resource's alias into a source of truth URI. + * Multiple aliases may convert to the same source of truth URI. + * @param provider A provider which can convert URIs of scheme @param scheme to + * a canonical URI which is stable across machines. + */ + export function registerCanonicalUriProvider(scheme: string, provider: CanonicalUriProvider): Disposable; + + /** + * + * @param uri The URI to provide a canonical URI for. + * @param token A cancellation token for the request. + */ + export function getCanonicalUri(uri: Uri, options: CanonicalUriRequestOptions, token: CancellationToken): ProviderResult; + } + + export interface CanonicalUriProvider { + /** + * + * @param uri The URI to provide a canonical URI for. + * @param options Options that the provider should honor in the URI it returns. + * @param token A cancellation token for the request. + * @returns The canonical URI for the requested URI or undefined if no canonical URI can be provided. + */ + provideCanonicalUri(uri: Uri, options: CanonicalUriRequestOptions, token: CancellationToken): ProviderResult; + } + + export interface CanonicalUriRequestOptions { + /** + * + * The desired scheme of the canonical URI. + */ + targetScheme: string; + } +} diff --git a/extensions/github/package.json b/extensions/github/package.json index 3da6c53904c..305742e8c11 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -28,7 +28,7 @@ "enabledApiProposals": [ "contribShareMenu", "contribEditSessions", - "canonicalUriIdentityProvider" + "canonicalUriProvider" ], "contributes": { "commands": [ diff --git a/extensions/github/src/canonicalUriIdentityProvider.ts b/extensions/github/src/canonicalUriIdentityProvider.ts deleted file mode 100644 index 89c9585c1f6..00000000000 --- a/extensions/github/src/canonicalUriIdentityProvider.ts +++ /dev/null @@ -1,37 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { CancellationToken, CanonicalUriIdentityProvider, Disposable, Uri, workspace } from 'vscode'; - -const SUPPORTED_SCHEMES = ['ssh', 'https']; - -export class GitHubCanonicalUriIdentityProvider implements CanonicalUriIdentityProvider { - - private disposables: Disposable[] = []; - constructor() { - this.disposables.push(...SUPPORTED_SCHEMES.map((scheme) => workspace.registerCanonicalUriIdentityProvider(scheme, this))); - } - - dispose() { this.disposables.forEach((disposable) => disposable.dispose()); } - - async provideCanonicalUriIdentity(uri: Uri, _token: CancellationToken): Promise { - switch (uri.scheme) { - case 'ssh': - // if this is a git@github.com URI, return the HTTPS equivalent - if (uri.authority === 'git@github.com') { - const [owner, repo] = (uri.path.endsWith('.git') ? uri.path.slice(0, -4) : uri.path).split('/').filter((segment) => segment.length > 0); - return Uri.parse(`https://github.com/${owner}/${repo}`); - } - break; - case 'https': - if (uri.authority === 'github.com') { - return uri; - } - break; - } - - return undefined; - } -} diff --git a/extensions/github/src/canonicalUriProvider.ts b/extensions/github/src/canonicalUriProvider.ts new file mode 100644 index 00000000000..09f5e243bc1 --- /dev/null +++ b/extensions/github/src/canonicalUriProvider.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken, CanonicalUriProvider, CanonicalUriRequestOptions, Disposable, ProviderResult, Uri, workspace } from 'vscode'; +import { API } from './typings/git'; + +const SUPPORTED_SCHEMES = ['ssh', 'https', 'file']; + +export class GitHubCanonicalUriProvider implements CanonicalUriProvider { + + private disposables: Disposable[] = []; + constructor(private gitApi: API) { + this.disposables.push(...SUPPORTED_SCHEMES.map((scheme) => workspace.registerCanonicalUriProvider(scheme, this))); + } + + dispose() { this.disposables.forEach((disposable) => disposable.dispose()); } + + provideCanonicalUri(uri: Uri, options: CanonicalUriRequestOptions, _token: CancellationToken): ProviderResult { + if (options.targetScheme !== 'https') { + return; + } + + switch (uri.scheme) { + case 'file': { + const repository = this.gitApi.getRepository(uri); + const remote = repository?.state.remotes.find((remote) => remote.name === repository.state.HEAD?.remote)?.pushUrl?.replace(/^(git@[^\/:]+)(:)/i, 'ssh://$1/'); + if (remote) { + return toHttpsGitHubRemote(uri); + } + } + default: + return toHttpsGitHubRemote(uri); + } + } +} + +function toHttpsGitHubRemote(uri: Uri) { + if (uri.scheme === 'ssh' && uri.authority === 'git@github.com') { + // if this is a git@github.com URI, return the HTTPS equivalent + const [owner, repo] = (uri.path.endsWith('.git') ? uri.path.slice(0, -4) : uri.path).split('/').filter((segment) => segment.length > 0); + return Uri.parse(`https://github.com/${owner}/${repo}`); + } + if (uri.scheme === 'https' && uri.authority === 'github.com') { + return uri; + } + return undefined; +} diff --git a/extensions/github/src/extension.ts b/extensions/github/src/extension.ts index 26ed3bb39b7..57fdc4a938a 100644 --- a/extensions/github/src/extension.ts +++ b/extensions/github/src/extension.ts @@ -13,7 +13,7 @@ import { GithubPushErrorHandler } from './pushErrorHandler'; import { GitBaseExtension } from './typings/git-base'; import { GithubRemoteSourcePublisher } from './remoteSourcePublisher'; import { GithubBranchProtectionProviderManager } from './branchProtection'; -import { GitHubCanonicalUriIdentityProvider } from './canonicalUriIdentityProvider'; +import { GitHubCanonicalUriProvider } from './canonicalUriProvider'; export function activate(context: ExtensionContext): void { const disposables: Disposable[] = []; @@ -30,7 +30,6 @@ export function activate(context: ExtensionContext): void { disposables.push(initializeGitBaseExtension()); disposables.push(initializeGitExtension(context, logger)); - disposables.push(new GitHubCanonicalUriIdentityProvider()); } function initializeGitBaseExtension(): Disposable { @@ -95,6 +94,7 @@ function initializeGitExtension(context: ExtensionContext, logger: LogOutputChan disposables.add(new GithubBranchProtectionProviderManager(gitAPI, context.globalState, logger)); disposables.add(gitAPI.registerPushErrorHandler(new GithubPushErrorHandler())); disposables.add(gitAPI.registerRemoteSourcePublisher(new GithubRemoteSourcePublisher(gitAPI))); + disposables.add(new GitHubCanonicalUriProvider(gitAPI)); setGitHubContext(gitAPI, disposables); commands.executeCommand('setContext', 'git-base.gitEnabled', true); diff --git a/extensions/github/src/typings/vscode.proposed.canonicalUriIdentityProvider.d.ts b/extensions/github/src/typings/vscode.proposed.canonicalUriIdentityProvider.d.ts deleted file mode 100644 index 3a61ca15798..00000000000 --- a/extensions/github/src/typings/vscode.proposed.canonicalUriIdentityProvider.d.ts +++ /dev/null @@ -1,38 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - - // https://github.com/microsoft/vscode/issues/180582 - - export namespace workspace { - /** - * - * @param scheme The URI scheme that this provider can provide canonical URI identities for. - * A canonical URI represents the conversion of a resource's alias into a source of truth URI. - * Multiple aliases may convert to the same source of truth URI. - * @param provider A provider which can convert URIs for workspace folders of scheme @param scheme to - * a canonical URI identifier which is stable across machines. - */ - export function registerCanonicalUriIdentityProvider(scheme: string, provider: CanonicalUriIdentityProvider): Disposable; - - /** - * - * @param uri The URI to provide a canonical URI identity for. - * @param token A cancellation token for the request. - */ - export function provideCanonicalUriIdentity(uri: Uri, token: CancellationToken): ProviderResult; - } - - export interface CanonicalUriIdentityProvider { - /** - * - * @param uri The URI to provide a canonical URI identity for. - * @param token A cancellation token for the request. - * @returns The canonical URI identity for the requested URI. - */ - provideCanonicalUriIdentity(uri: Uri, token: CancellationToken): ProviderResult; - } -} diff --git a/extensions/github/src/typings/vscode.proposed.canonicalUriProvider.d.ts b/extensions/github/src/typings/vscode.proposed.canonicalUriProvider.d.ts new file mode 100644 index 00000000000..84ee599797d --- /dev/null +++ b/extensions/github/src/typings/vscode.proposed.canonicalUriProvider.d.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/180582 + + export namespace workspace { + /** + * + * @param scheme The URI scheme that this provider can provide canonical URIs for. + * A canonical URI represents the conversion of a resource's alias into a source of truth URI. + * Multiple aliases may convert to the same source of truth URI. + * @param provider A provider which can convert URIs of scheme @param scheme to + * a canonical URI which is stable across machines. + */ + export function registerCanonicalUriProvider(scheme: string, provider: CanonicalUriProvider): Disposable; + + /** + * + * @param uri The URI to provide a canonical URI for. + * @param token A cancellation token for the request. + */ + export function getCanonicalUri(uri: Uri, options: CanonicalUriRequestOptions, token: CancellationToken): ProviderResult; + } + + export interface CanonicalUriProvider { + /** + * + * @param uri The URI to provide a canonical URI for. + * @param options Options that the provider should honor in the URI it returns. + * @param token A cancellation token for the request. + * @returns The canonical URI for the requested URI or undefined if no canonical URI can be provided. + */ + provideCanonicalUri(uri: Uri, options: CanonicalUriRequestOptions, token: CancellationToken): ProviderResult; + } + + export interface CanonicalUriRequestOptions { + /** + * + * The desired scheme of the canonical URI. + */ + targetScheme: string; + } +} diff --git a/extensions/package.json b/extensions/package.json index 5d6cb795bcd..a01536c91ad 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -4,7 +4,7 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "^5.1.0-dev.20230515" + "typescript": "5.1.1-rc" }, "scripts": { "postinstall": "node ./postinstall.mjs" diff --git a/extensions/php/snippets/php.code-snippets b/extensions/php/snippets/php.code-snippets index 3c213765b4b..5cc31f19661 100644 --- a/extensions/php/snippets/php.code-snippets +++ b/extensions/php/snippets/php.code-snippets @@ -1,8 +1,66 @@ { + "$… = ( … ) ? … : …": { + "prefix": "if?", + "body": "$${1:retVal} = (${2:condition}) ? ${3:a} : ${4:b} ;", + "description": "Ternary conditional assignment" + }, + "$… = array (…)": { + "prefix": "array", + "body": "$${1:arrayName} = array($0);", + "description": "Array initializer" + }, + "$… = […]": { + "prefix": "shorray", + "body": "$${1:arrayName} = [$0];", + "description": "Array initializer" + }, + "… => …": { + "prefix": "keyval,kvp", + "body": "'$1' => $2$0", + "description": "Key-Value pair" + }, + "$a <=> $b": { + "prefix": "spaceship", + "body": "(${1:$$a} <=> ${2:$$b} === ${3|0,1,-1|})", + "description": "Spaceship equality check" + }, + "attribute": { + "prefix": "attr", + "body": [ + "#[\\\\Attribute]", + "class ${1:My}Attribute${2: extends ${3:MyOther}Attribute} {", + "\t$0", + "}" + ], + "description": "Attribute" + }, + "attribute target": { + "prefix": "attr_target", + "body": "\\Attribute::${1|TARGET_ALL,TARGET_CLASS,TARGET_FUNCTION,TARGET_METHOD,TARGET_PROPERTY,TARGET_CLASS_CONSTANT,TARGET_PARAMETER,IS_REPEATABLE|}$0" + }, + "attribute with target": { + "prefix": "attr_with_target", + "body": [ + "#[\\\\Attribute(\\Attribute::${1|TARGET_ALL,TARGET_CLASS,TARGET_FUNCTION,TARGET_METHOD,TARGET_PROPERTY,TARGET_CLASS_CONSTANT,TARGET_PARAMETER,IS_REPEATABLE|}$2)]", + "class ${3:My}Attribute${4: extends ${5:MyOther}Attribute} {", + "\t$0", + "}" + ], + "description": "Attribute - Chain targets with attr_target snippet" + }, + "case …": { + "prefix": "case", + "body": [ + "case '${1:value}':", + "\t${0:# code...}", + "\tbreak;" + ], + "description": "Case Block" + }, "class …": { "prefix": "class", "body": [ - "class ${1:ClassName} ${2:extends ${3:AnotherClass}} ${4:implements ${5:Interface}}", + "${1:${2|final ,readonly |}}class ${3:${TM_FILENAME_BASE}}${4: extends ${5:AnotherClass}} ${6:implements ${7:Interface}}", "{", "\t$0", "}", @@ -10,87 +68,36 @@ ], "description": "Class definition" }, - "PHPDoc class …": { - "prefix": "doc_class", - "isFileTemplate": true, + "class __construct": { + "prefix": "construct", "body": [ - "/**", - " * ${6:undocumented class}", - " */", - "class ${1:ClassName} ${2:extends ${3:AnotherClass}} ${4:implements ${5:Interface}}", - "{", - "\t$0", - "}", - "" - ], - "description": "Documented Class Declaration" - }, - "function __construct": { - "prefix": "con", - "body": [ - "${1:public} function __construct(${2:${3:Type} $${4:var}${5: = ${6:null}}}) {", - "\t\\$this->${4:var} = $${4:var};$0", - "}" + "${1|public,private,protected|} function __construct(${2:${3:Type} $${4:var}${5: = ${6:null}}}$7) {", + "\t\\$this->${4:var} = $${4:var};$0", + "}" ] }, - "PHPDoc property": { - "prefix": "doc_v", + "class function …": { + "prefix": "class_fun", "body": [ - "/** @var ${1:Type} $${2:var} ${3:description} */", - "${4:protected} $${2:var}${5: = ${6:null}};$0" - ], - "description": "Documented Class Variable" - }, - "PHPDoc function …": { - "prefix": "doc_f", - "isFileTemplate": true, - "body": [ - "/**", - " * ${1:undocumented function summary}", - " *", - " * ${2:Undocumented function long description}", - " *", - "${3: * @param ${4:Type} $${5:var} ${6:Description}}", - "${7: * @return ${8:type}}", - "${9: * @throws ${10:conditon}}", - " **/", - "${11:public }function ${12:FunctionName}(${13:${14:${4:Type} }$${5:var}${15: = ${16:null}}})", + "${1|public ,private ,protected |}${2: static }function ${3:FunctionName}(${4:${5:${6:Type} }$${7:var}${8: = ${9:null}}}$10) : ${11:Returntype}", "{", "\t${0:# code...}", "}" ], - "description": "Documented function" + "description": "Function for classes, traits and enums" }, - "PHPDoc param …": { - "prefix": "param", - "body": [ - "* @param ${1:Type} ${2:var} ${3:Description}$0" - ], - "description": "Parameter documentation" + "const": { + "prefix": "const", + "body": "${1|public ,private ,protected |}const ${2:NAME} = $3;", + "description": "Constant for classes, traits, enums" }, - "function …": { - "prefix": "fun", + "enum": { + "prefix": "enum", "body": [ - "${1:public }function ${2:FunctionName}(${3:${4:${5:Type} }$${6:var}${7: = ${8:null}}})", - "{", - "\t${0:# code...}", + "enum $1 {", + "\tcase $2;$0", "}" - ], - "description": "Function" - }, - "trait …": { - "prefix": "trait", - "body": [ - "/**", - " * $1", - " */", - "trait ${2:TraitName}", - "{", - "\t$0", - "}", - "" - ], - "description": "Trait" + ] }, "define(…, …)": { "prefix": "def", @@ -108,41 +115,6 @@ "} while (${1:$${2:a} <= ${3:10}});" ], "description": "Do-While loop" - }, - "while …": { - "prefix": "while", - "body": [ - "while (${1:$${2:a} <= ${3:10}}) {", - "\t${0:# code...}", - "}" - ], - "description": "While-loop" - }, - "if …": { - "prefix": "if", - "body": [ - "if (${1:condition}) {", - "\t${0:# code...}", - "}" - ], - "description": "If block" - }, - "if … else …": { - "prefix": "ifelse", - "body": [ - "if (${1:condition}) {", - "\t${2:# code...}", - "} else {", - "\t${3:# code...}", - "}", - "$0" - ], - "description": "If Else block" - }, - "$… = ( … ) ? … : …": { - "prefix": "if?", - "body": "$${1:retVal} = (${2:condition}) ? ${3:a} : ${4:b} ;", - "description": "Ternary conditional assignment" }, "else …": { "prefix": "else", @@ -174,26 +146,146 @@ "foreach …": { "prefix": "foreach", "body": [ - "foreach ($${1:variable} as $${2:key} ${3:=> $${4:value}}) {", + "foreach ($${1:variable} as $${2:key}${3: => $${4:value}}) {", "\t${0:# code...}", "}" ], "description": "Foreach loop" }, - "$… = array (…)": { - "prefix": "array", - "body": "$${1:arrayName} = array('$2' => $3${4:,} $0);", - "description": "Array initializer" + "function": { + "prefix": "fun", + "body": [ + "function ${1:FunctionName}($2)${3: : ${4:Returntype}} {", + "\t$0", + "}" + ], + "description": "Function - use param snippet for parameters" }, - "$… = […]": { - "prefix": "shorray", - "body": "$${1:arrayName} = ['$2' => $3${4:,} $0];", - "description": "Array initializer" + "anonymous function": { + "prefix": "fun_anonymous", + "body": [ + "function ($1)${2: use ($${3:var})} {", + "\t$0", + "}" + ], + "description": "Anonymous Function" }, - "… => …": { - "prefix": "keyval", - "body": "'$1' => $2${3:,} $0", - "description": "Key-Value initializer" + "if …": { + "prefix": "if", + "body": [ + "if (${1:condition}) {", + "\t${0:# code...}", + "}" + ], + "description": "If block" + }, + "if … else …": { + "prefix": "ifelse", + "body": [ + "if (${1:condition}) {", + "\t${2:# code...}", + "} else {", + "\t${3:# code...}", + "}", + "$0" + ], + "description": "If Else block" + }, + "match": { + "prefix": "match", + "body": [ + "match (${1:expression}) {", + "\t$2 => $3,", + "\t$4 => $5,$0", + "}" + ], + "description": "Match expression; like switch with identity checks. Use keyval snippet to chain expressions" + }, + "param": { + "prefix": "param", + "body": "${1:Type} $${2:var}${3: = ${4:null}}$5", + "description": "Parameter definition" + }, + "property": { + "prefix": "property", + "body": "${1|public ,private ,protected |}${2|static ,readonly |}${3:Type} $${4:var}${5: = ${6:null}};$0", + "description": "Property" + }, + "PHPDoc class …": { + "prefix": "doc_class", + "body": [ + "/**", + " * ${8:undocumented class}", + " */", + "${1:${2|final ,readonly |}}class ${3:${TM_FILENAME_BASE}}${4: extends ${5:AnotherClass}} ${6:implements ${7:Interface}}", + "{", + "\t$0", + "}", + "" + ], + "description": "Documented Class Declaration" + }, + "PHPDoc function …": { + "prefix": "doc_fun", + "body": [ + "/**", + " * ${1:undocumented function summary}", + " *", + " * ${2:Undocumented function long description}", + " *", + "${3: * @param ${4:Type} $${5:var} ${6:Description}}", + "${7: * @return ${8:type}}", + "${9: * @throws ${10:conditon}}", + " **/", + "${11:public }function ${12:FunctionName}(${13:${14:${4:Type} }$${5:var}${15: = ${16:null}}}17)", + "{", + "\t${0:# code...}", + "}" + ], + "description": "Documented function" + }, + "PHPDoc param …": { + "prefix": "doc_param", + "body": [ + "* @param ${1:Type} ${2:var} ${3:Description}$0" + ], + "description": "Paramater documentation" + }, + "PHPDoc trait": { + "prefix": "doc_trait", + "body": [ + "/**", + " * $1", + " */", + "trait ${2:TraitName}", + "{", + "\t$0", + "}", + "" + ], + "description": "Trait" + }, + "PHPDoc var": { + "prefix": "doc_var", + "body": [ + "/** @var ${1:Type} $${2:var} ${3:description} */", + "${4:protected} $${2:var}${5: = ${6:null}};$0" + ], + "description": "Documented Class Variable" + }, + "Region End": { + "prefix": "#endregion", + "body": [ + "#endregion" + ], + "description": "Folding Region End" + }, + "Region Start": { + "prefix": "#region", + "body": [ + "#region" + ], + "description": "Folding Region Start" }, "switch …": { "prefix": "switch", @@ -210,25 +302,11 @@ ], "description": "Switch block" }, - "case …": { - "prefix": "case", - "body": [ - "case '${1:value}':", - "\t${0:# code...}", - "\tbreak;" - ], - "description": "Case Block" - }, "$this->…": { "prefix": "this", "body": "\\$this->$0;", "description": "$this->..." }, - "echo $this->…": { - "prefix": "ethis", - "body": "echo \\$this->$0;", - "description": "Echo this" - }, "Throw Exception": { "prefix": "throw", "body": [ @@ -237,19 +315,16 @@ ], "description": "Throw exception" }, - "Region Start": { - "prefix": "#region", + "trait …": { + "prefix": "trait", "body": [ - "#region" + "trait ${1:TraitName}", + "{", + "\t$0", + "}", + "" ], - "description": "Folding Region Start" - }, - "Region End": { - "prefix": "#endregion", - "body": [ - "#endregion" - ], - "description": "Folding Region End" + "description": "Trait" }, "Try Catch Block": { "prefix": "try", @@ -261,5 +336,36 @@ "}" ], "description": "Try catch block" + }, + "use function": { + "prefix": "use_fun", + "body": "use function $1;" + }, + "use const": { + "prefix": "use_const", + "body": "use const $1;" + }, + "use grouping": { + "prefix": "use_group", + "body": [ + "use${1| const , function |}$2\\{", + "\t$0,", + "}" + ], + "description": "Use grouping imports" + }, + "use as ": { + "prefix": "use_as", + "body": "use${1| const , function |}$2 as $3;", + "description": "Use as alias" + }, + "while …": { + "prefix": "while", + "body": [ + "while (${1:$${2:a} <= ${3:10}}) {", + "\t${0:# code...}", + "}" + ], + "description": "While-loop" } } diff --git a/extensions/theme-defaults/themes/dark_plus_experimental.json b/extensions/theme-defaults/themes/dark_plus_experimental.json index d5623cc1a5b..0d55d6a20a1 100644 --- a/extensions/theme-defaults/themes/dark_plus_experimental.json +++ b/extensions/theme-defaults/themes/dark_plus_experimental.json @@ -102,6 +102,7 @@ "statusBar.border": "#ffffff15", "statusBar.debuggingBackground": "#0078d4", "statusBar.debuggingForeground": "#ffffff", + "statusBar.focusBorder": "#0078d4", "statusBar.foreground": "#cccccc", "statusBar.noFolderBackground": "#1f1f1f", "statusBarItem.focusBorder": "#0078d4", diff --git a/extensions/typescript-language-features/extension-browser.webpack.config.js b/extensions/typescript-language-features/extension-browser.webpack.config.js index 9b4880ab882..7e131117174 100644 --- a/extensions/typescript-language-features/extension-browser.webpack.config.js +++ b/extensions/typescript-language-features/extension-browser.webpack.config.js @@ -41,7 +41,7 @@ module.exports = [withBrowserDefaults({ patterns: [ { from: '../node_modules/typescript/lib/*.d.ts', - to: 'typescript/', + to: 'typescript/[name][ext]', }, { from: '../node_modules/typescript/lib/typesMap.json', diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 8451a6176f0..0393fbb0ea4 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -68,15 +68,6 @@ "main": "./out/extension", "browser": "./dist/browser/extension", "contributes": { - "walkthroughs": [ - { - "id": "tempNodejsWelcome", - "title": "tempNodejsTitle", - "description": "tempNodejsDescription", - "steps": [], - "when": "false" - } - ], "jsonValidation": [ { "fileMatch": "package.json", @@ -1241,14 +1232,17 @@ "description": "%configuration.suggest.objectLiteralMethodSnippets.enabled%", "scope": "resource" }, - "typescript.experimental.tsserver.web.enableProjectWideIntellisense": { + "typescript.tsserver.web.projectWideIntellisense.enabled": { "type": "boolean", - "default": false, - "description": "%typescript.experimental.tsserver.web.enableProjectWideIntellisense%", - "scope": "window", - "tags": [ - "experimental" - ] + "default": true, + "description": "%configuration.tsserver.web.projectWideIntellisense.enabled%", + "scope": "window" + }, + "typescript.tsserver.web.projectWideIntellisense.suppressSemanticErrors": { + "type": "boolean", + "default": true, + "description": "%configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors%", + "scope": "window" }, "typescript.preferGoToSourceDefinition": { "type": "boolean", diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 20306b9e271..2d5aa3e7eb7 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -87,29 +87,41 @@ "inlayHints.parameterNames.all": "Enable parameter name hints for literal and non-literal arguments.", "configuration.inlayHints.parameterNames.enabled": { "message": "Enable/disable inlay hints for parameter names:\n```typescript\n\nparseInt(/* str: */ '123', /* radix: */ 8)\n \n```", - "comment": ["The text inside the ``` block is code and should not be localized."] + "comment": [ + "The text inside the ``` block is code and should not be localized." + ] }, "configuration.inlayHints.parameterNames.suppressWhenArgumentMatchesName": "Suppress parameter name hints on arguments whose text is identical to the parameter name.", "configuration.inlayHints.parameterTypes.enabled": { "message": "Enable/disable inlay hints for implicit parameter types:\n```typescript\n\nel.addEventListener('click', e /* :MouseEvent */ => ...)\n \n```", - "comment": ["The text inside the ``` block is code and should not be localized."] + "comment": [ + "The text inside the ``` block is code and should not be localized." + ] }, "configuration.inlayHints.variableTypes.enabled": { "message": "Enable/disable inlay hints for implicit variable types:\n```typescript\n\nconst foo /* :number */ = Date.now();\n \n```", - "comment": ["The text inside the ``` block is code and should not be localized."] + "comment": [ + "The text inside the ``` block is code and should not be localized." + ] }, "configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName": "Suppress type hints on variables whose name is identical to the type name. Requires using TypeScript 4.8+ in the workspace.", "configuration.inlayHints.propertyDeclarationTypes.enabled": { "message": "Enable/disable inlay hints for implicit types on property declarations:\n```typescript\n\nclass Foo {\n\tprop /* :number */ = Date.now();\n}\n \n```", - "comment": ["The text inside the ``` block is code and should not be localized."] + "comment": [ + "The text inside the ``` block is code and should not be localized." + ] }, "configuration.inlayHints.functionLikeReturnTypes.enabled": { "message": "Enable/disable inlay hints for implicit return types on function signatures:\n```typescript\n\nfunction foo() /* :number */ {\n\treturn Date.now();\n} \n \n```", - "comment": ["The text inside the ``` block is code and should not be localized."] + "comment": [ + "The text inside the ``` block is code and should not be localized." + ] }, "configuration.inlayHints.enumMemberValues.enabled": { "message": "Enable/disable inlay hints for member values in enum declarations:\n```typescript\n\nenum MyValue {\n\tA /* = 0 */;\n\tB /* = 1 */;\n}\n \n```", - "comment": ["The text inside the ``` block is code and should not be localized."] + "comment": [ + "The text inside the ``` block is code and should not be localized." + ] }, "taskDefinition.tsconfig.description": "The tsconfig file that defines the TS build.", "javascript.suggestionActions.enabled": "Enable/disable suggestion diagnostics for JavaScript files in the editor.", @@ -197,26 +209,6 @@ "typescript.goToSourceDefinition": "Go to Source Definition", "configuration.suggest.classMemberSnippets.enabled": "Enable/disable snippet completions for class members.", "configuration.suggest.objectLiteralMethodSnippets.enabled": "Enable/disable snippet completions for methods in object literals. Requires using TypeScript 4.7+ in the workspace.", - - "typescript.experimental.tsserver.web.enableProjectWideIntellisense": "Enable/disable project-wide IntelliSense on web. Requires that VS Code is running in a trusted context.", - - "walkthroughs.nodejsWelcome.title": "Get started with JavaScript and Node.js", - "walkthroughs.nodejsWelcome.description": "Make the most of Visual Studio Code's first-class JavaScript experience.", - - "walkthroughs.nodejsWelcome.downloadNode.forMacOrWindows.title": "Install Node.js", - "walkthroughs.nodejsWelcome.downloadNode.forMacOrWindows.description": "Node.js is an easy way to run JavaScript code. You can use it to quickly build command-line apps and servers. It also comes with npm, a package manager which makes reusing and sharing JavaScript code easy.\n[Install Node.js](https://nodejs.org/en/download/)", - - "walkthroughs.nodejsWelcome.downloadNode.forLinux.title": "Install Node.js", - "walkthroughs.nodejsWelcome.downloadNode.forLinux.description": "Node.js is an easy way to run JavaScript code. You can use it to quickly build command-line apps and servers. It also comes with npm, a package manager which makes reusing and sharing JavaScript code easy.\n[Install Node.js](https://nodejs.org/en/download/package-manager/)", - - "walkthroughs.nodejsWelcome.makeJsFile.title": "Create a JavaScript File", - "walkthroughs.nodejsWelcome.makeJsFile.description": "Let's write our first JavaScript file. We'll have to create a new file and save it with the ``.js`` extension at the end of the file name.\n[Create a JavaScript File](command:javascript-walkthrough.commands.createJsFile)", - - "walkthroughs.nodejsWelcome.debugJsFile.title": "Run and Debug your JavaScript", - "walkthroughs.nodejsWelcome.debugJsFile.description": "Once you've installed Node.js, you can run JavaScript programs at a terminal by entering ``node your-file-name.js``\nAnother easy way to run Node.js programs is by using VS Code's debugger which lets you run your code, pause at different points, and help you understand what's going on step-by-step.\n[Start Debugging](command:javascript-walkthrough.commands.debugJsFile)", - "walkthroughs.nodejsWelcome.debugJsFile.altText": "Debug and run your JavaScript code in Node.js with Visual Studio Code.", - - "walkthroughs.nodejsWelcome.learnMoreAboutJs.title": "Explore More", - "walkthroughs.nodejsWelcome.learnMoreAboutJs.description": "Want to get more comfortable with JavaScript, Node.js, and VS Code? Be sure to check out our docs!\nWe've got lots of resources for learning [JavaScript](https://code.visualstudio.com/docs/nodejs/working-with-javascript) and [Node.js](https://code.visualstudio.com/docs/nodejs/nodejs-tutorial).\n\n[Learn More](https://code.visualstudio.com/docs/nodejs/nodejs-tutorial)", - "walkthroughs.nodejsWelcome.learnMoreAboutJs.altText": "Learn more about JavaScript and Node.js in Visual Studio Code." + "configuration.tsserver.web.projectWideIntellisense.enabled": "Enable/disable project-wide IntelliSense on web. Requires that VS Code is running in a trusted context.", + "configuration.tsserver.web.projectWideIntellisense.suppressSemanticErrors": "Suppresses semantic errors. This is needed when using external packages as these can't be included analyzed on web." } diff --git a/extensions/typescript-language-features/src/configuration/configuration.ts b/extensions/typescript-language-features/src/configuration/configuration.ts index 4fb86157ce8..cab1cf4c819 100644 --- a/extensions/typescript-language-features/src/configuration/configuration.ts +++ b/extensions/typescript-language-features/src/configuration/configuration.ts @@ -110,7 +110,8 @@ export interface TypeScriptServiceConfiguration { readonly implicitProjectConfiguration: ImplicitProjectConfiguration; readonly disableAutomaticTypeAcquisition: boolean; readonly useSyntaxServer: SyntaxServerConfiguration; - readonly enableProjectWideIntellisenseOnWeb: boolean; + readonly webProjectWideIntellisenseEnabled: boolean; + readonly webProjectWideIntellisenseSuppressSemanticErrors: boolean; readonly enableProjectDiagnostics: boolean; readonly maxTsServerMemory: number; readonly enablePromptUseWorkspaceTsdk: boolean; @@ -141,7 +142,8 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu implicitProjectConfiguration: new ImplicitProjectConfiguration(configuration), disableAutomaticTypeAcquisition: this.readDisableAutomaticTypeAcquisition(configuration), useSyntaxServer: this.readUseSyntaxServer(configuration), - enableProjectWideIntellisenseOnWeb: this.readEnableProjectWideIntellisenseOnWeb(configuration), + webProjectWideIntellisenseEnabled: this.readWebProjectWideIntellisenseEnable(configuration), + webProjectWideIntellisenseSuppressSemanticErrors: this.readWebProjectWideIntellisenseSuppressSemanticErrors(configuration), enableProjectDiagnostics: this.readEnableProjectDiagnostics(configuration), maxTsServerMemory: this.readMaxTsServerMemory(configuration), enablePromptUseWorkspaceTsdk: this.readEnablePromptUseWorkspaceTsdk(configuration), @@ -227,7 +229,11 @@ export abstract class BaseServiceConfigurationProvider implements ServiceConfigu return configuration.get('typescript.tsserver.enableTracing', false); } - private readEnableProjectWideIntellisenseOnWeb(configuration: vscode.WorkspaceConfiguration): boolean { - return configuration.get('typescript.experimental.tsserver.web.enableProjectWideIntellisense', false); + private readWebProjectWideIntellisenseEnable(configuration: vscode.WorkspaceConfiguration): boolean { + return configuration.get('typescript.tsserver.web.projectWideIntellisense.enabled', true); + } + + private readWebProjectWideIntellisenseSuppressSemanticErrors(configuration: vscode.WorkspaceConfiguration): boolean { + return configuration.get('typescript.tsserver.web.projectWideIntellisense.suppressSemanticErrors', true); } } diff --git a/extensions/typescript-language-features/src/languageFeatures/completions.ts b/extensions/typescript-language-features/src/languageFeatures/completions.ts index 4628a1788c1..fcc5dc64c6f 100644 --- a/extensions/typescript-language-features/src/languageFeatures/completions.ts +++ b/extensions/typescript-language-features/src/languageFeatures/completions.ts @@ -31,7 +31,6 @@ interface DotAccessorContext { interface CompletionContext { readonly isNewIdentifierLocation: boolean; readonly isMemberCompletion: boolean; - readonly isInValidCommitCharacterContext: boolean; readonly dotAccessorContext?: DotAccessorContext; @@ -40,8 +39,6 @@ interface CompletionContext { readonly wordRange: vscode.Range | undefined; readonly line: string; - - readonly useFuzzyWordRangeLogic: boolean; } type ResolvedCompletionItem = { @@ -96,12 +93,13 @@ class MyCompletionItem extends vscode.CompletionItem { this.range = this.getRangeFromReplacementSpan(tsEntry, completionContext); this.commitCharacters = MyCompletionItem.getCommitCharacters(completionContext, tsEntry); this.insertText = isSnippet && tsEntry.insertText ? new vscode.SnippetString(tsEntry.insertText) : tsEntry.insertText; - this.filterText = this.getFilterText(completionContext.line, tsEntry.insertText); + // @ts-expect-error until 5.2 + this.filterText = tsEntry.filterText || this.getFilterText(completionContext.line, tsEntry.insertText); if (completionContext.isMemberCompletion && completionContext.dotAccessorContext && !(this.insertText instanceof vscode.SnippetString)) { this.filterText = completionContext.dotAccessorContext.text + (this.insertText || this.textLabel); if (!this.range) { - const replacementRange = this.getFuzzyWordRange(); + const replacementRange = this.completionContext.wordRange; if (replacementRange) { this.range = { inserting: completionContext.dotAccessorContext.range, @@ -422,7 +420,7 @@ class MyCompletionItem extends vscode.CompletionItem { return; } - const replaceRange = this.getFuzzyWordRange(); + const replaceRange = this.completionContext.wordRange; if (replaceRange) { this.range = { inserting: new vscode.Range(replaceRange.start, this.position), @@ -431,23 +429,6 @@ class MyCompletionItem extends vscode.CompletionItem { } } - private getFuzzyWordRange() { - if (this.completionContext.useFuzzyWordRangeLogic) { - // Try getting longer, prefix based range for completions that span words - const text = this.completionContext.line.slice(Math.max(0, this.position.character - this.textLabel.length), this.position.character).toLowerCase(); - const entryName = this.textLabel.toLowerCase(); - for (let i = entryName.length; i >= 0; --i) { - if (text.endsWith(entryName.substr(0, i)) && (!this.completionContext.wordRange || this.completionContext.wordRange.start.character > this.position.character - i)) { - return new vscode.Range( - new vscode.Position(this.position.line, Math.max(0, this.position.character - i)), - this.position); - } - } - } - - return this.completionContext.wordRange; - } - private static convertKind(kind: string): vscode.CompletionItemKind { switch (kind) { case PConst.Kind.primitiveType: @@ -516,7 +497,7 @@ class MyCompletionItem extends vscode.CompletionItem { return undefined; } - if (context.isNewIdentifierLocation || !context.isInValidCommitCharacterContext) { + if (context.isNewIdentifierLocation) { return undefined; } @@ -789,16 +770,14 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider< metadata = response.metadata; } - const completionContext = { + const completionContext: CompletionContext = { isNewIdentifierLocation, isMemberCompletion, dotAccessorContext, - isInValidCommitCharacterContext: this.isInValidCommitCharacterContext(document, position), enableCallCompletions: !completionConfiguration.completeFunctionCalls, wordRange, line: line.text, completeFunctionCalls: completionConfiguration.completeFunctionCalls, - useFuzzyWordRangeLogic: this.client.apiVersion.lt(API.v390), }; let includesPackageJsonImport = false; @@ -863,26 +842,27 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider< private getTsTriggerCharacter(context: vscode.CompletionContext): Proto.CompletionsTriggerCharacter | undefined { switch (context.triggerCharacter) { - case '@': // Workaround for https://github.com/microsoft/TypeScript/issues/27321 + case '@': { // Workaround for https://github.com/microsoft/TypeScript/issues/27321 return this.client.apiVersion.gte(API.v310) && this.client.apiVersion.lt(API.v320) ? undefined : '@'; - - case '#': // Workaround for https://github.com/microsoft/TypeScript/issues/36367 + } + case '#': { // Workaround for https://github.com/microsoft/TypeScript/issues/36367 return this.client.apiVersion.lt(API.v381) ? undefined : '#'; - + } case ' ': { - const space: Proto.CompletionsTriggerCharacter = ' '; - return this.client.apiVersion.gte(API.v430) ? space : undefined; + return this.client.apiVersion.gte(API.v430) ? ' ' : undefined; } case '.': case '"': case '\'': case '`': case '/': - case '<': + case '<': { return context.triggerCharacter; + } + default: { + return undefined; + } } - - return undefined; } public async resolveCompletionItem( @@ -893,25 +873,6 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider< return item; } - private isInValidCommitCharacterContext( - document: vscode.TextDocument, - position: vscode.Position - ): boolean { - if (this.client.apiVersion.lt(API.v320)) { - // Workaround for https://github.com/microsoft/TypeScript/issues/27742 - // Only enable dot completions when previous character not a dot preceded by whitespace. - // Prevents incorrectly completing while typing spread operators. - if (position.character > 1) { - const preText = document.getText(new vscode.Range( - position.line, 0, - position.line, position.character)); - return preText.match(/(\s|^)\.$/ig) === null; - } - } - - return true; - } - private shouldTrigger( context: vscode.CompletionContext, line: vscode.TextLine, diff --git a/extensions/typescript-language-features/src/languageFeatures/tsconfig.ts b/extensions/typescript-language-features/src/languageFeatures/tsconfig.ts index 89f45060d2d..f861e3369b8 100644 --- a/extensions/typescript-language-features/src/languageFeatures/tsconfig.ts +++ b/extensions/typescript-language-features/src/languageFeatures/tsconfig.ts @@ -8,7 +8,7 @@ import { basename, posix } from 'path'; import * as vscode from 'vscode'; import { Utils } from 'vscode-uri'; import { coalesce } from '../utils/arrays'; -import { exists } from '../utils/fs'; +import { exists, looksLikeAbsoluteWindowsPath } from '../utils/fs'; function mapChildren(node: jsonc.Node | undefined, f: (x: jsonc.Node) => R): R[] { return node && node.type === 'array' && node.children @@ -48,10 +48,6 @@ class TsconfigLinkProvider implements vscode.DocumentLinkProvider { } const extendsValue: string = extendsNode.value; - if (extendsValue.startsWith('/')) { - return undefined; - } - const args: OpenExtendsLinkCommandArgs = { resourceUri: { ...document.uri.toJSON(), $mid: undefined }, // Prevent VS Code from trying to transform the uri extendsValue: extendsValue @@ -161,13 +157,8 @@ async function resolveNodeModulesPath(baseDirUri: vscode.Uri, pathCandidates: st * @returns Returns undefined in case of lack of result while trying to resolve from node_modules */ async function getTsconfigPath(baseDirUri: vscode.Uri, extendsValue: string): Promise { - // Don't take into account a case, where tsconfig might be resolved from the root (see the reference) - // e.g. C:/projects/shared-tsconfig/tsconfig.json (note that C: prefix is optional) - - const isRelativePath = ['./', '../'].some(str => extendsValue.startsWith(str)); - if (isRelativePath) { - const absolutePath = vscode.Uri.joinPath(baseDirUri, extendsValue); - if (await exists(absolutePath) || absolutePath.path.endsWith('.json')) { + async function resolve(absolutePath: vscode.Uri): Promise { + if (absolutePath.path.endsWith('.json') || await exists(absolutePath)) { return absolutePath; } return absolutePath.with({ @@ -175,6 +166,15 @@ async function getTsconfigPath(baseDirUri: vscode.Uri, extendsValue: string): Pr }); } + const isRelativePath = ['./', '../'].some(str => extendsValue.startsWith(str)); + if (isRelativePath) { + return resolve(vscode.Uri.joinPath(baseDirUri, extendsValue)); + } + + if (extendsValue.startsWith('/') || looksLikeAbsoluteWindowsPath(extendsValue)) { + return resolve(vscode.Uri.file(extendsValue)); + } + // Otherwise resolve like a module return resolveNodeModulesPath(baseDirUri, [ extendsValue, diff --git a/extensions/typescript-language-features/src/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index f978c800c95..b9e478b2b77 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -6,17 +6,18 @@ import { basename, extname } from 'path'; import * as vscode from 'vscode'; import { CommandManager } from './commands/commandManager'; +import { DocumentSelector } from './configuration/documentSelector'; +import * as fileSchemes from './configuration/fileSchemes'; +import { LanguageDescription } from './configuration/languageDescription'; import { DiagnosticKind } from './languageFeatures/diagnostics'; import FileConfigurationManager from './languageFeatures/fileConfigurationManager'; +import { TelemetryReporter } from './logging/telemetry'; import { CachedResponse } from './tsServer/cachedResponse'; import { ClientCapability } from './typescriptService'; import TypeScriptServiceClient from './typescriptServiceClient'; import TypingsStatus from './ui/typingsStatus'; import { Disposable } from './utils/dispose'; -import { DocumentSelector } from './configuration/documentSelector'; -import * as fileSchemes from './configuration/fileSchemes'; -import { LanguageDescription } from './configuration/languageDescription'; -import { TelemetryReporter } from './logging/telemetry'; +import { isWeb } from './utils/platform'; const validateSetting = 'validate.enable'; @@ -139,6 +140,10 @@ export default class LanguageProvider extends Disposable { return; } + if (diagnosticsKind === DiagnosticKind.Semantic && isWeb() && this.client.configuration.webProjectWideIntellisenseSuppressSemanticErrors) { + return; + } + const config = vscode.workspace.getConfiguration(this.id, file); const reportUnnecessary = config.get('showUnused', true); const reportDeprecated = config.get('showDeprecated', true); diff --git a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts index a38b67f0a02..0f6218e938e 100644 --- a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts +++ b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts @@ -16,17 +16,6 @@ import { ResourceMap } from '../utils/resourceMap'; import { API } from './api'; import type * as Proto from './protocol/protocol'; -const enum BufferLanguage { - TypeScript = 1, - JavaScript = 2, -} - -const enum BufferState { - Initial = 1, - Open = 2, - Closed = 2, -} - type ScriptKind = 'TS' | 'TSX' | 'JS' | 'JSX'; function mode2ScriptKind(mode: string): ScriptKind | undefined { @@ -39,6 +28,8 @@ function mode2ScriptKind(mode: string): ScriptKind | undefined { return undefined; } +const enum BufferState { Initial, Open, Closed } + const enum BufferOperationType { Close, Open, Change } class CloseOperation { @@ -236,17 +227,8 @@ class SyncedBuffer { return this.document.lineCount; } - public get language(): BufferLanguage { - switch (this.document.languageId) { - case languageModeIds.javascript: - case languageModeIds.javascriptreact: - return BufferLanguage.JavaScript; - - case languageModeIds.typescript: - case languageModeIds.typescriptreact: - default: - return BufferLanguage.TypeScript; - } + public get languageId(): string { + return this.document.languageId; } /** @@ -462,8 +444,9 @@ export default class BufferSyncSupport extends Disposable { private readonly client: ITypeScriptServiceClient; - private _validateJavaScript: boolean = true; - private _validateTypeScript: boolean = true; + private _validateJavaScript = true; + private _validateTypeScript = true; + private readonly modeIds: Set; private readonly syncedBuffers: SyncedBufferMap; private readonly pendingDiagnostics: PendingDiagnostics; @@ -755,11 +738,13 @@ export default class BufferSyncSupport extends Disposable { return false; } - switch (buffer.language) { - case BufferLanguage.JavaScript: + switch (buffer.languageId) { + case languageModeIds.javascript: + case languageModeIds.javascriptreact: return this._validateJavaScript; - case BufferLanguage.TypeScript: + case languageModeIds.typescript: + case languageModeIds.typescriptreact: default: return this._validateTypeScript; } diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 31e86e6100f..a9b48a10724 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -258,7 +258,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event; private isProjectWideIntellisenseOnWebEnabled(): boolean { - return isWebAndHasSharedArrayBuffers() && this._configuration.enableProjectWideIntellisenseOnWeb; + return isWebAndHasSharedArrayBuffers() && this._configuration.webProjectWideIntellisenseEnabled; } private cancelInflightRequestsForResource(resource: vscode.Uri): void { @@ -902,7 +902,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType const diagnosticEvent = event as Proto.DiagnosticEvent; if (diagnosticEvent.body?.diagnostics) { this._onDiagnosticsReceived.fire({ - kind: getDignosticsKind(event), + kind: getDiagnosticsKind(event), resource: this.toResource(diagnosticEvent.body.file), diagnostics: diagnosticEvent.body.diagnostics }); @@ -1089,7 +1089,7 @@ ${error.serverStack} }; } -function getDignosticsKind(event: Proto.Event) { +function getDiagnosticsKind(event: Proto.Event) { switch (event.event) { case 'syntaxDiag': return DiagnosticKind.Syntax; case 'semanticDiag': return DiagnosticKind.Semantic; diff --git a/extensions/typescript-language-features/src/utils/fs.ts b/extensions/typescript-language-features/src/utils/fs.ts index 88ce3e3aa75..a742b9604f8 100644 --- a/extensions/typescript-language-features/src/utils/fs.ts +++ b/extensions/typescript-language-features/src/utils/fs.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; -export const exists = async (resource: vscode.Uri): Promise => { +export async function exists(resource: vscode.Uri): Promise { try { const stat = await vscode.workspace.fs.stat(resource); // stat.type is an enum flag @@ -13,4 +13,8 @@ export const exists = async (resource: vscode.Uri): Promise => { } catch { return false; } -}; +} + +export function looksLikeAbsoluteWindowsPath(path: string): boolean { + return /^[a-zA-Z]:[\/\\]/.test(path); +} diff --git a/extensions/typescript-language-features/src/utils/resourceMap.ts b/extensions/typescript-language-features/src/utils/resourceMap.ts index a024a761da3..8356c817df7 100644 --- a/extensions/typescript-language-features/src/utils/resourceMap.ts +++ b/extensions/typescript-language-features/src/utils/resourceMap.ts @@ -5,6 +5,7 @@ import * as vscode from 'vscode'; import * as fileSchemes from '../configuration/fileSchemes'; +import { looksLikeAbsoluteWindowsPath } from './fs'; /** * Maps of file resources @@ -89,13 +90,9 @@ export class ResourceMap { } private isCaseInsensitivePath(path: string) { - if (isWindowsPath(path)) { + if (looksLikeAbsoluteWindowsPath(path)) { return true; } return path[0] === '/' && this.config.onCaseInsensitiveFileSystem; } } - -function isWindowsPath(path: string): boolean { - return /^[a-zA-Z]:[\/\\]/.test(path); -} diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/documentPaste.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/documentPaste.test.ts index 9ea13d1ff5e..dd5b9eae41c 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/documentPaste.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/documentPaste.test.ts @@ -9,7 +9,8 @@ import { closeAllEditors, createRandomFile, disposeAll } from '../utils'; const textPlain = 'text/plain'; -(vscode.env.uiKind === vscode.UIKind.Web ? suite.skip : suite)('vscode API - Copy Paste', function () { +// Skipped due to flakiness on Linux Desktop and errors on web +suite.skip('vscode API - Copy Paste', function () { this.retries(3); diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 34530cbba39..df5d7ccc876 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -228,10 +228,10 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -typescript@^5.1.0-dev.20230515: - version "5.1.0-dev.20230515" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.0-dev.20230515.tgz#cf1c5b9542d7bb6f5eeb7878f041705ee6f963be" - integrity sha512-yn0MGsy6U0QAVF+lXW6LPupQmuRsyA0xUJetqw2tDqa+49231BpkhTuY6oEwLsc98tiEzCfIw7hzbLsiwVGFaA== +typescript@5.1.1-rc: + version "5.1.1-rc" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.1-rc.tgz#7be6e85bb4ad36e07e0125e501eb08ed3a6e3769" + integrity sha512-+yHTPe5QCxw5cgN+B81z+k65xTHcwNCRwJN7OGVUe3srPULTZHF7J9QCgrptL7F8mrO7gmsert7XrMksAjutRw== vscode-grammar-updater@^1.1.0: version "1.1.0" diff --git a/package.json b/package.json index df467731a0c..6b5788e6f28 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.79.0", - "distro": "4e88da3231fbd0b33666c0d1b550b1ecdb739449", + "distro": "63a5d9b46d8e5c9c838d3d6afc42fd1aba8dfde2", "author": { "name": "Microsoft Corporation" }, @@ -67,7 +67,7 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.4", "@vscode/proxy-agent": "^0.13.2", - "@vscode/ripgrep": "^1.15.2", + "@vscode/ripgrep": "^1.15.3", "@vscode/spdlog": "^0.13.10", "@vscode/sqlite3": "5.1.4-vscode", "@vscode/sudo-prompt": "9.3.1", @@ -87,14 +87,14 @@ "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "9.0.0", - "xterm": "5.2.0-beta.41", + "xterm": "5.2.0-beta.43", "xterm-addon-canvas": "0.4.0-beta.11", "xterm-addon-image": "0.4.0", - "xterm-addon-search": "0.11.0", + "xterm-addon-search": "0.12.0-beta.3", "xterm-addon-serialize": "0.9.0", "xterm-addon-unicode11": "0.5.0", "xterm-addon-webgl": "0.15.0-beta.10", - "xterm-headless": "5.2.0-beta.41", + "xterm-headless": "5.2.0-beta.43", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/product.json b/product.json index 56048315511..f801597290e 100644 --- a/product.json +++ b/product.json @@ -24,8 +24,8 @@ "win32arm64UserAppId": "{{3AEBF0C8-F733-4AD4-BADE-FDB816D53D7B}", "win32AppUserModelId": "Microsoft.CodeOSS", "win32ShellNameShort": "C&ode - OSS", - "win32TunnelServiceMutex": "vscodetunnelserviceoss", - "win32TunnelMutex": "vscodetunneloss", + "win32TunnelServiceMutex": "vscodeoss-tunnelservice", + "win32TunnelMutex": "vscodeoss-tunnel", "darwinBundleIdentifier": "com.visualstudio.code.oss", "linuxIconName": "code-oss", "licenseFileName": "LICENSE.txt", diff --git a/remote/package.json b/remote/package.json index 02c1acf7651..e816b36a8d6 100644 --- a/remote/package.json +++ b/remote/package.json @@ -8,7 +8,7 @@ "@parcel/watcher": "2.1.0", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/proxy-agent": "^0.13.2", - "@vscode/ripgrep": "^1.15.2", + "@vscode/ripgrep": "^1.15.3", "@vscode/spdlog": "^0.13.10", "@vscode/vscode-languagedetection": "1.0.21", "cookie": "^0.4.0", @@ -24,14 +24,14 @@ "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "9.0.0", - "xterm": "5.2.0-beta.41", + "xterm": "5.2.0-beta.43", "xterm-addon-canvas": "0.4.0-beta.11", "xterm-addon-image": "0.4.0", - "xterm-addon-search": "0.11.0", + "xterm-addon-search": "0.12.0-beta.3", "xterm-addon-serialize": "0.9.0", "xterm-addon-unicode11": "0.5.0", "xterm-addon-webgl": "0.15.0-beta.10", - "xterm-headless": "5.2.0-beta.41", + "xterm-headless": "5.2.0-beta.43", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/web/package.json b/remote/web/package.json index 5a43f81b18c..6ed75aa98a2 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -11,10 +11,10 @@ "tas-client-umd": "0.1.8", "vscode-oniguruma": "1.7.0", "vscode-textmate": "9.0.0", - "xterm": "5.2.0-beta.41", + "xterm": "5.2.0-beta.43", "xterm-addon-canvas": "0.4.0-beta.11", "xterm-addon-image": "0.4.0", - "xterm-addon-search": "0.11.0", + "xterm-addon-search": "0.12.0-beta.3", "xterm-addon-unicode11": "0.5.0", "xterm-addon-webgl": "0.15.0-beta.10" } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 48f9cb8b376..7190da9556e 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -78,10 +78,10 @@ xterm-addon-image@0.4.0: resolved "https://registry.yarnpkg.com/xterm-addon-image/-/xterm-addon-image-0.4.0.tgz#36e98fa892db11755a5f6e9654f924e876e29bf8" integrity sha512-3wumCJo4WTzxvecSMxJ7XtpVQeFe4gE2cdHCyUdo7zagVkS18YXJacGx6DjlAIccdJn6/LhGuD99xOSSvYx9Gw== -xterm-addon-search@0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.11.0.tgz#2a00ff7f9848f6140e7c4d1782486b0b18b06e0d" - integrity sha512-6U4uHXcQ7G5igsdaGqrJ9ehm7vep24bXqWxuy3AnIosXF2Z5uy2MvmYRyTGNembIqPV/x1YhBQ7uShtuqBHhOQ== +xterm-addon-search@0.12.0-beta.3: + version "0.12.0-beta.3" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.12.0-beta.3.tgz#9ced9431378d37f4b42010691eb5b76f66b8b9e5" + integrity sha512-NgzmUP5/764+csltcteHvIoNOScQHkIXkwEj+KgQr4QR48qFZMq3iGybcvv6icJsGglvvdG7YRputyT35eCjpQ== xterm-addon-unicode11@0.5.0: version "0.5.0" @@ -93,7 +93,7 @@ xterm-addon-webgl@0.15.0-beta.10: resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.15.0-beta.10.tgz#39ebbfb1b89c6773a2d8cb8e1d2f3ef1f08b28f9" integrity sha512-JVv4t5q6QGWyLiEAcAk9H2B83hFlIalzEwWu1VVYso0MJyZAlZ0NP5Za03iSKxYi7RQIA5bOe8r7W24esQDjLg== -xterm@5.2.0-beta.41: - version "5.2.0-beta.41" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.2.0-beta.41.tgz#ff9f5ed8890a751db2263ebdd362f5443699026d" - integrity sha512-yvDMaeELF8YuqTv220/+i5MKvy9GB4Vu7tH1T3OIqPvHwgnC8SY1vOSjskxHsHsRE5qLluiQJo6/wvt3kZH8+A== +xterm@5.2.0-beta.43: + version "5.2.0-beta.43" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.2.0-beta.43.tgz#08657de5aebaef2cbac3f030f2017441ba9b1453" + integrity sha512-/0BcW7ZavHCtnHsr+t6jHWXBQ5H2J+yg22t8R8YaGebb+0LlMhut2cPTbyOZxYOFHkkiGolEIgGmU4XW1b6toA== diff --git a/remote/yarn.lock b/remote/yarn.lock index 5616669478a..fc818ca719a 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -72,10 +72,10 @@ optionalDependencies: "@vscode/windows-ca-certs" "^0.3.1" -"@vscode/ripgrep@^1.15.2": - version "1.15.2" - resolved "https://registry.yarnpkg.com/@vscode/ripgrep/-/ripgrep-1.15.2.tgz#85b55181353d6d204210e64e03853c5e2ee6edd9" - integrity sha512-8zmyoxV6F+CY1Rinaq7LO/bGShaX2+B333X+Nqo984nC6jg2OvfZtQHzU+PKNQte2fjhm9h2ZlZTufnJxHaX9w== +"@vscode/ripgrep@^1.15.3": + version "1.15.3" + resolved "https://registry.yarnpkg.com/@vscode/ripgrep/-/ripgrep-1.15.3.tgz#bd53c555ed7f2f546edc46a47d72b1914a5ba23d" + integrity sha512-fCJP+4MRnhSTWw+GYAH93kSIomWYvdSe5206IqcHofBFcaFKR51XQNU0D5RB26Ps/5zRf5AQS26DIqqbMsB1Cw== dependencies: https-proxy-agent "^5.0.0" proxy-from-env "^1.1.0" @@ -846,10 +846,10 @@ xterm-addon-image@0.4.0: resolved "https://registry.yarnpkg.com/xterm-addon-image/-/xterm-addon-image-0.4.0.tgz#36e98fa892db11755a5f6e9654f924e876e29bf8" integrity sha512-3wumCJo4WTzxvecSMxJ7XtpVQeFe4gE2cdHCyUdo7zagVkS18YXJacGx6DjlAIccdJn6/LhGuD99xOSSvYx9Gw== -xterm-addon-search@0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.11.0.tgz#2a00ff7f9848f6140e7c4d1782486b0b18b06e0d" - integrity sha512-6U4uHXcQ7G5igsdaGqrJ9ehm7vep24bXqWxuy3AnIosXF2Z5uy2MvmYRyTGNembIqPV/x1YhBQ7uShtuqBHhOQ== +xterm-addon-search@0.12.0-beta.3: + version "0.12.0-beta.3" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.12.0-beta.3.tgz#9ced9431378d37f4b42010691eb5b76f66b8b9e5" + integrity sha512-NgzmUP5/764+csltcteHvIoNOScQHkIXkwEj+KgQr4QR48qFZMq3iGybcvv6icJsGglvvdG7YRputyT35eCjpQ== xterm-addon-serialize@0.9.0: version "0.9.0" @@ -866,15 +866,15 @@ xterm-addon-webgl@0.15.0-beta.10: resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.15.0-beta.10.tgz#39ebbfb1b89c6773a2d8cb8e1d2f3ef1f08b28f9" integrity sha512-JVv4t5q6QGWyLiEAcAk9H2B83hFlIalzEwWu1VVYso0MJyZAlZ0NP5Za03iSKxYi7RQIA5bOe8r7W24esQDjLg== -xterm-headless@5.2.0-beta.41: - version "5.2.0-beta.41" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.2.0-beta.41.tgz#17085c0ef255214c244bdaa73e5914fd722be28e" - integrity sha512-cAKuiYPs2GQpCWFIWHakRF+57vPDmqLzMFX3kOcMj+deHPuDnxucQTZvjxKNbMZKG9u9r+shIyfGzgC0oA+bBw== +xterm-headless@5.2.0-beta.43: + version "5.2.0-beta.43" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.2.0-beta.43.tgz#efcf143f29d5e656329e732d46280c50508d2c36" + integrity sha512-iMPkICe93emuX0KBjbE3fBmeRlBoE05ohLAlsBEr1OtSIpFervMxXP18Ch8EULSU25lSmZ6xISuAj9TdQSbvYA== -xterm@5.2.0-beta.41: - version "5.2.0-beta.41" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.2.0-beta.41.tgz#ff9f5ed8890a751db2263ebdd362f5443699026d" - integrity sha512-yvDMaeELF8YuqTv220/+i5MKvy9GB4Vu7tH1T3OIqPvHwgnC8SY1vOSjskxHsHsRE5qLluiQJo6/wvt3kZH8+A== +xterm@5.2.0-beta.43: + version "5.2.0-beta.43" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.2.0-beta.43.tgz#08657de5aebaef2cbac3f030f2017441ba9b1453" + integrity sha512-/0BcW7ZavHCtnHsr+t6jHWXBQ5H2J+yg22t8R8YaGebb+0LlMhut2cPTbyOZxYOFHkkiGolEIgGmU4XW1b6toA== yallist@^4.0.0: version "4.0.0" diff --git a/src/bootstrap-window.js b/src/bootstrap-window.js index c9f1b4a9583..1c8e0831975 100644 --- a/src/bootstrap-window.js +++ b/src/bootstrap-window.js @@ -114,6 +114,7 @@ loaderConfig.paths = { 'vscode-textmate': `${baseNodeModulesPath}/vscode-textmate/release/main.js`, 'vscode-oniguruma': `${baseNodeModulesPath}/vscode-oniguruma/release/main.js`, + 'vsda': `${baseNodeModulesPath}/vsda/index.js`, 'xterm': `${baseNodeModulesPath}/xterm/lib/xterm.js`, 'xterm-addon-canvas': `${baseNodeModulesPath}/xterm-addon-canvas/lib/xterm-addon-canvas.js`, 'xterm-addon-image': `${baseNodeModulesPath}/xterm-addon-image/lib/xterm-addon-image.js`, diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index ea2309d11d3..3d1dc3d86b8 100644 Binary files a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf differ diff --git a/src/vs/base/browser/ui/dialog/dialog.ts b/src/vs/base/browser/ui/dialog/dialog.ts index d7fc96598f1..0b252ae46e9 100644 --- a/src/vs/base/browser/ui/dialog/dialog.ts +++ b/src/vs/base/browser/ui/dialog/dialog.ts @@ -172,16 +172,16 @@ export class Dialog extends Disposable { } private getIconAriaLabel(): string { - const typeLabel = nls.localize('dialogInfoMessage', 'Info'); + let typeLabel = nls.localize('dialogInfoMessage', 'Info'); switch (this.options.type) { case 'error': - nls.localize('dialogErrorMessage', 'Error'); + typeLabel = nls.localize('dialogErrorMessage', 'Error'); break; case 'warning': - nls.localize('dialogWarningMessage', 'Warning'); + typeLabel = nls.localize('dialogWarningMessage', 'Warning'); break; case 'pending': - nls.localize('dialogPendingMessage', 'In Progress'); + typeLabel = nls.localize('dialogPendingMessage', 'In Progress'); break; case 'none': case 'info': diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index 096790cc3d7..ab7e3a18dc5 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -69,7 +69,7 @@ export namespace Schemas { export const vscodeTerminal = 'vscode-terminal'; - export const vscodeInteractiveSesssion = 'vscode-chat-editor'; + export const vscodeChatSesssion = 'vscode-chat-editor'; /** * Scheme used internally for webviews that aren't linked to a resource (i.e. not custom editors) diff --git a/src/vs/base/node/unc.d.ts b/src/vs/base/node/unc.d.ts index 1e78aba97a1..75e53310c8c 100644 --- a/src/vs/base/node/unc.d.ts +++ b/src/vs/base/node/unc.d.ts @@ -17,3 +17,9 @@ export function getUNCHostAllowlist(): string[]; * Adds one to many UNC host(s) to the allowed list in node.js. */ export function addUNCHostToAllowlist(allowedHost: string | string[]): void; + +/** + * Disables UNC Host allow list in node.js and thus disables UNC + * path validation. + */ +export function disableUNCAccessRestrictions(): void; diff --git a/src/vs/base/node/unc.js b/src/vs/base/node/unc.js index 7b046936764..7a1c9e14ac6 100644 --- a/src/vs/base/node/unc.js +++ b/src/vs/base/node/unc.js @@ -109,10 +109,19 @@ return host; } + function disableUNCAccessRestrictions() { + if (process.platform !== 'win32') { + return; + } + + process.enableUNCAccessChecks = false; + } + return { getUNCHostAllowlist, addUNCHostToAllowlist, - getUNCHost + getUNCHost, + disableUNCAccessRestrictions }; } diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 8a5181c3dab..09510958be4 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { app, BrowserWindow, dialog, protocol, session, Session, systemPreferences, WebFrameMain } from 'electron'; -import { addUNCHostToAllowlist } from 'vs/base/node/unc'; +import { addUNCHostToAllowlist, disableUNCAccessRestrictions } from 'vs/base/node/unc'; import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { hostname, release } from 'os'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -320,7 +320,11 @@ export class CodeApplication extends Disposable { //#region UNC Host Allowlist (Windows) if (isWindows) { - addUNCHostToAllowlist(this.configurationService.getValue('security.allowedUNCHosts')); + if (this.configurationService.getValue('security.restrictUNCAccess') === false) { + disableUNCAccessRestrictions(); + } else { + addUNCHostToAllowlist(this.configurationService.getValue('security.allowedUNCHosts')); + } } //#endregion diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index b0378159a26..2dc0e7f3774 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -100,7 +100,7 @@ export async function main(argv: string[]): Promise { // Usage: `[[ "$TERM_PROGRAM" == "vscode" ]] && . "$(code --locate-shell-integration-path zsh)"` case 'zsh': file = 'shellIntegration-rc.zsh'; break; // Usage: `string match -q "$TERM_PROGRAM" "vscode"; and . (code --locate-shell-integration-path fish)` - case 'fish': file = 'shellIntegration.fish'; break; + case 'fish': file = 'fish_xdg_data/fish/vendor_conf.d/shellIntegration.fish'; break; default: throw new Error('Error using --locate-shell-integration-path: Invalid shell type'); } console.log(join(getAppRoot(), 'out', 'vs', 'workbench', 'contrib', 'terminal', 'browser', 'media', file)); diff --git a/src/vs/code/node/sharedProcess/sharedProcessMain.ts b/src/vs/code/node/sharedProcess/sharedProcessMain.ts index 579979ce3c5..28053c1cdd6 100644 --- a/src/vs/code/node/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/node/sharedProcess/sharedProcessMain.ts @@ -109,6 +109,9 @@ import { UserDataAutoSyncService } from 'vs/platform/userDataSync/node/userDataA import { ExtensionTipsService } from 'vs/platform/extensionManagement/node/extensionTipsService'; import { IMainProcessService, MainProcessService } from 'vs/platform/ipc/common/mainProcessService'; import { RemoteStorageService } from 'vs/platform/storage/common/storageService'; +import { IRemoteSocketFactoryService, RemoteSocketFactoryService } from 'vs/platform/remote/common/remoteSocketFactoryService'; +import { RemoteConnectionType } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory'; class SharedProcessMain extends Disposable { @@ -338,6 +341,9 @@ class SharedProcessMain extends Disposable { services.set(ISignService, new SyncDescriptor(SignService, undefined, false /* proxied to other processes */)); // Tunnel + const remoteSocketFactoryService = new RemoteSocketFactoryService(); + services.set(IRemoteSocketFactoryService, remoteSocketFactoryService); + remoteSocketFactoryService.register(RemoteConnectionType.WebSocket, nodeSocketFactory); services.set(ISharedTunnelsService, new SyncDescriptor(SharedTunnelsService)); services.set(ISharedProcessTunnelService, new SyncDescriptor(SharedProcessTunnelService)); diff --git a/src/vs/platform/sign/browser/signService.ts b/src/vs/platform/sign/browser/signService.ts index 0f14d64eed2..8625763cd8e 100644 --- a/src/vs/platform/sign/browser/signService.ts +++ b/src/vs/platform/sign/browser/signService.ts @@ -3,24 +3,94 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IMessage, ISignService } from 'vs/platform/sign/common/sign'; +import { IntervalTimer } from 'vs/base/common/async'; +import { memoize } from 'vs/base/common/decorators'; +import { FileAccess } from 'vs/base/common/network'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { AbstractSignService, IVsdaValidator } from 'vs/platform/sign/common/abstractSignService'; +import { ISignService } from 'vs/platform/sign/common/sign'; -export class SignService implements ISignService { +declare module vsdaWeb { + export function sign(salted_message: string): string; - declare readonly _serviceBrand: undefined; - - constructor( - private readonly _token: Promise | string | undefined - ) { } - - async createNewMessage(value: string): Promise { - return { id: '', data: value }; + // eslint-disable-next-line @typescript-eslint/naming-convention + export class validator { + free(): void; + constructor(); + createNewMessage(original: string): string; + validate(signed_message: string): 'ok' | 'error'; } - async validate(message: IMessage, value: string): Promise { - return true; + + export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; + export function init(module_or_path?: InitInput | Promise): Promise; +} + +// Initialized if/when vsda is loaded +declare const vsda_web: { + default: typeof vsdaWeb.init; + sign: typeof vsdaWeb.sign; + validator: typeof vsdaWeb.validator; +}; + +const KEY_SIZE = 32; +const IV_SIZE = 16; +const STEP_SIZE = KEY_SIZE + IV_SIZE; + +export class SignService extends AbstractSignService implements ISignService { + constructor(@IProductService private readonly productService: IProductService) { + super(); } - async sign(value: string): Promise { - const token = await Promise.resolve(this._token); - return token || ''; + protected override getValidator(): Promise { + return this.vsda().then(vsda => { + const v = new vsda.validator(); + return { + createNewMessage: arg => v.createNewMessage(arg), + validate: arg => v.validate(arg), + dispose: () => v.free(), + }; + }); + } + + protected override signValue(arg: string): Promise { + return this.vsda().then(vsda => vsda.sign(arg)); + } + + @memoize + private async vsda(): Promise { + const checkInterval = new IntervalTimer(); + let [wasm] = await Promise.all([ + this.getWasmBytes(), + new Promise((resolve, reject) => { + require(['vsda'], resolve, reject); + + // todo@connor4312: there seems to be a bug(?) in vscode-loader with + // require() not resolving in web once the script loads, so check manually + checkInterval.cancelAndSet(() => { + if (typeof vsda_web !== 'undefined') { + resolve(); + } + }, 50); + }).finally(() => checkInterval!.dispose()), + ]); + + + const keyBytes = new TextEncoder().encode(this.productService.serverLicense?.join('\n') || ''); + for (let i = 0; i + STEP_SIZE < keyBytes.length; i += STEP_SIZE) { + const key = await crypto.subtle.importKey('raw', keyBytes.slice(i + IV_SIZE, i + IV_SIZE + KEY_SIZE), { name: 'AES-CBC' }, false, ['decrypt']); + wasm = await crypto.subtle.decrypt({ name: 'AES-CBC', iv: keyBytes.slice(i, i + IV_SIZE) }, key, wasm); + } + + await vsda_web.default(wasm); + + return vsda_web; + } + + private async getWasmBytes(): Promise { + const response = await fetch(FileAccess.asBrowserUri('vsda/../vsda_bg.wasm').toString(true)); + if (!response.ok) { + throw new Error('error loading vsda'); + } + + return response.arrayBuffer(); } } diff --git a/src/vs/platform/sign/common/abstractSignService.ts b/src/vs/platform/sign/common/abstractSignService.ts new file mode 100644 index 00000000000..6f7c91ba958 --- /dev/null +++ b/src/vs/platform/sign/common/abstractSignService.ts @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IMessage, ISignService } from 'vs/platform/sign/common/sign'; + +export interface IVsdaSigner { + sign(arg: string): string; +} + +export interface IVsdaValidator { + createNewMessage(arg: string): string; + validate(arg: string): 'ok' | 'error'; + dispose?(): void; +} + +export abstract class AbstractSignService implements ISignService { + declare readonly _serviceBrand: undefined; + + private static _nextId = 1; + private readonly validators = new Map(); + + protected abstract getValidator(): Promise; + protected abstract signValue(arg: string): Promise; + + public async createNewMessage(value: string): Promise { + try { + const validator = await this.getValidator(); + if (validator) { + const id = String(AbstractSignService._nextId++); + this.validators.set(id, validator); + return { + id: id, + data: validator.createNewMessage(value) + }; + } + } catch (e) { + // ignore errors silently + } + return { id: '', data: value }; + } + + async validate(message: IMessage, value: string): Promise { + if (!message.id) { + return true; + } + + const validator = this.validators.get(message.id); + if (!validator) { + return false; + } + this.validators.delete(message.id); + try { + return (validator.validate(value) === 'ok'); + } catch (e) { + // ignore errors silently + return false; + } finally { + validator.dispose?.(); + } + } + + async sign(value: string): Promise { + try { + return await this.signValue(value); + } catch (e) { + // ignore errors silently + } + return value; + } +} diff --git a/src/vs/platform/sign/node/signService.ts b/src/vs/platform/sign/node/signService.ts index 1cb3f02801b..d07ba9cfbe9 100644 --- a/src/vs/platform/sign/node/signService.ts +++ b/src/vs/platform/sign/node/signService.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IMessage, ISignService } from 'vs/platform/sign/common/sign'; +import { AbstractSignService, IVsdaValidator } from 'vs/platform/sign/common/abstractSignService'; +import { ISignService } from 'vs/platform/sign/common/sign'; declare module vsda { // the signer is a native module that for historical reasons uses a lower case class name @@ -19,62 +20,15 @@ declare module vsda { } } -export class SignService implements ISignService { - declare readonly _serviceBrand: undefined; - - private static _nextId = 1; - private readonly validators = new Map(); +export class SignService extends AbstractSignService implements ISignService { + protected override getValidator(): Promise { + return this.vsda().then(vsda => new vsda.validator()); + } + protected override signValue(arg: string): Promise { + return this.vsda().then(vsda => new vsda.signer().sign(arg)); + } private vsda(): Promise { return new Promise((resolve, reject) => require(['vsda'], resolve, reject)); } - - async createNewMessage(value: string): Promise { - try { - const vsda = await this.vsda(); - const validator = new vsda.validator(); - if (validator) { - const id = String(SignService._nextId++); - this.validators.set(id, validator); - return { - id: id, - data: validator.createNewMessage(value) - }; - } - } catch (e) { - // ignore errors silently - } - return { id: '', data: value }; - } - - async validate(message: IMessage, value: string): Promise { - if (!message.id) { - return true; - } - - const validator = this.validators.get(message.id); - if (!validator) { - return false; - } - this.validators.delete(message.id); - try { - return (validator.validate(value) === 'ok'); - } catch (e) { - // ignore errors silently - return false; - } - } - - async sign(value: string): Promise { - try { - const vsda = await this.vsda(); - const signer = new vsda.signer(); - if (signer) { - return signer.sign(value); - } - } catch (e) { - // ignore errors silently - } - return value; - } } diff --git a/src/vs/platform/terminal/node/terminalEnvironment.ts b/src/vs/platform/terminal/node/terminalEnvironment.ts index 5b780d51756..fc3dab089b2 100644 --- a/src/vs/platform/terminal/node/terminalEnvironment.ts +++ b/src/vs/platform/terminal/node/terminalEnvironment.ts @@ -177,7 +177,7 @@ export function getShellIntegrationInjection( // The injection mechanism used for fish is to add a custom dir to $XDG_DATA_DIRS which // is similar to $ZDOTDIR in zsh but contains a list of directories to run from. const oldDataDirs = env?.XDG_DATA_DIRS ?? '/usr/local/share:/usr/share'; - const newDataDir = path.join(appRoot, 'out/vs/workbench/contrib/xdg_data'); + const newDataDir = path.join(appRoot, 'out/vs/workbench/contrib/terminal/browser/media/fish_xdg_data'); envMixin['XDG_DATA_DIRS'] = `${oldDataDirs}:${newDataDir}`; addEnvMixinPathPrefix(options, envMixin); return { newArgs: undefined, envMixin }; diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index 724fdc44923..01d503a174f 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -375,7 +375,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess if (this._ptyProcess) { await this._throttleKillSpawn(); this._logService.trace('IPty#kill'); - this._ptyProcess.kill(!isWindows ? 'SIGKILL' : undefined); + this._ptyProcess.kill(); } } catch (ex) { // Swallow, the pty has already been killed diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index f9013d85a3d..2c65a1a908d 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -450,7 +450,7 @@ export const listWarningForeground = registerColor('list.warningForeground', { d export const listFilterWidgetBackground = registerColor('listFilterWidget.background', { light: darken(editorWidgetBackground, 0), dark: lighten(editorWidgetBackground, 0), hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, nls.localize('listFilterWidgetBackground', 'Background color of the type filter widget in lists and trees.')); export const listFilterWidgetOutline = registerColor('listFilterWidget.outline', { dark: Color.transparent, light: Color.transparent, hcDark: '#f38518', hcLight: '#007ACC' }, nls.localize('listFilterWidgetOutline', 'Outline color of the type filter widget in lists and trees.')); export const listFilterWidgetNoMatchesOutline = registerColor('listFilterWidget.noMatchesOutline', { dark: '#BE1100', light: '#BE1100', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('listFilterWidgetNoMatchesOutline', 'Outline color of the type filter widget in lists and trees, when there are no matches.')); -export const listFilterWidgetShadow = registerColor('listFilterWidget.shadow', { dark: widgetShadow, light: widgetShadow, hcDark: widgetShadow, hcLight: widgetShadow }, nls.localize('listFilterWidgetShadow', 'Shadown color of the type filter widget in lists and trees.')); +export const listFilterWidgetShadow = registerColor('listFilterWidget.shadow', { dark: widgetShadow, light: widgetShadow, hcDark: widgetShadow, hcLight: widgetShadow }, nls.localize('listFilterWidgetShadow', 'Shadow color of the type filter widget in lists and trees.')); export const listFilterMatchHighlight = registerColor('list.filterMatchBackground', { dark: editorFindMatchHighlight, light: editorFindMatchHighlight, hcDark: null, hcLight: null }, nls.localize('listFilterMatchHighlight', 'Background color of the filtered match.')); export const listFilterMatchHighlightBorder = registerColor('list.filterMatchBorder', { dark: editorFindMatchHighlightBorder, light: editorFindMatchHighlightBorder, hcDark: contrastBorder, hcLight: activeContrastBorder }, nls.localize('listFilterMatchHighlightBorder', 'Border color of the filtered match.')); export const treeIndentGuidesStroke = registerColor('tree.indentGuidesStroke', { dark: '#585858', light: '#a9a9a9', hcDark: '#a9a9a9', hcLight: '#a5a5a5' }, nls.localize('treeIndentGuidesStroke', "Tree stroke color for the indentation guides.")); diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index f4c506f55d1..d45291e67e6 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -212,7 +212,7 @@ export class Win32UpdateService extends AbstractUpdateService { this.availableUpdate.updateFilePath = path.join(cachePath, `CodeSetup-${this.productService.quality}-${update.version}.flag`); await pfs.Promises.writeFile(this.availableUpdate.updateFilePath, 'flag'); - const child = spawn(this.availableUpdate.packagePath, ['/verysilent', `/update="${this.availableUpdate.updateFilePath}"`, '/nocloseapplications', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], { + const child = spawn(this.availableUpdate.packagePath, ['/verysilent', '/log', `/update="${this.availableUpdate.updateFilePath}"`, '/nocloseapplications', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], { detached: true, stdio: ['ignore', 'ignore', 'ignore'], windowsVerbatimArguments: true @@ -241,7 +241,7 @@ export class Win32UpdateService extends AbstractUpdateService { if (this.state.update.supportsFastUpdate && this.availableUpdate.updateFilePath) { fs.unlinkSync(this.availableUpdate.updateFilePath); } else { - spawn(this.availableUpdate.packagePath, ['/silent', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], { + spawn(this.availableUpdate.packagePath, ['/silent', '/log', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], { detached: true, stdio: ['ignore', 'ignore', 'ignore'] }); diff --git a/src/vs/platform/workspace/common/canonicalUriIdentity.ts b/src/vs/platform/workspace/common/canonicalUri.ts similarity index 62% rename from src/vs/platform/workspace/common/canonicalUriIdentity.ts rename to src/vs/platform/workspace/common/canonicalUri.ts index aa781962a7e..11196429b0b 100644 --- a/src/vs/platform/workspace/common/canonicalUriIdentity.ts +++ b/src/vs/platform/workspace/common/canonicalUri.ts @@ -8,14 +8,14 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -export interface ICanonicalUriIdentityProvider { +export interface ICanonicalUriProvider { readonly scheme: string; - provideCanonicalUriIdentity(uri: UriComponents, token: CancellationToken): Promise; + provideCanonicalUri(uri: UriComponents, targetScheme: string, token: CancellationToken): Promise; } -export const ICanonicalUriIdentityService = createDecorator('canonicalUriIdentityService'); +export const ICanonicalUriService = createDecorator('canonicalUriIdentityService'); -export interface ICanonicalUriIdentityService { +export interface ICanonicalUriService { readonly _serviceBrand: undefined; - registerCanonicalUriIdentityProvider(provider: ICanonicalUriIdentityProvider): IDisposable; + registerCanonicalUriProvider(provider: ICanonicalUriProvider): IDisposable; } diff --git a/src/vs/server/node/remoteExtensionHostAgentCli.ts b/src/vs/server/node/remoteExtensionHostAgentCli.ts index 561637d13b9..5f6c64c3a1d 100644 --- a/src/vs/server/node/remoteExtensionHostAgentCli.ts +++ b/src/vs/server/node/remoteExtensionHostAgentCli.ts @@ -49,7 +49,7 @@ import { ExtensionsProfileScannerService } from 'vs/platform/extensionManagement import { LogService } from 'vs/platform/log/common/logService'; import { LoggerService } from 'vs/platform/log/node/loggerService'; import { localize } from 'vs/nls'; -import { addUNCHostToAllowlist } from 'vs/base/node/unc'; +import { addUNCHostToAllowlist, disableUNCAccessRestrictions } from 'vs/base/node/unc'; class CliMain extends Disposable { @@ -72,7 +72,11 @@ class CliMain extends Disposable { // On Windows, configure the UNC allow list based on settings if (isWindows) { - addUNCHostToAllowlist(configurationService.getValue('security.allowedUNCHosts')); + if (configurationService.getValue('security.restrictUNCAccess') === false) { + disableUNCAccessRestrictions(); + } else { + addUNCHostToAllowlist(configurationService.getValue('security.allowedUNCHosts')); + } } try { diff --git a/src/vs/server/node/remoteExtensionHostAgentServer.ts b/src/vs/server/node/remoteExtensionHostAgentServer.ts index 743bbd6145a..86fa775403e 100644 --- a/src/vs/server/node/remoteExtensionHostAgentServer.ts +++ b/src/vs/server/node/remoteExtensionHostAgentServer.ts @@ -23,7 +23,7 @@ import { createRegExp, escapeRegExpCharacters } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { findFreePort } from 'vs/base/node/ports'; -import { addUNCHostToAllowlist } from 'vs/base/node/unc'; +import { addUNCHostToAllowlist, disableUNCAccessRestrictions } from 'vs/base/node/unc'; import { PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net'; import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -719,7 +719,11 @@ export async function createServer(address: string | net.AddressInfo | null, arg const configurationService = accessor.get(IConfigurationService); if (platform.isWindows) { - addUNCHostToAllowlist(configurationService.getValue('security.allowedUNCHosts')); + if (configurationService.getValue('security.restrictUNCAccess') === false) { + disableUNCAccessRestrictions(); + } else { + addUNCHostToAllowlist(configurationService.getValue('security.allowedUNCHosts')); + } } }); diff --git a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts index 1614c9466a5..132bd6a39ef 100644 --- a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts @@ -70,9 +70,9 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, @ICustomEditorService private readonly _customEditorService: ICustomEditorService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, - @IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, @IEditorService private readonly _editorService: IEditorService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService, ) { super(); @@ -106,6 +106,26 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc // Working copy operations this._register(workingCopyFileService.onWillRunWorkingCopyFileOperation(async e => this.onWillRunWorkingCopyFileOperation(e))); + + this._register(extensionService.onWillStop(e => { + const dirtyCustomEditors = workingCopyService.workingCopies.filter(workingCopy => { + return workingCopy instanceof MainThreadCustomEditorModel && workingCopy.isDirty(); + }); + if (!dirtyCustomEditors.length) { + return; + } + + e.veto((async () => { + for (const dirtyCustomEditor of dirtyCustomEditors) { + const didSave = await dirtyCustomEditor.save(); + if (!didSave) { + // Veto + return true; + } + } + return false; // Don't veto + })(), localize('vetoExtHostRestart', "One or more custom editors could not be saved.")); + })); } public $registerTextEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: extHostProtocol.IWebviewPanelOptions, capabilities: extHostProtocol.CustomTextEditorCapabilities, serializeBuffersForPostMessage: boolean): void { diff --git a/src/vs/workbench/api/browser/mainThreadWorkspace.ts b/src/vs/workbench/api/browser/mainThreadWorkspace.ts index 304141dcca9..e346677647c 100644 --- a/src/vs/workbench/api/browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/browser/mainThreadWorkspace.ts @@ -28,7 +28,7 @@ import { ExtHostContext, ExtHostWorkspaceShape, ITextSearchComplete, IWorkspaceD import { IEditSessionIdentityService } from 'vs/platform/workspace/common/editSessions'; import { EditorResourceAccessor, SaveReason, SideBySideEditor } from 'vs/workbench/common/editor'; import { coalesce, firstOrDefault } from 'vs/base/common/arrays'; -import { ICanonicalUriIdentityService } from 'vs/platform/workspace/common/canonicalUriIdentity'; +import { ICanonicalUriService } from 'vs/platform/workspace/common/canonicalUri'; @extHostNamedCustomer(MainContext.MainThreadWorkspace) export class MainThreadWorkspace implements MainThreadWorkspaceShape { @@ -43,7 +43,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { @ISearchService private readonly _searchService: ISearchService, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @IEditSessionIdentityService private readonly _editSessionIdentityService: IEditSessionIdentityService, - @ICanonicalUriIdentityService private readonly _canonicalUriIdentityService: ICanonicalUriIdentityService, + @ICanonicalUriService private readonly _canonicalUriService: ICanonicalUriService, @IEditorService private readonly _editorService: IEditorService, @IWorkspaceEditingService private readonly _workspaceEditingService: IWorkspaceEditingService, @INotificationService private readonly _notificationService: INotificationService, @@ -273,13 +273,13 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { } // --- canonical uri identities --- - private registeredCanonicalUriIdentityProviders = new Map(); + private registeredCanonicalUriProviders = new Map(); - $registerCanonicalUriIdentityProvider(handle: number, scheme: string) { - const disposable = this._canonicalUriIdentityService.registerCanonicalUriIdentityProvider({ + $registerCanonicalUriProvider(handle: number, scheme: string) { + const disposable = this._canonicalUriService.registerCanonicalUriProvider({ scheme: scheme, - provideCanonicalUriIdentity: async (uri: UriComponents, token: CancellationToken) => { - const result = await this._proxy.$provideCanonicalUriIdentity(uri, token); + provideCanonicalUri: async (uri: UriComponents, targetScheme: string, token: CancellationToken) => { + const result = await this._proxy.$provideCanonicalUri(uri, targetScheme, token); if (result) { return URI.revive(result); } @@ -287,13 +287,13 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { } }); - this.registeredCanonicalUriIdentityProviders.set(handle, disposable); + this.registeredCanonicalUriProviders.set(handle, disposable); this._toDispose.add(disposable); } - $unregisterCanonicalUriIdentityProvider(handle: number) { - const disposable = this.registeredCanonicalUriIdentityProviders.get(handle); + $unregisterCanonicalUriProvider(handle: number) { + const disposable = this.registeredCanonicalUriProviders.get(handle); disposable?.dispose(); - this.registeredCanonicalUriIdentityProviders.delete(handle); + this.registeredCanonicalUriProviders.delete(handle); } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 416c940fa7d..26b89254bc6 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1097,13 +1097,13 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'editSessionIdentityProvider'); return extHostWorkspace.getOnWillCreateEditSessionIdentityEvent(extension)(listener, thisArgs, disposables); }, - registerCanonicalUriIdentityProvider: (scheme: string, provider: vscode.CanonicalUriIdentityProvider) => { - checkProposedApiEnabled(extension, 'canonicalUriIdentityProvider'); - return extHostWorkspace.registerCanonicalUriIdentityProvider(scheme, provider); + registerCanonicalUriProvider: (scheme: string, provider: vscode.CanonicalUriProvider) => { + checkProposedApiEnabled(extension, 'canonicalUriProvider'); + return extHostWorkspace.registerCanonicalUriProvider(scheme, provider); }, - provideCanonicalUriIdentity: (uri: vscode.Uri, token: vscode.CancellationToken) => { - checkProposedApiEnabled(extension, 'canonicalUriIdentityProvider'); - return extHostWorkspace.provideCanonicalUriIdentity(uri, token); + getCanonicalUri: (uri: vscode.Uri, options: vscode.CanonicalUriRequestOptions, token: vscode.CancellationToken) => { + checkProposedApiEnabled(extension, 'canonicalUriProvider'); + return extHostWorkspace.provideCanonicalUri(uri, options, token); } }; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 3a5304ff495..97d6f8a2741 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1204,8 +1204,8 @@ export interface MainThreadWorkspaceShape extends IDisposable { $requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Promise; $registerEditSessionIdentityProvider(handle: number, scheme: string): void; $unregisterEditSessionIdentityProvider(handle: number): void; - $registerCanonicalUriIdentityProvider(handle: number, scheme: string): void; - $unregisterCanonicalUriIdentityProvider(handle: number): void; + $registerCanonicalUriProvider(handle: number, scheme: string): void; + $unregisterCanonicalUriProvider(handle: number): void; } export interface IFileChangeDto { @@ -1552,7 +1552,7 @@ export interface ExtHostWorkspaceShape { $getEditSessionIdentifier(folder: UriComponents, token: CancellationToken): Promise; $provideEditSessionIdentityMatch(folder: UriComponents, identity1: string, identity2: string, token: CancellationToken): Promise; $onWillCreateEditSessionIdentity(folder: UriComponents, token: CancellationToken, timeout: number): Promise; - $provideCanonicalUriIdentity(uri: UriComponents, token: CancellationToken): Promise; + $provideCanonicalUri(uri: UriComponents, targetScheme: string, token: CancellationToken): Promise; } export interface ExtHostFileSystemInfoShape { diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index c7031319846..e20dfbd98e9 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -50,9 +50,9 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, ID registerLinkProvider(provider: vscode.TerminalLinkProvider): vscode.Disposable; registerProfileProvider(extension: IExtensionDescription, id: string, provider: vscode.TerminalProfileProvider): vscode.Disposable; registerTerminalQuickFixProvider(id: string, extensionId: string, provider: vscode.TerminalQuickFixProvider): vscode.Disposable; - getEnvironmentVariableCollection(extension: IExtensionDescription, persistent?: boolean): vscode.EnvironmentVariableCollection; + getEnvironmentVariableCollection(extension: IExtensionDescription): IEnvironmentVariableCollection; } - +type IEnvironmentVariableCollection = vscode.EnvironmentVariableCollection & { getScopedEnvironmentVariableCollection(scope: vscode.EnvironmentVariableScope | undefined): vscode.EnvironmentVariableCollection }; export interface ITerminalInternalOptions { isFeatureTerminal?: boolean; useShellEnvironment?: boolean; @@ -823,13 +823,13 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I return index; } - public getEnvironmentVariableCollection(extension: IExtensionDescription): vscode.EnvironmentVariableCollection { + public getEnvironmentVariableCollection(extension: IExtensionDescription): IEnvironmentVariableCollection { let collection = this._environmentVariableCollections.get(extension.identifier.value); if (!collection) { collection = new EnvironmentVariableCollection(); this._setEnvironmentVariableCollection(extension.identifier.value, collection); } - return collection; + return collection.getScopedEnvironmentVariableCollection(undefined); } private _syncEnvironmentVariableCollection(extensionIdentifier: string, collection: EnvironmentVariableCollection): void { @@ -867,8 +867,9 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } } -class EnvironmentVariableCollection implements vscode.EnvironmentVariableCollection { +class EnvironmentVariableCollection { readonly map: Map = new Map(); + private readonly scopedCollections: Map = new Map(); readonly descriptionMap: Map = new Map(); private _persistent: boolean = true; @@ -887,23 +888,30 @@ class EnvironmentVariableCollection implements vscode.EnvironmentVariableCollect this.map = new Map(serialized); } - get size(): number { - return this.map.size; + getScopedEnvironmentVariableCollection(scope: vscode.EnvironmentVariableScope | undefined): IEnvironmentVariableCollection { + const scopedCollectionKey = this.getScopeKey(scope); + let scopedCollection = this.scopedCollections.get(scopedCollectionKey); + if (!scopedCollection) { + scopedCollection = new ScopedEnvironmentVariableCollection(this, scope); + this.scopedCollections.set(scopedCollectionKey, scopedCollection); + scopedCollection.onDidChangeCollection(() => this._onDidChangeCollection.fire()); + } + return scopedCollection; } - replace(variable: string, value: string, scope?: vscode.EnvironmentVariableScope): void { + replace(variable: string, value: string, scope: vscode.EnvironmentVariableScope | undefined): void { this._setIfDiffers(variable, { value, type: EnvironmentVariableMutatorType.Replace, scope }); } - append(variable: string, value: string, scope?: vscode.EnvironmentVariableScope): void { + append(variable: string, value: string, scope: vscode.EnvironmentVariableScope | undefined): void { this._setIfDiffers(variable, { value, type: EnvironmentVariableMutatorType.Append, scope }); } - prepend(variable: string, value: string, scope?: vscode.EnvironmentVariableScope): void { + prepend(variable: string, value: string, scope: vscode.EnvironmentVariableScope | undefined): void { this._setIfDiffers(variable, { value, type: EnvironmentVariableMutatorType.Prepend, scope }); } - private _setIfDiffers(variable: string, mutator: vscode.EnvironmentVariableMutator): void { + private _setIfDiffers(variable: string, mutator: vscode.EnvironmentVariableMutator & { scope: vscode.EnvironmentVariableScope | undefined }): void { if (!mutator.scope) { delete (mutator as any).scope; // Convenient for tests } @@ -917,44 +925,42 @@ class EnvironmentVariableCollection implements vscode.EnvironmentVariableCollect } } - get(variable: string, scope?: vscode.EnvironmentVariableScope): vscode.EnvironmentVariableMutator | undefined { + get(variable: string, scope: vscode.EnvironmentVariableScope | undefined): vscode.EnvironmentVariableMutator | undefined { const key = this.getKey(variable, scope); const value = this.map.get(key); return value ? convertMutator(value) : undefined; } private getKey(variable: string, scope: vscode.EnvironmentVariableScope | undefined) { - const workspaceKey = this.getWorkspaceKey(scope?.workspaceFolder); - return workspaceKey ? `${variable}:::${workspaceKey}` : variable; + const scopeKey = this.getScopeKey(scope); + return scopeKey.length ? `${variable}:::${scopeKey}` : variable; + } + + private getScopeKey(scope: vscode.EnvironmentVariableScope | undefined): string { + return this.getWorkspaceKey(scope?.workspaceFolder) ?? ''; } private getWorkspaceKey(workspaceFolder: vscode.WorkspaceFolder | undefined): string | undefined { return workspaceFolder ? workspaceFolder.uri.toString() : undefined; } - forEach(callback: (variable: string, mutator: vscode.EnvironmentVariableMutator, collection: vscode.EnvironmentVariableCollection) => any, thisArg?: any): void { - this.map.forEach((value, _) => callback.call(thisArg, value.variable, convertMutator(value), this)); - } - - [Symbol.iterator](): IterableIterator<[variable: string, mutator: vscode.EnvironmentVariableMutator]> { - const map: Map = new Map(); - this.map.forEach((mutator, _key) => { - if (mutator.scope) { - // Scoped mutators are not supported via this iterator, as it returns variable as the key which is supposed to be unique. - return; + public getVariableMap(scope: vscode.EnvironmentVariableScope | undefined): Map { + const map = new Map(); + for (const [key, value] of this.map) { + if (this.getScopeKey(value.scope) === this.getScopeKey(scope)) { + map.set(key, value); } - map.set(mutator.variable, convertMutator(mutator)); - }); - return map.entries(); + } + return map; } - delete(variable: string, scope?: vscode.EnvironmentVariableScope): void { + delete(variable: string, scope: vscode.EnvironmentVariableScope | undefined): void { const key = this.getKey(variable, scope); this.map.delete(key); this._onDidChangeCollection.fire(); } - clear(scope?: vscode.EnvironmentVariableScope): void { + clear(scope: vscode.EnvironmentVariableScope | undefined): void { if (scope?.workspaceFolder) { for (const [key, mutator] of this.map) { if (mutator.scope?.workspaceFolder?.index === scope.workspaceFolder.index) { @@ -969,8 +975,8 @@ class EnvironmentVariableCollection implements vscode.EnvironmentVariableCollect this._onDidChangeCollection.fire(); } - setDescription(description: string | vscode.MarkdownString | undefined, scope?: vscode.EnvironmentVariableScope): void { - const key = this.getKey('', scope); + setDescription(description: string | vscode.MarkdownString | undefined, scope: vscode.EnvironmentVariableScope | undefined): void { + const key = this.getScopeKey(scope); const current = this.descriptionMap.get(key); if (!current || current.description !== description) { let descriptionStr: string | undefined; @@ -986,12 +992,78 @@ class EnvironmentVariableCollection implements vscode.EnvironmentVariableCollect } } - private clearDescription(scope?: vscode.EnvironmentVariableScope): void { - const key = this.getKey('', scope); + public getDescription(scope: vscode.EnvironmentVariableScope | undefined): string | vscode.MarkdownString | undefined { + const key = this.getScopeKey(scope); + return this.descriptionMap.get(key)?.description; + } + + private clearDescription(scope: vscode.EnvironmentVariableScope | undefined): void { + const key = this.getScopeKey(scope); this.descriptionMap.delete(key); } } +class ScopedEnvironmentVariableCollection implements vscode.EnvironmentVariableCollection, IEnvironmentVariableCollection { + public get persistent(): boolean { return this.collection.persistent; } + public set persistent(value: boolean) { + this.collection.persistent = value; + } + + protected readonly _onDidChangeCollection = new Emitter(); + get onDidChangeCollection(): Event { return this._onDidChangeCollection && this._onDidChangeCollection.event; } + + constructor( + private readonly collection: EnvironmentVariableCollection, + private readonly scope: vscode.EnvironmentVariableScope | undefined + ) { + } + + getScopedEnvironmentVariableCollection() { + return this.collection.getScopedEnvironmentVariableCollection(this.scope); + } + + replace(variable: string, value: string): void { + this.collection.replace(variable, value, this.scope); + } + + append(variable: string, value: string): void { + this.collection.append(variable, value, this.scope); + } + + prepend(variable: string, value: string): void { + this.collection.prepend(variable, value, this.scope); + } + + get(variable: string): vscode.EnvironmentVariableMutator | undefined { + return this.collection.get(variable, this.scope); + } + + forEach(callback: (variable: string, mutator: vscode.EnvironmentVariableMutator, collection: vscode.EnvironmentVariableCollection) => any, thisArg?: any): void { + this.collection.getVariableMap(this.scope).forEach((value, variable) => callback.call(thisArg, variable, convertMutator(value), this), this.scope); + } + + [Symbol.iterator](): IterableIterator<[variable: string, mutator: vscode.EnvironmentVariableMutator]> { + return this.collection.getVariableMap(this.scope).entries(); + } + + delete(variable: string): void { + this.collection.delete(variable, this.scope); + this._onDidChangeCollection.fire(undefined); + } + + clear(): void { + this.collection.clear(this.scope); + } + + set description(description: string | vscode.MarkdownString | undefined) { + this.collection.setDescription(description, this.scope); + } + + get description(): string | vscode.MarkdownString | undefined { + return this.collection.getDescription(this.scope); + } +} + export class WorkerExtHostTerminalService extends BaseExtHostTerminalService { constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index c4c0b1bfbbc..7984790f7b9 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -24,6 +24,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { ITreeViewsDnDService, TreeViewsDnDService } from 'vs/editor/common/services/treeViewsDnd'; +import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; type TreeItemHandle = string; @@ -768,14 +769,15 @@ class ExtHostTreeView extends Disposable { } let checkboxState: TreeItemCheckboxState; let tooltip: string | undefined = undefined; + let accessibilityInformation: IAccessibilityInformation | undefined = undefined; if (typeof extensionTreeItem.checkboxState === 'number') { checkboxState = extensionTreeItem.checkboxState; - } - else { + } else { checkboxState = extensionTreeItem.checkboxState.state; tooltip = extensionTreeItem.checkboxState.tooltip; + accessibilityInformation = extensionTreeItem.checkboxState.accessibilityInformation; } - return { isChecked: checkboxState === TreeItemCheckboxState.Checked, tooltip }; + return { isChecked: checkboxState === TreeItemCheckboxState.Checked, tooltip, accessibilityInformation }; } private validateTreeItem(extensionTreeItem: vscode.TreeItem) { diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index 08271f0670d..c4f627da3c9 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -699,32 +699,32 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac // --- canonical uri identity --- - private readonly _canonicalUriIdentityProviders = new Map(); + private readonly _canonicalUriProviders = new Map(); // called by ext host - registerCanonicalUriIdentityProvider(scheme: string, provider: vscode.CanonicalUriIdentityProvider) { - if (this._canonicalUriIdentityProviders.has(scheme)) { + registerCanonicalUriProvider(scheme: string, provider: vscode.CanonicalUriProvider) { + if (this._canonicalUriProviders.has(scheme)) { throw new Error(`A provider has already been registered for scheme ${scheme}`); } - this._canonicalUriIdentityProviders.set(scheme, provider); + this._canonicalUriProviders.set(scheme, provider); const outgoingScheme = this._uriTransformerService.transformOutgoingScheme(scheme); const handle = this._providerHandlePool++; - this._proxy.$registerCanonicalUriIdentityProvider(handle, outgoingScheme); + this._proxy.$registerCanonicalUriProvider(handle, outgoingScheme); return toDisposable(() => { - this._canonicalUriIdentityProviders.delete(scheme); - this._proxy.$unregisterCanonicalUriIdentityProvider(handle); + this._canonicalUriProviders.delete(scheme); + this._proxy.$unregisterCanonicalUriProvider(handle); }); } - async provideCanonicalUriIdentity(uri: URI, cancellationToken: CancellationToken): Promise { - const provider = this._canonicalUriIdentityProviders.get(uri.scheme); + async provideCanonicalUri(uri: URI, options: vscode.CanonicalUriRequestOptions, cancellationToken: CancellationToken): Promise { + const provider = this._canonicalUriProviders.get(uri.scheme); if (!provider) { return undefined; } - const result = await provider.provideCanonicalUriIdentity?.(URI.revive(uri), cancellationToken); + const result = await provider.provideCanonicalUri?.(URI.revive(uri), options, cancellationToken); if (!result) { return undefined; } @@ -733,8 +733,8 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac } // called by main thread - async $provideCanonicalUriIdentity(uri: UriComponents, cancellationToken: CancellationToken): Promise { - return this.provideCanonicalUriIdentity(URI.revive(uri), cancellationToken); + async $provideCanonicalUri(uri: UriComponents, targetScheme: string, cancellationToken: CancellationToken): Promise { + return this.provideCanonicalUri(URI.revive(uri), { targetScheme }, cancellationToken); } } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 364f0ad171c..736b70e040e 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -982,9 +982,15 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this._onWillOpenEditor.fire({ editor, groupId: this.id }); // Determine options + const 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.model.isSticky(options.index)) + || editor.hasCapability(EditorInputCapabilities.Scratchpad); const openEditorOptions: IEditorOpenOptions = { index: options ? options.index : undefined, - 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.model.isSticky(options.index)), + pinned, sticky: options?.sticky || (typeof options?.index === 'number' && this.model.isSticky(options.index)), active: this.count === 0 || !options || !options.inactive, supportSideBySide: internalOptions?.supportSideBySide diff --git a/src/vs/workbench/browser/parts/editor/editorsObserver.ts b/src/vs/workbench/browser/parts/editor/editorsObserver.ts index 96888700ba2..9c7b411e5dc 100644 --- a/src/vs/workbench/browser/parts/editor/editorsObserver.ts +++ b/src/vs/workbench/browser/parts/editor/editorsObserver.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEditorFactoryRegistry, IEditorIdentifier, GroupIdentifier, EditorExtensions, IEditorPartOptionsChangeEvent, EditorsOrder, GroupModelChangeKind } from 'vs/workbench/common/editor'; +import { IEditorFactoryRegistry, IEditorIdentifier, GroupIdentifier, EditorExtensions, IEditorPartOptionsChangeEvent, EditorsOrder, GroupModelChangeKind, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -345,8 +345,8 @@ export class EditorsObserver extends Disposable { let mostRecentEditorsCountingForLimit: IEditorIdentifier[]; if (this.editorGroupsService.partOptions.limit?.excludeDirty) { mostRecentEditorsCountingForLimit = mostRecentEditors.filter(({ editor }) => { - if (editor.isDirty() && !editor.isSaving()) { - return false; + if ((editor.isDirty() && !editor.isSaving()) || editor.hasCapability(EditorInputCapabilities.Scratchpad)) { + return false; // not dirty editors (unless in the process of saving) or scratchpads } return true; @@ -361,8 +361,8 @@ export class EditorsObserver extends Disposable { // Extract least recently used editors that can be closed const leastRecentlyClosableEditors = mostRecentEditorsCountingForLimit.reverse().filter(({ editor, groupId }) => { - if (editor.isDirty() && !editor.isSaving()) { - return false; // not dirty editors (unless in the process of saving) + if ((editor.isDirty() && !editor.isSaving()) || editor.hasCapability(EditorInputCapabilities.Scratchpad)) { + return false; // not dirty editors (unless in the process of saving) or scratchpads } if (exclude && editor === exclude.editor && groupId === exclude.groupId) { diff --git a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css index 8dfe362d08c..676c6e0fff8 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css +++ b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css @@ -53,7 +53,7 @@ max-height: 100%; aspect-ratio: 1/1; background-image: url('./letterpress-light.svg'); - background-size: 100%; + background-size: contain; background-position-x: center; background-repeat: no-repeat; } diff --git a/src/vs/workbench/browser/parts/views/checkbox.ts b/src/vs/workbench/browser/parts/views/checkbox.ts index 5da59e75b1a..b109ec29a0c 100644 --- a/src/vs/workbench/browser/parts/views/checkbox.ts +++ b/src/vs/workbench/browser/parts/views/checkbox.ts @@ -56,7 +56,7 @@ export class TreeItemCheckbox extends Disposable { icon: node.checkbox.isChecked ? Codicon.check : undefined, ...defaultToggleStyles }); - + this.setAccessibilityInformation(node.checkbox); this.toggle.domNode.classList.add(TreeItemCheckbox.checkboxClass); DOM.append(this.checkboxContainer, this.toggle.domNode); this.registerListener(node); @@ -78,6 +78,7 @@ export class TreeItemCheckbox extends Disposable { node.checkbox.isChecked = this.toggle.checked; this.toggle.setIcon(this.toggle.checked ? Codicon.check : undefined); this.toggle.setTitle(this.createCheckboxTitle(node.checkbox)); + this.setAccessibilityInformation(node.checkbox); this.checkboxStateHandler.setCheckboxState(node); } } @@ -87,6 +88,15 @@ export class TreeItemCheckbox extends Disposable { checkbox.isChecked ? localize('checked', 'Checked') : localize('unchecked', 'Unchecked'); } + private setAccessibilityInformation(checkbox: ITreeItemCheckboxState) { + if (this.toggle && checkbox.accessibilityInformation) { + this.toggle.domNode.ariaLabel = checkbox.accessibilityInformation.label; + if (checkbox.accessibilityInformation.role) { + this.toggle.domNode.role = checkbox.accessibilityInformation.role; + } + } + } + private removeCheckbox() { const children = this.checkboxContainer.children; for (const child of children) { diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 0e530e63913..fceae7563bf 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -282,7 +282,7 @@ export class BrowserMain extends Disposable { serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService); // Signing - const signService = new SignService(connectionToken); + const signService = new SignService(productService); serviceCollection.set(ISignService, signService); diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index cef5e663bd6..73941e9c331 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -704,6 +704,13 @@ const registry = Registry.as(ConfigurationExtensions.Con 'markdownDescription': localize('security.allowedUNCHosts', 'A set of UNC host names (without leading or trailing backslash, for example `192.168.0.1` or `my-server`) to allow without user confirmation. If a UNC host is being accessed that is not allowed via this setting or has not been acknowledged via user confirmation, an error will occur and the operation stopped. A restart is required when changing this setting. Find out more about this setting at https://aka.ms/vscode-windows-unc.'), 'included': isWeb ? true /* web maybe connected to a windows machine */ : isWindows, 'scope': ConfigurationScope.MACHINE + }, + 'security.restrictUNCAccess': { + 'type': 'boolean', + 'default': true, + 'markdownDescription': localize('security.restrictUNCAccess', 'If enabled, only allows access to UNC host names that are allowed by the `#security.allowedUNCHosts#` setting or after user confirmation. Find out more about this setting at https://aka.ms/vscode-windows-unc.'), + 'included': isWeb ? true /* web maybe connected to a windows machine */ : isWindows, + 'scope': ConfigurationScope.MACHINE } } }); diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index ac753c2d743..58d17e8f95d 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -750,7 +750,13 @@ export const enum EditorInputCapabilities { * Signals that the editor is composed of multiple editors * within. */ - MultipleEditors = 1 << 8 + MultipleEditors = 1 << 8, + + /** + * Signals that the editor cannot be in a dirty state + * and may still have unsaved changes + */ + Scratchpad = 1 << 9 } export type IUntypedEditorInput = IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput | IResourceDiffEditorInput | IResourceSideBySideEditorInput | IResourceMergeEditorInput; diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 8b38396e36c..99efb9f3133 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -756,6 +756,7 @@ export type TreeCommand = Command & { originalId?: string }; export interface ITreeItemCheckboxState { isChecked: boolean; tooltip?: string; + accessibilityInformation?: IAccessibilityInformation; } export interface ITreeItem { diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityContribution.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityContribution.ts index f4f914b84a5..5ea4c0c3168 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityContribution.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityContribution.ts @@ -7,25 +7,38 @@ import { localize } from 'vs/nls'; import { Extensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; +export const enum AccessibilityVerbositySettingId { + Terminal = 'accessibility.verbosity.terminal', + DiffEditor = 'accessibility.verbosity.diff-editor', + Chat = 'accessibility.verbosity.chat', + InteractiveEditor = 'accessibility.verbosity.interactiveEditor' +} + const configuration: IConfigurationNode = { id: 'accessibility', title: localize('accessibilityConfigurationTitle', "Accessibility"), type: 'object', properties: { - 'accessibility.verbosity.terminal': { + [AccessibilityVerbositySettingId.Terminal]: { description: localize('verbosity.terminal.description', 'Provide information about how to access the terminal accessibility help menu when the terminal is focused'), type: 'boolean', default: true, tags: ['accessibility'] }, - 'accessibility.verbosity.diff-editor': { + [AccessibilityVerbositySettingId.DiffEditor]: { description: localize('verbosity.diff-editor.description', 'Provide information about how to navigate changes in the diff editor when it is focused'), type: 'boolean', default: true, tags: ['accessibility'] }, - 'accessibility.verbosity.chatInput': { - description: localize('verbosity.chatInput.description', 'Provide information about how to access the interactive session accessibility help menu when the interactive input is focused'), + [AccessibilityVerbositySettingId.Chat]: { + description: localize('verbosity.chat.description', 'Provide information about how to access the chat help menu when the chat input is focused'), + type: 'boolean', + default: true, + tags: ['accessibility'] + }, + [AccessibilityVerbositySettingId.InteractiveEditor]: { + description: localize('verbosity.interactiveEditor.description', 'Provide information about how to access the interactive editor accessibility help menu when the interactive editor input is focused'), type: 'boolean', default: true, tags: ['accessibility'] diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index a22e8f156ad..781bd9bc98f 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -6,12 +6,43 @@ import { localize } from 'vs/nls'; import { format } from 'vs/base/common/strings'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { addStandardDisposableListener } from 'vs/base/browser/dom'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { EditMode } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor'; -export function getAccessibilityHelpText(keybindingService: IKeybindingService): string { +export function getAccessibilityHelpText(accessor: ServicesAccessor, type: 'chat' | 'editor', currentInput?: string): string { + const keybindingService = accessor.get(IKeybindingService); + const configurationService = accessor.get(IConfigurationService); const content = []; - content.push(localize('interactiveSession.helpMenuExit', "Exit this menu and return to the interactive editor input via the Escape key.")); - content.push(descriptionForCommand('chat.action.focus', localize('workbench.action.chat.focus', 'The Focus Chat command ({0}) focuses the chat request/response list, which can be navigated with UpArrow/DownArrow.',), localize('workbench.action.chat.focusNoKb', 'The Focus Interactive Session command focuses the chat request/response list, which can be navigated with UpArrow/DownArrow and is currently not triggerable by a keybinding.'), keybindingService)); - content.push(descriptionForCommand('workbench.action.chat.focusInput', localize('workbench.action.chat.focusInput', 'The Focus Chat Input command ({0}) focuses the input box for chat requests.'), localize('workbench.action.interactiveSession.focusInputNoKb', 'Focus Interactive Session Input command focuses the input box for chat requests and is currently not triggerable by a keybinding.'), keybindingService)); + content.push(localize('interactiveSession.helpMenuExit', "Exit this menu and return to the input via the Escape key.")); + if (type === 'chat') { + content.push(descriptionForCommand('chat.action.focus', localize('workbench.action.chat.focus', 'The Focus Chat command ({0}) focuses the chat request/response list, which can be navigated with UpArrow/DownArrow.',), localize('workbench.action.chat.focusNoKb', 'The Focus Chat List command focuses the chat request/response list, which can be navigated with UpArrow/DownArrow and is currently not triggerable by a keybinding.'), keybindingService)); + content.push(descriptionForCommand('workbench.action.chat.focusInput', localize('workbench.action.chat.focusInput', 'The Focus Chat Input command ({0}) focuses the input box for chat requests.'), localize('workbench.action.interactiveSession.focusInputNoKb', 'Focus Chat Input command focuses the input box for chat requests and is currently not triggerable by a keybinding.'), keybindingService)); + } else { + content.push(localize('interactiveSession.makeRequest', "Tab once to reach the make request button, which will re-run the request.")); + const regex = /^(\/fix|\/explain)/; + const match = currentInput?.match(regex); + const command = match && match.length ? match[0].substring(1) : undefined; + if (command === 'fix') { + const editMode = configurationService.getValue('interactiveEditor.editMode'); + if (editMode === EditMode.Preview) { + const keybinding = keybindingService.lookupKeybinding('editor.action.diffReview.next')?.getAriaLabel(); + content.push(keybinding ? localize('interactiveSession.diff', "Tab again to enter the Diff editor with the changes and enter review mode with ({0}). Use Up/DownArrow to navigate lines with the proposed changes.", keybinding) : localize('interactiveSession.diffNoKb', "Tab again to enter the Diff editor with the changes and enter review mode with the Go to Next Difference Command. Use Up/DownArrow to navigate lines with the proposed changes.")); + content.push(localize('interactiveSession.acceptReject', "Tab again to reach the action bar, which can be navigated with Left/RightArrow.")); + } + } else if (command === 'explain') { + content.push(localize('interactiveSession.explain', "/explain commands will be run in the chat view.")); + content.push(localize('interactiveSession.chatViewFocus', "To focus the chat view, run the GitHub Copilot: Focus on GitHub Copilot View command, which will focus the input box.")); + } else { + content.push(localize('interactiveSession.toolbar', "Tab again to reach the action bar, if any, which can be navigated with Left/RightArrow.")); + content.push(localize('interactiveSession.toolbarButtons', "Tab again to focus the response.")); + } + } return content.join('\n'); } @@ -22,3 +53,40 @@ function descriptionForCommand(commandId: string, msg: string, noKbMsg: string, } return format(noKbMsg, commandId); } + +export async function runAccessibilityHelpAction(accessor: ServicesAccessor, editor: ICodeEditor, type: 'chat' | 'editor'): Promise { + const widgetService = accessor.get(IChatWidgetService); + const inputEditor: ICodeEditor | undefined = type === 'chat' ? widgetService.lastFocusedWidget?.inputEditor : editor; + const editorUri = editor.getModel()?.uri; + + if (!inputEditor || !editorUri) { + return; + } + const domNode = withNullAsUndefined(inputEditor.getDomNode()); + if (!domNode) { + return; + } + + const cachedInput = inputEditor.getValue(); + const cachedPosition = inputEditor.getPosition(); + inputEditor.getSupportedActions(); + const helpText = getAccessibilityHelpText(accessor, type, type === 'editor' ? cachedInput : undefined); + inputEditor.setValue(helpText); + inputEditor.updateOptions({ readOnly: true }); + inputEditor.focus(); + const disposable = addStandardDisposableListener(domNode, 'keydown', e => { + if (!inputEditor) { + return; + } + if (e.keyCode === KeyCode.Escape && inputEditor.getValue() === helpText) { + inputEditor.updateOptions({ readOnly: false }); + inputEditor.setValue(cachedInput); + if (cachedPosition) { + inputEditor.setPosition(cachedPosition); + } + inputEditor.focus(); + disposable.dispose(); + e.stopPropagation(); + } + }); +} diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index d82e7f3662c..f8551107310 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -3,28 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { addStandardDisposableListener } from 'vs/base/browser/dom'; import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { withNullAsUndefined } from 'vs/base/common/types'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions'; +import { EditorAction, EditorAction2, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { localize } from 'vs/nls'; import { Action2, IAction2Options, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { ActiveEditorContext } from 'vs/workbench/common/contextkeys'; -import { getAccessibilityHelpText } from 'vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp'; +import { runAccessibilityHelpAction } from 'vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp'; import { clearChatEditor, clearChatSession } from 'vs/workbench/contrib/chat/browser/actions/chatClear'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; -import { CONTEXT_IN_INTERACTIVE_INPUT, CONTEXT_IN_INTERACTIVE_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatDetail, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -38,7 +35,7 @@ export function registerChatActions() { id: 'chat.action.acceptInput', label: localize({ key: 'actions.chat.acceptInput', comment: ['Apply input from the chat input box'] }, "Accept Chat Input"), alias: 'Accept Chat Input', - precondition: CONTEXT_IN_INTERACTIVE_INPUT, + precondition: CONTEXT_IN_CHAT_INPUT, kbOpts: { kbExpr: EditorContextKeys.textInputFocus, primary: KeyCode.Enter, @@ -104,22 +101,22 @@ export function registerChatActions() { } }); - registerEditorAction(class FocusChatAction extends EditorAction { + registerAction2(class FocusChatAction extends EditorAction2 { constructor() { super({ id: 'chat.action.focus', - label: localize('actions.interactiveSession.focus', "Focus Interactive Session"), - alias: 'Focus Interactive Session', - precondition: CONTEXT_IN_INTERACTIVE_INPUT, - kbOpts: { - kbExpr: EditorContextKeys.textInputFocus, + title: { value: localize('actions.interactiveSession.focus', "Focus Chat List"), original: 'Focus Chat List' }, + precondition: CONTEXT_IN_CHAT_INPUT, + category: CHAT_CATEGORY, + keybinding: { + when: EditorContextKeys.textInputFocus, primary: KeyMod.CtrlCmd | KeyCode.UpArrow, weight: KeybindingWeight.EditorContrib } }); } - run(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise { + runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise { const editorUri = editor.getModel()?.uri; if (editorUri) { const widgetService = accessor.get(IChatWidgetService); @@ -134,49 +131,15 @@ export function registerChatActions() { id: 'chat.action.accessibilityHelp', label: localize('chat.action.accessibiltyHelp', "Chat View Accessibility Help"), alias: 'Chat View Accessibility Help', - precondition: CONTEXT_IN_INTERACTIVE_INPUT, + precondition: CONTEXT_IN_CHAT_INPUT, kbOpts: { primary: KeyMod.Alt | KeyCode.F1, weight: KeybindingWeight.EditorContrib + 10 } }); } - async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { - const widgetService = accessor.get(IChatWidgetService); - const keybindingService = accessor.get(IKeybindingService); - const inputEditor = widgetService.lastFocusedWidget?.inputEditor; - const editorUri = editor.getModel()?.uri; - - if (!inputEditor || !editorUri) { - return; - } - - const widget = widgetService.getWidgetByInputUri(editorUri); - const domNode = withNullAsUndefined(inputEditor.getDomNode()); - - if (!domNode || !widget) { - return; - } - - const cachedInput = inputEditor.getValue(); - const cachedPosition = inputEditor.getPosition(); - - const helpText = getAccessibilityHelpText(keybindingService); - inputEditor.setValue(helpText); - inputEditor.updateOptions({ readOnly: true }); - inputEditor.focus(); - const disposable = addStandardDisposableListener(domNode, 'keydown', e => { - if (e.keyCode === KeyCode.Escape && inputEditor.getValue() === helpText) { - inputEditor.updateOptions({ readOnly: false }); - inputEditor.setValue(cachedInput); - if (cachedPosition) { - inputEditor.setPosition(cachedPosition); - } - widget.focusInput(); - disposable.dispose(); - } - }); + runAccessibilityHelpAction(accessor, editor, 'chat'); } }); @@ -185,14 +148,14 @@ export function registerChatActions() { super({ id: 'workbench.action.chat.focusInput', title: { - value: localize('interactiveSession.focusInput.label', "Focus Input"), - original: 'Focus Input' + value: localize('interactiveSession.focusInput.label', "Focus Chat Input"), + original: 'Focus Chat Input' }, f1: false, keybinding: { primary: KeyMod.CtrlCmd | KeyCode.DownArrow, weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(CONTEXT_IN_INTERACTIVE_SESSION, ContextKeyExpr.not(EditorContextKeys.focus.key)) + when: ContextKeyExpr.and(CONTEXT_IN_CHAT_SESSION, ContextKeyExpr.not(EditorContextKeys.focus.key)) } }); } @@ -216,7 +179,7 @@ export function registerChatActions() { keybinding: { weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.WinCtrl | KeyCode.KeyL, - when: CONTEXT_IN_INTERACTIVE_SESSION, + when: CONTEXT_IN_CHAT_SESSION, mac: { primary: KeyMod.WinCtrl | KeyCode.KeyL, secondary: [KeyMod.CtrlCmd | KeyCode.KeyK] diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 7faf2dcf42f..b2745377f45 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -21,9 +21,9 @@ import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { CONTEXT_IN_INTERACTIVE_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatCopyAction, IChatService, IChatUserActionEvent, InteractiveSessionCopyKind } from 'vs/workbench/contrib/chat/common/chatService'; -import { IInteractiveResponseViewModel, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { IChatResponseViewModel, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { insertCell } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellKind, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -35,7 +35,7 @@ export interface IChatCodeBlockActionContext { code: string; languageId: string; codeBlockIndex: number; - element: IInteractiveResponseViewModel; + element: IChatResponseViewModel; } export function isCodeBlockActionContext(thing: unknown): thing is IChatCodeBlockActionContext { @@ -374,7 +374,7 @@ export function registerChatCodeBlockActions() { const focusResponse = curCodeBlockInfo ? curCodeBlockInfo.element : - widget.viewModel?.getItems().reverse().find((item): item is IInteractiveResponseViewModel => isResponseVM(item)); + widget.viewModel?.getItems().reverse().find((item): item is IChatResponseViewModel => isResponseVM(item)); if (!focusResponse) { return; } @@ -398,7 +398,7 @@ export function registerChatCodeBlockActions() { keybinding: { primary: KeyCode.F9, weight: KeybindingWeight.WorkbenchContrib, - when: CONTEXT_IN_INTERACTIVE_SESSION, + when: CONTEXT_IN_CHAT_SESSION, }, f1: true, category: CHAT_CATEGORY, @@ -421,7 +421,7 @@ export function registerChatCodeBlockActions() { keybinding: { primary: KeyMod.Shift | KeyCode.F9, weight: KeybindingWeight.WorkbenchContrib, - when: CONTEXT_IN_INTERACTIVE_SESSION, + when: CONTEXT_IN_CHAT_SESSION, }, f1: true, category: CHAT_CATEGORY, diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts index 630ebea22f8..41624ff1316 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCopyActions.ts @@ -9,7 +9,7 @@ import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/act import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { IInteractiveRequestViewModel, IInteractiveResponseViewModel, isRequestVM, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; export function registerChatCopyActions() { registerAction2(class CopyAllAction extends Action2 { @@ -30,12 +30,12 @@ export function registerChatCopyActions() { run(accessor: ServicesAccessor, ...args: any[]) { const clipboardService = accessor.get(IClipboardService); - const interactiveWidgetService = accessor.get(IChatWidgetService); - const widget = interactiveWidgetService.lastFocusedWidget; + const chatWidgetService = accessor.get(IChatWidgetService); + const widget = chatWidgetService.lastFocusedWidget; if (widget) { const viewModel = widget.viewModel; const sessionAsText = viewModel?.getItems() - .filter((item): item is (IInteractiveRequestViewModel | IInteractiveResponseViewModel) => isRequestVM(item) || isResponseVM(item)) + .filter((item): item is (IChatRequestViewModel | IChatResponseViewModel) => isRequestVM(item) || isResponseVM(item)) .map(stringifyItem) .join('\n\n'); if (sessionAsText) { @@ -74,7 +74,7 @@ export function registerChatCopyActions() { }); } -function stringifyItem(item: IInteractiveRequestViewModel | IInteractiveResponseViewModel): string { +function stringifyItem(item: IChatRequestViewModel | IChatResponseViewModel): string { return isRequestVM(item) ? `${item.username}: ${item.messageText}` : `${item.username}: ${item.response.value}`; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index f65fd8725eb..4ad2f46951b 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -9,7 +9,7 @@ import { localize } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatWidget } from 'vs/workbench/contrib/chat/browser/chat'; -import { CONTEXT_INTERACTIVE_INPUT_HAS_TEXT, CONTEXT_INTERACTIVE_REQUEST_IN_PROGRESS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_CHAT_REQUEST_IN_PROGRESS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; export interface IChatExecuteActionContext { @@ -32,10 +32,10 @@ export function registerChatExecuteActions() { f1: false, category: CHAT_CATEGORY, icon: Codicon.send, - precondition: CONTEXT_INTERACTIVE_INPUT_HAS_TEXT, + precondition: CONTEXT_CHAT_INPUT_HAS_TEXT, menu: { id: MenuId.ChatExecute, - when: CONTEXT_INTERACTIVE_REQUEST_IN_PROGRESS.negate(), + when: CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), group: 'navigation', } }); @@ -64,7 +64,7 @@ export function registerChatExecuteActions() { icon: Codicon.debugStop, menu: { id: MenuId.ChatExecute, - when: CONTEXT_INTERACTIVE_REQUEST_IN_PROGRESS, + when: CONTEXT_CHAT_REQUEST_IN_PROGRESS, group: 'navigation', } }); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts index 42e74478ca8..f17a1535299 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts @@ -17,7 +17,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputService, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { asCssVariable, editorBackground, foreground, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry'; -import { InteractiveListItemRenderer } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; +import { ChatListItemRenderer } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatReplyFollowup, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; @@ -184,11 +184,11 @@ class InteractiveQuickPickSession extends Disposable { this._listDisposable = new DisposableStore(); const options = this._listDisposable.add(this._instantiationService.createInstance(ChatEditorOptions, 'quickpick-interactive', foreground, editorBackground, editorBackground)); const list = this._listDisposable.add(this._instantiationService.createInstance( - InteractiveListItemRenderer, + ChatListItemRenderer, options, { getListLength: () => { - return this._viewModel.getItems().length; + return 1; }, getSlashCommands() { return []; diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 4dca0d6143e..e798ba93bfd 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -45,28 +45,28 @@ configurationRegistry.registerConfiguration({ properties: { 'chat.editor.fontSize': { type: 'number', - description: nls.localize('interactiveSession.editor.fontSize', "Controls the font size in pixels in Interactive Sessions."), + description: nls.localize('interactiveSession.editor.fontSize', "Controls the font size in pixels in chat codeblocks."), default: isMacintosh ? 12 : 14, }, 'chat.editor.fontFamily': { type: 'string', - description: nls.localize('interactiveSession.editor.fontFamily', "Controls the font family in Interactive Sessions."), + description: nls.localize('interactiveSession.editor.fontFamily', "Controls the font family in chat codeblocks."), default: 'default' }, 'chat.editor.fontWeight': { type: 'string', - description: nls.localize('interactiveSession.editor.fontWeight', "Controls the font weight in Interactive Sessions."), + description: nls.localize('interactiveSession.editor.fontWeight', "Controls the font weight in chat codeblocks."), default: 'default' }, 'chat.editor.wordWrap': { type: 'string', - description: nls.localize('interactiveSession.editor.wordWrap', "Controls whether lines should wrap in Interactive Sessions."), + description: nls.localize('interactiveSession.editor.wordWrap', "Controls whether lines should wrap in chat codeblocks."), default: 'off', enum: ['on', 'off'] }, 'chat.editor.lineHeight': { type: 'number', - description: nls.localize('interactiveSession.editor.lineHeight', "Controls the line height in pixels in Interactive Sessions. Use 0 to compute the line height from the font size."), + description: nls.localize('interactiveSession.editor.lineHeight', "Controls the line height in pixels in chat codeblocks. Use 0 to compute the line height from the font size."), default: 0 }, 'chat.experimental.quickQuestion.enable': { @@ -98,7 +98,7 @@ class ChatResolverContribution extends Disposable { super(); this._register(editorResolverService.registerEditor( - `${Schemas.vscodeInteractiveSesssion}:**/**`, + `${Schemas.vscodeChatSesssion}:**/**`, { id: ChatEditorInput.EditorID, label: nls.localize('chat', "Chat"), @@ -106,7 +106,7 @@ class ChatResolverContribution extends Disposable { }, { singlePerResource: true, - canSupportResource: resource => resource.scheme === Schemas.vscodeInteractiveSesssion + canSupportResource: resource => resource.scheme === Schemas.vscodeChatSesssion }, { createEditorInput: ({ resource, options }) => { diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 0156d118eba..d5f4a134ed5 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -5,7 +5,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService'; -import { IInteractiveResponseViewModel, IChatViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { IChatResponseViewModel, IChatViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -31,7 +31,7 @@ export interface IChatWidgetService { export interface IChatCodeBlockInfo { codeBlockIndex: number; - element: IInteractiveResponseViewModel; + element: IChatResponseViewModel; focus(): void; } @@ -49,7 +49,7 @@ export interface IChatWidget { focusInput(): void; getSlashCommands(): Promise; getCodeBlockInfoForEditor(uri: URI): IChatCodeBlockInfo | undefined; - getCodeBlockInfosForResponse(response: IInteractiveResponseViewModel): IChatCodeBlockInfo[]; + getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[]; } export interface IChatViewPane { diff --git a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts index 7e9522836a2..a9f2f20cc14 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts @@ -17,7 +17,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, ViewContainerLocation, Extensions as ViewExtensions } from 'vs/workbench/common/views'; import { getClearAction, getHistoryAction, getOpenChatEditorAction } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; -import { IChatViewOptions, INTERACTIVE_SIDEBAR_PANEL_ID, ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; +import { IChatViewOptions, CHAT_SIDEBAR_PANEL_ID, ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; import { IChatContributionService, IChatProviderContribution, IRawChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService'; import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; @@ -113,7 +113,7 @@ export class ChatContributionService implements IChatContributionService { private registerChatProvider(extension: Readonly, providerDescriptor: IRawChatProviderContribution): IDisposable { // Register View Container - const viewContainerId = INTERACTIVE_SIDEBAR_PANEL_ID + '.' + providerDescriptor.id; + const viewContainerId = CHAT_SIDEBAR_PANEL_ID + '.' + providerDescriptor.id; const viewContainer: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ id: viewContainerId, title: providerDescriptor.label, @@ -140,7 +140,7 @@ export class ChatContributionService implements IChatContributionService { const historyAction = registerAction2(getHistoryAction(viewId, providerDescriptor.id)); const clearAction = registerAction2(getClearAction(viewId, providerDescriptor.id)); - // "Open Interactive Session Editor" Action + // "Open Chat Editor" Action const openEditor = registerAction2(getOpenChatEditorAction(providerDescriptor.id, providerDescriptor.label, providerDescriptor.when)); return { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditor.ts b/src/vs/workbench/contrib/chat/browser/chatEditor.ts index c808cb3bde0..c98b0b826fd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditor.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./media/chatEditor'; import * as dom from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IContextKeyService, IScopedContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -88,7 +89,7 @@ export class ChatEditor extends EditorPane { const editorModel = await input.resolve(); if (!editorModel) { - throw new Error(`Failed to get model for interactive session editor. id: ${input.sessionId}`); + throw new Error(`Failed to get model for chat editor. id: ${input.sessionId}`); } if (!this.widget) { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts b/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts index 6c1ca45f5c0..42ed0215e03 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts @@ -18,14 +18,16 @@ import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; export class ChatEditorInput extends EditorInput { - static readonly TypeID: string = 'workbench.input.interactiveSession'; - static readonly EditorID: string = 'workbench.editor.interactiveSession'; + static readonly TypeID: string = 'workbench.input.chatSession'; + static readonly EditorID: string = 'workbench.editor.chatSession'; static count = 0; private readonly inputCount: number; public sessionId: string | undefined; public providerId: string | undefined; + private model: IChatModel | undefined; + static getNewEditorUri(): URI { const handle = Math.floor(Math.random() * 1e9); return ChatUri.generate(handle); @@ -40,7 +42,7 @@ export class ChatEditorInput extends EditorInput { const parsed = ChatUri.parse(resource); if (typeof parsed?.handle !== 'number') { - throw new Error('Invalid interactive session URI'); + throw new Error('Invalid chat URI'); } this.sessionId = 'sessionId' in options.target ? options.target.sessionId : undefined; @@ -65,26 +67,31 @@ export class ChatEditorInput extends EditorInput { } override getName(): string { - return nls.localize('chatEditorName', "Chat") + (this.inputCount > 0 ? ` ${this.inputCount + 1}` : ''); + return this.model?.title || nls.localize('chatEditorName', "Chat") + (this.inputCount > 0 ? ` ${this.inputCount + 1}` : ''); + } + + override getLabelExtraClasses(): string[] { + return ['chat-editor-label']; } override async resolve(): Promise { - let model: IChatModel | undefined; if (typeof this.sessionId === 'string') { - model = this.chatService.getOrRestoreSession(this.sessionId); + this.model = this.chatService.getOrRestoreSession(this.sessionId); } else if (typeof this.providerId === 'string') { - model = this.chatService.startSession(this.providerId, CancellationToken.None); + this.model = this.chatService.startSession(this.providerId, CancellationToken.None); } else if ('data' in this.options.target) { - model = this.chatService.loadSessionFromContent(this.options.target.data); + this.model = this.chatService.loadSessionFromContent(this.options.target.data); } - if (!model) { + if (!this.model) { return null; } - this.sessionId = model.sessionId; - await model.waitForInitialization(); - return this._register(new ChatEditorModel(model)); + this.sessionId = this.model.sessionId; + await this.model.waitForInitialization(); + this._register(this.model.onDidChange(() => this._onDidChangeLabel.fire())); + + return this._register(new ChatEditorModel(this.model)); } override dispose(): void { @@ -126,11 +133,11 @@ export class ChatEditorModel extends Disposable implements IEditorModel { export namespace ChatUri { - export const scheme = Schemas.vscodeInteractiveSesssion; + export const scheme = Schemas.vscodeChatSesssion; export function generate(handle: number): URI { - return URI.from({ scheme, path: `interactiveSession-${handle}` }); + return URI.from({ scheme, path: `chat-${handle}` }); } export function parse(resource: URI): { handle: number } | undefined { @@ -138,7 +145,7 @@ export namespace ChatUri { return undefined; } - const match = resource.path.match(/interactiveSession-(\d+)/); + const match = resource.path.match(/chat-(\d+)/); const handleStr = match?.[1]; if (typeof handleStr !== 'string') { return undefined; diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 21a39b7e51c..d4ac5f52d13 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -27,16 +27,17 @@ import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions } from 'vs/wor import { IChatExecuteActionContext } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; import { IChatWidget } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; -import { CONTEXT_INTERACTIVE_INPUT_HAS_TEXT, CONTEXT_IN_INTERACTIVE_INPUT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_IN_CHAT_INPUT } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatReplyFollowup } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; const $ = dom.$; const INPUT_EDITOR_MAX_HEIGHT = 250; export class ChatInputPart extends Disposable implements IHistoryNavigationWidget { - public static readonly INPUT_SCHEME = 'interactiveSessionInput'; + public static readonly INPUT_SCHEME = 'chatSessionInput'; private static _counter = 0; private _onDidChangeHeight = this._register(new Emitter()); @@ -81,23 +82,23 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge ) { super(); - this.inputEditorHasText = CONTEXT_INTERACTIVE_INPUT_HAS_TEXT.bindTo(contextKeyService); + this.inputEditorHasText = CONTEXT_CHAT_INPUT_HAS_TEXT.bindTo(contextKeyService); this.history = new HistoryNavigator([], 5); this._register(this.historyService.onDidClearHistory(() => this.history.clear())); this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('accessibility.verbosity.chatInput')) { + if (e.affectsConfiguration(AccessibilityVerbositySettingId.Chat)) { this.inputEditor.updateOptions({ ariaLabel: this._getAriaLabel() }); } })); } private _getAriaLabel(): string { - const verbose = this.configurationService.getValue('accessibility.verbosity.chatInput'); + const verbose = this.configurationService.getValue(AccessibilityVerbositySettingId.Chat); if (verbose) { const kbLabel = this.keybindingService.lookupKeybinding('chat.action.accessibilityHelp')?.getLabel(); - return kbLabel ? localize('actions.chat.accessibiltyHelp', "Chat Input, Type code here and press enter to run. Use {0} for Chat Accessibility Help.", kbLabel) : localize('interactiveSessionInput.accessibilityHelpNoKb', "Chat Input, Type code here and press Enter to run. Use the Chat Accessibility Help command for more information."); + return kbLabel ? localize('actions.chat.accessibiltyHelp', "Chat Input, Type code here and press enter to run. Use {0} for Chat Accessibility Help.", kbLabel) : localize('chatInput.accessibilityHelpNoKb', "Chat Input, Type code here and press Enter to run. Use the Chat Accessibility Help command for more information."); } - return localize('interactiveSessionInput', "Chat Input"); + return localize('chatInput', "Chat Input"); } setState(providerId: string, inputValue: string): void { @@ -158,7 +159,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const inputContainer = dom.append(this.container, $('.interactive-input-and-toolbar')); const inputScopedContextKeyService = this._register(this.contextKeyService.createScoped(inputContainer)); - CONTEXT_IN_INTERACTIVE_INPUT.bindTo(inputScopedContextKeyService).set(true); + CONTEXT_IN_CHAT_INPUT.bindTo(inputScopedContextKeyService).set(true); const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, inputScopedContextKeyService])); const { historyNavigationBackwardsEnablement, historyNavigationForwardsEnablement } = this._register(registerAndCreateHistoryNavigationContext(inputScopedContextKeyService, this)); diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index e5ae3a34a66..8f833b4c57c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -56,14 +56,14 @@ import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; import { CONTEXT_RESPONSE_HAS_PROVIDER_ID, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatReplyFollowup, IChatService, ISlashCommand, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; -import { IInteractiveRequestViewModel, IInteractiveResponseViewModel, IInteractiveWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { IChatRequestViewModel, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IWordCountResult, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; const $ = dom.$; -export type InteractiveTreeItem = IInteractiveRequestViewModel | IInteractiveResponseViewModel | IInteractiveWelcomeMessageViewModel; +export type ChatTreeItem = IChatRequestViewModel | IChatResponseViewModel | IChatWelcomeMessageViewModel; -interface IInteractiveListItemTemplate { +interface IChatListItemTemplate { rowContainer: HTMLElement; titleToolbar: MenuWorkbenchToolBar; avatar: HTMLElement; @@ -75,7 +75,7 @@ interface IInteractiveListItemTemplate { } interface IItemHeightChangeParams { - element: InteractiveTreeItem; + element: ChatTreeItem; height: number; } @@ -86,7 +86,7 @@ export interface IChatRendererDelegate { getSlashCommands(): ISlashCommand[]; } -export class InteractiveListItemRenderer extends Disposable implements ITreeRenderer { +export class ChatListItemRenderer extends Disposable implements ITreeRenderer { static readonly cursorCharacter = '\u258c'; static readonly ID = 'item'; @@ -122,14 +122,14 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend } get templateId(): string { - return InteractiveListItemRenderer.ID; + return ChatListItemRenderer.ID; } private traceLayout(method: string, message: string) { if (forceVerboseLayoutTracing) { - this.logService.info(`InteractiveListItemRenderer#${method}: ${message}`); + this.logService.info(`ChatListItemRenderer#${method}: ${message}`); } else { - this.logService.trace(`InteractiveListItemRenderer#${method}: ${message}`); + this.logService.trace(`ChatListItemRenderer#${method}: ${message}`); } } @@ -137,7 +137,7 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend return !this.configService.getValue('interactive.experimental.disableProgressiveRendering'); } - private getProgressiveRenderRate(element: IInteractiveResponseViewModel): number { + private getProgressiveRenderRate(element: IChatResponseViewModel): number { const configuredRate = this.configService.getValue('interactive.experimental.progressiveRenderingRate'); if (typeof configuredRate === 'number') { return configuredRate; @@ -158,7 +158,7 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend return 8; } - getCodeBlockInfosForResponse(response: IInteractiveResponseViewModel): IChatCodeBlockInfo[] { + getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[] { const codeBlocks = this.codeBlocksByResponseId.get(response.id); return codeBlocks ?? []; } @@ -178,7 +178,7 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend }); } - renderTemplate(container: HTMLElement): IInteractiveListItemTemplate { + renderTemplate(container: HTMLElement): IChatListItemTemplate { const templateDisposables = new DisposableStore(); const rowContainer = dom.append(container, $('.interactive-item-container')); const header = dom.append(rowContainer, $('.header')); @@ -204,11 +204,11 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend })); - const template: IInteractiveListItemTemplate = { avatar, username, value, rowContainer, elementDisposables, titleToolbar, templateDisposables, contextKeyService }; + const template: IChatListItemTemplate = { avatar, username, value, rowContainer, elementDisposables, titleToolbar, templateDisposables, contextKeyService }; return template; } - renderElement(node: ITreeNode, index: number, templateData: IInteractiveListItemTemplate): void { + renderElement(node: ITreeNode, index: number, templateData: IChatListItemTemplate): void { const { element } = node; const kind = isRequestVM(element) ? 'request' : isResponseVM(element) ? 'response' : @@ -272,7 +272,7 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend } } - private basicRenderElement(markdownValue: string, element: InteractiveTreeItem, index: number, templateData: IInteractiveListItemTemplate) { + private basicRenderElement(markdownValue: string, element: ChatTreeItem, index: number, templateData: IChatListItemTemplate) { const fillInIncompleteTokens = isResponseVM(element) && (!element.isComplete || element.isCanceled || element.errorDetails?.responseIsFiltered || element.errorDetails?.responseIsIncomplete); const result = this.renderMarkdown(new MarkdownString(markdownValue), element, templateData.elementDisposables, templateData, fillInIncompleteTokens); dom.clearNode(templateData.value); @@ -304,7 +304,7 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend } } - private renderWelcomeMessage(element: IInteractiveWelcomeMessageViewModel, templateData: IInteractiveListItemTemplate) { + private renderWelcomeMessage(element: IChatWelcomeMessageViewModel, templateData: IChatListItemTemplate) { dom.clearNode(templateData.value); const slashCommands = this.delegate.getSlashCommands(); @@ -331,7 +331,7 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend /** * @returns true if progressive rendering should be considered complete- the element's data is fully rendered or the view is not visible */ - private doNextProgressiveRender(element: IInteractiveResponseViewModel, index: number, templateData: IInteractiveListItemTemplate, isInRenderElement: boolean, disposables: DisposableStore): boolean { + private doNextProgressiveRender(element: IChatResponseViewModel, index: number, templateData: IChatListItemTemplate, isInRenderElement: boolean, disposables: DisposableStore): boolean { if (!this._isVisible) { return true; } @@ -369,7 +369,7 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend // when the codeblock is the last thing in the response, and that happens often. const plusCursor = renderValue.value.match(/```\s*$/) ? renderValue.value : - renderValue.value + ` ${InteractiveListItemRenderer.cursorCharacter}`; + renderValue.value + ` ${ChatListItemRenderer.cursorCharacter}`; const result = this.renderMarkdown(new MarkdownString(plusCursor), element, disposables, templateData, true); // Doing the progressive render dom.clearNode(templateData.value); @@ -391,7 +391,7 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend return !!isFullyRendered; } - private renderMarkdown(markdown: IMarkdownString, element: InteractiveTreeItem, disposables: DisposableStore, templateData: IInteractiveListItemTemplate, fillInIncompleteTokens = false): IMarkdownRenderResult { + private renderMarkdown(markdown: IMarkdownString, element: ChatTreeItem, disposables: DisposableStore, templateData: IChatListItemTemplate, fillInIncompleteTokens = false): IMarkdownRenderResult { const disposablesList: IDisposable[] = []; let codeBlockIndex = 0; @@ -450,7 +450,7 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend return result; } - private renderCodeBlock(data: IInteractiveResultCodeBlockData, disposables: DisposableStore): IDisposableReference { + private renderCodeBlock(data: IChatResultCodeBlockData, disposables: DisposableStore): IDisposableReference { const ref = this._editorPool.get(); const editorInfo = ref.object; editorInfo.render(data, this._currentLayoutWidth); @@ -458,7 +458,7 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend return ref; } - private getWordsForProgressiveRender(element: IInteractiveResponseViewModel): IWordCountResult | undefined { + private getWordsForProgressiveRender(element: IChatResponseViewModel): IWordCountResult | undefined { const renderData = element.renderData ?? { renderedWordCount: 0, lastRenderTime: 0 }; const rate = this.getProgressiveRenderRate(element); const numWordsToRender = renderData.lastRenderTime === 0 ? @@ -474,16 +474,16 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend return getNWords(element.response.value, numWordsToRender); } - disposeElement(node: ITreeNode, index: number, templateData: IInteractiveListItemTemplate): void { + disposeElement(node: ITreeNode, index: number, templateData: IChatListItemTemplate): void { templateData.elementDisposables.clear(); } - disposeTemplate(templateData: IInteractiveListItemTemplate): void { + disposeTemplate(templateData: IChatListItemTemplate): void { templateData.templateDisposables.dispose(); } } -export class ChatListDelegate implements IListVirtualDelegate { +export class ChatListDelegate implements IListVirtualDelegate { constructor( @ILogService private readonly logService: ILogService ) { } @@ -496,29 +496,29 @@ export class ChatListDelegate implements IListVirtualDelegate { +export class ChatAccessibilityProvider implements IListAccessibilityProvider { getWidgetRole(): AriaRole { return 'list'; } - getRole(element: InteractiveTreeItem): AriaRole | undefined { + getRole(element: ChatTreeItem): AriaRole | undefined { return 'listitem'; } @@ -526,7 +526,7 @@ export class ChatAccessibilityProvider implements IListAccessibilityProvider; readonly element: HTMLElement; readonly textModel: ITextModel; layout(width: number): void; - render(data: IInteractiveResultCodeBlockData, width: number): void; + render(data: IChatResultCodeBlockData, width: number): void; focus(): void; dispose(): void; } const defaultCodeblockPadding = 10; -class CodeBlockPart extends Disposable implements IInteractiveResultCodeBlockPart { +class CodeBlockPart extends Disposable implements IChatResultCodeBlockPart { private readonly _onDidChangeContentHeight = this._register(new Emitter()); public readonly onDidChangeContentHeight = this._onDidChangeContentHeight.event; @@ -683,7 +683,7 @@ class CodeBlockPart extends Disposable implements IInteractiveResultCodeBlockPar this.updatePaddingForLayout(); } - render(data: IInteractiveResultCodeBlockData, width: number): void { + render(data: IChatResultCodeBlockData, width: number): void { this.contextKeyService.updateParent(data.parentContextKeyService); if (this.options.configuration.resultEditor.wordWrap === 'on') { @@ -725,9 +725,9 @@ class CodeBlockPart extends Disposable implements IInteractiveResultCodeBlockPar } let removedChars = 0; - if (currentText.endsWith(` ${InteractiveListItemRenderer.cursorCharacter}`)) { + if (currentText.endsWith(` ${ChatListItemRenderer.cursorCharacter}`)) { removedChars = 2; - } else if (currentText.endsWith(InteractiveListItemRenderer.cursorCharacter)) { + } else if (currentText.endsWith(ChatListItemRenderer.cursorCharacter)) { removedChars = 1; } @@ -757,9 +757,9 @@ interface IDisposableReference extends IDisposable { } class EditorPool extends Disposable { - private _pool: ResourcePool; + private _pool: ResourcePool; - public get inUse(): ReadonlySet { + public get inUse(): ReadonlySet { return this._pool.inUse; } @@ -773,11 +773,11 @@ class EditorPool extends Disposable { // TODO listen to changes on options } - private editorFactory(): IInteractiveResultCodeBlockPart { + private editorFactory(): IChatResultCodeBlockPart { return this.instantiationService.createInstance(CodeBlockPart, this.options); } - get(): IDisposableReference { + get(): IDisposableReference { const object = this._pool.get(); return { object, diff --git a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index d28651f82de..4bfa2aae538 100644 --- a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts @@ -34,9 +34,9 @@ interface IViewPaneState extends IViewState { sessionId?: string; } -export const INTERACTIVE_SIDEBAR_PANEL_ID = 'workbench.panel.interactiveSessionSidebar'; +export const CHAT_SIDEBAR_PANEL_ID = 'workbench.panel.chatSidebar'; export class ChatViewPane extends ViewPane implements IChatViewPane { - static ID = 'workbench.panel.interactiveSession.view'; + static ID = 'workbench.panel.chat.view'; private _widget!: ChatWidget; get widget(): ChatWidget { return this._widget; } @@ -46,7 +46,7 @@ export class ChatViewPane extends ViewPane implements IChatViewPane { private viewState: IViewPaneState; constructor( - private readonly interactiveSessionViewOptions: IChatViewOptions, + private readonly chatViewOptions: IChatViewOptions, options: IViewPaneOptions, @IKeybindingService keybindingService: IKeybindingService, @IContextMenuService contextMenuService: IContextMenuService, @@ -64,16 +64,16 @@ export class ChatViewPane extends ViewPane implements IChatViewPane { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); // View state for the ViewPane is currently global per-provider basically, but some other strictly per-model state will require a separate memento. - this.memento = new Memento('interactive-session-view-' + this.interactiveSessionViewOptions.providerId, this.storageService); + this.memento = new Memento('interactive-session-view-' + this.chatViewOptions.providerId, this.storageService); this.viewState = this.memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE) as IViewPaneState; } private updateModel(model?: IChatModel | undefined): void { this.modelDisposables.clear(); - model = model ?? this.chatService.startSession(this.interactiveSessionViewOptions.providerId, CancellationToken.None); + model = model ?? this.chatService.startSession(this.chatViewOptions.providerId, CancellationToken.None); if (!model) { - throw new Error('Could not start interactive session'); + throw new Error('Could not start chat session'); } this._widget.setModel(model, { ...this.viewState }); @@ -134,13 +134,15 @@ export class ChatViewPane extends ViewPane implements IChatViewPane { } override saveState(): void { - // Since input history is per-provider, this is handled by a separate service and not the memento here. - // TODO multiple chat views will overwrite each other - this._widget.saveState(); + if (this._widget) { + // Since input history is per-provider, this is handled by a separate service and not the memento here. + // TODO multiple chat views will overwrite each other + this._widget.saveState(); - const widgetViewState = this._widget.getViewState(); - this.viewState.inputValue = widgetViewState.inputValue; - this.memento.saveMemento(); + const widgetViewState = this._widget.getViewState(); + this.viewState.inputValue = widgetViewState.inputValue; + this.memento.saveMemento(); + } super.saveState(); } diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index e74309e561d..c851bff9e91 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { alert } from 'vs/base/browser/ui/aria/aria'; import * as dom from 'vs/base/browser/dom'; import { ITreeContextMenuEvent, ITreeElement } from 'vs/base/browser/ui/tree/tree'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -24,14 +25,14 @@ import { IViewsService } from 'vs/workbench/common/views'; import { clearChatSession } from 'vs/workbench/contrib/chat/browser/actions/chatClear'; import { IChatCodeBlockInfo, IChatWidget, IChatWidgetService, IChatWidgetViewContext } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; -import { IChatRendererDelegate, InteractiveListItemRenderer, ChatAccessibilityProvider, ChatListDelegate, InteractiveTreeItem } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; +import { IChatRendererDelegate, ChatListItemRenderer, ChatAccessibilityProvider, ChatListDelegate, ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane'; -import { CONTEXT_INTERACTIVE_REQUEST_IN_PROGRESS, CONTEXT_IN_INTERACTIVE_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatReplyFollowup, IChatService, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService'; -import { IInteractiveResponseViewModel, ChatViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; +import { IChatResponseViewModel, ChatViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; const $ = dom.$; @@ -60,8 +61,8 @@ export class ChatWidget extends Disposable implements IChatWidget { private _onDidChangeViewModel = this._register(new Emitter()); readonly onDidChangeViewModel = this._onDidChangeViewModel.event; - private tree!: WorkbenchObjectTree; - private renderer!: InteractiveListItemRenderer; + private tree!: WorkbenchObjectTree; + private renderer!: ChatListItemRenderer; private inputPart!: ChatInputPart; private editorOptions!: ChatEditorOptions; @@ -116,8 +117,8 @@ export class ChatWidget extends Disposable implements IChatWidget { @IContextMenuService private readonly contextMenuService: IContextMenuService, ) { super(); - CONTEXT_IN_INTERACTIVE_SESSION.bindTo(contextKeyService).set(true); - this.requestInProgress = CONTEXT_INTERACTIVE_REQUEST_IN_PROGRESS.bindTo(contextKeyService); + CONTEXT_IN_CHAT_SESSION.bindTo(contextKeyService).set(true); + this.requestInProgress = CONTEXT_CHAT_REQUEST_IN_PROGRESS.bindTo(contextKeyService); this._register((chatWidgetService as ChatWidgetService).register(this)); } @@ -163,7 +164,7 @@ export class ChatWidget extends Disposable implements IChatWidget { if (this.tree && this.visible) { const treeItems = (this.viewModel?.getItems() ?? []) .map(item => { - return >{ + return >{ element: item, collapsed: false, collapsible: false @@ -174,6 +175,8 @@ export class ChatWidget extends Disposable implements IChatWidget { diffIdentityProvider: { getId: (element) => { return ((isResponseVM(element) || isRequestVM(element)) ? element.dataId : element.id) + + // TODO? We can give the welcome message a proper VM or get rid of the rest of the VMs + ((isWelcomeVM(element) && !this.viewModel?.isInitialized) ? '_initializing' : '') + // Ensure re-rendering an element once slash commands are loaded, so the colorization can be applied. `${(isRequestVM(element) || isWelcomeVM(element)) && !!this.lastSlashCommands ? '_scLoaded' : ''}` + // If a response is in the process of progressive rendering, we need to ensure that it will @@ -247,24 +250,24 @@ export class ChatWidget extends Disposable implements IChatWidget { getListLength: () => this.tree.getNode(null).visibleChildrenCount, getSlashCommands: () => this.lastSlashCommands ?? [], }; - this.renderer = this._register(scopedInstantiationService.createInstance(InteractiveListItemRenderer, this.editorOptions, rendererDelegate)); + this.renderer = this._register(scopedInstantiationService.createInstance(ChatListItemRenderer, this.editorOptions, rendererDelegate)); this._register(this.renderer.onDidClickFollowup(item => { this.acceptInput(item); })); - this.tree = >scopedInstantiationService.createInstance( + this.tree = >scopedInstantiationService.createInstance( WorkbenchObjectTree, 'Chat', listContainer, delegate, [this.renderer], { - identityProvider: { getId: (e: InteractiveTreeItem) => e.id }, + identityProvider: { getId: (e: ChatTreeItem) => e.id }, horizontalScrolling: false, supportDynamicHeights: true, hideTwistiesOfChildlessElements: true, accessibilityProvider: new ChatAccessibilityProvider(), - keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: InteractiveTreeItem) => isRequestVM(e) ? e.message : isResponseVM(e) ? e.response.value : '' }, // TODO + keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: ChatTreeItem) => isRequestVM(e) ? e.message : isResponseVM(e) ? e.response.value : '' }, // TODO setRowLineHeight: false, overrideStyles: { listFocusBackground: this.styles.listBackground, @@ -295,7 +298,7 @@ export class ChatWidget extends Disposable implements IChatWidget { })); } - private onContextMenu(e: ITreeContextMenuEvent): void { + private onContextMenu(e: ITreeContextMenuEvent): void { e.browserEvent.preventDefault(); e.browserEvent.stopPropagation(); @@ -379,11 +382,18 @@ export class ChatWidget extends Disposable implements IChatWidget { if (result) { revealLastElement(this.tree); this.inputPart.acceptInput(query); + result.responseCompletePromise.then(() => { + const responses = this.viewModel?.getItems().filter(isResponseVM); + const lastResponse = responses?.[responses.length - 1]; + if (lastResponse) { + alert(lastResponse.response.value); + } + }); } } } - getCodeBlockInfosForResponse(response: IInteractiveResponseViewModel): IChatCodeBlockInfo[] { + getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[] { return this.renderer.getCodeBlockInfosForResponse(response); } @@ -450,7 +460,7 @@ export class ChatWidgetService implements IChatWidgetService { constructor( @IViewsService private readonly viewsService: IViewsService, - @IChatContributionService private readonly interactiveSessionContributionService: IChatContributionService, + @IChatContributionService private readonly chatContributionService: IChatContributionService, ) { } getWidgetByInputUri(uri: URI): ChatWidget | undefined { @@ -458,7 +468,7 @@ export class ChatWidgetService implements IChatWidgetService { } async revealViewForProvider(providerId: string): Promise { - const viewId = this.interactiveSessionContributionService.getViewIdForProvider(providerId); + const viewId = this.chatContributionService.getViewIdForProvider(providerId); const view = await this.viewsService.openView(viewId); return view?.widget; diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index 16b5aef2eb7..b7534541f15 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -22,9 +22,9 @@ import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; -const decorationDescription = 'interactive session'; -const slashCommandPlaceholderDecorationType = 'interactive-session-detail'; -const slashCommandTextDecorationType = 'interactive-session-text'; +const decorationDescription = 'chat'; +const slashCommandPlaceholderDecorationType = 'chat-session-detail'; +const slashCommandTextDecorationType = 'chat-session-text'; class InputEditorDecorations extends Disposable { diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 2e4f5046f04..3feecf368cf 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -103,7 +103,7 @@ .interactive-item-container .value table, .interactive-item-container .value table td, .interactive-item-container .value table th { - border: 1px solid var(--vscode-interactive-requestBorder); + border: 1px solid var(--vscode-chat-requestBorder); border-collapse: collapse; padding: 4px 6px; } @@ -122,9 +122,9 @@ } .interactive-request { - background-color: var(--vscode-interactive-requestBackground); - border-bottom: 1px solid var(--vscode-interactive-requestBorder); - border-top: 1px solid var(--vscode-interactive-requestBorder); + background-color: var(--vscode-chat-requestBackground); + border-bottom: 1px solid var(--vscode-chat-requestBorder); + border-top: 1px solid var(--vscode-chat-requestBorder); } .interactive-item-container .value { @@ -222,7 +222,7 @@ right: 10px; height: 26px; background-color: var(--vscode-interactive-result-editor-background-color, var(--vscode-editor-background)); - border: 1px solid var(--vscode-interactive-requestBorder); + border: 1px solid var(--vscode-chat-requestBorder); z-index: 100; } @@ -279,7 +279,7 @@ padding: 12px 0px; display: flex; flex-direction: column; - border-top: solid 1px var(--vscode-interactive-requestBorder); + border-top: solid 1px var(--vscode-chat-requestBorder); } .interactive-session-followups { diff --git a/src/vs/workbench/contrib/chat/browser/media/chatEditor.css b/src/vs/workbench/contrib/chat/browser/media/chatEditor.css new file mode 100644 index 00000000000..e4e59010a20 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/media/chatEditor.css @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.show-file-icons .chat-editor-label.file-icon::before { + content: '\EAC7'; + font-family: 'codicon'; + font-size: 16px; +} diff --git a/src/vs/workbench/contrib/chat/common/chatColors.ts b/src/vs/workbench/contrib/chat/common/chatColors.ts index f0067e1662b..05f680b9387 100644 --- a/src/vs/workbench/contrib/chat/common/chatColors.ts +++ b/src/vs/workbench/contrib/chat/common/chatColors.ts @@ -8,14 +8,14 @@ import { localize } from 'vs/nls'; import { registerColor } from 'vs/platform/theme/common/colorRegistry'; -export const interactiveRequestBackground = registerColor( - 'interactive.requestBackground', +export const chatRequestBackground = registerColor( + 'chat.requestBackground', { dark: new Color(new RGBA(255, 255, 255, 0.03)), light: new Color(new RGBA(0, 0, 0, 0.03)), hcDark: null, hcLight: null, }, - localize('interactive.requestBackground', 'The background color of an interactive request.') + localize('chat.requestBackground', 'The background color of a chat request.') ); -export const interactiveRequestBorder = registerColor( - 'interactive.requestBorder', +export const chatRequestBorder = registerColor( + 'chat.requestBorder', { dark: new Color(new RGBA(255, 255, 255, 0.10)), light: new Color(new RGBA(0, 0, 0, 0.10)), hcDark: null, hcLight: null, }, - localize('interactive.requestBorder', 'The border color of an interactive request.') + localize('chat.requestBorder', 'The border color of a chat request.') ); diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 3277c6e42fc..8921237759a 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -6,12 +6,12 @@ import { localize } from 'vs/nls'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -export const CONTEXT_RESPONSE_HAS_PROVIDER_ID = new RawContextKey('interactiveSessionResponseHasProviderId', false, { type: 'boolean', description: localize('interactiveSessionResponseHasProviderId', "True when the provider has assigned an id to this response.") }); -export const CONTEXT_RESPONSE_VOTE = new RawContextKey('interactiveSessionResponseVote', '', { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") }); -export const CONTEXT_INTERACTIVE_REQUEST_IN_PROGRESS = new RawContextKey('interactiveSessionRequestInProgress', false, { type: 'boolean', description: localize('interactiveSessionRequestInProgress', "True when the current request is still in progress.") }); +export const CONTEXT_RESPONSE_HAS_PROVIDER_ID = new RawContextKey('chatSessionResponseHasProviderId', false, { type: 'boolean', description: localize('interactiveSessionResponseHasProviderId', "True when the provider has assigned an id to this response.") }); +export const CONTEXT_RESPONSE_VOTE = new RawContextKey('chatSessionResponseVote', '', { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") }); +export const CONTEXT_CHAT_REQUEST_IN_PROGRESS = new RawContextKey('chatSessionRequestInProgress', false, { type: 'boolean', description: localize('interactiveSessionRequestInProgress', "True when the current request is still in progress.") }); -export const CONTEXT_INTERACTIVE_INPUT_HAS_TEXT = new RawContextKey('interactiveInputHasText', false, { type: 'boolean', description: localize('interactiveInputHasText', "True when the interactive input has text.") }); -export const CONTEXT_IN_INTERACTIVE_INPUT = new RawContextKey('inInteractiveInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the interactive input, false otherwise.") }); -export const CONTEXT_IN_INTERACTIVE_SESSION = new RawContextKey('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the interactive session widget, false otherwise.") }); +export const CONTEXT_CHAT_INPUT_HAS_TEXT = new RawContextKey('chatInputHasText', false, { type: 'boolean', description: localize('interactiveInputHasText', "True when the chat input has text.") }); +export const CONTEXT_IN_CHAT_INPUT = new RawContextKey('inChatInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the chat input, false otherwise.") }); +export const CONTEXT_IN_CHAT_SESSION = new RawContextKey('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") }); -export const CONTEXT_PROVIDER_EXISTS = new RawContextKey('hasChatProvider', false, { type: 'boolean', description: localize('hasChatProvider', "True when some interactive session provider has been registered.") }); +export const CONTEXT_PROVIDER_EXISTS = new RawContextKey('hasChatProvider', false, { type: 'boolean', description: localize('hasChatProvider', "True when some chat provider has been registered.") }); diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 817036974a5..f614c7d38fb 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -10,18 +10,18 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { ILogService } from 'vs/platform/log/common/log'; -import { IChatProgress, IChatResponse, IChatResponseErrorDetails, IChat, IChatFollowup, IChatReplyFollowup, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChat, IChatFollowup, IChatProgress, IChatReplyFollowup, IChatResponse, IChatResponseErrorDetails, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; -export interface IInteractiveRequestModel { +export interface IChatRequestModel { readonly id: string; readonly username: string; readonly avatarIconUri?: URI; readonly session: IChatModel; readonly message: string | IChatReplyFollowup; - readonly response: IInteractiveResponseModel | undefined; + readonly response: IChatResponseModel | undefined; } -export interface IInteractiveResponseModel { +export interface IChatResponseModel { readonly onDidChange: Event; readonly id: string; readonly providerId: string; @@ -38,18 +38,18 @@ export interface IInteractiveResponseModel { setVote(vote: InteractiveSessionVoteDirection): void; } -export function isRequest(item: unknown): item is IInteractiveRequestModel { - return !!item && typeof (item as IInteractiveRequestModel).message !== 'undefined'; +export function isRequest(item: unknown): item is IChatRequestModel { + return !!item && typeof (item as IChatRequestModel).message !== 'undefined'; } -export function isResponse(item: unknown): item is IInteractiveResponseModel { +export function isResponse(item: unknown): item is IChatResponseModel { return !isRequest(item); } -export class InteractiveRequestModel implements IInteractiveRequestModel { +export class ChatRequestModel implements IChatRequestModel { private static nextId = 0; - public response: InteractiveResponseModel | undefined; + public response: ChatResponseModel | undefined; private _id: string; public get id(): string { @@ -67,11 +67,11 @@ export class InteractiveRequestModel implements IInteractiveRequestModel { constructor( public readonly session: ChatModel, public readonly message: string | IChatReplyFollowup) { - this._id = 'request_' + InteractiveRequestModel.nextId++; + this._id = 'request_' + ChatRequestModel.nextId++; } } -export class InteractiveResponseModel extends Disposable implements IInteractiveResponseModel { +export class ChatResponseModel extends Disposable implements IChatResponseModel { private readonly _onDidChange = this._register(new Emitter()); readonly onDidChange = this._onDidChange.event; @@ -133,7 +133,7 @@ export class InteractiveResponseModel extends Disposable implements IInteractive private _followups?: IChatFollowup[] ) { super(); - this._id = 'response_' + InteractiveResponseModel.nextId++; + this._id = 'response_' + ChatResponseModel.nextId++; } updateContent(responsePart: string) { @@ -174,11 +174,11 @@ export interface IChatModel { readonly sessionId: string; readonly providerId: string; readonly isInitialized: boolean; - // readonly title: string; + readonly title: string; readonly welcomeMessage: IChatWelcomeMessageModel | undefined; readonly requestInProgress: boolean; readonly inputPlaceholder?: string; - getRequests(): IInteractiveRequestModel[]; + getRequests(): IChatRequestModel[]; waitForInitialization(): Promise; toExport(): IExportableChatData; toJSON(): ISerializableChatData; @@ -234,12 +234,12 @@ export type IChatChangeEvent = IChatAddRequestEvent | IChatAddResponseEvent | IC export interface IChatAddRequestEvent { kind: 'addRequest'; - request: IInteractiveRequestModel; + request: IChatRequestModel; } export interface IChatAddResponseEvent { kind: 'addResponse'; - response: IInteractiveResponseModel; + response: IChatResponseModel; } export interface IChatInitEvent { @@ -253,7 +253,7 @@ export class ChatModel extends Disposable implements IChatModel { private readonly _onDidChange = this._register(new Emitter()); readonly onDidChange = this._onDidChange.event; - private _requests: InteractiveRequestModel[]; + private _requests: ChatRequestModel[]; private _isInitializedDeferred = new DeferredPromise(); private _session: IChat | undefined; @@ -319,6 +319,17 @@ export class ChatModel extends Disposable implements IChatModel { return this._isImported; } + get title(): string { + const firstRequestMessage = this._requests[0]?.message; + if (typeof firstRequestMessage === 'string') { + return firstRequestMessage; + } else if (firstRequestMessage) { + return firstRequestMessage.message; + } else { + return ''; + } + } + constructor( public readonly providerId: string, private readonly initialData: ISerializableChatData | IExportableChatData | undefined, @@ -336,7 +347,7 @@ export class ChatModel extends Disposable implements IChatModel { this._initialResponderAvatarIconUri = initialData?.responderAvatarIconUri && URI.revive(initialData.responderAvatarIconUri); } - private _deserialize(obj: IExportableChatData): InteractiveRequestModel[] { + private _deserialize(obj: IExportableChatData): ChatRequestModel[] { const requests = obj.requests; if (!Array.isArray(requests)) { this.logService.error(`Ignoring malformed session data: ${obj}`); @@ -349,9 +360,9 @@ export class ChatModel extends Disposable implements IChatModel { } return requests.map((raw: ISerializableChatRequestData) => { - const request = new InteractiveRequestModel(this, raw.message); + const request = new ChatRequestModel(this, raw.message); if (raw.response || raw.responseErrorDetails) { - request.response = new InteractiveResponseModel(new MarkdownString(raw.response), this, true, raw.isCanceled, raw.vote, raw.providerResponseId, raw.responseErrorDetails, raw.followups); + request.response = new ChatResponseModel(new MarkdownString(raw.response), this, true, raw.isCanceled, raw.vote, raw.providerResponseId, raw.responseErrorDetails, raw.followups); } return request; }); @@ -389,30 +400,30 @@ export class ChatModel extends Disposable implements IChatModel { return this._isInitializedDeferred.p; } - getRequests(): InteractiveRequestModel[] { + getRequests(): ChatRequestModel[] { return this._requests; } - addRequest(message: string | IChatReplyFollowup): InteractiveRequestModel { + addRequest(message: string | IChatReplyFollowup): ChatRequestModel { if (!this._session) { throw new Error('addRequest: No session'); } - const request = new InteractiveRequestModel(this, message); - request.response = new InteractiveResponseModel(new MarkdownString(''), this); + const request = new ChatRequestModel(this, message); + request.response = new ChatResponseModel(new MarkdownString(''), this); this._requests.push(request); this._onDidChange.fire({ kind: 'addRequest', request }); return request; } - acceptResponseProgress(request: InteractiveRequestModel, progress: IChatProgress): void { + acceptResponseProgress(request: ChatRequestModel, progress: IChatProgress): void { if (!this._session) { throw new Error('acceptResponseProgress: No session'); } if (!request.response) { - request.response = new InteractiveResponseModel(new MarkdownString(''), this); + request.response = new ChatResponseModel(new MarkdownString(''), this); } if (request.response.isComplete) { @@ -426,25 +437,25 @@ export class ChatModel extends Disposable implements IChatModel { } } - cancelRequest(request: InteractiveRequestModel): void { + cancelRequest(request: ChatRequestModel): void { if (request.response) { request.response.cancel(); } } - completeResponse(request: InteractiveRequestModel, rawResponse: IChatResponse): void { + completeResponse(request: ChatRequestModel, rawResponse: IChatResponse): void { if (!this._session) { throw new Error('completeResponse: No session'); } if (!request.response) { - request.response = new InteractiveResponseModel(new MarkdownString(''), this); + request.response = new ChatResponseModel(new MarkdownString(''), this); } request.response.complete(rawResponse.errorDetails); } - setFollowups(request: InteractiveRequestModel, followups: IChatFollowup[] | undefined): void { + setFollowups(request: ChatRequestModel, followups: IChatFollowup[] | undefined): void { if (!request.response) { // Maybe something went wrong? return; @@ -453,7 +464,7 @@ export class ChatModel extends Disposable implements IChatModel { request.response.setFollowups(followups); } - setResponse(request: InteractiveRequestModel, response: InteractiveResponseModel): void { + setResponse(request: ChatRequestModel, response: ChatResponseModel): void { request.response = response; this._onDidChange.fire({ kind: 'addResponse', response }); } @@ -508,11 +519,11 @@ export class ChatModel extends Disposable implements IChatModel { } } -export type IInteractiveWelcomeMessageContent = IMarkdownString | IChatReplyFollowup[]; +export type IChatWelcomeMessageContent = IMarkdownString | IChatReplyFollowup[]; export interface IChatWelcomeMessageModel { readonly id: string; - readonly content: IInteractiveWelcomeMessageContent[]; + readonly content: IChatWelcomeMessageContent[]; readonly username: string; readonly avatarIconUri?: URI; @@ -526,7 +537,7 @@ export class ChatWelcomeMessageModel implements IChatWelcomeMessageModel { return this._id; } - constructor(public readonly content: IInteractiveWelcomeMessageContent[], public readonly username: string, public readonly avatarIconUri?: URI) { + constructor(public readonly content: IChatWelcomeMessageContent[], public readonly username: string, public readonly avatarIconUri?: URI) { this._id = 'welcome_' + ChatWelcomeMessageModel.nextId++; } } diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index a4d20dc6818..82d07f0c8d0 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -88,6 +88,7 @@ export interface IChatResponseCommandFollowup { export type IChatFollowup = IChatReplyFollowup | IChatResponseCommandFollowup; +// Name has to match the one in vscode.d.ts for some reason export enum InteractiveSessionVoteDirection { Up = 1, Down = 2 @@ -184,7 +185,7 @@ export interface IChatService { /** * Returns whether the request was accepted. */ - sendRequest(sessionId: string, message: string | IChatReplyFollowup): Promise; + sendRequest(sessionId: string, message: string | IChatReplyFollowup): Promise<{ responseCompletePromise: Promise } | undefined>; cancelCurrentRequestForSession(sessionId: string): void; getSlashCommands(sessionId: string, token: CancellationToken): Promise; clearSession(sessionId: string): void; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 8885116808e..45ec1a33217 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -309,12 +309,12 @@ export class ChatService extends Disposable implements IChatService { return this._startSession(data.providerId, data, CancellationToken.None); } - async sendRequest(sessionId: string, request: string | IChatReplyFollowup): Promise { + async sendRequest(sessionId: string, request: string | IChatReplyFollowup): Promise<{ responseCompletePromise: Promise } | undefined> { const messageText = typeof request === 'string' ? request : request.message; this.trace('sendRequest', `sessionId: ${sessionId}, message: ${messageText.substring(0, 20)}${messageText.length > 20 ? '[...]' : ''}}`); if (!messageText.trim()) { this.trace('sendRequest', 'Rejected empty message'); - return false; + return; } const model = this._sessionModels.get(sessionId); @@ -330,12 +330,11 @@ export class ChatService extends Disposable implements IChatService { if (this._pendingRequests.has(sessionId)) { this.trace('sendRequest', `Session ${sessionId} already has a pending request`); - return false; + return; } // This method is only returning whether the request was accepted - don't block on the actual request - this._sendRequestAsync(model, provider, request); - return true; + return { responseCompletePromise: this._sendRequestAsync(model, provider, request) }; } private async _sendRequestAsync(model: ChatModel, provider: IChatProvider, message: string | IChatReplyFollowup): Promise { @@ -412,6 +411,7 @@ export class ChatService extends Disposable implements IChatService { rawResponsePromise.finally(() => { this._pendingRequests.delete(model.sessionId); }); + return rawResponsePromise; } private async handleSlashCommand(sessionId: string, command: string): Promise { @@ -467,30 +467,30 @@ export class ChatService extends Disposable implements IChatService { const model = Iterable.first(this._sessionModels.values()); if (!model) { // If no session, create one- how and is the service the right place to decide this? - this.trace('addInteractiveRequest', 'No session available'); + this.trace('addRequest', 'No session available'); return; } const provider = this._providers.get(model.providerId); if (!provider || !provider.resolveRequest) { - this.trace('addInteractiveRequest', 'No provider available'); + this.trace('addRequest', 'No provider available'); return undefined; } - this.trace('addInteractiveRequest', `Calling resolveRequest for session ${model.sessionId}`); + this.trace('addRequest', `Calling resolveRequest for session ${model.sessionId}`); const request = await provider.resolveRequest(model.session!, context, CancellationToken.None); if (!request) { - this.trace('addInteractiveRequest', `Provider returned no request for session ${model.sessionId}`); + this.trace('addRequest', `Provider returned no request for session ${model.sessionId}`); return; } // Maybe this API should queue a request after the current one? - this.trace('addInteractiveRequest', `Sending resolved request for session ${model.sessionId}`); + this.trace('addRequest', `Sending resolved request for session ${model.sessionId}`); this.sendRequest(model.sessionId, request.message); } async sendRequestToProvider(sessionId: string, message: IChatDynamicRequest): Promise { - this.trace('sendInteractiveRequestToProvider', `sessionId: ${sessionId}`); + this.trace('sendRequestToProvider', `sessionId: ${sessionId}`); await this.sendRequest(sessionId, message.message); } diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index c40a42aba78..ba66ad2c6cd 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -10,33 +10,34 @@ import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IInteractiveRequestModel, IInteractiveResponseModel, IChatModel, IInteractiveWelcomeMessageContent } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatRequestModel, IChatResponseModel, IChatModel, IChatWelcomeMessageContent } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatResponseErrorDetails, IChatReplyFollowup, IChatResponseCommandFollowup, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; -export function isRequestVM(item: unknown): item is IInteractiveRequestViewModel { +export function isRequestVM(item: unknown): item is IChatRequestViewModel { return !!item && typeof item === 'object' && 'message' in item; } -export function isResponseVM(item: unknown): item is IInteractiveResponseViewModel { - return !!item && typeof (item as IInteractiveResponseViewModel).onDidChange !== 'undefined'; +export function isResponseVM(item: unknown): item is IChatResponseViewModel { + return !!item && typeof (item as IChatResponseViewModel).onDidChange !== 'undefined'; } -export function isWelcomeVM(item: unknown): item is IInteractiveWelcomeMessageViewModel { +export function isWelcomeVM(item: unknown): item is IChatWelcomeMessageViewModel { return !!item && typeof item === 'object' && 'content' in item; } export interface IChatViewModel { + readonly isInitialized: boolean; readonly providerId: string; readonly sessionId: string; readonly onDidDisposeModel: Event; readonly onDidChange: Event; readonly requestInProgress: boolean; readonly inputPlaceholder?: string; - getItems(): (IInteractiveRequestViewModel | IInteractiveResponseViewModel | IInteractiveWelcomeMessageViewModel)[]; + getItems(): (IChatRequestViewModel | IChatResponseViewModel | IChatWelcomeMessageViewModel)[]; } -export interface IInteractiveRequestViewModel { +export interface IChatRequestViewModel { readonly id: string; /** This ID updates every time the underlying data changes */ readonly dataId: string; @@ -47,7 +48,7 @@ export interface IInteractiveRequestViewModel { currentRenderedHeight: number | undefined; } -export interface IInteractiveResponseRenderData { +export interface IChatResponseRenderData { renderedWordCount: number; lastRenderTime: number; isFullyRendered: boolean; @@ -60,7 +61,7 @@ export interface IChatLiveUpdateData { impliedWordLoadRate: number; } -export interface IInteractiveResponseViewModel { +export interface IChatResponseViewModel { readonly onDidChange: Event; readonly id: string; /** This ID updates every time the underlying data changes */ @@ -78,7 +79,7 @@ export interface IInteractiveResponseViewModel { readonly commandFollowups?: IChatResponseCommandFollowup[]; readonly errorDetails?: IChatResponseErrorDetails; readonly contentUpdateTimings?: IChatLiveUpdateData; - renderData?: IInteractiveResponseRenderData; + renderData?: IChatResponseRenderData; currentRenderedHeight: number | undefined; setVote(vote: InteractiveSessionVoteDirection): void; } @@ -90,7 +91,7 @@ export class ChatViewModel extends Disposable implements IChatViewModel { private readonly _onDidChange = this._register(new Emitter()); readonly onDidChange = this._onDidChange.event; - private readonly _items: (IInteractiveRequestViewModel | IInteractiveResponseViewModel)[] = []; + private readonly _items: (IChatRequestViewModel | IChatResponseViewModel)[] = []; get inputPlaceholder(): string | undefined { return this._model.inputPlaceholder; @@ -108,6 +109,10 @@ export class ChatViewModel extends Disposable implements IChatViewModel { return this._model.providerId; } + get isInitialized() { + return this._model.isInitialized; + } + constructor( private readonly _model: IChatModel, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -115,7 +120,7 @@ export class ChatViewModel extends Disposable implements IChatViewModel { super(); _model.getRequests().forEach((request, i) => { - this._items.push(new InteractiveRequestViewModel(request)); + this._items.push(new ChatRequestViewModel(request)); if (request.response) { this.onAddResponse(request.response); } @@ -124,7 +129,7 @@ export class ChatViewModel extends Disposable implements IChatViewModel { this._register(_model.onDidDispose(() => this._onDidDisposeModel.fire())); this._register(_model.onDidChange(e => { if (e.kind === 'addRequest') { - this._items.push(new InteractiveRequestViewModel(e.request)); + this._items.push(new ChatRequestViewModel(e.request)); if (e.request.response) { this.onAddResponse(e.request.response); } @@ -136,8 +141,8 @@ export class ChatViewModel extends Disposable implements IChatViewModel { })); } - private onAddResponse(responseModel: IInteractiveResponseModel) { - const response = this.instantiationService.createInstance(InteractiveResponseViewModel, responseModel); + private onAddResponse(responseModel: IChatResponseModel) { + const response = this.instantiationService.createInstance(ChatResponseViewModel, responseModel); this._register(response.onDidChange(() => this._onDidChange.fire())); this._items.push(response); } @@ -149,12 +154,12 @@ export class ChatViewModel extends Disposable implements IChatViewModel { override dispose() { super.dispose(); this._items - .filter((item): item is InteractiveResponseViewModel => item instanceof InteractiveResponseViewModel) - .forEach((item: InteractiveResponseViewModel) => item.dispose()); + .filter((item): item is ChatResponseViewModel => item instanceof ChatResponseViewModel) + .forEach((item: ChatResponseViewModel) => item.dispose()); } } -export class InteractiveRequestViewModel implements IInteractiveRequestViewModel { +export class ChatRequestViewModel implements IChatRequestViewModel { get id() { return this._model.id; } @@ -181,10 +186,10 @@ export class InteractiveRequestViewModel implements IInteractiveRequestViewModel currentRenderedHeight: number | undefined; - constructor(readonly _model: IInteractiveRequestModel) { } + constructor(readonly _model: IChatRequestModel) { } } -export class InteractiveResponseViewModel extends Disposable implements IInteractiveResponseViewModel { +export class ChatResponseViewModel extends Disposable implements IChatResponseViewModel { private _modelChangeCount = 0; private readonly _onDidChange = this._register(new Emitter()); @@ -251,7 +256,7 @@ export class InteractiveResponseViewModel extends Disposable implements IInterac return this._model.vote; } - renderData: IInteractiveResponseRenderData | undefined = undefined; + renderData: IChatResponseRenderData | undefined = undefined; currentRenderedHeight: number | undefined; @@ -261,7 +266,7 @@ export class InteractiveResponseViewModel extends Disposable implements IInterac } constructor( - private readonly _model: IInteractiveResponseModel, + private readonly _model: IChatResponseModel, @ILogService private readonly logService: ILogService ) { super(); @@ -300,7 +305,7 @@ export class InteractiveResponseViewModel extends Disposable implements IInterac this.trace(`onDidChange`, `Done- got ${wordCount} words over ${timeDiff}ms = ${impliedWordLoadRate} words/s. ${this.renderData?.renderedWordCount} words are rendered.`); } } else { - this.logService.warn('InteractiveResponseViewModel#onDidChange: got model update but contentUpdateTimings is not initialized'); + this.logService.warn('ChatResponseViewModel#onDidChange: got model update but contentUpdateTimings is not initialized'); } // new data -> new id, new content to render @@ -315,7 +320,7 @@ export class InteractiveResponseViewModel extends Disposable implements IInterac } private trace(tag: string, message: string) { - this.logService.trace(`InteractiveResponseViewModel#${tag}: ${message}`); + this.logService.trace(`ChatResponseViewModel#${tag}: ${message}`); } setVote(vote: InteractiveSessionVoteDirection): void { @@ -324,9 +329,9 @@ export class InteractiveResponseViewModel extends Disposable implements IInterac } } -export interface IInteractiveWelcomeMessageViewModel { +export interface IChatWelcomeMessageViewModel { readonly id: string; readonly username: string; readonly avatarIconUri?: URI; - readonly content: IInteractiveWelcomeMessageContent[]; + readonly content: IChatWelcomeMessageContent[]; } diff --git a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts index 3dc46cc26fc..135bfded453 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -188,7 +188,7 @@ suite('Chat', () => { assert.strictEqual(commands?.[0].sortText, 'sortText'); }); - test('sendInteractiveRequestToProvider', async () => { + test('sendRequestToProvider', async () => { const testService = instantiationService.createInstance(ChatService); testService.registerProvider(new SimpleTestProvider('testProvider')); diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css index e15ab55dacb..8264a8d7adf 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css @@ -34,6 +34,7 @@ border: 1px solid var(--vscode-contrastBorder); border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; + font-size: 12px; } .monaco-workbench.reduce-motion .monaco-editor .find-widget { @@ -57,9 +58,9 @@ } .monaco-workbench .simple-find-part .matchesCount { - width: 68px; - max-width: 68px; - min-width: 68px; + width: 73px; + max-width: 73px; + min-width: 73px; padding-left: 5px; } diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts index a59a6decbd5..bf463408a20 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts @@ -37,11 +37,12 @@ interface IFindOptions { appendCaseSensitiveLabel?: string; appendRegexLabel?: string; appendWholeWordsLabel?: string; + matchesLimit?: number; type?: 'Terminal' | 'Webview'; } const SIMPLE_FIND_WIDGET_INITIAL_WIDTH = 310; -const MATCHES_COUNT_WIDTH = 68; +const MATCHES_COUNT_WIDTH = 73; export abstract class SimpleFindWidget extends Widget { private readonly _findInput: FindInput; @@ -52,6 +53,7 @@ export abstract class SimpleFindWidget extends Widget { private readonly _updateHistoryDelayer: Delayer; private readonly prevBtn: SimpleButton; private readonly nextBtn: SimpleButton; + private readonly _matchesLimit: number; private _matchesCount: HTMLElement | undefined; private _isVisible: boolean = false; @@ -68,6 +70,8 @@ export abstract class SimpleFindWidget extends Widget { ) { super(); + this._matchesLimit = options.matchesLimit ?? Number.MAX_SAFE_INTEGER; + this._findInput = this._register(new ContextScopedFindInput(null, contextViewService, { label: NLS_FIND_INPUT_LABEL, placeholder: NLS_FIND_INPUT_PLACEHOLDER, @@ -251,7 +255,7 @@ export abstract class SimpleFindWidget extends Widget { } this._isVisible = true; - this.updateButtons(this._foundMatch); + this.updateResultCount(); this.layout(); setTimeout(() => { @@ -354,15 +358,21 @@ export abstract class SimpleFindWidget extends Widget { const count = await this._getResultCount(); this._matchesCount.innerText = ''; + const showRedOutline = (this.inputValue.length > 0 && count?.resultCount === 0); + this._matchesCount.classList.toggle('no-results', showRedOutline); let label = ''; - this._matchesCount.classList.toggle('no-results', false); - if (count?.resultCount !== undefined && count?.resultCount === 0) { - label = NLS_NO_RESULTS; - if (!!this.inputValue) { - this._matchesCount.classList.toggle('no-results', true); + if (count?.resultCount) { + let matchesCount: string = String(count.resultCount); + if (count.resultCount >= this._matchesLimit) { + matchesCount += '+'; } - } else if (count?.resultCount) { - label = strings.format(NLS_MATCHES_LOCATION, count.resultIndex + 1, count?.resultCount); + let matchesPosition: string = String(count.resultIndex + 1); + if (matchesPosition === '0') { + matchesPosition = '?'; + } + label = strings.format(NLS_MATCHES_LOCATION, matchesPosition, matchesCount); + } else { + label = NLS_NO_RESULTS; } alertFn(this._announceSearchResults(label, this.inputValue)); this._matchesCount.appendChild(document.createTextNode(label)); diff --git a/src/vs/workbench/contrib/debug/browser/debugHover.ts b/src/vs/workbench/contrib/debug/browser/debugHover.ts index 0b707139699..ef367d12de0 100644 --- a/src/vs/workbench/contrib/debug/browser/debugHover.ts +++ b/src/vs/workbench/contrib/debug/browser/debugHover.ts @@ -14,10 +14,12 @@ import { coalesce } from 'vs/base/common/arrays'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { KeyCode } from 'vs/base/common/keyCodes'; import * as lifecycle from 'vs/base/common/lifecycle'; +import { clamp } from 'vs/base/common/numbers'; import { isMacintosh } from 'vs/base/common/platform'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IDimension } from 'vs/editor/common/core/dimension'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; @@ -91,6 +93,9 @@ export class DebugHoverWidget implements IContentWidget { private scrollbar!: DomScrollableElement; private debugHoverComputer: DebugHoverComputer; + private expressionToRender: IExpression | undefined; + private isUpdatingTree = false; + constructor( private editor: ICodeEditor, @IDebugService private readonly debugService: IDebugService, @@ -107,6 +112,7 @@ export class DebugHoverWidget implements IContentWidget { private create(): void { this.domNode = $('.debug-hover-widget'); this.complexValueContainer = dom.append(this.domNode, $('.complex-value')); + this.complexValueContainer.style.visibility = 'hidden'; this.complexValueTitle = dom.append(this.complexValueContainer, $('.title')); this.treeContainer = dom.append(this.complexValueContainer, $('.debug-hover-tree')); this.treeContainer.setAttribute('role', 'tree'); @@ -138,7 +144,12 @@ export class DebugHoverWidget implements IContentWidget { this.domNode.style.border = `1px solid ${asCssVariable(editorHoverBorder)}`; this.domNode.style.color = asCssVariable(editorHoverForeground); - this.toDispose.push(this.tree.onDidChangeContentHeight(() => this.layoutTreeAndContainer(false))); + this.toDispose.push(this.tree.onDidChangeContentHeight(() => { + if (!this.isUpdatingTree) { + // Don't do a layout in the middle of the async setInput + this.layoutTreeAndContainer(); + } + })); this.registerListeners(); this.editor.addContentWidget(this); @@ -261,10 +272,10 @@ export class DebugHoverWidget implements IContentWidget { this.valueContainer.hidden = true; - await this.tree.setInput(expression); + this.expressionToRender = expression; this.complexValueTitle.textContent = expression.value; this.complexValueTitle.title = expression.value; - this.layoutTreeAndContainer(true); + this.editor.layoutContentWidget(this); this.tree.scrollTop = 0; this.tree.scrollLeft = 0; this.complexValueContainer.hidden = false; @@ -275,15 +286,50 @@ export class DebugHoverWidget implements IContentWidget { } } - private layoutTreeAndContainer(initialLayout: boolean): void { + private layoutTreeAndContainer(): void { + this.layoutTree(); + this.editor.layoutContentWidget(this); + } + + private layoutTree(): void { const scrollBarHeight = 10; const treeHeight = Math.min(Math.max(266, this.editor.getLayoutInfo().height * 0.55), this.tree.contentHeight + scrollBarHeight); + + // Reset to a smaller width, if it was previously rendered wide + this.tree.layout(treeHeight, 400); + + // const titleWidth = this.complexValueTitle.clientWidth; + const realTreeWidth = this.tree.getHTMLElement().offsetWidth; + // const contentWidth = Math.max(titleWidth, realTreeWidth); + const treeWidth = clamp(realTreeWidth, 400, 550); + this.tree.layout(treeHeight, treeWidth); this.treeContainer.style.height = `${treeHeight}px`; - this.tree.layout(treeHeight, initialLayout ? 400 : undefined); - this.editor.layoutContentWidget(this); this.scrollbar.scanDomNode(); } + beforeRender(): IDimension | null { + // beforeRender will be called each time the hover size changes, and the content widget is layed out again. + if (this.expressionToRender) { + const expression = this.expressionToRender; + this.expressionToRender = undefined; + + // Do this in beforeRender once the content widget is no longer display=none so that its elements' sizes will be measured correctly. + this.isUpdatingTree = true; + this.tree.setInput(expression).then(() => { + dom.scheduleAtNextAnimationFrame(() => { + // Wait for scrollWidth to update after a frame + this.layoutTree(); + this.editor.layoutContentWidget(this); + this.complexValueContainer.style.visibility = ''; + }); + }).finally(() => { + this.isUpdatingTree = false; + }); + } + + return null; + } + afterRender(positionPreference: ContentWidgetPositionPreference | null) { if (positionPreference) { // Remember where the editor placed you to keep position stable #109226 @@ -293,6 +339,7 @@ export class DebugHoverWidget implements IContentWidget { hide(): void { + this.complexValueContainer.style.visibility = 'hidden'; if (this.showCancellationSource) { this.showCancellationSource.cancel(); this.showCancellationSource = undefined; diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 91510113258..13decde4682 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -975,76 +975,81 @@ export class DebugSession implements IDebugSession { } })); + const statusQueue = new Queue(); this.rawListeners.push(this.raw.onDidStop(async event => { - this.passFocusScheduler.cancel(); - this.stoppedDetails.push(event.body); - await this.fetchThreads(event.body); - // If the focus for the current session is on a non-existent thread, clear the focus. - const focusedThread = this.debugService.getViewModel().focusedThread; - const focusedThreadDoesNotExist = focusedThread !== undefined && focusedThread.session === this && !this.threads.has(focusedThread.threadId); - if (focusedThreadDoesNotExist) { - this.debugService.focusStackFrame(undefined, undefined); - } - const thread = typeof event.body.threadId === 'number' ? this.getThread(event.body.threadId) : undefined; - if (thread) { - // Call fetch call stack twice, the first only return the top stack frame. - // Second retrieves the rest of the call stack. For performance reasons #25605 - const promises = this.model.refreshTopOfCallstack(thread); - const focus = async () => { - if (focusedThreadDoesNotExist || (!event.body.preserveFocusHint && thread.getCallStack().length)) { - const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; - if (!focusedStackFrame || focusedStackFrame.thread.session === this) { - // Only take focus if nothing is focused, or if the focus is already on the current session - const preserveFocus = !this.configurationService.getValue('debug').focusEditorOnBreak; - await this.debugService.focusStackFrame(undefined, thread, undefined, { preserveFocus }); - } - - if (thread.stoppedDetails) { - if (thread.stoppedDetails.reason === 'breakpoint' && this.configurationService.getValue('debug').openDebug === 'openOnDebugBreak' && !this.suppressDebugView) { - await this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar); - } - - if (this.configurationService.getValue('debug').focusWindowOnBreak && !this.workbenchEnvironmentService.extensionTestsLocationURI) { - await this.hostService.focus({ force: true /* Application may not be active */ }); - } - } - } - }; - - await promises.topCallStack; - focus(); - await promises.wholeCallStack; - const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; - if (!focusedStackFrame || !focusedStackFrame.source || focusedStackFrame.source.presentationHint === 'deemphasize' || focusedStackFrame.presentationHint === 'deemphasize') { - // The top stack frame can be deemphesized so try to focus again #68616 - focus(); + statusQueue.queue(async () => { + this.passFocusScheduler.cancel(); + this.stoppedDetails.push(event.body); + await this.fetchThreads(event.body); + // If the focus for the current session is on a non-existent thread, clear the focus. + const focusedThread = this.debugService.getViewModel().focusedThread; + const focusedThreadDoesNotExist = focusedThread !== undefined && focusedThread.session === this && !this.threads.has(focusedThread.threadId); + if (focusedThreadDoesNotExist) { + this.debugService.focusStackFrame(undefined, undefined); } - } - this._onDidChangeState.fire(); + const thread = typeof event.body.threadId === 'number' ? this.getThread(event.body.threadId) : undefined; + if (thread) { + // Call fetch call stack twice, the first only return the top stack frame. + // Second retrieves the rest of the call stack. For performance reasons #25605 + const promises = this.model.refreshTopOfCallstack(thread); + const focus = async () => { + if (focusedThreadDoesNotExist || (!event.body.preserveFocusHint && thread.getCallStack().length)) { + const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; + if (!focusedStackFrame || focusedStackFrame.thread.session === this) { + // Only take focus if nothing is focused, or if the focus is already on the current session + const preserveFocus = !this.configurationService.getValue('debug').focusEditorOnBreak; + await this.debugService.focusStackFrame(undefined, thread, undefined, { preserveFocus }); + } + + if (thread.stoppedDetails) { + if (thread.stoppedDetails.reason === 'breakpoint' && this.configurationService.getValue('debug').openDebug === 'openOnDebugBreak' && !this.suppressDebugView) { + await this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar); + } + + if (this.configurationService.getValue('debug').focusWindowOnBreak && !this.workbenchEnvironmentService.extensionTestsLocationURI) { + await this.hostService.focus({ force: true /* Application may not be active */ }); + } + } + } + }; + + await promises.topCallStack; + focus(); + await promises.wholeCallStack; + const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; + if (!focusedStackFrame || !focusedStackFrame.source || focusedStackFrame.source.presentationHint === 'deemphasize' || focusedStackFrame.presentationHint === 'deemphasize') { + // The top stack frame can be deemphesized so try to focus again #68616 + focus(); + } + } + this._onDidChangeState.fire(); + }); })); this.rawListeners.push(this.raw.onDidThread(event => { - if (event.body.reason === 'started') { - // debounce to reduce threadsRequest frequency and improve performance - if (!this.fetchThreadsScheduler) { - this.fetchThreadsScheduler = new RunOnceScheduler(() => { - this.fetchThreads(); - }, 100); - this.rawListeners.push(this.fetchThreadsScheduler); + statusQueue.queue(async () => { + if (event.body.reason === 'started') { + // debounce to reduce threadsRequest frequency and improve performance + if (!this.fetchThreadsScheduler) { + this.fetchThreadsScheduler = new RunOnceScheduler(() => { + this.fetchThreads(); + }, 100); + this.rawListeners.push(this.fetchThreadsScheduler); + } + if (!this.fetchThreadsScheduler.isScheduled()) { + this.fetchThreadsScheduler.schedule(); + } + } else if (event.body.reason === 'exited') { + this.model.clearThreads(this.getId(), true, event.body.threadId); + const viewModel = this.debugService.getViewModel(); + const focusedThread = viewModel.focusedThread; + this.passFocusScheduler.cancel(); + if (focusedThread && event.body.threadId === focusedThread.threadId) { + // De-focus the thread in case it was focused + this.debugService.focusStackFrame(undefined, undefined, viewModel.focusedSession, { explicit: false }); + } } - if (!this.fetchThreadsScheduler.isScheduled()) { - this.fetchThreadsScheduler.schedule(); - } - } else if (event.body.reason === 'exited') { - this.model.clearThreads(this.getId(), true, event.body.threadId); - const viewModel = this.debugService.getViewModel(); - const focusedThread = viewModel.focusedThread; - this.passFocusScheduler.cancel(); - if (focusedThread && event.body.threadId === focusedThread.threadId) { - // De-focus the thread in case it was focused - this.debugService.focusStackFrame(undefined, undefined, viewModel.focusedSession, { explicit: false }); - } - } + }); })); this.rawListeners.push(this.raw.onDidTerminateDebugee(async event => { @@ -1057,21 +1062,23 @@ export class DebugSession implements IDebugSession { })); this.rawListeners.push(this.raw.onDidContinued(event => { - const threadId = event.body.allThreadsContinued !== false ? undefined : event.body.threadId; - if (typeof threadId === 'number') { - this.stoppedDetails = this.stoppedDetails.filter(sd => sd.threadId !== threadId); - const tokens = this.cancellationMap.get(threadId); - this.cancellationMap.delete(threadId); - tokens?.forEach(t => t.cancel()); - } else { - this.stoppedDetails = []; - this.cancelAllRequests(); - } - this.lastContinuedThreadId = threadId; - // We need to pass focus to other sessions / threads with a timeout in case a quick stop event occurs #130321 - this.passFocusScheduler.schedule(); - this.model.clearThreads(this.getId(), false, threadId); - this._onDidChangeState.fire(); + statusQueue.queue(async () => { + const threadId = event.body.allThreadsContinued !== false ? undefined : event.body.threadId; + if (typeof threadId === 'number') { + this.stoppedDetails = this.stoppedDetails.filter(sd => sd.threadId !== threadId); + const tokens = this.cancellationMap.get(threadId); + this.cancellationMap.delete(threadId); + tokens?.forEach(t => t.cancel()); + } else { + this.stoppedDetails = []; + this.cancelAllRequests(); + } + this.lastContinuedThreadId = threadId; + // We need to pass focus to other sessions / threads with a timeout in case a quick stop event occurs #130321 + this.passFocusScheduler.schedule(); + this.model.clearThreads(this.getId(), false, threadId); + this._onDidChangeState.fire(); + }); })); const outputQueue = new Queue(); diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index ea89bb7ef1d..51e5d6ed635 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { distinct, lastIndex } from 'vs/base/common/arrays'; -import { RunOnceScheduler } from 'vs/base/common/async'; +import { DeferredPromise, RunOnceScheduler } from 'vs/base/common/async'; import { decodeBase64, encodeBase64, VSBuffer } from 'vs/base/common/buffer'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { stringHash } from 'vs/base/common/hash'; @@ -852,6 +852,7 @@ export abstract class BaseBreakpoint extends Enablement implements IBaseBreakpoi toJSON(): any { const result = Object.create(null); + result.id = this.getId(); result.enabled = this.enabled; result.condition = this.condition; result.hitCondition = this.hitCondition; @@ -1175,7 +1176,7 @@ export class ThreadAndSessionIds implements ITreeElement { export class DebugModel implements IDebugModel { private sessions: IDebugSession[]; - private schedulers = new Map(); + private schedulers = new Map }>(); private breakpointsActivated = true; private readonly _onDidChangeBreakpoints = new Emitter(); private readonly _onDidChangeCallStack = new Emitter(); @@ -1273,7 +1274,10 @@ export class DebugModel implements IDebugModel { clearThreads(id: string, removeThreads: boolean, reference: number | undefined = undefined): void { const session = this.sessions.find(p => p.getId() === id); - this.schedulers.forEach(scheduler => scheduler.dispose()); + this.schedulers.forEach(entry => { + entry.scheduler.dispose(); + entry.completeDeferred.complete(); + }); this.schedulers.clear(); if (session) { @@ -1313,24 +1317,32 @@ export class DebugModel implements IDebugModel { const wholeCallStack = new Promise((c, e) => { topCallStack = thread.fetchCallStack(1).then(() => { if (!this.schedulers.has(thread.getId())) { - this.schedulers.set(thread.getId(), new RunOnceScheduler(() => { - thread.fetchCallStack(19).then(() => { - const stale = thread.getStaleCallStack(); - const current = thread.getCallStack(); - let bottomOfCallStackChanged = stale.length !== current.length; - for (let i = 1; i < stale.length && !bottomOfCallStackChanged; i++) { - bottomOfCallStackChanged = !stale[i].equals(current[i]); - } + const deferred = new DeferredPromise(); + this.schedulers.set(thread.getId(), { + completeDeferred: deferred, + scheduler: new RunOnceScheduler(() => { + thread.fetchCallStack(19).then(() => { + const stale = thread.getStaleCallStack(); + const current = thread.getCallStack(); + let bottomOfCallStackChanged = stale.length !== current.length; + for (let i = 1; i < stale.length && !bottomOfCallStackChanged; i++) { + bottomOfCallStackChanged = !stale[i].equals(current[i]); + } - if (bottomOfCallStackChanged) { - this._onDidChangeCallStack.fire(); - } - c(); - }); - }, 420)); + if (bottomOfCallStackChanged) { + this._onDidChangeCallStack.fire(); + } + }).finally(() => { + deferred.complete(); + this.schedulers.delete(thread.getId()); + }); + }, 420) + }); } - this.schedulers.get(thread.getId())!.schedule(); + const entry = this.schedulers.get(thread.getId())!; + entry.scheduler.schedule(); + entry.completeDeferred.p.then(c, e); this._onDidChangeCallStack.fire(); }); }); diff --git a/src/vs/workbench/contrib/debug/common/debugStorage.ts b/src/vs/workbench/contrib/debug/common/debugStorage.ts index 53f39e89cb9..1265ad5fa13 100644 --- a/src/vs/workbench/contrib/debug/common/debugStorage.ts +++ b/src/vs/workbench/contrib/debug/common/debugStorage.ts @@ -39,7 +39,7 @@ export class DebugStorage { let result: Breakpoint[] | undefined; try { result = JSON.parse(this.storageService.get(DEBUG_BREAKPOINTS_KEY, StorageScope.WORKSPACE, '[]')).map((breakpoint: any) => { - return new Breakpoint(URI.parse(breakpoint.uri.external || breakpoint.source.uri.external), breakpoint.lineNumber, breakpoint.column, breakpoint.enabled, breakpoint.condition, breakpoint.hitCondition, breakpoint.logMessage, breakpoint.adapterData, this.textFileService, this.uriIdentityService, this.logService); + return new Breakpoint(URI.parse(breakpoint.uri.external || breakpoint.source.uri.external), breakpoint.lineNumber, breakpoint.column, breakpoint.enabled, breakpoint.condition, breakpoint.hitCondition, breakpoint.logMessage, breakpoint.adapterData, this.textFileService, this.uriIdentityService, this.logService, breakpoint.id); }); } catch (e) { } @@ -50,7 +50,7 @@ export class DebugStorage { let result: FunctionBreakpoint[] | undefined; try { result = JSON.parse(this.storageService.get(DEBUG_FUNCTION_BREAKPOINTS_KEY, StorageScope.WORKSPACE, '[]')).map((fb: any) => { - return new FunctionBreakpoint(fb.name, fb.enabled, fb.hitCondition, fb.condition, fb.logMessage); + return new FunctionBreakpoint(fb.name, fb.enabled, fb.hitCondition, fb.condition, fb.logMessage, fb.id); }); } catch (e) { } @@ -72,7 +72,7 @@ export class DebugStorage { let result: DataBreakpoint[] | undefined; try { result = JSON.parse(this.storageService.get(DEBUG_DATA_BREAKPOINTS_KEY, StorageScope.WORKSPACE, '[]')).map((dbp: any) => { - return new DataBreakpoint(dbp.description, dbp.dataId, true, dbp.enabled, dbp.hitCondition, dbp.condition, dbp.logMessage, dbp.accessTypes, dbp.accessType); + return new DataBreakpoint(dbp.description, dbp.dataId, true, dbp.enabled, dbp.hitCondition, dbp.condition, dbp.logMessage, dbp.accessTypes, dbp.accessType, dbp.id); }); } catch (e) { } diff --git a/src/vs/workbench/contrib/debug/test/common/debugModel.test.ts b/src/vs/workbench/contrib/debug/test/common/debugModel.test.ts index 8d9ce2ac421..779f301a4eb 100644 --- a/src/vs/workbench/contrib/debug/test/common/debugModel.test.ts +++ b/src/vs/workbench/contrib/debug/test/common/debugModel.test.ts @@ -4,9 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { ExceptionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; +import { DeferredPromise } from 'vs/base/common/async'; +import { mockObject } from 'vs/base/test/common/mock'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { DebugModel, ExceptionBreakpoint, FunctionBreakpoint, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; +import { MockDebugStorage } from 'vs/workbench/contrib/debug/test/common/mockDebug'; suite('DebugModel', () => { + suite('FunctionBreakpoint', () => { + test('Id is saved', () => { + const fbp = new FunctionBreakpoint('function', true, 'hit condition', 'condition', 'log message'); + const strigified = JSON.stringify(fbp); + const parsed = JSON.parse(strigified); + assert.equal(parsed.id, fbp.getId()); + }); + }); + suite('ExceptionBreakpoint', () => { test('Restored matches new', () => { const ebp = new ExceptionBreakpoint('id', 'label', true, true, 'condition', 'description', 'condition description', false); @@ -16,4 +29,47 @@ suite('DebugModel', () => { assert.ok(ebp.matches(newEbp)); }); }); + + suite('DebugModel', () => { + test('refreshTopOfCallstack resolves all returned promises when called multiple times', async () => { + const topFrameDeferred = new DeferredPromise(); + const wholeStackDeferred = new DeferredPromise(); + const fakeThread = mockObject()({ + session: { capabilities: { supportsDelayedStackTraceLoading: true } } as any, + }); + fakeThread.fetchCallStack.callsFake((levels: number) => { + return levels === 1 ? topFrameDeferred.p : wholeStackDeferred.p; + }); + fakeThread.getId.returns(1); + + const model = new DebugModel(new MockDebugStorage(), { isDirty: (e: any) => false }, undefined!, new NullLogService()); + + let top1Resolved = false; + let whole1Resolved = false; + let top2Resolved = false; + let whole2Resolved = false; + const result1 = model.refreshTopOfCallstack(fakeThread as any); + result1.topCallStack.then(() => top1Resolved = true); + result1.wholeCallStack.then(() => whole1Resolved = true); + + const result2 = model.refreshTopOfCallstack(fakeThread as any); + result2.topCallStack.then(() => top2Resolved = true); + result2.wholeCallStack.then(() => whole2Resolved = true); + + assert.ok(!top1Resolved); + assert.ok(!whole1Resolved); + assert.ok(!top2Resolved); + assert.ok(!whole2Resolved); + + await topFrameDeferred.complete(); + await result1.topCallStack; + await result2.topCallStack; + assert.ok(!whole1Resolved); + assert.ok(!whole2Resolved); + + await wholeStackDeferred.complete(); + await result1.wholeCallStack; + await result2.wholeCallStack; + }); + }); }); diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditor.contribution.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditor.contribution.ts index 4a48ba8d0fb..ebdc1d83cf3 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditor.contribution.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditor.contribution.ts @@ -33,6 +33,7 @@ registerAction2(interactiveEditorActions.NextFromHistory); registerAction2(interactiveEditorActions.ViewInChatAction); registerAction2(interactiveEditorActions.ExpandMessageAction); registerAction2(interactiveEditorActions.ContractMessageAction); +registerAction2(interactiveEditorActions.AccessibilityHelpEditorAction); registerAction2(interactiveEditorActions.ToggleInlineDiff); registerAction2(interactiveEditorActions.FeebackHelpfulCommand); diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditor.css b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditor.css index b7c74c6d38f..2e5e71da482 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditor.css +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditor.css @@ -103,6 +103,7 @@ .monaco-editor .interactive-editor .status .label { overflow: hidden; padding-left: 10px; + padding-right: 4px; margin-left: auto; color: var(--vscode-descriptionForeground); } diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorActions.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorActions.ts index 38afd9876fc..4946a918df2 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorActions.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorActions.ts @@ -24,6 +24,7 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService import { Range } from 'vs/editor/common/core/range'; import { fromNow } from 'vs/base/common/date'; import { IInteractiveEditorSessionService, Recording } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorSession'; +import { runAccessibilityHelpAction } from 'vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp'; export class StartSessionAction extends EditorAction2 { @@ -547,3 +548,21 @@ export class ContractMessageAction extends AbstractInteractiveEditorAction { ctrl.updateExpansionState(false); } } + +export class AccessibilityHelpEditorAction extends EditorAction2 { + constructor() { + super({ + id: 'interactiveEditor.accessibilityHelp', + title: localize('actions.interactiveSession.accessibiltyHelpEditor', "Interactive Session Editor Accessibility Help"), + category: AbstractInteractiveEditorAction.category, + keybinding: { + when: CTX_INTERACTIVE_EDITOR_FOCUSED, + primary: KeyMod.Alt | KeyCode.F1, + weight: KeybindingWeight.EditorContrib + } + }); + } + async runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): Promise { + runAccessibilityHelpAction(accessor, editor, 'editor'); + } +} diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts index 8a8da325718..89b799d7842 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController.ts @@ -555,7 +555,11 @@ export class InteractiveEditorController implements IEditorContribution { this._ctxLastFeedbackKind.reset(); this._zone.hide(); - this._editor.focus(); + + // Return focus to the editor only if the current focus is within the editor widget + if (this._editor.hasWidgetFocus()) { + this._editor.focus(); + } this._sessionStore?.dispose(); this._sessionStore = undefined; diff --git a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts index 9d7f6296923..c83e3b23ede 100644 --- a/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts +++ b/src/vs/workbench/contrib/interactiveEditor/browser/interactiveEditorWidget.ts @@ -43,7 +43,13 @@ import { LineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer'; import { invertLineRange, lineRangeAsRange } from 'vs/workbench/contrib/interactiveEditor/browser/utils'; import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon'; import { LineRange } from 'vs/editor/common/core/lineRange'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; + +const defaultAriaLabel = localize('aria-label', "Interactive Editor Input"); const _inputEditorOptions: IEditorConstructionOptions = { padding: { top: 3, bottom: 2 }, @@ -84,7 +90,7 @@ const _inputEditorOptions: IEditorConstructionOptions = { showStatusBar: false, }, wordWrap: 'on', - ariaLabel: localize('aria-label', "Interactive Editor Input"), + ariaLabel: defaultAriaLabel, fontFamily: DEFAULT_FONT_FAMILY, fontSize: 13, lineHeight: 20, @@ -172,7 +178,10 @@ export class InteractiveEditorWidget { @ILanguageService private readonly _languageService: ILanguageService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, + @IConfigurationService private readonly _configurationService: IConfigurationService ) { // input editor logic @@ -185,10 +194,16 @@ export class InteractiveEditorWidget { }; this._inputEditor = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.editor, _inputEditorOptions, codeEditorWidgetOptions, parentEditor); + this._updateAriaLabel(); this._store.add(this._inputEditor); this._store.add(this._inputEditor.onDidChangeModelContent(() => this._onDidChangeInput.fire(this))); this._store.add(this._inputEditor.onDidLayoutChange(() => this._onDidChangeHeight.fire())); this._store.add(this._inputEditor.onDidContentSizeChange(() => this._onDidChangeHeight.fire())); + this._store.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(AccessibilityVerbositySettingId.InteractiveEditor)) { + this._updateAriaLabel(); + } + })); const uri = URI.from({ scheme: 'vscode', authority: 'interactive-editor', path: `/interactive-editor/model${InteractiveEditorWidget._modelPool++}.txt` }); this._inputModel = this._modelService.getModel(uri) ?? this._modelService.createModel('', null, uri); @@ -289,6 +304,19 @@ export class InteractiveEditorWidget { this._store.add(markdownMessageToolbar); } + private _updateAriaLabel(): void { + if (!this._accessibilityService.isScreenReaderOptimized()) { + return; + } + let label = defaultAriaLabel; + if (this._configurationService.getValue(AccessibilityVerbositySettingId.InteractiveEditor)) { + const kbLabel = this._keybindingService.lookupKeybinding('interactiveEditor.accessibilityHelp')?.getLabel(); + label = kbLabel ? localize('interactiveEditor.accessibilityHelp', "Interactive Editor Input, Use {0} for Interactive Editor Accessibility Help.", kbLabel) : localize('interactiveSessionInput.accessibilityHelpNoKb', "Interactive Editor Input, Run the Interactive Editor Accessibility Help command for more information."); + } + _inputEditorOptions.ariaLabel = label; + this._inputEditor.updateOptions({ ariaLabel: label }); + } + dispose(): void { this._store.dispose(); this._ctxInputEmpty.reset(); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts b/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts index 2569e028db2..745e8df6ed5 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/navigation/arrow.ts @@ -5,7 +5,6 @@ import { timeout } from 'vs/base/common/async'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { isEqual } from 'vs/base/common/resources'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; @@ -20,8 +19,8 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { Registry } from 'vs/platform/registry/common/platform'; import { InteractiveEditorController } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController'; import { CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor'; -import { INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; -import { CellEditState, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT, findTargetCellEditor } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; +import { CellEditState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE, NOTEBOOK_CURSOR_NAVIGATION_MODE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; @@ -37,18 +36,6 @@ const NOTEBOOK_CURSOR_PAGEUP_SELECT_COMMAND_ID = 'notebook.cell.cursorPageUpSele const NOTEBOOK_CURSOR_PAGEDOWN_COMMAND_ID = 'notebook.cell.cursorPageDown'; const NOTEBOOK_CURSOR_PAGEDOWN_SELECT_COMMAND_ID = 'notebook.cell.cursorPageDownSelect'; -function findTargetCellEditor(context: INotebookCellActionContext, targetCell: ICellViewModel) { - let foundEditor: ICodeEditor | undefined = undefined; - for (const [, codeEditor] of context.notebookEditor.codeEditors) { - if (isEqual(codeEditor.getModel()?.uri, targetCell.uri)) { - foundEditor = codeEditor; - break; - } - } - - return foundEditor; -} - registerAction2(class FocusNextCellAction extends NotebookCellAction { constructor() { super({ diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts b/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts index 7305bc2736f..0c007dd4326 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts @@ -22,9 +22,14 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { IStoredFileWorkingCopy, IStoredFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; import { IStoredFileWorkingCopySaveParticipant, IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ITextModel } from 'vs/editor/common/model'; import { ILogService } from 'vs/platform/log/common/log'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; +import { CodeActionKind, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types'; +import { CodeActionTriggerType, CodeActionProvider, IWorkspaceTextEdit } from 'vs/editor/common/languages'; +import { applyCodeAction, ApplyCodeActionReason, getCodeActions } from 'vs/editor/contrib/codeAction/browser/codeAction'; +import { isEqual } from 'vs/base/common/resources'; + class FormatOnSaveParticipant implements IStoredFileWorkingCopySaveParticipant { constructor( @@ -91,13 +96,15 @@ class FormatOnSaveParticipant implements IStoredFileWorkingCopySaveParticipant { class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipant { constructor( @IConfigurationService private readonly configurationService: IConfigurationService, - @ICommandService private readonly commandService: ICommandService, @ILogService private readonly logService: ILogService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, + @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @ITextModelService private readonly textModelService: ITextModelService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { } - async participate(workingCopy: IStoredFileWorkingCopy, context: { reason: SaveReason }, progress: IProgress, _token: CancellationToken): Promise { + async participate(workingCopy: IStoredFileWorkingCopy, context: { reason: SaveReason }, progress: IProgress, token: CancellationToken): Promise { const isTrusted = this.workspaceTrustManagementService.isWorkspaceTrusted(); if (!isTrusted) { return; @@ -111,11 +118,13 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa return undefined; } - const setting = this.configurationService.getValue<{ [kind: string]: boolean } | string[]>('notebook.experimental.codeActionsOnSave'); + const setting = this.configurationService.getValue<{ [kind: string]: boolean } | string[]>(NotebookSetting.codeActionsOnSave); if (!setting) { return undefined; } + const notebookModel = workingCopy.model.notebookModel; + const settingItems: string[] = Array.isArray(setting) ? setting : Object.keys(setting).filter(x => setting[x]); @@ -124,21 +133,136 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa return undefined; } - progress.report({ message: 'CodeActionsOnSave running' }); + const codeActionsOnSave = this.createCodeActionsOnSave(settingItems); + + // TODO: potentially modify to account for new `Notebook` code action kind + // prioritize `source.fixAll` code actions + if (!Array.isArray(setting)) { + codeActionsOnSave.sort((a, b) => { + if (CodeActionKind.SourceFixAll.contains(a)) { + if (CodeActionKind.SourceFixAll.contains(b)) { + return 0; + } + return -1; + } + if (CodeActionKind.SourceFixAll.contains(b)) { + return 1; + } + return 0; + }); + } + + if (!codeActionsOnSave.length) { + return undefined; + } + + const excludedActions = Array.isArray(setting) + ? [] + : Object.keys(setting) + .filter(x => setting[x] === false) + .map(x => new CodeActionKind(x)); + + + progress.report({ message: localize('notebookSaveParticipants.codeActions', "Running code actions") }); const disposable = new DisposableStore(); try { - for (const cmd of settingItems) { - await this.commandService.executeCommand(cmd); - } + await Promise.all(notebookModel.cells.map(async cell => { + const ref = await this.textModelService.createModelReference(cell.uri); + disposable.add(ref); + + const textEditorModel = ref.object.textEditorModel; + + await this.applyOnSaveActions(textEditorModel, codeActionsOnSave, excludedActions, progress, token); + })); } catch { - // Failure to apply a code action should not block other on save actions - this.logService.warn('CodeActionsOnSave failed to apply a code action'); + this.logService.error('Failed to apply code action on save'); } finally { progress.report({ increment: 100 }); disposable.dispose(); } } + + private createCodeActionsOnSave(settingItems: readonly string[]): CodeActionKind[] { + const kinds = settingItems.map(x => new CodeActionKind(x)); + + // Remove subsets + return kinds.filter(kind => { + return kinds.every(otherKind => otherKind.equals(kind) || !otherKind.contains(kind)); + }); + } + + private async applyOnSaveActions(model: ITextModel, codeActionsOnSave: readonly CodeActionKind[], excludes: readonly CodeActionKind[], progress: IProgress, token: CancellationToken): Promise { + + const getActionProgress = new class implements IProgress { + private _names = new Set(); + private _report(): void { + progress.report({ + message: localize( + { key: 'codeaction.get2', comment: ['[configure]({1}) is a link. Only translate `configure`. Do not change brackets and parentheses or {1}'] }, + "Getting code actions from '{0}' ([configure]({1})).", + [...this._names].map(name => `'${name}'`).join(', '), + 'command:workbench.action.openSettings?%5B%22editor.codeActionsOnSave%22%5D' + ) + }); + } + report(provider: CodeActionProvider) { + if (provider.displayName && !this._names.has(provider.displayName)) { + this._names.add(provider.displayName); + this._report(); + } + } + }; + + for (const codeActionKind of codeActionsOnSave) { + const actionsToRun = await this.getActionsToRun(model, codeActionKind, excludes, getActionProgress, token); + if (token.isCancellationRequested) { + actionsToRun.dispose(); + return; + } + + try { + for (const action of actionsToRun.validActions) { + const codeActionEdits = action.action.edit?.edits; + let breakFlag = false; + for (const edit of codeActionEdits ?? []) { + const workspaceTextEdit = edit as IWorkspaceTextEdit; + if (workspaceTextEdit.resource && isEqual(workspaceTextEdit.resource, model.uri)) { + continue; + } else { + // error -> applied to multiple resources + breakFlag = true; + break; + } + } + if (breakFlag) { + this.logService.warn('Failed to apply code action on save, applied to multiple resources.'); + continue; + } + progress.report({ message: localize('codeAction.apply', "Applying code action '{0}'.", action.action.title) }); + await this.instantiationService.invokeFunction(applyCodeAction, action, ApplyCodeActionReason.OnSave, {}, token); + if (token.isCancellationRequested) { + return; + } + } + } catch { + // Failure to apply a code action should not block other on save actions + } finally { + actionsToRun.dispose(); + } + } + } + + private getActionsToRun(model: ITextModel, codeActionKind: CodeActionKind, excludes: readonly CodeActionKind[], progress: IProgress, token: CancellationToken) { + return getCodeActions(this.languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), { + type: CodeActionTriggerType.Auto, + triggerAction: CodeActionTriggerSource.OnSave, + filter: { include: codeActionKind, excludes: excludes, includeSourceActions: true }, + }, progress, token); + } } + + + export class SaveParticipantsContribution extends Disposable implements IWorkbenchContribution { constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, diff --git a/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts index a1445c5766c..34031dc51c3 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts @@ -20,6 +20,8 @@ import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } f import { TypeConstraint } from 'vs/base/common/types'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { MarshalledId } from 'vs/base/common/marshallingIds'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { isEqual } from 'vs/base/common/resources'; // Kernel Command export const SELECT_KERNEL_ID = '_notebook.selectKernel'; @@ -110,6 +112,18 @@ export function getContextFromUri(accessor: ServicesAccessor, context?: any) { return undefined; } +export function findTargetCellEditor(context: INotebookCellActionContext, targetCell: ICellViewModel) { + let foundEditor: ICodeEditor | undefined = undefined; + for (const [, codeEditor] of context.notebookEditor.codeEditors) { + if (isEqual(codeEditor.getModel()?.uri, targetCell.uri)) { + foundEditor = codeEditor; + break; + } + } + + return foundEditor; +} + export abstract class NotebookAction extends Action2 { constructor(desc: IAction2Options) { if (desc.f1 !== false) { diff --git a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts index 5893835591d..ccb551c92db 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts @@ -18,7 +18,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { changeCellToKind, runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; -import { CellToolbarOrder, CELL_TITLE_CELL_GROUP_ID, CELL_TITLE_OUTPUT_GROUP_ID, executeNotebookCondition, INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; +import { CellToolbarOrder, CELL_TITLE_CELL_GROUP_ID, CELL_TITLE_OUTPUT_GROUP_ID, executeNotebookCondition, INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT, findTargetCellEditor } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_HAS_OUTPUTS, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_OUTPUT_FOCUSED, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { CellEditState, CHANGE_CELL_LANGUAGE, DETECT_CELL_LANGUAGE, QUIT_EDIT_CELL_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; @@ -30,6 +30,8 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IDialogService, IConfirmationResult } from 'vs/platform/dialogs/common/dialogs'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { InteractiveEditorController } from 'vs/workbench/contrib/interactiveEditor/browser/interactiveEditorController'; const CLEAR_ALL_CELLS_OUTPUTS_COMMAND_ID = 'notebook.clearAllCellsOutputs'; @@ -71,6 +73,10 @@ registerAction2(class EditCellAction extends NotebookCellAction { } await context.notebookEditor.focusNotebookCell(context.cell, 'editor'); + const foundEditor: ICodeEditor | undefined = context.cell ? findTargetCellEditor(context, context.cell) : undefined; + if (foundEditor && foundEditor.hasTextFocus() && InteractiveEditorController.get(foundEditor)?.getWidgetPosition()?.lineNumber === foundEditor.getPosition()?.lineNumber) { + InteractiveEditorController.get(foundEditor)?.focus(); + } } }); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts index 3d4c270f712..0bf60aef63b 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -41,6 +41,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { fixedDiffEditorOptions, fixedEditorOptions, fixedEditorPadding } from 'vs/workbench/contrib/notebook/browser/diff/diffCellEditorOptions'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; export function getOptimizedNestedCodeEditorWidgetOptions(): ICodeEditorWidgetOptions { return { @@ -615,7 +616,7 @@ abstract class AbstractElementRenderer extends Disposable { height: Math.min(OUTPUT_EDITOR_HEIGHT_MAGIC, this.cell.layoutInfo.rawOutputHeight || lineHeight * lineCount), width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true) }, - accessibilityVerbose: this.configurationService.getValue('accessibility.verbosity.diff-editor') ?? false + accessibilityVerbose: this.configurationService.getValue(AccessibilityVerbositySettingId.DiffEditor) ?? false }, { originalEditor: getOptimizedNestedCodeEditorWidgetOptions(), modifiedEditor: getOptimizedNestedCodeEditorWidgetOptions() diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 4a84b5f510f..8cefb48ea34 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -920,15 +920,15 @@ configurationRegistry.registerConfiguration({ tags: ['notebookLayout'], default: false }, - // [NotebookSetting.codeActionsOnSave]: { - // markdownDescription: nls.localize('notebook.codeActionsOnSave', "Experimental. Run a series of CodeActions for a notebook on save. CodeActions must be specified, the file must not be saved after delay, and the editor must not be shutting down. Example: `notebook.format: true`"), - // type: 'object', - // additionalProperties: { - // type: 'boolean' - // }, - // tags: ['notebookLayout'], - // default: {} - // }, + [NotebookSetting.codeActionsOnSave]: { + markdownDescription: nls.localize('notebook.codeActionsOnSave', "Experimental. Run a series of CodeActions for a notebook on save. CodeActions must be specified, the file must not be saved after delay, and the editor must not be shutting down. Example: `source.fixAll: true`"), + type: 'object', + additionalProperties: { + type: 'boolean' + }, + tags: ['notebookLayout'], + default: {} + }, [NotebookSetting.confirmDeleteRunningCell]: { markdownDescription: nls.localize('notebook.confirmDeleteRunningCell', "Control whether a confirmation prompt is required to delete a running cell."), type: 'boolean', diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts index 2da8f490a0e..c471f826fe9 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts @@ -568,7 +568,7 @@ export class KernelPickerMRUStrategy extends KernelPickerStrategyBase { } return true; } else { - return this.displaySelectAnotherQuickPick(editor, false); + return false; } } catch (ex) { return false; diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 9c6a84e21b1..0a72b5c3f94 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -938,7 +938,7 @@ export const NotebookSetting = { outputScrolling: 'notebook.output.scrolling', textOutputLineLimit: 'notebook.output.textLineLimit', formatOnSave: 'notebook.formatOnSave.enabled', - codeActionsOnSave: 'notebook.experimental.codeActionsOnSave', + codeActionsOnSave: 'notebook.codeActionsOnSave', outputWordWrap: 'notebook.output.wordWrap', outputLineHeightDeprecated: 'notebook.outputLineHeight', outputLineHeight: 'notebook.output.lineHeight', diff --git a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts index 40435534028..39fbc52fbdc 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts @@ -307,7 +307,7 @@ export class AskInInteractiveAction extends Action2 { constructor() { super({ id: AskInInteractiveAction.ID, - title: { value: localize('askInChat', "Ask In Interactive Session"), original: 'Ask In Interactive Session' }, + title: { value: localize('askInChat', "Ask In Chat"), original: 'Ask In Chat' }, f1: false }); } @@ -325,12 +325,12 @@ export class AskInInteractiveAction extends Action2 { const providerInfos = chatService.getProviderInfos(); switch (providerInfos.length) { case 0: - throw new Error('No interactive session provider found.'); + throw new Error('No chat provider found.'); case 1: providerId = providerInfos[0].id; break; default: - logService.warn('Multiple interactive session providers found. Using the first one.'); + logService.warn('Multiple chat providers found. Using the first one.'); providerId = providerInfos[0].id; break; } diff --git a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts index c4918b91c13..9942f0f95c3 100644 --- a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts +++ b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts @@ -25,7 +25,7 @@ interface IConfiguration extends IWindowsConfiguration { update?: { mode?: string }; debug?: { console?: { wordWrap?: boolean } }; editor?: { accessibilitySupport?: 'on' | 'off' | 'auto' }; - security?: { workspace?: { trust?: { enabled?: boolean } } }; + security?: { workspace?: { trust?: { enabled?: boolean } }; restrictUNCAccess?: boolean }; window: IWindowSettings & { experimental?: { windowControlsOverlay?: { enabled?: boolean } } }; workbench?: { enableExperiments?: boolean }; _extensionsGallery?: { enablePPE?: boolean }; @@ -43,7 +43,8 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo 'editor.accessibilitySupport', 'security.workspace.trust.enabled', 'workbench.enableExperiments', - '_extensionsGallery.enablePPE' + '_extensionsGallery.enablePPE', + 'security.restrictUNCAccess' ]; private readonly titleBarStyle = new ChangeObserver<'native' | 'custom'>('string'); @@ -56,6 +57,7 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo private readonly workspaceTrustEnabled = new ChangeObserver('boolean'); private readonly experimentsEnabled = new ChangeObserver('boolean'); private readonly enablePPEExtensionsGallery = new ChangeObserver('boolean'); + private readonly restrictUNCAccess = new ChangeObserver('boolean'); constructor( @IHostService private readonly hostService: IHostService, @@ -112,6 +114,9 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo // Workspace trust processChanged(this.workspaceTrustEnabled.handleChange(config?.security?.workspace?.trust?.enabled)); + + // UNC host access restrictions + processChanged(this.restrictUNCAccess.handleChange(config?.security?.restrictUNCAccess)); } // Experiments diff --git a/src/vs/workbench/contrib/remote/browser/remote.contribution.ts b/src/vs/workbench/contrib/remote/browser/remote.contribution.ts index 1041e179ddd..63e7f2df0b4 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.contribution.ts @@ -11,7 +11,6 @@ import { TunnelFactoryContribution } from 'vs/workbench/contrib/remote/browser/t import { RemoteAgentConnectionStatusListener, RemoteMarkers } from 'vs/workbench/contrib/remote/browser/remote'; import { RemoteStatusIndicator } from 'vs/workbench/contrib/remote/browser/remoteIndicator'; import { AutomaticPortForwarding, ForwardedPortsView, PortRestore } from 'vs/workbench/contrib/remote/browser/remoteExplorer'; -import { RemoteStartEntry } from 'vs/workbench/contrib/remote/browser/remoteStartEntry'; const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(ShowCandidateContribution, LifecyclePhase.Ready); @@ -22,4 +21,3 @@ workbenchContributionsRegistry.registerWorkbenchContribution(ForwardedPortsView, workbenchContributionsRegistry.registerWorkbenchContribution(PortRestore, LifecyclePhase.Eventually); workbenchContributionsRegistry.registerWorkbenchContribution(AutomaticPortForwarding, LifecyclePhase.Eventually); workbenchContributionsRegistry.registerWorkbenchContribution(RemoteMarkers, LifecyclePhase.Eventually); -workbenchContributionsRegistry.registerWorkbenchContribution(RemoteStartEntry, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index d1a87ba7fe2..b9b853d7ad1 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -316,7 +316,7 @@ class HelpItemValue { } else if (this.urlOrCommandOrId?.id) { try { const walkthroughId = `${this.extensionDescription.id}#${this.urlOrCommandOrId.id}`; - const walkthrough = this.walkthroughService.getWalkthrough(walkthroughId); + const walkthrough = await this.walkthroughService.getWalkthrough(walkthroughId); this._description = walkthrough.title; this._url = walkthroughId; } catch { } diff --git a/src/vs/workbench/contrib/search/browser/searchFindInput.ts b/src/vs/workbench/contrib/search/browser/searchFindInput.ts index 798cb5f6ecd..b63ffa716ff 100644 --- a/src/vs/workbench/contrib/search/browser/searchFindInput.ts +++ b/src/vs/workbench/contrib/search/browser/searchFindInput.ts @@ -63,7 +63,7 @@ export class SearchFindInput extends ContextScopedFindInput { // filter is checked if it's in a non-default state this._filterChecked = !this.filters.markupInput || - this.filters.markupPreview || + !this.filters.markupPreview || !this.filters.codeInput || !this.filters.codeOutput; diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index d384e5aeffe..b3aec586648 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -349,8 +349,7 @@ export class SearchView extends ViewPane { })); // folder includes list - const folderIncludesList = dom.append(this.queryDetails, - $('.file-types.includes')); + const folderIncludesList = dom.append(this.queryDetails, $('.file-types.includes')); const filesToIncludeTitle = nls.localize('searchScope.includes', "files to include"); dom.append(folderIncludesList, $('h4', undefined, filesToIncludeTitle)); @@ -689,6 +688,8 @@ export class SearchView extends ViewPane { errors.isCancellationError(error); this.notificationService.error(error); }); + } else { + progressComplete(); } }); } @@ -1994,6 +1995,11 @@ export class SearchView extends ViewPane { } public override saveState(): void { + // This can be called before renderBody() method gets called for the first time + // if we move the searchView inside another viewPaneContainer + if (!this.searchWidget) { + return; + } const patternExcludes = this.inputPatternExcludes?.getValue().trim() ?? ''; const patternIncludes = this.inputPatternIncludes?.getValue().trim() ?? ''; @@ -2010,6 +2016,7 @@ export class SearchView extends ViewPane { const isInNotebookCellInput = this.searchWidget.getNotebookFilters().codeInput; const isInNotebookCellOutput = this.searchWidget.getNotebookFilters().codeOutput; const isInNotebookMarkdownInput = this.searchWidget.getNotebookFilters().markupInput; + const isInNotebookMarkdownPreview = this.searchWidget.getNotebookFilters().markupPreview; this.viewletState['query.contentPattern'] = contentPattern; this.viewletState['query.regex'] = isRegex; @@ -2017,6 +2024,7 @@ export class SearchView extends ViewPane { this.viewletState['query.caseSensitive'] = isCaseSensitive; this.viewletState['query.isInNotebookMarkdownInput'] = isInNotebookMarkdownInput; + this.viewletState['query.isInNotebookMarkdownPreview'] = isInNotebookMarkdownPreview; this.viewletState['query.isInNotebookCellInput'] = isInNotebookCellInput; this.viewletState['query.isInNotebookCellOutput'] = isInNotebookCellOutput; } diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts index b7541e97760..a1511370fa3 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts @@ -131,6 +131,7 @@ export class SearchEditorInput extends EditorInput { readonly onDidChangeContent = input.onDidChangeContent; readonly onDidSave = input.onDidSave; isDirty(): boolean { return input.isDirty(); } + isModified(): boolean { return input.isDirty(); } backup(token: CancellationToken): Promise { return input.backup(token); } save(options?: ISaveOptions): Promise { return input.save(0, options).then(editor => !!editor); } revert(options?: IRevertOptions): Promise { return input.revert(0, options); } diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index 850c50249c3..7108ada6f8a 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -1277,8 +1277,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { } for (let i = 0; i < this._reconnectedTerminals.length; i++) { const terminal = this._reconnectedTerminals[i]; - const taskForTerminal = terminal.shellLaunchConfig.attachPersistentProcess?.reconnectionProperties?.data as IReconnectionTaskData; - if (taskForTerminal.lastTask === task.getCommonTaskId()) { + if (getReconnectionData(terminal)?.lastTask === task.getCommonTaskId()) { this._reconnectedTerminals.splice(i, 1); return terminal; } @@ -1323,19 +1322,18 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { this._logService.trace(`Already reconnected, to ${this._reconnectedTerminals?.length} terminals so returning`); return; } - this._reconnectedTerminals = this._terminalService.getReconnectedTerminals(ReconnectionType)?.filter(t => !t.isDisposed); + this._reconnectedTerminals = this._terminalService.getReconnectedTerminals(ReconnectionType)?.filter(t => !t.isDisposed && getReconnectionData(t)) || []; this._logService.trace(`Attempting reconnection of ${this._reconnectedTerminals?.length} terminals`); if (!this._reconnectedTerminals?.length) { this._logService.trace(`No terminals to reconnect to so returning`); } else { for (const terminal of this._reconnectedTerminals) { - const task = terminal.shellLaunchConfig.attachPersistentProcess?.reconnectionProperties?.data as IReconnectionTaskData; - this._logService.trace(`Reconnecting to task: ${JSON.stringify(task)}`); - if (!task) { - continue; + const data = getReconnectionData(terminal) as IReconnectionTaskData | undefined; + if (data) { + const terminalData = { lastTask: data.lastTask, group: data.group, terminal }; + this._terminals[terminal.instanceId] = terminalData; + this._logService.trace('Reconnecting to task terminal', terminalData.lastTask, terminal.instanceId); } - const terminalData = { lastTask: task.lastTask, group: task.group, terminal }; - this._terminals[terminal.instanceId] = terminalData; } } this._hasReconnected = true; @@ -1839,3 +1837,7 @@ function taskShellIntegrationWaitOnExitSequence(message: string): (exitCode: num return `${VSCodeSequence(VSCodeOscPt.CommandFinished, exitCode.toString())}${message}`; }; } + +function getReconnectionData(terminal: ITerminalInstance): IReconnectionTaskData | undefined { + return terminal.shellLaunchConfig.attachPersistentProcess?.reconnectionProperties?.data as IReconnectionTaskData | undefined; +} diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.fish b/src/vs/workbench/contrib/terminal/browser/media/fish_xdg_data/fish/vendor_conf.d/shellIntegration.fish similarity index 95% rename from src/vs/workbench/contrib/terminal/browser/media/shellIntegration.fish rename to src/vs/workbench/contrib/terminal/browser/media/fish_xdg_data/fish/vendor_conf.d/shellIntegration.fish index 35e79079b30..5a5e4b42a4c 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.fish +++ b/src/vs/workbench/contrib/terminal/browser/media/fish_xdg_data/fish/vendor_conf.d/shellIntegration.fish @@ -33,7 +33,7 @@ if test -n "$VSCODE_ENV_REPLACE" set ITEMS (string split : $VSCODE_ENV_REPLACE) for B in $ITEMS set split (string split = $B) - set -gx "$split[1]" "$split[2]" + set -gx "$split[1]" (echo -e "$split[2]") end set -e VSCODE_ENV_REPLACE end @@ -41,7 +41,7 @@ if test -n "$VSCODE_ENV_PREPEND" set ITEMS (string split : $VSCODE_ENV_PREPEND) for B in $ITEMS set split (string split = $B) - set -gx "$split[1]" "$split[2]$$split[1]" # avoid -p as it adds a space + set -gx "$split[1]" (echo -e "$split[2]")"$$split[1]" # avoid -p as it adds a space end set -e VSCODE_ENV_PREPEND end @@ -49,7 +49,7 @@ if test -n "$VSCODE_ENV_APPEND" set ITEMS (string split : $VSCODE_ENV_APPEND) for B in $ITEMS set split (string split = $B) - set -gx "$split[1]" "$$split[1]$split[2]" # avoid -a as it adds a space + set -gx "$split[1]" "$$split[1]"(echo -e "$split[2]") # avoid -a as it adds a space end set -e VSCODE_ENV_APPEND end diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh index 8f86b8eb6d4..8c4f4fbbef0 100755 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh @@ -50,7 +50,7 @@ if [ -n "$VSCODE_ENV_REPLACE" ]; then IFS=':' read -ra ADDR <<< "$VSCODE_ENV_REPLACE" for ITEM in "${ADDR[@]}"; do VARNAME="$(echo $ITEM | cut -d "=" -f 1)" - VALUE="$(echo $ITEM | cut -d "=" -f 2)" + VALUE="$(echo -e "$ITEM" | cut -d "=" -f 2)" export $VARNAME="$VALUE" done builtin unset VSCODE_ENV_REPLACE @@ -59,7 +59,7 @@ if [ -n "$VSCODE_ENV_PREPEND" ]; then IFS=':' read -ra ADDR <<< "$VSCODE_ENV_PREPEND" for ITEM in "${ADDR[@]}"; do VARNAME="$(echo $ITEM | cut -d "=" -f 1)" - VALUE="$(echo $ITEM | cut -d "=" -f 2)" + VALUE="$(echo -e "$ITEM" | cut -d "=" -f 2)" export $VARNAME="$VALUE${!VARNAME}" done builtin unset VSCODE_ENV_PREPEND @@ -68,7 +68,7 @@ if [ -n "$VSCODE_ENV_APPEND" ]; then IFS=':' read -ra ADDR <<< "$VSCODE_ENV_APPEND" for ITEM in "${ADDR[@]}"; do VARNAME="$(echo $ITEM | cut -d "=" -f 1)" - VALUE="$(echo $ITEM | cut -d "=" -f 2)" + VALUE="$(echo -e "$ITEM" | cut -d "=" -f 2)" export $VARNAME="${!VARNAME}$VALUE" done builtin unset VSCODE_ENV_APPEND diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh index 09a88949f87..d4e21dacda7 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh @@ -40,29 +40,26 @@ fi # Apply EnvironmentVariableCollections if needed if [ -n "$VSCODE_ENV_REPLACE" ]; then - echo "VSCODE_ENV_REPLACE: $VSCODE_ENV_REPLACE" IFS=':' read -rA ADDR <<< "$VSCODE_ENV_REPLACE" for ITEM in "${ADDR[@]}"; do VARNAME="$(echo ${ITEM%%=*})" - export $VARNAME="${ITEM#*=}" + export $VARNAME="$(echo -e ${ITEM#*=})" done unset VSCODE_ENV_REPLACE fi if [ -n "$VSCODE_ENV_PREPEND" ]; then - echo "VSCODE_ENV_PREPEND: $VSCODE_ENV_PREPEND" IFS=':' read -rA ADDR <<< "$VSCODE_ENV_PREPEND" for ITEM in "${ADDR[@]}"; do VARNAME="$(echo ${ITEM%%=*})" - export $VARNAME="${ITEM#*=}${(P)VARNAME}" + export $VARNAME="$(echo -e {ITEM#*=})${(P)VARNAME}" done unset VSCODE_ENV_PREPEND fi if [ -n "$VSCODE_ENV_APPEND" ]; then - echo "VSCODE_ENV_APPEND: $VSCODE_ENV_APPEND" IFS=':' read -rA ADDR <<< "$VSCODE_ENV_APPEND" for ITEM in "${ADDR[@]}"; do VARNAME="$(echo ${ITEM%%=*})" - export $VARNAME="${(P)VARNAME}${ITEM#*=}" + export $VARNAME="${(P)VARNAME}$(echo -e {ITEM#*=})" done unset VSCODE_ENV_APPEND fi diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 index cded6871aa8..e3201449086 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 @@ -25,7 +25,7 @@ if ($env:VSCODE_ENV_REPLACE) { $Split = $env:VSCODE_ENV_REPLACE.Split(":") foreach ($Item in $Split) { $Inner = $Item.Split('=') - [Environment]::SetEnvironmentVariable($Inner[0], $Inner[1]) + [Environment]::SetEnvironmentVariable($Inner[0], $Inner[1].Replace('\x3a', ':')) } $env:VSCODE_ENV_REPLACE = $null } @@ -33,7 +33,7 @@ if ($env:VSCODE_ENV_PREPEND) { $Split = $env:VSCODE_ENV_PREPEND.Split(":") foreach ($Item in $Split) { $Inner = $Item.Split('=') - [Environment]::SetEnvironmentVariable($Inner[0], $Inner[1] + [Environment]::GetEnvironmentVariable($Inner[0])) + [Environment]::SetEnvironmentVariable($Inner[0], $Inner[1].Replace('\x3a', ':') + [Environment]::GetEnvironmentVariable($Inner[0])) } $env:VSCODE_ENV_PREPEND = $null } @@ -41,7 +41,7 @@ if ($env:VSCODE_ENV_APPEND) { $Split = $env:VSCODE_ENV_APPEND.Split(":") foreach ($Item in $Split) { $Inner = $Item.Split('=') - [Environment]::SetEnvironmentVariable($Inner[0], [Environment]::GetEnvironmentVariable($Inner[0]) + $Inner[1]) + [Environment]::SetEnvironmentVariable($Inner[0], [Environment]::GetEnvironmentVariable($Inner[0]) + $Inner[1].Replace('\x3a', ':')) } $env:VSCODE_ENV_APPEND = $null } diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 684cfcc14aa..91c80d201f3 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -580,3 +580,7 @@ z-index: 33; background-color: var(--vscode-terminal-background, --vscode-panel-background); } + +.monaco-workbench .xterm.terminal.hide { + visibility: hidden; +} diff --git a/src/vs/workbench/contrib/terminal/browser/media/xterm.css b/src/vs/workbench/contrib/terminal/browser/media/xterm.css index e9d326036d0..3889931ba82 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/xterm.css +++ b/src/vs/workbench/contrib/terminal/browser/media/xterm.css @@ -153,6 +153,7 @@ right: 0; z-index: 10; color: transparent; + pointer-events: none; } .xterm .live-region { @@ -208,7 +209,3 @@ z-index: 2; position: relative; } - -.xterm.terminal.hide { - visibility: hidden; -} diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 863186f779f..0905d82207a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -185,7 +185,7 @@ export interface ITerminalService extends ITerminalInstanceHost { getActiveOrCreateInstance(options?: { acceptsInput?: boolean }): Promise; revealActiveTerminal(): Promise; moveToEditor(source: ITerminalInstance): void; - moveToTerminalView(source?: ITerminalInstance | URI): Promise; + moveToTerminalView(source: ITerminalInstance | URI): Promise; getPrimaryBackend(): ITerminalBackend | undefined; /** @@ -237,7 +237,6 @@ export interface ITerminalEditorService extends ITerminalInstanceHost { readonly instances: readonly ITerminalInstance[]; openEditor(instance: ITerminalInstance, editorOptions?: TerminalEditorLocation): Promise; - detachActiveEditorInstance(): ITerminalInstance; detachInstance(instance: ITerminalInstance): void; splitInstance(instanceToSplit: ITerminalInstance, shellLaunchConfig?: IShellLaunchConfig): ITerminalInstance; revealActiveEditor(preserveFocus?: boolean): Promise; @@ -956,6 +955,10 @@ export interface ITerminalChildElement { xtermReady?(xterm: IXtermTerminal): void; } +export const enum XtermTerminalConstants { + SearchHighlightLimit = 1000 +} + export interface IXtermTerminal { /** * An object that tracks when commands are run and enables navigating and selecting between @@ -969,7 +972,7 @@ export interface IXtermTerminal { readonly shellIntegration: IShellIntegration; readonly onDidChangeSelection: Event; - readonly onDidChangeFindResults: Event<{ resultIndex: number; resultCount: number } | undefined>; + readonly onDidChangeFindResults: Event<{ resultIndex: number; resultCount: number }>; /** * Gets a view of the current texture atlas used by the renderers. diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 96f2f086ce5..018b4bd55bc 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -266,7 +266,12 @@ export function registerTerminalActions() { id: TerminalCommandId.MoveToTerminalPanel, title: terminalStrings.moveToTerminalPanel, precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.terminalEditorActive), - run: (c, _, args) => c.service.moveToTerminalView(toOptionalUri(args)) + run: (c, _, args) => { + const source = toOptionalUri(args) ?? c.editorService.activeInstance; + if (source) { + c.service.moveToTerminalView(source); + } + } }); registerTerminalAction({ diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts index ac44495fb20..02217becd6a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts @@ -67,7 +67,11 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor this._terminalEditorActive.set(terminalEditorActive); if (terminalEditorActive) { activeEditor?.setGroup(this._editorService.activeEditorPane?.group); - this._setActiveInstance(instance); + this.setActiveInstance(instance); + } else { + for (const instance of this.instances) { + instance.resetFocusContextKey(); + } } })); this._register(this._editorService.onDidVisibleEditorsChange(() => { @@ -86,7 +90,6 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor this.instances.push(unknownEditor.terminalInstance); } })); - this._register(this.onDidDisposeInstance(instance => this.detachInstance(instance))); // Remove the terminal from the managed instances when the editor closes. This fires when // dragging and dropping to another editor or closing the editor via cmd/ctrl+w. @@ -95,15 +98,11 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor if (instance) { const instanceIndex = this.instances.findIndex(e => e === instance); if (instanceIndex !== -1) { - this.instances.splice(instanceIndex, 1); - } - } - })); - this._register(this._editorService.onDidActiveEditorChange(() => { - const instance = this._editorService.activeEditor instanceof TerminalEditorInput ? this._editorService.activeEditor : undefined; - if (!instance) { - for (const instance of this.instances) { - instance.resetFocusContextKey(); + const wasActiveInstance = this.instances[instanceIndex] === this.activeInstance; + this._removeInstance(instance); + if (wasActiveInstance) { + this.setActiveInstance(undefined); + } } } })); @@ -120,23 +119,15 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor return this.instances[this._activeInstanceIndex]; } - setActiveInstance(instance: ITerminalInstance): void { - this._setActiveInstance(instance); + setActiveInstance(instance: ITerminalInstance | undefined): void { + this._activeInstanceIndex = instance ? this.instances.findIndex(e => e === instance) : -1; + this._onDidChangeActiveInstance.fire(this.activeInstance); } async focusActiveInstance(): Promise { return this.activeInstance?.focusWhenReady(true); } - private _setActiveInstance(instance: ITerminalInstance | undefined): void { - if (instance === undefined) { - this._activeInstanceIndex = -1; - } else { - this._activeInstanceIndex = this.instances.findIndex(e => e === instance); - } - this._onDidChangeActiveInstance.fire(this.activeInstance); - } - async openEditor(instance: ITerminalInstance, editorOptions?: TerminalEditorLocation): Promise { const resource = this.resolveResource(instance); if (resource) { @@ -193,6 +184,21 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor this._onDidChangeInstances.fire(); } + private _removeInstance(instance: ITerminalInstance) { + const inputKey = instance.resource.path; + this._editorInputs.delete(inputKey); + const instanceIndex = this.instances.findIndex(e => e === instance); + if (instanceIndex !== -1) { + this.instances.splice(instanceIndex, 1); + } + const disposables = this._instanceDisposables.get(inputKey); + this._instanceDisposables.delete(inputKey); + if (disposables) { + dispose(disposables); + } + this._onDidChangeInstances.fire(); + } + getInstanceFromResource(resource?: URI): ITerminalInstance | undefined { return getInstanceFromResource(this.instances, resource); } @@ -232,39 +238,15 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor } } - detachActiveEditorInstance(): ITerminalInstance { - const activeEditor = this._editorService.activeEditor; - if (!(activeEditor instanceof TerminalEditorInput)) { - // should never happen now with the terminalEditorActive context key - throw new Error('Active editor is not a terminal'); - } - const instance = activeEditor.terminalInstance; - if (!instance) { - throw new Error('Terminal is already detached'); - } - this.detachInstance(instance); - return instance; - } - detachInstance(instance: ITerminalInstance) { const inputKey = instance.resource.path; const editorInput = this._editorInputs.get(inputKey); editorInput?.detachInstance(); - this._editorInputs.delete(inputKey); - const instanceIndex = this.instances.findIndex(e => e === instance); - if (instanceIndex !== -1) { - this.instances.splice(instanceIndex, 1); - } + this._removeInstance(instance); // Don't dispose the input when shutting down to avoid layouts in the editor area if (!this._isShuttingDown) { editorInput?.dispose(); } - const disposables = this._instanceDisposables.get(inputKey); - this._instanceDisposables.delete(inputKey); - if (disposables) { - dispose(disposables); - } - this._onDidChangeInstances.fire(); } async revealActiveEditor(preserveFocus?: boolean): Promise { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 8141b5dfb1f..12b9d913b1c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -89,6 +89,7 @@ import { TerminalExtensionsRegistry } from 'vs/workbench/contrib/terminal/browse import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { ResultKind } from 'vs/platform/keybinding/common/keybindingResolver'; import { getWorkspaceForTerminal } from 'vs/workbench/services/configurationResolver/common/terminalResolver'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; const enum Constants { /** @@ -485,8 +486,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { const defaultProfile = (await this._terminalProfileResolverService.getDefaultProfile({ remoteAuthority: this.remoteAuthority, os })); this.shellLaunchConfig.executable = defaultProfile.path; this.shellLaunchConfig.args = defaultProfile.args; - this.shellLaunchConfig.icon = defaultProfile.icon; - this.shellLaunchConfig.color = defaultProfile.color; + // Only use default icon and color if they are undefined in the SLC + this.shellLaunchConfig.icon ??= defaultProfile.icon; + this.shellLaunchConfig.color ??= defaultProfile.color; } await this._createProcess(); @@ -509,7 +511,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { }); this._register(this._configurationService.onDidChangeConfiguration(async e => { - if (e.affectsConfiguration('accessibility.verbosity.terminal')) { + if (e.affectsConfiguration(AccessibilityVerbositySettingId.Terminal)) { this._setAriaLabel(this.xterm?.raw, this._instanceId, this.title); } if (e.affectsConfiguration('terminal.integrated')) { @@ -1923,7 +1925,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { labelParts.push(nls.localize('terminalScreenReaderMode', "Run the command: Toggle Screen Reader Accessibility Mode for an optimized screen reader experience")); } const accessibilityHelpKeybinding = this._keybindingService.lookupKeybinding(TerminalCommandId.ShowTerminalAccessibilityHelp)?.getLabel(); - if (this._configurationService.getValue('accessibility.verbosity.terminal') && accessibilityHelpKeybinding) { + if (this._configurationService.getValue(AccessibilityVerbositySettingId.Terminal) && accessibilityHelpKeybinding) { labelParts.push(nls.localize('terminalHelpAriaLabel', "Use {0} for terminal accessibility help", accessibilityHelpKeybinding)); } xterm.textarea.setAttribute('aria-label', labelParts.join('\n')); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index fdb2516e15f..4a2e36cceca 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -506,8 +506,7 @@ export class TerminalService implements ITerminalService { return this.createTerminal(); } // Active instance, ensure accepts input - // Don't use task terminals or other terminals that don't accept input - if (!options?.acceptsInput || activeInstance?.shellLaunchConfig.type !== 'Task' && activeInstance.xterm?.isStdinDisabled !== true) { + if (!options?.acceptsInput || activeInstance.xterm?.isStdinDisabled !== true) { return activeInstance; } // Active instance doesn't accept input, create and focus @@ -745,20 +744,17 @@ export class TerminalService implements ITerminalService { this._terminalEditorService.openEditor(source); } - async moveToTerminalView(source?: ITerminalInstance, target?: ITerminalInstance, side?: 'before' | 'after'): Promise { + async moveToTerminalView(source?: ITerminalInstance | URI, target?: ITerminalInstance, side?: 'before' | 'after'): Promise { if (URI.isUri(source)) { source = this.getInstanceFromResource(source); } - if (source) { - this._terminalEditorService.detachInstance(source); - } else { - source = this._terminalEditorService.detachActiveEditorInstance(); - if (!source) { - return; - } + if (!source) { + return; } + this._terminalEditorService.detachInstance(source); + if (source.target !== TerminalLocation.Editor) { await this._terminalGroupService.showPanel(true); return; @@ -1018,13 +1014,14 @@ export class TerminalService implements ITerminalService { } private _addToReconnected(instance: ITerminalInstance): void { - if (instance.reconnectionProperties) { - const reconnectedTerminals = this._reconnectedTerminals.get(instance.reconnectionProperties.ownerId); - if (reconnectedTerminals) { - reconnectedTerminals.push(instance); - } else { - this._reconnectedTerminals.set(instance.reconnectionProperties.ownerId, [instance]); - } + if (!instance.reconnectionProperties?.ownerId) { + return; + } + const reconnectedTerminals = this._reconnectedTerminals.get(instance.reconnectionProperties.ownerId); + if (reconnectedTerminals) { + reconnectedTerminals.push(instance); + } else { + this._reconnectedTerminals.set(instance.reconnectionProperties.ownerId, [instance]); } } diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index adbbbbdb10d..3995b604773 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -18,7 +18,7 @@ import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IShellIntegration, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { ITerminalFont } from 'vs/workbench/contrib/terminal/common/terminal'; import { isSafari } from 'vs/base/browser/browser'; -import { IMarkTracker, IInternalXtermTerminal, IXtermTerminal, ISuggestController, IXtermColorProvider } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IMarkTracker, IInternalXtermTerminal, IXtermTerminal, ISuggestController, IXtermColorProvider, XtermTerminalConstants } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ILogService } from 'vs/platform/log/common/log'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { TerminalStorageKeys } from 'vs/workbench/contrib/terminal/common/terminalStorageKeys'; @@ -152,7 +152,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II readonly onDidRequestSendText = this._onDidRequestSendText.event; private readonly _onDidRequestFreePort = new Emitter(); readonly onDidRequestFreePort = this._onDidRequestFreePort.event; - private readonly _onDidChangeFindResults = new Emitter<{ resultIndex: number; resultCount: number } | undefined>(); + private readonly _onDidChangeFindResults = new Emitter<{ resultIndex: number; resultCount: number }>(); readonly onDidChangeFindResults = this._onDidChangeFindResults.event; private readonly _onDidChangeSelection = new Emitter(); readonly onDidChangeSelection = this._onDidChangeSelection.event; @@ -409,9 +409,9 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, II return this._searchAddon; } const AddonCtor = await this._getSearchAddonConstructor(); - this._searchAddon = new AddonCtor(); + this._searchAddon = new AddonCtor({ highlightLimit: XtermTerminalConstants.SearchHighlightLimit }); this.raw.loadAddon(this._searchAddon); - this._searchAddon.onDidChangeResults((results: { resultIndex: number; resultCount: number } | undefined) => { + this._searchAddon.onDidChangeResults((results: { resultIndex: number; resultCount: number }) => { this._lastFindResult = results; this._onDidChangeFindResults.fire(results); }); diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index b677e5799db..cb0d0c78fe5 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -269,7 +269,7 @@ const terminalConfiguration: IConfigurationNode = { default: 1 }, [TerminalSettingId.Scrollback]: { - description: localize('terminal.integrated.scrollback', "Controls the maximum number of lines the terminal keeps in its buffer."), + description: localize('terminal.integrated.scrollback', "Controls the maximum number of lines the terminal keeps in its buffer. We pre-allocate memory based on this value in order to ensure a smooth experience. As such, as the value increases, so will the amount of memory."), type: 'number', default: 1000 }, diff --git a/src/vs/workbench/contrib/terminalContrib/find/browser/terminalFindWidget.ts b/src/vs/workbench/contrib/terminalContrib/find/browser/terminalFindWidget.ts index 8a7d706834c..94311826099 100644 --- a/src/vs/workbench/contrib/terminalContrib/find/browser/terminalFindWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/find/browser/terminalFindWidget.ts @@ -6,7 +6,7 @@ import { SimpleFindWidget } from 'vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { ITerminalInstance, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalInstance, IXtermTerminal, XtermTerminalConstants } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -27,7 +27,7 @@ export class TerminalFindWidget extends SimpleFindWidget { @IThemeService private readonly _themeService: IThemeService, @IConfigurationService private readonly _configurationService: IConfigurationService ) { - super({ showCommonFindToggles: true, checkImeCompletionState: true, showResultCount: true, type: 'Terminal' }, _contextViewService, _contextKeyService, keybindingService); + super({ showCommonFindToggles: true, checkImeCompletionState: true, showResultCount: true, type: 'Terminal', matchesLimit: XtermTerminalConstants.SearchHighlightLimit }, _contextViewService, _contextKeyService, keybindingService); this._register(this.state.onFindReplaceStateChange(() => { this.show(); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts index 5b4255eabaa..8c2aa420360 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts @@ -31,6 +31,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { StartupPageContribution, } from 'vs/workbench/contrib/welcomeGettingStarted/browser/startupPage'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; +import { RemoteStartEntry } from 'vs/workbench/contrib/remote/browser/remoteStartEntry'; export * as icons from 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedIcons'; @@ -205,11 +206,11 @@ registerAction2(class extends Action2 { }); } - private getQuickPickItems( + private async getQuickPickItems( contextService: IContextKeyService, gettingStartedService: IWalkthroughsService - ): IQuickPickItem[] { - const categories = gettingStartedService.getWalkthroughs(); + ): Promise { + const categories = await gettingStartedService.getWalkthroughs(); return categories .filter(c => contextService.contextMatchesRules(c.when)) .map(x => ({ @@ -232,7 +233,7 @@ registerAction2(class extends Action2 { quickPick.matchOnDescription = true; quickPick.matchOnDetail = true; quickPick.placeholder = localize('pickWalkthroughs', 'Select a walkthrough to open'); - quickPick.items = this.getQuickPickItems(contextService, gettingStartedService); + quickPick.items = await this.getQuickPickItems(contextService, gettingStartedService); quickPick.busy = true; quickPick.onDidAccept(() => { const selection = quickPick.selectedItems[0]; @@ -245,8 +246,7 @@ registerAction2(class extends Action2 { quickPick.show(); await extensionService.whenInstalledExtensionsRegistered(); quickPick.busy = false; - await gettingStartedService.installedExtensionsRegistered; - quickPick.items = this.getQuickPickItems(contextService, gettingStartedService); + quickPick.items = await this.getQuickPickItems(contextService, gettingStartedService); } }); @@ -325,3 +325,6 @@ configurationRegistry.registerConfiguration({ Registry.as(WorkbenchExtensions.Workbench) .registerWorkbenchContribution(StartupPageContribution, LifecyclePhase.Restored); + +Registry.as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(RemoteStartEntry, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 26a2070b944..7fd6d1e2904 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -29,7 +29,6 @@ import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configur import { ContextKeyExpr, ContextKeyExpression, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IRecentFolder, IRecentlyOpened, IRecentWorkspace, isRecentFolder, isRecentWorkspace, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { onUnexpectedError } from 'vs/base/common/errors'; import { ILabelService, Verbosity } from 'vs/platform/label/common/label'; import { IWindowOpenable } from 'vs/platform/window/common/window'; import { splitName } from 'vs/base/common/labels'; @@ -74,6 +73,7 @@ import { IFeaturedExtensionsService } from 'vs/workbench/contrib/welcomeGettingS import { IFeaturedExtension } from 'vs/base/common/product'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; const SLIDE_TRANSITION_TIME_MS = 250; const configurationKey = 'workbench.startupEditor'; @@ -129,8 +129,11 @@ export class GettingStartedPage extends EditorPane { private stepDisposables: DisposableStore = new DisposableStore(); private detailsPageDisposables: DisposableStore = new DisposableStore(); - private gettingStartedCategories: IResolvedWalkthrough[]; - private featuredExtensions?: Promise; + // Ensure that the these are initialized before use. + // Currently initialized before use in buildCategoriesSlide and scrollToCategory + private recentlyOpened!: IRecentlyOpened; + private gettingStartedCategories!: IResolvedWalkthrough[]; + private featuredExtensions!: IFeaturedExtension[]; private currentWalkthrough: IResolvedWalkthrough | undefined; @@ -145,7 +148,6 @@ export class GettingStartedPage extends EditorPane { private contextService: IContextKeyService; - private recentlyOpened: Promise; private hasScrolledToFirstCategory = false; private recentlyOpenedList?: GettingStartedIndexList; private startList?: GettingStartedIndexList; @@ -182,13 +184,14 @@ export class GettingStartedPage extends EditorPane { @IEditorGroupsService private readonly groupsService: IEditorGroupsService, @IContextKeyService contextService: IContextKeyService, @IQuickInputService private quickInputService: IQuickInputService, - @IWorkspacesService workspacesService: IWorkspacesService, + @IWorkspacesService private readonly workspacesService: IWorkspacesService, @ILabelService private readonly labelService: ILabelService, @IHostService private readonly hostService: IHostService, @IWebviewService private readonly webviewService: IWebviewService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @ILifecycleService private readonly lifecycleService: ILifecycleService, ) { super(GettingStartedPage.ID, telemetryService, themeService, storageService); @@ -209,28 +212,11 @@ export class GettingStartedPage extends EditorPane { this.contextService = this._register(contextService.createScoped(this.container)); inWelcomeContext.bindTo(this.contextService).set(true); - this.gettingStartedCategories = this.gettingStartedService.getWalkthroughs(); - this.featuredExtensions = this.featuredExtensionService.getExtensions(); - this._register(this.dispatchListeners); this.buildSlideThrottle = new Throttler(); const rerender = () => { - this.gettingStartedCategories = this.gettingStartedService.getWalkthroughs(); - this.featuredExtensions = this.featuredExtensionService.getExtensions(); - - if (this.currentWalkthrough) { - const existingSteps = this.currentWalkthrough.steps.map(step => step.id); - const newCategory = this.gettingStartedCategories.find(category => this.currentWalkthrough?.id === category.id); - if (newCategory) { - const newSteps = newCategory.steps.map(step => step.id); - if (!equals(newSteps, existingSteps)) { - this.buildSlideThrottle.queue(() => this.buildCategoriesSlide()); - } - } - } else { - this.buildSlideThrottle.queue(() => this.buildCategoriesSlide()); - } + this.buildSlideThrottle.queue(async () => await this.buildCategoriesSlide()); }; this._register(this.extensionManagementService.onDidInstallExtensions(async (result) => { @@ -242,30 +228,9 @@ export class GettingStartedPage extends EditorPane { } })); - this._register(this.gettingStartedService.onDidAddBuiltInWalkthrough(() => { - rerender(); - const someStepsComplete = this.gettingStartedCategories.some(category => category.steps.find(s => s.done)); - if (!this.productService.openToWelcomeMainPage && !someStepsComplete && !this.hasScrolledToFirstCategory) { - const firstSessionDateString = this.storageService.get(firstSessionDateStorageKey, StorageScope.APPLICATION) || new Date().toUTCString(); - const daysSinceFirstSession = ((+new Date()) - (+new Date(firstSessionDateString))) / 1000 / 60 / 60 / 24; - const fistContentBehaviour = daysSinceFirstSession < 1 ? 'openToFirstCategory' : 'index'; - - if (fistContentBehaviour === 'openToFirstCategory') { - const first = this.gettingStartedCategories.filter(c => !c.when || this.contextService.contextMatchesRules(c.when))[0]; - this.hasScrolledToFirstCategory = true; - if (first) { - this.currentWalkthrough = first; - this.editorInput.selectedCategory = this.currentWalkthrough?.id; - this.buildCategorySlide(this.editorInput.selectedCategory, undefined); - this.setSlide('details'); - return; - } - } - } - })); - this._register(this.gettingStartedService.onDidAddWalkthrough(rerender)); this._register(this.gettingStartedService.onDidRemoveWalkthrough(rerender)); + this._register(workspacesService.onDidChangeRecentlyOpened(rerender)); this._register(this.gettingStartedService.onDidChangeWalkthrough(category => { const ourCategory = this.gettingStartedCategories.find(c => c.id === category.id); @@ -315,12 +280,6 @@ export class GettingStartedPage extends EditorPane { } this.updateCategoryProgress(); })); - - this.recentlyOpened = workspacesService.getRecentlyOpened(); - this._register(workspacesService.onDidChangeRecentlyOpened(() => { - this.recentlyOpened = workspacesService.getRecentlyOpened(); - rerender(); - })); } // remove when 'workbench.welcomePage.preferReducedMotion' deprecated @@ -345,6 +304,7 @@ export class GettingStartedPage extends EditorPane { override async setInput(newInput: GettingStartedInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken) { this.container.classList.remove('animatable'); this.editorInput = newInput; + await this.lifecycleService.when(LifecyclePhase.Restored); await super.setInput(newInput, options, context, token); await this.buildCategoriesSlide(); if (this.shouldAnimate()) { @@ -353,16 +313,6 @@ export class GettingStartedPage extends EditorPane { } async makeCategoryVisibleWhenAvailable(categoryID: string, stepId?: string) { - if (!this.gettingStartedCategories.some(c => c.id === categoryID)) { - await this.gettingStartedService.installedExtensionsRegistered; - this.gettingStartedCategories = this.gettingStartedService.getWalkthroughs(); - } - - const ourCategory = this.gettingStartedCategories.find(c => c.id === categoryID); - if (!ourCategory) { - throw Error('Could not find category with ID: ' + categoryID); - } - this.scrollToCategory(categoryID, stepId); } @@ -419,12 +369,8 @@ export class GettingStartedPage extends EditorPane { break; } case 'selectCategory': { - const selectedCategory = this.gettingStartedCategories.find(category => category.id === argument); - if (!selectedCategory) { throw Error('Could not find category with ID ' + argument); } - - this.gettingStartedService.markWalkthroughOpened(argument); - this.gettingStartedList?.setEntries(this.gettingStartedService.getWalkthroughs()); this.scrollToCategory(argument); + this.gettingStartedService.markWalkthroughOpened(argument); break; } case 'selectStartEntry': { @@ -717,8 +663,6 @@ export class GettingStartedPage extends EditorPane { } private async selectStep(id: string | undefined, delayFocus = true) { - if (id && this.editorInput.selectedStep === id) { return; } - if (id) { let stepElement = this.container.querySelector(`[data-step-id="${id}"]`); if (!stepElement) { @@ -784,6 +728,11 @@ export class GettingStartedPage extends EditorPane { } private async buildCategoriesSlide() { + + this.recentlyOpened = await this.workspacesService.getRecentlyOpened(); + this.gettingStartedCategories = await this.gettingStartedService.getWalkthroughs(); + this.featuredExtensions = await this.featuredExtensionService.getExtensions(); + this.categoriesSlideDisposables.clear(); const showOnStartupCheckbox = new Toggle({ icon: Codicon.check, @@ -883,10 +832,6 @@ export class GettingStartedPage extends EditorPane { this.currentWalkthrough = this.gettingStartedCategories.find(category => category.id === this.editorInput.selectedCategory); if (!this.currentWalkthrough) { - this.container.classList.add('loading'); - await this.gettingStartedService.installedExtensionsRegistered; - this.container.classList.remove('loading'); - this.gettingStartedCategories = this.gettingStartedService.getWalkthroughs(); this.currentWalkthrough = this.gettingStartedCategories.find(category => category.id === this.editorInput.selectedCategory); } @@ -895,17 +840,35 @@ export class GettingStartedPage extends EditorPane { this.editorInput.selectedCategory = undefined; this.editorInput.selectedStep = undefined; } else { - this.buildCategorySlide(this.editorInput.selectedCategory, this.editorInput.selectedStep); + await this.buildCategorySlide(this.editorInput.selectedCategory, this.editorInput.selectedStep); this.setSlide('details'); return; } } + const someStepsComplete = this.gettingStartedCategories.some(category => category.steps.find(s => s.done)); if (this.editorInput.showTelemetryNotice && this.productService.openToWelcomeMainPage) { const telemetryNotice = $('p.telemetry-notice'); this.buildTelemetryFooter(telemetryNotice); footer.appendChild(telemetryNotice); + } else if (!this.productService.openToWelcomeMainPage && !someStepsComplete && !this.hasScrolledToFirstCategory) { + const firstSessionDateString = this.storageService.get(firstSessionDateStorageKey, StorageScope.APPLICATION) || new Date().toUTCString(); + const daysSinceFirstSession = ((+new Date()) - (+new Date(firstSessionDateString))) / 1000 / 60 / 60 / 24; + const fistContentBehaviour = daysSinceFirstSession < 1 ? 'openToFirstCategory' : 'index'; + + if (fistContentBehaviour === 'openToFirstCategory') { + const first = this.gettingStartedCategories.filter(c => !c.when || this.contextService.contextMatchesRules(c.when))[0]; + this.hasScrolledToFirstCategory = true; + if (first) { + this.currentWalkthrough = first; + this.editorInput.selectedCategory = this.currentWalkthrough?.id; + await this.buildCategorySlide(this.editorInput.selectedCategory, undefined); + this.setSlide('details'); + return; + } + } } + this.setSlide('categories'); } @@ -974,20 +937,10 @@ export class GettingStartedPage extends EditorPane { recentlyOpenedList.onDidChange(() => this.registerDispatchListeners()); - this.recentlyOpened.then(({ workspaces }) => { - // Filter out the current workspace - const workspacesWithID = workspaces - .filter(recent => !this.workspaceContextService.isCurrentWorkspace(isRecentWorkspace(recent) ? recent.workspace : recent.folderUri)) - .map(recent => ({ ...recent, id: isRecentWorkspace(recent) ? recent.workspace.id : recent.folderUri.toString() })); + const entries = this.recentlyOpened.workspaces.filter(recent => !this.workspaceContextService.isCurrentWorkspace(isRecentWorkspace(recent) ? recent.workspace : recent.folderUri)) + .map(recent => ({ ...recent, id: isRecentWorkspace(recent) ? recent.workspace.id : recent.folderUri.toString() })); - const updateEntries = () => { - recentlyOpenedList.setEntries(workspacesWithID); - }; - - updateEntries(); - - recentlyOpenedList.register(this.labelService.onDidChangeFormatters(() => updateEntries())); - }).catch(onUnexpectedError); + recentlyOpenedList.setEntries(entries); return recentlyOpenedList; } @@ -1151,14 +1104,12 @@ export class GettingStartedPage extends EditorPane { contextService: this.contextService, }); - this.featuredExtensions?.then(extensions => { - featuredExtensionsList.setEntries(extensions); - }); + featuredExtensionsList.setEntries(this.featuredExtensions); this.featuredExtensionsList?.onDidChange(() => { - this.registerDispatchListeners(); }); + return featuredExtensionsList; } @@ -1207,12 +1158,22 @@ export class GettingStartedPage extends EditorPane { } private async scrollToCategory(categoryID: string, stepId?: string) { + + if (!this.gettingStartedCategories.some(c => c.id === categoryID)) { + this.gettingStartedCategories = await this.gettingStartedService.getWalkthroughs(); + } + + const ourCategory = this.gettingStartedCategories.find(c => c.id === categoryID); + if (!ourCategory) { + throw Error('Could not find category with ID: ' + categoryID); + } + this.inProgressScroll = this.inProgressScroll.then(async () => { reset(this.stepsContent); this.editorInput.selectedCategory = categoryID; this.editorInput.selectedStep = stepId; - this.currentWalkthrough = this.gettingStartedCategories.find(category => category.id === categoryID); - this.buildCategorySlide(categoryID); + this.currentWalkthrough = ourCategory; + await this.buildCategorySlide(categoryID); this.setSlide('details'); }); } @@ -1345,13 +1306,13 @@ export class GettingStartedPage extends EditorPane { super.clearInput(); } - private buildCategorySlide(categoryID: string, selectedStep?: string) { + private async buildCategorySlide(categoryID: string, selectedStep?: string) { if (this.detailsScrollbar) { this.detailsScrollbar.dispose(); } - this.extensionService.whenInstalledExtensionsRegistered().then(() => { - // Remove internal extension id specifier from exposed id's - this.extensionService.activateByEvent(`onWalkthrough:${categoryID.replace(/[^#]+#/, '')}`); - }); + await this.extensionService.whenInstalledExtensionsRegistered(); + + // Remove internal extension id specifier from exposed id's + await this.extensionService.activateByEvent(`onWalkthrough:${categoryID.replace(/[^#]+#/, '')}`); this.detailsPageDisposables.clear(); @@ -1541,12 +1502,14 @@ export class GettingStartedPage extends EditorPane { if (toEnable === 'categories') { slideManager.classList.remove('showDetails'); slideManager.classList.add('showCategories'); + this.container.querySelector('.prev-button.button-link')!.style.display = 'none'; this.container.querySelector('.gettingStartedSlideDetails')!.querySelectorAll('button').forEach(button => button.disabled = true); this.container.querySelector('.gettingStartedSlideCategories')!.querySelectorAll('button').forEach(button => button.disabled = false); this.container.querySelector('.gettingStartedSlideCategories')!.querySelectorAll('input').forEach(button => button.disabled = false); } else { slideManager.classList.add('showDetails'); slideManager.classList.remove('showCategories'); + this.container.querySelector('.prev-button.button-link')!.style.display = 'block'; this.container.querySelector('.gettingStartedSlideDetails')!.querySelectorAll('button').forEach(button => button.disabled = false); this.container.querySelector('.gettingStartedSlideCategories')!.querySelectorAll('button').forEach(button => button.disabled = true); this.container.querySelector('.gettingStartedSlideCategories')!.querySelectorAll('input').forEach(button => button.disabled = true); @@ -1554,7 +1517,18 @@ export class GettingStartedPage extends EditorPane { } override focus() { - this.container.focus(); + const active = document.activeElement; + + let parent = this.container.parentElement; + while (parent && parent !== active) { + parent = parent.parentElement; + } + + if (parent) { + // Only set focus if there is no other focued element outside this chain. + // This prevents us from stealing back focus from other focused elements such as quick pick due to delayed load. + this.container.focus(); + } } } diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts index 2bed700f645..3090b7b71fb 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts @@ -35,7 +35,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { DefaultIconPath } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IProductService } from 'vs/platform/product/common/productService'; -import { disposableTimeout } from 'vs/base/common/async'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; export const HasMultipleNewFileEntries = new RawContextKey('hasMultipleNewFileEntries', false); @@ -97,12 +97,9 @@ export interface IWalkthroughsService { readonly onDidRemoveWalkthrough: Event; readonly onDidChangeWalkthrough: Event; readonly onDidProgressStep: Event; - readonly onDidAddBuiltInWalkthrough: Event; - readonly installedExtensionsRegistered: Promise; - - getWalkthroughs(): IResolvedWalkthrough[]; - getWalkthrough(id: string): IResolvedWalkthrough; + getWalkthroughs(): Promise; + getWalkthrough(id: string): Promise; registerWalkthrough(descriptor: IWalkthroughLoose): void; @@ -135,9 +132,6 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ private readonly _onDidProgressStep = new Emitter(); readonly onDidProgressStep: Event = this._onDidProgressStep.event; - private readonly _onDidAddBuiltInWalkthrough = new Emitter(); - readonly onDidAddBuiltInWalkthrough: Event = this._onDidAddBuiltInWalkthrough.event; - private memento: Memento; private stepProgress: Record; @@ -147,18 +141,15 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ private gettingStartedContributions = new Map(); private steps = new Map(); - private tasExperimentService?: IWorkbenchAssignmentService; private sessionInstalledExtensions: Set = new Set(); private categoryVisibilityContextKeys = new Set(); private stepCompletionContextKeyExpressions = new Set(); private stepCompletionContextKeys = new Set(); - private triggerInstalledExtensionsRegistered!: () => void; - installedExtensionsRegistered: Promise; - private metadata: WalkthroughMetaDataType; + private registeredWalkthroughs: boolean = false; constructor( @IStorageService private readonly storageService: IStorageService, @ICommandService private readonly commandService: ICommandService, @@ -171,13 +162,12 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ @IHostService private readonly hostService: IHostService, @IViewsService private readonly viewsService: IViewsService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IWorkbenchAssignmentService tasExperimentService: IWorkbenchAssignmentService, + @IWorkbenchAssignmentService private readonly tasExperimentService: IWorkbenchAssignmentService, @IProductService private readonly productService: IProductService, + @ILifecycleService private readonly lifecycleService: ILifecycleService, ) { super(); - this.tasExperimentService = tasExperimentService; - this.metadata = new Map( JSON.parse( this.storageService.get(walkthroughMetadataConfigurationKey, StorageScope.PROFILE, '[]'))); @@ -185,23 +175,12 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ this.memento = new Memento('gettingStartedService', this.storageService); this.stepProgress = this.memento.getMemento(StorageScope.PROFILE, StorageTarget.USER); - walkthroughsExtensionPoint.setHandler(async (_, { added, removed }) => { - await Promise.all( - [...added.map(e => this.registerExtensionWalkthroughContributions(e.description)), - ...removed.map(e => this.unregisterExtensionWalkthroughContributions(e.description))]); - this.triggerInstalledExtensionsRegistered(); - }); - this.initCompletionEventListeners(); HasMultipleNewFileEntries.bindTo(this.contextService).set(false); - - this.installedExtensionsRegistered = new Promise(r => this.triggerInstalledExtensionsRegistered = r); - - this._register(disposableTimeout(() => this.registerBuiltInWalkthroughs().finally(/*do nothing*/), 0)); } - private async registerBuiltInWalkthroughs() { + private async registerWalkthroughs() { const treatmentString = await Promise.race([ this.tasExperimentService?.getTreatment('welcome.walkthrough.content'), @@ -260,7 +239,12 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ }); }); - this._onDidAddBuiltInWalkthrough.fire(); + await this.lifecycleService.when(LifecyclePhase.Restored); + + walkthroughsExtensionPoint.setHandler((_, { added, removed }) => { + added.map(e => this.registerExtensionWalkthroughContributions(e.description)); + removed.map(e => this.unregisterExtensionWalkthroughContributions(e.description)); + }); } private updateWalkthroughContent(walkthrough: BuiltinGettingStartedCategory, experimentTreatment: WalkthroughTreatment): BuiltinGettingStartedCategory { @@ -509,13 +493,23 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ }); } - getWalkthrough(id: string): IResolvedWalkthrough { + async getWalkthrough(id: string): Promise { + if (!this.registeredWalkthroughs) { + await this.registerWalkthroughs(); + this.registeredWalkthroughs = true; + } + const walkthrough = this.gettingStartedContributions.get(id); if (!walkthrough) { throw Error('Trying to get unknown walkthrough: ' + id); } return this.resolveWalkthrough(walkthrough); } - getWalkthroughs(): IResolvedWalkthrough[] { + async getWalkthroughs(): Promise { + if (!this.registeredWalkthroughs) { + await this.registerWalkthroughs(); + this.registeredWalkthroughs = true; + } + const registeredCategories = [...this.gettingStartedContributions.values()]; const categoriesWithCompletion = registeredCategories .map(category => { @@ -693,9 +687,6 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ } this.registerCompletionListener(event, step); - if (this.sessionEvents.has(event)) { - this.progressStep(step.id); - } } } diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css index 792cf409e49..5019352f008 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css @@ -678,6 +678,7 @@ padding: 0 2px 2px; margin: 10px; z-index: 1; + display: none; } .monaco-workbench .part.editor>.content .gettingStartedContainer.width-semi-constrained .prev-button.button-link { diff --git a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts index d15a25b1bb1..5e88a041e7d 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts @@ -90,7 +90,7 @@ suite('WorkspaceContextService - Folder', () => { const uriIdentityService = new UriIdentityService(fileService); const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService); const userDataProfileService = new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService); - testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(new RemoteSocketFactoryService(), userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(false, undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService())); + testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(new RemoteSocketFactoryService(), userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(false, undefined, undefined, TestProductService, logService), new SignService(TestProductService), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService())); await (testObject).initialize(convertToWorkspacePayload(folder)); }); @@ -133,7 +133,7 @@ suite('WorkspaceContextService - Folder', () => { const uriIdentityService = new UriIdentityService(fileService); const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService); const userDataProfileService = new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService); - const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(new RemoteSocketFactoryService(), userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(false, undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService())); + const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(new RemoteSocketFactoryService(), userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(false, undefined, undefined, TestProductService, logService), new SignService(TestProductService), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService())); await (testObject).initialize(convertToWorkspacePayload(folder)); const actual = testObject.getWorkspaceFolder(joinPath(folder, 'a')); @@ -156,7 +156,7 @@ suite('WorkspaceContextService - Folder', () => { const uriIdentityService = new UriIdentityService(fileService); const userDataProfilesService = new UserDataProfilesService(environmentService, fileService, uriIdentityService, logService); const userDataProfileService = new UserDataProfileService(userDataProfilesService.defaultProfile, userDataProfilesService); - const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(new RemoteSocketFactoryService(), userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(false, undefined, undefined, TestProductService, logService), new SignService(undefined), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService())); + const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, userDataProfileService, userDataProfilesService, fileService, new RemoteAgentService(new RemoteSocketFactoryService(), userDataProfileService, environmentService, TestProductService, new RemoteAuthorityResolverService(false, undefined, undefined, TestProductService, logService), new SignService(TestProductService), new NullLogService()), uriIdentityService, new NullLogService(), new NullPolicyService())); await (testObject).initialize(convertToWorkspacePayload(folder)); 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 1e75fe2429d..2ff788fd408 100644 --- a/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IEditorFactoryRegistry, EditorExtensions } from 'vs/workbench/common/editor'; +import { IEditorFactoryRegistry, EditorExtensions, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; import { workbenchInstantiationService, TestFileEditorInput, registerTestEditor, TestEditorPart, createEditorPart, registerTestSideBySideEditor } from 'vs/workbench/test/browser/workbenchTestServices'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -628,4 +628,35 @@ suite('EditorsObserver', function () { assert.strictEqual(observer.hasEditor({ resource: input3.resource, typeId: input3.typeId, editorId: input3.editorId }), true); assert.strictEqual(observer.hasEditor({ resource: input4.resource, typeId: input4.typeId, editorId: input4.editorId }), true); }); + + test('observer does not close scratchpads', async () => { + const [part] = await createPart(); + part.enforcePartOptions({ limit: { enabled: true, value: 3 } }); + + const storage = new TestStorageService(); + const observer = disposables.add(new EditorsObserver(part, storage)); + + const rootGroup = part.activeGroup; + + const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_EDITOR_INPUT_ID); + input1.capabilities = EditorInputCapabilities.Untitled | EditorInputCapabilities.Scratchpad; + 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, { pinned: true }); + await rootGroup.openEditor(input2, { pinned: true }); + await rootGroup.openEditor(input3, { pinned: true }); + await rootGroup.openEditor(input4, { pinned: true }); + + assert.strictEqual(rootGroup.count, 3); + assert.strictEqual(rootGroup.contains(input1), true); + assert.strictEqual(rootGroup.contains(input2), false); + assert.strictEqual(rootGroup.contains(input3), true); + assert.strictEqual(rootGroup.contains(input4), true); + assert.strictEqual(observer.hasEditor({ resource: input1.resource, typeId: input1.typeId, editorId: input1.editorId }), true); + assert.strictEqual(observer.hasEditor({ resource: input2.resource, typeId: input2.typeId, editorId: input2.editorId }), false); + assert.strictEqual(observer.hasEditor({ resource: input3.resource, typeId: input3.typeId, editorId: input3.editorId }), true); + assert.strictEqual(observer.hasEditor({ resource: input4.resource, typeId: input4.typeId, editorId: input4.editorId }), true); + }); }); diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index d5055feaba1..0facecce253 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -8,7 +8,7 @@ export const allApiProposals = Object.freeze({ authGetSessions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authGetSessions.d.ts', authSession: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authSession.d.ts', - canonicalUriIdentityProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.canonicalUriIdentityProvider.d.ts', + canonicalUriProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts', codiconDecoration: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codiconDecoration.d.ts', commentsDraftState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentsDraftState.d.ts', contribCommentEditorActionsMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentEditorActionsMenu.d.ts', diff --git a/src/vs/workbench/services/hover/browser/hoverWidget.ts b/src/vs/workbench/services/hover/browser/hoverWidget.ts index 0dc0d9f4830..772538f3a33 100644 --- a/src/vs/workbench/services/hover/browser/hoverWidget.ts +++ b/src/vs/workbench/services/hover/browser/hoverWidget.ts @@ -201,8 +201,9 @@ export class HoverWidget extends Widget { } else { if (options.hideOnHover === undefined) { // When unset, will default to true when it's a string or when it's markdown that - // appears to have a link using a naive check for '](' - hideOnHover = typeof options.content === 'string' || isMarkdownString(options.content) && !options.content.value.includes(']('); + // appears to have a link using a naive check for '](' and '' + hideOnHover = typeof options.content === 'string' || + isMarkdownString(options.content) && !options.content.value.includes('](') && !options.content.value.includes(''); } else { // It's set explicitly hideOnHover = options.hideOnHover; diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 935a36e5d76..cc10d5e4734 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -674,6 +674,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return this.dirty; } + isModified(): boolean { + return this.isDirty(); + } + setDirty(dirty: boolean): void { if (!this.isResolved()) { return; // only resolved models can be marked dirty diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts index 5e2241175f8..7595d417a5f 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts @@ -104,6 +104,7 @@ suite('Files - TextFileEditorModel', () => { model.updateTextEditorModel(createTextBufferFactory('bar')); assert.ok(getLastModifiedTime(model) <= Date.now()); assert.ok(model.hasState(TextFileEditorModelState.DIRTY)); + assert.ok(model.isModified()); assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), true); @@ -123,6 +124,7 @@ suite('Files - TextFileEditorModel', () => { assert.ok(model.hasState(TextFileEditorModelState.SAVED)); assert.ok(!model.isDirty()); + assert.ok(!model.isModified()); assert.ok(savedEvent); assert.ok((savedEvent as ITextFileEditorModelSaveEvent).stat); assert.strictEqual((savedEvent as ITextFileEditorModelSaveEvent).reason, SaveReason.AUTO); @@ -182,6 +184,7 @@ suite('Files - TextFileEditorModel', () => { assert.ok(model.hasState(TextFileEditorModelState.ERROR)); assert.ok(model.isDirty()); + assert.ok(model.isModified()); assert.ok(saveErrorEvent); assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); @@ -238,6 +241,7 @@ suite('Files - TextFileEditorModel', () => { assert.ok(model.hasState(TextFileEditorModelState.ERROR)); assert.ok(model.isDirty()); + assert.ok(model.isModified()); assert.ok(saveErrorEvent); assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); @@ -268,6 +272,7 @@ suite('Files - TextFileEditorModel', () => { assert.ok(model.hasState(TextFileEditorModelState.CONFLICT)); assert.ok(model.isDirty()); + assert.ok(model.isModified()); assert.ok(saveErrorEvent); assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); @@ -457,6 +462,7 @@ suite('Files - TextFileEditorModel', () => { await model.resolve(); model.updateTextEditorModel(createTextBufferFactory('foo')); assert.ok(model.isDirty()); + assert.ok(model.isModified()); assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), true); @@ -471,6 +477,7 @@ suite('Files - TextFileEditorModel', () => { model = accessor.workingCopyService.get(model) as TextFileEditorModel; assert.strictEqual(model.isDirty(), false); + assert.strictEqual(model.isModified(), false); assert.strictEqual(eventCounter, 1); assert.ok(workingCopyEvent); @@ -497,12 +504,14 @@ suite('Files - TextFileEditorModel', () => { await model.resolve(); model.updateTextEditorModel(createTextBufferFactory('foo')); assert.ok(model.isDirty()); + assert.ok(model.isModified()); assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), true); await model.revert({ soft: true }); assert.strictEqual(model.isDirty(), false); + assert.strictEqual(model.isModified(), false); assert.strictEqual(model.textEditorModel!.getValue(), 'foo'); assert.strictEqual(eventCounter, 1); diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts index d1b613383bc..41ff4edf313 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts @@ -243,6 +243,10 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt return this.dirty; } + isModified(): boolean { + return this.isDirty(); + } + private setDirty(dirty: boolean): void { if (this.dirty === dirty) { return; diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts index 44ca40c9fdf..798d14dce33 100644 --- a/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileManagement.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationError } from 'vs/base/common/errors'; import { Disposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -117,7 +118,9 @@ export class UserDataProfileManagementService extends Disposable implements IUse const isRemoteWindow = !!this.environmentService.remoteAuthority; if (!isRemoteWindow) { - this.extensionService.stopExtensionHosts(true); // TODO@sandy081 adopt support for extension host to veto stopping + if (!(await this.extensionService.stopExtensionHosts(localize('switch profile', "Switching Profile")))) { + throw new CancellationError(); + } } // In a remote window update current profile before reloading so that data is preserved from current profile if asked to preserve diff --git a/src/vs/workbench/services/workingCopy/browser/workingCopyBackupTracker.ts b/src/vs/workbench/services/workingCopy/browser/workingCopyBackupTracker.ts index 82c5d7a8b83..2b21497e1bc 100644 --- a/src/vs/workbench/services/workingCopy/browser/workingCopyBackupTracker.ts +++ b/src/vs/workbench/services/workingCopy/browser/workingCopyBackupTracker.ts @@ -32,27 +32,27 @@ export class BrowserWorkingCopyBackupTracker extends WorkingCopyBackupTracker im protected onFinalBeforeShutdown(reason: ShutdownReason): boolean { // Web: we cannot perform long running in the shutdown phase - // As such we need to check sync if there are any dirty working + // As such we need to check sync if there are any modified working // copies that have not been backed up yet and then prevent the // shutdown if that is the case. - const dirtyWorkingCopies = this.workingCopyService.dirtyWorkingCopies; - if (!dirtyWorkingCopies.length) { - return false; // no dirty: no veto + const modifiedWorkingCopies = this.workingCopyService.modifiedWorkingCopies; + if (!modifiedWorkingCopies.length) { + return false; // nothing modified: no veto } if (!this.filesConfigurationService.isHotExitEnabled) { - return true; // dirty without backup: veto + return true; // modified without backup: veto } - for (const dirtyWorkingCopy of dirtyWorkingCopies) { - if (!this.workingCopyBackupService.hasBackupSync(dirtyWorkingCopy, this.getContentVersion(dirtyWorkingCopy))) { + for (const modifiedWorkingCopy of modifiedWorkingCopies) { + if (!this.workingCopyBackupService.hasBackupSync(modifiedWorkingCopy, this.getContentVersion(modifiedWorkingCopy))) { this.logService.warn('Unload veto: pending backups'); - return true; // dirty without backup: veto + return true; // modified without backup: veto } } - return false; // dirty with backups: no veto + return false; // modified and backed up: no veto } } diff --git a/src/vs/workbench/services/workingCopy/common/resourceWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/resourceWorkingCopy.ts index cecd01413f0..bbe5100566c 100644 --- a/src/vs/workbench/services/workingCopy/common/resourceWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/resourceWorkingCopy.ts @@ -143,6 +143,13 @@ export abstract class ResourceWorkingCopy extends Disposable implements IResourc //#endregion + //#region Modified Tracking + + isModified(): boolean { + return this.isDirty(); + } + + //#endregion //#region Abstract diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts index 59d79abeefd..4c01ac142cb 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -37,9 +37,9 @@ export interface IUntitledFileWorkingCopyModelContentChangedEvent { /** * Flag that indicates that the content change should - * clear the dirty flag, e.g. because the contents are + * clear the dirty/modified flags, e.g. because the contents are * back to being empty or back to an initial state that - * should not be considered as dirty. + * should not be considered as modified. */ readonly isInitial: boolean; } @@ -92,7 +92,7 @@ export interface IUntitledFileWorkingCopyInitialContents { export class UntitledFileWorkingCopy extends Disposable implements IUntitledFileWorkingCopy { - readonly capabilities = WorkingCopyCapabilities.Untitled; + readonly capabilities = this.isScratchpad ? WorkingCopyCapabilities.Untitled | WorkingCopyCapabilities.Scratchpad : WorkingCopyCapabilities.Untitled; private _model: M | undefined = undefined; get model(): M | undefined { return this._model; } @@ -121,6 +121,7 @@ export class UntitledFileWorkingCopy ex readonly resource: URI, readonly name: string, readonly hasAssociatedFilePath: boolean, + private readonly isScratchpad: boolean, private readonly initialContents: IUntitledFileWorkingCopyInitialContents | undefined, private readonly modelFactory: IUntitledFileWorkingCopyModelFactory, private readonly saveDelegate: IUntitledFileWorkingCopySaveDelegate, @@ -136,19 +137,25 @@ export class UntitledFileWorkingCopy ex //#region Dirty - private dirty = this.hasAssociatedFilePath || Boolean(this.initialContents && this.initialContents.markDirty !== false); + private modified = this.hasAssociatedFilePath || Boolean(this.initialContents && this.initialContents.markDirty !== false); isDirty(): boolean { - return this.dirty; + return this.modified && !this.isScratchpad; // Scratchpad working copies are never dirty } - private setDirty(dirty: boolean): void { - if (this.dirty === dirty) { + isModified(): boolean { + return this.modified; + } + + private setModified(modified: boolean): void { + if (this.modified === modified) { return; } - this.dirty = dirty; - this._onDidChangeDirty.fire(); + this.modified = modified; + if (!this.isScratchpad) { + this._onDidChangeDirty.fire(); + } } //#endregion @@ -189,8 +196,8 @@ export class UntitledFileWorkingCopy ex // Create model await this.doCreateModel(untitledContents); - // Untitled associated to file path are dirty right away as well as untitled with content - this.setDirty(this.hasAssociatedFilePath || !!backup || Boolean(this.initialContents && this.initialContents.markDirty !== false)); + // Untitled associated to file path are modified right away as well as untitled with content + this.setModified(this.hasAssociatedFilePath || !!backup || Boolean(this.initialContents && this.initialContents.markDirty !== false)); // If we have initial contents, make sure to emit this // as the appropriate events to the outside. @@ -220,16 +227,16 @@ export class UntitledFileWorkingCopy ex private onModelContentChanged(e: IUntitledFileWorkingCopyModelContentChangedEvent): void { - // Mark the untitled file working copy as non-dirty once its + // Mark the untitled file working copy as non-modified once its // in case provided by the change event and in case we do not // have an associated path set if (!this.hasAssociatedFilePath && e.isInitial) { - this.setDirty(false); + this.setModified(false); } - // Turn dirty otherwise + // Turn modified otherwise else { - this.setDirty(true); + this.setModified(true); } // Emit as general content change event @@ -287,8 +294,8 @@ export class UntitledFileWorkingCopy ex async revert(): Promise { this.trace('revert()'); - // No longer dirty - this.setDirty(false); + // No longer modified + this.setModified(false); // Emit as event this._onDidRevert.fire(); diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts index f985afb7dc9..1b2269830c9 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopyManager.ts @@ -91,6 +91,12 @@ export interface INewOrExistingUntitledFileWorkingCopyOptions extends INewUntitl * Note: the resource will not be used unless the scheme is `untitled`. */ untitledResource: URI; + + /** + * A flag that will prevent the working copy from appearing dirty in the UI + * and not show a confirmation dialog when closed with unsaved content. + */ + isScratchpad?: boolean; } type IInternalUntitledFileWorkingCopyOptions = INewUntitledFileWorkingCopyOptions & INewUntitledFileWorkingCopyWithAssociatedResourceOptions & INewOrExistingUntitledFileWorkingCopyOptions; @@ -167,6 +173,7 @@ export class UntitledFileWorkingCopyManager { diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts index e9a5927e310..a5f58b91345 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts @@ -68,6 +68,18 @@ export interface IWorkingCopyService { */ readonly dirtyWorkingCopies: readonly IWorkingCopy[]; + /** + * The number of modified working copies that are registered, + * including scratchpads, which are never dirty. + */ + readonly modifiedCount: number; + + /** + * All working copies with unsaved changes, + * including scratchpads, which are never dirty. + */ + readonly modifiedWorkingCopies: readonly IWorkingCopy[]; + /** * Whether there is any registered working copy that is dirty. */ @@ -265,6 +277,22 @@ export class WorkingCopyService extends Disposable implements IWorkingCopyServic return this.workingCopies.filter(workingCopy => workingCopy.isDirty()); } + get modifiedCount(): number { + let totalModifiedCount = 0; + + for (const workingCopy of this._workingCopies) { + if (workingCopy.isModified()) { + totalModifiedCount++; + } + } + + return totalModifiedCount; + } + + get modifiedWorkingCopies(): IWorkingCopy[] { + return this.workingCopies.filter(workingCopy => workingCopy.isModified()); + } + isDirty(resource: URI, typeId?: string): boolean { const workingCopies = this.mapResourceToWorkingCopies.get(resource); if (workingCopies) { diff --git a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts index 7fcce30fdba..622cbc8c48d 100644 --- a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts +++ b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts @@ -49,7 +49,7 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp protected async onFinalBeforeShutdown(reason: ShutdownReason): Promise { - // Important: we are about to shutdown and handle dirty working copies + // Important: we are about to shutdown and handle modified working copies // and backups. We do not want any pending backup ops to interfer with // this because there is a risk of a backup being scheduled after we have // acknowledged to shutdown and then might end up with partial backups @@ -67,49 +67,49 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp try { - // Dirty working copies need treatment on shutdown - const dirtyWorkingCopies = this.workingCopyService.dirtyWorkingCopies; - if (dirtyWorkingCopies.length) { - return await this.onBeforeShutdownWithDirty(reason, dirtyWorkingCopies); + // Modified working copies need treatment on shutdown + const modifiedWorkingCopies = this.workingCopyService.modifiedWorkingCopies; + if (modifiedWorkingCopies.length) { + return await this.onBeforeShutdownWithModified(reason, modifiedWorkingCopies); } - // No dirty working copies + // No modified working copies else { - return await this.onBeforeShutdownWithoutDirty(); + return await this.onBeforeShutdownWithoutModified(); } } finally { resume(); } } - protected async onBeforeShutdownWithDirty(reason: ShutdownReason, dirtyWorkingCopies: readonly IWorkingCopy[]): Promise { + protected async onBeforeShutdownWithModified(reason: ShutdownReason, modifiedWorkingCopies: readonly IWorkingCopy[]): Promise { // If auto save is enabled, save all non-untitled working copies - // and then check again for dirty copies + // and then check again for modified copies if (this.filesConfigurationService.getAutoSaveMode() !== AutoSaveMode.OFF) { - // Save all dirty working copies + // Save all modified working copies try { await this.doSaveAllBeforeShutdown(false /* not untitled */, SaveReason.AUTO); } catch (error) { - this.logService.error(`[backup tracker] error saving dirty working copies: ${error}`); // guard against misbehaving saves, we handle remaining dirty below + this.logService.error(`[backup tracker] error saving modified working copies: ${error}`); // guard against misbehaving saves, we handle remaining modified below } - // If we still have dirty working copies, we either have untitled ones or working copies that cannot be saved - const remainingDirtyWorkingCopies = this.workingCopyService.dirtyWorkingCopies; - if (remainingDirtyWorkingCopies.length) { - return this.handleDirtyBeforeShutdown(remainingDirtyWorkingCopies, reason); + // If we still have modified working copies, we either have untitled ones or working copies that cannot be saved + const remainingModifiedWorkingCopies = this.workingCopyService.modifiedWorkingCopies; + if (remainingModifiedWorkingCopies.length) { + return this.handleModifiedBeforeShutdown(remainingModifiedWorkingCopies, reason); } - return this.noVeto([...dirtyWorkingCopies]); // no veto (dirty auto-saved) + return this.noVeto([...modifiedWorkingCopies]); // no veto (modified auto-saved) } // Auto save is not enabled - return this.handleDirtyBeforeShutdown(dirtyWorkingCopies, reason); + return this.handleModifiedBeforeShutdown(modifiedWorkingCopies, reason); } - private async handleDirtyBeforeShutdown(dirtyWorkingCopies: readonly IWorkingCopy[], reason: ShutdownReason): Promise { + private async handleModifiedBeforeShutdown(modifiedWorkingCopies: readonly IWorkingCopy[], reason: ShutdownReason): Promise { // Trigger backup if configured and enabled for shutdown reason let backups: IWorkingCopy[] = []; @@ -117,11 +117,11 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp const backup = await this.shouldBackupBeforeShutdown(reason); if (backup) { try { - const backupResult = await this.backupBeforeShutdown(dirtyWorkingCopies); + const backupResult = await this.backupBeforeShutdown(modifiedWorkingCopies); backups = backupResult.backups; backupError = backupResult.error; - if (backups.length === dirtyWorkingCopies.length) { + if (backups.length === modifiedWorkingCopies.length) { return false; // no veto (backup was successful for all working copies) } } catch (error) { @@ -129,7 +129,7 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp } } - const remainingDirtyWorkingCopies = dirtyWorkingCopies.filter(workingCopy => !backups.includes(workingCopy)); + const remainingModifiedWorkingCopies = modifiedWorkingCopies.filter(workingCopy => !backups.includes(workingCopy)); // We ran a backup but received an error that we show to the user if (backupError) { @@ -139,7 +139,7 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp return false; // do not block shutdown during extension development (https://github.com/microsoft/vscode/issues/115028) } - this.showErrorDialog(localize('backupTrackerBackupFailed', "The following editors with unsaved changes could not be saved to the back up location."), remainingDirtyWorkingCopies, backupError); + this.showErrorDialog(localize('backupTrackerBackupFailed', "The following editors with unsaved changes could not be saved to the back up location."), remainingModifiedWorkingCopies, backupError); return true; // veto (the backup failed) } @@ -148,15 +148,15 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp // the working copies that did not successfully backup try { - return await this.confirmBeforeShutdown(remainingDirtyWorkingCopies); + return await this.confirmBeforeShutdown(remainingModifiedWorkingCopies); } catch (error) { if (this.environmentService.isExtensionDevelopment) { - this.logService.error(`[backup tracker] error saving or reverting dirty working copies: ${error}`); + this.logService.error(`[backup tracker] error saving or reverting modified working copies: ${error}`); return false; // do not block shutdown during extension development (https://github.com/microsoft/vscode/issues/115028) } - this.showErrorDialog(localize('backupTrackerConfirmFailed', "The following editors with unsaved changes could not be saved or reverted."), remainingDirtyWorkingCopies, error); + this.showErrorDialog(localize('backupTrackerConfirmFailed', "The following editors with unsaved changes could not be saved or reverted."), remainingModifiedWorkingCopies, error); return true; // veto (save or revert failed) } @@ -208,11 +208,11 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp } private showErrorDialog(msg: string, workingCopies: readonly IWorkingCopy[], error?: Error): void { - const dirtyWorkingCopies = workingCopies.filter(workingCopy => workingCopy.isDirty()); + const modifiedWorkingCopies = workingCopies.filter(workingCopy => workingCopy.isModified()); const advice = localize('backupErrorDetails', "Try saving or reverting the editors with unsaved changes first and then try again."); - const detail = dirtyWorkingCopies.length - ? getFileNamesMessage(dirtyWorkingCopies.map(x => x.name)) + '\n' + advice + const detail = modifiedWorkingCopies.length + ? getFileNamesMessage(modifiedWorkingCopies.map(x => x.name)) + '\n' + advice : advice; this.dialogService.error(msg, detail); @@ -220,15 +220,15 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp this.logService.error(error ? `[backup tracker] ${msg}: ${error}` : `[backup tracker] ${msg}`); } - private async backupBeforeShutdown(dirtyWorkingCopies: readonly IWorkingCopy[]): Promise<{ backups: IWorkingCopy[]; error?: Error }> { + private async backupBeforeShutdown(modifiedWorkingCopies: readonly IWorkingCopy[]): Promise<{ backups: IWorkingCopy[]; error?: Error }> { const backups: IWorkingCopy[] = []; let error: Error | undefined = undefined; await this.withProgressAndCancellation(async token => { - // Perform a backup of all dirty working copies unless a backup already exists + // Perform a backup of all modified working copies unless a backup already exists try { - await Promises.settled(dirtyWorkingCopies.map(async workingCopy => { + await Promises.settled(modifiedWorkingCopies.map(async workingCopy => { // Backup exists const contentVersion = this.getContentVersion(workingCopy); @@ -262,46 +262,46 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp return { backups, error }; } - private async confirmBeforeShutdown(dirtyWorkingCopies: IWorkingCopy[]): Promise { + private async confirmBeforeShutdown(modifiedWorkingCopies: IWorkingCopy[]): Promise { // Save - const confirm = await this.fileDialogService.showSaveConfirm(dirtyWorkingCopies.map(workingCopy => workingCopy.name)); + const confirm = await this.fileDialogService.showSaveConfirm(modifiedWorkingCopies.map(workingCopy => workingCopy.name)); if (confirm === ConfirmResult.SAVE) { - const dirtyCountBeforeSave = this.workingCopyService.dirtyCount; + const modifiedCountBeforeSave = this.workingCopyService.modifiedCount; try { - await this.doSaveAllBeforeShutdown(dirtyWorkingCopies, SaveReason.EXPLICIT); + await this.doSaveAllBeforeShutdown(modifiedWorkingCopies, SaveReason.EXPLICIT); } catch (error) { - this.logService.error(`[backup tracker] error saving dirty working copies: ${error}`); // guard against misbehaving saves, we handle remaining dirty below + this.logService.error(`[backup tracker] error saving modified working copies: ${error}`); // guard against misbehaving saves, we handle remaining modified below } - const savedWorkingCopies = dirtyCountBeforeSave - this.workingCopyService.dirtyCount; - if (savedWorkingCopies < dirtyWorkingCopies.length) { + const savedWorkingCopies = modifiedCountBeforeSave - this.workingCopyService.modifiedCount; + if (savedWorkingCopies < modifiedWorkingCopies.length) { return true; // veto (save failed or was canceled) } - return this.noVeto(dirtyWorkingCopies); // no veto (dirty saved) + return this.noVeto(modifiedWorkingCopies); // no veto (modified saved) } // Don't Save else if (confirm === ConfirmResult.DONT_SAVE) { try { - await this.doRevertAllBeforeShutdown(dirtyWorkingCopies); + await this.doRevertAllBeforeShutdown(modifiedWorkingCopies); } catch (error) { - this.logService.error(`[backup tracker] error reverting dirty working copies: ${error}`); // do not block the shutdown on errors from revert + this.logService.error(`[backup tracker] error reverting modified working copies: ${error}`); // do not block the shutdown on errors from revert } - return this.noVeto(dirtyWorkingCopies); // no veto (dirty reverted) + return this.noVeto(modifiedWorkingCopies); // no veto (modified reverted) } // Cancel return true; // veto (user canceled) } - private doSaveAllBeforeShutdown(dirtyWorkingCopies: IWorkingCopy[], reason: SaveReason): Promise; + private doSaveAllBeforeShutdown(modifiedWorkingCopies: IWorkingCopy[], reason: SaveReason): Promise; private doSaveAllBeforeShutdown(includeUntitled: boolean, reason: SaveReason): Promise; private doSaveAllBeforeShutdown(arg1: IWorkingCopy[] | boolean, reason: SaveReason): Promise { - const dirtyWorkingCopies = Array.isArray(arg1) ? arg1 : this.workingCopyService.dirtyWorkingCopies.filter(workingCopy => { + const modifiedWorkingCopies = Array.isArray(arg1) ? arg1 : this.workingCopyService.modifiedWorkingCopies.filter(workingCopy => { if (arg1 === false && (workingCopy.capabilities & WorkingCopyCapabilities.Untitled)) { return false; // skip untitled unless explicitly included } @@ -315,40 +315,40 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp const saveOptions = { skipSaveParticipants: true, reason }; // First save through the editor service if we save all to benefit - // from some extras like switching to untitled dirty editors before saving. + // from some extras like switching to untitled modified editors before saving. let result: boolean | undefined = undefined; - if (typeof arg1 === 'boolean' || dirtyWorkingCopies.length === this.workingCopyService.dirtyCount) { + if (typeof arg1 === 'boolean' || modifiedWorkingCopies.length === this.workingCopyService.modifiedCount) { result = (await this.editorService.saveAll({ includeUntitled: typeof arg1 === 'boolean' ? arg1 : true, ...saveOptions })).success; } - // If we still have dirty working copies, save those directly + // If we still have modified working copies, save those directly // unless the save was not successful (e.g. cancelled) if (result !== false) { - await Promises.settled(dirtyWorkingCopies.map(workingCopy => workingCopy.isDirty() ? workingCopy.save(saveOptions) : Promise.resolve(true))); + await Promises.settled(modifiedWorkingCopies.map(workingCopy => workingCopy.isModified() ? workingCopy.save(saveOptions) : Promise.resolve(true))); } }, localize('saveBeforeShutdown', "Saving editors with unsaved changes is taking a bit longer...")); } - private doRevertAllBeforeShutdown(dirtyWorkingCopies: IWorkingCopy[]): Promise { + private doRevertAllBeforeShutdown(modifiedWorkingCopies: IWorkingCopy[]): Promise { return this.withProgressAndCancellation(async () => { // Soft revert is good enough on shutdown const revertOptions = { soft: true }; // First revert through the editor service if we revert all - if (dirtyWorkingCopies.length === this.workingCopyService.dirtyCount) { + if (modifiedWorkingCopies.length === this.workingCopyService.modifiedCount) { await this.editorService.revertAll(revertOptions); } - // If we still have dirty working copies, revert those directly - await Promises.settled(dirtyWorkingCopies.map(workingCopy => workingCopy.isDirty() ? workingCopy.revert(revertOptions) : Promise.resolve())); + // If we still have modified working copies, revert those directly + await Promises.settled(modifiedWorkingCopies.map(workingCopy => workingCopy.isModified() ? workingCopy.revert(revertOptions) : Promise.resolve())); }, localize('revertBeforeShutdown', "Reverting editors with unsaved changes is taking a bit longer...")); } - private onBeforeShutdownWithoutDirty(): Promise { + private onBeforeShutdownWithoutModified(): Promise { - // We are about to shutdown without dirty editors + // We are about to shutdown without modified editors // and will discard any backups that are still // around that have not been handled depending // on the window state. @@ -377,7 +377,7 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp await this.discardBackupsBeforeShutdown(arg1); - return false; // no veto (no dirty) + return false; // no veto (no modified) } private discardBackupsBeforeShutdown(backupsToDiscard: IWorkingCopyIdentifier[]): Promise; @@ -396,7 +396,7 @@ export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker imp await this.withProgressAndCancellation(async () => { - // When we shutdown either with no dirty working copies left + // When we shutdown either with no modified working copies left // or with some handled, we start to discard these backups // to free them up. This helps to get rid of stale backups // as reported in https://github.com/microsoft/vscode/issues/92962 diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts index 6d4bbc6e1d2..c98fad63dac 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test.ts @@ -102,6 +102,7 @@ suite('UntitledFileWorkingCopy', () => { uri, basename(uri), hasAssociatedFilePath, + false, initialValue.length > 0 ? { value: bufferToStream(VSBuffer.fromString(initialValue)) } : undefined, factory, async workingCopy => { await workingCopy.revert(); return true; }, diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts index 69f8c133efc..b3cee873d4a 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopyManager.test.ts @@ -89,20 +89,24 @@ suite('UntitledFileWorkingCopyManager', () => { for (const workingCopy of [workingCopy1, workingCopy2]) { assert.strictEqual(workingCopy.capabilities, WorkingCopyCapabilities.Untitled); assert.strictEqual(workingCopy.isDirty(), false); + assert.strictEqual(workingCopy.isModified(), false); assert.ok(workingCopy.model); } workingCopy1.model?.updateContents('Hello World'); assert.strictEqual(workingCopy1.isDirty(), true); + assert.strictEqual(workingCopy1.isModified(), true); assert.strictEqual(dirtyCounter, 1); - workingCopy1.model?.updateContents(''); // change to empty clears dirty flag + workingCopy1.model?.updateContents(''); // change to empty clears dirty/modified flags assert.strictEqual(workingCopy1.isDirty(), false); + assert.strictEqual(workingCopy1.isModified(), false); assert.strictEqual(dirtyCounter, 2); workingCopy2.model?.fireContentChangeEvent({ isInitial: false }); assert.strictEqual(workingCopy2.isDirty(), true); + assert.strictEqual(workingCopy2.isModified(), true); assert.strictEqual(dirtyCounter, 3); workingCopy1.dispose(); @@ -118,6 +122,33 @@ suite('UntitledFileWorkingCopyManager', () => { assert.strictEqual(disposeCounter, 2); }); + test('dirty - scratchpads are never dirty', async () => { + let dirtyCounter = 0; + manager.untitled.onDidChangeDirty(e => { + dirtyCounter++; + }); + + const workingCopy1 = await manager.resolve({ + untitledResource: URI.from({ scheme: Schemas.untitled, path: `/myscratchpad` }), + isScratchpad: true + }); + + assert.strictEqual(workingCopy1.resource.scheme, Schemas.untitled); + assert.strictEqual(manager.untitled.workingCopies.length, 1); + + workingCopy1.model?.updateContents('contents'); + assert.strictEqual(workingCopy1.isDirty(), false); + assert.strictEqual(workingCopy1.isModified(), true); + + workingCopy1.model?.fireContentChangeEvent({ isInitial: true }); + assert.strictEqual(workingCopy1.isDirty(), false); + assert.strictEqual(workingCopy1.isModified(), false); + + assert.strictEqual(dirtyCounter, 0); + + workingCopy1.dispose(); + }); + test('resolve - with initial value', async () => { let dirtyCounter = 0; manager.untitled.onDidChangeDirty(e => { diff --git a/src/vs/workbench/services/workingCopy/test/browser/untitledScratchpadWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/untitledScratchpadWorkingCopy.test.ts new file mode 100644 index 00000000000..b3f94c4ab9c --- /dev/null +++ b/src/vs/workbench/services/workingCopy/test/browser/untitledScratchpadWorkingCopy.test.ts @@ -0,0 +1,273 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { VSBufferReadableStream, VSBuffer, streamToBuffer, bufferToStream, readableToBuffer, VSBufferReadable } from 'vs/base/common/buffer'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { basename } from 'vs/base/common/resources'; +import { consumeReadable, consumeStream, isReadable, isReadableStream } from 'vs/base/common/stream'; +import { URI } from 'vs/base/common/uri'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IUntitledFileWorkingCopyModelFactory, UntitledFileWorkingCopy } from 'vs/workbench/services/workingCopy/common/untitledFileWorkingCopy'; +import { TestUntitledFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/test/browser/untitledFileWorkingCopy.test'; +import { TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; + +export class TestUntitledFileWorkingCopyModelFactory implements IUntitledFileWorkingCopyModelFactory { + + async createModel(resource: URI, contents: VSBufferReadableStream, token: CancellationToken): Promise { + return new TestUntitledFileWorkingCopyModel(resource, (await streamToBuffer(contents)).toString()); + } +} + +suite('UntitledScratchpadWorkingCopy', () => { + + const factory = new TestUntitledFileWorkingCopyModelFactory(); + + let disposables: DisposableStore; + const resource = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); + let instantiationService: IInstantiationService; + let accessor: TestServiceAccessor; + let workingCopy: UntitledFileWorkingCopy; + + function createWorkingCopy(uri: URI = resource, hasAssociatedFilePath = false, initialValue = '') { + return new UntitledFileWorkingCopy( + 'testUntitledWorkingCopyType', + uri, + basename(uri), + hasAssociatedFilePath, + true, + initialValue.length > 0 ? { value: bufferToStream(VSBuffer.fromString(initialValue)) } : undefined, + factory, + async workingCopy => { await workingCopy.revert(); return true; }, + accessor.workingCopyService, + accessor.workingCopyBackupService, + accessor.logService + ); + } + + setup(() => { + disposables = new DisposableStore(); + instantiationService = workbenchInstantiationService(undefined, disposables); + accessor = instantiationService.createInstance(TestServiceAccessor); + + workingCopy = createWorkingCopy(); + }); + + teardown(() => { + workingCopy.dispose(); + disposables.dispose(); + }); + + test('registers with working copy service', async () => { + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 1); + + workingCopy.dispose(); + + assert.strictEqual(accessor.workingCopyService.workingCopies.length, 0); + }); + + test('modified - not dirty', async () => { + assert.strictEqual(workingCopy.isDirty(), false); + + let changeDirtyCounter = 0; + workingCopy.onDidChangeDirty(() => { + changeDirtyCounter++; + }); + + let contentChangeCounter = 0; + workingCopy.onDidChangeContent(() => { + contentChangeCounter++; + }); + + await workingCopy.resolve(); + assert.strictEqual(workingCopy.isResolved(), true); + + // Modified from: Model content change + workingCopy.model?.updateContents('hello modified'); + assert.strictEqual(contentChangeCounter, 1); + + assert.strictEqual(workingCopy.isDirty(), false); + assert.strictEqual(workingCopy.isModified(), true); + assert.strictEqual(changeDirtyCounter, 0); + + await workingCopy.save(); + + assert.strictEqual(workingCopy.isDirty(), false); + assert.strictEqual(changeDirtyCounter, 0); + }); + + test('modified - cleared when content event signals isEmpty', async () => { + assert.strictEqual(workingCopy.isModified(), false); + + await workingCopy.resolve(); + + workingCopy.model?.updateContents('hello modified'); + + assert.strictEqual(workingCopy.isModified(), true); + + workingCopy.model?.fireContentChangeEvent({ isInitial: true }); + + assert.strictEqual(workingCopy.isModified(), false); + }); + + test('modified - not cleared when content event signals isEmpty when associated resource', async () => { + workingCopy.dispose(); + workingCopy = createWorkingCopy(resource, true); + + await workingCopy.resolve(); + + workingCopy.model?.updateContents('hello modified'); + assert.strictEqual(workingCopy.isModified(), true); + + workingCopy.model?.fireContentChangeEvent({ isInitial: true }); + + assert.strictEqual(workingCopy.isModified(), true); + }); + + test('revert', async () => { + let revertCounter = 0; + workingCopy.onDidRevert(() => { + revertCounter++; + }); + + let disposeCounter = 0; + workingCopy.onWillDispose(() => { + disposeCounter++; + }); + + await workingCopy.resolve(); + + workingCopy.model?.updateContents('hello modified'); + assert.strictEqual(workingCopy.isModified(), true); + + await workingCopy.revert(); + + assert.strictEqual(revertCounter, 1); + assert.strictEqual(disposeCounter, 1); + assert.strictEqual(workingCopy.isModified(), false); + }); + + test('dispose', async () => { + let disposeCounter = 0; + workingCopy.onWillDispose(() => { + disposeCounter++; + }); + + await workingCopy.resolve(); + workingCopy.dispose(); + + assert.strictEqual(disposeCounter, 1); + }); + + test('backup', async () => { + assert.strictEqual((await workingCopy.backup(CancellationToken.None)).content, undefined); + + await workingCopy.resolve(); + + workingCopy.model?.updateContents('Hello Backup'); + const backup = await workingCopy.backup(CancellationToken.None); + + let backupContents: string | undefined = undefined; + if (isReadableStream(backup.content)) { + backupContents = (await consumeStream(backup.content, chunks => VSBuffer.concat(chunks))).toString(); + } else if (backup.content) { + backupContents = consumeReadable(backup.content, chunks => VSBuffer.concat(chunks)).toString(); + } + + assert.strictEqual(backupContents, 'Hello Backup'); + }); + + test('resolve - without contents', async () => { + assert.strictEqual(workingCopy.isResolved(), false); + assert.strictEqual(workingCopy.hasAssociatedFilePath, false); + assert.strictEqual(workingCopy.model, undefined); + + await workingCopy.resolve(); + + assert.strictEqual(workingCopy.isResolved(), true); + assert.ok(workingCopy.model); + }); + + test('resolve - with initial contents', async () => { + workingCopy.dispose(); + + workingCopy = createWorkingCopy(resource, false, 'Hello Initial'); + + let contentChangeCounter = 0; + workingCopy.onDidChangeContent(() => { + contentChangeCounter++; + }); + + assert.strictEqual(workingCopy.isModified(), true); + + await workingCopy.resolve(); + + assert.strictEqual(workingCopy.isModified(), true); + assert.strictEqual(workingCopy.model?.contents, 'Hello Initial'); + assert.strictEqual(contentChangeCounter, 1); + + workingCopy.model.updateContents('Changed contents'); + + await workingCopy.resolve(); // second resolve should be ignored + assert.strictEqual(workingCopy.model?.contents, 'Changed contents'); + }); + + test('backup - with initial contents uses those even if unresolved', async () => { + workingCopy.dispose(); + + workingCopy = createWorkingCopy(resource, false, 'Hello Initial'); + + assert.strictEqual(workingCopy.isModified(), true); + + const backup = (await workingCopy.backup(CancellationToken.None)).content; + if (isReadableStream(backup)) { + const value = await streamToBuffer(backup as VSBufferReadableStream); + assert.strictEqual(value.toString(), 'Hello Initial'); + } else if (isReadable(backup)) { + const value = readableToBuffer(backup as VSBufferReadable); + assert.strictEqual(value.toString(), 'Hello Initial'); + } else { + assert.fail('Missing untitled backup'); + } + }); + + + test('resolve - with associated resource', async () => { + workingCopy.dispose(); + workingCopy = createWorkingCopy(resource, true); + + await workingCopy.resolve(); + + assert.strictEqual(workingCopy.isModified(), true); + assert.strictEqual(workingCopy.hasAssociatedFilePath, true); + }); + + test('resolve - with backup', async () => { + await workingCopy.resolve(); + workingCopy.model?.updateContents('Hello Backup'); + + const backup = await workingCopy.backup(CancellationToken.None); + await accessor.workingCopyBackupService.backup(workingCopy, backup.content, undefined, backup.meta); + + assert.strictEqual(accessor.workingCopyBackupService.hasBackupSync(workingCopy), true); + + workingCopy.dispose(); + + workingCopy = createWorkingCopy(); + + let contentChangeCounter = 0; + workingCopy.onDidChangeContent(() => { + contentChangeCounter++; + }); + + await workingCopy.resolve(); + + assert.strictEqual(workingCopy.isModified(), true); + assert.strictEqual(workingCopy.model?.contents, 'Hello Backup'); + assert.strictEqual(contentChangeCounter, 1); + }); +}); diff --git a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts index 679b0ceff4a..81f856c6d9d 100644 --- a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts @@ -48,6 +48,7 @@ suite('WorkingCopyService', () => { assert.strictEqual(onDidRegister.length, 1); assert.strictEqual(onDidRegister[0], copy1); assert.strictEqual(service.dirtyCount, 0); + assert.strictEqual(service.modifiedCount, 0); assert.strictEqual(service.isDirty(resource1), false); assert.strictEqual(service.has(resource1), true); assert.strictEqual(service.has(copy1), true); @@ -65,6 +66,9 @@ suite('WorkingCopyService', () => { assert.strictEqual(service.dirtyCount, 1); assert.strictEqual(service.dirtyWorkingCopies.length, 1); assert.strictEqual(service.dirtyWorkingCopies[0], copy1); + assert.strictEqual(service.modifiedCount, 1); + assert.strictEqual(service.modifiedWorkingCopies.length, 1); + assert.strictEqual(service.modifiedWorkingCopies[0], copy1); assert.strictEqual(service.workingCopies.length, 1); assert.strictEqual(service.workingCopies[0], copy1); assert.strictEqual(service.isDirty(resource1), true); diff --git a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupTracker.test.ts b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupTracker.test.ts index 8c82012f80e..e2398e4ae67 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupTracker.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-sandbox/workingCopyBackupTracker.test.ts @@ -37,7 +37,7 @@ import { IProgressService } from 'vs/platform/progress/common/progress'; import { IWorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; import { TestContextService, TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { Event, Emitter } from 'vs/base/common/event'; import { generateUuid } from 'vs/base/common/uuid'; import { Schemas } from 'vs/base/common/network'; @@ -381,6 +381,48 @@ suite('WorkingCopyBackupTracker (native)', function () { await cleanup(); }); + test('onWillShutdown - scratchpads - veto if backup fails', async function () { + const { accessor, cleanup } = await createTracker(); + + class TestBackupWorkingCopy extends TestWorkingCopy { + + constructor(resource: URI) { + super(resource); + + accessor.workingCopyService.registerWorkingCopy(this); + } + + override capabilities = WorkingCopyCapabilities.Untitled | WorkingCopyCapabilities.Scratchpad; + + override async backup(token: CancellationToken): Promise { + throw new Error('unable to backup'); + } + + override isDirty(): boolean { + return false; + } + + override isModified(): boolean { + return true; + } + } + + const resource = toResource.call(this, '/path/custom.txt'); + new TestBackupWorkingCopy(resource); + + const event = new TestBeforeShutdownEvent(); + event.reason = ShutdownReason.QUIT; + accessor.lifecycleService.fireBeforeShutdown(event); + + const veto = await event.value; + assert.ok(veto); + + const finalVeto = await event.finalValue?.(); + assert.ok(finalVeto); // assert the tracker uses the internal finalVeto API + + await cleanup(); + }); + test('onWillShutdown - pending backup operations canceled and tracker suspended/resumsed', async function () { const { accessor, tracker, cleanup } = await createTracker(); diff --git a/src/vs/workbench/services/workspaces/common/canonicalUriIdentityService.ts b/src/vs/workbench/services/workspaces/common/canonicalUriService.ts similarity index 57% rename from src/vs/workbench/services/workspaces/common/canonicalUriIdentityService.ts rename to src/vs/workbench/services/workspaces/common/canonicalUriService.ts index d5f47b09d94..bd2a007835c 100644 --- a/src/vs/workbench/services/workspaces/common/canonicalUriIdentityService.ts +++ b/src/vs/workbench/services/workspaces/common/canonicalUriService.ts @@ -7,27 +7,27 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ICanonicalUriIdentityService, ICanonicalUriIdentityProvider } from 'vs/platform/workspace/common/canonicalUriIdentity'; +import { ICanonicalUriService, ICanonicalUriProvider } from 'vs/platform/workspace/common/canonicalUri'; -export class CanonicalUriIdentityService implements ICanonicalUriIdentityService { +export class CanonicalUriService implements ICanonicalUriService { declare readonly _serviceBrand: undefined; - private readonly _providers = new Map(); + private readonly _providers = new Map(); - registerCanonicalUriIdentityProvider(provider: ICanonicalUriIdentityProvider): IDisposable { + registerCanonicalUriProvider(provider: ICanonicalUriProvider): IDisposable { this._providers.set(provider.scheme, provider); return { dispose: () => this._providers.delete(provider.scheme) }; } - async provideCanonicalUriIdentity(uri: URI, token: CancellationToken): Promise { + async provideCanonicalUri(uri: URI, targetScheme: string, token: CancellationToken): Promise { const provider = this._providers.get(uri.scheme); if (provider) { - return provider.provideCanonicalUriIdentity(uri, token); + return provider.provideCanonicalUri(uri, targetScheme, token); } return undefined; } } -registerSingleton(ICanonicalUriIdentityService, CanonicalUriIdentityService, InstantiationType.Delayed); +registerSingleton(ICanonicalUriService, CanonicalUriService, InstantiationType.Delayed); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index b1d0d2edb5e..55fb0e1f6bd 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -1803,7 +1803,6 @@ export class TestTerminalEditorService implements ITerminalEditorService { onDidChangeActiveInstance = Event.None; onDidChangeInstances = Event.None; openEditor(instance: ITerminalInstance, editorOptions?: TerminalEditorLocation): Promise { throw new Error('Method not implemented.'); } - detachActiveEditorInstance(): ITerminalInstance { throw new Error('Method not implemented.'); } detachInstance(instance: ITerminalInstance): void { throw new Error('Method not implemented.'); } splitInstance(instanceToSplit: ITerminalInstance, shellLaunchConfig?: IShellLaunchConfig): ITerminalInstance { throw new Error('Method not implemented.'); } revealActiveEditor(preserveFocus?: boolean): Promise { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/test/common/workbenchTestServices.ts b/src/vs/workbench/test/common/workbenchTestServices.ts index c9b154089d4..627f0ea0b77 100644 --- a/src/vs/workbench/test/common/workbenchTestServices.ts +++ b/src/vs/workbench/test/common/workbenchTestServices.ts @@ -203,6 +203,10 @@ export class TestWorkingCopy extends Disposable implements IWorkingCopy { return this.dirty; } + isModified(): boolean { + return this.isDirty(); + } + async save(options?: ISaveOptions, stat?: IFileStatWithMetadata): Promise { this._onDidSave.fire({ reason: options?.reason ?? SaveReason.EXPLICIT, stat: stat ?? createFileStat(this.resource), source: options?.source }); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 0786b7607fe..7561fbf1fb9 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -55,7 +55,7 @@ import 'vs/workbench/browser/parts/views/viewsService'; import 'vs/platform/actions/common/actions.contribution'; import 'vs/platform/undoRedo/common/undoRedoService'; import 'vs/workbench/services/workspaces/common/editSessionIdentityService'; -import 'vs/workbench/services/workspaces/common/canonicalUriIdentityService'; +import 'vs/workbench/services/workspaces/common/canonicalUriService'; import 'vs/workbench/services/extensions/browser/extensionUrlHandler'; import 'vs/workbench/services/keybinding/common/keybindingEditing'; import 'vs/workbench/services/decorations/browser/decorationsService'; diff --git a/src/vscode-dts/vscode.proposed.canonicalUriIdentityProvider.d.ts b/src/vscode-dts/vscode.proposed.canonicalUriIdentityProvider.d.ts deleted file mode 100644 index 3a61ca15798..00000000000 --- a/src/vscode-dts/vscode.proposed.canonicalUriIdentityProvider.d.ts +++ /dev/null @@ -1,38 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - - // https://github.com/microsoft/vscode/issues/180582 - - export namespace workspace { - /** - * - * @param scheme The URI scheme that this provider can provide canonical URI identities for. - * A canonical URI represents the conversion of a resource's alias into a source of truth URI. - * Multiple aliases may convert to the same source of truth URI. - * @param provider A provider which can convert URIs for workspace folders of scheme @param scheme to - * a canonical URI identifier which is stable across machines. - */ - export function registerCanonicalUriIdentityProvider(scheme: string, provider: CanonicalUriIdentityProvider): Disposable; - - /** - * - * @param uri The URI to provide a canonical URI identity for. - * @param token A cancellation token for the request. - */ - export function provideCanonicalUriIdentity(uri: Uri, token: CancellationToken): ProviderResult; - } - - export interface CanonicalUriIdentityProvider { - /** - * - * @param uri The URI to provide a canonical URI identity for. - * @param token A cancellation token for the request. - * @returns The canonical URI identity for the requested URI. - */ - provideCanonicalUriIdentity(uri: Uri, token: CancellationToken): ProviderResult; - } -} diff --git a/src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts b/src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts new file mode 100644 index 00000000000..84ee599797d --- /dev/null +++ b/src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/180582 + + export namespace workspace { + /** + * + * @param scheme The URI scheme that this provider can provide canonical URIs for. + * A canonical URI represents the conversion of a resource's alias into a source of truth URI. + * Multiple aliases may convert to the same source of truth URI. + * @param provider A provider which can convert URIs of scheme @param scheme to + * a canonical URI which is stable across machines. + */ + export function registerCanonicalUriProvider(scheme: string, provider: CanonicalUriProvider): Disposable; + + /** + * + * @param uri The URI to provide a canonical URI for. + * @param token A cancellation token for the request. + */ + export function getCanonicalUri(uri: Uri, options: CanonicalUriRequestOptions, token: CancellationToken): ProviderResult; + } + + export interface CanonicalUriProvider { + /** + * + * @param uri The URI to provide a canonical URI for. + * @param options Options that the provider should honor in the URI it returns. + * @param token A cancellation token for the request. + * @returns The canonical URI for the requested URI or undefined if no canonical URI can be provided. + */ + provideCanonicalUri(uri: Uri, options: CanonicalUriRequestOptions, token: CancellationToken): ProviderResult; + } + + export interface CanonicalUriRequestOptions { + /** + * + * The desired scheme of the canonical URI. + */ + targetScheme: string; + } +} diff --git a/src/vscode-dts/vscode.proposed.envCollectionWorkspace.d.ts b/src/vscode-dts/vscode.proposed.envCollectionWorkspace.d.ts index b97e4afb28a..d778e53e508 100644 --- a/src/vscode-dts/vscode.proposed.envCollectionWorkspace.d.ts +++ b/src/vscode-dts/vscode.proposed.envCollectionWorkspace.d.ts @@ -5,33 +5,30 @@ declare module 'vscode' { - // https://github.com/microsoft/vscode/issues/171173 + // https://github.com/microsoft/vscode/issues/182069 - export interface EnvironmentVariableMutator { - readonly type: EnvironmentVariableMutatorType; - readonly value: string; - readonly scope: EnvironmentVariableScope | undefined; - } - - export interface EnvironmentVariableCollection extends Iterable<[variable: string, mutator: EnvironmentVariableMutator]> { - /** - * Sets a description for the environment variable collection, this will be used to describe the changes in the UI. - * @param description A description for the environment variable collection. - * @param scope Specific scope to which this description applies to. - */ - setDescription(description: string | MarkdownString | undefined, scope?: EnvironmentVariableScope): void; - replace(variable: string, value: string, scope?: EnvironmentVariableScope): void; - append(variable: string, value: string, scope?: EnvironmentVariableScope): void; - prepend(variable: string, value: string, scope?: EnvironmentVariableScope): void; - get(variable: string, scope?: EnvironmentVariableScope): EnvironmentVariableMutator | undefined; - delete(variable: string, scope?: EnvironmentVariableScope): void; - clear(scope?: EnvironmentVariableScope): void; - } + // export interface ExtensionContext { + // /** + // * Gets the extension's environment variable collection for this workspace, enabling changes + // * to be applied to terminal environment variables. + // * + // * @param scope The scope to which the environment variable collection applies to. + // */ + // readonly environmentVariableCollection: EnvironmentVariableCollection & { getScopedEnvironmentVariableCollection(scope: EnvironmentVariableScope): EnvironmentVariableCollection }; + // } export type EnvironmentVariableScope = { /** - * The workspace folder to which this collection applies to. If unspecified, collection applies to all workspace folders. - */ + * Any specific workspace folder to get collection for. If unspecified, collection applicable to all workspace folders is returned. + */ workspaceFolder?: WorkspaceFolder; }; + + export interface EnvironmentVariableCollection extends Iterable<[variable: string, mutator: EnvironmentVariableMutator]> { + /** + * A description for the environment variable collection, this will be used to describe the + * changes in the UI. + */ + description: string | MarkdownString | undefined; + } } diff --git a/src/vscode-dts/vscode.proposed.portsAttributes.d.ts b/src/vscode-dts/vscode.proposed.portsAttributes.d.ts index 0ae5840ffd8..6f07fe7aadf 100644 --- a/src/vscode-dts/vscode.proposed.portsAttributes.d.ts +++ b/src/vscode-dts/vscode.proposed.portsAttributes.d.ts @@ -8,11 +8,29 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/115616 @alexr00 export enum PortAutoForwardAction { + /** + * Notify the user that the port is being forwarded. This is the default action. + */ Notify = 1, + /** + * Once the port is forwarded, open the browser to the forwarded port. + */ OpenBrowser = 2, + /** + * Once the port is forwarded, open the preview browser to the forwarded port. + */ OpenPreview = 3, + /** + * Forward the port silently. + */ Silent = 4, + /** + * Do not forward the port. + */ Ignore = 5, + /** + * Once the port is forwarded, open the browser to the forwarded port. Only open the browser the first time the port is forwarded in a session. + */ OpenBrowserOnce = 6 } @@ -49,6 +67,26 @@ declare module 'vscode' { providePortAttributes(port: number, pid: number | undefined, commandLine: string | undefined, token: CancellationToken): ProviderResult; } + export interface PortAttributesProviderSelector { + /** + * TODO: @alexr00 no one is currently using this. Should we delete it? + * If your {@link PortAttributesProvider PortAttributesProvider} is registered after your process has started then already know the process id of port you are listening on. + * Specifying a pid will cause your provider to only be called for ports that match the pid. + */ + pid?: number; + + /** + * Specifying a port range will cause your provider to only be called for ports within the range. + */ + portRange?: [number, number]; + + /** + * TODO: @alexr00 no one is currently using this. Should we delete it? + * Specifying a command pattern will cause your provider to only be called for processes whose command line matches the pattern. + */ + commandPattern?: RegExp; + } + export namespace workspace { /** * If your extension listens on ports, consider registering a PortAttributesProvider to provide information @@ -56,13 +94,12 @@ declare module 'vscode' { * this information with a PortAttributesProvider the extension can tell the editor that these ports should be * ignored, since they don't need to be user facing. * - * @param portSelector If registerPortAttributesProvider is called after you start your process then you may already - * know the range of ports or the pid of your process. All properties of a the portSelector must be true for your - * provider to get called. - * The `portRange` is start inclusive and end exclusive. - * The `commandPattern` is a regular expression that will be matched against the command line of the process. - * @param provider The PortAttributesProvider + * The results of the PortAttributesProvider are merged with the user setting `remote.portsAttributes`. If the values conflict, the user setting takes precedence. + * + * @param portSelector It is best practice to specify a port selector to avoid unnecessary calls to your provider. + * If you don't specify a port selector your provider will be called for every port, which will result in slower port forwarding for the user. + * @param provider The {@link PortAttributesProvider PortAttributesProvider}. */ - export function registerPortAttributesProvider(portSelector: { pid?: number; portRange?: [number, number]; commandPattern?: RegExp }, provider: PortAttributesProvider): Disposable; + export function registerPortAttributesProvider(portSelector: PortAttributesProviderSelector, provider: PortAttributesProvider): Disposable; } } diff --git a/yarn.lock b/yarn.lock index 668efa4efc7..79c9a882b65 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1309,10 +1309,10 @@ https-proxy-agent "^5.0.0" proxy-from-env "^1.1.0" -"@vscode/ripgrep@^1.15.2": - version "1.15.2" - resolved "https://registry.yarnpkg.com/@vscode/ripgrep/-/ripgrep-1.15.2.tgz#85b55181353d6d204210e64e03853c5e2ee6edd9" - integrity sha512-8zmyoxV6F+CY1Rinaq7LO/bGShaX2+B333X+Nqo984nC6jg2OvfZtQHzU+PKNQte2fjhm9h2ZlZTufnJxHaX9w== +"@vscode/ripgrep@^1.15.3": + version "1.15.3" + resolved "https://registry.yarnpkg.com/@vscode/ripgrep/-/ripgrep-1.15.3.tgz#bd53c555ed7f2f546edc46a47d72b1914a5ba23d" + integrity sha512-fCJP+4MRnhSTWw+GYAH93kSIomWYvdSe5206IqcHofBFcaFKR51XQNU0D5RB26Ps/5zRf5AQS26DIqqbMsB1Cw== dependencies: https-proxy-agent "^5.0.0" proxy-from-env "^1.1.0" @@ -10463,10 +10463,10 @@ xterm-addon-image@0.4.0: resolved "https://registry.yarnpkg.com/xterm-addon-image/-/xterm-addon-image-0.4.0.tgz#36e98fa892db11755a5f6e9654f924e876e29bf8" integrity sha512-3wumCJo4WTzxvecSMxJ7XtpVQeFe4gE2cdHCyUdo7zagVkS18YXJacGx6DjlAIccdJn6/LhGuD99xOSSvYx9Gw== -xterm-addon-search@0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.11.0.tgz#2a00ff7f9848f6140e7c4d1782486b0b18b06e0d" - integrity sha512-6U4uHXcQ7G5igsdaGqrJ9ehm7vep24bXqWxuy3AnIosXF2Z5uy2MvmYRyTGNembIqPV/x1YhBQ7uShtuqBHhOQ== +xterm-addon-search@0.12.0-beta.3: + version "0.12.0-beta.3" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.12.0-beta.3.tgz#9ced9431378d37f4b42010691eb5b76f66b8b9e5" + integrity sha512-NgzmUP5/764+csltcteHvIoNOScQHkIXkwEj+KgQr4QR48qFZMq3iGybcvv6icJsGglvvdG7YRputyT35eCjpQ== xterm-addon-serialize@0.9.0: version "0.9.0" @@ -10483,15 +10483,15 @@ xterm-addon-webgl@0.15.0-beta.10: resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.15.0-beta.10.tgz#39ebbfb1b89c6773a2d8cb8e1d2f3ef1f08b28f9" integrity sha512-JVv4t5q6QGWyLiEAcAk9H2B83hFlIalzEwWu1VVYso0MJyZAlZ0NP5Za03iSKxYi7RQIA5bOe8r7W24esQDjLg== -xterm-headless@5.2.0-beta.41: - version "5.2.0-beta.41" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.2.0-beta.41.tgz#17085c0ef255214c244bdaa73e5914fd722be28e" - integrity sha512-cAKuiYPs2GQpCWFIWHakRF+57vPDmqLzMFX3kOcMj+deHPuDnxucQTZvjxKNbMZKG9u9r+shIyfGzgC0oA+bBw== +xterm-headless@5.2.0-beta.43: + version "5.2.0-beta.43" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-5.2.0-beta.43.tgz#efcf143f29d5e656329e732d46280c50508d2c36" + integrity sha512-iMPkICe93emuX0KBjbE3fBmeRlBoE05ohLAlsBEr1OtSIpFervMxXP18Ch8EULSU25lSmZ6xISuAj9TdQSbvYA== -xterm@5.2.0-beta.41: - version "5.2.0-beta.41" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.2.0-beta.41.tgz#ff9f5ed8890a751db2263ebdd362f5443699026d" - integrity sha512-yvDMaeELF8YuqTv220/+i5MKvy9GB4Vu7tH1T3OIqPvHwgnC8SY1vOSjskxHsHsRE5qLluiQJo6/wvt3kZH8+A== +xterm@5.2.0-beta.43: + version "5.2.0-beta.43" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.2.0-beta.43.tgz#08657de5aebaef2cbac3f030f2017441ba9b1453" + integrity sha512-/0BcW7ZavHCtnHsr+t6jHWXBQ5H2J+yg22t8R8YaGebb+0LlMhut2cPTbyOZxYOFHkkiGolEIgGmU4XW1b6toA== y18n@^3.2.1: version "3.2.2"