src/vs/platform/userDataSync/common/keybindingsMerge.ts

361 LOC · 332 covered · 29 uncovered · 115 ranges · 585 concepts · 31 introducers · 348 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- abstractSynchronizer.ts ×49
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { equals } from '../../../base/common/arrays.js';
7 > import { IStringDictionary } from '../../../base/common/collections.js';
8 > import { parse } from '../../../base/common/json.js';
9 > import { FormattingOptions } from '../../../base/common/jsonFormatter.js';
10 > import * as objects from '../../../base/common/objects.js';
11 > import { ContextKeyExpr } from '../../contextkey/common/contextkey.js';
12 > import { IUserFriendlyKeybinding } from '../../keybinding/common/keybinding.js';
13 > import * as contentUtil from './content.js';
14 > import { IUserDataSyncUtilService } from './userDataSync.js';
15 >
16 > interface ICompareResult {
17 > added: Set<string>;
18 > removed: Set<string>;
19 > updated: Set<string>;
20 > }
21 >
22 > interface IMergeResult {
23 > hasLocalForwarded: boolean;
24 > hasRemoteForwarded: boolean;
25 > added: Set<string>;
26 > removed: Set<string>;
27 > updated: Set<string>;
28 > conflicts: Set<string>;
29 > }
30 >
31 > function parseKeybindings(content: string): IUserFriendlyKeybinding[] { keybindingsMerge.ts ×12
32 > return parse(content) || [];
33 > }
35 > export async function merge(localContent: string, remoteContent: string, baseContent: string | null, formattingOptions: FormattingOptions, userDataSyncUtilService: IUserDataSyncUtilService): Promise<{ mergeContent: string; hasChanges: boolean; hasConflicts: boolean }> { keybindingsMerge.ts ×12
36 > const local = parseKeybindings(localContent);
37 > const remote = parseKeybindings(remoteContent);
38 > const base = baseContent ? parseKeybindings(baseContent) : null;
39 >
40 > const userbindings: string[] = [...local, ...remote, ...(base || [])].map(keybinding => keybinding.key);
41 > const normalizedKeys = await userDataSyncUtilService.resolveUserBindings(userbindings);
42 > const keybindingsMergeResult = computeMergeResultByKeybinding(local, remote, base, normalizedKeys);
43 >
44 > if (!keybindingsMergeResult.hasLocalForwarded && !keybindingsMergeResult.hasRemoteForwarded) {
45 > // No changes found between local and remote. keybindingsMerge.ts ×2
46 > return { mergeContent: localContent, hasChanges: false, hasConflicts: false };
47 > }
49 > if (!keybindingsMergeResult.hasLocalForwarded && keybindingsMergeResult.hasRemoteForwarded) { keybindingsMerge.ts ×12
50 > return { mergeContent: remoteContent, hasChanges: true, hasConflicts: false }; keybindingsMerge.ts ×2
51 > }
53 > if (keybindingsMergeResult.hasLocalForwarded && !keybindingsMergeResult.hasRemoteForwarded) { keybindingsMerge.ts ×12
54 > // Local has moved forward and remote has not. Return local. keybindingsMerge.ts ×2
55 > return { mergeContent: localContent, hasChanges: true, hasConflicts: false };
56 > }
58 > // Both local and remote has moved forward.
59 > const localByCommand = byCommand(local);
60 > const remoteByCommand = byCommand(remote);
61 > const baseByCommand = base ? byCommand(base) : null; keybindingsMerge.ts ×12
62 > const localToRemoteByCommand = compareByCommand(localByCommand, remoteByCommand, normalizedKeys);
63 > const baseToLocalByCommand = baseByCommand ? compareByCommand(baseByCommand, localByCommand, normalizedKeys) : { added: [...localByCommand.keys()].reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
64 > const baseToRemoteByCommand = baseByCommand ? compareByCommand(baseByCommand, remoteByCommand, normalizedKeys) : { added: [...remoteByCommand.keys()].reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
65 >
66 > const commandsMergeResult = computeMergeResult(localToRemoteByCommand, baseToLocalByCommand, baseToRemoteByCommand);
67 > let mergeContent = localContent;
68 >
69 > // Removed commands in Remote
70 > for (const command of commandsMergeResult.removed.values()) {
71 > if (commandsMergeResult.conflicts.has(command)) { keybindingsMerge.ts ×12
72 continue;
73 }
74 > mergeContent = removeKeybindings(mergeContent, command, formattingOptions); keybindingsMerge.ts ×12
75 > }
77 > // Added commands in remote
78 > for (const command of commandsMergeResult.added.values()) {
79 > if (commandsMergeResult.conflicts.has(command)) { keybindingsMerge.ts ×4
80 continue;
81 }
82 > const keybindings = remoteByCommand.get(command)!; keybindingsMerge.ts ×4
83 > // Ignore negated commands
84 > if (keybindings.some(keybinding => keybinding.command !== `-${command}` && keybindingsMergeResult.conflicts.has(normalizedKeys[keybinding.key]))) {
85 commandsMergeResult.conflicts.add(command);
86 continue;
87 }
88 > mergeContent = addKeybindings(mergeContent, keybindings, formattingOptions); keybindingsMerge.ts ×4
89 > }
91 > // Updated commands in Remote
92 > for (const command of commandsMergeResult.updated.values()) {
93 > if (commandsMergeResult.conflicts.has(command)) { keybindingsMerge.ts ×12
94 continue;
95 }
96 > const keybindings = remoteByCommand.get(command)!; keybindingsMerge.ts ×12
97 > // Ignore negated commands
98 > if (keybindings.some(keybinding => keybinding.command !== `-${command}` && keybindingsMergeResult.conflicts.has(normalizedKeys[keybinding.key]))) {
99 > commandsMergeResult.conflicts.add(command); keybindingsMerge.ts ×1
100 > continue;
101 > }
102 > mergeContent = updateKeybindings(mergeContent, command, keybindings, formattingOptions); keybindingsMerge.ts ×12
103 > }
105 > return { mergeContent, hasChanges: true, hasConflicts: commandsMergeResult.conflicts.size > 0 };
106 > }
108 > function computeMergeResult(localToRemote: ICompareResult, baseToLocal: ICompareResult, baseToRemote: ICompareResult): { added: Set<string>; removed: Set<string>; updated: Set<string>; conflicts: Set<string> } { keybindingsMerge.ts ×17
109 > const added: Set<string> = new Set<string>();
110 > const removed: Set<string> = new Set<string>();
111 > const updated: Set<string> = new Set<string>();
112 > const conflicts: Set<string> = new Set<string>();
113 >
114 > // Removed keys in Local
115 > for (const key of baseToLocal.removed.values()) {
116 > // Got updated in remote keybindingsMerge.ts ×5
117 > if (baseToRemote.updated.has(key)) {
118 > conflicts.add(key); keybindingsMerge.ts ×2
119 > }
122 > // Removed keys in Remote
123 > for (const key of baseToRemote.removed.values()) {
124 > if (conflicts.has(key)) { keybindingsMerge.ts ×5
125 continue;
126 }
127 > // Got updated in local keybindingsMerge.ts ×5
128 > if (baseToLocal.updated.has(key)) {
129 > conflicts.add(key); keybindingsMerge.ts ×2
130 > } else { keybindingsMerge.ts ×5
131 > // remove the key
132 > removed.add(key);
133 > }
134 > }
136 > // Added keys in Local
137 > for (const key of baseToLocal.added.values()) {
138 > if (conflicts.has(key)) { keybindingsMerge.ts ×3
139 continue;
140 }
141 > // Got added in remote keybindingsMerge.ts ×3
142 > if (baseToRemote.added.has(key)) {
143 > // Has different value keybindingsMerge.ts ×2
144 > if (localToRemote.updated.has(key)) {
145 > conflicts.add(key); keybindingsMerge.ts ×2
146 > }
150 > // Added keys in remote
151 > for (const key of baseToRemote.added.values()) {
152 > if (conflicts.has(key)) { keybindingsMerge.ts ×4
153 > continue; keybindingsMerge.ts ×2
154 > }
155 > // Got added in local keybindingsMerge.ts ×4
156 > if (baseToLocal.added.has(key)) {
157 > // Has different value keybindingsMerge.ts ×1
158 > if (localToRemote.updated.has(key)) {
159 conflicts.add(key);
160 }
161 > } else { keybindingsMerge.ts ×4
162 > added.add(key); keybindingsMerge.ts ×1
163 > }
166 > // Updated keys in Local
167 > for (const key of baseToLocal.updated.values()) {
168 > if (conflicts.has(key)) { keybindingsMerge.ts ×2
169 > continue; keybindingsMerge.ts ×2
170 > }
171 > // Got updated in remote keybindingsMerge.ts ×12
172 > if (baseToRemote.updated.has(key)) {
173 > // Has different value
174 > if (localToRemote.updated.has(key)) {
175 conflicts.add(key);
176 }
180 > // Updated keys in Remote
181 > for (const key of baseToRemote.updated.values()) {
182 > if (conflicts.has(key)) { keybindingsMerge.ts ×2
183 > continue; keybindingsMerge.ts ×2
184 > }
185 > // Got updated in local keybindingsMerge.ts ×12
186 > if (baseToLocal.updated.has(key)) {
187 > // Has different value
188 > if (localToRemote.updated.has(key)) {
189 conflicts.add(key);
190 }
191 > } else { keybindingsMerge.ts ×12
192 > // updated key
193 > updated.add(key);
194 > }
196 > return { added, removed, updated, conflicts }; keybindingsMerge.ts ×17
197 > }
199 > function computeMergeResultByKeybinding(local: IUserFriendlyKeybinding[], remote: IUserFriendlyKeybinding[], base: IUserFriendlyKeybinding[] | null, normalizedKeys: IStringDictionary<string>): IMergeResult { keybindingsMerge.ts ×12
200 > const empty = new Set<string>();
201 > const localByKeybinding = byKeybinding(local, normalizedKeys);
202 > const remoteByKeybinding = byKeybinding(remote, normalizedKeys);
203 > const baseByKeybinding = base ? byKeybinding(base, normalizedKeys) : null;
204 >
205 > const localToRemoteByKeybinding = compareByKeybinding(localByKeybinding, remoteByKeybinding);
206 > if (localToRemoteByKeybinding.added.size === 0 && localToRemoteByKeybinding.removed.size === 0 && localToRemoteByKeybinding.updated.size === 0) {
207 > return { hasLocalForwarded: false, hasRemoteForwarded: false, added: empty, removed: empty, updated: empty, conflicts: empty }; keybindingsMerge.ts ×2
208 > }
210 > const baseToLocalByKeybinding = baseByKeybinding ? compareByKeybinding(baseByKeybinding, localByKeybinding) : { added: [...localByKeybinding.keys()].reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() }; keybindingsMerge.ts ×12
211 > if (baseToLocalByKeybinding.added.size === 0 && baseToLocalByKeybinding.removed.size === 0 && baseToLocalByKeybinding.updated.size === 0) {
212 > // Remote has moved forward and local has not. keybindingsMerge.ts ×2
213 > return { hasLocalForwarded: false, hasRemoteForwarded: true, added: empty, removed: empty, updated: empty, conflicts: empty };
214 > }
216 > const baseToRemoteByKeybinding = baseByKeybinding ? compareByKeybinding(baseByKeybinding, remoteByKeybinding) : { added: [...remoteByKeybinding.keys()].reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() }; keybindingsMerge.ts ×12
217 > if (baseToRemoteByKeybinding.added.size === 0 && baseToRemoteByKeybinding.removed.size === 0 && baseToRemoteByKeybinding.updated.size === 0) {
218 > return { hasLocalForwarded: true, hasRemoteForwarded: false, added: empty, removed: empty, updated: empty, conflicts: empty }; keybindingsMerge.ts ×2
219 > }
221 > const { added, removed, updated, conflicts } = computeMergeResult(localToRemoteByKeybinding, baseToLocalByKeybinding, baseToRemoteByKeybinding);
222 > return { hasLocalForwarded: true, hasRemoteForwarded: true, added, removed, updated, conflicts };
223 > }
225 > function byKeybinding(keybindings: IUserFriendlyKeybinding[], keys: IStringDictionary<string>) { keybindingsMerge.ts ×12
226 > const map: Map<string, IUserFriendlyKeybinding[]> = new Map<string, IUserFriendlyKeybinding[]>();
227 > for (const keybinding of keybindings) {
228 > const key = keys[keybinding.key]; keybindingsMerge.ts ×1
229 > let value = map.get(key);
230 > if (!value) {
231 > value = [];
232 > map.set(key, value);
233 > }
234 > value.push(keybinding);
235 >
236 > }
237 > return map; keybindingsMerge.ts ×12
238 > }
240 > function byCommand(keybindings: IUserFriendlyKeybinding[]): Map<string, IUserFriendlyKeybinding[]> { keybindingsMerge.ts ×17
241 > const map: Map<string, IUserFriendlyKeybinding[]> = new Map<string, IUserFriendlyKeybinding[]>();
242 > for (const keybinding of keybindings) {
243 > const command = keybinding.command[0] === '-' ? keybinding.command.substring(1) : keybinding.command;
244 > let value = map.get(command);
245 > if (!value) {
246 > value = [];
247 > map.set(command, value);
248 > }
249 > value.push(keybinding);
250 > }
251 > return map;
252 > }
254 >
255 > function compareByKeybinding(from: Map<string, IUserFriendlyKeybinding[]>, to: Map<string, IUserFriendlyKeybinding[]>): ICompareResult { keybindingsMerge.ts ×12
256 > const fromKeys = [...from.keys()];
257 > const toKeys = [...to.keys()];
258 > const added = toKeys.filter(key => !fromKeys.includes(key)).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
259 > const removed = fromKeys.filter(key => !toKeys.includes(key)).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
260 > const updated: Set<string> = new Set<string>();
261 >
262 > for (const key of fromKeys) {
263 > if (removed.has(key)) { keybindingsMerge.ts ×2
264 > continue; keybindingsMerge.ts ×1
265 > }
266 > const value1: IUserFriendlyKeybinding[] = from.get(key)!.map(keybinding => ({ ...keybinding, ...{ key } })); keybindingsMerge.ts ×4
267 > const value2: IUserFriendlyKeybinding[] = to.get(key)!.map(keybinding => ({ ...keybinding, ...{ key } }));
268 > if (!equals(value1, value2, (a, b) => isSameKeybinding(a, b))) {
269 > updated.add(key); keybindingsMerge.ts ×2
270 > }
273 > return { added, removed, updated };
274 > }
276 > function compareByCommand(from: Map<string, IUserFriendlyKeybinding[]>, to: Map<string, IUserFriendlyKeybinding[]>, normalizedKeys: IStringDictionary<string>): ICompareResult { keybindingsMerge.ts ×17
277 > const fromKeys = [...from.keys()];
278 > const toKeys = [...to.keys()];
279 > const added = toKeys.filter(key => !fromKeys.includes(key)).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
280 > const removed = fromKeys.filter(key => !toKeys.includes(key)).reduce((r, key) => { r.add(key); return r; }, new Set<string>());
281 > const updated: Set<string> = new Set<string>();
282 >
283 > for (const key of fromKeys) {
284 > if (removed.has(key)) {
285 > continue; keybindingsMerge.ts ×1
286 > }
287 > const value1: IUserFriendlyKeybinding[] = from.get(key)!.map(keybinding => ({ ...keybinding, ...{ key: normalizedKeys[keybinding.key] } })); keybindingsMerge.ts ×17
288 > const value2: IUserFriendlyKeybinding[] = to.get(key)!.map(keybinding => ({ ...keybinding, ...{ key: normalizedKeys[keybinding.key] } }));
289 > if (!areSameKeybindingsWithSameCommand(value1, value2)) {
290 > updated.add(key); keybindingsMerge.ts ×3
291 > }
293 >
294 > return { added, removed, updated };
295 > }
297 > function areSameKeybindingsWithSameCommand(value1: IUserFriendlyKeybinding[], value2: IUserFriendlyKeybinding[]): boolean { keybindingsMerge.ts ×17
298 > // Compare entries adding keybindings
299 > if (!equals(value1.filter(({ command }) => command[0] !== '-'), value2.filter(({ command }) => command[0] !== '-'), (a, b) => isSameKeybinding(a, b))) {
300 > return false; keybindingsMerge.ts ×3
301 > }
302 > // Compare entries removing keybindings keybindingsMerge.ts ×2
303 > if (!equals(value1.filter(({ command }) => command[0] === '-'), value2.filter(({ command }) => command[0] === '-'), (a, b) => isSameKeybinding(a, b))) {
304 return false;
305 }
306 > return true; keybindingsMerge.ts ×2
307 > }
309 > function isSameKeybinding(a: IUserFriendlyKeybinding, b: IUserFriendlyKeybinding): boolean { keybindingsMerge.ts ×4
310 > if (a.command !== b.command) {
311 > return false; keybindingsMerge.ts ×2
312 > }
313 > if (a.key !== b.key) { keybindingsMerge.ts ×4
314 > return false; keybindingsMerge.ts ×3
315 > }
316 > const whenA = ContextKeyExpr.deserialize(a.when); keybindingsMerge.ts ×4
317 > const whenB = ContextKeyExpr.deserialize(b.when);
318 > if ((whenA && !whenB) || (!whenA && whenB)) { keybindingsMerge.ts ×4
319 return false;
320 }
321 > if (whenA && whenB && !whenA.equals(whenB)) { keybindingsMerge.ts ×4
322 return false;
323 }
324 > if (!objects.equals(a.args, b.args)) { keybindingsMerge.ts ×4
325 return false;
326 }
327 > return true; keybindingsMerge.ts ×4
328 > }
330 > function addKeybindings(content: string, keybindings: IUserFriendlyKeybinding[], formattingOptions: FormattingOptions): string { keybindingsMerge.ts ×4
331 > for (const keybinding of keybindings) {
332 > content = contentUtil.edit(content, [-1], keybinding, formattingOptions);
333 > }
334 > return content;
335 > }
337 > function removeKeybindings(content: string, command: string, formattingOptions: FormattingOptions): string { keybindingsMerge.ts ×12
338 > const keybindings = parseKeybindings(content);
339 > for (let index = keybindings.length - 1; index >= 0; index--) {
340 > if (keybindings[index].command === command || keybindings[index].command === `-${command}`) {
341 content = contentUtil.edit(content, [index], undefined, formattingOptions);
342 }
344 > return content;
345 > }
347 > function updateKeybindings(content: string, command: string, keybindings: IUserFriendlyKeybinding[], formattingOptions: FormattingOptions): string { keybindingsMerge.ts ×12
348 > const allKeybindings = parseKeybindings(content);
349 > const location = allKeybindings.findIndex(keybinding => keybinding.command === command || keybinding.command === `-${command}`);
350 > // Remove all entries with this command
351 > for (let index = allKeybindings.length - 1; index >= 0; index--) {
352 > if (allKeybindings[index].command === command || allKeybindings[index].command === `-${command}`) {
353 > content = contentUtil.edit(content, [index], undefined, formattingOptions);
354 > }
355 > }
356 > // add all entries at the same location where the entry with this command was located.
357 > for (let index = keybindings.length - 1; index >= 0; index--) {
358 > content = contentUtil.edit(content, [location], keybindings[index], formattingOptions);
359 > }
360 > return content;
361 > }