Archived
Merge from vscode 2f984aad710215f4e4684a035bb02f55d1a9e2cc (#9819)
This commit is contained in:
Vendored
-2
@@ -142,13 +142,11 @@ function pipeLoggingToParent() {
|
||||
function handleExceptions() {
|
||||
|
||||
// Handle uncaught exceptions
|
||||
// @ts-ignore
|
||||
process.on('uncaughtException', function (err) {
|
||||
console.error('Uncaught Exception: ', err);
|
||||
});
|
||||
|
||||
// Handle unhandled promise rejections
|
||||
// @ts-ignore
|
||||
process.on('unhandledRejection', function (reason) {
|
||||
console.error('Unhandled Promise Rejection: ', reason);
|
||||
});
|
||||
|
||||
Vendored
-4
@@ -25,7 +25,6 @@ exports.assign = function assign(destination, source) {
|
||||
*/
|
||||
exports.load = function (modulePaths, resultCallback, options) {
|
||||
|
||||
// @ts-ignore
|
||||
const webFrame = require('electron').webFrame;
|
||||
const path = require('path');
|
||||
|
||||
@@ -49,7 +48,6 @@ exports.load = function (modulePaths, resultCallback, options) {
|
||||
}
|
||||
|
||||
// Error handler
|
||||
// @ts-ignore
|
||||
process.on('uncaughtException', function (error) {
|
||||
onUnexpectedError(error, enableDeveloperTools);
|
||||
});
|
||||
@@ -184,7 +182,6 @@ function parseURLQueryArgs() {
|
||||
*/
|
||||
function registerDeveloperKeybindings(disallowReloadKeybinding) {
|
||||
|
||||
// @ts-ignore
|
||||
const ipc = require('electron').ipcRenderer;
|
||||
|
||||
const extractKey = function (e) {
|
||||
@@ -223,7 +220,6 @@ function registerDeveloperKeybindings(disallowReloadKeybinding) {
|
||||
|
||||
function onUnexpectedError(error, enableDeveloperTools) {
|
||||
|
||||
// @ts-ignore
|
||||
const ipc = require('electron').ipcRenderer;
|
||||
|
||||
if (enableDeveloperTools) {
|
||||
|
||||
+1
-1
@@ -323,7 +323,7 @@ function getUserDataPath(cliArgs) {
|
||||
* @returns {ParsedArgs}
|
||||
*/
|
||||
function parseCLIArgs() {
|
||||
const minimist = require('vscode-minimist');
|
||||
const minimist = require('minimist');
|
||||
|
||||
return minimist(process.argv, {
|
||||
string: [
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@
|
||||
//@ts-check
|
||||
'use strict';
|
||||
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
// const pkg = require('../package.json');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
@@ -34,4 +34,4 @@ function getDefaultUserDataPath(platform) {
|
||||
}
|
||||
|
||||
exports.getAppDataPath = getAppDataPath;
|
||||
exports.getDefaultUserDataPath = getDefaultUserDataPath;
|
||||
exports.getDefaultUserDataPath = getDefaultUserDataPath;
|
||||
|
||||
+1
-2
@@ -17,7 +17,6 @@ import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { ScrollableDirective } from 'sql/base/browser/ui/scrollable/scrollable.directive';
|
||||
import { values } from 'vs/base/common/collections';
|
||||
import { fill } from 'vs/base/common/arrays';
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
|
||||
export interface GridCellConfig {
|
||||
@@ -243,7 +242,7 @@ export class DashboardGridContainer extends DashboardTab implements OnDestroy {
|
||||
|
||||
private createIndexes(indexes: number[]) {
|
||||
const max = Math.max(...indexes) + 1;
|
||||
return fill(max, 0).map((x, i) => i);
|
||||
return new Array(max).fill(0).map((x, i) => i);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
||||
@@ -32,7 +32,7 @@ import { tableBackground, cellBackground, cellBorderColor } from 'sql/platform/t
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
|
||||
import { attachButtonStyler } from 'sql/platform/theme/common/styler';
|
||||
import { find, fill } from 'vs/base/common/arrays';
|
||||
import { find } from 'vs/base/common/arrays';
|
||||
import { IColorTheme } from 'vs/platform/theme/common/themeService';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
|
||||
@@ -672,7 +672,7 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe
|
||||
// if the durations are all 0 secs, show minimal chart
|
||||
// instead of nothing
|
||||
if (zeroDurationJobCount === jobHistories.length) {
|
||||
return fill(jobHistories.length, '5px');
|
||||
return new Array(jobHistories.length).fill('5px');
|
||||
} else {
|
||||
return chartHeights;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
|
||||
import { attachButtonStyler } from 'sql/platform/theme/common/styler';
|
||||
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||
import { find, fill } from 'vs/base/common/arrays';
|
||||
import { find } from 'vs/base/common/arrays';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { IColorTheme } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
@@ -731,7 +731,7 @@ export class NotebooksViewComponent extends JobManagementView implements OnInit,
|
||||
// if the durations are all 0 secs, show minimal chart
|
||||
// instead of nothing
|
||||
if (zeroDurationJobCount === jobHistories.length) {
|
||||
return fill(jobHistories.length, '5px');
|
||||
return new Array(jobHistories.length).fill('5px');
|
||||
} else {
|
||||
return chartHeights;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: "codicon";
|
||||
src: url("./codicon.ttf?70edf2c63384951357ed8517386759dd") format("truetype");
|
||||
src: url("./codicon.ttf?a76e99e42eab7c1a55601640b708d820") format("truetype");
|
||||
}
|
||||
|
||||
.codicon[class*='codicon-'] {
|
||||
@@ -416,11 +416,12 @@
|
||||
.codicon-feedback:before { content: "\eb96" }
|
||||
.codicon-group-by-ref-type:before { content: "\eb97" }
|
||||
.codicon-ungroup-by-ref-type:before { content: "\eb98" }
|
||||
.codicon-account:before { content: "\f101" }
|
||||
.codicon-bell-dot:before { content: "\f102" }
|
||||
.codicon-debug-alt-2:before { content: "\f103" }
|
||||
.codicon-debug-alt:before { content: "\f104" }
|
||||
.codicon-debug-console:before { content: "\f105" }
|
||||
.codicon-library:before { content: "\f106" }
|
||||
.codicon-output:before { content: "\f107" }
|
||||
.codicon-run-all:before { content: "\f108" }
|
||||
.codicon-account:before { content: "\eb99" }
|
||||
.codicon-bell-dot:before { content: "\eb9a" }
|
||||
.codicon-debug-console:before { content: "\eb9b" }
|
||||
.codicon-library:before { content: "\eb9c" }
|
||||
.codicon-output:before { content: "\eb9d" }
|
||||
.codicon-run-all:before { content: "\eb9e" }
|
||||
.codicon-sync-ignored:before { content: "\eb9f" }
|
||||
.codicon-debug-alt-2:before { content: "\f101" }
|
||||
.codicon-debug-alt:before { content: "\f102" }
|
||||
|
||||
Binary file not shown.
@@ -105,7 +105,7 @@ export abstract class Pane extends Disposable implements IView {
|
||||
get minimumSize(): number {
|
||||
const headerSize = this.headerSize;
|
||||
const expanded = !this.headerVisible || this.isExpanded();
|
||||
const minimumBodySize = expanded ? this._minimumBodySize : this._orientation === Orientation.HORIZONTAL ? 50 : 0;
|
||||
const minimumBodySize = expanded ? this.minimumBodySize : this._orientation === Orientation.HORIZONTAL ? 50 : 0;
|
||||
|
||||
return headerSize + minimumBodySize;
|
||||
}
|
||||
@@ -113,7 +113,7 @@ export abstract class Pane extends Disposable implements IView {
|
||||
get maximumSize(): number {
|
||||
const headerSize = this.headerSize;
|
||||
const expanded = !this.headerVisible || this.isExpanded();
|
||||
const maximumBodySize = expanded ? this._maximumBodySize : this._orientation === Orientation.HORIZONTAL ? 50 : 0;
|
||||
const maximumBodySize = expanded ? this.maximumBodySize : this._orientation === Orientation.HORIZONTAL ? 50 : 0;
|
||||
|
||||
return headerSize + maximumBodySize;
|
||||
}
|
||||
|
||||
@@ -472,17 +472,6 @@ export function range(arg: number, to?: number): number[] {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated ES6: use `Array.fill`
|
||||
*/
|
||||
export function fill<T>(num: number, value: T, arr: T[] = []): T[] {
|
||||
for (let i = 0; i < num; i++) {
|
||||
arr[i] = value;
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
export function index<T>(array: ReadonlyArray<T>, indexer: (t: T) => string): { [key: string]: T; };
|
||||
export function index<T, R>(array: ReadonlyArray<T>, indexer: (t: T) => string, merger?: (t: T, r: R) => R): { [key: string]: R; };
|
||||
export function index<T, R>(array: ReadonlyArray<T>, indexer: (t: T) => string, merger: (t: T, r: R) => R = t => t as any): { [key: string]: R; } {
|
||||
|
||||
@@ -94,8 +94,14 @@ export class VSBuffer {
|
||||
return new VSBuffer(this.buffer.subarray(start!/*bad lib.d.ts*/, end));
|
||||
}
|
||||
|
||||
set(array: VSBuffer, offset?: number): void {
|
||||
this.buffer.set(array.buffer, offset);
|
||||
set(array: VSBuffer, offset?: number): void;
|
||||
set(array: Uint8Array, offset?: number): void;
|
||||
set(array: VSBuffer | Uint8Array, offset?: number): void {
|
||||
if (array instanceof VSBuffer) {
|
||||
this.buffer.set(array.buffer, offset);
|
||||
} else {
|
||||
this.buffer.set(array, offset);
|
||||
}
|
||||
}
|
||||
|
||||
readUInt32BE(offset: number): number {
|
||||
@@ -106,6 +112,14 @@ export class VSBuffer {
|
||||
writeUInt32BE(this.buffer, value, offset);
|
||||
}
|
||||
|
||||
readUInt32LE(offset: number): number {
|
||||
return readUInt32LE(this.buffer, offset);
|
||||
}
|
||||
|
||||
writeUInt32LE(value: number, offset: number): void {
|
||||
writeUInt32LE(this.buffer, value, offset);
|
||||
}
|
||||
|
||||
readUInt8(offset: number): number {
|
||||
return readUInt8(this.buffer, offset);
|
||||
}
|
||||
@@ -117,15 +131,15 @@ export class VSBuffer {
|
||||
|
||||
export function readUInt16LE(source: Uint8Array, offset: number): number {
|
||||
return (
|
||||
source[offset]
|
||||
+ source[offset + 1] * 2 ** 8
|
||||
((source[offset + 0] << 0) >>> 0) |
|
||||
((source[offset + 1] << 8) >>> 0)
|
||||
);
|
||||
}
|
||||
|
||||
export function writeUInt16LE(destination: Uint8Array, value: number, offset: number): void {
|
||||
destination[offset] = value;
|
||||
destination[offset + 0] = (value & 0b11111111);
|
||||
value = value >>> 8;
|
||||
destination[offset + 1] = value;
|
||||
destination[offset + 1] = (value & 0b11111111);
|
||||
}
|
||||
|
||||
export function readUInt32BE(source: Uint8Array, offset: number): number {
|
||||
@@ -147,6 +161,25 @@ export function writeUInt32BE(destination: Uint8Array, value: number, offset: nu
|
||||
destination[offset] = value;
|
||||
}
|
||||
|
||||
export function readUInt32LE(source: Uint8Array, offset: number): number {
|
||||
return (
|
||||
((source[offset + 0] << 0) >>> 0) |
|
||||
((source[offset + 1] << 8) >>> 0) |
|
||||
((source[offset + 2] << 16) >>> 0) |
|
||||
((source[offset + 3] << 24) >>> 0)
|
||||
);
|
||||
}
|
||||
|
||||
export function writeUInt32LE(destination: Uint8Array, value: number, offset: number): void {
|
||||
destination[offset + 0] = (value & 0b11111111);
|
||||
value = value >>> 8;
|
||||
destination[offset + 1] = (value & 0b11111111);
|
||||
value = value >>> 8;
|
||||
destination[offset + 2] = (value & 0b11111111);
|
||||
value = value >>> 8;
|
||||
destination[offset + 3] = (value & 0b11111111);
|
||||
}
|
||||
|
||||
export function readUInt8(source: Uint8Array, offset: number): number {
|
||||
return source[offset];
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { sep } from 'vs/base/common/path';
|
||||
import { IdleValue } from 'vs/base/common/async';
|
||||
|
||||
@@ -133,8 +132,8 @@ export function compareAnything(one: string, other: string, lookFor: string): nu
|
||||
}
|
||||
|
||||
// Sort suffix matches over non suffix matches
|
||||
const elementASuffixMatch = strings.endsWith(elementAName, lookFor);
|
||||
const elementBSuffixMatch = strings.endsWith(elementBName, lookFor);
|
||||
const elementASuffixMatch = elementAName.endsWith(lookFor);
|
||||
const elementBSuffixMatch = elementBName.endsWith(lookFor);
|
||||
if (elementASuffixMatch !== elementBSuffixMatch) {
|
||||
return elementASuffixMatch ? -1 : 1;
|
||||
}
|
||||
@@ -154,8 +153,8 @@ export function compareByPrefix(one: string, other: string, lookFor: string): nu
|
||||
const elementBName = other.toLowerCase();
|
||||
|
||||
// Sort prefix matches over non prefix matches
|
||||
const elementAPrefixMatch = strings.startsWith(elementAName, lookFor);
|
||||
const elementBPrefixMatch = strings.startsWith(elementBName, lookFor);
|
||||
const elementAPrefixMatch = elementAName.startsWith(lookFor);
|
||||
const elementBPrefixMatch = elementBName.startsWith(lookFor);
|
||||
if (elementAPrefixMatch !== elementBPrefixMatch) {
|
||||
return elementAPrefixMatch ? -1 : 1;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
import { startsWithIgnoreCase, equalsIgnoreCase, endsWith, rtrim } from 'vs/base/common/strings';
|
||||
import { startsWithIgnoreCase, equalsIgnoreCase, rtrim } from 'vs/base/common/strings';
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
import { sep, posix, isAbsolute, join, normalize } from 'vs/base/common/path';
|
||||
|
||||
@@ -235,7 +235,7 @@ export function isWindowsDriveLetter(char0: number): boolean {
|
||||
export function sanitizeFilePath(candidate: string, cwd: string): string {
|
||||
|
||||
// Special case: allow to open a drive letter without trailing backslash
|
||||
if (isWindows && endsWith(candidate, ':')) {
|
||||
if (isWindows && candidate.endsWith(':')) {
|
||||
candidate += sep;
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ export function sanitizeFilePath(candidate: string, cwd: string): string {
|
||||
candidate = rtrim(candidate, sep);
|
||||
|
||||
// Special case: allow to open drive root ('C:\')
|
||||
if (endsWith(candidate, ':')) {
|
||||
if (candidate.endsWith(':')) {
|
||||
candidate += sep;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,15 +25,15 @@ export function score(target: string, query: IPreparedQuery, fuzzy: boolean): Sc
|
||||
return scoreMultiple(target, query.values, fuzzy);
|
||||
}
|
||||
|
||||
return scoreSingle(target, query.value, query.valueLowercase, fuzzy);
|
||||
return scoreSingle(target, query.normalized, query.normalizedLowercase, fuzzy);
|
||||
}
|
||||
|
||||
function scoreMultiple(target: string, query: IPreparedQueryPiece[], fuzzy: boolean): Score {
|
||||
let totalScore = NO_MATCH;
|
||||
const totalPositions: number[] = [];
|
||||
|
||||
for (const { value, valueLowercase } of query) {
|
||||
const [scoreValue, positions] = scoreSingle(target, value, valueLowercase, fuzzy);
|
||||
for (const { normalized, normalizedLowercase } of query) {
|
||||
const [scoreValue, positions] = scoreSingle(target, normalized, normalizedLowercase, fuzzy);
|
||||
if (scoreValue === NO_MATCH) {
|
||||
// if a single query value does not match, return with
|
||||
// no score entirely, we require all queries to match
|
||||
@@ -338,11 +338,26 @@ const LABEL_CAMELCASE_SCORE = 1 << 16;
|
||||
const LABEL_SCORE_THRESHOLD = 1 << 15;
|
||||
|
||||
export interface IPreparedQueryPiece {
|
||||
|
||||
/**
|
||||
* The original query as provided as input.
|
||||
*/
|
||||
original: string;
|
||||
originalLowercase: string;
|
||||
|
||||
value: string;
|
||||
valueLowercase: string;
|
||||
/**
|
||||
* Original normalized to platform separators:
|
||||
* - Windows: \
|
||||
* - Posix: /
|
||||
*/
|
||||
pathNormalized: string;
|
||||
|
||||
/**
|
||||
* In addition to the normalized path, will have
|
||||
* whitespace and wildcards removed.
|
||||
*/
|
||||
normalized: string;
|
||||
normalizedLowercase: string;
|
||||
}
|
||||
|
||||
export interface IPreparedQuery extends IPreparedQueryPiece {
|
||||
@@ -364,17 +379,21 @@ export function prepareQuery(original: string): IPreparedQuery {
|
||||
}
|
||||
|
||||
const originalLowercase = original.toLowerCase();
|
||||
const value = prepareQueryValue(original);
|
||||
const valueLowercase = value.toLowerCase();
|
||||
const containsPathSeparator = value.indexOf(sep) >= 0;
|
||||
const { pathNormalized, normalized, normalizedLowercase } = normalizeQuery(original);
|
||||
const containsPathSeparator = pathNormalized.indexOf(sep) >= 0;
|
||||
|
||||
let values: IPreparedQueryPiece[] | undefined = undefined;
|
||||
|
||||
const originalSplit = original.split(MULTIPL_QUERY_VALUES_SEPARATOR);
|
||||
if (originalSplit.length > 1) {
|
||||
for (const originalPiece of originalSplit) {
|
||||
const valuePiece = prepareQueryValue(originalPiece);
|
||||
if (valuePiece) {
|
||||
const {
|
||||
pathNormalized: pathNormalizedPiece,
|
||||
normalized: normalizedPiece,
|
||||
normalizedLowercase: normalizedLowercasePiece
|
||||
} = normalizeQuery(originalPiece);
|
||||
|
||||
if (normalizedPiece) {
|
||||
if (!values) {
|
||||
values = [];
|
||||
}
|
||||
@@ -382,29 +401,36 @@ export function prepareQuery(original: string): IPreparedQuery {
|
||||
values.push({
|
||||
original: originalPiece,
|
||||
originalLowercase: originalPiece.toLowerCase(),
|
||||
value: valuePiece,
|
||||
valueLowercase: valuePiece.toLowerCase()
|
||||
pathNormalized: pathNormalizedPiece,
|
||||
normalized: normalizedPiece,
|
||||
normalizedLowercase: normalizedLowercasePiece
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { original, originalLowercase, value, valueLowercase, values, containsPathSeparator };
|
||||
return { original, originalLowercase, pathNormalized, normalized, normalizedLowercase, values, containsPathSeparator };
|
||||
}
|
||||
|
||||
function prepareQueryValue(original: string): string {
|
||||
let value = stripWildcards(original).replace(/\s/g, ''); // get rid of all wildcards and whitespace
|
||||
function normalizeQuery(original: string): { pathNormalized: string, normalized: string, normalizedLowercase: string } {
|
||||
let pathNormalized: string;
|
||||
if (isWindows) {
|
||||
value = value.replace(/\//g, sep); // Help Windows users to search for paths when using slash
|
||||
pathNormalized = original.replace(/\//g, sep); // Help Windows users to search for paths when using slash
|
||||
} else {
|
||||
value = value.replace(/\\/g, sep); // Help macOS/Linux users to search for paths when using backslash
|
||||
pathNormalized = original.replace(/\\/g, sep); // Help macOS/Linux users to search for paths when using backslash
|
||||
}
|
||||
|
||||
return value;
|
||||
const normalized = stripWildcards(pathNormalized).replace(/\s/g, '');
|
||||
|
||||
return {
|
||||
pathNormalized,
|
||||
normalized,
|
||||
normalizedLowercase: normalized.toLowerCase()
|
||||
};
|
||||
}
|
||||
|
||||
export function scoreItem<T>(item: T, query: IPreparedQuery, fuzzy: boolean, accessor: IItemAccessor<T>, cache: ScorerCache): IItemScore {
|
||||
if (!item || !query.value) {
|
||||
if (!item || !query.normalized) {
|
||||
return NO_ITEM_SCORE; // we need an item and query to score on at least
|
||||
}
|
||||
|
||||
@@ -417,9 +443,9 @@ export function scoreItem<T>(item: T, query: IPreparedQuery, fuzzy: boolean, acc
|
||||
|
||||
let cacheHash: string;
|
||||
if (description) {
|
||||
cacheHash = `${label}${description}${query.value}${fuzzy}`;
|
||||
cacheHash = `${label}${description}${query.normalized}${fuzzy}`;
|
||||
} else {
|
||||
cacheHash = `${label}${query.value}${fuzzy}`;
|
||||
cacheHash = `${label}${query.normalized}${fuzzy}`;
|
||||
}
|
||||
|
||||
const cached = cache[cacheHash];
|
||||
@@ -455,7 +481,7 @@ function createMatches(offsets: undefined | number[]): IMatch[] {
|
||||
function doScoreItem(label: string, description: string | undefined, path: string | undefined, query: IPreparedQuery, fuzzy: boolean): IItemScore {
|
||||
|
||||
// 1.) treat identity matches on full path highest
|
||||
if (path && (isLinux ? query.original === path : equalsIgnoreCase(query.original, path))) {
|
||||
if (path && (isLinux ? query.pathNormalized === path : equalsIgnoreCase(query.pathNormalized, path))) {
|
||||
return { score: PATH_IDENTITY_SCORE, labelMatch: [{ start: 0, end: label.length }], descriptionMatch: description ? [{ start: 0, end: description.length }] : undefined };
|
||||
}
|
||||
|
||||
@@ -464,13 +490,13 @@ function doScoreItem(label: string, description: string | undefined, path: strin
|
||||
if (preferLabelMatches) {
|
||||
|
||||
// 2.) treat prefix matches on the label second highest
|
||||
const prefixLabelMatch = matchesPrefix(query.value, label);
|
||||
const prefixLabelMatch = matchesPrefix(query.normalized, label);
|
||||
if (prefixLabelMatch) {
|
||||
return { score: LABEL_PREFIX_SCORE, labelMatch: prefixLabelMatch };
|
||||
}
|
||||
|
||||
// 3.) treat camelcase matches on the label third highest
|
||||
const camelcaseLabelMatch = matchesCamelCase(query.value, label);
|
||||
const camelcaseLabelMatch = matchesCamelCase(query.normalized, label);
|
||||
if (camelcaseLabelMatch) {
|
||||
return { score: LABEL_CAMELCASE_SCORE, labelMatch: camelcaseLabelMatch };
|
||||
}
|
||||
@@ -702,17 +728,17 @@ function fallbackCompare<T>(itemA: T, itemB: T, query: IPreparedQuery, accessor:
|
||||
|
||||
// compare by label
|
||||
if (labelA !== labelB) {
|
||||
return compareAnything(labelA, labelB, query.value);
|
||||
return compareAnything(labelA, labelB, query.normalized);
|
||||
}
|
||||
|
||||
// compare by description
|
||||
if (descriptionA && descriptionB && descriptionA !== descriptionB) {
|
||||
return compareAnything(descriptionA, descriptionB, query.value);
|
||||
return compareAnything(descriptionA, descriptionB, query.normalized);
|
||||
}
|
||||
|
||||
// compare by path
|
||||
if (pathA && pathB && pathA !== pathB) {
|
||||
return compareAnything(pathA, pathB, query.value);
|
||||
return compareAnything(pathA, pathB, query.normalized);
|
||||
}
|
||||
|
||||
// equal
|
||||
|
||||
@@ -302,7 +302,7 @@ function parsePattern(arg1: string | IRelativePattern, options: IGlobOptions): P
|
||||
if (T1.test(pattern)) { // common pattern: **/*.txt just need endsWith check
|
||||
const base = pattern.substr(4); // '**/*'.length === 4
|
||||
parsedPattern = function (path, basename) {
|
||||
return typeof path === 'string' && strings.endsWith(path, base) ? pattern : null;
|
||||
return typeof path === 'string' && path.endsWith(base) ? pattern : null;
|
||||
};
|
||||
} else if (match = T2.exec(trimForExclusions(pattern, options))) { // common pattern: **/some.txt just need basename check
|
||||
parsedPattern = trivia2(match[1], pattern);
|
||||
@@ -339,7 +339,7 @@ function wrapRelativePattern(parsedPattern: ParsedStringPattern, arg2: string |
|
||||
}
|
||||
|
||||
function trimForExclusions(pattern: string, options: IGlobOptions): string {
|
||||
return options.trimForExclusions && strings.endsWith(pattern, '/**') ? pattern.substr(0, pattern.length - 2) : pattern; // dropping **, tailing / is dropped later
|
||||
return options.trimForExclusions && pattern.endsWith('/**') ? pattern.substr(0, pattern.length - 2) : pattern; // dropping **, tailing / is dropped later
|
||||
}
|
||||
|
||||
// common pattern: **/some.txt just need basename check
|
||||
@@ -353,7 +353,7 @@ function trivia2(base: string, originalPattern: string): ParsedStringPattern {
|
||||
if (basename) {
|
||||
return basename === base ? originalPattern : null;
|
||||
}
|
||||
return path === base || strings.endsWith(path, slashBase) || strings.endsWith(path, backslashBase) ? originalPattern : null;
|
||||
return path === base || path.endsWith(slashBase) || path.endsWith(backslashBase) ? originalPattern : null;
|
||||
};
|
||||
const basenames = [base];
|
||||
parsedPattern.basenames = basenames;
|
||||
@@ -398,7 +398,7 @@ function trivia4and5(path: string, pattern: string, matchPathEnds: boolean): Par
|
||||
const nativePath = paths.sep !== paths.posix.sep ? path.replace(ALL_FORWARD_SLASHES, paths.sep) : path;
|
||||
const nativePathEnd = paths.sep + nativePath;
|
||||
const parsedPattern: ParsedStringPattern = matchPathEnds ? function (path, basename) {
|
||||
return typeof path === 'string' && (path === nativePath || strings.endsWith(path, nativePathEnd)) ? pattern : null;
|
||||
return typeof path === 'string' && (path === nativePath || path.endsWith(nativePathEnd)) ? pattern : null;
|
||||
} : function (path, basename) {
|
||||
return typeof path === 'string' && path === nativePath ? pattern : null;
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { posix, normalize, win32, sep } from 'vs/base/common/path';
|
||||
import { endsWith, startsWithIgnoreCase, rtrim, startsWith } from 'vs/base/common/strings';
|
||||
import { startsWithIgnoreCase, rtrim } from 'vs/base/common/strings';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { isLinux, isWindows, isMacintosh } from 'vs/base/common/platform';
|
||||
import { isEqual, basename, relativePath } from 'vs/base/common/resources';
|
||||
@@ -117,7 +117,7 @@ export function tildify(path: string, userHome: string): string {
|
||||
}
|
||||
|
||||
// Linux: case sensitive, macOS: case insensitive
|
||||
if (isLinux ? startsWith(path, normalizedUserHome) : startsWithIgnoreCase(path, normalizedUserHome)) {
|
||||
if (isLinux ? path.startsWith(normalizedUserHome) : startsWithIgnoreCase(path, normalizedUserHome)) {
|
||||
path = `~/${path.substr(normalizedUserHome.length)}`;
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ export function shorten(paths: string[], pathSeparator: string = sep): string[]
|
||||
// Adding separator as prefix for subpath, such that 'endsWith(src, trgt)' considers subpath as directory name instead of plain string.
|
||||
// prefix is not added when either subpath is root directory or path[otherPathIndex] does not have multiple directories.
|
||||
const subpathWithSep: string = (start > 0 && paths[otherPathIndex].indexOf(pathSeparator) > -1) ? pathSeparator + subpath : subpath;
|
||||
const isOtherPathEnding: boolean = endsWith(paths[otherPathIndex], subpathWithSep);
|
||||
const isOtherPathEnding: boolean = paths[otherPathIndex].endsWith(subpathWithSep);
|
||||
|
||||
match = !isSubpathEnding || isOtherPathEnding;
|
||||
}
|
||||
@@ -221,7 +221,7 @@ export function shorten(paths: string[], pathSeparator: string = sep): string[]
|
||||
let result = '';
|
||||
|
||||
// preserve disk drive or root prefix
|
||||
if (endsWith(segments[0], ':') || prefix !== '') {
|
||||
if (segments[0].endsWith(':') || prefix !== '') {
|
||||
if (start === 1) {
|
||||
// extend subpath to include disk drive prefix
|
||||
start = 0;
|
||||
|
||||
@@ -56,32 +56,6 @@ export function setToString<K>(set: Set<K>): string {
|
||||
return `Set(${set.size}) {${entries.join(', ')}}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated ES6: use `...Map.entries()`
|
||||
*/
|
||||
export function mapToSerializable(map: Map<string, string>): [string, string][] {
|
||||
const serializable: [string, string][] = [];
|
||||
|
||||
map.forEach((value, key) => {
|
||||
serializable.push([key, value]);
|
||||
});
|
||||
|
||||
return serializable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated ES6: use `new Map([[key1, value1],[key2, value2]])`
|
||||
*/
|
||||
export function serializableToMap(serializable: [string, string][]): Map<string, string> {
|
||||
const items = new Map<string, string>();
|
||||
|
||||
for (const [key, value] of serializable) {
|
||||
items.set(key, value);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export interface IKeyIterator {
|
||||
reset(key: string): this;
|
||||
next(): this;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { basename, posix, extname } from 'vs/base/common/path';
|
||||
import { endsWith, startsWithUTF8BOM, startsWith } from 'vs/base/common/strings';
|
||||
import { startsWithUTF8BOM } from 'vs/base/common/strings';
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
import { match } from 'vs/base/common/glob';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
@@ -185,7 +185,7 @@ function guessMimeTypeByPath(path: string, filename: string, associations: IText
|
||||
// Longest extension match
|
||||
if (association.extension) {
|
||||
if (!extensionMatch || association.extension.length > extensionMatch.extension!.length) {
|
||||
if (endsWith(filename, association.extensionLowercase!)) {
|
||||
if (filename.endsWith(association.extensionLowercase!)) {
|
||||
extensionMatch = association;
|
||||
}
|
||||
}
|
||||
@@ -259,11 +259,11 @@ export function suggestFilename(mode: string | undefined, prefix: string): strin
|
||||
.map(assoc => assoc.extension);
|
||||
|
||||
const extensionsWithDotFirst = coalesce(extensions)
|
||||
.filter(assoc => startsWith(assoc, '.'));
|
||||
.filter(assoc => assoc.startsWith('.'));
|
||||
|
||||
if (extensionsWithDotFirst.length > 0) {
|
||||
const candidateExtension = extensionsWithDotFirst[0];
|
||||
if (endsWith(prefix, candidateExtension)) {
|
||||
if (prefix.endsWith(candidateExtension)) {
|
||||
// do not add the prefix if it already exists
|
||||
// https://github.com/microsoft/vscode/issues/83603
|
||||
return prefix;
|
||||
|
||||
@@ -588,11 +588,11 @@ function _makeFsPath(uri: URI, keepDriveLetterCasing: boolean): string {
|
||||
&& (uri.path.charCodeAt(1) >= CharCode.A && uri.path.charCodeAt(1) <= CharCode.Z || uri.path.charCodeAt(1) >= CharCode.a && uri.path.charCodeAt(1) <= CharCode.z)
|
||||
&& uri.path.charCodeAt(2) === CharCode.Colon
|
||||
) {
|
||||
// windows drive letter: file:///c:/far/boo
|
||||
if (!keepDriveLetterCasing) {
|
||||
// windows drive letter: file:///c:/far/boo
|
||||
value = uri.path[1].toLowerCase() + uri.path.substr(2);
|
||||
} else {
|
||||
value = uri.path.substr(1, 2);
|
||||
value = uri.path.substr(1);
|
||||
}
|
||||
} else {
|
||||
// other path
|
||||
|
||||
@@ -9,7 +9,6 @@ import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { endsWith } from 'vs/base/common/strings';
|
||||
import { promisify } from 'util';
|
||||
import { isRootOrDriveLetter } from 'vs/base/common/extpath';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
@@ -492,7 +491,7 @@ export async function move(source: string, target: string): Promise<void> {
|
||||
//
|
||||
// 2.) The user tries to rename a file/folder that ends with a dot. This is not
|
||||
// really possible to move then, at least on UNC devices.
|
||||
if (source.toLowerCase() !== target.toLowerCase() && error.code === 'EXDEV' || endsWith(source, '.')) {
|
||||
if (source.toLowerCase() !== target.toLowerCase() && error.code === 'EXDEV' || source.endsWith('.')) {
|
||||
await copy(source, target);
|
||||
await rimraf(source, RimRafMode.MOVE);
|
||||
await updateMtime(target);
|
||||
|
||||
@@ -246,8 +246,8 @@
|
||||
|
||||
.quick-input-list .quick-input-list-entry-action-bar .action-label.codicon {
|
||||
margin: 0;
|
||||
width: 19px;
|
||||
height: 100%;
|
||||
padding: 0 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { timeout } from 'vs/base/common/async';
|
||||
import { mapToString, setToString } from 'vs/base/common/map';
|
||||
import { basename } from 'vs/base/common/path';
|
||||
import { copy, renameIgnoreError, unlink } from 'vs/base/node/pfs';
|
||||
import { fill } from 'vs/base/common/arrays';
|
||||
import { IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage';
|
||||
|
||||
interface IDatabaseConnection {
|
||||
@@ -97,7 +96,7 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
|
||||
});
|
||||
|
||||
keysValuesChunks.forEach(keysValuesChunk => {
|
||||
this.prepare(connection, `INSERT INTO ItemTable VALUES ${fill(keysValuesChunk.length / 2, '(?,?)').join(',')}`, stmt => stmt.run(keysValuesChunk), () => {
|
||||
this.prepare(connection, `INSERT INTO ItemTable VALUES ${new Array(keysValuesChunk.length / 2).fill('(?,?)').join(',')}`, stmt => stmt.run(keysValuesChunk), () => {
|
||||
const keys: string[] = [];
|
||||
let length = 0;
|
||||
toInsert.forEach((value, key) => {
|
||||
@@ -132,7 +131,7 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
|
||||
});
|
||||
|
||||
keysChunks.forEach(keysChunk => {
|
||||
this.prepare(connection, `DELETE FROM ItemTable WHERE key IN (${fill(keysChunk.length, '?').join(',')})`, stmt => stmt.run(keysChunk), () => {
|
||||
this.prepare(connection, `DELETE FROM ItemTable WHERE key IN (${new Array(keysChunk.length).fill('?').join(',')})`, stmt => stmt.run(keysChunk), () => {
|
||||
const keys: string[] = [];
|
||||
toDelete.forEach(key => {
|
||||
keys.push(key);
|
||||
|
||||
@@ -857,41 +857,58 @@ suite('Fuzzy Scorer', () => {
|
||||
});
|
||||
|
||||
test('prepareQuery', () => {
|
||||
assert.equal(scorer.prepareQuery(' f*a ').value, 'fa');
|
||||
assert.equal(scorer.prepareQuery(' f*a ').normalized, 'fa');
|
||||
assert.equal(scorer.prepareQuery('model Tester.ts').original, 'model Tester.ts');
|
||||
assert.equal(scorer.prepareQuery('model Tester.ts').originalLowercase, 'model Tester.ts'.toLowerCase());
|
||||
assert.equal(scorer.prepareQuery('model Tester.ts').value, 'modelTester.ts');
|
||||
assert.equal(scorer.prepareQuery('Model Tester.ts').valueLowercase, 'modeltester.ts');
|
||||
assert.equal(scorer.prepareQuery('model Tester.ts').normalized, 'modelTester.ts');
|
||||
assert.equal(scorer.prepareQuery('Model Tester.ts').normalizedLowercase, 'modeltester.ts');
|
||||
assert.equal(scorer.prepareQuery('ModelTester.ts').containsPathSeparator, false);
|
||||
assert.equal(scorer.prepareQuery('Model' + sep + 'Tester.ts').containsPathSeparator, true);
|
||||
|
||||
// with spaces
|
||||
let query = scorer.prepareQuery('He*llo World');
|
||||
assert.equal(query.original, 'He*llo World');
|
||||
assert.equal(query.value, 'HelloWorld');
|
||||
assert.equal(query.valueLowercase, 'HelloWorld'.toLowerCase());
|
||||
assert.equal(query.normalized, 'HelloWorld');
|
||||
assert.equal(query.normalizedLowercase, 'HelloWorld'.toLowerCase());
|
||||
assert.equal(query.values?.length, 2);
|
||||
assert.equal(query.values?.[0].original, 'He*llo');
|
||||
assert.equal(query.values?.[0].value, 'Hello');
|
||||
assert.equal(query.values?.[0].valueLowercase, 'Hello'.toLowerCase());
|
||||
assert.equal(query.values?.[0].normalized, 'Hello');
|
||||
assert.equal(query.values?.[0].normalizedLowercase, 'Hello'.toLowerCase());
|
||||
assert.equal(query.values?.[1].original, 'World');
|
||||
assert.equal(query.values?.[1].value, 'World');
|
||||
assert.equal(query.values?.[1].valueLowercase, 'World'.toLowerCase());
|
||||
assert.equal(query.values?.[1].normalized, 'World');
|
||||
assert.equal(query.values?.[1].normalizedLowercase, 'World'.toLowerCase());
|
||||
|
||||
// with spaces that are empty
|
||||
query = scorer.prepareQuery(' Hello World ');
|
||||
assert.equal(query.original, ' Hello World ');
|
||||
assert.equal(query.originalLowercase, ' Hello World '.toLowerCase());
|
||||
assert.equal(query.value, 'HelloWorld');
|
||||
assert.equal(query.valueLowercase, 'HelloWorld'.toLowerCase());
|
||||
assert.equal(query.normalized, 'HelloWorld');
|
||||
assert.equal(query.normalizedLowercase, 'HelloWorld'.toLowerCase());
|
||||
assert.equal(query.values?.length, 2);
|
||||
assert.equal(query.values?.[0].original, 'Hello');
|
||||
assert.equal(query.values?.[0].originalLowercase, 'Hello'.toLowerCase());
|
||||
assert.equal(query.values?.[0].value, 'Hello');
|
||||
assert.equal(query.values?.[0].valueLowercase, 'Hello'.toLowerCase());
|
||||
assert.equal(query.values?.[0].normalized, 'Hello');
|
||||
assert.equal(query.values?.[0].normalizedLowercase, 'Hello'.toLowerCase());
|
||||
assert.equal(query.values?.[1].original, 'World');
|
||||
assert.equal(query.values?.[1].originalLowercase, 'World'.toLowerCase());
|
||||
assert.equal(query.values?.[1].value, 'World');
|
||||
assert.equal(query.values?.[1].valueLowercase, 'World'.toLowerCase());
|
||||
assert.equal(query.values?.[1].normalized, 'World');
|
||||
assert.equal(query.values?.[1].normalizedLowercase, 'World'.toLowerCase());
|
||||
|
||||
// Path related
|
||||
if (isWindows) {
|
||||
assert.equal(scorer.prepareQuery('C:\\some\\path').pathNormalized, 'C:\\some\\path');
|
||||
assert.equal(scorer.prepareQuery('C:\\some\\path').normalized, 'C:\\some\\path');
|
||||
assert.equal(scorer.prepareQuery('C:\\some\\path').containsPathSeparator, true);
|
||||
assert.equal(scorer.prepareQuery('C:/some/path').pathNormalized, 'C:\\some\\path');
|
||||
assert.equal(scorer.prepareQuery('C:/some/path').normalized, 'C:\\some\\path');
|
||||
assert.equal(scorer.prepareQuery('C:/some/path').containsPathSeparator, true);
|
||||
} else {
|
||||
assert.equal(scorer.prepareQuery('/some/path').pathNormalized, '/some/path');
|
||||
assert.equal(scorer.prepareQuery('/some/path').normalized, '/some/path');
|
||||
assert.equal(scorer.prepareQuery('/some/path').containsPathSeparator, true);
|
||||
assert.equal(scorer.prepareQuery('\\some\\path').pathNormalized, '/some/path');
|
||||
assert.equal(scorer.prepareQuery('\\some\\path').normalized, '/some/path');
|
||||
assert.equal(scorer.prepareQuery('\\some\\path').containsPathSeparator, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ResourceMap, TernarySearchTree, PathIterator, StringIterator, LinkedMap, Touch, LRUCache, mapToSerializable, serializableToMap } from 'vs/base/common/map';
|
||||
import { ResourceMap, TernarySearchTree, PathIterator, StringIterator, LinkedMap, Touch, LRUCache } from 'vs/base/common/map';
|
||||
import * as assert from 'assert';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
@@ -629,17 +629,4 @@ suite('Map', () => {
|
||||
// assert.equal(map.get(windowsFile), 'true');
|
||||
// assert.equal(map.get(uncFile), 'true');
|
||||
// });
|
||||
|
||||
test('mapToSerializable / serializableToMap', function () {
|
||||
const map = new Map<string, string>();
|
||||
map.set('1', 'foo');
|
||||
map.set('2', null!);
|
||||
map.set('3', 'bar');
|
||||
|
||||
const map2 = serializableToMap(mapToSerializable(map));
|
||||
assert.equal(map2.size, map.size);
|
||||
assert.equal(map2.get('1'), map.get('1'));
|
||||
assert.equal(map2.get('2'), map.get('2'));
|
||||
assert.equal(map2.get('3'), map.get('3'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -563,5 +563,8 @@ suite('URI', () => {
|
||||
|
||||
assertJoined(('file://ser/foo/'), '../../bazz', 'file://ser/foo/bazz', false);
|
||||
assertJoined(('file://ser/foo'), '../../bazz', 'file://ser/foo/bazz', false);
|
||||
|
||||
//https://github.com/microsoft/vscode/issues/93831
|
||||
assertJoined('file:///c:/foo/bar', './other/foo.img', 'file:///c:/foo/bar/other/foo.img', false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -401,9 +401,9 @@ suite('Paths (Node Implementation)', () => {
|
||||
];
|
||||
resolveTests.forEach((test) => {
|
||||
const resolve = test[0];
|
||||
//@ts-ignore
|
||||
//@ts-expect-error
|
||||
test[1].forEach((test) => {
|
||||
//@ts-ignore
|
||||
//@ts-expect-error
|
||||
const actual = resolve.apply(null, test[0]);
|
||||
let actualAlt;
|
||||
const os = resolve === path.win32.resolve ? 'win32' : 'posix';
|
||||
@@ -579,9 +579,9 @@ suite('Paths (Node Implementation)', () => {
|
||||
];
|
||||
relativeTests.forEach((test) => {
|
||||
const relative = test[0];
|
||||
//@ts-ignore
|
||||
//@ts-expect-error
|
||||
test[1].forEach((test) => {
|
||||
//@ts-ignore
|
||||
//@ts-expect-error
|
||||
const actual = relative(test[0], test[1]);
|
||||
const expected = test[2];
|
||||
const os = relative === path.win32.relative ? 'win32' : 'posix';
|
||||
|
||||
@@ -61,7 +61,6 @@ import { Schemas } from 'vs/base/common/network';
|
||||
import { SnapUpdateService } from 'vs/platform/update/electron-main/updateService.snap';
|
||||
import { IStorageMainService, StorageMainService } from 'vs/platform/storage/node/storageMainService';
|
||||
import { GlobalStorageDatabaseChannel } from 'vs/platform/storage/node/storageIpc';
|
||||
import { startsWith } from 'vs/base/common/strings';
|
||||
import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainService';
|
||||
import { IBackupMainService } from 'vs/platform/backup/electron-main/backup';
|
||||
import { WorkspacesHistoryMainService, IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService';
|
||||
@@ -178,7 +177,7 @@ export class CodeApplication extends Disposable {
|
||||
const srcUri = URI.parse(source).fsPath.toLowerCase();
|
||||
const rootUri = URI.file(this.environmentService.appRoot).fsPath.toLowerCase();
|
||||
|
||||
return startsWith(srcUri, rootUri + sep);
|
||||
return srcUri.startsWith(rootUri + sep);
|
||||
};
|
||||
|
||||
// Ensure defaults
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import * as path from 'vs/base/common/path';
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Event as CommonEvent, Emitter } from 'vs/base/common/event';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage, Rectangle, Display, TouchBarSegmentedControl, NativeImage, BrowserWindowConstructorOptions, SegmentedControlSegment, nativeTheme } from 'electron';
|
||||
import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment';
|
||||
@@ -26,7 +26,6 @@ import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
|
||||
import * as perf from 'vs/base/common/performance';
|
||||
import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/common/extensionGalleryService';
|
||||
import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService';
|
||||
import { endsWith } from 'vs/base/common/strings';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs';
|
||||
@@ -69,13 +68,13 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
private static readonly MAX_URL_LENGTH = 2 * 1024 * 1024; // https://cs.chromium.org/chromium/src/url/url_constants.cc?l=32
|
||||
|
||||
private readonly _onClose = this._register(new Emitter<void>());
|
||||
readonly onClose: CommonEvent<void> = this._onClose.event;
|
||||
readonly onClose = this._onClose.event;
|
||||
|
||||
private readonly _onDestroy = this._register(new Emitter<void>());
|
||||
readonly onDestroy: CommonEvent<void> = this._onDestroy.event;
|
||||
readonly onDestroy = this._onDestroy.event;
|
||||
|
||||
private readonly _onLoad = this._register(new Emitter<void>());
|
||||
readonly onLoad: CommonEvent<void> = this._onLoad.event;
|
||||
readonly onLoad = this._onLoad.event;
|
||||
|
||||
private hiddenTitleBarStyle: boolean | undefined;
|
||||
private showTimeoutHandle: NodeJS.Timeout | undefined;
|
||||
@@ -83,7 +82,9 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
private _readyState: ReadyState;
|
||||
private windowState: IWindowState;
|
||||
private currentMenuBarVisibility: MenuBarVisibility | undefined;
|
||||
|
||||
private representedFilename: string | undefined;
|
||||
private documentEdited: boolean | undefined;
|
||||
|
||||
private readonly whenReadyCallbacks: { (window: ICodeWindow): void }[];
|
||||
|
||||
@@ -271,6 +272,22 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
return this.representedFilename;
|
||||
}
|
||||
|
||||
setDocumentEdited(edited: boolean): void {
|
||||
if (isMacintosh) {
|
||||
this._win.setDocumentEdited(edited);
|
||||
}
|
||||
|
||||
this.documentEdited = edited;
|
||||
}
|
||||
|
||||
isDocumentEdited(): boolean {
|
||||
if (isMacintosh) {
|
||||
return this._win.isDocumentEdited();
|
||||
}
|
||||
|
||||
return !!this.documentEdited;
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
if (!this._win) {
|
||||
return;
|
||||
@@ -349,7 +366,7 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
this._win.webContents.session.webRequest.onBeforeRequest(null!, (details, callback) => {
|
||||
if (details.url.indexOf('.svg') > 0) {
|
||||
const uri = URI.parse(details.url);
|
||||
if (uri && !uri.scheme.match(/file/i) && endsWith(uri.path, '.svg')) {
|
||||
if (uri && !uri.scheme.match(/file/i) && uri.path.endsWith('.svg')) {
|
||||
return callback({ cancel: true });
|
||||
}
|
||||
}
|
||||
@@ -586,9 +603,9 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
}
|
||||
|
||||
// Clear Document Edited if needed
|
||||
if (isMacintosh && this._win.isDocumentEdited()) {
|
||||
if (this.isDocumentEdited()) {
|
||||
if (!isReload || !this.backupMainService.isHotExitEnabled()) {
|
||||
this._win.setDocumentEdited(false);
|
||||
this.setDocumentEdited(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ var CSSBuildLoaderPlugin;
|
||||
BrowserCSSLoader.prototype._insertLinkNode = function (linkNode) {
|
||||
this._pendingLoads++;
|
||||
var head = document.head || document.getElementsByTagName('head')[0];
|
||||
var other = head.getElementsByTagName('link') || document.head.getElementsByTagName('script');
|
||||
var other = head.getElementsByTagName('link') || head.getElementsByTagName('script');
|
||||
if (other.length > 0) {
|
||||
head.insertBefore(linkNode, other[other.length - 1]);
|
||||
}
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ var CSSLoaderPlugin;
|
||||
BrowserCSSLoader.prototype._insertLinkNode = function (linkNode) {
|
||||
this._pendingLoads++;
|
||||
var head = document.head || document.getElementsByTagName('head')[0];
|
||||
var other = head.getElementsByTagName('link') || document.head.getElementsByTagName('script');
|
||||
var other = head.getElementsByTagName('link') || head.getElementsByTagName('script');
|
||||
if (other.length > 0) {
|
||||
head.insertBefore(linkNode, other[other.length - 1]);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,34 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IDimension } from 'vs/editor/common/editorCommon';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
|
||||
interface ResizeObserver {
|
||||
observe(target: Element): void;
|
||||
unobserve(target: Element): void;
|
||||
disconnect(): void;
|
||||
}
|
||||
|
||||
interface ResizeObserverSize {
|
||||
inlineSize: number;
|
||||
blockSize: number;
|
||||
}
|
||||
|
||||
interface ResizeObserverEntry {
|
||||
readonly target: Element;
|
||||
readonly contentRect: DOMRectReadOnly;
|
||||
readonly borderBoxSize: ResizeObserverSize;
|
||||
readonly contentBoxSize: ResizeObserverSize;
|
||||
}
|
||||
|
||||
type ResizeObserverCallback = (entries: ReadonlyArray<ResizeObserverEntry>, observer: ResizeObserver) => void;
|
||||
|
||||
declare const ResizeObserver: {
|
||||
prototype: ResizeObserver;
|
||||
new(callback: ResizeObserverCallback): ResizeObserver;
|
||||
};
|
||||
|
||||
|
||||
export class ElementSizeObserver extends Disposable {
|
||||
|
||||
@@ -13,8 +38,8 @@ export class ElementSizeObserver extends Disposable {
|
||||
private readonly changeCallback: () => void;
|
||||
private width: number;
|
||||
private height: number;
|
||||
private mutationObserver: MutationObserver | null;
|
||||
private windowSizeListener: IDisposable | null;
|
||||
private resizeObserver: ResizeObserver | null;
|
||||
private measureReferenceDomElementToken: number;
|
||||
|
||||
constructor(referenceDomElement: HTMLElement | null, dimension: IDimension | undefined, changeCallback: () => void) {
|
||||
super();
|
||||
@@ -22,8 +47,8 @@ export class ElementSizeObserver extends Disposable {
|
||||
this.changeCallback = changeCallback;
|
||||
this.width = -1;
|
||||
this.height = -1;
|
||||
this.mutationObserver = null;
|
||||
this.windowSizeListener = null;
|
||||
this.resizeObserver = null;
|
||||
this.measureReferenceDomElementToken = -1;
|
||||
this.measureReferenceDomElement(false, dimension);
|
||||
}
|
||||
|
||||
@@ -41,25 +66,33 @@ export class ElementSizeObserver extends Disposable {
|
||||
}
|
||||
|
||||
public startObserving(): void {
|
||||
if (!this.mutationObserver && this.referenceDomElement) {
|
||||
this.mutationObserver = new MutationObserver(() => this._onDidMutate());
|
||||
this.mutationObserver.observe(this.referenceDomElement, {
|
||||
attributes: true,
|
||||
});
|
||||
}
|
||||
if (!this.windowSizeListener) {
|
||||
this.windowSizeListener = dom.addDisposableListener(window, 'resize', () => this._onDidResizeWindow());
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
if (!this.resizeObserver && this.referenceDomElement) {
|
||||
this.resizeObserver = new ResizeObserver((entries) => {
|
||||
if (entries && entries[0] && entries[0].contentRect) {
|
||||
this.observe({ width: entries[0].contentRect.width, height: entries[0].contentRect.height });
|
||||
} else {
|
||||
this.observe();
|
||||
}
|
||||
});
|
||||
this.resizeObserver.observe(this.referenceDomElement);
|
||||
}
|
||||
} else {
|
||||
if (this.measureReferenceDomElementToken === -1) {
|
||||
// setInterval type defaults to NodeJS.Timeout instead of number, so specify it as a number
|
||||
this.measureReferenceDomElementToken = <number><any>setInterval(() => this.observe(), 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public stopObserving(): void {
|
||||
if (this.mutationObserver) {
|
||||
this.mutationObserver.disconnect();
|
||||
this.mutationObserver = null;
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect();
|
||||
this.resizeObserver = null;
|
||||
}
|
||||
if (this.windowSizeListener) {
|
||||
this.windowSizeListener.dispose();
|
||||
this.windowSizeListener = null;
|
||||
if (this.measureReferenceDomElementToken !== -1) {
|
||||
clearInterval(this.measureReferenceDomElementToken);
|
||||
this.measureReferenceDomElementToken = -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,14 +100,6 @@ export class ElementSizeObserver extends Disposable {
|
||||
this.measureReferenceDomElement(true, dimension);
|
||||
}
|
||||
|
||||
private _onDidMutate(): void {
|
||||
this.measureReferenceDomElement(true);
|
||||
}
|
||||
|
||||
private _onDidResizeWindow(): void {
|
||||
this.measureReferenceDomElement(true);
|
||||
}
|
||||
|
||||
private measureReferenceDomElement(callChangeCallback: boolean, dimension?: IDimension): void {
|
||||
let observedWidth = 0;
|
||||
let observedHeight = 0;
|
||||
|
||||
@@ -2722,10 +2722,6 @@ export interface ISuggestOptions {
|
||||
* Overwrite word ends on accept. Default to false.
|
||||
*/
|
||||
insertMode?: 'insert' | 'replace';
|
||||
/**
|
||||
* Show a highlight when suggestion replaces or keep text after the cursor. Defaults to false.
|
||||
*/
|
||||
insertHighlight?: boolean;
|
||||
/**
|
||||
* Enable graceful matching. Defaults to true.
|
||||
*/
|
||||
@@ -2876,7 +2872,6 @@ class EditorSuggest extends BaseEditorOption<EditorOption.suggest, InternalSugge
|
||||
constructor() {
|
||||
const defaults: InternalSuggestOptions = {
|
||||
insertMode: 'insert',
|
||||
insertHighlight: true,
|
||||
filterGraceful: true,
|
||||
snippetsPreventQuickSuggestions: true,
|
||||
localityBonus: false,
|
||||
@@ -2927,11 +2922,6 @@ class EditorSuggest extends BaseEditorOption<EditorOption.suggest, InternalSugge
|
||||
default: defaults.insertMode,
|
||||
description: nls.localize('suggest.insertMode', "Controls whether words are overwritten when accepting completions. Note that this depends on extensions opting into this feature.")
|
||||
},
|
||||
'editor.suggest.insertHighlight': {
|
||||
type: 'boolean',
|
||||
default: defaults.insertHighlight,
|
||||
description: nls.localize('suggest.insertHighlight', "Controls whether unexpected text modifications while accepting completions should be highlighted, e.g `insertMode` is `replace` but the completion only supports `insert`.")
|
||||
},
|
||||
'editor.suggest.filterGraceful': {
|
||||
type: 'boolean',
|
||||
default: defaults.filterGraceful,
|
||||
@@ -3124,7 +3114,6 @@ class EditorSuggest extends BaseEditorOption<EditorOption.suggest, InternalSugge
|
||||
const input = _input as ISuggestOptions;
|
||||
return {
|
||||
insertMode: EditorStringEnumOption.stringSet(input.insertMode, this.defaultValue.insertMode, ['insert', 'replace']),
|
||||
insertHighlight: EditorBooleanOption.boolean(input.insertHighlight, this.defaultValue.insertHighlight),
|
||||
filterGraceful: EditorBooleanOption.boolean(input.filterGraceful, this.defaultValue.filterGraceful),
|
||||
snippetsPreventQuickSuggestions: EditorBooleanOption.boolean(input.snippetsPreventQuickSuggestions, this.defaultValue.filterGraceful),
|
||||
localityBonus: EditorBooleanOption.boolean(input.localityBonus, this.defaultValue.localityBonus),
|
||||
|
||||
@@ -238,7 +238,7 @@ export class WordOperations {
|
||||
const left = lineContent.charCodeAt(column - 2);
|
||||
const right = lineContent.charCodeAt(column - 1);
|
||||
|
||||
if (left !== CharCode.Underline && right === CharCode.Underline) {
|
||||
if (left === CharCode.Underline && right !== CharCode.Underline) {
|
||||
// snake_case_variables
|
||||
return new Position(lineNumber, column);
|
||||
}
|
||||
@@ -340,7 +340,7 @@ export class WordOperations {
|
||||
const left = lineContent.charCodeAt(column - 2);
|
||||
const right = lineContent.charCodeAt(column - 1);
|
||||
|
||||
if (left === CharCode.Underline && right !== CharCode.Underline) {
|
||||
if (left !== CharCode.Underline && right === CharCode.Underline) {
|
||||
// snake_case_variables
|
||||
return new Position(lineNumber, column);
|
||||
}
|
||||
|
||||
@@ -24,17 +24,18 @@ export interface IStringBuilder {
|
||||
}
|
||||
|
||||
let _platformTextDecoder: TextDecoder | null;
|
||||
function getPlatformTextDecoder(): TextDecoder {
|
||||
export function getPlatformTextDecoder(): TextDecoder {
|
||||
if (!_platformTextDecoder) {
|
||||
_platformTextDecoder = new TextDecoder(platform.isLittleEndian() ? 'UTF-16LE' : 'UTF-16BE');
|
||||
}
|
||||
return _platformTextDecoder;
|
||||
}
|
||||
|
||||
export const hasTextDecoder = (typeof TextDecoder !== 'undefined');
|
||||
export let createStringBuilder: (capacity: number) => IStringBuilder;
|
||||
export let decodeUTF16LE: (source: Uint8Array, offset: number, len: number) => string;
|
||||
|
||||
if (typeof TextDecoder !== 'undefined') {
|
||||
if (hasTextDecoder) {
|
||||
createStringBuilder = (capacity) => new StringBuilder(capacity);
|
||||
decodeUTF16LE = standardDecodeUTF16LE;
|
||||
} else {
|
||||
|
||||
@@ -827,7 +827,17 @@ export interface ITextModel {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
setSemanticTokens(tokens: MultilineTokens2[] | null): void;
|
||||
setSemanticTokens(tokens: MultilineTokens2[] | null, isComplete: boolean): void;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
setPartialSemanticTokens(range: Range, tokens: MultilineTokens2[] | null): void;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
hasSemanticTokens(): boolean;
|
||||
|
||||
/**
|
||||
* Flush all tokenization state.
|
||||
|
||||
@@ -10,10 +10,13 @@ import { EndOfLineSequence, ICursorStateComputer, IIdentifiedSingleEditOperation
|
||||
import { TextModel } from 'vs/editor/common/model/textModel';
|
||||
import { IUndoRedoService, IResourceUndoRedoElement, UndoRedoElementType, IWorkspaceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources';
|
||||
import { TextChange, compressConsecutiveTextChanges } from 'vs/editor/common/model/textChange';
|
||||
import * as buffer from 'vs/base/common/buffer';
|
||||
|
||||
function uriGetComparisonKey(resource: URI): string {
|
||||
return resource.toString();
|
||||
}
|
||||
|
||||
class SingleModelEditStackData {
|
||||
|
||||
public static create(model: ITextModel, beforeCursorState: Selection[] | null): SingleModelEditStackData {
|
||||
|
||||
@@ -1793,8 +1793,8 @@ export class TextModel extends Disposable implements model.ITextModel {
|
||||
}
|
||||
}
|
||||
|
||||
public setSemanticTokens(tokens: MultilineTokens2[] | null): void {
|
||||
this._tokens2.set(tokens);
|
||||
public setSemanticTokens(tokens: MultilineTokens2[] | null, isComplete: boolean): void {
|
||||
this._tokens2.set(tokens, isComplete);
|
||||
|
||||
this._emitModelTokensChangedEvent({
|
||||
tokenizationSupportChanged: false,
|
||||
@@ -1803,6 +1803,23 @@ export class TextModel extends Disposable implements model.ITextModel {
|
||||
});
|
||||
}
|
||||
|
||||
public hasSemanticTokens(): boolean {
|
||||
return this._tokens2.isComplete();
|
||||
}
|
||||
|
||||
public setPartialSemanticTokens(range: Range, tokens: MultilineTokens2[]): void {
|
||||
if (this.hasSemanticTokens()) {
|
||||
return;
|
||||
}
|
||||
const changedRange = this._tokens2.setPartial(range, tokens);
|
||||
|
||||
this._emitModelTokensChangedEvent({
|
||||
tokenizationSupportChanged: false,
|
||||
semanticTokensApplied: true,
|
||||
ranges: [{ fromLineNumber: changedRange.startLineNumber, toLineNumber: changedRange.endLineNumber }]
|
||||
});
|
||||
}
|
||||
|
||||
public tokenizeViewport(startLineNumber: number, endLineNumber: number): void {
|
||||
startLineNumber = Math.max(1, startLineNumber);
|
||||
endLineNumber = Math.min(this._buffer.getLineCount(), endLineNumber);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import * as arrays from 'vs/base/common/arrays';
|
||||
import { LineTokens } from 'vs/editor/common/core/lineTokens';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { IRange } from 'vs/editor/common/core/range';
|
||||
import { IRange, Range } from 'vs/editor/common/core/range';
|
||||
import { ColorId, FontStyle, LanguageId, MetadataConsts, StandardTokenType, TokenMetadata } from 'vs/editor/common/modes';
|
||||
import { writeUInt32BE, readUInt32BE } from 'vs/base/common/buffer';
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
@@ -124,20 +124,7 @@ export class MultilineTokensBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
export interface IEncodedTokens {
|
||||
getTokenCount(): number;
|
||||
getDeltaLine(tokenIndex: number): number;
|
||||
getMaxDeltaLine(): number;
|
||||
getStartCharacter(tokenIndex: number): number;
|
||||
getEndCharacter(tokenIndex: number): number;
|
||||
getMetadata(tokenIndex: number): number;
|
||||
|
||||
clear(): void;
|
||||
acceptDeleteRange(horizontalShiftForFirstLineTokens: number, startDeltaLine: number, startCharacter: number, endDeltaLine: number, endCharacter: number): void;
|
||||
acceptInsertText(deltaLine: number, character: number, eolCount: number, firstLineLength: number, lastLineLength: number, firstCharCode: number): void;
|
||||
}
|
||||
|
||||
export class SparseEncodedTokens implements IEncodedTokens {
|
||||
export class SparseEncodedTokens {
|
||||
/**
|
||||
* The encoding of tokens is:
|
||||
* 4*i deltaLine (from `startLineNumber`)
|
||||
@@ -145,7 +132,7 @@ export class SparseEncodedTokens implements IEncodedTokens {
|
||||
* 4*i+2 endCharacter (from the line start)
|
||||
* 4*i+3 metadata
|
||||
*/
|
||||
private _tokens: Uint32Array;
|
||||
private readonly _tokens: Uint32Array;
|
||||
private _tokenCount: number;
|
||||
|
||||
constructor(tokens: Uint32Array) {
|
||||
@@ -153,38 +140,167 @@ export class SparseEncodedTokens implements IEncodedTokens {
|
||||
this._tokenCount = tokens.length / 4;
|
||||
}
|
||||
|
||||
public toString(startLineNumber: number): string {
|
||||
let pieces: string[] = [];
|
||||
for (let i = 0; i < this._tokenCount; i++) {
|
||||
pieces.push(`(${this._getDeltaLine(i) + startLineNumber},${this._getStartCharacter(i)}-${this._getEndCharacter(i)})`);
|
||||
}
|
||||
return `[${pieces.join(',')}]`;
|
||||
}
|
||||
|
||||
public getMaxDeltaLine(): number {
|
||||
const tokenCount = this.getTokenCount();
|
||||
const tokenCount = this._getTokenCount();
|
||||
if (tokenCount === 0) {
|
||||
return -1;
|
||||
}
|
||||
return this.getDeltaLine(tokenCount - 1);
|
||||
return this._getDeltaLine(tokenCount - 1);
|
||||
}
|
||||
|
||||
public getTokenCount(): number {
|
||||
public getRange(): Range | null {
|
||||
const tokenCount = this._getTokenCount();
|
||||
if (tokenCount === 0) {
|
||||
return null;
|
||||
}
|
||||
const startChar = this._getStartCharacter(0);
|
||||
const maxDeltaLine = this._getDeltaLine(tokenCount - 1);
|
||||
const endChar = this._getEndCharacter(tokenCount - 1);
|
||||
return new Range(0, startChar + 1, maxDeltaLine, endChar + 1);
|
||||
}
|
||||
|
||||
private _getTokenCount(): number {
|
||||
return this._tokenCount;
|
||||
}
|
||||
|
||||
public getDeltaLine(tokenIndex: number): number {
|
||||
private _getDeltaLine(tokenIndex: number): number {
|
||||
return this._tokens[4 * tokenIndex];
|
||||
}
|
||||
|
||||
public getStartCharacter(tokenIndex: number): number {
|
||||
private _getStartCharacter(tokenIndex: number): number {
|
||||
return this._tokens[4 * tokenIndex + 1];
|
||||
}
|
||||
|
||||
public getEndCharacter(tokenIndex: number): number {
|
||||
private _getEndCharacter(tokenIndex: number): number {
|
||||
return this._tokens[4 * tokenIndex + 2];
|
||||
}
|
||||
|
||||
public getMetadata(tokenIndex: number): number {
|
||||
return this._tokens[4 * tokenIndex + 3];
|
||||
public isEmpty(): boolean {
|
||||
return (this._getTokenCount() === 0);
|
||||
}
|
||||
|
||||
public getLineTokens(deltaLine: number): LineTokens2 | null {
|
||||
let low = 0;
|
||||
let high = this._getTokenCount() - 1;
|
||||
|
||||
while (low < high) {
|
||||
const mid = low + Math.floor((high - low) / 2);
|
||||
const midDeltaLine = this._getDeltaLine(mid);
|
||||
|
||||
if (midDeltaLine < deltaLine) {
|
||||
low = mid + 1;
|
||||
} else if (midDeltaLine > deltaLine) {
|
||||
high = mid - 1;
|
||||
} else {
|
||||
let min = mid;
|
||||
while (min > low && this._getDeltaLine(min - 1) === deltaLine) {
|
||||
min--;
|
||||
}
|
||||
let max = mid;
|
||||
while (max < high && this._getDeltaLine(max + 1) === deltaLine) {
|
||||
max++;
|
||||
}
|
||||
return new LineTokens2(this._tokens.subarray(4 * min, 4 * max + 4));
|
||||
}
|
||||
}
|
||||
|
||||
if (this._getDeltaLine(low) === deltaLine) {
|
||||
return new LineTokens2(this._tokens.subarray(4 * low, 4 * low + 4));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._tokenCount = 0;
|
||||
}
|
||||
|
||||
public removeTokens(startDeltaLine: number, startChar: number, endDeltaLine: number, endChar: number): number {
|
||||
const tokens = this._tokens;
|
||||
const tokenCount = this._tokenCount;
|
||||
let newTokenCount = 0;
|
||||
let hasDeletedTokens = false;
|
||||
let firstDeltaLine = 0;
|
||||
for (let i = 0; i < tokenCount; i++) {
|
||||
const srcOffset = 4 * i;
|
||||
const tokenDeltaLine = tokens[srcOffset];
|
||||
const tokenStartCharacter = tokens[srcOffset + 1];
|
||||
const tokenEndCharacter = tokens[srcOffset + 2];
|
||||
const tokenMetadata = tokens[srcOffset + 3];
|
||||
|
||||
if (
|
||||
(tokenDeltaLine > startDeltaLine || (tokenDeltaLine === startDeltaLine && tokenEndCharacter >= startChar))
|
||||
&& (tokenDeltaLine < endDeltaLine || (tokenDeltaLine === endDeltaLine && tokenStartCharacter <= endChar))
|
||||
) {
|
||||
hasDeletedTokens = true;
|
||||
} else {
|
||||
if (newTokenCount === 0) {
|
||||
firstDeltaLine = tokenDeltaLine;
|
||||
}
|
||||
if (hasDeletedTokens) {
|
||||
// must move the token to the left
|
||||
const destOffset = 4 * newTokenCount;
|
||||
tokens[destOffset] = tokenDeltaLine - firstDeltaLine;
|
||||
tokens[destOffset + 1] = tokenStartCharacter;
|
||||
tokens[destOffset + 2] = tokenEndCharacter;
|
||||
tokens[destOffset + 3] = tokenMetadata;
|
||||
}
|
||||
newTokenCount++;
|
||||
}
|
||||
}
|
||||
|
||||
this._tokenCount = newTokenCount;
|
||||
|
||||
return firstDeltaLine;
|
||||
}
|
||||
|
||||
public split(startDeltaLine: number, startChar: number, endDeltaLine: number, endChar: number): [SparseEncodedTokens, SparseEncodedTokens, number] {
|
||||
const tokens = this._tokens;
|
||||
const tokenCount = this._tokenCount;
|
||||
let aTokens: number[] = [];
|
||||
let bTokens: number[] = [];
|
||||
let destTokens: number[] = aTokens;
|
||||
let destOffset = 0;
|
||||
let destFirstDeltaLine: number = 0;
|
||||
for (let i = 0; i < tokenCount; i++) {
|
||||
const srcOffset = 4 * i;
|
||||
const tokenDeltaLine = tokens[srcOffset];
|
||||
const tokenStartCharacter = tokens[srcOffset + 1];
|
||||
const tokenEndCharacter = tokens[srcOffset + 2];
|
||||
const tokenMetadata = tokens[srcOffset + 3];
|
||||
|
||||
if ((tokenDeltaLine > startDeltaLine || (tokenDeltaLine === startDeltaLine && tokenEndCharacter >= startChar))) {
|
||||
if ((tokenDeltaLine < endDeltaLine || (tokenDeltaLine === endDeltaLine && tokenStartCharacter <= endChar))) {
|
||||
// this token is touching the range
|
||||
continue;
|
||||
} else {
|
||||
// this token is after the range
|
||||
if (destTokens !== bTokens) {
|
||||
// this token is the first token after the range
|
||||
destTokens = bTokens;
|
||||
destOffset = 0;
|
||||
destFirstDeltaLine = tokenDeltaLine;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destTokens[destOffset++] = tokenDeltaLine - destFirstDeltaLine;
|
||||
destTokens[destOffset++] = tokenStartCharacter;
|
||||
destTokens[destOffset++] = tokenEndCharacter;
|
||||
destTokens[destOffset++] = tokenMetadata;
|
||||
}
|
||||
|
||||
return [new SparseEncodedTokens(new Uint32Array(aTokens)), new SparseEncodedTokens(new Uint32Array(bTokens)), destFirstDeltaLine];
|
||||
}
|
||||
|
||||
public acceptDeleteRange(horizontalShiftForFirstLineTokens: number, startDeltaLine: number, startCharacter: number, endDeltaLine: number, endCharacter: number): void {
|
||||
// This is a bit complex, here are the cases I used to think about this:
|
||||
//
|
||||
@@ -414,30 +530,26 @@ export class SparseEncodedTokens implements IEncodedTokens {
|
||||
|
||||
export class LineTokens2 {
|
||||
|
||||
private readonly _actual: IEncodedTokens;
|
||||
private readonly _startTokenIndex: number;
|
||||
private readonly _endTokenIndex: number;
|
||||
private readonly _tokens: Uint32Array;
|
||||
|
||||
constructor(actual: IEncodedTokens, startTokenIndex: number, endTokenIndex: number) {
|
||||
this._actual = actual;
|
||||
this._startTokenIndex = startTokenIndex;
|
||||
this._endTokenIndex = endTokenIndex;
|
||||
constructor(tokens: Uint32Array) {
|
||||
this._tokens = tokens;
|
||||
}
|
||||
|
||||
public getCount(): number {
|
||||
return this._endTokenIndex - this._startTokenIndex + 1;
|
||||
return this._tokens.length / 4;
|
||||
}
|
||||
|
||||
public getStartCharacter(tokenIndex: number): number {
|
||||
return this._actual.getStartCharacter(this._startTokenIndex + tokenIndex);
|
||||
return this._tokens[4 * tokenIndex + 1];
|
||||
}
|
||||
|
||||
public getEndCharacter(tokenIndex: number): number {
|
||||
return this._actual.getEndCharacter(this._startTokenIndex + tokenIndex);
|
||||
return this._tokens[4 * tokenIndex + 2];
|
||||
}
|
||||
|
||||
public getMetadata(tokenIndex: number): number {
|
||||
return this._actual.getMetadata(this._startTokenIndex + tokenIndex);
|
||||
return this._tokens[4 * tokenIndex + 3];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,59 +557,58 @@ export class MultilineTokens2 {
|
||||
|
||||
public startLineNumber: number;
|
||||
public endLineNumber: number;
|
||||
public tokens: IEncodedTokens;
|
||||
public tokens: SparseEncodedTokens;
|
||||
|
||||
constructor(startLineNumber: number, tokens: IEncodedTokens) {
|
||||
constructor(startLineNumber: number, tokens: SparseEncodedTokens) {
|
||||
this.startLineNumber = startLineNumber;
|
||||
this.tokens = tokens;
|
||||
this.endLineNumber = this.startLineNumber + this.tokens.getMaxDeltaLine();
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.tokens.toString(this.startLineNumber);
|
||||
}
|
||||
|
||||
private _updateEndLineNumber(): void {
|
||||
this.endLineNumber = this.startLineNumber + this.tokens.getMaxDeltaLine();
|
||||
}
|
||||
|
||||
public isEmpty(): boolean {
|
||||
return this.tokens.isEmpty();
|
||||
}
|
||||
|
||||
public getLineTokens(lineNumber: number): LineTokens2 | null {
|
||||
if (this.startLineNumber <= lineNumber && lineNumber <= this.endLineNumber) {
|
||||
const findResult = MultilineTokens2._findTokensWithLine(this.tokens, lineNumber - this.startLineNumber);
|
||||
if (findResult) {
|
||||
const [startTokenIndex, endTokenIndex] = findResult;
|
||||
return new LineTokens2(this.tokens, startTokenIndex, endTokenIndex);
|
||||
}
|
||||
return this.tokens.getLineTokens(lineNumber - this.startLineNumber);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static _findTokensWithLine(tokens: IEncodedTokens, deltaLine: number): [number, number] | null {
|
||||
let low = 0;
|
||||
let high = tokens.getTokenCount() - 1;
|
||||
|
||||
while (low < high) {
|
||||
const mid = low + Math.floor((high - low) / 2);
|
||||
const midDeltaLine = tokens.getDeltaLine(mid);
|
||||
|
||||
if (midDeltaLine < deltaLine) {
|
||||
low = mid + 1;
|
||||
} else if (midDeltaLine > deltaLine) {
|
||||
high = mid - 1;
|
||||
} else {
|
||||
let min = mid;
|
||||
while (min > low && tokens.getDeltaLine(min - 1) === deltaLine) {
|
||||
min--;
|
||||
}
|
||||
let max = mid;
|
||||
while (max < high && tokens.getDeltaLine(max + 1) === deltaLine) {
|
||||
max++;
|
||||
}
|
||||
return [min, max];
|
||||
}
|
||||
public getRange(): Range | null {
|
||||
const deltaRange = this.tokens.getRange();
|
||||
if (!deltaRange) {
|
||||
return deltaRange;
|
||||
}
|
||||
return new Range(this.startLineNumber + deltaRange.startLineNumber, deltaRange.startColumn, this.startLineNumber + deltaRange.endLineNumber, deltaRange.endColumn);
|
||||
}
|
||||
|
||||
if (tokens.getDeltaLine(low) === deltaLine) {
|
||||
return [low, low];
|
||||
}
|
||||
public removeTokens(range: Range): void {
|
||||
const startLineIndex = range.startLineNumber - this.startLineNumber;
|
||||
const endLineIndex = range.endLineNumber - this.startLineNumber;
|
||||
|
||||
return null;
|
||||
this.startLineNumber += this.tokens.removeTokens(startLineIndex, range.startColumn - 1, endLineIndex, range.endColumn - 1);
|
||||
this._updateEndLineNumber();
|
||||
}
|
||||
|
||||
public split(range: Range): [MultilineTokens2, MultilineTokens2] {
|
||||
// split tokens to two:
|
||||
// a) all the tokens before `range`
|
||||
// b) all the tokens after `range`
|
||||
const startLineIndex = range.startLineNumber - this.startLineNumber;
|
||||
const endLineIndex = range.endLineNumber - this.startLineNumber;
|
||||
|
||||
const [a, b, bDeltaLine] = this.tokens.split(startLineIndex, range.startColumn - 1, endLineIndex, range.endColumn - 1);
|
||||
return [new MultilineTokens2(this.startLineNumber, a), new MultilineTokens2(this.startLineNumber + bDeltaLine, b)];
|
||||
}
|
||||
|
||||
public applyEdit(range: IRange, text: string): void {
|
||||
@@ -761,17 +872,91 @@ function toUint32Array(arr: Uint32Array | ArrayBuffer): Uint32Array {
|
||||
export class TokensStore2 {
|
||||
|
||||
private _pieces: MultilineTokens2[];
|
||||
private _isComplete: boolean;
|
||||
|
||||
constructor() {
|
||||
this._pieces = [];
|
||||
this._isComplete = false;
|
||||
}
|
||||
|
||||
public flush(): void {
|
||||
this._pieces = [];
|
||||
this._isComplete = false;
|
||||
}
|
||||
|
||||
public set(pieces: MultilineTokens2[] | null) {
|
||||
public set(pieces: MultilineTokens2[] | null, isComplete: boolean): void {
|
||||
this._pieces = pieces || [];
|
||||
this._isComplete = isComplete;
|
||||
}
|
||||
|
||||
public setPartial(_range: Range, pieces: MultilineTokens2[]): Range {
|
||||
if (pieces.length === 0) {
|
||||
return _range;
|
||||
}
|
||||
const _firstRange = pieces[0].getRange();
|
||||
const _lastRange = pieces[pieces.length - 1].getRange();
|
||||
if (!_firstRange || !_lastRange) {
|
||||
return _range;
|
||||
}
|
||||
const range = _range.plusRange(_firstRange).plusRange(_lastRange);
|
||||
let insertPosition: { index: number; } | null = null;
|
||||
for (let i = 0, len = this._pieces.length; i < len; i++) {
|
||||
const piece = this._pieces[i];
|
||||
if (piece.endLineNumber < range.startLineNumber) {
|
||||
// this piece is before the range
|
||||
continue;
|
||||
}
|
||||
|
||||
if (piece.startLineNumber > range.endLineNumber) {
|
||||
// this piece is after the range, so mark the spot before this piece
|
||||
// as a good insertion position and stop looping
|
||||
insertPosition = insertPosition || { index: i };
|
||||
break;
|
||||
}
|
||||
|
||||
// this piece might intersect with the range
|
||||
piece.removeTokens(range);
|
||||
|
||||
if (piece.isEmpty()) {
|
||||
// remove the piece if it became empty
|
||||
this._pieces.splice(i, 1);
|
||||
i--;
|
||||
len--;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (piece.endLineNumber < range.startLineNumber) {
|
||||
// after removal, this piece is before the range
|
||||
continue;
|
||||
}
|
||||
|
||||
if (piece.startLineNumber > range.endLineNumber) {
|
||||
// after removal, this piece is after the range
|
||||
insertPosition = insertPosition || { index: i };
|
||||
continue;
|
||||
}
|
||||
|
||||
// after removal, this piece contains the range
|
||||
const [a, b] = piece.split(range);
|
||||
this._pieces.splice(i, 1, a, b);
|
||||
i++;
|
||||
len++;
|
||||
|
||||
insertPosition = insertPosition || { index: i };
|
||||
}
|
||||
|
||||
insertPosition = insertPosition || { index: this._pieces.length };
|
||||
|
||||
this._pieces = arrays.arrayInsert(this._pieces, insertPosition.index, pieces);
|
||||
|
||||
// console.log(`I HAVE ${this._pieces.length} pieces`);
|
||||
// console.log(`${this._pieces.map(p => p.toString()).join(', ')}`);
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
public isComplete(): boolean {
|
||||
return this._isComplete;
|
||||
}
|
||||
|
||||
public addSemanticTokens(lineNumber: number, aTokens: LineTokens): LineTokens {
|
||||
@@ -782,7 +967,7 @@ export class TokensStore2 {
|
||||
}
|
||||
|
||||
const pieceIndex = TokensStore2._findFirstPieceWithLine(pieces, lineNumber);
|
||||
const bTokens = this._pieces[pieceIndex].getLineTokens(lineNumber);
|
||||
const bTokens = pieces[pieceIndex].getLineTokens(lineNumber);
|
||||
|
||||
if (!bTokens) {
|
||||
return aTokens;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import * as stringBuilder from 'vs/editor/common/core/stringBuilder';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { LanguageIdentifier } from 'vs/editor/common/modes';
|
||||
import { CharacterPair } from 'vs/editor/common/modes/languageConfiguration';
|
||||
@@ -264,14 +265,24 @@ function createBracketOrRegExp(pieces: string[]): RegExp {
|
||||
return strings.createRegExp(regexStr, true);
|
||||
}
|
||||
|
||||
let toReversedString = (function () {
|
||||
const toReversedString = (function () {
|
||||
|
||||
function reverse(str: string): string {
|
||||
let reversedStr = '';
|
||||
for (let i = str.length - 1; i >= 0; i--) {
|
||||
reversedStr += str.charAt(i);
|
||||
if (stringBuilder.hasTextDecoder) {
|
||||
// create a Uint16Array and then use a TextDecoder to create a string
|
||||
const arr = new Uint16Array(str.length);
|
||||
let offset = 0;
|
||||
for (let i = str.length - 1; i >= 0; i--) {
|
||||
arr[offset++] = str.charCodeAt(i);
|
||||
}
|
||||
return stringBuilder.getPlatformTextDecoder().decode(arr);
|
||||
} else {
|
||||
let result: string[] = [], resultLen = 0;
|
||||
for (let i = str.length - 1; i >= 0; i--) {
|
||||
result[resultLen++] = str.charAt(i);
|
||||
}
|
||||
return result.join('');
|
||||
}
|
||||
return reversedStr;
|
||||
}
|
||||
|
||||
let lastInput: string | null = null;
|
||||
|
||||
@@ -8,9 +8,13 @@ import { URI } from 'vs/base/common/uri';
|
||||
import { ITextBufferFactory, ITextModel, ITextModelCreationOptions } from 'vs/editor/common/model';
|
||||
import { ILanguageSelection } from 'vs/editor/common/services/modeService';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider } from 'vs/editor/common/modes';
|
||||
import { SemanticTokensProviderStyling } from 'vs/editor/common/services/semanticTokensProviderStyling';
|
||||
|
||||
export const IModelService = createDecorator<IModelService>('modelService');
|
||||
|
||||
export type DocumentTokensProvider = DocumentSemanticTokensProvider | DocumentRangeSemanticTokensProvider;
|
||||
|
||||
export interface IModelService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
@@ -28,6 +32,8 @@ export interface IModelService {
|
||||
|
||||
getModel(resource: URI): ITextModel | null;
|
||||
|
||||
getSemanticTokensProviderStyling(provider: DocumentTokensProvider): SemanticTokensProviderStyling;
|
||||
|
||||
onModelAdded: Event<ITextModel>;
|
||||
|
||||
onModelRemoved: Event<ITextModel>;
|
||||
|
||||
@@ -12,26 +12,26 @@ import { URI } from 'vs/base/common/uri';
|
||||
import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/config/editorOptions';
|
||||
import { EditOperation } from 'vs/editor/common/core/editOperation';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { DefaultEndOfLine, EndOfLinePreference, EndOfLineSequence, IIdentifiedSingleEditOperation, ITextBuffer, ITextBufferFactory, ITextModel, ITextModelCreationOptions, IValidEditOperation } from 'vs/editor/common/model';
|
||||
import { DefaultEndOfLine, EndOfLinePreference, EndOfLineSequence, IIdentifiedSingleEditOperation, ITextBuffer, ITextBufferFactory, ITextModel, ITextModelCreationOptions } from 'vs/editor/common/model';
|
||||
import { TextModel, createTextBuffer } from 'vs/editor/common/model/textModel';
|
||||
import { IModelLanguageChangedEvent, IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents';
|
||||
import { LanguageIdentifier, DocumentSemanticTokensProviderRegistry, DocumentSemanticTokensProvider, SemanticTokensLegend, SemanticTokens, SemanticTokensEdits, TokenMetadata, FontStyle, MetadataConsts } from 'vs/editor/common/modes';
|
||||
import { LanguageIdentifier, DocumentSemanticTokensProviderRegistry, DocumentSemanticTokensProvider, SemanticTokens, SemanticTokensEdits } from 'vs/editor/common/modes';
|
||||
import { PLAINTEXT_LANGUAGE_IDENTIFIER } from 'vs/editor/common/modes/modesRegistry';
|
||||
import { ILanguageSelection } from 'vs/editor/common/services/modeService';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { IModelService, DocumentTokensProvider } from 'vs/editor/common/services/modelService';
|
||||
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { SparseEncodedTokens, MultilineTokens2 } from 'vs/editor/common/model/tokensStore';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IUndoRedoService, IUndoRedoElement, IPastFutureElements } from 'vs/platform/undoRedo/common/undoRedo';
|
||||
import { StringSHA1 } from 'vs/base/common/hash';
|
||||
import { SingleModelEditStackElement, MultiModelEditStackElement, EditStackElement } from 'vs/editor/common/model/editStack';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { SemanticTokensProviderStyling, toMultilineTokens2 } from 'vs/editor/common/services/semanticTokensProviderStyling';
|
||||
|
||||
export const MAINTAIN_UNDO_REDO_STACK = true;
|
||||
|
||||
@@ -171,6 +171,7 @@ export class ModelServiceImpl extends Disposable implements IModelService {
|
||||
*/
|
||||
private readonly _models: { [modelId: string]: ModelData; };
|
||||
private readonly _disposedModels: Map<string, DisposedModelInfo>;
|
||||
private readonly _semanticStyling: SemanticStyling;
|
||||
|
||||
constructor(
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@@ -184,11 +185,12 @@ export class ModelServiceImpl extends Disposable implements IModelService {
|
||||
this._modelCreationOptionsByLanguageAndResource = Object.create(null);
|
||||
this._models = {};
|
||||
this._disposedModels = new Map<string, DisposedModelInfo>();
|
||||
this._semanticStyling = this._register(new SemanticStyling(this._themeService, this._logService));
|
||||
|
||||
this._register(this._configurationService.onDidChangeConfiguration(e => this._updateModelOptions()));
|
||||
this._register(this._configurationService.onDidChangeConfiguration(() => this._updateModelOptions()));
|
||||
this._updateModelOptions();
|
||||
|
||||
this._register(new SemanticColoringFeature(this, this._themeService, this._configurationService, this._logService));
|
||||
this._register(new SemanticColoringFeature(this, this._themeService, this._configurationService, this._semanticStyling));
|
||||
}
|
||||
|
||||
private static _readModelOptions(config: IRawConfig, isForSimpleWidget: boolean): ITextModelCreationOptions {
|
||||
@@ -380,7 +382,7 @@ export class ModelServiceImpl extends Disposable implements IModelService {
|
||||
model.pushEditOperations(
|
||||
[],
|
||||
ModelServiceImpl._computeEdits(model, textBuffer),
|
||||
(inverseEditOperations: IValidEditOperation[]) => []
|
||||
() => []
|
||||
);
|
||||
model.pushStackElement();
|
||||
}
|
||||
@@ -542,6 +544,10 @@ export class ModelServiceImpl extends Disposable implements IModelService {
|
||||
return modelData.model;
|
||||
}
|
||||
|
||||
public getSemanticTokensProviderStyling(provider: DocumentTokensProvider): SemanticTokensProviderStyling {
|
||||
return this._semanticStyling.get(provider);
|
||||
}
|
||||
|
||||
// --- end IModelService
|
||||
|
||||
private _onWillDispose(model: ITextModel): void {
|
||||
@@ -575,13 +581,13 @@ class SemanticColoringFeature extends Disposable {
|
||||
|
||||
private static readonly SETTING_ID = 'editor.semanticHighlighting';
|
||||
|
||||
private _watchers: Record<string, ModelSemanticColoring>;
|
||||
private _semanticStyling: SemanticStyling;
|
||||
private readonly _watchers: Record<string, ModelSemanticColoring>;
|
||||
private readonly _semanticStyling: SemanticStyling;
|
||||
|
||||
constructor(modelService: IModelService, themeService: IThemeService, configurationService: IConfigurationService, logService: ILogService) {
|
||||
constructor(modelService: IModelService, themeService: IThemeService, configurationService: IConfigurationService, semanticStyling: SemanticStyling) {
|
||||
super();
|
||||
this._watchers = Object.create(null);
|
||||
this._semanticStyling = this._register(new SemanticStyling(themeService, logService));
|
||||
this._semanticStyling = semanticStyling;
|
||||
|
||||
const isSemanticColoringEnabled = (model: ITextModel) => {
|
||||
if (!themeService.getColorTheme().semanticHighlighting) {
|
||||
@@ -633,204 +639,27 @@ class SemanticColoringFeature extends Disposable {
|
||||
|
||||
class SemanticStyling extends Disposable {
|
||||
|
||||
private _caches: WeakMap<DocumentSemanticTokensProvider, SemanticColoringProviderStyling>;
|
||||
private _caches: WeakMap<DocumentTokensProvider, SemanticTokensProviderStyling>;
|
||||
|
||||
constructor(
|
||||
private readonly _themeService: IThemeService,
|
||||
private readonly _logService: ILogService
|
||||
) {
|
||||
super();
|
||||
this._caches = new WeakMap<DocumentSemanticTokensProvider, SemanticColoringProviderStyling>();
|
||||
this._caches = new WeakMap<DocumentTokensProvider, SemanticTokensProviderStyling>();
|
||||
this._register(this._themeService.onDidColorThemeChange(() => {
|
||||
this._caches = new WeakMap<DocumentSemanticTokensProvider, SemanticColoringProviderStyling>();
|
||||
this._caches = new WeakMap<DocumentTokensProvider, SemanticTokensProviderStyling>();
|
||||
}));
|
||||
}
|
||||
|
||||
public get(provider: DocumentSemanticTokensProvider): SemanticColoringProviderStyling {
|
||||
public get(provider: DocumentTokensProvider): SemanticTokensProviderStyling {
|
||||
if (!this._caches.has(provider)) {
|
||||
this._caches.set(provider, new SemanticColoringProviderStyling(provider.getLegend(), this._themeService, this._logService));
|
||||
this._caches.set(provider, new SemanticTokensProviderStyling(provider.getLegend(), this._themeService, this._logService));
|
||||
}
|
||||
return this._caches.get(provider)!;
|
||||
}
|
||||
}
|
||||
|
||||
const enum Constants {
|
||||
NO_STYLING = 0b01111111111111111111111111111111
|
||||
}
|
||||
|
||||
class HashTableEntry {
|
||||
public readonly tokenTypeIndex: number;
|
||||
public readonly tokenModifierSet: number;
|
||||
public readonly languageId: number;
|
||||
public readonly metadata: number;
|
||||
public next: HashTableEntry | null;
|
||||
|
||||
constructor(tokenTypeIndex: number, tokenModifierSet: number, languageId: number, metadata: number) {
|
||||
this.tokenTypeIndex = tokenTypeIndex;
|
||||
this.tokenModifierSet = tokenModifierSet;
|
||||
this.languageId = languageId;
|
||||
this.metadata = metadata;
|
||||
this.next = null;
|
||||
}
|
||||
}
|
||||
|
||||
class HashTable {
|
||||
|
||||
private static _SIZES = [3, 7, 13, 31, 61, 127, 251, 509, 1021, 2039, 4093, 8191, 16381, 32749, 65521, 131071, 262139, 524287, 1048573, 2097143];
|
||||
|
||||
private _elementsCount: number;
|
||||
private _currentLengthIndex: number;
|
||||
private _currentLength: number;
|
||||
private _growCount: number;
|
||||
private _elements: (HashTableEntry | null)[];
|
||||
|
||||
constructor() {
|
||||
this._elementsCount = 0;
|
||||
this._currentLengthIndex = 0;
|
||||
this._currentLength = HashTable._SIZES[this._currentLengthIndex];
|
||||
this._growCount = Math.round(this._currentLengthIndex + 1 < HashTable._SIZES.length ? 2 / 3 * this._currentLength : 0);
|
||||
this._elements = [];
|
||||
HashTable._nullOutEntries(this._elements, this._currentLength);
|
||||
}
|
||||
|
||||
private static _nullOutEntries(entries: (HashTableEntry | null)[], length: number): void {
|
||||
for (let i = 0; i < length; i++) {
|
||||
entries[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
private _hashFunc(tokenTypeIndex: number, tokenModifierSet: number, languageId: number): number {
|
||||
const hash = (n1: number, n2: number) => (((n1 << 5) - n1) + n2) | 0; // n1 * 31 + n2, keep as int32
|
||||
return hash(hash(tokenTypeIndex, tokenModifierSet), languageId) % this._currentLength;
|
||||
}
|
||||
|
||||
public get(tokenTypeIndex: number, tokenModifierSet: number, languageId: number): HashTableEntry | null {
|
||||
const hash = this._hashFunc(tokenTypeIndex, tokenModifierSet, languageId);
|
||||
|
||||
let p = this._elements[hash];
|
||||
while (p) {
|
||||
if (p.tokenTypeIndex === tokenTypeIndex && p.tokenModifierSet === tokenModifierSet && p.languageId === languageId) {
|
||||
return p;
|
||||
}
|
||||
p = p.next;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public add(tokenTypeIndex: number, tokenModifierSet: number, languageId: number, metadata: number): void {
|
||||
this._elementsCount++;
|
||||
if (this._growCount !== 0 && this._elementsCount >= this._growCount) {
|
||||
// expand!
|
||||
const oldElements = this._elements;
|
||||
|
||||
this._currentLengthIndex++;
|
||||
this._currentLength = HashTable._SIZES[this._currentLengthIndex];
|
||||
this._growCount = Math.round(this._currentLengthIndex + 1 < HashTable._SIZES.length ? 2 / 3 * this._currentLength : 0);
|
||||
this._elements = [];
|
||||
HashTable._nullOutEntries(this._elements, this._currentLength);
|
||||
|
||||
for (const first of oldElements) {
|
||||
let p = first;
|
||||
while (p) {
|
||||
const oldNext = p.next;
|
||||
p.next = null;
|
||||
this._add(p);
|
||||
p = oldNext;
|
||||
}
|
||||
}
|
||||
}
|
||||
this._add(new HashTableEntry(tokenTypeIndex, tokenModifierSet, languageId, metadata));
|
||||
}
|
||||
|
||||
private _add(element: HashTableEntry): void {
|
||||
const hash = this._hashFunc(element.tokenTypeIndex, element.tokenModifierSet, element.languageId);
|
||||
element.next = this._elements[hash];
|
||||
this._elements[hash] = element;
|
||||
}
|
||||
}
|
||||
|
||||
class SemanticColoringProviderStyling {
|
||||
|
||||
private readonly _hashTable: HashTable;
|
||||
|
||||
constructor(
|
||||
private readonly _legend: SemanticTokensLegend,
|
||||
private readonly _themeService: IThemeService,
|
||||
private readonly _logService: ILogService
|
||||
) {
|
||||
this._hashTable = new HashTable();
|
||||
}
|
||||
|
||||
public getMetadata(tokenTypeIndex: number, tokenModifierSet: number, languageId: LanguageIdentifier): number {
|
||||
const entry = this._hashTable.get(tokenTypeIndex, tokenModifierSet, languageId.id);
|
||||
let metadata: number;
|
||||
if (entry) {
|
||||
metadata = entry.metadata;
|
||||
} else {
|
||||
const tokenType = this._legend.tokenTypes[tokenTypeIndex];
|
||||
const tokenModifiers: string[] = [];
|
||||
let modifierSet = tokenModifierSet;
|
||||
for (let modifierIndex = 0; modifierSet > 0 && modifierIndex < this._legend.tokenModifiers.length; modifierIndex++) {
|
||||
if (modifierSet & 1) {
|
||||
tokenModifiers.push(this._legend.tokenModifiers[modifierIndex]);
|
||||
}
|
||||
modifierSet = modifierSet >> 1;
|
||||
}
|
||||
|
||||
const tokenStyle = this._themeService.getColorTheme().getTokenStyleMetadata(tokenType, tokenModifiers, languageId.language);
|
||||
if (typeof tokenStyle === 'undefined') {
|
||||
metadata = Constants.NO_STYLING;
|
||||
} else {
|
||||
metadata = 0;
|
||||
if (typeof tokenStyle.italic !== 'undefined') {
|
||||
const italicBit = (tokenStyle.italic ? FontStyle.Italic : 0) << MetadataConsts.FONT_STYLE_OFFSET;
|
||||
metadata |= italicBit | MetadataConsts.SEMANTIC_USE_ITALIC;
|
||||
}
|
||||
if (typeof tokenStyle.bold !== 'undefined') {
|
||||
const boldBit = (tokenStyle.bold ? FontStyle.Bold : 0) << MetadataConsts.FONT_STYLE_OFFSET;
|
||||
metadata |= boldBit | MetadataConsts.SEMANTIC_USE_BOLD;
|
||||
}
|
||||
if (typeof tokenStyle.underline !== 'undefined') {
|
||||
const underlineBit = (tokenStyle.underline ? FontStyle.Underline : 0) << MetadataConsts.FONT_STYLE_OFFSET;
|
||||
metadata |= underlineBit | MetadataConsts.SEMANTIC_USE_UNDERLINE;
|
||||
}
|
||||
if (tokenStyle.foreground) {
|
||||
const foregroundBits = (tokenStyle.foreground) << MetadataConsts.FOREGROUND_OFFSET;
|
||||
metadata |= foregroundBits | MetadataConsts.SEMANTIC_USE_FOREGROUND;
|
||||
}
|
||||
if (metadata === 0) {
|
||||
// Nothing!
|
||||
metadata = Constants.NO_STYLING;
|
||||
}
|
||||
}
|
||||
this._hashTable.add(tokenTypeIndex, tokenModifierSet, languageId.id, metadata);
|
||||
}
|
||||
if (this._logService.getLevel() === LogLevel.Trace) {
|
||||
const type = this._legend.tokenTypes[tokenTypeIndex];
|
||||
const modifiers = tokenModifierSet ? ' ' + this._legend.tokenModifiers.filter((_, i) => tokenModifierSet & (1 << i)).join(' ') : '';
|
||||
this._logService.trace(`tokenStyleMetadata ${entry ? '[CACHED] ' : ''}${type}${modifiers}: foreground ${TokenMetadata.getForeground(metadata)}, fontStyle ${TokenMetadata.getFontStyle(metadata).toString(2)}`);
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
const enum SemanticColoringConstants {
|
||||
/**
|
||||
* Let's aim at having 8KB buffers if possible...
|
||||
* So that would be 8192 / (5 * 4) = 409.6 tokens per area
|
||||
*/
|
||||
DesiredTokensPerArea = 400,
|
||||
|
||||
/**
|
||||
* Try to keep the total number of areas under 1024 if possible,
|
||||
* simply compensate by having more tokens per area...
|
||||
*/
|
||||
DesiredMaxAreas = 1024,
|
||||
}
|
||||
|
||||
class SemanticTokensResponse {
|
||||
constructor(
|
||||
private readonly _provider: DocumentSemanticTokensProvider,
|
||||
@@ -848,10 +677,10 @@ class ModelSemanticColoring extends Disposable {
|
||||
private _isDisposed: boolean;
|
||||
private readonly _model: ITextModel;
|
||||
private readonly _semanticStyling: SemanticStyling;
|
||||
private readonly _fetchSemanticTokens: RunOnceScheduler;
|
||||
private _currentResponse: SemanticTokensResponse | null;
|
||||
private _currentRequestCancellationTokenSource: CancellationTokenSource | null;
|
||||
private _providersChangeListeners: IDisposable[];
|
||||
private readonly _fetchDocumentSemanticTokens: RunOnceScheduler;
|
||||
private _currentDocumentResponse: SemanticTokensResponse | null;
|
||||
private _currentDocumentRequestCancellationTokenSource: CancellationTokenSource | null;
|
||||
private _documentProvidersChangeListeners: IDisposable[];
|
||||
|
||||
constructor(model: ITextModel, themeService: IThemeService, stylingProvider: SemanticStyling) {
|
||||
super();
|
||||
@@ -859,57 +688,57 @@ class ModelSemanticColoring extends Disposable {
|
||||
this._isDisposed = false;
|
||||
this._model = model;
|
||||
this._semanticStyling = stylingProvider;
|
||||
this._fetchSemanticTokens = this._register(new RunOnceScheduler(() => this._fetchSemanticTokensNow(), 300));
|
||||
this._currentResponse = null;
|
||||
this._currentRequestCancellationTokenSource = null;
|
||||
this._providersChangeListeners = [];
|
||||
this._fetchDocumentSemanticTokens = this._register(new RunOnceScheduler(() => this._fetchDocumentSemanticTokensNow(), 300));
|
||||
this._currentDocumentResponse = null;
|
||||
this._currentDocumentRequestCancellationTokenSource = null;
|
||||
this._documentProvidersChangeListeners = [];
|
||||
|
||||
this._register(this._model.onDidChangeContent(e => {
|
||||
if (!this._fetchSemanticTokens.isScheduled()) {
|
||||
this._fetchSemanticTokens.schedule();
|
||||
this._register(this._model.onDidChangeContent(() => {
|
||||
if (!this._fetchDocumentSemanticTokens.isScheduled()) {
|
||||
this._fetchDocumentSemanticTokens.schedule();
|
||||
}
|
||||
}));
|
||||
const bindChangeListeners = () => {
|
||||
dispose(this._providersChangeListeners);
|
||||
this._providersChangeListeners = [];
|
||||
const bindDocumentChangeListeners = () => {
|
||||
dispose(this._documentProvidersChangeListeners);
|
||||
this._documentProvidersChangeListeners = [];
|
||||
for (const provider of DocumentSemanticTokensProviderRegistry.all(model)) {
|
||||
if (typeof provider.onDidChange === 'function') {
|
||||
this._providersChangeListeners.push(provider.onDidChange(() => this._fetchSemanticTokens.schedule(0)));
|
||||
this._documentProvidersChangeListeners.push(provider.onDidChange(() => this._fetchDocumentSemanticTokens.schedule(0)));
|
||||
}
|
||||
}
|
||||
};
|
||||
bindChangeListeners();
|
||||
this._register(DocumentSemanticTokensProviderRegistry.onDidChange(e => {
|
||||
bindChangeListeners();
|
||||
this._fetchSemanticTokens.schedule();
|
||||
bindDocumentChangeListeners();
|
||||
this._register(DocumentSemanticTokensProviderRegistry.onDidChange(() => {
|
||||
bindDocumentChangeListeners();
|
||||
this._fetchDocumentSemanticTokens.schedule();
|
||||
}));
|
||||
|
||||
this._register(themeService.onDidColorThemeChange(_ => {
|
||||
// clear out existing tokens
|
||||
this._setSemanticTokens(null, null, null, []);
|
||||
this._fetchSemanticTokens.schedule();
|
||||
this._setDocumentSemanticTokens(null, null, null, []);
|
||||
this._fetchDocumentSemanticTokens.schedule();
|
||||
}));
|
||||
|
||||
this._fetchSemanticTokens.schedule(0);
|
||||
this._fetchDocumentSemanticTokens.schedule(0);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._currentResponse) {
|
||||
this._currentResponse.dispose();
|
||||
this._currentResponse = null;
|
||||
if (this._currentDocumentResponse) {
|
||||
this._currentDocumentResponse.dispose();
|
||||
this._currentDocumentResponse = null;
|
||||
}
|
||||
if (this._currentRequestCancellationTokenSource) {
|
||||
this._currentRequestCancellationTokenSource.cancel();
|
||||
this._currentRequestCancellationTokenSource = null;
|
||||
if (this._currentDocumentRequestCancellationTokenSource) {
|
||||
this._currentDocumentRequestCancellationTokenSource.cancel();
|
||||
this._currentDocumentRequestCancellationTokenSource = null;
|
||||
}
|
||||
this._setSemanticTokens(null, null, null, []);
|
||||
this._setDocumentSemanticTokens(null, null, null, []);
|
||||
this._isDisposed = true;
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private _fetchSemanticTokensNow(): void {
|
||||
if (this._currentRequestCancellationTokenSource) {
|
||||
private _fetchDocumentSemanticTokensNow(): void {
|
||||
if (this._currentDocumentRequestCancellationTokenSource) {
|
||||
// there is already a request running, let it finish...
|
||||
return;
|
||||
}
|
||||
@@ -917,7 +746,7 @@ class ModelSemanticColoring extends Disposable {
|
||||
if (!provider) {
|
||||
return;
|
||||
}
|
||||
this._currentRequestCancellationTokenSource = new CancellationTokenSource();
|
||||
this._currentDocumentRequestCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
const pendingChanges: IModelContentChangedEvent[] = [];
|
||||
const contentChangeListener = this._model.onDidChangeContent((e) => {
|
||||
@@ -926,13 +755,13 @@ class ModelSemanticColoring extends Disposable {
|
||||
|
||||
const styling = this._semanticStyling.get(provider);
|
||||
|
||||
const lastResultId = this._currentResponse ? this._currentResponse.resultId || null : null;
|
||||
const request = Promise.resolve(provider.provideDocumentSemanticTokens(this._model, lastResultId, this._currentRequestCancellationTokenSource.token));
|
||||
const lastResultId = this._currentDocumentResponse ? this._currentDocumentResponse.resultId || null : null;
|
||||
const request = Promise.resolve(provider.provideDocumentSemanticTokens(this._model, lastResultId, this._currentDocumentRequestCancellationTokenSource.token));
|
||||
|
||||
request.then((res) => {
|
||||
this._currentRequestCancellationTokenSource = null;
|
||||
this._currentDocumentRequestCancellationTokenSource = null;
|
||||
contentChangeListener.dispose();
|
||||
this._setSemanticTokens(provider, res || null, styling, pendingChanges);
|
||||
this._setDocumentSemanticTokens(provider, res || null, styling, pendingChanges);
|
||||
}, (err) => {
|
||||
if (!err || typeof err.message !== 'string' || err.message.indexOf('busy') === -1) {
|
||||
errors.onUnexpectedError(err);
|
||||
@@ -940,13 +769,13 @@ class ModelSemanticColoring extends Disposable {
|
||||
|
||||
// Semantic tokens eats up all errors and considers errors to mean that the result is temporarily not available
|
||||
// The API does not have a special error kind to express this...
|
||||
this._currentRequestCancellationTokenSource = null;
|
||||
this._currentDocumentRequestCancellationTokenSource = null;
|
||||
contentChangeListener.dispose();
|
||||
|
||||
if (pendingChanges.length > 0) {
|
||||
// More changes occurred while the request was running
|
||||
if (!this._fetchSemanticTokens.isScheduled()) {
|
||||
this._fetchSemanticTokens.schedule();
|
||||
if (!this._fetchDocumentSemanticTokens.isScheduled()) {
|
||||
this._fetchDocumentSemanticTokens.schedule();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -966,11 +795,11 @@ class ModelSemanticColoring extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
private _setSemanticTokens(provider: DocumentSemanticTokensProvider | null, tokens: SemanticTokens | SemanticTokensEdits | null, styling: SemanticColoringProviderStyling | null, pendingChanges: IModelContentChangedEvent[]): void {
|
||||
const currentResponse = this._currentResponse;
|
||||
if (this._currentResponse) {
|
||||
this._currentResponse.dispose();
|
||||
this._currentResponse = null;
|
||||
private _setDocumentSemanticTokens(provider: DocumentSemanticTokensProvider | null, tokens: SemanticTokens | SemanticTokensEdits | null, styling: SemanticTokensProviderStyling | null, pendingChanges: IModelContentChangedEvent[]): void {
|
||||
const currentResponse = this._currentDocumentResponse;
|
||||
if (this._currentDocumentResponse) {
|
||||
this._currentDocumentResponse.dispose();
|
||||
this._currentDocumentResponse = null;
|
||||
}
|
||||
if (this._isDisposed) {
|
||||
// disposed!
|
||||
@@ -979,15 +808,19 @@ class ModelSemanticColoring extends Disposable {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!provider || !tokens || !styling) {
|
||||
this._model.setSemanticTokens(null);
|
||||
if (!provider || !styling) {
|
||||
this._model.setSemanticTokens(null, false);
|
||||
return;
|
||||
}
|
||||
if (!tokens) {
|
||||
this._model.setSemanticTokens(null, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ModelSemanticColoring._isSemanticTokensEdits(tokens)) {
|
||||
if (!currentResponse) {
|
||||
// not possible!
|
||||
this._model.setSemanticTokens(null);
|
||||
this._model.setSemanticTokens(null, true);
|
||||
return;
|
||||
}
|
||||
if (tokens.edits.length === 0) {
|
||||
@@ -1037,80 +870,9 @@ class ModelSemanticColoring extends Disposable {
|
||||
|
||||
if (ModelSemanticColoring._isSemanticTokens(tokens)) {
|
||||
|
||||
this._currentResponse = new SemanticTokensResponse(provider, tokens.resultId, tokens.data);
|
||||
this._currentDocumentResponse = new SemanticTokensResponse(provider, tokens.resultId, tokens.data);
|
||||
|
||||
const srcData = tokens.data;
|
||||
const tokenCount = (tokens.data.length / 5) | 0;
|
||||
const tokensPerArea = Math.max(Math.ceil(tokenCount / SemanticColoringConstants.DesiredMaxAreas), SemanticColoringConstants.DesiredTokensPerArea);
|
||||
|
||||
const result: MultilineTokens2[] = [];
|
||||
|
||||
const languageId = this._model.getLanguageIdentifier();
|
||||
|
||||
let tokenIndex = 0;
|
||||
let lastLineNumber = 1;
|
||||
let lastStartCharacter = 0;
|
||||
while (tokenIndex < tokenCount) {
|
||||
const tokenStartIndex = tokenIndex;
|
||||
let tokenEndIndex = Math.min(tokenStartIndex + tokensPerArea, tokenCount);
|
||||
|
||||
// Keep tokens on the same line in the same area...
|
||||
if (tokenEndIndex < tokenCount) {
|
||||
|
||||
let smallTokenEndIndex = tokenEndIndex;
|
||||
while (smallTokenEndIndex - 1 > tokenStartIndex && srcData[5 * smallTokenEndIndex] === 0) {
|
||||
smallTokenEndIndex--;
|
||||
}
|
||||
|
||||
if (smallTokenEndIndex - 1 === tokenStartIndex) {
|
||||
// there are so many tokens on this line that our area would be empty, we must now go right
|
||||
let bigTokenEndIndex = tokenEndIndex;
|
||||
while (bigTokenEndIndex + 1 < tokenCount && srcData[5 * bigTokenEndIndex] === 0) {
|
||||
bigTokenEndIndex++;
|
||||
}
|
||||
tokenEndIndex = bigTokenEndIndex;
|
||||
} else {
|
||||
tokenEndIndex = smallTokenEndIndex;
|
||||
}
|
||||
}
|
||||
|
||||
let destData = new Uint32Array((tokenEndIndex - tokenStartIndex) * 4);
|
||||
let destOffset = 0;
|
||||
let areaLine = 0;
|
||||
while (tokenIndex < tokenEndIndex) {
|
||||
const srcOffset = 5 * tokenIndex;
|
||||
const deltaLine = srcData[srcOffset];
|
||||
const deltaCharacter = srcData[srcOffset + 1];
|
||||
const lineNumber = lastLineNumber + deltaLine;
|
||||
const startCharacter = (deltaLine === 0 ? lastStartCharacter + deltaCharacter : deltaCharacter);
|
||||
const length = srcData[srcOffset + 2];
|
||||
const tokenTypeIndex = srcData[srcOffset + 3];
|
||||
const tokenModifierSet = srcData[srcOffset + 4];
|
||||
const metadata = styling.getMetadata(tokenTypeIndex, tokenModifierSet, languageId);
|
||||
|
||||
if (metadata !== Constants.NO_STYLING) {
|
||||
if (areaLine === 0) {
|
||||
areaLine = lineNumber;
|
||||
}
|
||||
destData[destOffset] = lineNumber - areaLine;
|
||||
destData[destOffset + 1] = startCharacter;
|
||||
destData[destOffset + 2] = startCharacter + length;
|
||||
destData[destOffset + 3] = metadata;
|
||||
destOffset += 4;
|
||||
}
|
||||
|
||||
lastLineNumber = lineNumber;
|
||||
lastStartCharacter = startCharacter;
|
||||
tokenIndex++;
|
||||
}
|
||||
|
||||
if (destOffset !== destData.length) {
|
||||
destData = destData.subarray(0, destOffset);
|
||||
}
|
||||
|
||||
const tokens = new MultilineTokens2(areaLine, new SparseEncodedTokens(destData));
|
||||
result.push(tokens);
|
||||
}
|
||||
const result = toMultilineTokens2(tokens, styling, this._model.getLanguageIdentifier());
|
||||
|
||||
// Adjust incoming semantic tokens
|
||||
if (pendingChanges.length > 0) {
|
||||
@@ -1126,16 +888,16 @@ class ModelSemanticColoring extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._fetchSemanticTokens.isScheduled()) {
|
||||
this._fetchSemanticTokens.schedule();
|
||||
if (!this._fetchDocumentSemanticTokens.isScheduled()) {
|
||||
this._fetchDocumentSemanticTokens.schedule();
|
||||
}
|
||||
}
|
||||
|
||||
this._model.setSemanticTokens(result);
|
||||
this._model.setSemanticTokens(result, true);
|
||||
return;
|
||||
}
|
||||
|
||||
this._model.setSemanticTokens(null);
|
||||
this._model.setSemanticTokens(null, true);
|
||||
}
|
||||
|
||||
private _getSemanticColoringProvider(): DocumentSemanticTokensProvider | null {
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SemanticTokensLegend, TokenMetadata, FontStyle, MetadataConsts, SemanticTokens, LanguageIdentifier } from 'vs/editor/common/modes';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
|
||||
import { MultilineTokens2, SparseEncodedTokens } from 'vs/editor/common/model/tokensStore';
|
||||
|
||||
export const enum SemanticTokensProviderStylingConstants {
|
||||
NO_STYLING = 0b01111111111111111111111111111111
|
||||
}
|
||||
|
||||
export class SemanticTokensProviderStyling {
|
||||
|
||||
private readonly _hashTable: HashTable;
|
||||
|
||||
constructor(
|
||||
private readonly _legend: SemanticTokensLegend,
|
||||
private readonly _themeService: IThemeService,
|
||||
private readonly _logService: ILogService
|
||||
) {
|
||||
this._hashTable = new HashTable();
|
||||
}
|
||||
|
||||
public getMetadata(tokenTypeIndex: number, tokenModifierSet: number, languageId: LanguageIdentifier): number {
|
||||
const entry = this._hashTable.get(tokenTypeIndex, tokenModifierSet, languageId.id);
|
||||
let metadata: number;
|
||||
if (entry) {
|
||||
metadata = entry.metadata;
|
||||
} else {
|
||||
const tokenType = this._legend.tokenTypes[tokenTypeIndex];
|
||||
const tokenModifiers: string[] = [];
|
||||
let modifierSet = tokenModifierSet;
|
||||
for (let modifierIndex = 0; modifierSet > 0 && modifierIndex < this._legend.tokenModifiers.length; modifierIndex++) {
|
||||
if (modifierSet & 1) {
|
||||
tokenModifiers.push(this._legend.tokenModifiers[modifierIndex]);
|
||||
}
|
||||
modifierSet = modifierSet >> 1;
|
||||
}
|
||||
|
||||
const tokenStyle = this._themeService.getColorTheme().getTokenStyleMetadata(tokenType, tokenModifiers, languageId.language);
|
||||
if (typeof tokenStyle === 'undefined') {
|
||||
metadata = SemanticTokensProviderStylingConstants.NO_STYLING;
|
||||
} else {
|
||||
metadata = 0;
|
||||
if (typeof tokenStyle.italic !== 'undefined') {
|
||||
const italicBit = (tokenStyle.italic ? FontStyle.Italic : 0) << MetadataConsts.FONT_STYLE_OFFSET;
|
||||
metadata |= italicBit | MetadataConsts.SEMANTIC_USE_ITALIC;
|
||||
}
|
||||
if (typeof tokenStyle.bold !== 'undefined') {
|
||||
const boldBit = (tokenStyle.bold ? FontStyle.Bold : 0) << MetadataConsts.FONT_STYLE_OFFSET;
|
||||
metadata |= boldBit | MetadataConsts.SEMANTIC_USE_BOLD;
|
||||
}
|
||||
if (typeof tokenStyle.underline !== 'undefined') {
|
||||
const underlineBit = (tokenStyle.underline ? FontStyle.Underline : 0) << MetadataConsts.FONT_STYLE_OFFSET;
|
||||
metadata |= underlineBit | MetadataConsts.SEMANTIC_USE_UNDERLINE;
|
||||
}
|
||||
if (tokenStyle.foreground) {
|
||||
const foregroundBits = (tokenStyle.foreground) << MetadataConsts.FOREGROUND_OFFSET;
|
||||
metadata |= foregroundBits | MetadataConsts.SEMANTIC_USE_FOREGROUND;
|
||||
}
|
||||
if (metadata === 0) {
|
||||
// Nothing!
|
||||
metadata = SemanticTokensProviderStylingConstants.NO_STYLING;
|
||||
}
|
||||
}
|
||||
this._hashTable.add(tokenTypeIndex, tokenModifierSet, languageId.id, metadata);
|
||||
}
|
||||
if (this._logService.getLevel() === LogLevel.Trace) {
|
||||
const type = this._legend.tokenTypes[tokenTypeIndex];
|
||||
const modifiers = tokenModifierSet ? ' ' + this._legend.tokenModifiers.filter((_, i) => tokenModifierSet & (1 << i)).join(' ') : '';
|
||||
this._logService.trace(`tokenStyleMetadata ${entry ? '[CACHED] ' : ''}${type}${modifiers}: foreground ${TokenMetadata.getForeground(metadata)}, fontStyle ${TokenMetadata.getFontStyle(metadata).toString(2)}`);
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
|
||||
const enum SemanticColoringConstants {
|
||||
/**
|
||||
* Let's aim at having 8KB buffers if possible...
|
||||
* So that would be 8192 / (5 * 4) = 409.6 tokens per area
|
||||
*/
|
||||
DesiredTokensPerArea = 400,
|
||||
|
||||
/**
|
||||
* Try to keep the total number of areas under 1024 if possible,
|
||||
* simply compensate by having more tokens per area...
|
||||
*/
|
||||
DesiredMaxAreas = 1024,
|
||||
}
|
||||
|
||||
export function toMultilineTokens2(tokens: SemanticTokens, styling: SemanticTokensProviderStyling, languageId: LanguageIdentifier): MultilineTokens2[] {
|
||||
const srcData = tokens.data;
|
||||
const tokenCount = (tokens.data.length / 5) | 0;
|
||||
const tokensPerArea = Math.max(Math.ceil(tokenCount / SemanticColoringConstants.DesiredMaxAreas), SemanticColoringConstants.DesiredTokensPerArea);
|
||||
const result: MultilineTokens2[] = [];
|
||||
|
||||
let tokenIndex = 0;
|
||||
let lastLineNumber = 1;
|
||||
let lastStartCharacter = 0;
|
||||
while (tokenIndex < tokenCount) {
|
||||
const tokenStartIndex = tokenIndex;
|
||||
let tokenEndIndex = Math.min(tokenStartIndex + tokensPerArea, tokenCount);
|
||||
|
||||
// Keep tokens on the same line in the same area...
|
||||
if (tokenEndIndex < tokenCount) {
|
||||
|
||||
let smallTokenEndIndex = tokenEndIndex;
|
||||
while (smallTokenEndIndex - 1 > tokenStartIndex && srcData[5 * smallTokenEndIndex] === 0) {
|
||||
smallTokenEndIndex--;
|
||||
}
|
||||
|
||||
if (smallTokenEndIndex - 1 === tokenStartIndex) {
|
||||
// there are so many tokens on this line that our area would be empty, we must now go right
|
||||
let bigTokenEndIndex = tokenEndIndex;
|
||||
while (bigTokenEndIndex + 1 < tokenCount && srcData[5 * bigTokenEndIndex] === 0) {
|
||||
bigTokenEndIndex++;
|
||||
}
|
||||
tokenEndIndex = bigTokenEndIndex;
|
||||
} else {
|
||||
tokenEndIndex = smallTokenEndIndex;
|
||||
}
|
||||
}
|
||||
|
||||
let destData = new Uint32Array((tokenEndIndex - tokenStartIndex) * 4);
|
||||
let destOffset = 0;
|
||||
let areaLine = 0;
|
||||
while (tokenIndex < tokenEndIndex) {
|
||||
const srcOffset = 5 * tokenIndex;
|
||||
const deltaLine = srcData[srcOffset];
|
||||
const deltaCharacter = srcData[srcOffset + 1];
|
||||
const lineNumber = lastLineNumber + deltaLine;
|
||||
const startCharacter = (deltaLine === 0 ? lastStartCharacter + deltaCharacter : deltaCharacter);
|
||||
const length = srcData[srcOffset + 2];
|
||||
const tokenTypeIndex = srcData[srcOffset + 3];
|
||||
const tokenModifierSet = srcData[srcOffset + 4];
|
||||
const metadata = styling.getMetadata(tokenTypeIndex, tokenModifierSet, languageId);
|
||||
|
||||
if (metadata !== SemanticTokensProviderStylingConstants.NO_STYLING) {
|
||||
if (areaLine === 0) {
|
||||
areaLine = lineNumber;
|
||||
}
|
||||
destData[destOffset] = lineNumber - areaLine;
|
||||
destData[destOffset + 1] = startCharacter;
|
||||
destData[destOffset + 2] = startCharacter + length;
|
||||
destData[destOffset + 3] = metadata;
|
||||
destOffset += 4;
|
||||
}
|
||||
|
||||
lastLineNumber = lineNumber;
|
||||
lastStartCharacter = startCharacter;
|
||||
tokenIndex++;
|
||||
}
|
||||
|
||||
if (destOffset !== destData.length) {
|
||||
destData = destData.subarray(0, destOffset);
|
||||
}
|
||||
|
||||
const tokens = new MultilineTokens2(areaLine, new SparseEncodedTokens(destData));
|
||||
result.push(tokens);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
class HashTableEntry {
|
||||
public readonly tokenTypeIndex: number;
|
||||
public readonly tokenModifierSet: number;
|
||||
public readonly languageId: number;
|
||||
public readonly metadata: number;
|
||||
public next: HashTableEntry | null;
|
||||
|
||||
constructor(tokenTypeIndex: number, tokenModifierSet: number, languageId: number, metadata: number) {
|
||||
this.tokenTypeIndex = tokenTypeIndex;
|
||||
this.tokenModifierSet = tokenModifierSet;
|
||||
this.languageId = languageId;
|
||||
this.metadata = metadata;
|
||||
this.next = null;
|
||||
}
|
||||
}
|
||||
|
||||
class HashTable {
|
||||
|
||||
private static _SIZES = [3, 7, 13, 31, 61, 127, 251, 509, 1021, 2039, 4093, 8191, 16381, 32749, 65521, 131071, 262139, 524287, 1048573, 2097143];
|
||||
|
||||
private _elementsCount: number;
|
||||
private _currentLengthIndex: number;
|
||||
private _currentLength: number;
|
||||
private _growCount: number;
|
||||
private _elements: (HashTableEntry | null)[];
|
||||
|
||||
constructor() {
|
||||
this._elementsCount = 0;
|
||||
this._currentLengthIndex = 0;
|
||||
this._currentLength = HashTable._SIZES[this._currentLengthIndex];
|
||||
this._growCount = Math.round(this._currentLengthIndex + 1 < HashTable._SIZES.length ? 2 / 3 * this._currentLength : 0);
|
||||
this._elements = [];
|
||||
HashTable._nullOutEntries(this._elements, this._currentLength);
|
||||
}
|
||||
|
||||
private static _nullOutEntries(entries: (HashTableEntry | null)[], length: number): void {
|
||||
for (let i = 0; i < length; i++) {
|
||||
entries[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
private _hash2(n1: number, n2: number): number {
|
||||
return (((n1 << 5) - n1) + n2) | 0; // n1 * 31 + n2, keep as int32
|
||||
}
|
||||
|
||||
private _hashFunc(tokenTypeIndex: number, tokenModifierSet: number, languageId: number): number {
|
||||
return this._hash2(this._hash2(tokenTypeIndex, tokenModifierSet), languageId) % this._currentLength;
|
||||
}
|
||||
|
||||
public get(tokenTypeIndex: number, tokenModifierSet: number, languageId: number): HashTableEntry | null {
|
||||
const hash = this._hashFunc(tokenTypeIndex, tokenModifierSet, languageId);
|
||||
|
||||
let p = this._elements[hash];
|
||||
while (p) {
|
||||
if (p.tokenTypeIndex === tokenTypeIndex && p.tokenModifierSet === tokenModifierSet && p.languageId === languageId) {
|
||||
return p;
|
||||
}
|
||||
p = p.next;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public add(tokenTypeIndex: number, tokenModifierSet: number, languageId: number, metadata: number): void {
|
||||
this._elementsCount++;
|
||||
if (this._growCount !== 0 && this._elementsCount >= this._growCount) {
|
||||
// expand!
|
||||
const oldElements = this._elements;
|
||||
|
||||
this._currentLengthIndex++;
|
||||
this._currentLength = HashTable._SIZES[this._currentLengthIndex];
|
||||
this._growCount = Math.round(this._currentLengthIndex + 1 < HashTable._SIZES.length ? 2 / 3 * this._currentLength : 0);
|
||||
this._elements = [];
|
||||
HashTable._nullOutEntries(this._elements, this._currentLength);
|
||||
|
||||
for (const first of oldElements) {
|
||||
let p = first;
|
||||
while (p) {
|
||||
const oldNext = p.next;
|
||||
p.next = null;
|
||||
this._add(p);
|
||||
p = oldNext;
|
||||
}
|
||||
}
|
||||
}
|
||||
this._add(new HashTableEntry(tokenTypeIndex, tokenModifierSet, languageId, metadata));
|
||||
}
|
||||
|
||||
private _add(element: HashTableEntry): void {
|
||||
const hash = this._hashFunc(element.tokenTypeIndex, element.tokenModifierSet, element.languageId);
|
||||
element.next = this._elements[hash];
|
||||
this._elements[hash] = element;
|
||||
}
|
||||
}
|
||||
@@ -813,7 +813,11 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render
|
||||
let prevPartContentCnt = 0;
|
||||
let partAbsoluteOffset = 0;
|
||||
|
||||
sb.appendASCIIString('<span>');
|
||||
if (containsRTL) {
|
||||
sb.appendASCIIString('<span dir="ltr">');
|
||||
} else {
|
||||
sb.appendASCIIString('<span>');
|
||||
}
|
||||
|
||||
for (let partIndex = 0, tokensLen = parts.length; partIndex < tokensLen; partIndex++) {
|
||||
partAbsoluteOffset += prevPartContentCnt;
|
||||
@@ -890,9 +894,6 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render
|
||||
|
||||
let partContentCnt = 0;
|
||||
|
||||
if (containsRTL) {
|
||||
sb.appendASCIIString(' dir="ltr"');
|
||||
}
|
||||
sb.appendASCII(CharCode.GreaterThan);
|
||||
|
||||
for (; charIndex < partEndIndex; charIndex++) {
|
||||
|
||||
@@ -563,8 +563,11 @@ export class ModesContentHoverWidget extends ContentHoverWidget {
|
||||
const disposables = new DisposableStore();
|
||||
const actionsElement = dom.append(hoverElement, $('div.actions'));
|
||||
if (markerHover.marker.severity === MarkerSeverity.Error || markerHover.marker.severity === MarkerSeverity.Warning || markerHover.marker.severity === MarkerSeverity.Info) {
|
||||
const peekProblemLabel = nls.localize('peek problem', "Peek Problem");
|
||||
const peekProblemKeybinding = this._keybindingService.lookupKeybinding(NextMarkerAction.ID);
|
||||
const peekProblemKeybindingLabel = peekProblemKeybinding && peekProblemKeybinding.getLabel();
|
||||
disposables.add(this.renderAction(actionsElement, {
|
||||
label: nls.localize('peek problem', "Peek Problem"),
|
||||
label: peekProblemKeybindingLabel ? nls.localize('titleAndKb', "{0} ({1})", peekProblemLabel, peekProblemKeybindingLabel) : peekProblemLabel,
|
||||
commandId: NextMarkerAction.ID,
|
||||
run: () => {
|
||||
this.hide();
|
||||
@@ -581,7 +584,6 @@ export class ModesContentHoverWidget extends ContentHoverWidget {
|
||||
quickfixPlaceholderElement.textContent = nls.localize('checkingForQuickFixes', "Checking for quick fixes...");
|
||||
disposables.add(toDisposable(() => quickfixPlaceholderElement.remove()));
|
||||
|
||||
|
||||
const codeActionsPromise = this.getCodeActions(markerHover.marker);
|
||||
disposables.add(toDisposable(() => codeActionsPromise.cancel()));
|
||||
codeActionsPromise.then(actions => {
|
||||
@@ -602,8 +604,12 @@ export class ModesContentHoverWidget extends ContentHoverWidget {
|
||||
}
|
||||
}));
|
||||
|
||||
const quickFixLabel = nls.localize('quick fixes', "Quick Fix...");
|
||||
const quickFixKeybinding = this._keybindingService.lookupKeybinding(QuickFixAction.Id);
|
||||
const quickFixKeybindingLabel = quickFixKeybinding && quickFixKeybinding.getLabel();
|
||||
|
||||
disposables.add(this.renderAction(actionsElement, {
|
||||
label: nls.localize('quick fixes', "Quick Fix..."),
|
||||
label: quickFixKeybindingLabel ? nls.localize('titleAndKb', "{0} ({1})", quickFixLabel, quickFixKeybindingLabel) : quickFixLabel,
|
||||
commandId: QuickFixAction.Id,
|
||||
run: (target) => {
|
||||
showing = true;
|
||||
|
||||
@@ -786,11 +786,13 @@ class SelectionHighlighterState {
|
||||
public readonly searchText: string;
|
||||
public readonly matchCase: boolean;
|
||||
public readonly wordSeparators: string | null;
|
||||
public readonly modelVersionId: number;
|
||||
|
||||
constructor(searchText: string, matchCase: boolean, wordSeparators: string | null) {
|
||||
constructor(searchText: string, matchCase: boolean, wordSeparators: string | null, modelVersionId: number) {
|
||||
this.searchText = searchText;
|
||||
this.matchCase = matchCase;
|
||||
this.wordSeparators = wordSeparators;
|
||||
this.modelVersionId = modelVersionId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -807,6 +809,7 @@ class SelectionHighlighterState {
|
||||
a.searchText === b.searchText
|
||||
&& a.matchCase === b.matchCase
|
||||
&& a.wordSeparators === b.wordSeparators
|
||||
&& a.modelVersionId === b.modelVersionId
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -857,6 +860,11 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut
|
||||
this._register(editor.onDidChangeModel((e) => {
|
||||
this._setState(null);
|
||||
}));
|
||||
this._register(editor.onDidChangeModelContent((e) => {
|
||||
if (this._isEnabled) {
|
||||
this.updateSoon.schedule();
|
||||
}
|
||||
}));
|
||||
this._register(CommonFindController.get(editor).getState().onFindReplaceStateChange((e) => {
|
||||
this._update();
|
||||
}));
|
||||
@@ -939,7 +947,7 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut
|
||||
}
|
||||
}
|
||||
|
||||
return new SelectionHighlighterState(r.searchText, r.matchCase, r.wholeWord ? editor.getOption(EditorOption.wordSeparators) : null);
|
||||
return new SelectionHighlighterState(r.searchText, r.matchCase, r.wholeWord ? editor.getOption(EditorOption.wordSeparators) : null, editor.getModel().getVersionId());
|
||||
}
|
||||
|
||||
private _setState(state: SelectionHighlighterState | null): void {
|
||||
|
||||
@@ -134,10 +134,10 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor
|
||||
const position = editor.getPosition() || { lineNumber: 1, column: 1 };
|
||||
const lineCount = this.lineCount(editor);
|
||||
if (lineCount > 1) {
|
||||
return localize('gotoLineLabelEmptyWithLimit', "Current Line: {0}, Column: {1}. Type a line number between 1 and {2} to navigate to.", position.lineNumber, position.column, lineCount);
|
||||
return localize('gotoLineLabelEmptyWithLimit', "Current Line: {0}, Character: {1}. Type a line number between 1 and {2} to navigate to.", position.lineNumber, position.column, lineCount);
|
||||
}
|
||||
|
||||
return localize('gotoLineLabelEmpty', "Current Line: {0}, Column: {1}. Type a line number to navigate to.", position.lineNumber, position.column);
|
||||
return localize('gotoLineLabelEmpty', "Current Line: {0}, Character: {1}. Type a line number to navigate to.", position.lineNumber, position.column);
|
||||
}
|
||||
|
||||
private isValidLineNumber(editor: IEditor, lineNumber: number | undefined): boolean {
|
||||
|
||||
@@ -41,7 +41,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit
|
||||
}
|
||||
|
||||
protected provideWithoutTextEditor(picker: IQuickPick<IGotoSymbolQuickPickItem>): IDisposable {
|
||||
const label = localize('cannotRunGotoSymbolWithoutEditor', "Open a text editor first to go to a symbol.");
|
||||
const label = localize('cannotRunGotoSymbolWithoutEditor', "To go to a symbol, first open a text editor with symbol information.");
|
||||
|
||||
picker.items = [{ label, index: 0, kind: SymbolKind.String }];
|
||||
picker.ariaLabel = label;
|
||||
@@ -70,7 +70,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
// Generic pick for not having any symbol information
|
||||
const label = localize('cannotRunGotoSymbolWithoutSymbolProvider', "Open a text editor with symbol information first to go to a symbol.");
|
||||
const label = localize('cannotRunGotoSymbolWithoutSymbolProvider', "The active text editor does not provide symbol information.");
|
||||
picker.items = [{ label, index: 0, kind: SymbolKind.String }];
|
||||
picker.ariaLabel = label;
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ import { IPosition, Position } from 'vs/editor/common/core/position';
|
||||
import { TrackedRangeStickiness, ITextModel } from 'vs/editor/common/model';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { SuggestRangeHighlighter } from 'vs/editor/contrib/suggest/suggestRangeHighlighter';
|
||||
import { MenuRegistry } from 'vs/platform/actions/common/actions';
|
||||
|
||||
// sticky suggest widget which doesn't disappear on focus out and such
|
||||
@@ -233,9 +232,6 @@ export class SuggestController implements IEditorContribution {
|
||||
};
|
||||
this._toDispose.add(this.editor.onDidChangeConfiguration(() => updateFromConfig()));
|
||||
updateFromConfig();
|
||||
|
||||
// create range highlighter
|
||||
this._toDispose.add(new SuggestRangeHighlighter(this));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { CompletionItem } from 'vs/editor/contrib/suggest/suggest';
|
||||
import { IModelDeltaDecoration } from 'vs/editor/common/model';
|
||||
import { SuggestController } from 'vs/editor/contrib/suggest/suggestController';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
import { domContentLoaded } from 'vs/base/browser/dom';
|
||||
|
||||
export class SuggestRangeHighlighter {
|
||||
|
||||
private readonly _disposables = new DisposableStore();
|
||||
|
||||
private _decorations: string[] = [];
|
||||
private _widgetListener?: IDisposable;
|
||||
private _shiftKeyListener?: IDisposable;
|
||||
private _currentItem?: CompletionItem;
|
||||
|
||||
constructor(private readonly _controller: SuggestController) {
|
||||
|
||||
this._disposables.add(_controller.model.onDidSuggest(e => {
|
||||
if (!e.shy) {
|
||||
const widget = this._controller.widget.getValue();
|
||||
const focused = widget.getFocusedItem();
|
||||
if (focused) {
|
||||
this._highlight(focused.item);
|
||||
}
|
||||
if (!this._widgetListener) {
|
||||
this._widgetListener = widget.onDidFocus(e => this._highlight(e.item));
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this._disposables.add(_controller.model.onDidCancel(() => {
|
||||
this._reset();
|
||||
}));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._reset();
|
||||
this._disposables.dispose();
|
||||
dispose(this._widgetListener);
|
||||
dispose(this._shiftKeyListener);
|
||||
}
|
||||
|
||||
private _reset(): void {
|
||||
this._decorations = this._controller.editor.deltaDecorations(this._decorations, []);
|
||||
if (this._shiftKeyListener) {
|
||||
this._shiftKeyListener.dispose();
|
||||
this._shiftKeyListener = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _highlight(item: CompletionItem) {
|
||||
|
||||
this._currentItem = item;
|
||||
const opts = this._controller.editor.getOption(EditorOption.suggest);
|
||||
let newDeco: IModelDeltaDecoration[] = [];
|
||||
|
||||
if (opts.insertHighlight) {
|
||||
if (!this._shiftKeyListener) {
|
||||
this._shiftKeyListener = shiftKey.event(() => this._highlight(this._currentItem!));
|
||||
}
|
||||
|
||||
const info = this._controller.getOverwriteInfo(item, shiftKey.isPressed);
|
||||
const position = this._controller.editor.getPosition()!;
|
||||
|
||||
if (opts.insertMode === 'insert' && info.overwriteAfter > 0) {
|
||||
// wants inserts but got replace-mode -> highlight AFTER range
|
||||
newDeco = [{
|
||||
range: new Range(position.lineNumber, position.column, position.lineNumber, position.column + info.overwriteAfter),
|
||||
options: { inlineClassName: 'suggest-insert-unexpected' }
|
||||
}];
|
||||
|
||||
} else if (opts.insertMode === 'replace' && info.overwriteAfter === 0) {
|
||||
// want replace but likely got insert -> highlight AFTER range
|
||||
const wordInfo = this._controller.editor.getModel()?.getWordAtPosition(position);
|
||||
if (wordInfo && wordInfo.endColumn > position.column) {
|
||||
newDeco = [{
|
||||
range: new Range(position.lineNumber, position.column, position.lineNumber, wordInfo.endColumn),
|
||||
options: { inlineClassName: 'suggest-insert-unexpected' }
|
||||
}];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update editor decorations
|
||||
this._decorations = this._controller.editor.deltaDecorations(this._decorations, newDeco);
|
||||
}
|
||||
}
|
||||
|
||||
const shiftKey = new class ShiftKey extends Emitter<boolean> {
|
||||
|
||||
private readonly _subscriptions = new DisposableStore();
|
||||
private _isPressed: boolean = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
domContentLoaded().then(() => {
|
||||
this._subscriptions.add(domEvent(document.body, 'keydown')(e => this.isPressed = e.shiftKey));
|
||||
this._subscriptions.add(domEvent(document.body, 'keyup')(() => this.isPressed = false));
|
||||
this._subscriptions.add(domEvent(document.body, 'mouseleave')(() => this.isPressed = false));
|
||||
this._subscriptions.add(domEvent(document.body, 'blur')(() => this.isPressed = false));
|
||||
});
|
||||
}
|
||||
|
||||
get isPressed(): boolean {
|
||||
return this._isPressed;
|
||||
}
|
||||
|
||||
set isPressed(value: boolean) {
|
||||
if (this._isPressed !== value) {
|
||||
this._isPressed = value;
|
||||
this.fire(value);
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._subscriptions.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { RunOnceScheduler, createCancelablePromise, CancelablePromise } from 'vs/base/common/async';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { DocumentRangeSemanticTokensProviderRegistry, DocumentRangeSemanticTokensProvider, SemanticTokens } from 'vs/editor/common/modes';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { toMultilineTokens2, SemanticTokensProviderStyling } from 'vs/editor/common/services/semanticTokensProviderStyling';
|
||||
|
||||
class ViewportSemanticTokensContribution extends Disposable implements IEditorContribution {
|
||||
|
||||
public static readonly ID = 'editor.contrib.viewportSemanticTokens';
|
||||
|
||||
public static get(editor: ICodeEditor): ViewportSemanticTokensContribution {
|
||||
return editor.getContribution<ViewportSemanticTokensContribution>(ViewportSemanticTokensContribution.ID);
|
||||
}
|
||||
|
||||
private readonly _editor: ICodeEditor;
|
||||
private readonly _tokenizeViewport: RunOnceScheduler;
|
||||
private _outstandingRequests: CancelablePromise<SemanticTokens | null | undefined>[];
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@IModelService private readonly _modelService: IModelService
|
||||
) {
|
||||
super();
|
||||
this._editor = editor;
|
||||
this._tokenizeViewport = new RunOnceScheduler(() => this._tokenizeViewportNow(), 100);
|
||||
this._outstandingRequests = [];
|
||||
this._register(this._editor.onDidScrollChange(() => {
|
||||
this._tokenizeViewport.schedule();
|
||||
}));
|
||||
this._register(this._editor.onDidChangeModel(() => {
|
||||
this._cancelAll();
|
||||
this._tokenizeViewport.schedule();
|
||||
}));
|
||||
this._register(this._editor.onDidChangeModelContent((e) => {
|
||||
this._cancelAll();
|
||||
this._tokenizeViewport.schedule();
|
||||
}));
|
||||
this._register(DocumentRangeSemanticTokensProviderRegistry.onDidChange(() => {
|
||||
this._cancelAll();
|
||||
this._tokenizeViewport.schedule();
|
||||
}));
|
||||
}
|
||||
|
||||
private static _getSemanticColoringProvider(model: ITextModel): DocumentRangeSemanticTokensProvider | null {
|
||||
const result = DocumentRangeSemanticTokensProviderRegistry.ordered(model);
|
||||
return (result.length > 0 ? result[0] : null);
|
||||
}
|
||||
|
||||
private _cancelAll(): void {
|
||||
for (const request of this._outstandingRequests) {
|
||||
request.cancel();
|
||||
}
|
||||
this._outstandingRequests = [];
|
||||
}
|
||||
|
||||
private _removeOutstandingRequest(req: CancelablePromise<SemanticTokens | null | undefined>): void {
|
||||
for (let i = 0, len = this._outstandingRequests.length; i < len; i++) {
|
||||
if (this._outstandingRequests[i] === req) {
|
||||
this._outstandingRequests.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _tokenizeViewportNow(): void {
|
||||
if (!this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
const model = this._editor.getModel();
|
||||
if (model.hasSemanticTokens()) {
|
||||
return;
|
||||
}
|
||||
const provider = ViewportSemanticTokensContribution._getSemanticColoringProvider(model);
|
||||
if (!provider) {
|
||||
return;
|
||||
}
|
||||
const styling = this._modelService.getSemanticTokensProviderStyling(provider);
|
||||
const visibleRanges = this._editor.getVisibleRanges();
|
||||
|
||||
this._outstandingRequests = this._outstandingRequests.concat(visibleRanges.map(range => this._requestRange(model, range, provider, styling)));
|
||||
}
|
||||
|
||||
private _requestRange(model: ITextModel, range: Range, provider: DocumentRangeSemanticTokensProvider, styling: SemanticTokensProviderStyling): CancelablePromise<SemanticTokens | null | undefined> {
|
||||
const requestVersionId = model.getVersionId();
|
||||
const request = createCancelablePromise(token => Promise.resolve(provider.provideDocumentRangeSemanticTokens(model, range, token)));
|
||||
request.then((r) => {
|
||||
if (!r || model.isDisposed() || model.getVersionId() !== requestVersionId) {
|
||||
return;
|
||||
}
|
||||
model.setPartialSemanticTokens(range, toMultilineTokens2(r, styling, model.getLanguageIdentifier()));
|
||||
}).then(() => this._removeOutstandingRequest(request), () => this._removeOutstandingRequest(request));
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorContribution(ViewportSemanticTokensContribution.ID, ViewportSemanticTokensContribution);
|
||||
@@ -37,7 +37,7 @@ suite('WordPartOperations', () => {
|
||||
test('cursorWordPartLeft - basic', () => {
|
||||
const EXPECTED = [
|
||||
'|start| |line|',
|
||||
'|this|Is|A|Camel|Case|Var| |this|_is|_a|_snake|_case|_var| |THIS|_IS|_CAPS|_SNAKE| |this|_IS|Mixed|Use|',
|
||||
'|this|Is|A|Camel|Case|Var| |this_|is_|a_|snake_|case_|var| |THIS_|IS_|CAPS_|SNAKE| |this_|IS|Mixed|Use|',
|
||||
'|end| |line'
|
||||
].join('\n');
|
||||
const [text,] = deserializePipePositions(EXPECTED);
|
||||
@@ -67,7 +67,7 @@ suite('WordPartOperations', () => {
|
||||
});
|
||||
|
||||
test('cursorWordPartLeft - issue #53899: underscores', () => {
|
||||
const EXPECTED = '|myvar| |=| |\'|demonstration|_____of| |selection| |with| |space|\'';
|
||||
const EXPECTED = '|myvar| |=| |\'|demonstration_____|of| |selection| |with| |space|\'';
|
||||
const [text,] = deserializePipePositions(EXPECTED);
|
||||
const actualStops = testRepeatedActionAndExtractPositions(
|
||||
text,
|
||||
@@ -83,7 +83,7 @@ suite('WordPartOperations', () => {
|
||||
test('cursorWordPartRight - basic', () => {
|
||||
const EXPECTED = [
|
||||
'start| |line|',
|
||||
'|this|Is|A|Camel|Case|Var| |this_|is_|a_|snake_|case_|var| |THIS_|IS_|CAPS_|SNAKE| |this_|IS|Mixed|Use|',
|
||||
'|this|Is|A|Camel|Case|Var| |this|_is|_a|_snake|_case|_var| |THIS|_IS|_CAPS|_SNAKE| |this|_IS|Mixed|Use|',
|
||||
'|end| |line|'
|
||||
].join('\n');
|
||||
const [text,] = deserializePipePositions(EXPECTED);
|
||||
@@ -113,7 +113,7 @@ suite('WordPartOperations', () => {
|
||||
});
|
||||
|
||||
test('cursorWordPartRight - issue #53899: underscores', () => {
|
||||
const EXPECTED = 'myvar| |=| |\'|demonstration_____|of| |selection| |with| |space|\'|';
|
||||
const EXPECTED = 'myvar| |=| |\'|demonstration|_____of| |selection| |with| |space|\'|';
|
||||
const [text,] = deserializePipePositions(EXPECTED);
|
||||
const actualStops = testRepeatedActionAndExtractPositions(
|
||||
text,
|
||||
@@ -145,8 +145,40 @@ suite('WordPartOperations', () => {
|
||||
assert.deepEqual(actual, EXPECTED);
|
||||
});
|
||||
|
||||
test('issue #93239 - cursorWordPartRight', () => {
|
||||
const EXPECTED = [
|
||||
'foo|_bar|',
|
||||
].join('\n');
|
||||
const [text,] = deserializePipePositions(EXPECTED);
|
||||
const actualStops = testRepeatedActionAndExtractPositions(
|
||||
text,
|
||||
new Position(1, 1),
|
||||
ed => cursorWordPartRight(ed),
|
||||
ed => ed.getPosition()!,
|
||||
ed => ed.getPosition()!.equals(new Position(1, 8))
|
||||
);
|
||||
const actual = serializePipePositions(text, actualStops);
|
||||
assert.deepEqual(actual, EXPECTED);
|
||||
});
|
||||
|
||||
test('issue #93239 - cursorWordPartLeft', () => {
|
||||
const EXPECTED = [
|
||||
'|foo_|bar',
|
||||
].join('\n');
|
||||
const [text,] = deserializePipePositions(EXPECTED);
|
||||
const actualStops = testRepeatedActionAndExtractPositions(
|
||||
text,
|
||||
new Position(1, 8),
|
||||
ed => cursorWordPartLeft(ed),
|
||||
ed => ed.getPosition()!,
|
||||
ed => ed.getPosition()!.equals(new Position(1, 1))
|
||||
);
|
||||
const actual = serializePipePositions(text, actualStops);
|
||||
assert.deepEqual(actual, EXPECTED);
|
||||
});
|
||||
|
||||
test('deleteWordPartLeft - basic', () => {
|
||||
const EXPECTED = '| |/*| |Just| |some| |text| |a|+=| |3| |+|5|-|3| |*/| |this|Is|A|Camel|Case|Var| |this|_is|_a|_snake|_case|_var| |THIS|_IS|_CAPS|_SNAKE| |this|_IS|Mixed|Use';
|
||||
const EXPECTED = '| |/*| |Just| |some| |text| |a|+=| |3| |+|5|-|3| |*/| |this|Is|A|Camel|Case|Var| |this_|is_|a_|snake_|case_|var| |THIS_|IS_|CAPS_|SNAKE| |this_|IS|Mixed|Use';
|
||||
const [text,] = deserializePipePositions(EXPECTED);
|
||||
const actualStops = testRepeatedActionAndExtractPositions(
|
||||
text,
|
||||
@@ -160,7 +192,7 @@ suite('WordPartOperations', () => {
|
||||
});
|
||||
|
||||
test('deleteWordPartRight - basic', () => {
|
||||
const EXPECTED = ' |/*| |Just| |some| |text| |a|+=| |3| |+|5|-|3| |*/| |this|Is|A|Camel|Case|Var| |this_|is_|a_|snake_|case_|var| |THIS_|IS_|CAPS_|SNAKE| |this_|IS|Mixed|Use|';
|
||||
const EXPECTED = ' |/*| |Just| |some| |text| |a|+=| |3| |+|5|-|3| |*/| |this|Is|A|Camel|Case|Var| |this|_is|_a|_snake|_case|_var| |THIS|_IS|_CAPS|_SNAKE| |this|_IS|Mixed|Use|';
|
||||
const [text,] = deserializePipePositions(EXPECTED);
|
||||
const actualStops = testRepeatedActionAndExtractPositions(
|
||||
text,
|
||||
|
||||
@@ -22,6 +22,7 @@ import 'vs/editor/contrib/find/findController';
|
||||
import 'vs/editor/contrib/folding/folding';
|
||||
import 'vs/editor/contrib/fontZoom/fontZoom';
|
||||
import 'vs/editor/contrib/format/formatActions';
|
||||
import 'vs/editor/contrib/gotoSymbol/documentSymbols';
|
||||
import 'vs/editor/contrib/gotoSymbol/goToCommands';
|
||||
import 'vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition';
|
||||
import 'vs/editor/contrib/gotoError/gotoError';
|
||||
@@ -39,6 +40,7 @@ import 'vs/editor/contrib/snippet/snippetController2';
|
||||
import 'vs/editor/contrib/suggest/suggestController';
|
||||
import 'vs/editor/contrib/tokenization/tokenization';
|
||||
import 'vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode';
|
||||
import 'vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens';
|
||||
import 'vs/editor/contrib/wordHighlighter/wordHighlighter';
|
||||
import 'vs/editor/contrib/wordOperations/wordOperations';
|
||||
import 'vs/editor/contrib/wordPartOperations/wordPartOperations';
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { MultilineTokens2, SparseEncodedTokens } from 'vs/editor/common/model/tokensStore';
|
||||
import { MultilineTokens2, SparseEncodedTokens, TokensStore2 } from 'vs/editor/common/model/tokensStore';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { TextModel } from 'vs/editor/common/model/textModel';
|
||||
import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model';
|
||||
import { MetadataConsts, TokenMetadata } from 'vs/editor/common/modes';
|
||||
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
|
||||
import { LineTokens } from 'vs/editor/common/core/lineTokens';
|
||||
|
||||
suite('TokensStore', () => {
|
||||
|
||||
@@ -98,7 +99,7 @@ suite('TokensStore', () => {
|
||||
function testTokensAdjustment(rawInitialState: string[], edits: IIdentifiedSingleEditOperation[], rawFinalState: string[]) {
|
||||
const initialState = parseTokensState(rawInitialState);
|
||||
const model = createTextModel(initialState.text);
|
||||
model.setSemanticTokens([initialState.tokens]);
|
||||
model.setSemanticTokens([initialState.tokens], true);
|
||||
|
||||
model.applyEdits(edits);
|
||||
|
||||
@@ -183,7 +184,7 @@ suite('TokensStore', () => {
|
||||
0, 38, 42, 245768,
|
||||
0, 43, 47, 180232,
|
||||
])))
|
||||
]);
|
||||
], true);
|
||||
const lineTokens = model.getLineTokens(1);
|
||||
let decodedTokens: number[] = [];
|
||||
for (let i = 0, len = lineTokens.getCount(); i < len; i++) {
|
||||
@@ -212,4 +213,114 @@ suite('TokensStore', () => {
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
test('partial tokens 1', () => {
|
||||
const store = new TokensStore2();
|
||||
|
||||
// setPartial: [1,1 -> 31,2], [(5,5-10),(10,5-10),(15,5-10),(20,5-10),(25,5-10),(30,5-10)]
|
||||
store.setPartial(new Range(1, 1, 31, 2), [
|
||||
new MultilineTokens2(5, new SparseEncodedTokens(new Uint32Array([
|
||||
0, 5, 10, 1,
|
||||
5, 5, 10, 2,
|
||||
10, 5, 10, 3,
|
||||
15, 5, 10, 4,
|
||||
20, 5, 10, 5,
|
||||
25, 5, 10, 6,
|
||||
])))
|
||||
]);
|
||||
|
||||
// setPartial: [18,1 -> 42,1], [(20,5-10),(25,5-10),(30,5-10),(35,5-10),(40,5-10)]
|
||||
store.setPartial(new Range(18, 1, 42, 1), [
|
||||
new MultilineTokens2(20, new SparseEncodedTokens(new Uint32Array([
|
||||
0, 5, 10, 4,
|
||||
5, 5, 10, 5,
|
||||
10, 5, 10, 6,
|
||||
15, 5, 10, 7,
|
||||
20, 5, 10, 8,
|
||||
])))
|
||||
]);
|
||||
|
||||
// setPartial: [1,1 -> 31,2], [(5,5-10),(10,5-10),(15,5-10),(20,5-10),(25,5-10),(30,5-10)]
|
||||
store.setPartial(new Range(1, 1, 31, 2), [
|
||||
new MultilineTokens2(5, new SparseEncodedTokens(new Uint32Array([
|
||||
0, 5, 10, 1,
|
||||
5, 5, 10, 2,
|
||||
10, 5, 10, 3,
|
||||
15, 5, 10, 4,
|
||||
20, 5, 10, 5,
|
||||
25, 5, 10, 6,
|
||||
])))
|
||||
]);
|
||||
|
||||
const lineTokens = store.addSemanticTokens(10, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`));
|
||||
assert.equal(lineTokens.getCount(), 3);
|
||||
});
|
||||
|
||||
test('partial tokens 2', () => {
|
||||
const store = new TokensStore2();
|
||||
|
||||
// setPartial: [1,1 -> 31,2], [(5,5-10),(10,5-10),(15,5-10),(20,5-10),(25,5-10),(30,5-10)]
|
||||
store.setPartial(new Range(1, 1, 31, 2), [
|
||||
new MultilineTokens2(5, new SparseEncodedTokens(new Uint32Array([
|
||||
0, 5, 10, 1,
|
||||
5, 5, 10, 2,
|
||||
10, 5, 10, 3,
|
||||
15, 5, 10, 4,
|
||||
20, 5, 10, 5,
|
||||
25, 5, 10, 6,
|
||||
])))
|
||||
]);
|
||||
|
||||
// setPartial: [6,1 -> 36,2], [(10,5-10),(15,5-10),(20,5-10),(25,5-10),(30,5-10),(35,5-10)]
|
||||
store.setPartial(new Range(6, 1, 36, 2), [
|
||||
new MultilineTokens2(10, new SparseEncodedTokens(new Uint32Array([
|
||||
0, 5, 10, 2,
|
||||
5, 5, 10, 3,
|
||||
10, 5, 10, 4,
|
||||
15, 5, 10, 5,
|
||||
20, 5, 10, 6,
|
||||
])))
|
||||
]);
|
||||
|
||||
// setPartial: [17,1 -> 42,1], [(20,5-10),(25,5-10),(30,5-10),(35,5-10),(40,5-10)]
|
||||
store.setPartial(new Range(17, 1, 42, 1), [
|
||||
new MultilineTokens2(20, new SparseEncodedTokens(new Uint32Array([
|
||||
0, 5, 10, 4,
|
||||
5, 5, 10, 5,
|
||||
10, 5, 10, 6,
|
||||
15, 5, 10, 7,
|
||||
20, 5, 10, 8,
|
||||
])))
|
||||
]);
|
||||
|
||||
const lineTokens = store.addSemanticTokens(20, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`));
|
||||
assert.equal(lineTokens.getCount(), 3);
|
||||
});
|
||||
|
||||
test('partial tokens 3', () => {
|
||||
const store = new TokensStore2();
|
||||
|
||||
// setPartial: [1,1 -> 31,2], [(5,5-10),(10,5-10),(15,5-10),(20,5-10),(25,5-10),(30,5-10)]
|
||||
store.setPartial(new Range(1, 1, 31, 2), [
|
||||
new MultilineTokens2(5, new SparseEncodedTokens(new Uint32Array([
|
||||
0, 5, 10, 1,
|
||||
5, 5, 10, 2,
|
||||
10, 5, 10, 3,
|
||||
15, 5, 10, 4,
|
||||
20, 5, 10, 5,
|
||||
25, 5, 10, 6,
|
||||
])))
|
||||
]);
|
||||
|
||||
// setPartial: [11,1 -> 16,2], [(15,5-10),(20,5-10)]
|
||||
store.setPartial(new Range(11, 1, 16, 2), [
|
||||
new MultilineTokens2(10, new SparseEncodedTokens(new Uint32Array([
|
||||
0, 5, 10, 3,
|
||||
5, 5, 10, 4,
|
||||
])))
|
||||
]);
|
||||
|
||||
const lineTokens = store.addSemanticTokens(5, new LineTokens(new Uint32Array([12, 1]), `enum Enum1 {`));
|
||||
assert.equal(lineTokens.getCount(), 3);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -459,10 +459,10 @@ suite('viewLineRenderer.renderLine', () => {
|
||||
]);
|
||||
|
||||
let expectedOutput = [
|
||||
'<span class="mtk6" dir="ltr">var</span>',
|
||||
'<span class="mtk1" dir="ltr">\u00a0קודמות\u00a0=\u00a0</span>',
|
||||
'<span class="mtk20" dir="ltr">"מיותר\u00a0קודמות\u00a0צ\'ט\u00a0של,\u00a0אם\u00a0לשון\u00a0העברית\u00a0שינויים\u00a0ויש,\u00a0אם"</span>',
|
||||
'<span class="mtk1" dir="ltr">;</span>'
|
||||
'<span class="mtk6">var</span>',
|
||||
'<span class="mtk1">\u00a0קודמות\u00a0=\u00a0</span>',
|
||||
'<span class="mtk20">"מיותר\u00a0קודמות\u00a0צ\'ט\u00a0של,\u00a0אם\u00a0לשון\u00a0העברית\u00a0שינויים\u00a0ויש,\u00a0אם"</span>',
|
||||
'<span class="mtk1">;</span>'
|
||||
].join('');
|
||||
|
||||
let _actual = renderViewLine(new RenderLineInput(
|
||||
@@ -487,7 +487,7 @@ suite('viewLineRenderer.renderLine', () => {
|
||||
null
|
||||
));
|
||||
|
||||
assert.equal(_actual.html, '<span>' + expectedOutput + '</span>');
|
||||
assert.equal(_actual.html, '<span dir="ltr">' + expectedOutput + '</span>');
|
||||
assert.equal(_actual.containsRTL, true);
|
||||
});
|
||||
|
||||
@@ -676,7 +676,7 @@ suite('viewLineRenderer.renderLine', () => {
|
||||
let lineText = 'את גרמנית בהתייחסות שמו, שנתי המשפט אל חפש, אם כתב אחרים ולחבר. של התוכן אודות בויקיפדיה כלל, של עזרה כימיה היא. על עמוד יוצרים מיתולוגיה סדר, אם שכל שתפו לעברית שינויים, אם שאלות אנגלית עזה. שמות בקלות מה סדר.';
|
||||
let lineParts = createViewLineTokens([createPart(lineText.length, 1)]);
|
||||
let expectedOutput = [
|
||||
'<span class="mtk1" dir="ltr">את\u00a0גרמנית\u00a0בהתייחסות\u00a0שמו,\u00a0שנתי\u00a0המשפט\u00a0אל\u00a0חפש,\u00a0אם\u00a0כתב\u00a0אחרים\u00a0ולחבר.\u00a0של\u00a0התוכן\u00a0אודות\u00a0בויקיפדיה\u00a0כלל,\u00a0של\u00a0עזרה\u00a0כימיה\u00a0היא.\u00a0על\u00a0עמוד\u00a0יוצרים\u00a0מיתולוגיה\u00a0סדר,\u00a0אם\u00a0שכל\u00a0שתפו\u00a0לעברית\u00a0שינויים,\u00a0אם\u00a0שאלות\u00a0אנגלית\u00a0עזה.\u00a0שמות\u00a0בקלות\u00a0מה\u00a0סדר.</span>'
|
||||
'<span class="mtk1">את\u00a0גרמנית\u00a0בהתייחסות\u00a0שמו,\u00a0שנתי\u00a0המשפט\u00a0אל\u00a0חפש,\u00a0אם\u00a0כתב\u00a0אחרים\u00a0ולחבר.\u00a0של\u00a0התוכן\u00a0אודות\u00a0בויקיפדיה\u00a0כלל,\u00a0של\u00a0עזרה\u00a0כימיה\u00a0היא.\u00a0על\u00a0עמוד\u00a0יוצרים\u00a0מיתולוגיה\u00a0סדר,\u00a0אם\u00a0שכל\u00a0שתפו\u00a0לעברית\u00a0שינויים,\u00a0אם\u00a0שאלות\u00a0אנגלית\u00a0עזה.\u00a0שמות\u00a0בקלות\u00a0מה\u00a0סדר.</span>'
|
||||
];
|
||||
let actual = renderViewLine(new RenderLineInput(
|
||||
false,
|
||||
@@ -699,7 +699,7 @@ suite('viewLineRenderer.renderLine', () => {
|
||||
false,
|
||||
null
|
||||
));
|
||||
assert.equal(actual.html, '<span>' + expectedOutput.join('') + '</span>');
|
||||
assert.equal(actual.html, '<span dir="ltr">' + expectedOutput.join('') + '</span>');
|
||||
assert.equal(actual.containsRTL, true);
|
||||
});
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ function parseTest(fileName: string): ITest {
|
||||
return { content, assertions };
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
function executeTest(fileName: string, parseFunc: IParseFunc): void {
|
||||
const { content, assertions } = parseTest(fileName);
|
||||
const actual = parseFunc(content);
|
||||
|
||||
Vendored
-4
@@ -3649,10 +3649,6 @@ declare namespace monaco.editor {
|
||||
* Overwrite word ends on accept. Default to false.
|
||||
*/
|
||||
insertMode?: 'insert' | 'replace';
|
||||
/**
|
||||
* Show a highlight when suggestion replaces or keep text after the cursor. Defaults to false.
|
||||
*/
|
||||
insertHighlight?: boolean;
|
||||
/**
|
||||
* Enable graceful matching. Defaults to true.
|
||||
*/
|
||||
|
||||
+8
-1
@@ -14,6 +14,13 @@
|
||||
*---------------------------------------------------------------------------------------------
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
var __spreadArrays = (this && this.__spreadArrays) || function () {
|
||||
for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;
|
||||
for (var r = Array(s), k = 0, i = 0; i < il; i++)
|
||||
for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)
|
||||
r[k] = a[j];
|
||||
return r;
|
||||
};
|
||||
var NLSLoaderPlugin;
|
||||
(function (NLSLoaderPlugin) {
|
||||
var Environment = /** @class */ (function () {
|
||||
@@ -94,7 +101,7 @@ var NLSLoaderPlugin;
|
||||
for (var _i = 2; _i < arguments.length; _i++) {
|
||||
args[_i - 2] = arguments[_i];
|
||||
}
|
||||
return localize.apply(void 0, [_this._env, data, message].concat(args));
|
||||
return localize.apply(void 0, __spreadArrays([_this._env, data, message], args));
|
||||
};
|
||||
}
|
||||
NLSPlugin.prototype.setPseudoTranslation = function (value) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { IWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
|
||||
|
||||
@@ -15,6 +15,12 @@ export interface IWorkspaceBackupInfo {
|
||||
remoteAuthority?: string;
|
||||
}
|
||||
|
||||
export function isWorkspaceBackupInfo(obj: unknown): obj is IWorkspaceBackupInfo {
|
||||
const candidate = obj as IWorkspaceBackupInfo;
|
||||
|
||||
return candidate && isWorkspaceIdentifier(candidate.workspace);
|
||||
}
|
||||
|
||||
export interface IBackupMainService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
@@ -31,4 +37,12 @@ export interface IBackupMainService {
|
||||
unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void;
|
||||
unregisterFolderBackupSync(folderUri: URI): void;
|
||||
unregisterEmptyWindowBackupSync(backupFolder: string): void;
|
||||
|
||||
/**
|
||||
* All folders or workspaces that are known to have
|
||||
* backups stored. This call is long running because
|
||||
* it checks for each backup location if any backups
|
||||
* are stored.
|
||||
*/
|
||||
getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>>;
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@ import * as crypto from 'crypto';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { writeFileSync, writeFile, readFile, readdir, exists, rimraf, rename, RimRafMode } from 'vs/base/node/pfs';
|
||||
import * as arrays from 'vs/base/common/arrays';
|
||||
import { IBackupMainService, IWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup';
|
||||
import { IBackupMainService, IWorkspaceBackupInfo, isWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup';
|
||||
import { IBackupWorkspacesFormat, IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
@@ -28,9 +27,9 @@ export class BackupMainService implements IBackupMainService {
|
||||
protected backupHome: string;
|
||||
protected workspacesJsonPath: string;
|
||||
|
||||
private rootWorkspaces: IWorkspaceBackupInfo[] = [];
|
||||
private folderWorkspaces: URI[] = [];
|
||||
private emptyWorkspaces: IEmptyWindowBackupInfo[] = [];
|
||||
private workspaces: IWorkspaceBackupInfo[] = [];
|
||||
private folders: URI[] = [];
|
||||
private emptyWindows: IEmptyWindowBackupInfo[] = [];
|
||||
|
||||
constructor(
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@@ -51,31 +50,31 @@ export class BackupMainService implements IBackupMainService {
|
||||
|
||||
// read empty workspaces backups first
|
||||
if (backups.emptyWorkspaceInfos) {
|
||||
this.emptyWorkspaces = await this.validateEmptyWorkspaces(backups.emptyWorkspaceInfos);
|
||||
this.emptyWindows = await this.validateEmptyWorkspaces(backups.emptyWorkspaceInfos);
|
||||
} else if (Array.isArray(backups.emptyWorkspaces)) {
|
||||
// read legacy entries
|
||||
this.emptyWorkspaces = await this.validateEmptyWorkspaces(backups.emptyWorkspaces.map(backupFolder => ({ backupFolder })));
|
||||
this.emptyWindows = await this.validateEmptyWorkspaces(backups.emptyWorkspaces.map(emptyWindow => ({ backupFolder: emptyWindow })));
|
||||
}
|
||||
|
||||
// read workspace backups
|
||||
let rootWorkspaces: IWorkspaceBackupInfo[] = [];
|
||||
try {
|
||||
if (Array.isArray(backups.rootURIWorkspaces)) {
|
||||
rootWorkspaces = backups.rootURIWorkspaces.map(f => ({ workspace: { id: f.id, configPath: URI.parse(f.configURIPath) }, remoteAuthority: f.remoteAuthority }));
|
||||
rootWorkspaces = backups.rootURIWorkspaces.map(workspace => ({ workspace: { id: workspace.id, configPath: URI.parse(workspace.configURIPath) }, remoteAuthority: workspace.remoteAuthority }));
|
||||
} else if (Array.isArray(backups.rootWorkspaces)) {
|
||||
rootWorkspaces = backups.rootWorkspaces.map(f => ({ workspace: { id: f.id, configPath: URI.file(f.configPath) } }));
|
||||
rootWorkspaces = backups.rootWorkspaces.map(workspace => ({ workspace: { id: workspace.id, configPath: URI.file(workspace.configPath) } }));
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore URI parsing exceptions
|
||||
}
|
||||
|
||||
this.rootWorkspaces = await this.validateWorkspaces(rootWorkspaces);
|
||||
this.workspaces = await this.validateWorkspaces(rootWorkspaces);
|
||||
|
||||
// read folder backups
|
||||
let workspaceFolders: URI[] = [];
|
||||
try {
|
||||
if (Array.isArray(backups.folderURIWorkspaces)) {
|
||||
workspaceFolders = backups.folderURIWorkspaces.map(f => URI.parse(f));
|
||||
workspaceFolders = backups.folderURIWorkspaces.map(folder => URI.parse(folder));
|
||||
} else if (Array.isArray(backups.folderWorkspaces)) {
|
||||
// migrate legacy folder paths
|
||||
workspaceFolders = [];
|
||||
@@ -93,7 +92,7 @@ export class BackupMainService implements IBackupMainService {
|
||||
// ignore URI parsing exceptions
|
||||
}
|
||||
|
||||
this.folderWorkspaces = await this.validateFolders(workspaceFolders);
|
||||
this.folders = await this.validateFolders(workspaceFolders);
|
||||
|
||||
// save again in case some workspaces or folders have been removed
|
||||
await this.save();
|
||||
@@ -106,7 +105,7 @@ export class BackupMainService implements IBackupMainService {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.rootWorkspaces.slice(0); // return a copy
|
||||
return this.workspaces.slice(0); // return a copy
|
||||
}
|
||||
|
||||
getFolderBackupPaths(): URI[] {
|
||||
@@ -116,7 +115,7 @@ export class BackupMainService implements IBackupMainService {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.folderWorkspaces.slice(0); // return a copy
|
||||
return this.folders.slice(0); // return a copy
|
||||
}
|
||||
|
||||
isHotExitEnabled(): boolean {
|
||||
@@ -134,12 +133,12 @@ export class BackupMainService implements IBackupMainService {
|
||||
}
|
||||
|
||||
getEmptyWindowBackupPaths(): IEmptyWindowBackupInfo[] {
|
||||
return this.emptyWorkspaces.slice(0); // return a copy
|
||||
return this.emptyWindows.slice(0); // return a copy
|
||||
}
|
||||
|
||||
registerWorkspaceBackupSync(workspaceInfo: IWorkspaceBackupInfo, migrateFrom?: string): string {
|
||||
if (!this.rootWorkspaces.some(window => workspaceInfo.workspace.id === window.workspace.id)) {
|
||||
this.rootWorkspaces.push(workspaceInfo);
|
||||
if (!this.workspaces.some(workspace => workspaceInfo.workspace.id === workspace.workspace.id)) {
|
||||
this.workspaces.push(workspaceInfo);
|
||||
this.saveSync();
|
||||
}
|
||||
|
||||
@@ -188,16 +187,16 @@ export class BackupMainService implements IBackupMainService {
|
||||
|
||||
unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void {
|
||||
const id = workspace.id;
|
||||
let index = arrays.firstIndex(this.rootWorkspaces, w => w.workspace.id === id);
|
||||
const index = this.workspaces.findIndex(workspace => workspace.workspace.id === id);
|
||||
if (index !== -1) {
|
||||
this.rootWorkspaces.splice(index, 1);
|
||||
this.workspaces.splice(index, 1);
|
||||
this.saveSync();
|
||||
}
|
||||
}
|
||||
|
||||
registerFolderBackupSync(folderUri: URI): string {
|
||||
if (!this.folderWorkspaces.some(uri => areResourcesEquals(folderUri, uri))) {
|
||||
this.folderWorkspaces.push(folderUri);
|
||||
if (!this.folders.some(folder => areResourcesEquals(folderUri, folder))) {
|
||||
this.folders.push(folderUri);
|
||||
this.saveSync();
|
||||
}
|
||||
|
||||
@@ -205,9 +204,9 @@ export class BackupMainService implements IBackupMainService {
|
||||
}
|
||||
|
||||
unregisterFolderBackupSync(folderUri: URI): void {
|
||||
let index = arrays.firstIndex(this.folderWorkspaces, uri => areResourcesEquals(folderUri, uri));
|
||||
const index = this.folders.findIndex(folder => areResourcesEquals(folderUri, folder));
|
||||
if (index !== -1) {
|
||||
this.folderWorkspaces.splice(index, 1);
|
||||
this.folders.splice(index, 1);
|
||||
this.saveSync();
|
||||
}
|
||||
}
|
||||
@@ -216,8 +215,8 @@ export class BackupMainService implements IBackupMainService {
|
||||
|
||||
// Generate a new folder if this is a new empty workspace
|
||||
const backupFolder = backupFolderCandidate || this.getRandomEmptyWindowId();
|
||||
if (!this.emptyWorkspaces.some(window => !!window.backupFolder && isEqual(window.backupFolder, backupFolder, !platform.isLinux))) {
|
||||
this.emptyWorkspaces.push({ backupFolder, remoteAuthority });
|
||||
if (!this.emptyWindows.some(emptyWindow => !!emptyWindow.backupFolder && isEqual(emptyWindow.backupFolder, backupFolder, !platform.isLinux))) {
|
||||
this.emptyWindows.push({ backupFolder, remoteAuthority });
|
||||
this.saveSync();
|
||||
}
|
||||
|
||||
@@ -225,9 +224,9 @@ export class BackupMainService implements IBackupMainService {
|
||||
}
|
||||
|
||||
unregisterEmptyWindowBackupSync(backupFolder: string): void {
|
||||
let index = arrays.firstIndex(this.emptyWorkspaces, w => !!w.backupFolder && isEqual(w.backupFolder, backupFolder, !platform.isLinux));
|
||||
const index = this.emptyWindows.findIndex(emptyWindow => !!emptyWindow.backupFolder && isEqual(emptyWindow.backupFolder, backupFolder, !platform.isLinux));
|
||||
if (index !== -1) {
|
||||
this.emptyWorkspaces.splice(index, 1);
|
||||
this.emptyWindows.splice(index, 1);
|
||||
this.saveSync();
|
||||
}
|
||||
}
|
||||
@@ -255,7 +254,7 @@ export class BackupMainService implements IBackupMainService {
|
||||
seenIds.add(workspace.id);
|
||||
|
||||
const backupPath = this.getBackupPath(workspace.id);
|
||||
const hasBackups = await this.hasBackups(backupPath);
|
||||
const hasBackups = await this.doHasBackups(backupPath);
|
||||
|
||||
// If the workspace has no backups, ignore it
|
||||
if (hasBackups) {
|
||||
@@ -287,7 +286,7 @@ export class BackupMainService implements IBackupMainService {
|
||||
seenIds.add(key);
|
||||
|
||||
const backupPath = this.getBackupPath(this.getFolderHash(folderURI));
|
||||
const hasBackups = await this.hasBackups(backupPath);
|
||||
const hasBackups = await this.doHasBackups(backupPath);
|
||||
|
||||
// If the folder has no backups, ignore it
|
||||
if (hasBackups) {
|
||||
@@ -325,7 +324,7 @@ export class BackupMainService implements IBackupMainService {
|
||||
seenIds.add(backupFolder);
|
||||
|
||||
const backupPath = this.getBackupPath(backupFolder);
|
||||
if (await this.hasBackups(backupPath)) {
|
||||
if (await this.doHasBackups(backupPath)) {
|
||||
result.push(backupInfo);
|
||||
} else {
|
||||
await this.deleteStaleBackup(backupPath);
|
||||
@@ -350,7 +349,7 @@ export class BackupMainService implements IBackupMainService {
|
||||
|
||||
// New empty window backup
|
||||
let newBackupFolder = this.getRandomEmptyWindowId();
|
||||
while (this.emptyWorkspaces.some(window => !!window.backupFolder && isEqual(window.backupFolder, newBackupFolder, platform.isLinux))) {
|
||||
while (this.emptyWindows.some(emptyWindow => !!emptyWindow.backupFolder && isEqual(emptyWindow.backupFolder, newBackupFolder, platform.isLinux))) {
|
||||
newBackupFolder = this.getRandomEmptyWindowId();
|
||||
}
|
||||
|
||||
@@ -362,7 +361,7 @@ export class BackupMainService implements IBackupMainService {
|
||||
this.logService.error(`Backup: Could not rename backup folder: ${ex.toString()}`);
|
||||
return false;
|
||||
}
|
||||
this.emptyWorkspaces.push({ backupFolder: newBackupFolder });
|
||||
this.emptyWindows.push({ backupFolder: newBackupFolder });
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -371,7 +370,7 @@ export class BackupMainService implements IBackupMainService {
|
||||
|
||||
// New empty window backup
|
||||
let newBackupFolder = this.getRandomEmptyWindowId();
|
||||
while (this.emptyWorkspaces.some(window => !!window.backupFolder && isEqual(window.backupFolder, newBackupFolder, platform.isLinux))) {
|
||||
while (this.emptyWindows.some(emptyWindow => !!emptyWindow.backupFolder && isEqual(emptyWindow.backupFolder, newBackupFolder, platform.isLinux))) {
|
||||
newBackupFolder = this.getRandomEmptyWindowId();
|
||||
}
|
||||
|
||||
@@ -383,12 +382,53 @@ export class BackupMainService implements IBackupMainService {
|
||||
this.logService.error(`Backup: Could not rename backup folder: ${ex.toString()}`);
|
||||
return false;
|
||||
}
|
||||
this.emptyWorkspaces.push({ backupFolder: newBackupFolder });
|
||||
this.emptyWindows.push({ backupFolder: newBackupFolder });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async hasBackups(backupPath: string): Promise<boolean> {
|
||||
async getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>> {
|
||||
const dirtyWorkspaces: Array<IWorkspaceIdentifier | URI> = [];
|
||||
|
||||
// Workspaces with backups
|
||||
for (const workspace of this.workspaces) {
|
||||
if ((await this.hasBackups(workspace))) {
|
||||
dirtyWorkspaces.push(workspace.workspace);
|
||||
}
|
||||
}
|
||||
|
||||
// Folders with backups
|
||||
for (const folder of this.folders) {
|
||||
if ((await this.hasBackups(folder))) {
|
||||
dirtyWorkspaces.push(folder);
|
||||
}
|
||||
}
|
||||
|
||||
return dirtyWorkspaces;
|
||||
}
|
||||
|
||||
private hasBackups(backupLocation: IWorkspaceBackupInfo | IEmptyWindowBackupInfo | URI): Promise<boolean> {
|
||||
let backupPath: string;
|
||||
|
||||
// Folder
|
||||
if (URI.isUri(backupLocation)) {
|
||||
backupPath = this.getBackupPath(this.getFolderHash(backupLocation));
|
||||
}
|
||||
|
||||
// Workspace
|
||||
else if (isWorkspaceBackupInfo(backupLocation)) {
|
||||
backupPath = this.getBackupPath(backupLocation.workspace.id);
|
||||
}
|
||||
|
||||
// Empty
|
||||
else {
|
||||
backupPath = backupLocation.backupFolder;
|
||||
}
|
||||
|
||||
return this.doHasBackups(backupPath);
|
||||
}
|
||||
|
||||
private async doHasBackups(backupPath: string): Promise<boolean> {
|
||||
try {
|
||||
const backupSchemas = await readdir(backupPath);
|
||||
|
||||
@@ -427,10 +467,10 @@ export class BackupMainService implements IBackupMainService {
|
||||
|
||||
private serializeBackups(): IBackupWorkspacesFormat {
|
||||
return {
|
||||
rootURIWorkspaces: this.rootWorkspaces.map(f => ({ id: f.workspace.id, configURIPath: f.workspace.configPath.toString(), remoteAuthority: f.remoteAuthority })),
|
||||
folderURIWorkspaces: this.folderWorkspaces.map(f => f.toString()),
|
||||
emptyWorkspaceInfos: this.emptyWorkspaces,
|
||||
emptyWorkspaces: this.emptyWorkspaces.map(info => info.backupFolder)
|
||||
rootURIWorkspaces: this.workspaces.map(workspace => ({ id: workspace.workspace.id, configURIPath: workspace.workspace.configPath.toString(), remoteAuthority: workspace.remoteAuthority })),
|
||||
folderURIWorkspaces: this.folders.map(folder => folder.toString()),
|
||||
emptyWorkspaceInfos: this.emptyWindows,
|
||||
emptyWorkspaces: this.emptyWindows.map(emptyWindow => emptyWindow.backupFolder)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { createHash } from 'crypto';
|
||||
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { isEqual } from 'vs/base/common/resources';
|
||||
|
||||
suite('BackupMainService', () => {
|
||||
|
||||
@@ -731,4 +732,45 @@ suite('BackupMainService', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
suite('getDirtyWorkspaces', () => {
|
||||
test('should report if a workspace or folder has backups', async () => {
|
||||
const folderBackupPath = service.registerFolderBackupSync(fooFile);
|
||||
|
||||
const backupWorkspaceInfo = toWorkspaceBackupInfo(fooFile.fsPath);
|
||||
const workspaceBackupPath = service.registerWorkspaceBackupSync(backupWorkspaceInfo);
|
||||
|
||||
assert.equal(((await service.getDirtyWorkspaces()).length), 0);
|
||||
|
||||
try {
|
||||
await pfs.mkdirp(path.join(folderBackupPath, Schemas.file));
|
||||
await pfs.mkdirp(path.join(workspaceBackupPath, Schemas.untitled));
|
||||
} catch (error) {
|
||||
// ignore - folder might exist already
|
||||
}
|
||||
|
||||
assert.equal(((await service.getDirtyWorkspaces()).length), 0);
|
||||
|
||||
fs.writeFileSync(path.join(folderBackupPath, Schemas.file, '594a4a9d82a277a899d4713a5b08f504'), '');
|
||||
fs.writeFileSync(path.join(workspaceBackupPath, Schemas.untitled, '594a4a9d82a277a899d4713a5b08f504'), '');
|
||||
|
||||
const dirtyWorkspaces = await service.getDirtyWorkspaces();
|
||||
assert.equal(dirtyWorkspaces.length, 2);
|
||||
|
||||
let found = 0;
|
||||
for (const dirtyWorkpspace of dirtyWorkspaces) {
|
||||
if (URI.isUri(dirtyWorkpspace)) {
|
||||
if (isEqual(fooFile, dirtyWorkpspace)) {
|
||||
found++;
|
||||
}
|
||||
} else {
|
||||
if (isEqual(backupWorkspaceInfo.workspace.configPath, dirtyWorkpspace.configPath)) {
|
||||
found++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.equal(found, 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,8 +54,7 @@ export class BrowserClipboardService implements IClipboardService {
|
||||
}
|
||||
|
||||
readFindText(): string {
|
||||
// @ts-ignore
|
||||
return undefined;
|
||||
return undefined; // {{SQL CARBON EDIT}} strict-null-checks
|
||||
}
|
||||
|
||||
writeFindText(text: string): void { }
|
||||
|
||||
@@ -29,7 +29,6 @@ export class ConfigurationService extends Disposable implements IConfigurationSe
|
||||
fileService: IFileService
|
||||
) {
|
||||
super();
|
||||
this._register(fileService.watch(settingsResource));
|
||||
this.userConfiguration = this._register(new UserSettings(this.settingsResource, undefined, fileService));
|
||||
this.configuration = new Configuration(new DefaultConfigurationModel(), new ConfigurationModel());
|
||||
|
||||
|
||||
@@ -65,7 +65,8 @@ export class ElectronMainService implements IElectronMainService {
|
||||
workspace: window.openedWorkspace,
|
||||
folderUri: window.openedFolderUri,
|
||||
title: window.win.getTitle(),
|
||||
filename: window.getRepresentedFilename()
|
||||
filename: window.getRepresentedFilename(),
|
||||
dirty: window.isDocumentEdited()
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -271,7 +272,7 @@ export class ElectronMainService implements IElectronMainService {
|
||||
async setDocumentEdited(windowId: number | undefined, edited: boolean): Promise<void> {
|
||||
const window = this.windowById(windowId);
|
||||
if (window) {
|
||||
window.win.setDocumentEdited(edited);
|
||||
window.setDocumentEdited(edited);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ export interface ParsedArgs {
|
||||
'nolazy'?: boolean;
|
||||
'force-device-scale-factor'?: string;
|
||||
'force-renderer-accessibility'?: boolean;
|
||||
'ignore-certificate-error'?: boolean;
|
||||
'ignore-certificate-errors'?: boolean;
|
||||
'allow-insecure-localhost'?: boolean;
|
||||
}
|
||||
|
||||
@@ -154,6 +154,7 @@ export interface IEnvironmentService extends IUserHomeProvider {
|
||||
extensionsPath?: string;
|
||||
extensionDevelopmentLocationURI?: URI[];
|
||||
extensionTestsLocationURI?: URI;
|
||||
extensionEnabledProposedApi?: string[] | undefined;
|
||||
logExtensionHostCommunication?: boolean;
|
||||
|
||||
debugExtensionHost: IExtensionHostDebugParams;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as minimist from 'vscode-minimist';
|
||||
import * as minimist from 'minimist';
|
||||
import * as os from 'os';
|
||||
import { localize } from 'vs/nls';
|
||||
import { ParsedArgs } from 'vs/platform/environment/common/environment';
|
||||
@@ -128,7 +128,7 @@ export const OPTIONS: OptionDescriptions<Required<ParsedArgs>> = {
|
||||
'nolazy': { type: 'boolean' }, // node inspect
|
||||
'force-device-scale-factor': { type: 'string' },
|
||||
'force-renderer-accessibility': { type: 'boolean' },
|
||||
'ignore-certificate-error': { type: 'boolean' },
|
||||
'ignore-certificate-errors': { type: 'boolean' },
|
||||
'allow-insecure-localhost': { type: 'boolean' },
|
||||
'_urls': { type: 'string[]' },
|
||||
|
||||
|
||||
@@ -233,6 +233,18 @@ export class EnvironmentService implements IEnvironmentService {
|
||||
return false;
|
||||
}
|
||||
|
||||
get extensionEnabledProposedApi(): string[] | undefined {
|
||||
if (Array.isArray(this.args['enable-proposed-api'])) {
|
||||
return this.args['enable-proposed-api'];
|
||||
}
|
||||
|
||||
if ('enable-proposed-api' in this.args) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@memoize
|
||||
get debugExtensionHost(): IExtensionHostDebugParams { return parseExtensionHostPort(this._args, this.isBuilt); }
|
||||
@memoize
|
||||
|
||||
@@ -26,6 +26,7 @@ import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { ReadableStreamEvents, transform } from 'vs/base/common/stream';
|
||||
import { createReadStream } from 'vs/platform/files/common/io';
|
||||
import { insert } from 'vs/base/common/arrays';
|
||||
|
||||
export interface IWatcherOptions {
|
||||
pollingInterval?: number;
|
||||
@@ -524,7 +525,7 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
|
||||
// Add to list of folders to watch recursively
|
||||
const folderToWatch = { path: this.toFilePath(resource), excludes };
|
||||
this.recursiveFoldersToWatch.push(folderToWatch);
|
||||
const remove = insert(this.recursiveFoldersToWatch, folderToWatch);
|
||||
|
||||
// Trigger update
|
||||
this.refreshRecursiveWatchers();
|
||||
@@ -532,7 +533,7 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
return toDisposable(() => {
|
||||
|
||||
// Remove from list of folders to watch recursively
|
||||
this.recursiveFoldersToWatch.splice(this.recursiveFoldersToWatch.indexOf(folderToWatch), 1);
|
||||
remove();
|
||||
|
||||
// Trigger update
|
||||
this.refreshRecursiveWatchers();
|
||||
@@ -543,10 +544,8 @@ export class DiskFileSystemProvider extends Disposable implements
|
||||
|
||||
// Buffer requests for recursive watching to decide on right watcher
|
||||
// that supports potentially watching more than one folder at once
|
||||
this.recursiveWatchRequestDelayer.trigger(() => {
|
||||
this.recursiveWatchRequestDelayer.trigger(async () => {
|
||||
this.doRefreshRecursiveWatchers();
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
|
||||
import { OutOfProcessWin32FolderWatcher } from 'vs/platform/files/node/watcher/win32/csharpWatcherService';
|
||||
import { posix } from 'vs/base/common/path';
|
||||
import { rtrim, endsWith } from 'vs/base/common/strings';
|
||||
import { rtrim } from 'vs/base/common/strings';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export class FileWatcher implements IDisposable {
|
||||
@@ -22,7 +22,7 @@ export class FileWatcher implements IDisposable {
|
||||
) {
|
||||
this.folder = folders[0];
|
||||
|
||||
if (this.folder.path.indexOf('\\\\') === 0 && endsWith(this.folder.path, posix.sep)) {
|
||||
if (this.folder.path.indexOf('\\\\') === 0 && this.folder.path.endsWith(posix.sep)) {
|
||||
// for some weird reason, node adds a trailing slash to UNC paths
|
||||
// we never ever want trailing slashes as our base path unless
|
||||
// someone opens root ("/").
|
||||
|
||||
@@ -11,7 +11,6 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
|
||||
import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { localize } from 'vs/nls';
|
||||
import { isEqualOrParent, basename } from 'vs/base/common/resources';
|
||||
import { endsWith } from 'vs/base/common/strings';
|
||||
|
||||
export interface ILabelService {
|
||||
_serviceBrand: undefined;
|
||||
@@ -61,7 +60,7 @@ export function getSimpleWorkspaceLabel(workspace: IWorkspaceIdentifier | URI, w
|
||||
}
|
||||
|
||||
let filename = basename(workspace.configPath);
|
||||
if (endsWith(filename, WORKSPACE_EXTENSION)) {
|
||||
if (filename.endsWith(WORKSPACE_EXTENSION)) {
|
||||
filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1);
|
||||
}
|
||||
return localize('workspaceName', "{0} (Workspace)", filename);
|
||||
|
||||
@@ -65,7 +65,7 @@ export interface INeverShowAgainOptions {
|
||||
|
||||
/**
|
||||
* Whether to persist the choice in the current workspace or for all workspaces. By
|
||||
* default it will be persisted for all workspaces.
|
||||
* default it will be persisted for all workspaces (= `NeverShowAgainScope.GLOBAL`).
|
||||
*/
|
||||
readonly scope?: NeverShowAgainScope;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export class HelpQuickAccessProvider implements IQuickAccessProvider {
|
||||
disposables.add(picker.onDidAccept(() => {
|
||||
const [item] = picker.selectedItems;
|
||||
if (item) {
|
||||
this.quickInputService.quickAccess.show(item.prefix);
|
||||
this.quickInputService.quickAccess.show(item.prefix, { preserveValue: true });
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -37,7 +37,7 @@ export class HelpQuickAccessProvider implements IQuickAccessProvider {
|
||||
disposables.add(picker.onDidChangeValue(value => {
|
||||
const providerDescriptor = this.registry.getQuickAccessProvider(value.substr(HelpQuickAccessProvider.PREFIX.length));
|
||||
if (providerDescriptor && providerDescriptor.prefix && providerDescriptor.prefix !== HelpQuickAccessProvider.PREFIX) {
|
||||
this.quickInputService.quickAccess.show(providerDescriptor.prefix);
|
||||
this.quickInputService.quickAccess.show(providerDescriptor.prefix, { preserveValue: true });
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
@@ -11,14 +11,6 @@ import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cance
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { once } from 'vs/base/common/functional';
|
||||
|
||||
interface IInternalQuickAccessOptions extends IQuickAccessOptions {
|
||||
|
||||
/**
|
||||
* Internal option to not rewrite the filter value at all but use it as is.
|
||||
*/
|
||||
preserveFilterValue?: boolean;
|
||||
}
|
||||
|
||||
export class QuickAccessController extends Disposable implements IQuickAccessController {
|
||||
|
||||
private readonly registry = Registry.as<IQuickAccessRegistry>(Extensions.Quickaccess);
|
||||
@@ -39,7 +31,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
|
||||
super();
|
||||
}
|
||||
|
||||
show(value = '', options?: IInternalQuickAccessOptions): void {
|
||||
show(value = '', options?: IQuickAccessOptions): void {
|
||||
|
||||
// Find provider for the value to show
|
||||
const [provider, descriptor] = this.getOrInstantiateProvider(value);
|
||||
@@ -51,7 +43,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
|
||||
|
||||
// Apply value only if it is more specific than the prefix
|
||||
// from the provider and we are not instructed to preserve
|
||||
if (value !== descriptor.prefix && !options?.preserveFilterValue) {
|
||||
if (value !== descriptor.prefix && !options?.preserveValue) {
|
||||
visibleQuickAccess.picker.value = value;
|
||||
}
|
||||
|
||||
@@ -62,7 +54,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
|
||||
}
|
||||
|
||||
// Rewrite the filter value based on certain rules unless disabled
|
||||
if (descriptor && !options?.preserveFilterValue) {
|
||||
if (descriptor && !options?.preserveValue) {
|
||||
let newValue: string | undefined = undefined;
|
||||
|
||||
// If we have a visible provider with a value, take it's filter value but
|
||||
@@ -116,11 +108,11 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
|
||||
picker.show();
|
||||
}
|
||||
|
||||
private adjustValueSelection(picker: IQuickPick<IQuickPickItem>, descriptor?: IQuickAccessProviderDescriptor, options?: IInternalQuickAccessOptions): void {
|
||||
private adjustValueSelection(picker: IQuickPick<IQuickPickItem>, descriptor?: IQuickAccessProviderDescriptor, options?: IQuickAccessOptions): void {
|
||||
let valueSelection: [number, number];
|
||||
|
||||
// Preserve: just always put the cursor at the end
|
||||
if (options?.preserveFilterValue) {
|
||||
if (options?.preserveValue) {
|
||||
valueSelection = [picker.value.length, picker.value.length];
|
||||
}
|
||||
|
||||
@@ -147,7 +139,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
|
||||
disposables.add(picker.onDidChangeValue(value => {
|
||||
const [providerForValue] = this.getOrInstantiateProvider(value);
|
||||
if (providerForValue !== provider) {
|
||||
this.show(value, { preserveFilterValue: true } /* do not rewrite value from user typing! */);
|
||||
this.show(value, { preserveValue: true } /* do not rewrite value from user typing! */);
|
||||
} else {
|
||||
visibleQuickAccess.value = value; // remember the value in our visible one
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
import { IQuickPick, IQuickPickItem, IQuickNavigateConfiguration } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { first, coalesce } from 'vs/base/common/arrays';
|
||||
import { startsWith } from 'vs/base/common/strings';
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ItemActivation } from 'vs/base/parts/quickinput/common/quickInput';
|
||||
|
||||
@@ -22,7 +21,13 @@ export interface IQuickAccessOptions {
|
||||
* Allows to configure a different item activation strategy.
|
||||
* By default the first item in the list will get activated.
|
||||
*/
|
||||
itemActivation?: ItemActivation
|
||||
itemActivation?: ItemActivation;
|
||||
|
||||
/**
|
||||
* Wether to take the input value as is and not restore it
|
||||
* from any existing value if quick access is visible.
|
||||
*/
|
||||
preserveValue?: boolean;
|
||||
}
|
||||
|
||||
export interface IQuickAccessController {
|
||||
@@ -177,7 +182,7 @@ export class QuickAccessRegistry implements IQuickAccessRegistry {
|
||||
}
|
||||
|
||||
getQuickAccessProvider(prefix: string): IQuickAccessProviderDescriptor | undefined {
|
||||
const result = prefix ? (first(this.providers, provider => startsWith(prefix, provider.prefix)) || undefined) : undefined;
|
||||
const result = prefix ? (this.providers.find(provider => prefix.startsWith(provider.prefix)) || undefined) : undefined;
|
||||
|
||||
return result || this.defaultProvider;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import { IStorage, Storage, IStorageDatabase, IStorageItemsChangeEvent, IUpdateR
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { runWhenIdle, RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { serializableToMap, mapToSerializable } from 'vs/base/common/map';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { assertIsDefined, assertAllDefined } from 'vs/base/common/types';
|
||||
|
||||
@@ -291,7 +290,7 @@ export class FileStorageDatabase extends Disposable implements IStorageDatabase
|
||||
|
||||
this.ensureWatching(); // now that the file must exist, ensure we watch it for changes
|
||||
|
||||
return serializableToMap(JSON.parse(itemsRaw.value.toString()));
|
||||
return new Map(JSON.parse(itemsRaw.value.toString()));
|
||||
}
|
||||
|
||||
async updateItems(request: IUpdateRequest): Promise<void> {
|
||||
@@ -311,7 +310,7 @@ export class FileStorageDatabase extends Disposable implements IStorageDatabase
|
||||
try {
|
||||
this._hasPendingUpdate = true;
|
||||
|
||||
await this.fileService.writeFile(this.file, VSBuffer.fromString(JSON.stringify(mapToSerializable(items))));
|
||||
await this.fileService.writeFile(this.file, VSBuffer.fromString(JSON.stringify(Array.from(items.entries()))));
|
||||
|
||||
this.ensureWatching(); // now that the file must exist, ensure we watch it for changes
|
||||
} finally {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IStorageChangeEvent, IStorageMainService } from 'vs/platform/storage/node/storageMainService';
|
||||
import { IUpdateRequest, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/base/parts/storage/common/storage';
|
||||
import { mapToSerializable, serializableToMap, values } from 'vs/base/common/map';
|
||||
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
@@ -117,7 +116,10 @@ export class GlobalStorageDatabaseChannel extends Disposable implements IServerC
|
||||
}
|
||||
});
|
||||
|
||||
return { changed: mapToSerializable(changed), deleted: values(deleted) };
|
||||
return {
|
||||
changed: Array.from(changed.entries()),
|
||||
deleted: Array.from(deleted.values())
|
||||
};
|
||||
}
|
||||
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
@@ -136,7 +138,7 @@ export class GlobalStorageDatabaseChannel extends Disposable implements IServerC
|
||||
// handle call
|
||||
switch (command) {
|
||||
case 'getItems': {
|
||||
return mapToSerializable(this.storageMainService.items);
|
||||
return Array.from(this.storageMainService.items.entries());
|
||||
}
|
||||
|
||||
case 'updateItems': {
|
||||
@@ -182,7 +184,7 @@ export class GlobalStorageDatabaseChannelClient extends Disposable implements IS
|
||||
private onDidChangeItemsOnMain(e: ISerializableItemsChangeEvent): void {
|
||||
if (Array.isArray(e.changed) || Array.isArray(e.deleted)) {
|
||||
this._onDidChangeItemsExternal.fire({
|
||||
changed: e.changed ? serializableToMap(e.changed) : undefined,
|
||||
changed: e.changed ? new Map(e.changed) : undefined,
|
||||
deleted: e.deleted ? new Set<string>(e.deleted) : undefined
|
||||
});
|
||||
}
|
||||
@@ -191,18 +193,18 @@ export class GlobalStorageDatabaseChannelClient extends Disposable implements IS
|
||||
async getItems(): Promise<Map<string, string>> {
|
||||
const items: Item[] = await this.channel.call('getItems');
|
||||
|
||||
return serializableToMap(items);
|
||||
return new Map(items);
|
||||
}
|
||||
|
||||
updateItems(request: IUpdateRequest): Promise<void> {
|
||||
const serializableRequest: ISerializableUpdateRequest = Object.create(null);
|
||||
|
||||
if (request.insert) {
|
||||
serializableRequest.insert = mapToSerializable(request.insert);
|
||||
serializableRequest.insert = Array.from(request.insert.entries());
|
||||
}
|
||||
|
||||
if (request.delete) {
|
||||
serializableRequest.delete = values(request.delete);
|
||||
serializableRequest.delete = Array.from(request.delete.values());
|
||||
}
|
||||
|
||||
return this.channel.call('updateItems', serializableRequest);
|
||||
|
||||
@@ -28,7 +28,7 @@ export const fontStylePattern = '^(\\s*(-?italic|-?bold|-?underline))*\\s*$';
|
||||
|
||||
export interface TokenSelector {
|
||||
match(type: string, modifiers: string[], language: string): number;
|
||||
readonly selectorString: string;
|
||||
readonly id: string;
|
||||
}
|
||||
|
||||
export interface TokenTypeOrModifierContribution {
|
||||
@@ -155,7 +155,7 @@ export namespace TokenStylingRule {
|
||||
}
|
||||
export function toJSONObject(rule: TokenStylingRule): any {
|
||||
return {
|
||||
_selector: rule.selector.selectorString,
|
||||
_selector: rule.selector.id,
|
||||
_style: TokenStyle.toJSONObject(rule.style)
|
||||
};
|
||||
}
|
||||
@@ -164,7 +164,7 @@ export namespace TokenStylingRule {
|
||||
return true;
|
||||
}
|
||||
return r1 !== undefined && r2 !== undefined
|
||||
&& r1.selector && r2.selector && r1.selector.selectorString === r2.selector.selectorString
|
||||
&& r1.selector && r2.selector && r1.selector.id === r2.selector.id
|
||||
&& TokenStyle.equals(r1.style, r2.style);
|
||||
}
|
||||
export function is(r: any): r is TokenStylingRule {
|
||||
@@ -203,10 +203,11 @@ export interface ITokenClassificationRegistry {
|
||||
/**
|
||||
* Parses a token selector from a selector string.
|
||||
* @param selectorString selector string in the form (*|type)(.modifier)*
|
||||
* @param language language to which the selector applies or undefined if the selector is for all languafe
|
||||
* @returns the parsesd selector
|
||||
* @throws an error if the string is not a valid selector
|
||||
*/
|
||||
parseTokenSelector(selectorString: string): TokenSelector;
|
||||
parseTokenSelector(selectorString: string, language?: string): TokenSelector;
|
||||
|
||||
/**
|
||||
* Register a TokenStyle default to the registry.
|
||||
@@ -335,13 +336,13 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry {
|
||||
this.tokenStylingSchema.properties[`*.${id}`] = getStylingSchemeEntry(description, deprecationMessage);
|
||||
}
|
||||
|
||||
public parseTokenSelector(selectorString: string): TokenSelector {
|
||||
const selector = parseClassifierString(selectorString);
|
||||
public parseTokenSelector(selectorString: string, language?: string): TokenSelector {
|
||||
const selector = parseClassifierString(selectorString, language);
|
||||
|
||||
if (!selector.type) {
|
||||
return {
|
||||
match: () => -1,
|
||||
selectorString
|
||||
id: '$invalid'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -352,7 +353,7 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry {
|
||||
if (selector.language !== language) {
|
||||
return -1;
|
||||
}
|
||||
score += 100;
|
||||
score += 10;
|
||||
}
|
||||
if (selector.type !== TOKEN_TYPE_WILDCARD) {
|
||||
const hierarchy = this.getTypeHierarchy(type);
|
||||
@@ -370,7 +371,7 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry {
|
||||
}
|
||||
return score + selector.modifiers.length * 100;
|
||||
},
|
||||
selectorString
|
||||
id: `${[selector.type, ...selector.modifiers.sort()].join('.')}${selector.language !== undefined ? ':' + selector.language : ''}`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -379,8 +380,8 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry {
|
||||
}
|
||||
|
||||
public deregisterTokenStyleDefault(selector: TokenSelector): void {
|
||||
const selectorString = selector.selectorString;
|
||||
this.tokenStylingDefaultRules = this.tokenStylingDefaultRules.filter(r => r.selector.selectorString !== selectorString);
|
||||
const selectorString = selector.id;
|
||||
this.tokenStylingDefaultRules = this.tokenStylingDefaultRules.filter(r => r.selector.id !== selectorString);
|
||||
}
|
||||
|
||||
public deregisterTokenType(id: string): void {
|
||||
@@ -442,9 +443,11 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry {
|
||||
const CHAR_LANGUAGE = TOKEN_CLASSIFIER_LANGUAGE_SEPARATOR.charCodeAt(0);
|
||||
const CHAR_MODIFIER = CLASSIFIER_MODIFIER_SEPARATOR.charCodeAt(0);
|
||||
|
||||
export function parseClassifierString(s: string): { type: string, modifiers: string[], language: string | undefined; } {
|
||||
export function parseClassifierString(s: string, defaultLanguage: string): { type: string, modifiers: string[], language: string; };
|
||||
export function parseClassifierString(s: string, defaultLanguage?: string): { type: string, modifiers: string[], language: string | undefined; };
|
||||
export function parseClassifierString(s: string, defaultLanguage: string | undefined): { type: string, modifiers: string[], language: string | undefined; } {
|
||||
let k = s.length;
|
||||
let language: string | undefined = undefined;
|
||||
let language: string | undefined = defaultLanguage;
|
||||
const modifiers = [];
|
||||
|
||||
for (let i = k - 1; i >= 0; i--) {
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import * as nls from 'vs/nls';
|
||||
import { IUndoRedoService, IResourceUndoRedoElement, IWorkspaceUndoRedoElement, UndoRedoElementType, IUndoRedoElement, IPastFutureElements } from 'vs/platform/undoRedo/common/undoRedo';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { getComparisonKey as uriGetComparisonKey } from 'vs/base/common/resources';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
@@ -14,6 +13,10 @@ import Severity from 'vs/base/common/severity';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
|
||||
function uriGetComparisonKey(resource: URI): string {
|
||||
return resource.toString();
|
||||
}
|
||||
|
||||
class ResourceStackElement {
|
||||
public readonly type = UndoRedoElementType.Resource;
|
||||
public readonly actual: IResourceUndoRedoElement;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IFileService, IFileContent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { joinPath, dirname, isEqual, basename } from 'vs/base/common/resources';
|
||||
import { CancelablePromise } from 'vs/base/common/async';
|
||||
@@ -144,6 +144,16 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
async getSyncPreview(): Promise<ISyncPreviewResult> {
|
||||
if (!this.isEnabled()) {
|
||||
return { hasLocalChanged: false, hasRemoteChanged: false };
|
||||
}
|
||||
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
return this.generatePreview(remoteUserData, lastSyncUserData);
|
||||
}
|
||||
|
||||
protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> {
|
||||
if (remoteUserData.syncData && remoteUserData.syncData.version > this.version) {
|
||||
// current version is not compatible with cloud version
|
||||
@@ -285,15 +295,14 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
|
||||
protected abstract readonly version: number;
|
||||
protected abstract performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus>;
|
||||
protected abstract generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncPreviewResult>;
|
||||
}
|
||||
|
||||
export interface IFileSyncPreviewResult {
|
||||
export interface IFileSyncPreviewResult extends ISyncPreviewResult {
|
||||
readonly fileContent: IFileContent | null;
|
||||
readonly remoteUserData: IRemoteUserData;
|
||||
readonly lastSyncUserData: IRemoteUserData | null;
|
||||
readonly content: string | null;
|
||||
readonly hasLocalChanged: boolean;
|
||||
readonly hasRemoteChanged: boolean;
|
||||
readonly hasConflicts: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
@@ -20,7 +20,7 @@ import { joinPath, dirname, basename } from 'vs/base/common/resources';
|
||||
import { format } from 'vs/base/common/jsonFormatter';
|
||||
import { applyEdits } from 'vs/base/common/jsonEdit';
|
||||
|
||||
interface ISyncPreviewResult {
|
||||
interface IExtensionsSyncPreviewResult extends ISyncPreviewResult {
|
||||
readonly localExtensions: ISyncExtension[];
|
||||
readonly remoteUserData: IRemoteUserData;
|
||||
readonly lastSyncUserData: ILastSyncUserData | null;
|
||||
@@ -82,7 +82,11 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
const localExtensions = await this.getLocalExtensions();
|
||||
const remoteExtensions = this.parseExtensions(remoteUserData.syncData);
|
||||
const { added, updated, remote, removed } = merge(localExtensions, remoteExtensions, localExtensions, [], this.getIgnoredExtensions());
|
||||
await this.apply({ added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData });
|
||||
await this.apply({
|
||||
added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
|
||||
hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
|
||||
hasRemoteChanged: remote !== null
|
||||
});
|
||||
}
|
||||
|
||||
// No remote exists to pull
|
||||
@@ -112,7 +116,11 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
const { added, removed, updated, remote } = merge(localExtensions, null, null, [], this.getIgnoredExtensions());
|
||||
const lastSyncUserData = await this.getLastSyncUserData<ILastSyncUserData>();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
await this.apply({ added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData }, true);
|
||||
await this.apply({
|
||||
added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
|
||||
hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
|
||||
hasRemoteChanged: remote !== null
|
||||
}, true);
|
||||
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Finished pushing extensions.`);
|
||||
} finally {
|
||||
@@ -163,12 +171,12 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
}
|
||||
|
||||
protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<SyncStatus> {
|
||||
const previewResult = await this.getPreview(remoteUserData, lastSyncUserData);
|
||||
const previewResult = await this.generatePreview(remoteUserData, lastSyncUserData);
|
||||
await this.apply(previewResult);
|
||||
return SyncStatus.Idle;
|
||||
}
|
||||
|
||||
private async getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<ISyncPreviewResult> {
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionsSyncPreviewResult> {
|
||||
const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? this.parseExtensions(remoteUserData.syncData) : null;
|
||||
const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? this.parseExtensions(lastSyncUserData.syncData!) : null;
|
||||
const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : [];
|
||||
@@ -183,22 +191,31 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
|
||||
const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, this.getIgnoredExtensions());
|
||||
|
||||
return { added, removed, updated, remote, skippedExtensions, remoteUserData, localExtensions, lastSyncUserData };
|
||||
return {
|
||||
added,
|
||||
removed,
|
||||
updated,
|
||||
remote,
|
||||
skippedExtensions,
|
||||
remoteUserData,
|
||||
localExtensions,
|
||||
lastSyncUserData,
|
||||
hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
|
||||
hasRemoteChanged: remote !== null
|
||||
};
|
||||
}
|
||||
|
||||
private getIgnoredExtensions() {
|
||||
return this.configurationService.getValue<string[]>('sync.ignoredExtensions') || [];
|
||||
}
|
||||
|
||||
private async apply({ added, removed, updated, remote, remoteUserData, skippedExtensions, lastSyncUserData, localExtensions }: ISyncPreviewResult, forcePush?: boolean): Promise<void> {
|
||||
private async apply({ added, removed, updated, remote, remoteUserData, skippedExtensions, lastSyncUserData, localExtensions, hasLocalChanged, hasRemoteChanged }: IExtensionsSyncPreviewResult, forcePush?: boolean): Promise<void> {
|
||||
|
||||
const hasChanges = added.length || removed.length || updated.length || remote;
|
||||
|
||||
if (!hasChanges) {
|
||||
if (!hasLocalChanged && !hasRemoteChanged) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing extensions.`);
|
||||
}
|
||||
|
||||
if (added.length || removed.length || updated.length) {
|
||||
if (hasLocalChanged) {
|
||||
// back up all disabled or market place extensions
|
||||
const backUpExtensions = localExtensions.filter(e => e.disabled || !!e.identifier.uuid);
|
||||
await this.backupLocal(JSON.stringify(backUpExtensions));
|
||||
|
||||
@@ -13,17 +13,18 @@ import { ILogService } from 'vs/platform/log/common/log';
|
||||
export interface IMergeResult {
|
||||
local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> };
|
||||
remote: IStringDictionary<IStorageValue> | null;
|
||||
skipped: string[];
|
||||
}
|
||||
|
||||
export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStorage: IStringDictionary<IStorageValue> | null, baseStorage: IStringDictionary<IStorageValue> | null, storageKeys: ReadonlyArray<IStorageKey>, logService: ILogService): IMergeResult {
|
||||
export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStorage: IStringDictionary<IStorageValue> | null, baseStorage: IStringDictionary<IStorageValue> | null, storageKeys: ReadonlyArray<IStorageKey>, previouslySkipped: string[], logService: ILogService): IMergeResult {
|
||||
if (!remoteStorage) {
|
||||
return { remote: localStorage, local: { added: {}, removed: [], updated: {} } };
|
||||
return { remote: localStorage, local: { added: {}, removed: [], updated: {} }, skipped: [] };
|
||||
}
|
||||
|
||||
const localToRemote = compare(localStorage, remoteStorage);
|
||||
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
|
||||
// No changes found between local and remote.
|
||||
return { remote: null, local: { added: {}, removed: [], updated: {} } };
|
||||
return { remote: null, local: { added: {}, removed: [], updated: {} }, skipped: [] };
|
||||
}
|
||||
|
||||
const baseToRemote = baseStorage ? compare(baseStorage, remoteStorage) : { added: Object.keys(remoteStorage).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
|
||||
@@ -31,17 +32,19 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
|
||||
|
||||
const local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> } = { added: {}, removed: [], updated: {} };
|
||||
const remote: IStringDictionary<IStorageValue> = objects.deepClone(remoteStorage);
|
||||
const skipped: string[] = [];
|
||||
|
||||
// Added in remote
|
||||
for (const key of values(baseToRemote.added)) {
|
||||
const remoteValue = remoteStorage[key];
|
||||
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
|
||||
if (!storageKey) {
|
||||
logService.info(`GlobalState: Skipped updating ${key} in storage. It is not registered to sync.`);
|
||||
skipped.push(key);
|
||||
logService.info(`GlobalState: Skipped adding ${key} in local storage as it is not registered.`);
|
||||
continue;
|
||||
}
|
||||
if (storageKey.version !== remoteValue.version) {
|
||||
logService.info(`GlobalState: Skipped updating ${key} in storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`);
|
||||
logService.info(`GlobalState: Skipped adding ${key} in local storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`);
|
||||
continue;
|
||||
}
|
||||
const localValue = localStorage[key];
|
||||
@@ -60,11 +63,12 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
|
||||
const remoteValue = remoteStorage[key];
|
||||
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
|
||||
if (!storageKey) {
|
||||
logService.info(`GlobalState: Skipped updating ${key} in storage. It is not registered to sync.`);
|
||||
skipped.push(key);
|
||||
logService.info(`GlobalState: Skipped updating ${key} in local storage as is not registered.`);
|
||||
continue;
|
||||
}
|
||||
if (storageKey.version !== remoteValue.version) {
|
||||
logService.info(`GlobalState: Skipped updating ${key} in storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`);
|
||||
logService.info(`GlobalState: Skipped updating ${key} in local storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`);
|
||||
continue;
|
||||
}
|
||||
const localValue = localStorage[key];
|
||||
@@ -78,7 +82,7 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
|
||||
for (const key of values(baseToRemote.removed)) {
|
||||
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
|
||||
if (!storageKey) {
|
||||
logService.info(`GlobalState: Skipped updating ${key} in storage. It is not registered to sync.`);
|
||||
logService.info(`GlobalState: Skipped removing ${key} in local storage. It is not registered to sync.`);
|
||||
continue;
|
||||
}
|
||||
local.removed.push(key);
|
||||
@@ -99,6 +103,7 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
|
||||
const remoteValue = remote[key];
|
||||
const localValue = localStorage[key];
|
||||
if (localValue.version < remoteValue.version) {
|
||||
logService.info(`GlobalState: Skipped updating ${key} in remote storage. Local version '${localValue.version}' and remote version '${remoteValue.version} are not same.`);
|
||||
continue;
|
||||
}
|
||||
remote[key] = localValue;
|
||||
@@ -106,18 +111,36 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
|
||||
|
||||
// Removed in local
|
||||
for (const key of values(baseToLocal.removed)) {
|
||||
// do not remove from remote if it is updated in remote
|
||||
if (baseToRemote.updated.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const remoteValue = remote[key];
|
||||
|
||||
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
|
||||
if (storageKey && storageKey.version < remoteValue.version) {
|
||||
// do not remove from remote if storage key is not found
|
||||
if (!storageKey) {
|
||||
skipped.push(key);
|
||||
logService.info(`GlobalState: Skipped removing ${key} in remote storage. It is not registered to sync.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const remoteValue = remote[key];
|
||||
// do not remove from remote if local data version is old
|
||||
if (storageKey.version < remoteValue.version) {
|
||||
logService.info(`GlobalState: Skipped updating ${key} in remote storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// add to local if it was skipped before
|
||||
if (previouslySkipped.indexOf(key) !== -1) {
|
||||
local.added[key] = remote[key];
|
||||
continue;
|
||||
}
|
||||
|
||||
delete remote[key];
|
||||
}
|
||||
|
||||
return { local, remote: areSame(remote, remoteStorage) ? null : remote };
|
||||
return { local, remote: areSame(remote, remoteStorage) ? null : remote, skipped };
|
||||
}
|
||||
|
||||
function compare(from: IStringDictionary<any>, to: IStringDictionary<any>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
@@ -21,16 +21,22 @@ import { format } from 'vs/base/common/jsonFormatter';
|
||||
import { applyEdits } from 'vs/base/common/jsonEdit';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { IStorageKeysSyncRegistryService, IStorageKey } from 'vs/platform/userDataSync/common/storageKeys';
|
||||
import { equals } from 'vs/base/common/arrays';
|
||||
|
||||
const argvStoragePrefx = 'globalState.argv.';
|
||||
const argvProperties: string[] = ['locale'];
|
||||
|
||||
interface ISyncPreviewResult {
|
||||
interface IGlobalSyncPreviewResult extends ISyncPreviewResult {
|
||||
readonly local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> };
|
||||
readonly remote: IStringDictionary<IStorageValue> | null;
|
||||
readonly skippedStorageKeys: string[];
|
||||
readonly localUserData: IGlobalState;
|
||||
readonly remoteUserData: IRemoteUserData;
|
||||
readonly lastSyncUserData: IRemoteUserData | null;
|
||||
readonly lastSyncUserData: ILastSyncUserData | null;
|
||||
}
|
||||
|
||||
interface ILastSyncUserData extends IRemoteUserData {
|
||||
skippedStorageKeys: string[] | undefined;
|
||||
}
|
||||
|
||||
export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
|
||||
@@ -51,8 +57,16 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
) {
|
||||
super(SyncResource.GlobalState, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService);
|
||||
this._register(this.fileService.watch(dirname(this.environmentService.argvResource)));
|
||||
this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.environmentService.argvResource))(() => this._onDidChangeLocal.fire()));
|
||||
this._register(Event.filter(this.storageService.onDidChangeStorage, e => storageKeysSyncRegistryService.storageKeys.some(({ key }) => e.key === key))(() => this._onDidChangeLocal.fire()));
|
||||
this._register(
|
||||
Event.any(
|
||||
/* Locale change */
|
||||
Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.environmentService.argvResource)),
|
||||
/* Storage change */
|
||||
Event.filter(this.storageService.onDidChangeStorage, e => storageKeysSyncRegistryService.storageKeys.some(({ key }) => e.key === key)),
|
||||
/* Storage key registered */
|
||||
this.storageKeysSyncRegistryService.onDidChangeStorageKeys
|
||||
)((() => this._onDidChangeLocal.fire()))
|
||||
);
|
||||
}
|
||||
|
||||
async pull(): Promise<void> {
|
||||
@@ -67,14 +81,19 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Started pulling ui state...`);
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const lastSyncUserData = await this.getLastSyncUserData<ILastSyncUserData>();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
|
||||
if (remoteUserData.syncData !== null) {
|
||||
const localGlobalState = await this.getLocalGlobalState();
|
||||
const remoteGlobalState: IGlobalState = JSON.parse(remoteUserData.syncData.content);
|
||||
const { local, remote } = merge(localGlobalState.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), this.logService);
|
||||
await this.apply({ local, remote, remoteUserData, localUserData: localGlobalState, lastSyncUserData });
|
||||
const { local, remote, skipped } = merge(localGlobalState.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
|
||||
await this.apply({
|
||||
local, remote, remoteUserData, localUserData: localGlobalState, lastSyncUserData,
|
||||
skippedStorageKeys: skipped,
|
||||
hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0,
|
||||
hasRemoteChanged: remote !== null
|
||||
});
|
||||
}
|
||||
|
||||
// No remote exists to pull
|
||||
@@ -101,9 +120,14 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
const localUserData = await this.getLocalGlobalState();
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const lastSyncUserData = await this.getLastSyncUserData<ILastSyncUserData>();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
await this.apply({ local: { added: {}, removed: [], updated: {} }, remote: localUserData.storage, remoteUserData, localUserData, lastSyncUserData }, true);
|
||||
await this.apply({
|
||||
local: { added: {}, removed: [], updated: {} }, remote: localUserData.storage, remoteUserData, localUserData, lastSyncUserData,
|
||||
skippedStorageKeys: [],
|
||||
hasLocalChanged: false,
|
||||
hasRemoteChanged: true
|
||||
}, true);
|
||||
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Finished pushing UI State.`);
|
||||
} finally {
|
||||
@@ -153,13 +177,13 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
return false;
|
||||
}
|
||||
|
||||
protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<SyncStatus> {
|
||||
const result = await this.getPreview(remoteUserData, lastSyncUserData);
|
||||
protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<SyncStatus> {
|
||||
const result = await this.generatePreview(remoteUserData, lastSyncUserData);
|
||||
await this.apply(result);
|
||||
return SyncStatus.Idle;
|
||||
}
|
||||
|
||||
private async getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncPreviewResult> {
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IGlobalSyncPreviewResult> {
|
||||
const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
|
||||
const lastSyncGlobalState: IGlobalState = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null;
|
||||
|
||||
@@ -171,15 +195,17 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Remote ui state does not exist. Synchronizing ui state for the first time.`);
|
||||
}
|
||||
|
||||
const { local, remote } = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, this.getSyncStorageKeys(), this.logService);
|
||||
const { local, remote, skipped } = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
|
||||
|
||||
return { local, remote, remoteUserData, localUserData: localGloablState, lastSyncUserData };
|
||||
return {
|
||||
local, remote, remoteUserData, localUserData: localGloablState, lastSyncUserData,
|
||||
skippedStorageKeys: skipped,
|
||||
hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0,
|
||||
hasRemoteChanged: remote !== null
|
||||
};
|
||||
}
|
||||
|
||||
private async apply({ local, remote, remoteUserData, lastSyncUserData, localUserData }: ISyncPreviewResult, forcePush?: boolean): Promise<void> {
|
||||
|
||||
const hasLocalChanged = Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0;
|
||||
const hasRemoteChanged = remote !== null;
|
||||
private async apply({ local, remote, remoteUserData, lastSyncUserData, localUserData, hasLocalChanged, hasRemoteChanged, skippedStorageKeys }: IGlobalSyncPreviewResult, forcePush?: boolean): Promise<void> {
|
||||
|
||||
if (!hasLocalChanged && !hasRemoteChanged) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing ui state.`);
|
||||
@@ -201,10 +227,10 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated remote ui state`);
|
||||
}
|
||||
|
||||
if (lastSyncUserData?.ref !== remoteUserData.ref) {
|
||||
if (lastSyncUserData?.ref !== remoteUserData.ref || !equals(lastSyncUserData.skippedStorageKeys, skippedStorageKeys)) {
|
||||
// update last sync
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized ui state...`);
|
||||
await this.updateLastSyncUserData(remoteUserData);
|
||||
await this.updateLastSyncUserData(remoteUserData, { skippedStorageKeys });
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized ui state`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,7 +265,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
return this.syncPreviewResultPromise;
|
||||
}
|
||||
|
||||
private async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileSyncPreviewResult> {
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise<IFileSyncPreviewResult> {
|
||||
const remoteContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
|
||||
const lastSyncContent = lastSyncUserData && lastSyncUserData.syncData ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content) : null;
|
||||
// Get file content last to get the latest
|
||||
|
||||
@@ -309,14 +309,14 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
|
||||
this.syncPreviewResultPromise = null;
|
||||
}
|
||||
|
||||
private getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: { key: string, value: any }[]): Promise<IFileSyncPreviewResult> {
|
||||
private getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: { key: string, value: any }[] = []): Promise<IFileSyncPreviewResult> {
|
||||
if (!this.syncPreviewResultPromise) {
|
||||
this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, resolvedConflicts, token));
|
||||
}
|
||||
return this.syncPreviewResultPromise;
|
||||
}
|
||||
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: { key: string, value: any }[], token: CancellationToken): Promise<IFileSyncPreviewResult> {
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: { key: string, value: any }[] = [], token: CancellationToken = CancellationToken.None): Promise<IFileSyncPreviewResult> {
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const formattingOptions = await this.getFormattingOptions();
|
||||
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, UserDataSyncError, UserDataSyncErrorCode, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, UserDataSyncError, UserDataSyncErrorCode, ISyncResourceHandle, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IFileService, FileChangesEvent, IFileStat, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
@@ -17,7 +17,7 @@ import { merge } from 'vs/platform/userDataSync/common/snippetsMerge';
|
||||
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
interface ISyncPreviewResult {
|
||||
interface ISinppetsSyncPreviewResult extends ISyncPreviewResult {
|
||||
readonly local: IStringDictionary<IFileContent>;
|
||||
readonly remoteUserData: IRemoteUserData;
|
||||
readonly lastSyncUserData: IRemoteUserData | null;
|
||||
@@ -34,7 +34,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
protected readonly version: number = 1;
|
||||
private readonly snippetsFolder: URI;
|
||||
private readonly snippetsPreviewFolder: URI;
|
||||
private syncPreviewResultPromise: CancelablePromise<ISyncPreviewResult> | null = null;
|
||||
private syncPreviewResultPromise: CancelablePromise<ISinppetsSyncPreviewResult> | null = null;
|
||||
|
||||
constructor(
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@@ -94,8 +94,10 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
const localSnippets = this.toSnippetsContents(local);
|
||||
const remoteSnippets = this.parseSnippets(remoteUserData.syncData);
|
||||
const { added, updated, remote, removed } = merge(localSnippets, remoteSnippets, localSnippets);
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISyncPreviewResult>({
|
||||
added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {}
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISinppetsSyncPreviewResult>({
|
||||
added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {},
|
||||
hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0,
|
||||
hasRemoteChanged: remote !== null
|
||||
}));
|
||||
await this.apply();
|
||||
}
|
||||
@@ -128,8 +130,10 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
const { added, removed, updated, remote } = merge(localSnippets, null, null);
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISyncPreviewResult>({
|
||||
added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {}
|
||||
this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve<ISinppetsSyncPreviewResult>({
|
||||
added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {},
|
||||
hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0,
|
||||
hasRemoteChanged: remote !== null
|
||||
}));
|
||||
|
||||
await this.apply(true);
|
||||
@@ -207,7 +211,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
let previewResult = await this.syncPreviewResultPromise!;
|
||||
this.cancel();
|
||||
previewResult.resolvedConflicts[key] = content || null;
|
||||
this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(previewResult.local, previewResult.remoteUserData, previewResult.lastSyncUserData, previewResult.resolvedConflicts, token));
|
||||
this.syncPreviewResultPromise = createCancelablePromise(token => this.doGeneratePreview(previewResult.local, previewResult.remoteUserData, previewResult.lastSyncUserData, previewResult.resolvedConflicts, token));
|
||||
previewResult = await this.syncPreviewResultPromise;
|
||||
this.setConflicts(previewResult.conflicts);
|
||||
if (!this.conflicts.length) {
|
||||
@@ -252,10 +256,9 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
}
|
||||
}
|
||||
|
||||
private getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncPreviewResult> {
|
||||
protected getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISinppetsSyncPreviewResult> {
|
||||
if (!this.syncPreviewResultPromise) {
|
||||
this.syncPreviewResultPromise = createCancelablePromise(token => this.getSnippetsFileContents()
|
||||
.then(local => this.generatePreview(local, remoteUserData, lastSyncUserData, {}, token)));
|
||||
this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, token));
|
||||
}
|
||||
return this.syncPreviewResultPromise;
|
||||
}
|
||||
@@ -274,7 +277,12 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
}
|
||||
}
|
||||
|
||||
private async generatePreview(local: IStringDictionary<IFileContent>, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: IStringDictionary<string | null>, token: CancellationToken): Promise<ISyncPreviewResult> {
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise<ISinppetsSyncPreviewResult> {
|
||||
return this.getSnippetsFileContents()
|
||||
.then(local => this.doGeneratePreview(local, remoteUserData, lastSyncUserData, {}, token));
|
||||
}
|
||||
|
||||
private async doGeneratePreview(local: IStringDictionary<IFileContent>, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: IStringDictionary<string | null> = {}, token: CancellationToken = CancellationToken.None): Promise<ISinppetsSyncPreviewResult> {
|
||||
const localSnippets = this.toSnippetsContents(local);
|
||||
const remoteSnippets: IStringDictionary<string> | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null;
|
||||
const lastSyncSnippets: IStringDictionary<string> | null = lastSyncUserData ? this.parseSnippets(lastSyncUserData.syncData!) : null;
|
||||
@@ -309,7 +317,18 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
}
|
||||
}
|
||||
|
||||
return { remoteUserData, local, lastSyncUserData, added: mergeResult.added, removed: mergeResult.removed, updated: mergeResult.updated, conflicts, remote: mergeResult.remote, resolvedConflicts };
|
||||
return {
|
||||
remoteUserData, local,
|
||||
lastSyncUserData,
|
||||
added: mergeResult.added,
|
||||
removed: mergeResult.removed,
|
||||
updated: mergeResult.updated,
|
||||
conflicts,
|
||||
remote: mergeResult.remote,
|
||||
resolvedConflicts,
|
||||
hasLocalChanged: Object.keys(mergeResult.added).length > 0 || mergeResult.removed.length > 0 || Object.keys(mergeResult.updated).length > 0,
|
||||
hasRemoteChanged: mergeResult.remote !== null
|
||||
};
|
||||
}
|
||||
|
||||
private async apply(forcePush?: boolean): Promise<void> {
|
||||
@@ -317,15 +336,13 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
return;
|
||||
}
|
||||
|
||||
let { added, removed, updated, local, remote, remoteUserData, lastSyncUserData } = await this.syncPreviewResultPromise;
|
||||
let { added, removed, updated, local, remote, remoteUserData, lastSyncUserData, hasLocalChanged, hasRemoteChanged } = await this.syncPreviewResultPromise;
|
||||
|
||||
const hasChanges = Object.keys(added).length || removed.length || Object.keys(updated).length || remote;
|
||||
|
||||
if (!hasChanges) {
|
||||
if (!hasLocalChanged && !hasRemoteChanged) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing snippets.`);
|
||||
}
|
||||
|
||||
if (Object.keys(added).length || removed.length || Object.keys(updated).length) {
|
||||
if (hasLocalChanged) {
|
||||
// back up all snippets
|
||||
await this.backupLocal(JSON.stringify(this.toSnippetsContents(local)));
|
||||
await this.updateLocalSnippets(added, removed, updated, local);
|
||||
|
||||
@@ -254,6 +254,11 @@ export interface ISyncResourceHandle {
|
||||
|
||||
export type Conflict = { remote: URI, local: URI };
|
||||
|
||||
export interface ISyncPreviewResult {
|
||||
readonly hasLocalChanged: boolean;
|
||||
readonly hasRemoteChanged: boolean;
|
||||
}
|
||||
|
||||
export interface IUserDataSynchroniser {
|
||||
|
||||
readonly resource: SyncResource;
|
||||
@@ -268,6 +273,7 @@ export interface IUserDataSynchroniser {
|
||||
sync(ref?: string): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
|
||||
getSyncPreview(): Promise<ISyncPreviewResult>
|
||||
hasPreviouslySynced(): Promise<boolean>
|
||||
hasLocalData(): Promise<boolean>;
|
||||
resetLocal(): Promise<void>;
|
||||
|
||||
@@ -217,7 +217,16 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
if (await this.hasPreviouslySynced()) {
|
||||
return false;
|
||||
}
|
||||
return await this.hasLocalData();
|
||||
if (!(await this.hasLocalData())) {
|
||||
return false;
|
||||
}
|
||||
for (const synchroniser of [this.settingsSynchroniser, this.keybindingsSynchroniser, this.snippetsSynchroniser, this.extensionsSynchroniser]) {
|
||||
const preview = await synchroniser.getSyncPreview();
|
||||
if (preview.hasLocalChanged || preview.hasRemoteChanged) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async reset(): Promise<void> {
|
||||
|
||||
@@ -12,7 +12,6 @@ import { IHeaders, IRequestOptions, IRequestContext } from 'vs/base/parts/reques
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
@@ -66,7 +65,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
}
|
||||
|
||||
const result = await asJson<{ url: string, created: number }[]>(context) || [];
|
||||
return result.map(({ url, created }) => ({ ref: relativePath(uri, URI.parse(url).with({ scheme: uri.scheme, authority: uri.authority }))!, created: created * 1000 /* Server returns in seconds */ }));
|
||||
return result.map(({ url, created }) => ({ ref: relativePath(uri, uri.with({ path: url }))!, created: created * 1000 /* Server returns in seconds */ }));
|
||||
}
|
||||
|
||||
async resolveContent(resource: SyncResource, ref: string): Promise<string | null> {
|
||||
@@ -76,6 +75,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
|
||||
const url = joinPath(this.userDataSyncStore.url, 'resource', resource, ref).toString();
|
||||
const headers: IHeaders = {};
|
||||
headers['Cache-Control'] = 'no-cache';
|
||||
|
||||
const context = await this.request({ type: 'GET', url, headers }, undefined, CancellationToken.None);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -25,7 +25,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -37,7 +37,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -50,7 +50,7 @@ suite('GlobalStateMerge', () => {
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
const base = { 'b': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -62,7 +62,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'b': { version: 1, value: 'b' } });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -74,7 +74,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = {};
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -86,7 +86,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'b': { version: 1, value: 'b' } });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -98,7 +98,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -110,7 +110,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
const remote = {};
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -122,7 +122,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } });
|
||||
@@ -134,7 +134,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'a': { version: 1, value: 'd' }, 'c': { version: 1, value: 'c' } };
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'c': { version: 1, value: 'c' } });
|
||||
assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'd' } });
|
||||
@@ -146,7 +146,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -158,7 +158,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' }, 'c': { version: 1, value: 'c' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -170,7 +170,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -182,7 +182,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'b' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -194,7 +194,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'd' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' }, 'c': { version: 1, value: 'c' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -206,7 +206,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } });
|
||||
@@ -219,7 +219,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'd' } };
|
||||
const remote = { 'a': { version: 1, value: 'a' }, 'c': { version: 1, value: 'c' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, { 'c': { version: 1, value: 'c' } });
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -232,7 +232,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = {};
|
||||
const remote = { 'a': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } });
|
||||
@@ -245,7 +245,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'd' } };
|
||||
const remote = { 'a': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }, { key: 'c', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, { 'a': { version: 1, value: 'b' } });
|
||||
@@ -257,7 +257,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -269,7 +269,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, null, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -281,7 +281,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'a': { version: 1, value: 'b' } };
|
||||
|
||||
const actual = merge(local, remote, local, [], new NullLogService());
|
||||
const actual = merge(local, remote, local, [], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -293,7 +293,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, local, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -305,7 +305,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'c' } };
|
||||
const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -317,7 +317,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' }, 'b': { version: 2, value: 'c' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 2 }], new NullLogService());
|
||||
const actual = merge(local, remote, remote, [{ key: 'a', version: 1 }, { key: 'b', version: 2 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -330,7 +330,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -343,7 +343,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 2, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], new NullLogService());
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -356,7 +356,7 @@ suite('GlobalStateMerge', () => {
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 2 }], new NullLogService());
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }, { key: 'b', version: 2 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
@@ -364,4 +364,17 @@ suite('GlobalStateMerge', () => {
|
||||
assert.deepEqual(actual.remote, local);
|
||||
});
|
||||
|
||||
test('merge when a local value is not yet registered', async () => {
|
||||
const base = { 'a': { version: 1, value: 'a' }, 'b': { version: 1, value: 'b' } };
|
||||
const local = { 'a': { version: 1, value: 'a' } };
|
||||
const remote = { 'b': { version: 1, value: 'b' }, 'a': { version: 1, value: 'a' } };
|
||||
|
||||
const actual = merge(local, remote, base, [{ key: 'a', version: 1 }], [], new NullLogService());
|
||||
|
||||
assert.deepEqual(actual.local.added, {});
|
||||
assert.deepEqual(actual.local.updated, {});
|
||||
assert.deepEqual(actual.local.removed, []);
|
||||
assert.deepEqual(actual.remote, null);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncEnablementService, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
|
||||
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { AbstractSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
@@ -49,6 +49,10 @@ class TestSynchroniser extends AbstractSynchroniser {
|
||||
this.syncBarrier.open();
|
||||
}
|
||||
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncPreviewResult> {
|
||||
return { hasLocalChanged: false, hasRemoteChanged: false };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
suite('TestSynchronizer', () => {
|
||||
|
||||
@@ -103,17 +103,17 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
await testObject.pull();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
/* first time sync */
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
// Snippets
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
/* pull */
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
// Extensions
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
]);
|
||||
|
||||
@@ -143,17 +143,14 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
await testObject.pull();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
/* first time sync */
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
// Keybindings
|
||||
/* pull */
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
// Snippets
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
// Extensions
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
]);
|
||||
|
||||
@@ -178,18 +175,18 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
await testObject.sync();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
/* first time sync */
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
/* sync */
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
// Snippets
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
// Extensions
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
]);
|
||||
|
||||
@@ -220,21 +217,19 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
await testObject.sync();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
// Manifest
|
||||
/* first time sync */
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
|
||||
/* first time sync */
|
||||
{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} },
|
||||
// Settings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } },
|
||||
// Keybindings
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '1' } },
|
||||
// Snippets
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
{ type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '1' } },
|
||||
// Global state
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
// Extensions
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
]);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface IOpenedWindow {
|
||||
folderUri?: ISingleFolderWorkspaceIdentifier;
|
||||
title: string;
|
||||
filename?: string;
|
||||
dirty: boolean;
|
||||
}
|
||||
|
||||
export interface IBaseOpenWindowsOptions {
|
||||
|
||||
@@ -80,6 +80,9 @@ export interface ICodeWindow extends IDisposable {
|
||||
setRepresentedFilename(name: string): void;
|
||||
getRepresentedFilename(): string | undefined;
|
||||
|
||||
setDocumentEdited(edited: boolean): void;
|
||||
isDocumentEdited(): boolean;
|
||||
|
||||
handleTitleDoubleClick(): void;
|
||||
|
||||
updateTouchBar(items: ISerializableCommandAction[][]): void;
|
||||
|
||||
@@ -454,16 +454,14 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
|
||||
//
|
||||
// These are windows to restore because of hot-exit or from previous session (only performed once on startup!)
|
||||
//
|
||||
let foldersToRestore: URI[] = [];
|
||||
let workspacesToRestore: IWorkspacePathToOpen[] = [];
|
||||
if (openConfig.initialStartup && !openConfig.cli.extensionDevelopmentPath && !openConfig.cli['disable-restore-windows']) {
|
||||
let foldersToRestore = this.backupMainService.getFolderBackupPaths();
|
||||
foldersToOpen.push(...foldersToRestore.map(f => ({ folderUri: f, remoteAuhority: getRemoteAuthority(f) })));
|
||||
|
||||
// collect from workspaces with hot-exit backups and from previous window session
|
||||
workspacesToRestore = [...this.backupMainService.getWorkspaceBackups(), ...this.workspacesMainService.getUntitledWorkspacesSync()];
|
||||
// Untitled workspaces are always restored
|
||||
workspacesToRestore = this.workspacesMainService.getUntitledWorkspacesSync();
|
||||
workspacesToOpen.push(...workspacesToRestore);
|
||||
|
||||
// Empty windows with backups are always restored
|
||||
emptyToRestore.push(...this.backupMainService.getEmptyWindowBackupPaths());
|
||||
} else {
|
||||
emptyToRestore.length = 0;
|
||||
@@ -495,7 +493,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
|
||||
const usedWindow = usedWindows[i];
|
||||
if (
|
||||
(usedWindow.openedWorkspace && workspacesToRestore.some(workspace => usedWindow.openedWorkspace && workspace.workspace.id === usedWindow.openedWorkspace.id)) || // skip over restored workspace
|
||||
(usedWindow.openedFolderUri && foldersToRestore.some(uri => isEqual(uri, usedWindow.openedFolderUri))) || // skip over restored folder
|
||||
(usedWindow.backupPath && emptyToRestore.some(empty => usedWindow.backupPath && empty.backupFolder === basename(usedWindow.backupPath))) // skip over restored empty window
|
||||
) {
|
||||
continue;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { localize } from 'vs/nls';
|
||||
import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/workspace';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform';
|
||||
import { extname } from 'vs/base/common/path';
|
||||
import { extname, isAbsolute } from 'vs/base/common/path';
|
||||
import { dirname, resolvePath, isEqualAuthority, isEqualOrParent, relativePath, extname as resourceExtname } from 'vs/base/common/resources';
|
||||
import * as jsonEdit from 'vs/base/common/jsonEdit';
|
||||
import * as json from 'vs/base/common/json';
|
||||
@@ -18,7 +18,7 @@ import { toSlashes } from 'vs/base/common/extpath';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { Event as CommonEvent } from 'vs/base/common/event';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
|
||||
export const WORKSPACE_EXTENSION = 'code-workspace';
|
||||
@@ -31,18 +31,21 @@ export interface IWorkspacesService {
|
||||
|
||||
_serviceBrand: undefined;
|
||||
|
||||
// Management
|
||||
// Workspaces Management
|
||||
enterWorkspace(path: URI): Promise<IEnterWorkspaceResult | null>;
|
||||
createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise<IWorkspaceIdentifier>;
|
||||
deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise<void>;
|
||||
getWorkspaceIdentifier(workspacePath: URI): Promise<IWorkspaceIdentifier>;
|
||||
|
||||
// History
|
||||
readonly onRecentlyOpenedChange: CommonEvent<void>;
|
||||
// Workspaces History
|
||||
readonly onRecentlyOpenedChange: Event<void>;
|
||||
addRecentlyOpened(recents: IRecent[]): Promise<void>;
|
||||
removeRecentlyOpened(workspaces: URI[]): Promise<void>;
|
||||
clearRecentlyOpened(): Promise<void>;
|
||||
getRecentlyOpened(): Promise<IRecentlyOpened>;
|
||||
|
||||
// Dirty Workspaces
|
||||
getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>>;
|
||||
}
|
||||
|
||||
export interface IRecentlyOpened {
|
||||
@@ -203,21 +206,22 @@ const SLASH = '/';
|
||||
* Undefined is returned if the folderURI and the targetConfigFolderURI don't have the same schema or authority
|
||||
*
|
||||
* @param folderURI a workspace folder
|
||||
* @param forceAbsolute if set, keep the path absolute
|
||||
* @param folderName a workspace name
|
||||
* @param targetConfigFolderURI the folder where the workspace is living in
|
||||
* @param useSlashForPath if set, use forward slashes for file paths on windows
|
||||
*/
|
||||
export function getStoredWorkspaceFolder(folderURI: URI, folderName: string | undefined, targetConfigFolderURI: URI, useSlashForPath = !isWindows): IStoredWorkspaceFolder {
|
||||
export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean, folderName: string | undefined, targetConfigFolderURI: URI, useSlashForPath = !isWindows): IStoredWorkspaceFolder {
|
||||
|
||||
if (folderURI.scheme !== targetConfigFolderURI.scheme) {
|
||||
return { name: folderName, uri: folderURI.toString(true) };
|
||||
}
|
||||
|
||||
let folderPath: string | undefined;
|
||||
if (isEqualOrParent(folderURI, targetConfigFolderURI)) {
|
||||
// use relative path
|
||||
folderPath = relativePath(targetConfigFolderURI, folderURI) || '.'; // always uses forward slashes
|
||||
if (isWindows && folderURI.scheme === Schemas.file && !useSlashForPath) {
|
||||
let folderPath = !forceAbsolute ? relativePath(targetConfigFolderURI, folderURI) : undefined;
|
||||
if (folderPath !== undefined) {
|
||||
if (folderPath.length === 0) {
|
||||
folderPath = '.';
|
||||
} else if (isWindows && folderURI.scheme === Schemas.file && !useSlashForPath) {
|
||||
// Windows gets special treatment:
|
||||
// - use backslahes unless slash is used by other existing folders
|
||||
folderPath = folderPath.replace(/\//g, '\\');
|
||||
@@ -249,7 +253,7 @@ export function getStoredWorkspaceFolder(folderURI: URI, folderName: string | un
|
||||
* Rewrites the content of a workspace file to be saved at a new location.
|
||||
* Throws an exception if file is not a valid workspace file
|
||||
*/
|
||||
export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, configPathURI: URI, targetConfigPathURI: URI) {
|
||||
export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, configPathURI: URI, isFromUntitledWorkspace: boolean, targetConfigPathURI: URI) {
|
||||
let storedWorkspace = doParseStoredWorkspace(configPathURI, rawWorkspaceContents);
|
||||
|
||||
const sourceConfigFolder = dirname(configPathURI);
|
||||
@@ -258,12 +262,17 @@ export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string,
|
||||
const rewrittenFolders: IStoredWorkspaceFolder[] = [];
|
||||
const slashForPath = useSlashForPath(storedWorkspace.folders);
|
||||
|
||||
// Rewrite absolute paths to relative paths if the target workspace folder
|
||||
// is a parent of the location of the workspace file itself. Otherwise keep
|
||||
// using absolute paths.
|
||||
for (const folder of storedWorkspace.folders) {
|
||||
let folderURI = isRawFileWorkspaceFolder(folder) ? resolvePath(sourceConfigFolder, folder.path) : URI.parse(folder.uri);
|
||||
rewrittenFolders.push(getStoredWorkspaceFolder(folderURI, folder.name, targetConfigFolder, slashForPath));
|
||||
const folderURI = isRawFileWorkspaceFolder(folder) ? resolvePath(sourceConfigFolder, folder.path) : URI.parse(folder.uri);
|
||||
let absolute;
|
||||
if (isFromUntitledWorkspace) {
|
||||
// if it was an untitled workspace, try to make paths relative
|
||||
absolute = false;
|
||||
} else {
|
||||
// for existing workspaces, preserve whether a path was absolute or relative
|
||||
absolute = !isRawFileWorkspaceFolder(folder) || isAbsolute(folder.path);
|
||||
}
|
||||
rewrittenFolders.push(getStoredWorkspaceFolder(folderURI, absolute, folder.name, targetConfigFolder, slashForPath));
|
||||
}
|
||||
|
||||
// Preserve as much of the existing workspace as possible by using jsonEdit
|
||||
|
||||
@@ -9,7 +9,6 @@ import { IStateService } from 'vs/platform/state/node/state';
|
||||
import { app, JumpListCategory } from 'electron';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { getBaseLabel, getPathLabel, splitName } from 'vs/base/common/labels';
|
||||
import { IPath } from 'vs/platform/windows/common/windows';
|
||||
import { Event as CommonEvent, Emitter } from 'vs/base/common/event';
|
||||
import { isWindows, isMacintosh } from 'vs/base/common/platform';
|
||||
import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IRecentlyOpened, isRecentWorkspace, isRecentFolder, IRecent, isRecentFile, IRecentFolder, IRecentWorkspace, IRecentFile, toStoreData, restoreRecentlyOpened, RecentlyOpenedStorageData } from 'vs/platform/workspaces/common/workspaces';
|
||||
@@ -24,6 +23,7 @@ import { exists } from 'vs/base/node/pfs';
|
||||
import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { ICodeWindow } from 'vs/platform/windows/electron-main/windows';
|
||||
|
||||
export const IWorkspacesHistoryMainService = createDecorator<IWorkspacesHistoryMainService>('workspacesHistoryMainService');
|
||||
|
||||
@@ -34,7 +34,7 @@ export interface IWorkspacesHistoryMainService {
|
||||
readonly onRecentlyOpenedChange: CommonEvent<void>;
|
||||
|
||||
addRecentlyOpened(recents: IRecent[]): void;
|
||||
getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier, currentFolder?: ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened;
|
||||
getRecentlyOpened(include?: ICodeWindow): IRecentlyOpened;
|
||||
removeRecentlyOpened(paths: URI[]): void;
|
||||
clearRecentlyOpened(): void;
|
||||
|
||||
@@ -241,20 +241,23 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa
|
||||
this._onRecentlyOpenedChange.fire();
|
||||
}
|
||||
|
||||
getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier, currentFolder?: ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened {
|
||||
getRecentlyOpened(include?: ICodeWindow): IRecentlyOpened {
|
||||
const workspaces: Array<IRecentFolder | IRecentWorkspace> = [];
|
||||
const files: IRecentFile[] = [];
|
||||
|
||||
// Add current workspace to beginning if set
|
||||
const currentWorkspace = include?.config?.workspace;
|
||||
if (currentWorkspace && !this.workspacesMainService.isUntitledWorkspace(currentWorkspace)) {
|
||||
workspaces.push({ workspace: currentWorkspace });
|
||||
}
|
||||
|
||||
const currentFolder = include?.config?.folderUri;
|
||||
if (currentFolder) {
|
||||
workspaces.push({ folderUri: currentFolder });
|
||||
}
|
||||
|
||||
// Add currently files to open to the beginning if any
|
||||
const currentFiles = include?.config?.filesToOpenOrCreate;
|
||||
if (currentFiles) {
|
||||
for (let currentFile of currentFiles) {
|
||||
const fileUri = currentFile.fileUri;
|
||||
@@ -402,14 +405,14 @@ function location(recent: IRecent): URI {
|
||||
return recent.workspace.configPath;
|
||||
}
|
||||
|
||||
function indexOfWorkspace(arr: IRecent[], workspace: IWorkspaceIdentifier): number {
|
||||
return arrays.firstIndex(arr, w => isRecentWorkspace(w) && w.workspace.id === workspace.id);
|
||||
function indexOfWorkspace(arr: IRecent[], candidate: IWorkspaceIdentifier): number {
|
||||
return arr.findIndex(workspace => isRecentWorkspace(workspace) && workspace.workspace.id === candidate.id);
|
||||
}
|
||||
|
||||
function indexOfFolder(arr: IRecent[], folderURI: ISingleFolderWorkspaceIdentifier): number {
|
||||
return arrays.firstIndex(arr, f => isRecentFolder(f) && areResourcesEqual(f.folderUri, folderURI));
|
||||
function indexOfFolder(arr: IRecent[], candidate: ISingleFolderWorkspaceIdentifier): number {
|
||||
return arr.findIndex(folder => isRecentFolder(folder) && areResourcesEqual(folder.folderUri, candidate));
|
||||
}
|
||||
|
||||
function indexOfFile(arr: IRecentFile[], fileURI: URI): number {
|
||||
return arrays.firstIndex(arr, f => areResourcesEqual(f.fileUri, fileURI));
|
||||
function indexOfFile(arr: IRecentFile[], candidate: URI): number {
|
||||
return arr.findIndex(file => areResourcesEqual(file.fileUri, candidate));
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain
|
||||
const storedWorkspaceFolder: IStoredWorkspaceFolder[] = [];
|
||||
|
||||
for (const folder of folders) {
|
||||
storedWorkspaceFolder.push(getStoredWorkspaceFolder(folder.uri, folder.name, untitledWorkspaceConfigFolder));
|
||||
storedWorkspaceFolder.push(getStoredWorkspaceFolder(folder.uri, true, folder.name, untitledWorkspaceConfigFolder));
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { URI } from 'vs/base/common/uri';
|
||||
import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService';
|
||||
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
|
||||
import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService';
|
||||
import { IBackupMainService } from 'vs/platform/backup/electron-main/backup';
|
||||
|
||||
export class WorkspacesService implements AddFirstParameterToFunctions<IWorkspacesService, Promise<unknown> /* only methods, not events */, number /* window ID */> {
|
||||
|
||||
@@ -17,7 +18,8 @@ export class WorkspacesService implements AddFirstParameterToFunctions<IWorkspac
|
||||
constructor(
|
||||
@IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService,
|
||||
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
|
||||
@IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService
|
||||
@IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService,
|
||||
@IBackupMainService private readonly backupMainService: IBackupMainService
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -51,12 +53,7 @@ export class WorkspacesService implements AddFirstParameterToFunctions<IWorkspac
|
||||
readonly onRecentlyOpenedChange = this.workspacesHistoryMainService.onRecentlyOpenedChange;
|
||||
|
||||
async getRecentlyOpened(windowId: number): Promise<IRecentlyOpened> {
|
||||
const window = this.windowsMainService.getWindowById(windowId);
|
||||
if (window?.config) {
|
||||
return this.workspacesHistoryMainService.getRecentlyOpened(window.config.workspace, window.config.folderUri, window.config.filesToOpenOrCreate);
|
||||
}
|
||||
|
||||
return this.workspacesHistoryMainService.getRecentlyOpened();
|
||||
return this.workspacesHistoryMainService.getRecentlyOpened(this.windowsMainService.getWindowById(windowId));
|
||||
}
|
||||
|
||||
async addRecentlyOpened(windowId: number, recents: IRecent[]): Promise<void> {
|
||||
@@ -72,4 +69,13 @@ export class WorkspacesService implements AddFirstParameterToFunctions<IWorkspac
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region Dirty Workspaces
|
||||
|
||||
async getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>> {
|
||||
return this.backupMainService.getDirtyWorkspaces();
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import * as pfs from 'vs/base/node/pfs';
|
||||
import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
|
||||
import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
|
||||
import { WorkspacesMainService, IStoredWorkspace } from 'vs/platform/workspaces/electron-main/workspacesMainService';
|
||||
import { WORKSPACE_EXTENSION, IRawFileWorkspaceFolder, IWorkspaceFolderCreationData, IRawUriWorkspaceFolder, rewriteWorkspaceFileForNewLocation, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { WORKSPACE_EXTENSION, IRawFileWorkspaceFolder, IWorkspaceFolderCreationData, IRawUriWorkspaceFolder, rewriteWorkspaceFileForNewLocation, IWorkspaceIdentifier, IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
|
||||
@@ -56,6 +56,7 @@ export class TestDialogMainService implements IDialogMainService {
|
||||
}
|
||||
|
||||
export class TestBackupMainService implements IBackupMainService {
|
||||
|
||||
_serviceBrand: undefined;
|
||||
|
||||
isHotExitEnabled(): boolean {
|
||||
@@ -97,6 +98,10 @@ export class TestBackupMainService implements IBackupMainService {
|
||||
unregisterEmptyWindowBackupSync(backupFolder: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
async getDirtyWorkspaces(): Promise<(IWorkspaceIdentifier | URI)[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
suite('WorkspacesMainService', () => {
|
||||
@@ -109,11 +114,27 @@ suite('WorkspacesMainService', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function createWorkspace(folders: string[], names?: string[]) {
|
||||
function createUntitledWorkspace(folders: string[], names?: string[]) {
|
||||
return service.createUntitledWorkspace(folders.map((folder, index) => ({ uri: URI.file(folder), name: names ? names[index] : undefined } as IWorkspaceFolderCreationData)));
|
||||
}
|
||||
|
||||
function createWorkspaceSync(folders: string[], names?: string[]) {
|
||||
function createWorkspace(workspaceConfigPath: string, folders: (string | URI)[], names?: string[]): void {
|
||||
|
||||
const ws: IStoredWorkspace = {
|
||||
folders: []
|
||||
};
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
const f = folders[i];
|
||||
const s: IStoredWorkspaceFolder = f instanceof URI ? { uri: f.toString() } : { path: f };
|
||||
if (names) {
|
||||
s.name = names[i];
|
||||
}
|
||||
ws.folders.push(s);
|
||||
}
|
||||
fs.writeFileSync(workspaceConfigPath, JSON.stringify(ws));
|
||||
}
|
||||
|
||||
function createUntitledWorkspaceSync(folders: string[], names?: string[]) {
|
||||
return service.createUntitledWorkspaceSync(folders.map((folder, index) => ({ uri: URI.file(folder), name: names ? names[index] : undefined } as IWorkspaceFolderCreationData)));
|
||||
}
|
||||
|
||||
@@ -149,7 +170,7 @@ suite('WorkspacesMainService', () => {
|
||||
}
|
||||
|
||||
test('createWorkspace (folders)', async () => {
|
||||
const workspace = await createWorkspace([process.cwd(), os.tmpdir()]);
|
||||
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
|
||||
assert.ok(workspace);
|
||||
assert.ok(fs.existsSync(workspace.configPath.fsPath));
|
||||
assert.ok(service.isUntitledWorkspace(workspace));
|
||||
@@ -163,7 +184,7 @@ suite('WorkspacesMainService', () => {
|
||||
});
|
||||
|
||||
test('createWorkspace (folders with name)', async () => {
|
||||
const workspace = await createWorkspace([process.cwd(), os.tmpdir()], ['currentworkingdirectory', 'tempdir']);
|
||||
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()], ['currentworkingdirectory', 'tempdir']);
|
||||
assert.ok(workspace);
|
||||
assert.ok(fs.existsSync(workspace.configPath.fsPath));
|
||||
assert.ok(service.isUntitledWorkspace(workspace));
|
||||
@@ -195,7 +216,7 @@ suite('WorkspacesMainService', () => {
|
||||
});
|
||||
|
||||
test('createWorkspaceSync (folders)', () => {
|
||||
const workspace = createWorkspaceSync([process.cwd(), os.tmpdir()]);
|
||||
const workspace = createUntitledWorkspaceSync([process.cwd(), os.tmpdir()]);
|
||||
assert.ok(workspace);
|
||||
assert.ok(fs.existsSync(workspace.configPath.fsPath));
|
||||
assert.ok(service.isUntitledWorkspace(workspace));
|
||||
@@ -210,7 +231,7 @@ suite('WorkspacesMainService', () => {
|
||||
});
|
||||
|
||||
test('createWorkspaceSync (folders with names)', () => {
|
||||
const workspace = createWorkspaceSync([process.cwd(), os.tmpdir()], ['currentworkingdirectory', 'tempdir']);
|
||||
const workspace = createUntitledWorkspaceSync([process.cwd(), os.tmpdir()], ['currentworkingdirectory', 'tempdir']);
|
||||
assert.ok(workspace);
|
||||
assert.ok(fs.existsSync(workspace.configPath.fsPath));
|
||||
assert.ok(service.isUntitledWorkspace(workspace));
|
||||
@@ -243,7 +264,7 @@ suite('WorkspacesMainService', () => {
|
||||
});
|
||||
|
||||
test('resolveWorkspaceSync', async () => {
|
||||
const workspace = await createWorkspace([process.cwd(), os.tmpdir()]);
|
||||
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
|
||||
assert.ok(service.resolveLocalWorkspaceSync(workspace.configPath));
|
||||
|
||||
// make it a valid workspace path
|
||||
@@ -262,7 +283,7 @@ suite('WorkspacesMainService', () => {
|
||||
});
|
||||
|
||||
test('resolveWorkspaceSync (support relative paths)', async () => {
|
||||
const workspace = await createWorkspace([process.cwd(), os.tmpdir()]);
|
||||
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
|
||||
fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: './ticino-playground/lib' }] }));
|
||||
|
||||
const resolved = service.resolveLocalWorkspaceSync(workspace.configPath);
|
||||
@@ -270,7 +291,7 @@ suite('WorkspacesMainService', () => {
|
||||
});
|
||||
|
||||
test('resolveWorkspaceSync (support relative paths #2)', async () => {
|
||||
const workspace = await createWorkspace([process.cwd(), os.tmpdir()]);
|
||||
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
|
||||
fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: './ticino-playground/lib/../other' }] }));
|
||||
|
||||
const resolved = service.resolveLocalWorkspaceSync(workspace.configPath);
|
||||
@@ -278,7 +299,7 @@ suite('WorkspacesMainService', () => {
|
||||
});
|
||||
|
||||
test('resolveWorkspaceSync (support relative paths #3)', async () => {
|
||||
const workspace = await createWorkspace([process.cwd(), os.tmpdir()]);
|
||||
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
|
||||
fs.writeFileSync(workspace.configPath.fsPath, JSON.stringify({ folders: [{ path: 'ticino-playground/lib' }] }));
|
||||
|
||||
const resolved = service.resolveLocalWorkspaceSync(workspace.configPath);
|
||||
@@ -286,7 +307,7 @@ suite('WorkspacesMainService', () => {
|
||||
});
|
||||
|
||||
test('resolveWorkspaceSync (support invalid JSON via fault tolerant parsing)', async () => {
|
||||
const workspace = await createWorkspace([process.cwd(), os.tmpdir()]);
|
||||
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
|
||||
fs.writeFileSync(workspace.configPath.fsPath, '{ "folders": [ { "path": "./ticino-playground/lib" } , ] }'); // trailing comma
|
||||
|
||||
const resolved = service.resolveLocalWorkspaceSync(workspace.configPath);
|
||||
@@ -296,14 +317,15 @@ suite('WorkspacesMainService', () => {
|
||||
test('rewriteWorkspaceFileForNewLocation', async () => {
|
||||
const folder1 = process.cwd(); // absolute path because outside of tmpDir
|
||||
const tmpDir = os.tmpdir();
|
||||
const tmpInsideDir = path.join(os.tmpdir(), 'inside');
|
||||
const tmpInsideDir = path.join(tmpDir, 'inside');
|
||||
|
||||
const workspace = await createWorkspace([folder1, tmpInsideDir, path.join(tmpInsideDir, 'somefolder')]);
|
||||
const origContent = fs.readFileSync(workspace.configPath.fsPath).toString();
|
||||
const firstConfigPath = path.join(tmpDir, 'myworkspace0.code-workspace');
|
||||
createWorkspace(firstConfigPath, [folder1, 'inside', path.join('inside', 'somefolder')]);
|
||||
const origContent = fs.readFileSync(firstConfigPath).toString();
|
||||
|
||||
let origConfigPath = workspace.configPath;
|
||||
let origConfigPath = URI.file(firstConfigPath);
|
||||
let workspaceConfigPath = URI.file(path.join(tmpDir, 'inside', 'myworkspace1.code-workspace'));
|
||||
let newContent = rewriteWorkspaceFileForNewLocation(origContent, origConfigPath, workspaceConfigPath);
|
||||
let newContent = rewriteWorkspaceFileForNewLocation(origContent, origConfigPath, false, workspaceConfigPath);
|
||||
let ws = (JSON.parse(newContent) as IStoredWorkspace);
|
||||
assert.equal(ws.folders.length, 3);
|
||||
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, folder1); // absolute path because outside of tmpdir
|
||||
@@ -312,7 +334,7 @@ suite('WorkspacesMainService', () => {
|
||||
|
||||
origConfigPath = workspaceConfigPath;
|
||||
workspaceConfigPath = URI.file(path.join(tmpDir, 'myworkspace2.code-workspace'));
|
||||
newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, workspaceConfigPath);
|
||||
newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath);
|
||||
ws = (JSON.parse(newContent) as IStoredWorkspace);
|
||||
assert.equal(ws.folders.length, 3);
|
||||
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, folder1);
|
||||
@@ -321,51 +343,51 @@ suite('WorkspacesMainService', () => {
|
||||
|
||||
origConfigPath = workspaceConfigPath;
|
||||
workspaceConfigPath = URI.file(path.join(tmpDir, 'other', 'myworkspace2.code-workspace'));
|
||||
newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, workspaceConfigPath);
|
||||
newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath);
|
||||
ws = (JSON.parse(newContent) as IStoredWorkspace);
|
||||
assert.equal(ws.folders.length, 3);
|
||||
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, folder1);
|
||||
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[1]).path, tmpInsideDir);
|
||||
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[2]).path, path.join(tmpInsideDir, 'somefolder'));
|
||||
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[1]).path, isWindows ? '..\\inside' : '../inside');
|
||||
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[2]).path, isWindows ? '..\\inside\\somefolder' : '../inside/somefolder');
|
||||
|
||||
origConfigPath = workspaceConfigPath;
|
||||
workspaceConfigPath = URI.parse('foo://foo/bar/myworkspace2.code-workspace');
|
||||
newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, workspaceConfigPath);
|
||||
newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath);
|
||||
ws = (JSON.parse(newContent) as IStoredWorkspace);
|
||||
assert.equal(ws.folders.length, 3);
|
||||
assert.equal((<IRawUriWorkspaceFolder>ws.folders[0]).uri, URI.file(folder1).toString(true));
|
||||
assert.equal((<IRawUriWorkspaceFolder>ws.folders[1]).uri, URI.file(tmpInsideDir).toString(true));
|
||||
assert.equal((<IRawUriWorkspaceFolder>ws.folders[2]).uri, URI.file(path.join(tmpInsideDir, 'somefolder')).toString(true));
|
||||
|
||||
service.deleteUntitledWorkspaceSync(workspace);
|
||||
fs.unlinkSync(firstConfigPath);
|
||||
});
|
||||
|
||||
test('rewriteWorkspaceFileForNewLocation (preserves comments)', async () => {
|
||||
const workspace = await createWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]);
|
||||
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]);
|
||||
const workspaceConfigPath = URI.file(path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`));
|
||||
|
||||
let origContent = fs.readFileSync(workspace.configPath.fsPath).toString();
|
||||
origContent = `// this is a comment\n${origContent}`;
|
||||
|
||||
let newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, workspaceConfigPath);
|
||||
let newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath);
|
||||
assert.equal(0, newContent.indexOf('// this is a comment'));
|
||||
service.deleteUntitledWorkspaceSync(workspace);
|
||||
});
|
||||
|
||||
test('rewriteWorkspaceFileForNewLocation (preserves forward slashes)', async () => {
|
||||
const workspace = await createWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]);
|
||||
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]);
|
||||
const workspaceConfigPath = URI.file(path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`));
|
||||
|
||||
let origContent = fs.readFileSync(workspace.configPath.fsPath).toString();
|
||||
origContent = origContent.replace(/[\\]/g, '/'); // convert backslash to slash
|
||||
|
||||
const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, workspaceConfigPath);
|
||||
const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath);
|
||||
const ws = (JSON.parse(newContent) as IStoredWorkspace);
|
||||
assert.ok(ws.folders.every(f => (<IRawFileWorkspaceFolder>f).path.indexOf('\\') < 0));
|
||||
service.deleteUntitledWorkspaceSync(workspace);
|
||||
});
|
||||
|
||||
test('rewriteWorkspaceFileForNewLocation (unc paths)', async () => {
|
||||
test.skip('rewriteWorkspaceFileForNewLocation (unc paths)', async () => {
|
||||
if (!isWindows) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -375,10 +397,10 @@ suite('WorkspacesMainService', () => {
|
||||
const folder2Location = '\\\\server\\share2\\some\\path';
|
||||
const folder3Location = path.join(os.tmpdir(), 'wsloc', 'inner', 'more');
|
||||
|
||||
const workspace = await createWorkspace([folder1Location, folder2Location, folder3Location]);
|
||||
const workspace = await createUntitledWorkspace([folder1Location, folder2Location, folder3Location]);
|
||||
const workspaceConfigPath = URI.file(path.join(workspaceLocation, `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`));
|
||||
let origContent = fs.readFileSync(workspace.configPath.fsPath).toString();
|
||||
const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, workspaceConfigPath);
|
||||
const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath);
|
||||
const ws = (JSON.parse(newContent) as IStoredWorkspace);
|
||||
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, folder1Location);
|
||||
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[1]).path, folder2Location);
|
||||
@@ -388,14 +410,14 @@ suite('WorkspacesMainService', () => {
|
||||
});
|
||||
|
||||
test('deleteUntitledWorkspaceSync (untitled)', async () => {
|
||||
const workspace = await createWorkspace([process.cwd(), os.tmpdir()]);
|
||||
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
|
||||
assert.ok(fs.existsSync(workspace.configPath.fsPath));
|
||||
service.deleteUntitledWorkspaceSync(workspace);
|
||||
assert.ok(!fs.existsSync(workspace.configPath.fsPath));
|
||||
});
|
||||
|
||||
test('deleteUntitledWorkspaceSync (saved)', async () => {
|
||||
const workspace = await createWorkspace([process.cwd(), os.tmpdir()]);
|
||||
const workspace = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
|
||||
service.deleteUntitledWorkspaceSync(workspace);
|
||||
});
|
||||
|
||||
@@ -405,14 +427,14 @@ suite('WorkspacesMainService', () => {
|
||||
let untitled = service.getUntitledWorkspacesSync();
|
||||
assert.equal(untitled.length, 0);
|
||||
|
||||
const untitledOne = await createWorkspace([process.cwd(), os.tmpdir()]);
|
||||
const untitledOne = await createUntitledWorkspace([process.cwd(), os.tmpdir()]);
|
||||
assert.ok(fs.existsSync(untitledOne.configPath.fsPath));
|
||||
|
||||
untitled = service.getUntitledWorkspacesSync();
|
||||
assert.equal(1, untitled.length);
|
||||
assert.equal(untitledOne.id, untitled[0].workspace.id);
|
||||
|
||||
const untitledTwo = await createWorkspace([os.tmpdir(), process.cwd()]);
|
||||
const untitledTwo = await createUntitledWorkspace([os.tmpdir(), process.cwd()]);
|
||||
assert.ok(fs.existsSync(untitledTwo.configPath.fsPath));
|
||||
assert.ok(fs.existsSync(untitledOne.configPath.fsPath), `Unexpected workspaces count of 1 (expected 2): ${untitledOne.configPath.fsPath} does not exist anymore?`);
|
||||
const untitledHome = dirname(dirname(untitledTwo.configPath));
|
||||
|
||||
Vendored
+580
@@ -3150,6 +3150,258 @@ declare module 'vscode' {
|
||||
prepareRename?(document: TextDocument, position: Position, token: CancellationToken): ProviderResult<Range | { range: Range, placeholder: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A semantic tokens legend contains the needed information to decipher
|
||||
* the integer encoded representation of semantic tokens.
|
||||
*/
|
||||
export class SemanticTokensLegend {
|
||||
/**
|
||||
* The possible token types.
|
||||
*/
|
||||
public readonly tokenTypes: string[];
|
||||
/**
|
||||
* The possible token modifiers.
|
||||
*/
|
||||
public readonly tokenModifiers: string[];
|
||||
|
||||
constructor(tokenTypes: string[], tokenModifiers: string[]);
|
||||
}
|
||||
|
||||
/**
|
||||
* A semantic tokens builder can help with creating a `SemanticTokens` instance
|
||||
* which contains delta encoded semantic tokens.
|
||||
*/
|
||||
export class SemanticTokensBuilder {
|
||||
|
||||
constructor(legend?: SemanticTokensLegend);
|
||||
|
||||
/**
|
||||
* Add another token.
|
||||
*
|
||||
* @param line The token start line number (absolute value).
|
||||
* @param char The token start character (absolute value).
|
||||
* @param length The token length in characters.
|
||||
* @param tokenType The encoded token type.
|
||||
* @param tokenModifiers The encoded token modifiers.
|
||||
*/
|
||||
push(line: number, char: number, length: number, tokenType: number, tokenModifiers: number): void;
|
||||
|
||||
/**
|
||||
* Add another token. Use only when providing a legend.
|
||||
*
|
||||
* @param range The range of the token. Must be single-line.
|
||||
* @param tokenType The token type.
|
||||
* @param tokenModifiers The token modifiers.
|
||||
*/
|
||||
push(range: Range, tokenType: string, tokenModifiers?: string[]): void;
|
||||
|
||||
/**
|
||||
* Finish and create a `SemanticTokens` instance.
|
||||
*/
|
||||
build(resultId?: string): SemanticTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents semantic tokens, either in a range or in an entire document.
|
||||
* See [provideDocumentSemanticTokens](#DocumentSemanticTokensProvider.provideDocumentSemanticTokens) for an explanation of the format.
|
||||
* See `SemanticTokensBuilder` for a helper to create an instance.
|
||||
*/
|
||||
export class SemanticTokens {
|
||||
/**
|
||||
* The result id of the tokens.
|
||||
*
|
||||
* This is the id that will be passed to `DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits` (if implemented).
|
||||
*/
|
||||
readonly resultId?: string;
|
||||
/**
|
||||
* The actual tokens data.
|
||||
* See [provideDocumentSemanticTokens](#DocumentSemanticTokensProvider.provideDocumentSemanticTokens) for an explanation of the format.
|
||||
*/
|
||||
readonly data: Uint32Array;
|
||||
|
||||
constructor(data: Uint32Array, resultId?: string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents edits to semantic tokens.
|
||||
* See [provideDocumentSemanticTokensEdits](#DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits) for an explanation of the format.
|
||||
*/
|
||||
export class SemanticTokensEdits {
|
||||
/**
|
||||
* The result id of the tokens.
|
||||
*
|
||||
* This is the id that will be passed to `DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits` (if implemented).
|
||||
*/
|
||||
readonly resultId?: string;
|
||||
/**
|
||||
* The edits to the tokens data.
|
||||
* All edits refer to the initial data state.
|
||||
*/
|
||||
readonly edits: SemanticTokensEdit[];
|
||||
|
||||
constructor(edits: SemanticTokensEdit[], resultId?: string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an edit to semantic tokens.
|
||||
* See [provideDocumentSemanticTokensEdits](#DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits) for an explanation of the format.
|
||||
*/
|
||||
export class SemanticTokensEdit {
|
||||
/**
|
||||
* The start offset of the edit.
|
||||
*/
|
||||
readonly start: number;
|
||||
/**
|
||||
* The count of elements to remove.
|
||||
*/
|
||||
readonly deleteCount: number;
|
||||
/**
|
||||
* The elements to insert.
|
||||
*/
|
||||
readonly data?: Uint32Array;
|
||||
|
||||
constructor(start: number, deleteCount: number, data?: Uint32Array);
|
||||
}
|
||||
|
||||
/**
|
||||
* The document semantic tokens provider interface defines the contract between extensions and
|
||||
* semantic tokens.
|
||||
*/
|
||||
export interface DocumentSemanticTokensProvider {
|
||||
/**
|
||||
* An optional event to signal that the semantic tokens from this provider have changed.
|
||||
*/
|
||||
onDidChangeSemanticTokens?: Event<void>;
|
||||
|
||||
/**
|
||||
* A file can contain many tokens, perhaps even hundreds of thousands of tokens. Therefore, to improve
|
||||
* the memory consumption around describing semantic tokens, we have decided to avoid allocating an object
|
||||
* for each token and we represent tokens from a file as an array of integers. Furthermore, the position
|
||||
* of each token is expressed relative to the token before it because most tokens remain stable relative to
|
||||
* each other when edits are made in a file.
|
||||
*
|
||||
* ---
|
||||
* In short, each token takes 5 integers to represent, so a specific token `i` in the file consists of the following array indices:
|
||||
* - at index `5*i` - `deltaLine`: token line number, relative to the previous token
|
||||
* - at index `5*i+1` - `deltaStart`: token start character, relative to the previous token (relative to 0 or the previous token's start if they are on the same line)
|
||||
* - at index `5*i+2` - `length`: the length of the token. A token cannot be multiline.
|
||||
* - at index `5*i+3` - `tokenType`: will be looked up in `SemanticTokensLegend.tokenTypes`. We currently ask that `tokenType` < 65536.
|
||||
* - at index `5*i+4` - `tokenModifiers`: each set bit will be looked up in `SemanticTokensLegend.tokenModifiers`
|
||||
*
|
||||
* ---
|
||||
* ### How to encode tokens
|
||||
*
|
||||
* Here is an example for encoding a file with 3 tokens in a uint32 array:
|
||||
* ```
|
||||
* { line: 2, startChar: 5, length: 3, tokenType: "property", tokenModifiers: ["private", "static"] },
|
||||
* { line: 2, startChar: 10, length: 4, tokenType: "type", tokenModifiers: [] },
|
||||
* { line: 5, startChar: 2, length: 7, tokenType: "class", tokenModifiers: [] }
|
||||
* ```
|
||||
*
|
||||
* 1. First of all, a legend must be devised. This legend must be provided up-front and capture all possible token types.
|
||||
* For this example, we will choose the following legend which must be passed in when registering the provider:
|
||||
* ```
|
||||
* tokenTypes: ['property', 'type', 'class'],
|
||||
* tokenModifiers: ['private', 'static']
|
||||
* ```
|
||||
*
|
||||
* 2. The first transformation step is to encode `tokenType` and `tokenModifiers` as integers using the legend. Token types are looked
|
||||
* up by index, so a `tokenType` value of `1` means `tokenTypes[1]`. Multiple token modifiers can be set by using bit flags,
|
||||
* so a `tokenModifier` value of `3` is first viewed as binary `0b00000011`, which means `[tokenModifiers[0], tokenModifiers[1]]` because
|
||||
* bits 0 and 1 are set. Using this legend, the tokens now are:
|
||||
* ```
|
||||
* { line: 2, startChar: 5, length: 3, tokenType: 0, tokenModifiers: 3 },
|
||||
* { line: 2, startChar: 10, length: 4, tokenType: 1, tokenModifiers: 0 },
|
||||
* { line: 5, startChar: 2, length: 7, tokenType: 2, tokenModifiers: 0 }
|
||||
* ```
|
||||
*
|
||||
* 3. The next step is to represent each token relative to the previous token in the file. In this case, the second token
|
||||
* is on the same line as the first token, so the `startChar` of the second token is made relative to the `startChar`
|
||||
* of the first token, so it will be `10 - 5`. The third token is on a different line than the second token, so the
|
||||
* `startChar` of the third token will not be altered:
|
||||
* ```
|
||||
* { deltaLine: 2, deltaStartChar: 5, length: 3, tokenType: 0, tokenModifiers: 3 },
|
||||
* { deltaLine: 0, deltaStartChar: 5, length: 4, tokenType: 1, tokenModifiers: 0 },
|
||||
* { deltaLine: 3, deltaStartChar: 2, length: 7, tokenType: 2, tokenModifiers: 0 }
|
||||
* ```
|
||||
*
|
||||
* 4. Finally, the last step is to inline each of the 5 fields for a token in a single array, which is a memory friendly representation:
|
||||
* ```
|
||||
* // 1st token, 2nd token, 3rd token
|
||||
* [ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ]
|
||||
* ```
|
||||
*
|
||||
* *NOTE*: When doing edits, it is possible that multiple edits occur until VS Code decides to invoke the semantic tokens provider.
|
||||
* *NOTE*: If the provider cannot temporarily compute semantic tokens, it can indicate this by throwing an error with the message 'Busy'.
|
||||
*/
|
||||
provideDocumentSemanticTokens(document: TextDocument, token: CancellationToken): ProviderResult<SemanticTokens>;
|
||||
|
||||
/**
|
||||
* Instead of always returning all the tokens in a file, it is possible for a `DocumentSemanticTokensProvider` to implement
|
||||
* this method (`updateSemanticTokens`) and then return incremental updates to the previously provided semantic tokens.
|
||||
*
|
||||
* ---
|
||||
* ### How tokens change when the document changes
|
||||
*
|
||||
* Let's look at how tokens might change.
|
||||
*
|
||||
* Continuing with the above example, suppose a new line was inserted at the top of the file.
|
||||
* That would make all the tokens move down by one line (notice how the line has changed for each one):
|
||||
* ```
|
||||
* { line: 3, startChar: 5, length: 3, tokenType: "property", tokenModifiers: ["private", "static"] },
|
||||
* { line: 3, startChar: 10, length: 4, tokenType: "type", tokenModifiers: [] },
|
||||
* { line: 6, startChar: 2, length: 7, tokenType: "class", tokenModifiers: [] }
|
||||
* ```
|
||||
* The integer encoding of the tokens does not change substantially because of the delta-encoding of positions:
|
||||
* ```
|
||||
* // 1st token, 2nd token, 3rd token
|
||||
* [ 3,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ]
|
||||
* ```
|
||||
* It is possible to express these new tokens in terms of an edit applied to the previous tokens:
|
||||
* ```
|
||||
* [ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] // old tokens
|
||||
* [ 3,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] // new tokens
|
||||
*
|
||||
* edit: { start: 0, deleteCount: 1, data: [3] } // replace integer at offset 0 with 3
|
||||
* ```
|
||||
*
|
||||
* Furthermore, let's assume that a new token has appeared on line 4:
|
||||
* ```
|
||||
* { line: 3, startChar: 5, length: 3, tokenType: "property", tokenModifiers: ["private", "static"] },
|
||||
* { line: 3, startChar: 10, length: 4, tokenType: "type", tokenModifiers: [] },
|
||||
* { line: 4, startChar: 3, length: 5, tokenType: "property", tokenModifiers: ["static"] },
|
||||
* { line: 6, startChar: 2, length: 7, tokenType: "class", tokenModifiers: [] }
|
||||
* ```
|
||||
* The integer encoding of the tokens is:
|
||||
* ```
|
||||
* // 1st token, 2nd token, 3rd token, 4th token
|
||||
* [ 3,5,3,0,3, 0,5,4,1,0, 1,3,5,0,2, 2,2,7,2,0, ]
|
||||
* ```
|
||||
* Again, it is possible to express these new tokens in terms of an edit applied to the previous tokens:
|
||||
* ```
|
||||
* [ 3,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] // old tokens
|
||||
* [ 3,5,3,0,3, 0,5,4,1,0, 1,3,5,0,2, 2,2,7,2,0, ] // new tokens
|
||||
*
|
||||
* edit: { start: 10, deleteCount: 1, data: [1,3,5,0,2,2] } // replace integer at offset 10 with [1,3,5,0,2,2]
|
||||
* ```
|
||||
*
|
||||
* *NOTE*: If the provider cannot compute `SemanticTokensEdits`, it can "give up" and return all the tokens in the document again.
|
||||
* *NOTE*: All edits in `SemanticTokensEdits` contain indices in the old integers array, so they all refer to the previous result state.
|
||||
*/
|
||||
provideDocumentSemanticTokensEdits?(document: TextDocument, previousResultId: string, token: CancellationToken): ProviderResult<SemanticTokens | SemanticTokensEdits>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The document range semantic tokens provider interface defines the contract between extensions and
|
||||
* semantic tokens.
|
||||
*/
|
||||
export interface DocumentRangeSemanticTokensProvider {
|
||||
/**
|
||||
* See [provideDocumentSemanticTokens](#DocumentSemanticTokensProvider.provideDocumentSemanticTokens).
|
||||
*/
|
||||
provideDocumentRangeSemanticTokens(document: TextDocument, range: Range, token: CancellationToken): ProviderResult<SemanticTokens>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Value-object describing what options formatting should use.
|
||||
*/
|
||||
@@ -6582,6 +6834,290 @@ declare module 'vscode' {
|
||||
deserializeWebviewPanel(webviewPanel: WebviewPanel, state: any): Thenable<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider for text based custom editors.
|
||||
*
|
||||
* Text based custom editors use a [`TextDocument`](#TextDocument) as their data model. This considerably simplifies
|
||||
* implementing a custom editor as it allows VS Code to handle many common operations such as
|
||||
* undo and backup. The provider is responsible for synchronizing text changes between the webview and the `TextDocument`.
|
||||
*
|
||||
* You should use text based custom editors when dealing with text based file formats, such as `xml` or `json`.
|
||||
* For binary files or more specialized use cases, see [CustomEditorProvider](#CustomEditorProvider).
|
||||
*/
|
||||
export interface CustomTextEditorProvider {
|
||||
|
||||
/**
|
||||
* Resolve a custom editor for a given text resource.
|
||||
*
|
||||
* This is called when a user first opens a resource for a `CustomTextEditorProvider`, or if they reopen an
|
||||
* existing editor using this `CustomTextEditorProvider`.
|
||||
*
|
||||
* To resolve a custom editor, the provider must fill in its initial html content and hook up all
|
||||
* the event listeners it is interested it. The provider can also hold onto the `WebviewPanel` to use later,
|
||||
* for example in a command. See [`WebviewPanel`](#WebviewPanel) for additional details.
|
||||
*
|
||||
* @param document Document for the resource to resolve.
|
||||
* @param webviewPanel Webview to resolve.
|
||||
* @param token A cancellation token that indicates the result is no longer needed.
|
||||
*
|
||||
* @return Thenable indicating that the custom editor has been resolved.
|
||||
*/
|
||||
resolveCustomTextEditor(document: TextDocument, webviewPanel: WebviewPanel, token: CancellationToken): Thenable<void> | void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the editing capability of a custom editor. This allows the custom editor to hook into standard
|
||||
* editor events such as `undo` or `save`.
|
||||
*
|
||||
* @param EditType Type of edits used for the documents this delegate handles.
|
||||
*/
|
||||
interface CustomEditorEditingDelegate<EditType = unknown> {
|
||||
/**
|
||||
* Save the resource.
|
||||
*
|
||||
* @param document Document to save.
|
||||
* @param cancellation Token that signals the save is no longer required (for example, if another save was triggered).
|
||||
*
|
||||
* @return Thenable signaling that the save has completed.
|
||||
*/
|
||||
save(document: CustomDocument<EditType>, cancellation: CancellationToken): Thenable<void>;
|
||||
|
||||
/**
|
||||
* Save the existing resource at a new path.
|
||||
*
|
||||
* @param document Document to save.
|
||||
* @param targetResource Location to save to.
|
||||
*
|
||||
* @return Thenable signaling that the save has completed.
|
||||
*/
|
||||
saveAs(document: CustomDocument<EditType>, targetResource: Uri): Thenable<void>;
|
||||
|
||||
/**
|
||||
* Event triggered by extensions to signal to VS Code that an edit has occurred.
|
||||
*/
|
||||
readonly onDidEdit: Event<CustomDocumentEditEvent<EditType>>;
|
||||
|
||||
/**
|
||||
* Apply a set of edits.
|
||||
*
|
||||
* Note that is not invoked when `onDidEdit` is called because `onDidEdit` implies also updating the view to reflect the edit.
|
||||
*
|
||||
* @param document Document to apply edits to.
|
||||
* @param edit Array of edits. Sorted from oldest to most recent.
|
||||
*
|
||||
* @return Thenable signaling that the change has completed.
|
||||
*/
|
||||
applyEdits(document: CustomDocument<EditType>, edits: ReadonlyArray<EditType>): Thenable<void>;
|
||||
|
||||
/**
|
||||
* Undo a set of edits.
|
||||
*
|
||||
* This is triggered when a user undoes an edit.
|
||||
*
|
||||
* @param document Document to undo edits from.
|
||||
* @param edit Array of edits. Sorted from most recent to oldest.
|
||||
*
|
||||
* @return Thenable signaling that the change has completed.
|
||||
*/
|
||||
undoEdits(document: CustomDocument<EditType>, edits: ReadonlyArray<EditType>): Thenable<void>;
|
||||
|
||||
/**
|
||||
* Revert the file to its last saved state.
|
||||
*
|
||||
* @param document Document to revert.
|
||||
* @param edits Added or applied edits.
|
||||
*
|
||||
* @return Thenable signaling that the change has completed.
|
||||
*/
|
||||
revert(document: CustomDocument<EditType>, edits: CustomDocumentRevert<EditType>): Thenable<void>;
|
||||
|
||||
/**
|
||||
* Back up the resource in its current state.
|
||||
*
|
||||
* Backups are used for hot exit and to prevent data loss. Your `backup` method should persist the resource in
|
||||
* its current state, i.e. with the edits applied. Most commonly this means saving the resource to disk in
|
||||
* the `ExtensionContext.storagePath`. When VS Code reloads and your custom editor is opened for a resource,
|
||||
* your extension should first check to see if any backups exist for the resource. If there is a backup, your
|
||||
* extension should load the file contents from there instead of from the resource in the workspace.
|
||||
*
|
||||
* `backup` is triggered whenever an edit it made. Calls to `backup` are debounced so that if multiple edits are
|
||||
* made in quick succession, `backup` is only triggered after the last one. `backup` is not invoked when
|
||||
* `auto save` is enabled (since auto save already persists resource ).
|
||||
*
|
||||
* @param document Document to backup.
|
||||
* @param cancellation Token that signals the current backup since a new backup is coming in. It is up to your
|
||||
* extension to decided how to respond to cancellation. If for example your extension is backing up a large file
|
||||
* in an operation that takes time to complete, your extension may decide to finish the ongoing backup rather
|
||||
* than cancelling it to ensure that VS Code has some valid backup.
|
||||
*/
|
||||
backup(document: CustomDocument<EditType>, cancellation: CancellationToken): Thenable<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event triggered by extensions to signal to VS Code that an edit has occurred on a `CustomDocument`.
|
||||
*
|
||||
* @param EditType Type of edits used for the document.
|
||||
*/
|
||||
interface CustomDocumentEditEvent<EditType = unknown> {
|
||||
/**
|
||||
* Document the edit is for.
|
||||
*/
|
||||
readonly document: CustomDocument<EditType>;
|
||||
|
||||
/**
|
||||
* Object that describes the edit.
|
||||
*
|
||||
* Edit objects are passed back to your extension in `CustomEditorEditingDelegate.undoEdits`,
|
||||
* `CustomEditorEditingDelegate.applyEdits`, and `CustomEditorEditingDelegate.revert`.
|
||||
*/
|
||||
readonly edit: EditType;
|
||||
|
||||
/**
|
||||
* Display name describing the edit.
|
||||
*/
|
||||
readonly label?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delta for edits undone/redone while reverting for a `CustomDocument`.
|
||||
*
|
||||
* @param EditType Type of edits used for the document being reverted.
|
||||
*/
|
||||
interface CustomDocumentRevert<EditType = unknown> {
|
||||
/**
|
||||
* List of edits that were undone to get the document back to its on disk state.
|
||||
*/
|
||||
readonly undoneEdits: ReadonlyArray<EditType>;
|
||||
|
||||
/**
|
||||
* List of edits that were reapplied to get the document back to its on disk state.
|
||||
*/
|
||||
readonly appliedEdits: ReadonlyArray<EditType>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a custom document used by a [`CustomEditorProvider`](#CustomEditorProvider).
|
||||
*
|
||||
* All custom documents must subclass `CustomDocument`. Custom documents are only used within a given
|
||||
* `CustomEditorProvider`. The lifecycle of a `CustomDocument` is managed by VS Code. When no more references
|
||||
* remain to a `CustomDocument`, it is disposed of.
|
||||
*
|
||||
* @param EditType Type of edits used in this document.
|
||||
*/
|
||||
class CustomDocument<EditType = unknown> {
|
||||
/**
|
||||
* @param uri The associated resource for this document.
|
||||
*/
|
||||
constructor(uri: Uri);
|
||||
|
||||
/**
|
||||
* The associated uri for this document.
|
||||
*/
|
||||
readonly uri: Uri;
|
||||
|
||||
/**
|
||||
* Is this document representing an untitled file which has never been saved yet.
|
||||
*/
|
||||
readonly isUntitled: boolean;
|
||||
|
||||
/**
|
||||
* The version number of this document (it will strictly increase after each
|
||||
* change, including undo/redo).
|
||||
*/
|
||||
readonly version: number;
|
||||
|
||||
/**
|
||||
* `true` if there are unpersisted changes.
|
||||
*/
|
||||
readonly isDirty: boolean;
|
||||
|
||||
/**
|
||||
* List of edits from document open to the document's current state.
|
||||
*
|
||||
* `appliedEdits` returns a copy of the edit stack at the current point in time. Your extension should always
|
||||
* use `CustomDocument.appliedEdits` to check the edit stack instead of holding onto a reference to `appliedEdits`.
|
||||
*/
|
||||
readonly appliedEdits: ReadonlyArray<EditType>;
|
||||
|
||||
/**
|
||||
* List of edits from document open to the document's last saved point.
|
||||
*
|
||||
* The save point will be behind `appliedEdits` if the user saves and then continues editing,
|
||||
* or in front of the last entry in `appliedEdits` if the user saves and then hits undo.
|
||||
*
|
||||
* `savedEdits` returns a copy of the edit stack at the current point in time. Your extension should always
|
||||
* use `CustomDocument.savedEdits` to check the edit stack instead of holding onto a reference to `savedEdits`.
|
||||
*/
|
||||
readonly savedEdits: ReadonlyArray<EditType>;
|
||||
|
||||
/**
|
||||
* `true` if the document has been closed. A closed document isn't synchronized anymore
|
||||
* and won't be reused when the same resource is opened again.
|
||||
*/
|
||||
readonly isClosed: boolean;
|
||||
|
||||
/**
|
||||
* Event fired when there are no more references to the `CustomDocument`.
|
||||
*
|
||||
* This happens when all custom editors for the document have been closed. Once a `CustomDocument` is disposed,
|
||||
* it will not be reused when the same resource is opened again.
|
||||
*/
|
||||
readonly onDidDispose: Event<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider for custom editors that use a custom document model.
|
||||
*
|
||||
* Custom editors use [`CustomDocument`](#CustomDocument) as their document model instead of a [`TextDocument`](#TextDocument).
|
||||
* This gives extensions full control over actions such as edit, save, and backup.
|
||||
*
|
||||
* You should use this type of custom editor when dealing with binary files or more complex scenarios. For simple
|
||||
* text based documents, use [`CustomTextEditorProvider`](#CustomTextEditorProvider) instead.
|
||||
*
|
||||
* @param EditType Type of edits used by the editors of this provider.
|
||||
*/
|
||||
export interface CustomEditorProvider<EditType = unknown> {
|
||||
|
||||
/**
|
||||
* Create a new document for a given resource.
|
||||
*
|
||||
* `openCustomDocument` is called when the first editor for a given resource is opened, and the resolve document
|
||||
* is passed to `resolveCustomEditor`. The resolved `CustomDocument` is re-used for subsequent editor opens.
|
||||
* If all editors for a given resource are closed, the `CustomDocument` is disposed of. Opening an editor at
|
||||
* this point will trigger another call to `openCustomDocument`.
|
||||
*
|
||||
* @param uri Uri of the document to open.
|
||||
* @param token A cancellation token that indicates the result is no longer needed.
|
||||
*
|
||||
* @return The custom document.
|
||||
*/
|
||||
openCustomDocument(uri: Uri, token: CancellationToken): Thenable<CustomDocument<EditType>> | CustomDocument<EditType>;
|
||||
|
||||
/**
|
||||
* Resolve a custom editor for a given resource.
|
||||
*
|
||||
* This is called whenever the user opens a new editor for this `CustomEditorProvider`.
|
||||
*
|
||||
* To resolve a custom editor, the provider must fill in its initial html content and hook up all
|
||||
* the event listeners it is interested it. The provider can also hold onto the `WebviewPanel` to use later,
|
||||
* for example in a command. See [`WebviewPanel`](#WebviewPanel) for additional details.
|
||||
*
|
||||
* @param document Document for the resource being resolved.
|
||||
* @param webviewPanel Webview to resolve.
|
||||
* @param token A cancellation token that indicates the result is no longer needed.
|
||||
*
|
||||
* @return Optional thenable indicating that the custom editor has been resolved.
|
||||
*/
|
||||
resolveCustomEditor(document: CustomDocument<EditType>, webviewPanel: WebviewPanel, token: CancellationToken): Thenable<void> | void;
|
||||
|
||||
/**
|
||||
* Defines the editing capability of the provider.
|
||||
*
|
||||
* When not provided, editors for this provider are considered readonly.
|
||||
*/
|
||||
readonly editingDelegate?: CustomEditorEditingDelegate<EditType>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The clipboard provides read and write access to the system's clipboard.
|
||||
*/
|
||||
@@ -7418,6 +7954,24 @@ declare module 'vscode' {
|
||||
* @param serializer Webview serializer.
|
||||
*/
|
||||
export function registerWebviewPanelSerializer(viewType: string, serializer: WebviewPanelSerializer): Disposable;
|
||||
|
||||
/**
|
||||
* Register a new provider for a custom editor.
|
||||
*
|
||||
* @param viewType Type of the custom editor provider. This should match the `viewType` from the
|
||||
* `package.json` contributions.
|
||||
* @param provider Provider that resolves custom editors.
|
||||
* @param options Options for the provider.
|
||||
*
|
||||
* @return Disposable that unregisters the provider.
|
||||
*/
|
||||
export function registerCustomEditorProvider(
|
||||
viewType: string,
|
||||
provider: CustomEditorProvider | CustomTextEditorProvider,
|
||||
options?: {
|
||||
readonly webviewOptions?: WebviewPanelOptions;
|
||||
}
|
||||
): Disposable;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9291,6 +9845,32 @@ declare module 'vscode' {
|
||||
*/
|
||||
export function registerRenameProvider(selector: DocumentSelector, provider: RenameProvider): Disposable;
|
||||
|
||||
/**
|
||||
* Register a semantic tokens provider for a whole document.
|
||||
*
|
||||
* Multiple providers can be registered for a language. In that case providers are sorted
|
||||
* by their [score](#languages.match) and the best-matching provider is used. Failure
|
||||
* of the selected provider will cause a failure of the whole operation.
|
||||
*
|
||||
* @param selector A selector that defines the documents this provider is applicable to.
|
||||
* @param provider A document semantic tokens provider.
|
||||
* @return A [disposable](#Disposable) that unregisters this provider when being disposed.
|
||||
*/
|
||||
export function registerDocumentSemanticTokensProvider(selector: DocumentSelector, provider: DocumentSemanticTokensProvider, legend: SemanticTokensLegend): Disposable;
|
||||
|
||||
/**
|
||||
* Register a semantic tokens provider for a document range.
|
||||
*
|
||||
* Multiple providers can be registered for a language. In that case providers are sorted
|
||||
* by their [score](#languages.match) and the best-matching provider is used. Failure
|
||||
* of the selected provider will cause a failure of the whole operation.
|
||||
*
|
||||
* @param selector A selector that defines the documents this provider is applicable to.
|
||||
* @param provider A document range semantic tokens provider.
|
||||
* @return A [disposable](#Disposable) that unregisters this provider when being disposed.
|
||||
*/
|
||||
export function registerDocumentRangeSemanticTokensProvider(selector: DocumentSelector, provider: DocumentRangeSemanticTokensProvider, legend: SemanticTokensLegend): Disposable;
|
||||
|
||||
/**
|
||||
* Register a formatting provider for a document.
|
||||
*
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user