diff --git a/code/cli/src/bin/code/main.rs b/code/cli/src/bin/code/main.rs index d000998c7dc..62e4195c7e4 100644 --- a/code/cli/src/bin/code/main.rs +++ b/code/cli/src/bin/code/main.rs @@ -95,7 +95,9 @@ 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::CommandShell(cs_args)) => { + tunnels::command_shell(context!(), cs_args).await + } Some(args::Commands::Tunnel(tunnel_args)) => match tunnel_args.subcommand { Some(args::TunnelSubcommand::Prune) => tunnels::prune(context!()).await, diff --git a/code/cli/src/commands/args.rs b/code/cli/src/commands/args.rs index e716e58b7c0..d34519d6810 100644 --- a/code/cli/src/commands/args.rs +++ b/code/cli/src/commands/args.rs @@ -174,7 +174,14 @@ pub enum Commands { /// Runs the control server on process stdin/stdout #[clap(hide = true)] - CommandShell, + CommandShell(CommandShellArgs), +} + +#[derive(Args, Debug, Clone)] +pub struct CommandShellArgs { + /// Listen on a socket instead of stdin/stdout. + #[clap(long)] + pub on_socket: bool, } #[derive(Args, Debug, Clone)] diff --git a/code/cli/src/commands/tunnels.rs b/code/cli/src/commands/tunnels.rs index c098188135e..9831de6e426 100644 --- a/code/cli/src/commands/tunnels.rs +++ b/code/cli/src/commands/tunnels.rs @@ -5,6 +5,7 @@ use async_trait::async_trait; use base64::{engine::general_purpose as b64, Engine as _}; +use futures::{stream::FuturesUnordered, StreamExt}; use serde::Serialize; use sha2::{Digest, Sha256}; use std::{str::FromStr, time::Duration}; @@ -12,13 +13,14 @@ use sysinfo::Pid; use super::{ args::{ - AuthProvider, CliCore, ExistingTunnelArgs, TunnelRenameArgs, TunnelServeArgs, - TunnelServiceSubCommands, TunnelUserSubCommands, + AuthProvider, CliCore, CommandShellArgs, ExistingTunnelArgs, TunnelRenameArgs, + TunnelServeArgs, TunnelServiceSubCommands, TunnelUserSubCommands, }, CommandContext, }; use crate::{ + async_pipe::{get_socket_name, listen_socket_rw_stream, socket_stream_split}, auth::Auth, constants::{APPLICATION_NAME, TUNNEL_CLI_LOCK_NAME, TUNNEL_SERVICE_LOCK_NAME}, log, @@ -120,23 +122,55 @@ impl ServiceContainer for TunnelServiceContainer { } } -pub async fn command_shell(ctx: CommandContext) -> Result { +pub async fn command_shell(ctx: CommandContext, args: CommandShellArgs) -> 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; + let mut params = 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(), + }; - Ok(0) + if !args.on_socket { + serve_stream(tokio::io::stdin(), tokio::io::stderr(), params).await; + return Ok(0); + } + + let socket = get_socket_name(); + let mut listener = listen_socket_rw_stream(&socket) + .await + .map_err(|e| wrap(e, "error listening on socket"))?; + + params + .log + .result(format!("Listening on {}", socket.display())); + + let mut servers = FuturesUnordered::new(); + + loop { + tokio::select! { + Some(_) = servers.next() => {}, + socket = listener.accept() => { + match socket { + Ok(s) => { + let (read, write) = socket_stream_split(s); + servers.push(serve_stream(read, write, params.clone())); + }, + Err(e) => { + error!(params.log, &format!("Error accepting connection: {}", e)); + return Ok(1); + } + } + }, + _ = params.exit_barrier.wait() => { + // wait for all servers to finish up: + while (servers.next().await).is_some() { } + return Ok(0); + } + } + } } pub async fn service( diff --git a/code/cli/src/tunnels/control_server.rs b/code/cli/src/tunnels/control_server.rs index e0c1ec19fc2..8577f9668e9 100644 --- a/code/cli/src/tunnels/control_server.rs +++ b/code/cli/src/tunnels/control_server.rs @@ -233,6 +233,7 @@ pub async fn serve( } } +#[derive(Clone)] pub struct ServeStreamParams { pub log: log::Logger, pub launcher_paths: LauncherPaths, diff --git a/code/extensions/github/src/links.ts b/code/extensions/github/src/links.ts index b270792404f..911f0e5376b 100644 --- a/code/extensions/github/src/links.ts +++ b/code/extensions/github/src/links.ts @@ -191,6 +191,8 @@ export function getVscodeDevHost(): string { } export async function ensurePublished(repository: Repository, file: vscode.Uri) { + await repository.status(); + if ((repository.state.HEAD?.type === RefType.Head || repository.state.HEAD?.type === RefType.Tag) // If HEAD is not published, make sure it is && !repository?.state.HEAD?.upstream diff --git a/code/extensions/markdown-language-features/notebook/index.ts b/code/extensions/markdown-language-features/notebook/index.ts index d52d6b6b12a..f050f5a3162 100644 --- a/code/extensions/markdown-language-features/notebook/index.ts +++ b/code/extensions/markdown-language-features/notebook/index.ts @@ -176,42 +176,39 @@ export const activate: ActivationFunction = (ctx) => { hr { border: 0; - height: 2px; - border-bottom: 2px solid; - } - - h2, h3, h4, h5, h6 { - font-weight: normal; + height: 1px; + border-bottom: 1px solid; } h1 { - font-size: 2.3em; + font-size: 2em; + margin-top: 0; + padding-bottom: 0.3em; + border-bottom-width: 1px; + border-bottom-style: solid; } h2 { - font-size: 2em; + font-size: 1.5em; + padding-bottom: 0.3em; + border-bottom-width: 1px; + border-bottom-style: solid; } h3 { - font-size: 1.7em; - } - - h3 { - font-size: 1.5em; + font-size: 1.25em; } h4 { - font-size: 1.3em; + font-size: 1em; } h5 { - font-size: 1.2em; + font-size: 0.875em; } - h1, - h2, - h3 { - font-weight: normal; + h6 { + font-size: 0.85em; } div { @@ -229,12 +226,38 @@ export const activate: ActivationFunction = (ctx) => { } /* Removes bottom margin when only one item exists in markdown cell */ - #preview > *:only-child, - #preview > *:last-child { + #preview > *:not(h1):not(h2):only-child, + #preview > *:not(h1):not(h2):last-child { margin-bottom: 0; padding-bottom: 0; } + h1, + h2, + h3, + h4, + h5, + h6 { + font-weight: 600; + margin-top: 24px; + margin-bottom: 16px; + line-height: 1.25; + } + + .vscode-light h1, + .vscode-light h2, + .vscode-light hr, + .vscode-light td { + border-color: rgba(0, 0, 0, 0.18); + } + + .vscode-dark h1, + .vscode-dark h2, + .vscode-dark hr, + .vscode-dark td { + border-color: rgba(255, 255, 255, 0.18); + } + /* makes all markdown cells consistent */ div { min-height: var(--notebook-markdown-min-height); diff --git a/code/extensions/markdown-language-features/src/markdownEngine.ts b/code/extensions/markdown-language-features/src/markdownEngine.ts index 282cef1548d..cd999c44116 100644 --- a/code/extensions/markdown-language-features/src/markdownEngine.ts +++ b/code/extensions/markdown-language-features/src/markdownEngine.ts @@ -114,6 +114,7 @@ export class MarkdownItEngine implements IMdParser { _contributionProvider.onContributionsChanged(() => { // Markdown plugin contributions may have changed this._md = undefined; + this._tokenCache.clean(); }); } diff --git a/code/extensions/markdown-language-features/src/preview/preview.ts b/code/extensions/markdown-language-features/src/preview/preview.ts index de58e54784a..7ccbc625b47 100644 --- a/code/extensions/markdown-language-features/src/preview/preview.ts +++ b/code/extensions/markdown-language-features/src/preview/preview.ts @@ -95,7 +95,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { } this._register(_contributionProvider.onContributionsChanged(() => { - setTimeout(() => this.refresh(), 0); + setTimeout(() => this.refresh(true), 0); })); this._register(vscode.workspace.onDidChangeTextDocument(event => { diff --git a/code/extensions/notebook-renderers/src/index.ts b/code/extensions/notebook-renderers/src/index.ts index df674353633..090e9719420 100644 --- a/code/extensions/notebook-renderers/src/index.ts +++ b/code/extensions/notebook-renderers/src/index.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import type { ActivationFunction, OutputItem, RendererContext } from 'vscode-notebook-renderer'; -import { createOutputContent, scrollableClass } from './textHelper'; -import { HtmlRenderingHook, IDisposable, IRichRenderContext, JavaScriptRenderingHook, RenderOptions } from './rendererTypes'; +import { createOutputContent, appendOutput, scrollableClass } from './textHelper'; +import { HtmlRenderingHook, IDisposable, IRichRenderContext, JavaScriptRenderingHook, OutputWithAppend, RenderOptions } from './rendererTypes'; import { ttPolicy } from './htmlHelper'; function clearContainer(container: HTMLElement) { @@ -152,7 +152,7 @@ function renderError( outputInfo: OutputItem, outputElement: HTMLElement, ctx: IRichRenderContext, - trustHTML: boolean + trustHtml: boolean ): IDisposable { const disposableStore = createDisposableStore(); @@ -172,7 +172,7 @@ function renderError( outputElement.classList.add('traceback'); const outputScrolling = scrollingEnabled(outputInfo, ctx.settings); - const content = createOutputContent(outputInfo.id, [err.stack ?? ''], ctx.settings.lineLimit, outputScrolling, trustHTML); + const content = createOutputContent(outputInfo.id, err.stack ?? '', { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml }); const contentParent = document.createElement('div'); contentParent.classList.toggle('word-wrap', ctx.settings.outputWordWrap); disposableStore.push(ctx.onDidChangeSettings(e => { @@ -270,19 +270,13 @@ function scrollingEnabled(output: OutputItem, options: RenderOptions) { // div.output.output-stream <-- outputElement parameter // div.scrollable? tabindex="0" <-- contentParent // div output-item-id="{guid}" <-- content from outputItem parameter -function renderStream(outputInfo: OutputItem, outputElement: HTMLElement, error: boolean, ctx: IRichRenderContext): IDisposable { +function renderStream(outputInfo: OutputWithAppend, outputElement: HTMLElement, error: boolean, ctx: IRichRenderContext): IDisposable { const disposableStore = createDisposableStore(); const outputScrolling = scrollingEnabled(outputInfo, ctx.settings); + const outputOptions = { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml: false, error }; outputElement.classList.add('output-stream'); - const text = outputInfo.text(); - const newContent = createOutputContent(outputInfo.id, [text], ctx.settings.lineLimit, outputScrolling, false); - newContent.setAttribute('output-item-id', outputInfo.id); - if (error) { - newContent.classList.add('error'); - } - const scrollTop = outputScrolling ? findScrolledHeight(outputElement) : undefined; const previousOutputParent = getPreviousMatchingContentGroup(outputElement); @@ -290,9 +284,9 @@ function renderStream(outputInfo: OutputItem, outputElement: HTMLElement, error: if (previousOutputParent) { const existingContent = previousOutputParent.querySelector(`[output-item-id="${outputInfo.id}"]`) as HTMLElement | null; if (existingContent) { - existingContent.replaceWith(newContent); - + appendOutput(outputInfo, existingContent, outputOptions); } else { + const newContent = createOutputContent(outputInfo.id, outputInfo.text(), outputOptions); previousOutputParent.appendChild(newContent); } previousOutputParent.classList.toggle('scrollbar-visible', previousOutputParent.scrollHeight > previousOutputParent.clientHeight); @@ -301,12 +295,9 @@ function renderStream(outputInfo: OutputItem, outputElement: HTMLElement, error: const existingContent = outputElement.querySelector(`[output-item-id="${outputInfo.id}"]`) as HTMLElement | null; let contentParent = existingContent?.parentElement; if (existingContent && contentParent) { - existingContent.replaceWith(newContent); - while (newContent.nextSibling) { - // clear out any stale content if we had previously combined streaming outputs into this one - newContent.nextSibling.remove(); - } + appendOutput(outputInfo, existingContent, outputOptions); } else { + const newContent = createOutputContent(outputInfo.id, outputInfo.text(), outputOptions); contentParent = document.createElement('div'); contentParent.appendChild(newContent); while (outputElement.firstChild) { @@ -333,7 +324,7 @@ function renderText(outputInfo: OutputItem, outputElement: HTMLElement, ctx: IRi const text = outputInfo.text(); const outputScrolling = scrollingEnabled(outputInfo, ctx.settings); - const content = createOutputContent(outputInfo.id, [text], ctx.settings.lineLimit, outputScrolling, false); + const content = createOutputContent(outputInfo.id, text, { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml: false }); content.classList.add('output-plaintext'); if (ctx.settings.outputWordWrap) { content.classList.add('word-wrap'); diff --git a/code/extensions/notebook-renderers/src/rendererTypes.ts b/code/extensions/notebook-renderers/src/rendererTypes.ts index 9da94aeef5d..ded12bdcacc 100644 --- a/code/extensions/notebook-renderers/src/rendererTypes.ts +++ b/code/extensions/notebook-renderers/src/rendererTypes.ts @@ -35,3 +35,14 @@ export interface RenderOptions { } export type IRichRenderContext = RendererContext & { readonly settings: RenderOptions; readonly onDidChangeSettings: Event }; + +export type OutputElementOptions = { + linesLimit: number; + scrollable?: boolean; + error?: boolean; + trustHtml?: boolean; +}; + +export interface OutputWithAppend extends OutputItem { + appendedText?(): string | undefined; +} diff --git a/code/extensions/notebook-renderers/src/test/notebookRenderer.test.ts b/code/extensions/notebook-renderers/src/test/notebookRenderer.test.ts index e67d1d8ce26..0f747900377 100644 --- a/code/extensions/notebook-renderers/src/test/notebookRenderer.test.ts +++ b/code/extensions/notebook-renderers/src/test/notebookRenderer.test.ts @@ -5,8 +5,8 @@ import * as assert from 'assert'; import { activate } from '..'; -import { OutputItem, RendererApi } from 'vscode-notebook-renderer'; -import { IDisposable, IRichRenderContext, RenderOptions } from '../rendererTypes'; +import { RendererApi } from 'vscode-notebook-renderer'; +import { IDisposable, IRichRenderContext, OutputWithAppend, RenderOptions } from '../rendererTypes'; import { JSDOM } from "jsdom"; const dom = new JSDOM(); @@ -116,10 +116,13 @@ suite('Notebook builtin output renderer', () => { } } - function createOutputItem(text: string, mime: string, id: string = '123'): OutputItem { + function createOutputItem(text: string, mime: string, id: string = '123', appendedText?: string): OutputWithAppend { return { id: id, mime: mime, + appendedText() { + return appendedText; + }, text() { return text; }, @@ -177,9 +180,9 @@ suite('Notebook builtin output renderer', () => { assert.ok(renderer, 'Renderer not created'); const outputElement = new OutputHtml().getFirstOuputElement(); - const outputItem = createOutputItem('content', 'text/plain'); + const outputItem = createOutputItem('content', mimeType); await renderer!.renderOutputItem(outputItem, outputElement); - const outputItem2 = createOutputItem('replaced content', 'text/plain'); + const outputItem2 = createOutputItem('replaced content', mimeType); await renderer!.renderOutputItem(outputItem2, outputElement); const inserted = outputElement.firstChild as HTMLElement; @@ -189,6 +192,87 @@ suite('Notebook builtin output renderer', () => { }); + test('Append streaming output', async () => { + const context = createContext({ outputWordWrap: false, outputScrolling: true }); + const renderer = await activate(context); + assert.ok(renderer, 'Renderer not created'); + + const outputElement = new OutputHtml().getFirstOuputElement(); + const outputItem = createOutputItem('content', stdoutMimeType, '123', 'ignoredAppend'); + await renderer!.renderOutputItem(outputItem, outputElement); + const outputItem2 = createOutputItem('content\nappended', stdoutMimeType, '123', '\nappended'); + await renderer!.renderOutputItem(outputItem2, outputElement); + + const inserted = outputElement.firstChild as HTMLElement; + assert.ok(inserted.innerHTML.indexOf('>contentappendedcontentcontent { + const context = createContext({ outputScrolling: true }); + const renderer = await activate(context); + assert.ok(renderer, 'Renderer not created'); + + const outputHtml = new OutputHtml(); + const firstOutputElement = outputHtml.getFirstOuputElement(); + const outputItem1 = createOutputItem('first stream content', stdoutMimeType, '1'); + const outputItem2 = createOutputItem(JSON.stringify(error), errorMimeType, '2'); + const outputItem3 = createOutputItem('second stream content', stdoutMimeType, '3'); + await renderer!.renderOutputItem(outputItem1, firstOutputElement); + const secondOutputElement = outputHtml.appendOutputElement(); + await renderer!.renderOutputItem(outputItem2, secondOutputElement); + const thirdOutputElement = outputHtml.appendOutputElement(); + await renderer!.renderOutputItem(outputItem3, thirdOutputElement); + + const appendedItem1 = createOutputItem('', stdoutMimeType, '1', ' appended1'); + await renderer!.renderOutputItem(appendedItem1, firstOutputElement); + const appendedItem3 = createOutputItem('', stdoutMimeType, '3', ' appended3'); + await renderer!.renderOutputItem(appendedItem3, thirdOutputElement); + + assert.ok(firstOutputElement.innerHTML.indexOf('>first stream content') > -1, `Content was not added to output element: ${outputHtml.cellElement.innerHTML}`); + assert.ok(firstOutputElement.innerHTML.indexOf('appended1') > -1, `Content was not appended to output element: ${outputHtml.cellElement.innerHTML}`); + assert.ok(secondOutputElement.innerHTML.indexOf('>NameError -1, `Content was not added to output element: ${outputHtml.cellElement.innerHTML}`); + assert.ok(thirdOutputElement.innerHTML.indexOf('>second stream content') > -1, `Content was not added to output element: ${outputHtml.cellElement.innerHTML}`); + assert.ok(thirdOutputElement.innerHTML.indexOf('appended3') > -1, `Content was not appended to output element: ${outputHtml.cellElement.innerHTML}`); + }); + + test('Append large streaming outputs', async () => { + const context = createContext({ outputWordWrap: false, outputScrolling: true }); + const renderer = await activate(context); + assert.ok(renderer, 'Renderer not created'); + + const outputElement = new OutputHtml().getFirstOuputElement(); + const lotsOfLines = new Array(4998).fill('line').join('\n'); + const firstOuput = lotsOfLines + 'expected1'; + const outputItem = createOutputItem(firstOuput, stdoutMimeType, '123'); + await renderer!.renderOutputItem(outputItem, outputElement); + const appended = '\n' + lotsOfLines + 'expectedAppend'; + const outputItem2 = createOutputItem(firstOuput + appended, stdoutMimeType, '123', appended); + await renderer!.renderOutputItem(outputItem2, outputElement); + + const inserted = outputElement.firstChild as HTMLElement; + assert.ok(inserted.innerHTML.indexOf('expected1') !== -1, `Last bit of previous content should still exist`); + assert.ok(inserted.innerHTML.indexOf('expectedAppend') !== -1, `Content was not appended to output element`); + }); + + test('Streaming outputs larger than the line limit are truncated', async () => { + const context = createContext({ outputWordWrap: false, outputScrolling: true }); + const renderer = await activate(context); + assert.ok(renderer, 'Renderer not created'); + + const outputElement = new OutputHtml().getFirstOuputElement(); + const lotsOfLines = new Array(11000).fill('line').join('\n'); + const firstOuput = 'shouldBeTruncated' + lotsOfLines + 'expected1'; + const outputItem = createOutputItem(firstOuput, stdoutMimeType, '123'); + await renderer!.renderOutputItem(outputItem, outputElement); + + const inserted = outputElement.firstChild as HTMLElement; + assert.ok(inserted.innerHTML.indexOf('expected1') !== -1, `Last bit of content should exist`); + assert.ok(inserted.innerHTML.indexOf('shouldBeTruncated') === -1, `Beginning content should be truncated`); + }); + test(`Render with wordwrap and scrolling for error output`, async () => { const context = createContext({ outputWordWrap: true, outputScrolling: true }); const renderer = await activate(context); @@ -268,6 +352,29 @@ suite('Notebook builtin output renderer', () => { assert.ok(inserted.innerHTML.indexOf('>second stream content { + const context = createContext({ outputScrolling: true }); + const renderer = await activate(context); + assert.ok(renderer, 'Renderer not created'); + + const outputHtml = new OutputHtml(); + const outputElement = outputHtml.getFirstOuputElement(); + const outputItem1 = createOutputItem('first stream content', stdoutMimeType, '1'); + const outputItem2 = createOutputItem('second stream content', stdoutMimeType, '2'); + await renderer!.renderOutputItem(outputItem1, outputElement); + const secondOutput = outputHtml.appendOutputElement(); + await renderer!.renderOutputItem(outputItem2, secondOutput); + const appendingOutput = createOutputItem('', stdoutMimeType, '2', ' appended'); + await renderer!.renderOutputItem(appendingOutput, secondOutput); + + + const inserted = outputElement.firstChild as HTMLElement; + assert.ok(inserted, `nothing appended to output element: ${outputElement.innerHTML}`); + assert.ok(inserted.innerHTML.indexOf('>first stream content -1, `Content was not added to output element: ${outputHtml.cellElement.innerHTML}`); + assert.ok(inserted.innerHTML.indexOf('>second stream content') > -1, `Second content was not added to ouptut element: ${outputHtml.cellElement.innerHTML}`); + assert.ok(inserted.innerHTML.indexOf('appended') > -1, `Content was not appended to ouptut element: ${outputHtml.cellElement.innerHTML}`); + }); + test(`Streaming outputs interleaved with other mime types will produce separate outputs`, async () => { const context = createContext({ outputScrolling: false }); const renderer = await activate(context); diff --git a/code/extensions/notebook-renderers/src/textHelper.ts b/code/extensions/notebook-renderers/src/textHelper.ts index 8cc03fd543e..5cf0e24eb96 100644 --- a/code/extensions/notebook-renderers/src/textHelper.ts +++ b/code/extensions/notebook-renderers/src/textHelper.ts @@ -4,9 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { handleANSIOutput } from './ansi'; - +import { OutputElementOptions, OutputWithAppend } from './rendererTypes'; export const scrollableClass = 'scrollable'; +const softScrollableLineLimit = 5000; +const hardScrollableLineLimit = 8000; + /** * Output is Truncated. View as a [scrollable element] or open in a [text editor]. Adjust cell output [settings...] */ @@ -91,22 +94,70 @@ function truncatedArrayOfString(id: string, buffer: string[], linesLimit: number function scrollableArrayOfString(id: string, buffer: string[], trustHtml: boolean) { const element = document.createElement('div'); - if (buffer.length > 5000) { + if (buffer.length > softScrollableLineLimit) { element.appendChild(generateNestedViewAllElement(id)); } - element.appendChild(handleANSIOutput(buffer.slice(-5000).join('\n'), trustHtml)); + element.appendChild(handleANSIOutput(buffer.slice(-1 * softScrollableLineLimit).join('\n'), trustHtml)); return element; } -export function createOutputContent(id: string, outputs: string[], linesLimit: number, scrollable: boolean, trustHtml: boolean): HTMLElement { +const outputLengths: Record = {}; - const buffer = outputs.join('\n').split(/\r\n|\r|\n/g); +function appendScrollableOutput(element: HTMLElement, id: string, appended: string, trustHtml: boolean) { + if (!outputLengths[id]) { + outputLengths[id] = 0; + } + const buffer = appended.split(/\r\n|\r|\n/g); + const appendedLength = buffer.length + outputLengths[id]; + // Only append outputs up to the hard limit of lines, then replace it with the last softLimit number of lines + if (appendedLength > hardScrollableLineLimit) { + return false; + } + else { + element.appendChild(handleANSIOutput(buffer.join('\n'), trustHtml)); + outputLengths[id] = appendedLength; + } + return true; +} + +export function createOutputContent(id: string, outputText: string, options: OutputElementOptions): HTMLElement { + const { linesLimit, error, scrollable, trustHtml } = options; + const buffer = outputText.split(/\r\n|\r|\n/g); + outputLengths[id] = outputLengths[id] = Math.min(buffer.length, softScrollableLineLimit); + + let outputElement: HTMLElement; if (scrollable) { - return scrollableArrayOfString(id, buffer, trustHtml); + outputElement = scrollableArrayOfString(id, buffer, !!trustHtml); } else { - return truncatedArrayOfString(id, buffer, linesLimit, trustHtml); + outputElement = truncatedArrayOfString(id, buffer, linesLimit, !!trustHtml); + } + + outputElement.setAttribute('output-item-id', id); + if (error) { + outputElement.classList.add('error'); } + + return outputElement; } + +export function appendOutput(outputInfo: OutputWithAppend, existingContent: HTMLElement, options: OutputElementOptions) { + const appendedText = outputInfo.appendedText?.(); + // appending output only supported for scrollable ouputs currently + if (appendedText && options.scrollable) { + if (appendScrollableOutput(existingContent, outputInfo.id, appendedText, false)) { + return; + } + } + + const newContent = createOutputContent(outputInfo.id, outputInfo.text(), options); + existingContent.replaceWith(newContent); + while (newContent.nextSibling) { + // clear out any stale content if we had previously combined streaming outputs into this one + newContent.nextSibling.remove(); + } + +} + diff --git a/code/extensions/typescript-language-features/src/languageFeatures/util/textRendering.ts b/code/extensions/typescript-language-features/src/languageFeatures/util/textRendering.ts index 2918a7de54c..fe26dd64029 100644 --- a/code/extensions/typescript-language-features/src/languageFeatures/util/textRendering.ts +++ b/code/extensions/typescript-language-features/src/languageFeatures/util/textRendering.ts @@ -188,10 +188,10 @@ function convertLinkTags( if (/^https?:/.test(text)) { const parts = text.split(' '); if (parts.length === 1) { - out.push(parts[0]); + out.push(`<${parts[0]}>`); } else if (parts.length > 1) { - const linkText = escapeMarkdownSyntaxTokensForCode(parts.slice(1).join(' ')); - out.push(`[${currentLink.linkcode ? '`' + linkText + '`' : linkText}](${parts[0]})`); + const linkText = parts.slice(1).join(' '); + out.push(`[${currentLink.linkcode ? '`' + escapeMarkdownSyntaxTokensForCode(linkText) + '`' : linkText}](${parts[0]})`); } } else { out.push(escapeMarkdownSyntaxTokensForCode(text)); diff --git a/code/extensions/typescript-language-features/src/typescriptServiceClient.ts b/code/extensions/typescript-language-features/src/typescriptServiceClient.ts index 984356f17b4..86c6bb8d9f1 100644 --- a/code/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/code/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -29,6 +29,7 @@ import { PluginManager, TypeScriptServerPlugin } from './tsServer/plugins'; import { TelemetryProperties, TelemetryReporter, VSCodeTelemetryReporter } from './logging/telemetry'; import Tracer from './logging/tracer'; import { ProjectType, inferredProjectCompilerOptions } from './tsconfig'; +import { Schemes } from './configuration/schemes'; export interface TsDiagnostics { @@ -762,6 +763,18 @@ export default class TypeScriptServiceClient extends Disposable implements IType return undefined; } + // For notebook cells, we need to use the notebook document to look up the workspace + if (resource.scheme === Schemes.notebookCell) { + for (const notebook of vscode.workspace.notebookDocuments) { + for (const cell of notebook.getCells()) { + if (cell.document.uri.toString() === resource.toString()) { + resource = notebook.uri; + break; + } + } + } + } + for (const root of roots.sort((a, b) => a.uri.fsPath.length - b.uri.fsPath.length)) { if (root.uri.scheme === resource.scheme && root.uri.authority === resource.authority) { if (resource.fsPath.startsWith(root.uri.fsPath + path.sep)) { @@ -770,7 +783,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType } } - return undefined; + return vscode.workspace.getWorkspaceFolder(resource)?.uri; } public execute(command: keyof TypeScriptRequests, args: any, token: vscode.CancellationToken, config?: ExecConfig): Promise> { diff --git a/code/extensions/vscode-api-tests/package.json b/code/extensions/vscode-api-tests/package.json index 369927c00f2..b3e681dc25b 100644 --- a/code/extensions/vscode-api-tests/package.json +++ b/code/extensions/vscode-api-tests/package.json @@ -46,7 +46,6 @@ "treeItemCheckbox", "treeViewActiveItem", "treeViewReveal", - "testInvalidateResults", "workspaceTrust", "telemetry", "windowActivity", diff --git a/code/package.json b/code/package.json index 48909973c1d..0ce6acae090 100644 --- a/code/package.json +++ b/code/package.json @@ -212,7 +212,7 @@ "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "tsec": "0.2.7", - "typescript": "^5.2.0-dev.20230712", + "typescript": "^5.2.0-dev.20230718", "typescript-formatter": "7.1.0", "underscore": "^1.12.1", "util": "^0.12.4", diff --git a/code/src/vs/base/common/event.ts b/code/src/vs/base/common/event.ts index c4f9b728259..872263f2e9e 100644 --- a/code/src/vs/base/common/event.ts +++ b/code/src/vs/base/common/event.ts @@ -682,6 +682,7 @@ export namespace Event { } }; observable.addObserver(observer); + observable.reportChanges(); return { dispose() { observable.removeObserver(observer); diff --git a/code/src/vs/base/common/htmlContent.ts b/code/src/vs/base/common/htmlContent.ts index 9cefc0e56a4..10e25c9c9e3 100644 --- a/code/src/vs/base/common/htmlContent.ts +++ b/code/src/vs/base/common/htmlContent.ts @@ -60,7 +60,7 @@ export class MarkdownString implements IMarkdownString { this.value += escapeMarkdownSyntaxTokens(this.supportThemeIcons ? escapeIcons(value) : value) .replace(/([ \t]+)/g, (_match, g1) => ' '.repeat(g1.length)) .replace(/\>/gm, '\\>') - .replace(/\n/g, newlineStyle === MarkdownStringTextNewlineStyle.Break ? '\\\n' : '\n\n'); + .replace(/\n/g, newlineStyle === MarkdownStringTextNewlineStyle.Break ? '\\\n' : '\n\n'); // CodeQL [SM02383] The Markdown is fully sanitized after being rendered. return this; } diff --git a/code/src/vs/base/common/observableImpl/utils.ts b/code/src/vs/base/common/observableImpl/utils.ts index 5d9a2c568a8..b922cea1c95 100644 --- a/code/src/vs/base/common/observableImpl/utils.ts +++ b/code/src/vs/base/common/observableImpl/utils.ts @@ -278,6 +278,7 @@ export function wasEventTriggeredRecently(event: Event, timeoutMs: number, return observable; } +// TODO@hediet: Have `keepCacheAlive` and `recomputeOnChange` instead of forceRecompute /** * This ensures the observable is being observed. * Observed observables (such as {@link derived}s) can maintain a cache, as they receive invalidation events. diff --git a/code/src/vs/base/common/prefixTree.ts b/code/src/vs/base/common/prefixTree.ts new file mode 100644 index 00000000000..b4001390526 --- /dev/null +++ b/code/src/vs/base/common/prefixTree.ts @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const unset = Symbol('unset'); + +/** + * A simple prefix tree implementation where a value is stored based on + * well-defined prefix segments. + */ +export class WellDefinedPrefixTree { + private readonly root = new Node(); + + /** Inserts a new value in the prefix tree. */ + insert(key: Iterable, value: V): void { + let node = this.root; + for (const part of key) { + if (!node.children) { + const next = new Node(); + node.children = new Map([[part, next]]); + node = next; + } else if (!node.children.has(part)) { + const next = new Node(); + node.children.set(part, next); + node = next; + } else { + node = node.children.get(part)!; + } + + } + + node.value = value; + } + + /** Gets a value from the tree. */ + find(key: Iterable): V | undefined { + let node = this.root; + for (const segment of key) { + const next = node.children?.get(segment); + if (!next) { + return undefined; + } + + node = next; + } + + return node.value === unset ? undefined : node.value; + } + + /** Gets whether the tree has the key, or a parent of the key, already inserted. */ + hasKeyOrParent(key: Iterable): boolean { + let node = this.root; + for (const segment of key) { + const next = node.children?.get(segment); + if (!next) { + return false; + } + if (next.value !== unset) { + return true; + } + + node = next; + } + + return false; + } + + /** Gets whether the tree has the given key or any children. */ + hasKeyOrChildren(key: Iterable): boolean { + let node = this.root; + for (const segment of key) { + const next = node.children?.get(segment); + if (!next) { + return false; + } + + node = next; + } + + return true; + } +} + +class Node { + public children?: Map>; + public value: T | typeof unset = unset; +} diff --git a/code/src/vs/base/test/common/observable.test.ts b/code/src/vs/base/test/common/observable.test.ts index 64d08a6c4d5..822becee547 100644 --- a/code/src/vs/base/test/common/observable.test.ts +++ b/code/src/vs/base/test/common/observable.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { ISettableObservable, autorun, derived, ITransaction, observableFromEvent, observableValue, transaction, keepAlive } from 'vs/base/common/observable'; import { BaseObservable, IObservable, IObserver } from 'vs/base/common/observableImpl/base'; @@ -962,6 +962,36 @@ suite('observables', () => { myObservable2.set(1, tx); }); }); + + test('bug: fromObservableLight doesnt subscribe', () => { + const log = new Log(); + const myObservable = new LoggingObservableValue('myObservable', 0, log); + + const myDerived = derived('myDerived', reader => { + const val = myObservable.read(reader); + log.log(`myDerived.computed(myObservable2: ${val})`); + return val % 10; + }); + + const e = Event.fromObservableLight(myDerived); + log.log('event created'); + e(() => { + log.log('event fired'); + }); + + myObservable.set(1, undefined); + + assert.deepStrictEqual(log.getAndClearEntries(), [ + 'event created', + 'myObservable.firstObserverAdded', + 'myObservable.get', + 'myDerived.computed(myObservable2: 0)', + 'myObservable.set (value 1)', + 'myObservable.get', + 'myDerived.computed(myObservable2: 1)', + 'event fired', + ]); + }); }); export class LoggingObserver implements IObserver { diff --git a/code/src/vs/base/test/common/prefixTree.test.ts b/code/src/vs/base/test/common/prefixTree.test.ts new file mode 100644 index 00000000000..3b1cb88ae86 --- /dev/null +++ b/code/src/vs/base/test/common/prefixTree.test.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. + *--------------------------------------------------------------------------------------------*/ + +import { WellDefinedPrefixTree } from 'vs/base/common/prefixTree'; +import * as assert from 'assert'; + +suite('WellDefinedPrefixTree', () => { + let tree: WellDefinedPrefixTree; + + setup(() => { + tree = new WellDefinedPrefixTree(); + }); + + test('find', () => { + const key1 = ['foo', 'bar']; + const key2 = ['foo', 'baz']; + tree.insert(key1, 42); + tree.insert(key2, 43); + assert.strictEqual(tree.find(key1), 42); + assert.strictEqual(tree.find(key2), 43); + assert.strictEqual(tree.find(['foo', 'baz', 'bop']), undefined); + assert.strictEqual(tree.find(['foo']), undefined); + }); + + test('hasParentOfKey', () => { + const key = ['foo', 'bar']; + tree.insert(key, 42); + + assert.strictEqual(tree.hasKeyOrParent(['foo', 'bar', 'baz']), true); + assert.strictEqual(tree.hasKeyOrParent(['foo', 'bar']), true); + assert.strictEqual(tree.hasKeyOrParent(['foo']), false); + assert.strictEqual(tree.hasKeyOrParent(['baz']), false); + }); + + + test('hasKeyOrChildren', () => { + const key = ['foo', 'bar']; + tree.insert(key, 42); + + assert.strictEqual(tree.hasKeyOrChildren([]), true); + assert.strictEqual(tree.hasKeyOrChildren(['foo']), true); + assert.strictEqual(tree.hasKeyOrChildren(['foo', 'bar']), true); + assert.strictEqual(tree.hasKeyOrChildren(['foo', 'bar', 'baz']), false); + }); +}); diff --git a/code/src/vs/editor/browser/widget/codeEditorWidget.ts b/code/src/vs/editor/browser/widget/codeEditorWidget.ts index 0903e174700..c05cb46f268 100644 --- a/code/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/code/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -73,6 +73,8 @@ export interface ICodeEditorWidgetOptions { /** * Contributions to instantiate. + * When provided, only the contributions included will be instantiated. + * To include the defaults, those must be provided as well via [...EditorExtensionsRegistry.getEditorContributions()] * Defaults to EditorExtensionsRegistry.getEditorContributions(). */ contributions?: IEditorContributionDescription[]; diff --git a/code/src/vs/editor/browser/widget/diffEditorWidget2/accessibleDiffViewer.ts b/code/src/vs/editor/browser/widget/diffEditorWidget2/accessibleDiffViewer.ts new file mode 100644 index 00000000000..346f847085e --- /dev/null +++ b/code/src/vs/editor/browser/widget/diffEditorWidget2/accessibleDiffViewer.ts @@ -0,0 +1,680 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { addDisposableListener, addStandardDisposableListener, reset } from 'vs/base/browser/dom'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { Action } from 'vs/base/common/actions'; +import { Codicon } from 'vs/base/common/codicons'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { IObservable, ISettableObservable, ITransaction, autorun, constObservable, derived, keepAlive, observableValue, transaction } from 'vs/base/common/observable'; +import { autorunWithStore2 } from 'vs/base/common/observableImpl/autorun'; +import { subtransaction } from 'vs/base/common/observableImpl/base'; +import { derivedWithStore } from 'vs/base/common/observableImpl/derived'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { applyFontInfo } from 'vs/editor/browser/config/domFontInfo'; +import { DiffEditorEditors } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorEditors'; +import { applyStyle } from 'vs/editor/browser/widget/diffEditorWidget2/utils'; +import { DiffReview } from 'vs/editor/browser/widget/diffReview'; +import { EditorFontLigatures, EditorOption, IComputedEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { LineRange } from 'vs/editor/common/core/lineRange'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { LineRangeMapping, SimpleLineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer'; +import { ILanguageIdCodec } from 'vs/editor/common/languages'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ITextModel, TextModelResolvedOptions } from 'vs/editor/common/model'; +import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; +import { RenderLineInput, renderViewLine2 } from 'vs/editor/common/viewLayout/viewLineRenderer'; +import { ViewLineRenderingData } from 'vs/editor/common/viewModel'; +import { localize } from 'vs/nls'; +import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; + +const diffReviewInsertIcon = registerIcon('diff-review-insert', Codicon.add, localize('diffReviewInsertIcon', 'Icon for \'Insert\' in diff review.')); +const diffReviewRemoveIcon = registerIcon('diff-review-remove', Codicon.remove, localize('diffReviewRemoveIcon', 'Icon for \'Remove\' in diff review.')); +const diffReviewCloseIcon = registerIcon('diff-review-close', Codicon.close, localize('diffReviewCloseIcon', 'Icon for \'Close\' in diff review.')); + +export class AccessibleDiffViewer extends Disposable { + constructor( + private readonly _parentNode: HTMLElement, + private readonly _visible: ISettableObservable, + private readonly _width: IObservable, + private readonly _height: IObservable, + private readonly _diffs: IObservable, + private readonly _editors: DiffEditorEditors, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + this._register(keepAlive(this.model, true)); + } + + private readonly model = derivedWithStore('model', (reader, store) => { + const visible = this._visible.read(reader); + this._parentNode.style.visibility = visible ? 'visible' : 'hidden'; + if (!visible) { + return null; + } + const model = store.add(this._instantiationService.createInstance(ViewModel, this._diffs, this._editors, this._visible)); + const view = store.add(this._instantiationService.createInstance(View, this._parentNode, model, this._width, this._height, this._editors)); + return { + model, + view + }; + }); + + next(): void { + transaction(tx => { + const isVisible = this._visible.get(); + this._visible.set(true, tx); + if (isVisible) { + this.model.get()!.model.nextGroup(tx); + } + }); + } + + prev(): void { + transaction(tx => { + this._visible.set(true, tx); + this.model.get()!.model.previousGroup(tx); + }); + } + + close(): void { + transaction(tx => { + this._visible.set(false, tx); + }); + } +} + +class ViewModel extends Disposable { + private readonly _groups = observableValue('groups', []); + private readonly _currentGroupIdx = observableValue('currentGroupIdx', 0); + private readonly _currentElementIdx = observableValue('currentElementIdx', 0); + + public readonly groups: IObservable = this._groups; + public readonly currentGroup: IObservable + = this._currentGroupIdx.map((idx, r) => this._groups.read(r)[idx]); + public readonly currentGroupIndex: IObservable = this._currentGroupIdx; + + public readonly currentElement: IObservable + = this._currentElementIdx.map((idx, r) => this.currentGroup.read(r)?.lines[idx]); + + public readonly canClose: IObservable = constObservable(true); + + constructor( + private readonly _diffs: IObservable, + private readonly _editors: DiffEditorEditors, + private readonly _visible: ISettableObservable, + @IAudioCueService private readonly _audioCueService: IAudioCueService, + ) { + super(); + + this._register(autorun('update groups', reader => { + const diffs = this._diffs.read(reader); + if (!diffs) { + this._groups.set([], undefined); + return; + } + + const groups = computeViewElementGroups( + diffs, + this._editors.original.getModel()!.getLineCount(), + this._editors.modified.getModel()!.getLineCount() + ); + + transaction(tx => { + const p = this._editors.modified.getPosition(); + if (p) { + const nextGroup = groups.findIndex(g => p?.lineNumber < g.range.modified.endLineNumberExclusive); + if (nextGroup !== -1) { + this._currentGroupIdx.set(nextGroup, tx); + } + } + this._groups.set(groups, tx); + }); + })); + + this._register(autorun('play audio-cue for diff', reader => { + const currentViewItem = this.currentElement.read(reader); + if (currentViewItem?.type === LineType.Deleted) { + this._audioCueService.playAudioCue(AudioCue.diffLineDeleted); + } else if (currentViewItem?.type === LineType.Added) { + this._audioCueService.playAudioCue(AudioCue.diffLineInserted); + } + })); + + this._register(autorun('select lines in editor', reader => { + // This ensures editor commands (like revert/stage) work + const currentViewItem = this.currentElement.read(reader); + if (currentViewItem && currentViewItem.type !== LineType.Header) { + const lineNumber = currentViewItem.modifiedLineNumber ?? currentViewItem.diff.modifiedRange.startLineNumber; + this._editors.modified.setSelection(Range.fromPositions(new Position(lineNumber, 1))); + } + })); + } + + private _goToGroupDelta(delta: number, tx?: ITransaction): void { + const groups = this.groups.get(); + if (!groups || groups.length <= 1) { return; } + subtransaction(tx, tx => { + this._currentGroupIdx.set((this._currentGroupIdx.get() + groups.length + delta) % groups.length, tx); + this._currentElementIdx.set(0, tx); + }); + } + + nextGroup(tx?: ITransaction): void { this._goToGroupDelta(1, tx); } + previousGroup(tx?: ITransaction): void { this._goToGroupDelta(-1, tx); } + + private _goToLineDelta(delta: number): void { + const group = this.currentGroup.get(); + if (!group || group.lines.length <= 1) { return; } + transaction(tx => { + this._currentElementIdx.set((this._currentElementIdx.get() + group.lines.length + delta) % group.lines.length, tx); + }); + } + + goToNextLine(): void { this._goToLineDelta(1); } + goToPreviousLine(): void { this._goToLineDelta(-1); } + + goToLine(line: ViewElement): void { + const group = this.currentGroup.get(); + if (!group) { return; } + const idx = group.lines.indexOf(line); + if (idx === -1) { return; } + transaction(tx => { + this._currentElementIdx.set(idx, tx); + }); + } + + revealCurrentElementInEditor(): void { + this._visible.set(false, undefined); + + const curElem = this.currentElement.get(); + if (curElem) { + if (curElem.type === LineType.Deleted) { + this._editors.original.setSelection(Range.fromPositions(new Position(curElem.originalLineNumber, 1))); + this._editors.original.revealLine(curElem.originalLineNumber); + this._editors.original.focus(); + } else { + if (curElem.type !== LineType.Header) { + this._editors.modified.setSelection(Range.fromPositions(new Position(curElem.modifiedLineNumber, 1))); + this._editors.modified.revealLine(curElem.modifiedLineNumber); + } + this._editors.modified.focus(); + } + } + } + + close(): void { + this._visible.set(false, undefined); + this._editors.modified.focus(); + } +} + + +const viewElementGroupLineMargin = 3; + +function computeViewElementGroups(diffs: LineRangeMapping[], originalLineCount: number, modifiedLineCount: number): ViewElementGroup[] { + const result: ViewElementGroup[] = []; + + for (const g of group(diffs, (a, b) => (b.modifiedRange.startLineNumber - a.modifiedRange.endLineNumberExclusive < 2 * viewElementGroupLineMargin))) { + const viewElements: ViewElement[] = []; + viewElements.push(new HeaderViewElement()); + + const origFullRange = new LineRange( + Math.max(1, g[0].originalRange.startLineNumber - viewElementGroupLineMargin), + Math.min(g[g.length - 1].originalRange.endLineNumberExclusive + viewElementGroupLineMargin, originalLineCount + 1) + ); + const modifiedFullRange = new LineRange( + Math.max(1, g[0].modifiedRange.startLineNumber - viewElementGroupLineMargin), + Math.min(g[g.length - 1].modifiedRange.endLineNumberExclusive + viewElementGroupLineMargin, modifiedLineCount + 1) + ); + + forEachAdjacentItems(g, (a, b) => { + const origRange = new LineRange(a ? a.originalRange.endLineNumberExclusive : origFullRange.startLineNumber, b ? b.originalRange.startLineNumber : origFullRange.endLineNumberExclusive); + const modifiedRange = new LineRange(a ? a.modifiedRange.endLineNumberExclusive : modifiedFullRange.startLineNumber, b ? b.modifiedRange.startLineNumber : modifiedFullRange.endLineNumberExclusive); + + origRange.forEach(origLineNumber => { + viewElements.push(new UnchangedLineViewElement(origLineNumber, modifiedRange.startLineNumber + (origLineNumber - origRange.startLineNumber))); + }); + + if (b) { + b.originalRange.forEach(origLineNumber => { + viewElements.push(new DeletedLineViewElement(b, origLineNumber)); + }); + b.modifiedRange.forEach(modifiedLineNumber => { + viewElements.push(new AddedLineViewElement(b, modifiedLineNumber)); + }); + } + }); + + const modifiedRange = g[0].modifiedRange.join(g[g.length - 1].modifiedRange); + const originalRange = g[0].originalRange.join(g[g.length - 1].originalRange); + + result.push(new ViewElementGroup(new SimpleLineRangeMapping(modifiedRange, originalRange), viewElements)); + } + return result; +} + +enum LineType { + Header, + Unchanged, + Deleted, + Added, +} + +class ViewElementGroup { + constructor( + public readonly range: SimpleLineRangeMapping, + public readonly lines: readonly ViewElement[], + ) { } +} + +type ViewElement = HeaderViewElement | UnchangedLineViewElement | DeletedLineViewElement | AddedLineViewElement; + +class HeaderViewElement { + public readonly type = LineType.Header; +} + +class DeletedLineViewElement { + public readonly type = LineType.Deleted; + + public readonly modifiedLineNumber = undefined; + + constructor( + public readonly diff: LineRangeMapping, + public readonly originalLineNumber: number, + ) { + } +} + +class AddedLineViewElement { + public readonly type = LineType.Added; + + public readonly originalLineNumber = undefined; + + constructor( + public readonly diff: LineRangeMapping, + public readonly modifiedLineNumber: number, + ) { + } +} + +class UnchangedLineViewElement { + public readonly type = LineType.Unchanged; + constructor( + public readonly originalLineNumber: number, + public readonly modifiedLineNumber: number, + ) { + } +} + +class View extends Disposable { + public readonly domNode: HTMLElement; + private readonly _content: HTMLElement; + private readonly _scrollbar: DomScrollableElement; + private readonly _actionBar: ActionBar; + + constructor( + private readonly _element: HTMLElement, + private readonly _model: ViewModel, + private readonly _width: IObservable, + private readonly _height: IObservable, + private readonly _editors: DiffEditorEditors, + @ILanguageService private readonly _languageService: ILanguageService, + ) { + super(); + + this.domNode = this._element; + this.domNode.className = 'diff-review monaco-editor-background'; + + const actionBarContainer = document.createElement('div'); + actionBarContainer.className = 'diff-review-actions'; + this._actionBar = this._register(new ActionBar( + actionBarContainer + )); + this._actionBar.push(new Action( + 'diffreview.close', + localize('label.close', "Close"), + 'close-diff-review ' + ThemeIcon.asClassName(diffReviewCloseIcon), + true, + async () => _model.close() + ), { label: false, icon: true }); + + + this._content = document.createElement('div'); + this._content.className = 'diff-review-content'; + this._content.setAttribute('role', 'code'); + this._scrollbar = this._register(new DomScrollableElement(this._content, {})); + reset(this.domNode, this._scrollbar.getDomNode(), actionBarContainer); + + this._register(toDisposable(() => { reset(this.domNode); })); + + this._register(applyStyle(this.domNode, { width: this._width, height: this._height })); + this._register(applyStyle(this._content, { width: this._width, height: this._height })); + + this._register(autorunWithStore2('render', (reader, store) => { + this._model.currentGroup.read(reader); + this._render(store); + })); + + // TODO@hediet use commands + this._register(addStandardDisposableListener(this.domNode, 'keydown', (e) => { + if ( + e.equals(KeyCode.DownArrow) + || e.equals(KeyMod.CtrlCmd | KeyCode.DownArrow) + || e.equals(KeyMod.Alt | KeyCode.DownArrow) + ) { + e.preventDefault(); + this._model.goToNextLine(); + } + + if ( + e.equals(KeyCode.UpArrow) + || e.equals(KeyMod.CtrlCmd | KeyCode.UpArrow) + || e.equals(KeyMod.Alt | KeyCode.UpArrow) + ) { + e.preventDefault(); + this._model.goToPreviousLine(); + } + + if ( + e.equals(KeyCode.Escape) + || e.equals(KeyMod.CtrlCmd | KeyCode.Escape) + || e.equals(KeyMod.Alt | KeyCode.Escape) + || e.equals(KeyMod.Shift | KeyCode.Escape) + ) { + e.preventDefault(); + this._model.close(); + } + + if ( + e.equals(KeyCode.Space) + || e.equals(KeyCode.Enter) + ) { + e.preventDefault(); + this._model.revealCurrentElementInEditor(); + } + })); + } + + private _render(store: DisposableStore): void { + const originalOptions = this._editors.original.getOptions(); + const modifiedOptions = this._editors.modified.getOptions(); + + const container = document.createElement('div'); + container.className = 'diff-review-table'; + container.setAttribute('role', 'list'); + container.setAttribute('aria-label', localize('ariaLabel', 'Accessible Diff Viewer. Use arrow up and down to navigate.')); + applyFontInfo(container, modifiedOptions.get(EditorOption.fontInfo)); + + reset(this._content, container); + + const originalModel = this._editors.original.getModel(); + const modifiedModel = this._editors.modified.getModel(); + if (!originalModel || !modifiedModel) { + return; + } + + const originalModelOpts = originalModel.getOptions(); + const modifiedModelOpts = modifiedModel.getOptions(); + + const lineHeight = modifiedOptions.get(EditorOption.lineHeight); + const group = this._model.currentGroup.get(); + for (const viewItem of group?.lines || []) { + if (!group) { + break; + } + let row: HTMLDivElement; + + if (viewItem.type === LineType.Header) { + + const header = document.createElement('div'); + header.className = 'diff-review-row'; + header.setAttribute('role', 'listitem'); + + const r = group.range; + const diffIndex = this._model.currentGroupIndex.get(); + const diffsLength = this._model.groups.get().length; + const getAriaLines = (lines: number) => + lines === 0 ? localize('no_lines_changed', "no lines changed") + : lines === 1 ? localize('one_line_changed', "1 line changed") + : localize('more_lines_changed', "{0} lines changed", lines); + + const originalChangedLinesCntAria = getAriaLines(r.original.length); + const modifiedChangedLinesCntAria = getAriaLines(r.modified.length); + header.setAttribute('aria-label', localize({ + key: 'header', + comment: [ + 'This is the ARIA label for a git diff header.', + 'A git diff header looks like this: @@ -154,12 +159,39 @@.', + 'That encodes that at original line 154 (which is now line 159), 12 lines were removed/changed with 39 lines.', + 'Variables 0 and 1 refer to the diff index out of total number of diffs.', + 'Variables 2 and 4 will be numbers (a line number).', + 'Variables 3 and 5 will be "no lines changed", "1 line changed" or "X lines changed", localized separately.' + ] + }, "Difference {0} of {1}: original line {2}, {3}, modified line {4}, {5}", + (diffIndex + 1), + diffsLength, + r.original.startLineNumber, + originalChangedLinesCntAria, + r.modified.startLineNumber, + modifiedChangedLinesCntAria + )); + + const cell = document.createElement('div'); + cell.className = 'diff-review-cell diff-review-summary'; + // e.g.: `1/10: @@ -504,7 +517,7 @@` + cell.appendChild(document.createTextNode(`${diffIndex + 1}/${diffsLength}: @@ -${r.original.startLineNumber},${r.original.length} +${r.modified.startLineNumber},${r.modified.length} @@`)); + header.appendChild(cell); + + row = header; + } else { + row = this._createRow(viewItem, lineHeight, + this._width.get(), originalOptions, originalModel, originalModelOpts, modifiedOptions, modifiedModel, modifiedModelOpts, + ); + } + + container.appendChild(row); + + const isSelectedObs = derived('isSelected', reader => this._model.currentElement.read(reader) === viewItem); + + store.add(autorun('update tab index', reader => { + const isSelected = isSelectedObs.read(reader); + row.tabIndex = isSelected ? 0 : -1; + if (isSelected) { + row.focus(); + } + })); + + store.add(addDisposableListener(row, 'focus', () => { + this._model.goToLine(viewItem); + })); + } + + this._scrollbar.scanDomNode(); + } + + private _createRow( + item: DeletedLineViewElement | AddedLineViewElement | UnchangedLineViewElement, + lineHeight: number, + width: number, + originalOptions: IComputedEditorOptions, originalModel: ITextModel, originalModelOpts: TextModelResolvedOptions, + modifiedOptions: IComputedEditorOptions, modifiedModel: ITextModel, modifiedModelOpts: TextModelResolvedOptions, + ): HTMLDivElement { + const originalLayoutInfo = originalOptions.get(EditorOption.layoutInfo); + const originalLineNumbersWidth = originalLayoutInfo.glyphMarginWidth + originalLayoutInfo.lineNumbersWidth; + + const modifiedLayoutInfo = modifiedOptions.get(EditorOption.layoutInfo); + const modifiedLineNumbersWidth = 10 + modifiedLayoutInfo.glyphMarginWidth + modifiedLayoutInfo.lineNumbersWidth; + + let rowClassName: string = 'diff-review-row'; + let lineNumbersExtraClassName: string = ''; + const spacerClassName: string = 'diff-review-spacer'; + let spacerIcon: ThemeIcon | null = null; + switch (item.type) { + case LineType.Added: + rowClassName = 'diff-review-row line-insert'; + lineNumbersExtraClassName = ' char-insert'; + spacerIcon = diffReviewInsertIcon; + break; + case LineType.Deleted: + rowClassName = 'diff-review-row line-delete'; + lineNumbersExtraClassName = ' char-delete'; + spacerIcon = diffReviewRemoveIcon; + break; + } + + const row = document.createElement('div'); + row.style.minWidth = width + 'px'; + row.className = rowClassName; + row.setAttribute('role', 'listitem'); + row.ariaLevel = ''; + + const cell = document.createElement('div'); + cell.className = 'diff-review-cell'; + cell.style.height = `${lineHeight}px`; + row.appendChild(cell); + + const originalLineNumber = document.createElement('span'); + originalLineNumber.style.width = (originalLineNumbersWidth + 'px'); + originalLineNumber.style.minWidth = (originalLineNumbersWidth + 'px'); + originalLineNumber.className = 'diff-review-line-number' + lineNumbersExtraClassName; + if (item.originalLineNumber !== undefined) { + originalLineNumber.appendChild(document.createTextNode(String(item.originalLineNumber))); + } else { + originalLineNumber.innerText = '\u00a0'; + } + cell.appendChild(originalLineNumber); + + const modifiedLineNumber = document.createElement('span'); + modifiedLineNumber.style.width = (modifiedLineNumbersWidth + 'px'); + modifiedLineNumber.style.minWidth = (modifiedLineNumbersWidth + 'px'); + modifiedLineNumber.style.paddingRight = '10px'; + modifiedLineNumber.className = 'diff-review-line-number' + lineNumbersExtraClassName; + if (item.modifiedLineNumber !== undefined) { + modifiedLineNumber.appendChild(document.createTextNode(String(item.modifiedLineNumber))); + } else { + modifiedLineNumber.innerText = '\u00a0'; + } + cell.appendChild(modifiedLineNumber); + + const spacer = document.createElement('span'); + spacer.className = spacerClassName; + + if (spacerIcon) { + const spacerCodicon = document.createElement('span'); + spacerCodicon.className = ThemeIcon.asClassName(spacerIcon); + spacerCodicon.innerText = '\u00a0\u00a0'; + spacer.appendChild(spacerCodicon); + } else { + spacer.innerText = '\u00a0\u00a0'; + } + cell.appendChild(spacer); + + let lineContent: string; + if (item.modifiedLineNumber !== undefined) { + let html: string | TrustedHTML = this._getLineHtml(modifiedModel, modifiedOptions, modifiedModelOpts.tabSize, item.modifiedLineNumber, this._languageService.languageIdCodec); + if (DiffReview._ttPolicy) { + html = DiffReview._ttPolicy.createHTML(html as string); + } + cell.insertAdjacentHTML('beforeend', html as string); + lineContent = modifiedModel.getLineContent(item.modifiedLineNumber); + } else { + let html: string | TrustedHTML = this._getLineHtml(originalModel, originalOptions, originalModelOpts.tabSize, item.originalLineNumber, this._languageService.languageIdCodec); + if (DiffReview._ttPolicy) { + html = DiffReview._ttPolicy.createHTML(html as string); + } + cell.insertAdjacentHTML('beforeend', html as string); + lineContent = originalModel.getLineContent(item.originalLineNumber); + } + + if (lineContent.length === 0) { + lineContent = localize('blankLine', "blank"); + } + + let ariaLabel: string = ''; + switch (item.type) { + case LineType.Unchanged: + if (item.originalLineNumber === item.modifiedLineNumber) { + ariaLabel = localize({ key: 'unchangedLine', comment: ['The placeholders are contents of the line and should not be translated.'] }, "{0} unchanged line {1}", lineContent, item.originalLineNumber); + } else { + ariaLabel = localize('equalLine', "{0} original line {1} modified line {2}", lineContent, item.originalLineNumber, item.modifiedLineNumber); + } + break; + case LineType.Added: + ariaLabel = localize('insertLine', "+ {0} modified line {1}", lineContent, item.modifiedLineNumber); + break; + case LineType.Deleted: + ariaLabel = localize('deleteLine', "- {0} original line {1}", lineContent, item.originalLineNumber); + break; + } + row.setAttribute('aria-label', ariaLabel); + + return row; + } + + private _getLineHtml(model: ITextModel, options: IComputedEditorOptions, tabSize: number, lineNumber: number, languageIdCodec: ILanguageIdCodec): string { + const lineContent = model.getLineContent(lineNumber); + const fontInfo = options.get(EditorOption.fontInfo); + const lineTokens = LineTokens.createEmpty(lineContent, languageIdCodec); + const isBasicASCII = ViewLineRenderingData.isBasicASCII(lineContent, model.mightContainNonBasicASCII()); + const containsRTL = ViewLineRenderingData.containsRTL(lineContent, isBasicASCII, model.mightContainRTL()); + const r = renderViewLine2(new RenderLineInput( + (fontInfo.isMonospace && !options.get(EditorOption.disableMonospaceOptimizations)), + fontInfo.canUseHalfwidthRightwardsArrow, + lineContent, + false, + isBasicASCII, + containsRTL, + 0, + lineTokens, + [], + tabSize, + 0, + fontInfo.spaceWidth, + fontInfo.middotWidth, + fontInfo.wsmiddotWidth, + options.get(EditorOption.stopRenderingLineAfter), + options.get(EditorOption.renderWhitespace), + options.get(EditorOption.renderControlCharacters), + options.get(EditorOption.fontLigatures) !== EditorFontLigatures.OFF, + null + )); + + return r.html; + } +} + +function forEachAdjacentItems(items: T[], callback: (item1: T | undefined, item2: T | undefined) => void) { + let last: T | undefined; + for (const item of items) { + callback(last, item); + last = item; + } + callback(last, undefined); +} + +function* group(items: Iterable, shouldBeGrouped: (item1: T, item2: T) => boolean): Iterable { + let currentGroup: T[] | undefined; + let last: T | undefined; + for (const item of items) { + if (last !== undefined && shouldBeGrouped(last, item)) { + currentGroup!.push(item); + } else { + if (currentGroup) { + yield currentGroup; + } + currentGroup = [item]; + } + last = item; + } + if (currentGroup) { + yield currentGroup; + } +} diff --git a/code/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorDecorations.ts b/code/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorDecorations.ts index 8a821c0b135..7e062b0ab09 100644 --- a/code/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorDecorations.ts +++ b/code/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorDecorations.ts @@ -42,13 +42,13 @@ export class DiffEditorDecorations extends Disposable { const originalDecorations: IModelDeltaDecoration[] = []; const modifiedDecorations: IModelDeltaDecoration[] = []; for (const m of diff.mappings) { - const fullRangeOriginal = LineRange.subtract(m.lineRangeMapping.originalRange, currentMove?.lineRangeMapping.originalRange) + const fullRangeOriginal = LineRange.subtract(m.lineRangeMapping.originalRange, currentMove?.lineRangeMapping.original) .map(i => i.toInclusiveRange()).filter(isDefined); for (const range of fullRangeOriginal) { originalDecorations.push({ range, options: renderIndicators ? diffLineDeleteDecorationBackgroundWithIndicator : diffLineDeleteDecorationBackground }); } - const fullRangeModified = LineRange.subtract(m.lineRangeMapping.modifiedRange, currentMove?.lineRangeMapping.modifiedRange) + const fullRangeModified = LineRange.subtract(m.lineRangeMapping.modifiedRange, currentMove?.lineRangeMapping.modified) .map(i => i.toInclusiveRange()).filter(isDefined); for (const range of fullRangeModified) { modifiedDecorations.push({ range, options: renderIndicators ? diffLineAddDecorationBackgroundWithIndicator : diffLineAddDecorationBackground }); @@ -64,8 +64,8 @@ export class DiffEditorDecorations extends Disposable { } else { for (const i of m.lineRangeMapping.innerChanges || []) { if (currentMove - && (currentMove.lineRangeMapping.originalRange.intersect(new LineRange(i.originalRange.startLineNumber, i.originalRange.endLineNumber)) - || currentMove.lineRangeMapping.modifiedRange.intersect(new LineRange(i.modifiedRange.startLineNumber, i.modifiedRange.endLineNumber)))) { + && (currentMove.lineRangeMapping.original.intersect(new LineRange(i.originalRange.startLineNumber, i.originalRange.endLineNumber)) + || currentMove.lineRangeMapping.modified.intersect(new LineRange(i.modifiedRange.startLineNumber, i.modifiedRange.endLineNumber)))) { continue; } @@ -104,7 +104,7 @@ export class DiffEditorDecorations extends Disposable { for (const m of diff.movedTexts) { originalDecorations.push({ - range: m.lineRangeMapping.originalRange.toInclusiveRange()!, options: { + range: m.lineRangeMapping.original.toInclusiveRange()!, options: { description: 'moved', blockClassName: 'movedOriginal', blockPadding: [MovedBlocksLinesPart.movedCodeBlockPadding, 0, MovedBlocksLinesPart.movedCodeBlockPadding, MovedBlocksLinesPart.movedCodeBlockPadding], @@ -112,7 +112,7 @@ export class DiffEditorDecorations extends Disposable { }); modifiedDecorations.push({ - range: m.lineRangeMapping.modifiedRange.toInclusiveRange()!, options: { + range: m.lineRangeMapping.modified.toInclusiveRange()!, options: { description: 'moved', blockClassName: 'movedModified', blockPadding: [4, 0, 4, 4], diff --git a/code/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorEditors.ts b/code/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorEditors.ts index a77d3c1727b..645205e061f 100644 --- a/code/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorEditors.ts +++ b/code/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorEditors.ts @@ -15,7 +15,7 @@ import { IContentSizeChangedEvent } from 'vs/editor/common/editorCommon'; import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { DiffEditorOptions } from './diffEditorOptions'; -import { IObservable, IReader } from 'vs/base/common/observable'; +import { IReader } from 'vs/base/common/observable'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; export class DiffEditorEditors extends Disposable { @@ -31,7 +31,6 @@ export class DiffEditorEditors extends Disposable { private readonly _options: DiffEditorOptions, codeEditorWidgetOptions: IDiffCodeEditorWidgetOptions, private readonly _createInnerEditor: (instantiationService: IInstantiationService, container: HTMLElement, options: Readonly, editorWidgetOptions: ICodeEditorWidgetOptions) => CodeEditorWidget, - private readonly _modifiedReadOnlyOverride: IObservable, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IKeybindingService private readonly _keybindingService: IKeybindingService, ) { @@ -117,7 +116,6 @@ export class DiffEditorEditors extends Disposable { result.revealHorizontalRightPadding = EditorOptions.revealHorizontalRightPadding.defaultValue + OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH; result.scrollbar!.verticalHasArrows = false; result.extraEditorClassName = 'modified-in-monaco-diff-editor'; - result.readOnly = this._modifiedReadOnlyOverride.read(reader) || this._options.editorOptions.get().readOnly; return result; } diff --git a/code/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorViewModel.ts b/code/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorViewModel.ts index f9d77296cea..6c9a8899466 100644 --- a/code/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorViewModel.ts +++ b/code/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorViewModel.ts @@ -117,7 +117,7 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo this._diff.set(DiffState.fromDiffResult(this._lastDiff!), tx); updateUnchangedRegions(result, tx); const currentSyncedMovedText = this.syncedMovedTexts.get(); - this.syncedMovedTexts.set(currentSyncedMovedText ? this._lastDiff!.moves.find(m => m.lineRangeMapping.modifiedRange.intersect(currentSyncedMovedText.lineRangeMapping.modifiedRange)) : undefined, tx); + this.syncedMovedTexts.set(currentSyncedMovedText ? this._lastDiff!.moves.find(m => m.lineRangeMapping.modified.intersect(currentSyncedMovedText.lineRangeMapping.modified)) : undefined, tx); }); } @@ -137,7 +137,7 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo this._diff.set(DiffState.fromDiffResult(this._lastDiff!), tx); updateUnchangedRegions(result, tx); const currentSyncedMovedText = this.syncedMovedTexts.get(); - this.syncedMovedTexts.set(currentSyncedMovedText ? this._lastDiff!.moves.find(m => m.lineRangeMapping.modifiedRange.intersect(currentSyncedMovedText.lineRangeMapping.modifiedRange)) : undefined, tx); + this.syncedMovedTexts.set(currentSyncedMovedText ? this._lastDiff!.moves.find(m => m.lineRangeMapping.modified.intersect(currentSyncedMovedText.lineRangeMapping.modified)) : undefined, tx); }); } @@ -183,7 +183,7 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo this._diff.set(state, tx); this._isDiffUpToDate.set(true, tx); const currentSyncedMovedText = this.syncedMovedTexts.get(); - this.syncedMovedTexts.set(currentSyncedMovedText ? this._lastDiff.moves.find(m => m.lineRangeMapping.modifiedRange.intersect(currentSyncedMovedText.lineRangeMapping.modifiedRange)) : undefined, tx); + this.syncedMovedTexts.set(currentSyncedMovedText ? this._lastDiff.moves.find(m => m.lineRangeMapping.modified.intersect(currentSyncedMovedText.lineRangeMapping.modified)) : undefined, tx); }); })); } @@ -443,9 +443,9 @@ function applyModifiedEdits(diff: IDocumentDiff, textEdits: TextEditInfo[], orig const changes = applyModifiedEditsToLineRangeMappings(diff.changes, textEdits, originalTextModel, modifiedTextModel); const moves = diff.moves.map(m => { - const newModifiedRange = applyEditToLineRange(m.lineRangeMapping.modifiedRange, textEdits); + const newModifiedRange = applyEditToLineRange(m.lineRangeMapping.modified, textEdits); return newModifiedRange ? new MovedText( - new SimpleLineRangeMapping(m.lineRangeMapping.originalRange, newModifiedRange), + new SimpleLineRangeMapping(m.lineRangeMapping.original, newModifiedRange), applyModifiedEditsToLineRangeMappings(m.changes, textEdits, originalTextModel, modifiedTextModel), ) : undefined; }).filter(isDefined); diff --git a/code/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts b/code/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts index 2e3980aac92..82c59dd4ddc 100644 --- a/code/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts +++ b/code/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts @@ -20,12 +20,11 @@ import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/wi import { IDiffCodeEditorWidgetOptions } from 'vs/editor/browser/widget/diffEditorWidget'; import { DiffEditorDecorations } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorDecorations'; import { DiffEditorSash } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorSash'; -import { DiffReview2 } from 'vs/editor/browser/widget/diffEditorWidget2/diffReview'; import { ViewZoneManager } from 'vs/editor/browser/widget/diffEditorWidget2/lineAlignment'; import { MovedBlocksLinesPart } from 'vs/editor/browser/widget/diffEditorWidget2/movedBlocksLines'; import { OverviewRulerPart } from 'vs/editor/browser/widget/diffEditorWidget2/overviewRulerPart'; import { UnchangedRangesFeature } from 'vs/editor/browser/widget/diffEditorWidget2/unchangedRanges'; -import { ObservableElementSizeObserver, applyStyle, readHotReloadableExport } from 'vs/editor/browser/widget/diffEditorWidget2/utils'; +import { CSSStyle, ObservableElementSizeObserver, applyStyle, readHotReloadableExport } from 'vs/editor/browser/widget/diffEditorWidget2/utils'; import { WorkerBasedDocumentDiffProvider } from 'vs/editor/browser/widget/workerBasedDocumentDiffProvider'; import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IDimension } from 'vs/editor/common/core/dimension'; @@ -42,12 +41,14 @@ import { DelegatingEditor } from './delegatingEditorImpl'; import { DiffEditorEditors } from './diffEditorEditors'; import { DiffEditorOptions } from './diffEditorOptions'; import { DiffEditorViewModel, DiffMapping, DiffState } from './diffEditorViewModel'; +import { AccessibleDiffViewer } from 'vs/editor/browser/widget/diffEditorWidget2/accessibleDiffViewer'; export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { private readonly elements = h('div.monaco-diff-editor.side-by-side', { style: { position: 'relative', height: '100%' } }, [ h('div.noModificationsOverlay@overlay', { style: { position: 'absolute', height: '100%', visibility: 'hidden', } }, [$('span', {}, 'No Changes')]), h('div.editor.original@original', { style: { position: 'absolute', height: '100%' } }), h('div.editor.modified@modified', { style: { position: 'absolute', height: '100%' } }), + h('div.accessibleDiffViewer@accessibleDiffViewer', { style: { position: 'absolute', height: '100%' } }), ]); private readonly _diffModel = this._register(disposableObservableValue('diffModel', undefined)); public readonly onDidChangeModel = Event.fromObservableLight(this._diffModel); @@ -65,7 +66,8 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { private unchangedRangesFeature!: UnchangedRangesFeature; - private readonly _reviewPane: DiffReview2; + private _accessibleDiffViewerVisible = observableValue('accessibleDiffViewerVisible', false); + private _accessibleDiffViewer!: AccessibleDiffViewer; private readonly _options: DiffEditorOptions; private readonly _editors: DiffEditorEditors; @@ -96,15 +98,13 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { this._rootSizeObserver = this._register(new ObservableElementSizeObserver(this.elements.root, options.dimension)); this._rootSizeObserver.setAutomaticLayout(options.automaticLayout ?? false); - const reviewPaneObservable = observableValue('reviewPane', undefined); this._editors = this._register(this._instantiationService.createInstance( DiffEditorEditors, this.elements.original, this.elements.modified, this._options, codeEditorWidgetOptions, - (i, c, o, o2) => this._createInnerEditor(i, c, o, o2), - reviewPaneObservable.map((r, reader) => r?.isVisible.read(reader) ?? false), + (i, c, o, o2) => this._createInnerEditor(i, c, o, o2) )); this._sash = derivedWithStore('sash', (reader, store) => { @@ -158,10 +158,20 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { )); })); - this._reviewPane = this._register(this._instantiationService.createInstance(DiffReview2, this)); - this.elements.root.appendChild(this._reviewPane.domNode.domNode); - this.elements.root.appendChild(this._reviewPane.actionBarContainer.domNode); - reviewPaneObservable.set(this._reviewPane, undefined); + this._register(autorunWithStore2('_accessibleDiffViewer', (reader, store) => { + this._accessibleDiffViewer = store.add(this._register(this._instantiationService.createInstance( + readHotReloadableExport(AccessibleDiffViewer, reader), + this.elements.accessibleDiffViewer, + this._accessibleDiffViewerVisible, + this._rootSizeObserver.width, + this._rootSizeObserver.height, + this._diffModel.map((m, r) => m?.diff.read(r)?.mappings.map(m => m.lineRangeMapping)), + this._editors, + ))); + })); + const visibility = this._accessibleDiffViewerVisible.map(v => v ? 'hidden' : 'visible'); + this._register(applyStyle(this.elements.modified, { visibility })); + this._register(applyStyle(this.elements.original, { visibility })); this._createDiffEditorContributions(); @@ -188,13 +198,13 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { this._register(this._editors.original.onDidChangeCursorPosition(e => { const m = this._diffModel.get(); if (!m) { return; } - const movedText = m.diff.get()!.movedTexts.find(m => m.lineRangeMapping.originalRange.contains(e.position.lineNumber)); + const movedText = m.diff.get()!.movedTexts.find(m => m.lineRangeMapping.original.contains(e.position.lineNumber)); m.syncedMovedTexts.set(movedText, undefined); })); this._register(this._editors.modified.onDidChangeCursorPosition(e => { const m = this._diffModel.get(); if (!m) { return; } - const movedText = m.diff.get()!.movedTexts.find(m => m.lineRangeMapping.modifiedRange.contains(e.position.lineNumber)); + const movedText = m.diff.get()!.movedTexts.find(m => m.lineRangeMapping.modified.contains(e.position.lineNumber)); m.syncedMovedTexts.set(movedText, undefined); })); @@ -248,7 +258,6 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { (this._options.renderOverviewRuler.read(reader) ? OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH : 0), height }); - this._reviewPane.layout(0, width, height); return { modifiedEditor: this._editors.modified.getLayoutInfo(), @@ -321,7 +330,7 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { override setModel(model: IDiffEditorModel | null | IDiffEditorViewModel): void { if (!model && this._diffModel.get()) { // Transitioning from a model to no-model - this._reviewPane.hide(); + this._accessibleDiffViewer.close(); } const vm = model ? ('model' in model) ? model : this.createViewModel(model) : undefined; @@ -433,9 +442,9 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { }); } - diffReviewNext(): void { this._reviewPane.next(); } + diffReviewNext(): void { this._accessibleDiffViewer.next(); } - diffReviewPrev(): void { this._reviewPane.prev(); } + diffReviewPrev(): void { this._accessibleDiffViewer.prev(); } async waitForDiff(): Promise { const diffModel = this._diffModel.get(); diff --git a/code/src/vs/editor/browser/widget/diffEditorWidget2/diffReview.ts b/code/src/vs/editor/browser/widget/diffEditorWidget2/diffReview.ts deleted file mode 100644 index 82359ffaef0..00000000000 --- a/code/src/vs/editor/browser/widget/diffEditorWidget2/diffReview.ts +++ /dev/null @@ -1,821 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from 'vs/base/browser/dom'; -import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; -import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; -import { Action } from 'vs/base/common/actions'; -import { Codicon } from 'vs/base/common/codicons'; -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { IObservable, observableValue } from 'vs/base/common/observable'; -import { ThemeIcon } from 'vs/base/common/themables'; -import { Constants } from 'vs/base/common/uint'; -import { applyFontInfo } from 'vs/editor/browser/config/domFontInfo'; -import { DiffEditorWidget2 } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2'; -import { DiffReview } from 'vs/editor/browser/widget/diffReview'; -import { EditorFontLigatures, EditorOption, IComputedEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { Position } from 'vs/editor/common/core/position'; -import { ILineChange } from 'vs/editor/common/diff/smartLinesDiffComputer'; -import { ScrollType } from 'vs/editor/common/editorCommon'; -import { ILanguageIdCodec } from 'vs/editor/common/languages'; -import { ILanguageService } from 'vs/editor/common/languages/language'; -import { ITextModel, TextModelResolvedOptions } from 'vs/editor/common/model'; -import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; -import { RenderLineInput, renderViewLine2 as renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer'; -import { ViewLineRenderingData } from 'vs/editor/common/viewModel'; -import * as nls from 'vs/nls'; -import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; - -const DIFF_LINES_PADDING = 3; - -const enum DiffEntryType { - Equal = 0, - Insert = 1, - Delete = 2 -} - -class DiffEntry { - readonly originalLineStart: number; - readonly originalLineEnd: number; - readonly modifiedLineStart: number; - readonly modifiedLineEnd: number; - - constructor(originalLineStart: number, originalLineEnd: number, modifiedLineStart: number, modifiedLineEnd: number) { - this.originalLineStart = originalLineStart; - this.originalLineEnd = originalLineEnd; - this.modifiedLineStart = modifiedLineStart; - this.modifiedLineEnd = modifiedLineEnd; - } - - public getType(): DiffEntryType { - if (this.originalLineStart === 0) { - return DiffEntryType.Insert; - } - if (this.modifiedLineStart === 0) { - return DiffEntryType.Delete; - } - return DiffEntryType.Equal; - } -} - -const enum DiffEditorLineClasses { - Insert = 'line-insert', - Delete = 'line-delete' -} - -class Diff { - readonly entries: DiffEntry[]; - - constructor(entries: DiffEntry[]) { - this.entries = entries; - } -} - -const diffReviewInsertIcon = registerIcon('diff-review-insert', Codicon.add, nls.localize('diffReviewInsertIcon', 'Icon for \'Insert\' in diff review.')); -const diffReviewRemoveIcon = registerIcon('diff-review-remove', Codicon.remove, nls.localize('diffReviewRemoveIcon', 'Icon for \'Remove\' in diff review.')); -const diffReviewCloseIcon = registerIcon('diff-review-close', Codicon.close, nls.localize('diffReviewCloseIcon', 'Icon for \'Close\' in diff review.')); - -export class DiffReview2 extends Disposable { - - private static _ttPolicy = DiffReview._ttPolicy; // TODO inline once DiffReview is deprecated. - - private readonly _diffEditor: DiffEditorWidget2; - private get _isVisible() { return this._isVisibleObs.get(); } - private readonly _actionBar: ActionBar; - public readonly actionBarContainer: FastDomNode; - public readonly domNode: FastDomNode; - private readonly _content: FastDomNode; - private readonly scrollbar: DomScrollableElement; - private _diffs: Diff[]; - private _currentDiff: Diff | null; - - private readonly _isVisibleObs = observableValue('isVisible', false); - - public readonly isVisible: IObservable = this._isVisibleObs; - - constructor( - diffEditor: DiffEditorWidget2, - @ILanguageService private readonly _languageService: ILanguageService, - @IAudioCueService private readonly _audioCueService: IAudioCueService, - @IConfigurationService private readonly _configurationService: IConfigurationService - ) { - super(); - this._diffEditor = diffEditor; - - this.actionBarContainer = createFastDomNode(document.createElement('div')); - this.actionBarContainer.setClassName('diff-review-actions'); - this._actionBar = this._register(new ActionBar( - this.actionBarContainer.domNode - )); - - this._actionBar.push(new Action('diffreview.close', nls.localize('label.close', "Close"), 'close-diff-review ' + ThemeIcon.asClassName(diffReviewCloseIcon), true, async () => this.hide()), { label: false, icon: true }); - - this.domNode = createFastDomNode(document.createElement('div')); - this.domNode.setClassName('diff-review monaco-editor-background'); - - this._content = createFastDomNode(document.createElement('div')); - this._content.setClassName('diff-review-content'); - this._content.setAttribute('role', 'code'); - this.scrollbar = this._register(new DomScrollableElement(this._content.domNode, {})); - this.domNode.domNode.appendChild(this.scrollbar.getDomNode()); - - this._register(diffEditor.onDidUpdateDiff(() => { - if (!this._isVisible) { - return; - } - this._diffs = this._compute(); - this._render(); - })); - this._register(diffEditor.getModifiedEditor().onDidChangeCursorPosition(() => { - if (!this._isVisible) { - return; - } - this._render(); - })); - this._register(dom.addStandardDisposableListener(this.domNode.domNode, 'click', (e) => { - e.preventDefault(); - - const row = dom.findParentWithClass(e.target, 'diff-review-row'); - if (row) { - this._goToRow(row); - } - })); - this._register(dom.addStandardDisposableListener(this.domNode.domNode, 'keydown', (e) => { - if ( - e.equals(KeyCode.DownArrow) - || e.equals(KeyMod.CtrlCmd | KeyCode.DownArrow) - || e.equals(KeyMod.Alt | KeyCode.DownArrow) - ) { - e.preventDefault(); - this._goToRow(this._getNextRow(), 'next'); - } - - if ( - e.equals(KeyCode.UpArrow) - || e.equals(KeyMod.CtrlCmd | KeyCode.UpArrow) - || e.equals(KeyMod.Alt | KeyCode.UpArrow) - ) { - e.preventDefault(); - this._goToRow(this._getPrevRow(), 'previous'); - } - - if ( - e.equals(KeyCode.Escape) - || e.equals(KeyMod.CtrlCmd | KeyCode.Escape) - || e.equals(KeyMod.Alt | KeyCode.Escape) - || e.equals(KeyMod.Shift | KeyCode.Escape) - || e.equals(KeyCode.Space) - || e.equals(KeyCode.Enter) - ) { - e.preventDefault(); - this.accept(); - } - })); - this._register(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('accessibility.verbosity.diffEditor')) { - this._diffEditor.updateOptions({ accessibilityVerbose: this._configurationService.getValue('accessibility.verbosity.diffEditor') }); - } - })); - this._diffs = []; - this._currentDiff = null; - } - - public prev(): void { - let index = 0; - - if (!this._isVisible) { - this._diffs = this._compute(); - } - - if (this._isVisible) { - let currentIndex = -1; - for (let i = 0, len = this._diffs.length; i < len; i++) { - if (this._diffs[i] === this._currentDiff) { - currentIndex = i; - break; - } - } - index = (this._diffs.length + currentIndex - 1); - } else { - index = this._findDiffIndex(this._diffEditor.getPosition()!); - } - - if (this._diffs.length === 0) { - // Nothing to do - return; - } - - index = index % this._diffs.length; - const entries = this._diffs[index].entries; - this._diffEditor.setPosition(new Position(entries[0].modifiedLineStart, 1)); - this._diffEditor.setSelection({ startColumn: 1, startLineNumber: entries[0].modifiedLineStart, endColumn: Constants.MAX_SAFE_SMALL_INTEGER, endLineNumber: entries[entries.length - 1].modifiedLineEnd }); - this._isVisibleObs.set(true, undefined); - this.layout(); - this._render(); - this._goToRow(this._getPrevRow(), 'previous'); - } - - public next(): void { - let index = 0; - - if (!this._isVisible) { - this._diffs = this._compute(); - } - - if (this._isVisible) { - let currentIndex = -1; - for (let i = 0, len = this._diffs.length; i < len; i++) { - if (this._diffs[i] === this._currentDiff) { - currentIndex = i; - break; - } - } - index = (currentIndex + 1); - } else { - index = this._findDiffIndex(this._diffEditor.getPosition()!); - } - - if (this._diffs.length === 0) { - // Nothing to do - return; - } - - index = index % this._diffs.length; - const entries = this._diffs[index].entries; - this._diffEditor.setPosition(new Position(entries[0].modifiedLineStart, 1)); - this._diffEditor.setSelection({ startColumn: 1, startLineNumber: entries[0].modifiedLineStart, endColumn: Constants.MAX_SAFE_SMALL_INTEGER, endLineNumber: entries[entries.length - 1].modifiedLineEnd }); - this._isVisibleObs.set(true, undefined); - this.layout(); - this._render(); - this._goToRow(this._getNextRow(), 'next'); - } - - private accept(): void { - let jumpToLineNumber = -1; - const current = this._getCurrentFocusedRow(); - if (current) { - const lineNumber = parseInt(current.getAttribute('data-line')!, 10); - if (!isNaN(lineNumber)) { - jumpToLineNumber = lineNumber; - } - } - this.hide(); - - if (jumpToLineNumber !== -1) { - this._diffEditor.setPosition(new Position(jumpToLineNumber, 1)); - this._diffEditor.revealPosition(new Position(jumpToLineNumber, 1), ScrollType.Immediate); - } - } - - public hide(): void { - this._isVisibleObs.set(false, undefined); - this._diffEditor.focus(); - this.layout(); - this._render(); - } - - private _getPrevRow(): HTMLElement { - const current = this._getCurrentFocusedRow(); - if (!current) { - return this._getFirstRow(); - } - if (current.previousElementSibling) { - return current.previousElementSibling; - } - return current; - } - - private _getNextRow(): HTMLElement { - const current = this._getCurrentFocusedRow(); - if (!current) { - return this._getFirstRow(); - } - if (current.nextElementSibling) { - return current.nextElementSibling; - } - return current; - } - - private _getFirstRow(): HTMLElement { - return this.domNode.domNode.querySelector('.diff-review-row'); - } - - private _getCurrentFocusedRow(): HTMLElement | null { - const result = document.activeElement; - if (result && /diff-review-row/.test(result.className)) { - return result; - } - return null; - } - - private _goToRow(row: HTMLElement, type?: 'next' | 'previous'): void { - const current = this._getCurrentFocusedRow(); - row.tabIndex = 0; - row.focus(); - if (current && current !== row) { - current.tabIndex = -1; - } - const element = !type ? current : type === 'next' ? current?.nextElementSibling : current?.previousElementSibling; - if (element?.classList.contains(DiffEditorLineClasses.Insert)) { - this._audioCueService.playAudioCue(AudioCue.diffLineInserted, true); - } else if (element?.classList.contains(DiffEditorLineClasses.Delete)) { - this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, true); - } - this.scrollbar.scanDomNode(); - } - - private _width: number = 0; - private _top: number = 0; - private _height: number = 0; - - public layout(top: number = this._top, width: number = this._width, height: number = this._height): void { - this._width = width; - this._top = top; - this._height = height; - - this.domNode.setTop(top); - this.domNode.setWidth(width); - this.domNode.setHeight(height); - this._content.setHeight(height); - this._content.setWidth(width); - - if (this._isVisible) { - this.domNode.setDisplay('block'); - this.actionBarContainer.setAttribute('aria-hidden', 'false'); - this.actionBarContainer.setDisplay('block'); - } else { - this.domNode.setDisplay('none'); - this.actionBarContainer.setAttribute('aria-hidden', 'true'); - this.actionBarContainer.setDisplay('none'); - } - } - - private _compute(): Diff[] { - const lineChanges = this._diffEditor.getLineChanges(); - if (!lineChanges || lineChanges.length === 0) { - return []; - } - const originalModel = this._diffEditor.getOriginalEditor().getModel(); - const modifiedModel = this._diffEditor.getModifiedEditor().getModel(); - - if (!originalModel || !modifiedModel) { - return []; - } - - return DiffReview2._mergeAdjacent(lineChanges, originalModel.getLineCount(), modifiedModel.getLineCount()); - } - - private static _mergeAdjacent(lineChanges: ILineChange[], originalLineCount: number, modifiedLineCount: number): Diff[] { - if (!lineChanges || lineChanges.length === 0) { - return []; - } - - const diffs: Diff[] = []; - let diffsLength = 0; - - for (let i = 0, len = lineChanges.length; i < len; i++) { - const lineChange = lineChanges[i]; - - const originalStart = lineChange.originalStartLineNumber; - const originalEnd = lineChange.originalEndLineNumber; - const modifiedStart = lineChange.modifiedStartLineNumber; - const modifiedEnd = lineChange.modifiedEndLineNumber; - - const r: DiffEntry[] = []; - let rLength = 0; - - // Emit before anchors - { - const originalEqualAbove = (originalEnd === 0 ? originalStart : originalStart - 1); - const modifiedEqualAbove = (modifiedEnd === 0 ? modifiedStart : modifiedStart - 1); - - // Make sure we don't step into the previous diff - let minOriginal = 1; - let minModified = 1; - if (i > 0) { - const prevLineChange = lineChanges[i - 1]; - - if (prevLineChange.originalEndLineNumber === 0) { - minOriginal = prevLineChange.originalStartLineNumber + 1; - } else { - minOriginal = prevLineChange.originalEndLineNumber + 1; - } - - if (prevLineChange.modifiedEndLineNumber === 0) { - minModified = prevLineChange.modifiedStartLineNumber + 1; - } else { - minModified = prevLineChange.modifiedEndLineNumber + 1; - } - } - - let fromOriginal = originalEqualAbove - DIFF_LINES_PADDING + 1; - let fromModified = modifiedEqualAbove - DIFF_LINES_PADDING + 1; - if (fromOriginal < minOriginal) { - const delta = minOriginal - fromOriginal; - fromOriginal = fromOriginal + delta; - fromModified = fromModified + delta; - } - if (fromModified < minModified) { - const delta = minModified - fromModified; - fromOriginal = fromOriginal + delta; - fromModified = fromModified + delta; - } - - r[rLength++] = new DiffEntry( - fromOriginal, originalEqualAbove, - fromModified, modifiedEqualAbove - ); - } - - // Emit deleted lines - { - if (originalEnd !== 0) { - r[rLength++] = new DiffEntry(originalStart, originalEnd, 0, 0); - } - } - - // Emit inserted lines - { - if (modifiedEnd !== 0) { - r[rLength++] = new DiffEntry(0, 0, modifiedStart, modifiedEnd); - } - } - - // Emit after anchors - { - const originalEqualBelow = (originalEnd === 0 ? originalStart + 1 : originalEnd + 1); - const modifiedEqualBelow = (modifiedEnd === 0 ? modifiedStart + 1 : modifiedEnd + 1); - - // Make sure we don't step into the next diff - let maxOriginal = originalLineCount; - let maxModified = modifiedLineCount; - if (i + 1 < len) { - const nextLineChange = lineChanges[i + 1]; - - if (nextLineChange.originalEndLineNumber === 0) { - maxOriginal = nextLineChange.originalStartLineNumber; - } else { - maxOriginal = nextLineChange.originalStartLineNumber - 1; - } - - if (nextLineChange.modifiedEndLineNumber === 0) { - maxModified = nextLineChange.modifiedStartLineNumber; - } else { - maxModified = nextLineChange.modifiedStartLineNumber - 1; - } - } - - let toOriginal = originalEqualBelow + DIFF_LINES_PADDING - 1; - let toModified = modifiedEqualBelow + DIFF_LINES_PADDING - 1; - - if (toOriginal > maxOriginal) { - const delta = maxOriginal - toOriginal; - toOriginal = toOriginal + delta; - toModified = toModified + delta; - } - if (toModified > maxModified) { - const delta = maxModified - toModified; - toOriginal = toOriginal + delta; - toModified = toModified + delta; - } - - r[rLength++] = new DiffEntry( - originalEqualBelow, toOriginal, - modifiedEqualBelow, toModified, - ); - } - - diffs[diffsLength++] = new Diff(r); - } - - // Merge adjacent diffs - let curr: DiffEntry[] = diffs[0].entries; - const r: Diff[] = []; - let rLength = 0; - for (let i = 1, len = diffs.length; i < len; i++) { - const thisDiff = diffs[i].entries; - - const currLast = curr[curr.length - 1]; - const thisFirst = thisDiff[0]; - - if ( - currLast.getType() === DiffEntryType.Equal - && thisFirst.getType() === DiffEntryType.Equal - && thisFirst.originalLineStart <= currLast.originalLineEnd - ) { - // We are dealing with equal lines that overlap - - curr[curr.length - 1] = new DiffEntry( - currLast.originalLineStart, thisFirst.originalLineEnd, - currLast.modifiedLineStart, thisFirst.modifiedLineEnd - ); - curr = curr.concat(thisDiff.slice(1)); - continue; - } - - r[rLength++] = new Diff(curr); - curr = thisDiff; - } - r[rLength++] = new Diff(curr); - return r; - } - - private _findDiffIndex(pos: Position): number { - const lineNumber = pos.lineNumber; - for (let i = 0, len = this._diffs.length; i < len; i++) { - const diff = this._diffs[i].entries; - const lastModifiedLine = diff[diff.length - 1].modifiedLineEnd; - if (lineNumber <= lastModifiedLine) { - return i; - } - } - return 0; - } - - private _render(): void { - - const originalOptions = this._diffEditor.getOriginalEditor().getOptions(); - const modifiedOptions = this._diffEditor.getModifiedEditor().getOptions(); - - const originalModel = this._diffEditor.getOriginalEditor().getModel(); - const modifiedModel = this._diffEditor.getModifiedEditor().getModel(); - - const originalModelOpts = originalModel!.getOptions(); - const modifiedModelOpts = modifiedModel!.getOptions(); - - if (!this._isVisible || !originalModel || !modifiedModel) { - dom.clearNode(this._content.domNode); - this._currentDiff = null; - this.scrollbar.scanDomNode(); - return; - } - - const diffIndex = this._findDiffIndex(this._diffEditor.getPosition()!); - - if (this._diffs[diffIndex] === this._currentDiff) { - return; - } - this._currentDiff = this._diffs[diffIndex]; - - const diffs = this._diffs[diffIndex].entries; - const container = document.createElement('div'); - container.className = 'diff-review-table'; - container.setAttribute('role', 'list'); - container.setAttribute('aria-label', 'Difference review. Use "Stage | Unstage | Revert Selected Ranges" commands'); - applyFontInfo(container, modifiedOptions.get(EditorOption.fontInfo)); - - let minOriginalLine = 0; - let maxOriginalLine = 0; - let minModifiedLine = 0; - let maxModifiedLine = 0; - for (let i = 0, len = diffs.length; i < len; i++) { - const diffEntry = diffs[i]; - const originalLineStart = diffEntry.originalLineStart; - const originalLineEnd = diffEntry.originalLineEnd; - const modifiedLineStart = diffEntry.modifiedLineStart; - const modifiedLineEnd = diffEntry.modifiedLineEnd; - - if (originalLineStart !== 0 && ((minOriginalLine === 0 || originalLineStart < minOriginalLine))) { - minOriginalLine = originalLineStart; - } - if (originalLineEnd !== 0 && ((maxOriginalLine === 0 || originalLineEnd > maxOriginalLine))) { - maxOriginalLine = originalLineEnd; - } - if (modifiedLineStart !== 0 && ((minModifiedLine === 0 || modifiedLineStart < minModifiedLine))) { - minModifiedLine = modifiedLineStart; - } - if (modifiedLineEnd !== 0 && ((maxModifiedLine === 0 || modifiedLineEnd > maxModifiedLine))) { - maxModifiedLine = modifiedLineEnd; - } - } - - const header = document.createElement('div'); - header.className = 'diff-review-row'; - - const cell = document.createElement('div'); - cell.className = 'diff-review-cell diff-review-summary'; - const originalChangedLinesCnt = maxOriginalLine - minOriginalLine + 1; - const modifiedChangedLinesCnt = maxModifiedLine - minModifiedLine + 1; - cell.appendChild(document.createTextNode(`${diffIndex + 1}/${this._diffs.length}: @@ -${minOriginalLine},${originalChangedLinesCnt} +${minModifiedLine},${modifiedChangedLinesCnt} @@`)); - header.setAttribute('data-line', String(minModifiedLine)); - - const getAriaLines = (lines: number) => { - if (lines === 0) { - return nls.localize('no_lines_changed', "no lines changed"); - } else if (lines === 1) { - return nls.localize('one_line_changed', "1 line changed"); - } else { - return nls.localize('more_lines_changed', "{0} lines changed", lines); - } - }; - - const originalChangedLinesCntAria = getAriaLines(originalChangedLinesCnt); - const modifiedChangedLinesCntAria = getAriaLines(modifiedChangedLinesCnt); - header.setAttribute('aria-label', nls.localize({ - key: 'header', - comment: [ - 'This is the ARIA label for a git diff header.', - 'A git diff header looks like this: @@ -154,12 +159,39 @@.', - 'That encodes that at original line 154 (which is now line 159), 12 lines were removed/changed with 39 lines.', - 'Variables 0 and 1 refer to the diff index out of total number of diffs.', - 'Variables 2 and 4 will be numbers (a line number).', - 'Variables 3 and 5 will be "no lines changed", "1 line changed" or "X lines changed", localized separately.' - ] - }, "Difference {0} of {1}: original line {2}, {3}, modified line {4}, {5}", (diffIndex + 1), this._diffs.length, minOriginalLine, originalChangedLinesCntAria, minModifiedLine, modifiedChangedLinesCntAria)); - header.appendChild(cell); - - // @@ -504,7 +517,7 @@ - header.setAttribute('role', 'listitem'); - container.appendChild(header); - - const lineHeight = modifiedOptions.get(EditorOption.lineHeight); - let modLine = minModifiedLine; - for (let i = 0, len = diffs.length; i < len; i++) { - const diffEntry = diffs[i]; - DiffReview2._renderSection(container, diffEntry, modLine, lineHeight, this._width, originalOptions, originalModel, originalModelOpts, modifiedOptions, modifiedModel, modifiedModelOpts, this._languageService.languageIdCodec); - if (diffEntry.modifiedLineStart !== 0) { - modLine = diffEntry.modifiedLineEnd; - } - } - - dom.clearNode(this._content.domNode); - this._content.domNode.appendChild(container); - this.scrollbar.scanDomNode(); - } - - private static _renderSection( - dest: HTMLElement, diffEntry: DiffEntry, modLine: number, lineHeight: number, width: number, - originalOptions: IComputedEditorOptions, originalModel: ITextModel, originalModelOpts: TextModelResolvedOptions, - modifiedOptions: IComputedEditorOptions, modifiedModel: ITextModel, modifiedModelOpts: TextModelResolvedOptions, - languageIdCodec: ILanguageIdCodec - ): void { - - const type = diffEntry.getType(); - - let rowClassName: string = 'diff-review-row'; - let lineNumbersExtraClassName: string = ''; - const spacerClassName: string = 'diff-review-spacer'; - let spacerIcon: ThemeIcon | null = null; - switch (type) { - case DiffEntryType.Insert: - rowClassName = 'diff-review-row line-insert'; - lineNumbersExtraClassName = ' char-insert'; - spacerIcon = diffReviewInsertIcon; - break; - case DiffEntryType.Delete: - rowClassName = 'diff-review-row line-delete'; - lineNumbersExtraClassName = ' char-delete'; - spacerIcon = diffReviewRemoveIcon; - break; - } - - const originalLineStart = diffEntry.originalLineStart; - const originalLineEnd = diffEntry.originalLineEnd; - const modifiedLineStart = diffEntry.modifiedLineStart; - const modifiedLineEnd = diffEntry.modifiedLineEnd; - - const cnt = Math.max( - modifiedLineEnd - modifiedLineStart, - originalLineEnd - originalLineStart - ); - - const originalLayoutInfo = originalOptions.get(EditorOption.layoutInfo); - const originalLineNumbersWidth = originalLayoutInfo.glyphMarginWidth + originalLayoutInfo.lineNumbersWidth; - - const modifiedLayoutInfo = modifiedOptions.get(EditorOption.layoutInfo); - const modifiedLineNumbersWidth = 10 + modifiedLayoutInfo.glyphMarginWidth + modifiedLayoutInfo.lineNumbersWidth; - - for (let i = 0; i <= cnt; i++) { - const originalLine = (originalLineStart === 0 ? 0 : originalLineStart + i); - const modifiedLine = (modifiedLineStart === 0 ? 0 : modifiedLineStart + i); - - const row = document.createElement('div'); - row.style.minWidth = width + 'px'; - row.className = rowClassName; - row.setAttribute('role', 'listitem'); - if (modifiedLine !== 0) { - modLine = modifiedLine; - } - row.setAttribute('data-line', String(modLine)); - - const cell = document.createElement('div'); - cell.className = 'diff-review-cell'; - cell.style.height = `${lineHeight}px`; - row.appendChild(cell); - - const originalLineNumber = document.createElement('span'); - originalLineNumber.style.width = (originalLineNumbersWidth + 'px'); - originalLineNumber.style.minWidth = (originalLineNumbersWidth + 'px'); - originalLineNumber.className = 'diff-review-line-number' + lineNumbersExtraClassName; - if (originalLine !== 0) { - originalLineNumber.appendChild(document.createTextNode(String(originalLine))); - } else { - originalLineNumber.innerText = '\u00a0'; - } - cell.appendChild(originalLineNumber); - - const modifiedLineNumber = document.createElement('span'); - modifiedLineNumber.style.width = (modifiedLineNumbersWidth + 'px'); - modifiedLineNumber.style.minWidth = (modifiedLineNumbersWidth + 'px'); - modifiedLineNumber.style.paddingRight = '10px'; - modifiedLineNumber.className = 'diff-review-line-number' + lineNumbersExtraClassName; - if (modifiedLine !== 0) { - modifiedLineNumber.appendChild(document.createTextNode(String(modifiedLine))); - } else { - modifiedLineNumber.innerText = '\u00a0'; - } - cell.appendChild(modifiedLineNumber); - - const spacer = document.createElement('span'); - spacer.className = spacerClassName; - - if (spacerIcon) { - const spacerCodicon = document.createElement('span'); - spacerCodicon.className = ThemeIcon.asClassName(spacerIcon); - spacerCodicon.innerText = '\u00a0\u00a0'; - spacer.appendChild(spacerCodicon); - } else { - spacer.innerText = '\u00a0\u00a0'; - } - cell.appendChild(spacer); - - let lineContent: string; - if (modifiedLine !== 0) { - let html: string | TrustedHTML = this._renderLine(modifiedModel, modifiedOptions, modifiedModelOpts.tabSize, modifiedLine, languageIdCodec); - if (DiffReview2._ttPolicy) { - html = DiffReview2._ttPolicy.createHTML(html as string); - } - cell.insertAdjacentHTML('beforeend', html as string); - lineContent = modifiedModel.getLineContent(modifiedLine); - } else { - let html: string | TrustedHTML = this._renderLine(originalModel, originalOptions, originalModelOpts.tabSize, originalLine, languageIdCodec); - if (DiffReview2._ttPolicy) { - html = DiffReview2._ttPolicy.createHTML(html as string); - } - cell.insertAdjacentHTML('beforeend', html as string); - lineContent = originalModel.getLineContent(originalLine); - } - - if (lineContent.length === 0) { - lineContent = nls.localize('blankLine', "blank"); - } - - let ariaLabel: string = ''; - switch (type) { - case DiffEntryType.Equal: - if (originalLine === modifiedLine) { - ariaLabel = nls.localize({ key: 'unchangedLine', comment: ['The placeholders are contents of the line and should not be translated.'] }, "{0} unchanged line {1}", lineContent, originalLine); - } else { - ariaLabel = nls.localize('equalLine', "{0} original line {1} modified line {2}", lineContent, originalLine, modifiedLine); - } - break; - case DiffEntryType.Insert: - ariaLabel = nls.localize('insertLine', "+ {0} modified line {1}", lineContent, modifiedLine); - break; - case DiffEntryType.Delete: - ariaLabel = nls.localize('deleteLine', "- {0} original line {1}", lineContent, originalLine); - break; - } - row.setAttribute('aria-label', ariaLabel); - - dest.appendChild(row); - } - } - - private static _renderLine(model: ITextModel, options: IComputedEditorOptions, tabSize: number, lineNumber: number, languageIdCodec: ILanguageIdCodec): string { - const lineContent = model.getLineContent(lineNumber); - const fontInfo = options.get(EditorOption.fontInfo); - const lineTokens = LineTokens.createEmpty(lineContent, languageIdCodec); - const isBasicASCII = ViewLineRenderingData.isBasicASCII(lineContent, model.mightContainNonBasicASCII()); - const containsRTL = ViewLineRenderingData.containsRTL(lineContent, isBasicASCII, model.mightContainRTL()); - const r = renderViewLine(new RenderLineInput( - (fontInfo.isMonospace && !options.get(EditorOption.disableMonospaceOptimizations)), - fontInfo.canUseHalfwidthRightwardsArrow, - lineContent, - false, - isBasicASCII, - containsRTL, - 0, - lineTokens, - [], - tabSize, - 0, - fontInfo.spaceWidth, - fontInfo.middotWidth, - fontInfo.wsmiddotWidth, - options.get(EditorOption.stopRenderingLineAfter), - options.get(EditorOption.renderWhitespace), - options.get(EditorOption.renderControlCharacters), - options.get(EditorOption.fontLigatures) !== EditorFontLigatures.OFF, - null - )); - - return r.html; - } -} diff --git a/code/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts b/code/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts index 507e6d2039c..9960732a03f 100644 --- a/code/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts +++ b/code/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts @@ -243,7 +243,7 @@ export class ViewZoneManager extends Disposable { } else { const delta = a.modifiedHeightInPx - a.originalHeightInPx; if (delta > 0) { - if (syncedMovedText?.lineRangeMapping.originalRange.contains(a.originalRange.endLineNumberExclusive - 1)) { + if (syncedMovedText?.lineRangeMapping.original.contains(a.originalRange.endLineNumberExclusive - 1)) { continue; } @@ -254,7 +254,7 @@ export class ViewZoneManager extends Disposable { showInHiddenAreas: true, }); } else { - if (syncedMovedText?.lineRangeMapping.modifiedRange.contains(a.modifiedRange.endLineNumberExclusive - 1)) { + if (syncedMovedText?.lineRangeMapping.modified.contains(a.modifiedRange.endLineNumberExclusive - 1)) { continue; } @@ -281,8 +281,8 @@ export class ViewZoneManager extends Disposable { } for (const a of alignmentsSyncedMovedText.read(reader) ?? []) { - if (!syncedMovedText?.lineRangeMapping.originalRange.intersect(a.originalRange) - && !syncedMovedText?.lineRangeMapping.modifiedRange.intersect(a.modifiedRange)) { + if (!syncedMovedText?.lineRangeMapping.original.intersect(a.originalRange) + && !syncedMovedText?.lineRangeMapping.modified.intersect(a.modifiedRange)) { // ignore unrelated alignments outside the synced moved text continue; } @@ -402,8 +402,8 @@ export class ViewZoneManager extends Disposable { let deltaOrigToMod = 0; if (m) { - const trueTopOriginal = this._editors.original.getTopForLineNumber(m.lineRangeMapping.originalRange.startLineNumber, true) - this._originalTopPadding.get(); - const trueTopModified = this._editors.modified.getTopForLineNumber(m.lineRangeMapping.modifiedRange.startLineNumber, true) - this._modifiedTopPadding.get(); + const trueTopOriginal = this._editors.original.getTopForLineNumber(m.lineRangeMapping.original.startLineNumber, true) - this._originalTopPadding.get(); + const trueTopModified = this._editors.modified.getTopForLineNumber(m.lineRangeMapping.modified.startLineNumber, true) - this._modifiedTopPadding.get(); deltaOrigToMod = trueTopModified - trueTopOriginal; } diff --git a/code/src/vs/editor/browser/widget/diffEditorWidget2/movedBlocksLines.ts b/code/src/vs/editor/browser/widget/diffEditorWidget2/movedBlocksLines.ts index 4c85fd8afa6..88aed424b95 100644 --- a/code/src/vs/editor/browser/widget/diffEditorWidget2/movedBlocksLines.ts +++ b/code/src/vs/editor/browser/widget/diffEditorWidget2/movedBlocksLines.ts @@ -67,9 +67,9 @@ export class MovedBlocksLinesPart extends Disposable { return (t1 + t2) / 2; } - const start = computeLineStart(m.lineRangeMapping.originalRange, this._editors.original); + const start = computeLineStart(m.lineRangeMapping.original, this._editors.original); const startOffset = originalScrollTop.read(reader); - const end = computeLineStart(m.lineRangeMapping.modifiedRange, this._editors.modified); + const end = computeLineStart(m.lineRangeMapping.modified, this._editors.modified); const endOffset = modifiedScrollTop.read(reader); const top = start - startOffset; diff --git a/code/src/vs/editor/common/core/lineRange.ts b/code/src/vs/editor/common/core/lineRange.ts index d39b5e1901e..c8779a2881a 100644 --- a/code/src/vs/editor/common/core/lineRange.ts +++ b/code/src/vs/editor/common/core/lineRange.ts @@ -219,6 +219,12 @@ export class LineRange { return result; } + public forEach(f: (lineNumber: number) => void): void { + for (let lineNumber = this.startLineNumber; lineNumber < this.endLineNumberExclusive; lineNumber++) { + f(lineNumber); + } + } + /** * @internal */ diff --git a/code/src/vs/editor/common/diff/algorithms/joinSequenceDiffs.ts b/code/src/vs/editor/common/diff/algorithms/joinSequenceDiffs.ts index 5afffbae370..0886c2da1e6 100644 --- a/code/src/vs/editor/common/diff/algorithms/joinSequenceDiffs.ts +++ b/code/src/vs/editor/common/diff/algorithms/joinSequenceDiffs.ts @@ -5,6 +5,7 @@ import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { ISequence, SequenceDiff } from 'vs/editor/common/diff/algorithms/diffAlgorithm'; +import { LinesSliceCharSequence } from 'vs/editor/common/diff/standardLinesDiffComputer'; export function optimizeSequenceDiffs(sequence1: ISequence, sequence2: ISequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] { let result = sequenceDiffs; @@ -32,6 +33,74 @@ export function smoothenSequenceDiffs(sequence1: ISequence, sequence2: ISequence return result; } +export function randomRandomMatches(sequence1: LinesSliceCharSequence, sequence2: LinesSliceCharSequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] { + let diffs = sequenceDiffs; + + let counter = 0; + let shouldRepeat: boolean; + do { + shouldRepeat = false; + + const result: SequenceDiff[] = [ + diffs[0] + ]; + + for (let i = 1; i < diffs.length; i++) { + const cur = diffs[i]; + const lastResult = result[result.length - 1]; + + function shouldJoinDiffs(before: SequenceDiff, after: SequenceDiff): boolean { + const unchangedRange = new OffsetRange(lastResult.seq1Range.endExclusive, cur.seq1Range.start); + + const unchangedLineCount = sequence1.countLinesIn(unchangedRange); + if (unchangedLineCount > 5 || unchangedRange.length > 500) { + return false; + } + + const unchangedText = sequence1.getText(unchangedRange).trim(); + if (unchangedText.length > 20 || unchangedText.split(/\r\n|\r|\n/).length > 1) { + return false; + } + + const beforeLineCount1 = sequence1.countLinesIn(before.seq1Range); + const beforeSeq1Length = before.seq1Range.length; + const beforeLineCount2 = sequence2.countLinesIn(before.seq2Range); + const beforeSeq2Length = before.seq2Range.length; + + const afterLineCount1 = sequence1.countLinesIn(after.seq1Range); + const afterSeq1Length = after.seq1Range.length; + const afterLineCount2 = sequence2.countLinesIn(after.seq2Range); + const afterSeq2Length = after.seq2Range.length; + + // TODO: Maybe a neural net can be used to derive the result from these numbers + + const max = 2 * 40 + 50; + function cap(v: number): number { + return Math.min(v, max); + } + + if (Math.pow(Math.pow(cap(beforeLineCount1 * 40 + beforeSeq1Length), 1.5) + Math.pow(cap(beforeLineCount2 * 40 + beforeSeq2Length), 1.5), 1.5) + + Math.pow(Math.pow(cap(afterLineCount1 * 40 + afterSeq1Length), 1.5) + Math.pow(cap(afterLineCount2 * 40 + afterSeq2Length), 1.5), 1.5) > ((max ** 1.5) ** 1.5) * 1.3) { + return true; + } + return false; + } + + const shouldJoin = shouldJoinDiffs(lastResult, cur); + if (shouldJoin) { + shouldRepeat = true; + result[result.length - 1] = result[result.length - 1].join(cur); + } else { + result.push(cur); + } + } + + diffs = result; + } while (counter++ < 10 && shouldRepeat); + + return diffs; +} + /** * This function fixes issues like this: * ``` diff --git a/code/src/vs/editor/common/diff/linesDiffComputer.ts b/code/src/vs/editor/common/diff/linesDiffComputer.ts index a096042419f..84505ab563a 100644 --- a/code/src/vs/editor/common/diff/linesDiffComputer.ts +++ b/code/src/vs/editor/common/diff/linesDiffComputer.ts @@ -140,19 +140,20 @@ export class RangeMapping { } } +// TODO@hediet: Make LineRangeMapping extend from this! export class SimpleLineRangeMapping { constructor( - public readonly originalRange: LineRange, - public readonly modifiedRange: LineRange, + public readonly original: LineRange, + public readonly modified: LineRange, ) { } public toString(): string { - return `{${this.originalRange.toString()}->${this.modifiedRange.toString()}}`; + return `{${this.original.toString()}->${this.modified.toString()}}`; } public flip(): SimpleLineRangeMapping { - return new SimpleLineRangeMapping(this.modifiedRange, this.originalRange); + return new SimpleLineRangeMapping(this.modified, this.original); } } diff --git a/code/src/vs/editor/common/diff/standardLinesDiffComputer.ts b/code/src/vs/editor/common/diff/standardLinesDiffComputer.ts index 51f62509b02..942308e0f09 100644 --- a/code/src/vs/editor/common/diff/standardLinesDiffComputer.ts +++ b/code/src/vs/editor/common/diff/standardLinesDiffComputer.ts @@ -11,7 +11,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { DateTimeout, ISequence, ITimeout, InfiniteTimeout, SequenceDiff } from 'vs/editor/common/diff/algorithms/diffAlgorithm'; import { DynamicProgrammingDiffing } from 'vs/editor/common/diff/algorithms/dynamicProgrammingDiffing'; -import { optimizeSequenceDiffs, smoothenSequenceDiffs } from 'vs/editor/common/diff/algorithms/joinSequenceDiffs'; +import { optimizeSequenceDiffs, randomRandomMatches, smoothenSequenceDiffs } from 'vs/editor/common/diff/algorithms/joinSequenceDiffs'; import { MyersDiffAlgorithm } from 'vs/editor/common/diff/algorithms/myersDiffAlgorithm'; import { ILinesDiffComputer, ILinesDiffComputerOptions, LineRangeMapping, LinesDiff, MovedText, RangeMapping, SimpleLineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer'; @@ -173,8 +173,8 @@ export class StandardLinesDiffComputer implements ILinesDiffComputer { } private refineDiff(originalLines: string[], modifiedLines: string[], diff: SequenceDiff, timeout: ITimeout, considerWhitespaceChanges: boolean): { mappings: RangeMapping[]; hitTimeout: boolean } { - const slice1 = new Slice(originalLines, diff.seq1Range, considerWhitespaceChanges); - const slice2 = new Slice(modifiedLines, diff.seq2Range, considerWhitespaceChanges); + const slice1 = new LinesSliceCharSequence(originalLines, diff.seq1Range, considerWhitespaceChanges); + const slice2 = new LinesSliceCharSequence(modifiedLines, diff.seq2Range, considerWhitespaceChanges); const diffResult = slice1.length + slice2.length < 500 ? this.dynamicProgrammingDiffing.compute(slice1, slice2, timeout) @@ -184,6 +184,7 @@ export class StandardLinesDiffComputer implements ILinesDiffComputer { diffs = optimizeSequenceDiffs(slice1, slice2, diffs); diffs = coverFullWords(slice1, slice2, diffs); diffs = smoothenSequenceDiffs(slice1, slice2, diffs); + diffs = randomRandomMatches(slice1, slice2, diffs); const result = diffs.map( (d) => @@ -202,7 +203,7 @@ export class StandardLinesDiffComputer implements ILinesDiffComputer { } } -function coverFullWords(sequence1: Slice, sequence2: Slice, sequenceDiffs: SequenceDiff[]): SequenceDiff[] { +function coverFullWords(sequence1: LinesSliceCharSequence, sequence2: LinesSliceCharSequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] { const additional: SequenceDiff[] = []; let lastModifiedWord: { added: number; deleted: number; count: number; s1Range: OffsetRange; s2Range: OffsetRange } | undefined = undefined; @@ -417,7 +418,7 @@ function getIndentation(str: string): number { return i; } -class Slice implements ISequence { +export class LinesSliceCharSequence implements ISequence { private readonly elements: number[] = []; private readonly firstCharOffsetByLineMinusOne: number[] = []; public readonly lineRange: OffsetRange; @@ -471,7 +472,11 @@ class Slice implements ISequence { } get text(): string { - return [...this.elements].map(e => String.fromCharCode(e)).join(''); + return this.getText(new OffsetRange(0, this.length)); + } + + getText(range: OffsetRange): string { + return this.elements.slice(range.start, range.endExclusive).map(e => String.fromCharCode(e)).join(''); } getElement(offset: number): number { @@ -559,6 +564,10 @@ class Slice implements ISequence { return new OffsetRange(start, end); } + + public countLinesIn(range: OffsetRange): number { + return this.translateOffset(range.endExclusive).lineNumber - this.translateOffset(range.start).lineNumber; + } } function isWordChar(charCode: number): boolean { diff --git a/code/src/vs/editor/common/services/editorSimpleWorker.ts b/code/src/vs/editor/common/services/editorSimpleWorker.ts index 2f51814946b..2c26721fd47 100644 --- a/code/src/vs/editor/common/services/editorSimpleWorker.ts +++ b/code/src/vs/editor/common/services/editorSimpleWorker.ts @@ -440,10 +440,10 @@ export class EditorSimpleWorker implements IRequestHandler, IDisposable { quitEarly: result.hitTimeout, changes: getLineChanges(result.changes), moves: result.moves.map(m => ([ - m.lineRangeMapping.originalRange.startLineNumber, - m.lineRangeMapping.originalRange.endLineNumberExclusive, - m.lineRangeMapping.modifiedRange.startLineNumber, - m.lineRangeMapping.modifiedRange.endLineNumberExclusive, + m.lineRangeMapping.original.startLineNumber, + m.lineRangeMapping.original.endLineNumberExclusive, + m.lineRangeMapping.modified.startLineNumber, + m.lineRangeMapping.modified.endLineNumberExclusive, getLineChanges(m.changes) ])), }; diff --git a/code/src/vs/editor/standalone/browser/standaloneServices.ts b/code/src/vs/editor/standalone/browser/standaloneServices.ts index 0546f5fc282..f026f61f400 100644 --- a/code/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/code/src/vs/editor/standalone/browser/standaloneServices.ts @@ -87,7 +87,7 @@ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService, InMemoryStorageService } from 'vs/platform/storage/common/storage'; import { DefaultConfiguration } from 'vs/platform/configuration/common/configurations'; import { WorkspaceEdit } from 'vs/editor/common/languages'; -import { AudioCue, AudioCueGroupId, IAudioCueService, Sound } from 'vs/platform/audioCues/browser/audioCueService'; +import { AudioCue, IAudioCueService, Sound } from 'vs/platform/audioCues/browser/audioCueService'; import { LogService } from 'vs/platform/log/common/logService'; import { getEditorFeatures } from 'vs/editor/common/editorFeatures'; import { onUnexpectedError } from 'vs/base/common/errors'; @@ -1058,8 +1058,6 @@ class StandaloneAudioService implements IAudioCueService { playAudioCueLoop(cue: AudioCue): IDisposable { return toDisposable(() => { }); } - playRandomAudioCue(groupId: AudioCueGroupId, allowManyInParallel?: boolean): void { - } } export interface IEditorOverrideServices { diff --git a/code/src/vs/editor/test/node/diffing/diffingFixture.test.ts b/code/src/vs/editor/test/node/diffing/diffingFixture.test.ts index 2cdcb08d472..59173290fc8 100644 --- a/code/src/vs/editor/test/node/diffing/diffingFixture.test.ts +++ b/code/src/vs/editor/test/node/diffing/diffingFixture.test.ts @@ -59,8 +59,8 @@ suite('diff fixtures', () => { modified: { content: secondContent, fileName: `./${secondFileName}` }, diffs: getDiffs(diff.changes), moves: diff.moves.map(v => ({ - originalRange: v.lineRangeMapping.originalRange.toString(), - modifiedRange: v.lineRangeMapping.modifiedRange.toString(), + originalRange: v.lineRangeMapping.original.toString(), + modifiedRange: v.lineRangeMapping.modified.toString(), changes: getDiffs(v.changes), })) }; diff --git a/code/src/vs/editor/test/node/diffing/fixtures/class-replacement/advanced.expected.diff.json b/code/src/vs/editor/test/node/diffing/fixtures/class-replacement/advanced.expected.diff.json index c6bc1333002..41e9321310a 100644 --- a/code/src/vs/editor/test/node/diffing/fixtures/class-replacement/advanced.expected.diff.json +++ b/code/src/vs/editor/test/node/diffing/fixtures/class-replacement/advanced.expected.diff.json @@ -13,12 +13,8 @@ "modifiedRange": "[29,31)", "innerChanges": [ { - "originalRange": "[29,1 -> 33,1]", - "modifiedRange": "[29,1 -> 29,1]" - }, - { - "originalRange": "[33,14 -> 33,41]", - "modifiedRange": "[29,14 -> 30,54]" + "originalRange": "[29,1 -> 33,41]", + "modifiedRange": "[29,1 -> 30,54]" } ] }, @@ -47,16 +43,8 @@ "modifiedRange": "[36,37)", "innerChanges": [ { - "originalRange": "[41,9 -> 41,18]", - "modifiedRange": "[36,9 -> 36,44]" - }, - { - "originalRange": "[41,26 -> 42,34]", - "modifiedRange": "[36,52 -> 36,64]" - }, - { - "originalRange": "[43,1 -> 46,1]", - "modifiedRange": "[37,1 -> 37,1]" + "originalRange": "[41,9 -> 46,1]", + "modifiedRange": "[36,9 -> 37,1]" } ] }, @@ -65,12 +53,8 @@ "modifiedRange": "[39,40)", "innerChanges": [ { - "originalRange": "[48,9 -> 63,48]", - "modifiedRange": "[39,9 -> 39,43]" - }, - { - "originalRange": "[64,1 -> 72,1]", - "modifiedRange": "[40,1 -> 40,1]" + "originalRange": "[48,9 -> 72,1]", + "modifiedRange": "[39,9 -> 40,1]" } ] } diff --git a/code/src/vs/editor/test/node/diffing/fixtures/difficult-move/advanced.expected.diff.json b/code/src/vs/editor/test/node/diffing/fixtures/difficult-move/advanced.expected.diff.json index 6d0a4cf9977..b2155f7f625 100644 --- a/code/src/vs/editor/test/node/diffing/fixtures/difficult-move/advanced.expected.diff.json +++ b/code/src/vs/editor/test/node/diffing/fixtures/difficult-move/advanced.expected.diff.json @@ -43,60 +43,8 @@ "modifiedRange": "[226,234)", "innerChanges": [ { - "originalRange": "[222,17 -> 222,19]", - "modifiedRange": "[226,17 -> 226,17]" - }, - { - "originalRange": "[223,4 -> 223,28]", - "modifiedRange": "[227,4 -> 227,37]" - }, - { - "originalRange": "[223,32 -> 223,49]", - "modifiedRange": "[227,41 -> 227,48]" - }, - { - "originalRange": "[223,54 -> 223,65]", - "modifiedRange": "[227,53 -> 227,62]" - }, - { - "originalRange": "[224,4 -> 224,29]", - "modifiedRange": "[228,4 -> 228,55]" - }, - { - "originalRange": "[224,43 -> 225,63]", - "modifiedRange": "[228,69 -> 228,108]" - }, - { - "originalRange": "[225,78 -> 226,8]", - "modifiedRange": "[228,123 -> 228,152]" - }, - { - "originalRange": "[226,22 -> 226,25]", - "modifiedRange": "[228,166 -> 228,169]" - }, - { - "originalRange": "[227,5 -> 227,93]", - "modifiedRange": "[229,5 -> 229,67]" - }, - { - "originalRange": "[228,5 -> 228,51]", - "modifiedRange": "[230,5 -> 230,30]" - }, - { - "originalRange": "[229,6 -> 229,143]", - "modifiedRange": "[231,6 -> 231,40]" - }, - { - "originalRange": "[230,5 -> 232,42]", - "modifiedRange": "[232,5 -> 232,19]" - }, - { - "originalRange": "[232,48 -> 232,98]", - "modifiedRange": "[232,25 -> 233,58]" - }, - { - "originalRange": "[233,1 -> 234,1]", - "modifiedRange": "[234,1 -> 234,1]" + "originalRange": "[222,17 -> 234,1]", + "modifiedRange": "[226,17 -> 234,1]" } ] } diff --git a/code/src/vs/editor/test/node/diffing/fixtures/method-splitting/advanced.expected.diff.json b/code/src/vs/editor/test/node/diffing/fixtures/method-splitting/advanced.expected.diff.json index 8bc08d08086..61e711a47e3 100644 --- a/code/src/vs/editor/test/node/diffing/fixtures/method-splitting/advanced.expected.diff.json +++ b/code/src/vs/editor/test/node/diffing/fixtures/method-splitting/advanced.expected.diff.json @@ -33,16 +33,8 @@ "modifiedRange": "[7,9 -> 7,27]" }, { - "originalRange": "[7,61 -> 7,105]", - "modifiedRange": "[7,50 -> 7,85]" - }, - { - "originalRange": "[7,112 -> 8,10]", - "modifiedRange": "[7,92 -> 7,130]" - }, - { - "originalRange": "[8,15 -> 10,1]", - "modifiedRange": "[7,135 -> 8,1]" + "originalRange": "[7,61 -> 10,1]", + "modifiedRange": "[7,50 -> 8,1]" } ] }, diff --git a/code/src/vs/editor/test/node/diffing/fixtures/noise-1/1.tst b/code/src/vs/editor/test/node/diffing/fixtures/noise-1/1.tst new file mode 100644 index 00000000000..bd5594cabb4 --- /dev/null +++ b/code/src/vs/editor/test/node/diffing/fixtures/noise-1/1.tst @@ -0,0 +1,57 @@ +this._sash = derivedWithStore('sash', (reader, store) => { + const showSash = this._options.renderSideBySide.read(reader); + this.elements.root.classList.toggle('side-by-side', showSash); + if (!showSash) { return undefined; } + const result = store.add(new DiffEditorSash( + this._options, + this.elements.root, + { + height: this._rootSizeObserver.height, + width: this._rootSizeObserver.width.map((w, reader) => w - (this._options.renderOverviewRuler.read(reader) ? OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)), + } + )); + store.add(autorun('setBoundarySashes', reader => { + const boundarySashes = this._boundarySashes.read(reader); + if (boundarySashes) { + result.setBoundarySashes(boundarySashes); + } + })); + return result; +}); +this._register(keepAlive(this._sash, true)); + +this._register(autorunWithStore2('UnchangedRangesFeature', (reader, store) => { + this.unchangedRangesFeature = store.add(new (readHotReloadableExport(UnchangedRangesFeature, reader))(this._editors, this._diffModel, this._options)); +})); + +this._register(autorunWithStore2('DiffEditorDecorations', (reader, store) => { + store.add(new (readHotReloadableExport(DiffEditorDecorations, reader))(this._editors, this._diffModel, this._options)); +})); +this._register(autorunWithStore2('ViewZoneManager', (reader, store) => { + store.add(this._instantiationService.createInstance( + readHotReloadableExport(ViewZoneManager, reader), + this._editors, + this._diffModel, + this._options, + this, + () => this.unchangedRangesFeature.isUpdatingViewZones, + )); +})); + +this._register(autorunWithStore2('OverviewRulerPart', (reader, store) => { + store.add(this._instantiationService.createInstance(readHotReloadableExport(OverviewRulerPart, reader), this._editors, + this.elements.root, + this._diffModel, + this._rootSizeObserver.width, + this._rootSizeObserver.height, + this._layoutInfo.map(i => i.modifiedEditor), + this._options, + )); +})); + +this._reviewPane = this._register(this._instantiationService.createInstance(DiffReview2, this)); +this.elements.root.appendChild(this._reviewPane.domNode.domNode); +this.elements.root.appendChild(this._reviewPane.actionBarContainer.domNode); +reviewPaneObservable.set(this._reviewPane, undefined); + +this._createDiffEditorContributions(); diff --git a/code/src/vs/editor/test/node/diffing/fixtures/noise-1/2.tst b/code/src/vs/editor/test/node/diffing/fixtures/noise-1/2.tst new file mode 100644 index 00000000000..0432b64d75f --- /dev/null +++ b/code/src/vs/editor/test/node/diffing/fixtures/noise-1/2.tst @@ -0,0 +1,67 @@ +this._sash = derivedWithStore('sash', (reader, store) => { + const showSash = this._options.renderSideBySide.read(reader); + this.elements.root.classList.toggle('side-by-side', showSash); + if (!showSash) { return undefined; } + const result = store.add(new DiffEditorSash( + this._options, + this.elements.root, + { + height: this._rootSizeObserver.height, + width: this._rootSizeObserver.width.map((w, reader) => w - (this._options.renderOverviewRuler.read(reader) ? OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)), + } + )); + store.add(autorun('setBoundarySashes', reader => { + const boundarySashes = this._boundarySashes.read(reader); + if (boundarySashes) { + result.setBoundarySashes(boundarySashes); + } + })); + return result; +}); +this._register(keepAlive(this._sash, true)); + +this._register(autorunWithStore2('UnchangedRangesFeature', (reader, store) => { + this.unchangedRangesFeature = store.add(new (readHotReloadableExport(UnchangedRangesFeature, reader))(this._editors, this._diffModel, this._options)); +})); + +this._register(autorunWithStore2('DiffEditorDecorations', (reader, store) => { + store.add(new (readHotReloadableExport(DiffEditorDecorations, reader))(this._editors, this._diffModel, this._options)); +})); +this._register(autorunWithStore2('ViewZoneManager', (reader, store) => { + store.add(this._instantiationService.createInstance( + readHotReloadableExport(ViewZoneManager, reader), + this._editors, + this._diffModel, + this._options, + this, + () => this.unchangedRangesFeature.isUpdatingViewZones, + )); +})); + +this._register(autorunWithStore2('OverviewRulerPart', (reader, store) => { + store.add(this._instantiationService.createInstance(readHotReloadableExport(OverviewRulerPart, reader), this._editors, + this.elements.root, + this._diffModel, + this._rootSizeObserver.width, + this._rootSizeObserver.height, + this._layoutInfo.map(i => i.modifiedEditor), + this._options, + )); +})); + +this._register(autorunWithStore2('_accessibleDiffViewer', (reader, store) => { + this._accessibleDiffViewer = store.add(this._register(this._instantiationService.createInstance( + readHotReloadableExport(AccessibleDiffViewer, reader), + this.elements.accessibleDiffViewer, + this._accessibleDiffViewerVisible, + this._rootSizeObserver.width, + this._rootSizeObserver.height, + this._diffModel.map((m, r) => m?.diff.read(r)?.mappings.map(m => m.lineRangeMapping)), + this._editors, + ))); +})); +const visibility = this._accessibleDiffViewerVisible.map(v => v ? 'hidden' : 'visible'); +this._register(applyStyle(this.elements.modified, { visibility })); +this._register(applyStyle(this.elements.original, { visibility })); + +this._createDiffEditorContributions(); diff --git a/code/src/vs/editor/test/node/diffing/fixtures/noise-1/advanced.expected.diff.json b/code/src/vs/editor/test/node/diffing/fixtures/noise-1/advanced.expected.diff.json new file mode 100644 index 00000000000..5a679831447 --- /dev/null +++ b/code/src/vs/editor/test/node/diffing/fixtures/noise-1/advanced.expected.diff.json @@ -0,0 +1,30 @@ +{ + "original": { + "content": "this._sash = derivedWithStore('sash', (reader, store) => {\n\tconst showSash = this._options.renderSideBySide.read(reader);\n\tthis.elements.root.classList.toggle('side-by-side', showSash);\n\tif (!showSash) { return undefined; }\n\tconst result = store.add(new DiffEditorSash(\n\t\tthis._options,\n\t\tthis.elements.root,\n\t\t{\n\t\t\theight: this._rootSizeObserver.height,\n\t\t\twidth: this._rootSizeObserver.width.map((w, reader) => w - (this._options.renderOverviewRuler.read(reader) ? OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)),\n\t\t}\n\t));\n\tstore.add(autorun('setBoundarySashes', reader => {\n\t\tconst boundarySashes = this._boundarySashes.read(reader);\n\t\tif (boundarySashes) {\n\t\t\tresult.setBoundarySashes(boundarySashes);\n\t\t}\n\t}));\n\treturn result;\n});\nthis._register(keepAlive(this._sash, true));\n\nthis._register(autorunWithStore2('UnchangedRangesFeature', (reader, store) => {\n\tthis.unchangedRangesFeature = store.add(new (readHotReloadableExport(UnchangedRangesFeature, reader))(this._editors, this._diffModel, this._options));\n}));\n\nthis._register(autorunWithStore2('DiffEditorDecorations', (reader, store) => {\n\tstore.add(new (readHotReloadableExport(DiffEditorDecorations, reader))(this._editors, this._diffModel, this._options));\n}));\nthis._register(autorunWithStore2('ViewZoneManager', (reader, store) => {\n\tstore.add(this._instantiationService.createInstance(\n\t\treadHotReloadableExport(ViewZoneManager, reader),\n\t\tthis._editors,\n\t\tthis._diffModel,\n\t\tthis._options,\n\t\tthis,\n\t\t() => this.unchangedRangesFeature.isUpdatingViewZones,\n\t));\n}));\n\nthis._register(autorunWithStore2('OverviewRulerPart', (reader, store) => {\n\tstore.add(this._instantiationService.createInstance(readHotReloadableExport(OverviewRulerPart, reader), this._editors,\n\t\tthis.elements.root,\n\t\tthis._diffModel,\n\t\tthis._rootSizeObserver.width,\n\t\tthis._rootSizeObserver.height,\n\t\tthis._layoutInfo.map(i => i.modifiedEditor),\n\t\tthis._options,\n\t));\n}));\n\nthis._reviewPane = this._register(this._instantiationService.createInstance(DiffReview2, this));\nthis.elements.root.appendChild(this._reviewPane.domNode.domNode);\nthis.elements.root.appendChild(this._reviewPane.actionBarContainer.domNode);\nreviewPaneObservable.set(this._reviewPane, undefined);\n\nthis._createDiffEditorContributions();\n", + "fileName": "./1.tst" + }, + "modified": { + "content": "this._sash = derivedWithStore('sash', (reader, store) => {\n\tconst showSash = this._options.renderSideBySide.read(reader);\n\tthis.elements.root.classList.toggle('side-by-side', showSash);\n\tif (!showSash) { return undefined; }\n\tconst result = store.add(new DiffEditorSash(\n\t\tthis._options,\n\t\tthis.elements.root,\n\t\t{\n\t\t\theight: this._rootSizeObserver.height,\n\t\t\twidth: this._rootSizeObserver.width.map((w, reader) => w - (this._options.renderOverviewRuler.read(reader) ? OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)),\n\t\t}\n\t));\n\tstore.add(autorun('setBoundarySashes', reader => {\n\t\tconst boundarySashes = this._boundarySashes.read(reader);\n\t\tif (boundarySashes) {\n\t\t\tresult.setBoundarySashes(boundarySashes);\n\t\t}\n\t}));\n\treturn result;\n});\nthis._register(keepAlive(this._sash, true));\n\nthis._register(autorunWithStore2('UnchangedRangesFeature', (reader, store) => {\n\tthis.unchangedRangesFeature = store.add(new (readHotReloadableExport(UnchangedRangesFeature, reader))(this._editors, this._diffModel, this._options));\n}));\n\nthis._register(autorunWithStore2('DiffEditorDecorations', (reader, store) => {\n\tstore.add(new (readHotReloadableExport(DiffEditorDecorations, reader))(this._editors, this._diffModel, this._options));\n}));\nthis._register(autorunWithStore2('ViewZoneManager', (reader, store) => {\n\tstore.add(this._instantiationService.createInstance(\n\t\treadHotReloadableExport(ViewZoneManager, reader),\n\t\tthis._editors,\n\t\tthis._diffModel,\n\t\tthis._options,\n\t\tthis,\n\t\t() => this.unchangedRangesFeature.isUpdatingViewZones,\n\t));\n}));\n\nthis._register(autorunWithStore2('OverviewRulerPart', (reader, store) => {\n\tstore.add(this._instantiationService.createInstance(readHotReloadableExport(OverviewRulerPart, reader), this._editors,\n\t\tthis.elements.root,\n\t\tthis._diffModel,\n\t\tthis._rootSizeObserver.width,\n\t\tthis._rootSizeObserver.height,\n\t\tthis._layoutInfo.map(i => i.modifiedEditor),\n\t\tthis._options,\n\t));\n}));\n\nthis._register(autorunWithStore2('_accessibleDiffViewer', (reader, store) => {\n\tthis._accessibleDiffViewer = store.add(this._register(this._instantiationService.createInstance(\n\t\treadHotReloadableExport(AccessibleDiffViewer, reader),\n\t\tthis.elements.accessibleDiffViewer,\n\t\tthis._accessibleDiffViewerVisible,\n\t\tthis._rootSizeObserver.width,\n\t\tthis._rootSizeObserver.height,\n\t\tthis._diffModel.map((m, r) => m?.diff.read(r)?.mappings.map(m => m.lineRangeMapping)),\n\t\tthis._editors,\n\t)));\n}));\nconst visibility = this._accessibleDiffViewerVisible.map(v => v ? 'hidden' : 'visible');\nthis._register(applyStyle(this.elements.modified, { visibility }));\nthis._register(applyStyle(this.elements.original, { visibility }));\n\nthis._createDiffEditorContributions();\n", + "fileName": "./2.tst" + }, + "diffs": [ + { + "originalRange": "[52,56)", + "modifiedRange": "[52,66)", + "innerChanges": [ + { + "originalRange": "[52,7 -> 52,20]", + "modifiedRange": "[52,7 -> 53,2]" + }, + { + "originalRange": "[52,24 -> 52,24]", + "modifiedRange": "[53,6 -> 53,45]" + }, + { + "originalRange": "[52,77 -> 55,53]", + "modifiedRange": "[53,98 -> 65,66]" + } + ] + } + ] +} \ No newline at end of file diff --git a/code/src/vs/editor/test/node/diffing/fixtures/noise-1/legacy.expected.diff.json b/code/src/vs/editor/test/node/diffing/fixtures/noise-1/legacy.expected.diff.json new file mode 100644 index 00000000000..65a39f37dbe --- /dev/null +++ b/code/src/vs/editor/test/node/diffing/fixtures/noise-1/legacy.expected.diff.json @@ -0,0 +1,70 @@ +{ + "original": { + "content": "this._sash = derivedWithStore('sash', (reader, store) => {\n\tconst showSash = this._options.renderSideBySide.read(reader);\n\tthis.elements.root.classList.toggle('side-by-side', showSash);\n\tif (!showSash) { return undefined; }\n\tconst result = store.add(new DiffEditorSash(\n\t\tthis._options,\n\t\tthis.elements.root,\n\t\t{\n\t\t\theight: this._rootSizeObserver.height,\n\t\t\twidth: this._rootSizeObserver.width.map((w, reader) => w - (this._options.renderOverviewRuler.read(reader) ? OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)),\n\t\t}\n\t));\n\tstore.add(autorun('setBoundarySashes', reader => {\n\t\tconst boundarySashes = this._boundarySashes.read(reader);\n\t\tif (boundarySashes) {\n\t\t\tresult.setBoundarySashes(boundarySashes);\n\t\t}\n\t}));\n\treturn result;\n});\nthis._register(keepAlive(this._sash, true));\n\nthis._register(autorunWithStore2('UnchangedRangesFeature', (reader, store) => {\n\tthis.unchangedRangesFeature = store.add(new (readHotReloadableExport(UnchangedRangesFeature, reader))(this._editors, this._diffModel, this._options));\n}));\n\nthis._register(autorunWithStore2('DiffEditorDecorations', (reader, store) => {\n\tstore.add(new (readHotReloadableExport(DiffEditorDecorations, reader))(this._editors, this._diffModel, this._options));\n}));\nthis._register(autorunWithStore2('ViewZoneManager', (reader, store) => {\n\tstore.add(this._instantiationService.createInstance(\n\t\treadHotReloadableExport(ViewZoneManager, reader),\n\t\tthis._editors,\n\t\tthis._diffModel,\n\t\tthis._options,\n\t\tthis,\n\t\t() => this.unchangedRangesFeature.isUpdatingViewZones,\n\t));\n}));\n\nthis._register(autorunWithStore2('OverviewRulerPart', (reader, store) => {\n\tstore.add(this._instantiationService.createInstance(readHotReloadableExport(OverviewRulerPart, reader), this._editors,\n\t\tthis.elements.root,\n\t\tthis._diffModel,\n\t\tthis._rootSizeObserver.width,\n\t\tthis._rootSizeObserver.height,\n\t\tthis._layoutInfo.map(i => i.modifiedEditor),\n\t\tthis._options,\n\t));\n}));\n\nthis._reviewPane = this._register(this._instantiationService.createInstance(DiffReview2, this));\nthis.elements.root.appendChild(this._reviewPane.domNode.domNode);\nthis.elements.root.appendChild(this._reviewPane.actionBarContainer.domNode);\nreviewPaneObservable.set(this._reviewPane, undefined);\n\nthis._createDiffEditorContributions();\n", + "fileName": "./1.tst" + }, + "modified": { + "content": "this._sash = derivedWithStore('sash', (reader, store) => {\n\tconst showSash = this._options.renderSideBySide.read(reader);\n\tthis.elements.root.classList.toggle('side-by-side', showSash);\n\tif (!showSash) { return undefined; }\n\tconst result = store.add(new DiffEditorSash(\n\t\tthis._options,\n\t\tthis.elements.root,\n\t\t{\n\t\t\theight: this._rootSizeObserver.height,\n\t\t\twidth: this._rootSizeObserver.width.map((w, reader) => w - (this._options.renderOverviewRuler.read(reader) ? OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)),\n\t\t}\n\t));\n\tstore.add(autorun('setBoundarySashes', reader => {\n\t\tconst boundarySashes = this._boundarySashes.read(reader);\n\t\tif (boundarySashes) {\n\t\t\tresult.setBoundarySashes(boundarySashes);\n\t\t}\n\t}));\n\treturn result;\n});\nthis._register(keepAlive(this._sash, true));\n\nthis._register(autorunWithStore2('UnchangedRangesFeature', (reader, store) => {\n\tthis.unchangedRangesFeature = store.add(new (readHotReloadableExport(UnchangedRangesFeature, reader))(this._editors, this._diffModel, this._options));\n}));\n\nthis._register(autorunWithStore2('DiffEditorDecorations', (reader, store) => {\n\tstore.add(new (readHotReloadableExport(DiffEditorDecorations, reader))(this._editors, this._diffModel, this._options));\n}));\nthis._register(autorunWithStore2('ViewZoneManager', (reader, store) => {\n\tstore.add(this._instantiationService.createInstance(\n\t\treadHotReloadableExport(ViewZoneManager, reader),\n\t\tthis._editors,\n\t\tthis._diffModel,\n\t\tthis._options,\n\t\tthis,\n\t\t() => this.unchangedRangesFeature.isUpdatingViewZones,\n\t));\n}));\n\nthis._register(autorunWithStore2('OverviewRulerPart', (reader, store) => {\n\tstore.add(this._instantiationService.createInstance(readHotReloadableExport(OverviewRulerPart, reader), this._editors,\n\t\tthis.elements.root,\n\t\tthis._diffModel,\n\t\tthis._rootSizeObserver.width,\n\t\tthis._rootSizeObserver.height,\n\t\tthis._layoutInfo.map(i => i.modifiedEditor),\n\t\tthis._options,\n\t));\n}));\n\nthis._register(autorunWithStore2('_accessibleDiffViewer', (reader, store) => {\n\tthis._accessibleDiffViewer = store.add(this._register(this._instantiationService.createInstance(\n\t\treadHotReloadableExport(AccessibleDiffViewer, reader),\n\t\tthis.elements.accessibleDiffViewer,\n\t\tthis._accessibleDiffViewerVisible,\n\t\tthis._rootSizeObserver.width,\n\t\tthis._rootSizeObserver.height,\n\t\tthis._diffModel.map((m, r) => m?.diff.read(r)?.mappings.map(m => m.lineRangeMapping)),\n\t\tthis._editors,\n\t)));\n}));\nconst visibility = this._accessibleDiffViewerVisible.map(v => v ? 'hidden' : 'visible');\nthis._register(applyStyle(this.elements.modified, { visibility }));\nthis._register(applyStyle(this.elements.original, { visibility }));\n\nthis._createDiffEditorContributions();\n", + "fileName": "./2.tst" + }, + "diffs": [ + { + "originalRange": "[52,56)", + "modifiedRange": "[52,66)", + "innerChanges": [ + { + "originalRange": "[52,9 -> 52,20]", + "modifiedRange": "[52,9 -> 53,41]" + }, + { + "originalRange": "[52,77 -> 52,77]", + "modifiedRange": "[53,98 -> 54,37]" + }, + { + "originalRange": "[52,81 -> 52,84]", + "modifiedRange": "[54,41 -> 54,42]" + }, + { + "originalRange": "[52,87 -> 53,1]", + "modifiedRange": "[54,45 -> 55,3]" + }, + { + "originalRange": "[53,15 -> 53,32]", + "modifiedRange": "[55,17 -> 56,3]" + }, + { + "originalRange": "[53,38 -> 53,41]", + "modifiedRange": "[56,9 -> 56,24]" + }, + { + "originalRange": "[53,44 -> 54,1]", + "modifiedRange": "[56,27 -> 59,3]" + }, + { + "originalRange": "[54,6 -> 54,20]", + "modifiedRange": "[59,8 -> 59,80]" + }, + { + "originalRange": "[54,23 -> 54,32]", + "modifiedRange": "[59,83 -> 63,20]" + }, + { + "originalRange": "[54,38 -> 54,41]", + "modifiedRange": "[63,26 -> 63,41]" + }, + { + "originalRange": "[54,44 -> 54,75]", + "modifiedRange": "[63,44 -> 63,111]" + }, + { + "originalRange": "[55,1 -> 55,26]", + "modifiedRange": "[64,1 -> 65,1]" + }, + { + "originalRange": "[55,34 -> 55,53]", + "modifiedRange": "[65,9 -> 65,66]" + } + ] + } + ] +} \ No newline at end of file diff --git a/code/src/vs/editor/test/node/diffing/fixtures/noise-2/1.tst b/code/src/vs/editor/test/node/diffing/fixtures/noise-2/1.tst new file mode 100644 index 00000000000..684409b3bbb --- /dev/null +++ b/code/src/vs/editor/test/node/diffing/fixtures/noise-2/1.tst @@ -0,0 +1,91 @@ + +const maxPersistedSessions = 25; + +export class ChatService extends Disposable implements IChatService { + + private async _sendRequestAsync(model: ChatModel, provider: IChatProvider, message: string | IChatReplyFollowup, usedSlashCommand?: ISlashCommand): Promise { + const request = model.addRequest(message); + + const resolvedCommand = typeof message === 'string' && message.startsWith('/') ? await this.handleSlashCommand(model.sessionId, message) : message; + + let gotProgress = false; + const requestType = typeof message === 'string' ? + (message.startsWith('/') ? 'slashCommand' : 'string') : + 'followup'; + + const rawResponsePromise = createCancelablePromise(async token => { + const progressCallback = (progress: IChatProgress) => { + if (token.isCancellationRequested) { + return; + } + + gotProgress = true; + if ('content' in progress) { + this.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${progress.content.length} chars`); + } else { + this.trace('sendRequest', `Provider returned id for session ${model.sessionId}, ${progress.requestId}`); + } + + model.acceptResponseProgress(request, progress); + }; + + const stopWatch = new StopWatch(false); + token.onCancellationRequested(() => { + this.trace('sendRequest', `Request for session ${model.sessionId} was cancelled`); + this.telemetryService.publicLog2('interactiveSessionProviderInvoked', { + providerId: provider.id, + timeToFirstProgress: -1, + // Normally timings happen inside the EH around the actual provider. For cancellation we can measure how long the user waited before cancelling + totalTime: stopWatch.elapsed(), + result: 'cancelled', + requestType, + slashCommand: usedSlashCommand?.command + }); + + model.cancelRequest(request); + }); + if (usedSlashCommand?.command) { + this._onDidSubmitSlashCommand.fire({ slashCommand: usedSlashCommand.command, sessionId: model.sessionId }); + } + let rawResponse = await provider.provideReply({ session: model.session!, message: resolvedCommand }, progressCallback, token); + if (token.isCancellationRequested) { + return; + } else { + if (!rawResponse) { + this.trace('sendRequest', `Provider returned no response for session ${model.sessionId}`); + rawResponse = { session: model.session!, errorDetails: { message: localize('emptyResponse', "Provider returned null response") } }; + } + + const result = rawResponse.errorDetails?.responseIsFiltered ? 'filtered' : + rawResponse.errorDetails && gotProgress ? 'errorWithOutput' : + rawResponse.errorDetails ? 'error' : + 'success'; + this.telemetryService.publicLog2('interactiveSessionProviderInvoked', { + providerId: provider.id, + timeToFirstProgress: rawResponse.timings?.firstProgress ?? 0, + totalTime: rawResponse.timings?.totalElapsed ?? 0, + result, + requestType, + slashCommand: usedSlashCommand?.command + }); + model.setResponse(request, rawResponse); + this.trace('sendRequest', `Provider returned response for session ${model.sessionId}`); + + // TODO refactor this or rethink the API https://github.com/microsoft/vscode-copilot/issues/593 + if (provider.provideFollowups) { + Promise.resolve(provider.provideFollowups(model.session!, CancellationToken.None)).then(followups => { + model.setFollowups(request, withNullAsUndefined(followups)); + model.completeResponse(request); + }); + } else { + model.completeResponse(request); + } + } + }); + this._pendingRequests.set(model.sessionId, rawResponsePromise); + rawResponsePromise.finally(() => { + this._pendingRequests.delete(model.sessionId); + }); + return rawResponsePromise; + } +} diff --git a/code/src/vs/editor/test/node/diffing/fixtures/noise-2/2.tst b/code/src/vs/editor/test/node/diffing/fixtures/noise-2/2.tst new file mode 100644 index 00000000000..b7778d336a9 --- /dev/null +++ b/code/src/vs/editor/test/node/diffing/fixtures/noise-2/2.tst @@ -0,0 +1,111 @@ + +const maxPersistedSessions = 25; + +export class ChatService extends Disposable implements IChatService { + + private async _sendRequestAsync(model: ChatModel, provider: IChatProvider, message: string | IChatReplyFollowup, usedSlashCommand?: ISlashCommand): Promise { + const request = model.addRequest(message); + + const resolvedCommand = typeof message === 'string' && message.startsWith('/') ? await this.handleSlashCommand(model.sessionId, message) : message; + + let gotProgress = false; + const requestType = typeof message === 'string' ? + (message.startsWith('/') ? 'slashCommand' : 'string') : + 'followup'; + + const rawResponsePromise = createCancelablePromise(async token => { + const progressCallback = (progress: IChatProgress) => { + if (token.isCancellationRequested) { + return; + } + + gotProgress = true; + if ('content' in progress) { + this.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${progress.content.length} chars`); + } else { + this.trace('sendRequest', `Provider returned id for session ${model.sessionId}, ${progress.requestId}`); + } + + model.acceptResponseProgress(request, progress); + }; + + const stopWatch = new StopWatch(false); + token.onCancellationRequested(() => { + this.trace('sendRequest', `Request for session ${model.sessionId} was cancelled`); + this.telemetryService.publicLog2('interactiveSessionProviderInvoked', { + providerId: provider.id, + timeToFirstProgress: -1, + // Normally timings happen inside the EH around the actual provider. For cancellation we can measure how long the user waited before cancelling + totalTime: stopWatch.elapsed(), + result: 'cancelled', + requestType, + slashCommand: usedSlashCommand?.command + }); + + model.cancelRequest(request); + }); + if (usedSlashCommand?.command) { + this._onDidSubmitSlashCommand.fire({ slashCommand: usedSlashCommand.command, sessionId: model.sessionId }); + } + + let rawResponse: IChatResponse | null | undefined; + + if ((typeof resolvedCommand === 'string' && typeof message === 'string' && this.chatSlashCommandService.hasCommand(resolvedCommand))) { + // contributed slash commands + // TODO: spell this out in the UI + const history: IChatMessage[] = []; + for (const request of model.getRequests()) { + if (typeof request.message !== 'string' || !request.response) { + continue; + } + history.push({ role: ChatMessageRole.User, content: request.message }); + history.push({ role: ChatMessageRole.Assistant, content: request.response?.response.value }); + } + await this.chatSlashCommandService.executeCommand(resolvedCommand, message.substring(resolvedCommand.length + 1).trimStart(), new Progress(p => progressCallback(p)), history, token); + rawResponse = { session: model.session! }; + + } else { + rawResponse = await provider.provideReply({ session: model.session!, message: resolvedCommand }, progressCallback, token); + } + + if (token.isCancellationRequested) { + return; + } else { + if (!rawResponse) { + this.trace('sendRequest', `Provider returned no response for session ${model.sessionId}`); + rawResponse = { session: model.session!, errorDetails: { message: localize('emptyResponse', "Provider returned null response") } }; + } + + const result = rawResponse.errorDetails?.responseIsFiltered ? 'filtered' : + rawResponse.errorDetails && gotProgress ? 'errorWithOutput' : + rawResponse.errorDetails ? 'error' : + 'success'; + this.telemetryService.publicLog2('interactiveSessionProviderInvoked', { + providerId: provider.id, + timeToFirstProgress: rawResponse.timings?.firstProgress ?? 0, + totalTime: rawResponse.timings?.totalElapsed ?? 0, + result, + requestType, + slashCommand: usedSlashCommand?.command + }); + model.setResponse(request, rawResponse); + this.trace('sendRequest', `Provider returned response for session ${model.sessionId}`); + + // TODO refactor this or rethink the API https://github.com/microsoft/vscode-copilot/issues/593 + if (provider.provideFollowups) { + Promise.resolve(provider.provideFollowups(model.session!, CancellationToken.None)).then(followups => { + model.setFollowups(request, withNullAsUndefined(followups)); + model.completeResponse(request); + }); + } else { + model.completeResponse(request); + } + } + }); + this._pendingRequests.set(model.sessionId, rawResponsePromise); + rawResponsePromise.finally(() => { + this._pendingRequests.delete(model.sessionId); + }); + return rawResponsePromise; + } +} diff --git a/code/src/vs/editor/test/node/diffing/fixtures/noise-2/advanced.expected.diff.json b/code/src/vs/editor/test/node/diffing/fixtures/noise-2/advanced.expected.diff.json new file mode 100644 index 00000000000..a30c65cb9ee --- /dev/null +++ b/code/src/vs/editor/test/node/diffing/fixtures/noise-2/advanced.expected.diff.json @@ -0,0 +1,30 @@ +{ + "original": { + "content": "\nconst maxPersistedSessions = 25;\n\nexport class ChatService extends Disposable implements IChatService {\n\n\tprivate async _sendRequestAsync(model: ChatModel, provider: IChatProvider, message: string | IChatReplyFollowup, usedSlashCommand?: ISlashCommand): Promise {\n\t\tconst request = model.addRequest(message);\n\n\t\tconst resolvedCommand = typeof message === 'string' && message.startsWith('/') ? await this.handleSlashCommand(model.sessionId, message) : message;\n\n\t\tlet gotProgress = false;\n\t\tconst requestType = typeof message === 'string' ?\n\t\t\t(message.startsWith('/') ? 'slashCommand' : 'string') :\n\t\t\t'followup';\n\n\t\tconst rawResponsePromise = createCancelablePromise(async token => {\n\t\t\tconst progressCallback = (progress: IChatProgress) => {\n\t\t\t\tif (token.isCancellationRequested) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tgotProgress = true;\n\t\t\t\tif ('content' in progress) {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${progress.content.length} chars`);\n\t\t\t\t} else {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned id for session ${model.sessionId}, ${progress.requestId}`);\n\t\t\t\t}\n\n\t\t\t\tmodel.acceptResponseProgress(request, progress);\n\t\t\t};\n\n\t\t\tconst stopWatch = new StopWatch(false);\n\t\t\ttoken.onCancellationRequested(() => {\n\t\t\t\tthis.trace('sendRequest', `Request for session ${model.sessionId} was cancelled`);\n\t\t\t\tthis.telemetryService.publicLog2('interactiveSessionProviderInvoked', {\n\t\t\t\t\tproviderId: provider.id,\n\t\t\t\t\ttimeToFirstProgress: -1,\n\t\t\t\t\t// Normally timings happen inside the EH around the actual provider. For cancellation we can measure how long the user waited before cancelling\n\t\t\t\t\ttotalTime: stopWatch.elapsed(),\n\t\t\t\t\tresult: 'cancelled',\n\t\t\t\t\trequestType,\n\t\t\t\t\tslashCommand: usedSlashCommand?.command\n\t\t\t\t});\n\n\t\t\t\tmodel.cancelRequest(request);\n\t\t\t});\n\t\t\tif (usedSlashCommand?.command) {\n\t\t\t\tthis._onDidSubmitSlashCommand.fire({ slashCommand: usedSlashCommand.command, sessionId: model.sessionId });\n\t\t\t}\n\t\t\tlet rawResponse = await provider.provideReply({ session: model.session!, message: resolvedCommand }, progressCallback, token);\n\t\t\tif (token.isCancellationRequested) {\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\tif (!rawResponse) {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned no response for session ${model.sessionId}`);\n\t\t\t\t\trawResponse = { session: model.session!, errorDetails: { message: localize('emptyResponse', \"Provider returned null response\") } };\n\t\t\t\t}\n\n\t\t\t\tconst result = rawResponse.errorDetails?.responseIsFiltered ? 'filtered' :\n\t\t\t\t\trawResponse.errorDetails && gotProgress ? 'errorWithOutput' :\n\t\t\t\t\t\trawResponse.errorDetails ? 'error' :\n\t\t\t\t\t\t\t'success';\n\t\t\t\tthis.telemetryService.publicLog2('interactiveSessionProviderInvoked', {\n\t\t\t\t\tproviderId: provider.id,\n\t\t\t\t\ttimeToFirstProgress: rawResponse.timings?.firstProgress ?? 0,\n\t\t\t\t\ttotalTime: rawResponse.timings?.totalElapsed ?? 0,\n\t\t\t\t\tresult,\n\t\t\t\t\trequestType,\n\t\t\t\t\tslashCommand: usedSlashCommand?.command\n\t\t\t\t});\n\t\t\t\tmodel.setResponse(request, rawResponse);\n\t\t\t\tthis.trace('sendRequest', `Provider returned response for session ${model.sessionId}`);\n\n\t\t\t\t// TODO refactor this or rethink the API https://github.com/microsoft/vscode-copilot/issues/593\n\t\t\t\tif (provider.provideFollowups) {\n\t\t\t\t\tPromise.resolve(provider.provideFollowups(model.session!, CancellationToken.None)).then(followups => {\n\t\t\t\t\t\tmodel.setFollowups(request, withNullAsUndefined(followups));\n\t\t\t\t\t\tmodel.completeResponse(request);\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tmodel.completeResponse(request);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tthis._pendingRequests.set(model.sessionId, rawResponsePromise);\n\t\trawResponsePromise.finally(() => {\n\t\t\tthis._pendingRequests.delete(model.sessionId);\n\t\t});\n\t\treturn rawResponsePromise;\n\t}\n}\n", + "fileName": "./1.tst" + }, + "modified": { + "content": "\nconst maxPersistedSessions = 25;\n\nexport class ChatService extends Disposable implements IChatService {\n\n\tprivate async _sendRequestAsync(model: ChatModel, provider: IChatProvider, message: string | IChatReplyFollowup, usedSlashCommand?: ISlashCommand): Promise {\n\t\tconst request = model.addRequest(message);\n\n\t\tconst resolvedCommand = typeof message === 'string' && message.startsWith('/') ? await this.handleSlashCommand(model.sessionId, message) : message;\n\n\t\tlet gotProgress = false;\n\t\tconst requestType = typeof message === 'string' ?\n\t\t\t(message.startsWith('/') ? 'slashCommand' : 'string') :\n\t\t\t'followup';\n\n\t\tconst rawResponsePromise = createCancelablePromise(async token => {\n\t\t\tconst progressCallback = (progress: IChatProgress) => {\n\t\t\t\tif (token.isCancellationRequested) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tgotProgress = true;\n\t\t\t\tif ('content' in progress) {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${progress.content.length} chars`);\n\t\t\t\t} else {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned id for session ${model.sessionId}, ${progress.requestId}`);\n\t\t\t\t}\n\n\t\t\t\tmodel.acceptResponseProgress(request, progress);\n\t\t\t};\n\n\t\t\tconst stopWatch = new StopWatch(false);\n\t\t\ttoken.onCancellationRequested(() => {\n\t\t\t\tthis.trace('sendRequest', `Request for session ${model.sessionId} was cancelled`);\n\t\t\t\tthis.telemetryService.publicLog2('interactiveSessionProviderInvoked', {\n\t\t\t\t\tproviderId: provider.id,\n\t\t\t\t\ttimeToFirstProgress: -1,\n\t\t\t\t\t// Normally timings happen inside the EH around the actual provider. For cancellation we can measure how long the user waited before cancelling\n\t\t\t\t\ttotalTime: stopWatch.elapsed(),\n\t\t\t\t\tresult: 'cancelled',\n\t\t\t\t\trequestType,\n\t\t\t\t\tslashCommand: usedSlashCommand?.command\n\t\t\t\t});\n\n\t\t\t\tmodel.cancelRequest(request);\n\t\t\t});\n\t\t\tif (usedSlashCommand?.command) {\n\t\t\t\tthis._onDidSubmitSlashCommand.fire({ slashCommand: usedSlashCommand.command, sessionId: model.sessionId });\n\t\t\t}\n\n\t\t\tlet rawResponse: IChatResponse | null | undefined;\n\n\t\t\tif ((typeof resolvedCommand === 'string' && typeof message === 'string' && this.chatSlashCommandService.hasCommand(resolvedCommand))) {\n\t\t\t\t// contributed slash commands\n\t\t\t\t// TODO: spell this out in the UI\n\t\t\t\tconst history: IChatMessage[] = [];\n\t\t\t\tfor (const request of model.getRequests()) {\n\t\t\t\t\tif (typeof request.message !== 'string' || !request.response) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\thistory.push({ role: ChatMessageRole.User, content: request.message });\n\t\t\t\t\thistory.push({ role: ChatMessageRole.Assistant, content: request.response?.response.value });\n\t\t\t\t}\n\t\t\t\tawait this.chatSlashCommandService.executeCommand(resolvedCommand, message.substring(resolvedCommand.length + 1).trimStart(), new Progress(p => progressCallback(p)), history, token);\n\t\t\t\trawResponse = { session: model.session! };\n\n\t\t\t} else {\n\t\t\t\trawResponse = await provider.provideReply({ session: model.session!, message: resolvedCommand }, progressCallback, token);\n\t\t\t}\n\n\t\t\tif (token.isCancellationRequested) {\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\tif (!rawResponse) {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned no response for session ${model.sessionId}`);\n\t\t\t\t\trawResponse = { session: model.session!, errorDetails: { message: localize('emptyResponse', \"Provider returned null response\") } };\n\t\t\t\t}\n\n\t\t\t\tconst result = rawResponse.errorDetails?.responseIsFiltered ? 'filtered' :\n\t\t\t\t\trawResponse.errorDetails && gotProgress ? 'errorWithOutput' :\n\t\t\t\t\t\trawResponse.errorDetails ? 'error' :\n\t\t\t\t\t\t\t'success';\n\t\t\t\tthis.telemetryService.publicLog2('interactiveSessionProviderInvoked', {\n\t\t\t\t\tproviderId: provider.id,\n\t\t\t\t\ttimeToFirstProgress: rawResponse.timings?.firstProgress ?? 0,\n\t\t\t\t\ttotalTime: rawResponse.timings?.totalElapsed ?? 0,\n\t\t\t\t\tresult,\n\t\t\t\t\trequestType,\n\t\t\t\t\tslashCommand: usedSlashCommand?.command\n\t\t\t\t});\n\t\t\t\tmodel.setResponse(request, rawResponse);\n\t\t\t\tthis.trace('sendRequest', `Provider returned response for session ${model.sessionId}`);\n\n\t\t\t\t// TODO refactor this or rethink the API https://github.com/microsoft/vscode-copilot/issues/593\n\t\t\t\tif (provider.provideFollowups) {\n\t\t\t\t\tPromise.resolve(provider.provideFollowups(model.session!, CancellationToken.None)).then(followups => {\n\t\t\t\t\t\tmodel.setFollowups(request, withNullAsUndefined(followups));\n\t\t\t\t\t\tmodel.completeResponse(request);\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tmodel.completeResponse(request);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tthis._pendingRequests.set(model.sessionId, rawResponsePromise);\n\t\trawResponsePromise.finally(() => {\n\t\t\tthis._pendingRequests.delete(model.sessionId);\n\t\t});\n\t\treturn rawResponsePromise;\n\t}\n}\n", + "fileName": "./2.tst" + }, + "diffs": [ + { + "originalRange": "[50,51)", + "modifiedRange": "[50,71)", + "innerChanges": [ + { + "originalRange": "[50,1 -> 50,1]", + "modifiedRange": "[50,1 -> 51,1]" + }, + { + "originalRange": "[50,19 -> 50,27]", + "modifiedRange": "[51,19 -> 68,24]" + }, + { + "originalRange": "[51,1 -> 51,1]", + "modifiedRange": "[69,1 -> 71,1]" + } + ] + } + ] +} \ No newline at end of file diff --git a/code/src/vs/editor/test/node/diffing/fixtures/noise-2/legacy.expected.diff.json b/code/src/vs/editor/test/node/diffing/fixtures/noise-2/legacy.expected.diff.json new file mode 100644 index 00000000000..1c4aecc9899 --- /dev/null +++ b/code/src/vs/editor/test/node/diffing/fixtures/noise-2/legacy.expected.diff.json @@ -0,0 +1,17 @@ +{ + "original": { + "content": "\nconst maxPersistedSessions = 25;\n\nexport class ChatService extends Disposable implements IChatService {\n\n\tprivate async _sendRequestAsync(model: ChatModel, provider: IChatProvider, message: string | IChatReplyFollowup, usedSlashCommand?: ISlashCommand): Promise {\n\t\tconst request = model.addRequest(message);\n\n\t\tconst resolvedCommand = typeof message === 'string' && message.startsWith('/') ? await this.handleSlashCommand(model.sessionId, message) : message;\n\n\t\tlet gotProgress = false;\n\t\tconst requestType = typeof message === 'string' ?\n\t\t\t(message.startsWith('/') ? 'slashCommand' : 'string') :\n\t\t\t'followup';\n\n\t\tconst rawResponsePromise = createCancelablePromise(async token => {\n\t\t\tconst progressCallback = (progress: IChatProgress) => {\n\t\t\t\tif (token.isCancellationRequested) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tgotProgress = true;\n\t\t\t\tif ('content' in progress) {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${progress.content.length} chars`);\n\t\t\t\t} else {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned id for session ${model.sessionId}, ${progress.requestId}`);\n\t\t\t\t}\n\n\t\t\t\tmodel.acceptResponseProgress(request, progress);\n\t\t\t};\n\n\t\t\tconst stopWatch = new StopWatch(false);\n\t\t\ttoken.onCancellationRequested(() => {\n\t\t\t\tthis.trace('sendRequest', `Request for session ${model.sessionId} was cancelled`);\n\t\t\t\tthis.telemetryService.publicLog2('interactiveSessionProviderInvoked', {\n\t\t\t\t\tproviderId: provider.id,\n\t\t\t\t\ttimeToFirstProgress: -1,\n\t\t\t\t\t// Normally timings happen inside the EH around the actual provider. For cancellation we can measure how long the user waited before cancelling\n\t\t\t\t\ttotalTime: stopWatch.elapsed(),\n\t\t\t\t\tresult: 'cancelled',\n\t\t\t\t\trequestType,\n\t\t\t\t\tslashCommand: usedSlashCommand?.command\n\t\t\t\t});\n\n\t\t\t\tmodel.cancelRequest(request);\n\t\t\t});\n\t\t\tif (usedSlashCommand?.command) {\n\t\t\t\tthis._onDidSubmitSlashCommand.fire({ slashCommand: usedSlashCommand.command, sessionId: model.sessionId });\n\t\t\t}\n\t\t\tlet rawResponse = await provider.provideReply({ session: model.session!, message: resolvedCommand }, progressCallback, token);\n\t\t\tif (token.isCancellationRequested) {\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\tif (!rawResponse) {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned no response for session ${model.sessionId}`);\n\t\t\t\t\trawResponse = { session: model.session!, errorDetails: { message: localize('emptyResponse', \"Provider returned null response\") } };\n\t\t\t\t}\n\n\t\t\t\tconst result = rawResponse.errorDetails?.responseIsFiltered ? 'filtered' :\n\t\t\t\t\trawResponse.errorDetails && gotProgress ? 'errorWithOutput' :\n\t\t\t\t\t\trawResponse.errorDetails ? 'error' :\n\t\t\t\t\t\t\t'success';\n\t\t\t\tthis.telemetryService.publicLog2('interactiveSessionProviderInvoked', {\n\t\t\t\t\tproviderId: provider.id,\n\t\t\t\t\ttimeToFirstProgress: rawResponse.timings?.firstProgress ?? 0,\n\t\t\t\t\ttotalTime: rawResponse.timings?.totalElapsed ?? 0,\n\t\t\t\t\tresult,\n\t\t\t\t\trequestType,\n\t\t\t\t\tslashCommand: usedSlashCommand?.command\n\t\t\t\t});\n\t\t\t\tmodel.setResponse(request, rawResponse);\n\t\t\t\tthis.trace('sendRequest', `Provider returned response for session ${model.sessionId}`);\n\n\t\t\t\t// TODO refactor this or rethink the API https://github.com/microsoft/vscode-copilot/issues/593\n\t\t\t\tif (provider.provideFollowups) {\n\t\t\t\t\tPromise.resolve(provider.provideFollowups(model.session!, CancellationToken.None)).then(followups => {\n\t\t\t\t\t\tmodel.setFollowups(request, withNullAsUndefined(followups));\n\t\t\t\t\t\tmodel.completeResponse(request);\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tmodel.completeResponse(request);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tthis._pendingRequests.set(model.sessionId, rawResponsePromise);\n\t\trawResponsePromise.finally(() => {\n\t\t\tthis._pendingRequests.delete(model.sessionId);\n\t\t});\n\t\treturn rawResponsePromise;\n\t}\n}\n", + "fileName": "./1.tst" + }, + "modified": { + "content": "\nconst maxPersistedSessions = 25;\n\nexport class ChatService extends Disposable implements IChatService {\n\n\tprivate async _sendRequestAsync(model: ChatModel, provider: IChatProvider, message: string | IChatReplyFollowup, usedSlashCommand?: ISlashCommand): Promise {\n\t\tconst request = model.addRequest(message);\n\n\t\tconst resolvedCommand = typeof message === 'string' && message.startsWith('/') ? await this.handleSlashCommand(model.sessionId, message) : message;\n\n\t\tlet gotProgress = false;\n\t\tconst requestType = typeof message === 'string' ?\n\t\t\t(message.startsWith('/') ? 'slashCommand' : 'string') :\n\t\t\t'followup';\n\n\t\tconst rawResponsePromise = createCancelablePromise(async token => {\n\t\t\tconst progressCallback = (progress: IChatProgress) => {\n\t\t\t\tif (token.isCancellationRequested) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tgotProgress = true;\n\t\t\t\tif ('content' in progress) {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${progress.content.length} chars`);\n\t\t\t\t} else {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned id for session ${model.sessionId}, ${progress.requestId}`);\n\t\t\t\t}\n\n\t\t\t\tmodel.acceptResponseProgress(request, progress);\n\t\t\t};\n\n\t\t\tconst stopWatch = new StopWatch(false);\n\t\t\ttoken.onCancellationRequested(() => {\n\t\t\t\tthis.trace('sendRequest', `Request for session ${model.sessionId} was cancelled`);\n\t\t\t\tthis.telemetryService.publicLog2('interactiveSessionProviderInvoked', {\n\t\t\t\t\tproviderId: provider.id,\n\t\t\t\t\ttimeToFirstProgress: -1,\n\t\t\t\t\t// Normally timings happen inside the EH around the actual provider. For cancellation we can measure how long the user waited before cancelling\n\t\t\t\t\ttotalTime: stopWatch.elapsed(),\n\t\t\t\t\tresult: 'cancelled',\n\t\t\t\t\trequestType,\n\t\t\t\t\tslashCommand: usedSlashCommand?.command\n\t\t\t\t});\n\n\t\t\t\tmodel.cancelRequest(request);\n\t\t\t});\n\t\t\tif (usedSlashCommand?.command) {\n\t\t\t\tthis._onDidSubmitSlashCommand.fire({ slashCommand: usedSlashCommand.command, sessionId: model.sessionId });\n\t\t\t}\n\n\t\t\tlet rawResponse: IChatResponse | null | undefined;\n\n\t\t\tif ((typeof resolvedCommand === 'string' && typeof message === 'string' && this.chatSlashCommandService.hasCommand(resolvedCommand))) {\n\t\t\t\t// contributed slash commands\n\t\t\t\t// TODO: spell this out in the UI\n\t\t\t\tconst history: IChatMessage[] = [];\n\t\t\t\tfor (const request of model.getRequests()) {\n\t\t\t\t\tif (typeof request.message !== 'string' || !request.response) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\thistory.push({ role: ChatMessageRole.User, content: request.message });\n\t\t\t\t\thistory.push({ role: ChatMessageRole.Assistant, content: request.response?.response.value });\n\t\t\t\t}\n\t\t\t\tawait this.chatSlashCommandService.executeCommand(resolvedCommand, message.substring(resolvedCommand.length + 1).trimStart(), new Progress(p => progressCallback(p)), history, token);\n\t\t\t\trawResponse = { session: model.session! };\n\n\t\t\t} else {\n\t\t\t\trawResponse = await provider.provideReply({ session: model.session!, message: resolvedCommand }, progressCallback, token);\n\t\t\t}\n\n\t\t\tif (token.isCancellationRequested) {\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\tif (!rawResponse) {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned no response for session ${model.sessionId}`);\n\t\t\t\t\trawResponse = { session: model.session!, errorDetails: { message: localize('emptyResponse', \"Provider returned null response\") } };\n\t\t\t\t}\n\n\t\t\t\tconst result = rawResponse.errorDetails?.responseIsFiltered ? 'filtered' :\n\t\t\t\t\trawResponse.errorDetails && gotProgress ? 'errorWithOutput' :\n\t\t\t\t\t\trawResponse.errorDetails ? 'error' :\n\t\t\t\t\t\t\t'success';\n\t\t\t\tthis.telemetryService.publicLog2('interactiveSessionProviderInvoked', {\n\t\t\t\t\tproviderId: provider.id,\n\t\t\t\t\ttimeToFirstProgress: rawResponse.timings?.firstProgress ?? 0,\n\t\t\t\t\ttotalTime: rawResponse.timings?.totalElapsed ?? 0,\n\t\t\t\t\tresult,\n\t\t\t\t\trequestType,\n\t\t\t\t\tslashCommand: usedSlashCommand?.command\n\t\t\t\t});\n\t\t\t\tmodel.setResponse(request, rawResponse);\n\t\t\t\tthis.trace('sendRequest', `Provider returned response for session ${model.sessionId}`);\n\n\t\t\t\t// TODO refactor this or rethink the API https://github.com/microsoft/vscode-copilot/issues/593\n\t\t\t\tif (provider.provideFollowups) {\n\t\t\t\t\tPromise.resolve(provider.provideFollowups(model.session!, CancellationToken.None)).then(followups => {\n\t\t\t\t\t\tmodel.setFollowups(request, withNullAsUndefined(followups));\n\t\t\t\t\t\tmodel.completeResponse(request);\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tmodel.completeResponse(request);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tthis._pendingRequests.set(model.sessionId, rawResponsePromise);\n\t\trawResponsePromise.finally(() => {\n\t\t\tthis._pendingRequests.delete(model.sessionId);\n\t\t});\n\t\treturn rawResponsePromise;\n\t}\n}\n", + "fileName": "./2.tst" + }, + "diffs": [ + { + "originalRange": "[50,51)", + "modifiedRange": "[50,71)", + "innerChanges": null + } + ] +} \ No newline at end of file diff --git a/code/src/vs/editor/test/node/diffing/fixtures/ts-class/advanced.expected.diff.json b/code/src/vs/editor/test/node/diffing/fixtures/ts-class/advanced.expected.diff.json index 85e7beaa072..92f5e20707f 100644 --- a/code/src/vs/editor/test/node/diffing/fixtures/ts-class/advanced.expected.diff.json +++ b/code/src/vs/editor/test/node/diffing/fixtures/ts-class/advanced.expected.diff.json @@ -47,20 +47,8 @@ "modifiedRange": "[8,12)", "innerChanges": [ { - "originalRange": "[11,10 -> 11,14]", - "modifiedRange": "[8,10 -> 8,11]" - }, - { - "originalRange": "[12,4 -> 13,52]", - "modifiedRange": "[9,4 -> 9,12]" - }, - { - "originalRange": "[13,55 -> 19,67]", - "modifiedRange": "[9,15 -> 9,61]" - }, - { - "originalRange": "[20,17 -> 20,25]", - "modifiedRange": "[10,17 -> 11,51]" + "originalRange": "[11,10 -> 20,25]", + "modifiedRange": "[8,10 -> 11,51]" } ] }, diff --git a/code/src/vs/editor/test/node/diffing/fixtures/ts-confusing-2/advanced.expected.diff.json b/code/src/vs/editor/test/node/diffing/fixtures/ts-confusing-2/advanced.expected.diff.json index 07656313cdc..f639ee6bed4 100644 --- a/code/src/vs/editor/test/node/diffing/fixtures/ts-confusing-2/advanced.expected.diff.json +++ b/code/src/vs/editor/test/node/diffing/fixtures/ts-confusing-2/advanced.expected.diff.json @@ -43,12 +43,8 @@ "modifiedRange": "[14,18)", "innerChanges": [ { - "originalRange": "[14,120 -> 14,154]", - "modifiedRange": "[14,120 -> 14,162]" - }, - { - "originalRange": "[14,165 -> 14,211]", - "modifiedRange": "[14,173 -> 17,1]" + "originalRange": "[14,120 -> 14,211]", + "modifiedRange": "[14,120 -> 17,1]" } ] }, @@ -65,12 +61,8 @@ "modifiedRange": "[21,8 -> 21,43]" }, { - "originalRange": "[17,71 -> 17,72]", - "modifiedRange": "[21,74 -> 22,1]" - }, - { - "originalRange": "[18,3 -> 23,4]", - "modifiedRange": "[23,3 -> 23,120]" + "originalRange": "[17,71 -> 23,4]", + "modifiedRange": "[21,74 -> 23,120]" } ] }, @@ -89,20 +81,8 @@ "modifiedRange": "[26,38)", "innerChanges": [ { - "originalRange": "[28,1 -> 28,1]", - "modifiedRange": "[26,1 -> 27,1]" - }, - { - "originalRange": "[28,15 -> 28,20]", - "modifiedRange": "[27,15 -> 27,16]" - }, - { - "originalRange": "[29,4 -> 29,24]", - "modifiedRange": "[28,4 -> 29,36]" - }, - { - "originalRange": "[30,4 -> 30,31]", - "modifiedRange": "[30,4 -> 37,9]" + "originalRange": "[28,1 -> 30,31]", + "modifiedRange": "[26,1 -> 37,9]" } ] } diff --git a/code/src/vs/editor/test/node/diffing/fixtures/ts-example1/advanced.expected.diff.json b/code/src/vs/editor/test/node/diffing/fixtures/ts-example1/advanced.expected.diff.json index cc5f77b5dc8..da419b4a59f 100644 --- a/code/src/vs/editor/test/node/diffing/fixtures/ts-example1/advanced.expected.diff.json +++ b/code/src/vs/editor/test/node/diffing/fixtures/ts-example1/advanced.expected.diff.json @@ -31,16 +31,8 @@ "modifiedRange": "[17,80)", "innerChanges": [ { - "originalRange": "[13,10 -> 13,41]", - "modifiedRange": "[17,10 -> 35,18]" - }, - { - "originalRange": "[14,2 -> 14,8]", - "modifiedRange": "[36,2 -> 39,8]" - }, - { - "originalRange": "[14,13 -> 14,43]", - "modifiedRange": "[39,13 -> 79,2]" + "originalRange": "[13,10 -> 14,43]", + "modifiedRange": "[17,10 -> 79,2]" } ] } diff --git a/code/src/vs/editor/test/node/diffing/fixtures/ws-alignment/advanced.expected.diff.json b/code/src/vs/editor/test/node/diffing/fixtures/ws-alignment/advanced.expected.diff.json index 92c6e475761..85a9a2fe928 100644 --- a/code/src/vs/editor/test/node/diffing/fixtures/ws-alignment/advanced.expected.diff.json +++ b/code/src/vs/editor/test/node/diffing/fixtures/ws-alignment/advanced.expected.diff.json @@ -23,28 +23,8 @@ "modifiedRange": "[7,18)", "innerChanges": [ { - "originalRange": "[7,5 -> 7,43]", - "modifiedRange": "[7,5 -> 11,140]" - }, - { - "originalRange": "[8,5 -> 9,12]", - "modifiedRange": "[12,5 -> 12,131]" - }, - { - "originalRange": "[10,7 -> 10,22]", - "modifiedRange": "[13,7 -> 13,17]" - }, - { - "originalRange": "[10,30 -> 10,48]", - "modifiedRange": "[13,25 -> 13,118]" - }, - { - "originalRange": "[11,6 -> 11,13]", - "modifiedRange": "[14,6 -> 14,8]" - }, - { - "originalRange": "[12,5 -> 12,17]", - "modifiedRange": "[15,5 -> 16,7]" + "originalRange": "[7,5 -> 12,17]", + "modifiedRange": "[7,5 -> 16,7]" }, { "originalRange": "[13,6 -> 13,11]", @@ -53,4 +33,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/code/src/vs/monaco.d.ts b/code/src/vs/monaco.d.ts index 770c1268f3a..dbcada860e6 100644 --- a/code/src/vs/monaco.d.ts +++ b/code/src/vs/monaco.d.ts @@ -2480,6 +2480,7 @@ declare namespace monaco.editor { toInclusiveRange(): Range | null; toExclusiveRange(): Range; mapToLineArray(f: (lineNumber: number) => T): T[]; + forEach(f: (lineNumber: number) => void): void; includes(lineNumber: number): boolean; } @@ -2539,9 +2540,9 @@ declare namespace monaco.editor { } export class SimpleLineRangeMapping { - readonly originalRange: LineRange; - readonly modifiedRange: LineRange; - constructor(originalRange: LineRange, modifiedRange: LineRange); + readonly original: LineRange; + readonly modified: LineRange; + constructor(original: LineRange, modified: LineRange); toString(): string; flip(): SimpleLineRangeMapping; } diff --git a/code/src/vs/platform/audioCues/browser/audioCueService.ts b/code/src/vs/platform/audioCues/browser/audioCueService.ts index ca6756406e1..b746426dac3 100644 --- a/code/src/vs/platform/audioCues/browser/audioCueService.ts +++ b/code/src/vs/platform/audioCues/browser/audioCueService.ts @@ -23,7 +23,6 @@ export interface IAudioCueService { playSound(cue: Sound, allowManyInParallel?: boolean): Promise; playAudioCueLoop(cue: AudioCue, milliseconds: number): IDisposable; - playRandomAudioCue(groupId: AudioCueGroupId, allowManyInParallel?: boolean): void; } export class AudioCueService extends Disposable implements IAudioCueService { @@ -43,25 +42,16 @@ export class AudioCueService extends Disposable implements IAudioCueService { public async playAudioCue(cue: AudioCue, allowManyInParallel = false): Promise { if (this.isEnabled(cue)) { - await this.playSound(cue.sound, allowManyInParallel); + await this.playSound(cue.sound.getSound(), allowManyInParallel); } } public async playAudioCues(cues: AudioCue[]): Promise { // Some audio cues might reuse sounds. Don't play the same sound twice. - const sounds = new Set(cues.filter(cue => this.isEnabled(cue)).map(cue => cue.sound)); + const sounds = new Set(cues.filter(cue => this.isEnabled(cue)).map(cue => cue.sound.getSound())); await Promise.all(Array.from(sounds).map(sound => this.playSound(sound, true))); } - /** - * Gaming and other apps often play a sound variant when the same event happens again - * for an improved experience. This function plays a random sound from the given group to accomplish that. - */ - public playRandomAudioCue(groupId: AudioCueGroupId, allowManyInParallel?: boolean): void { - const cues = AudioCue.allAudioCues.filter(cue => cue.groupId === groupId); - const index = Math.floor(Math.random() * cues.length); - this.playAudioCue(cues[index], allowManyInParallel); - } private getVolumeInPercent(): number { const volume = this.configurationService.getValue('audioCues.volume'); @@ -206,7 +196,6 @@ export class Sound { return sound; } - public static readonly error = Sound.register({ fileName: 'error.mp3' }); public static readonly warning = Sound.register({ fileName: 'warning.mp3' }); public static readonly foldedArea = Sound.register({ fileName: 'foldedAreas.mp3' }); @@ -228,19 +217,36 @@ export class Sound { private constructor(public readonly fileName: string) { } } -export const enum AudioCueGroupId { - chatResponseReceived = 'chatResponseReceived' +export class SoundSource { + constructor( + public readonly randomOneOf: Sound[] + ) { } + + public getSound(deterministic = false): Sound { + if (deterministic || this.randomOneOf.length === 1) { + return this.randomOneOf[0]; + } else { + const index = Math.floor(Math.random() * this.randomOneOf.length); + return this.randomOneOf[index]; + } + } } export class AudioCue { private static _audioCues = new Set(); private static register(options: { name: string; - sound: Sound; + sound: Sound | { + /** + * Gaming and other apps often play a sound variant when the same event happens again + * for an improved experience. This option enables audio cues to play a random sound. + */ + randomOneOf: Sound[]; + }; settingsKey: string; - groupId?: AudioCueGroupId; }): AudioCue { - const audioCue = new AudioCue(options.sound, options.name, options.settingsKey, options.groupId); + const soundSource = new SoundSource('randomOneOf' in options.sound ? options.sound.randomOneOf : [options.sound]); + const audioCue = new AudioCue(soundSource, options.name, options.settingsKey); AudioCue._audioCues.add(audioCue); return audioCue; } @@ -353,30 +359,17 @@ export class AudioCue { settingsKey: 'audioCues.chatRequestSent' }); - private static readonly chatResponseReceived = { + public static readonly chatResponseReceived = AudioCue.register({ name: localize('audioCues.chatResponseReceived', 'Chat Response Received'), settingsKey: 'audioCues.chatResponseReceived', - groupId: AudioCueGroupId.chatResponseReceived - }; - - public static readonly chatResponseReceived1 = AudioCue.register({ - sound: Sound.chatResponseReceived1, - ...this.chatResponseReceived - }); - - public static readonly chatResponseReceived2 = AudioCue.register({ - sound: Sound.chatResponseReceived2, - ...this.chatResponseReceived - }); - - public static readonly chatResponseReceived3 = AudioCue.register({ - sound: Sound.chatResponseReceived3, - ...this.chatResponseReceived - }); - - public static readonly chatResponseReceived4 = AudioCue.register({ - sound: Sound.chatResponseReceived4, - ...this.chatResponseReceived + sound: { + randomOneOf: [ + Sound.chatResponseReceived1, + Sound.chatResponseReceived2, + Sound.chatResponseReceived3, + Sound.chatResponseReceived4 + ] + } }); public static readonly chatResponsePending = AudioCue.register({ @@ -386,9 +379,8 @@ export class AudioCue { }); private constructor( - public readonly sound: Sound, + public readonly sound: SoundSource, public readonly name: string, public readonly settingsKey: string, - public readonly groupId?: string ) { } } diff --git a/code/src/vs/platform/list/browser/listService.ts b/code/src/vs/platform/list/browser/listService.ts index 69f70223ba9..7c5a1eb9cd6 100644 --- a/code/src/vs/platform/list/browser/listService.ts +++ b/code/src/vs/platform/list/browser/listService.ts @@ -113,6 +113,14 @@ export class ListService implements IListService { } } +export const RawWorkbenchListScrollAtBoundaryContextKey = new RawContextKey<'none' | 'top' | 'bottom' | 'both'>('listScrollAtBoundary', 'none'); +export const WorkbenchListScrollAtTopContextKey = ContextKeyExpr.or( + RawWorkbenchListScrollAtBoundaryContextKey.isEqualTo('top'), + RawWorkbenchListScrollAtBoundaryContextKey.isEqualTo('both')); +export const WorkbenchListScrollAtBottomContextKey = ContextKeyExpr.or( + RawWorkbenchListScrollAtBoundaryContextKey.isEqualTo('bottom'), + RawWorkbenchListScrollAtBoundaryContextKey.isEqualTo('both')); + export const RawWorkbenchListFocusContextKey = new RawContextKey('listFocus', true); export const WorkbenchListSupportsMultiSelectContextKey = new RawContextKey('listSupportsMultiselect', true); export const WorkbenchListFocusContextKey = ContextKeyExpr.and(RawWorkbenchListFocusContextKey, ContextKeyExpr.not(InputFocusedContextKey)); @@ -139,6 +147,33 @@ function createScopedContextKeyService(contextKeyService: IContextKeyService, wi return result; } +// Note: We must declare IScrollObservarable as the arithmetic of concrete classes, +// instead of object type like { onDidScroll: Event; ... }. The latter will not mark +// those properties as referenced during tree-shaking, causing them to be shaked away. +type IScrollObservarable = Exclude> | List; + +function createScrollObserver(contextKeyService: IContextKeyService, widget: IScrollObservarable): IDisposable { + const listScrollAt = RawWorkbenchListScrollAtBoundaryContextKey.bindTo(contextKeyService); + const update = () => { + const atTop = widget.scrollTop === 0; + + // We need a threshold `1` since scrollHeight is rounded. + // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled + const atBottom = widget.scrollHeight - widget.renderHeight - widget.scrollTop < 1; + if (atTop && atBottom) { + listScrollAt.set('both'); + } else if (atTop) { + listScrollAt.set('top'); + } else if (atBottom) { + listScrollAt.set('bottom'); + } else { + listScrollAt.set('none'); + } + }; + update(); + return widget.onDidScroll(update); +} + const multiSelectModifierSettingKey = 'workbench.list.multiSelectModifier'; const openModeSettingKey = 'workbench.list.openMode'; const horizontalScrollingKey = 'workbench.list.horizontalScrolling'; @@ -259,6 +294,8 @@ export class WorkbenchList extends List { this.contextKeyService = createScopedContextKeyService(contextKeyService, this); + this.disposables.add(createScrollObserver(this.contextKeyService, this)); + this.listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService); this.listSupportsMultiSelect.set(options.multipleSelectionSupport !== false); @@ -390,6 +427,8 @@ export class WorkbenchPagedList extends PagedList { this.contextKeyService = createScopedContextKeyService(contextKeyService, this); + this.disposables.add(createScrollObserver(this.contextKeyService, this.widget)); + this.horizontalScrolling = options.horizontalScrolling; this.listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService); @@ -514,6 +553,8 @@ export class WorkbenchTable extends Table { this.contextKeyService = createScopedContextKeyService(contextKeyService, this); + this.disposables.add(createScrollObserver(this.contextKeyService, this)); + this.listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService); this.listSupportsMultiSelect.set(options.multipleSelectionSupport !== false); @@ -1165,6 +1206,8 @@ class WorkbenchTreeInternals { ) { this.contextKeyService = createScopedContextKeyService(contextKeyService, tree); + this.disposables.push(createScrollObserver(this.contextKeyService, tree)); + this.listSupportsMultiSelect = WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService); this.listSupportsMultiSelect.set(options.multipleSelectionSupport !== false); diff --git a/code/src/vs/platform/telemetry/common/1dsAppender.ts b/code/src/vs/platform/telemetry/common/1dsAppender.ts index ccfbc3f7d9b..02295cc4968 100644 --- a/code/src/vs/platform/telemetry/common/1dsAppender.ts +++ b/code/src/vs/platform/telemetry/common/1dsAppender.ts @@ -52,14 +52,14 @@ async function getClient(instrumentationKey: string, addInternalFlag?: boolean, appInsightsCore.initialize(coreConfig, []); - appInsightsCore.addTelemetryInitializer((envelope) => { - if (addInternalFlag) { - envelope['ext'] = envelope['ext'] ?? {}; - envelope['ext']['utc'] = envelope['ext']['utc'] ?? {}; - // Sets it to be internal only based on Windows UTC flagging - envelope['ext']['utc']['flags'] = 0x0000811ECD; - } - }); + // appInsightsCore.addTelemetryInitializer((envelope) => { + // if (addInternalFlag) { + // envelope['ext'] = envelope['ext'] ?? {}; + // envelope['ext']['utc'] = envelope['ext']['utc'] ?? {}; + // // Sets it to be internal only based on Windows UTC flagging + // envelope['ext']['utc']['flags'] = 0x0000811ECD; + // } + // }); return appInsightsCore; } diff --git a/code/src/vs/workbench/api/browser/mainThreadTesting.ts b/code/src/vs/workbench/api/browser/mainThreadTesting.ts index d9ea8c1e677..d4858ccb417 100644 --- a/code/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/code/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -18,6 +18,8 @@ import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResu import { IMainThreadTestController, ITestRootProvider, ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { ExtHostContext, ExtHostTestingShape, ILocationDto, ITestControllerPatch, MainContext, MainThreadTestingShape } from '../common/extHost.protocol'; +import { WellDefinedPrefixTree } from 'vs/base/common/prefixTree'; +import { TestId } from 'vs/workbench/contrib/testing/common/testId'; @extHostNamedCustomer(MainContext.MainThreadTesting) export class MainThreadTesting extends Disposable implements MainThreadTestingShape, ITestRootProvider { @@ -55,11 +57,19 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh /** * @inheritdoc */ - $markTestRetired(testId: string): void { + $markTestRetired(testIds: string[] | undefined): void { + let tree: WellDefinedPrefixTree | undefined; + if (testIds) { + tree = new WellDefinedPrefixTree(); + for (const id of testIds) { + tree.insert(TestId.fromString(id).path, undefined); + } + } + for (const result of this.resultService.results) { // all non-live results are already entirely outdated if (result instanceof LiveTestResult) { - result.markRetired(testId); + result.markRetired(tree); } } } diff --git a/code/src/vs/workbench/api/common/extHost.protocol.ts b/code/src/vs/workbench/api/common/extHost.protocol.ts index 0dc33e49a04..a9dabecd9de 100644 --- a/code/src/vs/workbench/api/common/extHost.protocol.ts +++ b/code/src/vs/workbench/api/common/extHost.protocol.ts @@ -2514,7 +2514,7 @@ export interface MainThreadTestingShape { /** Signals that an extension-provided test run finished. */ $finishedExtensionTestRun(runId: string): void; /** Marks a test (or controller) as retired in all results. */ - $markTestRetired(testId: string): void; + $markTestRetired(testIds: string[] | undefined): void; } // --- proxy identifiers diff --git a/code/src/vs/workbench/api/common/extHostDiagnostics.ts b/code/src/vs/workbench/api/common/extHostDiagnostics.ts index 91f90bc5f91..e23ce395cc8 100644 --- a/code/src/vs/workbench/api/common/extHostDiagnostics.ts +++ b/code/src/vs/workbench/api/common/extHostDiagnostics.ts @@ -31,12 +31,14 @@ export class DiagnosticCollection implements vscode.DiagnosticCollection { constructor( private readonly _name: string, private readonly _owner: string, + private readonly _maxDiagnosticsTotal: number, private readonly _maxDiagnosticsPerFile: number, private readonly _modelVersionIdProvider: (uri: URI) => number | undefined, extUri: IExtUri, proxy: MainThreadDiagnosticsShape | undefined, onDidChangeDiagnostics: Emitter ) { + this._maxDiagnosticsTotal = Math.max(_maxDiagnosticsPerFile, _maxDiagnosticsTotal); this.#data = new ResourceMap(uri => extUri.getComparisonKey(uri)); this.#proxy = proxy; this.#onDidChangeDiagnostics = onDidChangeDiagnostics; @@ -123,6 +125,7 @@ export class DiagnosticCollection implements vscode.DiagnosticCollection { return; } const entries: [URI, IMarkerData[]][] = []; + let totalMarkerCount = 0; for (const uri of toSync) { let marker: IMarkerData[] = []; const diagnostics = this.#data.get(uri); @@ -158,6 +161,12 @@ export class DiagnosticCollection implements vscode.DiagnosticCollection { } entries.push([uri, marker]); + + totalMarkerCount += marker.length; + if (totalMarkerCount > this._maxDiagnosticsTotal) { + // ignore markers that are above the limit + break; + } } this.#proxy.$changeMany(this._owner, entries); } @@ -225,6 +234,7 @@ export class ExtHostDiagnostics implements ExtHostDiagnosticsShape { private static _idPool: number = 0; private static readonly _maxDiagnosticsPerFile: number = 1000; + private static readonly _maxDiagnosticsTotal: number = 1.1 * ExtHostDiagnostics._maxDiagnosticsPerFile; private readonly _proxy: MainThreadDiagnosticsShape; private readonly _collections = new Map(); @@ -284,7 +294,9 @@ export class ExtHostDiagnostics implements ExtHostDiagnosticsShape { const result = new class extends DiagnosticCollection { constructor() { super( - name!, owner, ExtHostDiagnostics._maxDiagnosticsPerFile, + name!, owner, + ExtHostDiagnostics._maxDiagnosticsTotal, + ExtHostDiagnostics._maxDiagnosticsPerFile, uri => _extHostDocumentsAndEditors.getDocument(uri)?.version, _fileSystemInfoService.extUri, loggingProxy, _onDidChangeDiagnostics ); @@ -339,7 +351,12 @@ export class ExtHostDiagnostics implements ExtHostDiagnosticsShape { if (!this._mirrorCollection) { const name = '_generated_mirror'; - const collection = new DiagnosticCollection(name, name, ExtHostDiagnostics._maxDiagnosticsPerFile, _uri => undefined, this._fileSystemInfoService.extUri, undefined, this._onDidChangeDiagnostics); + const collection = new DiagnosticCollection( + name, name, + Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, // no limits because this collection is just a mirror of "sanitized" data + _uri => undefined, + this._fileSystemInfoService.extUri, undefined, this._onDidChangeDiagnostics + ); this._collections.set(name, collection); this._mirrorCollection = collection; } diff --git a/code/src/vs/workbench/api/common/extHostExtensionService.ts b/code/src/vs/workbench/api/common/extHostExtensionService.ts index 82618146832..0c35ea254f3 100644 --- a/code/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/code/src/vs/workbench/api/common/extHostExtensionService.ts @@ -966,7 +966,8 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme public $activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise { if (activationKind === ActivationKind.Immediate) { - return this._activateByEvent(activationEvent, false); + return this._almostReadyToRunExtensions.wait() + .then(_ => this._activateByEvent(activationEvent, false)); } return ( diff --git a/code/src/vs/workbench/api/common/extHostInlineChat.ts b/code/src/vs/workbench/api/common/extHostInlineChat.ts index 3e706f12286..866af7a087d 100644 --- a/code/src/vs/workbench/api/common/extHostInlineChat.ts +++ b/code/src/vs/workbench/api/common/extHostInlineChat.ts @@ -123,7 +123,7 @@ export class ExtHostInteractiveEditor implements ExtHostInlineChatShape { return { id, placeholder: session.placeholder, - slashCommands: session.slashCommands?.map(c => ({ command: c.command, detail: c.detail, refer: c.refer })), + slashCommands: session.slashCommands?.map(c => ({ command: c.command, detail: c.detail, refer: c.refer, executeImmediately: c.executeImmediately })), wholeRange: typeConvert.Range.from(session.wholeRange), message: session.message }; diff --git a/code/src/vs/workbench/api/common/extHostLanguages.ts b/code/src/vs/workbench/api/common/extHostLanguages.ts index a0d1bbb5a77..c56e3340760 100644 --- a/code/src/vs/workbench/api/common/extHostLanguages.ts +++ b/code/src/vs/workbench/api/common/extHostLanguages.ts @@ -101,10 +101,17 @@ export class ExtHostLanguages implements ExtHostLanguagesShape { busy: false }; + let soonHandle: IDisposable | undefined; const commandDisposables = new DisposableStore(); const updateAsync = () => { soonHandle?.dispose(); + + if (!ids.has(fullyQualifiedId)) { + console.warn(`LanguageStatusItem (${id}) from ${extension.identifier.value} has been disposed and CANNOT be updated anymore`); + return; // disposed in the meantime + } + soonHandle = disposableTimeout(() => { commandDisposables.clear(); this._proxy.$setLanguageStatus(handle, { diff --git a/code/src/vs/workbench/api/common/extHostNotebookDocument.ts b/code/src/vs/workbench/api/common/extHostNotebookDocument.ts index 6f48147c9e4..e8970f70dee 100644 --- a/code/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/code/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -132,7 +132,7 @@ export class ExtHostCell { const compressed = notebookCommon.compressOutputItemStreams(mimeOutputs.get(mime)!); output.items.push({ mime, - data: compressed.buffer + data: compressed.data.buffer }); }); } diff --git a/code/src/vs/workbench/api/common/extHostQuickOpen.ts b/code/src/vs/workbench/api/common/extHostQuickOpen.ts index ab76d1881ba..edc8a951ffc 100644 --- a/code/src/vs/workbench/api/common/extHostQuickOpen.ts +++ b/code/src/vs/workbench/api/common/extHostQuickOpen.ts @@ -86,7 +86,6 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx } const allowedTooltips = isProposedApiEnabled(extension, 'quickPickItemTooltip'); - const allowedIcons = isProposedApiEnabled(extension, 'quickPickItemIcon'); return itemsPromise.then(items => { @@ -101,10 +100,8 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx if (item.tooltip && !allowedTooltips) { console.warn(`Extension '${extension.identifier.value}' uses a tooltip which is proposed API that is only available when running out of dev or with the following command line switch: --enable-proposed-api ${extension.identifier.value}`); } - if (item.iconPath && !allowedIcons) { - console.warn(`Extension '${extension.identifier.value}' uses an icon which is proposed API that is only available when running out of dev or with the following command line switch: --enable-proposed-api ${extension.identifier.value}`); - } - const icon = (item.iconPath && allowedIcons) ? getIconPathOrClass(item.iconPath) : undefined; + + const icon = (item.iconPath) ? getIconPathOrClass(item.iconPath) : undefined; pickItems.push({ label: item.label, iconPath: icon?.iconPath, @@ -573,7 +570,6 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx }); const allowedTooltips = isProposedApiEnabled(this.extension, 'quickPickItemTooltip'); - const allowedIcons = isProposedApiEnabled(this.extension, 'quickPickItemIcon'); const pickItems: TransferQuickPickItemOrSeparator[] = []; for (let handle = 0; handle < items.length; handle++) { @@ -584,10 +580,8 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx if (item.tooltip && !allowedTooltips) { console.warn(`Extension '${this.extension.identifier.value}' uses a tooltip which is proposed API that is only available when running out of dev or with the following command line switch: --enable-proposed-api ${this.extension.identifier.value}`); } - if (item.iconPath && !allowedIcons) { - console.warn(`Extension '${this.extension.identifier.value}' uses an icon which is proposed API that is only available when running out of dev or with the following command line switch: --enable-proposed-api ${this.extension.identifier.value}`); - } - const icon = (item.iconPath && allowedIcons) ? getIconPathOrClass(item.iconPath) : undefined; + + const icon = (item.iconPath) ? getIconPathOrClass(item.iconPath) : undefined; pickItems.push({ handle, label: item.label, diff --git a/code/src/vs/workbench/api/common/extHostTesting.ts b/code/src/vs/workbench/api/common/extHostTesting.ts index 147394051c6..4227806cb8d 100644 --- a/code/src/vs/workbench/api/common/extHostTesting.ts +++ b/code/src/vs/workbench/api/common/extHostTesting.ts @@ -29,7 +29,6 @@ import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants'; import { TestId, TestIdPathParts, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; import { InvalidTestItemError } from 'vs/workbench/contrib/testing/common/testItemCollection'; import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestItem, ITestItemContext, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes'; -import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import type * as vscode from 'vscode'; interface ControllerInfo { @@ -141,10 +140,11 @@ export class ExtHostTesting implements ExtHostTestingShape { return this.runTracker.createTestRun(controllerId, collection, request, name, persist); }, invalidateTestResults: items => { - checkProposedApiEnabled(extension, 'testInvalidateResults'); - for (const item of items instanceof Array ? items : [items]) { - const id = item ? TestId.fromExtHostTestItem(item, controllerId).toString() : controllerId; - this.proxy.$markTestRetired(id); + if (items === undefined) { + this.proxy.$markTestRetired(undefined); + } else { + const itemsArr = items instanceof Array ? items : [items]; + this.proxy.$markTestRetired(itemsArr.map(i => TestId.fromExtHostTestItem(i!, controllerId).toString())); } }, set resolveHandler(fn) { diff --git a/code/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts b/code/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts index da1f8de0240..b6bf3ee4406 100644 --- a/code/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts +++ b/code/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts @@ -40,7 +40,7 @@ suite('ExtHostDiagnostics', () => { test('disposeCheck', () => { - const collection = new DiagnosticCollection('test', 'test', 100, versionProvider, extUri, new DiagnosticsShape(), new Emitter()); + const collection = new DiagnosticCollection('test', 'test', 100, 100, versionProvider, extUri, new DiagnosticsShape(), new Emitter()); collection.dispose(); collection.dispose(); // that's OK @@ -56,13 +56,13 @@ suite('ExtHostDiagnostics', () => { test('diagnostic collection, forEach, clear, has', function () { - let collection = new DiagnosticCollection('test', 'test', 100, versionProvider, extUri, new DiagnosticsShape(), new Emitter()); + let collection = new DiagnosticCollection('test', 'test', 100, 100, versionProvider, extUri, new DiagnosticsShape(), new Emitter()); assert.strictEqual(collection.name, 'test'); collection.dispose(); assert.throws(() => collection.name); let c = 0; - collection = new DiagnosticCollection('test', 'test', 100, versionProvider, extUri, new DiagnosticsShape(), new Emitter()); + collection = new DiagnosticCollection('test', 'test', 100, 100, versionProvider, extUri, new DiagnosticsShape(), new Emitter()); collection.forEach(() => c++); assert.strictEqual(c, 0); @@ -99,7 +99,7 @@ suite('ExtHostDiagnostics', () => { }); test('diagnostic collection, immutable read', function () { - const collection = new DiagnosticCollection('test', 'test', 100, versionProvider, extUri, new DiagnosticsShape(), new Emitter()); + const collection = new DiagnosticCollection('test', 'test', 100, 100, versionProvider, extUri, new DiagnosticsShape(), new Emitter()); collection.set(URI.parse('foo:bar'), [ new Diagnostic(new Range(0, 0, 1, 1), 'message-1'), new Diagnostic(new Range(0, 0, 1, 1), 'message-2') @@ -124,7 +124,7 @@ suite('ExtHostDiagnostics', () => { test('diagnostics collection, set with dupliclated tuples', function () { - const collection = new DiagnosticCollection('test', 'test', 100, versionProvider, extUri, new DiagnosticsShape(), new Emitter()); + const collection = new DiagnosticCollection('test', 'test', 100, 100, versionProvider, extUri, new DiagnosticsShape(), new Emitter()); const uri = URI.parse('sc:hightower'); collection.set([ [uri, [new Diagnostic(new Range(0, 0, 0, 1), 'message-1')]], @@ -175,7 +175,7 @@ suite('ExtHostDiagnostics', () => { test('diagnostics collection, set tuple overrides, #11547', function () { let lastEntries!: [UriComponents, IMarkerData[]][]; - const collection = new DiagnosticCollection('test', 'test', 100, versionProvider, extUri, new class extends DiagnosticsShape { + const collection = new DiagnosticCollection('test', 'test', 100, 100, versionProvider, extUri, new class extends DiagnosticsShape { override $changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]): void { lastEntries = entries; return super.$changeMany(owner, entries); @@ -209,7 +209,7 @@ suite('ExtHostDiagnostics', () => { const emitter = new Emitter(); emitter.event(_ => eventCount += 1); - const collection = new DiagnosticCollection('test', 'test', 100, versionProvider, extUri, new class extends DiagnosticsShape { + const collection = new DiagnosticCollection('test', 'test', 100, 100, versionProvider, extUri, new class extends DiagnosticsShape { override $changeMany() { changeCount += 1; } @@ -229,7 +229,7 @@ suite('ExtHostDiagnostics', () => { test('diagnostics collection, tuples and undefined (small array), #15585', function () { - const collection = new DiagnosticCollection('test', 'test', 100, versionProvider, extUri, new DiagnosticsShape(), new Emitter()); + const collection = new DiagnosticCollection('test', 'test', 100, 100, versionProvider, extUri, new DiagnosticsShape(), new Emitter()); const uri = URI.parse('sc:hightower'); const uri2 = URI.parse('sc:nomad'); const diag = new Diagnostic(new Range(0, 0, 0, 1), 'ffff'); @@ -250,7 +250,7 @@ suite('ExtHostDiagnostics', () => { test('diagnostics collection, tuples and undefined (large array), #15585', function () { - const collection = new DiagnosticCollection('test', 'test', 100, versionProvider, extUri, new DiagnosticsShape(), new Emitter()); + const collection = new DiagnosticCollection('test', 'test', 100, 100, versionProvider, extUri, new DiagnosticsShape(), new Emitter()); const tuples: [URI, Diagnostic[]][] = []; for (let i = 0; i < 500; i++) { @@ -271,10 +271,10 @@ suite('ExtHostDiagnostics', () => { } }); - test('diagnostic capping', function () { + test('diagnostic capping (max per file)', function () { let lastEntries!: [UriComponents, IMarkerData[]][]; - const collection = new DiagnosticCollection('test', 'test', 250, versionProvider, extUri, new class extends DiagnosticsShape { + const collection = new DiagnosticCollection('test', 'test', 100, 250, versionProvider, extUri, new class extends DiagnosticsShape { override $changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]): void { lastEntries = entries; return super.$changeMany(owner, entries); @@ -298,9 +298,31 @@ suite('ExtHostDiagnostics', () => { assert.strictEqual(lastEntries[0][1][250].severity, MarkerSeverity.Info); }); + test('diagnostic capping (max files)', function () { + + let lastEntries!: [UriComponents, IMarkerData[]][]; + const collection = new DiagnosticCollection('test', 'test', 2, 1, versionProvider, extUri, new class extends DiagnosticsShape { + override $changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]): void { + lastEntries = entries; + return super.$changeMany(owner, entries); + } + }, new Emitter()); + + const diag = new Diagnostic(new Range(0, 0, 1, 1), 'Hello'); + + + collection.set([ + [URI.parse('aa:bb1'), [diag]], + [URI.parse('aa:bb2'), [diag]], + [URI.parse('aa:bb3'), [diag]], + [URI.parse('aa:bb4'), [diag]], + ]); + assert.strictEqual(lastEntries.length, 3); // goes above the limit and then stops + }); + test('diagnostic eventing', async function () { const emitter = new Emitter(); - const collection = new DiagnosticCollection('ddd', 'test', 100, versionProvider, extUri, new DiagnosticsShape(), emitter); + const collection = new DiagnosticCollection('ddd', 'test', 100, 100, versionProvider, extUri, new DiagnosticsShape(), emitter); const diag1 = new Diagnostic(new Range(1, 1, 2, 3), 'diag1'); const diag2 = new Diagnostic(new Range(1, 1, 2, 3), 'diag2'); @@ -338,7 +360,7 @@ suite('ExtHostDiagnostics', () => { test('vscode.languages.onDidChangeDiagnostics Does Not Provide Document URI #49582', async function () { const emitter = new Emitter(); - const collection = new DiagnosticCollection('ddd', 'test', 100, versionProvider, extUri, new DiagnosticsShape(), emitter); + const collection = new DiagnosticCollection('ddd', 'test', 100, 100, versionProvider, extUri, new DiagnosticsShape(), emitter); const diag1 = new Diagnostic(new Range(1, 1, 2, 3), 'diag1'); @@ -361,7 +383,7 @@ suite('ExtHostDiagnostics', () => { test('diagnostics with related information', function (done) { - const collection = new DiagnosticCollection('ddd', 'test', 100, versionProvider, extUri, new class extends DiagnosticsShape { + const collection = new DiagnosticCollection('ddd', 'test', 100, 100, versionProvider, extUri, new class extends DiagnosticsShape { override $changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]) { const [[, data]] = entries; @@ -424,7 +446,7 @@ suite('ExtHostDiagnostics', () => { test('Error updating diagnostics from extension #60394', function () { let callCount = 0; - const collection = new DiagnosticCollection('ddd', 'test', 100, versionProvider, extUri, new class extends DiagnosticsShape { + const collection = new DiagnosticCollection('ddd', 'test', 100, 100, versionProvider, extUri, new class extends DiagnosticsShape { override $changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]) { callCount += 1; } @@ -451,7 +473,7 @@ suite('ExtHostDiagnostics', () => { const all: [UriComponents, IMarkerData[]][] = []; - const collection = new DiagnosticCollection('ddd', 'test', 100, uri => { + const collection = new DiagnosticCollection('ddd', 'test', 100, 100, uri => { return 7; }, extUri, new class extends DiagnosticsShape { override $changeMany(_owner: string, entries: [UriComponents, IMarkerData[]][]) { diff --git a/code/src/vs/workbench/browser/actions/listCommands.ts b/code/src/vs/workbench/browser/actions/listCommands.ts index 2cb52e52204..e17f1463d54 100644 --- a/code/src/vs/workbench/browser/actions/listCommands.ts +++ b/code/src/vs/workbench/browser/actions/listCommands.ts @@ -7,7 +7,7 @@ import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { List } from 'vs/base/browser/ui/list/listWidget'; -import { WorkbenchListFocusContextKey, IListService, WorkbenchListSupportsMultiSelectContextKey, ListWidget, WorkbenchListHasSelectionOrFocus, getSelectionKeyboardEvent, WorkbenchListWidget, WorkbenchListSelectionNavigation, WorkbenchTreeElementCanCollapse, WorkbenchTreeElementHasParent, WorkbenchTreeElementHasChild, WorkbenchTreeElementCanExpand, RawWorkbenchListFocusContextKey, WorkbenchTreeFindOpen, WorkbenchListSupportsFind } from 'vs/platform/list/browser/listService'; +import { WorkbenchListFocusContextKey, IListService, WorkbenchListSupportsMultiSelectContextKey, ListWidget, WorkbenchListHasSelectionOrFocus, getSelectionKeyboardEvent, WorkbenchListWidget, WorkbenchListSelectionNavigation, WorkbenchTreeElementCanCollapse, WorkbenchTreeElementHasParent, WorkbenchTreeElementHasChild, WorkbenchTreeElementCanExpand, RawWorkbenchListFocusContextKey, WorkbenchTreeFindOpen, WorkbenchListSupportsFind, WorkbenchListScrollAtBottomContextKey, WorkbenchListScrollAtTopContextKey } from 'vs/platform/list/browser/listService'; import { PagedList } from 'vs/base/browser/ui/list/listPaging'; import { equals, range } from 'vs/base/common/arrays'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -692,7 +692,12 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.scrollUp', weight: KeybindingWeight.WorkbenchContrib, - when: WorkbenchListFocusContextKey, + // Since the default keybindings for list.scrollUp and widgetNavigation.focusPrevious + // are both Ctrl+UpArrow, we disable this command when the scrollbar is at + // top-most position. This will give chance for widgetNavigation.focusPrevious to execute + when: ContextKeyExpr.and( + WorkbenchListFocusContextKey, + WorkbenchListScrollAtTopContextKey?.negate()), primary: KeyMod.CtrlCmd | KeyCode.UpArrow, handler: accessor => { const focused = accessor.get(IListService).lastFocusedList; @@ -708,7 +713,10 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'list.scrollDown', weight: KeybindingWeight.WorkbenchContrib, - when: WorkbenchListFocusContextKey, + // same as above + when: ContextKeyExpr.and( + WorkbenchListFocusContextKey, + WorkbenchListScrollAtBottomContextKey?.negate()), primary: KeyMod.CtrlCmd | KeyCode.DownArrow, handler: accessor => { const focused = accessor.get(IListService).lastFocusedList; diff --git a/code/src/vs/workbench/browser/actions/widgetNavigationCommands.ts b/code/src/vs/workbench/browser/actions/widgetNavigationCommands.ts new file mode 100644 index 00000000000..e62c9af8d1f --- /dev/null +++ b/code/src/vs/workbench/browser/actions/widgetNavigationCommands.ts @@ -0,0 +1,149 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { WorkbenchListFocusContextKey, WorkbenchListScrollAtBottomContextKey, WorkbenchListScrollAtTopContextKey } from 'vs/platform/list/browser/listService'; +import { Event } from 'vs/base/common/event'; +import { combinedDisposable, toDisposable, IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; + +/** INavigableContainer represents a logical container composed of widgets that can + be navigated back and forth with key shortcuts */ +interface INavigableContainer { + /** + * The container may coomposed of multiple parts that share no DOM ancestor + * (e.g., the main body and filter box of MarkersView may be separated). + * To track the focus of container we must pass in focus/blur events of all parts + * as `focusNotifiers`. + * + * Each element of `focusNotifiers` notifies the focus/blur event for a part of + * the container. The container is considered focused if at least one part being + * focused, and blurred if all parts being blurred. + */ + readonly focusNotifiers: readonly IFocusNotifier[]; + focusPreviousWidget(): void; + focusNextWidget(): void; +} + +interface IFocusNotifier { + readonly onDidFocus: Event; + readonly onDidBlur: Event; +} + +function handleFocusEventsGroup(group: readonly IFocusNotifier[], handler: (isFocus: boolean) => void): IDisposable { + const focusedIndices = new Set(); + return combinedDisposable(...group.map((events, index) => combinedDisposable( + events.onDidFocus(() => { + if (!focusedIndices.size) { + handler(true); + } + focusedIndices.add(index); + }), + events.onDidBlur(() => { + focusedIndices.delete(index); + if (!focusedIndices.size) { + handler(false); + } + }), + ))); +} + +const NavigableContainerFocusedContextKey = new RawContextKey('navigableContainerFocused', false); + +class NavigableContainerManager implements IDisposable { + private static INSTANCE: NavigableContainerManager | undefined; + + private readonly containers = new Set(); + private lastContainer: INavigableContainer | undefined; + private focused: IContextKey; + + + constructor(@IContextKeyService contextKeyService: IContextKeyService) { + this.focused = NavigableContainerFocusedContextKey.bindTo(contextKeyService); + NavigableContainerManager.INSTANCE = this; + } + + dispose(): void { + this.containers.clear(); + this.focused.reset(); + NavigableContainerManager.INSTANCE = undefined; + } + + static register(container: INavigableContainer): IDisposable { + const instance = this.INSTANCE; + if (!instance) { + return Disposable.None; + } + instance.containers.add(container); + + return combinedDisposable( + handleFocusEventsGroup(container.focusNotifiers, (isFocus) => { + if (isFocus) { + instance.focused.set(true); + instance.lastContainer = container; + } else if (instance.lastContainer === container) { + instance.focused.set(false); + instance.lastContainer = undefined; + } + }), + toDisposable(() => { + instance.containers.delete(container); + if (instance.lastContainer === container) { + instance.focused.set(false); + instance.lastContainer = undefined; + } + }) + ); + } + + static getActive(): INavigableContainer | undefined { + return this.INSTANCE?.lastContainer; + } +} + +export function registerNavigableContainer(container: INavigableContainer): IDisposable { + return NavigableContainerManager.register(container); +} + +Registry.as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(NavigableContainerManager, LifecyclePhase.Starting); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'widgetNavigation.focusPrevious', + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and( + NavigableContainerFocusedContextKey, + ContextKeyExpr.or( + WorkbenchListFocusContextKey?.negate(), + WorkbenchListScrollAtTopContextKey, + ) + ), + primary: KeyMod.CtrlCmd | KeyCode.UpArrow, + handler: () => { + const activeContainer = NavigableContainerManager.getActive(); + activeContainer?.focusPreviousWidget(); + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'widgetNavigation.focusNext', + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and( + NavigableContainerFocusedContextKey, + ContextKeyExpr.or( + WorkbenchListFocusContextKey?.negate(), + WorkbenchListScrollAtBottomContextKey, + ) + ), + primary: KeyMod.CtrlCmd | KeyCode.DownArrow, + handler: () => { + const activeContainer = NavigableContainerManager.getActive(); + activeContainer?.focusNextWidget(); + } +}); diff --git a/code/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/code/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 19048f9ae60..784b56215fe 100644 --- a/code/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/code/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -255,7 +255,7 @@ export class TextDiffEditor extends AbstractTextEditor imp return true; } - return e.affectsConfiguration(resource, 'diffEditor'); + return e.affectsConfiguration(resource, 'diffEditor') || e.affectsConfiguration(resource, 'accessibility.verbosity.diffEditor'); } protected override computeConfiguration(configuration: IEditorConfiguration): ICodeEditorOptions { @@ -276,6 +276,9 @@ export class TextDiffEditor extends AbstractTextEditor imp Object.assign(editorConfiguration, diffEditorConfiguration); } + const verbose = configuration.accessibility?.verbosity?.diffEditor ?? false; + (editorConfiguration as IDiffEditorOptions).accessibilityVerbose = verbose; + return editorConfiguration; } diff --git a/code/src/vs/workbench/browser/parts/editor/textEditor.ts b/code/src/vs/workbench/browser/parts/editor/textEditor.ts index ca459e1d841..4b9db046d01 100644 --- a/code/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/code/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -33,6 +33,11 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; export interface IEditorConfiguration { editor: object; diffEditor: object; + accessibility?: { + verbosity?: { + diffEditor?: boolean; + }; + }; } /** diff --git a/code/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts b/code/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index ffe7592d9f1..71df63ebd3d 100644 --- a/code/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/code/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts @@ -61,23 +61,23 @@ export interface INotificationsToastController { hide(): void; } -export function registerNotificationCommands(center: INotificationsCenterController, toasts: INotificationsToastController, model: NotificationsModel): void { +export function getNotificationFromContext(listService: IListService, context?: unknown): INotificationViewItem | undefined { + if (isNotificationViewItem(context)) { + return context; + } - function getNotificationFromContext(listService: IListService, context?: unknown): INotificationViewItem | undefined { - if (isNotificationViewItem(context)) { - return context; + const list = listService.lastFocusedList; + if (list instanceof WorkbenchList) { + const focusedElement = list.getFocusedElements()[0]; + if (isNotificationViewItem(focusedElement)) { + return focusedElement; } + } - const list = listService.lastFocusedList; - if (list instanceof WorkbenchList) { - const focusedElement = list.getFocusedElements()[0]; - if (isNotificationViewItem(focusedElement)) { - return focusedElement; - } - } + return undefined; +} - return undefined; - } +export function registerNotificationCommands(center: INotificationsCenterController, toasts: INotificationsToastController, model: NotificationsModel): void { // Show Notifications Cneter CommandsRegistry.registerCommand(SHOW_NOTIFICATIONS_CENTER, () => { diff --git a/code/src/vs/workbench/browser/parts/views/media/paneviewlet.css b/code/src/vs/workbench/browser/parts/views/media/paneviewlet.css index 19dc50ab13a..3037583c587 100644 --- a/code/src/vs/workbench/browser/parts/views/media/paneviewlet.css +++ b/code/src/vs/workbench/browser/parts/views/media/paneviewlet.css @@ -15,7 +15,8 @@ position: relative; } -.monaco-pane-view .pane > .pane-header > .actions.show { +.monaco-pane-view .pane > .pane-header > .actions.show-always, +.monaco-pane-view .pane.expanded > .pane-header > .actions.show-expanded { display: initial; } diff --git a/code/src/vs/workbench/browser/parts/views/viewFilter.ts b/code/src/vs/workbench/browser/parts/views/viewFilter.ts index bbda6b1983a..9986e46f084 100644 --- a/code/src/vs/workbench/browser/parts/views/viewFilter.ts +++ b/code/src/vs/workbench/browser/parts/views/viewFilter.ts @@ -81,6 +81,10 @@ export class FilterWidget extends Widget { private moreFiltersActionViewItem: MoreFiltersActionViewItem | undefined; private isMoreFiltersChecked: boolean = false; + private focusTracker: DOM.IFocusTracker; + public get onDidFocus() { return this.focusTracker.onDidFocus; } + public get onDidBlur() { return this.focusTracker.onDidBlur; } + constructor( private readonly options: IFilterWidgetOptions, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -97,7 +101,7 @@ export class FilterWidget extends Widget { } this.element = DOM.$('.viewpane-filter'); - this.filterInputBox = this.createInput(this.element); + [this.filterInputBox, this.focusTracker] = this.createInput(this.element); const controlsContainer = DOM.append(this.element, DOM.$('.viewpane-filter-controls')); this.filterBadge = this.createBadge(controlsContainer); @@ -106,6 +110,10 @@ export class FilterWidget extends Widget { this.adjustInputBox(); } + hasFocus(): boolean { + return this.filterInputBox.hasFocus(); + } + focus(): void { this.filterInputBox.focus(); } @@ -145,7 +153,7 @@ export class FilterWidget extends Widget { } } - private createInput(container: HTMLElement): ContextScopedHistoryInputBox { + private createInput(container: HTMLElement): [ContextScopedHistoryInputBox, DOM.IFocusTracker] { const inputBox = this._register(this.instantiationService.createInstance(ContextScopedHistoryInputBox, container, this.contextViewService, { placeholder: this.options.placeholder, ariaLabel: this.options.ariaLabel, @@ -171,7 +179,7 @@ export class FilterWidget extends Widget { this._register(focusTracker.onDidBlur(() => this.focusContextKey!.set(false))); this._register(toDisposable(() => this.focusContextKey!.reset())); } - return inputBox; + return [inputBox, focusTracker]; } private createBadge(container: HTMLElement): HTMLElement { diff --git a/code/src/vs/workbench/browser/parts/views/viewPane.ts b/code/src/vs/workbench/browser/parts/views/viewPane.ts index 3414f9e428c..844c4bf5a96 100644 --- a/code/src/vs/workbench/browser/parts/views/viewPane.ts +++ b/code/src/vs/workbench/browser/parts/views/viewPane.ts @@ -47,11 +47,22 @@ import { BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { defaultButtonStyles, defaultProgressBarStyles } from 'vs/platform/theme/browser/defaultStyles'; +export enum ViewPaneShowActions { + /** Show the actions when the view is hovered. This is the default behavior. */ + Default, + + /** Always shows the actions when the view is expanded */ + WhenExpanded, + + /** Always shows the actions */ + Always, +} + export interface IViewPaneOptions extends IPaneOptions { - id: string; - showActionsAlways?: boolean; - titleMenuId?: MenuId; - donotForwardArgs?: boolean; + readonly id: string; + readonly showActions?: ViewPaneShowActions; + readonly titleMenuId?: MenuId; + readonly donotForwardArgs?: boolean; } export interface IFilterViewPaneOptions extends IViewPaneOptions { @@ -188,7 +199,7 @@ export abstract class ViewPane extends Pane implements IView { private progressIndicator!: IProgressIndicator; private toolbar?: WorkbenchToolBar; - private readonly showActionsAlways: boolean = false; + private readonly showActions: ViewPaneShowActions; private headerContainer?: HTMLElement; private titleContainer?: HTMLElement; private titleDescriptionContainer?: HTMLElement; @@ -219,7 +230,7 @@ export abstract class ViewPane extends Pane implements IView { this.id = options.id; this._title = options.title; this._titleDescription = options.titleDescription; - this.showActionsAlways = !!options.showActionsAlways; + this.showActions = options.showActions ?? ViewPaneShowActions.Default; this.scopedContextKeyService = this._register(contextKeyService.createScoped(this.element)); this.scopedContextKeyService.createKey('view', this.id); @@ -288,7 +299,8 @@ export abstract class ViewPane extends Pane implements IView { this.renderHeaderTitle(container, this.title); const actions = append(container, $('.actions')); - actions.classList.toggle('show', this.showActionsAlways); + actions.classList.toggle('show-always', this.showActions === ViewPaneShowActions.Always); + actions.classList.toggle('show-expanded', this.showActions === ViewPaneShowActions.WhenExpanded); this.toolbar = this.instantiationService.createInstance(WorkbenchToolBar, actions, { orientation: ActionsOrientation.HORIZONTAL, actionViewItemProvider: action => this.getActionViewItem(action), diff --git a/code/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts b/code/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts index e84811c0e05..63adda5f1fa 100644 --- a/code/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts +++ b/code/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts @@ -12,8 +12,7 @@ import { ToggleTabFocusModeAction } from 'vs/editor/contrib/toggleTabFocusMode/b import { localize } from 'vs/nls'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { AccessibilityHelpAction, AccessibleViewAction, registerAccessibilityConfiguration } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; -import { AccessibleViewService, AccessibleViewType, IAccessibleContentProvider, IAccessibleViewOptions, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { AccessibilityHelpAction, AccessibleViewAction, AccessibleViewNextAction, AccessibleViewPreviousAction, registerAccessibilityConfiguration } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; import * as strings from 'vs/base/common/strings'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; @@ -24,6 +23,10 @@ import { NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser import { ModesHoverController } from 'vs/editor/contrib/hover/browser/hover'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { getNotificationFromContext } from 'vs/workbench/browser/parts/notifications/notificationsCommands'; +import { IListService, WorkbenchList } from 'vs/platform/list/browser/listService'; +import { NotificationFocusedContext } from 'vs/workbench/common/contextkeys'; +import { IAccessibleViewService, AccessibleViewService, IAccessibleContentProvider, IAccessibleViewOptions, AccessibleViewType, accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; registerAccessibilityConfiguration(); registerSingleton(IAccessibleViewService, AccessibleViewService, InstantiationType.Delayed); @@ -145,3 +148,96 @@ class HoverAccessibleViewContribution extends Disposable { const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(HoverAccessibleViewContribution, LifecyclePhase.Eventually); + +class NotificationAccessibleViewContribution extends Disposable { + static ID: 'notificationAccessibleViewContribution'; + constructor() { + super(); + this._register(AccessibleViewAction.addImplementation(90, 'notifications', accessor => { + const accessibleViewService = accessor.get(IAccessibleViewService); + const listService = accessor.get(IListService); + const commandService = accessor.get(ICommandService); + + function renderAccessibleView(): boolean { + const notification = getNotificationFromContext(listService); + if (!notification) { + return false; + } + commandService.executeCommand('notifications.showList'); + let notificationIndex: number | undefined; + const list = listService.lastFocusedList; + if (list instanceof WorkbenchList) { + notificationIndex = list.indexOf(notification); + } + if (notificationIndex === undefined) { + return false; + } + function focusList(): void { + commandService.executeCommand('notifications.showList'); + if (list && notificationIndex !== undefined) { + list.domFocus(); + try { + list.setFocus([notificationIndex]); + } catch { } + } + } + const message = notification.message.original.toString(); + if (!message) { + return false; + } + accessibleViewService.show({ + provideContent: () => { + return localize('notification.accessibleView', '{0} Source: {1}', message, notification.source); + }, + onClose(): void { + focusList(); + }, + next(): void { + if (!list) { + return; + } + focusList(); + list.focusNext(); + renderAccessibleView(); + }, + previous(): void { + if (!list) { + return; + } + focusList(); + list.focusPrevious(); + renderAccessibleView(); + }, + verbositySettingKey: 'notifications', + options: { + ariaLabel: localize('notification', "Notification Accessible View"), + type: AccessibleViewType.View + } + }); + return true; + } + return renderAccessibleView(); + }, NotificationFocusedContext)); + } +} + +workbenchContributionsRegistry.registerWorkbenchContribution(NotificationAccessibleViewContribution, LifecyclePhase.Eventually); + +class AccessibleViewNavigatorContribution extends Disposable { + static ID: 'AccessibleViewNavigatorContribution'; + constructor() { + super(); + this._register(AccessibleViewNextAction.addImplementation(95, 'next', accessor => { + const accessibleViewService = accessor.get(IAccessibleViewService); + accessibleViewService.next(); + return true; + }, accessibleViewIsShown)); + this._register(AccessibleViewPreviousAction.addImplementation(95, 'previous', accessor => { + const accessibleViewService = accessor.get(IAccessibleViewService); + accessibleViewService.previous(); + return true; + }, accessibleViewIsShown)); + } +} + +workbenchContributionsRegistry.registerWorkbenchContribution(AccessibleViewNavigatorContribution, LifecyclePhase.Eventually); diff --git a/code/src/vs/workbench/contrib/accessibility/browser/accessibilityContribution.ts b/code/src/vs/workbench/contrib/accessibility/browser/accessibilityContribution.ts index 455c3d3050f..9dc863f3efd 100644 --- a/code/src/vs/workbench/contrib/accessibility/browser/accessibilityContribution.ts +++ b/code/src/vs/workbench/contrib/accessibility/browser/accessibilityContribution.ts @@ -106,3 +106,34 @@ export const AccessibleViewAction = registerCommand(new MultiCommand({ order: 1 }], })); + + +export const AccessibleViewNextAction = registerCommand(new MultiCommand({ + id: 'editor.action.accessibleViewNext', + precondition: undefined, + kbOpts: { + primary: KeyMod.Alt | KeyCode.BracketRight, + weight: KeybindingWeight.WorkbenchContrib + }, + menuOpts: [{ + menuId: MenuId.CommandPalette, + group: '', + title: localize('editor.action.accessibleViewNext', "Next Accessible View"), + order: 1 + }], +})); + +export const AccessibleViewPreviousAction = registerCommand(new MultiCommand({ + id: 'editor.action.accessibleViewPrevious', + precondition: undefined, + kbOpts: { + primary: KeyMod.Alt | KeyCode.BracketLeft, + weight: KeybindingWeight.WorkbenchContrib + }, + menuOpts: [{ + menuId: MenuId.CommandPalette, + group: '', + title: localize('editor.action.accessibleViewPrevious', "Previous Accessible View"), + order: 1 + }], +})); diff --git a/code/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/code/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index f6bfc202d91..52e818673be 100644 --- a/code/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/code/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -22,9 +22,9 @@ import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/cont import { IContextViewDelegate, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; -import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; import { alert } from 'vs/base/browser/ui/aria/aria'; +import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; +import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; const enum DEFAULT { WIDTH = 800, @@ -36,6 +36,8 @@ export interface IAccessibleContentProvider { provideContent(): string; onClose(): void; onKeyDown?(e: IKeyboardEvent): void; + previous?(): void; + next?(): void; options: IAccessibleViewOptions; } @@ -44,6 +46,8 @@ export const IAccessibleViewService = createDecorator('a export interface IAccessibleViewService { readonly _serviceBrand: undefined; show(provider: IAccessibleContentProvider): void; + next(): void; + previous(): void; } export const enum AccessibleViewType { @@ -59,9 +63,11 @@ export interface IAccessibleViewOptions { } export const accessibilityHelpIsShown = new RawContextKey('accessibilityHelpIsShown', false, true); +export const accessibleViewIsShown = new RawContextKey('accessibleViewIsShown', false, true); class AccessibleView extends Disposable { private _editorWidget: CodeEditorWidget; private _accessiblityHelpIsShown: IContextKey; + private _accessibleViewIsShown: IContextKey; get editorWidget() { return this._editorWidget; } private _editorContainer: HTMLElement; private _currentProvider: IAccessibleContentProvider | undefined; @@ -77,10 +83,11 @@ class AccessibleView extends Disposable { ) { super(); this._accessiblityHelpIsShown = accessibilityHelpIsShown.bindTo(this._contextKeyService); + this._accessibleViewIsShown = accessibleViewIsShown.bindTo(this._contextKeyService); this._editorContainer = document.createElement('div'); this._editorContainer.classList.add('accessible-view'); const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { - contributions: EditorExtensionsRegistry.getSomeEditorContributions([LinkDetector.ID, SelectionClipboardContributionID, 'editor.contrib.selectionAnchorController']) + contributions: [...EditorExtensionsRegistry.getEditorContributions(), ...EditorExtensionsRegistry.getSomeEditorContributions([LinkDetector.ID, SelectionClipboardContributionID, 'editor.contrib.selectionAnchorController'])] }; const editorOptions: IEditorConstructionOptions = { ...getSimpleEditorOptions(this._configurationService), @@ -119,6 +126,8 @@ class AccessibleView extends Disposable { onHide: () => { if (provider.options.type === AccessibleViewType.HelpMenu) { this._accessiblityHelpIsShown.reset(); + } else { + this._accessibleViewIsShown.reset(); } this._currentProvider = undefined; } @@ -126,7 +135,24 @@ class AccessibleView extends Disposable { this._contextViewService.showContextView(delegate); if (provider.options.type === AccessibleViewType.HelpMenu) { this._accessiblityHelpIsShown.set(true); + } else { + this._accessibleViewIsShown.set(true); + } + this._currentProvider = provider; + } + + previous(): void { + if (!this._currentProvider) { + return; + } + this._currentProvider.previous?.(); + } + + next(): void { + if (!this._currentProvider) { + return; } + this._currentProvider.next?.(); } private _render(provider: IAccessibleContentProvider, container: HTMLElement): IDisposable { @@ -172,19 +198,20 @@ class AccessibleView extends Disposable { }); const disposableStore = new DisposableStore(); disposableStore.add(this._editorWidget.onKeyUp((e) => { - if (e.keyCode === KeyCode.Escape) { - this._contextViewService.hideContextView(); - // Delay to allow the context view to hide #186514 - setTimeout(() => provider.onClose(), 100); - } else if (e.keyCode === KeyCode.KeyD && this._configurationService.getValue(settingKey)) { + if (e.keyCode === KeyCode.KeyD && this._configurationService.getValue(settingKey)) { alert(localize('disableAccessibilityHelp', '{0} accessibility verbosity is now disabled', provider.verbositySettingKey)); this._configurationService.updateValue(settingKey, false); } - e.stopPropagation(); provider.onKeyDown?.(e); + // e.stopPropagation(); })); disposableStore.add(this._editorWidget.onKeyDown((e) => { - if (e.keyCode === KeyCode.KeyH && provider.options.readMoreUrl) { + if (e.keyCode === KeyCode.Escape) { + e.stopPropagation(); + this._contextViewService.hideContextView(); + // Delay to allow the context view to hide #186514 + setTimeout(() => provider.onClose(), 100); + } else if (e.keyCode === KeyCode.KeyH && provider.options.readMoreUrl) { const url: string = provider.options.readMoreUrl!; alert(AccessibilityHelpNLS.openingDocs); this._openerService.open(URI.parse(url)); @@ -226,4 +253,10 @@ export class AccessibleViewService extends Disposable implements IAccessibleView } this._accessibleView.show(provider); } + next(): void { + this._accessibleView?.next(); + } + previous(): void { + this._accessibleView?.previous(); + } } diff --git a/code/src/vs/workbench/contrib/audioCues/browser/commands.ts b/code/src/vs/workbench/contrib/audioCues/browser/commands.ts index cec8946ee74..f5d295f7c4c 100644 --- a/code/src/vs/workbench/contrib/audioCues/browser/commands.ts +++ b/code/src/vs/workbench/contrib/audioCues/browser/commands.ts @@ -49,7 +49,7 @@ export class ShowAudioCueHelp extends Action2 { { activeItem: items[0], onDidFocus: (item) => { - audioCueService.playSound(item.audioCue.sound, true); + audioCueService.playSound(item.audioCue.sound.getSound(true), true); }, onDidTriggerItemButton: (context) => { preferencesService.openSettings({ query: context.item.audioCue.settingsKey }); diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index 10a34e44a6a..fd147f172ea 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/code/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -10,10 +10,8 @@ import { withNullAsUndefined } from 'vs/base/common/types'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; - - +import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; export function getAccessibilityHelpText(accessor: ServicesAccessor, type: 'panelChat' | 'inlineChat'): string { const keybindingService = accessor.get(IKeybindingService); diff --git a/code/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/code/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 4ad2f46951b..10a24bd4b4b 100644 --- a/code/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/code/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -20,38 +20,42 @@ function isExecuteActionContext(thing: unknown): thing is IChatExecuteActionCont return typeof thing === 'object' && thing !== null && 'widget' in thing; } -export function registerChatExecuteActions() { - registerAction2(class SubmitAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.chat.submit', - title: { - value: localize('interactive.submit.label', "Submit"), - original: 'Submit' - }, - f1: false, - category: CHAT_CATEGORY, - icon: Codicon.send, - precondition: CONTEXT_CHAT_INPUT_HAS_TEXT, - menu: { - id: MenuId.ChatExecute, - when: CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), - group: 'navigation', - } - }); - } +export class SubmitAction extends Action2 { + static readonly ID = 'workbench.action.chat.submit'; - run(accessor: ServicesAccessor, ...args: any[]) { - const context = args[0]; - if (!isExecuteActionContext(context)) { - return; + constructor() { + super({ + id: SubmitAction.ID, + title: { + value: localize('interactive.submit.label', "Submit"), + original: 'Submit' + }, + f1: false, + category: CHAT_CATEGORY, + icon: Codicon.send, + precondition: CONTEXT_CHAT_INPUT_HAS_TEXT, + menu: { + id: MenuId.ChatExecute, + when: CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), + group: 'navigation', } + }); + } - context.widget.acceptInput(); + run(accessor: ServicesAccessor, ...args: any[]) { + const context = args[0]; + if (!isExecuteActionContext(context)) { + return; } - }); - registerAction2(class SubmitAction extends Action2 { + context.widget.acceptInput(); + } +} + +export function registerChatExecuteActions() { + registerAction2(SubmitAction); + + registerAction2(class CancelAction extends Action2 { constructor() { super({ id: 'workbench.action.chat.cancel', diff --git a/code/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts b/code/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts index 60b4bd10fdc..87d9abc836c 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts @@ -6,7 +6,7 @@ import { status } from 'vs/base/browser/ui/aria/aria'; import { RunOnceScheduler } from 'vs/base/common/async'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { AudioCue, AudioCueGroupId, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; +import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; @@ -37,7 +37,7 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi const isPanelChat = typeof response !== 'string'; this._responsePendingAudioCue?.dispose(); this._runOnceScheduler?.cancel(); - this._audioCueService.playRandomAudioCue(AudioCueGroupId.chatResponseReceived, true); + this._audioCueService.playAudioCue(AudioCue.chatResponseReceived, true); this._hasReceivedRequest = false; if (!response) { return; diff --git a/code/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/code/src/vs/workbench/contrib/chat/browser/chatWidget.ts index bea6bc2f7a9..38b3a62da6e 100644 --- a/code/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/code/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -238,6 +238,7 @@ export class ChatWidget extends Disposable implements IChatWidget { command: 'clear', sortText: 'z_clear', detail: localize('clear', "Clear the session"), + executeImmediately: true }; this.lastSlashCommands = [ ...(commands ?? []), diff --git a/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index 46b61d5efa3..80d2c1989f1 100644 --- a/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/code/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -23,6 +23,7 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { SlashCommandContentWidget } from 'vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget'; +import { SubmitAction } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; const decorationDescription = 'chat'; const slashCommandPlaceholderDecorationType = 'chat-session-detail'; @@ -165,7 +166,7 @@ class InputEditorDecorations extends Disposable { } } -class InputEditorSlashCommandFollowups extends Disposable { +class InputEditorSlashCommandMode extends Disposable { constructor( private readonly widget: IChatWidget, @IChatService private readonly chatService: IChatService @@ -194,7 +195,7 @@ class InputEditorSlashCommandFollowups extends Disposable { } } -ChatWidget.CONTRIBS.push(InputEditorDecorations, InputEditorSlashCommandFollowups); +ChatWidget.CONTRIBS.push(InputEditorDecorations, InputEditorSlashCommandMode); class SlashCommandCompletions extends Disposable { constructor( @@ -230,7 +231,8 @@ class SlashCommandCompletions extends Disposable { detail: c.detail, range: new Range(1, 1, 1, 1), sortText: c.sortText ?? c.command, - kind: CompletionItemKind.Text // The icons are disabled here anyway + kind: CompletionItemKind.Text, // The icons are disabled here anyway, + command: c.executeImmediately ? { id: SubmitAction.ID, title: withSlash, arguments: [{ widget }] } : undefined, }; }) }; diff --git a/code/src/vs/workbench/contrib/chat/common/chatService.ts b/code/src/vs/workbench/contrib/chat/common/chatService.ts index 59f031eb120..a93be123a49 100644 --- a/code/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/code/src/vs/workbench/contrib/chat/common/chatService.ts @@ -67,10 +67,26 @@ export interface ISlashCommandProvider { export interface ISlashCommand { command: string; - shouldRepopulate?: boolean; provider?: ISlashCommandProvider; sortText?: string; detail?: string; + + /** + * Whether the command should execute as soon + * as it is entered. Defaults to `false`. + */ + executeImmediately?: boolean; + /** + * Whether executing the command puts the + * chat into a persistent mode, where the + * slash command is prepended to the chat input. + */ + shouldRepopulate?: boolean; + /** + * Placeholder text to render in the chat input + * when the slash command has been repopulated. + * Has no effect if `shouldRepopulate` is `false`. + */ followupPlaceholder?: string; } diff --git a/code/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts b/code/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts index 7414c5b26a5..7f017f54f5c 100644 --- a/code/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts +++ b/code/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts @@ -9,10 +9,8 @@ import { registerDiffEditorContribution } from 'vs/editor/browser/editorExtensio import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { DiffReviewNext, DiffReviewPrev } from 'vs/editor/browser/widget/diffEditor.contribution'; import { DiffEditorWidget2 } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2'; -import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; -import { IDiffComputationResult } from 'vs/editor/common/diff/smartLinesDiffComputer'; +import { EmbeddedDiffEditorWidget, EmbeddedDiffEditorWidget2 } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; import { IDiffEditorContribution } from 'vs/editor/common/editorCommon'; -import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyEqualsExpr, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -20,22 +18,15 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { FloatingClickWidget } from 'vs/workbench/browser/codeeditor'; import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; -import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; - -const enum WidgetState { - Hidden, - HintWhitespace -} +import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { localize } from 'vs/nls'; +import { observableFromEvent } from 'vs/base/common/observable'; +import { autorunWithStore2 } from 'vs/base/common/observableImpl/autorun'; class DiffEditorHelperContribution extends Disposable implements IDiffEditorContribution { - public static readonly ID = 'editor.contrib.diffEditorHelper'; - private _helperWidget: FloatingClickWidget | null; - private _helperWidgetListener: IDisposable | null; - private _state: WidgetState; - constructor( private readonly _diffEditor: IDiffEditor, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -44,58 +35,38 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont ) { super(); - this._register(AccessibilityHelpAction.addImplementation(105, 'diff-editor', async accessor => { - const accessibleViewService = accessor.get(IAccessibleViewService); - const editorService = accessor.get(IEditorService); - const codeEditorService = accessor.get(ICodeEditorService); - const keybindingService = accessor.get(IKeybindingService); - - const next = keybindingService.lookupKeybinding(DiffReviewNext.id)?.getAriaLabel(); - const previous = keybindingService.lookupKeybinding(DiffReviewPrev.id)?.getAriaLabel(); - - if (!(editorService.activeTextEditorControl instanceof DiffEditorWidget2)) { - return; - } - - const codeEditor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); - if (!codeEditor) { - return; - } - - const keys = ['audioCues.diffLineDeleted', 'audioCues.diffLineInserted', 'audioCues.diffLineModified']; - - accessibleViewService.show({ - verbositySettingKey: 'diffEditor', - provideContent: () => [ - nls.localize('msg1', "You are in a diff editor."), - nls.localize('msg2', "Press {0} or {1} to view the next or previous diff in the diff review mode that is optimized for screen readers.", next, previous), - nls.localize('msg3', "To control which audio cues should be played, the following settings can be configured: {0}.", keys.join(', ')), - ].join('\n'), - onClose: () => { - codeEditor.focus(); - }, - options: { type: AccessibleViewType.HelpMenu, ariaLabel: nls.localize('chat-help-label', "Diff editor accessibility help") } - }); - }, ContextKeyExpr.and( - ContextKeyEqualsExpr.create('diffEditorVersion', 2), - ContextKeyEqualsExpr.create('isInDiffEditor', true), - ))); - - this._helperWidget = null; - this._helperWidgetListener = null; - this._state = WidgetState.Hidden; - - if (!(this._diffEditor instanceof EmbeddedDiffEditorWidget)) { + this._register(createScreenReaderHelp()); + + const isEmbeddedDiffEditor = (this._diffEditor instanceof EmbeddedDiffEditorWidget) || (this._diffEditor instanceof EmbeddedDiffEditorWidget2); + + if (!isEmbeddedDiffEditor) { + const computationResult = observableFromEvent(e => this._diffEditor.onDidUpdateDiff(e), () => this._diffEditor.getDiffComputationResult()); + const onlyWhiteSpaceChange = computationResult.map(r => r && !r.identical && r.changes2.length === 0); + + this._register(autorunWithStore2('update state', (reader, store) => { + if (onlyWhiteSpaceChange.read(reader)) { + const helperWidget = store.add(this._instantiationService.createInstance( + FloatingClickWidget, + this._diffEditor.getModifiedEditor(), + localize('hintWhitespace', "Show Whitespace Differences"), + null + )); + store.add(helperWidget.onClick(() => { + this._configurationService.updateValue('diffEditor.ignoreTrimWhitespace', false); + })); + helperWidget.render(); + } + })); + this._register(this._diffEditor.onDidUpdateDiff(() => { const diffComputationResult = this._diffEditor.getDiffComputationResult(); - this._setState(this._deduceState(diffComputationResult)); if (diffComputationResult && diffComputationResult.quitEarly) { this._notificationService.prompt( Severity.Warning, - nls.localize('hintTimeout', "The diff algorithm was stopped early (after {0} ms.)", this._diffEditor.maxComputationTime), + localize('hintTimeout', "The diff algorithm was stopped early (after {0} ms.)", this._diffEditor.maxComputationTime), [{ - label: nls.localize('removeTimeout', "Remove Limit"), + label: localize('removeTimeout', "Remove Limit"), run: () => { this._configurationService.updateValue('diffEditor.maxComputationTime', 0); } @@ -106,49 +77,45 @@ class DiffEditorHelperContribution extends Disposable implements IDiffEditorCont })); } } +} - private _deduceState(diffComputationResult: IDiffComputationResult | null): WidgetState { - if (!diffComputationResult) { - return WidgetState.Hidden; - } - if (this._diffEditor.ignoreTrimWhitespace && diffComputationResult.changes.length === 0 && !diffComputationResult.identical) { - return WidgetState.HintWhitespace; - } - return WidgetState.Hidden; - } - - private _setState(newState: WidgetState) { - if (this._state === newState) { - return; - } - - this._state = newState; +function createScreenReaderHelp(): IDisposable { + return AccessibilityHelpAction.addImplementation(105, 'diff-editor', async (accessor) => { + const accessibleViewService = accessor.get(IAccessibleViewService); + const editorService = accessor.get(IEditorService); + const codeEditorService = accessor.get(ICodeEditorService); + const keybindingService = accessor.get(IKeybindingService); - if (this._helperWidgetListener) { - this._helperWidgetListener.dispose(); - this._helperWidgetListener = null; - } - if (this._helperWidget) { - this._helperWidget.dispose(); - this._helperWidget = null; - } + const next = keybindingService.lookupKeybinding(DiffReviewNext.id)?.getAriaLabel(); + const previous = keybindingService.lookupKeybinding(DiffReviewPrev.id)?.getAriaLabel(); - if (this._state === WidgetState.HintWhitespace) { - this._helperWidget = this._instantiationService.createInstance(FloatingClickWidget, this._diffEditor.getModifiedEditor(), nls.localize('hintWhitespace', "Show Whitespace Differences"), null); - this._helperWidgetListener = this._helperWidget.onClick(() => this._onDidClickHelperWidget()); - this._helperWidget.render(); + if (!(editorService.activeTextEditorControl instanceof DiffEditorWidget2)) { + return; } - } - private _onDidClickHelperWidget(): void { - if (this._state === WidgetState.HintWhitespace) { - this._configurationService.updateValue('diffEditor.ignoreTrimWhitespace', false); + const codeEditor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor(); + if (!codeEditor) { + return; } - } - override dispose(): void { - super.dispose(); - } + const keys = ['audioCues.diffLineDeleted', 'audioCues.diffLineInserted', 'audioCues.diffLineModified']; + + accessibleViewService.show({ + verbositySettingKey: 'diffEditor', + provideContent: () => [ + localize('msg1', "You are in a diff editor."), + localize('msg2', "Press {0} or {1} to view the next or previous diff in the diff review mode that is optimized for screen readers.", next, previous), + localize('msg3', "To control which audio cues should be played, the following settings can be configured: {0}.", keys.join(', ')), + ].join('\n'), + onClose: () => { + codeEditor.focus(); + }, + options: { type: AccessibleViewType.HelpMenu, ariaLabel: localize('chat-help-label', "Diff editor accessibility help") } + }); + }, ContextKeyExpr.and( + ContextKeyEqualsExpr.create('diffEditorVersion', 2), + ContextKeyEqualsExpr.create('isInDiffEditor', true) + )); } registerDiffEditorContribution(DiffEditorHelperContribution.ID, DiffEditorHelperContribution); diff --git a/code/src/vs/workbench/contrib/comments/browser/commentsView.ts b/code/src/vs/workbench/contrib/comments/browser/commentsView.ts index 24cb5e44f5c..4be02c8c76c 100644 --- a/code/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/code/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -42,6 +42,7 @@ import { ITreeElement } from 'vs/base/browser/ui/tree/tree'; import { Iterable } from 'vs/base/common/iterator'; import { CommentController } from 'vs/workbench/contrib/comments/browser/commentsController'; import { Range } from 'vs/editor/common/core/range'; +import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands'; const CONTEXT_KEY_HAS_COMMENTS = new RawContextKey('commentsView.hasComments', false); const CONTEXT_KEY_SOME_COMMENTS_EXPANDED = new RawContextKey('commentsView.someCommentsExpanded', false); @@ -145,6 +146,23 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { super.saveState(); } + override render(): void { + super.render(); + this._register(registerNavigableContainer({ + focusNotifiers: [this, this.filterWidget], + focusNextWidget: () => { + if (this.filterWidget.hasFocus()) { + this.focus(); + } + }, + focusPreviousWidget: () => { + if (!this.filterWidget.hasFocus()) { + this.focusFilter(); + } + } + })); + } + public focusFilter(): void { this.filterWidget.focus(); } diff --git a/code/src/vs/workbench/contrib/debug/browser/repl.ts b/code/src/vs/workbench/contrib/debug/browser/repl.ts index b12c97f0b8f..2ee486737c6 100644 --- a/code/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/code/src/vs/workbench/contrib/debug/browser/repl.ts @@ -68,6 +68,7 @@ import { CONTEXT_DEBUG_STATE, CONTEXT_IN_DEBUG_REPL, CONTEXT_MULTI_SESSION_REPL, import { Variable } from 'vs/workbench/contrib/debug/common/debugModel'; import { ReplEvaluationResult, ReplGroup } from 'vs/workbench/contrib/debug/common/replModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands'; const $ = dom.$; @@ -563,6 +564,27 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { // --- Creation + override render(): void { + super.render(); + this._register(registerNavigableContainer({ + focusNotifiers: [this, this.filterWidget], + focusNextWidget: () => { + if (this.filterWidget.hasFocus()) { + this.tree?.domFocus(); + } else if (this.tree?.getHTMLElement() === document.activeElement) { + this.focus(); + } + }, + focusPreviousWidget: () => { + if (this.replInput.hasTextFocus()) { + this.tree?.domFocus(); + } else if (this.tree?.getHTMLElement() === document.activeElement) { + this.focusFilter(); + } + } + })); + } + protected override renderBody(parent: HTMLElement): void { super.renderBody(parent); this.container = dom.append(parent, $('.repl')); diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/code/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 70bc8da775c..1fc4cef8e46 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -1420,36 +1420,36 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi }); this.registerExtensionAction({ - id: TOGGLE_IGNORE_EXTENSION_ACTION_ID, - title: { value: localize('workbench.extensions.action.toggleIgnoreExtension', "Sync This Extension"), original: `Sync This Extension` }, + id: 'workbench.extensions.action.toggleApplyToAllProfiles', + title: { value: localize('workbench.extensions.action.toggleApplyToAllProfiles', "Apply Extension to all Profiles"), original: `Apply Extension to all Profiles` }, + toggled: ContextKeyExpr.has('isApplicationScopedExtension'), menu: { id: MenuId.ExtensionContext, group: '2_configure', - when: ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, ContextKeyExpr.has('inExtensionEditor').negate()), + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.has('isDefaultApplicationScopedExtension').negate()), order: 3 }, run: async (accessor: ServicesAccessor, id: string) => { const extension = this.extensionsWorkbenchService.local.find(e => areSameExtensions({ id }, e.identifier)); if (extension) { - return this.extensionsWorkbenchService.toggleExtensionIgnoredToSync(extension); + return this.extensionsWorkbenchService.toggleApplyExtensionToAllProfiles(extension); } } }); this.registerExtensionAction({ - id: 'workbench.extensions.action.toggleApplyToAllProfiles', - title: { value: localize('workbench.extensions.action.toggleApplyToAllProfiles', "Apply Extension to all Profiles"), original: `Apply Extension to all Profiles` }, - toggled: ContextKeyExpr.has('isApplicationScopedExtension'), + id: TOGGLE_IGNORE_EXTENSION_ACTION_ID, + title: { value: localize('workbench.extensions.action.toggleIgnoreExtension', "Sync This Extension"), original: `Sync This Extension` }, menu: { id: MenuId.ExtensionContext, group: '2_configure', - when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.has('isDefaultApplicationScopedExtension').negate()), + when: ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, ContextKeyExpr.has('inExtensionEditor').negate()), order: 4 }, run: async (accessor: ServicesAccessor, id: string) => { const extension = this.extensionsWorkbenchService.local.find(e => areSameExtensions({ id }, e.identifier)); if (extension) { - return this.extensionsWorkbenchService.toggleApplyExtensionToAllProfiles(extension); + return this.extensionsWorkbenchService.toggleExtensionIgnoredToSync(extension); } } }); diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index db35de5c3b2..1318ae45e40 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -12,7 +12,7 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; import { Action } from 'vs/base/common/actions'; -import { append, $, Dimension, hide, show, DragAndDropObserver } from 'vs/base/browser/dom'; +import { append, $, Dimension, hide, show, DragAndDropObserver, trackFocus } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -61,6 +61,7 @@ import { extractEditorsAndFilesDropData } from 'vs/platform/dnd/browser/dnd'; import { extname } from 'vs/base/common/resources'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ILocalizedString } from 'vs/platform/action/common/action'; +import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands'; export const DefaultViewsContext = new RawContextKey('defaultExtensionViews', true); export const ExtensionsSortByContext = new RawContextKey('extensionsSortByValue', ''); @@ -610,6 +611,22 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE })); super.create(append(this.root, $('.extensions'))); + + const focusTracker = this._register(trackFocus(this.root)); + const isSearchBoxFocused = () => this.searchBox?.inputWidget.hasWidgetFocus(); + this._register(registerNavigableContainer({ + focusNotifiers: [focusTracker], + focusNextWidget: () => { + if (isSearchBoxFocused()) { + this.focusListView(); + } + }, + focusPreviousWidget: () => { + if (!isSearchBoxFocused()) { + this.searchBox?.focus(); + } + } + })); } override focus(): void { diff --git a/code/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/code/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 781d85025f6..f8d7c21cea7 100644 --- a/code/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/code/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -29,7 +29,7 @@ import { ManageExtensionAction, getContextMenuActions, ExtensionAction } from 'v import { WorkbenchPagedList } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; +import { ViewPane, IViewPaneOptions, ViewPaneShowActions } from 'vs/workbench/browser/parts/views/viewPane'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { coalesce, distinct, flatten } from 'vs/base/common/arrays'; import { alert } from 'vs/base/browser/ui/aria/aria'; @@ -150,7 +150,7 @@ export class ExtensionsListView extends ViewPane { ) { super({ ...(viewletViewOptions as IViewPaneOptions), - showActionsAlways: true, + showActions: ViewPaneShowActions.Always, maximumBodySize: options.flexibleHeight ? (storageService.getNumber(`${viewletViewOptions.id}.size`, StorageScope.PROFILE, 0) ? undefined : 0) : undefined }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); if (this.options.onDidChangeTitle) { diff --git a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index e2c4e8a394e..4cde6edcb51 100644 --- a/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/code/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -670,6 +670,7 @@ export class InlineChatWidget { insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, kind: CompletionItemKind.Text, range: new Range(1, 1, 1, 1), + command: command.executeImmediately ? { id: 'inlineChat.accept', title: withSlash } : undefined }; }); diff --git a/code/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/code/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 61cf7d493a1..855ffa54301 100644 --- a/code/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/code/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -24,6 +24,7 @@ export interface IInlineChatSlashCommand { command: string; detail?: string; refer?: boolean; + executeImmediately?: boolean; } export interface IInlineChatSession { diff --git a/code/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts b/code/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts index 0cf38581b03..622adff44b5 100644 --- a/code/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts +++ b/code/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts @@ -132,12 +132,14 @@ pre code { } .vscode-light h1, +.vscode-light h2, .vscode-light hr, .vscode-light td { border-color: rgba(0, 0, 0, 0.18); } .vscode-dark h1, +.vscode-dark h2, .vscode-dark hr, .vscode-dark td { border-color: rgba(255, 255, 255, 0.18); diff --git a/code/src/vs/workbench/contrib/markers/browser/markersView.ts b/code/src/vs/workbench/contrib/markers/browser/markersView.ts index 1511472ab9f..fd436d62639 100644 --- a/code/src/vs/workbench/contrib/markers/browser/markersView.ts +++ b/code/src/vs/workbench/contrib/markers/browser/markersView.ts @@ -54,6 +54,7 @@ import { ResourceListDnDHandler } from 'vs/workbench/browser/dnd'; import { ITableContextMenuEvent, ITableEvent } from 'vs/base/browser/ui/table/table'; import { MarkersTable } from 'vs/workbench/contrib/markers/browser/markersTable'; import { Markers, MarkersContextKeys, MarkersViewMode } from 'vs/workbench/contrib/markers/common/markers'; +import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands'; function createResourceMarkersIterator(resourceMarkers: ResourceMarkers): Iterable> { return Iterable.map(resourceMarkers.markers, m => { @@ -181,6 +182,23 @@ export class MarkersView extends FilterViewPane implements IMarkersView { })); } + override render(): void { + super.render(); + this._register(registerNavigableContainer({ + focusNotifiers: [this, this.filterWidget], + focusNextWidget: () => { + if (this.filterWidget.hasFocus()) { + this.focus(); + } + }, + focusPreviousWidget: () => { + if (!this.filterWidget.hasFocus()) { + this.focusFilter(); + } + } + })); + } + protected override renderBody(parent: HTMLElement): void { super.renderBody(parent); diff --git a/code/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/code/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index a73901e182d..72cb92908d6 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -676,7 +676,7 @@ class NotebookLanguageSelectorScoreRefine { } class NotebookAccessibilityHelpContribution extends Disposable { - static ID: 'chatAccessibilityHelpContribution'; + static ID: 'notebookAccessibilityHelpContribution'; constructor() { super(); this._register(AccessibilityHelpAction.addImplementation(105, 'notebook', async accessor => { diff --git a/code/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/code/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 92fb5866d8b..71f9504ed23 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -90,6 +90,8 @@ import { INotebookLoggingService } from 'vs/workbench/contrib/notebook/common/no import { Schemas } from 'vs/base/common/network'; import { DropIntoEditorController } from 'vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController'; import { CopyPasteController } from 'vs/editor/contrib/dropOrPasteInto/browser/copyPasteController'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; const $ = DOM.$; @@ -284,6 +286,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD @INotebookExecutionStateService notebookExecutionStateService: INotebookExecutionStateService, @IEditorProgressService private editorProgressService: IEditorProgressService, @INotebookLoggingService readonly logService: INotebookLoggingService, + @IKeybindingService readonly keybindingService: IKeybindingService ) { super(); @@ -851,6 +854,17 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._listDelegate = this.instantiationService.createInstance(NotebookCellListDelegate); this._register(this._listDelegate); + const createNotebookAriaLabel = () => { + const keybinding = this.keybindingService.lookupKeybinding('editor.action.accessibilityHelp')?.getLabel(); + + if (this.configurationService.getValue(AccessibilityVerbositySettingId.Notebook)) { + return keybinding + ? nls.localize('notebookTreeAriaLabelHelp', "Notebook\nUse {0} for accessibility help", keybinding) + : nls.localize('notebookTreeAriaLabelHelpNoKb', "Notebook\nRun the Open Accessibility Help command for more information", keybinding); + } + return nls.localize('notebookTreeAriaLabel', "Notebook"); + }; + this._list = this.instantiationService.createInstance( NotebookCellList, 'NotebookCellList', @@ -904,9 +918,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return ''; }, - getWidgetAriaLabel() { - return nls.localize('notebookTreeAriaLabel', "Notebook"); - } + getWidgetAriaLabel: createNotebookAriaLabel }, }, ); @@ -987,6 +999,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD })); this._registerNotebookActionsToolbar(); + + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(AccessibilityVerbositySettingId.Notebook)) { + this._list.ariaLabel = createNotebookAriaLabel(); + } + })); } private showListContextMenu(e: IListContextMenuEvent) { diff --git a/code/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts b/code/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts index b5468a14cdd..0276ee9e10e 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts @@ -43,6 +43,7 @@ export interface INotebookCellList { length: number; rowsContainer: HTMLElement; scrollableElement: HTMLElement; + ariaLabel: string; readonly onDidRemoveOutputs: Event; readonly onDidHideOutputs: Event; readonly onDidRemoveCellsFromView: Event; diff --git a/code/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/code/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index d45c567c327..189bd557488 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -1413,15 +1413,11 @@ export class BackLayerWebView extends Themable { } // create new output - const createOutput = () => { - const { message, renderer, transfer: transferable } = this._createOutputCreationMessage(cellInfo, content, cellTop, offset, false, false); - this._sendMessageToWebview(message, transferable); - this.insetMapping.set(content.source, { outputId: message.outputId, versionId: content.source.model.versionId, cellInfo: cellInfo, renderer, cachedCreation: message }); - this.hiddenInsetMapping.delete(content.source); - this.reversedInsetMapping.set(message.outputId, content.source); - }; - - createOutput(); + const { message, renderer, transfer: transferable } = this._createOutputCreationMessage(cellInfo, content, cellTop, offset, false, false); + this._sendMessageToWebview(message, transferable); + this.insetMapping.set(content.source, { outputId: message.outputId, versionId: content.source.model.versionId, cellInfo: cellInfo, renderer, cachedCreation: message }); + this.hiddenInsetMapping.delete(content.source); + this.reversedInsetMapping.set(message.outputId, content.source); } private createMetadata(output: ICellOutput, mimeType: string) { @@ -1503,6 +1499,12 @@ export class BackLayerWebView extends Themable { } const outputCache = this.insetMapping.get(content.source)!; + + if (outputCache.versionId === content.source.model.versionId) { + // already sent this output version to the renderer + return; + } + this.hiddenInsetMapping.delete(content.source); let updatedContent: ICreationContent | undefined = undefined; @@ -1510,6 +1512,8 @@ export class BackLayerWebView extends Themable { if (content.type === RenderOutputType.Extension) { const output = content.source.model; const firstBuffer = output.outputs.find(op => op.mime === content.mimeType)!; + const appenededData = output.appendedSinceVersion(outputCache.versionId, content.mimeType); + const appended = appenededData ? { valueBytes: appenededData.buffer, previousVersion: outputCache.versionId } : undefined; const valueBytes = copyBufferIfNeeded(firstBuffer.data.buffer, transfer); updatedContent = { @@ -1519,6 +1523,7 @@ export class BackLayerWebView extends Themable { output: { mime: content.mimeType, valueBytes, + appended: appended }, allOutputs: output.outputs.map(output => ({ mime: output.mime })) }; diff --git a/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts b/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts index 1b21208b7b4..35cac90d4aa 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts @@ -188,11 +188,18 @@ export interface IOutputRequestDto { export interface OutputItemEntry { readonly mime: string; readonly valueBytes: Uint8Array; + readonly appended?: { valueBytes: Uint8Array; previousVersion: number }; } export type ICreationContent = | { readonly type: RenderOutputType.Html; readonly htmlContent: string } - | { readonly type: RenderOutputType.Extension; readonly outputId: string; readonly metadata: unknown; readonly output: OutputItemEntry; readonly allOutputs: ReadonlyArray<{ readonly mime: string }> }; + | { + readonly type: RenderOutputType.Extension; + readonly outputId: string; + readonly metadata: unknown; + readonly output: OutputItemEntry; + readonly allOutputs: ReadonlyArray<{ readonly mime: string }>; + }; export interface ICreationRequestMessage { readonly type: 'html'; diff --git a/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 649d3421949..f7eb1abd38f 100644 --- a/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/code/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -804,6 +804,7 @@ async function webviewPreloads(ctx: PreloadContext) { interface ExtendedOutputItem extends rendererApi.OutputItem { readonly _allOutputItems: ReadonlyArray; + appendedText?(): string | undefined; } let hasWarnedAboutAllOutputItemsProposal = false; @@ -813,7 +814,8 @@ async function webviewPreloads(ctx: PreloadContext) { mime: string, metadata: unknown, valueBytes: Uint8Array, - allOutputItemData: ReadonlyArray<{ readonly mime: string }> + allOutputItemData: ReadonlyArray<{ readonly mime: string }>, + appended?: { valueBytes: Uint8Array; previousVersion: number } ): ExtendedOutputItem { function create( @@ -821,12 +823,20 @@ async function webviewPreloads(ctx: PreloadContext) { mime: string, metadata: unknown, valueBytes: Uint8Array, + appended?: { valueBytes: Uint8Array; previousVersion: number } ): ExtendedOutputItem { return Object.freeze({ id, mime, metadata, + appendedText(): string | undefined { + if (appended) { + return textDecoder.decode(appended.valueBytes); + } + return undefined; + }, + data(): Uint8Array { return valueBytes; }, @@ -874,7 +884,7 @@ async function webviewPreloads(ctx: PreloadContext) { }); })); - const item = create(id, mime, metadata, valueBytes); + const item = create(id, mime, metadata, valueBytes, appended); allOutputItemCache.set(mime, Promise.resolve(item)); return item; } @@ -2611,13 +2621,13 @@ async function webviewPreloads(ctx: PreloadContext) { this._content = { preferredRendererId, preloadErrors }; if (content.type === 0 /* RenderOutputType.Html */) { - const trustedHtml = ttPolicy?.createHTML(content.htmlContent) ?? content.htmlContent; + const trustedHtml = ttPolicy?.createHTML(content.htmlContent) ?? content.htmlContent; // CodeQL [SM03712] The content comes from renderer extensions, not from direct user input. this.element.innerHTML = trustedHtml as string; } else if (preloadErrors.some(e => e instanceof Error)) { const errors = preloadErrors.filter((e): e is Error => e instanceof Error); showRenderError(`Error loading preloads`, this.element, errors); } else { - const item = createOutputItem(this.outputId, content.output.mime, content.metadata, content.output.valueBytes, content.allOutputs); + const item = createOutputItem(this.outputId, content.output.mime, content.metadata, content.output.valueBytes, content.allOutputs, content.output.appended); const controller = new AbortController(); this.renderTaskAbort = controller; diff --git a/code/src/vs/workbench/contrib/notebook/common/model/notebookCellOutputTextModel.ts b/code/src/vs/workbench/contrib/notebook/common/model/notebookCellOutputTextModel.ts index 5d7ac8195b6..74ed0aaf158 100644 --- a/code/src/vs/workbench/contrib/notebook/common/model/notebookCellOutputTextModel.ts +++ b/code/src/vs/workbench/contrib/notebook/common/model/notebookCellOutputTextModel.ts @@ -3,9 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { VSBuffer } from 'vs/base/common/buffer'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { ICellOutput, IOutputDto, IOutputItemDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICellOutput, IOutputDto, IOutputItemDto, compressOutputItemStreams, isTextStreamMime } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export class NotebookCellOutputTextModel extends Disposable implements ICellOutput { @@ -37,18 +38,77 @@ export class NotebookCellOutputTextModel extends Disposable implements ICellOutp } replaceData(rawData: IOutputDto) { + this.versionedBufferLengths = {}; this._rawOutput = rawData; + this.optimizeOutputItems(); this._versionId = this._versionId + 1; - this._onDidChangeData.fire(); } appendData(items: IOutputItemDto[]) { + this.trackBufferLengths(); this._rawOutput.outputs.push(...items); + this.optimizeOutputItems(); this._versionId = this._versionId + 1; this._onDidChangeData.fire(); } + private trackBufferLengths() { + this.outputs.forEach(output => { + if (isTextStreamMime(output.mime)) { + if (!this.versionedBufferLengths[output.mime]) { + this.versionedBufferLengths[output.mime] = {}; + } + this.versionedBufferLengths[output.mime][this.versionId] = output.data.byteLength; + } + }); + } + + // mime: versionId: buffer length + private versionedBufferLengths: Record> = {}; + + appendedSinceVersion(versionId: number, mime: string): VSBuffer | undefined { + const bufferLength = this.versionedBufferLengths[mime]?.[versionId]; + const output = this.outputs.find(output => output.mime === mime); + if (bufferLength && output) { + return output.data.slice(bufferLength); + } + + return undefined; + } + + private optimizeOutputItems() { + if (this.outputs.length > 1 && this.outputs.every(item => isTextStreamMime(item.mime))) { + // Look for the mimes in the items, and keep track of their order. + // Merge the streams into one output item, per mime type. + const mimeOutputs = new Map(); + const mimeTypes: string[] = []; + this.outputs.forEach(item => { + let items: Uint8Array[]; + if (mimeOutputs.has(item.mime)) { + items = mimeOutputs.get(item.mime)!; + } else { + items = []; + mimeOutputs.set(item.mime, items); + mimeTypes.push(item.mime); + } + items.push(item.data.buffer); + }); + this.outputs.length = 0; + mimeTypes.forEach(mime => { + const compressionResult = compressOutputItemStreams(mimeOutputs.get(mime)!); + this.outputs.push({ + mime, + data: compressionResult.data + }); + if (compressionResult.didCompression) { + // we can't rely on knowing buffer lengths if we've erased previous lines + this.versionedBufferLengths = {}; + } + }); + } + } + toJSON(): IOutputDto { return { // data: this._data, @@ -57,4 +117,6 @@ export class NotebookCellOutputTextModel extends Disposable implements ICellOutp outputId: this._rawOutput.outputId }; } + + } diff --git a/code/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/code/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index c5d927de580..e32021676e5 100644 --- a/code/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/code/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -16,7 +16,7 @@ import { TextModel } from 'vs/editor/common/model/textModel'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { NotebookCellOutputTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellOutputTextModel'; -import { CellInternalMetadataChangedEvent, CellKind, compressOutputItemStreams, ICell, ICellDto2, ICellOutput, IOutputDto, IOutputItemDto, isTextStreamMime, NotebookCellCollapseState, NotebookCellInternalMetadata, NotebookCellMetadata, NotebookCellOutputsSplice, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellInternalMetadataChangedEvent, CellKind, ICell, ICellDto2, ICellOutput, IOutputDto, IOutputItemDto, NotebookCellCollapseState, NotebookCellInternalMetadata, NotebookCellMetadata, NotebookCellOutputsSplice, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export class NotebookCellTextModel extends Disposable implements ICell { private readonly _onDidChangeOutputs = this._register(new Emitter()); @@ -298,34 +298,6 @@ export class NotebookCellTextModel extends Disposable implements ICell { } } - private _optimizeOutputItems(output: ICellOutput) { - if (output.outputs.length > 1 && output.outputs.every(item => isTextStreamMime(item.mime))) { - // Look for the mimes in the items, and keep track of their order. - // Merge the streams into one output item, per mime type. - const mimeOutputs = new Map(); - const mimeTypes: string[] = []; - output.outputs.forEach(item => { - let items: Uint8Array[]; - if (mimeOutputs.has(item.mime)) { - items = mimeOutputs.get(item.mime)!; - } else { - items = []; - mimeOutputs.set(item.mime, items); - mimeTypes.push(item.mime); - } - items.push(item.data.buffer); - }); - output.outputs.length = 0; - mimeTypes.forEach(mime => { - const compressed = compressOutputItemStreams(mimeOutputs.get(mime)!); - output.outputs.push({ - mime, - data: compressed - }); - }); - } - } - replaceOutput(outputId: string, newOutputItem: ICellOutput) { const outputIndex = this.outputs.findIndex(output => output.outputId === outputId); @@ -335,7 +307,6 @@ export class NotebookCellTextModel extends Disposable implements ICell { const output = this.outputs[outputIndex]; output.replaceData(newOutputItem); - this._optimizeOutputItems(output); this._onDidChangeOutputItems.fire(); return true; } @@ -353,8 +324,6 @@ export class NotebookCellTextModel extends Disposable implements ICell { } else { output.replaceData({ outputId: outputId, outputs: items, metadata: output.metadata }); } - - this._optimizeOutputItems(output); this._onDidChangeOutputItems.fire(); return true; } diff --git a/code/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/code/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 558f6c04765..d031650968a 100644 --- a/code/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/code/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -217,6 +217,7 @@ export interface ICellOutput { onDidChangeData: Event; replaceData(items: IOutputDto): void; appendData(items: IOutputItemDto[]): void; + appendedSinceVersion(versionId: number, mime: string): VSBuffer | undefined; } export interface CellInternalMetadataChangedEvent { @@ -995,6 +996,7 @@ const textDecoder = new TextDecoder(); * Given a stream of individual stdout outputs, this function will return the compressed lines, escaping some of the common terminal escape codes. * E.g. some terminal escape codes would result in the previous line getting cleared, such if we had 3 lines and * last line contained such a code, then the result string would be just the first two lines. + * @returns a single VSBuffer with the concatenated and compressed data, and whether any compression was done. */ export function compressOutputItemStreams(outputs: Uint8Array[]) { const buffers: Uint8Array[] = []; @@ -1007,13 +1009,17 @@ export function compressOutputItemStreams(outputs: Uint8Array[]) { startAppending = true; } } - compressStreamBuffer(buffers); - return formatStreamText(VSBuffer.concat(buffers.map(buffer => VSBuffer.wrap(buffer)))); + + const didCompression = compressStreamBuffer(buffers); + const data = formatStreamText(VSBuffer.concat(buffers.map(buffer => VSBuffer.wrap(buffer)))); + return { data, didCompression }; } -const MOVE_CURSOR_1_LINE_COMMAND = `${String.fromCharCode(27)}[A`; + +export const MOVE_CURSOR_1_LINE_COMMAND = `${String.fromCharCode(27)}[A`; const MOVE_CURSOR_1_LINE_COMMAND_BYTES = MOVE_CURSOR_1_LINE_COMMAND.split('').map(c => c.charCodeAt(0)); const LINE_FEED = 10; function compressStreamBuffer(streams: Uint8Array[]) { + let didCompress = false; streams.forEach((stream, index) => { if (index === 0 || stream.length < MOVE_CURSOR_1_LINE_COMMAND.length) { return; @@ -1028,10 +1034,13 @@ function compressStreamBuffer(streams: Uint8Array[]) { if (lastIndexOfLineFeed === -1) { return; } + + didCompress = true; streams[index - 1] = previousStream.subarray(0, lastIndexOfLineFeed); streams[index] = stream.subarray(MOVE_CURSOR_1_LINE_COMMAND.length); } }); + return didCompress; } diff --git a/code/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts b/code/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts index 4da03a9f248..c2cce9c8e78 100644 --- a/code/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts +++ b/code/src/vs/workbench/contrib/notebook/test/browser/notebookTextModel.test.ts @@ -11,7 +11,7 @@ import { ILanguageService } from 'vs/editor/common/languages/language'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CellEditType, CellKind, ICellEditOperation, NotebookTextModelChangedEvent, NotebookTextModelWillAddRemoveEvent, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, CellKind, ICellEditOperation, MOVE_CURSOR_1_LINE_COMMAND, NotebookTextModelChangedEvent, NotebookTextModelWillAddRemoveEvent, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { setupInstantiationService, TestCell, valueBytesFromString, withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; suite('NotebookTextModel', () => { @@ -311,6 +311,215 @@ suite('NotebookTextModel', () => { ); }); + const stdOutMime = 'application/vnd.code.notebook.stdout'; + const stdErrMime = 'application/vnd.code.notebook.stderr'; + + test('appending streaming outputs', async function () { + await withTestNotebook( + [ + ['var a = 1;', 'javascript', CellKind.Code, [], {}], + ], + (editor) => { + const textModel = editor.textModel; + + textModel.applyEdits([ + { + index: 0, + editType: CellEditType.Output, + append: true, + outputs: [{ + outputId: 'append1', + outputs: [{ mime: stdOutMime, data: valueBytesFromString('append 1') }] + }] + }], true, undefined, () => undefined, undefined, true); + const [output] = textModel.cells[0].outputs; + assert.strictEqual(output.versionId, 0, 'initial output version should be 0'); + + textModel.applyEdits([ + { + editType: CellEditType.OutputItems, + append: true, + outputId: 'append1', + items: [ + { mime: stdOutMime, data: valueBytesFromString('append 2') }, + { mime: stdOutMime, data: valueBytesFromString('append 3') } + ] + }], true, undefined, () => undefined, undefined, true); + assert.strictEqual(output.versionId, 1, 'version should bump per append'); + + textModel.applyEdits([ + { + editType: CellEditType.OutputItems, + append: true, + outputId: 'append1', + items: [ + { mime: stdOutMime, data: valueBytesFromString('append 4') }, + { mime: stdOutMime, data: valueBytesFromString('append 5') } + ] + }], true, undefined, () => undefined, undefined, true); + assert.strictEqual(output.versionId, 2, 'version should bump per append'); + + assert.strictEqual(textModel.cells.length, 1); + assert.strictEqual(textModel.cells[0].outputs.length, 1, 'has 1 output'); + assert.strictEqual(output.outputId, 'append1'); + assert.strictEqual(output.outputs.length, 1, 'outputs are compressed'); + assert.strictEqual(output.outputs[0].data.toString(), 'append 1append 2append 3append 4append 5'); + assert.strictEqual(output.appendedSinceVersion(0, stdOutMime)?.toString(), 'append 2append 3append 4append 5'); + assert.strictEqual(output.appendedSinceVersion(1, stdOutMime)?.toString(), 'append 4append 5'); + assert.strictEqual(output.appendedSinceVersion(2, stdOutMime), undefined); + assert.strictEqual(output.appendedSinceVersion(2, stdErrMime), undefined); + } + ); + }); + + test('replacing streaming outputs', async function () { + await withTestNotebook( + [ + ['var a = 1;', 'javascript', CellKind.Code, [], {}], + ], + (editor) => { + const textModel = editor.textModel; + + textModel.applyEdits([ + { + index: 0, + editType: CellEditType.Output, + append: true, + outputs: [{ + outputId: 'append1', + outputs: [{ mime: stdOutMime, data: valueBytesFromString('append 1') }] + }] + }], true, undefined, () => undefined, undefined, true); + const [output] = textModel.cells[0].outputs; + assert.strictEqual(output.versionId, 0, 'initial output version should be 0'); + + textModel.applyEdits([ + { + editType: CellEditType.OutputItems, + append: true, + outputId: 'append1', + items: [{ + mime: stdOutMime, data: valueBytesFromString('append 2') + }] + }], true, undefined, () => undefined, undefined, true); + assert.strictEqual(output.versionId, 1, 'version should bump per append'); + + textModel.applyEdits([ + { + editType: CellEditType.OutputItems, + append: false, + outputId: 'append1', + items: [{ + mime: stdOutMime, data: valueBytesFromString('replace 3') + }] + }], true, undefined, () => undefined, undefined, true); + assert.strictEqual(output.versionId, 2, 'version should bump per replace'); + + textModel.applyEdits([ + { + editType: CellEditType.OutputItems, + append: true, + outputId: 'append1', + items: [{ + mime: stdOutMime, data: valueBytesFromString('append 4') + }] + }], true, undefined, () => undefined, undefined, true); + assert.strictEqual(output.versionId, 3, 'version should bump per append'); + + assert.strictEqual(output.outputs[0].data.toString(), 'replace 3append 4'); + assert.strictEqual(output.appendedSinceVersion(0, stdOutMime), undefined, + 'replacing output should clear out previous versioned output buffers'); + assert.strictEqual(output.appendedSinceVersion(1, stdOutMime), undefined, + 'replacing output should clear out previous versioned output buffers'); + assert.strictEqual(output.appendedSinceVersion(2, stdOutMime)?.toString(), 'append 4'); + } + ); + }); + + test('appending streaming outputs with compression', async function () { + + await withTestNotebook( + [ + ['var a = 1;', 'javascript', CellKind.Code, [], {}], + ], + (editor) => { + const textModel = editor.textModel; + + textModel.applyEdits([ + { + index: 0, + editType: CellEditType.Output, + append: true, + outputs: [{ + outputId: 'append1', + outputs: [ + { mime: stdOutMime, data: valueBytesFromString('append 1') }, + { mime: stdOutMime, data: valueBytesFromString('\nappend 1') }] + }] + }], true, undefined, () => undefined, undefined, true); + const [output] = textModel.cells[0].outputs; + assert.strictEqual(output.versionId, 0, 'initial output version should be 0'); + + textModel.applyEdits([ + { + editType: CellEditType.OutputItems, + append: true, + outputId: 'append1', + items: [{ + mime: stdOutMime, data: valueBytesFromString(MOVE_CURSOR_1_LINE_COMMAND + '\nappend 2') + }] + }], true, undefined, () => undefined, undefined, true); + assert.strictEqual(output.versionId, 1, 'version should bump per append'); + + assert.strictEqual(output.outputs[0].data.toString(), 'append 1\nappend 2'); + assert.strictEqual(output.appendedSinceVersion(0, stdOutMime), undefined, + 'compressing outputs should clear out previous versioned output buffers'); + } + ); + }); + + test('appending multiple different mime streaming outputs', async function () { + await withTestNotebook( + [ + ['var a = 1;', 'javascript', CellKind.Code, [], {}], + ], + (editor) => { + const textModel = editor.textModel; + + textModel.applyEdits([ + { + index: 0, + editType: CellEditType.Output, + append: true, + outputs: [{ + outputId: 'append1', + outputs: [ + { mime: stdOutMime, data: valueBytesFromString('stdout 1') }, + { mime: stdErrMime, data: valueBytesFromString('stderr 1') } + ] + }] + }], true, undefined, () => undefined, undefined, true); + const [output] = textModel.cells[0].outputs; + assert.strictEqual(output.versionId, 0, 'initial output version should be 0'); + + textModel.applyEdits([ + { + editType: CellEditType.OutputItems, + append: true, + outputId: 'append1', + items: [ + { mime: stdOutMime, data: valueBytesFromString('stdout 2') }, + { mime: stdErrMime, data: valueBytesFromString('stderr 2') } + ] + }], true, undefined, () => undefined, undefined, true); + assert.strictEqual(output.versionId, 1, 'version should bump per replace'); + + assert.strictEqual(output.appendedSinceVersion(0, stdErrMime)?.toString(), 'stderr 2'); + assert.strictEqual(output.appendedSinceVersion(0, stdOutMime)?.toString(), 'stdout 2'); + } + ); + }); + test('metadata', async function () { await withTestNotebook( [ diff --git a/code/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/code/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index c8ea7b23901..744d6cb1720 100644 --- a/code/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/code/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -56,6 +56,7 @@ import { CompletionItemKind } from 'vs/editor/common/languages'; import { settingsTextInputBorder } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution'; +import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands'; const $ = DOM.$; @@ -134,6 +135,23 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP this.overflowWidgetsDomNode = $('.keybindings-overflow-widgets-container.monaco-editor'); } + override create(parent: HTMLElement): void { + super.create(parent); + this._register(registerNavigableContainer({ + focusNotifiers: [this], + focusNextWidget: () => { + if (this.searchWidget.hasFocus()) { + this.focusKeybindings(); + } + }, + focusPreviousWidget: () => { + if (!this.searchWidget.hasFocus()) { + this.focusSearch(); + } + } + })); + } + protected createEditor(parent: HTMLElement): void { const keybindingsEditorElement = DOM.append(parent, $('div', { class: 'keybindings-editor' })); diff --git a/code/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/code/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 6889de6378d..91e2db9339b 100644 --- a/code/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/code/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -758,7 +758,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { icon: Codicon.bell, tooltip: nls.localize('bellStatus', "Bell") }, this._configHelper.config.bellDuration); - this._audioCueService.playSound(AudioCue.terminalBell.sound); + this._audioCueService.playSound(AudioCue.terminalBell.sound.getSound()); } }); }, 1000); diff --git a/code/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts b/code/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts index d89cc49042a..e31e6347df9 100644 --- a/code/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts +++ b/code/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts @@ -37,6 +37,7 @@ import { DeferredPromise } from 'vs/base/common/async'; import { IStatusbarService } from 'vs/workbench/services/statusbar/browser/statusbar'; import { memoize } from 'vs/base/common/decorators'; import { StopWatch } from 'vs/base/common/stopwatch'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; export class LocalTerminalBackendContribution implements IWorkbenchContribution { constructor( @@ -87,6 +88,7 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke @IHistoryService historyService: IHistoryService, @INativeWorkbenchEnvironmentService private readonly _environmentService: INativeWorkbenchEnvironmentService, @IStatusbarService statusBarService: IStatusbarService, + @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, ) { super(_localPtyService, logService, historyService, _configurationResolverService, statusBarService, workspaceContextService); @@ -110,7 +112,10 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke this._directProxy = directProxy; // The pty host should not get launched until at least the window restored phase - await this._lifecycleService.when(LifecyclePhase.Restored); + // if remote auth exists, don't await + if (!this._remoteAgentService.getConnection()?.remoteAuthority) { + await this._lifecycleService.when(LifecyclePhase.Restored); + } mark('code/terminal/willConnectPtyHost'); this._logService.trace('Renderer->PtyHost#connect: before acquirePort'); diff --git a/code/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleWidget.ts b/code/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleWidget.ts index d1316db9bee..b28e397f7d1 100644 --- a/code/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleWidget.ts +++ b/code/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleWidget.ts @@ -62,7 +62,7 @@ export abstract class TerminalAccessibleWidget extends DisposableStore { this._element.classList.add(ClassName.Widget); this._editorContainer = document.createElement('div'); const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { - contributions: EditorExtensionsRegistry.getSomeEditorContributions([LinkDetector.ID, SelectionClipboardContributionID, 'editor.contrib.selectionAnchorController']) + contributions: [...EditorExtensionsRegistry.getEditorContributions(), ...EditorExtensionsRegistry.getSomeEditorContributions([LinkDetector.ID, SelectionClipboardContributionID, 'editor.contrib.selectionAnchorController'])] }; const font = _xterm.getFont(); const editorOptions: IEditorConstructionOptions = { diff --git a/code/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/code/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index 9b7feea3e14..d5121a326aa 100644 --- a/code/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/code/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -71,6 +71,7 @@ import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingP import { cmpPriority, isFailedState, isStateWithResult } from 'vs/workbench/contrib/testing/common/testingStates'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands'; const enum LastFocusState { Input, @@ -248,6 +249,23 @@ export class TestingExplorerView extends ViewPane { return { include: [...include], exclude }; } + override render(): void { + super.render(); + this._register(registerNavigableContainer({ + focusNotifiers: [this], + focusNextWidget: () => { + if (!this.viewModel.tree.isDOMFocused()) { + this.viewModel.tree.domFocus(); + } + }, + focusPreviousWidget: () => { + if (this.viewModel.tree.isDOMFocused()) { + this.filter.value?.focus(); + } + } + })); + } + /** * @override */ diff --git a/code/src/vs/workbench/contrib/testing/common/testId.ts b/code/src/vs/workbench/contrib/testing/common/testId.ts index 7b6f5f04dc7..98bd5faec9b 100644 --- a/code/src/vs/workbench/contrib/testing/common/testId.ts +++ b/code/src/vs/workbench/contrib/testing/common/testId.ts @@ -102,6 +102,7 @@ export class TestId { /** * Gets whether maybeChild is a child of maybeParent. + * todo@connor4312: review usages of this to see if using the WellDefinedPrefixTree is better */ public static isChild(maybeParent: string, maybeChild: string) { return maybeChild.startsWith(maybeParent) && maybeChild[maybeParent.length] === TestIdPathParts.Delimiter; @@ -109,6 +110,7 @@ export class TestId { /** * Compares the position of the two ID strings. + * todo@connor4312: review usages of this to see if using the WellDefinedPrefixTree is better */ public static compare(a: string, b: string) { if (a === b) { diff --git a/code/src/vs/workbench/contrib/testing/common/testResult.ts b/code/src/vs/workbench/contrib/testing/common/testResult.ts index ffb2ed27172..c9c9ab15b75 100644 --- a/code/src/vs/workbench/contrib/testing/common/testResult.ts +++ b/code/src/vs/workbench/contrib/testing/common/testResult.ts @@ -8,6 +8,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { Emitter, Event } from 'vs/base/common/event'; import { Lazy } from 'vs/base/common/lazy'; import { language } from 'vs/base/common/platform'; +import { WellDefinedPrefixTree } from 'vs/base/common/prefixTree'; import { removeAnsiEscapeCodes } from 'vs/base/common/strings'; import { localize } from 'vs/nls'; import { IComputedStateAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState'; @@ -233,6 +234,7 @@ export class LiveTestResult implements ITestResult { private readonly newTaskEmitter = new Emitter(); private readonly endTaskEmitter = new Emitter(); private readonly changeEmitter = new Emitter(); + /** todo@connor4312: convert to a WellDefinedPrefixTree */ private readonly testById = new Map(); private testMarkerCounter = 0; private _completedAt?: number; @@ -436,9 +438,9 @@ export class LiveTestResult implements ITestResult { /** * Marks the test and all of its children in the run as retired. */ - public markRetired(testId: string) { + public markRetired(testIds: WellDefinedPrefixTree | undefined) { for (const [id, test] of this.testById) { - if (!test.retired && id === testId || TestId.isChild(testId, id)) { + if (!test.retired && (!testIds || testIds.hasKeyOrParent(TestId.fromString(id).path))) { test.retired = true; this.changeEmitter.fire({ reason: TestResultItemChangeReason.ComputedStateChange, item: test, result: this }); } diff --git a/code/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts b/code/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts index 6565826cb11..313cda95762 100644 --- a/code/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts +++ b/code/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts @@ -20,7 +20,7 @@ import { IProgressService } from 'vs/platform/progress/common/progress'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; +import { ViewPane, ViewPaneShowActions } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { IViewBadge, IViewDescriptorService, IViewsService } from 'vs/workbench/common/views'; @@ -84,7 +84,7 @@ export class WebviewViewPane extends ViewPane { @IWebviewService private readonly webviewService: IWebviewService, @IWebviewViewService private readonly webviewViewService: IWebviewViewService, ) { - super({ ...options, titleMenuId: MenuId.ViewTitle, showActionsAlways: true }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + super({ ...options, titleMenuId: MenuId.ViewTitle, showActions: ViewPaneShowActions.WhenExpanded }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.extensionId = options.fromExtensionId; this.defaultTitle = this.title; diff --git a/code/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/code/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index d42bf233a5c..c251c3a399d 100644 --- a/code/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/code/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -67,7 +67,6 @@ export const allApiProposals = Object.freeze({ portsAttributes: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.portsAttributes.d.ts', profileContentHandlers: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.profileContentHandlers.d.ts', quickDiffProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts', - quickPickItemIcon: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickItemIcon.d.ts', quickPickItemTooltip: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickItemTooltip.d.ts', quickPickSortByLabel: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts', readonlyMessage: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.readonlyMessage.d.ts', @@ -87,7 +86,6 @@ export const allApiProposals = Object.freeze({ terminalDimensions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDimensions.d.ts', terminalQuickFixProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts', testCoverage: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testCoverage.d.ts', - testInvalidateResults: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testInvalidateResults.d.ts', testObserver: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts', textSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchProvider.d.ts', timeline: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.timeline.d.ts', diff --git a/code/src/vs/workbench/workbench.common.main.ts b/code/src/vs/workbench/workbench.common.main.ts index 84fcf6128ee..117e05eadbf 100644 --- a/code/src/vs/workbench/workbench.common.main.ts +++ b/code/src/vs/workbench/workbench.common.main.ts @@ -25,6 +25,7 @@ import 'vs/workbench/browser/actions/windowActions'; import 'vs/workbench/browser/actions/workspaceActions'; import 'vs/workbench/browser/actions/workspaceCommands'; import 'vs/workbench/browser/actions/quickAccessActions'; +import 'vs/workbench/browser/actions/widgetNavigationCommands'; //#endregion diff --git a/code/src/vscode-dts/vscode.d.ts b/code/src/vscode-dts/vscode.d.ts index d819f00fa1c..8e9c1cef485 100644 --- a/code/src/vscode-dts/vscode.d.ts +++ b/code/src/vscode-dts/vscode.d.ts @@ -1733,6 +1733,11 @@ declare module 'vscode' { */ kind?: QuickPickItemKind; + /** + * The icon path or {@link ThemeIcon} for the QuickPickItem. + */ + iconPath?: Uri | { light: Uri; dark: Uri } | ThemeIcon; + /** * A human-readable string which is rendered less prominent in the same line. Supports rendering of * {@link ThemeIcon theme icons} via the `$()`-syntax. @@ -16299,6 +16304,24 @@ declare module 'vscode' { */ createTestItem(id: string, label: string, uri?: Uri): TestItem; + /** + * Marks an item's results as being outdated. This is commonly called when + * code or configuration changes and previous results should no longer + * be considered relevant. The same logic used to mark results as outdated + * may be used to drive {@link TestRunRequest.continuous continuous test runs}. + * + * If an item is passed to this method, test results for the item and all of + * its children will be marked as outdated. If no item is passed, then all + * test owned by the TestController will be marked as outdated. + * + * Any test runs started before the moment this method is called, including + * runs which may still be ongoing, will be marked as outdated and deprioritized + * in the editor's UI. + * + * @param item Item to mark as outdated. If undefined, all the controller's items are marked outdated. + */ + invalidateTestResults(items?: TestItem | readonly TestItem[]): void; + /** * Unregisters the test controller, disposing of its associated tests * and unpersisted results. diff --git a/code/src/vscode-dts/vscode.proposed.interactive.d.ts b/code/src/vscode-dts/vscode.proposed.interactive.d.ts index 91224906d1b..b83e3ab1da1 100644 --- a/code/src/vscode-dts/vscode.proposed.interactive.d.ts +++ b/code/src/vscode-dts/vscode.proposed.interactive.d.ts @@ -9,6 +9,11 @@ declare module 'vscode' { command: string; detail?: string; refer?: boolean; + /** + * Whether the command should execute as soon + * as it is entered. Defaults to `false`. + */ + executeImmediately?: boolean; // kind: CompletionItemKind; } @@ -130,10 +135,11 @@ declare module 'vscode' { export interface InteractiveSessionSlashCommand { command: string; - shouldRepopulate?: boolean; kind: CompletionItemKind; detail?: string; + shouldRepopulate?: boolean; followupPlaceholder?: string; + executeImmediately?: boolean; } export interface InteractiveSessionReplyFollowup { diff --git a/code/src/vscode-dts/vscode.proposed.quickPickItemIcon.d.ts b/code/src/vscode-dts/vscode.proposed.quickPickItemIcon.d.ts deleted file mode 100644 index b53c8350117..00000000000 --- a/code/src/vscode-dts/vscode.proposed.quickPickItemIcon.d.ts +++ /dev/null @@ -1,17 +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' { - /** - * Represents an item that can be selected from - * a list of items. - */ - export interface QuickPickItem { - /** - * The icon path or {@link ThemeIcon} for the QuickPickItem. - */ - iconPath?: Uri | { light: Uri; dark: Uri } | ThemeIcon; - } -} diff --git a/code/src/vscode-dts/vscode.proposed.testInvalidateResults.d.ts b/code/src/vscode-dts/vscode.proposed.testInvalidateResults.d.ts deleted file mode 100644 index 87d21c13c16..00000000000 --- a/code/src/vscode-dts/vscode.proposed.testInvalidateResults.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// https://github.com/microsoft/vscode/issues/134970 - -declare module 'vscode' { - - export interface TestController { - - /** - * Marks an item's results as being outdated. This is commonly called when - * code or configuration changes and previous results should no longer - * be considered relevant. The same logic used to mark results as outdated - * may be used to drive {@link TestRunRequest.continuous continuous test runs}. - * - * If an item is passed to this method, test results for the item and all of - * its children will be marked as outdated. If no item is passed, then all - * test owned by the TestController will be marked as outdated. - * - * Any test runs started before the moment this method is called, including - * runs which may still be ongoing, will be marked as outdated and deprioritized - * in the editor's UI. - * - * @param item Item to mark as outdated. If undefined, all the controller's items are marked outdated. - */ - invalidateTestResults(items?: TestItem | readonly TestItem[]): void; - } -} diff --git a/code/yarn.lock b/code/yarn.lock index 9000177351c..20d4607caef 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -10055,10 +10055,10 @@ typescript@^4.7.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== -typescript@^5.2.0-dev.20230712: - version "5.2.0-dev.20230712" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.0-dev.20230712.tgz#6da271394793fd1c1cba26800f43bb8c25825a87" - integrity sha512-F/VND6YrBGr8RvnmpFpTDC5sT0Hh5cuXFWurGEhSBjwXodW+POmfGU0uLn+s/Rl0+Yvffpd2WRrpYC7bSDQS/Q== +typescript@^5.2.0-dev.20230718: + version "5.2.0-dev.20230718" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.0-dev.20230718.tgz#8c60b2f6807b3f8b2db47980ee6c73dea1f45e42" + integrity sha512-ED1Vm+2UzdbtKui+0lVswEuAX94fQXeoghXyy/+aTNers8X/WB81r5sFg6nA4e43nVQ2MP/Qsa7/XJRFuHR+Cg== typical@^4.0.0: version "4.0.0"