Merge from vscode 7653d836944892f83ce9e1f95c1204bafa1aec31

This commit is contained in:
ADS Merger
2020-05-08 03:58:34 +00:00
parent dac1970c43
commit fa62ec1f34
209 changed files with 5131 additions and 2480 deletions

View File

@@ -172,6 +172,20 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess
updatePickerItems();
disposables.add(picker.onDidChangeValue(updatePickerItems));
let ignoreFirstActiveEvent = true;
disposables.add(picker.onDidChangeActive(() => {
const [entry] = picker.activeItems;
if (entry && entries[entry.index]) {
if (ignoreFirstActiveEvent) {
ignoreFirstActiveEvent = false;
return;
}
entries[entry.index]?.reveal();
}
}));
}).catch(err => {
onUnexpectedError(err);
picker.hide();

View File

@@ -350,7 +350,7 @@ class BreakpointsRenderer implements IListRenderer<IBreakpoint, IBreakpointTempl
data.filePath = dom.append(data.breakpoint, $('span.file-path'));
const lineNumberContainer = dom.append(data.breakpoint, $('.line-number-container'));
data.lineNumber = dom.append(lineNumberContainer, $('span.line-number'));
data.lineNumber = dom.append(lineNumberContainer, $('span.line-number.monaco-count-badge'));
return data;
}

View File

@@ -41,6 +41,8 @@ import { IOpenerService } from 'vs/platform/opener/common/opener';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { attachStylerCallback } from 'vs/platform/theme/common/styler';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { commonSuffixLength } from 'vs/base/common/strings';
import { posix } from 'vs/base/common/path';
const $ = dom.$;
@@ -78,6 +80,26 @@ export function getContextForContributedActions(element: CallStackItem | null):
return '';
}
export function getSpecificSourceName(stackFrame: IStackFrame): string {
// To reduce flashing of the path name and the way we fetch stack frames
// We need to compute the source name based on the other frames in the stale call stack
let callStack = (<Thread>stackFrame.thread).getStaleCallStack();
callStack = callStack.length > 0 ? callStack : stackFrame.thread.getCallStack();
const otherSources = callStack.map(sf => sf.source).filter(s => s !== stackFrame.source);
let suffixLength = 0;
otherSources.forEach(s => {
if (s.name === stackFrame.source.name) {
suffixLength = Math.max(suffixLength, commonSuffixLength(stackFrame.source.uri.path, s.uri.path));
}
});
if (suffixLength === 0) {
return stackFrame.source.name;
}
const from = Math.max(0, stackFrame.source.uri.path.lastIndexOf(posix.sep, stackFrame.source.uri.path.length - suffixLength - 1));
return (from > 0 ? '...' : '') + stackFrame.source.uri.path.substr(from);
}
export class CallStackView extends ViewPane {
private pauseMessage!: HTMLSpanElement;
private pauseMessageLabel!: HTMLSpanElement;
@@ -453,7 +475,7 @@ class SessionsRenderer implements ITreeRenderer<IDebugSession, FuzzyScore, ISess
dom.append(session, $('.codicon.codicon-bug'));
const name = dom.append(session, $('.name'));
const state = dom.append(session, $('.state'));
const stateLabel = dom.append(state, $('span.label'));
const stateLabel = dom.append(state, $('span.label.monaco-count-badge.long'));
const label = new HighlightedLabel(name, false);
const actionBar = new ActionBar(session, {
actionViewItemProvider: action => {
@@ -562,7 +584,7 @@ class StackFramesRenderer implements ITreeRenderer<IStackFrame, FuzzyScore, ISta
const file = dom.append(stackFrame, $('.file'));
const fileName = dom.append(file, $('span.file-name'));
const wrapper = dom.append(file, $('span.line-number-wrapper'));
const lineNumber = dom.append(wrapper, $('span.line-number'));
const lineNumber = dom.append(wrapper, $('span.line-number.monaco-count-badge'));
const label = new HighlightedLabel(labelDiv, false);
const actionBar = new ActionBar(stackFrame);
@@ -582,7 +604,7 @@ class StackFramesRenderer implements ITreeRenderer<IStackFrame, FuzzyScore, ISta
data.file.title += `\n${stackFrame.source.raw.origin}`;
}
data.label.set(stackFrame.name, createMatches(element.filterData), stackFrame.name);
data.fileName.textContent = stackFrame.getSpecificSourceName();
data.fileName.textContent = getSpecificSourceName(stackFrame);
if (stackFrame.range.startLineNumber !== undefined) {
data.lineNumber.textContent = `${stackFrame.range.startLineNumber}`;
if (stackFrame.range.startColumn) {
@@ -855,7 +877,7 @@ class CallStackAccessibilityProvider implements IListAccessibilityProvider<CallS
return nls.localize('threadAriaLabel', "Thread {0}, callstack, debug", (<Thread>element).name);
}
if (element instanceof StackFrame) {
return nls.localize('stackFrameAriaLabel', "Stack Frame {0}, line {1}, {2}, callstack, debug", element.name, element.range.startLineNumber, element.getSpecificSourceName());
return nls.localize('stackFrameAriaLabel', "Stack Frame {0}, line {1}, {2}, callstack, debug", element.name, element.range.startLineNumber, getSpecificSourceName(element));
}
if (isDebugSession(element)) {
return nls.localize('sessionLabel', "Debug Session {0}", element.getLabel());

View File

@@ -229,11 +229,6 @@ configurationRegistry.registerConfiguration({
default: 'openOnSessionStart',
description: nls.localize('openDebug', "Controls when the debug view should open.")
},
'debug.enableAllHovers': {
type: 'boolean',
description: nls.localize({ comment: ['This is the description for a setting'], key: 'enableAllHovers' }, "Controls whether the non-debug hovers should be enabled while debugging. When enabled the hover providers will be called to provide a hover. Regular hovers will not be shown even if this setting is enabled."),
default: false
},
'debug.showSubSessionsInToolBar': {
type: 'boolean',
description: nls.localize({ comment: ['This is the description for a setting'], key: 'showSubSessionsInToolBar' }, "Controls whether the debug sub-sessions are shown in the debug tool bar. When this setting is false the stop command on a sub-session will also stop the parent session."),

View File

@@ -24,7 +24,6 @@ import { InputFocusedContext } from 'vs/platform/contextkey/common/contextkeys';
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { PanelFocusContext } from 'vs/workbench/common/panel';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { onUnexpectedError } from 'vs/base/common/errors';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@@ -206,7 +205,7 @@ export function registerCommands(): void {
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.F5,
when: CONTEXT_IN_DEBUG_MODE,
handler: (accessor: ServicesAccessor, _: string, context: CallStackContext | unknown) => {
handler: async (accessor: ServicesAccessor, _: string, context: CallStackContext | unknown) => {
const debugService = accessor.get(IDebugService);
let session: IDebugSession | undefined;
if (isSessionContext(context)) {
@@ -217,10 +216,10 @@ export function registerCommands(): void {
if (!session) {
const { launch, name } = debugService.getConfigurationManager().selectedConfiguration;
debugService.startDebugging(launch, name, { noDebug: false });
await debugService.startDebugging(launch, name, { noDebug: false });
} else {
session.removeReplExpressions();
debugService.restartSession(session).then(undefined, onUnexpectedError);
await debugService.restartSession(session);
}
}
});
@@ -268,10 +267,15 @@ export function registerCommands(): void {
CommandsRegistry.registerCommand({
id: DISCONNECT_ID,
handler: (accessor: ServicesAccessor, sessionId: string | undefined) => {
handler: async (accessor: ServicesAccessor, _: string, context: CallStackContext | unknown) => {
const debugService = accessor.get(IDebugService);
const session = debugService.getModel().getSession(sessionId) || debugService.getViewModel().focusedSession;
debugService.stopSession(session).then(undefined, onUnexpectedError);
let session: IDebugSession | undefined;
if (isSessionContext(context)) {
session = debugService.getModel().getSession(context.sessionId);
} else {
session = debugService.getViewModel().focusedSession;
}
await debugService.stopSession(session);
}
});
@@ -280,7 +284,7 @@ export function registerCommands(): void {
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyMod.Shift | KeyCode.F5,
when: CONTEXT_IN_DEBUG_MODE,
handler: (accessor: ServicesAccessor, _: string, context: CallStackContext | unknown) => {
handler: async (accessor: ServicesAccessor, _: string, context: CallStackContext | unknown) => {
const debugService = accessor.get(IDebugService);
let session: IDebugSession | undefined;
if (isSessionContext(context)) {
@@ -296,7 +300,7 @@ export function registerCommands(): void {
session = session.parentSession;
}
debugService.stopSession(session).then(undefined, onUnexpectedError);
await debugService.stopSession(session);
}
});
@@ -336,9 +340,9 @@ export function registerCommands(): void {
CommandsRegistry.registerCommand({
id: 'debug.startFromConfig',
handler: (accessor, config: IConfig) => {
handler: async (accessor, config: IConfig) => {
const debugService = accessor.get(IDebugService);
debugService.startDebugging(undefined, config).then(undefined, onUnexpectedError);
await debugService.startDebugging(undefined, config);
}
});

View File

@@ -30,10 +30,8 @@ import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands';
import { first } from 'vs/base/common/arrays';
import { memoize, createMemoizer } from 'vs/base/common/decorators';
import { IEditorHoverOptions, EditorOption } from 'vs/editor/common/config/editorOptions';
import { CancellationToken } from 'vs/base/common/cancellation';
import { DebugHoverWidget } from 'vs/workbench/contrib/debug/browser/debugHover';
import { ITextModel } from 'vs/editor/common/model';
import { getHover } from 'vs/editor/contrib/hover/getHover';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { basename } from 'vs/base/common/path';
@@ -170,7 +168,6 @@ class DebugEditorContribution implements IDebugEditorContribution {
private toDispose: IDisposable[];
private hoverWidget: DebugHoverWidget;
private nonDebugHoverPosition: Position | undefined;
private hoverRange: Range | null = null;
private mouseDown = false;
private static readonly MEMOIZER = createMemoizer();
@@ -204,7 +201,6 @@ class DebugEditorContribution implements IDebugEditorContribution {
this.toDispose.push(this.editor.onMouseUp(() => this.mouseDown = false));
this.toDispose.push(this.editor.onMouseMove((e: IEditorMouseEvent) => this.onEditorMouseMove(e)));
this.toDispose.push(this.editor.onMouseLeave((e: IPartialEditorMouseEvent) => {
this.provideNonDebugHoverScheduler.cancel();
const hoverDomNode = this.hoverWidget.getDomNode();
if (!hoverDomNode) {
return;
@@ -315,24 +311,11 @@ class DebugEditorContribution implements IDebugEditorContribution {
return scheduler;
}
@memoize
private get provideNonDebugHoverScheduler(): RunOnceScheduler {
const scheduler = new RunOnceScheduler(() => {
if (this.editor.hasModel() && this.nonDebugHoverPosition) {
getHover(this.editor.getModel(), this.nonDebugHoverPosition, CancellationToken.None);
}
}, HOVER_DELAY);
this.toDispose.push(scheduler);
return scheduler;
}
private hideHoverWidget(): void {
if (!this.hideHoverScheduler.isScheduled() && this.hoverWidget.isVisible()) {
this.hideHoverScheduler.schedule();
}
this.showHoverScheduler.cancel();
this.provideNonDebugHoverScheduler.cancel();
}
// hover business
@@ -351,10 +334,6 @@ class DebugEditorContribution implements IDebugEditorContribution {
return;
}
if (this.configurationService.getValue<IDebugConfiguration>('debug').enableAllHovers && mouseEvent.target.position) {
this.nonDebugHoverPosition = mouseEvent.target.position;
this.provideNonDebugHoverScheduler.schedule();
}
const targetType = mouseEvent.target.type;
const stopKey = env.isMacintosh ? 'metaKey' : 'ctrlKey';

View File

@@ -66,10 +66,8 @@
/* Debug viewlet trees */
.debug-pane .line-number {
border-radius: 2px;
font-size: 0.9em;
padding: 0 3px;
line-height: 20px;
padding-left: 4px;
padding-right: 4px;
}
.debug-pane .disabled {
@@ -118,6 +116,8 @@
.debug-pane .debug-call-stack .thread > .state,
.debug-pane .debug-call-stack .session > .state {
display: flex;
align-items: center;
text-align: right;
overflow: hidden;
text-overflow: ellipsis;
@@ -159,9 +159,8 @@
.debug-pane .debug-call-stack .thread > .state > .label,
.debug-pane .debug-call-stack .session > .state > .label {
border-radius: 2px;
font-size: 0.8em;
padding: 0 3px;
min-height: auto;
}
.debug-pane .debug-call-stack .stack-frame {

View File

@@ -318,7 +318,6 @@ export interface IStackFrame extends ITreeElement {
readonly source: Source;
getScopes(): Promise<IScope[]>;
getMostSpecificScopes(range: IRange): Promise<ReadonlyArray<IScope>>;
getSpecificSourceName(): string;
forgetScopes(): void;
restart(): Promise<any>;
toString(): string;

View File

@@ -17,8 +17,6 @@ import {
IThread, IRawModelUpdate, IScope, IRawStoppedDetails, IEnablement, IBreakpointData, IExceptionInfo, IBreakpointsChangeEvent, IBreakpointUpdateData, IBaseBreakpoint, State, IDataBreakpoint
} from 'vs/workbench/contrib/debug/common/debug';
import { Source, UNKNOWN_SOURCE_LABEL, getUriFromSource } from 'vs/workbench/contrib/debug/common/debugSource';
import { commonSuffixLength } from 'vs/base/common/strings';
import { posix } from 'vs/base/common/path';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { ITextEditorPane } from 'vs/workbench/common/editor';
@@ -325,26 +323,6 @@ export class StackFrame implements IStackFrame {
return this.scopes;
}
getSpecificSourceName(): string {
// To reduce flashing of the path name and the way we fetch stack frames
// We need to compute the source name based on the other frames in the stale call stack
let callStack = (<Thread>this.thread).getStaleCallStack();
callStack = callStack.length > 0 ? callStack : this.thread.getCallStack();
const otherSources = callStack.map(sf => sf.source).filter(s => s !== this.source);
let suffixLength = 0;
otherSources.forEach(s => {
if (s.name === this.source.name) {
suffixLength = Math.max(suffixLength, commonSuffixLength(this.source.uri.path, s.uri.path));
}
});
if (suffixLength === 0) {
return this.source.name;
}
const from = Math.max(0, this.source.uri.path.lastIndexOf(posix.sep, this.source.uri.path.length - suffixLength - 1));
return (from > 0 ? '...' : '') + this.source.uri.path.substr(from);
}
async getMostSpecificScopes(range: IRange): Promise<IScope[]> {
const scopes = await this.getScopes();
const nonExpensiveScopes = scopes.filter(s => !s.expensive);

View File

@@ -14,7 +14,7 @@ import { IDebugSessionOptions, State } from 'vs/workbench/contrib/debug/common/d
import { NullOpenerService } from 'vs/platform/opener/common/opener';
import { createDecorationsForStackFrame } from 'vs/workbench/contrib/debug/browser/callStackEditorContribution';
import { Constants } from 'vs/base/common/uint';
import { getContext, getContextForContributedActions } from 'vs/workbench/contrib/debug/browser/callStackView';
import { getContext, getContextForContributedActions, getSpecificSourceName } from 'vs/workbench/contrib/debug/browser/callStackView';
import { getStackFrameThreadAndSessionToFocus } from 'vs/workbench/contrib/debug/browser/debugService';
import { generateUuid } from 'vs/base/common/uuid';
@@ -250,8 +250,8 @@ suite('Debug - CallStack', () => {
model.addSession(session);
const { firstStackFrame, secondStackFrame } = createTwoStackFrames(session);
assert.equal(firstStackFrame.getSpecificSourceName(), '.../b/c/d/internalModule.js');
assert.equal(secondStackFrame.getSpecificSourceName(), '.../x/c/d/internalModule.js');
assert.equal(getSpecificSourceName(firstStackFrame), '.../b/c/d/internalModule.js');
assert.equal(getSpecificSourceName(secondStackFrame), '.../x/c/d/internalModule.js');
});
test('stack frame toString()', () => {

View File

@@ -1629,11 +1629,11 @@ export class ShowInstalledExtensionsAction extends Action {
super(id, label, undefined, true);
}
run(): Promise<void> {
run(refresh?: boolean): Promise<void> {
return this.viewletService.openViewlet(VIEWLET_ID, true)
.then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer)
.then(viewlet => {
viewlet.search('@installed ');
viewlet.search('@installed ', refresh);
viewlet.focus();
});
}
@@ -2983,90 +2983,84 @@ export class InstallVSIXAction extends Action {
@IHostService private readonly hostService: IHostService,
@IFileDialogService private readonly fileDialogService: IFileDialogService,
@IExtensionService private readonly extensionService: IExtensionService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IConfigurationService private readonly configurationService: IConfigurationService, // {{SQL CARBON EDIT}}
@IStorageService private storageService: IStorageService
) {
super(id, label, 'extension-action install-vsix', true);
}
run(): Promise<any> {
async run(vsixPaths?: URI[]): Promise<void> {
// {{SQL CARBON EDIT}} - Replace run body
let extensionPolicy = this.configurationService.getValue<string>(ExtensionsPolicyKey);
if (extensionPolicy === ExtensionsPolicy.allowAll) {
return Promise.resolve(this.fileDialogService.showOpenDialog({
title: localize('installFromVSIX', "Install from VSIX"),
filters: [{ name: 'VSIX Extensions', extensions: ['vsix'] }],
canSelectFiles: true,
openLabel: mnemonicButtonLabel(localize({ key: 'installButton', comment: ['&& denotes a mnemonic'] }, "&&Install"))
}).then(result => {
if (!result) {
return Promise.resolve();
}
return Promise.all(result.map(vsix => {
if (!this.storageService.getBoolean(vsix.fsPath, StorageScope.GLOBAL)) {
if (!vsixPaths) {
vsixPaths = await this.fileDialogService.showOpenDialog({
title: localize('installFromVSIX', "Install from VSIX"),
filters: [{ name: 'VSIX Extensions', extensions: ['vsix'] }],
canSelectFiles: true,
openLabel: mnemonicButtonLabel(localize({ key: 'installButton', comment: ['&& denotes a mnemonic'] }, "&&Install"))
});
}
await Promise.all(vsixPaths.map(async vsix => {
if (!this.storageService.getBoolean(vsix.fsPath, StorageScope.GLOBAL)) {
const accept = await new Promise<boolean>(resolve => {
this.notificationService.prompt(
Severity.Warning,
localize('thirdPartyExtension.vsix', 'This is a third party extension and might involve security risks. Are you sure you want to install this extension?'),
[
{
label: localize('thirdPartExt.yes', 'Yes'),
run: () => {
this.extensionsWorkbenchService.install(vsix).then(extension => {
const requireReload = !(extension.local && this.extensionService.canAddExtension(toExtensionDescription(extension.local)));
const message = requireReload ? localize('InstallVSIXAction.successReload', "Please reload Azure Data Studio to complete installing the extension {0}.", extension.identifier.id)
: localize('InstallVSIXAction.success', "Completed installing the extension {0}.", extension.identifier.id);
const actions = requireReload ? [{
label: localize('InstallVSIXAction.reloadNow', "Reload Now"),
run: () => this.hostService.reload()
}] : [];
this.notificationService.prompt(
Severity.Info,
message,
actions,
{ sticky: true }
);
});
}
run: () => resolve(true)
},
{
label: localize('thirdPartyExt.no', 'No'),
run: () => { return Promise.resolve(); }
run: () => resolve(false)
},
{
label: localize('thirdPartyExt.dontShowAgain', 'Don\'t Show Again'),
isSecondary: true,
run: () => {
this.storageService.store(vsix.fsPath, true, StorageScope.GLOBAL);
return Promise.resolve();
resolve(true);
}
}
],
{ sticky: true }
);
} else {
this.extensionsWorkbenchService.install(vsix).then(extension => {
const requireReload = !(extension.local && this.extensionService.canAddExtension(toExtensionDescription(extension.local)));
const message = requireReload ? localize('InstallVSIXAction.successReload', "Please reload Azure Data Studio to complete installing the extension {0}.", extension.identifier.id)
: localize('InstallVSIXAction.success', "Completed installing the extension {0}.", extension.identifier.id);
const actions = requireReload ? [{
label: localize('InstallVSIXAction.reloadNow', "Reload Now"),
run: () => this.hostService.reload()
}] : [];
this.notificationService.prompt(
Severity.Info,
message,
actions,
{ sticky: true }
);
});
});
if (!accept) {
return undefined;
}
})).then(() => Promise.resolve());
}));
}
return this.extensionsWorkbenchService.install(vsix);
})).then(async (extensions) => {
for (const extension of extensions) {
if (!extension) {
return;
}
const requireReload = !(extension.local && this.extensionService.canAddExtension(toExtensionDescription(extension.local)));
const message = requireReload ? localize('InstallVSIXAction.successReload', "Please reload Visual Studio Code to complete installing the extension {0}.", extension.displayName || extension.name)
: localize('InstallVSIXAction.success', "Completed installing the extension {0}.", extension.displayName || extension.name);
const actions = requireReload ? [{
label: localize('InstallVSIXAction.reloadNow', "Reload Now"),
run: () => this.hostService.reload()
}] : [];
this.notificationService.prompt(
Severity.Info,
message,
actions,
{ sticky: true }
);
}
await this.instantiationService.createInstance(ShowInstalledExtensionsAction, ShowInstalledExtensionsAction.ID, ShowInstalledExtensionsAction.LABEL).run(true);
});
} else {
this.notificationService.error(localize('InstallVSIXAction.allowNone', 'Your extension policy does not allow downloading extensions. Please change your extension policy and try again.'));
return Promise.resolve();
}
// {{SQL CARBON EDIT}} - End
}
get enabled(): boolean { // {{SQL CARBON EDIT}} add enabled logic

View File

@@ -14,7 +14,7 @@ import { IAction, Action } from 'vs/base/common/actions';
import { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { IViewlet } from 'vs/workbench/common/viewlet';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { append, $, addClass, toggleClass, Dimension } from 'vs/base/browser/dom';
import { append, $, addClass, toggleClass, Dimension, hide, show } from 'vs/base/browser/dom';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
@@ -57,6 +57,9 @@ import { ILabelService } from 'vs/platform/label/common/label';
import { MementoObject } from 'vs/workbench/common/memento';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
import { DragAndDropObserver } from 'vs/workbench/browser/dnd';
import { URI } from 'vs/base/common/uri';
import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme';
const NonEmptyWorkspaceContext = new RawContextKey<boolean>('nonEmptyWorkspace', false);
const DefaultViewsContext = new RawContextKey<boolean>('defaultExtensionViews', true);
@@ -400,8 +403,12 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
addClass(parent, 'extensions-viewlet');
this.root = parent;
const header = append(this.root, $('.header'));
const overlay = append(this.root, $('.overlay'));
const overlayBackgroundColor = this.getColor(SIDE_BAR_DRAG_AND_DROP_BACKGROUND) ?? '';
overlay.style.backgroundColor = overlayBackgroundColor;
hide(overlay);
const header = append(this.root, $('.header'));
const placeholder = localize('searchExtensions', "Search Extensions in Marketplace");
const searchValue = this.searchViewletState['query.value'] ? this.searchViewletState['query.value'] : '';
@@ -435,6 +442,49 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
}
}));
// Register DragAndDrop support
this._register(new DragAndDropObserver(this.root, {
onDragEnd: (e: DragEvent) => undefined,
onDragEnter: (e: DragEvent) => {
if (this.isSupportedDragElement(e)) {
show(overlay);
}
},
onDragLeave: (e: DragEvent) => {
if (this.isSupportedDragElement(e)) {
hide(overlay);
}
},
onDragOver: (e: DragEvent) => {
if (e.dataTransfer) {
e.dataTransfer.dropEffect = this.isSupportedDragElement(e) ? 'copy' : 'none';
}
},
onDrop: async (e: DragEvent) => {
if (this.isSupportedDragElement(e)) {
hide(overlay);
if (e.dataTransfer && e.dataTransfer.files.length > 0) {
let vsixPaths: URI[] = [];
for (let index = 0; index < e.dataTransfer.files.length; index++) {
const path = e.dataTransfer.files.item(index)!.path;
if (path.indexOf('.vsix') !== -1) {
vsixPaths.push(URI.parse(path));
}
}
try {
// Attempt to install the extension(s)
await this.instantiationService.createInstance(InstallVSIXAction, InstallVSIXAction.ID, InstallVSIXAction.LABEL).run(vsixPaths);
}
catch (err) {
this.notificationService.error(err);
}
}
}
},
}));
super.create(append(this.root, $('.extensions')));
}
@@ -622,6 +672,15 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE
this.notificationService.error(err);
}
private isSupportedDragElement(e: DragEvent): boolean {
if (e.dataTransfer) {
const typesLowerCase = e.dataTransfer.types.map(t => t.toLocaleLowerCase());
return typesLowerCase.indexOf('files') !== -1;
}
return false;
}
}
export class StatusUpdater extends Disposable implements IWorkbenchContribution {

View File

@@ -4,9 +4,19 @@
*--------------------------------------------------------------------------------------------*/
.extensions-viewlet {
position: relative;
height: 100%;
}
.extensions-viewlet > .overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
}
.extensions-viewlet > .header {
height: 41px;
box-sizing: border-box;

View File

@@ -60,10 +60,9 @@
}
.dirty-count.monaco-count-badge {
padding: 1px 6px 2px;
padding: 2px 4px;
margin-left: 6px;
min-height: auto;
border-radius: 0; /* goes better when ellipsis shows up on narrow sidebar */
}
.explorer-viewlet .explorer-item.nonexistent-root {

View File

@@ -890,7 +890,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
const items = FileDragAndDrop.getStatsFromDragAndDropData(data as ElementsDragAndDropData<ExplorerItem, ExplorerItem[]>, originalEvent);
if (items && items.length && originalEvent.dataTransfer) {
// Apply some datatransfer types to allow for dragging the element outside of the application
this.instantiationService.invokeFunction(fillResourceDataTransfers, items, originalEvent);
this.instantiationService.invokeFunction(fillResourceDataTransfers, items, undefined, originalEvent);
// The only custom data transfer we set from the explorer is a file transfer
// to be able to DND between multiple code file explorers across windows
@@ -965,7 +965,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop<ExplorerItem> {
private async handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise<void> {
const droppedResources = extractResources(originalEvent, true);
// Check for dropped external files to be folders
const result = await this.fileService.resolveAll(droppedResources);
const result = await this.fileService.resolveAll(droppedResources.map(droppedResource => ({ resource: droppedResource.resource })));
// Pass focus to window
this.hostService.focus();

View File

@@ -184,7 +184,7 @@ export class OpenEditorsView extends ViewPane {
super.renderHeaderTitle(container, this.title);
const count = dom.append(container, $('.count'));
this.dirtyCountElement = dom.append(count, $('.dirty-count.monaco-count-badge'));
this.dirtyCountElement = dom.append(count, $('.dirty-count.monaco-count-badge.long'));
this._register((attachStylerCallback(this.themeService, { badgeBackground, badgeForeground, contrastBorder }, colors => {
const background = colors.badgeBackground ? colors.badgeBackground.toString() : '';
@@ -660,7 +660,7 @@ class OpenEditorsDragAndDrop implements IListDragAndDrop<OpenEditor | IEditorGro
if (resources.length) {
// Apply some datatransfer types to allow for dragging the element outside of the application
this.instantiationService.invokeFunction(fillResourceDataTransfers, resources, originalEvent);
this.instantiationService.invokeFunction(fillResourceDataTransfers, resources, undefined, originalEvent);
}
}

View File

@@ -849,7 +849,7 @@ export class ResourceDragAndDrop implements ITreeDragAndDrop<TreeElement> {
if (resources.length) {
// Apply some datatransfer types to allow for dragging the element outside of the application
this.instantiationService.invokeFunction(fillResourceDataTransfers, resources, originalEvent);
this.instantiationService.invokeFunction(fillResourceDataTransfers, resources, undefined, originalEvent);
}
}

View File

@@ -540,7 +540,12 @@ registerAction2(class extends InsertCellCommand {
id: INSERT_CODE_CELL_ABOVE_COMMAND_ID,
title: localize('notebookActions.insertCodeCellAbove', "Insert Code Cell Above"),
category: NOTEBOOK_ACTIONS_CATEGORY,
f1: true
f1: true,
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter,
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()),
weight: KeybindingWeight.WorkbenchContrib
}
},
CellKind.Code,
'above');
@@ -572,7 +577,12 @@ registerAction2(class extends InsertCellCommand {
title: localize('notebookActions.insertCodeCellBelow', "Insert Code Cell Below"),
category: NOTEBOOK_ACTIONS_CATEGORY,
icon: { id: 'codicon/add' },
f1: true
f1: true,
keybinding: {
primary: KeyMod.CtrlCmd | KeyCode.Enter,
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()),
weight: KeybindingWeight.WorkbenchContrib
}
},
CellKind.Code,
'below');
@@ -770,7 +780,12 @@ registerAction2(class extends Action2 {
title: localize('notebookActions.moveCellUp', "Move Cell Up"),
category: NOTEBOOK_ACTIONS_CATEGORY,
icon: { id: 'codicon/arrow-up' },
f1: true
f1: true,
keybinding: {
primary: KeyMod.Alt | KeyCode.UpArrow,
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()),
weight: KeybindingWeight.WorkbenchContrib
}
});
}
@@ -794,7 +809,12 @@ registerAction2(class extends Action2 {
title: localize('notebookActions.moveCellDown', "Move Cell Down"),
category: NOTEBOOK_ACTIONS_CATEGORY,
icon: { id: 'codicon/arrow-down' },
f1: true
f1: true,
keybinding: {
primary: KeyMod.Alt | KeyCode.DownArrow,
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()),
weight: KeybindingWeight.WorkbenchContrib
}
});
}
@@ -969,7 +989,12 @@ registerAction2(class extends Action2 {
id: COPY_CELL_UP_COMMAND_ID,
title: localize('notebookActions.copyCellUp', "Copy Cell Up"),
category: NOTEBOOK_ACTIONS_CATEGORY,
f1: true
f1: true,
keybinding: {
primary: KeyMod.Alt | KeyMod.Shift | KeyCode.UpArrow,
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()),
weight: KeybindingWeight.WorkbenchContrib
}
});
}
@@ -992,7 +1017,12 @@ registerAction2(class extends Action2 {
id: COPY_CELL_DOWN_COMMAND_ID,
title: localize('notebookActions.copyCellDown', "Copy Cell Down"),
category: NOTEBOOK_ACTIONS_CATEGORY,
f1: true
f1: true,
keybinding: {
primary: KeyMod.Alt | KeyMod.Shift | KeyCode.DownArrow,
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()),
weight: KeybindingWeight.WorkbenchContrib
}
});
}

View File

@@ -21,6 +21,10 @@
overflow: visible !important;
} */
.monaco-workbench .part.editor > .content .notebook-editor .simple-fr-find-part-wrapper.visible {
z-index: 100;
}
.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container .overflowingContentWidgets > div {
z-index: 600 !important;
/* @rebornix: larger than the editor title bar */

View File

@@ -110,7 +110,6 @@ export interface ICellViewModel {
resolveTextModel(): Promise<ITextModel>;
getEvaluatedMetadata(documentMetadata: NotebookDocumentMetadata | undefined): NotebookCellMetadata;
getSelectionsStartPosition(): IPosition[] | undefined;
getLinesContent(): string[];
}
export interface IEditableCellViewModel extends ICellViewModel {

View File

@@ -47,6 +47,7 @@ import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor
import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions';
import { onUnexpectedError } from 'vs/base/common/errors';
import { IPosition, Position } from 'vs/editor/common/core/position';
import { IReadonlyTextBuffer } from 'vs/editor/common/model';
const $ = DOM.$;
const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState';
@@ -170,19 +171,20 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor {
return true;
}
private updateEditorFocus() {
// Note - focus going to the webview will fire 'blur', but the webview element will be
// a descendent of the notebook editor root.
this.editorFocus?.set(DOM.isAncestor(document.activeElement, this.getDomNode()));
}
protected createEditor(parent: HTMLElement): void {
this._rootElement = DOM.append(parent, $('.notebook-editor'));
this.createBody(this._rootElement);
this.generateFontInfo();
this.editorFocus = NOTEBOOK_EDITOR_FOCUSED.bindTo(this.contextKeyService);
this.editorFocus.set(true);
this._register(this.onDidFocus(() => {
this.editorFocus?.set(true);
}));
this._register(this.onDidBlur(() => {
this.editorFocus?.set(false);
}));
this._register(this.onDidFocus(() => this.updateEditorFocus()));
this._register(this.onDidBlur(() => this.updateEditorFocus()));
this.editorEditable = NOTEBOOK_EDITOR_EDITABLE.bindTo(this.contextKeyService);
this.editorEditable.set(true);
@@ -307,6 +309,8 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor {
this.control = new NotebookCodeEditors(this.list, this.renderedEditors);
this.webview = this.instantiationService.createInstance(BackLayerWebView, this);
this.webview.webview.onDidBlur(() => this.updateEditorFocus());
this.webview.webview.onDidFocus(() => this.updateEditorFocus());
this._register(this.webview.onMessage(message => {
if (this.viewModel) {
this.notebookService.onDidReceiveMessage(this.viewModel.viewType, this.viewModel.uri, message);
@@ -778,11 +782,6 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor {
return newCell;
}
private isAtEOL(p: IPosition, lines: string[]) {
const line = lines[p.lineNumber - 1];
return line.length + 1 === p.column;
}
private pushIfAbsent(positions: IPosition[], p: IPosition) {
const last = positions.length > 0 ? positions[positions.length - 1] : undefined;
if (!last || last.lineNumber !== p.lineNumber || last.column !== p.column) {
@@ -795,8 +794,12 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor {
* Move end of line split points to the beginning of the next line;
* Avoid duplicate split points
*/
private splitPointsToBoundaries(splitPoints: IPosition[], lines: string[]): IPosition[] | null {
private splitPointsToBoundaries(splitPoints: IPosition[], textBuffer: IReadonlyTextBuffer): IPosition[] | null {
const boundaries: IPosition[] = [];
const lineCnt = textBuffer.getLineCount();
const getLineLen = (lineNumber: number) => {
return textBuffer.getLineLength(lineNumber);
};
// split points need to be sorted
splitPoints = splitPoints.sort((l, r) => {
@@ -809,22 +812,21 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor {
this.pushIfAbsent(boundaries, new Position(1, 1));
for (let sp of splitPoints) {
if (this.isAtEOL(sp, lines) && sp.lineNumber < lines.length) {
if (getLineLen(sp.lineNumber) + 1 === sp.column && sp.lineNumber < lineCnt) {
sp = new Position(sp.lineNumber + 1, 1);
}
this.pushIfAbsent(boundaries, sp);
}
// eat-up any split point at the beginning, i.e. we ignore the split point at the very end
this.pushIfAbsent(boundaries, new Position(lines.length, lines[lines.length - 1].length + 1));
this.pushIfAbsent(boundaries, new Position(lineCnt, getLineLen(lineCnt) + 1));
// if we only have two then they describe the whole range and nothing needs to be split
return boundaries.length > 2 ? boundaries : null;
}
private computeCellLinesContents(cell: IEditableCellViewModel, splitPoints: IPosition[]): string[] | null {
const lines = cell.getLinesContent();
const rangeBoundaries = this.splitPointsToBoundaries(splitPoints, lines);
const rangeBoundaries = this.splitPointsToBoundaries(splitPoints, cell.textBuffer);
if (!rangeBoundaries) {
return null;
}

View File

@@ -3,13 +3,14 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { EditorInput, IEditorInput, GroupIdentifier, ISaveOptions } from 'vs/workbench/common/editor';
import { EditorInput, IEditorInput, GroupIdentifier, ISaveOptions, IMoveResult, IRevertOptions } from 'vs/workbench/common/editor';
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
import { URI } from 'vs/base/common/uri';
import { isEqual } from 'vs/base/common/resources';
import { isEqual, basename } from 'vs/base/common/resources';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel';
import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
export class NotebookEditorInput extends EditorInput {
@@ -39,7 +40,10 @@ export class NotebookEditorInput extends EditorInput {
public name: string,
public readonly viewType: string | undefined,
@INotebookService private readonly notebookService: INotebookService,
@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService
@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService,
@IFileDialogService private readonly fileDialogService: IFileDialogService,
// @IEditorService private readonly editorService: IEditorService,
@IInstantiationService private readonly instantiationService: IInstantiationService
) {
super();
}
@@ -85,6 +89,48 @@ export class NotebookEditorInput extends EditorInput {
return undefined;
}
async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise<IEditorInput | undefined> {
if (!this.textModel) {
return undefined;
}
const dialogPath = this.textModel.resource;
const target = await this.fileDialogService.pickFileToSave(dialogPath, options?.availableFileSystems);
if (!target) {
return undefined; // save cancelled
}
if (!await this.textModel.saveAs(target)) {
return undefined;
}
return this._move(group, target)?.editor;
}
move(group: GroupIdentifier, target: URI): IMoveResult | undefined {
if (this.textModel) {
const contributedNotebookProviders = this.notebookService.getContributedNotebookProviders(target);
if (contributedNotebookProviders.find(provider => provider.id === this.textModel!.viewType)) {
return this._move(group, target);
}
}
return undefined;
}
_move(group: GroupIdentifier, newResource: URI): { editor: IEditorInput } | undefined {
const editorInput = NotebookEditorInput.getOrCreate(this.instantiationService, newResource, basename(newResource), this.viewType);
return { editor: editorInput };
}
async revert(group: GroupIdentifier, options?: IRevertOptions): Promise<void> {
if (this.textModel) {
await this.textModel.revert(options);
}
return;
}
async resolve(): Promise<NotebookEditorModel> {
if (!await this.notebookService.canResolve(this.viewType!)) {
throw new Error(`Cannot open notebook of type '${this.viewType}'`);
@@ -96,6 +142,10 @@ export class NotebookEditorInput extends EditorInput {
this._onDidChangeDirty.fire();
}));
if (this.textModel.isDirty()) {
this._onDidChangeDirty.fire();
}
return this.textModel;
}

View File

@@ -11,7 +11,7 @@ import { notebookProviderExtensionPoint, notebookRendererExtensionPoint } from '
import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider';
import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol';
import { Emitter, Event } from 'vs/base/common/event';
import { INotebookTextModel, INotebookMimeTypeSelector, INotebookRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotebookTextModel, INotebookMimeTypeSelector, INotebookRendererInfo, NotebookDocumentMetadata, CellEditType, ICellDto2 } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer';
import { Iterable } from 'vs/base/common/iterator';
@@ -202,13 +202,13 @@ export class NotebookService extends Disposable implements INotebookService, ICu
return undefined; // {{SQL CARBON EDIT}} strict-null-check
}
async resolveNotebook(viewType: string, uri: URI): Promise<NotebookTextModel | undefined> {
async createNotebookFromBackup(viewType: string, uri: URI, metadata: NotebookDocumentMetadata, languages: string[], cells: ICellDto2[]): Promise<NotebookTextModel | undefined> {
const provider = this._notebookProviders.get(viewType);
if (!provider) {
return undefined;
}
const notebookModel = await provider.controller.resolveNotebook(viewType, uri);
const notebookModel = await provider.controller.createNotebook(viewType, uri, true, false);
if (!notebookModel) {
return undefined;
}
@@ -219,6 +219,39 @@ export class NotebookService extends Disposable implements INotebookService, ICu
notebookModel,
(model) => this._onWillDispose(model),
);
this._models[modelId] = modelData;
notebookModel.metadata = metadata;
notebookModel.languages = languages;
notebookModel.applyEdit(notebookModel.versionId, [
{
editType: CellEditType.Insert,
index: 0,
cells: cells
}
]);
return modelData.model;
}
async resolveNotebook(viewType: string, uri: URI, forceReload: boolean): Promise<NotebookTextModel | undefined> {
const provider = this._notebookProviders.get(viewType);
if (!provider) {
return undefined;
}
let notebookModel: NotebookTextModel | undefined;
notebookModel = await provider.controller.createNotebook(viewType, uri, false, forceReload);
// new notebook model created
const modelId = MODEL_ID(uri);
const modelData = new ModelData(
notebookModel!,
(model) => this._onWillDispose(model),
);
this._models[modelId] = modelData;
return modelData.model;
}
@@ -265,7 +298,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu
let provider = this._notebookProviders.get(viewType);
if (provider) {
provider.controller.destoryNotebookDocument(notebook);
provider.controller.removeNotebookDocument(notebook);
}
}
@@ -291,6 +324,16 @@ export class NotebookService extends Disposable implements INotebookService, ICu
return false;
}
async saveAs(viewType: string, resource: URI, target: URI, token: CancellationToken): Promise<boolean> {
let provider = this._notebookProviders.get(viewType);
if (provider) {
return provider.controller.saveAs(resource, target, token);
}
return false;
}
onDidReceiveMessage(viewType: string, uri: URI, message: any): void {
let provider = this._notebookProviders.get(viewType);

View File

@@ -157,7 +157,7 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
for (let i = diff.start; i < diff.start + diff.deleteCount; i++) {
const cell = this.element(i);
if (this._viewModel!.hasCell(cell)) {
if (this._viewModel!.hasCell(cell.handle)) {
hideOutputs.push(...cell?.model.outputs);
} else {
deletedOutputs.push(...cell?.model.outputs);
@@ -177,7 +177,7 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
for (let i = diff.start; i < diff.start + diff.deleteCount; i++) {
const cell = this.element(i);
if (this._viewModel!.hasCell(cell)) {
if (this._viewModel!.hasCell(cell.handle)) {
hideOutputs.push(...cell?.model.outputs);
} else {
deletedOutputs.push(...cell?.model.outputs);
@@ -299,7 +299,7 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
for (let i = diff.start; i < diff.start + diff.deleteCount; i++) {
const cell = this.element(i);
if (this._viewModel!.hasCell(cell)) {
if (this._viewModel!.hasCell(cell.handle)) {
hideOutputs.push(...cell?.model.outputs);
} else {
deletedOutputs.push(...cell?.model.outputs);
@@ -316,6 +316,18 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
splice2(start: number, deleteCount: number, elements: CellViewModel[] = []): void {
// we need to convert start and delete count based on hidden ranges
super.splice(start, deleteCount, elements);
const selectionsLeft = [];
this._viewModel!.selectionHandles.forEach(handle => {
if (this._viewModel!.hasCell(handle)) {
selectionsLeft.push(handle);
}
});
if (!selectionsLeft.length && this._viewModel!.viewCells) {
// after splice, the selected cells are deleted
this._viewModel!.selectionHandles = [this._viewModel!.viewCells[0].handle];
}
}
getViewIndex(cell: ICellViewModel) {

View File

@@ -250,7 +250,6 @@ export class StatefullMarkdownCell extends Disposable {
bindEditorListeners(model: ITextModel, dimension?: IDimension) {
this.localDisposables.add(model.onDidChangeContent(() => {
// we don't need to update view cell text anymore as the textbuffer is shared
// this.viewCell.setText(model.getLinesContent());
this.viewCell.clearHTML();
let clientHeight = this.markdownContainer.clientHeight;
this.markdownContainer.innerHTML = '';

View File

@@ -205,25 +205,6 @@ export abstract class BaseCellViewModel extends Disposable {
return this.model.getValue();
}
getLinesContent(): string[] {
if (this._textModel) {
return this._textModel.getLinesContent();
}
return this.model.textBuffer.getLinesContent();
}
// setLinesContent(value: string[]) {
// if (this._textModel) {
// // TODO @rebornix we should avoid creating a new string here
// return this._textModel.setValue(value.join('\n'));
// } else {
// const range = this.model.getFullModelRange();
// this.model.textBuffer.
// this.model.source = value;
// }
// }
private saveViewState(): void {
if (!this._textEditor) {
return;

View File

@@ -268,7 +268,12 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
});
diffs.reverse().forEach(diff => {
this._viewCells.splice(diff[0], diff[1], ...diff[2]);
const deletedCells = this._viewCells.splice(diff[0], diff[1], ...diff[2]);
deletedCells.forEach(cell => {
this._handleToViewCellMapping.delete(cell.handle);
});
diff[2].forEach(cell => {
this._handleToViewCellMapping.set(cell.handle, cell);
this._localStore.add(cell);
@@ -456,8 +461,8 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
return index + 1;
}
hasCell(cell: ICellViewModel) {
return this._handleToViewCellMapping.has(cell.handle);
hasCell(handle: number) {
return this._handleToViewCellMapping.has(handle);
}
getVersionId() {
@@ -586,7 +591,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
this._viewCells.splice(deleteIndex, 1);
this._handleToViewCellMapping.delete(deleteCell.handle);
this._notebook.removeCell(deleteIndex);
this._notebook.removeCell(deleteIndex, 1);
this._onDidChangeViewCells.fire({ synchronous: true, splices: [[deleteIndex, 1, []]] });
}
@@ -638,7 +643,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
this._viewCells.splice(index, 1);
this._handleToViewCellMapping.delete(viewCell.handle);
this._notebook.removeCell(index);
this._notebook.removeCell(index, 1);
let endSelections: number[] = [];
if (this.selectionHandles.length) {

View File

@@ -7,7 +7,8 @@ import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellTextModelSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, ICellInsertEdit, NotebookCellsChangedEvent, CellKind, IOutput, notebookDocumentMetadataDefaults, diff, ICellDeleteEdit, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellTextModelSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, ICellInsertEdit, NotebookCellsChangedEvent, CellKind, IOutput, notebookDocumentMetadataDefaults, diff, ICellDeleteEdit, NotebookCellsChangeType, ICellDto2 } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { ITextSnapshot } from 'vs/editor/common/model';
function compareRangesUsingEnds(a: [number, number], b: [number, number]): number {
if (a[1] === b[1]) {
@@ -17,6 +18,49 @@ function compareRangesUsingEnds(a: [number, number], b: [number, number]): numbe
return a[1] - b[1];
}
export class NotebookTextModelSnapshot implements ITextSnapshot {
// private readonly _pieces: Ce[] = [];
private _index: number = -1;
constructor(private _model: NotebookTextModel) {
// for (let i = 0; i < this._model.cells.length; i++) {
// const cell = this._model.cells[i];
// this._pieces.push(this._model.cells[i].textBuffer.createSnapshot(true));
// }
}
read(): string | null {
if (this._index === -1) {
this._index++;
return `{ "metadata": ${JSON.stringify(this._model.metadata)}, "languages": ${JSON.stringify(this._model.languages)}, "cells": [`;
}
if (this._index < this._model.cells.length) {
const cell = this._model.cells[this._index];
const data = {
source: cell.getValue(),
metadata: cell.metadata,
cellKind: cell.cellKind,
language: cell.language
};
const rawStr = JSON.stringify(data);
const isLastCell = this._index === this._model.cells.length - 1;
this._index++;
return isLastCell ? rawStr : (rawStr + ',');
} else if (this._index === this._model.cells.length) {
this._index++;
return `]}`;
} else {
return null;
}
}
}
export class NotebookTextModel extends Disposable implements INotebookTextModel {
private static _cellhandlePool: number = 0;
@@ -77,6 +121,18 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel
return new NotebookCellTextModel(cellUri, cellHandle, source, language, cellKind, outputs || [], metadata);
}
initialize(cells: ICellDto2[]) {
this.cells = [];
this._versionId = 0;
const mainCells = cells.map(cell => {
const cellHandle = NotebookTextModel._cellhandlePool++;
const cellUri = CellUri.generate(this.uri, cellHandle);
return new NotebookCellTextModel(cellUri, cellHandle, cell.source, cell.language, cell.cellKind, cell.outputs || [], cell.metadata);
});
this.insertNewCell(0, mainCells);
}
applyEdit(modelVersionId: number, rawEdits: ICellEditOperation[]): boolean {
if (modelVersionId !== this._versionId) {
return false;
@@ -127,7 +183,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel
this.insertNewCell(insertEdit.index, mainCells);
break;
case CellEditType.Delete:
this.removeCell(operations[i].index);
this.removeCell(operations[i].index, operations[i].end - operations[i].start);
break;
}
}
@@ -142,6 +198,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel
return true;
}
createSnapshot(preserveBOM?: boolean): ITextSnapshot {
return new NotebookTextModelSnapshot(this);
}
private _increaseVersionId(): void {
this._versionId = this._versionId + 1;
}
@@ -250,17 +310,19 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel
return;
}
removeCell(index: number) {
removeCell(index: number, count: number) {
this._isUntitled = false;
let cell = this.cells[index];
this._cellListeners.get(cell.handle)?.dispose();
this._cellListeners.delete(cell.handle);
this.cells.splice(index, 1);
for (let i = index; i < index + count; i++) {
let cell = this.cells[i];
this._cellListeners.get(cell.handle)?.dispose();
this._cellListeners.delete(cell.handle);
}
this.cells.splice(index, count);
this._onDidChangeContent.fire();
this._increaseVersionId();
this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.ModelChange, versionId: this._versionId, changes: [[index, 1, []]] });
this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.ModelChange, versionId: this._versionId, changes: [[index, count, []]] });
}
moveCellToIdx(index: number, newIdx: number) {

View File

@@ -273,7 +273,7 @@ export enum CellEditType {
}
export interface ICellDto2 {
source: string[];
source: string | string[];
language: string;
cellKind: CellKind;
outputs: IOutput[];
@@ -300,6 +300,13 @@ export interface INotebookEditData {
renderers: number[];
}
export interface NotebookDataDto {
readonly cells: ICellDto2[];
readonly languages: string[];
readonly metadata: NotebookDocumentMetadata;
}
export namespace CellUri {
export const scheme = 'vscode-notebook';

View File

@@ -15,6 +15,8 @@ import { URI } from 'vs/base/common/uri';
import { IWorkingCopyService, IWorkingCopy, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { basename } from 'vs/base/common/resources';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { DefaultEndOfLine, ITextBuffer, EndOfLinePreference } from 'vs/editor/common/model';
export interface INotebookEditorModelManager {
models: NotebookEditorModel[];
@@ -24,6 +26,13 @@ export interface INotebookEditorModelManager {
get(resource: URI): NotebookEditorModel | undefined;
}
export interface INotebookRevertOptions {
/**
* Go to disk bypassing any cache of the model if any.
*/
forceReadFromDisk?: boolean;
}
export class NotebookEditorModel extends EditorModel implements IWorkingCopy, INotebookEditorModel {
private _dirty = false;
@@ -47,7 +56,8 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN
public readonly resource: URI,
public readonly viewType: string,
@INotebookService private readonly notebookService: INotebookService,
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
@IBackupFileService private readonly backupFileService: IBackupFileService
) {
super();
this._register(this.workingCopyService.registerWorkingCopy(this));
@@ -56,28 +66,91 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN
capabilities = 0;
async backup(): Promise<IWorkingCopyBackup> {
return {};
return { content: this._notebook.createSnapshot(true) };
}
async revert(options?: IRevertOptions | undefined): Promise<void> {
if (options?.soft) {
await this.backupFileService.discardBackup(this.resource);
return;
}
await this.load({ forceReadFromDisk: true });
this._dirty = false;
this._onDidChangeDirty.fire();
return;
}
async load(): Promise<NotebookEditorModel> {
const notebook = await this.notebookService.resolveNotebook(this.viewType!, this.resource);
async load(options?: INotebookRevertOptions): Promise<NotebookEditorModel> {
if (options?.forceReadFromDisk) {
return this.loadFromProvider(true);
}
if (this.isResolved()) {
return this;
}
const backup = await this.backupFileService.resolve(this.resource);
if (this.isResolved()) {
return this; // Make sure meanwhile someone else did not succeed in loading
}
if (backup) {
try {
return await this.loadFromBackup(backup.value.create(DefaultEndOfLine.LF));
} catch (error) {
// this.logService.error('[text file model] load() from backup', error); // ignore error and continue to load as file below
}
}
return this.loadFromProvider(false);
}
private async loadFromBackup(content: ITextBuffer): Promise<NotebookEditorModel> {
const fullRange = content.getRangeAt(0, content.getLength());
const data = JSON.parse(content.getValueInRange(fullRange, EndOfLinePreference.LF));
const notebook = await this.notebookService.createNotebookFromBackup(this.viewType!, this.resource, data.metadata, data.languages, data.cells);
this._notebook = notebook!;
this._name = basename(this._notebook!.uri);
this._register(this._notebook.onDidChangeContent(() => {
this._dirty = true;
this._onDidChangeDirty.fire();
this.setDirty(true);
this._onDidChangeContent.fire();
}));
await this.backupFileService.discardBackup(this.resource);
this.setDirty(true);
return this;
}
private async loadFromProvider(forceReloadFromDisk: boolean) {
const notebook = await this.notebookService.resolveNotebook(this.viewType!, this.resource, forceReloadFromDisk);
this._notebook = notebook!;
this._name = basename(this._notebook!.uri);
this._register(this._notebook.onDidChangeContent(() => {
this.setDirty(true);
this._onDidChangeContent.fire();
}));
return this;
}
isResolved(): boolean {
return !!this._notebook;
}
setDirty(newState: boolean) {
if (this._dirty !== newState) {
this._dirty = newState;
this._onDidChangeDirty.fire();
}
}
isDirty() {
return this._dirty;
}
@@ -89,6 +162,14 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN
this._onDidChangeDirty.fire();
return true;
}
async saveAs(targetResource: URI): Promise<boolean> {
const tokenSource = new CancellationTokenSource();
await this.notebookService.saveAs(this.notebook.viewType, this.notebook.uri, targetResource, tokenSource.token);
this._dirty = false;
this._onDidChangeDirty.fire();
return true;
}
}
export class NotebookEditorModelManager extends Disposable implements INotebookEditorModelManager {

View File

@@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri';
import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider';
import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol';
import { Event } from 'vs/base/common/event';
import { INotebookTextModel, INotebookMimeTypeSelector, INotebookRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotebookTextModel, INotebookMimeTypeSelector, INotebookRendererInfo, NotebookDocumentMetadata, ICellDto2 } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
import { CancellationToken } from 'vs/base/common/cancellation';
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
@@ -17,12 +17,13 @@ import { INotebookEditorModelManager } from 'vs/workbench/contrib/notebook/commo
export const INotebookService = createDecorator<INotebookService>('notebookService');
export interface IMainNotebookController {
resolveNotebook(viewType: string, uri: URI): Promise<NotebookTextModel | undefined>;
createNotebook(viewType: string, uri: URI, forBackup: boolean, forceReload: boolean): Promise<NotebookTextModel | undefined>;
executeNotebook(viewType: string, uri: URI, token: CancellationToken): Promise<void>;
onDidReceiveMessage(uri: URI, message: any): void;
executeNotebookCell(uri: URI, handle: number, token: CancellationToken): Promise<void>;
destoryNotebookDocument(notebook: INotebookTextModel): Promise<void>;
removeNotebookDocument(notebook: INotebookTextModel): Promise<void>;
save(uri: URI, token: CancellationToken): Promise<boolean>;
saveAs(uri: URI, target: URI, token: CancellationToken): Promise<boolean>;
}
export interface INotebookService {
@@ -35,7 +36,8 @@ export interface INotebookService {
registerNotebookRenderer(handle: number, extensionData: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, preloads: URI[]): void;
unregisterNotebookRenderer(handle: number): void;
getRendererInfo(handle: number): INotebookRendererInfo | undefined;
resolveNotebook(viewType: string, uri: URI): Promise<NotebookTextModel | undefined>;
resolveNotebook(viewType: string, uri: URI, forceReload: boolean): Promise<NotebookTextModel | undefined>;
createNotebookFromBackup(viewType: string, uri: URI, metadata: NotebookDocumentMetadata, languages: string[], cells: ICellDto2[]): Promise<NotebookTextModel | undefined>;
executeNotebook(viewType: string, uri: URI): Promise<void>;
executeNotebookCell(viewType: string, uri: URI, handle: number, token: CancellationToken): Promise<void>;
@@ -45,6 +47,7 @@ export interface INotebookService {
destoryNotebookDocument(viewType: string, notebook: INotebookTextModel): void;
updateActiveNotebookDocument(viewType: string, resource: URI): void;
save(viewType: string, resource: URI, token: CancellationToken): Promise<boolean>;
saveAs(viewType: string, resource: URI, target: URI, token: CancellationToken): Promise<boolean>;
onDidReceiveMessage(viewType: string, uri: URI, message: any): void;
setToCopy(items: NotebookCellTextModel[]): void;
getToCopy(): NotebookCellTextModel[] | undefined;

View File

@@ -47,6 +47,7 @@ import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { Emitter, Event } from 'vs/base/common/event';
import { MenuRegistry, MenuId, isIMenuItem } from 'vs/platform/actions/common/actions';
import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
import { preferencesEditIcon } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets';
const $ = DOM.$;
@@ -400,7 +401,7 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditorP
}
private createRecordingBadge(container: HTMLElement): HTMLElement {
const recordingBadge = DOM.append(container, DOM.$('.recording-badge.disabled'));
const recordingBadge = DOM.append(container, DOM.$('.recording-badge.monaco-count-badge.long.disabled'));
recordingBadge.textContent = localize('recording', "Recording Keys");
this._register(attachStylerCallback(this.themeService, { badgeBackground, contrastBorder, badgeForeground }, colors => {
const background = colors.badgeBackground ? colors.badgeBackground.toString() : '';
@@ -891,7 +892,7 @@ class ActionsColumn extends Column {
private createEditAction(keybindingItemEntry: IKeybindingItemEntry): IAction {
const keybinding = this.keybindingsService.lookupKeybinding(KEYBINDINGS_EDITOR_COMMAND_DEFINE);
return <IAction>{
class: 'codicon-edit',
class: preferencesEditIcon.classNames,
enabled: true,
id: 'editKeybinding',
tooltip: keybinding ? localize('editKeybindingLabelWithKey', "Change Keybinding {0}", `(${keybinding.getLabel()})`) : localize('editKeybindingLabel', "Change Keybinding"),

View File

@@ -27,8 +27,6 @@
.keybindings-editor > .keybindings-header > .search-container > .keybindings-search-actions-container > .recording-badge {
margin-right: 8px;
padding: 0px 8px;
border-radius: 2px;
}
.keybindings-editor > .keybindings-header.small > .search-container > .keybindings-search-actions-container > .recording-badge,

View File

@@ -40,12 +40,9 @@
}
.settings-editor > .settings-header > .search-container > .settings-count-widget {
margin: 6px 0px;
padding: 0px 8px;
border-radius: 2px;
position: absolute;
right: 35px;
top: 0;
top: 6px;
}
.settings-editor > .settings-header > .search-container > .settings-clear-widget {

View File

@@ -26,7 +26,7 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { RangeHighlightDecorations } from 'vs/workbench/browser/parts/editor/rangeDecorations';
import { DefaultSettingsHeaderWidget, EditPreferenceWidget, SettingsGroupTitleWidget, SettingsHeaderWidget } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets';
import { DefaultSettingsHeaderWidget, EditPreferenceWidget, SettingsGroupTitleWidget, SettingsHeaderWidget, preferencesEditIcon } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets';
import { IFilterResult, IPreferencesEditorModel, IPreferencesService, ISetting, ISettingsEditorModel, ISettingsGroup } from 'vs/workbench/services/preferences/common/preferences';
import { DefaultSettingsEditorModel, SettingsEditorModel, WorkspaceConfigurationEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels';
import { IMarkerService, IMarkerData, MarkerSeverity, MarkerTag } from 'vs/platform/markers/common/markers';
@@ -748,7 +748,7 @@ class EditSettingRenderer extends Disposable {
const decorations = this.editor.getLineDecorations(line);
if (decorations) {
for (const { options } of decorations) {
if (options.glyphMarginClassName && options.glyphMarginClassName.indexOf(EditPreferenceWidget.GLYPH_MARGIN_CLASS_NAME) === -1) {
if (options.glyphMarginClassName && options.glyphMarginClassName.indexOf(preferencesEditIcon.classNames) === -1) {
return false;
}
}

View File

@@ -35,6 +35,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/
import { ISettingsGroup } from 'vs/workbench/services/preferences/common/preferences';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { isEqual } from 'vs/base/common/resources';
import { registerIcon, Codicon } from 'vs/base/common/codicons';
export class SettingsHeaderWidget extends Widget implements IViewZone {
@@ -736,9 +737,9 @@ export class SearchWidget extends Widget {
}
}
export class EditPreferenceWidget<T> extends Disposable {
export const preferencesEditIcon = registerIcon('preferences-edit', Codicon.edit, localize('preferencesEditIcon', 'Icon for the edit action in preferences.'));
static readonly GLYPH_MARGIN_CLASS_NAME = 'codicon codicon-edit';
export class EditPreferenceWidget<T> extends Disposable {
private _line: number = -1;
private _preferences: T[] = [];
@@ -775,7 +776,7 @@ export class EditPreferenceWidget<T> extends Disposable {
this._line = line;
newDecoration.push({
options: {
glyphMarginClassName: EditPreferenceWidget.GLYPH_MARGIN_CLASS_NAME,
glyphMarginClassName: preferencesEditIcon.classNames,
glyphMarginHoverMessage: new MarkdownString().appendText(hoverMessage),
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
},

View File

@@ -450,7 +450,7 @@ export class SettingsEditor2 extends BaseEditor {
inputBorder: settingsTextInputBorder
}));
this.countElement = DOM.append(searchContainer, DOM.$('.settings-count-widget'));
this.countElement = DOM.append(searchContainer, DOM.$('.settings-count-widget.monaco-count-badge.long'));
this._register(attachStylerCallback(this.themeService, { badgeBackground, contrastBorder, badgeForeground }, colors => {
const background = colors.badgeBackground ? colors.badgeBackground.toString() : '';
const border = colors.contrastBorder ? colors.contrastBorder.toString() : '';

View File

@@ -21,6 +21,7 @@ import { attachButtonStyler, attachInputBoxStyler } from 'vs/platform/theme/comm
import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { disposableTimeout } from 'vs/base/common/async';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { preferencesEditIcon } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets';
const $ = DOM.$;
export const settingsHeaderForeground = registerColor('settings.headerForeground', { light: '#444444', dark: '#e7e7e7', hc: '#ffffff' }, localize('headerForeground', "The foreground color for a section header or active title."));
@@ -367,7 +368,7 @@ export class ListSettingWidget extends Disposable {
private createEditAction(idx: number): IAction {
return <IAction>{
class: 'codicon-edit',
class: preferencesEditIcon.classNames,
enabled: true,
id: 'workbench.action.editListItem',
tooltip: this.getLocalizedStrings().editActionTooltip,

View File

@@ -152,9 +152,7 @@
}
.search-view .message {
padding-left: 22px;
padding-right: 22px;
padding-top: 0px;
padding: 0 22px 8px;
}
.search-view .message p:first-child {

View File

@@ -826,6 +826,11 @@ configurationRegistry.registerConfiguration({
],
markdownDescription: nls.localize('search.searchEditor.doubleClickBehaviour', "Configure effect of double clicking a result in a search editor.")
},
'search.searchEditor.reusePriorSearchConfiguration': {
type: 'boolean',
default: false,
markdownDescription: nls.localize('search.searchEditor.reusePriorSearchConfiguration', "When enabled, new Search Editors will reuse the includes, excludes, and flags of the previously opened Search Editor")
},
'search.sortOrder': {
'type': 'string',
'enum': [SearchSortOrder.Default, SearchSortOrder.FileNames, SearchSortOrder.Type, SearchSortOrder.Modified, SearchSortOrder.CountDescending, SearchSortOrder.CountAscending],

View File

@@ -378,7 +378,7 @@ export class SearchDND implements ITreeDragAndDrop<RenderableMatch> {
if (resources.length) {
// Apply some datatransfer types to allow for dragging the element outside of the application
this.instantiationService.invokeFunction(fillResourceDataTransfers, resources, originalEvent);
this.instantiationService.invokeFunction(fillResourceDataTransfers, resources, undefined, originalEvent);
}
}

View File

@@ -1022,6 +1022,7 @@ export class SearchView extends ViewPane {
}
this.viewModel.cancelSearch();
this.updateActions();
this.tree.ariaLabel = nls.localize('emptySearch', "Empty Search");
aria.status(nls.localize('ariaSearchResultsClearStatus', "The search results have been cleared"));
}
@@ -1557,9 +1558,12 @@ export class SearchView extends ViewPane {
this.hasSearchResultsKey.set(fileCount > 0);
const msgWasHidden = this.messagesElement.style.display === 'none';
const messageEl = this.clearMessage();
let resultMsg = this.buildResultCountMessage(this.viewModel.searchResult.count(), fileCount);
this.tree.ariaLabel = resultMsg + nls.localize('forTerm', " - Search: {0}", this.searchResult.query?.contentPattern.pattern ?? '');
if (fileCount > 0) {
const messageEl = this.clearMessage();
let resultMsg = this.buildResultCountMessage(this.viewModel.searchResult.count(), fileCount);
if (disregardExcludesAndIgnores) {
resultMsg += nls.localize('useIgnoresAndExcludesDisabled', " - exclude settings and ignore files are disabled");
}

View File

@@ -103,7 +103,7 @@ export function getOutOfWorkspaceEditorResources(accessor: ServicesAccessor): UR
}
// Supports patterns of <path><#|:|(><line><#|:|,><col?>
const LINE_COLON_PATTERN = /\s?[#:\(](\d*)([#:,](\d*))?\)?\s*$/;
const LINE_COLON_PATTERN = /\s?[#:\(](?:line )?(\d*)(?:[#:,](\d*))?\)?\s*$/;
export interface IFilterAndRange {
filter: string;
@@ -119,8 +119,9 @@ export function extractRangeFromFilter(filter: string, unless?: string[]): IFilt
// Find Line/Column number from search value using RegExp
const patternMatch = LINE_COLON_PATTERN.exec(filter);
if (patternMatch && patternMatch.length > 1) {
const startLineNumber = parseInt(patternMatch[1], 10);
if (patternMatch) {
const startLineNumber = parseInt(patternMatch[1] ?? '', 10);
// Line Number
if (isNumber(startLineNumber)) {
@@ -132,16 +133,14 @@ export function extractRangeFromFilter(filter: string, unless?: string[]): IFilt
};
// Column Number
if (patternMatch.length > 3) {
const startColumn = parseInt(patternMatch[3], 10);
if (isNumber(startColumn)) {
range = {
startLineNumber: range.startLineNumber,
startColumn: startColumn,
endLineNumber: range.endLineNumber,
endColumn: startColumn
};
}
const startColumn = parseInt(patternMatch[2] ?? '', 10);
if (isNumber(startColumn)) {
range = {
startLineNumber: range.startLineNumber,
startColumn: startColumn,
endLineNumber: range.endLineNumber,
endColumn: startColumn
};
}
}

View File

@@ -13,7 +13,7 @@ suite('extractRangeFromFilter', () => {
assert.ok(!extractRangeFromFilter('/some/path'));
assert.ok(!extractRangeFromFilter('/some/path/file.txt'));
for (const lineSep of [':', '#', '(']) {
for (const lineSep of [':', '#', '(', ':line ']) {
for (const colSep of [':', '#', ',']) {
const base = '/some/path/file.txt';

View File

@@ -5,27 +5,12 @@
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
export const OpenInEditorCommandId = 'search.action.openInEditor';
export const OpenNewEditorCommandId = 'search.action.openNewEditor';
export const OpenNewEditorToSideCommandId = 'search.action.openNewEditorToSide';
export const FocusQueryEditorWidgetCommandId = 'search.action.focusQueryEditorWidget';
export const ToggleSearchEditorCaseSensitiveCommandId = 'toggleSearchEditorCaseSensitive';
export const ToggleSearchEditorWholeWordCommandId = 'toggleSearchEditorWholeWord';
export const ToggleSearchEditorRegexCommandId = 'toggleSearchEditorRegex';
export const ToggleSearchEditorContextLinesCommandId = 'toggleSearchEditorContextLines';
export const IncreaseSearchEditorContextLinesCommandId = 'increaseSearchEditorContextLines';
export const DecreaseSearchEditorContextLinesCommandId = 'decreaseSearchEditorContextLines';
export const RerunSearchEditorSearchCommandId = 'rerunSearchEditorSearch';
export const CleanSearchEditorStateCommandId = 'cleanSearchEditorState';
export const SelectAllSearchEditorMatchesCommandId = 'selectAllSearchEditorMatches';
export const InSearchEditor = new RawContextKey<boolean>('inSearchEditor', false);
export const SearchEditorScheme = 'search-editor';
export const SearchEditorBodyScheme = 'search-editor-body';
export const SearchEditorFindMatchClass = 'seaarchEditorFindMatch';
export const SearchEditorID = 'workbench.editor.searchEditor';
export const OpenNewEditorCommandId = 'search.action.openNewEditor';

View File

@@ -8,9 +8,10 @@ import * as objects from 'vs/base/common/objects';
import { endsWith } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { Range } from 'vs/editor/common/core/range';
import { ToggleCaseSensitiveKeybinding, ToggleRegexKeybinding, ToggleWholeWordKeybinding } from 'vs/editor/contrib/find/findModel';
import { localize } from 'vs/nls';
import { MenuId, SyncActionDescriptor, registerAction2, Action2 } from 'vs/platform/actions/common/actions';
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
@@ -20,20 +21,36 @@ import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { Registry } from 'vs/platform/registry/common/platform';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor';
import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions';
import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
import { Extensions as EditorInputExtensions, IEditorInputFactory, IEditorInputFactoryRegistry, ActiveEditorContext } from 'vs/workbench/common/editor';
import { ActiveEditorContext, Extensions as EditorInputExtensions, IEditorInputFactory, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor';
import { IViewsService } from 'vs/workbench/common/views';
import { getSearchView } from 'vs/workbench/contrib/search/browser/searchActions';
import { searchRefreshIcon } from 'vs/workbench/contrib/search/browser/searchIcons';
import * as SearchConstants from 'vs/workbench/contrib/search/common/constants';
import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants';
import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor';
import { modifySearchEditorContextLinesCommand, OpenSearchEditorAction, selectAllSearchEditorMatchesCommand, toggleSearchEditorCaseSensitiveCommand, toggleSearchEditorContextLinesCommand, toggleSearchEditorRegexCommand, toggleSearchEditorWholeWordCommand, createEditorFromSearchResult, openNewSearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions';
import { getOrMakeSearchEditorInput, SearchEditorInput, SearchConfiguration } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { createEditorFromSearchResult, modifySearchEditorContextLinesCommand, openNewSearchEditor, selectAllSearchEditorMatchesCommand, toggleSearchEditorCaseSensitiveCommand, toggleSearchEditorContextLinesCommand, toggleSearchEditorRegexCommand, toggleSearchEditorWholeWordCommand } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions';
import { getOrMakeSearchEditorInput, SearchConfiguration, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput';
import { parseSavedSearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization';
import { Range } from 'vs/editor/common/core/range';
import { searchRefreshIcon } from 'vs/workbench/contrib/search/browser/searchIcons';
import { IViewsService } from 'vs/workbench/common/views';
import { getSearchView } from 'vs/workbench/contrib/search/browser/searchActions';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
const OpenInEditorCommandId = 'search.action.openInEditor';
const OpenNewEditorToSideCommandId = 'search.action.openNewEditorToSide';
const FocusQueryEditorWidgetCommandId = 'search.action.focusQueryEditorWidget';
const ToggleSearchEditorCaseSensitiveCommandId = 'toggleSearchEditorCaseSensitive';
const ToggleSearchEditorWholeWordCommandId = 'toggleSearchEditorWholeWord';
const ToggleSearchEditorRegexCommandId = 'toggleSearchEditorRegex';
const ToggleSearchEditorContextLinesCommandId = 'toggleSearchEditorContextLines';
const IncreaseSearchEditorContextLinesCommandId = 'increaseSearchEditorContextLines';
const DecreaseSearchEditorContextLinesCommandId = 'decreaseSearchEditorContextLines';
const RerunSearchEditorSearchCommandId = 'rerunSearchEditorSearch';
const CleanSearchEditorStateCommandId = 'cleanSearchEditorState';
const SelectAllSearchEditorMatchesCommandId = 'selectAllSearchEditorMatches';
//#region Editor Descriptior
Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
@@ -128,28 +145,28 @@ Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactor
//#region Commands
KeybindingsRegistry.registerCommandAndKeybindingRule(objects.assign({
id: SearchEditorConstants.ToggleSearchEditorCaseSensitiveCommandId,
id: ToggleSearchEditorCaseSensitiveCommandId,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor, SearchConstants.SearchInputBoxFocusedKey),
handler: toggleSearchEditorCaseSensitiveCommand
}, ToggleCaseSensitiveKeybinding));
KeybindingsRegistry.registerCommandAndKeybindingRule(objects.assign({
id: SearchEditorConstants.ToggleSearchEditorWholeWordCommandId,
id: ToggleSearchEditorWholeWordCommandId,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor, SearchConstants.SearchInputBoxFocusedKey),
handler: toggleSearchEditorWholeWordCommand
}, ToggleWholeWordKeybinding));
KeybindingsRegistry.registerCommandAndKeybindingRule(objects.assign({
id: SearchEditorConstants.ToggleSearchEditorRegexCommandId,
id: ToggleSearchEditorRegexCommandId,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor, SearchConstants.SearchInputBoxFocusedKey),
handler: toggleSearchEditorRegexCommand
}, ToggleRegexKeybinding));
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: SearchEditorConstants.ToggleSearchEditorContextLinesCommandId,
id: ToggleSearchEditorContextLinesCommandId,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor),
handler: toggleSearchEditorContextLinesCommand,
@@ -158,7 +175,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: SearchEditorConstants.IncreaseSearchEditorContextLinesCommandId,
id: IncreaseSearchEditorContextLinesCommandId,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor),
handler: (accessor: ServicesAccessor) => modifySearchEditorContextLinesCommand(accessor, true),
@@ -166,7 +183,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: SearchEditorConstants.DecreaseSearchEditorContextLinesCommandId,
id: DecreaseSearchEditorContextLinesCommandId,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor),
handler: (accessor: ServicesAccessor) => modifySearchEditorContextLinesCommand(accessor, false),
@@ -174,7 +191,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
});
KeybindingsRegistry.registerCommandAndKeybindingRule({
id: SearchEditorConstants.SelectAllSearchEditorMatchesCommandId,
id: SelectAllSearchEditorMatchesCommandId,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor),
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_L,
@@ -182,7 +199,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
});
CommandsRegistry.registerCommand(
SearchEditorConstants.CleanSearchEditorStateCommandId,
CleanSearchEditorStateCommandId,
(accessor: ServicesAccessor) => {
const activeEditorPane = accessor.get(IEditorService).activeEditorPane;
if (activeEditorPane instanceof SearchEditor) {
@@ -192,18 +209,40 @@ CommandsRegistry.registerCommand(
//#endregion
//#region Actions
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
const category = localize('search', "Search Editor");
// TODO: Not an action2 becuase used in view pane container action bar, which uses actions
registry.registerWorkbenchAction(
SyncActionDescriptor.from(OpenSearchEditorAction),
'Search Editor: Open New Search Editor', category);
registerAction2(class extends Action2 {
constructor() {
super({
id: SearchEditorConstants.OpenInEditorCommandId,
id: SearchEditorConstants.OpenNewEditorCommandId,
title: localize('search.openNewSearchEditor', "Open new Search Editor"),
category,
f1: true,
});
}
async run(accessor: ServicesAccessor, args: Partial<SearchConfiguration>) {
await accessor.get(IInstantiationService).invokeFunction(openNewSearchEditor, args);
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: OpenNewEditorToSideCommandId,
title: localize('search.openNewEditorToSide', "Open new Search Editor to the Side"),
category,
f1: true,
});
}
async run(accessor: ServicesAccessor, args: Partial<SearchConfiguration>) {
await accessor.get(IInstantiationService).invokeFunction(openNewSearchEditor, args, true);
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: OpenInEditorCommandId,
title: localize('search.openResultsInEditor', "Open Results in Editor"),
category,
f1: true,
@@ -227,22 +266,7 @@ registerAction2(class extends Action2 {
registerAction2(class extends Action2 {
constructor() {
super({
id: SearchEditorConstants.OpenNewEditorToSideCommandId,
title: localize('search.openNewEditorToSide', "Open New Search Editor to Side"),
category,
f1: true,
});
}
async run(accessor: ServicesAccessor) {
const instantiationService = accessor.get(IInstantiationService);
await instantiationService.invokeFunction(openNewSearchEditor, true);
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: SearchEditorConstants.RerunSearchEditorSearchCommandId,
id: RerunSearchEditorSearchCommandId,
title: localize('search.rerunSearchInEditor', "Search Again"),
category,
keybinding: {
@@ -274,7 +298,7 @@ registerAction2(class extends Action2 {
registerAction2(class extends Action2 {
constructor() {
super({
id: SearchEditorConstants.FocusQueryEditorWidgetCommandId,
id: FocusQueryEditorWidgetCommandId,
title: localize('search.action.focusQueryEditorWidget', "Focus Search Editor Input"),
category,
menu: {

View File

@@ -13,13 +13,18 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati
import { ILabelService } from 'vs/platform/label/common/label';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { SearchResult } from 'vs/workbench/contrib/search/common/searchModel';
import * as Constants from 'vs/workbench/contrib/searchEditor/browser/constants';
import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor';
import { getOrMakeSearchEditorInput, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput';
import { getOrMakeSearchEditorInput, SearchEditorInput, SearchConfiguration } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput';
import { serializeSearchResultForEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization';
import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search';
import { searchNewEditorIcon } from 'vs/workbench/contrib/search/browser/searchIcons';
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { Schemas } from 'vs/base/common/network';
import { withNullAsUndefined } from 'vs/base/common/types';
import { OpenNewEditorCommandId } from 'vs/workbench/contrib/searchEditor/browser/constants';
export const toggleSearchEditorCaseSensitiveCommand = (accessor: ServicesAccessor) => {
const editorService = accessor.get(IEditorService);
@@ -71,7 +76,7 @@ export const selectAllSearchEditorMatchesCommand = (accessor: ServicesAccessor)
export class OpenSearchEditorAction extends Action {
static readonly ID: string = Constants.OpenNewEditorCommandId;
static readonly ID: string = OpenNewEditorCommandId;
static readonly LABEL = localize('search.openNewEditor', "Open New Search Editor");
constructor(id: string, label: string,
@@ -94,12 +99,23 @@ export class OpenSearchEditorAction extends Action {
}
export const openNewSearchEditor =
async (accessor: ServicesAccessor, toSide = false) => {
async (accessor: ServicesAccessor, args: Partial<SearchConfiguration> = {}, toSide = false) => {
const editorService = accessor.get(IEditorService);
const telemetryService = accessor.get(ITelemetryService);
const instantiationService = accessor.get(IInstantiationService);
const configurationService = accessor.get(IConfigurationService);
const configurationResolverService = accessor.get(IConfigurationResolverService);
const workspaceContextService = accessor.get(IWorkspaceContextService);
const historyService = accessor.get(IHistoryService);
const activeWorkspaceRootUri = historyService.getLastActiveWorkspaceRoot(Schemas.file);
const lastActiveWorkspaceRoot = activeWorkspaceRootUri ? withNullAsUndefined(workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri)) : undefined;
const resolvedArgs: Record<string, any> = {};
Object.entries(args).forEach(([name, value]) => {
resolvedArgs[name as any] = (typeof value === 'string') ? configurationResolverService.resolve(lastActiveWorkspaceRoot, value) : value;
});
const activeEditorControl = editorService.activeTextEditorControl;
let activeModel: ICodeEditor | undefined;
let selected = '';
@@ -124,7 +140,7 @@ export const openNewSearchEditor =
telemetryService.publicLog2('searchEditor/openNewSearchEditor');
const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { config: { query: selected }, text: '' });
const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { config: { query: selected, ...resolvedArgs }, text: '' });
const editor = await editorService.openEditor(input, { pinned: true }, toSide ? SIDE_GROUP : ACTIVE_GROUP) as SearchEditor;
if (selected && configurationService.getValue<ISearchConfigurationProperties>('search').searchOnType) {

View File

@@ -36,7 +36,7 @@ import { IWorkingCopy, IWorkingCopyBackup, IWorkingCopyService, WorkingCopyCapab
export type SearchConfiguration = {
query: string,
includes: string,
excludes: string
excludes: string,
contextLines: number,
wholeWord: boolean,
caseSensitive: boolean,
@@ -304,7 +304,7 @@ export const getOrMakeSearchEditorInput = (
const storageService = accessor.get(IStorageService);
const configurationService = accessor.get(IConfigurationService);
const reuseOldSettings = configurationService.getValue<ISearchConfigurationProperties>('search').searchEditor?.experimental?.reusePriorSearchConfiguration;
const reuseOldSettings = configurationService.getValue<ISearchConfigurationProperties>('search').searchEditor?.reusePriorSearchConfiguration;
const priorConfig: SearchConfiguration = reuseOldSettings ? new Memento(SearchEditorInput.ID, storageService).getMemento(StorageScope.WORKSPACE).searchConfig : {};
const defaultConfig = defaultSearchConfig();
let config = { ...defaultConfig, ...priorConfig, ...existingData.config };

View File

@@ -114,7 +114,7 @@ const contentPatternToSearchConfiguration = (pattern: ITextQuery, includes: stri
wholeWord: !!pattern.contentPattern.isWordMatch,
excludes, includes,
showIncludesExcludes: !!(includes || excludes || pattern?.userDisabledExcludesAndIgnoreFiles),
useIgnores: !!(pattern?.userDisabledExcludesAndIgnoreFiles === undefined ? undefined : !pattern.userDisabledExcludesAndIgnoreFiles),
useIgnores: (pattern?.userDisabledExcludesAndIgnoreFiles === undefined ? true : !pattern.userDisabledExcludesAndIgnoreFiles),
contextLines,
};
};

View File

@@ -791,7 +791,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
if (quickOpenHistoryLimit === 0) {
return;
}
let keys = [...this._recentlyUsedTasks.keys()];
let keys = this._recentlyUsedTasks.keys();
if (keys.length > quickOpenHistoryLimit) {
keys = keys.slice(0, quickOpenHistoryLimit);
}
@@ -1579,7 +1579,9 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
for (const task of taskSet.tasks) {
if (task.type !== this._providerTypes.get(handle)) {
this._outputChannel.append(nls.localize('unexpectedTaskType', "The task provider for \"{0}\" tasks unexpectedly provided a task of type \"{1}\".\n", this._providerTypes.get(handle), task.type));
this.showOutput();
if ((task.type !== 'shell') && (task.type !== 'process')) {
this.showOutput();
}
break;
}
}
@@ -2323,7 +2325,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
taskMap[key] = task;
}
});
const reversed = [...recentlyUsedTasks.keys()].reverse();
const reversed = recentlyUsedTasks.keys().reverse();
for (const key in reversed) {
let task = taskMap[key];
if (task) {

View File

@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ILinkProvider, ILink } from 'xterm';
import { TerminalLink } from 'vs/workbench/contrib/terminal/browser/links/terminalLink';
export abstract class TerminalBaseLinkProvider implements ILinkProvider {
private _activeLinks: TerminalLink[] | undefined;
async provideLinks(bufferLineNumber: number, callback: (links: ILink[] | undefined) => void): Promise<void> {
this._activeLinks?.forEach(l => l.dispose);
this._activeLinks = await this._provideLinks(bufferLineNumber);
callback(this._activeLinks);
}
protected abstract _provideLinks(bufferLineNumber: number): Promise<TerminalLink[]> | TerminalLink[];
}

View File

@@ -20,6 +20,9 @@ export const FOLDER_NOT_IN_WORKSPACE_LABEL = localize('openFolder', 'Open folder
export class TerminalLink extends DisposableStore implements ILink {
decorations: ILinkDecorations;
private _tooltipScheduler: RunOnceScheduler | undefined;
private _hoverListeners: DisposableStore | undefined;
private readonly _onLeave = new Emitter<void>();
public get onLeave(): Event<void> { return this._onLeave.event; }
@@ -40,6 +43,14 @@ export class TerminalLink extends DisposableStore implements ILink {
};
}
dispose(): void {
super.dispose();
this._hoverListeners?.dispose();
this._hoverListeners = undefined;
this._tooltipScheduler?.dispose();
this._tooltipScheduler = undefined;
}
activate(event: MouseEvent | undefined, text: string): void {
this._activateCallback(event, text);
}
@@ -58,20 +69,23 @@ export class TerminalLink extends DisposableStore implements ILink {
}));
const timeout = this._configurationService.getValue<number>('editor.hover.delay');
const scheduler = new RunOnceScheduler(() => {
this._tooltipScheduler = new RunOnceScheduler(() => {
this._tooltipCallback(
this,
convertBufferRangeToViewport(this.range, this._viewportY),
this._isHighConfidenceLink ? () => this._enableDecorations() : undefined,
this._isHighConfidenceLink ? () => this._disableDecorations() : undefined
);
this.dispose();
// Clear out scheduler until next hover event
this._tooltipScheduler?.dispose();
this._tooltipScheduler = undefined;
}, timeout);
this.add(scheduler);
scheduler.schedule();
this.add(this._tooltipScheduler);
this._tooltipScheduler.schedule();
const origin = { x: event.pageX, y: event.pageY };
this.add(dom.addDisposableListener(document, dom.EventType.MOUSE_MOVE, e => {
this._hoverListeners = new DisposableStore();
this._hoverListeners.add(dom.addDisposableListener(document, dom.EventType.MOUSE_MOVE, e => {
// Update decorations
if (this._isModifierDown(e)) {
this._enableDecorations();
@@ -83,14 +97,17 @@ export class TerminalLink extends DisposableStore implements ILink {
if (Math.abs(e.pageX - origin.x) > window.devicePixelRatio * 2 || Math.abs(e.pageY - origin.y) > window.devicePixelRatio * 2) {
origin.x = e.pageX;
origin.y = e.pageY;
scheduler.schedule();
this._tooltipScheduler?.schedule();
}
}));
}
leave(): void {
this._hoverListeners?.dispose();
this._hoverListeners = undefined;
this._tooltipScheduler?.dispose();
this._tooltipScheduler = undefined;
this._onLeave.fire();
this.dispose();
}
private _enableDecorations(): void {

View File

@@ -3,14 +3,15 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Terminal, IViewportRange, ILinkProvider, IBufferCellPosition, ILink, IBufferLine } from 'xterm';
import { Terminal, IViewportRange, IBufferLine } from 'xterm';
import { ILinkComputerTarget, LinkComputer } from 'vs/editor/common/modes/linkComputer';
import { getXtermLineContent, convertLinkRangeToBuffer, positionIsInRange } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers';
import { getXtermLineContent, convertLinkRangeToBuffer } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers';
import { TerminalLink, OPEN_FILE_LABEL } from 'vs/workbench/contrib/terminal/browser/links/terminalLink';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { URI } from 'vs/base/common/uri';
import { TerminalBaseLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalBaseLinkProvider';
export class TerminalProtocolLinkProvider implements ILinkProvider {
export class TerminalProtocolLinkProvider extends TerminalBaseLinkProvider {
private _linkComputerTarget: ILinkComputerTarget | undefined;
constructor(
@@ -19,10 +20,11 @@ export class TerminalProtocolLinkProvider implements ILinkProvider {
private readonly _tooltipCallback: (link: TerminalLink, viewportRange: IViewportRange, modifierDownCallback?: () => void, modifierUpCallback?: () => void) => void,
@IInstantiationService private readonly _instantiationService: IInstantiationService
) {
super();
}
public provideLink(position: IBufferCellPosition, callback: (link: ILink | undefined) => void): void {
let startLine = position.y - 1;
protected _provideLinks(y: number): TerminalLink[] {
let startLine = y - 1;
let endLine = startLine;
const lines: IBufferLine[] = [
@@ -42,24 +44,16 @@ export class TerminalProtocolLinkProvider implements ILinkProvider {
this._linkComputerTarget = new TerminalLinkAdapter(this._xterm, startLine, endLine);
const links = LinkComputer.computeLinks(this._linkComputerTarget);
let found = false;
links.forEach(link => {
return links.map(link => {
const range = convertLinkRangeToBuffer(lines, this._xterm.cols, link.range, startLine);
// Check if the link if within the mouse position
if (positionIsInRange(position, range)) {
found = true;
const uri = link.url
? (typeof link.url === 'string' ? URI.parse(link.url) : link.url)
: undefined;
const label = (uri?.scheme === 'file') ? OPEN_FILE_LABEL : undefined;
callback(this._instantiationService.createInstance(TerminalLink, range, link.url?.toString() || '', this._xterm.buffer.active.viewportY, this._activateCallback, this._tooltipCallback, true, label));
}
const uri = link.url
? (typeof link.url === 'string' ? URI.parse(link.url) : link.url)
: undefined;
const label = (uri?.scheme === 'file') ? OPEN_FILE_LABEL : undefined;
return this._instantiationService.createInstance(TerminalLink, range, link.url?.toString() || '', this._xterm.buffer.active.viewportY, this._activateCallback, this._tooltipCallback, true, label);
});
if (!found) {
callback(undefined);
}
}
}

View File

@@ -3,8 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Terminal, ILinkProvider, IViewportRange, IBufferCellPosition, ILink, IBufferLine } from 'xterm';
import { getXtermLineContent, convertLinkRangeToBuffer, positionIsInRange } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers';
import { Terminal, IViewportRange, IBufferLine } from 'xterm';
import { getXtermLineContent, convertLinkRangeToBuffer } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers';
import { OperatingSystem } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import { TerminalLink, OPEN_FILE_LABEL, FOLDER_IN_WORKSPACE_LABEL, FOLDER_NOT_IN_WORKSPACE_LABEL } from 'vs/workbench/contrib/terminal/browser/links/terminalLink';
@@ -14,6 +14,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { XtermLinkMatcherHandler } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager';
import { TerminalBaseLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalBaseLinkProvider';
const pathPrefix = '(\\.\\.?|\\~)';
const pathSeparatorClause = '\\/';
@@ -41,7 +42,7 @@ const lineAndColumnClause = [
'(([^:\\s\\(\\)<>\'\"\\[\\]]*)(:(\\d+))?(:(\\d+))?)' // (file path):336, (file path):336:9
].join('|').replace(/ /g, `[${'\u00A0'} ]`);
export class TerminalValidatedLocalLinkProvider implements ILinkProvider {
export class TerminalValidatedLocalLinkProvider extends TerminalBaseLinkProvider {
constructor(
private readonly _xterm: Terminal,
private readonly _processOperatingSystem: OperatingSystem,
@@ -54,10 +55,12 @@ export class TerminalValidatedLocalLinkProvider implements ILinkProvider {
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
@IHostService private readonly _hostService: IHostService
) {
super();
}
async provideLink(position: IBufferCellPosition, callback: (link: ILink | undefined) => void) {
let startLine = position.y - 1;
protected async _provideLinks(y: number): Promise<TerminalLink[]> {
const result: TerminalLink[] = [];
let startLine = y - 1;
let endLine = startLine;
const lines: IBufferLine[] = [
@@ -121,34 +124,31 @@ export class TerminalValidatedLocalLinkProvider implements ILinkProvider {
endLineNumber: 1
}, startLine);
if (positionIsInRange(position, bufferRange)) {
const validatedLink = await new Promise<ILink | undefined>(r => {
this._validationCallback(link, (result) => {
if (result) {
const label = result.isDirectory
? (this._isDirectoryInsideWorkspace(result.uri) ? FOLDER_IN_WORKSPACE_LABEL : FOLDER_NOT_IN_WORKSPACE_LABEL)
: OPEN_FILE_LABEL;
const activateCallback = this._wrapLinkHandler((event: MouseEvent | undefined, text: string) => {
if (result.isDirectory) {
this._handleLocalFolderLink(result.uri);
} else {
this._activateFileCallback(event, text);
}
});
r(this._instantiationService.createInstance(TerminalLink, bufferRange, link, this._xterm.buffer.active.viewportY, activateCallback, this._tooltipCallback, true, label));
} else {
r(undefined);
}
});
const validatedLink = await new Promise<TerminalLink | undefined>(r => {
this._validationCallback(link, (result) => {
if (result) {
const label = result.isDirectory
? (this._isDirectoryInsideWorkspace(result.uri) ? FOLDER_IN_WORKSPACE_LABEL : FOLDER_NOT_IN_WORKSPACE_LABEL)
: OPEN_FILE_LABEL;
const activateCallback = this._wrapLinkHandler((event: MouseEvent | undefined, text: string) => {
if (result.isDirectory) {
this._handleLocalFolderLink(result.uri);
} else {
this._activateFileCallback(event, text);
}
});
r(this._instantiationService.createInstance(TerminalLink, bufferRange, link, this._xterm.buffer.active.viewportY, activateCallback, this._tooltipCallback, true, label));
} else {
r(undefined);
}
});
if (validatedLink) {
callback(validatedLink);
return;
}
});
if (validatedLink) {
result.push(validatedLink);
}
}
callback(undefined);
return result;
}
protected get _localLinkRegex(): RegExp {

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Terminal, ILinkProvider, IViewportRange, IBufferCellPosition, ILink } from 'xterm';
import { Terminal, IViewportRange } from 'xterm';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ITerminalConfiguration, TERMINAL_CONFIG_SECTION } from 'vs/workbench/contrib/terminal/common/terminal';
import { TerminalLink } from 'vs/workbench/contrib/terminal/browser/links/terminalLink';
@@ -15,8 +15,9 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
import { QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { XtermLinkMatcherHandler } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager';
import { TerminalBaseLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalBaseLinkProvider';
export class TerminalWordLinkProvider implements ILinkProvider {
export class TerminalWordLinkProvider extends TerminalBaseLinkProvider {
private readonly _fileQueryBuilder = this._instantiationService.createInstance(QueryBuilder);
constructor(
@@ -30,54 +31,49 @@ export class TerminalWordLinkProvider implements ILinkProvider {
@ISearchService private readonly _searchService: ISearchService,
@IEditorService private readonly _editorService: IEditorService
) {
super();
}
public provideLink(position: IBufferCellPosition, callback: (link: ILink | undefined) => void): void {
const start: IBufferCellPosition = { x: position.x, y: position.y };
const end: IBufferCellPosition = { x: position.x, y: position.y };
protected _provideLinks(y: number): TerminalLink[] {
// TODO: Support wrapping
// Expand to the left until a word separator is hit
const line = this._xterm.buffer.active.getLine(position.y - 1)!;
let text = '';
start.x++; // The hovered cell is considered first
for (let x = position.x; x > 0; x--) {
const cell = line.getCell(x - 1);
if (!cell) {
break;
}
const char = cell.getChars();
const config = this._configurationService.getValue<ITerminalConfiguration>(TERMINAL_CONFIG_SECTION);
if (cell.getWidth() !== 0 && config.wordSeparators.indexOf(char) >= 0) {
break;
}
start.x = x;
text = char + text;
}
// No links were found (the hovered cell is whitespace)
if (text.length === 0) {
callback(undefined);
return;
}
// Expand to the right until a word separator is hit
for (let x = position.x + 1; x <= line.length; x++) {
const cell = line.getCell(x - 1);
if (!cell) {
break;
}
const char = cell.getChars();
const config = this._configurationService.getValue<ITerminalConfiguration>(TERMINAL_CONFIG_SECTION);
if (cell.getWidth() !== 0 && config.wordSeparators.indexOf(char) >= 0) {
break;
}
end.x = x;
text += char;
}
// Dispose of all old links if new links are provides, links are only cached for the current line
const result: TerminalLink[] = [];
const wordSeparators = this._configurationService.getValue<ITerminalConfiguration>(TERMINAL_CONFIG_SECTION).wordSeparators;
const activateCallback = this._wrapLinkHandler((_, link) => this._activate(link));
callback(new TerminalLink({ start, end }, text, this._xterm.buffer.active.viewportY, activateCallback, this._tooltipCallback, false, localize('searchWorkspace', 'Search workspace'), this._configurationService));
const line = this._xterm.buffer.active.getLine(y - 1)!;
let text = '';
let startX = -1;
const cellData = line.getCell(0)!;
for (let x = 0; x < line.length; x++) {
line.getCell(x, cellData);
const chars = cellData.getChars();
const width = cellData.getWidth();
// Add a link if this is a separator
if (width !== 0 && wordSeparators.indexOf(chars) >= 0) {
if (startX !== -1) {
result.push(new TerminalLink({ start: { x: startX + 1, y }, end: { x, y } }, text, this._xterm.buffer.active.viewportY, activateCallback, this._tooltipCallback, false, localize('searchWorkspace', 'Search workspace'), this._configurationService));
text = '';
startX = -1;
}
continue;
}
// Mark the start of a link if it hasn't started yet
if (startX === -1) {
startX = x;
}
text += chars;
}
// Add the final link if there is one
if (startX !== -1) {
result.push(new TerminalLink({ start: { x: startX + 1, y }, end: { x: line.length, y } }, text, this._xterm.buffer.active.viewportY, activateCallback, this._tooltipCallback, false, localize('searchWorkspace', 'Search workspace'), this._configurationService));
}
return result;
}
private async _activate(link: string) {

View File

@@ -34,7 +34,7 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess';
import { TerminalQuickAccessProvider } from 'vs/workbench/contrib/terminal/browser/terminalQuickAccess';
import { terminalConfiguration, getTerminalShellConfiguration } from 'vs/workbench/contrib/terminal/common/terminalConfiguration';
import { terminalConfiguration } from 'vs/workbench/contrib/terminal/common/terminalConfiguration';
import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility';
// Register services
@@ -58,11 +58,6 @@ CommandsRegistry.registerCommand({ id: quickAccessNavigatePreviousInTerminalPick
// Register configurations
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
configurationRegistry.registerConfiguration(terminalConfiguration);
if (platform.isWeb) {
// Desktop shell configuration are registered in electron-browser as their default values rely
// on process.env
configurationRegistry.registerConfiguration(getTerminalShellConfiguration());
}
// Register views
const VIEW_CONTAINER = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({
@@ -164,17 +159,6 @@ if (BrowserFeatures.clipboard.readText) {
}
}
if (platform.isWeb) {
// Register standard external terminal keybinding as integrated terminal when in web as the
// external terminal is not available
KeybindingsRegistry.registerKeybindingRule({
id: TERMINAL_COMMAND_ID.NEW,
weight: KeybindingWeight.WorkbenchContrib,
when: undefined,
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C
});
}
// Delete word left: ctrl+w
registerSendSequenceKeybinding(String.fromCharCode('W'.charCodeAt(0) - 64), {
primary: KeyMod.CtrlCmd | KeyCode.Backspace,

View File

@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { TERMINAL_COMMAND_ID } from 'vs/workbench/contrib/terminal/common/terminal';
import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry';
import { getTerminalShellConfiguration } from 'vs/workbench/contrib/terminal/common/terminalConfiguration';
// Desktop shell configuration are registered in electron-browser as their default values rely
// on process.env
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
configurationRegistry.registerConfiguration(getTerminalShellConfiguration());
// Register standard external terminal keybinding as integrated terminal when in web as the
// external terminal is not available
KeybindingsRegistry.registerKeybindingRule({
id: TERMINAL_COMMAND_ID.NEW,
weight: KeybindingWeight.WorkbenchContrib,
when: undefined,
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C
});

View File

@@ -34,6 +34,10 @@ export class TerminalHover extends Disposable implements ITerminalWidget {
super();
}
dispose() {
super.dispose();
}
attach(container: HTMLElement): void {
const target = new CellHoverTarget(container, this._targetOptions);
this._register(this._instantiationService.createInstance(HoverWidget, container, target, this._text, this._linkHandler, []));

View File

@@ -315,7 +315,7 @@ export const terminalConfiguration: IConfigurationNode = {
'terminal.integrated.experimentalLinkProvider': {
description: localize('terminal.integrated.experimentalLinkProvider', "An experimental setting that aims to improve link detection in the terminal by improving when links are detected and by enabling shared link detection with the editor. Currently this only supports web links."),
type: 'boolean',
default: false
default: true
}
}
};

View File

@@ -5,7 +5,7 @@
import * as assert from 'assert';
import { TerminalProtocolLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalProtocolLinkProvider';
import { Terminal, ILink, IBufferRange, IBufferCellPosition } from 'xterm';
import { Terminal, ILink } from 'xterm';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@@ -18,77 +18,69 @@ suite('Workbench - TerminalWebLinkProvider', () => {
instantiationService.stub(IConfigurationService, TestConfigurationService);
});
async function assertLink(text: string, expected: { text: string, range: [number, number][] }) {
async function assertLink(text: string, expected: { text: string, range: [number, number][] }[]) {
const xterm = new Terminal();
const provider = instantiationService.createInstance(TerminalProtocolLinkProvider, xterm, () => { }, () => { });
// Write the text and wait for the parser to finish
await new Promise<void>(r => xterm.write(text, r));
// Calculate positions just outside of link boundaries
const noLinkPositions: IBufferCellPosition[] = [
{ x: expected.range[0][0] - 1, y: expected.range[0][1] },
{ x: expected.range[1][0] + 1, y: expected.range[1][1] }
];
// Ensure outside positions do not detect the link
for (let i = 0; i < noLinkPositions.length; i++) {
const link = await new Promise<ILink | undefined>(r => provider.provideLink(noLinkPositions[i], r));
assert.equal(link, undefined, `Just outside range boundary should not result in link, link found at (${link?.range.start.x}, ${link?.range.start.y}) to (${link?.range.end.x}, ${link?.range.end.y}) while checking (${noLinkPositions[i].x}, ${noLinkPositions[i].y})\nExpected link text=${expected.text}\nActual link text=${link?.text}`);
}
// Convert range from [[startx, starty], [endx, endy]] to an IBufferRange
const linkRange: IBufferRange = {
start: { x: expected.range[0][0], y: expected.range[0][1] },
end: { x: expected.range[1][0], y: expected.range[1][1] },
};
// Calculate positions inside the link boundaries
const linkPositions: IBufferCellPosition[] = [
linkRange.start,
linkRange.end
];
// Ensure inside positions do detect the link
for (let i = 0; i < linkPositions.length; i++) {
const link = await new Promise<ILink | undefined>(r => provider.provideLink(linkPositions[i], r));
assert.deepEqual(link?.text, expected.text);
assert.deepEqual(link?.range, linkRange);
}
// Ensure all links are provided
const links = (await new Promise<ILink[] | undefined>(r => provider.provideLinks(1, r)))!;
assert.equal(links.length, expected.length);
const actual = links.map(e => ({
text: e.text,
range: e.range
}));
const expectedVerbose = expected.map(e => ({
text: e.text,
range: {
start: { x: e.range[0][0], y: e.range[0][1] },
end: { x: e.range[1][0], y: e.range[1][1] },
}
}));
assert.deepEqual(actual, expectedVerbose);
}
// These tests are based on LinkComputer.test.ts
test('LinkComputer cases', async () => {
await assertLink('x = "http://foo.bar";', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
await assertLink('x = (http://foo.bar);', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
await assertLink('x = \'http://foo.bar\';', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
await assertLink('x = http://foo.bar ;', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
await assertLink('x = <http://foo.bar>;', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
await assertLink('x = {http://foo.bar};', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
await assertLink('(see http://foo.bar)', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
await assertLink('[see http://foo.bar]', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
await assertLink('{see http://foo.bar}', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
await assertLink('<see http://foo.bar>', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
await assertLink('<url>http://foo.bar</url>', { range: [[6, 1], [19, 1]], text: 'http://foo.bar' });
await assertLink('// Click here to learn more. https://go.microsoft.com/fwlink/?LinkID=513275&clcid=0x409', { range: [[30, 1], [7, 2]], text: 'https://go.microsoft.com/fwlink/?LinkID=513275&clcid=0x409' });
await assertLink('// Click here to learn more. https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx', { range: [[30, 1], [28, 2]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx' });
await assertLink('// https://github.com/projectkudu/kudu/blob/master/Kudu.Core/Scripts/selectNodeVersion.js', { range: [[4, 1], [9, 2]], text: 'https://github.com/projectkudu/kudu/blob/master/Kudu.Core/Scripts/selectNodeVersion.js' });
await assertLink('<!-- !!! Do not remove !!! WebContentRef(link:https://go.microsoft.com/fwlink/?LinkId=166007, area:Admin, updated:2015, nextUpdate:2016, tags:SqlServer) !!! Do not remove !!! -->', { range: [[49, 1], [14, 2]], text: 'https://go.microsoft.com/fwlink/?LinkId=166007' });
await assertLink('For instructions, see https://go.microsoft.com/fwlink/?LinkId=166007.</value>', { range: [[23, 1], [68, 1]], text: 'https://go.microsoft.com/fwlink/?LinkId=166007' });
await assertLink('For instructions, see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx.</value>', { range: [[23, 1], [21, 2]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx' });
await assertLink('x = "https://en.wikipedia.org/wiki/Zürich";', { range: [[6, 1], [41, 1]], text: 'https://en.wikipedia.org/wiki/Zürich' });
await assertLink('請參閱 http://go.microsoft.com/fwlink/?LinkId=761051。', { range: [[8, 1], [53, 1]], text: 'http://go.microsoft.com/fwlink/?LinkId=761051' });
await assertLink('(請參閱 http://go.microsoft.com/fwlink/?LinkId=761051', { range: [[10, 1], [55, 1]], text: 'http://go.microsoft.com/fwlink/?LinkId=761051' });
await assertLink('x = "file:///foo.bar";', { range: [[6, 1], [20, 1]], text: 'file:///foo.bar' });
await assertLink('x = "file://c:/foo.bar";', { range: [[6, 1], [22, 1]], text: 'file://c:/foo.bar' });
await assertLink('x = "file://shares/foo.bar";', { range: [[6, 1], [26, 1]], text: 'file://shares/foo.bar' });
await assertLink('x = "file://shäres/foo.bar";', { range: [[6, 1], [26, 1]], text: 'file://shäres/foo.bar' });
await assertLink('Some text, then http://www.bing.com.', { range: [[17, 1], [35, 1]], text: 'http://www.bing.com' });
await assertLink('let url = `http://***/_api/web/lists/GetByTitle(\'Teambuildingaanvragen\')/items`;', { range: [[12, 1], [78, 1]], text: 'http://***/_api/web/lists/GetByTitle(\'Teambuildingaanvragen\')/items' });
await assertLink('7. At this point, ServiceMain has been called. There is no functionality presently in ServiceMain, but you can consult the [MSDN documentation](https://msdn.microsoft.com/en-us/library/windows/desktop/ms687414(v=vs.85).aspx) to add functionality as desired!', { range: [[66, 2], [64, 3]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/ms687414(v=vs.85).aspx' });
await assertLink('let x = "http://[::1]:5000/connect/token"', { range: [[10, 1], [40, 1]], text: 'http://[::1]:5000/connect/token' });
await assertLink('2. Navigate to **https://portal.azure.com**', { range: [[18, 1], [41, 1]], text: 'https://portal.azure.com' });
await assertLink('POST|https://portal.azure.com|2019-12-05|', { range: [[6, 1], [29, 1]], text: 'https://portal.azure.com' });
await assertLink('aa https://foo.bar/[this is foo site] aa', { range: [[5, 1], [38, 1]], text: 'https://foo.bar/[this is foo site]' });
await assertLink('x = "http://foo.bar";', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
await assertLink('x = (http://foo.bar);', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
await assertLink('x = \'http://foo.bar\';', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
await assertLink('x = http://foo.bar ;', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
await assertLink('x = <http://foo.bar>;', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
await assertLink('x = {http://foo.bar};', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
await assertLink('(see http://foo.bar)', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
await assertLink('[see http://foo.bar]', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
await assertLink('{see http://foo.bar}', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
await assertLink('<see http://foo.bar>', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
await assertLink('<url>http://foo.bar</url>', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]);
await assertLink('// Click here to learn more. https://go.microsoft.com/fwlink/?LinkID=513275&clcid=0x409', [{ range: [[30, 1], [7, 2]], text: 'https://go.microsoft.com/fwlink/?LinkID=513275&clcid=0x409' }]);
await assertLink('// Click here to learn more. https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx', [{ range: [[30, 1], [28, 2]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx' }]);
await assertLink('// https://github.com/projectkudu/kudu/blob/master/Kudu.Core/Scripts/selectNodeVersion.js', [{ range: [[4, 1], [9, 2]], text: 'https://github.com/projectkudu/kudu/blob/master/Kudu.Core/Scripts/selectNodeVersion.js' }]);
await assertLink('<!-- !!! Do not remove !!! WebContentRef(link:https://go.microsoft.com/fwlink/?LinkId=166007, area:Admin, updated:2015, nextUpdate:2016, tags:SqlServer) !!! Do not remove !!! -->', [{ range: [[49, 1], [14, 2]], text: 'https://go.microsoft.com/fwlink/?LinkId=166007' }]);
await assertLink('For instructions, see https://go.microsoft.com/fwlink/?LinkId=166007.</value>', [{ range: [[23, 1], [68, 1]], text: 'https://go.microsoft.com/fwlink/?LinkId=166007' }]);
await assertLink('For instructions, see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx.</value>', [{ range: [[23, 1], [21, 2]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx' }]);
await assertLink('x = "https://en.wikipedia.org/wiki/Zürich";', [{ range: [[6, 1], [41, 1]], text: 'https://en.wikipedia.org/wiki/Zürich' }]);
await assertLink('請參閱 http://go.microsoft.com/fwlink/?LinkId=761051。', [{ range: [[8, 1], [53, 1]], text: 'http://go.microsoft.com/fwlink/?LinkId=761051' }]);
await assertLink('(請參閱 http://go.microsoft.com/fwlink/?LinkId=761051', [{ range: [[10, 1], [55, 1]], text: 'http://go.microsoft.com/fwlink/?LinkId=761051' }]);
await assertLink('x = "file:///foo.bar";', [{ range: [[6, 1], [20, 1]], text: 'file:///foo.bar' }]);
await assertLink('x = "file://c:/foo.bar";', [{ range: [[6, 1], [22, 1]], text: 'file://c:/foo.bar' }]);
await assertLink('x = "file://shares/foo.bar";', [{ range: [[6, 1], [26, 1]], text: 'file://shares/foo.bar' }]);
await assertLink('x = "file://shäres/foo.bar";', [{ range: [[6, 1], [26, 1]], text: 'file://shäres/foo.bar' }]);
await assertLink('Some text, then http://www.bing.com.', [{ range: [[17, 1], [35, 1]], text: 'http://www.bing.com' }]);
await assertLink('let url = `http://***/_api/web/lists/GetByTitle(\'Teambuildingaanvragen\')/items`;', [{ range: [[12, 1], [78, 1]], text: 'http://***/_api/web/lists/GetByTitle(\'Teambuildingaanvragen\')/items' }]);
await assertLink('7. At this point, ServiceMain has been called. There is no functionality presently in ServiceMain, but you can consult the [MSDN documentation](https://msdn.microsoft.com/en-us/library/windows/desktop/ms687414(v=vs.85).aspx) to add functionality as desired!', [{ range: [[66, 2], [64, 3]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/ms687414(v=vs.85).aspx' }]);
await assertLink('let x = "http://[::1]:5000/connect/token"', [{ range: [[10, 1], [40, 1]], text: 'http://[::1]:5000/connect/token' }]);
await assertLink('2. Navigate to **https://portal.azure.com**', [{ range: [[18, 1], [41, 1]], text: 'https://portal.azure.com' }]);
await assertLink('POST|https://portal.azure.com|2019-12-05|', [{ range: [[6, 1], [29, 1]], text: 'https://portal.azure.com' }]);
await assertLink('aa https://foo.bar/[this is foo site] aa', [{ range: [[5, 1], [38, 1]], text: 'https://foo.bar/[this is foo site]' }]);
});
test('should support multiple link results', async () => {
await assertLink('http://foo.bar http://bar.foo', [
{ range: [[1, 1], [14, 1]], text: 'http://foo.bar' },
{ range: [[16, 1], [29, 1]], text: 'http://bar.foo' }
]);
});
});

View File

@@ -5,7 +5,7 @@
import * as assert from 'assert';
import { TerminalValidatedLocalLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider';
import { Terminal, ILink, IBufferRange, IBufferCellPosition } from 'xterm';
import { Terminal, ILink } from 'xterm';
import { OperatingSystem } from 'vs/base/common/platform';
import { format } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
@@ -76,43 +76,28 @@ suite('Workbench - TerminalValidatedLocalLinkProvider', () => {
instantiationService.stub(IConfigurationService, TestConfigurationService);
});
async function assertLink(text: string, os: OperatingSystem, expected: { text: string, range: [number, number][] }) {
async function assertLink(text: string, os: OperatingSystem, expected: { text: string, range: [number, number][] }[]) {
const xterm = new Terminal();
const provider = instantiationService.createInstance(TerminalValidatedLocalLinkProvider, xterm, os, () => { }, () => { }, () => { }, (_: string, cb: (result: { uri: URI, isDirectory: boolean } | undefined) => void) => { cb({ uri: URI.file('/'), isDirectory: false }); });
// Write the text and wait for the parser to finish
await new Promise<void>(r => xterm.write(text, r));
// Calculate positions just outside of link boundaries
const noLinkPositions: IBufferCellPosition[] = [
{ x: expected.range[0][0] - 1, y: expected.range[0][1] },
{ x: expected.range[1][0] + 1, y: expected.range[1][1] }
];
// Ensure outside positions do not detect the link
for (let i = 0; i < noLinkPositions.length; i++) {
const link = await new Promise<ILink | undefined>(r => provider.provideLink(noLinkPositions[i], r));
assert.equal(link, undefined, `Just outside range boundary should not result in link, link found at (${link?.range.start.x}, ${link?.range.start.y}) to (${link?.range.end.x}, ${link?.range.end.y}) while checking (${noLinkPositions[i].x}, ${noLinkPositions[i].y})\nExpected link text=${expected.text}\nActual link text=${link?.text}`);
}
// Convert range from [[startx, starty], [endx, endy]] to an IBufferRange
const linkRange: IBufferRange = {
start: { x: expected.range[0][0], y: expected.range[0][1] },
end: { x: expected.range[1][0], y: expected.range[1][1] },
};
// Calculate positions inside the link boundaries
const linkPositions: IBufferCellPosition[] = [
linkRange.start,
linkRange.end
];
// Ensure inside positions do detect the link
for (let i = 0; i < linkPositions.length; i++) {
const link = await new Promise<ILink | undefined>(r => provider.provideLink(linkPositions[i], r));
assert.deepEqual(link?.text, expected.text);
assert.deepEqual(link?.range, linkRange);
}
// Ensure all links are provided
const links = (await new Promise<ILink[] | undefined>(r => provider.provideLinks(1, r)))!;
assert.equal(links.length, expected.length);
const actual = links.map(e => ({
text: e.text,
range: e.range
}));
const expectedVerbose = expected.map(e => ({
text: e.text,
range: {
start: { x: e.range[0][0], y: e.range[0][1] },
end: { x: e.range[1][0], y: e.range[1][1] },
}
}));
assert.deepEqual(actual, expectedVerbose);
}
suite('Linux/macOS', () => {
@@ -122,19 +107,21 @@ suite('Workbench - TerminalValidatedLocalLinkProvider', () => {
const linkFormat = supportedLinkFormats[i];
test(`Format: ${linkFormat.urlFormat}`, async () => {
const formattedLink = format(linkFormat.urlFormat, baseLink, linkFormat.line, linkFormat.column);
await assertLink(formattedLink, OperatingSystem.Linux, { text: formattedLink, range: [[1, 1], [formattedLink.length, 1]] });
await assertLink(` ${formattedLink} `, OperatingSystem.Linux, { text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] });
await assertLink(`(${formattedLink})`, OperatingSystem.Linux, { text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] });
await assertLink(`[${formattedLink}]`, OperatingSystem.Linux, { text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] });
await assertLink(formattedLink, OperatingSystem.Linux, [{ text: formattedLink, range: [[1, 1], [formattedLink.length, 1]] }]);
await assertLink(` ${formattedLink} `, OperatingSystem.Linux, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]);
await assertLink(`(${formattedLink})`, OperatingSystem.Linux, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]);
await assertLink(`[${formattedLink}]`, OperatingSystem.Linux, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]);
});
}
});
});
test('Git diff links', async () => {
await assertLink(`diff --git a/foo/bar b/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[14, 1], [20, 1]] });
await assertLink(`diff --git a/foo/bar b/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[24, 1], [30, 1]] });
await assertLink(`--- a/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[7, 1], [13, 1]] });
await assertLink(`+++ b/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[7, 1], [13, 1]] });
await assertLink(`diff --git a/foo/bar b/foo/bar`, OperatingSystem.Linux, [
{ text: 'foo/bar', range: [[14, 1], [20, 1]] },
{ text: 'foo/bar', range: [[24, 1], [30, 1]] }
]);
await assertLink(`--- a/foo/bar`, OperatingSystem.Linux, [{ text: 'foo/bar', range: [[7, 1], [13, 1]] }]);
await assertLink(`+++ b/foo/bar`, OperatingSystem.Linux, [{ text: 'foo/bar', range: [[7, 1], [13, 1]] }]);
});
});
@@ -145,19 +132,28 @@ suite('Workbench - TerminalValidatedLocalLinkProvider', () => {
const linkFormat = supportedLinkFormats[i];
test(`Format: ${linkFormat.urlFormat}`, async () => {
const formattedLink = format(linkFormat.urlFormat, baseLink, linkFormat.line, linkFormat.column);
await assertLink(formattedLink, OperatingSystem.Windows, { text: formattedLink, range: [[1, 1], [formattedLink.length, 1]] });
await assertLink(` ${formattedLink} `, OperatingSystem.Windows, { text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] });
await assertLink(`(${formattedLink})`, OperatingSystem.Windows, { text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] });
await assertLink(`[${formattedLink}]`, OperatingSystem.Windows, { text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] });
await assertLink(formattedLink, OperatingSystem.Windows, [{ text: formattedLink, range: [[1, 1], [formattedLink.length, 1]] }]);
await assertLink(` ${formattedLink} `, OperatingSystem.Windows, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]);
await assertLink(`(${formattedLink})`, OperatingSystem.Windows, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]);
await assertLink(`[${formattedLink}]`, OperatingSystem.Windows, [{ text: formattedLink, range: [[2, 1], [formattedLink.length + 1, 1]] }]);
});
}
});
});
test('Git diff links', async () => {
await assertLink(`diff --git a/foo/bar b/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[14, 1], [20, 1]] });
await assertLink(`diff --git a/foo/bar b/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[24, 1], [30, 1]] });
await assertLink(`--- a/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[7, 1], [13, 1]] });
await assertLink(`+++ b/foo/bar`, OperatingSystem.Linux, { text: 'foo/bar', range: [[7, 1], [13, 1]] });
await assertLink(`diff --git a/foo/bar b/foo/bar`, OperatingSystem.Linux, [
{ text: 'foo/bar', range: [[14, 1], [20, 1]] },
{ text: 'foo/bar', range: [[24, 1], [30, 1]] }
]);
await assertLink(`--- a/foo/bar`, OperatingSystem.Linux, [{ text: 'foo/bar', range: [[7, 1], [13, 1]] }]);
await assertLink(`+++ b/foo/bar`, OperatingSystem.Linux, [{ text: 'foo/bar', range: [[7, 1], [13, 1]] }]);
});
});
test('should support multiple link results', async () => {
await assertLink('./foo ./bar', OperatingSystem.Linux, [
{ range: [[1, 1], [5, 1]], text: './foo' },
{ range: [[7, 1], [11, 1]], text: './bar' }
]);
});
});

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { Terminal, ILink, IBufferRange, IBufferCellPosition } from 'xterm';
import { Terminal, ILink } from 'xterm';
import { TerminalWordLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
@@ -21,68 +21,62 @@ suite('Workbench - TerminalWordLinkProvider', () => {
instantiationService.stub(IConfigurationService, configurationService);
});
async function assertLink(text: string, expected: { text: string, range: [number, number][] }) {
async function assertLink(text: string, expected: { text: string, range: [number, number][] }[]) {
const xterm = new Terminal();
const provider = instantiationService.createInstance(TerminalWordLinkProvider, xterm, () => { }, () => { });
const provider: TerminalWordLinkProvider = instantiationService.createInstance(TerminalWordLinkProvider, xterm, () => { }, () => { });
// Write the text and wait for the parser to finish
await new Promise<void>(r => xterm.write(text, r));
// Calculate positions just outside of link boundaries
const noLinkPositions: IBufferCellPosition[] = [
{ x: expected.range[0][0] - 1, y: expected.range[0][1] },
{ x: expected.range[1][0] + 1, y: expected.range[1][1] }
];
// Ensure outside positions do not detect the link
for (let i = 0; i < noLinkPositions.length; i++) {
const link = await new Promise<ILink | undefined>(r => provider.provideLink(noLinkPositions[i], r));
assert.equal(link, undefined, `Just outside range boundary should not result in link, link found at (${link?.range.start.x}, ${link?.range.start.y}) to (${link?.range.end.x}, ${link?.range.end.y}) while checking (${noLinkPositions[i].x}, ${noLinkPositions[i].y})\nExpected link text=${expected.text}\nActual link text=${link?.text}`);
}
// Convert range from [[startx, starty], [endx, endy]] to an IBufferRange
const linkRange: IBufferRange = {
start: { x: expected.range[0][0], y: expected.range[0][1] },
end: { x: expected.range[1][0], y: expected.range[1][1] },
};
// Calculate positions inside the link boundaries
const linkPositions: IBufferCellPosition[] = [
linkRange.start,
linkRange.end
];
// Ensure inside positions do detect the link
for (let i = 0; i < linkPositions.length; i++) {
const link = await new Promise<ILink | undefined>(r => provider.provideLink(linkPositions[i], r));
assert.deepEqual(link?.text, expected.text);
assert.deepEqual(link?.range, linkRange);
}
// Ensure all links are provided
const links = (await new Promise<ILink[] | undefined>(r => provider.provideLinks(1, r)))!;
assert.equal(links.length, expected.length);
const actual = links.map(e => ({
text: e.text,
range: e.range
}));
const expectedVerbose = expected.map(e => ({
text: e.text,
range: {
start: { x: e.range[0][0], y: e.range[0][1] },
end: { x: e.range[1][0], y: e.range[1][1] },
}
}));
assert.deepEqual(actual, expectedVerbose);
}
test('should link words as defined by wordSeparators', async () => {
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ()[]' } });
await assertLink('foo', { range: [[1, 1], [3, 1]], text: 'foo' });
await assertLink(' foo ', { range: [[2, 1], [4, 1]], text: 'foo' });
await assertLink('(foo)', { range: [[2, 1], [4, 1]], text: 'foo' });
await assertLink('[foo]', { range: [[2, 1], [4, 1]], text: 'foo' });
await assertLink('{foo}', { range: [[1, 1], [5, 1]], text: '{foo}' });
await assertLink('foo', [{ range: [[1, 1], [3, 1]], text: 'foo' }]);
await assertLink('foo', [{ range: [[1, 1], [3, 1]], text: 'foo' }]);
await assertLink(' foo ', [{ range: [[2, 1], [4, 1]], text: 'foo' }]);
await assertLink('(foo)', [{ range: [[2, 1], [4, 1]], text: 'foo' }]);
await assertLink('[foo]', [{ range: [[2, 1], [4, 1]], text: 'foo' }]);
await assertLink('{foo}', [{ range: [[1, 1], [5, 1]], text: '{foo}' }]);
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ' } });
await assertLink('foo', { range: [[1, 1], [3, 1]], text: 'foo' });
await assertLink(' foo ', { range: [[2, 1], [4, 1]], text: 'foo' });
await assertLink('(foo)', { range: [[1, 1], [5, 1]], text: '(foo)' });
await assertLink('[foo]', { range: [[1, 1], [5, 1]], text: '[foo]' });
await assertLink('{foo}', { range: [[1, 1], [5, 1]], text: '{foo}' });
await assertLink('foo', [{ range: [[1, 1], [3, 1]], text: 'foo' }]);
await assertLink(' foo ', [{ range: [[2, 1], [4, 1]], text: 'foo' }]);
await assertLink('(foo)', [{ range: [[1, 1], [5, 1]], text: '(foo)' }]);
await assertLink('[foo]', [{ range: [[1, 1], [5, 1]], text: '[foo]' }]);
await assertLink('{foo}', [{ range: [[1, 1], [5, 1]], text: '{foo}' }]);
});
test('should support wide characters', async () => {
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' []' } });
await assertLink('aabbccdd.txt ', { range: [[1, 1], [12, 1]], text: 'aabbccdd.txt' });
await assertLink('我是学生.txt ', { range: [[1, 1], [12, 1]], text: '我是学生.txt' });
await assertLink(' aabbccdd.txt ', { range: [[2, 1], [13, 1]], text: 'aabbccdd.txt' });
await assertLink(' 我是学生.txt ', { range: [[2, 1], [13, 1]], text: '我是学生.txt' });
await assertLink(' [aabbccdd.txt] ', { range: [[3, 1], [14, 1]], text: 'aabbccdd.txt' });
await assertLink(' [我是学生.txt] ', { range: [[3, 1], [14, 1]], text: '我是学生.txt' });
await assertLink('aabbccdd.txt ', [{ range: [[1, 1], [12, 1]], text: 'aabbccdd.txt' }]);
await assertLink('我是学生.txt ', [{ range: [[1, 1], [12, 1]], text: '我是学生.txt' }]);
await assertLink(' aabbccdd.txt ', [{ range: [[2, 1], [13, 1]], text: 'aabbccdd.txt' }]);
await assertLink(' 我是学生.txt ', [{ range: [[2, 1], [13, 1]], text: '我是学生.txt' }]);
await assertLink(' [aabbccdd.txt] ', [{ range: [[3, 1], [14, 1]], text: 'aabbccdd.txt' }]);
await assertLink(' [我是学生.txt] ', [{ range: [[3, 1], [14, 1]], text: '我是学生.txt' }]);
});
test('should support multiple link results', async () => {
await configurationService.setUserConfiguration('terminal', { integrated: { wordSeparators: ' ' } });
await assertLink('foo bar', [
{ range: [[1, 1], [3, 1]], text: 'foo' },
{ range: [[5, 1], [7, 1]], text: 'bar' }
]);
});
});

View File

@@ -100,7 +100,7 @@ class TimelineAggregate {
return this.items[this.items.length - 1];
}
add(timeline: Timeline) {
add(timeline: Timeline, options: TimelineOptions) {
let updated = false;
if (timeline.items.length !== 0 && this.items.length !== 0) {
@@ -139,7 +139,10 @@ class TimelineAggregate {
this.items.push(...timeline.items);
}
this._cursor = timeline.paging?.cursor;
// If we are not requesting more recent items than we have, then update the cursor
if (options.cursor !== undefined || typeof options.limit !== 'object') {
this._cursor = timeline.paging?.cursor;
}
if (updated) {
this.items.sort(
@@ -626,7 +629,7 @@ export class TimelinePane extends ViewPane {
updated = true;
}
else {
updated = timeline.add(response);
updated = timeline.add(response, request.options);
}
if (updated) {
@@ -923,6 +926,10 @@ export class TimelinePane extends ViewPane {
}
private loadMore(item: LoadMoreCommand) {
if (item.loading) {
return;
}
item.loading = true;
this.tree.rerender(item);

View File

@@ -7,10 +7,10 @@ import { IUserDataSyncService, IUserDataSyncLogService, IUserDataSyncEnablementS
import { Event } from 'vs/base/common/event';
import { UserDataAutoSyncService as BaseUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger';
export class UserDataAutoSyncService extends BaseUserDataAutoSyncService {

View File

@@ -8,7 +8,6 @@ import { toErrorMessage } from 'vs/base/common/errorMessage';
import { canceled, isPromiseCanceledError } from 'vs/base/common/errors';
import { Event } from 'vs/base/common/event';
import { Disposable, DisposableStore, dispose, MutableDisposable, toDisposable, IDisposable } from 'vs/base/common/lifecycle';
import { isWeb } from 'vs/base/common/platform';
import { isEqual, basename } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import type { ICodeEditor } from 'vs/editor/browser/editorBrowser';
@@ -39,7 +38,6 @@ import { IEditorInput, toResource, SideBySideEditor } from 'vs/workbench/common/
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import * as Constants from 'vs/workbench/contrib/logs/common/logConstants';
import { IOutputService } from 'vs/workbench/contrib/output/common/output';
import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger';
import { IActivityService, IBadge, NumberBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
@@ -81,9 +79,10 @@ const resolveSettingsConflictsCommand = { id: 'workbench.userData.actions.resolv
const resolveKeybindingsConflictsCommand = { id: 'workbench.userData.actions.resolveKeybindingsConflicts', title: localize('showKeybindingsConflicts', "Preferences Sync: Show Keybindings Conflicts") };
const resolveSnippetsConflictsCommand = { id: 'workbench.userData.actions.resolveSnippetsConflicts', title: localize('showSnippetsConflicts', "Preferences Sync: Show User Snippets Conflicts") };
const configureSyncCommand = { id: 'workbench.userData.actions.configureSync', title: localize('configure sync', "Preferences Sync: Configure...") };
const showSyncActivityCommand = {
id: 'workbench.userData.actions.showSyncActivity',
title: localize('show sync log', "Preferences Sync: Show Log"),
const showSyncActivityCommand = { id: 'workbench.userData.actions.showSyncActivity', title: localize('show sync log', "Preferences Sync: Show Log") };
const syncNowCommand = {
id: 'workbench.userData.actions.syncNow',
title: localize('sync now', "Preferences Sync: Sync Now"),
description(userDataSyncService: IUserDataSyncService): string | undefined {
if (userDataSyncService.status === SyncStatus.Syncing) {
return localize('sync is on with syncing', "syncing");
@@ -97,7 +96,7 @@ const showSyncActivityCommand = {
const showSyncSettingsCommand = { id: 'workbench.userData.actions.syncSettings', title: localize('sync settings', "Preferences Sync: Show Settings"), };
const CONTEXT_TURNING_ON_STATE = new RawContextKey<false>('userDataSyncTurningOn', false);
const CONTEXT_ACCOUNT_STATE = new RawContextKey<string>('userDataSyncAccountStatus', AccountStatus.Uninitialized);
export const CONTEXT_ACCOUNT_STATE = new RawContextKey<string>('userDataSyncAccountStatus', AccountStatus.Uninitialized);
export class UserDataSyncWorkbenchContribution extends Disposable implements IWorkbenchContribution {
@@ -161,10 +160,6 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
textModelResolverService.registerTextModelContentProvider(USER_DATA_SYNC_SCHEME, instantiationService.createInstance(UserDataRemoteContentProvider));
registerEditorContribution(AcceptChangesContribution.ID, AcceptChangesContribution);
if (!isWeb) {
this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(source => userDataAutoSyncService.triggerAutoSync([source])));
}
}
}
@@ -894,6 +889,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
private registerSyncStatusAction(): void {
const that = this;
const when = ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Available), CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized));
this.registerSyncNowAction();
this._register(registerAction2(class SyncStatusAction extends Action2 {
constructor() {
super({
@@ -946,8 +942,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
}
items.push({ id: configureSyncCommand.id, label: configureSyncCommand.title });
items.push({ id: showSyncSettingsCommand.id, label: showSyncSettingsCommand.title });
items.push({ id: showSyncActivityCommand.id, label: showSyncActivityCommand.title, description: showSyncActivityCommand.description(that.userDataSyncService) });
items.push({ id: showSyncActivityCommand.id, label: showSyncActivityCommand.title });
items.push({ type: 'separator' });
items.push({ id: syncNowCommand.id, label: syncNowCommand.title, description: syncNowCommand.description(that.userDataSyncService) });
if (that.userDataSyncEnablementService.canToggleEnablement()) {
const account = that.userDataSyncAccounts.current;
items.push({ id: stopSyncCommand.id, label: stopSyncCommand.title, description: account ? `${account.accountName} (${that.authenticationService.getDisplayName(account.authenticationProviderId)})` : undefined });
@@ -969,6 +966,21 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo
}));
}
private registerSyncNowAction(): void {
const that = this;
this._register(registerAction2(class SyncNowAction extends Action2 {
constructor() {
super({
id: syncNowCommand.id,
title: syncNowCommand.title,
});
}
run(): Promise<any> {
return that.userDataSyncService.sync();
}
}));
}
private registerTurnOffSyncAction(): void {
const that = this;
this._register(registerAction2(class StopSyncAction extends Action2 {

View File

@@ -9,10 +9,10 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
import { SettingsEditor2Input, KeybindingsEditorInput, PreferencesEditorInput } from 'vs/workbench/services/preferences/common/preferencesEditorInput';
import { isEqual } from 'vs/base/common/resources';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions';
import { IEditorInput } from 'vs/workbench/common/editor';
import { IViewlet } from 'vs/workbench/common/viewlet';
import { IViewsService } from 'vs/workbench/common/views';
import { VIEW_CONTAINER_ID } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncView';
export class UserDataSyncTrigger extends Disposable {
@@ -22,24 +22,15 @@ export class UserDataSyncTrigger extends Disposable {
constructor(
@IEditorService editorService: IEditorService,
@IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService,
@IViewletService viewletService: IViewletService,
@IViewsService viewsService: IViewsService,
) {
super();
this._register(Event.any<string | undefined>(
Event.map(editorService.onDidActiveEditorChange, () => this.getUserDataEditorInputSource(editorService.activeEditor)),
Event.map(viewletService.onDidViewletOpen, viewlet => this.getUserDataViewletSource(viewlet))
)(source => {
if (source) {
this._onDidTriggerSync.fire(source);
}
}));
}
private getUserDataViewletSource(viewlet: IViewlet): string | undefined {
if (viewlet.getId() === VIEWLET_ID) {
return 'extensionsViewlet';
}
return undefined;
this._register(
Event.filter(
Event.any<string | undefined>(
Event.map(editorService.onDidActiveEditorChange, () => this.getUserDataEditorInputSource(editorService.activeEditor)),
Event.map(Event.filter(viewsService.onDidChangeViewContainerVisibility, e => [VIEWLET_ID, VIEW_CONTAINER_ID].includes(e.id) && e.visible), e => e.id)
), source => source !== undefined)(source => this._onDidTriggerSync.fire(source!)));
}
private getUserDataEditorInputSource(editorInput: IEditorInput | undefined): string | undefined {

View File

@@ -10,9 +10,9 @@ import { localize } from 'vs/nls';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { TreeViewPane, TreeView } from 'vs/workbench/browser/parts/views/treeView';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ALL_SYNC_RESOURCES, CONTEXT_SYNC_ENABLEMENT, SyncResource, IUserDataSyncService, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync';
import { ALL_SYNC_RESOURCES, SyncResource, IUserDataSyncService, ISyncResourceHandle, CONTEXT_SYNC_STATE, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync';
import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions';
import { IContextKeyService, RawContextKey, ContextKeyExpr, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey';
import { IContextKeyService, RawContextKey, ContextKeyExpr, ContextKeyEqualsExpr, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { URI } from 'vs/base/common/uri';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { FolderThemeIcon } from 'vs/platform/theme/common/themeService';
@@ -20,38 +20,45 @@ import { fromNow } from 'vs/base/common/date';
import { pad, uppercaseFirstLetter } from 'vs/base/common/strings';
import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { Codicon } from 'vs/base/common/codicons';
import { CONTEXT_ACCOUNT_STATE } from 'vs/workbench/contrib/userDataSync/browser/userDataSync';
import { AccountStatus } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncAccount';
export const VIEW_CONTAINER_ID = 'workbench.view.sync';
const CONTEXT_ENABLE_VIEWS = new RawContextKey<boolean>(`showUserDataSyncViews`, false);
export class UserDataSyncViewContribution implements IWorkbenchContribution {
private readonly viewsEnablementContext: IContextKey<boolean>;
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
) {
const container = this.registerSyncViewContainer();
this.registerBackupView(container, true);
this.registerBackupView(container, false);
this.viewsEnablementContext = CONTEXT_ENABLE_VIEWS.bindTo(this.contextKeyService);
this.registerView(container, true);
this.registerView(container, false);
}
private registerSyncViewContainer(): ViewContainer {
return Registry.as<IViewContainersRegistry>(Extensions.ViewContainersRegistry).registerViewContainer(
{
id: 'workbench.view.sync',
id: VIEW_CONTAINER_ID,
name: localize('sync preferences', "Preferences Sync"),
ctorDescriptor: new SyncDescriptor(
ViewPaneContainer,
['workbench.view.sync', { mergeViewWithContainerWhenSingleView: true }]
[VIEW_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true }]
),
icon: Codicon.sync.classNames,
hideIfEmpty: true,
}, ViewContainerLocation.Sidebar);
}
private registerBackupView(container: ViewContainer, remote: boolean): void {
const id = `workbench.views.sync.${remote ? 'remote' : 'local'}BackupView`;
const name = remote ? localize('remote title', "Remote Backup") : localize('local title', "Local Backup");
const contextKey = new RawContextKey<boolean>(`showUserDataSync${remote ? 'Remote' : 'Local'}BackupView`, false);
const viewEnablementContext = contextKey.bindTo(this.contextKeyService);
private registerView(container: ViewContainer, remote: boolean): void {
const that = this;
const id = `workbench.views.sync.${remote ? 'remote' : 'local'}DataView`;
const name = remote ? localize('remote title', "Remote Data") : localize('local title', "Local Backup");
const treeView = this.instantiationService.createInstance(TreeView, id, name);
treeView.showCollapseAllAction = true;
treeView.showRefreshAction = true;
@@ -66,7 +73,7 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution {
id,
name,
ctorDescriptor: new SyncDescriptor(TreeViewPane),
when: ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, contextKey),
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Available), CONTEXT_ENABLE_VIEWS),
canToggleVisibility: true,
canMoveView: true,
treeView,
@@ -77,21 +84,21 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution {
registerAction2(class extends Action2 {
constructor() {
super({
id: `workbench.actions.showSync${remote ? 'Remote' : 'Local'}BackupView`,
id: `workbench.actions.showSync${remote ? 'Remote' : 'Local'}DataView`,
title: remote ?
{ value: localize('workbench.action.showSyncRemoteBackup', "Show Remote Backup"), original: `Show Remote Backup` }
{ value: localize('workbench.action.showSyncRemoteBackup', "Show Remote Data"), original: `Show Remote Data` }
: { value: localize('workbench.action.showSyncLocalBackup', "Show Local Backup"), original: `Show Local Backup` },
category: { value: localize('sync preferences', "Preferences Sync"), original: `Preferences Sync` },
menu: {
id: MenuId.CommandPalette,
when: CONTEXT_SYNC_ENABLEMENT
when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_ACCOUNT_STATE.isEqualTo(AccountStatus.Available)),
},
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const viewDescriptorService = accessor.get(IViewDescriptorService);
const viewsService = accessor.get(IViewsService);
viewEnablementContext.set(true);
that.viewsEnablementContext.set(true);
const viewContainer = viewDescriptorService.getViewContainerByViewId(id);
if (viewContainer) {
const model = viewDescriptorService.getViewContainerModel(viewContainer);
@@ -105,7 +112,6 @@ export class UserDataSyncViewContribution implements IWorkbenchContribution {
}
});
}
}
}
});

View File

@@ -0,0 +1,35 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IUserDataAutoSyncService, UserDataSyncError } from 'vs/platform/userDataSync/common/userDataSync';
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
import { Disposable } from 'vs/base/common/lifecycle';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { Event } from 'vs/base/common/event';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger';
export class UserDataAutoSyncService extends Disposable implements IUserDataAutoSyncService {
_serviceBrand: undefined;
private readonly channel: IChannel;
get onError(): Event<UserDataSyncError> { return Event.map(this.channel.listen<Error>('onError'), e => UserDataSyncError.toUserDataSyncError(e)); }
constructor(
@IInstantiationService instantiationService: IInstantiationService,
@ISharedProcessService sharedProcessService: ISharedProcessService
) {
super();
this.channel = sharedProcessService.getChannel('userDataAutoSync');
this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(source => this.triggerAutoSync([source])));
}
triggerAutoSync(sources: string[]): Promise<void> {
return this.channel.call('triggerAutoSync', [sources]);
}
}

View File

@@ -168,6 +168,9 @@ export abstract class BaseWebview<T extends HTMLElement> extends Disposable {
private readonly _onDidFocus = this._register(new Emitter<void>());
public readonly onDidFocus = this._onDidFocus.event;
private readonly _onDidBlur = this._register(new Emitter<void>());
public readonly onDidBlur = this._onDidBlur.event;
public sendMessage(data: any): void {
this._send('message', data);
}
@@ -267,6 +270,8 @@ export abstract class BaseWebview<T extends HTMLElement> extends Disposable {
this._focused = isFocused;
if (isFocused) {
this._onDidFocus.fire();
} else {
this._onDidBlur.fire();
}
}

View File

@@ -125,6 +125,7 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv
// Forward events from inner webview to outer listeners
this._webviewEvents.clear();
this._webviewEvents.add(webview.onDidFocus(() => { this._onDidFocus.fire(); }));
this._webviewEvents.add(webview.onDidBlur(() => { this._onDidBlur.fire(); }));
this._webviewEvents.add(webview.onDidClickLink(x => { this._onDidClickLink.fire(x); }));
this._webviewEvents.add(webview.onMessage(x => { this._onMessage.fire(x); }));
this._webviewEvents.add(webview.onMissingCsp(x => { this._onMissingCsp.fire(x); }));
@@ -182,6 +183,9 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv
private readonly _onDidFocus = this._register(new Emitter<void>());
public readonly onDidFocus: Event<void> = this._onDidFocus.event;
private readonly _onDidBlur = this._register(new Emitter<void>());
public readonly onDidBlur: Event<void> = this._onDidBlur.event;
private readonly _onDidClickLink = this._register(new Emitter<string>());
public readonly onDidClickLink: Event<string> = this._onDidClickLink.event;

View File

@@ -79,6 +79,7 @@ export interface Webview extends IDisposable {
state: string | undefined;
readonly onDidFocus: Event<void>;
readonly onDidBlur: Event<void>;
readonly onDidClickLink: Event<string>;
readonly onDidScroll: Event<{ scrollYPercentage: number }>;
readonly onDidWheel: Event<IMouseWheelEvent>;