mirror of
https://github.com/Microsoft/vscode
synced 2024-09-19 02:26:04 +00:00
Merge branch 'main' into feature/support-angle-bracket-matching-in-typescript
This commit is contained in:
commit
451ae1ffa3
2
.github/classifier.json
vendored
2
.github/classifier.json
vendored
|
@ -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"]},
|
||||
|
|
2
.github/workflows/basic.yml
vendored
2
.github/workflows/basic.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -129,7 +129,7 @@
|
|||
},
|
||||
"emmet.useInlineCompletions": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"default": false,
|
||||
"markdownDescription": "%emmetUseInlineCompletions%"
|
||||
},
|
||||
"emmet.preferences": {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
"contributes": {
|
||||
"notebookRenderer": [
|
||||
{
|
||||
"id": "markdownItRenderer",
|
||||
"id": "vscode.markdown-it-renderer",
|
||||
"displayName": "Markdown it renderer",
|
||||
"entrypoint": "./notebook-out/index.js",
|
||||
"mimeTypes": [
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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('.')) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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' },
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"git": {
|
||||
"name": "seti-ui",
|
||||
"repositoryUrl": "https://github.com/jesseweed/seti-ui",
|
||||
"commitHash": "6b83574de165123583d6d8d5b3b6c91f04b7153d"
|
||||
"commitHash": "4dd6c27e1f5aed8068c2451dbaf0db3364545937"
|
||||
}
|
||||
},
|
||||
"version": "0.1.0"
|
||||
|
|
Binary file not shown.
|
@ -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"
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
12
package.json
12
package.json
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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==
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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]>>;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
Binary file not shown.
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
30
src/vs/base/common/observable.ts
Normal file
30
src/vs/base/common/observable.ts
Normal 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());
|
||||
}
|
167
src/vs/base/common/observableImpl/autorun.ts
Normal file
167
src/vs/base/common/observableImpl/autorun.ts
Normal 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 });
|
||||
});
|
||||
}
|
244
src/vs/base/common/observableImpl/base.ts
Normal file
244
src/vs/base/common/observableImpl/base.ts
Normal 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}`;
|
||||
}
|
||||
}
|
||||
|
167
src/vs/base/common/observableImpl/derived.ts
Normal file
167
src/vs/base/common/observableImpl/derived.ts
Normal 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}>`;
|
||||
}
|
||||
}
|
312
src/vs/base/common/observableImpl/logging.ts
Normal file
312
src/vs/base/common/observableImpl/logging.ts
Normal 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;
|
||||
}
|
281
src/vs/base/common/observableImpl/utils.ts
Normal file
281
src/vs/base/common/observableImpl/utils.ts
Normal 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);
|
||||
},
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
468
src/vs/base/test/common/observable.test.ts
Normal file
468
src/vs/base/test/common/observable.test.ts
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 `*`
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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 = [];
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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[] {
|
||||
|
|
|
@ -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[] = [];
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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]));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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[] = [];
|
||||
|
|
|
@ -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})`);
|
||||
|
|
|
@ -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
6
src/vs/monaco.d.ts
vendored
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1597,6 +1597,7 @@ export interface IReadableSet<T> {
|
|||
|
||||
export interface IContextKeyChangeEvent {
|
||||
affectsSome(keys: IReadableSet<string>): boolean;
|
||||
allKeysContainedIn(keys: IReadableSet<string>): boolean;
|
||||
}
|
||||
|
||||
export interface IContextKeyService {
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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']);
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 })));
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue