Merge branch 'main' into feature/support-angle-bracket-matching-in-typescript

This commit is contained in:
Henning Dieterichs 2022-07-04 17:17:14 +02:00 committed by GitHub
commit 451ae1ffa3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
208 changed files with 4711 additions and 2368 deletions

View file

@ -145,7 +145,7 @@
"snippets": {"assign": ["jrieken"]},
"splitview": {"assign": ["joaomoreno"]},
"suggest": {"assign": ["jrieken"]},
"tasks": {"assign": ["alexr00"], "accuracy": 0.85},
"tasks": {"assign": ["meganrogge"], "accuracy": 0.85},
"telemetry": {"assign": []},
"themes": {"assign": ["aeschli"]},
"timeline": {"assign": ["lramos15"]},

View file

@ -22,8 +22,6 @@ jobs:
# TODO: rename azure-pipelines/linux/xvfb.init to github-actions
- name: Setup Build Environment
run: |
sudo apt-get update
sudo apt-get install -y libxkbfile-dev pkg-config libsecret-1-dev libxss1 dbus xvfb libgtk-3-0 libgbm1
sudo cp build/azure-pipelines/linux/xvfb.init /etc/init.d/xvfb
sudo chmod +x /etc/init.d/xvfb
sudo update-rc.d xvfb defaults

View file

@ -599,17 +599,24 @@ function createXlfFilesForExtensions() {
const basename = path.basename(file.path);
if (basename === 'package.nls.json') {
const json = JSON.parse(buffer.toString('utf8'));
const keys = Object.keys(json);
const messages = keys.map((key) => {
const keys = [];
const messages = [];
Object.keys(json).forEach((key) => {
const value = json[key];
if (Is.string(value)) {
return value;
keys.push(key);
messages.push(value);
}
else if (value) {
return value.message;
keys.push({
key,
comment: value.comment
});
messages.push(value.message);
}
else {
return `Unknown message for key: ${key}`;
keys.push(key);
messages.push(`Unknown message for key: ${key}`);
}
});
getXlf().addFile(`extensions/${extensionName}/package`, keys, messages);

View file

@ -714,15 +714,22 @@ export function createXlfFilesForExtensions(): ThroughStream {
const basename = path.basename(file.path);
if (basename === 'package.nls.json') {
const json: PackageJsonFormat = JSON.parse(buffer.toString('utf8'));
const keys = Object.keys(json);
const messages = keys.map((key) => {
const keys: Array<string | LocalizeInfo> = [];
const messages: string[] = [];
Object.keys(json).forEach((key) => {
const value = json[key];
if (Is.string(value)) {
return value;
keys.push(key);
messages.push(value);
} else if (value) {
return value.message;
keys.push({
key,
comment: value.comment
});
messages.push(value.message);
} else {
return `Unknown message for key: ${key}`;
keys.push(key);
messages.push(`Unknown message for key: ${key}`);
}
});
getXlf().addFile(`extensions/${extensionName}/package`, keys, messages);

View file

@ -129,7 +129,7 @@
},
"emmet.useInlineCompletions": {
"type": "boolean",
"default": true,
"default": false,
"markdownDescription": "%emmetUseInlineCompletions%"
},
"emmet.preferences": {

View file

@ -70,7 +70,7 @@ class RemoteSourceProviderQuickPick {
}));
}
} catch (err) {
this.quickpick!.items = [{ label: localize('error', "$(error) Error: {0}", err.message), alwaysShow: true }];
this.quickpick!.items = [{ label: localize('error', "{0} Error: {1}", '$(error)', err.message), alwaysShow: true }];
console.error(err);
} finally {
this.quickpick!.busy = false;

View file

@ -307,6 +307,18 @@
"category": "Git",
"enablement": "!commitInProgress"
},
{
"command": "git.commitMessageAccept",
"title": "%command.commitMessageAccept%",
"icon": "$(check)",
"category": "Git"
},
{
"command": "git.commitMessageDiscard",
"title": "%command.commitMessageDiscard%",
"icon": "$(discard)",
"category": "Git"
},
{
"command": "git.restoreCommitTemplate",
"title": "%command.restoreCommitTemplate%",
@ -315,7 +327,8 @@
{
"command": "git.undoCommit",
"title": "%command.undoCommit%",
"category": "Git"
"category": "Git",
"enablement": "!commitInProgress"
},
{
"command": "git.checkout",
@ -586,7 +599,8 @@
{
"command": "git.acceptMerge",
"title": "%command.git.acceptMerge%",
"category": "Git"
"category": "Git",
"enablement": "isMergeEditor"
}
],
"keybindings": [
@ -795,6 +809,14 @@
"command": "git.restoreCommitTemplate",
"when": "false"
},
{
"command": "git.commitMessageAccept",
"when": "false"
},
{
"command": "git.commitMessageDiscard",
"when": "false"
},
{
"command": "git.revealInExplorer",
"when": "false"
@ -1480,6 +1502,16 @@
"group": "navigation",
"when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && !isInDiffEditor && !isMergeEditor && resourceScheme == file && scmActiveResourceHasChanges"
},
{
"command": "git.commitMessageAccept",
"group": "navigation",
"when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && editorLangId == git-commit && commitInProgress"
},
{
"command": "git.commitMessageDiscard",
"group": "navigation",
"when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && editorLangId == git-commit && commitInProgress"
},
{
"command": "git.stageSelectedRanges",
"group": "2_git@1",
@ -2438,19 +2470,6 @@
"default": 10000,
"description": "%config.statusLimit%"
},
"git.experimental.installGuide": {
"type": "string",
"enum": [
"default",
"download"
],
"tags": [
"experimental"
],
"scope": "machine",
"description": "%config.experimental.installGuide%",
"default": "default"
},
"git.repositoryScanIgnoredFolders": {
"type": "array",
"items": {
@ -2627,56 +2646,51 @@
"contents": "%view.workbench.scm.disabled%",
"when": "!config.git.enabled"
},
{
"view": "scm",
"contents": "%view.workbench.scm.missing.guide%",
"when": "config.git.enabled && git.missing && config.git.experimental.installGuide == download"
},
{
"view": "scm",
"contents": "%view.workbench.scm.missing.guide.mac%",
"when": "config.git.enabled && git.missing && config.git.experimental.installGuide == download && isMac"
},
{
"view": "scm",
"contents": "%view.workbench.scm.missing.guide.windows%",
"when": "config.git.enabled && git.missing && config.git.experimental.installGuide == download && isWindows"
},
{
"view": "scm",
"contents": "%view.workbench.scm.missing.guide.linux%",
"when": "config.git.enabled && git.missing && config.git.experimental.installGuide == download && isLinux"
},
{
"view": "scm",
"contents": "%view.workbench.scm.missing%",
"when": "config.git.enabled && git.missing && config.git.experimental.installGuide == default"
"when": "config.git.enabled && git.missing"
},
{
"view": "scm",
"contents": "%view.workbench.scm.missing.mac%",
"when": "config.git.enabled && git.missing && isMac"
},
{
"view": "scm",
"contents": "%view.workbench.scm.missing.windows%",
"when": "config.git.enabled && git.missing && isWindows"
},
{
"view": "scm",
"contents": "%view.workbench.scm.missing.linux%",
"when": "config.git.enabled && git.missing && isLinux"
},
{
"view": "scm",
"contents": "%view.workbench.scm.empty%",
"when": "config.git.enabled && workbenchState == empty",
"when": "config.git.enabled && !git.missing && workbenchState == empty",
"enablement": "git.state == initialized",
"group": "2_open@1"
},
{
"view": "scm",
"contents": "%view.workbench.scm.folder%",
"when": "config.git.enabled && workbenchState == folder",
"when": "config.git.enabled && !git.missing && workbenchState == folder",
"enablement": "git.state == initialized",
"group": "5_scm@1"
},
{
"view": "scm",
"contents": "%view.workbench.scm.workspace%",
"when": "config.git.enabled && workbenchState == workspace && workspaceFolderCount != 0",
"when": "config.git.enabled && !git.missing && workbenchState == workspace && workspaceFolderCount != 0",
"enablement": "git.state == initialized",
"group": "5_scm@1"
},
{
"view": "scm",
"contents": "%view.workbench.scm.emptyWorkspace%",
"when": "config.git.enabled && workbenchState == workspace && workspaceFolderCount == 0",
"when": "config.git.enabled && !git.missing && workbenchState == workspace && workspaceFolderCount == 0",
"enablement": "git.state == initialized",
"group": "2_open@1"
},

View file

@ -46,6 +46,8 @@
"command.commitAllNoVerify": "Commit All (No Verify)",
"command.commitAllSignedNoVerify": "Commit All (Signed Off, No Verify)",
"command.commitAllAmendNoVerify": "Commit All (Amend, No Verify)",
"command.commitMessageAccept": "Accept Commit Message",
"command.commitMessageDiscard": "Discard Commit Message",
"command.restoreCommitTemplate": "Restore Commit Template",
"command.undoCommit": "Undo Last Commit",
"command.checkout": "Checkout to...",
@ -140,7 +142,7 @@
"config.ignoreLimitWarning": "Ignores the warning when there are too many changes in a repository.",
"config.ignoreRebaseWarning": "Ignores the warning when it looks like the branch might have been rebased when pulling.",
"config.defaultCloneDirectory": "The default location to clone a git repository.",
"config.useEditorAsCommitInput": "Use an editor to author the commit message.",
"config.useEditorAsCommitInput": "Controls whether a full text editor will be used to author commit messages, whenever no message is provided in the commit input box.",
"config.verboseCommit": "Enable verbose output when `#git.useEditorAsCommitInput#` is enabled.",
"config.enableSmartCommit": "Commit all changes when there are no staged changes.",
"config.smartCommitChanges": "Control which changes are automatically staged by Smart Commit.",
@ -264,15 +266,7 @@
"colors.ignored": "Color for ignored resources.",
"colors.conflict": "Color for resources with conflicts.",
"colors.submodule": "Color for submodule resources.",
"view.workbench.scm.missing": {
"message": "A valid git installation was not detected, more details can be found in the [git output](command:git.showOutput).\nPlease [install git](https://git-scm.com/), or learn more about how to use git and source control in VS Code in [our docs](https://aka.ms/vscode-scm).\nIf you're using a different version control system, you can [search the Marketplace](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22) for additional extensions.",
"comment": [
"{Locked='](command:git.showOutput'}",
"Do not translate the 'command:*' part inside of the '(..)'. It is an internal command syntax for VS Code",
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.missing.guide.windows": {
"view.workbench.scm.missing.windows": {
"message": "[Download Git for Windows](https://git-scm.com/download/win)\nAfter installing, please [reload](command:workbench.action.reloadWindow) (or [troubleshoot](command:git.showOutput)). Additional source control providers can be installed [from the Marketplace](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22).",
"comment": [
"{Locked='](command:workbench.action.reloadWindow'}",
@ -280,7 +274,7 @@
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.missing.guide.mac": {
"view.workbench.scm.missing.mac": {
"message": "[Download Git for macOS](https://git-scm.com/download/mac)\nAfter installing, please [reload](command:workbench.action.reloadWindow) (or [troubleshoot](command:git.showOutput)). Additional source control providers can be installed [from the Marketplace](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22).",
"comment": [
"{Locked='](command:workbench.action.reloadWindow'}",
@ -288,7 +282,7 @@
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.missing.guide.linux": {
"view.workbench.scm.missing.linux": {
"message": "Source control depends on Git being installed.\n[Download Git for Linux](https://git-scm.com/download/linux)\nAfter installing, please [reload](command:workbench.action.reloadWindow) (or [troubleshoot](command:git.showOutput)). Additional source control providers can be installed [from the Marketplace](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22).",
"comment": [
"{Locked='](command:workbench.action.reloadWindow'}",
@ -296,7 +290,7 @@
"Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links"
]
},
"view.workbench.scm.missing.guide": "Install Git, a popular source control system, to track code changes and collaborate with others. Learn more in our [Git guides](https://aka.ms/vscode-scm).",
"view.workbench.scm.missing": "Install Git, a popular source control system, to track code changes and collaborate with others. Learn more in our [Git guides](https://aka.ms/vscode-scm).",
"view.workbench.scm.disabled": {
"message": "If you would like to use git features, please enable git in your [settings](command:workbench.action.openSettings?%5B%22git.enabled%22%5D).\nTo learn more about how to use git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).",
"comment": [

View file

@ -13,8 +13,10 @@ const localize = nls.loadMessageBundle();
interface ActionButtonState {
readonly HEAD: Branch | undefined;
readonly isActionRunning: boolean;
readonly repositoryHasNoChanges: boolean;
readonly isCommitInProgress: boolean;
readonly isMergeInProgress: boolean;
readonly isSyncInProgress: boolean;
readonly repositoryHasChanges: boolean;
}
export class ActionButtonCommand {
@ -33,7 +35,13 @@ export class ActionButtonCommand {
private disposables: Disposable[] = [];
constructor(readonly repository: Repository) {
this._state = { HEAD: undefined, isActionRunning: false, repositoryHasNoChanges: false };
this._state = {
HEAD: undefined,
isCommitInProgress: false,
isMergeInProgress: false,
isSyncInProgress: false,
repositoryHasChanges: false
};
repository.onDidRunGitStatus(this.onDidRunGitStatus, this, this.disposables);
repository.onDidChangeOperations(this.onDidChangeOperations, this, this.disposables);
@ -49,158 +57,152 @@ export class ActionButtonCommand {
}
get button(): SourceControlActionButton | undefined {
if (!this.state.HEAD || !this.state.HEAD.name || !this.state.HEAD.commit) { return undefined; }
if (!this.state.HEAD || !this.state.HEAD.name) { return undefined; }
let actionButton: SourceControlActionButton | undefined;
if (this.state.repositoryHasNoChanges) {
if (this.state.HEAD.upstream) {
// Sync Changes
actionButton = this.getSyncChangesActionButton();
} else {
// Publish Branch
actionButton = this.getPublishBranchActionButton();
}
} else {
// Commit Changes
if (this.state.repositoryHasChanges) {
// Commit Changes (enabled)
actionButton = this.getCommitActionButton();
}
return actionButton;
// Commit Changes (enabled) -> Publish Branch -> Sync Changes -> Commit Changes (disabled)
return actionButton ?? this.getPublishBranchActionButton() ?? this.getSyncChangesActionButton() ?? this.getCommitActionButton();
}
private getCommitActionButton(): SourceControlActionButton | undefined {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const showActionButton = config.get<{ commit: boolean }>('showActionButton', { commit: true });
if (showActionButton.commit) {
let title: string, tooltip: string;
const postCommitCommand = config.get<string>('postCommitCommand');
// The button is disabled
if (!showActionButton.commit) { return undefined; }
switch (postCommitCommand) {
case 'push': {
title = localize('scm button commit and push title', "$(arrow-up) Commit & Push");
tooltip = this.state.isActionRunning ?
localize('scm button committing pushing tooltip', "Committing & Pushing Changes...") :
localize('scm button commit push tooltip', "Commit & Push Changes");
break;
}
case 'sync': {
title = localize('scm button commit and sync title', "$(sync) Commit & Sync");
tooltip = this.state.isActionRunning ?
localize('scm button committing synching tooltip', "Committing & Synching Changes...") :
localize('scm button commit sync tooltip', "Commit & Sync Changes");
break;
}
default: {
title = localize('scm button commit title', "$(check) Commit");
tooltip = this.state.isActionRunning ?
localize('scm button committing tooltip', "Committing Changes...") :
localize('scm button commit tooltip', "Commit Changes");
break;
}
let title: string, tooltip: string;
const postCommitCommand = config.get<string>('postCommitCommand');
switch (postCommitCommand) {
case 'push': {
title = localize('scm button commit and push title', "{0} Commit & Push", '$(arrow-up)');
tooltip = this.state.isCommitInProgress ?
localize('scm button committing pushing tooltip', "Committing & Pushing Changes...") :
localize('scm button commit push tooltip', "Commit & Push Changes");
break;
}
case 'sync': {
title = localize('scm button commit and sync title', "{0} Commit & Sync", '$(sync)');
tooltip = this.state.isCommitInProgress ?
localize('scm button committing synching tooltip', "Committing & Synching Changes...") :
localize('scm button commit sync tooltip', "Commit & Sync Changes");
break;
}
default: {
title = localize('scm button commit title', "{0} Commit", '$(check)');
tooltip = this.state.isCommitInProgress ?
localize('scm button committing tooltip', "Committing Changes...") :
localize('scm button commit tooltip', "Commit Changes");
break;
}
return {
command: {
command: 'git.commit',
title: title,
tooltip: tooltip,
arguments: [this.repository.sourceControl],
},
secondaryCommands: [
[
{
command: 'git.commit',
title: 'Commit',
arguments: [this.repository.sourceControl, ''],
},
{
command: 'git.commit',
title: 'Commit & Push',
arguments: [this.repository.sourceControl, 'push'],
},
{
command: 'git.commit',
title: 'Commit & Sync',
arguments: [this.repository.sourceControl, 'sync'],
},
]
],
enabled: !this.state.isActionRunning
};
}
return undefined;
return {
command: {
command: 'git.commit',
title: title,
tooltip: tooltip,
arguments: [this.repository.sourceControl],
},
secondaryCommands: [
[
{
command: 'git.commit',
title: localize('scm secondary button commit', "Commit"),
arguments: [this.repository.sourceControl, ''],
},
{
command: 'git.commit',
title: localize('scm secondary button commit and push', "Commit & Push"),
arguments: [this.repository.sourceControl, 'push'],
},
{
command: 'git.commit',
title: localize('scm secondary button commit and sync', "Commit & Sync"),
arguments: [this.repository.sourceControl, 'sync'],
},
]
],
enabled: this.state.repositoryHasChanges && !this.state.isCommitInProgress && !this.state.isMergeInProgress
};
}
private getPublishBranchActionButton(): SourceControlActionButton | undefined {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const showActionButton = config.get<{ publish: boolean }>('showActionButton', { publish: true });
if (showActionButton.publish) {
return {
command: {
command: 'git.publish',
title: localize('scm publish branch action button title', "{0} Publish Branch", '$(cloud-upload)'),
tooltip: this.state.isActionRunning ?
localize('scm button publish branch running', "Publishing Branch...") :
localize('scm button publish branch', "Publish Branch"),
arguments: [this.repository.sourceControl],
},
enabled: !this.state.isActionRunning
};
}
// Branch does have an upstream, commit/merge is in progress, or the button is disabled
if (this.state.HEAD?.upstream || this.state.isCommitInProgress || this.state.isMergeInProgress || !showActionButton.publish) { return undefined; }
return undefined;
return {
command: {
command: 'git.publish',
title: localize('scm publish branch action button title', "{0} Publish Branch", '$(cloud-upload)'),
tooltip: this.state.isSyncInProgress ?
localize('scm button publish branch running', "Publishing Branch...") :
localize('scm button publish branch', "Publish Branch"),
arguments: [this.repository.sourceControl],
},
enabled: !this.state.isSyncInProgress
};
}
private getSyncChangesActionButton(): SourceControlActionButton | undefined {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const showActionButton = config.get<{ sync: boolean }>('showActionButton', { sync: true });
if (this.state.HEAD?.ahead && showActionButton.sync) {
const rebaseWhenSync = config.get<string>('rebaseWhenSync');
// Branch does not have an upstream, commit/merge is in progress, or the button is disabled
if (!this.state.HEAD?.upstream || this.state.isCommitInProgress || this.state.isMergeInProgress || !showActionButton.sync) { return undefined; }
const ahead = `${this.state.HEAD.ahead}$(arrow-up)`;
const behind = this.state.HEAD.behind ? `${this.state.HEAD.behind}$(arrow-down) ` : '';
const icon = this.state.isActionRunning ? '$(sync~spin)' : '$(sync)';
const ahead = this.state.HEAD.ahead ? ` ${this.state.HEAD.ahead}$(arrow-up)` : '';
const behind = this.state.HEAD.behind ? ` ${this.state.HEAD.behind}$(arrow-down)` : '';
const icon = this.state.isSyncInProgress ? '$(sync~spin)' : '$(sync)';
return {
command: {
command: rebaseWhenSync ? 'git.syncRebase' : 'git.sync',
title: `${icon} ${behind} ${ahead}`,
tooltip: this.state.isActionRunning ?
localize('syncing changes', "Synchronizing Changes...")
: this.repository.syncTooltip,
arguments: [this.repository.sourceControl],
},
description: localize('scm button sync description', "{0} Sync Changes {1}{2}", icon, behind, ahead),
enabled: !this.state.isActionRunning
};
}
const rebaseWhenSync = config.get<string>('rebaseWhenSync');
return undefined;
return {
command: {
command: rebaseWhenSync ? 'git.syncRebase' : 'git.sync',
title: `${icon}${behind}${ahead}`,
tooltip: this.state.isSyncInProgress ?
localize('syncing changes', "Synchronizing Changes...")
: this.repository.syncTooltip,
arguments: [this.repository.sourceControl],
},
description: localize('scm button sync description', "{0} Sync Changes{1}{2}", icon, behind, ahead),
enabled: !this.state.isSyncInProgress
};
}
private onDidChangeOperations(): void {
const isActionRunning =
this.repository.operations.isRunning(Operation.Sync) ||
this.repository.operations.isRunning(Operation.Push) ||
this.repository.operations.isRunning(Operation.Pull) ||
const isCommitInProgress =
this.repository.operations.isRunning(Operation.Commit);
this.state = { ...this.state, isActionRunning };
const isSyncInProgress =
this.repository.operations.isRunning(Operation.Sync) ||
this.repository.operations.isRunning(Operation.Push) ||
this.repository.operations.isRunning(Operation.Pull);
this.state = { ...this.state, isCommitInProgress, isSyncInProgress };
}
private onDidRunGitStatus(): void {
this.state = {
...this.state,
HEAD: this.repository.HEAD,
repositoryHasNoChanges:
this.repository.indexGroup.resourceStates.length === 0 &&
this.repository.mergeGroup.resourceStates.length === 0 &&
this.repository.untrackedGroup.resourceStates.length === 0 &&
this.repository.workingTreeGroup.resourceStates.length === 0
isMergeInProgress:
this.repository.mergeGroup.resourceStates.length !== 0,
repositoryHasChanges:
this.repository.indexGroup.resourceStates.length !== 0 ||
this.repository.untrackedGroup.resourceStates.length !== 0 ||
this.repository.workingTreeGroup.resourceStates.length !== 0
};
}

View file

@ -28,6 +28,7 @@ class CheckoutItem implements QuickPickItem {
protected get shortCommit(): string { return (this.ref.commit || '').substr(0, 8); }
get label(): string { return `${this.repository.isBranchProtected(this.ref.name ?? '') ? '$(lock)' : '$(git-branch)'} ${this.ref.name || this.shortCommit}`; }
get description(): string { return this.shortCommit; }
get refName(): string | undefined { return this.ref.name; }
constructor(protected repository: Repository, protected ref: Ref) { }
@ -140,6 +141,7 @@ class HEADItem implements QuickPickItem {
get label(): string { return 'HEAD'; }
get description(): string { return (this.repository.HEAD && this.repository.HEAD.commit || '').substr(0, 8); }
get alwaysShow(): boolean { return true; }
get refName(): string { return 'HEAD'; }
}
class AddRemoteItem implements QuickPickItem {
@ -1104,13 +1106,21 @@ export class CommandCenter {
}
await doc.save();
await repository.add([uri]);
// TODO@jrieken there isn't a `TabInputTextMerge` instance yet, till now the merge editor
// uses the `TabInputText` for the out-resource and we use that to identify and CLOSE the tab
// see https://github.com/microsoft/vscode/issues/153213
const { activeTab } = window.tabGroups.activeTabGroup;
let didCloseTab = false;
if (activeTab && activeTab?.input instanceof TabInputText && activeTab.input.uri.toString() === uri.toString()) {
await window.tabGroups.close(activeTab, true);
didCloseTab = await window.tabGroups.close(activeTab, true);
}
// Only stage if the merge editor has been successfully closed. That means all conflicts have been
// handled or unhandled conflicts are OK by the user.
if (didCloseTab) {
await repository.add([uri]);
await commands.executeCommand('workbench.view.scm');
}
}
@ -1615,7 +1625,7 @@ export class CommandCenter {
const postCommitCommand = config.get<'none' | 'push' | 'sync'>('postCommitCommand');
if ((opts.postCommitCommand === undefined && postCommitCommand === 'push') || opts.postCommitCommand === 'push') {
await this._push(repository, { pushType: PushType.Push, silent: true });
await this._push(repository, { pushType: PushType.Push });
}
if ((opts.postCommitCommand === undefined && postCommitCommand === 'sync') || opts.postCommitCommand === 'sync') {
await this.sync(repository);
@ -1701,6 +1711,51 @@ export class CommandCenter {
await this.commitWithAnyInput(repository, { all: true, amend: true });
}
@command('git.commitMessageAccept')
async commitMessageAccept(arg?: Uri): Promise<void> {
if (!arg) { return; }
// Close the tab
this._closeEditorTab(arg);
}
@command('git.commitMessageDiscard')
async commitMessageDiscard(arg?: Uri): Promise<void> {
if (!arg) { return; }
// Clear the contents of the editor
const editors = window.visibleTextEditors
.filter(e => e.document.languageId === 'git-commit' && e.document.uri.toString() === arg.toString());
if (editors.length !== 1) { return; }
const commitMsgEditor = editors[0];
const commitMsgDocument = commitMsgEditor.document;
const editResult = await commitMsgEditor.edit(builder => {
const firstLine = commitMsgDocument.lineAt(0);
const lastLine = commitMsgDocument.lineAt(commitMsgDocument.lineCount - 1);
builder.delete(new Range(firstLine.range.start, lastLine.range.end));
});
if (!editResult) { return; }
// Save the document
const saveResult = await commitMsgDocument.save();
if (!saveResult) { return; }
// Close the tab
this._closeEditorTab(arg);
}
private _closeEditorTab(uri: Uri): void {
const tabToClose = window.tabGroups.all.map(g => g.tabs).flat()
.filter(t => t.input instanceof TabInputText && t.input.uri.toString() === uri.toString());
window.tabGroups.close(tabToClose);
}
private async _commitEmpty(repository: Repository, noVerify?: boolean): Promise<void> {
const root = Uri.file(repository.root);
const config = workspace.getConfiguration('git', root);
@ -1998,7 +2053,9 @@ export class CommandCenter {
return;
}
target = choice.label;
if (choice.refName) {
target = choice.refName;
}
}
await repository.branch(branchName, true, target);

View file

@ -1785,12 +1785,12 @@ export class Repository {
} catch (err) {
if (/^error: failed to push some refs to\b/m.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.PushRejected;
} else if (/Permission.*denied/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.PermissionDenied;
} else if (/Could not read from remote repository/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.RemoteConnectionError;
} else if (/^fatal: The current branch .* has no upstream branch/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.NoUpstreamBranch;
} else if (/Permission.*denied/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.PermissionDenied;
}
throw err;

View file

@ -37,6 +37,10 @@
{
"command": "github.copyVscodeDevLink",
"title": "Copy vscode.dev Link"
},
{
"command": "github.copyVscodeDevLinkFile",
"title": "Copy vscode.dev Link"
}
],
"menus": {
@ -48,18 +52,22 @@
{
"command": "github.copyVscodeDevLink",
"when": "false"
},
{
"command": "github.copyVscodeDevLinkFile",
"when": "false"
}
],
"file/share": [
{
"command": "github.copyVscodeDevLink",
"command": "github.copyVscodeDevLinkFile",
"when": "github.hasGitHubRepo"
}
],
"editor/context/share": [
{
"command": "github.copyVscodeDevLink",
"when": "github.hasGitHubRepo"
"when": "github.hasGitHubRepo && resourceScheme != untitled"
}
]
},

View file

@ -9,6 +9,17 @@ import { publishRepository } from './publish';
import { DisposableStore } from './util';
import { getPermalink } from './links';
async function copyVscodeDevLink(gitAPI: GitAPI, useSelection: boolean) {
try {
const permalink = getPermalink(gitAPI, useSelection, 'https://vscode.dev/github');
if (permalink) {
return vscode.env.clipboard.writeText(permalink);
}
} catch (err) {
vscode.window.showErrorMessage(err.message);
}
}
export function registerCommands(gitAPI: GitAPI): vscode.Disposable {
const disposables = new DisposableStore();
@ -21,14 +32,11 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable {
}));
disposables.add(vscode.commands.registerCommand('github.copyVscodeDevLink', async () => {
try {
const permalink = getPermalink(gitAPI, 'https://vscode.dev/github');
if (permalink) {
vscode.env.clipboard.writeText(permalink);
}
} catch (err) {
vscode.window.showErrorMessage(err.message);
}
return copyVscodeDevLink(gitAPI, true);
}));
disposables.add(vscode.commands.registerCommand('github.copyVscodeDevLinkFile', async () => {
return copyVscodeDevLink(gitAPI, false);
}));
return disposables;

View file

@ -43,13 +43,11 @@ function rangeString(range: vscode.Range | undefined) {
return hash;
}
export function getPermalink(gitAPI: GitAPI, hostPrefix?: string): string | undefined {
export function getPermalink(gitAPI: GitAPI, useSelection: boolean, hostPrefix?: string): string | undefined {
hostPrefix = hostPrefix ?? 'https://github.com';
const { uri, range } = getFileAndPosition();
if (!uri) {
return;
}
const gitRepo = getRepositoryForFile(gitAPI, uri);
// Use the first repo if we cannot determine a repo from the uri.
const gitRepo = (uri ? getRepositoryForFile(gitAPI, uri) : gitAPI.repositories[0]) ?? gitAPI.repositories[0];
if (!gitRepo) {
return;
}
@ -71,8 +69,8 @@ export function getPermalink(gitAPI: GitAPI, hostPrefix?: string): string | unde
}
const commitHash = gitRepo.state.HEAD?.commit;
const pathSegment = uri.path.substring(gitRepo.rootUri.path.length);
const fileSegments = (useSelection && uri) ? `${uri.path.substring(gitRepo.rootUri.path.length)}${rangeString(range)}` : '';
return `${hostPrefix}/${repo.owner}/${repo.repo}/blob/${commitHash
}${pathSegment}${rangeString(range)}`;
}${fileSegments}`;
}

View file

@ -47,7 +47,7 @@
"contributes": {
"notebookRenderer": [
{
"id": "markdownItRenderer",
"id": "vscode.markdown-it-renderer",
"displayName": "Markdown it renderer",
"entrypoint": "./notebook-out/index.js",
"mimeTypes": [

View file

@ -514,17 +514,17 @@ export class DiagnosticComputer {
const diagnostics: vscode.Diagnostic[] = [];
for (const link of links) {
if (link.href.kind === 'internal'
&& link.source.text.startsWith('#')
&& link.source.hrefText.startsWith('#')
&& link.href.path.toString() === doc.uri.toString()
&& link.href.fragment
&& !toc.lookup(link.href.fragment)
) {
if (!this.isIgnoredLink(options, link.source.text)) {
if (!this.isIgnoredLink(options, link.source.hrefText)) {
diagnostics.push(new LinkDoesNotExistDiagnostic(
link.source.hrefRange,
localize('invalidHeaderLink', 'No header found: \'{0}\'', link.href.fragment),
severity,
link.source.text));
link.source.hrefText));
}
}
}
@ -556,7 +556,7 @@ export class DiagnosticComputer {
const fragmentErrorSeverity = toSeverity(typeof options.validateMarkdownFileLinkFragments === 'undefined' ? options.validateFragmentLinks : options.validateMarkdownFileLinkFragments);
// We've already validated our own fragment links in `validateOwnHeaderLinks`
const linkSet = new FileLinkMap(links.filter(link => !link.source.text.startsWith('#')));
const linkSet = new FileLinkMap(links.filter(link => !link.source.hrefText.startsWith('#')));
if (linkSet.size === 0) {
return [];
}
@ -585,10 +585,10 @@ export class DiagnosticComputer {
if (fragmentLinks.length) {
const toc = await this.tocProvider.get(resolvedHrefPath);
for (const link of fragmentLinks) {
if (!toc.lookup(link.fragment) && !this.isIgnoredLink(options, link.source.pathText) && !this.isIgnoredLink(options, link.source.text)) {
if (!toc.lookup(link.fragment) && !this.isIgnoredLink(options, link.source.pathText) && !this.isIgnoredLink(options, link.source.hrefText)) {
const msg = localize('invalidLinkToHeaderInOtherFile', 'Header does not exist in file: {0}', link.fragment);
const range = link.source.fragmentRange?.with({ start: link.source.fragmentRange.start.translate(0, -1) }) ?? link.source.hrefRange;
diagnostics.push(new LinkDoesNotExistDiagnostic(range, msg, fragmentErrorSeverity, link.source.text));
diagnostics.push(new LinkDoesNotExistDiagnostic(range, msg, fragmentErrorSeverity, link.source.hrefText));
}
}
}

View file

@ -108,18 +108,34 @@ function getWorkspaceFolder(document: ITextDocument) {
}
export interface MdLinkSource {
/**
* The full range of the link.
*/
readonly range: vscode.Range;
/**
* The file where the link is defined.
*/
readonly resource: vscode.Uri;
/**
* The original text of the link destination in code.
*/
readonly text: string;
readonly hrefText: string;
/**
* The original text of just the link's path in code.
*/
readonly pathText: string;
readonly resource: vscode.Uri;
/**
* The range of the path.
*/
readonly hrefRange: vscode.Range;
/**
* The range of the fragment within the path.
*/
readonly fragmentRange: vscode.Range | undefined;
}
@ -145,32 +161,37 @@ function extractDocumentLink(
document: ITextDocument,
pre: string,
rawLink: string,
matchIndex: number | undefined
matchIndex: number,
fullMatch: string,
): MdLink | undefined {
const isAngleBracketLink = rawLink.startsWith('<');
const link = stripAngleBrackets(rawLink);
const offset = (matchIndex || 0) + pre.length + (isAngleBracketLink ? 1 : 0);
const linkStart = document.positionAt(offset);
const linkEnd = document.positionAt(offset + link.length);
let linkTarget: ExternalHref | InternalHref | undefined;
try {
const linkTarget = resolveLink(document, link);
if (!linkTarget) {
return undefined;
}
return {
kind: 'link',
href: linkTarget,
source: {
text: link,
resource: document.uri,
hrefRange: new vscode.Range(linkStart, linkEnd),
...getLinkSourceFragmentInfo(document, link, linkStart, linkEnd),
}
};
linkTarget = resolveLink(document, link);
} catch {
return undefined;
}
if (!linkTarget) {
return undefined;
}
const linkStart = document.positionAt(matchIndex);
const linkEnd = linkStart.translate(0, fullMatch.length);
const hrefStart = linkStart.translate(0, pre.length + (isAngleBracketLink ? 1 : 0));
const hrefEnd = hrefStart.translate(0, link.length);
return {
kind: 'link',
href: linkTarget,
source: {
hrefText: link,
resource: document.uri,
range: new vscode.Range(linkStart, linkEnd),
hrefRange: new vscode.Range(hrefStart, hrefEnd),
...getLinkSourceFragmentInfo(document, link, hrefStart, hrefEnd),
}
};
}
function getFragmentRange(text: string, start: vscode.Position, end: vscode.Position): vscode.Range | undefined {
@ -278,13 +299,28 @@ class NoLinkRanges {
/**
* Inline code spans where links should not be detected
*/
public readonly inline: Map</* line number */ number, readonly vscode.Range[]>
public readonly inline: Map</* line number */ number, vscode.Range[]>
) { }
contains(position: vscode.Position): boolean {
return this.multiline.some(interval => position.line >= interval[0] && position.line < interval[1]) ||
!!this.inline.get(position.line)?.some(inlineRange => inlineRange.contains(position));
}
concatInline(inlineRanges: Iterable<vscode.Range>): NoLinkRanges {
const newInline = new Map(this.inline);
for (const range of inlineRanges) {
for (let line = range.start.line; line <= range.end.line; ++line) {
let entry = newInline.get(line);
if (!entry) {
entry = [];
newInline.set(line, entry);
}
entry.push(range);
}
}
return new NoLinkRanges(this.multiline, newInline);
}
}
/**
@ -302,9 +338,10 @@ export class MdLinkComputer {
return [];
}
const inlineLinks = Array.from(this.getInlineLinks(document, noLinkRanges));
return Array.from([
...this.getInlineLinks(document, noLinkRanges),
...this.getReferenceLinks(document, noLinkRanges),
...inlineLinks,
...this.getReferenceLinks(document, noLinkRanges.concatInline(inlineLinks.map(x => x.source.range))),
...this.getLinkDefinitions(document, noLinkRanges),
...this.getAutoLinks(document, noLinkRanges),
]);
@ -313,13 +350,13 @@ export class MdLinkComputer {
private *getInlineLinks(document: ITextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLink> {
const text = document.getText();
for (const match of text.matchAll(linkPattern)) {
const matchLinkData = extractDocumentLink(document, match[1], match[2], match.index);
const matchLinkData = extractDocumentLink(document, match[1], match[2], match.index ?? 0, match[0]);
if (matchLinkData && !noLinkRanges.contains(matchLinkData.source.hrefRange.start)) {
yield matchLinkData;
// Also check link destination for links
for (const innerMatch of match[1].matchAll(linkPattern)) {
const innerData = extractDocumentLink(document, innerMatch[1], innerMatch[2], (match.index ?? 0) + (innerMatch.index ?? 0));
const innerData = extractDocumentLink(document, innerMatch[1], innerMatch[2], (match.index ?? 0) + (innerMatch.index ?? 0), innerMatch[0]);
if (innerData) {
yield innerData;
}
@ -328,77 +365,83 @@ export class MdLinkComputer {
}
}
private * getAutoLinks(document: ITextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLink> {
private *getAutoLinks(document: ITextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLink> {
const text = document.getText();
for (const match of text.matchAll(autoLinkPattern)) {
const linkOffset = (match.index ?? 0);
const linkStart = document.positionAt(linkOffset);
if (noLinkRanges.contains(linkStart)) {
continue;
}
const link = match[1];
const linkTarget = resolveLink(document, link);
if (linkTarget) {
const offset = (match.index ?? 0) + 1;
const linkStart = document.positionAt(offset);
const linkEnd = document.positionAt(offset + link.length);
const hrefRange = new vscode.Range(linkStart, linkEnd);
if (noLinkRanges.contains(hrefRange.start)) {
continue;
}
yield {
kind: 'link',
href: linkTarget,
source: {
text: link,
resource: document.uri,
hrefRange: new vscode.Range(linkStart, linkEnd),
...getLinkSourceFragmentInfo(document, link, linkStart, linkEnd),
}
};
if (!linkTarget) {
continue;
}
const linkEnd = linkStart.translate(0, match[0].length);
const hrefStart = linkStart.translate(0, 1);
const hrefEnd = hrefStart.translate(0, link.length);
yield {
kind: 'link',
href: linkTarget,
source: {
hrefText: link,
resource: document.uri,
hrefRange: new vscode.Range(hrefStart, hrefEnd),
range: new vscode.Range(linkStart, linkEnd),
...getLinkSourceFragmentInfo(document, link, hrefStart, hrefEnd),
}
};
}
}
private *getReferenceLinks(document: ITextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLink> {
const text = document.getText();
for (const match of text.matchAll(referenceLinkPattern)) {
let linkStart: vscode.Position;
let linkEnd: vscode.Position;
const linkStart = document.positionAt(match.index ?? 0);
if (noLinkRanges.contains(linkStart)) {
continue;
}
let hrefStart: vscode.Position;
let hrefEnd: vscode.Position;
let reference = match[4];
if (reference === '') { // [ref][],
reference = match[3];
const offset = ((match.index ?? 0) + match[1].length) + 1;
linkStart = document.positionAt(offset);
linkEnd = document.positionAt(offset + reference.length);
hrefStart = document.positionAt(offset);
hrefEnd = document.positionAt(offset + reference.length);
} else if (reference) { // [text][ref]
const pre = match[2];
const offset = ((match.index ?? 0) + match[1].length) + pre.length;
linkStart = document.positionAt(offset);
linkEnd = document.positionAt(offset + reference.length);
hrefStart = document.positionAt(offset);
hrefEnd = document.positionAt(offset + reference.length);
} else if (match[5]) { // [ref]
reference = match[5];
const offset = ((match.index ?? 0) + match[1].length) + 1;
linkStart = document.positionAt(offset);
const line = document.lineAt(linkStart.line);
hrefStart = document.positionAt(offset);
const line = document.lineAt(hrefStart.line);
// See if link looks like a checkbox
const checkboxMatch = line.text.match(/^\s*[\-\*]\s*\[x\]/i);
if (checkboxMatch && linkStart.character <= checkboxMatch[0].length) {
if (checkboxMatch && hrefStart.character <= checkboxMatch[0].length) {
continue;
}
linkEnd = document.positionAt(offset + reference.length);
hrefEnd = document.positionAt(offset + reference.length);
} else {
continue;
}
const hrefRange = new vscode.Range(linkStart, linkEnd);
if (noLinkRanges.contains(hrefRange.start)) {
continue;
}
const linkEnd = linkStart.translate(0, match[0].length);
yield {
kind: 'link',
source: {
text: reference,
hrefText: reference,
pathText: reference,
resource: document.uri,
hrefRange,
range: new vscode.Range(linkStart, linkEnd),
hrefRange: new vscode.Range(hrefStart, hrefEnd),
fragmentRange: undefined,
},
href: {
@ -412,44 +455,41 @@ export class MdLinkComputer {
private *getLinkDefinitions(document: ITextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLinkDefinition> {
const text = document.getText();
for (const match of text.matchAll(definitionPattern)) {
const pre = match[1];
const reference = match[2];
const link = match[3].trim();
const offset = (match.index || 0) + pre.length;
const refStart = document.positionAt((match.index ?? 0) + 1);
const refRange = new vscode.Range(refStart, refStart.translate({ characterDelta: reference.length }));
let linkStart: vscode.Position;
let linkEnd: vscode.Position;
let text: string;
if (angleBracketLinkRe.test(link)) {
linkStart = document.positionAt(offset + 1);
linkEnd = document.positionAt(offset + link.length - 1);
text = link.substring(1, link.length - 1);
} else {
linkStart = document.positionAt(offset);
linkEnd = document.positionAt(offset + link.length);
text = link;
}
const hrefRange = new vscode.Range(linkStart, linkEnd);
if (noLinkRanges.contains(hrefRange.start)) {
const offset = (match.index ?? 0);
const linkStart = document.positionAt(offset);
if (noLinkRanges.contains(linkStart)) {
continue;
}
const target = resolveLink(document, text);
if (target) {
yield {
kind: 'definition',
source: {
text: link,
resource: document.uri,
hrefRange,
...getLinkSourceFragmentInfo(document, link, linkStart, linkEnd),
},
ref: { text: reference, range: refRange },
href: target,
};
const pre = match[1];
const reference = match[2];
const rawLinkText = match[3].trim();
const target = resolveLink(document, rawLinkText);
if (!target) {
continue;
}
const isAngleBracketLink = angleBracketLinkRe.test(rawLinkText);
const linkText = stripAngleBrackets(rawLinkText);
const hrefStart = linkStart.translate(0, pre.length + (isAngleBracketLink ? 1 : 0));
const hrefEnd = hrefStart.translate(0, linkText.length);
const hrefRange = new vscode.Range(hrefStart, hrefEnd);
const refStart = linkStart.translate(0, 1);
const refRange = new vscode.Range(refStart, refStart.translate({ characterDelta: reference.length }));
const linkEnd = linkStart.translate(0, match[0].length);
yield {
kind: 'definition',
source: {
hrefText: linkText,
resource: document.uri,
range: new vscode.Range(linkStart, linkEnd),
hrefRange,
...getLinkSourceFragmentInfo(document, rawLinkText, hrefStart, hrefEnd),
},
ref: { text: reference, range: refRange },
href: target,
};
}
}
}

View file

@ -287,7 +287,13 @@ export class MdVsCodePathCompletionProvider implements vscode.CompletionItemProv
const pathSegmentEnd = position.translate({ characterDelta: context.linkSuffix.length });
const replacementRange = new vscode.Range(pathSegmentStart, pathSegmentEnd);
const dirInfo = await this.workspace.readDirectory(parentDir);
let dirInfo: [string, vscode.FileType][];
try {
dirInfo = await this.workspace.readDirectory(parentDir);
} catch {
return;
}
for (const [name, type] of dirInfo) {
// Exclude paths that start with `.`
if (name.startsWith('.')) {

View file

@ -259,7 +259,7 @@ export class MdReferencesProvider extends Disposable {
}
// Exclude cases where the file is implicitly referencing itself
if (link.source.text.startsWith('#') && link.source.resource.fsPath === resource.fsPath) {
if (link.source.hrefText.startsWith('#') && link.source.resource.fsPath === resource.fsPath) {
continue;
}

View file

@ -179,7 +179,7 @@ export class MdVsCodeRenameProvider extends Disposable implements vscode.RenameP
if (ref.kind === 'link') {
// Try to preserve style of existing links
let newPath: string;
if (ref.link.source.text.startsWith('/')) {
if (ref.link.source.hrefText.startsWith('/')) {
const root = resolveDocumentLink('/', ref.link.source.resource);
newPath = '/' + path.relative(root.toString(true), rawNewFilePath.toString(true));
} else {

View file

@ -15,7 +15,7 @@ import { nulLogger } from './nulLogging';
import { assertRangeEqual, joinLines, workspacePath } from './util';
suite('Markdown: MdLinkComputer', () => {
suite.only('Markdown: MdLinkComputer', () => {
function getLinksForFile(fileContents: string): Promise<MdLink[]> {
const doc = new InMemoryDocument(workspacePath('x.md'), fileContents);
@ -24,11 +24,17 @@ suite('Markdown: MdLinkComputer', () => {
return linkProvider.getAllLinks(doc, noopToken);
}
function assertLinksEqual(actualLinks: readonly MdLink[], expectedRanges: readonly vscode.Range[]) {
assert.strictEqual(actualLinks.length, expectedRanges.length);
function assertLinksEqual(actualLinks: readonly MdLink[], expected: ReadonlyArray<vscode.Range | { readonly range: vscode.Range; readonly sourceText: string }>) {
assert.strictEqual(actualLinks.length, expected.length);
for (let i = 0; i < actualLinks.length; ++i) {
assertRangeEqual(actualLinks[i].source.hrefRange, expectedRanges[i], `Range ${i} to be equal`);
const exp = expected[i];
if ('range' in exp) {
assertRangeEqual(actualLinks[i].source.hrefRange, exp.range, `Range ${i} to be equal`);
assert.strictEqual(actualLinks[i].source.hrefText, exp.sourceText, `Source text ${i} to be equal`);
} else {
assertRangeEqual(actualLinks[i].source.hrefRange, exp, `Range ${i} to be equal`);
}
}
}
@ -103,17 +109,23 @@ suite('Markdown: MdLinkComputer', () => {
}
});
test('Should ignore texts in brackets inside link title (#150921)', async () => {
test('Should ignore bracketed text inside link title (#150921)', async () => {
{
const links = await getLinksForFile('[some [inner bracket pairs] in title](<link>)');
const links = await getLinksForFile('[some [inner] in title](link)');
assertLinksEqual(links, [
new vscode.Range(0, 39, 0, 43),
new vscode.Range(0, 24, 0, 28),
]);
}
{
const links = await getLinksForFile('[some [inner bracket pairs] in title](link)');
const links = await getLinksForFile('[some [inner] in title](<link>)');
assertLinksEqual(links, [
new vscode.Range(0, 38, 0, 42)
new vscode.Range(0, 25, 0, 29),
]);
}
{
const links = await getLinksForFile('[some [inner with space] in title](link)');
assertLinksEqual(links, [
new vscode.Range(0, 35, 0, 39),
]);
}
});
@ -164,8 +176,8 @@ suite('Markdown: MdLinkComputer', () => {
));
assertLinksEqual(links, [
new vscode.Range(0, 6, 0, 9),
new vscode.Range(1, 6, 1, 8),
{ range: new vscode.Range(0, 6, 0, 9), sourceText: 'b c' },
{ range: new vscode.Range(1, 6, 1, 8), sourceText: 'cd' },
]);
});
@ -175,7 +187,7 @@ suite('Markdown: MdLinkComputer', () => {
));
assertLinksEqual(links, [
new vscode.Range(0, 9, 0, 28),
{ range: new vscode.Range(0, 9, 0, 28), sourceText: 'https://example.com' },
]);
});
@ -185,8 +197,8 @@ suite('Markdown: MdLinkComputer', () => {
'[ref]: https://example.com',
));
assertLinksEqual(links, [
new vscode.Range(0, 1, 0, 4),
new vscode.Range(1, 7, 1, 26),
{ range: new vscode.Range(0, 1, 0, 4), sourceText: 'ref' },
{ range: new vscode.Range(1, 7, 1, 26), sourceText: 'https://example.com' },
]);
});

View file

@ -8,9 +8,9 @@ import type { RendererContext } from 'vscode-notebook-renderer';
const styleHref = import.meta.url.replace(/katex.js$/, 'katex.min.css');
export async function activate(ctx: RendererContext<void>) {
const markdownItRenderer = (await ctx.getRenderer('markdownItRenderer')) as undefined | any;
const markdownItRenderer = (await ctx.getRenderer('vscode.markdown-it-renderer')) as undefined | any;
if (!markdownItRenderer) {
throw new Error('Could not load markdownItRenderer');
throw new Error(`Could not load 'vscode.markdown-it-renderer'`);
}
// Add katex styles to be copied to shadow dom

View file

@ -58,10 +58,10 @@
],
"notebookRenderer": [
{
"id": "markdownItRenderer-katex",
"id": "vscode.markdown-it-katex-extension",
"displayName": "Markdown it KaTeX renderer",
"entrypoint": {
"extends": "markdownItRenderer",
"extends": "vscode.markdown-it-renderer",
"path": "./notebook-out/katex.js"
}
}

View file

@ -6,7 +6,7 @@
"git": {
"name": "seti-ui",
"repositoryUrl": "https://github.com/jesseweed/seti-ui",
"commitHash": "6b83574de165123583d6d8d5b3b6c91f04b7153d"
"commitHash": "4dd6c27e1f5aed8068c2451dbaf0db3364545937"
}
},
"version": "0.1.0"

View file

@ -1881,6 +1881,7 @@
"dockerfile": "_docker",
"ignore": "_git",
"fsharp": "_f-sharp",
"git-commit": "_git",
"go": "_go2",
"groovy": "_grails",
"handlebars": "_mustache",
@ -2197,6 +2198,7 @@
"dockerfile": "_docker_light",
"ignore": "_git_light",
"fsharp": "_f-sharp_light",
"git-commit": "_git_light",
"go": "_go2_light",
"groovy": "_grails_light",
"handlebars": "_mustache_light",
@ -2338,5 +2340,5 @@
"npm-debug.log": "_npm_ignored_light"
}
},
"version": "https://github.com/jesseweed/seti-ui/commit/6b83574de165123583d6d8d5b3b6c91f04b7153d"
"version": "https://github.com/jesseweed/seti-ui/commit/4dd6c27e1f5aed8068c2451dbaf0db3364545937"
}

View file

@ -983,6 +983,22 @@
"markdownDescription": "%typescript.preferences.includePackageJsonAutoImports%",
"scope": "window"
},
"typescript.preferences.autoImportFileExcludePatterns": {
"type": "array",
"items": {
"type": "string"
},
"markdownDescription": "%typescript.preferences.autoImportFileExcludePatterns%",
"scope": "resource"
},
"javascript.preferences.autoImportFileExcludePatterns": {
"type": "array",
"items": {
"type": "string"
},
"markdownDescription": "%typescript.preferences.autoImportFileExcludePatterns%",
"scope": "resource"
},
"javascript.preferences.renameShorthandProperties": {
"type": "boolean",
"default": true,

View file

@ -136,6 +136,7 @@
"typescript.preferences.includePackageJsonAutoImports.auto": "Search dependencies based on estimated performance impact.",
"typescript.preferences.includePackageJsonAutoImports.on": "Always search dependencies.",
"typescript.preferences.includePackageJsonAutoImports.off": "Never search dependencies.",
"typescript.preferences.autoImportFileExcludePatterns": "Specify glob patterns of files to exclude from auto imports. Requires using TypeScript 4.8 or newer in the workspace.",
"typescript.updateImportsOnFileMove.enabled": "Enable/disable automatic updating of import paths when you rename or move a file in VS Code.",
"typescript.updateImportsOnFileMove.enabled.prompt": "Prompt on each rename.",
"typescript.updateImportsOnFileMove.enabled.always": "Always update paths automatically.",

View file

@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as vscode from 'vscode';
import type * as Proto from '../protocol';
import { ITypeScriptServiceClient } from '../typescriptService';
@ -189,6 +190,8 @@ export default class FileConfigurationManager extends Disposable {
includeCompletionsWithSnippetText: config.get<boolean>('suggest.includeCompletionsWithSnippetText', true),
includeCompletionsWithClassMemberSnippets: config.get<boolean>('suggest.classMemberSnippets.enabled', true),
includeCompletionsWithObjectLiteralMethodSnippets: config.get<boolean>('suggest.objectLiteralMethodSnippets.enabled', true),
// @ts-expect-error until TS 4.8
autoImportFileExcludePatterns: this.getAutoImportFileExcludePatternsPreference(preferencesConfig, vscode.workspace.getWorkspaceFolder(document.uri)?.uri),
useLabelDetailsInCompletionEntries: true,
allowIncompleteCompletions: true,
displayPartsForJSDoc: true,
@ -205,6 +208,18 @@ export default class FileConfigurationManager extends Disposable {
default: return this.client.apiVersion.gte(API.v333) ? 'auto' : undefined;
}
}
private getAutoImportFileExcludePatternsPreference(config: vscode.WorkspaceConfiguration, workspaceFolder: vscode.Uri | undefined): string[] | undefined {
return workspaceFolder && config.get<string[]>('autoImportFileExcludePatterns')?.map(p => {
// Normalization rules: https://github.com/microsoft/TypeScript/pull/49578
const slashNormalized = p.replace(/\\/g, '/');
const isRelative = /^\.\.?($|\/)/.test(slashNormalized);
return path.isAbsolute(p) ? p :
p.startsWith('*') ? '/' + slashNormalized :
isRelative ? vscode.Uri.joinPath(workspaceFolder, p).fsPath :
'/**/' + slashNormalized;
});
}
}
export class InlayHintSettingNames {

View file

@ -33,9 +33,9 @@ async function addCell(code: string, notebook: vscode.NotebookDocument) {
return notebook.cellAt(notebook.cellCount - 1);
}
async function addCellAndRun(code: string, notebook: vscode.NotebookDocument) {
async function addCellAndRun(code: string, notebook: vscode.NotebookDocument, i: number) {
const cell = await addCell(code, notebook);
await vscode.commands.executeCommand('notebook.execute');
await vscode.commands.executeCommand('notebook.cell.execute', { start: i, end: i + 1 });
assert.strictEqual(cell.outputs.length, 1, 'execute failed');
return cell;
}
@ -78,7 +78,7 @@ async function addCellAndRun(code: string, notebook: vscode.NotebookDocument) {
// Run and add a bunch of cells
for (let i = 0; i < 10; i++) {
await addCellAndRun(`print ${i}`, notebookEditor.notebook);
await addCellAndRun(`print ${i}`, notebookEditor.notebook, i);
}
// Verify visible range has the last cell

View file

@ -286,9 +286,6 @@ suite('vscode API - workspace', () => {
sub.dispose();
});
function assertEqualPath(a: string, b: string): void {
assert.ok(pathEquals(a, b), `${a} <-> ${b}`);
}
test('events: onDidOpenTextDocument, onDidChangeTextDocument, onDidSaveTextDocument', async () => {
const file = await createRandomFile();
@ -296,23 +293,20 @@ suite('vscode API - workspace', () => {
await revertAllDirty(); // needed for a clean state for `onDidSaveTextDocument` (#102365)
const pendingAsserts: Function[] = [];
let onDidOpenTextDocument = false;
const onDidOpenTextDocument = new Set<vscode.TextDocument>();
const onDidChangeTextDocument = new Set<vscode.TextDocument>();
const onDidSaveTextDocument = new Set<vscode.TextDocument>();
disposables.push(vscode.workspace.onDidOpenTextDocument(e => {
pendingAsserts.push(() => assertEqualPath(e.uri.fsPath, file.fsPath));
onDidOpenTextDocument = true;
onDidOpenTextDocument.add(e);
}));
let onDidChangeTextDocument = false;
disposables.push(vscode.workspace.onDidChangeTextDocument(e => {
pendingAsserts.push(() => assertEqualPath(e.document.uri.fsPath, file.fsPath));
onDidChangeTextDocument = true;
onDidChangeTextDocument.add(e.document);
}));
let onDidSaveTextDocument = false;
disposables.push(vscode.workspace.onDidSaveTextDocument(e => {
pendingAsserts.push(() => assertEqualPath(e.uri.fsPath, file.fsPath));
onDidSaveTextDocument = true;
onDidSaveTextDocument.add(e);
}));
const doc = await vscode.workspace.openTextDocument(file);
@ -323,10 +317,10 @@ suite('vscode API - workspace', () => {
});
await doc.save();
assert.ok(onDidOpenTextDocument);
assert.ok(onDidChangeTextDocument);
assert.ok(onDidSaveTextDocument);
pendingAsserts.forEach(assert => assert());
assert.ok(Array.from(onDidOpenTextDocument).find(e => e.uri.toString() === file.toString()), 'did Open: ' + file.toString());
assert.ok(Array.from(onDidChangeTextDocument).find(e => e.uri.toString() === file.toString()), 'did Change: ' + file.toString());
assert.ok(Array.from(onDidSaveTextDocument).find(e => e.uri.toString() === file.toString()), 'did Save: ' + file.toString());
disposeAll(disposables);
return deleteFile(file);
});
@ -334,14 +328,13 @@ suite('vscode API - workspace', () => {
test('events: onDidSaveTextDocument fires even for non dirty file when saved', async () => {
const file = await createRandomFile();
const disposables: vscode.Disposable[] = [];
const pendingAsserts: Function[] = [];
await revertAllDirty(); // needed for a clean state for `onDidSaveTextDocument` (#102365)
let onDidSaveTextDocument = false;
const onDidSaveTextDocument = new Set<vscode.TextDocument>();
disposables.push(vscode.workspace.onDidSaveTextDocument(e => {
pendingAsserts.push(() => assertEqualPath(e.uri.fsPath, file.fsPath));
onDidSaveTextDocument = true;
onDidSaveTextDocument.add(e);
}));
const doc = await vscode.workspace.openTextDocument(file);
@ -349,7 +342,7 @@ suite('vscode API - workspace', () => {
await vscode.commands.executeCommand('workbench.action.files.save');
assert.ok(onDidSaveTextDocument);
pendingAsserts.forEach(fn => fn());
assert.ok(Array.from(onDidSaveTextDocument).find(e => e.uri.toString() === file.toString()), 'did Save: ' + file.toString());
disposeAll(disposables);
return deleteFile(file);
});

View file

@ -1,7 +1,7 @@
{
"name": "code-oss-dev",
"version": "1.69.0",
"distro": "a44d562925abc5db8eb21089d4a69ae544b3ec69",
"distro": "daf000367ee34d716c5dbdf836b11c06c407ef5f",
"author": {
"name": "Microsoft Corporation"
},
@ -87,12 +87,12 @@
"vscode-proxy-agent": "^0.12.0",
"vscode-regexpp": "^3.1.0",
"vscode-textmate": "7.0.1",
"xterm": "4.19.0-beta.67",
"xterm-addon-search": "0.9.0-beta.41",
"xterm-addon-serialize": "0.7.0-beta.15",
"xterm": "4.19.0",
"xterm-addon-search": "0.9.0",
"xterm-addon-serialize": "0.7.0",
"xterm-addon-unicode11": "0.4.0-beta.3",
"xterm-addon-webgl": "0.12.0-beta.43",
"xterm-headless": "4.19.0-beta.67",
"xterm-addon-webgl": "0.12.0",
"xterm-headless": "4.19.0",
"yauzl": "^2.9.2",
"yazl": "^2.4.3"
},

View file

@ -46,7 +46,7 @@
},
{
"name": "ms-vscode.js-debug",
"version": "1.68.0",
"version": "1.69.0",
"repo": "https://github.com/microsoft/vscode-js-debug",
"metadata": {
"id": "25629058-ddac-4e17-abba-74678e126c5d",

View file

@ -26,12 +26,12 @@
"vscode-proxy-agent": "^0.12.0",
"vscode-regexpp": "^3.1.0",
"vscode-textmate": "7.0.1",
"xterm": "4.19.0-beta.67",
"xterm-addon-search": "0.9.0-beta.41",
"xterm-addon-serialize": "0.7.0-beta.15",
"xterm": "4.19.0",
"xterm-addon-search": "0.9.0",
"xterm-addon-serialize": "0.7.0",
"xterm-addon-unicode11": "0.4.0-beta.3",
"xterm-addon-webgl": "0.12.0-beta.43",
"xterm-headless": "4.19.0-beta.67",
"xterm-addon-webgl": "0.12.0",
"xterm-headless": "4.19.0",
"yauzl": "^2.9.2",
"yazl": "^2.4.3"
},

View file

@ -12,9 +12,9 @@
"tas-client-umd": "0.1.6",
"vscode-oniguruma": "1.6.1",
"vscode-textmate": "7.0.1",
"xterm": "4.19.0-beta.67",
"xterm-addon-search": "0.9.0-beta.41",
"xterm": "4.19.0",
"xterm-addon-search": "0.9.0",
"xterm-addon-unicode11": "0.4.0-beta.3",
"xterm-addon-webgl": "0.12.0-beta.43"
"xterm-addon-webgl": "0.12.0"
}
}

View file

@ -131,22 +131,22 @@ vscode-textmate@7.0.1:
resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-7.0.1.tgz#8118a32b02735dccd14f893b495fa5389ad7de79"
integrity sha512-zQ5U/nuXAAMsh691FtV0wPz89nSkHbs+IQV8FDk+wew9BlSDhf4UmWGlWJfTR2Ti6xZv87Tj5fENzKf6Qk7aLw==
xterm-addon-search@0.9.0-beta.41:
version "0.9.0-beta.41"
resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.41.tgz#0992da36fe01ff6d71449265a9dabeef7f9d0a3f"
integrity sha512-b1vuWR5JZ8QIiObbKkwSzpzf4x0B9hdzGCHJG+PXWT/xbxk65DOe/X9rgrRyOnCWe5ylQGG4DIHTiREigTp0lg==
xterm-addon-search@0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0.tgz#95278ebb818cfcf882209ae75be96e0bea5d52a5"
integrity sha512-aoolI8YuHvdGw+Qjg8g2M4kst0v86GtB7WeBm4F0jNXA005/6QbWWy9eCsvnIDLJOFI5JSSrZnD6CaOkvBQYPA==
xterm-addon-unicode11@0.4.0-beta.3:
version "0.4.0-beta.3"
resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.4.0-beta.3.tgz#f350184155fafd5ad0d6fbf31d13e6ca7dea1efa"
integrity sha512-FryZAVwbUjKTmwXnm1trch/2XO60F5JsDvOkZhzobV1hm10sFLVuZpFyHXiUx7TFeeFsvNP+S77LAtWoeT5z+Q==
xterm-addon-webgl@0.12.0-beta.43:
version "0.12.0-beta.43"
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.43.tgz#916ed4e390371403aab0d277097cd8a61be436e8"
integrity sha512-hGXfwT6TOmp0tBDiS/iF8s0SLHLd3shJ5zQyS4HNtq99B5cEhHhwaUAZAfjLt5rnUt0kvqR9YWtGsDux5dWVLA==
xterm-addon-webgl@0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0.tgz#2fba8d31890a122adafa1c2fb945482e2ae12973"
integrity sha512-3P5ihdjPnxH6Wrvqjki9UD+duoVrp1fvnO/pSpXP2F1L2GwY6TDNExgj8Yg141vMCNgQbcVqmsTLYEYZxjY92A==
xterm@4.19.0-beta.67:
version "4.19.0-beta.67"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.67.tgz#9a7d79be64469c91bb693b9f5a50b5a22e921cd3"
integrity sha512-4bYTnT6g91N5QId1qPaiSvkpsxMQYpuPdPJItvBZFOcQLTuFSX85x5NeFuDvhNqi33eMFPimyGhzH/JvAV1v/Q==
xterm@4.19.0:
version "4.19.0"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0.tgz#c0f9d09cd61de1d658f43ca75f992197add9ef6d"
integrity sha512-c3Cp4eOVsYY5Q839dR5IejghRPpxciGmLWWaP9g+ppfMeBChMeLa1DCA+pmX/jyDZ+zxFOmlJL/82qVdayVoGQ==

View file

@ -932,35 +932,35 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
xterm-addon-search@0.9.0-beta.41:
version "0.9.0-beta.41"
resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.41.tgz#0992da36fe01ff6d71449265a9dabeef7f9d0a3f"
integrity sha512-b1vuWR5JZ8QIiObbKkwSzpzf4x0B9hdzGCHJG+PXWT/xbxk65DOe/X9rgrRyOnCWe5ylQGG4DIHTiREigTp0lg==
xterm-addon-search@0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0.tgz#95278ebb818cfcf882209ae75be96e0bea5d52a5"
integrity sha512-aoolI8YuHvdGw+Qjg8g2M4kst0v86GtB7WeBm4F0jNXA005/6QbWWy9eCsvnIDLJOFI5JSSrZnD6CaOkvBQYPA==
xterm-addon-serialize@0.7.0-beta.15:
version "0.7.0-beta.15"
resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.7.0-beta.15.tgz#0f7d5f9b423802ac67c2a891d74de530a4fe6a65"
integrity sha512-gO/dxqGgOAuj7DN2ETTeCHyalkb655XogZh5408CmH5D6mjI1lxqVLGUdiFeBVU/OHfWZT2PZ95k06VXqLU5kA==
xterm-addon-serialize@0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.7.0.tgz#cc7ef78972c8425b81dd6ae0a76824ce033d1e5f"
integrity sha512-ZfZ4Zj4uTEBFnUA0exipDGZ14jfiWLCov7gIt2OwIjQEz2ey8ic5kL/cxYz5antNz8/hTSA2qZcyA6VyyQASOQ==
xterm-addon-unicode11@0.4.0-beta.3:
version "0.4.0-beta.3"
resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.4.0-beta.3.tgz#f350184155fafd5ad0d6fbf31d13e6ca7dea1efa"
integrity sha512-FryZAVwbUjKTmwXnm1trch/2XO60F5JsDvOkZhzobV1hm10sFLVuZpFyHXiUx7TFeeFsvNP+S77LAtWoeT5z+Q==
xterm-addon-webgl@0.12.0-beta.43:
version "0.12.0-beta.43"
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.43.tgz#916ed4e390371403aab0d277097cd8a61be436e8"
integrity sha512-hGXfwT6TOmp0tBDiS/iF8s0SLHLd3shJ5zQyS4HNtq99B5cEhHhwaUAZAfjLt5rnUt0kvqR9YWtGsDux5dWVLA==
xterm-addon-webgl@0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0.tgz#2fba8d31890a122adafa1c2fb945482e2ae12973"
integrity sha512-3P5ihdjPnxH6Wrvqjki9UD+duoVrp1fvnO/pSpXP2F1L2GwY6TDNExgj8Yg141vMCNgQbcVqmsTLYEYZxjY92A==
xterm-headless@4.19.0-beta.67:
version "4.19.0-beta.67"
resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.19.0-beta.67.tgz#fe44a243974bf307c9a0694114ab45289734c7f9"
integrity sha512-rpxUsE/te2LN4B/erI108uaAhuLTUsnRl9zIZgcqMqZGFRqDUTtyVM05jIA/8A32g06wFd3WioTkeKc6ow+sLg==
xterm-headless@4.19.0:
version "4.19.0"
resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.19.0.tgz#965eb293fe6258adff5888f24e2a0b778b765e17"
integrity sha512-rYP8I1AGwaztpCoWe9mwxNqmfz7zZCjbzw61QChFqPeiDERjW9CDnqyGhSElvicHAlf47dvA+p4qOuKltxWEkg==
xterm@4.19.0-beta.67:
version "4.19.0-beta.67"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.67.tgz#9a7d79be64469c91bb693b9f5a50b5a22e921cd3"
integrity sha512-4bYTnT6g91N5QId1qPaiSvkpsxMQYpuPdPJItvBZFOcQLTuFSX85x5NeFuDvhNqi33eMFPimyGhzH/JvAV1v/Q==
xterm@4.19.0:
version "4.19.0"
resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0.tgz#c0f9d09cd61de1d658f43ca75f992197add9ef6d"
integrity sha512-c3Cp4eOVsYY5Q839dR5IejghRPpxciGmLWWaP9g+ppfMeBChMeLa1DCA+pmX/jyDZ+zxFOmlJL/82qVdayVoGQ==
yallist@^4.0.0:
version "4.0.0"

View file

@ -1734,6 +1734,19 @@ export function computeClippingRect(elementOrRect: HTMLElement | DOMRectReadOnly
return { top, right, bottom, left };
}
interface DomNodeAttributes {
role?: string;
ariaHidden?: boolean;
style?: StyleAttributes;
}
interface StyleAttributes {
height?: number | string;
width?: number | string;
}
//<div role="presentation" aria-hidden="true" class="scroll-decoration"></div>
/**
* A helper function to create nested dom nodes.
*
@ -1749,22 +1762,22 @@ export function computeClippingRect(elementOrRect: HTMLElement | DOMRectReadOnly
* private readonly editor = createEditor(this.htmlElements.editor);
* ```
*/
export function h<TTag extends string>(tag: TTag): never;
export function h<TTag extends string, TId extends string>(
tag: TTag,
attributes: { $: TId }
attributes: { $: TId } & DomNodeAttributes
): Record<TId | 'root', TagToElement<TTag>>;
export function h<TTag extends string>(tag: TTag, attributes: DomNodeAttributes): Record<'root', TagToElement<TTag>>;
export function h<TTag extends string, T extends (HTMLElement | string | Record<string, HTMLElement>)[]>(
tag: TTag,
children: T
): (ArrayToObj<T> & Record<'root', TagToElement<TTag>>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
export function h<TTag extends string, TId extends string, T extends (HTMLElement | string | Record<string, HTMLElement>)[]>(
tag: TTag,
attributes: { $: TId },
attributes: { $: TId } & DomNodeAttributes,
children: T
): (ArrayToObj<T> & Record<TId, TagToElement<TTag>>) extends infer Y ? { [TKey in keyof Y]: Y[TKey] } : never;
export function h(tag: string, ...args: [] | [attributes: { $: string } | Record<string, any>, children?: any[]] | [children: any[]]): Record<string, HTMLElement> {
let attributes: Record<string, any>;
export function h(tag: string, ...args: [] | [attributes: { $: string } & DomNodeAttributes | Record<string, any>, children?: any[]] | [children: any[]]): Record<string, HTMLElement> {
let attributes: { $?: string } & DomNodeAttributes;
let children: (Record<string, HTMLElement> | HTMLElement)[] | undefined;
if (Array.isArray(args[0])) {
@ -1801,7 +1814,16 @@ export function h(tag: string, ...args: [] | [attributes: { $: string } | Record
result[value] = el;
continue;
}
el.setAttribute(key, value);
if (key === 'style') {
for (const [cssKey, cssValue] of Object.entries(value)) {
el.style.setProperty(
camelCaseToHyphenCase(cssKey),
typeof cssValue === 'number' ? cssValue + 'px' : '' + cssValue
);
}
continue;
}
el.setAttribute(camelCaseToHyphenCase(key), value.toString());
}
result['root'] = el;
@ -1809,6 +1831,10 @@ export function h(tag: string, ...args: [] | [attributes: { $: string } | Record
return result;
}
function camelCaseToHyphenCase(str: string) {
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
type RemoveHTMLElement<T> = T extends HTMLElement ? never : T;
type ArrayToObj<T extends any[]> = UnionToIntersection<RemoveHTMLElement<T[number]>>;

View file

@ -38,6 +38,10 @@
cursor: pointer;
}
.monaco-button-dropdown > .monaco-button:focus {
outline-offset: -1px !important;
}
.monaco-button-dropdown > .monaco-dropdown-button {
margin-left: 1px;
}

View file

@ -15,6 +15,7 @@ import { Emitter, Event as BaseEvent } from 'vs/base/common/event';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { mixin } from 'vs/base/common/objects';
import { localize } from 'vs/nls';
import 'vs/css!./button';
export interface IButtonOptions extends IButtonStyles {
@ -263,6 +264,7 @@ export class ButtonWithDropdown extends Disposable implements IButton {
this.action = this._register(new Action('primaryAction', this.button.label, undefined, true, async () => this._onDidClick.fire(undefined)));
this.dropdownButton = this._register(new Button(this.element, { ...options, title: false, supportIcons: true }));
this.dropdownButton.element.title = localize("button dropdown more actions", 'More Actions...');
this.dropdownButton.element.classList.add('monaco-dropdown-button');
this.dropdownButton.icon = Codicon.dropDownButton;
this._register(this.dropdownButton.onDidClick(e => {

View file

@ -258,6 +258,22 @@ export function isMonacoEditor(e: HTMLElement): boolean {
return isMonacoEditor(e.parentElement);
}
export function isButton(e: HTMLElement): boolean {
if (e.tagName === 'A' && e.classList.contains('monaco-button')) {
return true;
}
if (e.classList.contains('monaco-list')) {
return false;
}
if (!e.parentElement) {
return false;
}
return isButton(e.parentElement);
}
class KeyboardController<T> implements IDisposable {
private readonly disposables = new DisposableStore();

View file

@ -979,7 +979,7 @@ export class MenuBar extends Disposable {
const actualMenuIndex = menuIndex >= this.numMenusShown ? MenuBar.OVERFLOW_INDEX : menuIndex;
const customMenu = actualMenuIndex === MenuBar.OVERFLOW_INDEX ? this.overflowMenu : this.menus[actualMenuIndex];
if (!customMenu.actions || !customMenu.buttonElement) {
if (!customMenu.actions || !customMenu.buttonElement || !customMenu.titleElement) {
return;
}
@ -987,19 +987,19 @@ export class MenuBar extends Disposable {
customMenu.buttonElement.classList.add('open');
const buttonBoundingRect = customMenu.buttonElement.getBoundingClientRect();
const buttonBoundingRectZoom = DOM.getDomNodeZoomLevel(customMenu.buttonElement);
const titleBoundingRect = customMenu.titleElement.getBoundingClientRect();
const titleBoundingRectZoom = DOM.getDomNodeZoomLevel(customMenu.titleElement);
if (this.options.compactMode === Direction.Right) {
menuHolder.style.top = `${buttonBoundingRect.top}px`;
menuHolder.style.left = `${buttonBoundingRect.left + this.container.clientWidth}px`;
menuHolder.style.top = `${titleBoundingRect.top}px`;
menuHolder.style.left = `${titleBoundingRect.left + this.container.clientWidth}px`;
} else if (this.options.compactMode === Direction.Left) {
menuHolder.style.top = `${buttonBoundingRect.top}px`;
menuHolder.style.top = `${titleBoundingRect.top}px`;
menuHolder.style.right = `${this.container.clientWidth}px`;
menuHolder.style.left = 'auto';
} else {
menuHolder.style.top = `${buttonBoundingRect.bottom * buttonBoundingRectZoom}px`;
menuHolder.style.left = `${buttonBoundingRect.left * buttonBoundingRectZoom}px`;
menuHolder.style.top = `${titleBoundingRect.bottom * titleBoundingRectZoom}px`;
menuHolder.style.left = `${titleBoundingRect.left * titleBoundingRectZoom}px`;
}
customMenu.buttonElement.appendChild(menuHolder);

View file

@ -9,7 +9,7 @@ import { DomEmitter } from 'vs/base/browser/event';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { IIdentityProvider, IKeyboardNavigationDelegate, IKeyboardNavigationLabelProvider, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IListMouseEvent, IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
import { DefaultKeyboardNavigationDelegate, IListOptions, IListStyles, isInputElement, isMonacoEditor, List, MouseController } from 'vs/base/browser/ui/list/listWidget';
import { DefaultKeyboardNavigationDelegate, IListOptions, IListStyles, isButton, isInputElement, isMonacoEditor, List, MouseController } from 'vs/base/browser/ui/list/listWidget';
import { getVisibleState, isFilterResult } from 'vs/base/browser/ui/tree/indexTreeModel';
import { ICollapseStateChangeEvent, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeEvent, ITreeFilter, ITreeModel, ITreeModelSpliceEvent, ITreeMouseEvent, ITreeNavigator, ITreeNode, ITreeRenderer, TreeDragOverBubble, TreeError, TreeFilterResult, TreeMouseEventTarget, TreeVisibility } from 'vs/base/browser/ui/tree/tree';
import { distinct, equals, firstOrDefault, range } from 'vs/base/common/arrays';
@ -1153,7 +1153,9 @@ class TreeNodeListMouseController<T, TFilterData, TRef> extends MouseController<
}
protected override onViewPointer(e: IListMouseEvent<ITreeNode<T, TFilterData>>): void {
if (isInputElement(e.browserEvent.target as HTMLElement) || isMonacoEditor(e.browserEvent.target as HTMLElement)) {
if (isButton(e.browserEvent.target as HTMLElement) ||
isInputElement(e.browserEvent.target as HTMLElement) ||
isMonacoEditor(e.browserEvent.target as HTMLElement)) {
return;
}

View file

@ -558,6 +558,7 @@ export class Codicon implements CSSIcon {
public static readonly mapFilled = new Codicon('map-filled', { fontCharacter: '\\ec06' });
public static readonly circleSmall = new Codicon('circle-small', { fontCharacter: '\\ec07' });
public static readonly bellSlash = new Codicon('bell-slash', { fontCharacter: '\\ec08' });
public static readonly bellSlashDot = new Codicon('bell-slash-dot', { fontCharacter: '\\f101' });
// derived icons, that could become separate icons

View file

@ -8,6 +8,7 @@ import { onUnexpectedError } from 'vs/base/common/errors';
import { once as onceFn } from 'vs/base/common/functional';
import { combinedDisposable, Disposable, DisposableStore, IDisposable, SafeDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { LinkedList } from 'vs/base/common/linkedList';
import { IObservable, IObserver } from 'vs/base/common/observableImpl/base';
import { StopWatch } from 'vs/base/common/stopwatch';
@ -423,6 +424,55 @@ export namespace Event {
store?.dispose();
});
}
class EmitterObserver<T> implements IObserver {
readonly emitter: Emitter<T>;
private _counter = 0;
private _hasChanged = false;
constructor(readonly obs: IObservable<T, any>, store: DisposableStore | undefined) {
const options = {
onFirstListenerAdd: () => {
obs.addObserver(this);
},
onLastListenerRemove: () => {
obs.removeObserver(this);
}
};
if (!store) {
_addLeakageTraceLogic(options);
}
this.emitter = new Emitter<T>(options);
if (store) {
store.add(this.emitter);
}
}
beginUpdate<T>(_observable: IObservable<T, void>): void {
// console.assert(_observable === this.obs);
this._counter++;
}
handleChange<T, TChange>(_observable: IObservable<T, TChange>, _change: TChange): void {
this._hasChanged = true;
}
endUpdate<T>(_observable: IObservable<T, void>): void {
if (--this._counter === 0) {
if (this._hasChanged) {
this._hasChanged = false;
this.emitter.fire(this.obs.get());
}
}
}
}
export function fromObservable<T>(obs: IObservable<T, any>, store?: DisposableStore): Event<T> {
const observer = new EmitterObserver(obs, store);
return observer.emitter.event;
}
}
export interface EmitterOptions {

View file

@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export {
IObservable,
IObserver,
IReader,
ISettable,
ISettableObservable,
ITransaction,
observableValue,
transaction,
} from 'vs/base/common/observableImpl/base';
export { derived } from 'vs/base/common/observableImpl/derived';
export {
autorun,
autorunDelta,
autorunHandleChanges,
autorunWithStore,
} from 'vs/base/common/observableImpl/autorun';
export * from 'vs/base/common/observableImpl/utils';
import { ConsoleObservableLogger, setLogger } from 'vs/base/common/observableImpl/logging';
const enableLogging = false;
if (enableLogging) {
setLogger(new ConsoleObservableLogger());
}

View file

@ -0,0 +1,167 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { IReader, IObservable, IObserver } from 'vs/base/common/observableImpl/base';
import { getLogger } from 'vs/base/common/observableImpl/logging';
export function autorun(debugName: string, fn: (reader: IReader) => void): IDisposable {
return new AutorunObserver(debugName, fn, undefined);
}
interface IChangeContext {
readonly changedObservable: IObservable<any, any>;
readonly change: unknown;
didChange<T, TChange>(observable: IObservable<T, TChange>): this is { change: TChange };
}
export function autorunHandleChanges(
debugName: string,
options: {
/**
* Returns if this change should cause a re-run of the autorun.
*/
handleChange: (context: IChangeContext) => boolean;
},
fn: (reader: IReader) => void
): IDisposable {
return new AutorunObserver(debugName, fn, options.handleChange);
}
export function autorunWithStore(
fn: (reader: IReader, store: DisposableStore) => void,
debugName: string
): IDisposable {
const store = new DisposableStore();
const disposable = autorun(
debugName,
reader => {
store.clear();
fn(reader, store);
}
);
return toDisposable(() => {
disposable.dispose();
store.dispose();
});
}
export class AutorunObserver implements IObserver, IReader, IDisposable {
public needsToRun = true;
private updateCount = 0;
private disposed = false;
/**
* The actual dependencies.
*/
private _dependencies = new Set<IObservable<any>>();
public get dependencies() {
return this._dependencies;
}
/**
* Dependencies that have to be removed when {@link runFn} ran through.
*/
private staleDependencies = new Set<IObservable<any>>();
constructor(
public readonly debugName: string,
private readonly runFn: (reader: IReader) => void,
private readonly _handleChange: ((context: IChangeContext) => boolean) | undefined
) {
getLogger()?.handleAutorunCreated(this);
this.runIfNeeded();
}
public subscribeTo<T>(observable: IObservable<T>) {
// In case the run action disposes the autorun
if (this.disposed) {
return;
}
this._dependencies.add(observable);
if (!this.staleDependencies.delete(observable)) {
observable.addObserver(this);
}
}
public handleChange<T, TChange>(observable: IObservable<T, TChange>, change: TChange): void {
const shouldReact = this._handleChange ? this._handleChange({
changedObservable: observable,
change,
didChange: o => o === observable as any,
}) : true;
this.needsToRun = this.needsToRun || shouldReact;
if (this.updateCount === 0) {
this.runIfNeeded();
}
}
public beginUpdate(): void {
this.updateCount++;
}
public endUpdate(): void {
this.updateCount--;
if (this.updateCount === 0) {
this.runIfNeeded();
}
}
private runIfNeeded(): void {
if (!this.needsToRun) {
return;
}
// Assert: this.staleDependencies is an empty set.
const emptySet = this.staleDependencies;
this.staleDependencies = this._dependencies;
this._dependencies = emptySet;
this.needsToRun = false;
getLogger()?.handleAutorunTriggered(this);
try {
this.runFn(this);
} finally {
// We don't want our observed observables to think that they are (not even temporarily) not being observed.
// Thus, we only unsubscribe from observables that are definitely not read anymore.
for (const o of this.staleDependencies) {
o.removeObserver(this);
}
this.staleDependencies.clear();
}
}
public dispose(): void {
this.disposed = true;
for (const o of this._dependencies) {
o.removeObserver(this);
}
this._dependencies.clear();
}
public toString(): string {
return `Autorun<${this.debugName}>`;
}
}
export namespace autorun {
export const Observer = AutorunObserver;
}
export function autorunDelta<T>(
name: string,
observable: IObservable<T>,
handler: (args: { lastValue: T | undefined; newValue: T }) => void
): IDisposable {
let _lastValue: T | undefined;
return autorun(name, (reader) => {
const newValue = observable.read(reader);
const lastValue = _lastValue;
_lastValue = newValue;
handler({ lastValue, newValue });
});
}

View file

@ -0,0 +1,244 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type { derived } from 'vs/base/common/observableImpl/derived';
import { getLogger } from 'vs/base/common/observableImpl/logging';
export interface IObservable<T, TChange = void> {
readonly TChange: TChange;
/**
* Reads the current value.
*
* Must not be called from {@link IObserver.handleChange}.
*/
get(): T;
/**
* Adds an observer.
*/
addObserver(observer: IObserver): void;
removeObserver(observer: IObserver): void;
/**
* Subscribes the reader to this observable and returns the current value of this observable.
*/
read(reader: IReader): T;
map<TNew>(fn: (value: T) => TNew): IObservable<TNew>;
readonly debugName: string;
}
export interface IReader {
/**
* Reports an observable that was read.
*
* Is called by {@link IObservable.read}.
*/
subscribeTo<T>(observable: IObservable<T, any>): void;
}
export interface IObserver {
/**
* Indicates that an update operation is about to begin.
*
* During an update, invariants might not hold for subscribed observables and
* change events might be delayed.
* However, all changes must be reported before all update operations are over.
*/
beginUpdate<T>(observable: IObservable<T>): void;
/**
* Is called by a subscribed observable immediately after it notices a change.
*
* When {@link IObservable.get} returns and no change has been reported,
* there has been no change for that observable.
*
* Implementations must not call into other observables!
* The change should be processed when {@link IObserver.endUpdate} is called.
*/
handleChange<T, TChange>(observable: IObservable<T, TChange>, change: TChange): void;
/**
* Indicates that an update operation has completed.
*/
endUpdate<T>(observable: IObservable<T>): void;
}
export interface ISettable<T, TChange = void> {
set(value: T, transaction: ITransaction | undefined, change: TChange): void;
}
export interface ITransaction {
/**
* Calls `Observer.beginUpdate` immediately
* and `Observer.endUpdate` when the transaction is complete.
*/
updateObserver(
observer: IObserver,
observable: IObservable<any, any>
): void;
}
let _derived: typeof derived;
/**
* @internal
* This is to allow splitting files.
*/
export function _setDerived(derived: typeof _derived) {
_derived = derived;
}
export abstract class ConvenientObservable<T, TChange> implements IObservable<T, TChange> {
get TChange(): TChange { return null!; }
public abstract get(): T;
public abstract addObserver(observer: IObserver): void;
public abstract removeObserver(observer: IObserver): void;
/** @sealed */
public read(reader: IReader): T {
reader.subscribeTo(this);
return this.get();
}
/** @sealed */
public map<TNew>(fn: (value: T) => TNew): IObservable<TNew> {
return _derived(
() => {
const name = getFunctionName(fn);
return name !== undefined ? name : `${this.debugName} (mapped)`;
},
(reader) => fn(this.read(reader))
);
}
public abstract get debugName(): string;
}
export abstract class BaseObservable<T, TChange = void> extends ConvenientObservable<T, TChange> {
protected readonly observers = new Set<IObserver>();
/** @sealed */
public addObserver(observer: IObserver): void {
const len = this.observers.size;
this.observers.add(observer);
if (len === 0) {
this.onFirstObserverAdded();
}
}
/** @sealed */
public removeObserver(observer: IObserver): void {
const deleted = this.observers.delete(observer);
if (deleted && this.observers.size === 0) {
this.onLastObserverRemoved();
}
}
protected onFirstObserverAdded(): void { }
protected onLastObserverRemoved(): void { }
}
export function transaction(fn: (tx: ITransaction) => void, getDebugName?: () => string): void {
const tx = new TransactionImpl(fn, getDebugName);
try {
getLogger()?.handleBeginTransaction(tx);
fn(tx);
} finally {
tx.finish();
getLogger()?.handleEndTransaction();
}
}
export function getFunctionName(fn: Function): string | undefined {
const fnSrc = fn.toString();
// Pattern: /** @description ... */
const regexp = /\/\*\*\s*@description\s*([^*]*)\*\//;
const match = regexp.exec(fnSrc);
const result = match ? match[1] : undefined;
return result?.trim();
}
export class TransactionImpl implements ITransaction {
private updatingObservers: { observer: IObserver; observable: IObservable<any> }[] | null = [];
constructor(private readonly fn: Function, private readonly _getDebugName?: () => string) { }
public getDebugName(): string | undefined {
if (this._getDebugName) {
return this._getDebugName();
}
return getFunctionName(this.fn);
}
public updateObserver(
observer: IObserver,
observable: IObservable<any>
): void {
this.updatingObservers!.push({ observer, observable });
observer.beginUpdate(observable);
}
public finish(): void {
const updatingObservers = this.updatingObservers!;
// Prevent anyone from updating observers from now on.
this.updatingObservers = null;
for (const { observer, observable } of updatingObservers) {
observer.endUpdate(observable);
}
}
}
export interface ISettableObservable<T, TChange = void> extends IObservable<T, TChange>, ISettable<T, TChange> {
}
export function observableValue<T, TChange = void>(name: string, initialValue: T): ISettableObservable<T, TChange> {
return new ObservableValue(name, initialValue);
}
export class ObservableValue<T, TChange = void>
extends BaseObservable<T, TChange>
implements ISettableObservable<T, TChange>
{
private value: T;
constructor(public readonly debugName: string, initialValue: T) {
super();
this.value = initialValue;
}
public get(): T {
return this.value;
}
public set(value: T, tx: ITransaction | undefined, change: TChange): void {
if (this.value === value) {
return;
}
if (!tx) {
transaction((tx) => {
this.set(value, tx, change);
}, () => `Setting ${this.debugName}`);
return;
}
const oldValue = this.value;
this.value = value;
getLogger()?.handleObservableChanged(this, { oldValue, newValue: value, change, didChange: true });
for (const observer of this.observers) {
tx.updateObserver(observer, this);
observer.handleChange(this, change);
}
}
override toString(): string {
return `${this.debugName}: ${this.value}`;
}
}

View file

@ -0,0 +1,167 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IReader, IObservable, BaseObservable, IObserver, _setDerived } from 'vs/base/common/observableImpl/base';
import { getLogger } from 'vs/base/common/observableImpl/logging';
export function derived<T>(debugName: string | (() => string), computeFn: (reader: IReader) => T): IObservable<T> {
return new Derived(debugName, computeFn);
}
_setDerived(derived);
export class Derived<T> extends BaseObservable<T, void> implements IReader, IObserver {
private hadValue = false;
private hasValue = false;
private value: T | undefined = undefined;
private updateCount = 0;
private _dependencies = new Set<IObservable<any>>();
public get dependencies(): ReadonlySet<IObservable<any>> {
return this._dependencies;
}
/**
* Dependencies that have to be removed when {@link runFn} ran through.
*/
private staleDependencies = new Set<IObservable<any>>();
public override get debugName(): string {
return typeof this._debugName === 'function' ? this._debugName() : this._debugName;
}
constructor(
private readonly _debugName: string | (() => string),
private readonly computeFn: (reader: IReader) => T
) {
super();
getLogger()?.handleDerivedCreated(this);
}
protected override onLastObserverRemoved(): void {
/**
* We are not tracking changes anymore, thus we have to assume
* that our cache is invalid.
*/
this.hasValue = false;
this.hadValue = false;
this.value = undefined;
for (const d of this._dependencies) {
d.removeObserver(this);
}
this._dependencies.clear();
}
public get(): T {
if (this.observers.size === 0) {
// Cache is not valid and don't refresh the cache.
// Observables should not be read in non-reactive contexts.
const result = this.computeFn(this);
// Clear new dependencies
this.onLastObserverRemoved();
return result;
}
if (this.updateCount > 0 && this.hasValue) {
// Refresh dependencies
for (const d of this._dependencies) {
// Maybe `.get()` triggers `handleChange`?
d.get();
if (!this.hasValue) {
// The other dependencies will refresh on demand
break;
}
}
}
if (!this.hasValue) {
const emptySet = this.staleDependencies;
this.staleDependencies = this._dependencies;
this._dependencies = emptySet;
const oldValue = this.value;
try {
this.value = this.computeFn(this);
} finally {
// We don't want our observed observables to think that they are (not even temporarily) not being observed.
// Thus, we only unsubscribe from observables that are definitely not read anymore.
for (const o of this.staleDependencies) {
o.removeObserver(this);
}
this.staleDependencies.clear();
}
this.hasValue = true;
const didChange = this.hadValue && oldValue !== this.value;
getLogger()?.handleDerivedRecomputed(this, {
oldValue,
newValue: this.value,
change: undefined,
didChange
});
if (didChange) {
for (const r of this.observers) {
r.handleChange(this, undefined);
}
}
}
return this.value!;
}
// IObserver Implementation
public beginUpdate(): void {
if (this.updateCount === 0) {
for (const r of this.observers) {
r.beginUpdate(this);
}
}
this.updateCount++;
}
public handleChange<T, TChange>(
_observable: IObservable<T, TChange>,
_change: TChange
): void {
if (this.hasValue) {
this.hadValue = true;
this.hasValue = false;
}
// Not in transaction: Recompute & inform observers immediately
if (this.updateCount === 0 && this.observers.size > 0) {
this.get();
}
// Otherwise, recompute in `endUpdate` or on demand.
}
public endUpdate(): void {
this.updateCount--;
if (this.updateCount === 0) {
if (this.observers.size > 0) {
// Propagate invalidation
this.get();
}
for (const r of this.observers) {
r.endUpdate(this);
}
}
}
// IReader Implementation
public subscribeTo<T>(observable: IObservable<T>) {
this._dependencies.add(observable);
// We are already added as observer for stale dependencies.
if (!this.staleDependencies.delete(observable)) {
observable.addObserver(this);
}
}
override toString(): string {
return `LazyDerived<${this.debugName}>`;
}
}

View file

@ -0,0 +1,312 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AutorunObserver } from 'vs/base/common/observableImpl/autorun';
import { IObservable, ObservableValue, TransactionImpl } from 'vs/base/common/observableImpl/base';
import { Derived } from 'vs/base/common/observableImpl/derived';
import { FromEventObservable } from 'vs/base/common/observableImpl/utils';
let globalObservableLogger: IObservableLogger | undefined;
export function setLogger(logger: IObservableLogger): void {
globalObservableLogger = logger;
}
export function getLogger(): IObservableLogger | undefined {
return globalObservableLogger;
}
interface IChangeInformation {
oldValue: unknown;
newValue: unknown;
change: unknown;
didChange: boolean;
}
export interface IObservableLogger {
handleObservableChanged(observable: ObservableValue<unknown, unknown>, info: IChangeInformation): void;
handleFromEventObservableTriggered(observable: FromEventObservable<any, any>, info: IChangeInformation): void;
handleAutorunCreated(autorun: AutorunObserver): void;
handleAutorunTriggered(autorun: AutorunObserver): void;
handleDerivedCreated(observable: Derived<unknown>): void;
handleDerivedRecomputed(observable: Derived<unknown>, info: IChangeInformation): void;
handleBeginTransaction(transaction: TransactionImpl): void;
handleEndTransaction(): void;
}
export class ConsoleObservableLogger implements IObservableLogger {
private indentation = 0;
private textToConsoleArgs(text: ConsoleText): unknown[] {
return consoleTextToArgs([
normalText(repeat('| ', this.indentation)),
text,
]);
}
private formatInfo(info: IChangeInformation): ConsoleText[] {
return info.didChange
? [
normalText(` `),
styled(formatValue(info.oldValue, 70), {
color: 'red',
strikeThrough: true,
}),
normalText(` `),
styled(formatValue(info.newValue, 60), {
color: 'green',
}),
]
: [normalText(` (unchanged)`)];
}
handleObservableChanged(observable: IObservable<unknown, unknown>, info: IChangeInformation): void {
console.log(...this.textToConsoleArgs([
formatKind('observable value changed'),
styled(observable.debugName, { color: 'BlueViolet' }),
...this.formatInfo(info),
]));
}
private readonly changedObservablesSets = new WeakMap<object, Set<IObservable<any, any>>>();
formatChanges(changes: Set<IObservable<any, any>>): ConsoleText | undefined {
if (changes.size === 0) {
return undefined;
}
return styled(
' (changed deps: ' +
[...changes].map((o) => o.debugName).join(', ') +
')',
{ color: 'gray' }
);
}
handleDerivedCreated(derived: Derived<unknown>): void {
const existingHandleChange = derived.handleChange;
this.changedObservablesSets.set(derived, new Set());
derived.handleChange = (observable, change) => {
this.changedObservablesSets.get(derived)!.add(observable);
return existingHandleChange.apply(derived, [observable, change]);
};
}
handleDerivedRecomputed(derived: Derived<unknown>, info: IChangeInformation): void {
const changedObservables = this.changedObservablesSets.get(derived)!;
console.log(...this.textToConsoleArgs([
formatKind('derived recomputed'),
styled(derived.debugName, { color: 'BlueViolet' }),
...this.formatInfo(info),
this.formatChanges(changedObservables)
]));
changedObservables.clear();
}
handleFromEventObservableTriggered(observable: FromEventObservable<any, any>, info: IChangeInformation): void {
console.log(...this.textToConsoleArgs([
formatKind('observable from event triggered'),
styled(observable.debugName, { color: 'BlueViolet' }),
...this.formatInfo(info),
]));
}
handleAutorunCreated(autorun: AutorunObserver): void {
const existingHandleChange = autorun.handleChange;
this.changedObservablesSets.set(autorun, new Set());
autorun.handleChange = (observable, change) => {
this.changedObservablesSets.get(autorun)!.add(observable);
return existingHandleChange.apply(autorun, [observable, change]);
};
}
handleAutorunTriggered(autorun: AutorunObserver): void {
const changedObservables = this.changedObservablesSets.get(autorun)!;
console.log(...this.textToConsoleArgs([
formatKind('autorun'),
styled(autorun.debugName, { color: 'BlueViolet' }),
this.formatChanges(changedObservables)
]));
changedObservables.clear();
}
handleBeginTransaction(transaction: TransactionImpl): void {
let transactionName = transaction.getDebugName();
if (transactionName === undefined) {
transactionName = '';
}
console.log(...this.textToConsoleArgs([
formatKind('transaction'),
styled(transactionName, { color: 'BlueViolet' }),
]));
this.indentation++;
}
handleEndTransaction(): void {
this.indentation--;
}
}
type ConsoleText =
| (ConsoleText | undefined)[]
| { text: string; style: string; data?: Record<string, unknown> }
| { data: Record<string, unknown> };
function consoleTextToArgs(text: ConsoleText): unknown[] {
const styles = new Array<any>();
const initial = {};
const data = initial;
let firstArg = '';
function process(t: ConsoleText): void {
if ('length' in t) {
for (const item of t) {
if (item) {
process(item);
}
}
} else if ('text' in t) {
firstArg += `%c${t.text}`;
styles.push(t.style);
if (t.data) {
Object.assign(data, t.data);
}
} else if ('data' in t) {
Object.assign(data, t.data);
}
}
process(text);
const result = [firstArg, ...styles];
if (Object.keys(data).length > 0) {
result.push(data);
}
return result;
}
function normalText(text: string): ConsoleText {
return styled(text, { color: 'black' });
}
function formatKind(kind: string): ConsoleText {
return styled(padStr(`${kind}: `, 10), { color: 'black', bold: true });
}
function styled(
text: string,
options: { color: string; strikeThrough?: boolean; bold?: boolean } = {
color: 'black',
}
): ConsoleText {
function objToCss(styleObj: Record<string, string>): string {
return Object.entries(styleObj).reduce(
(styleString, [propName, propValue]) => {
return `${styleString}${propName}:${propValue};`;
},
''
);
}
const style: Record<string, string> = {
color: options.color,
};
if (options.strikeThrough) {
style['text-decoration'] = 'line-through';
}
if (options.bold) {
style['font-weight'] = 'bold';
}
return {
text,
style: objToCss(style),
};
}
function formatValue(value: unknown, availableLen: number): string {
switch (typeof value) {
case 'number':
return '' + value;
case 'string':
if (value.length + 2 <= availableLen) {
return `"${value}"`;
}
return `"${value.substr(0, availableLen - 7)}"+...`;
case 'boolean':
return value ? 'true' : 'false';
case 'undefined':
return 'undefined';
case 'object':
if (value === null) {
return 'null';
}
if (Array.isArray(value)) {
return formatArray(value, availableLen);
}
return formatObject(value, availableLen);
case 'symbol':
return value.toString();
case 'function':
return `[[Function${value.name ? ' ' + value.name : ''}]]`;
default:
return '' + value;
}
}
function formatArray(value: unknown[], availableLen: number): string {
let result = '[ ';
let first = true;
for (const val of value) {
if (!first) {
result += ', ';
}
if (result.length - 5 > availableLen) {
result += '...';
break;
}
first = false;
result += `${formatValue(val, availableLen - result.length)}`;
}
result += ' ]';
return result;
}
function formatObject(value: object, availableLen: number): string {
let result = '{ ';
let first = true;
for (const [key, val] of Object.entries(value)) {
if (!first) {
result += ', ';
}
if (result.length - 5 > availableLen) {
result += '...';
break;
}
first = false;
result += `${key}: ${formatValue(val, availableLen - result.length)}`;
}
result += ' }';
return result;
}
function repeat(str: string, count: number): string {
let result = '';
for (let i = 1; i <= count; i++) {
result += str;
}
return result;
}
function padStr(str: string, length: number): string {
while (str.length < length) {
str += ' ';
}
return str;
}

View file

@ -0,0 +1,281 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { autorun } from 'vs/base/common/observableImpl/autorun';
import { IObservable, BaseObservable, transaction, IReader, ITransaction, ConvenientObservable, IObserver, observableValue, getFunctionName } from 'vs/base/common/observableImpl/base';
import { derived } from 'vs/base/common/observableImpl/derived';
import { Event } from 'vs/base/common/event';
import { getLogger } from 'vs/base/common/observableImpl/logging';
export function constObservable<T>(value: T): IObservable<T> {
return new ConstObservable(value);
}
class ConstObservable<T> extends ConvenientObservable<T, void> {
constructor(private readonly value: T) {
super();
}
public override get debugName(): string {
return this.toString();
}
public get(): T {
return this.value;
}
public addObserver(observer: IObserver): void {
// NO OP
}
public removeObserver(observer: IObserver): void {
// NO OP
}
override toString(): string {
return `Const: ${this.value}`;
}
}
export function observableFromPromise<T>(promise: Promise<T>): IObservable<{ value?: T }> {
const observable = observableValue<{ value?: T }>('promiseValue', {});
promise.then((value) => {
observable.set({ value }, undefined);
});
return observable;
}
export function waitForState<T, TState extends T>(observable: IObservable<T>, predicate: (state: T) => state is TState): Promise<TState>;
export function waitForState<T>(observable: IObservable<T>, predicate: (state: T) => boolean): Promise<T>;
export function waitForState<T>(observable: IObservable<T>, predicate: (state: T) => boolean): Promise<T> {
return new Promise(resolve => {
const d = autorun('waitForState', reader => {
const currentState = observable.read(reader);
if (predicate(currentState)) {
d.dispose();
resolve(currentState);
}
});
});
}
export function observableFromEvent<T, TArgs = unknown>(
event: Event<TArgs>,
getValue: (args: TArgs | undefined) => T
): IObservable<T> {
return new FromEventObservable(event, getValue);
}
export class FromEventObservable<TArgs, T> extends BaseObservable<T> {
private value: T | undefined;
private hasValue = false;
private subscription: IDisposable | undefined;
constructor(
private readonly event: Event<TArgs>,
private readonly getValue: (args: TArgs | undefined) => T
) {
super();
}
private getDebugName(): string | undefined {
return getFunctionName(this.getValue);
}
public get debugName(): string {
const name = this.getDebugName();
return 'From Event' + (name ? `: ${name}` : '');
}
protected override onFirstObserverAdded(): void {
this.subscription = this.event(this.handleEvent);
}
private readonly handleEvent = (args: TArgs | undefined) => {
const newValue = this.getValue(args);
const didChange = this.value !== newValue;
getLogger()?.handleFromEventObservableTriggered(this, { oldValue: this.value, newValue, change: undefined, didChange });
if (didChange) {
this.value = newValue;
if (this.hasValue) {
transaction(
(tx) => {
for (const o of this.observers) {
tx.updateObserver(o, this);
o.handleChange(this, undefined);
}
},
() => {
const name = this.getDebugName();
return 'Event fired' + (name ? `: ${name}` : '');
}
);
}
this.hasValue = true;
}
};
protected override onLastObserverRemoved(): void {
this.subscription!.dispose();
this.subscription = undefined;
this.hasValue = false;
this.value = undefined;
}
public get(): T {
if (this.subscription) {
if (!this.hasValue) {
this.handleEvent(undefined);
}
return this.value!;
} else {
// no cache, as there are no subscribers to keep it updated
return this.getValue(undefined);
}
}
}
export namespace observableFromEvent {
export const Observer = FromEventObservable;
}
export function observableSignalFromEvent(
debugName: string,
event: Event<any>
): IObservable<void> {
return new FromEventObservableSignal(debugName, event);
}
class FromEventObservableSignal extends BaseObservable<void> {
private subscription: IDisposable | undefined;
constructor(
public readonly debugName: string,
private readonly event: Event<any>,
) {
super();
}
protected override onFirstObserverAdded(): void {
this.subscription = this.event(this.handleEvent);
}
private readonly handleEvent = () => {
transaction(
(tx) => {
for (const o of this.observers) {
tx.updateObserver(o, this);
o.handleChange(this, undefined);
}
},
() => this.debugName
);
};
protected override onLastObserverRemoved(): void {
this.subscription!.dispose();
this.subscription = undefined;
}
public override get(): void {
// NO OP
}
}
export function debouncedObservable<T>(observable: IObservable<T>, debounceMs: number, disposableStore: DisposableStore): IObservable<T | undefined> {
const debouncedObservable = observableValue<T | undefined>('debounced', undefined);
let timeout: any = undefined;
disposableStore.add(autorun('debounce', reader => {
const value = observable.read(reader);
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
transaction(tx => {
debouncedObservable.set(value, tx);
});
}, debounceMs);
}));
return debouncedObservable;
}
export function wasEventTriggeredRecently(event: Event<any>, timeoutMs: number, disposableStore: DisposableStore): IObservable<boolean> {
const observable = observableValue('triggeredRecently', false);
let timeout: any = undefined;
disposableStore.add(event(() => {
observable.set(true, undefined);
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
observable.set(false, undefined);
}, timeoutMs);
}));
return observable;
}
/**
* This ensures the observable is kept up-to-date.
* This is useful when the observables `get` method is used.
*/
export function keepAlive(observable: IObservable<any>): IDisposable {
const o = new KeepAliveObserver();
observable.addObserver(o);
return toDisposable(() => {
observable.removeObserver(o);
});
}
class KeepAliveObserver implements IObserver {
beginUpdate<T>(observable: IObservable<T, void>): void {
// NO OP
}
handleChange<T, TChange>(observable: IObservable<T, TChange>, change: TChange): void {
// NO OP
}
endUpdate<T>(observable: IObservable<T, void>): void {
// NO OP
}
}
export function derivedObservableWithCache<T>(name: string, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable<T> {
let lastValue: T | undefined = undefined;
const observable = derived(name, reader => {
lastValue = computeFn(reader, lastValue);
return lastValue;
});
return observable;
}
export function derivedObservableWithWritableCache<T>(name: string, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable<T> & { clearCache(transaction: ITransaction): void } {
let lastValue: T | undefined = undefined;
const counter = observableValue('derivedObservableWithWritableCache.counter', 0);
const observable = derived(name, reader => {
counter.read(reader);
lastValue = computeFn(reader, lastValue);
return lastValue;
});
return Object.assign(observable, {
clearCache: (transaction: ITransaction) => {
lastValue = undefined;
counter.set(counter.get() + 1, transaction);
},
});
}

View file

@ -68,7 +68,6 @@ const isElectronRenderer = isElectronProcess && nodeProcess?.type === 'renderer'
interface INavigator {
userAgent: string;
language: string;
maxTouchPoints?: number;
}
declare const navigator: INavigator;
@ -90,7 +89,7 @@ if (typeof navigator === 'object' && !isElectronRenderer) {
nls.localize({ key: 'ensureLoaderPluginIsLoaded', comment: ['{Locked}'] }, '_')
);
_locale = configuredLocale || navigator.language;
_locale = configuredLocale || LANGUAGE_DEFAULT;
_language = _locale;
}

View file

@ -52,6 +52,7 @@ export function listProcesses(rootPid: number): Promise<ProcessItem> {
const ISSUE_REPORTER_HINT = /--vscode-window-kind=issue-reporter/;
const PROCESS_EXPLORER_HINT = /--vscode-window-kind=process-explorer/;
const UTILITY_NETWORK_HINT = /--utility-sub-type=network/;
const UTILITY_EXTENSION_HOST_HINT = /--vscode-utility-kind=extensionHost/;
const WINDOWS_CRASH_REPORTER = /--crashes-directory/;
const WINDOWS_PTY = /\\pipe\\winpty-control/;
const WINDOWS_CONSOLE_HOST = /conhost\.exe/;
@ -93,6 +94,10 @@ export function listProcesses(rootPid: number): Promise<ProcessItem> {
if (UTILITY_NETWORK_HINT.exec(cmd)) {
return 'utility-network-service';
}
if (UTILITY_EXTENSION_HOST_HINT.exec(cmd)) {
return 'extension-host';
}
}
return matches[1];
}

View file

@ -35,6 +35,7 @@ interface IListElement {
readonly index: number;
readonly item: IQuickPickItem;
readonly saneLabel: string;
readonly saneSortLabel: string;
readonly saneMeta?: string;
readonly saneAriaLabel: string;
readonly saneDescription?: string;
@ -52,6 +53,7 @@ class ListElement implements IListElement, IDisposable {
index!: number;
item!: IQuickPickItem;
saneLabel!: string;
saneSortLabel!: string;
saneMeta!: string;
saneAriaLabel!: string;
saneDescription?: string;
@ -440,6 +442,7 @@ export class QuickInputList {
if (item.type !== 'separator') {
const previous = index && inputElements[index - 1];
const saneLabel = item.label && item.label.replace(/\r?\n/g, ' ');
const saneSortLabel = parseLabelWithIcons(saneLabel).text.trim();
const saneMeta = item.meta && item.meta.replace(/\r?\n/g, ' ');
const saneDescription = item.description && item.description.replace(/\r?\n/g, ' ');
const saneDetail = item.detail && item.detail.replace(/\r?\n/g, ' ');
@ -454,6 +457,7 @@ export class QuickInputList {
index,
item,
saneLabel,
saneSortLabel,
saneMeta,
saneAriaLabel,
saneDescription,
@ -738,7 +742,7 @@ function compareEntries(elementA: ListElement, elementB: ListElement, lookFor: s
return 0;
}
return compareAnything(elementA.saneLabel, elementB.saneLabel, lookFor);
return compareAnything(elementA.saneSortLabel, elementB.saneSortLabel, lookFor);
}
class QuickInputAccessibilityProvider implements IListAccessibilityProvider<ListElement> {

View file

@ -8,7 +8,8 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { errorHandler, setUnexpectedErrorHandler } from 'vs/base/common/errors';
import { AsyncEmitter, DebounceEmitter, Emitter, Event, EventBufferer, EventMultiplexer, IWaitUntil, MicrotaskEmitter, PauseableEmitter, Relay } from 'vs/base/common/event';
import { DisposableStore, IDisposable, isDisposable, setDisposableTracker, toDisposable } from 'vs/base/common/lifecycle';
import { DisposableTracker } from 'vs/base/test/common/utils';
import { observableValue, transaction } from 'vs/base/common/observable';
import { DisposableTracker, ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
namespace Samples {
@ -624,6 +625,33 @@ suite('PausableEmitter', function () {
});
});
suite('Event utils - ensureNoDisposablesAreLeakedInTestSuite', function () {
ensureNoDisposablesAreLeakedInTestSuite();
test('fromObservable', function () {
const obs = observableValue('test', 12);
const event = Event.fromObservable(obs);
const values: number[] = [];
const d = event(n => { values.push(n); });
obs.set(3, undefined);
obs.set(13, undefined);
obs.set(3, undefined);
obs.set(33, undefined);
obs.set(1, undefined);
transaction(tx => {
obs.set(334, tx);
obs.set(99, tx);
});
assert.deepStrictEqual(values, ([3, 13, 3, 33, 1, 99]));
d.dispose();
});
});
suite('Event utils', () => {
suite('EventBufferer', () => {

View file

@ -0,0 +1,468 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { Emitter } from 'vs/base/common/event';
import { autorun, derived, IObserver, ITransaction, observableFromEvent, observableValue, transaction } from 'vs/base/common/observable';
import { BaseObservable } from 'vs/base/common/observableImpl/base';
suite('observable integration', () => {
test('basic observable + autorun', () => {
const log = new Log();
const observable = observableValue('MyObservableValue', 0);
autorun('MyAutorun', (reader) => {
log.log(`value: ${observable.read(reader)}`);
});
assert.deepStrictEqual(log.getAndClearEntries(), ['value: 0']);
observable.set(1, undefined);
assert.deepStrictEqual(log.getAndClearEntries(), ['value: 1']);
observable.set(1, undefined);
assert.deepStrictEqual(log.getAndClearEntries(), []);
transaction((tx) => {
observable.set(2, tx);
assert.deepStrictEqual(log.getAndClearEntries(), []);
observable.set(3, tx);
assert.deepStrictEqual(log.getAndClearEntries(), []);
});
assert.deepStrictEqual(log.getAndClearEntries(), ['value: 3']);
});
test('basic computed + autorun', () => {
const log = new Log();
const observable1 = observableValue('MyObservableValue1', 0);
const observable2 = observableValue('MyObservableValue2', 0);
const computed = derived('computed', (reader) => {
const value1 = observable1.read(reader);
const value2 = observable2.read(reader);
const sum = value1 + value2;
log.log(`recompute: ${value1} + ${value2} = ${sum}`);
return sum;
});
autorun('MyAutorun', (reader) => {
log.log(`value: ${computed.read(reader)}`);
});
assert.deepStrictEqual(log.getAndClearEntries(), [
'recompute: 0 + 0 = 0',
'value: 0',
]);
observable1.set(1, undefined);
assert.deepStrictEqual(log.getAndClearEntries(), [
'recompute: 1 + 0 = 1',
'value: 1',
]);
observable2.set(1, undefined);
assert.deepStrictEqual(log.getAndClearEntries(), [
'recompute: 1 + 1 = 2',
'value: 2',
]);
transaction((tx) => {
observable1.set(5, tx);
assert.deepStrictEqual(log.getAndClearEntries(), []);
observable2.set(5, tx);
assert.deepStrictEqual(log.getAndClearEntries(), []);
});
assert.deepStrictEqual(log.getAndClearEntries(), [
'recompute: 5 + 5 = 10',
'value: 10',
]);
transaction((tx) => {
observable1.set(6, tx);
assert.deepStrictEqual(log.getAndClearEntries(), []);
observable2.set(4, tx);
assert.deepStrictEqual(log.getAndClearEntries(), []);
});
assert.deepStrictEqual(log.getAndClearEntries(), ['recompute: 6 + 4 = 10']);
});
test('read during transaction', () => {
const log = new Log();
const observable1 = observableValue('MyObservableValue1', 0);
const observable2 = observableValue('MyObservableValue2', 0);
const computed = derived('computed', (reader) => {
const value1 = observable1.read(reader);
const value2 = observable2.read(reader);
const sum = value1 + value2;
log.log(`recompute: ${value1} + ${value2} = ${sum}`);
return sum;
});
autorun('MyAutorun', (reader) => {
log.log(`value: ${computed.read(reader)}`);
});
assert.deepStrictEqual(log.getAndClearEntries(), [
'recompute: 0 + 0 = 0',
'value: 0',
]);
log.log(`computed is ${computed.get()}`);
assert.deepStrictEqual(log.getAndClearEntries(), ['computed is 0']);
transaction((tx) => {
observable1.set(-1, tx);
log.log(`computed is ${computed.get()}`);
assert.deepStrictEqual(log.getAndClearEntries(), [
'recompute: -1 + 0 = -1',
'computed is -1',
]);
log.log(`computed is ${computed.get()}`);
assert.deepStrictEqual(log.getAndClearEntries(), ['computed is -1']);
observable2.set(1, tx);
assert.deepStrictEqual(log.getAndClearEntries(), []);
});
assert.deepStrictEqual(log.getAndClearEntries(), [
'recompute: -1 + 1 = 0',
'value: 0',
]);
});
test('topological order', () => {
const log = new Log();
const observable1 = observableValue('MyObservableValue1', 0);
const observable2 = observableValue('MyObservableValue2', 0);
const computed1 = derived('computed1', (reader) => {
const value1 = observable1.read(reader);
const value2 = observable2.read(reader);
const sum = value1 + value2;
log.log(`recompute1: ${value1} + ${value2} = ${sum}`);
return sum;
});
const computed2 = derived('computed2', (reader) => {
const value1 = computed1.read(reader);
const value2 = observable1.read(reader);
const value3 = observable2.read(reader);
const sum = value1 + value2 + value3;
log.log(`recompute2: ${value1} + ${value2} + ${value3} = ${sum}`);
return sum;
});
const computed3 = derived('computed3', (reader) => {
const value1 = computed2.read(reader);
const value2 = observable1.read(reader);
const value3 = observable2.read(reader);
const sum = value1 + value2 + value3;
log.log(`recompute3: ${value1} + ${value2} + ${value3} = ${sum}`);
return sum;
});
autorun('MyAutorun', (reader) => {
log.log(`value: ${computed3.read(reader)}`);
});
assert.deepStrictEqual(log.getAndClearEntries(), [
'recompute1: 0 + 0 = 0',
'recompute2: 0 + 0 + 0 = 0',
'recompute3: 0 + 0 + 0 = 0',
'value: 0',
]);
observable1.set(1, undefined);
assert.deepStrictEqual(log.getAndClearEntries(), [
'recompute1: 1 + 0 = 1',
'recompute2: 1 + 1 + 0 = 2',
'recompute3: 2 + 1 + 0 = 3',
'value: 3',
]);
transaction((tx) => {
observable1.set(2, tx);
log.log(`computed2: ${computed2.get()}`);
assert.deepStrictEqual(log.getAndClearEntries(), [
'recompute1: 2 + 0 = 2',
'recompute2: 2 + 2 + 0 = 4',
'computed2: 4',
]);
observable1.set(3, tx);
log.log(`computed2: ${computed2.get()}`);
assert.deepStrictEqual(log.getAndClearEntries(), [
'recompute1: 3 + 0 = 3',
'recompute2: 3 + 3 + 0 = 6',
'computed2: 6',
]);
});
assert.deepStrictEqual(log.getAndClearEntries(), [
'recompute3: 6 + 3 + 0 = 9',
'value: 9',
]);
});
test('transaction from autorun', () => {
const log = new Log();
const observable1 = observableValue('MyObservableValue1', 0);
const observable2 = observableValue('MyObservableValue2', 0);
const computed = derived('computed', (reader) => {
const value1 = observable1.read(reader);
const value2 = observable2.read(reader);
const sum = value1 + value2;
log.log(`recompute: ${value1} + ${value2} = ${sum}`);
return sum;
});
autorun('autorun', (reader) => {
log.log(`value: ${computed.read(reader)}`);
transaction(tx => {
});
});
});
test('from event', () => {
const log = new Log();
let value = 0;
const eventEmitter = new Emitter<void>();
let id = 0;
const observable = observableFromEvent(
(handler) => {
const curId = id++;
log.log(`subscribed handler ${curId}`);
const disposable = eventEmitter.event(handler);
return {
dispose: () => {
log.log(`unsubscribed handler ${curId}`);
disposable.dispose();
},
};
},
() => {
log.log(`compute value ${value}`);
return value;
}
);
assert.deepStrictEqual(log.getAndClearEntries(), []);
log.log(`get value: ${observable.get()}`);
assert.deepStrictEqual(log.getAndClearEntries(), [
'compute value 0',
'get value: 0',
]);
log.log(`get value: ${observable.get()}`);
assert.deepStrictEqual(log.getAndClearEntries(), [
'compute value 0',
'get value: 0',
]);
const shouldReadObservable = observableValue('shouldReadObservable', true);
const autorunDisposable = autorun('MyAutorun', (reader) => {
if (shouldReadObservable.read(reader)) {
observable.read(reader);
log.log(
`autorun, should read: true, value: ${observable.read(reader)}`
);
} else {
log.log(`autorun, should read: false`);
}
});
assert.deepStrictEqual(log.getAndClearEntries(), [
'subscribed handler 0',
'compute value 0',
'autorun, should read: true, value: 0',
]);
log.log(`get value: ${observable.get()}`);
assert.deepStrictEqual(log.getAndClearEntries(), ['get value: 0']);
value = 1;
eventEmitter.fire();
assert.deepStrictEqual(log.getAndClearEntries(), [
'compute value 1',
'autorun, should read: true, value: 1',
]);
shouldReadObservable.set(false, undefined);
assert.deepStrictEqual(log.getAndClearEntries(), [
'autorun, should read: false',
'unsubscribed handler 0',
]);
shouldReadObservable.set(true, undefined);
assert.deepStrictEqual(log.getAndClearEntries(), [
'subscribed handler 1',
'compute value 1',
'autorun, should read: true, value: 1',
]);
autorunDisposable.dispose();
assert.deepStrictEqual(log.getAndClearEntries(), [
'unsubscribed handler 1',
]);
});
test('get without observers', () => {
// Maybe this scenario should not be supported.
const log = new Log();
const observable1 = observableValue('MyObservableValue1', 0);
const computed1 = derived('computed', (reader) => {
const value1 = observable1.read(reader);
const result = value1 % 3;
log.log(`recompute1: ${value1} % 3 = ${result}`);
return result;
});
const computed2 = derived('computed', (reader) => {
const value1 = computed1.read(reader);
const result = value1 * 2;
log.log(`recompute2: ${value1} * 2 = ${result}`);
return result;
});
const computed3 = derived('computed', (reader) => {
const value1 = computed1.read(reader);
const result = value1 * 3;
log.log(`recompute3: ${value1} * 3 = ${result}`);
return result;
});
const computedSum = derived('computed', (reader) => {
const value1 = computed2.read(reader);
const value2 = computed3.read(reader);
const result = value1 + value2;
log.log(`recompute4: ${value1} + ${value2} = ${result}`);
return result;
});
assert.deepStrictEqual(log.getAndClearEntries(), []);
observable1.set(1, undefined);
assert.deepStrictEqual(log.getAndClearEntries(), []);
log.log(`value: ${computedSum.get()}`);
assert.deepStrictEqual(log.getAndClearEntries(), [
'recompute1: 1 % 3 = 1',
'recompute2: 1 * 2 = 2',
'recompute3: 1 * 3 = 3',
'recompute4: 2 + 3 = 5',
'value: 5',
]);
log.log(`value: ${computedSum.get()}`);
assert.deepStrictEqual(log.getAndClearEntries(), [
'recompute1: 1 % 3 = 1',
'recompute2: 1 * 2 = 2',
'recompute3: 1 * 3 = 3',
'recompute4: 2 + 3 = 5',
'value: 5',
]);
});
});
suite('observable details', () => {
test('1', () => {
const log = new Log();
class TrackedObservableValue<T> extends BaseObservable<T> {
private value: T;
constructor(initialValue: T) {
super();
this.value = initialValue;
}
readonly debugName = 'TrackedObservableValue';
public override addObserver(observer: IObserver): void {
log.log(`observable.addObserver ${observer.toString()}`);
super.addObserver(observer);
}
public override removeObserver(observer: IObserver): void {
log.log(`observable.removeObserver ${observer.toString()}`);
super.removeObserver(observer);
}
public get(): T {
log.log('observable.get');
return this.value;
}
public set(value: T, tx: ITransaction): void {
log.log(`observable.set (value ${value})`);
if (this.value === value) {
return;
}
this.value = value;
for (const observer of this.observers) {
tx.updateObserver(observer, this);
observer.handleChange(this, undefined);
}
}
}
const shouldReadObservable = observableValue('shouldReadObservable', true);
const observable = new TrackedObservableValue(0);
const computed = derived('test', reader => {
if (shouldReadObservable.read(reader)) {
return observable.read(reader) * 2;
}
return 1;
});
autorun('test', reader => {
const value = computed.read(reader);
log.log(`autorun: ${value}`);
});
assert.deepStrictEqual(log.getAndClearEntries(), [
'observable.addObserver LazyDerived<test>',
'observable.get',
'autorun: 0',
]);
transaction(tx => {
observable.set(1, tx);
assert.deepStrictEqual(log.getAndClearEntries(), (["observable.set (value 1)"]));
shouldReadObservable.set(false, tx);
assert.deepStrictEqual(log.getAndClearEntries(), ([]));
computed.get();
assert.deepStrictEqual(log.getAndClearEntries(), (["observable.removeObserver LazyDerived<test>"]));
});
assert.deepStrictEqual(log.getAndClearEntries(), (["autorun: 1"]));
});
});
class Log {
private readonly entries: string[] = [];
public log(message: string): void {
this.entries.push(message);
}
public getAndClearEntries(): string[] {
const entries = [...this.entries];
this.entries.length = 0;
return entries;
}
}

View file

@ -34,7 +34,7 @@ export class ExtensionsCleaner extends Disposable {
) {
super();
extensionManagementService.removeUninstalledExtensions(this.userDataProfilesService.profiles.length > 1);
extensionManagementService.removeUninstalledExtensions(this.userDataProfilesService.profiles.length === 0);
migrateUnsupportedExtensions(extensionManagementService, extensionGalleryService, extensionStorageService, extensionEnablementService, logService);
ExtensionStorageService.removeOutdatedExtensionVersions(extensionManagementService, storageService);
this._register(instantiationService.createInstance(ProfileExtensionsCleaner));

View file

@ -256,7 +256,7 @@ class CodeMain {
configurationService.initialize()
]);
userDataProfilesMainService.setEnablement(configurationService.getValue(PROFILES_ENABLEMENT_CONFIG));
userDataProfilesMainService.setEnablement(!!configurationService.getValue(PROFILES_ENABLEMENT_CONFIG));
}
private async claimInstance(logService: ILogService, environmentMainService: IEnvironmentMainService, lifecycleMainService: ILifecycleMainService, instantiationService: IInstantiationService, productService: IProductService, retry: boolean): Promise<NodeIPCServer> {

View file

@ -164,7 +164,7 @@ class CliMain extends Disposable {
configurationService.initialize()
]);
userDataProfilesService.setEnablement(configurationService.getValue(PROFILES_ENABLEMENT_CONFIG));
userDataProfilesService.setEnablement(!!configurationService.getValue(PROFILES_ENABLEMENT_CONFIG));
// URI Identity
services.set(IUriIdentityService, new UriIdentityService(fileService));

View file

@ -41,8 +41,8 @@ const INTERNAL_DND_MIME_TYPES = Object.freeze([
DataTransfers.RESOURCES,
]);
export function addExternalEditorsDropData(dataTransfer: VSDataTransfer, dragEvent: DragEvent) {
if (dragEvent.dataTransfer && !dataTransfer.has(Mimes.uriList)) {
export function addExternalEditorsDropData(dataTransfer: VSDataTransfer, dragEvent: DragEvent, overwriteUriList = false) {
if (dragEvent.dataTransfer && (overwriteUriList || !dataTransfer.has(Mimes.uriList))) {
const editorData = extractEditorsDropData(dragEvent)
.filter(input => input.resource)
.map(input => input.resource!.toString());

View file

@ -852,24 +852,29 @@ export interface ICodeEditor extends editorCommon.IEditor {
/**
* All decorations added through this call will get the ownerId of this editor.
* @see {@link ITextModel.deltaDecorations}
* @deprecated
*/
deltaDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[];
/**
* @internal
* Remove previously added decorations.
*/
setDecorations(description: string, decorationTypeKey: string, ranges: editorCommon.IDecorationOptions[]): void;
removeDecorations(decorationIds: string[]): void;
/**
* @internal
*/
setDecorationsFast(decorationTypeKey: string, ranges: IRange[]): void;
setDecorationsByType(description: string, decorationTypeKey: string, ranges: editorCommon.IDecorationOptions[]): void;
/**
* @internal
*/
removeDecorations(decorationTypeKey: string): void;
setDecorationsByTypeFast(decorationTypeKey: string, ranges: IRange[]): void;
/**
* @internal
*/
removeDecorationsByType(decorationTypeKey: string): void;
/**
* Get the layout info for the editor.

View file

@ -161,7 +161,7 @@ export abstract class AbstractCodeEditorService extends Disposable implements IC
if (provider.refCount <= 0) {
this._decorationOptionProviders.delete(key);
provider.dispose();
this.listCodeEditors().forEach((ed) => ed.removeDecorations(key));
this.listCodeEditors().forEach((ed) => ed.removeDecorationsByType(key));
}
}
}

View file

@ -172,7 +172,13 @@ export class ViewCursor {
}
const range = firstVisibleRangeForCharacter.ranges[0];
const width = range.width < 1 ? this._typicalHalfwidthCharacterWidth : range.width;
const width = (
nextGrapheme === '\t'
? this._typicalHalfwidthCharacterWidth
: (range.width < 1
? this._typicalHalfwidthCharacterWidth
: range.width)
);
let textContentClassName = '';
if (this._cursorStyle === TextEditorCursorStyle.Block) {

View file

@ -267,7 +267,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
private _bannerDomNode: HTMLElement | null = null;
private _dropIntoEditorDecorationIds: string[] = [];
private _dropIntoEditorDecorations: EditorDecorationsCollection = this.createDecorationsCollection();
constructor(
domElement: HTMLElement,
@ -564,27 +564,43 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
return this._modelData.viewModel.viewLayout.getWhitespaces();
}
private static _getVerticalOffsetForPosition(modelData: ModelData, modelLineNumber: number, modelColumn: number): number {
private static _getVerticalOffsetAfterPosition(modelData: ModelData, modelLineNumber: number, modelColumn: number, includeViewZones: boolean): number {
const modelPosition = modelData.model.validatePosition({
lineNumber: modelLineNumber,
column: modelColumn
});
const viewPosition = modelData.viewModel.coordinatesConverter.convertModelPositionToViewPosition(modelPosition);
return modelData.viewModel.viewLayout.getVerticalOffsetForLineNumber(viewPosition.lineNumber);
return modelData.viewModel.viewLayout.getVerticalOffsetAfterLineNumber(viewPosition.lineNumber, includeViewZones);
}
public getTopForLineNumber(lineNumber: number): number {
public getTopForLineNumber(lineNumber: number, includeViewZones: boolean = false): number {
if (!this._modelData) {
return -1;
}
return CodeEditorWidget._getVerticalOffsetForPosition(this._modelData, lineNumber, 1);
return CodeEditorWidget._getVerticalOffsetForPosition(this._modelData, lineNumber, 1, includeViewZones);
}
public getTopForPosition(lineNumber: number, column: number): number {
if (!this._modelData) {
return -1;
}
return CodeEditorWidget._getVerticalOffsetForPosition(this._modelData, lineNumber, column);
return CodeEditorWidget._getVerticalOffsetForPosition(this._modelData, lineNumber, column, false);
}
private static _getVerticalOffsetForPosition(modelData: ModelData, modelLineNumber: number, modelColumn: number, includeViewZones: boolean = false): number {
const modelPosition = modelData.model.validatePosition({
lineNumber: modelLineNumber,
column: modelColumn
});
const viewPosition = modelData.viewModel.coordinatesConverter.convertModelPositionToViewPosition(modelPosition);
return modelData.viewModel.viewLayout.getVerticalOffsetForLineNumber(viewPosition.lineNumber, includeViewZones);
}
public getBottomForLineNumber(lineNumber: number, includeViewZones: boolean = false): number {
if (!this._modelData) {
return -1;
}
return CodeEditorWidget._getVerticalOffsetAfterPosition(this._modelData, lineNumber, 1, includeViewZones);
}
public setHiddenAreas(ranges: IRange[]): void {
@ -1286,6 +1302,9 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
return this._modelData.model.getDecorationsInRange(range, this._id, filterValidationDecorations(this._configuration.options));
}
/**
* @deprecated
*/
public deltaDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[] {
if (!this._modelData) {
return [];
@ -1298,7 +1317,17 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
return this._modelData.model.deltaDecorations(oldDecorations, newDecorations, this._id);
}
public setDecorations(description: string, decorationTypeKey: string, decorationOptions: editorCommon.IDecorationOptions[]): void {
public removeDecorations(decorationIds: string[]): void {
if (!this._modelData || decorationIds.length === 0) {
return;
}
this._modelData.model.changeDecorations((changeAccessor) => {
changeAccessor.deltaDecorations(decorationIds, []);
});
}
public setDecorationsByType(description: string, decorationTypeKey: string, decorationOptions: editorCommon.IDecorationOptions[]): void {
const newDecorationsSubTypes: { [key: string]: boolean } = {};
const oldDecorationsSubTypes = this._decorationTypeSubtypes[decorationTypeKey] || {};
@ -1340,7 +1369,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
this._decorationTypeKeysToIds[decorationTypeKey] = this.deltaDecorations(oldDecorationsIds, newModelDecorations);
}
public setDecorationsFast(decorationTypeKey: string, ranges: IRange[]): void {
public setDecorationsByTypeFast(decorationTypeKey: string, ranges: IRange[]): void {
// remove decoration sub types that are no longer used, deregister decoration type if necessary
const oldDecorationsSubTypes = this._decorationTypeSubtypes[decorationTypeKey] || {};
@ -1360,7 +1389,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
this._decorationTypeKeysToIds[decorationTypeKey] = this.deltaDecorations(oldDecorationsIds, newModelDecorations);
}
public removeDecorations(decorationTypeKey: string): void {
public removeDecorationsByType(decorationTypeKey: string): void {
// remove decorations for type and sub type
const oldDecorationsIds = this._decorationTypeKeysToIds[decorationTypeKey];
if (oldDecorationsIds) {
@ -1834,12 +1863,12 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
options: CodeEditorWidget.dropIntoEditorDecorationOptions
}];
this._dropIntoEditorDecorationIds = this.deltaDecorations(this._dropIntoEditorDecorationIds, newDecorations);
this._dropIntoEditorDecorations.set(newDecorations);
this.revealPosition(position, editorCommon.ScrollType.Immediate);
}
private removeDropIndicator(): void {
this._dropIntoEditorDecorationIds = this.deltaDecorations(this._dropIntoEditorDecorationIds, []);
this._dropIntoEditorDecorations.clear();
}
}

View file

@ -115,7 +115,9 @@ class VisualEditorState {
this._zonesMap = {};
// (2) Model decorations
this._decorations = editor.deltaDecorations(this._decorations, []);
editor.changeDecorations((changeAccessor) => {
this._decorations = changeAccessor.deltaDecorations(this._decorations, []);
});
}
public apply(editor: CodeEditorWidget, overviewRuler: editorBrowser.IOverviewRuler | null, newDecorations: IEditorDiffDecorationsWithZones, restoreScrollState: boolean): void {
@ -153,7 +155,9 @@ class VisualEditorState {
scrollState?.restore(editor);
// decorations
this._decorations = editor.deltaDecorations(this._decorations, newDecorations.decorations);
editor.changeDecorations((changeAccessor) => {
this._decorations = changeAccessor.deltaDecorations(this._decorations, newDecorations.decorations);
});
// overview ruler
overviewRuler?.setZones(newDecorations.overviewZones);

View file

@ -258,15 +258,15 @@ export class LinkComputer {
case CharCode.CloseCurlyBrace:
chClass = (hasOpenCurlyBracket ? CharacterClass.None : CharacterClass.ForceTermination);
break;
/* The following three rules make it that ' or " or ` are allowed inside links if the link began with a different one */
/* The following three rules make it that ' or " or ` are allowed inside links if the link didn't begin with them */
case CharCode.SingleQuote:
chClass = (linkBeginChCode === CharCode.DoubleQuote || linkBeginChCode === CharCode.BackTick) ? CharacterClass.None : CharacterClass.ForceTermination;
chClass = (linkBeginChCode === CharCode.SingleQuote ? CharacterClass.ForceTermination : CharacterClass.None);
break;
case CharCode.DoubleQuote:
chClass = (linkBeginChCode === CharCode.SingleQuote || linkBeginChCode === CharCode.BackTick) ? CharacterClass.None : CharacterClass.ForceTermination;
chClass = (linkBeginChCode === CharCode.DoubleQuote ? CharacterClass.ForceTermination : CharacterClass.None);
break;
case CharCode.BackTick:
chClass = (linkBeginChCode === CharCode.SingleQuote || linkBeginChCode === CharCode.DoubleQuote) ? CharacterClass.None : CharacterClass.ForceTermination;
chClass = (linkBeginChCode === CharCode.BackTick ? CharacterClass.ForceTermination : CharacterClass.None);
break;
case CharCode.Asterisk:
// `*` terminates a link if the link began with `*`

View file

@ -11,7 +11,7 @@ import { IRequestHandler } from 'vs/base/common/worker/simpleWorker';
import { IPosition, Position } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
import { DiffComputer, IChange, IDiffComputationResult } from 'vs/editor/common/diff/diffComputer';
import { EndOfLineSequence } from 'vs/editor/common/model';
import { EndOfLineSequence, ITextModel } from 'vs/editor/common/model';
import { IMirrorTextModel, IModelChangedEvent, MirrorTextModel as BaseMirrorModel } from 'vs/editor/common/model/mirrorTextModel';
import { ensureValidWordDefinition, getWordAtText, IWordAtPosition } from 'vs/editor/common/core/wordHelper';
import { IInplaceReplaceSupportResult, ILink, TextEdit } from 'vs/editor/common/languages';
@ -388,8 +388,12 @@ export class EditorSimpleWorker implements IRequestHandler, IDisposable {
return null;
}
const originalLines = original.getLinesContent();
const modifiedLines = modified.getLinesContent();
return EditorSimpleWorker.computeDiff(original, modified, ignoreTrimWhitespace, maxComputationTime);
}
public static computeDiff(originalTextModel: ICommonModel | ITextModel, modifiedTextModel: ICommonModel | ITextModel, ignoreTrimWhitespace: boolean, maxComputationTime: number): IDiffComputationResult | null {
const originalLines = originalTextModel.getLinesContent();
const modifiedLines = modifiedTextModel.getLinesContent();
const diffComputer = new DiffComputer(originalLines, modifiedLines, {
shouldComputeCharChanges: true,
shouldPostProcessCharChanges: true,
@ -399,7 +403,7 @@ export class EditorSimpleWorker implements IRequestHandler, IDisposable {
});
const diffResult = diffComputer.computeDiff();
const identical = (diffResult.changes.length > 0 ? false : this._modelsAreIdentical(original, modified));
const identical = (diffResult.changes.length > 0 ? false : this._modelsAreIdentical(originalTextModel, modifiedTextModel));
return {
quitEarly: diffResult.quitEarly,
identical: identical,
@ -407,7 +411,7 @@ export class EditorSimpleWorker implements IRequestHandler, IDisposable {
};
}
private _modelsAreIdentical(original: ICommonModel, modified: ICommonModel): boolean {
private static _modelsAreIdentical(original: ICommonModel | ITextModel, modified: ICommonModel | ITextModel): boolean {
const originalLineCount = original.getLineCount();
const modifiedLineCount = modified.getLineCount();
if (originalLineCount !== modifiedLineCount) {

View file

@ -482,7 +482,7 @@ export class LinesLayout {
* @param lineNumber The line number
* @return The sum of heights for all objects above `lineNumber`.
*/
public getVerticalOffsetForLineNumber(lineNumber: number): number {
public getVerticalOffsetForLineNumber(lineNumber: number, includeViewZones = false): number {
this._checkPendingChanges();
lineNumber = lineNumber | 0;
@ -493,11 +493,25 @@ export class LinesLayout {
previousLinesHeight = 0;
}
const previousWhitespacesHeight = this.getWhitespaceAccumulatedHeightBeforeLineNumber(lineNumber);
const previousWhitespacesHeight = this.getWhitespaceAccumulatedHeightBeforeLineNumber(lineNumber - (includeViewZones ? 1 : 0));
return previousLinesHeight + previousWhitespacesHeight + this._paddingTop;
}
/**
* Get the vertical offset (the sum of heights for all objects above) a certain line number.
*
* @param lineNumber The line number
* @return The sum of heights for all objects above `lineNumber`.
*/
public getVerticalOffsetAfterLineNumber(lineNumber: number, includeViewZones = false): number {
this._checkPendingChanges();
lineNumber = lineNumber | 0;
const previousLinesHeight = this._lineHeight * lineNumber;
const previousWhitespacesHeight = this.getWhitespaceAccumulatedHeightBeforeLineNumber(lineNumber + (includeViewZones ? 1 : 0));
return previousLinesHeight + previousWhitespacesHeight + this._paddingTop;
}
/**
* Returns if there is any whitespace in the document.
*/

View file

@ -362,8 +362,11 @@ export class ViewLayout extends Disposable implements IViewLayout {
}
return hadAChange;
}
public getVerticalOffsetForLineNumber(lineNumber: number): number {
return this._linesLayout.getVerticalOffsetForLineNumber(lineNumber);
public getVerticalOffsetForLineNumber(lineNumber: number, includeViewZones: boolean = false): number {
return this._linesLayout.getVerticalOffsetForLineNumber(lineNumber, includeViewZones);
}
public getVerticalOffsetAfterLineNumber(lineNumber: number, includeViewZones: boolean = false): number {
return this._linesLayout.getVerticalOffsetAfterLineNumber(lineNumber, includeViewZones);
}
public isAfterLines(verticalOffset: number): boolean {
return this._linesLayout.isAfterLines(verticalOffset);

View file

@ -172,10 +172,12 @@ export class ColorDetector extends Disposable implements IEditorContribution {
options: ModelDecorationOptions.EMPTY
}));
this._decorationsIds = this._editor.deltaDecorations(this._decorationsIds, decorations);
this._editor.changeDecorations((changeAccessor) => {
this._decorationsIds = changeAccessor.deltaDecorations(this._decorationsIds, decorations);
this._colorDatas = new Map<string, IColorData>();
this._decorationsIds.forEach((id, i) => this._colorDatas.set(id, colorDatas[i]));
this._colorDatas = new Map<string, IColorData>();
this._decorationsIds.forEach((id, i) => this._colorDatas.set(id, colorDatas[i]));
});
}
private _colorDecorationClassRefs = this._register(new DisposableStore());
@ -219,7 +221,8 @@ export class ColorDetector extends Disposable implements IEditorContribution {
}
private removeAllDecorations(): void {
this._decorationsIds = this._editor.deltaDecorations(this._decorationsIds, []);
this._editor.removeDecorations(this._decorationsIds);
this._decorationsIds = [];
this._colorDecoratorIds.clear();
this._colorDecorationClassRefs.clear();
}

View file

@ -33,7 +33,7 @@ export class FindDecorations implements IDisposable {
}
public dispose(): void {
this._editor.deltaDecorations(this._allDecorations(), []);
this._editor.removeDecorations(this._allDecorations());
this._decorations = [];
this._overviewRulerApproximateDecorations = [];

View file

@ -5,7 +5,7 @@
import { Codicon } from 'vs/base/common/codicons';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model';
import { IModelDecorationsChangeAccessor, TrackedRangeStickiness } from 'vs/editor/common/model';
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import { IDecorationProvider } from 'vs/editor/contrib/folding/browser/foldingModel';
import { localize } from 'vs/nls';
@ -72,11 +72,11 @@ export class FoldingDecorationProvider implements IDecorationProvider {
}
}
deltaDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[] {
return this.editor.deltaDecorations(oldDecorations, newDecorations);
}
changeDecorations<T>(callback: (changeAccessor: IModelDecorationsChangeAccessor) => T): T {
return this.editor.changeDecorations(callback);
}
removeDecorations(decorationIds: string[]): void {
this.editor.removeDecorations(decorationIds);
}
}

View file

@ -9,8 +9,8 @@ import { FoldingRegion, FoldingRegions, ILineRange } from './foldingRanges';
export interface IDecorationProvider {
getDecorationOption(isCollapsed: boolean, isHidden: boolean): IModelDecorationOptions;
deltaDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[];
changeDecorations<T>(callback: (changeAccessor: IModelDecorationsChangeAccessor) => T): T | null;
removeDecorations(decorationIds: string[]): void;
}
export interface FoldingModelChangeEvent {
@ -162,7 +162,9 @@ export class FoldingModel {
k++;
}
this._editorDecorationIds = this._decorationProvider.deltaDecorations(this._editorDecorationIds, newEditorDecorations);
this._decorationProvider.changeDecorations((changeAccessor) => {
this._editorDecorationIds = changeAccessor.deltaDecorations(this._editorDecorationIds, newEditorDecorations);
});
this._regions = newRegions;
this._isInitialized = true;
this._updateEventEmitter.fire({ model: this });
@ -207,7 +209,7 @@ export class FoldingModel {
}
public dispose() {
this._decorationProvider.deltaDecorations(this._editorDecorationIds, []);
this._decorationProvider.removeDecorations(this._editorDecorationIds);
}
getAllRegionsAtLine(lineNumber: number, filter?: (r: FoldingRegion, level: number) => boolean): FoldingRegion[] {

View file

@ -7,7 +7,7 @@ import { escapeRegExpCharacters } from 'vs/base/common/strings';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model';
import { IModelDecorationsChangeAccessor, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model';
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import { FoldingModel, getNextFoldLine, getParentFoldLine, getPreviousFoldLine, setCollapseStateAtLevel, setCollapseStateForMatchingLines, setCollapseStateForRest, setCollapseStateLevelsDown, setCollapseStateLevelsUp, setCollapseStateUp } from 'vs/editor/contrib/folding/browser/foldingModel';
import { FoldingRegion } from 'vs/editor/contrib/folding/browser/foldingRanges';
@ -59,14 +59,16 @@ export class TestDecorationProvider {
return TestDecorationProvider.expandedDecoration;
}
deltaDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[] {
return this.model.deltaDecorations(oldDecorations, newDecorations);
}
changeDecorations<T>(callback: (changeAccessor: IModelDecorationsChangeAccessor) => T): (T | null) {
return this.model.changeDecorations(callback);
}
removeDecorations(decorationIds: string[]): void {
this.model.changeDecorations((changeAccessor) => {
changeAccessor.deltaDecorations(decorationIds, []);
});
}
getDecorations(): ExpectedDecoration[] {
const decorations = this.model.getAllDecorations();
const res: ExpectedDecoration[] = [];

View file

@ -100,10 +100,12 @@ class DecorationsManager implements IDisposable {
newDecorationsActualIndex.push(i);
}
const decorations = this._editor.deltaDecorations([], newDecorations);
for (let i = 0; i < decorations.length; i++) {
this._decorations.set(decorations[i], reference.children[newDecorationsActualIndex[i]]);
}
this._editor.changeDecorations((changeAccessor) => {
const decorations = changeAccessor.deltaDecorations([], newDecorations);
for (let i = 0; i < decorations.length; i++) {
this._decorations.set(decorations[i], reference.children[newDecorationsActualIndex[i]]);
}
});
}
private _onDecorationChanged(): void {
@ -151,11 +153,11 @@ class DecorationsManager implements IDisposable {
for (let i = 0, len = toRemove.length; i < len; i++) {
this._decorations.delete(toRemove[i]);
}
this._editor.deltaDecorations(toRemove, []);
this._editor.removeDecorations(toRemove);
}
removeDecorations(): void {
this._editor.deltaDecorations([...this._decorations.keys()], []);
this._editor.removeDecorations([...this._decorations.keys()]);
this._decorations.clear();
}
}

View file

@ -415,7 +415,6 @@ export class ContentHoverWidget extends Disposable implements IContentWidget {
this._hover.contentsDomNode.style.paddingBottom = '';
this._updateFont();
this._editor.layoutContentWidget(this);
this.onContentsChanged();
// Simply force a synchronous render on the editor
@ -424,7 +423,6 @@ export class ContentHoverWidget extends Disposable implements IContentWidget {
// See https://github.com/microsoft/vscode/issues/140339
// TODO: Doing a second layout of the hover after force rendering the editor
this._editor.layoutContentWidget(this);
this.onContentsChanged();
if (visibleData.stoleFocus) {
@ -447,6 +445,7 @@ export class ContentHoverWidget extends Disposable implements IContentWidget {
}
public onContentsChanged(): void {
this._editor.layoutContentWidget(this);
this._hover.onContentsChanged();
const scrollDimensions = this._hover.scrollbar.getScrollDimensions();

View file

@ -468,7 +468,7 @@ export class InlayHintsController implements IEditorContribution {
//
const { fontSize, fontFamily, padding } = this._getLayoutInfo();
const { fontSize, fontFamily, padding, isUniform } = this._getLayoutInfo();
const fontFamilyVar = '--code-editorInlayHintsFontFamily';
this._editor.getContainerDomNode().style.setProperty(fontFamilyVar, fontFamily);
@ -493,7 +493,7 @@ export class InlayHintsController implements IEditorContribution {
const cssProperties: CssProperties = {
fontSize: `${fontSize}px`,
fontFamily: `var(${fontFamilyVar}), ${EDITOR_FONT_DEFAULTS.fontFamily}`,
verticalAlign: 'middle',
verticalAlign: isUniform ? 'baseline' : 'middle',
};
if (isNonEmptyArray(item.hint.textEdits)) {
@ -587,17 +587,27 @@ export class InlayHintsController implements IEditorContribution {
private _getLayoutInfo() {
const options = this._editor.getOption(EditorOption.inlayHints);
const padding = options.padding;
const editorFontSize = this._editor.getOption(EditorOption.fontSize);
const editorFontFamily = this._editor.getOption(EditorOption.fontFamily);
let fontSize = options.fontSize;
if (!fontSize || fontSize < 5 || fontSize > editorFontSize) {
fontSize = editorFontSize;
}
const fontFamily = options.fontFamily || this._editor.getOption(EditorOption.fontFamily);
return { fontSize, fontFamily, padding: options.padding };
const fontFamily = options.fontFamily || editorFontFamily;
const isUniform = !padding
&& fontFamily === editorFontFamily
&& fontSize === editorFontSize;
return { fontSize, fontFamily, padding, isUniform };
}
private _removeAllDecorations(): void {
this._editor.deltaDecorations(Array.from(this._decorationsMetadata.keys()), []);
this._editor.removeDecorations(Array.from(this._decorationsMetadata.keys()));
for (const obj of this._decorationsMetadata.values()) {
obj.classNameRef.dispose();
}

View file

@ -577,18 +577,21 @@ export class SynchronizedInlineCompletionsCache extends Disposable {
) {
super();
const decorationIds = editor.deltaDecorations(
[],
completionsSource.items.map(i => ({
range: i.range,
options: {
description: 'inline-completion-tracking-range'
},
}))
);
const decorationIds = editor.changeDecorations((changeAccessor) => {
return changeAccessor.deltaDecorations(
[],
completionsSource.items.map(i => ({
range: i.range,
options: {
description: 'inline-completion-tracking-range'
},
}))
);
});
this._register(toDisposable(() => {
this.isDisposing = true;
editor.deltaDecorations(decorationIds, []);
editor.removeDecorations(decorationIds);
}));
this.completions = completionsSource.items.map((c, idx) => new CachedInlineCompletion(c, decorationIds[idx]));

View file

@ -1162,6 +1162,49 @@ export class SnakeCaseAction extends AbstractCaseAction {
}
}
export class KebabCaseAction extends AbstractCaseAction {
public static isSupported(): boolean {
const areAllRegexpsSupported = [
this.caseBoundary,
this.singleLetters,
this.underscoreBoundary,
].every((regexp) => regexp.isSupported());
return areAllRegexpsSupported;
}
private static caseBoundary = new BackwardsCompatibleRegExp('(\\p{Ll})(\\p{Lu})', 'gmu');
private static singleLetters = new BackwardsCompatibleRegExp('(\\p{Lu}|\\p{N})(\\p{Lu}\\p{Ll})', 'gmu');
private static underscoreBoundary = new BackwardsCompatibleRegExp('(\\S)(_)(\\S)', 'gm');
constructor() {
super({
id: 'editor.action.transformToKebabcase',
label: nls.localize('editor.transformToKebabcase', 'Transform to Kebab Case'),
alias: 'Transform to Kebab Case',
precondition: EditorContextKeys.writable
});
}
protected _modifyText(text: string, _: string): string {
const caseBoundary = KebabCaseAction.caseBoundary.get();
const singleLetters = KebabCaseAction.singleLetters.get();
const underscoreBoundary = KebabCaseAction.underscoreBoundary.get();
if (!caseBoundary || !singleLetters || !underscoreBoundary) {
// one or more regexps aren't supported
return text;
}
return text
.replace(underscoreBoundary, '$1-$3')
.replace(caseBoundary, '$1-$2')
.replace(singleLetters, '$1-$2')
.toLocaleLowerCase();
}
}
registerEditorAction(CopyLinesUpAction);
registerEditorAction(CopyLinesDownAction);
registerEditorAction(DuplicateSelectionAction);
@ -1189,3 +1232,7 @@ if (SnakeCaseAction.caseBoundary.isSupported() && SnakeCaseAction.singleLetters.
if (TitleCaseAction.titleBoundary.isSupported()) {
registerEditorAction(TitleCaseAction);
}
if (KebabCaseAction.isSupported()) {
registerEditorAction(KebabCaseAction);
}

View file

@ -11,7 +11,7 @@ import { Selection } from 'vs/editor/common/core/selection';
import { Handler } from 'vs/editor/common/editorCommon';
import { ITextModel } from 'vs/editor/common/model';
import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl';
import { DeleteAllLeftAction, DeleteAllRightAction, DeleteDuplicateLinesAction, DeleteLinesAction, IndentLinesAction, InsertLineAfterAction, InsertLineBeforeAction, JoinLinesAction, LowerCaseAction, SnakeCaseAction, SortLinesAscendingAction, SortLinesDescendingAction, TitleCaseAction, TransposeAction, UpperCaseAction } from 'vs/editor/contrib/linesOperations/browser/linesOperations';
import { DeleteAllLeftAction, DeleteAllRightAction, DeleteDuplicateLinesAction, DeleteLinesAction, IndentLinesAction, InsertLineAfterAction, InsertLineBeforeAction, JoinLinesAction, KebabCaseAction, LowerCaseAction, SnakeCaseAction, SortLinesAscendingAction, SortLinesDescendingAction, TitleCaseAction, TransposeAction, UpperCaseAction } from 'vs/editor/contrib/linesOperations/browser/linesOperations';
import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
import { createTextModel } from 'vs/editor/test/common/testTextModel';
@ -814,6 +814,80 @@ suite('Editor Contrib - Line Operations', () => {
assertSelection(editor, new Selection(2, 2, 2, 2));
}
);
withTestCodeEditor(
[
'hello world',
'öçşğü',
'parseHTMLString',
'getElementById',
'PascalCase',
'öçşÖÇŞğüĞÜ',
'audioConverter.convertM4AToMP3();',
'Capital_Snake_Case',
'parseHTML4String',
'_accessor: ServicesAccessor',
'Kebab-Case',
], {}, (editor) => {
const model = editor.getModel()!;
const kebabCaseAction = new KebabCaseAction();
editor.setSelection(new Selection(1, 1, 1, 12));
executeAction(kebabCaseAction, editor);
assert.strictEqual(model.getLineContent(1), 'hello world');
assertSelection(editor, new Selection(1, 1, 1, 12));
editor.setSelection(new Selection(2, 1, 2, 6));
executeAction(kebabCaseAction, editor);
assert.strictEqual(model.getLineContent(2), 'öçşğü');
assertSelection(editor, new Selection(2, 1, 2, 6));
editor.setSelection(new Selection(3, 1, 3, 16));
executeAction(kebabCaseAction, editor);
assert.strictEqual(model.getLineContent(3), 'parse-html-string');
assertSelection(editor, new Selection(3, 1, 3, 18));
editor.setSelection(new Selection(4, 1, 4, 15));
executeAction(kebabCaseAction, editor);
assert.strictEqual(model.getLineContent(4), 'get-element-by-id');
assertSelection(editor, new Selection(4, 1, 4, 18));
editor.setSelection(new Selection(5, 1, 5, 11));
executeAction(kebabCaseAction, editor);
assert.strictEqual(model.getLineContent(5), 'pascal-case');
assertSelection(editor, new Selection(5, 1, 5, 12));
editor.setSelection(new Selection(6, 1, 6, 11));
executeAction(kebabCaseAction, editor);
assert.strictEqual(model.getLineContent(6), 'öçş-öç-şğü-ğü');
assertSelection(editor, new Selection(6, 1, 6, 14));
editor.setSelection(new Selection(7, 1, 7, 34));
executeAction(kebabCaseAction, editor);
assert.strictEqual(model.getLineContent(7), 'audio-converter.convert-m4a-to-mp3();');
assertSelection(editor, new Selection(7, 1, 7, 38));
editor.setSelection(new Selection(8, 1, 8, 19));
executeAction(kebabCaseAction, editor);
assert.strictEqual(model.getLineContent(8), 'capital-snake-case');
assertSelection(editor, new Selection(8, 1, 8, 19));
editor.setSelection(new Selection(9, 1, 9, 17));
executeAction(kebabCaseAction, editor);
assert.strictEqual(model.getLineContent(9), 'parse-html4-string');
assertSelection(editor, new Selection(9, 1, 9, 19));
editor.setSelection(new Selection(10, 1, 10, 28));
executeAction(kebabCaseAction, editor);
assert.strictEqual(model.getLineContent(10), '_accessor: services-accessor');
assertSelection(editor, new Selection(10, 1, 10, 29));
editor.setSelection(new Selection(11, 1, 11, 11));
executeAction(kebabCaseAction, editor);
assert.strictEqual(model.getLineContent(11), 'kebab-case');
assertSelection(editor, new Selection(11, 1, 11, 11));
}
);
});
suite('DeleteAllRightAction', () => {

View file

@ -163,14 +163,16 @@ export class LinkDetector extends Disposable implements IEditorContribution {
}
}
const decorations = this.editor.deltaDecorations(oldDecorations, newDecorations);
this.editor.changeDecorations((changeAccessor) => {
const decorations = changeAccessor.deltaDecorations(oldDecorations, newDecorations);
this.currentOccurrences = {};
this.activeLinkDecorationId = null;
for (let i = 0, len = decorations.length; i < len; i++) {
const occurence = new LinkOccurrence(links[i], decorations[i]);
this.currentOccurrences[occurence.decorationId] = occurence;
}
this.currentOccurrences = {};
this.activeLinkDecorationId = null;
for (let i = 0, len = decorations.length; i < len; i++) {
const occurence = new LinkOccurrence(links[i], decorations[i]);
this.currentOccurrences[occurence.decorationId] = occurence;
}
});
}
private _onEditorMouseMove(mouseEvent: ClickLinkMouseEvent, withKey: ClickLinkKeyboardEvent | null): void {

View file

@ -55,7 +55,7 @@ export class OneSnippet {
dispose(): void {
if (this._placeholderDecorations) {
this._editor.deltaDecorations([...this._placeholderDecorations.values()], []);
this._editor.removeDecorations([...this._placeholderDecorations.values()]);
}
this._placeholderGroups.length = 0;
}

View file

@ -502,18 +502,20 @@ suite('Editor Controller - Cursor', () => {
);
withTestCodeEditor(model, { wrappingIndent: 'indent', wordWrap: 'wordWrapColumn', wordWrapColumn: 20 }, (editor, viewModel) => {
editor.deltaDecorations([], [
{
range: new Range(1, 22, 1, 22),
options: {
showIfCollapsed: true,
description: 'test',
after: {
content: 'some very very very very very very very very long text',
editor.changeDecorations((changeAccessor) => {
changeAccessor.deltaDecorations([], [
{
range: new Range(1, 22, 1, 22),
options: {
showIfCollapsed: true,
description: 'test',
after: {
content: 'some very very very very very very very very long text',
}
}
}
}
]);
]);
});
viewModel.setSelections('test', [new Selection(1, 1, 1, 1)]);
const cursorPositions: any[] = [];

View file

@ -192,7 +192,9 @@ suite('CodeEditorWidget', () => {
const calls: string[] = [];
disposables.add(editor.onDidChangeModelContent((e) => {
calls.push(`listener1 - contentchange(${e.changes.reduce<any[]>((aggr, c) => [...aggr, c.text, c.rangeOffset, c.rangeLength], []).join(', ')})`);
editor.deltaDecorations([], [{ range: new Range(1, 1, 1, 1), options: { description: 'test' } }]);
editor.changeDecorations((changeAccessor) => {
changeAccessor.deltaDecorations([], [{ range: new Range(1, 1, 1, 1), options: { description: 'test' } }]);
});
}));
disposables.add(editor.onDidChangeCursorSelection((e) => {
calls.push(`listener1 - cursorchange(${e.selection.positionLineNumber}, ${e.selection.positionColumn})`);

View file

@ -258,4 +258,11 @@ suite('Editor Modes - Link Computer', () => {
'https://site.web/page.html '
);
});
test('issue #151631: Link parsing stoped where comments include a single quote ', () => {
assertLink(
`aa https://regexper.com/#%2F''%2F aa`,
` https://regexper.com/#%2F''%2F `,
);
});
});

6
src/vs/monaco.d.ts vendored
View file

@ -5313,9 +5313,13 @@ declare namespace monaco.editor {
getDecorationsInRange(range: Range): IModelDecoration[] | null;
/**
* All decorations added through this call will get the ownerId of this editor.
* @see {@link ITextModel.deltaDecorations}
* @deprecated
*/
deltaDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[];
/**
* Remove previously added decorations.
*/
removeDecorations(decorationIds: string[]): void;
/**
* Get the layout info for the editor.
*/

View file

@ -225,6 +225,9 @@ class SimpleContextKeyChangeEvent implements IContextKeyChangeEvent {
affectsSome(keys: IReadableSet<string>): boolean {
return keys.has(this.key);
}
allKeysContainedIn(keys: IReadableSet<string>): boolean {
return this.affectsSome(keys);
}
}
class ArrayContextKeyChangeEvent implements IContextKeyChangeEvent {
@ -237,6 +240,9 @@ class ArrayContextKeyChangeEvent implements IContextKeyChangeEvent {
}
return false;
}
allKeysContainedIn(keys: IReadableSet<string>): boolean {
return this.keys.every(key => keys.has(key));
}
}
class CompositeContextKeyChangeEvent implements IContextKeyChangeEvent {
@ -249,12 +255,13 @@ class CompositeContextKeyChangeEvent implements IContextKeyChangeEvent {
}
return false;
}
allKeysContainedIn(keys: IReadableSet<string>): boolean {
return this.events.every(evt => evt.allKeysContainedIn(keys));
}
}
function allEventKeysInContext(event: IContextKeyChangeEvent, context: Record<string, any>): boolean {
return (event instanceof ArrayContextKeyChangeEvent && event.keys.every(key => key in context)) ||
(event instanceof SimpleContextKeyChangeEvent && event.key in context) ||
(event instanceof CompositeContextKeyChangeEvent && event.events.every(e => allEventKeysInContext(e, context)));
return event.allKeysContainedIn(new Set(Object.keys(context)));
}
export abstract class AbstractContextKeyService implements IContextKeyService {

View file

@ -1597,6 +1597,7 @@ export interface IReadableSet<T> {
export interface IContextKeyChangeEvent {
affectsSome(keys: IReadableSet<string>): boolean;
allKeysContainedIn(keys: IReadableSet<string>): boolean;
}
export interface IContextKeyService {

View file

@ -16,6 +16,7 @@ export const IsMacNativeContext = new RawContextKey<boolean>('isMacNative', isMa
export const IsIOSContext = new RawContextKey<boolean>('isIOS', isIOS, localize('isIOS', "Whether the operating system is iOS"));
export const IsDevelopmentContext = new RawContextKey<boolean>('isDevelopment', false, true);
export const ProductQualityContext = new RawContextKey<string>('productQualityType', '', localize('productQualityType', "Quality type of VS Code"));
export const InputFocusedContextKey = 'inputFocus';
export const InputFocusedContext = new RawContextKey<boolean>(InputFocusedContextKey, false, localize('inputFocus', "Whether keyboard focus is inside an input box"));

View file

@ -224,10 +224,11 @@ export abstract class AbstractExtensionManagementService extends Disposable impl
await this.joinAllSettled(this.participants.map(participant => participant.postInstall(local, task.source, options, CancellationToken.None)));
if (!URI.isUri(task.source)) {
const isUpdate = task.operation === InstallOperation.Update;
const durationSinceUpdate = isUpdate ? undefined : (new Date().getTime() - task.source.lastUpdated) / 1000;
reportTelemetry(this.telemetryService, isUpdate ? 'extensionGallery:update' : 'extensionGallery:install', {
extensionData: getGalleryExtensionTelemetryData(task.source),
duration: new Date().getTime() - startTime,
durationSinceUpdate: isUpdate ? undefined : new Date().getTime() - task.source.lastUpdated
durationSinceUpdate
});
// In web, report extension install statistics explicitly. In Desktop, statistics are automatically updated while downloading the VSIX.
if (isWeb && task.operation !== InstallOperation.Update) {

View file

@ -584,7 +584,14 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
const config = productService.extensionsGallery;
this.extensionsGalleryUrl = config && config.serviceUrl;
this.extensionsControlUrl = config && config.controlUrl;
this.commonHeadersPromise = resolveMarketplaceHeaders(productService.version, productService, this.environmentService, this.configurationService, this.fileService, storageService);
this.commonHeadersPromise = resolveMarketplaceHeaders(
productService.version,
productService,
this.environmentService,
this.configurationService,
this.fileService,
storageService,
this.telemetryService);
}
private api(path = ''): string {
@ -928,7 +935,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi
'Content-Type': 'application/json',
'Accept': 'application/json;api-version=3.0-preview.1',
'Accept-Encoding': 'gzip',
'Content-Length': String(data.length)
'Content-Length': String(data.length),
};
const startTime = new Date().getTime();

View file

@ -23,6 +23,7 @@ import { resolveMarketplaceHeaders } from 'vs/platform/externalServices/common/m
import { InMemoryStorageService, IStorageService } from 'vs/platform/storage/common/storage';
import { TelemetryConfiguration, TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry';
import { TargetPlatform } from 'vs/platform/extensions/common/extensions';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
class EnvironmentServiceMock extends mock<IEnvironmentService>() {
override readonly serviceMachineIdResource: URI;
@ -52,9 +53,9 @@ suite('Extension Gallery Service', () => {
teardown(() => disposables.clear());
test('marketplace machine id', async () => {
const headers = await resolveMarketplaceHeaders(product.version, productService, environmentService, configurationService, fileService, storageService);
const headers = await resolveMarketplaceHeaders(product.version, productService, environmentService, configurationService, fileService, storageService, NullTelemetryService);
assert.ok(isUUID(headers['X-Market-User-Id']));
const headers2 = await resolveMarketplaceHeaders(product.version, productService, environmentService, configurationService, fileService, storageService);
const headers2 = await resolveMarketplaceHeaders(product.version, productService, environmentService, configurationService, fileService, storageService, NullTelemetryService);
assert.strictEqual(headers['X-Market-User-Id'], headers2['X-Market-User-Id']);
});

View file

@ -324,6 +324,7 @@ class UtilityExtensionHostProcess extends Disposable {
const modulePath = FileAccess.asFileUri('bootstrap-fork.js', require).fsPath;
const args: string[] = ['--type=extensionHost', '--skipWorkspaceStorageLock'];
const execArgv: string[] = opts.execArgv || [];
execArgv.push(`--vscode-utility-kind=extensionHost`);
const env: { [key: string]: any } = { ...opts.env };
// Make sure all values are strings, otherwise the process will not start

View file

@ -10,17 +10,26 @@ import { getServiceMachineId } from 'vs/platform/externalServices/common/service
import { IFileService } from 'vs/platform/files/common/files';
import { IProductService } from 'vs/platform/product/common/productService';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';
import { ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';
import { getTelemetryLevel, supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils';
export async function resolveMarketplaceHeaders(version: string, productService: IProductService, environmentService: IEnvironmentService, configurationService: IConfigurationService, fileService: IFileService, storageService: IStorageService | undefined): Promise<IHeaders> {
export async function resolveMarketplaceHeaders(version: string,
productService: IProductService,
environmentService: IEnvironmentService,
configurationService: IConfigurationService,
fileService: IFileService,
storageService: IStorageService | undefined,
telemetryService: ITelemetryService): Promise<IHeaders> {
const headers: IHeaders = {
'X-Market-Client-Id': `VSCode ${version}`,
'User-Agent': `VSCode ${version} (${productService.nameShort})`
};
const uuid = await getServiceMachineId(environmentService, fileService, storageService);
const { sessionId } = await telemetryService.getTelemetryInfo();
if (supportsTelemetry(productService, environmentService) && getTelemetryLevel(configurationService) === TelemetryLevel.USAGE) {
headers['X-Market-User-Id'] = uuid;
headers['VSCode-SessionId'] = sessionId;
}
return headers;
}

View file

@ -253,7 +253,7 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst
private readonly _onReportError = this._register(new Emitter<IndexedDBFileSystemProviderErrorData>());
readonly onReportError = this._onReportError.event;
private readonly versions = new Map<string, number>();
private readonly mtimes = new Map<string, number>();
private cachedFiletree: Promise<IndexedDBFileSystemNode> | undefined;
private writeManyThrottler: Throttler;
@ -289,7 +289,7 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst
return {
type: FileType.File,
ctime: 0,
mtime: this.versions.get(resource.toString()) || 0,
mtime: this.mtimes.get(resource.toString()) || 0,
size: entry.size ?? (await this.readFile(resource)).byteLength
};
}
@ -434,7 +434,7 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst
}
await this.deleteKeys(toDelete);
(await this.getFiletree()).delete(resource.path);
toDelete.forEach(key => this.versions.delete(key));
toDelete.forEach(key => this.mtimes.delete(key));
this.triggerChanges(toDelete.map(path => ({ resource: resource.with({ path }), type: FileChangeType.DELETED })));
}
@ -487,7 +487,7 @@ export class IndexedDBFileSystemProvider extends Disposable implements IFileSyst
const fileTree = await this.getFiletree();
for (const [resource, content] of files) {
fileTree.add(resource.path, { type: 'file', size: content.byteLength });
this.versions.set(resource.toString(), (this.versions.get(resource.toString()) || 0) + 1);
this.mtimes.set(resource.toString(), Date.now());
}
this.triggerChanges(files.map(([resource]) => ({ resource, type: FileChangeType.UPDATED })));

View file

@ -555,7 +555,7 @@ export abstract class AbstractStorageService extends Disposable implements IStor
return false; // both profiles are same
}
if (isProfileUsingDefaultStorage(to) === isProfileUsingDefaultStorage(from)) {
if (isProfileUsingDefaultStorage(to) && isProfileUsingDefaultStorage(from)) {
return false; // both profiles are using default
}

View file

@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import { hash } from 'vs/base/common/hash';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { joinPath } from 'vs/base/common/resources';
@ -16,8 +15,6 @@ import { IFileService } from 'vs/platform/files/common/files';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
import { Registry } from 'vs/platform/registry/common/platform';
import { isWeb } from 'vs/base/common/platform';
/**
* Flags to indicate whether to use the default profile or not.
@ -65,21 +62,6 @@ export function isUserDataProfile(thing: unknown): thing is IUserDataProfile {
}
export const PROFILES_ENABLEMENT_CONFIG = 'workbench.experimental.settingsProfiles.enabled';
export const PROFILES_ENABLEMENT_CONFIG_SCHEMA: IConfigurationPropertySchema = {
'type': 'boolean',
'default': false,
'description': localize('workbench.experimental.settingsProfiles.enabled', "Controls whether to enable the Settings Profiles preview feature."),
scope: ConfigurationScope.APPLICATION
};
if (!isWeb) {
// Registering here so that the configuration is read properly in main and cli processes.
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({
'properties': {
[PROFILES_ENABLEMENT_CONFIG]: PROFILES_ENABLEMENT_CONFIG_SCHEMA
}
});
}
export type EmptyWindowWorkspaceIdentifier = 'empty-window';
export type WorkspaceIdentifier = ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier | EmptyWindowWorkspaceIdentifier;

View file

@ -542,7 +542,14 @@ export class CodeWindow extends Disposable implements ICodeWindow {
private marketplaceHeadersPromise: Promise<object> | undefined;
private getMarketplaceHeaders(): Promise<object> {
if (!this.marketplaceHeadersPromise) {
this.marketplaceHeadersPromise = resolveMarketplaceHeaders(this.productService.version, this.productService, this.environmentMainService, this.configurationService, this.fileService, this.applicationStorageMainService);
this.marketplaceHeadersPromise = resolveMarketplaceHeaders(
this.productService.version,
this.productService,
this.environmentMainService,
this.configurationService,
this.fileService,
this.applicationStorageMainService,
this.telemetryService);
}
return this.marketplaceHeadersPromise;

View file

@ -74,6 +74,15 @@ export class MainThreadCommands implements MainThreadCommandsShape {
}
}
$fireCommandActivationEvent(id: string): void {
const activationEvent = `onCommand:${id}`;
if (!this._extensionService.activationEventIsDone(activationEvent)) {
// this is NOT awaited because we only use it as drive-by-activation
// for commands that are already known inside the extension host
this._extensionService.activateByEvent(activationEvent);
}
}
async $executeCommand<T>(id: string, args: any[] | SerializableObjectWithBuffers<any[]>, retry: boolean): Promise<T | undefined> {
if (args instanceof SerializableObjectWithBuffers) {
args = args.value;

Some files were not shown because too many files have changed in this diff Show more