VS Code merge to df8fe74bd55313de0dd2303bc47a4aab0ca56b0e (#17979)

* Merge from vscode 504f934659740e9d41501cad9f162b54d7745ad9

* delete unused folders

* distro

* Bump build node version

* update chokidar

* FIx hygiene errors

* distro

* Fix extension lint issues

* Remove strict-vscode

* Add copyright header exemptions

* Bump vscode-extension-telemetry to fix webpacking issue with zone.js

* distro

* Fix failing tests (revert marked.js back to current one until we decide to update)

* Skip searchmodel test

* Fix mac build

* temp debug script loading

* Try disabling coverage

* log error too

* Revert "log error too"

This reverts commit af0183e5d4ab458fdf44b88fbfab9908d090526f.

* Revert "temp debug script loading"

This reverts commit 3d687d541c76db2c5b55626c78ae448d3c25089c.

* Add comments explaining coverage disabling

* Fix ansi_up loading issue

* Merge latest from ads

* Use newer option

* Fix compile

* add debug logging warn

* Always log stack

* log more

* undo debug

* Update to use correct base path (+cleanup)

* distro

* fix compile errors

* Remove strict-vscode

* Fix sql editors not showing

* Show db dropdown input & fix styling

* Fix more info in gallery

* Fix gallery asset requests

* Delete unused workflow

* Fix tapable resolutions for smoke test compile error

* Fix smoke compile

* Disable crash reporting

* Disable interactive

Co-authored-by: ADS Merger <karlb@microsoft.com>
This commit is contained in:
Charles Gagnon
2022-01-06 09:06:56 -08:00
committed by GitHub
parent fd2736b6a6
commit 2bc6a0cd01
2099 changed files with 79520 additions and 43813 deletions

View File

@@ -12,6 +12,7 @@ import { USUAL_WORD_SEPARATORS } from 'vs/editor/common/model/wordHelper';
import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility';
import { IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import product from 'vs/platform/product/common/product';
//#region typed options
@@ -75,7 +76,7 @@ export interface IEditorOptions {
/**
* Control the rendering of line numbers.
* If it is a function, it will be invoked when rendering a line number and the return value will be rendered.
* Otherwise, if it is a truey, line numbers will be rendered normally (equivalent of using an identity function).
* Otherwise, if it is a truthy, line numbers will be rendered normally (equivalent of using an identity function).
* Otherwise, line numbers will not be rendered.
* Defaults to `on`.
*/
@@ -548,6 +549,11 @@ export interface IEditorOptions {
* Defaults to true.
*/
foldingHighlight?: boolean;
/**
* Auto fold imports folding regions.
* Defaults to true.
*/
foldingImportsByDefault?: boolean;
/**
* Controls whether the fold actions in the gutter stay always visible or hide unless the mouse is over the gutter.
* Defaults to 'mouseover'.
@@ -565,7 +571,7 @@ export interface IEditorOptions {
matchBrackets?: 'never' | 'near' | 'always';
/**
* Enable rendering of whitespace.
* Defaults to none.
* Defaults to 'selection'.
*/
renderWhitespace?: 'none' | 'boundary' | 'selection' | 'trailing' | 'all';
/**
@@ -714,7 +720,7 @@ export interface IDiffEditorOptions extends IEditorOptions {
*/
originalAriaLabel?: string;
/**
* Aria label for modifed editor.
* Aria label for modified editor.
*/
modifiedAriaLabel?: string;
}
@@ -1361,7 +1367,7 @@ export interface IEditorFindOptions {
/**
* Controls if we seed search string in the Find Widget with editor selection.
*/
seedSearchStringFromSelection?: boolean;
seedSearchStringFromSelection?: 'never' | 'always' | 'selection';
/**
* Controls if Find in Selection flag is turned on in the editor.
*/
@@ -1388,7 +1394,7 @@ class EditorFind extends BaseEditorOption<EditorOption.find, EditorFindOptions>
constructor() {
const defaults: EditorFindOptions = {
cursorMoveOnType: true,
seedSearchStringFromSelection: true,
seedSearchStringFromSelection: 'always',
autoFindInSelection: 'never',
globalFindClipboard: false,
addExtraSpaceOnTop: true,
@@ -1403,8 +1409,14 @@ class EditorFind extends BaseEditorOption<EditorOption.find, EditorFindOptions>
description: nls.localize('find.cursorMoveOnType', "Controls whether the cursor should jump to find matches while typing.")
},
'editor.find.seedSearchStringFromSelection': {
type: 'boolean',
type: 'string',
enum: ['never', 'always', 'selection'],
default: defaults.seedSearchStringFromSelection,
enumDescriptions: [
nls.localize('editor.find.seedSearchStringFromSelection.never', 'Never seed search string from the editor selection.'),
nls.localize('editor.find.seedSearchStringFromSelection.always', 'Always seed search string from the editor selection, including word at cursor position.'),
nls.localize('editor.find.seedSearchStringFromSelection.selection', 'Only seed search string from the editor selection.')
],
description: nls.localize('find.seedSearchStringFromSelection', "Controls whether the search string in the Find Widget is seeded from the editor selection.")
},
'editor.find.autoFindInSelection': {
@@ -1412,11 +1424,11 @@ class EditorFind extends BaseEditorOption<EditorOption.find, EditorFindOptions>
enum: ['never', 'always', 'multiline'],
default: defaults.autoFindInSelection,
enumDescriptions: [
nls.localize('editor.find.autoFindInSelection.never', 'Never turn on Find in selection automatically (default).'),
nls.localize('editor.find.autoFindInSelection.always', 'Always turn on Find in selection automatically.'),
nls.localize('editor.find.autoFindInSelection.multiline', 'Turn on Find in selection automatically when multiple lines of content are selected.')
nls.localize('editor.find.autoFindInSelection.never', 'Never turn on Find in Selection automatically (default).'),
nls.localize('editor.find.autoFindInSelection.always', 'Always turn on Find in Selection automatically.'),
nls.localize('editor.find.autoFindInSelection.multiline', 'Turn on Find in Selection automatically when multiple lines of content are selected.')
],
description: nls.localize('find.autoFindInSelection', "Controls the condition for turning on find in selection automatically.")
description: nls.localize('find.autoFindInSelection', "Controls the condition for turning on Find in Selection automatically.")
},
'editor.find.globalFindClipboard': {
type: 'boolean',
@@ -1446,7 +1458,9 @@ class EditorFind extends BaseEditorOption<EditorOption.find, EditorFindOptions>
const input = _input as IEditorFindOptions;
return {
cursorMoveOnType: boolean(input.cursorMoveOnType, this.defaultValue.cursorMoveOnType),
seedSearchStringFromSelection: boolean(input.seedSearchStringFromSelection, this.defaultValue.seedSearchStringFromSelection),
seedSearchStringFromSelection: typeof _input.seedSearchStringFromSelection === 'boolean'
? (_input.seedSearchStringFromSelection ? 'always' : 'never')
: stringSet<'never' | 'always' | 'selection'>(input.seedSearchStringFromSelection, this.defaultValue.seedSearchStringFromSelection, ['never', 'always', 'selection']),
autoFindInSelection: typeof _input.autoFindInSelection === 'boolean'
? (_input.autoFindInSelection ? 'always' : 'never')
: stringSet<'never' | 'always' | 'multiline'>(input.autoFindInSelection, this.defaultValue.autoFindInSelection, ['never', 'always', 'multiline']),
@@ -2139,14 +2153,6 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption<EditorOption.
maxMinimapScale = memory.stableFitMaxMinimapScale;
} else {
fitBecomesFill = (effectiveMinimapHeight > minimapCanvasInnerHeight);
if (isViewportWrapping && fitBecomesFill) {
// remember for next time
memory.stableMinimapLayoutInput = input;
memory.stableFitRemainingWidth = remainingWidth;
} else {
memory.stableMinimapLayoutInput = null;
memory.stableFitRemainingWidth = 0;
}
}
}
@@ -2154,14 +2160,28 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption<EditorOption.
minimapHeightIsEditorHeight = true;
const configuredMinimapScale = minimapScale;
minimapLineHeight = Math.min(lineHeight * pixelRatio, Math.max(1, Math.floor(1 / desiredRatio)));
if (isViewportWrapping && couldUseMemory && remainingWidth <= memory.stableFitRemainingWidth) {
// There is a loop when using `fill` and viewport wrapping:
// - view line count impacts minimap layout
// - minimap layout impacts viewport width
// - viewport width impacts view line count
// To break the loop, once we go to a smaller minimap scale, we try to stick with it.
maxMinimapScale = memory.stableFitMaxMinimapScale;
}
minimapScale = Math.min(maxMinimapScale, Math.max(1, Math.floor(minimapLineHeight / baseCharHeight)));
if (minimapScale > configuredMinimapScale) {
minimapWidthMultiplier = Math.min(2, minimapScale / configuredMinimapScale);
}
minimapCharWidth = minimapScale / pixelRatio / minimapWidthMultiplier;
minimapCanvasInnerHeight = Math.ceil((Math.max(typicalViewportLineCount, viewLineCount + extraLinesBeyondLastLine)) * minimapLineHeight);
if (isViewportWrapping && fitBecomesFill) {
if (isViewportWrapping) {
// remember for next time
memory.stableMinimapLayoutInput = input;
memory.stableFitRemainingWidth = remainingWidth;
memory.stableFitMaxMinimapScale = minimapScale;
} else {
memory.stableMinimapLayoutInput = null;
memory.stableFitRemainingWidth = 0;
}
}
}
@@ -2436,7 +2456,7 @@ export type EditorInlayHintsOptions = Readonly<Required<IEditorInlayHintsOptions
class EditorInlayHints extends BaseEditorOption<EditorOption.inlayHints, EditorInlayHintsOptions> {
constructor() {
const defaults: EditorInlayHintsOptions = { enabled: true, fontSize: 0, fontFamily: EDITOR_FONT_DEFAULTS.fontFamily };
const defaults: EditorInlayHintsOptions = { enabled: true, fontSize: 0, fontFamily: '' };
super(
EditorOption.inlayHints, 'inlayHints', defaults,
{
@@ -2448,12 +2468,12 @@ class EditorInlayHints extends BaseEditorOption<EditorOption.inlayHints, EditorI
'editor.inlayHints.fontSize': {
type: 'number',
default: defaults.fontSize,
description: nls.localize('inlayHints.fontSize', "Controls font size of inlay hints in the editor. When set to `0`, the 90% of `#editor.fontSize#` is used.")
markdownDescription: nls.localize('inlayHints.fontSize', "Controls font size of inlay hints in the editor. When set to `0`, the 90% of `#editor.fontSize#` is used.")
},
'editor.inlayHints.fontFamily': {
type: 'string',
default: defaults.fontFamily,
description: nls.localize('inlayHints.fontFamily', "Controls font family of inlay hints in the editor.")
description: nls.localize('inlayHints.fontFamily', "Controls font family of inlay hints in the editor. When set to empty, the `#editor.fontFamily#` is used.")
},
}
);
@@ -2476,13 +2496,14 @@ class EditorInlayHints extends BaseEditorOption<EditorOption.inlayHints, EditorI
//#region lineHeight
class EditorLineHeight extends EditorIntOption<EditorOption.lineHeight> {
class EditorLineHeight extends EditorFloatOption<EditorOption.lineHeight> {
constructor() {
super(
EditorOption.lineHeight, 'lineHeight',
EDITOR_FONT_DEFAULTS.lineHeight, 0, 150,
{ description: nls.localize('lineHeight', "Controls the line height. Use 0 to compute the line height from the font size.") }
EDITOR_FONT_DEFAULTS.lineHeight,
x => EditorFloatOption.clamp(x, 0, 150),
{ markdownDescription: nls.localize('lineHeight', "Controls the line height. \n - Use 0 to automatically compute the line height from the font size.\n - Values between 0 and 8 will be used as a multiplier with the font size.\n - Values greater than or equal to 8 will be used as effective values.") }
);
}
@@ -3005,6 +3026,7 @@ export interface IEditorScrollbarOptions {
/**
* The size of arrows (if displayed).
* Defaults to 11.
* **NOTE**: This option cannot be updated using `updateOptions()`
*/
arrowSize?: number;
/**
@@ -3020,16 +3042,19 @@ export interface IEditorScrollbarOptions {
/**
* Cast horizontal and vertical shadows when the content is scrolled.
* Defaults to true.
* **NOTE**: This option cannot be updated using `updateOptions()`
*/
useShadows?: boolean;
/**
* Render arrows at the top and bottom of the vertical scrollbar.
* Defaults to false.
* **NOTE**: This option cannot be updated using `updateOptions()`
*/
verticalHasArrows?: boolean;
/**
* Render arrows at the left and right of the horizontal scrollbar.
* Defaults to false.
* **NOTE**: This option cannot be updated using `updateOptions()`
*/
horizontalHasArrows?: boolean;
/**
@@ -3040,6 +3065,7 @@ export interface IEditorScrollbarOptions {
/**
* Always consume mouse wheel events (always call preventDefault() and stopPropagation() on the browser events).
* Defaults to true.
* **NOTE**: This option cannot be updated using `updateOptions()`
*/
alwaysConsumeMouseWheel?: boolean;
/**
@@ -3055,11 +3081,13 @@ export interface IEditorScrollbarOptions {
/**
* Width in pixels for the vertical slider.
* Defaults to `verticalScrollbarSize`.
* **NOTE**: This option cannot be updated using `updateOptions()`
*/
verticalSliderSize?: number;
/**
* Height in pixels for the horizontal slider.
* Defaults to `horizontalScrollbarSize`.
* **NOTE**: This option cannot be updated using `updateOptions()`
*/
horizontalSliderSize?: number;
/**
@@ -3099,22 +3127,61 @@ function _scrollbarVisibilityFromString(visibility: string | undefined, defaultV
class EditorScrollbar extends BaseEditorOption<EditorOption.scrollbar, InternalEditorScrollbarOptions> {
constructor() {
const defaults: InternalEditorScrollbarOptions = {
vertical: ScrollbarVisibility.Auto,
horizontal: ScrollbarVisibility.Auto,
arrowSize: 11,
useShadows: true,
verticalHasArrows: false,
horizontalHasArrows: false,
horizontalScrollbarSize: 12,
horizontalSliderSize: 12,
verticalScrollbarSize: 14,
verticalSliderSize: 14,
handleMouseWheel: true,
alwaysConsumeMouseWheel: true,
scrollByPage: false
};
super(
EditorOption.scrollbar, 'scrollbar',
EditorOption.scrollbar, 'scrollbar', defaults,
{
vertical: ScrollbarVisibility.Auto,
horizontal: ScrollbarVisibility.Auto,
arrowSize: 11,
useShadows: true,
verticalHasArrows: false,
horizontalHasArrows: false,
horizontalScrollbarSize: 12,
horizontalSliderSize: 12,
verticalScrollbarSize: 14,
verticalSliderSize: 14,
handleMouseWheel: true,
alwaysConsumeMouseWheel: true,
scrollByPage: false
'editor.scrollbar.vertical': {
type: 'string',
enum: ['auto', 'visible', 'hidden'],
enumDescriptions: [
nls.localize('scrollbar.vertical.auto', "The vertical scrollbar will be visible only when necessary."),
nls.localize('scrollbar.vertical.visible', "The vertical scrollbar will always be visible."),
nls.localize('scrollbar.vertical.fit', "The vertical scrollbar will always be hidden."),
],
default: 'auto',
description: nls.localize('scrollbar.vertical', "Controls the visibility of the vertical scrollbar.")
},
'editor.scrollbar.horizontal': {
type: 'string',
enum: ['auto', 'visible', 'hidden'],
enumDescriptions: [
nls.localize('scrollbar.horizontal.auto', "The horizontal scrollbar will be visible only when necessary."),
nls.localize('scrollbar.horizontal.visible', "The horizontal scrollbar will always be visible."),
nls.localize('scrollbar.horizontal.fit', "The horizontal scrollbar will always be hidden."),
],
default: 'auto',
description: nls.localize('scrollbar.horizontal', "Controls the visibility of the horizontal scrollbar.")
},
'editor.scrollbar.verticalScrollbarSize': {
type: 'number',
default: defaults.verticalScrollbarSize,
description: nls.localize('scrollbar.verticalScrollbarSize', "The width of the vertical scrollbar.")
},
'editor.scrollbar.horizontalScrollbarSize': {
type: 'number',
default: defaults.horizontalScrollbarSize,
description: nls.localize('scrollbar.horizontalScrollbarSize', "The height of the horizontal scrollbar.")
},
'editor.scrollbar.scrollByPage': {
type: 'boolean',
default: defaults.scrollByPage,
description: nls.localize('scrollbar.scrollByPage', "Controls whether clicks scroll by page or jump to click position.")
}
}
);
}
@@ -3153,6 +3220,15 @@ export interface IInlineSuggestOptions {
* Enable or disable the rendering of automatic inline completions.
*/
enabled?: boolean;
/**
* Configures the mode.
* Use `prefix` to only show ghost text if the text to replace is a prefix of the suggestion text.
* Use `subword` to only show ghost text if the replace text is a subword of the suggestion text.
* Use `subwordSmart` to only show ghost text if the replace text is a subword of the suggestion text, but the subword must start after the cursor position.
* Defaults to `prefix`.
*/
mode?: 'prefix' | 'subword' | 'subwordSmart';
}
export type InternalInlineSuggestOptions = Readonly<Required<IInlineSuggestOptions>>;
@@ -3163,7 +3239,8 @@ export type InternalInlineSuggestOptions = Readonly<Required<IInlineSuggestOptio
class InlineEditorSuggest extends BaseEditorOption<EditorOption.inlineSuggest, InternalInlineSuggestOptions> {
constructor() {
const defaults: InternalInlineSuggestOptions = {
enabled: false
enabled: true,
mode: 'subwordSmart'
};
super(
@@ -3174,6 +3251,17 @@ class InlineEditorSuggest extends BaseEditorOption<EditorOption.inlineSuggest, I
default: defaults.enabled,
description: nls.localize('inlineSuggest.enabled', "Controls whether to automatically show inline suggestions in the editor.")
},
'editor.inlineSuggest.mode': {
type: 'string',
enum: ['prefix', 'subword', 'subwordSmart'],
enumDescriptions: [
nls.localize('inlineSuggest.mode.prefix', "Only render an inline suggestion if the replace text is a prefix of the insert text."),
nls.localize('inlineSuggest.mode.subword', "Only render an inline suggestion if the replace text is a subword of the insert text."),
nls.localize('inlineSuggest.mode.subwordSmart', "Only render an inline suggestion if the replace text is a subword of the insert text, but the subword must start after the cursor."),
],
default: defaults.mode,
description: nls.localize('inlineSuggest.mode', "Controls which mode to use for rendering inline suggestions.")
},
}
);
}
@@ -3185,6 +3273,52 @@ class InlineEditorSuggest extends BaseEditorOption<EditorOption.inlineSuggest, I
const input = _input as IInlineSuggestOptions;
return {
enabled: boolean(input.enabled, this.defaultValue.enabled),
mode: stringSet(input.mode, this.defaultValue.mode, ['prefix', 'subword', 'subwordSmart']),
};
}
}
//#endregion
//#region bracketPairColorization
export interface IBracketPairColorizationOptions {
/**
* Enable or disable bracket pair colorization.
*/
enabled?: boolean;
}
export type InternalBracketPairColorizationOptions = Readonly<Required<IBracketPairColorizationOptions>>;
/**
* Configuration options for inline suggestions
*/
class BracketPairColorization extends BaseEditorOption<EditorOption.bracketPairColorization, InternalBracketPairColorizationOptions> {
constructor() {
const defaults: InternalBracketPairColorizationOptions = {
enabled: EDITOR_MODEL_DEFAULTS.bracketPairColorizationOptions.enabled
};
super(
EditorOption.bracketPairColorization, 'bracketPairColorization', defaults,
{
'editor.bracketPairColorization.enabled': {
type: 'boolean',
default: defaults.enabled,
description: nls.localize('bracketPairColorization.enabled', "Controls whether bracket pair colorization is enabled or not.")
}
}
);
}
public validate(_input: any): InternalBracketPairColorizationOptions {
if (!_input || typeof _input !== 'object') {
return this.defaultValue;
}
const input = _input as IBracketPairColorizationOptions;
return {
enabled: boolean(input.enabled, this.defaultValue.enabled)
};
}
}
@@ -3229,6 +3363,10 @@ export interface ISuggestOptions {
* Enable or disable the rendering of the suggestion preview.
*/
preview?: boolean;
/**
* Configures the mode of the preview.
*/
previewMode?: 'prefix' | 'subword' | 'subwordSmart';
/**
* Show details inline with the label. Defaults to true.
*/
@@ -3361,6 +3499,7 @@ class EditorSuggest extends BaseEditorOption<EditorOption.suggest, InternalSugge
showIcons: true,
showStatusBar: false,
preview: false,
previewMode: 'subwordSmart',
showInlineDetails: true,
showMethods: true,
showFunctions: true,
@@ -3439,6 +3578,17 @@ class EditorSuggest extends BaseEditorOption<EditorOption.suggest, InternalSugge
default: defaults.preview,
description: nls.localize('suggest.preview', "Controls whether to preview the suggestion outcome in the editor.")
},
'editor.suggest.previewMode': {
type: 'string',
enum: ['prefix', 'subword', 'subwordSmart'],
enumDescriptions: [
nls.localize('suggest.previewMode.prefix', "Only render a preview if the replace text is a prefix of the insert text."),
nls.localize('suggest.previewMode.subword', "Only render a preview if the replace text is a subword of the insert text."),
nls.localize('suggest.previewMode.subwordSmart', "Render a preview if the replace text is a subword of the insert text, or if it is a prefix of the insert text."),
],
default: defaults.previewMode,
description: nls.localize('suggest.previewMode', "Controls which mode to use for rendering the suggest preview.")
},
'editor.suggest.showInlineDetails': {
type: 'boolean',
default: defaults.showInlineDetails,
@@ -3615,6 +3765,7 @@ class EditorSuggest extends BaseEditorOption<EditorOption.suggest, InternalSugge
showIcons: boolean(input.showIcons, this.defaultValue.showIcons),
showStatusBar: boolean(input.showStatusBar, this.defaultValue.showStatusBar),
preview: boolean(input.preview, this.defaultValue.preview),
previewMode: stringSet(input.previewMode, this.defaultValue.previewMode, ['prefix', 'subword', 'subwordSmart']),
showInlineDetails: boolean(input.showInlineDetails, this.defaultValue.showInlineDetails),
showMethods: boolean(input.showMethods, this.defaultValue.showMethods),
showFunctions: boolean(input.showFunctions, this.defaultValue.showFunctions),
@@ -3798,7 +3949,8 @@ export const EDITOR_MODEL_DEFAULTS = {
insertSpaces: true,
detectIndentation: true,
trimAutoWhitespace: true,
largeFileOptimizations: true
largeFileOptimizations: true,
bracketPairColorizationOptions: { enabled: product.quality !== 'stable' }
};
/**
@@ -3824,6 +3976,7 @@ export const enum EditorOption {
autoIndent,
automaticLayout,
autoSurround,
bracketPairColorization,
codeLens,
codeLensFontFamily,
codeLensFontSize,
@@ -3850,6 +4003,7 @@ export const enum EditorOption {
folding,
foldingStrategy,
foldingHighlight,
foldingImportsByDefault,
unfoldOnClickAfterEndOfLine,
fontFamily,
fontInfo,
@@ -4073,6 +4227,7 @@ export const EditorOptions = {
description: nls.localize('autoSurround', "Controls whether the editor should automatically surround selections when typing quotes or brackets.")
}
)),
bracketPairColorization: register(new BracketPairColorization()),
stickyTabStops: register(new EditorBooleanOption(
EditorOption.stickyTabStops, 'stickyTabStops', false,
{ description: nls.localize('stickyTabStops', "Emulate selection behavior of tab characters when using spaces for indentation. Selection will stick to tab stops.") }
@@ -4090,7 +4245,7 @@ export const EditorOptions = {
default: 0,
minimum: 0,
maximum: 100,
description: nls.localize('codeLensFontSize', "Controls the font size in pixels for CodeLens. When set to `0`, the 90% of `#editor.fontSize#` is used.")
markdownDescription: nls.localize('codeLensFontSize', "Controls the font size in pixels for CodeLens. When set to `0`, the 90% of `#editor.fontSize#` is used.")
})),
colorDecorators: register(new EditorBooleanOption(
EditorOption.colorDecorators, 'colorDecorators', true,
@@ -4194,6 +4349,10 @@ export const EditorOptions = {
EditorOption.foldingHighlight, 'foldingHighlight', true,
{ description: nls.localize('foldingHighlight', "Controls whether the editor should highlight folded ranges.") }
)),
foldingImportsByDefault: register(new EditorBooleanOption(
EditorOption.foldingImportsByDefault, 'foldingImportsByDefault', false,
{ description: nls.localize('foldingImportsByDefault', "Controls whether the editor automatically collapses import ranges.") }
)),
unfoldOnClickAfterEndOfLine: register(new EditorBooleanOption(
EditorOption.unfoldOnClickAfterEndOfLine, 'unfoldOnClickAfterEndOfLine', false,
{ description: nls.localize('unfoldOnClickAfterEndOfLine', "Controls whether clicking on the empty content after a folded line will unfold the line.") }

View File

@@ -19,7 +19,7 @@ const GOLDEN_LINE_HEIGHT_RATIO = platform.isMacintosh ? 1.5 : 1.35;
const MINIMUM_LINE_HEIGHT = 8;
export class BareFontInfo {
readonly _bareFontInfoBrand: void;
readonly _bareFontInfoBrand: void = undefined;
/**
* @internal
@@ -52,8 +52,15 @@ export class BareFontInfo {
*/
private static _create(fontFamily: string, fontWeight: string, fontSize: number, fontFeatureSettings: string, lineHeight: number, letterSpacing: number, zoomLevel: number, pixelRatio: number, ignoreEditorZoom: boolean): BareFontInfo {
if (lineHeight === 0) {
lineHeight = Math.round(GOLDEN_LINE_HEIGHT_RATIO * fontSize);
lineHeight = GOLDEN_LINE_HEIGHT_RATIO * fontSize;
} else if (lineHeight < MINIMUM_LINE_HEIGHT) {
// Values too small to be line heights in pixels are probably in ems. Accept them gracefully.
lineHeight = lineHeight * fontSize;
}
// Enforce integer, minimum constraints
lineHeight = Math.round(lineHeight);
if (lineHeight < MINIMUM_LINE_HEIGHT) {
lineHeight = MINIMUM_LINE_HEIGHT;
}
@@ -133,7 +140,7 @@ export class BareFontInfo {
export const SERIALIZED_FONT_INFO_VERSION = 1;
export class FontInfo extends BareFontInfo {
readonly _editorStylingBrand: void;
readonly _editorStylingBrand: void = undefined;
readonly version: number = SERIALIZED_FONT_INFO_VERSION;
readonly isTrusted: boolean;

View File

@@ -15,7 +15,7 @@ import { Range, IRange } from 'vs/editor/common/core/range';
import { ISelection, Selection, SelectionDirection } from 'vs/editor/common/core/selection';
import * as editorCommon from 'vs/editor/common/editorCommon';
import { ITextModel, TrackedRangeStickiness, IModelDeltaDecoration, ICursorStateComputer, IIdentifiedSingleEditOperation, IValidEditOperation } from 'vs/editor/common/model';
import { RawContentChangedType, ModelRawContentChangedEvent } from 'vs/editor/common/model/textModelEvents';
import { RawContentChangedType, ModelRawContentChangedEvent, ModelInjectedTextChangedEvent } from 'vs/editor/common/model/textModelEvents';
import { VerticalRevealType, ViewCursorStateChangedEvent, ViewRevealRangeRequestEvent } from 'vs/editor/common/view/viewEvents';
import { dispose, Disposable } from 'vs/base/common/lifecycle';
import { ICoordinatesConverter } from 'vs/editor/common/viewModel/viewModel';
@@ -29,7 +29,7 @@ export class CursorModelState {
public readonly modelVersionId: number;
public readonly cursorState: CursorState[];
constructor(model: ITextModel, cursor: Cursor) {
constructor(model: ITextModel, cursor: CursorsController) {
this.modelVersionId = model.getVersionId();
this.cursorState = cursor.getCursorStates();
}
@@ -119,7 +119,7 @@ class AutoClosedAction {
}
}
export class Cursor extends Disposable {
export class CursorsController extends Disposable {
public static readonly MAX_CURSOR_COUNT = 10000;
@@ -144,7 +144,7 @@ export class Cursor extends Disposable {
this._knownModelVersionId = this._model.getVersionId();
this._viewModel = viewModel;
this._coordinatesConverter = coordinatesConverter;
this.context = new CursorContext(this._model, this._coordinatesConverter, cursorConfig);
this.context = new CursorContext(this._model, this._viewModel, this._coordinatesConverter, cursorConfig);
this._cursors = new CursorCollection(this.context);
this._hasFocus = false;
@@ -163,7 +163,7 @@ export class Cursor extends Disposable {
}
public updateConfiguration(cursorConfig: CursorConfiguration): void {
this.context = new CursorContext(this._model, this._coordinatesConverter, cursorConfig);
this.context = new CursorContext(this._model, this._viewModel, this._coordinatesConverter, cursorConfig);
this._cursors.updateContext(this.context);
}
@@ -216,8 +216,8 @@ export class Cursor extends Disposable {
public setStates(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, reason: CursorChangeReason, states: PartialCursorState[] | null): boolean {
let reachedMaxCursorCount = false;
if (states !== null && states.length > Cursor.MAX_CURSOR_COUNT) {
states = states.slice(0, Cursor.MAX_CURSOR_COUNT);
if (states !== null && states.length > CursorsController.MAX_CURSOR_COUNT) {
states = states.slice(0, CursorsController.MAX_CURSOR_COUNT);
reachedMaxCursorCount = true;
}
@@ -328,31 +328,44 @@ export class Cursor extends Disposable {
this.revealPrimary(eventsCollector, 'restoreState', true, editorCommon.ScrollType.Immediate);
}
public onModelContentChanged(eventsCollector: ViewModelEventsCollector, e: ModelRawContentChangedEvent): void {
public onModelContentChanged(eventsCollector: ViewModelEventsCollector, e: ModelRawContentChangedEvent | ModelInjectedTextChangedEvent): void {
if (e instanceof ModelInjectedTextChangedEvent) {
// If injected texts change, the view positions of all cursors need to be updated.
const selectionsFromMarkers = this._cursors.readSelectionFromMarkers();
const newState = CursorState.fromModelSelections(selectionsFromMarkers);
this._knownModelVersionId = e.versionId;
if (this._isHandling) {
return;
}
const hadFlushEvent = e.containsEvent(RawContentChangedType.Flush);
this._prevEditOperationType = EditOperationType.Other;
if (hadFlushEvent) {
// a model.setValue() was called
this._cursors.dispose();
this._cursors = new CursorCollection(this.context);
this._validateAutoClosedActions();
this._emitStateChangedIfNecessary(eventsCollector, 'model', CursorChangeReason.ContentFlush, null, false);
if (didStateChange(this.getCursorStates(), newState || [])) {
// setStates might remove markers, which could trigger a decoration change.
// If there are injected text decorations for that line, `onModelContentChanged` is emitted again
// and an endless recursion happens.
// This is why we only call setStates if we really need to (this fixes recursion).
this.setStates(eventsCollector, 'modelChange', CursorChangeReason.RecoverFromMarkers, newState);
}
} else {
if (this._hasFocus && e.resultingSelection && e.resultingSelection.length > 0) {
const cursorState = CursorState.fromModelSelections(e.resultingSelection);
if (this.setStates(eventsCollector, 'modelChange', e.isUndoing ? CursorChangeReason.Undo : e.isRedoing ? CursorChangeReason.Redo : CursorChangeReason.RecoverFromMarkers, cursorState)) {
this._revealPrimaryCursor(eventsCollector, 'modelChange', VerticalRevealType.Simple, true, editorCommon.ScrollType.Smooth);
}
this._knownModelVersionId = e.versionId;
if (this._isHandling) {
return;
}
const hadFlushEvent = e.containsEvent(RawContentChangedType.Flush);
this._prevEditOperationType = EditOperationType.Other;
if (hadFlushEvent) {
// a model.setValue() was called
this._cursors.dispose();
this._cursors = new CursorCollection(this.context);
this._validateAutoClosedActions();
this._emitStateChangedIfNecessary(eventsCollector, 'model', CursorChangeReason.ContentFlush, null, false);
} else {
const selectionsFromMarkers = this._cursors.readSelectionFromMarkers();
this.setStates(eventsCollector, 'modelChange', CursorChangeReason.RecoverFromMarkers, CursorState.fromModelSelections(selectionsFromMarkers));
if (this._hasFocus && e.resultingSelection && e.resultingSelection.length > 0) {
const cursorState = CursorState.fromModelSelections(e.resultingSelection);
if (this.setStates(eventsCollector, 'modelChange', e.isUndoing ? CursorChangeReason.Undo : e.isRedoing ? CursorChangeReason.Redo : CursorChangeReason.RecoverFromMarkers, cursorState)) {
this._revealPrimaryCursor(eventsCollector, 'modelChange', VerticalRevealType.Simple, true, editorCommon.ScrollType.Smooth);
}
} else {
const selectionsFromMarkers = this._cursors.readSelectionFromMarkers();
this.setStates(eventsCollector, 'modelChange', CursorChangeReason.RecoverFromMarkers, CursorState.fromModelSelections(selectionsFromMarkers));
}
}
}
}
@@ -714,6 +727,28 @@ export class Cursor extends Disposable {
}
}
function didStateChange(currentStates: CursorState[], newStates: PartialCursorState[]): boolean {
if (currentStates.length !== newStates.length) {
return true;
}
for (let i = 0; i < currentStates.length; i++) {
const curState = currentStates[i];
const newState = newStates[i];
if (newState.modelState) {
if (!newState.modelState.equals(curState.modelState)) {
return true;
}
}
if (newState.viewState) {
if (!newState.viewState.equals(curState.viewState)) {
return true;
}
}
}
return false;
}
interface IExecContext {
readonly model: ITextModel;
readonly selectionsBefore: Selection[];

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { CursorContext, CursorState, PartialCursorState } from 'vs/editor/common/controller/cursorCommon';
import { OneCursor } from 'vs/editor/common/controller/oneCursor';
import { Cursor } from 'vs/editor/common/controller/oneCursor';
import { Position } from 'vs/editor/common/core/position';
import { ISelection, Selection } from 'vs/editor/common/core/selection';
@@ -12,15 +12,15 @@ export class CursorCollection {
private context: CursorContext;
private primaryCursor: OneCursor;
private secondaryCursors: OneCursor[];
private primaryCursor: Cursor;
private secondaryCursors: Cursor[];
// An index which identifies the last cursor that was added / moved (think Ctrl+drag)
private lastAddedCursorIndex: number;
constructor(context: CursorContext) {
this.context = context;
this.primaryCursor = new OneCursor(context);
this.primaryCursor = new Cursor(context);
this.secondaryCursors = [];
this.lastAddedCursorIndex = 0;
}
@@ -167,7 +167,7 @@ export class CursorCollection {
}
private _addSecondaryCursor(): void {
this.secondaryCursors.push(new OneCursor(this.context));
this.secondaryCursors.push(new Cursor(this.context));
this.lastAddedCursorIndex = this.secondaryCursors.length;
}
@@ -186,8 +186,8 @@ export class CursorCollection {
this.secondaryCursors.splice(removeIndex, 1);
}
private _getAll(): OneCursor[] {
let result: OneCursor[] = [];
private _getAll(): Cursor[] {
let result: Cursor[] = [];
result[0] = this.primaryCursor;
for (let i = 0, len = this.secondaryCursors.length; i < len; i++) {
result[i + 1] = this.secondaryCursors[i];

View File

@@ -11,7 +11,7 @@ import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { ISelection, Selection } from 'vs/editor/common/core/selection';
import { ICommand, IConfiguration } from 'vs/editor/common/editorCommon';
import { ITextModel, PositionNormalizationAffinity, TextModelResolvedOptions } from 'vs/editor/common/model';
import { ITextModel, PositionAffinity, TextModelResolvedOptions } from 'vs/editor/common/model';
import { TextModel } from 'vs/editor/common/model/textModel';
import { LanguageIdentifier } from 'vs/editor/common/modes';
import { AutoClosingPairs, IAutoClosingPair } from 'vs/editor/common/modes/languageConfiguration';
@@ -58,7 +58,7 @@ const autoCloseNever = () => false;
const autoCloseBeforeWhitespace = (chr: string) => (chr === ' ' || chr === '\t');
export class CursorConfiguration {
_cursorMoveConfigurationBrand: void;
_cursorMoveConfigurationBrand: void = undefined;
public readonly readOnly: boolean;
public readonly tabSize: number;
@@ -221,7 +221,8 @@ export interface ICursorSimpleModel {
getLineMaxColumn(lineNumber: number): number;
getLineFirstNonWhitespaceColumn(lineNumber: number): number;
getLineLastNonWhitespaceColumn(lineNumber: number): number;
normalizePosition(position: Position, affinity: PositionNormalizationAffinity): Position;
normalizePosition(position: Position, affinity: PositionAffinity): Position;
/**
* Gets the column at which indentation stops at a given line.
* @internal
@@ -233,7 +234,7 @@ export interface ICursorSimpleModel {
* Represents the cursor state on either the model or on the view model.
*/
export class SingleCursorState {
_singleCursorStateBrand: void;
_singleCursorStateBrand: void = undefined;
// --- selection can start as a range (think double click and drag)
public readonly selectionStart: Range;
@@ -318,14 +319,16 @@ export class SingleCursorState {
}
export class CursorContext {
_cursorContextBrand: void;
_cursorContextBrand: void = undefined;
public readonly model: ITextModel;
public readonly viewModel: ICursorSimpleModel;
public readonly coordinatesConverter: ICoordinatesConverter;
public readonly cursorConfig: CursorConfiguration;
constructor(model: ITextModel, coordinatesConverter: ICoordinatesConverter, cursorConfig: CursorConfiguration) {
constructor(model: ITextModel, viewModel: ICursorSimpleModel, coordinatesConverter: ICoordinatesConverter, cursorConfig: CursorConfiguration) {
this.model = model;
this.viewModel = viewModel;
this.coordinatesConverter = coordinatesConverter;
this.cursorConfig = cursorConfig;
}
@@ -354,7 +357,7 @@ export class PartialViewCursorState {
export type PartialCursorState = CursorState | PartialModelCursorState | PartialViewCursorState;
export class CursorState {
_cursorStateBrand: void;
_cursorStateBrand: void = undefined;
public static fromModelState(modelState: SingleCursorState): PartialModelCursorState {
return new PartialModelCursorState(modelState);
@@ -398,7 +401,7 @@ export class CursorState {
}
export class EditOperationResult {
_editOperationResultBrand: void;
_editOperationResultBrand: void = undefined;
readonly type: EditOperationType;
readonly commands: Array<ICommand | null>;

View File

@@ -9,10 +9,10 @@ import { Range } from 'vs/editor/common/core/range';
import * as strings from 'vs/base/common/strings';
import { Constants } from 'vs/base/common/uint';
import { AtomicTabMoveOperations, Direction } from 'vs/editor/common/controller/cursorAtomicMoveOperations';
import { PositionNormalizationAffinity } from 'vs/editor/common/model';
import { PositionAffinity } from 'vs/editor/common/model';
export class CursorPosition {
_cursorPositionBrand: void;
_cursorPositionBrand: void = undefined;
public readonly lineNumber: number;
public readonly column: number;
@@ -75,7 +75,7 @@ export class MoveOperations {
const pos = cursor.position.delta(undefined, -(noOfColumns - 1));
// We clip the position before normalization, as normalization is not defined
// for possibly negative columns.
const normalizedPos = model.normalizePosition(MoveOperations.clipPositionColumn(pos, model), PositionNormalizationAffinity.Left);
const normalizedPos = model.normalizePosition(MoveOperations.clipPositionColumn(pos, model), PositionAffinity.Left);
const p = MoveOperations.left(config, model, normalizedPos);
lineNumber = p.lineNumber;
@@ -144,7 +144,7 @@ export class MoveOperations {
column = cursor.selection.endColumn;
} else {
const pos = cursor.position.delta(undefined, noOfColumns - 1);
const normalizedPos = model.normalizePosition(MoveOperations.clipPositionColumn(pos, model), PositionNormalizationAffinity.Right);
const normalizedPos = model.normalizePosition(MoveOperations.clipPositionColumn(pos, model), PositionAffinity.Right);
const r = MoveOperations.right(config, model, normalizedPos);
lineNumber = r.lineNumber;
column = r.column;

View File

@@ -3,13 +3,16 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CursorContext, CursorState, SingleCursorState } from 'vs/editor/common/controller/cursorCommon';
import { CursorContext, CursorState, ICursorSimpleModel, SingleCursorState } from 'vs/editor/common/controller/cursorCommon';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { Selection, SelectionDirection } from 'vs/editor/common/core/selection';
import { TrackedRangeStickiness } from 'vs/editor/common/model';
import { PositionAffinity, TrackedRangeStickiness } from 'vs/editor/common/model';
export class OneCursor {
/**
* Represents a single cursor.
*/
export class Cursor {
public modelState!: SingleCursorState;
public viewState!: SingleCursorState;
@@ -74,7 +77,40 @@ export class OneCursor {
this._setState(context, modelState, viewState);
}
private static _validatePositionWithCache(viewModel: ICursorSimpleModel, position: Position, cacheInput: Position, cacheOutput: Position): Position {
if (position.equals(cacheInput)) {
return cacheOutput;
}
return viewModel.normalizePosition(position, PositionAffinity.None);
}
private static _validateViewState(viewModel: ICursorSimpleModel, viewState: SingleCursorState): SingleCursorState {
const position = viewState.position;
const sStartPosition = viewState.selectionStart.getStartPosition();
const sEndPosition = viewState.selectionStart.getEndPosition();
const validPosition = viewModel.normalizePosition(position, PositionAffinity.None);
const validSStartPosition = this._validatePositionWithCache(viewModel, sStartPosition, position, validPosition);
const validSEndPosition = this._validatePositionWithCache(viewModel, sEndPosition, sStartPosition, validSStartPosition);
if (position.equals(validPosition) && sStartPosition.equals(validSStartPosition) && sEndPosition.equals(validSEndPosition)) {
// fast path: the state is valid
return viewState;
}
return new SingleCursorState(
Range.fromPositions(validSStartPosition, validSEndPosition),
viewState.selectionStartLeftoverVisibleColumns + sStartPosition.column - validSStartPosition.column,
validPosition,
viewState.leftoverVisibleColumns + position.column - validPosition.column,
);
}
private _setState(context: CursorContext, modelState: SingleCursorState | null, viewState: SingleCursorState | null): void {
if (viewState) {
viewState = Cursor._validateViewState(context.viewModel, viewState);
}
if (!modelState) {
if (!viewState) {
return;

View File

@@ -16,18 +16,20 @@ export interface IViewLineTokens {
}
export class LineTokens implements IViewLineTokens {
_lineTokensBrand: void;
_lineTokensBrand: void = undefined;
private readonly _tokens: Uint32Array;
private readonly _tokensCount: number;
private readonly _text: string;
public static defaultTokenMetadata = (
(FontStyle.None << MetadataConsts.FONT_STYLE_OFFSET)
| (ColorId.DefaultForeground << MetadataConsts.FOREGROUND_OFFSET)
| (ColorId.DefaultBackground << MetadataConsts.BACKGROUND_OFFSET)
) >>> 0;
public static createEmpty(lineContent: string): LineTokens {
const defaultMetadata = (
(FontStyle.None << MetadataConsts.FONT_STYLE_OFFSET)
| (ColorId.DefaultForeground << MetadataConsts.FOREGROUND_OFFSET)
| (ColorId.DefaultBackground << MetadataConsts.BACKGROUND_OFFSET)
) >>> 0;
const defaultMetadata = LineTokens.defaultTokenMetadata;
const tokens = new Uint32Array(2);
tokens[0] = lineContent.length;
@@ -165,6 +167,53 @@ export class LineTokens implements IViewLineTokens {
return low;
}
/**
* @pure
* @param insertTokens Must be sorted by offset.
*/
public withInserted(insertTokens: { offset: number, text: string, tokenMetadata: number }[]): LineTokens {
if (insertTokens.length === 0) {
return this;
}
let nextOriginalTokenIdx = 0;
let nextInsertTokenIdx = 0;
let text = '';
const newTokens = new Array<number>();
let originalEndOffset = 0;
while (true) {
let nextOriginalTokenEndOffset = nextOriginalTokenIdx < this._tokensCount ? this._tokens[nextOriginalTokenIdx << 1] : -1;
let nextInsertToken = nextInsertTokenIdx < insertTokens.length ? insertTokens[nextInsertTokenIdx] : null;
if (nextOriginalTokenEndOffset !== -1 && (nextInsertToken === null || nextOriginalTokenEndOffset <= nextInsertToken.offset)) {
// original token ends before next insert token
text += this._text.substring(originalEndOffset, nextOriginalTokenEndOffset);
const metadata = this._tokens[(nextOriginalTokenIdx << 1) + 1];
newTokens.push(text.length, metadata);
nextOriginalTokenIdx++;
originalEndOffset = nextOriginalTokenEndOffset;
} else if (nextInsertToken) {
if (nextInsertToken.offset > originalEndOffset) {
// insert token is in the middle of the next token.
text += this._text.substring(originalEndOffset, nextInsertToken.offset);
const metadata = this._tokens[(nextOriginalTokenIdx << 1) + 1];
newTokens.push(text.length, metadata);
originalEndOffset = nextInsertToken.offset;
}
text += nextInsertToken.text;
newTokens.push(text.length, nextInsertToken.tokenMetadata);
nextInsertTokenIdx++;
} else {
break;
}
}
return new LineTokens(new Uint32Array(newTokens), text);
}
}
export class SlicedLineTokens implements IViewLineTokens {

View File

@@ -134,7 +134,7 @@ export class Range {
}
/**
* Test if `otherRange` is strinctly in `range` (must start after, and end before). If the ranges are equal, will return false.
* Test if `otherRange` is strictly in `range` (must start after, and end before). If the ranges are equal, will return false.
*/
public static strictContainsRange(range: IRange, otherRange: IRange): boolean {
if (otherRange.startLineNumber < range.startLineNumber || otherRange.endLineNumber < range.startLineNumber) {

View File

@@ -8,7 +8,7 @@
* Please don't touch unless you take a look at the IR.
*/
export class RGBA8 {
_rgba8Brand: void;
_rgba8Brand: void = undefined;
static readonly Empty = new RGBA8(0, 0, 0, 0);

View File

@@ -6,7 +6,7 @@
import { IState } from 'vs/editor/common/modes';
export class Token {
_tokenBrand: void;
_tokenBrand: void = undefined;
public readonly offset: number;
public readonly type: string;
@@ -24,7 +24,7 @@ export class Token {
}
export class TokenizationResult {
_tokenizationResultBrand: void;
_tokenizationResultBrand: void = undefined;
public readonly tokens: Token[];
public readonly endState: IState;
@@ -36,7 +36,7 @@ export class TokenizationResult {
}
export class TokenizationResult2 {
_tokenizationResult2Brand: void;
_tokenizationResult2Brand: void = undefined;
/**
* The tokens in binary format. Each token occupies two array indices. For token i:

View File

@@ -620,6 +620,9 @@ export interface IThemeDecorationRenderOptions {
before?: IContentDecorationRenderOptions;
after?: IContentDecorationRenderOptions;
beforeInjectedText?: IContentDecorationRenderOptions & { affectsLetterSpacing?: boolean };
afterInjectedText?: IContentDecorationRenderOptions & { affectsLetterSpacing?: boolean };
}
/**
@@ -640,6 +643,7 @@ export interface IContentDecorationRenderOptions {
color?: string | ThemeColor;
backgroundColor?: string | ThemeColor;
opacity?: string;
verticalAlign?: string;
margin?: string;
padding?: string;

View File

@@ -11,12 +11,13 @@ import { LineTokens } from 'vs/editor/common/core/lineTokens';
import { IPosition, Position } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
import { IModelContentChange, IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, ModelRawContentChangedEvent } from 'vs/editor/common/model/textModelEvents';
import { IModelContentChange, IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, ModelInjectedTextChangedEvent, ModelRawContentChangedEvent } from 'vs/editor/common/model/textModelEvents';
import { SearchData } from 'vs/editor/common/model/textModelSearch';
import { LanguageId, LanguageIdentifier, FormattingOptions } from 'vs/editor/common/modes';
import { ThemeColor } from 'vs/platform/theme/common/themeService';
import { MultilineTokens, MultilineTokens2 } from 'vs/editor/common/model/tokensStore';
import { TextChange } from 'vs/editor/common/model/textChange';
import { equals } from 'vs/base/common/objects';
/**
* Vertical Lane in the overview ruler of the editor.
@@ -156,6 +157,35 @@ export interface IModelDecorationOptions {
* If set, the decoration will be rendered after the text with this CSS class name.
*/
afterContentClassName?: string | null;
/**
* If set, text will be injected in the view after the range.
*/
after?: InjectedTextOptions | null;
/**
* If set, text will be injected in the view before the range.
*/
before?: InjectedTextOptions | null;
}
/**
* Configures text that is injected into the view without changing the underlying document.
*/
export interface InjectedTextOptions {
/**
* Sets the text to inject. Must be a single line.
*/
readonly content: string;
/**
* If set, the decoration will be rendered inline with the text with this CSS class name.
*/
readonly inlineClassName?: string | null;
/**
* If there is an `inlineClassName` which affects letter spacing.
*/
readonly inlineClassNameAffectsLetterSpacing?: boolean;
}
/**
@@ -400,13 +430,14 @@ export interface ICursorStateComputer {
}
export class TextModelResolvedOptions {
_textModelResolvedOptionsBrand: void;
_textModelResolvedOptionsBrand: void = undefined;
readonly tabSize: number;
readonly indentSize: number;
readonly insertSpaces: boolean;
readonly defaultEOL: DefaultEndOfLine;
readonly trimAutoWhitespace: boolean;
readonly bracketPairColorizationOptions: BracketPairColorizationOptions;
/**
* @internal
@@ -417,12 +448,14 @@ export class TextModelResolvedOptions {
insertSpaces: boolean;
defaultEOL: DefaultEndOfLine;
trimAutoWhitespace: boolean;
bracketPairColorizationOptions: BracketPairColorizationOptions;
}) {
this.tabSize = Math.max(1, src.tabSize | 0);
this.indentSize = src.tabSize | 0;
this.insertSpaces = Boolean(src.insertSpaces);
this.defaultEOL = src.defaultEOL | 0;
this.trimAutoWhitespace = Boolean(src.trimAutoWhitespace);
this.bracketPairColorizationOptions = src.bracketPairColorizationOptions;
}
/**
@@ -435,6 +468,7 @@ export class TextModelResolvedOptions {
&& this.insertSpaces === other.insertSpaces
&& this.defaultEOL === other.defaultEOL
&& this.trimAutoWhitespace === other.trimAutoWhitespace
&& equals(this.bracketPairColorizationOptions, other.bracketPairColorizationOptions)
);
}
@@ -463,6 +497,11 @@ export interface ITextModelCreationOptions {
defaultEOL: DefaultEndOfLine;
isForSimpleWidget: boolean;
largeFileOptimizations: boolean;
bracketPairColorizationOptions: BracketPairColorizationOptions;
}
export interface BracketPairColorizationOptions {
enabled: boolean;
}
export interface ITextModelUpdateOptions {
@@ -470,10 +509,11 @@ export interface ITextModelUpdateOptions {
indentSize?: number;
insertSpaces?: boolean;
trimAutoWhitespace?: boolean;
bracketColorizationOptions?: BracketPairColorizationOptions;
}
export class FindMatch {
_findMatchBrand: void;
_findMatchBrand: void = undefined;
public readonly range: Range;
public readonly matches: string[] | null;
@@ -1068,6 +1108,12 @@ export interface ITextModel {
*/
getOverviewRulerDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[];
/**
* Gets all the decorations that contain injected text.
* @param ownerId If set, it will ignore decorations belonging to other owners.
*/
getInjectedTextDecorations(ownerId?: number): IModelDecoration[];
/**
* @internal
*/
@@ -1183,7 +1229,7 @@ export interface ITextModel {
* @internal
* @event
*/
onDidChangeRawContentFast(listener: (e: ModelRawContentChangedEvent) => void): IDisposable;
onDidChangeContentOrInjectedText(listener: (e: ModelRawContentChangedEvent | ModelInjectedTextChangedEvent) => void): IDisposable;
/**
* @deprecated Please use `onDidChangeContent` instead.
* An event emitted when the contents of the model have changed.
@@ -1263,9 +1309,16 @@ export interface ITextModel {
/**
* Among all positions that are projected to the same position in the underlying text model as
* the given position, select a unique position as indicated by the affinity.
*
* PositionAffinity.Left:
* The normalized position must be equal or left to the requested position.
*
* PositionAffinity.Right:
* The normalized position must be equal or right to the requested position.
*
* @internal
*/
normalizePosition(position: Position, affinity: PositionNormalizationAffinity): Position;
normalizePosition(position: Position, affinity: PositionAffinity): Position;
/**
* Gets the column at which indentation stops at a given line.
@@ -1277,15 +1330,21 @@ export interface ITextModel {
/**
* @internal
*/
export const enum PositionNormalizationAffinity {
export const enum PositionAffinity {
/**
* Prefers the left most position.
*/
Left = 0,
/**
* Prefers the right most position.
*/
Right = 1,
/**
* No preference.
*/
None = 2,
}
/**

View File

@@ -0,0 +1,480 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { tail } from 'vs/base/common/arrays';
import { DenseKeyProvider, SmallImmutableSet } from './smallImmutableSet';
import { lengthAdd, lengthZero, Length, lengthHash } from './length';
export const enum AstNodeKind {
Text = 0,
Bracket = 1,
Pair = 2,
UnexpectedClosingBracket = 3,
List = 4,
}
export type AstNode = PairAstNode | ListAstNode | BracketAstNode | InvalidBracketAstNode | TextAstNode;
abstract class BaseAstNode {
abstract readonly kind: AstNodeKind;
abstract readonly children: readonly AstNode[];
abstract readonly unopenedBrackets: SmallImmutableSet<number>;
/**
* In case of a list, determines the height of the (2,3) tree.
*/
abstract readonly listHeight: number;
abstract canBeReused(
expectedClosingCategories: SmallImmutableSet<number>,
endLineDidChange: boolean
): boolean;
/**
* Flattenes all lists in this AST. Only for debugging.
*/
abstract flattenLists(): AstNode;
/**
* Creates a deep clone.
*/
abstract clone(): AstNode;
protected _length: Length;
get length(): Length {
return this._length;
}
constructor(length: Length) {
this._length = length;
}
}
export class PairAstNode extends BaseAstNode {
public static create(
category: number,
openingBracket: BracketAstNode,
child: AstNode | null,
closingBracket: BracketAstNode | null
) {
const length = computeLength(openingBracket, child, closingBracket);
const children = new Array(1);
children[0] = openingBracket;
if (child) {
children.push(child);
}
if (closingBracket) {
children.push(closingBracket);
}
return new PairAstNode(length, category, children, child ? child.unopenedBrackets : SmallImmutableSet.getEmpty());
}
get kind(): AstNodeKind.Pair {
return AstNodeKind.Pair;
}
get listHeight() {
return 0;
}
canBeReused(
expectedClosingCategories: SmallImmutableSet<number>,
endLineDidChange: boolean
) {
if (this.closingBracket === null) {
// Unclosed pair ast nodes only
// end at the end of the document
// or when a parent node is closed.
// This could be improved:
// Only return false if some next token is neither "undefined" nor a bracket that closes a parent.
return false;
}
if (expectedClosingCategories.intersects(this.unopenedBrackets)) {
return false;
}
return true;
}
flattenLists(): PairAstNode {
return PairAstNode.create(
this.category,
this.openingBracket.flattenLists(),
this.child && this.child.flattenLists(),
this.closingBracket && this.closingBracket.flattenLists()
);
}
get openingBracket(): BracketAstNode {
return this.children[0] as BracketAstNode;
}
get child(): AstNode | null {
if (this.children.length <= 1) {
return null;
}
if (this.children[1].kind === AstNodeKind.Bracket) {
return null;
}
return this.children[1] || null;
}
get closingBracket(): BracketAstNode | null {
if (this.children.length <= 1) {
return null;
}
if (this.children[1].kind === AstNodeKind.Bracket) {
return this.children[1] || null;
}
return (this.children[2] as BracketAstNode) || null;
}
private constructor(
length: Length,
public readonly category: number,
public readonly children: readonly AstNode[],
public readonly unopenedBrackets: SmallImmutableSet<number>
) {
super(length);
}
clone(): PairAstNode {
return new PairAstNode(
this.length,
this.category,
clone(this.children),
this.unopenedBrackets
);
}
}
function computeLength(openingBracket: BracketAstNode, child: AstNode | null, closingBracket: BracketAstNode | null): Length {
let length = openingBracket.length;
if (child) {
length = lengthAdd(length, child.length);
}
if (closingBracket) {
length = lengthAdd(length, closingBracket.length);
}
return length;
}
export class ListAstNode extends BaseAstNode {
public static create(items: AstNode[]) {
if (items.length === 0) {
return new ListAstNode(lengthZero, 0, items, SmallImmutableSet.getEmpty());
} else {
let length = items[0].length;
let unopenedBrackets = items[0].unopenedBrackets;
for (let i = 1; i < items.length; i++) {
length = lengthAdd(length, items[i].length);
unopenedBrackets = unopenedBrackets.merge(items[i].unopenedBrackets);
}
return new ListAstNode(length, items[0].listHeight + 1, items, unopenedBrackets);
}
}
get kind(): AstNodeKind.List {
return AstNodeKind.List;
}
get children(): readonly AstNode[] {
return this._items;
}
get unopenedBrackets(): SmallImmutableSet<number> {
return this._unopenedBrackets;
}
private constructor(
length: Length,
public readonly listHeight: number,
private readonly _items: AstNode[],
private _unopenedBrackets: SmallImmutableSet<number>
) {
super(length);
}
canBeReused(
expectedClosingCategories: SmallImmutableSet<number>,
endLineDidChange: boolean
): boolean {
if (this._items.length === 0) {
// might not be very helpful
return true;
}
if (expectedClosingCategories.intersects(this.unopenedBrackets)) {
return false;
}
let lastChild: AstNode = this;
while (lastChild.children.length > 0 && lastChild.kind === AstNodeKind.List) {
lastChild = tail(lastChild.children);
}
return lastChild.canBeReused(
expectedClosingCategories,
endLineDidChange
);
}
flattenLists(): ListAstNode {
const items = new Array<AstNode>();
for (const c of this.children) {
const normalized = c.flattenLists();
if (normalized.kind === AstNodeKind.List) {
items.push(...normalized._items);
} else {
items.push(normalized);
}
}
return ListAstNode.create(items);
}
clone(): ListAstNode {
return new ListAstNode(this.length, this.listHeight, clone(this._items), this.unopenedBrackets);
}
private handleChildrenChanged(): void {
const items = this._items;
if (items.length === 0) {
return;
}
let length = items[0].length;
let unopenedBrackets = items[0].unopenedBrackets;
for (let i = 1; i < items.length; i++) {
length = lengthAdd(length, items[i].length);
unopenedBrackets = unopenedBrackets.merge(items[i].unopenedBrackets);
}
this._length = length;
this._unopenedBrackets = unopenedBrackets;
}
/**
* Appends the given node to the end of this (2,3) tree.
* Returns the new root.
*/
append(nodeToAppend: AstNode): AstNode {
const newNode = this._append(nodeToAppend);
if (newNode) {
return ListAstNode.create([this, newNode]);
}
return this;
}
/**
* @returns Additional node after tree
*/
private _append(nodeToAppend: AstNode): AstNode | undefined {
// assert nodeToInsert.listHeight <= tree.listHeight
if (nodeToAppend.listHeight === this.listHeight) {
return nodeToAppend;
}
const lastItem = this._items[this._items.length - 1];
const newNodeAfter = (lastItem.kind === AstNodeKind.List) ? lastItem._append(nodeToAppend) : nodeToAppend;
if (!newNodeAfter) {
this.handleChildrenChanged();
return undefined;
}
// Can we take the element?
if (this._items.length >= 3) {
// assert tree.items.length === 3
// we need to split to maintain (2,3)-tree property.
// Send the third element + the new element to the parent.
const third = this._items.pop()!;
this.handleChildrenChanged();
return ListAstNode.create([third, newNodeAfter]);
} else {
this._items.push(newNodeAfter);
this.handleChildrenChanged();
return undefined;
}
}
/**
* Prepends the given node to the end of this (2,3) tree.
* Returns the new root.
*/
prepend(nodeToPrepend: AstNode): AstNode {
const newNode = this._prepend(nodeToPrepend);
if (newNode) {
return ListAstNode.create([newNode, this]);
}
return this;
}
/**
* @returns Additional node before tree
*/
private _prepend(nodeToPrepend: AstNode): AstNode | undefined {
// assert nodeToInsert.listHeight <= tree.listHeight
if (nodeToPrepend.listHeight === this.listHeight) {
return nodeToPrepend;
}
if (this.kind !== AstNodeKind.List) {
throw new Error('unexpected');
}
const first = this._items[0];
const newNodeBefore = (first.kind === AstNodeKind.List) ? first._prepend(nodeToPrepend) : nodeToPrepend;
if (!newNodeBefore) {
this.handleChildrenChanged();
return undefined;
}
if (this._items.length >= 3) {
// assert this.items.length === 3
// we need to split to maintain (2,3)-this property.
const first = this._items.shift()!;
this.handleChildrenChanged();
return ListAstNode.create([newNodeBefore, first]);
} else {
this._items.unshift(newNodeBefore);
this.handleChildrenChanged();
return undefined;
}
}
}
function clone(arr: readonly AstNode[]): AstNode[] {
const result = new Array<AstNode>(arr.length);
for (let i = 0; i < arr.length; i++) {
result[i] = arr[i].clone();
}
return result;
}
const emptyArray: readonly AstNode[] = [];
export class TextAstNode extends BaseAstNode {
get kind(): AstNodeKind.Text {
return AstNodeKind.Text;
}
get listHeight() {
return 0;
}
get children(): readonly AstNode[] {
return emptyArray;
}
get unopenedBrackets(): SmallImmutableSet<number> {
return SmallImmutableSet.getEmpty();
}
canBeReused(
expectedClosingCategories: SmallImmutableSet<number>,
endLineDidChange: boolean
) {
// Don't reuse text from a line that got changed.
// Otherwise, long brackes might not be detected.
return !endLineDidChange;
}
flattenLists(): TextAstNode {
return this;
}
clone(): TextAstNode {
return this;
}
}
export class BracketAstNode extends BaseAstNode {
private static cacheByLength = new Map<number, BracketAstNode>();
public static create(length: Length): BracketAstNode {
const lengthKey = lengthHash(length);
const cached = BracketAstNode.cacheByLength.get(lengthKey);
if (cached) {
return cached;
}
const node = new BracketAstNode(length);
BracketAstNode.cacheByLength.set(lengthKey, node);
return node;
}
private constructor(length: Length) {
super(length);
}
get kind(): AstNodeKind.Bracket {
return AstNodeKind.Bracket;
}
get listHeight() {
return 0;
}
get children(): readonly AstNode[] {
return emptyArray;
}
get unopenedBrackets(): SmallImmutableSet<number> {
return SmallImmutableSet.getEmpty();
}
canBeReused(
expectedClosingCategories: SmallImmutableSet<number>,
endLineDidChange: boolean
) {
// These nodes could be reused,
// but not in a general way.
// Their parent may be reused.
return false;
}
flattenLists(): BracketAstNode {
return this;
}
clone(): BracketAstNode {
return this;
}
}
export class InvalidBracketAstNode extends BaseAstNode {
get kind(): AstNodeKind.UnexpectedClosingBracket {
return AstNodeKind.UnexpectedClosingBracket;
}
get listHeight() {
return 0;
}
get children(): readonly AstNode[] {
return emptyArray;
}
public readonly unopenedBrackets: SmallImmutableSet<number>;
constructor(category: number, length: Length, denseKeyProvider: DenseKeyProvider<number>) {
super(length);
this.unopenedBrackets = SmallImmutableSet.getEmpty().add(category, denseKeyProvider);
}
canBeReused(
expectedClosingCategories: SmallImmutableSet<number>,
endLineDidChange: boolean
) {
return !expectedClosingCategories.intersects(this.unopenedBrackets);
}
flattenLists(): InvalidBracketAstNode {
return this;
}
clone(): InvalidBracketAstNode {
return this;
}
}

View File

@@ -0,0 +1,120 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Length, lengthAdd, lengthDiffNonNegative, lengthLessThanEqual, LengthObj, lengthToObj, toLength } from './length';
export class TextEditInfo {
constructor(
public readonly startOffset: Length,
public readonly endOffset: Length,
public readonly newLength: Length
) {
}
}
export class BeforeEditPositionMapper {
private nextEditIdx = 0;
private deltaOldToNewLineCount = 0;
private deltaOldToNewColumnCount = 0;
private deltaLineIdxInOld = -1;
private readonly edits: readonly TextEditInfoCache[];
/**
* @param edits Must be sorted by offset in ascending order.
*/
constructor(
edits: readonly TextEditInfo[],
private readonly documentLength: Length,
) {
this.edits = edits.map(edit => TextEditInfoCache.from(edit));
}
/**
* @param offset Must be equal to or greater than the last offset this method has been called with.
*/
getOffsetBeforeChange(offset: Length): Length {
this.adjustNextEdit(offset);
return this.translateCurToOld(offset);
}
/**
* @param offset Must be equal to or greater than the last offset this method has been called with.
*/
getDistanceToNextChange(offset: Length): Length {
this.adjustNextEdit(offset);
const nextEdit = this.edits[this.nextEditIdx];
const nextChangeOffset = nextEdit ? this.translateOldToCur(nextEdit.offsetObj) : this.documentLength;
return lengthDiffNonNegative(offset, nextChangeOffset);
}
private translateOldToCur(oldOffsetObj: LengthObj): Length {
if (oldOffsetObj.lineCount === this.deltaLineIdxInOld) {
return toLength(oldOffsetObj.lineCount + this.deltaOldToNewLineCount, oldOffsetObj.columnCount + this.deltaOldToNewColumnCount);
} else {
return toLength(oldOffsetObj.lineCount + this.deltaOldToNewLineCount, oldOffsetObj.columnCount);
}
}
private translateCurToOld(newOffset: Length): Length {
const offsetObj = lengthToObj(newOffset);
if (offsetObj.lineCount - this.deltaOldToNewLineCount === this.deltaLineIdxInOld) {
return toLength(offsetObj.lineCount - this.deltaOldToNewLineCount, offsetObj.columnCount - this.deltaOldToNewColumnCount);
} else {
return toLength(offsetObj.lineCount - this.deltaOldToNewLineCount, offsetObj.columnCount);
}
}
private adjustNextEdit(offset: Length) {
while (this.nextEditIdx < this.edits.length) {
const nextEdit = this.edits[this.nextEditIdx];
// After applying the edit, what is its end offset (considering all previous edits)?
const nextEditEndOffsetInCur = this.translateOldToCur(nextEdit.endOffsetAfterObj);
if (lengthLessThanEqual(nextEditEndOffsetInCur, offset)) {
// We are after the edit, skip it
this.nextEditIdx++;
const nextEditEndOffsetInCurObj = lengthToObj(nextEditEndOffsetInCur);
// Before applying the edit, what is its end offset (considering all previous edits)?
const nextEditEndOffsetBeforeInCurObj = lengthToObj(this.translateOldToCur(nextEdit.endOffsetBeforeObj));
const lineDelta = nextEditEndOffsetInCurObj.lineCount - nextEditEndOffsetBeforeInCurObj.lineCount;
this.deltaOldToNewLineCount += lineDelta;
const previousColumnDelta = this.deltaLineIdxInOld === nextEdit.endOffsetBeforeObj.lineCount ? this.deltaOldToNewColumnCount : 0;
const columnDelta = nextEditEndOffsetInCurObj.columnCount - nextEditEndOffsetBeforeInCurObj.columnCount;
this.deltaOldToNewColumnCount = previousColumnDelta + columnDelta;
this.deltaLineIdxInOld = nextEdit.endOffsetBeforeObj.lineCount;
} else {
// We are in or before the edit.
break;
}
}
}
}
class TextEditInfoCache {
static from(edit: TextEditInfo): TextEditInfoCache {
return new TextEditInfoCache(edit.startOffset, edit.endOffset, edit.newLength);
}
public readonly endOffsetBeforeObj: LengthObj;
public readonly endOffsetAfterObj: LengthObj;
public readonly offsetObj: LengthObj;
constructor(
startOffset: Length,
endOffset: Length,
textLength: Length,
) {
this.endOffsetBeforeObj = lengthToObj(endOffset);
this.endOffsetAfterObj = lengthToObj(lengthAdd(startOffset, textLength));
this.offsetObj = lengthToObj(startOffset);
}
}

View File

@@ -0,0 +1,300 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Color } from 'vs/base/common/color';
import { Emitter } from 'vs/base/common/event';
import { Disposable, DisposableStore, IDisposable, IReference, MutableDisposable } from 'vs/base/common/lifecycle';
import { Range } from 'vs/editor/common/core/range';
import { IModelDecoration } from 'vs/editor/common/model';
import { DenseKeyProvider } from 'vs/editor/common/model/bracketPairColorizer/smallImmutableSet';
import { DecorationProvider } from 'vs/editor/common/model/decorationProvider';
import { BackgroundTokenizationState, TextModel } from 'vs/editor/common/model/textModel';
import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents';
import { LanguageId } from 'vs/editor/common/modes';
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
import {
editorBracketHighlightingForeground1, editorBracketHighlightingForeground2, editorBracketHighlightingForeground3, editorBracketHighlightingForeground4, editorBracketHighlightingForeground5, editorBracketHighlightingForeground6, editorBracketHighlightingUnexpectedBracketForeground
} from 'vs/editor/common/view/editorColorRegistry';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { AstNode, AstNodeKind } from './ast';
import { TextEditInfo } from './beforeEditPositionMapper';
import { LanguageAgnosticBracketTokens } from './brackets';
import { Length, lengthAdd, lengthGreaterThanEqual, lengthLessThanEqual, lengthOfString, lengthsToRange, lengthZero, positionToLength, toLength } from './length';
import { parseDocument } from './parser';
import { FastTokenizer, TextBufferTokenizer } from './tokenizer';
export class BracketPairColorizer extends Disposable implements DecorationProvider {
private readonly didChangeDecorationsEmitter = new Emitter<void>();
private readonly cache = this._register(new MutableDisposable<IReference<BracketPairColorizerImpl>>());
get isDocumentSupported() {
const maxSupportedDocumentLength = /* max lines */ 50_000 * /* average column count */ 100;
return this.textModel.getValueLength() <= maxSupportedDocumentLength;
}
constructor(private readonly textModel: TextModel) {
super();
this._register(LanguageConfigurationRegistry.onDidChange((e) => {
if (this.cache.value?.object.didLanguageChange(e.languageIdentifier.id)) {
this.cache.clear();
this.updateCache();
}
}));
this._register(textModel.onDidChangeOptions(e => {
this.cache.clear();
this.updateCache();
}));
this._register(textModel.onDidChangeAttached(() => {
this.updateCache();
}));
}
private updateCache() {
const options = this.textModel.getOptions().bracketPairColorizationOptions;
if (this.textModel.isAttachedToEditor() && this.isDocumentSupported && options.enabled) {
if (!this.cache.value) {
const store = new DisposableStore();
this.cache.value = createDisposableRef(store.add(new BracketPairColorizerImpl(this.textModel)), store);
store.add(this.cache.value.object.onDidChangeDecorations(e => this.didChangeDecorationsEmitter.fire(e)));
this.didChangeDecorationsEmitter.fire();
}
} else {
this.cache.clear();
this.didChangeDecorationsEmitter.fire();
}
}
handleContentChanged(change: IModelContentChangedEvent) {
this.cache.value?.object.handleContentChanged(change);
}
getDecorationsInRange(range: Range, ownerId?: number, filterOutValidation?: boolean): IModelDecoration[] {
if (ownerId === undefined) {
return [];
}
return this.cache.value?.object.getDecorationsInRange(range, ownerId, filterOutValidation) || [];
}
getAllDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[] {
if (ownerId === undefined) {
return [];
}
return this.cache.value?.object.getAllDecorations(ownerId, filterOutValidation) || [];
}
onDidChangeDecorations(listener: () => void): IDisposable {
return this.didChangeDecorationsEmitter.event(listener);
}
}
function createDisposableRef<T>(object: T, disposable?: IDisposable): IReference<T> {
return {
object,
dispose: () => disposable?.dispose(),
};
}
class BracketPairColorizerImpl extends Disposable implements DecorationProvider {
private readonly didChangeDecorationsEmitter = new Emitter<void>();
private readonly colorProvider = new ColorProvider();
/*
There are two trees:
* The initial tree that has no token information and is used for performant initial bracket colorization.
* The tree that used token information to detect bracket pairs.
To prevent flickering, we only switch from the initial tree to tree with token information
when tokenization completes.
Since the text can be edited while background tokenization is in progress, we need to update both trees.
*/
private initialAstWithoutTokens: AstNode | undefined;
private astWithTokens: AstNode | undefined;
private readonly brackets = new LanguageAgnosticBracketTokens([]);
private readonly denseKeyProvider = new DenseKeyProvider<number>();
public didLanguageChange(languageId: LanguageId): boolean {
return this.brackets.didLanguageChange(languageId);
}
constructor(private readonly textModel: TextModel) {
super();
this._register(textModel.onBackgroundTokenizationStateChanged(() => {
if (textModel.backgroundTokenizationState === BackgroundTokenizationState.Completed) {
const wasUndefined = this.initialAstWithoutTokens === undefined;
// Clear the initial tree as we can use the tree with token information now.
this.initialAstWithoutTokens = undefined;
if (!wasUndefined) {
this.didChangeDecorationsEmitter.fire();
}
}
}));
this._register(textModel.onDidChangeTokens(({ ranges }) => {
const edits = ranges.map(r =>
new TextEditInfo(
toLength(r.fromLineNumber - 1, 0),
toLength(r.toLineNumber, 0),
toLength(r.toLineNumber - r.fromLineNumber + 1, 0)
)
);
this.astWithTokens = this.parseDocumentFromTextBuffer(edits, this.astWithTokens);
if (!this.initialAstWithoutTokens) {
this.didChangeDecorationsEmitter.fire();
}
}));
if (textModel.backgroundTokenizationState === BackgroundTokenizationState.Uninitialized) {
// There are no token information yet
const brackets = this.brackets.getSingleLanguageBracketTokens(this.textModel.getLanguageIdentifier().id);
const tokenizer = new FastTokenizer(this.textModel.getValue(), brackets);
this.initialAstWithoutTokens = parseDocument(tokenizer, [], undefined, this.denseKeyProvider);
this.astWithTokens = this.initialAstWithoutTokens.clone();
} else if (textModel.backgroundTokenizationState === BackgroundTokenizationState.Completed) {
// Skip the initial ast, as there is no flickering.
// Directly create the tree with token information.
this.initialAstWithoutTokens = undefined;
this.astWithTokens = this.parseDocumentFromTextBuffer([], undefined);
} else if (textModel.backgroundTokenizationState === BackgroundTokenizationState.InProgress) {
this.initialAstWithoutTokens = this.parseDocumentFromTextBuffer([], undefined);
this.astWithTokens = this.initialAstWithoutTokens.clone();
}
}
handleContentChanged(change: IModelContentChangedEvent) {
const edits = change.changes.map(c => {
const range = Range.lift(c.range);
return new TextEditInfo(
positionToLength(range.getStartPosition()),
positionToLength(range.getEndPosition()),
lengthOfString(c.text)
);
}).reverse();
this.astWithTokens = this.parseDocumentFromTextBuffer(edits, this.astWithTokens);
if (this.initialAstWithoutTokens) {
this.initialAstWithoutTokens = this.parseDocumentFromTextBuffer(edits, this.initialAstWithoutTokens);
}
}
/**
* @pure (only if isPure = true)
*/
private parseDocumentFromTextBuffer(edits: TextEditInfo[], previousAst: AstNode | undefined): AstNode {
// Is much faster if `isPure = false`.
const isPure = false;
const previousAstClone = isPure ? previousAst?.clone() : previousAst;
const tokenizer = new TextBufferTokenizer(this.textModel, this.brackets);
const result = parseDocument(tokenizer, edits, previousAstClone, this.denseKeyProvider);
return result;
}
getBracketsInRange(range: Range): BracketInfo[] {
const startOffset = toLength(range.startLineNumber - 1, range.startColumn - 1);
const endOffset = toLength(range.endLineNumber - 1, range.endColumn - 1);
const result = new Array<BracketInfo>();
const node = this.initialAstWithoutTokens || this.astWithTokens!;
collectBrackets(node, lengthZero, node.length, startOffset, endOffset, result);
return result;
}
getDecorationsInRange(range: Range, ownerId?: number, filterOutValidation?: boolean): IModelDecoration[] {
const result = new Array<IModelDecoration>();
const bracketsInRange = this.getBracketsInRange(range);
for (const bracket of bracketsInRange) {
result.push({
id: `bracket${bracket.hash()}`,
options: { description: 'BracketPairColorization', inlineClassName: this.colorProvider.getInlineClassName(bracket) },
ownerId: 0,
range: bracket.range
});
}
return result;
}
getAllDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[] {
return this.getDecorationsInRange(new Range(1, 1, this.textModel.getLineCount(), 1), ownerId, filterOutValidation);
}
readonly onDidChangeDecorations = this.didChangeDecorationsEmitter.event;
}
function collectBrackets(node: AstNode, nodeOffsetStart: Length, nodeOffsetEnd: Length, startOffset: Length, endOffset: Length, result: BracketInfo[], level: number = 0): void {
if (node.kind === AstNodeKind.Bracket) {
const range = lengthsToRange(nodeOffsetStart, nodeOffsetEnd);
result.push(new BracketInfo(range, level - 1, false));
} else if (node.kind === AstNodeKind.UnexpectedClosingBracket) {
const range = lengthsToRange(nodeOffsetStart, nodeOffsetEnd);
result.push(new BracketInfo(range, level - 1, true));
} else {
if (node.kind === AstNodeKind.Pair) {
level++;
}
for (const child of node.children) {
nodeOffsetEnd = lengthAdd(nodeOffsetStart, child.length);
if (lengthLessThanEqual(nodeOffsetStart, endOffset) && lengthGreaterThanEqual(nodeOffsetEnd, startOffset)) {
collectBrackets(child, nodeOffsetStart, nodeOffsetEnd, startOffset, endOffset, result, level);
}
nodeOffsetStart = nodeOffsetEnd;
}
}
}
export class BracketInfo {
constructor(
public readonly range: Range,
/** 0-based level */
public readonly level: number,
public readonly isInvalid: boolean,
) { }
hash(): string {
return `${this.range.toString()}-${this.level}`;
}
}
class ColorProvider {
public readonly unexpectedClosingBracketClassName = 'unexpected-closing-bracket';
getInlineClassName(bracket: BracketInfo): string {
if (bracket.isInvalid) {
return this.unexpectedClosingBracketClassName;
}
return this.getInlineClassNameOfLevel(bracket.level);
}
getInlineClassNameOfLevel(level: number): string {
// To support a dynamic amount of colors up to 6 colors,
// we use a number that is a lcm of all numbers from 1 to 6.
return `bracket-highlighting-${level % 30}`;
}
}
registerThemingParticipant((theme, collector) => {
const colors = [
editorBracketHighlightingForeground1,
editorBracketHighlightingForeground2,
editorBracketHighlightingForeground3,
editorBracketHighlightingForeground4,
editorBracketHighlightingForeground5,
editorBracketHighlightingForeground6
];
const colorProvider = new ColorProvider();
collector.addRule(`.monaco-editor .${colorProvider.unexpectedClosingBracketClassName} { color: ${theme.getColor(editorBracketHighlightingUnexpectedBracketForeground)}; }`);
let colorValues = colors
.map(c => theme.getColor(c))
.filter((c): c is Color => !!c)
.filter(c => !c.isTransparent());
for (let level = 0; level < 30; level++) {
const color = colorValues[level % colorValues.length];
collector.addRule(`.monaco-editor .${colorProvider.getInlineClassNameOfLevel(level)} { color: ${color}; }`);
}
});

View File

@@ -0,0 +1,120 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { escapeRegExpCharacters } from 'vs/base/common/strings';
import { LanguageId } from 'vs/editor/common/modes';
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
import { BracketAstNode } from './ast';
import { toLength } from './length';
import { Token, TokenKind } from './tokenizer';
export class BracketTokens {
static createFromLanguage(languageId: LanguageId, customBracketPairs: readonly [string, string][]): BracketTokens {
const brackets = [...(LanguageConfigurationRegistry.getBracketsSupport(languageId)?.brackets || [])];
const tokens = new BracketTokens();
let idxOffset = 0;
for (const pair of brackets) {
const brackets = [
...pair.open.map((value, idx) => ({ value, kind: TokenKind.OpeningBracket, idx: idx + idxOffset })),
...pair.close.map((value, idx) => ({ value, kind: TokenKind.ClosingBracket, idx: idx + idxOffset })),
];
idxOffset += Math.max(pair.open.length, pair.close.length);
for (const bracket of brackets) {
tokens.addBracket(languageId, bracket.value, bracket.kind, bracket.idx);
}
}
for (const pair of customBracketPairs) {
idxOffset++;
tokens.addBracket(languageId, pair[0], TokenKind.OpeningBracket, idxOffset);
tokens.addBracket(languageId, pair[1], TokenKind.ClosingBracket, idxOffset);
}
return tokens;
}
private hasRegExp = false;
private _regExpGlobal: RegExp | null = null;
private readonly map = new Map<string, Token>();
private addBracket(languageId: LanguageId, value: string, kind: TokenKind, idx: number): void {
const length = toLength(0, value.length);
this.map.set(value,
new Token(
length,
kind,
// A language can have at most 1000 bracket pairs.
languageId * 1000 + idx,
languageId,
BracketAstNode.create(length)
)
);
}
getRegExpStr(): string | null {
if (this.isEmpty) {
return null;
} else {
const keys = [...this.map.keys()];
keys.sort();
keys.reverse();
return keys.map(k => escapeRegExpCharacters(k)).join('|');
}
}
/**
* Returns null if there is no such regexp (because there are no brackets).
*/
get regExpGlobal(): RegExp | null {
if (!this.hasRegExp) {
const regExpStr = this.getRegExpStr();
this._regExpGlobal = regExpStr ? new RegExp(regExpStr, 'g') : null;
this.hasRegExp = true;
}
return this._regExpGlobal;
}
getToken(value: string): Token | undefined {
return this.map.get(value);
}
get isEmpty(): boolean {
return this.map.size === 0;
}
}
export class LanguageAgnosticBracketTokens {
private readonly languageIdToBracketTokens: Map<LanguageId, BracketTokens> = new Map();
constructor(private readonly customBracketPairs: readonly [string, string][]) {
}
public didLanguageChange(languageId: LanguageId): boolean {
const existing = this.languageIdToBracketTokens.get(languageId);
if (!existing) {
return false;
}
const newRegExpStr = BracketTokens.createFromLanguage(languageId, this.customBracketPairs).getRegExpStr();
return existing.getRegExpStr() !== newRegExpStr;
}
getSingleLanguageBracketTokens(languageId: LanguageId): BracketTokens {
let singleLanguageBracketTokens = this.languageIdToBracketTokens.get(languageId);
if (!singleLanguageBracketTokens) {
singleLanguageBracketTokens = BracketTokens.createFromLanguage(languageId, this.customBracketPairs);
this.languageIdToBracketTokens.set(languageId, singleLanguageBracketTokens);
}
return singleLanguageBracketTokens;
}
getToken(value: string, languageId: LanguageId): Token | undefined {
const singleLanguageBracketTokens = this.getSingleLanguageBracketTokens(languageId);
return singleLanguageBracketTokens.getToken(value);
}
}

View File

@@ -0,0 +1,92 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AstNode, ListAstNode } from './ast';
/**
* Concatenates a list of (2,3) AstNode's into a single (2,3) AstNode.
* This mutates the items of the input array!
*/
export function concat23Trees(items: AstNode[]): AstNode | null {
if (items.length === 0) {
return null;
}
if (items.length === 1) {
return items[0];
}
if (allItemsHaveSameHeight(items)) {
return concatFast(items);
}
return concatSlow(items);
}
/**
* @param items must be non empty.
*/
function allItemsHaveSameHeight(items: AstNode[]): boolean {
const firstHeight = items[0].listHeight;
for (const item of items) {
if (item.listHeight !== firstHeight) {
return false;
}
}
return true;
}
function concatFast(items: AstNode[]): AstNode | null {
let length = items.length;
// All trees have same height, just create parent nodes.
while (length > 1) {
const newLength = length >> 1;
// Ideally, due to the slice, not a lot of memory is wasted.
const newItems = new Array<AstNode>(newLength);
for (let i = 0; i < newLength; i++) {
const j = i << 1;
newItems[i] = ListAstNode.create(items.slice(j, (j + 3 === length) ? length : j + 2));
}
length = newLength;
items = newItems;
}
return items[0];
}
function heightDiff(node1: AstNode, node2: AstNode): number {
return Math.abs(node1.listHeight - node2.listHeight);
}
function concatSlow(items: AstNode[]): AstNode | null {
// The items might not have the same height.
// We merge all items by using a binary concat operator.
let first = items[0];
let second = items[1];
for (let i = 2; i < items.length; i++) {
const item = items[i];
// Prefer concatenating smaller trees, as the runtime of concat depends on the tree height.
if (heightDiff(first, second) <= heightDiff(second, item)) {
first = concat(first, second);
second = item;
} else {
second = concat(second, item);
}
}
const result = concat(first, second);
return result;
}
function concat(node1: AstNode, node2: AstNode): AstNode {
if (node1.listHeight === node2.listHeight) {
return ListAstNode.create([node1, node2]);
}
else if (node1.listHeight > node2.listHeight) {
// node1 is the tree we want to insert into
return (node1 as ListAstNode).append(node2);
} else {
return (node2 as ListAstNode).prepend(node1);
}
}

View File

@@ -0,0 +1,230 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { splitLines } from 'vs/base/common/strings';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
/**
* Represents a non-negative length in terms of line and column count.
* Prefer using {@link Length}.
*/
export class LengthObj {
public static zero = new LengthObj(0, 0);
public static lengthDiffNonNegative(start: LengthObj, end: LengthObj): LengthObj {
if (end.isLessThan(start)) {
return LengthObj.zero;
}
if (start.lineCount === end.lineCount) {
return new LengthObj(0, end.columnCount - start.columnCount);
} else {
return new LengthObj(end.lineCount - start.lineCount, end.columnCount);
}
}
constructor(
public readonly lineCount: number,
public readonly columnCount: number
) { }
public isZero() {
return this.lineCount === 0 && this.columnCount === 0;
}
public toLength(): Length {
return toLength(this.lineCount, this.columnCount);
}
public isLessThan(other: LengthObj): boolean {
if (this.lineCount !== other.lineCount) {
return this.lineCount < other.lineCount;
}
return this.columnCount < other.columnCount;
}
public isGreaterThan(other: LengthObj): boolean {
if (this.lineCount !== other.lineCount) {
return this.lineCount > other.lineCount;
}
return this.columnCount > other.columnCount;
}
public equals(other: LengthObj): boolean {
return this.lineCount === other.lineCount && this.columnCount === other.columnCount;
}
public compare(other: LengthObj): number {
if (this.lineCount !== other.lineCount) {
return this.lineCount - other.lineCount;
}
return this.columnCount - other.columnCount;
}
public add(other: LengthObj): LengthObj {
if (other.lineCount === 0) {
return new LengthObj(this.lineCount, this.columnCount + other.columnCount);
} else {
return new LengthObj(this.lineCount + other.lineCount, other.columnCount);
}
}
toString() {
return `${this.lineCount},${this.columnCount}`;
}
}
/**
* The end must be greater than or equal to the start.
*/
export function lengthDiff(startLineCount: number, startColumnCount: number, endLineCount: number, endColumnCount: number): Length {
return (startLineCount !== endLineCount)
? toLength(endLineCount - startLineCount, endColumnCount)
: toLength(0, endColumnCount - startColumnCount);
}
/**
* Represents a non-negative length in terms of line and column count.
* Does not allocate.
*/
export type Length = { _brand: 'Length' };
export const lengthZero = 0 as any as Length;
export function lengthIsZero(length: Length): boolean {
return length as any as number === 0;
}
/*
* We have 52 bits available in a JS number.
* We use the upper 26 bits to store the line and the lower 26 bits to store the column.
*
* Set boolean to `true` when debugging, so that debugging is easier.
*/
const factor = /* is debug: */ false ? 100000 : 2 ** 26;
export function toLength(lineCount: number, columnCount: number): Length {
// llllllllllllllllllllllllllcccccccccccccccccccccccccc (52 bits)
// line count (26 bits) column count (26 bits)
// If there is no overflow (all values/sums below 2^26 = 67108864),
// we have `toLength(lns1, cols1) + toLength(lns2, cols2) = toLength(lns1 + lns2, cols1 + cols2)`.
return (lineCount * factor + columnCount) as any as Length;
}
export function lengthToObj(length: Length): LengthObj {
const l = length as any as number;
const lineCount = Math.floor(l / factor);
const columnCount = l - lineCount * factor;
return new LengthObj(lineCount, columnCount);
}
export function lengthGetLineCount(length: Length): number {
return Math.floor(length as any as number / factor);
}
/**
* Returns the amount of columns of the given length, assuming that it does not span any line.
*/
export function lengthGetColumnCountIfZeroLineCount(length: Length): number {
return length as any as number;
}
// [10 lines, 5 cols] + [ 0 lines, 3 cols] = [10 lines, 8 cols]
// [10 lines, 5 cols] + [20 lines, 3 cols] = [30 lines, 3 cols]
export function lengthAdd(length1: Length, length2: Length): Length;
export function lengthAdd(l1: any, l2: any): Length {
return ((l2 < factor)
? (l1 + l2) // l2 is the amount of columns (zero line count). Keep the column count from l1.
: (l1 - (l1 % factor) + l2)); // l1 - (l1 % factor) equals toLength(l1.lineCount, 0)
}
/**
* Returns a non negative length `result` such that `lengthAdd(length1, result) = length2`, or zero if such length does not exist.
*/
export function lengthDiffNonNegative(length1: Length, length2: Length): Length {
const l1 = length1 as any as number;
const l2 = length2 as any as number;
const diff = l2 - l1;
if (diff <= 0) {
// line-count of length1 is higher than line-count of length2
// or they are equal and column-count of length1 is higher than column-count of length2
return lengthZero;
}
const lineCount1 = Math.floor(l1 / factor);
const lineCount2 = Math.floor(l2 / factor);
const colCount2 = l2 - lineCount2 * factor;
if (lineCount1 === lineCount2) {
const colCount1 = l1 - lineCount1 * factor;
return toLength(0, colCount2 - colCount1);
} else {
return toLength(lineCount2 - lineCount1, colCount2);
}
}
export function lengthLessThan(length1: Length, length2: Length): boolean {
// First, compare line counts, then column counts.
return (length1 as any as number) < (length2 as any as number);
}
export function lengthLessThanEqual(length1: Length, length2: Length): boolean {
return (length1 as any as number) <= (length2 as any as number);
}
export function lengthGreaterThanEqual(length1: Length, length2: Length): boolean {
return (length1 as any as number) >= (length2 as any as number);
}
export function lengthToPosition(length: Length): Position {
const l = length as any as number;
const lineCount = Math.floor(l / factor);
const colCount = l - lineCount * factor;
return new Position(lineCount + 1, colCount + 1);
}
export function positionToLength(position: Position): Length {
return toLength(position.lineNumber - 1, position.column - 1);
}
export function lengthsToRange(lengthStart: Length, lengthEnd: Length): Range {
const l = lengthStart as any as number;
const lineCount = Math.floor(l / factor);
const colCount = l - lineCount * factor;
const l2 = lengthEnd as any as number;
const lineCount2 = Math.floor(l2 / factor);
const colCount2 = l2 - lineCount2 * factor;
return new Range(lineCount + 1, colCount + 1, lineCount2 + 1, colCount2 + 1);
}
export function lengthCompare(length1: Length, length2: Length): number {
const l1 = length1 as any as number;
const l2 = length2 as any as number;
return l1 - l2;
}
export function lengthOfString(str: string): Length {
const lines = splitLines(str);
return toLength(lines.length - 1, lines[lines.length - 1].length);
}
export function lengthOfStringObj(str: string): LengthObj {
const lines = splitLines(str);
return new LengthObj(lines.length - 1, lines[lines.length - 1].length);
}
/**
* Computes a numeric hash of the given length.
*/
export function lengthHash(length: Length): number {
return length as any;
}

View File

@@ -0,0 +1,123 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AstNode } from './ast';
import { lengthAdd, lengthZero, Length, lengthLessThan } from './length';
/**
* Allows to efficiently find a longest child at a given offset in a fixed node.
* The requested offsets must increase monotonously.
*/
export class NodeReader {
private readonly nextNodes: AstNode[];
private readonly offsets: Length[];
private readonly idxs: number[];
private lastOffset: Length = lengthZero;
constructor(node: AstNode) {
this.nextNodes = [node];
this.offsets = [lengthZero];
this.idxs = [];
}
/**
* Returns the longest node at `offset` that satisfies the predicate.
* @param offset must be greater than or equal to the last offset this method has been called with!
*/
readLongestNodeAt(offset: Length, predicate: (node: AstNode) => boolean): AstNode | undefined {
if (lengthLessThan(offset, this.lastOffset)) {
throw new Error('Invalid offset');
}
this.lastOffset = offset;
// Find the longest node of all those that are closest to the current offset.
while (true) {
const curNode = lastOrUndefined(this.nextNodes);
if (!curNode) {
return undefined;
}
const curNodeOffset = lastOrUndefined(this.offsets)!;
if (lengthLessThan(offset, curNodeOffset)) {
// The next best node is not here yet.
// The reader must advance before a cached node is hit.
return undefined;
}
if (lengthLessThan(curNodeOffset, offset)) {
// The reader is ahead of the current node.
if (lengthAdd(curNodeOffset, curNode.length) <= offset) {
// The reader is after the end of the current node.
this.nextNodeAfterCurrent();
} else {
// The reader is somewhere in the current node.
if (curNode.children.length > 0) {
// Go to the first child and repeat.
this.nextNodes.push(curNode.children[0]);
this.offsets.push(curNodeOffset);
this.idxs.push(0);
} else {
// We don't have children
this.nextNodeAfterCurrent();
}
}
} else {
// readerOffsetBeforeChange === curNodeOffset
if (predicate(curNode)) {
this.nextNodeAfterCurrent();
return curNode;
} else {
// look for shorter node
if (curNode.children.length === 0) {
// There is no shorter node.
this.nextNodeAfterCurrent();
return undefined;
} else {
// Descend into first child & repeat.
this.nextNodes.push(curNode.children[0]);
this.offsets.push(curNodeOffset);
this.idxs.push(0);
}
}
}
}
}
// Navigates to the longest node that continues after the current node.
private nextNodeAfterCurrent(): void {
while (true) {
const currentOffset = lastOrUndefined(this.offsets);
const currentNode = lastOrUndefined(this.nextNodes);
this.nextNodes.pop();
this.offsets.pop();
if (this.idxs.length === 0) {
// We just popped the root node, there is no next node.
break;
}
// Parent is not undefined, because idxs is not empty
const parent = lastOrUndefined(this.nextNodes)!;
this.idxs[this.idxs.length - 1]++;
const parentIdx = this.idxs[this.idxs.length - 1];
if (parentIdx < parent.children.length) {
this.nextNodes.push(parent.children[parentIdx]);
this.offsets.push(lengthAdd(currentOffset!, currentNode!.length));
break;
} else {
this.idxs.pop();
}
// We fully consumed the parent.
// Current node is now parent, so call nextNodeAfterCurrent again
}
}
}
function lastOrUndefined<T>(arr: readonly T[]): T | undefined {
return arr.length > 0 ? arr[arr.length - 1] : undefined;
}

View File

@@ -0,0 +1,153 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AstNode, AstNodeKind, BracketAstNode, InvalidBracketAstNode, ListAstNode, PairAstNode, TextAstNode } from './ast';
import { BeforeEditPositionMapper, TextEditInfo } from './beforeEditPositionMapper';
import { DenseKeyProvider, SmallImmutableSet } from './smallImmutableSet';
import { lengthGetLineCount, lengthIsZero, lengthLessThanEqual } from './length';
import { concat23Trees } from './concat23Trees';
import { NodeReader } from './nodeReader';
import { Tokenizer, TokenKind } from './tokenizer';
export function parseDocument(tokenizer: Tokenizer, edits: TextEditInfo[], oldNode: AstNode | undefined, denseKeyProvider: DenseKeyProvider<number>): AstNode {
const parser = new Parser(tokenizer, edits, oldNode, denseKeyProvider);
return parser.parseDocument();
}
class Parser {
private readonly oldNodeReader?: NodeReader;
private readonly positionMapper: BeforeEditPositionMapper;
private _itemsConstructed: number = 0;
private _itemsFromCache: number = 0;
/**
* Reports how many nodes were constructed in the last parse operation.
*/
get nodesConstructed() {
return this._itemsConstructed;
}
/**
* Reports how many nodes were reused in the last parse operation.
*/
get nodesReused() {
return this._itemsFromCache;
}
constructor(
private readonly tokenizer: Tokenizer,
edits: TextEditInfo[],
oldNode: AstNode | undefined,
private readonly denseKeyProvider: DenseKeyProvider<number>,
) {
this.oldNodeReader = oldNode ? new NodeReader(oldNode) : undefined;
this.positionMapper = new BeforeEditPositionMapper(edits, tokenizer.length);
}
parseDocument(): AstNode {
this._itemsConstructed = 0;
this._itemsFromCache = 0;
let result = this.parseList(SmallImmutableSet.getEmpty());
if (!result) {
result = ListAstNode.create([]);
}
return result;
}
private parseList(
expectedClosingCategories: SmallImmutableSet<number>,
): AstNode | null {
const items = new Array<AstNode>();
while (true) {
const token = this.tokenizer.peek();
if (
!token ||
(token.kind === TokenKind.ClosingBracket &&
expectedClosingCategories.has(token.category, this.denseKeyProvider))
) {
break;
}
const child = this.parseChild(expectedClosingCategories);
if (child.kind === AstNodeKind.List && child.children.length === 0) {
continue;
}
items.push(child);
}
const result = concat23Trees(items);
return result;
}
private parseChild(
expectingClosingCategories: SmallImmutableSet<number>,
): AstNode {
if (this.oldNodeReader) {
const maxCacheableLength = this.positionMapper.getDistanceToNextChange(this.tokenizer.offset);
if (!lengthIsZero(maxCacheableLength)) {
const cachedNode = this.oldNodeReader.readLongestNodeAt(this.positionMapper.getOffsetBeforeChange(this.tokenizer.offset), curNode => {
if (!lengthLessThanEqual(curNode.length, maxCacheableLength)) {
return false;
}
const endLineDidChange = lengthGetLineCount(curNode.length) === lengthGetLineCount(maxCacheableLength);
const canBeReused = curNode.canBeReused(expectingClosingCategories, endLineDidChange);
return canBeReused;
});
if (cachedNode) {
this._itemsFromCache++;
this.tokenizer.skip(cachedNode.length);
return cachedNode;
}
}
}
this._itemsConstructed++;
const token = this.tokenizer.read()!;
switch (token.kind) {
case TokenKind.ClosingBracket:
return new InvalidBracketAstNode(token.category, token.length, this.denseKeyProvider);
case TokenKind.Text:
return token.astNode as TextAstNode;
case TokenKind.OpeningBracket:
const set = expectingClosingCategories.add(token.category, this.denseKeyProvider);
const child = this.parseList(set);
const nextToken = this.tokenizer.peek();
if (
nextToken &&
nextToken.kind === TokenKind.ClosingBracket &&
nextToken.category === token.category
) {
this.tokenizer.read();
return PairAstNode.create(
token.category,
token.astNode as BracketAstNode,
child,
nextToken.astNode as BracketAstNode
);
} else {
return PairAstNode.create(
token.category,
token.astNode as BracketAstNode,
child,
null
);
}
default:
throw new Error('unexpected');
}
}
}

View File

@@ -0,0 +1,146 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const emptyArr = new Array<number>();
/**
* Represents an immutable set that works best for a small number of elements (less than 32).
* It uses bits to encode element membership efficiently.
*/
export class SmallImmutableSet<T> {
private static cache = new Array<SmallImmutableSet<any>>(129);
private static create<T>(items: number, additionalItems: readonly number[]): SmallImmutableSet<T> {
if (items <= 128 && additionalItems.length === 0) {
// We create a cache of 128=2^7 elements to cover all sets with up to 7 (dense) elements.
let cached = SmallImmutableSet.cache[items];
if (!cached) {
cached = new SmallImmutableSet(items, additionalItems);
SmallImmutableSet.cache[items] = cached;
}
return cached;
}
return new SmallImmutableSet(items, additionalItems);
}
private static empty = SmallImmutableSet.create<any>(0, emptyArr);
public static getEmpty<T>(): SmallImmutableSet<T> {
return this.empty;
}
private constructor(
private readonly items: number,
private readonly additionalItems: readonly number[]
) {
}
public add(value: T, keyProvider: DenseKeyProvider<T>): SmallImmutableSet<T> {
const key = keyProvider.getKey(value);
let idx = key >> 5; // divided by 32
if (idx === 0) {
// fast path
const newItem = (1 << key) | this.items;
if (newItem === this.items) {
return this;
}
return SmallImmutableSet.create(newItem, this.additionalItems);
}
idx--;
const newItems = this.additionalItems.slice(0);
while (newItems.length < idx) {
newItems.push(0);
}
newItems[idx] |= 1 << (key & 31);
return SmallImmutableSet.create(this.items, newItems);
}
public has(value: T, keyProvider: DenseKeyProvider<T>): boolean {
const key = keyProvider.getKey(value);
let idx = key >> 5; // divided by 32
if (idx === 0) {
// fast path
return (this.items & (1 << key)) !== 0;
}
idx--;
return ((this.additionalItems[idx] || 0) & (1 << (key & 31))) !== 0;
}
public merge(other: SmallImmutableSet<T>): SmallImmutableSet<T> {
const merged = this.items | other.items;
if (this.additionalItems === emptyArr && other.additionalItems === emptyArr) {
// fast path
if (merged === this.items) {
return this;
}
if (merged === other.items) {
return other;
}
return SmallImmutableSet.create(merged, emptyArr);
}
// This can be optimized, but it's not a common case
const newItems = new Array<number>();
for (let i = 0; i < Math.max(this.additionalItems.length, other.additionalItems.length); i++) {
const item1 = this.additionalItems[i] || 0;
const item2 = other.additionalItems[i] || 0;
newItems.push(item1 | item2);
}
return SmallImmutableSet.create(merged, newItems);
}
public intersects(other: SmallImmutableSet<T>): boolean {
if ((this.items & other.items) !== 0) {
return true;
}
for (let i = 0; i < Math.min(this.additionalItems.length, other.additionalItems.length); i++) {
if ((this.additionalItems[i] & other.additionalItems[i]) !== 0) {
return true;
}
}
return false;
}
public equals(other: SmallImmutableSet<T>): boolean {
if (this.items !== other.items) {
return false;
}
if (this.additionalItems.length !== other.additionalItems.length) {
return false;
}
for (let i = 0; i < this.additionalItems.length; i++) {
if (this.additionalItems[i] !== other.additionalItems[i]) {
return false;
}
}
return true;
}
}
/**
* Assigns values a unique incrementing key.
*/
export class DenseKeyProvider<T> {
private readonly items = new Map<T, number>();
getKey(value: T): number {
let existing = this.items.get(value);
if (existing === undefined) {
existing = this.items.size;
this.items.set(value, existing);
}
return existing;
}
}

View File

@@ -0,0 +1,349 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { NotSupportedError } from 'vs/base/common/errors';
import { LineTokens } from 'vs/editor/common/core/lineTokens';
import { ITextModel } from 'vs/editor/common/model';
import { LanguageId, StandardTokenType, TokenMetadata } from 'vs/editor/common/modes';
import { BracketAstNode, TextAstNode } from './ast';
import { BracketTokens, LanguageAgnosticBracketTokens } from './brackets';
import { lengthGetColumnCountIfZeroLineCount, Length, lengthAdd, lengthDiff, lengthToObj, lengthZero, toLength } from './length';
export interface Tokenizer {
readonly offset: Length;
readonly length: Length;
read(): Token | null;
peek(): Token | null;
skip(length: Length): void;
getText(): string;
}
export const enum TokenKind {
Text = 0,
OpeningBracket = 1,
ClosingBracket = 2,
}
export class Token {
constructor(
readonly length: Length,
readonly kind: TokenKind,
readonly category: number,
readonly languageId: LanguageId,
readonly astNode: BracketAstNode | TextAstNode | undefined,
) { }
}
export class TextBufferTokenizer implements Tokenizer {
private readonly textBufferLineCount: number;
private readonly textBufferLastLineLength: number;
private readonly reader = new NonPeekableTextBufferTokenizer(this.textModel, this.bracketTokens);
constructor(
private readonly textModel: ITextModel,
private readonly bracketTokens: LanguageAgnosticBracketTokens
) {
this.textBufferLineCount = textModel.getLineCount();
this.textBufferLastLineLength = textModel.getLineLength(this.textBufferLineCount);
}
private _offset: Length = lengthZero;
get offset() {
return this._offset;
}
get length() {
return toLength(this.textBufferLineCount, this.textBufferLastLineLength);
}
getText() {
return this.textModel.getValue();
}
skip(length: Length): void {
this.didPeek = false;
this._offset = lengthAdd(this._offset, length);
const obj = lengthToObj(this._offset);
this.reader.setPosition(obj.lineCount, obj.columnCount);
}
private didPeek = false;
private peeked: Token | null = null;
read(): Token | null {
let token: Token | null;
if (this.peeked) {
this.didPeek = false;
token = this.peeked;
} else {
token = this.reader.read();
}
if (token) {
this._offset = lengthAdd(this._offset, token.length);
}
return token;
}
peek(): Token | null {
if (!this.didPeek) {
this.peeked = this.reader.read();
this.didPeek = true;
}
return this.peeked;
}
}
/**
* Does not support peek.
*/
class NonPeekableTextBufferTokenizer {
private readonly textBufferLineCount: number;
private readonly textBufferLastLineLength: number;
constructor(private readonly textModel: ITextModel, private readonly bracketTokens: LanguageAgnosticBracketTokens) {
this.textBufferLineCount = textModel.getLineCount();
this.textBufferLastLineLength = textModel.getLineLength(this.textBufferLineCount);
}
private lineIdx = 0;
private line: string | null = null;
private lineCharOffset = 0;
private lineTokens: LineTokens | null = null;
private lineTokenOffset = 0;
public setPosition(lineIdx: number, column: number): void {
// We must not jump into a token!
if (lineIdx === this.lineIdx) {
this.lineCharOffset = column;
this.lineTokenOffset = this.lineCharOffset === 0 ? 0 : this.lineTokens!.findTokenIndexAtOffset(this.lineCharOffset);
} else {
this.lineIdx = lineIdx;
this.lineCharOffset = column;
this.line = null;
}
this.peekedToken = null;
}
/** Must be a zero line token. The end of the document cannot be peeked. */
private peekedToken: Token | null = null;
public read(): Token | null {
if (this.peekedToken) {
const token = this.peekedToken;
this.peekedToken = null;
this.lineCharOffset += lengthGetColumnCountIfZeroLineCount(token.length);
return token;
}
if (this.lineIdx > this.textBufferLineCount - 1 || (this.lineIdx === this.textBufferLineCount - 1 && this.lineCharOffset >= this.textBufferLastLineLength)) {
// We are after the end
return null;
}
if (this.line === null) {
this.lineTokens = this.textModel.getLineTokens(this.lineIdx + 1);
this.line = this.lineTokens.getLineContent();
this.lineTokenOffset = this.lineCharOffset === 0 ? 0 : this.lineTokens!.findTokenIndexAtOffset(this.lineCharOffset);
}
const startLineIdx = this.lineIdx;
const startLineCharOffset = this.lineCharOffset;
// limits the length of text tokens.
// If text tokens get too long, incremental updates will be slow
let lengthHeuristic = 0;
while (lengthHeuristic < 1000) {
const lineTokens = this.lineTokens!;
const tokenCount = lineTokens.getCount();
let peekedBracketToken: Token | null = null;
if (this.lineTokenOffset < tokenCount) {
let tokenMetadata = lineTokens.getMetadata(this.lineTokenOffset);
while (this.lineTokenOffset + 1 < tokenCount && tokenMetadata === lineTokens.getMetadata(this.lineTokenOffset + 1)) {
// Skip tokens that are identical.
// Sometimes, (bracket) identifiers are split up into multiple tokens.
this.lineTokenOffset++;
}
const isOther = TokenMetadata.getTokenType(tokenMetadata) === StandardTokenType.Other;
const endOffset = lineTokens.getEndOffset(this.lineTokenOffset);
// Is there a bracket token next? Only consume text.
if (isOther && endOffset !== this.lineCharOffset) {
const languageId = lineTokens.getLanguageId(this.lineTokenOffset);
const text = this.line.substring(this.lineCharOffset, endOffset);
const brackets = this.bracketTokens.getSingleLanguageBracketTokens(languageId);
const regexp = brackets.regExpGlobal;
if (regexp) {
regexp.lastIndex = 0;
const match = regexp.exec(text);
if (match) {
peekedBracketToken = brackets.getToken(match[0])!;
if (peekedBracketToken) {
// Consume leading text of the token
this.lineCharOffset += match.index;
}
}
}
}
lengthHeuristic += endOffset - this.lineCharOffset;
if (peekedBracketToken) {
// Don't skip the entire token, as a single token could contain multiple brackets.
if (startLineIdx !== this.lineIdx || startLineCharOffset !== this.lineCharOffset) {
// There is text before the bracket
this.peekedToken = peekedBracketToken;
break;
} else {
// Consume the peeked token
this.lineCharOffset += lengthGetColumnCountIfZeroLineCount(peekedBracketToken.length);
return peekedBracketToken;
}
} else {
// Skip the entire token, as the token contains no brackets at all.
this.lineTokenOffset++;
this.lineCharOffset = endOffset;
}
} else {
if (this.lineIdx === this.textBufferLineCount - 1) {
break;
}
this.lineIdx++;
this.lineTokens = this.textModel.getLineTokens(this.lineIdx + 1);
this.lineTokenOffset = 0;
this.line = this.lineTokens.getLineContent();
this.lineCharOffset = 0;
lengthHeuristic++;
}
}
const length = lengthDiff(startLineIdx, startLineCharOffset, this.lineIdx, this.lineCharOffset);
return new Token(length, TokenKind.Text, -1, -1, new TextAstNode(length));
}
}
export class FastTokenizer implements Tokenizer {
private _offset: Length = lengthZero;
private readonly tokens: readonly Token[];
private idx = 0;
constructor(private readonly text: string, brackets: BracketTokens) {
const regExpStr = brackets.getRegExpStr();
const regexp = regExpStr ? new RegExp(brackets.getRegExpStr() + '|\n', 'g') : null;
const tokens: Token[] = [];
let match: RegExpExecArray | null;
let curLineCount = 0;
let lastLineBreakOffset = 0;
let lastTokenEndOffset = 0;
let lastTokenEndLine = 0;
const smallTextTokens0Line = new Array<Token>();
for (let i = 0; i < 60; i++) {
smallTextTokens0Line.push(
new Token(
toLength(0, i), TokenKind.Text, -1, -1,
new TextAstNode(toLength(0, i))
)
);
}
const smallTextTokens1Line = new Array<Token>();
for (let i = 0; i < 60; i++) {
smallTextTokens1Line.push(
new Token(
toLength(1, i), TokenKind.Text, -1, -1,
new TextAstNode(toLength(1, i))
)
);
}
if (regexp) {
regexp.lastIndex = 0;
while ((match = regexp.exec(text)) !== null) {
const curOffset = match.index;
const value = match[0];
if (value === '\n') {
curLineCount++;
lastLineBreakOffset = curOffset + 1;
} else {
if (lastTokenEndOffset !== curOffset) {
let token: Token;
if (lastTokenEndLine === curLineCount) {
const colCount = curOffset - lastTokenEndOffset;
if (colCount < smallTextTokens0Line.length) {
token = smallTextTokens0Line[colCount];
} else {
const length = toLength(0, colCount);
token = new Token(length, TokenKind.Text, -1, -1, new TextAstNode(length));
}
} else {
const lineCount = curLineCount - lastTokenEndLine;
const colCount = curOffset - lastLineBreakOffset;
if (lineCount === 1 && colCount < smallTextTokens1Line.length) {
token = smallTextTokens1Line[colCount];
} else {
const length = toLength(lineCount, colCount);
token = new Token(length, TokenKind.Text, -1, -1, new TextAstNode(length));
}
}
tokens.push(token);
}
// value is matched by regexp, so the token must exist
tokens.push(brackets.getToken(value)!);
lastTokenEndOffset = curOffset + value.length;
lastTokenEndLine = curLineCount;
}
}
}
const offset = text.length;
if (lastTokenEndOffset !== offset) {
const length = (lastTokenEndLine === curLineCount)
? toLength(0, offset - lastTokenEndOffset)
: toLength(curLineCount - lastTokenEndLine, offset - lastLineBreakOffset);
tokens.push(new Token(length, TokenKind.Text, -1, -1, new TextAstNode(length)));
}
this.length = toLength(curLineCount, offset - lastLineBreakOffset);
this.tokens = tokens;
}
get offset(): Length {
return this._offset;
}
readonly length: Length;
read(): Token | null {
return this.tokens[this.idx++] || null;
}
peek(): Token | null {
return this.tokens[this.idx] || null;
}
skip(length: Length): void {
throw new NotSupportedError();
}
getText(): string {
return this.text;
}
}

View File

@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable } from 'vs/base/common/lifecycle';
import { Range } from 'vs/editor/common/core/range';
import { IModelDecoration } from 'vs/editor/common/model';
export interface DecorationProvider {
/**
* Gets all the decorations in a range as an array. Only `startLineNumber` and `endLineNumber` from `range` are used for filtering.
* So for now it returns all the decorations on the same line as `range`.
* @param range The range to search in
* @param ownerId If set, it will ignore decorations belonging to other owners.
* @param filterOutValidation If set, it will ignore decorations specific to validation (i.e. warnings, errors).
* @return An array with the decorations
*/
getDecorationsInRange(range: Range, ownerId?: number, filterOutValidation?: boolean): IModelDecoration[];
/**
* Gets all the decorations as an array.
* @param ownerId If set, it will ignore decorations belonging to other owners.
* @param filterOutValidation If set, it will ignore decorations specific to validation (i.e. warnings, errors).
*/
getAllDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[];
onDidChangeDecorations(listener: () => void): IDisposable;
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Range } from 'vs/editor/common/core/range';
import { IModelDecoration, TrackedRangeStickiness, TrackedRangeStickiness as ActualTrackedRangeStickiness } from 'vs/editor/common/model';
import { TrackedRangeStickiness, TrackedRangeStickiness as ActualTrackedRangeStickiness } from 'vs/editor/common/model';
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
//
@@ -39,17 +39,13 @@ const enum Constants {
IsForValidationMaskInverse = 0b11111011,
IsForValidationOffset = 2,
IsInOverviewRulerMask = 0b00001000,
IsInOverviewRulerMaskInverse = 0b11110111,
IsInOverviewRulerOffset = 3,
StickinessMask = 0b00011000,
StickinessMaskInverse = 0b11100111,
StickinessOffset = 3,
StickinessMask = 0b00110000,
StickinessMaskInverse = 0b11001111,
StickinessOffset = 4,
CollapseOnReplaceEditMask = 0b01000000,
CollapseOnReplaceEditMaskInverse = 0b10111111,
CollapseOnReplaceEditOffset = 6,
CollapseOnReplaceEditMask = 0b00100000,
CollapseOnReplaceEditMaskInverse = 0b11011111,
CollapseOnReplaceEditOffset = 5,
/**
* Due to how deletion works (in order to avoid always walking the right subtree of the deleted node),
@@ -98,14 +94,6 @@ function setNodeIsForValidation(node: IntervalNode, value: boolean): void {
(node.metadata & Constants.IsForValidationMaskInverse) | ((value ? 1 : 0) << Constants.IsForValidationOffset)
);
}
export function getNodeIsInOverviewRuler(node: IntervalNode): boolean {
return ((node.metadata & Constants.IsInOverviewRulerMask) >>> Constants.IsInOverviewRulerOffset) === 1;
}
function setNodeIsInOverviewRuler(node: IntervalNode, value: boolean): void {
node.metadata = (
(node.metadata & Constants.IsInOverviewRulerMaskInverse) | ((value ? 1 : 0) << Constants.IsInOverviewRulerOffset)
);
}
function getNodeStickiness(node: IntervalNode): TrackedRangeStickiness {
return ((node.metadata & Constants.StickinessMask) >>> Constants.StickinessOffset);
}
@@ -126,7 +114,7 @@ export function setNodeStickiness(node: IntervalNode, stickiness: ActualTrackedR
_setNodeStickiness(node, <number>stickiness);
}
export class IntervalNode implements IModelDecoration {
export class IntervalNode {
/**
* contains binary encoded information for color, visited, isForValidation and stickiness.
@@ -149,7 +137,7 @@ export class IntervalNode implements IModelDecoration {
public cachedVersionId: number;
public cachedAbsoluteStart: number;
public cachedAbsoluteEnd: number;
public range: Range;
public range: Range | null;
constructor(id: string, start: number, end: number) {
this.metadata = 0;
@@ -170,13 +158,12 @@ export class IntervalNode implements IModelDecoration {
this.options = null!;
setNodeIsForValidation(this, false);
_setNodeStickiness(this, TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges);
setNodeIsInOverviewRuler(this, false);
setCollapseOnReplaceEdit(this, false);
this.cachedVersionId = 0;
this.cachedAbsoluteStart = start;
this.cachedAbsoluteEnd = end;
this.range = null!;
this.range = null;
setNodeIsVisited(this, false);
}
@@ -200,13 +187,12 @@ export class IntervalNode implements IModelDecoration {
|| className === ClassName.EditorInfoDecoration
));
_setNodeStickiness(this, <number>this.options.stickiness);
setNodeIsInOverviewRuler(this, (this.options.overviewRuler && this.options.overviewRuler.color) ? true : false);
setCollapseOnReplaceEdit(this, this.options.collapseOnReplaceEdit);
}
public setCachedOffsets(absoluteStart: number, absoluteEnd: number, cachedVersionId: number): void {
if (this.cachedVersionId !== cachedVersionId) {
this.range = null!;
this.range = null;
}
this.cachedVersionId = cachedVersionId;
this.cachedAbsoluteStart = absoluteStart;

View File

@@ -23,6 +23,14 @@ export interface IModelChangedEvent {
* The new version id the model has transitioned to.
*/
readonly versionId: number;
/**
* Flag that indicates that this event was generated while undoing.
*/
readonly isUndoing: boolean;
/**
* Flag that indicates that this event was generated while redoing.
*/
readonly isRedoing: boolean;
}
export interface IMirrorTextModel {

View File

@@ -18,9 +18,9 @@ import { Selection } from 'vs/editor/common/core/selection';
import * as model from 'vs/editor/common/model';
import { EditStack } from 'vs/editor/common/model/editStack';
import { guessIndentation } from 'vs/editor/common/model/indentationGuesser';
import { IntervalNode, IntervalTree, getNodeIsInOverviewRuler, recomputeMaxEnd } from 'vs/editor/common/model/intervalTree';
import { IntervalNode, IntervalTree, recomputeMaxEnd } from 'vs/editor/common/model/intervalTree';
import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder';
import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, InternalModelContentChangeEvent, ModelRawChange, ModelRawContentChangedEvent, ModelRawEOLChanged, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted } from 'vs/editor/common/model/textModelEvents';
import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, InternalModelContentChangeEvent, LineInjectedText, ModelInjectedTextChangedEvent, ModelRawChange, ModelRawContentChangedEvent, ModelRawEOLChanged, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted } from 'vs/editor/common/model/textModelEvents';
import { SearchData, SearchParams, TextModelSearch } from 'vs/editor/common/model/textModelSearch';
import { TextModelTokenization } from 'vs/editor/common/model/textModelTokens';
import { getWordAtText } from 'vs/editor/common/model/wordHelper';
@@ -39,6 +39,9 @@ import { TextChange } from 'vs/editor/common/model/textChange';
import { Constants } from 'vs/base/common/uint';
import { PieceTreeTextBuffer } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer';
import { listenStream } from 'vs/base/common/stream';
import { ArrayQueue } from 'vs/base/common/arrays';
import { BracketPairColorizer } from 'vs/editor/common/model/bracketPairColorizer/bracketPairColorizer';
import { DecorationProvider } from 'vs/editor/common/model/decorationProvider';
function createTextBufferBuilder() {
return new PieceTreeTextBufferBuilder();
@@ -161,6 +164,12 @@ const enum StringOffsetValidationType {
SurrogatePairs = 1,
}
export const enum BackgroundTokenizationState {
Uninitialized = 0,
InProgress = 1,
Completed = 2,
}
type ContinueBracketSearchPredicate = null | (() => boolean);
class BracketSearchCanceled {
@@ -176,7 +185,7 @@ function stripBracketSearchCanceled<T>(result: T | null | BracketSearchCanceled)
return result;
}
export class TextModel extends Disposable implements model.ITextModel {
export class TextModel extends Disposable implements model.ITextModel, IDecorationsTreesHost {
private static readonly MODEL_SYNC_LIMIT = 50 * 1024 * 1024; // 50 MB
private static readonly LARGE_FILE_SIZE_THRESHOLD = 20 * 1024 * 1024; // 20 MB;
@@ -191,6 +200,7 @@ export class TextModel extends Disposable implements model.ITextModel {
defaultEOL: model.DefaultEndOfLine.LF,
trimAutoWhitespace: EDITOR_MODEL_DEFAULTS.trimAutoWhitespace,
largeFileOptimizations: EDITOR_MODEL_DEFAULTS.largeFileOptimizations,
bracketPairColorizationOptions: EDITOR_MODEL_DEFAULTS.bracketPairColorizationOptions,
};
public static resolveOptions(textBuffer: model.ITextBuffer, options: model.ITextModelCreationOptions): model.TextModelResolvedOptions {
@@ -201,7 +211,8 @@ export class TextModel extends Disposable implements model.ITextModel {
indentSize: guessedIndentation.tabSize, // TODO@Alex: guess indentSize independent of tabSize
insertSpaces: guessedIndentation.insertSpaces,
trimAutoWhitespace: options.trimAutoWhitespace,
defaultEOL: options.defaultEOL
defaultEOL: options.defaultEOL,
bracketPairColorizationOptions: options.bracketPairColorizationOptions,
});
}
@@ -210,7 +221,8 @@ export class TextModel extends Disposable implements model.ITextModel {
indentSize: options.indentSize,
insertSpaces: options.insertSpaces,
trimAutoWhitespace: options.trimAutoWhitespace,
defaultEOL: options.defaultEOL
defaultEOL: options.defaultEOL,
bracketPairColorizationOptions: options.bracketPairColorizationOptions,
});
}
@@ -219,7 +231,7 @@ export class TextModel extends Disposable implements model.ITextModel {
private readonly _onWillDispose: Emitter<void> = this._register(new Emitter<void>());
public readonly onWillDispose: Event<void> = this._onWillDispose.event;
private readonly _onDidChangeDecorations: DidChangeDecorationsEmitter = this._register(new DidChangeDecorationsEmitter());
private readonly _onDidChangeDecorations: DidChangeDecorationsEmitter = this._register(new DidChangeDecorationsEmitter(affectedInjectedTextLines => this.handleBeforeFireDecorationsChangedEvent(affectedInjectedTextLines)));
public readonly onDidChangeDecorations: Event<IModelDecorationsChangedEvent> = this._onDidChangeDecorations.event;
private readonly _onDidChangeLanguage: Emitter<IModelLanguageChangedEvent> = this._register(new Emitter<IModelLanguageChangedEvent>());
@@ -237,10 +249,10 @@ export class TextModel extends Disposable implements model.ITextModel {
private readonly _onDidChangeAttached: Emitter<void> = this._register(new Emitter<void>());
public readonly onDidChangeAttached: Event<void> = this._onDidChangeAttached.event;
private readonly _onDidChangeContentOrInjectedText: Emitter<ModelRawContentChangedEvent | ModelInjectedTextChangedEvent> = this._register(new Emitter<ModelRawContentChangedEvent | ModelInjectedTextChangedEvent>());
public readonly onDidChangeContentOrInjectedText: Event<ModelRawContentChangedEvent | ModelInjectedTextChangedEvent> = this._onDidChangeContentOrInjectedText.event;
private readonly _eventEmitter: DidChangeContentEmitter = this._register(new DidChangeContentEmitter());
public onDidChangeRawContentFast(listener: (e: ModelRawContentChangedEvent) => void): IDisposable {
return this._eventEmitter.fastEvent((e: InternalModelContentChangeEvent) => listener(e.rawContentChangedEvent));
}
public onDidChangeRawContent(listener: (e: ModelRawContentChangedEvent) => void): IDisposable {
return this._eventEmitter.slowEvent((e: InternalModelContentChangeEvent) => listener(e.rawContentChangedEvent));
}
@@ -288,6 +300,7 @@ export class TextModel extends Disposable implements model.ITextModel {
private _lastDecorationId: number;
private _decorations: { [decorationId: string]: IntervalNode; };
private _decorationsTree: DecorationsTrees;
private readonly _decorationProvider: DecorationProvider;
//#endregion
//#region Tokenization
@@ -298,6 +311,22 @@ export class TextModel extends Disposable implements model.ITextModel {
private readonly _tokenization: TextModelTokenization;
//#endregion
private readonly _bracketPairColorizer;
private _backgroundTokenizationState = BackgroundTokenizationState.Uninitialized;
public get backgroundTokenizationState(): BackgroundTokenizationState {
return this._backgroundTokenizationState;
}
private setBackgroundTokenizationState(newState: BackgroundTokenizationState) {
if (this._backgroundTokenizationState !== newState) {
this._backgroundTokenizationState = newState;
this._onBackgroundTokenizationStateChanged.fire();
}
}
private readonly _onBackgroundTokenizationStateChanged = this._register(new Emitter<void>());
public readonly onBackgroundTokenizationStateChanged: Event<void> = this._onBackgroundTokenizationStateChanged.event;
constructor(
source: string | model.ITextBufferFactory,
creationOptions: model.ITextModelCreationOptions,
@@ -307,6 +336,10 @@ export class TextModel extends Disposable implements model.ITextModel {
) {
super();
this._register(this._eventEmitter.fastEvent((e: InternalModelContentChangeEvent) => {
this._onDidChangeContentOrInjectedText.fire(e.rawContentChangedEvent);
}));
// Generate a new unique model id
MODEL_ID++;
this.id = '$model' + MODEL_ID;
@@ -370,6 +403,15 @@ export class TextModel extends Disposable implements model.ITextModel {
this._tokens = new TokensStore();
this._tokens2 = new TokensStore2();
this._tokenization = new TextModelTokenization(this);
this._bracketPairColorizer = this._register(new BracketPairColorizer(this));
this._decorationProvider = this._bracketPairColorizer;
this._register(this._decorationProvider.onDidChangeDecorations(() => {
this._onDidChangeDecorations.beginDeferredEmit();
this._onDidChangeDecorations.fire();
this._onDidChangeDecorations.endDeferredEmit();
}));
}
public override dispose(): void {
@@ -405,6 +447,7 @@ export class TextModel extends Disposable implements model.ITextModel {
}
private _emitContentChangedEvent(rawChange: ModelRawContentChangedEvent, change: IModelContentChangedEvent): void {
this._bracketPairColorizer.handleContentChanged(change);
if (this._isDisposing) {
// Do not confuse listeners by emitting any event after disposing
return;
@@ -509,9 +552,7 @@ export class TextModel extends Disposable implements model.ITextModel {
private _onBeforeEOLChange(): void {
// Ensure all decorations get their `range` set.
const versionId = this.getVersionId();
const allDecorations = this._decorationsTree.search(0, false, false, versionId);
this._ensureNodesHaveRanges(allDecorations);
this._decorationsTree.ensureAllNodesHaveRanges(this);
}
private _onAfterEOLChange(): void {
@@ -520,11 +561,12 @@ export class TextModel extends Disposable implements model.ITextModel {
const allDecorations = this._decorationsTree.collectNodesPostOrder();
for (let i = 0, len = allDecorations.length; i < len; i++) {
const node = allDecorations[i];
const range = node.range!; // the range is defined due to `_onBeforeEOLChange`
const delta = node.cachedAbsoluteStart - node.start;
const startOffset = this._buffer.getOffsetAt(node.range.startLineNumber, node.range.startColumn);
const endOffset = this._buffer.getOffsetAt(node.range.endLineNumber, node.range.endColumn);
const startOffset = this._buffer.getOffsetAt(range.startLineNumber, range.startColumn);
const endOffset = this._buffer.getOffsetAt(range.endLineNumber, range.endColumn);
node.cachedAbsoluteStart = startOffset;
node.cachedAbsoluteEnd = endOffset;
@@ -617,13 +659,15 @@ export class TextModel extends Disposable implements model.ITextModel {
let indentSize = (typeof _newOpts.indentSize !== 'undefined') ? _newOpts.indentSize : this._options.indentSize;
let insertSpaces = (typeof _newOpts.insertSpaces !== 'undefined') ? _newOpts.insertSpaces : this._options.insertSpaces;
let trimAutoWhitespace = (typeof _newOpts.trimAutoWhitespace !== 'undefined') ? _newOpts.trimAutoWhitespace : this._options.trimAutoWhitespace;
let bracketPairColorizationOptions = (typeof _newOpts.bracketColorizationOptions !== 'undefined') ? _newOpts.bracketColorizationOptions : this._options.bracketPairColorizationOptions;
let newOpts = new model.TextModelResolvedOptions({
tabSize: tabSize,
indentSize: indentSize,
insertSpaces: insertSpaces,
defaultEOL: this._options.defaultEOL,
trimAutoWhitespace: trimAutoWhitespace
trimAutoWhitespace: trimAutoWhitespace,
bracketPairColorizationOptions,
});
if (this._options.equals(newOpts)) {
@@ -1424,16 +1468,27 @@ export class TextModel extends Disposable implements model.ITextModel {
this._trimAutoWhitespaceLines = result.trimAutoWhitespaceLineNumbers;
if (contentChanges.length !== 0) {
let rawContentChanges: ModelRawChange[] = [];
let lineCount = oldLineCount;
// We do a first pass to update tokens and decorations
// because we want to read decorations in the second pass
// where we will emit content change events
// and we want to read the final decorations
for (let i = 0, len = contentChanges.length; i < len; i++) {
const change = contentChanges[i];
const [eolCount, firstLineLength, lastLineLength] = countEOL(change.text);
this._tokens.acceptEdit(change.range, eolCount, firstLineLength);
this._tokens2.acceptEdit(change.range, eolCount, firstLineLength, lastLineLength, change.text.length > 0 ? change.text.charCodeAt(0) : CharCode.Null);
this._onDidChangeDecorations.fire();
this._decorationsTree.acceptReplace(change.rangeOffset, change.rangeLength, change.text.length, change.forceMoveMarkers);
}
let rawContentChanges: ModelRawChange[] = [];
this._increaseVersionId();
let lineCount = oldLineCount;
for (let i = 0, len = contentChanges.length; i < len; i++) {
const change = contentChanges[i];
const [eolCount] = countEOL(change.text);
this._onDidChangeDecorations.fire();
const startLineNumber = change.range.startLineNumber;
const endLineNumber = change.range.endLineNumber;
@@ -1444,10 +1499,34 @@ export class TextModel extends Disposable implements model.ITextModel {
const changeLineCountDelta = (insertingLinesCnt - deletingLinesCnt);
const currentEditStartLineNumber = newLineCount - lineCount - changeLineCountDelta + startLineNumber;
const firstEditLineNumber = currentEditStartLineNumber;
const lastInsertedLineNumber = currentEditStartLineNumber + insertingLinesCnt;
const decorationsWithInjectedTextInEditedRange = this._decorationsTree.getInjectedTextInInterval(
this,
this.getOffsetAt(new Position(firstEditLineNumber, 1)),
this.getOffsetAt(new Position(lastInsertedLineNumber, this.getLineMaxColumn(lastInsertedLineNumber))),
0
);
const injectedTextInEditedRange = LineInjectedText.fromDecorations(decorationsWithInjectedTextInEditedRange);
const injectedTextInEditedRangeQueue = new ArrayQueue(injectedTextInEditedRange);
for (let j = editingLinesCnt; j >= 0; j--) {
const editLineNumber = startLineNumber + j;
const currentEditLineNumber = newLineCount - lineCount - changeLineCountDelta + editLineNumber;
rawContentChanges.push(new ModelRawLineChanged(editLineNumber, this.getLineContent(currentEditLineNumber)));
const currentEditLineNumber = currentEditStartLineNumber + j;
injectedTextInEditedRangeQueue.takeFromEndWhile(r => r.lineNumber > currentEditLineNumber);
const decorationsInCurrentLine = injectedTextInEditedRangeQueue.takeFromEndWhile(r => r.lineNumber === currentEditLineNumber);
rawContentChanges.push(
new ModelRawLineChanged(
editLineNumber,
this.getLineContent(currentEditLineNumber),
decorationsInCurrentLine
));
}
if (editingLinesCnt < deletingLinesCnt) {
@@ -1457,23 +1536,34 @@ export class TextModel extends Disposable implements model.ITextModel {
}
if (editingLinesCnt < insertingLinesCnt) {
const injectedTextInEditedRangeQueue = new ArrayQueue(injectedTextInEditedRange);
// Must insert some lines
const spliceLineNumber = startLineNumber + editingLinesCnt;
const cnt = insertingLinesCnt - editingLinesCnt;
const fromLineNumber = newLineCount - lineCount - cnt + spliceLineNumber + 1;
let injectedTexts: (LineInjectedText[] | null)[] = [];
let newLines: string[] = [];
for (let i = 0; i < cnt; i++) {
let lineNumber = fromLineNumber + i;
newLines[lineNumber - fromLineNumber] = this.getLineContent(lineNumber);
newLines[i] = this.getLineContent(lineNumber);
injectedTextInEditedRangeQueue.takeWhile(r => r.lineNumber < lineNumber);
injectedTexts[i] = injectedTextInEditedRangeQueue.takeWhile(r => r.lineNumber === lineNumber);
}
rawContentChanges.push(new ModelRawLinesInserted(spliceLineNumber + 1, startLineNumber + insertingLinesCnt, newLines));
rawContentChanges.push(
new ModelRawLinesInserted(
spliceLineNumber + 1,
startLineNumber + insertingLinesCnt,
newLines,
injectedTexts
)
);
}
lineCount += changeLineCountDelta;
}
this._increaseVersionId();
this._emitContentChangedEvent(
new ModelRawContentChangedEvent(
rawContentChanges,
@@ -1515,6 +1605,19 @@ export class TextModel extends Disposable implements model.ITextModel {
//#region Decorations
private handleBeforeFireDecorationsChangedEvent(affectedInjectedTextLines: Set<number> | null): void {
// This is called before the decoration changed event is fired.
if (affectedInjectedTextLines === null || affectedInjectedTextLines.size === 0) {
return;
}
const affectedLines = [...affectedInjectedTextLines];
const lineChangeEvents = affectedLines.map(lineNumber => new ModelRawLineChanged(lineNumber, this.getLineContent(lineNumber), this._getInjectedTextInLine(lineNumber)));
this._onDidChangeContentOrInjectedText.fire(new ModelInjectedTextChangedEvent(lineChangeEvents));
}
public changeDecorations<T>(callback: (changeAccessor: model.IModelDecorationsChangeAccessor) => T, ownerId: number = 0): T | null {
this._assertNotDisposed();
@@ -1643,21 +1746,13 @@ export class TextModel extends Disposable implements model.ITextModel {
if (!node) {
return null;
}
const versionId = this.getVersionId();
if (node.cachedVersionId !== versionId) {
this._decorationsTree.resolveNode(node, versionId);
}
if (node.range === null) {
node.range = this._getRangeAt(node.cachedAbsoluteStart, node.cachedAbsoluteEnd);
}
return node.range;
return this._decorationsTree.getNodeRange(this, node);
}
public getLineDecorations(lineNumber: number, ownerId: number = 0, filterOutValidation: boolean = false): model.IModelDecoration[] {
if (lineNumber < 1 || lineNumber > this.getLineCount()) {
return [];
}
return this.getLinesDecorations(lineNumber, lineNumber, ownerId, filterOutValidation);
}
@@ -1666,47 +1761,50 @@ export class TextModel extends Disposable implements model.ITextModel {
let startLineNumber = Math.min(lineCount, Math.max(1, _startLineNumber));
let endLineNumber = Math.min(lineCount, Math.max(1, _endLineNumber));
let endColumn = this.getLineMaxColumn(endLineNumber);
return this._getDecorationsInRange(new Range(startLineNumber, 1, endLineNumber, endColumn), ownerId, filterOutValidation);
const range = new Range(startLineNumber, 1, endLineNumber, endColumn);
const decorations = this._getDecorationsInRange(range, ownerId, filterOutValidation);
decorations.push(...this._decorationProvider.getDecorationsInRange(range, ownerId, filterOutValidation));
return decorations;
}
public getDecorationsInRange(range: IRange, ownerId: number = 0, filterOutValidation: boolean = false): model.IModelDecoration[] {
let validatedRange = this.validateRange(range);
return this._getDecorationsInRange(validatedRange, ownerId, filterOutValidation);
const decorations = this._getDecorationsInRange(validatedRange, ownerId, filterOutValidation);
decorations.push(...this._decorationProvider.getDecorationsInRange(validatedRange, ownerId, filterOutValidation));
return decorations;
}
public getOverviewRulerDecorations(ownerId: number = 0, filterOutValidation: boolean = false): model.IModelDecoration[] {
const versionId = this.getVersionId();
const result = this._decorationsTree.search(ownerId, filterOutValidation, true, versionId);
return this._ensureNodesHaveRanges(result);
return this._decorationsTree.getAll(this, ownerId, filterOutValidation, true);
}
public getInjectedTextDecorations(ownerId: number = 0): model.IModelDecoration[] {
return this._decorationsTree.getAllInjectedText(this, ownerId);
}
private _getInjectedTextInLine(lineNumber: number): LineInjectedText[] {
const startOffset = this._buffer.getOffsetAt(lineNumber, 1);
const endOffset = startOffset + this._buffer.getLineLength(lineNumber);
const result = this._decorationsTree.getInjectedTextInInterval(this, startOffset, endOffset, 0);
return LineInjectedText.fromDecorations(result).filter(t => t.lineNumber === lineNumber);
}
public getAllDecorations(ownerId: number = 0, filterOutValidation: boolean = false): model.IModelDecoration[] {
const versionId = this.getVersionId();
const result = this._decorationsTree.search(ownerId, filterOutValidation, false, versionId);
return this._ensureNodesHaveRanges(result);
const result = this._decorationsTree.getAll(this, ownerId, filterOutValidation, false);
result.push(...this._decorationProvider.getAllDecorations(ownerId, filterOutValidation));
return result;
}
private _getDecorationsInRange(filterRange: Range, filterOwnerId: number, filterOutValidation: boolean): IntervalNode[] {
private _getDecorationsInRange(filterRange: Range, filterOwnerId: number, filterOutValidation: boolean): model.IModelDecoration[] {
const startOffset = this._buffer.getOffsetAt(filterRange.startLineNumber, filterRange.startColumn);
const endOffset = this._buffer.getOffsetAt(filterRange.endLineNumber, filterRange.endColumn);
const versionId = this.getVersionId();
const result = this._decorationsTree.intervalSearch(startOffset, endOffset, filterOwnerId, filterOutValidation, versionId);
return this._ensureNodesHaveRanges(result);
return this._decorationsTree.getAllInInterval(this, startOffset, endOffset, filterOwnerId, filterOutValidation);
}
private _ensureNodesHaveRanges(nodes: IntervalNode[]): IntervalNode[] {
for (let i = 0, len = nodes.length; i < len; i++) {
const node = nodes[i];
if (node.range === null) {
node.range = this._getRangeAt(node.cachedAbsoluteStart, node.cachedAbsoluteEnd);
}
}
return nodes;
}
private _getRangeAt(start: number, end: number): Range {
public getRangeAt(start: number, end: number): Range {
return this._buffer.getRangeAt(start, end - start);
}
@@ -1715,6 +1813,16 @@ export class TextModel extends Disposable implements model.ITextModel {
if (!node) {
return;
}
if (node.options.after) {
const oldRange = this.getDecorationRange(decorationId);
this._onDidChangeDecorations.recordLineAffectedByInjectedText(oldRange!.endLineNumber);
}
if (node.options.before) {
const oldRange = this.getDecorationRange(decorationId);
this._onDidChangeDecorations.recordLineAffectedByInjectedText(oldRange!.startLineNumber);
}
const range = this._validateRangeRelaxedNoAllocations(_range);
const startOffset = this._buffer.getOffsetAt(range.startLineNumber, range.startColumn);
const endOffset = this._buffer.getOffsetAt(range.endLineNumber, range.endColumn);
@@ -1723,6 +1831,13 @@ export class TextModel extends Disposable implements model.ITextModel {
node.reset(this.getVersionId(), startOffset, endOffset, range);
this._decorationsTree.insert(node);
this._onDidChangeDecorations.checkAffectedAndFire(node.options);
if (node.options.after) {
this._onDidChangeDecorations.recordLineAffectedByInjectedText(range.endLineNumber);
}
if (node.options.before) {
this._onDidChangeDecorations.recordLineAffectedByInjectedText(range.startLineNumber);
}
}
private _changeDecorationOptionsImpl(decorationId: string, options: ModelDecorationOptions): void {
@@ -1737,6 +1852,15 @@ export class TextModel extends Disposable implements model.ITextModel {
this._onDidChangeDecorations.checkAffectedAndFire(node.options);
this._onDidChangeDecorations.checkAffectedAndFire(options);
if (node.options.after || options.after) {
const nodeRange = this._decorationsTree.getNodeRange(this, node);
this._onDidChangeDecorations.recordLineAffectedByInjectedText(nodeRange.endLineNumber);
}
if (node.options.before || options.before) {
const nodeRange = this._decorationsTree.getNodeRange(this, node);
this._onDidChangeDecorations.recordLineAffectedByInjectedText(nodeRange.startLineNumber);
}
if (nodeWasInOverviewRuler !== nodeIsInOverviewRuler) {
// Delete + Insert due to an overview ruler status change
this._decorationsTree.delete(node);
@@ -1769,7 +1893,17 @@ export class TextModel extends Disposable implements model.ITextModel {
// (2) remove the node from the tree (if it exists)
if (node) {
if (node.options.after) {
const nodeRange = this._decorationsTree.getNodeRange(this, node);
this._onDidChangeDecorations.recordLineAffectedByInjectedText(nodeRange.endLineNumber);
}
if (node.options.before) {
const nodeRange = this._decorationsTree.getNodeRange(this, node);
this._onDidChangeDecorations.recordLineAffectedByInjectedText(nodeRange.startLineNumber);
}
this._decorationsTree.delete(node);
this._onDidChangeDecorations.checkAffectedAndFire(node.options);
}
}
@@ -1793,6 +1927,14 @@ export class TextModel extends Disposable implements model.ITextModel {
node.ownerId = ownerId;
node.reset(versionId, startOffset, endOffset, range);
node.setOptions(options);
if (node.options.after) {
this._onDidChangeDecorations.recordLineAffectedByInjectedText(range.endLineNumber);
}
if (node.options.before) {
this._onDidChangeDecorations.recordLineAffectedByInjectedText(range.startLineNumber);
}
this._onDidChangeDecorations.checkAffectedAndFire(options);
this._decorationsTree.insert(node);
@@ -1822,44 +1964,43 @@ export class TextModel extends Disposable implements model.ITextModel {
this._tokens.setTokens(this._languageIdentifier.id, lineNumber - 1, this._buffer.getLineLength(lineNumber), tokens, false);
}
public setTokens(tokens: MultilineTokens[]): void {
if (tokens.length === 0) {
return;
}
public setTokens(tokens: MultilineTokens[], backgroundTokenizationCompleted: boolean = false): void {
if (tokens.length !== 0) {
let ranges: { fromLineNumber: number; toLineNumber: number; }[] = [];
let ranges: { fromLineNumber: number; toLineNumber: number; }[] = [];
for (let i = 0, len = tokens.length; i < len; i++) {
const element = tokens[i];
let minChangedLineNumber = 0;
let maxChangedLineNumber = 0;
let hasChange = false;
for (let j = 0, lenJ = element.tokens.length; j < lenJ; j++) {
const lineNumber = element.startLineNumber + j;
if (hasChange) {
this._tokens.setTokens(this._languageIdentifier.id, lineNumber - 1, this._buffer.getLineLength(lineNumber), element.tokens[j], false);
maxChangedLineNumber = lineNumber;
} else {
const lineHasChange = this._tokens.setTokens(this._languageIdentifier.id, lineNumber - 1, this._buffer.getLineLength(lineNumber), element.tokens[j], true);
if (lineHasChange) {
hasChange = true;
minChangedLineNumber = lineNumber;
for (let i = 0, len = tokens.length; i < len; i++) {
const element = tokens[i];
let minChangedLineNumber = 0;
let maxChangedLineNumber = 0;
let hasChange = false;
for (let j = 0, lenJ = element.tokens.length; j < lenJ; j++) {
const lineNumber = element.startLineNumber + j;
if (hasChange) {
this._tokens.setTokens(this._languageIdentifier.id, lineNumber - 1, this._buffer.getLineLength(lineNumber), element.tokens[j], false);
maxChangedLineNumber = lineNumber;
} else {
const lineHasChange = this._tokens.setTokens(this._languageIdentifier.id, lineNumber - 1, this._buffer.getLineLength(lineNumber), element.tokens[j], true);
if (lineHasChange) {
hasChange = true;
minChangedLineNumber = lineNumber;
maxChangedLineNumber = lineNumber;
}
}
}
if (hasChange) {
ranges.push({ fromLineNumber: minChangedLineNumber, toLineNumber: maxChangedLineNumber });
}
}
if (hasChange) {
ranges.push({ fromLineNumber: minChangedLineNumber, toLineNumber: maxChangedLineNumber });
}
}
if (ranges.length > 0) {
this._emitModelTokensChangedEvent({
tokenizationSupportChanged: false,
semanticTokensApplied: false,
ranges: ranges
});
if (ranges.length > 0) {
this._emitModelTokensChangedEvent({
tokenizationSupportChanged: false,
semanticTokensApplied: false,
ranges: ranges
});
}
}
this.setBackgroundTokenizationState(backgroundTokenizationCompleted ? BackgroundTokenizationState.Completed : BackgroundTokenizationState.InProgress);
}
public setSemanticTokens(tokens: MultilineTokens2[] | null, isComplete: boolean): void {
@@ -3028,7 +3169,7 @@ export class TextModel extends Disposable implements model.ITextModel {
}
//#endregion
normalizePosition(position: Position, affinity: model.PositionNormalizationAffinity): Position {
normalizePosition(position: Position, affinity: model.PositionAffinity): Position {
return position;
}
@@ -3056,6 +3197,19 @@ function indentOfLine(line: string): number {
//#region Decorations
function isNodeInOverviewRuler(node: IntervalNode): boolean {
return (node.options.overviewRuler && node.options.overviewRuler.color ? true : false);
}
function isNodeInjectedText(node: IntervalNode): boolean {
return !!node.options.after || !!node.options.before;
}
export interface IDecorationsTreesHost {
getVersionId(): number;
getRangeAt(start: number, end: number): Range;
}
class DecorationsTrees {
/**
@@ -3068,41 +3222,90 @@ class DecorationsTrees {
*/
private readonly _decorationsTree1: IntervalTree;
/**
* This tree holds decorations that contain injected text.
*/
private readonly _injectedTextDecorationsTree: IntervalTree;
constructor() {
this._decorationsTree0 = new IntervalTree();
this._decorationsTree1 = new IntervalTree();
this._injectedTextDecorationsTree = new IntervalTree();
}
public intervalSearch(start: number, end: number, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number): IntervalNode[] {
public ensureAllNodesHaveRanges(host: IDecorationsTreesHost): void {
this.getAll(host, 0, false, false);
}
private _ensureNodesHaveRanges(host: IDecorationsTreesHost, nodes: IntervalNode[]): model.IModelDecoration[] {
for (const node of nodes) {
if (node.range === null) {
node.range = host.getRangeAt(node.cachedAbsoluteStart, node.cachedAbsoluteEnd);
}
}
return <model.IModelDecoration[]>nodes;
}
public getAllInInterval(host: IDecorationsTreesHost, start: number, end: number, filterOwnerId: number, filterOutValidation: boolean): model.IModelDecoration[] {
const versionId = host.getVersionId();
const result = this._intervalSearch(start, end, filterOwnerId, filterOutValidation, versionId);
return this._ensureNodesHaveRanges(host, result);
}
private _intervalSearch(start: number, end: number, filterOwnerId: number, filterOutValidation: boolean, cachedVersionId: number): IntervalNode[] {
const r0 = this._decorationsTree0.intervalSearch(start, end, filterOwnerId, filterOutValidation, cachedVersionId);
const r1 = this._decorationsTree1.intervalSearch(start, end, filterOwnerId, filterOutValidation, cachedVersionId);
return r0.concat(r1);
const r2 = this._injectedTextDecorationsTree.intervalSearch(start, end, filterOwnerId, filterOutValidation, cachedVersionId);
return r0.concat(r1).concat(r2);
}
public search(filterOwnerId: number, filterOutValidation: boolean, overviewRulerOnly: boolean, cachedVersionId: number): IntervalNode[] {
public getInjectedTextInInterval(host: IDecorationsTreesHost, start: number, end: number, filterOwnerId: number): model.IModelDecoration[] {
const versionId = host.getVersionId();
const result = this._injectedTextDecorationsTree.intervalSearch(start, end, filterOwnerId, false, versionId);
return this._ensureNodesHaveRanges(host, result);
}
public getAllInjectedText(host: IDecorationsTreesHost, filterOwnerId: number): model.IModelDecoration[] {
const versionId = host.getVersionId();
const result = this._injectedTextDecorationsTree.search(filterOwnerId, false, versionId);
return this._ensureNodesHaveRanges(host, result);
}
public getAll(host: IDecorationsTreesHost, filterOwnerId: number, filterOutValidation: boolean, overviewRulerOnly: boolean): model.IModelDecoration[] {
const versionId = host.getVersionId();
const result = this._search(filterOwnerId, filterOutValidation, overviewRulerOnly, versionId);
return this._ensureNodesHaveRanges(host, result);
}
private _search(filterOwnerId: number, filterOutValidation: boolean, overviewRulerOnly: boolean, cachedVersionId: number): IntervalNode[] {
if (overviewRulerOnly) {
return this._decorationsTree1.search(filterOwnerId, filterOutValidation, cachedVersionId);
} else {
const r0 = this._decorationsTree0.search(filterOwnerId, filterOutValidation, cachedVersionId);
const r1 = this._decorationsTree1.search(filterOwnerId, filterOutValidation, cachedVersionId);
return r0.concat(r1);
const r2 = this._injectedTextDecorationsTree.search(filterOwnerId, filterOutValidation, cachedVersionId);
return r0.concat(r1).concat(r2);
}
}
public collectNodesFromOwner(ownerId: number): IntervalNode[] {
const r0 = this._decorationsTree0.collectNodesFromOwner(ownerId);
const r1 = this._decorationsTree1.collectNodesFromOwner(ownerId);
return r0.concat(r1);
const r2 = this._injectedTextDecorationsTree.collectNodesFromOwner(ownerId);
return r0.concat(r1).concat(r2);
}
public collectNodesPostOrder(): IntervalNode[] {
const r0 = this._decorationsTree0.collectNodesPostOrder();
const r1 = this._decorationsTree1.collectNodesPostOrder();
return r0.concat(r1);
const r2 = this._injectedTextDecorationsTree.collectNodesPostOrder();
return r0.concat(r1).concat(r2);
}
public insert(node: IntervalNode): void {
if (getNodeIsInOverviewRuler(node)) {
if (isNodeInjectedText(node)) {
this._injectedTextDecorationsTree.insert(node);
} else if (isNodeInOverviewRuler(node)) {
this._decorationsTree1.insert(node);
} else {
this._decorationsTree0.insert(node);
@@ -3110,15 +3313,30 @@ class DecorationsTrees {
}
public delete(node: IntervalNode): void {
if (getNodeIsInOverviewRuler(node)) {
if (isNodeInjectedText(node)) {
this._injectedTextDecorationsTree.delete(node);
} else if (isNodeInOverviewRuler(node)) {
this._decorationsTree1.delete(node);
} else {
this._decorationsTree0.delete(node);
}
}
public resolveNode(node: IntervalNode, cachedVersionId: number): void {
if (getNodeIsInOverviewRuler(node)) {
public getNodeRange(host: IDecorationsTreesHost, node: IntervalNode): Range {
const versionId = host.getVersionId();
if (node.cachedVersionId !== versionId) {
this._resolveNode(node, versionId);
}
if (node.range === null) {
node.range = host.getRangeAt(node.cachedAbsoluteStart, node.cachedAbsoluteEnd);
}
return node.range;
}
private _resolveNode(node: IntervalNode, cachedVersionId: number): void {
if (isNodeInjectedText(node)) {
this._injectedTextDecorationsTree.resolveNode(node, cachedVersionId);
} else if (isNodeInOverviewRuler(node)) {
this._decorationsTree1.resolveNode(node, cachedVersionId);
} else {
this._decorationsTree0.resolveNode(node, cachedVersionId);
@@ -3128,6 +3346,7 @@ class DecorationsTrees {
public acceptReplace(offset: number, length: number, textLength: number, forceMoveMarkers: boolean): void {
this._decorationsTree0.acceptReplace(offset, length, textLength, forceMoveMarkers);
this._decorationsTree1.acceptReplace(offset, length, textLength, forceMoveMarkers);
this._injectedTextDecorationsTree.acceptReplace(offset, length, textLength, forceMoveMarkers);
}
}
@@ -3217,6 +3436,25 @@ export class ModelDecorationMinimapOptions extends DecorationOptions {
}
}
export class ModelDecorationInjectedTextOptions implements model.InjectedTextOptions {
public static from(options: model.InjectedTextOptions): ModelDecorationInjectedTextOptions {
if (options instanceof ModelDecorationInjectedTextOptions) {
return options;
}
return new ModelDecorationInjectedTextOptions(options);
}
public readonly content: string;
readonly inlineClassName: string | null;
readonly inlineClassNameAffectsLetterSpacing: boolean;
private constructor(options: model.InjectedTextOptions) {
this.content = options.content || '';
this.inlineClassName = options.inlineClassName || null;
this.inlineClassNameAffectsLetterSpacing = options.inlineClassNameAffectsLetterSpacing || false;
}
}
export class ModelDecorationOptions implements model.IModelDecorationOptions {
public static EMPTY: ModelDecorationOptions;
@@ -3248,6 +3486,8 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions {
readonly inlineClassNameAffectsLetterSpacing: boolean;
readonly beforeContentClassName: string | null;
readonly afterContentClassName: string | null;
readonly after: ModelDecorationInjectedTextOptions | null;
readonly before: ModelDecorationInjectedTextOptions | null;
private constructor(options: model.IModelDecorationOptions) {
this.description = options.description;
@@ -3269,6 +3509,8 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions {
this.inlineClassNameAffectsLetterSpacing = options.inlineClassNameAffectsLetterSpacing || false;
this.beforeContentClassName = options.beforeContentClassName ? cleanClassName(options.beforeContentClassName) : null;
this.afterContentClassName = options.afterContentClassName ? cleanClassName(options.afterContentClassName) : null;
this.after = options.after ? ModelDecorationInjectedTextOptions.from(options.after) : null;
this.before = options.before ? ModelDecorationInjectedTextOptions.from(options.before) : null;
}
}
ModelDecorationOptions.EMPTY = ModelDecorationOptions.register({ description: 'empty' });
@@ -3299,8 +3541,9 @@ export class DidChangeDecorationsEmitter extends Disposable {
private _shouldFire: boolean;
private _affectsMinimap: boolean;
private _affectsOverviewRuler: boolean;
private _affectedInjectedTextLines: Set<number> | null = null;
constructor() {
constructor(private readonly handleBeforeFire: (affectedInjectedTextLines: Set<number> | null) => void) {
super();
this._deferredCnt = 0;
this._shouldFire = false;
@@ -3316,18 +3559,30 @@ export class DidChangeDecorationsEmitter extends Disposable {
this._deferredCnt--;
if (this._deferredCnt === 0) {
if (this._shouldFire) {
this.handleBeforeFire(this._affectedInjectedTextLines);
const event: IModelDecorationsChangedEvent = {
affectsMinimap: this._affectsMinimap,
affectsOverviewRuler: this._affectsOverviewRuler,
affectsOverviewRuler: this._affectsOverviewRuler
};
this._shouldFire = false;
this._affectsMinimap = false;
this._affectsOverviewRuler = false;
this._actual.fire(event);
}
this._affectedInjectedTextLines?.clear();
this._affectedInjectedTextLines = null;
}
}
public recordLineAffectedByInjectedText(lineNumber: number): void {
if (!this._affectedInjectedTextLines) {
this._affectedInjectedTextLines = new Set();
}
this._affectedInjectedTextLines.add(lineNumber);
}
public checkAffectedAndFire(options: ModelDecorationOptions): void {
if (!this._affectsMinimap) {
this._affectsMinimap = options.minimap && options.minimap.position ? true : false;

View File

@@ -5,6 +5,7 @@
import { IRange } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
import { IModelDecoration, InjectedTextOptions } from 'vs/editor/common/model';
/**
* An event describing that the current mode associated with a model has changed.
@@ -126,6 +127,73 @@ export class ModelRawFlush {
public readonly changeType = RawContentChangedType.Flush;
}
/**
* Represents text injected on a line
* @internal
*/
export class LineInjectedText {
public static applyInjectedText(lineText: string, injectedTexts: LineInjectedText[] | null): string {
if (!injectedTexts || injectedTexts.length === 0) {
return lineText;
}
let result = '';
let lastOriginalOffset = 0;
for (const injectedText of injectedTexts) {
result += lineText.substring(lastOriginalOffset, injectedText.column - 1);
lastOriginalOffset = injectedText.column - 1;
result += injectedText.options.content;
}
result += lineText.substring(lastOriginalOffset);
return result;
}
public static fromDecorations(decorations: IModelDecoration[]): LineInjectedText[] {
const result: LineInjectedText[] = [];
for (const decoration of decorations) {
if (decoration.options.before && decoration.options.before.content.length > 0) {
result.push(new LineInjectedText(
decoration.ownerId,
decoration.range.startLineNumber,
decoration.range.startColumn,
decoration.options.before,
0,
));
}
if (decoration.options.after && decoration.options.after.content.length > 0) {
result.push(new LineInjectedText(
decoration.ownerId,
decoration.range.endLineNumber,
decoration.range.endColumn,
decoration.options.after,
1,
));
}
}
result.sort((a, b) => {
if (a.lineNumber === b.lineNumber) {
if (a.column === b.column) {
return a.order - b.order;
}
return a.column - b.column;
}
return a.lineNumber - b.lineNumber;
});
return result;
}
constructor(
public readonly ownerId: number,
public readonly lineNumber: number,
public readonly column: number,
public readonly options: InjectedTextOptions,
public readonly order: number
) { }
public withText(text: string): LineInjectedText {
return new LineInjectedText(this.ownerId, this.lineNumber, this.column, { ...this.options, content: text }, this.order);
}
}
/**
* An event describing that a line has changed in a model.
* @internal
@@ -140,10 +208,15 @@ export class ModelRawLineChanged {
* The new value of the line.
*/
public readonly detail: string;
/**
* The injected text on the line.
*/
public readonly injectedText: LineInjectedText[] | null;
constructor(lineNumber: number, detail: string) {
constructor(lineNumber: number, detail: string, injectedText: LineInjectedText[] | null) {
this.lineNumber = lineNumber;
this.detail = detail;
this.injectedText = injectedText;
}
}
@@ -186,8 +259,13 @@ export class ModelRawLinesInserted {
* The text that was inserted
*/
public readonly detail: string[];
/**
* The injected texts for every inserted line.
*/
public readonly injectedTexts: (LineInjectedText[] | null)[];
constructor(fromLineNumber: number, toLineNumber: number, detail: string[]) {
constructor(fromLineNumber: number, toLineNumber: number, detail: string[], injectedTexts: (LineInjectedText[] | null)[]) {
this.injectedTexts = injectedTexts;
this.fromLineNumber = fromLineNumber;
this.toLineNumber = toLineNumber;
this.detail = detail;
@@ -256,6 +334,19 @@ export class ModelRawContentChangedEvent {
}
}
/**
* An event describing a change in injected text.
* @internal
*/
export class ModelInjectedTextChangedEvent {
public readonly changes: ModelRawLineChanged[];
constructor(changes: ModelRawLineChanged[]) {
this.changes = changes;
}
}
/**
* @internal
*/

View File

@@ -9,7 +9,6 @@ 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 { TokenizationResult2 } from 'vs/editor/common/core/token';
import { RawContentChangedType } from 'vs/editor/common/model/textModelEvents';
import { IState, ITokenizationSupport, LanguageIdentifier, TokenizationRegistry } from 'vs/editor/common/modes';
import { nullTokenize2 } from 'vs/editor/common/modes/nullMode';
import { TextModel } from 'vs/editor/common/model/textModel';
@@ -217,14 +216,11 @@ export class TextModelTokenization extends Disposable {
this._textModel.clearTokens();
}));
this._register(this._textModel.onDidChangeRawContentFast((e) => {
if (e.containsEvent(RawContentChangedType.Flush)) {
this._register(this._textModel.onDidChangeContentFast((e) => {
if (e.isFlush) {
this._resetTokenizationState();
return;
}
}));
this._register(this._textModel.onDidChangeContentFast((e) => {
for (let i = 0, len = e.changes.length; i < len; i++) {
const change = e.changes[i];
const [eolCount] = countEOL(change.text);
@@ -270,10 +266,13 @@ export class TextModelTokenization extends Disposable {
}
}
private _revalidateTokensNow(toLineNumber: number = this._textModel.getLineCount()): void {
private _revalidateTokensNow(): void {
const textModelLastLineNumber = this._textModel.getLineCount();
const MAX_ALLOWED_TIME = 1;
const builder = new MultilineTokensBuilder();
const sw = StopWatch.create(false);
let tokenizedLineNumber = -1;
while (this._hasLinesToTokenize()) {
if (sw.elapsed() > MAX_ALLOWED_TIME) {
@@ -281,15 +280,15 @@ export class TextModelTokenization extends Disposable {
break;
}
const tokenizedLineNumber = this._tokenizeOneInvalidLine(builder);
tokenizedLineNumber = this._tokenizeOneInvalidLine(builder);
if (tokenizedLineNumber >= toLineNumber) {
if (tokenizedLineNumber >= textModelLastLineNumber) {
break;
}
}
this._beginBackgroundTokenization();
this._textModel.setTokens(builder.tokens);
this._textModel.setTokens(builder.tokens, tokenizedLineNumber >= textModelLastLineNumber);
}
public tokenizeViewport(startLineNumber: number, endLineNumber: number): void {

View File

@@ -487,25 +487,9 @@ export let completionKindFromString: {
})();
export interface CompletionItemLabel {
/**
* The function or variable. Rendered leftmost.
*/
name: string;
/**
* The parameters without the return type. Render after `name`.
*/
parameters?: string;
/**
* The fully qualified name, like package name or file path. Rendered after `signature`.
*/
qualifier?: string;
/**
* The return-type of a function or type of a property/variable. Rendered rightmost.
*/
type?: string;
label: string;
detail?: string;
description?: string;
}
export const enum CompletionItemTag {
@@ -1560,6 +1544,7 @@ export interface AuthenticationSession {
id: string;
}
scopes: ReadonlyArray<string>;
idToken?: string;
}
/**

View File

@@ -254,7 +254,7 @@ export interface CompleteEnterAction {
* @internal
*/
export class StandardAutoClosingPairConditional {
_standardAutoClosingPairConditionalBrand: void;
_standardAutoClosingPairConditionalBrand: void = undefined;
readonly open: string;
readonly close: string;

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from 'vs/base/common/event';
import { hash } from 'vs/base/common/hash';
import { doHash } from 'vs/base/common/hash';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { LRUCache } from 'vs/base/common/map';
import { MovingAverage } from 'vs/base/common/numbers';
@@ -179,6 +179,18 @@ export class LanguageFeatureRegistry<T> {
}
const _hashes = new WeakMap<object, number>();
let pool = 0;
function weakHash(obj: object): number {
let value = _hashes.get(obj);
if (value === undefined) {
value = ++pool;
_hashes.set(obj, value);
}
return value;
}
/**
* Keeps moving average per model and set of providers so that requests
* can be debounce according to the provider performance
@@ -187,14 +199,15 @@ export class LanguageFeatureRequestDelays {
private readonly _cache = new LRUCache<string, MovingAverage>(50, 0.7);
constructor(
private readonly _registry: LanguageFeatureRegistry<any>,
private readonly _registry: LanguageFeatureRegistry<object>,
readonly min: number,
readonly max: number = Number.MAX_SAFE_INTEGER,
) { }
private _key(model: ITextModel): string {
return model.id + hash(this._registry.all(model));
return model.id + this._registry.all(model).reduce((hashVal, obj) => doHash(weakHash(obj), hashVal), 0);
}
private _clamp(value: number | undefined): number {

View File

@@ -10,6 +10,7 @@ import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageCo
import { ILanguageExtensionPoint } from 'vs/editor/common/services/modeService';
import { Registry } from 'vs/platform/registry/common/platform';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Mimes } from 'vs/base/common/mime';
// Define extension point ids
export const Extensions = {
@@ -65,7 +66,7 @@ ModesRegistry.registerLanguage({
id: PLAINTEXT_MODE_ID,
extensions: [PLAINTEXT_EXTENSION],
aliases: [nls.localize('plainText.alias', "Plain Text"), 'text'],
mimetypes: ['text/plain']
mimetypes: [Mimes.text]
});
LanguageConfigurationRegistry.register(PLAINTEXT_LANGUAGE_IDENTIFIER, {
brackets: [

View File

@@ -32,7 +32,7 @@ export function createScopedLineTokens(context: LineTokens, offset: number): Sco
}
export class ScopedLineTokens {
_scopedLineTokensBrand: void;
_scopedLineTokensBrand: void = undefined;
public readonly languageId: modes.LanguageId;
private readonly _actual: LineTokens;

View File

@@ -15,7 +15,7 @@ interface InternalBracket {
}
export class RichEditBracket {
_richEditBracketBrand: void;
_richEditBracketBrand: void = undefined;
readonly languageIdentifier: LanguageIdentifier;
readonly index: number;
@@ -113,7 +113,7 @@ function groupFuzzyBrackets(brackets: CharacterPair[]): InternalBracket[] {
}
export class RichEditBrackets {
_richEditBracketsBrand: void;
_richEditBracketsBrand: void = undefined;
public readonly brackets: RichEditBracket[];
public readonly forwardRegex: RegExp;

View File

@@ -14,7 +14,7 @@ export interface ITokenThemeRule {
}
export class ParsedTokenThemeRule {
_parsedThemeRuleBrand: void;
_parsedThemeRuleBrand: void = undefined;
readonly token: string;
readonly index: number;
@@ -270,7 +270,7 @@ export function strcmp(a: string, b: string): number {
}
export class ThemeTrieElementRule {
_themeTrieElementRuleBrand: void;
_themeTrieElementRuleBrand: void = undefined;
private _fontStyle: FontStyle;
private _foreground: ColorId;
@@ -332,7 +332,7 @@ export class ExternalThemeTrieElement {
}
export class ThemeTrieElement {
_themeTrieElementBrand: void;
_themeTrieElementBrand: void = undefined;
private readonly _mainRule: ThemeTrieElementRule;
private readonly _children: Map<string, ThemeTrieElement>;

View File

@@ -76,7 +76,7 @@ export interface ICommonModel extends ILinkComputerTarget, IMirrorModel {
* Range of a word inside a model.
* @internal
*/
interface IWordRange {
export interface IWordRange {
/**
* The index where the word starts.
*/
@@ -90,7 +90,7 @@ interface IWordRange {
/**
* @internal
*/
class MirrorModel extends BaseMirrorModel implements ICommonModel {
export class MirrorModel extends BaseMirrorModel implements ICommonModel {
public get uri(): URI {
return this._uri;
@@ -235,7 +235,7 @@ class MirrorModel extends BaseMirrorModel implements ICommonModel {
public offsetAt(position: IPosition): number {
position = this._validatePosition(position);
this._ensureLineStarts();
return this._lineStarts!.getAccumulatedValue(position.lineNumber - 2) + (position.column - 1);
return this._lineStarts!.getPrefixSum(position.lineNumber - 2) + (position.column - 1);
}
public positionAt(offset: number): IPosition {
@@ -326,7 +326,7 @@ declare const require: any;
export class EditorSimpleWorker implements IRequestHandler, IDisposable {
_requestHandlerBrand: any;
private readonly _host: EditorWorkerHost;
protected readonly _host: EditorWorkerHost;
private _models: { [uri: string]: MirrorModel; };
private readonly _foreignModuleFactory: IForeignModuleFactory | null;
private _foreignModule: any;

View File

@@ -388,11 +388,15 @@ class SynchronousWorkerClient<T extends IDisposable> implements IWorkerClient<T>
}
}
export interface IEditorWorkerClient {
fhr(method: string, args: any[]): Promise<any>;
}
export class EditorWorkerHost {
private readonly _workerClient: EditorWorkerClient;
private readonly _workerClient: IEditorWorkerClient;
constructor(workerClient: EditorWorkerClient) {
constructor(workerClient: IEditorWorkerClient) {
this._workerClient = workerClient;
}
@@ -402,12 +406,12 @@ export class EditorWorkerHost {
}
}
export class EditorWorkerClient extends Disposable {
export class EditorWorkerClient extends Disposable implements IEditorWorkerClient {
private readonly _modelService: IModelService;
private readonly _keepIdleModels: boolean;
private _worker: IWorkerClient<EditorSimpleWorker> | null;
private readonly _workerFactory: DefaultWorkerFactory;
protected _worker: IWorkerClient<EditorSimpleWorker> | null;
protected readonly _workerFactory: DefaultWorkerFactory;
private _modelManager: EditorModelManager | null;
private _disposed = false;

View File

@@ -16,10 +16,8 @@ import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDeco
import { Schemas } from 'vs/base/common/network';
import { Emitter, Event } from 'vs/base/common/event';
import { minimapWarning, minimapError } from 'vs/platform/theme/common/colorRegistry';
import { ResourceMap } from 'vs/base/common/map';
function MODEL_ID(resource: URI): string {
return resource.toString();
}
class MarkerDecorations extends Disposable {
@@ -68,7 +66,7 @@ export class MarkerDecorationsService extends Disposable implements IMarkerDecor
private readonly _onDidChangeMarker = this._register(new Emitter<ITextModel>());
readonly onDidChangeMarker: Event<ITextModel> = this._onDidChangeMarker.event;
private readonly _markerDecorations = new Map<string, MarkerDecorations>();
private readonly _markerDecorations = new ResourceMap<MarkerDecorations>();
constructor(
@IModelService modelService: IModelService,
@@ -88,18 +86,18 @@ export class MarkerDecorationsService extends Disposable implements IMarkerDecor
}
getMarker(uri: URI, decoration: IModelDecoration): IMarker | null {
const markerDecorations = this._markerDecorations.get(MODEL_ID(uri));
const markerDecorations = this._markerDecorations.get(uri);
return markerDecorations ? (markerDecorations.getMarker(decoration) || null) : null;
}
getLiveMarkers(uri: URI): [Range, IMarker][] {
const markerDecorations = this._markerDecorations.get(MODEL_ID(uri));
const markerDecorations = this._markerDecorations.get(uri);
return markerDecorations ? markerDecorations.getMarkers() : [];
}
private _handleMarkerChange(changedResources: readonly URI[]): void {
changedResources.forEach((resource) => {
const markerDecorations = this._markerDecorations.get(MODEL_ID(resource));
const markerDecorations = this._markerDecorations.get(resource);
if (markerDecorations) {
this._updateDecorations(markerDecorations);
}
@@ -108,15 +106,15 @@ export class MarkerDecorationsService extends Disposable implements IMarkerDecor
private _onModelAdded(model: ITextModel): void {
const markerDecorations = new MarkerDecorations(model);
this._markerDecorations.set(MODEL_ID(model.uri), markerDecorations);
this._markerDecorations.set(model.uri, markerDecorations);
this._updateDecorations(markerDecorations);
}
private _onModelRemoved(model: ITextModel): void {
const markerDecorations = this._markerDecorations.get(MODEL_ID(model.uri));
const markerDecorations = this._markerDecorations.get(model.uri);
if (markerDecorations) {
markerDecorations.dispose();
this._markerDecorations.delete(MODEL_ID(model.uri));
this._markerDecorations.delete(model.uri);
}
// clean up markers for internal, transient models

View File

@@ -30,6 +30,7 @@ import { EditStackElement, isEditStackElement } from 'vs/editor/common/model/edi
import { Schemas } from 'vs/base/common/network';
import { SemanticTokensProviderStyling, toMultilineTokens2 } from 'vs/editor/common/services/semanticTokensProviderStyling';
import { getDocumentSemanticTokens, isSemanticTokens, isSemanticTokensEdits } from 'vs/editor/common/services/getSemanticTokens';
import { equals } from 'vs/base/common/objects';
export interface IEditorSemanticHighlightingOptions {
enabled: true | false | 'configuredByTheme';
@@ -101,6 +102,7 @@ interface IRawEditorConfig {
trimAutoWhitespace?: any;
creationOptions?: any;
largeFileOptimizations?: any;
bracketPairColorization?: any;
}
interface IRawConfig {
@@ -133,6 +135,7 @@ function schemaShouldMaintainUndoRedoElements(resource: URI) {
resource.scheme === Schemas.file
|| resource.scheme === Schemas.vscodeRemote
|| resource.scheme === Schemas.userData
|| resource.scheme === Schemas.vscodeNotebookCell
|| resource.scheme === 'fake-fs' // for tests
);
}
@@ -232,6 +235,12 @@ export class ModelServiceImpl extends Disposable implements IModelService {
if (config.editor && typeof config.editor.largeFileOptimizations !== 'undefined') {
largeFileOptimizations = (config.editor.largeFileOptimizations === 'false' ? false : Boolean(config.editor.largeFileOptimizations));
}
let bracketPairColorizationOptions = EDITOR_MODEL_DEFAULTS.bracketPairColorizationOptions;
if (config.editor?.bracketPairColorization && typeof config.editor.bracketPairColorization === 'object') {
bracketPairColorizationOptions = {
enabled: !!config.editor.bracketPairColorization.enabled
};
}
return {
isForSimpleWidget: isForSimpleWidget,
@@ -241,7 +250,8 @@ export class ModelServiceImpl extends Disposable implements IModelService {
detectIndentation: detectIndentation,
defaultEOL: newDefaultEOL,
trimAutoWhitespace: trimAutoWhitespace,
largeFileOptimizations: largeFileOptimizations
largeFileOptimizations: largeFileOptimizations,
bracketPairColorizationOptions
};
}
@@ -249,15 +259,15 @@ export class ModelServiceImpl extends Disposable implements IModelService {
if (resource) {
return this._resourcePropertiesService.getEOL(resource, language);
}
const eol = this._configurationService.getValue<string>('files.eol', { overrideIdentifier: language });
if (eol && eol !== 'auto') {
const eol = this._configurationService.getValue('files.eol', { overrideIdentifier: language });
if (eol && typeof eol === 'string' && eol !== 'auto') {
return eol;
}
return platform.OS === platform.OperatingSystem.Linux || platform.OS === platform.OperatingSystem.Macintosh ? '\n' : '\r\n';
}
private _shouldRestoreUndoStack(): boolean {
const result = this._configurationService.getValue<boolean>('files.restoreUndoStack');
const result = this._configurationService.getValue('files.restoreUndoStack');
if (typeof result === 'boolean') {
return result;
}
@@ -303,6 +313,7 @@ export class ModelServiceImpl extends Disposable implements IModelService {
&& (currentOptions.tabSize === newOptions.tabSize)
&& (currentOptions.indentSize === newOptions.indentSize)
&& (currentOptions.trimAutoWhitespace === newOptions.trimAutoWhitespace)
&& equals(currentOptions.bracketPairColorizationOptions, newOptions.bracketPairColorizationOptions)
) {
// Same indent opts, no need to touch the model
return;
@@ -311,14 +322,16 @@ export class ModelServiceImpl extends Disposable implements IModelService {
if (newOptions.detectIndentation) {
model.detectIndentation(newOptions.insertSpaces, newOptions.tabSize);
model.updateOptions({
trimAutoWhitespace: newOptions.trimAutoWhitespace
trimAutoWhitespace: newOptions.trimAutoWhitespace,
bracketColorizationOptions: newOptions.bracketPairColorizationOptions
});
} else {
model.updateOptions({
insertSpaces: newOptions.insertSpaces,
tabSize: newOptions.tabSize,
indentSize: newOptions.indentSize,
trimAutoWhitespace: newOptions.trimAutoWhitespace
trimAutoWhitespace: newOptions.trimAutoWhitespace,
bracketColorizationOptions: newOptions.bracketPairColorizationOptions
});
}
}

View File

@@ -179,124 +179,126 @@ export enum EditorOption {
autoIndent = 9,
automaticLayout = 10,
autoSurround = 11,
codeLens = 12,
codeLensFontFamily = 13,
codeLensFontSize = 14,
colorDecorators = 15,
columnSelection = 16,
comments = 17,
contextmenu = 18,
copyWithSyntaxHighlighting = 19,
cursorBlinking = 20,
cursorSmoothCaretAnimation = 21,
cursorStyle = 22,
cursorSurroundingLines = 23,
cursorSurroundingLinesStyle = 24,
cursorWidth = 25,
disableLayerHinting = 26,
disableMonospaceOptimizations = 27,
domReadOnly = 28,
dragAndDrop = 29,
emptySelectionClipboard = 30,
extraEditorClassName = 31,
fastScrollSensitivity = 32,
find = 33,
fixedOverflowWidgets = 34,
folding = 35,
foldingStrategy = 36,
foldingHighlight = 37,
unfoldOnClickAfterEndOfLine = 38,
fontFamily = 39,
fontInfo = 40,
fontLigatures = 41,
fontSize = 42,
fontWeight = 43,
formatOnPaste = 44,
formatOnType = 45,
glyphMargin = 46,
gotoLocation = 47,
hideCursorInOverviewRuler = 48,
highlightActiveIndentGuide = 49,
hover = 50,
inDiffEditor = 51,
inlineSuggest = 52,
letterSpacing = 53,
lightbulb = 54,
lineDecorationsWidth = 55,
lineHeight = 56,
lineNumbers = 57,
lineNumbersMinChars = 58,
linkedEditing = 59,
links = 60,
matchBrackets = 61,
minimap = 62,
mouseStyle = 63,
mouseWheelScrollSensitivity = 64,
mouseWheelZoom = 65,
multiCursorMergeOverlapping = 66,
multiCursorModifier = 67,
multiCursorPaste = 68,
occurrencesHighlight = 69,
overviewRulerBorder = 70,
overviewRulerLanes = 71,
padding = 72,
parameterHints = 73,
peekWidgetDefaultFocus = 74,
definitionLinkOpensInPeek = 75,
quickSuggestions = 76,
quickSuggestionsDelay = 77,
readOnly = 78,
renameOnType = 79,
renderControlCharacters = 80,
renderIndentGuides = 81,
renderFinalNewline = 82,
renderLineHighlight = 83,
renderLineHighlightOnlyWhenFocus = 84,
renderValidationDecorations = 85,
renderWhitespace = 86,
revealHorizontalRightPadding = 87,
roundedSelection = 88,
rulers = 89,
scrollbar = 90,
scrollBeyondLastColumn = 91,
scrollBeyondLastLine = 92,
scrollPredominantAxis = 93,
selectionClipboard = 94,
selectionHighlight = 95,
selectOnLineNumbers = 96,
showFoldingControls = 97,
showUnused = 98,
snippetSuggestions = 99,
smartSelect = 100,
smoothScrolling = 101,
stickyTabStops = 102,
stopRenderingLineAfter = 103,
suggest = 104,
suggestFontSize = 105,
suggestLineHeight = 106,
suggestOnTriggerCharacters = 107,
suggestSelection = 108,
tabCompletion = 109,
tabIndex = 110,
unusualLineTerminators = 111,
useShadowDOM = 112,
useTabStops = 113,
wordSeparators = 114,
wordWrap = 115,
wordWrapBreakAfterCharacters = 116,
wordWrapBreakBeforeCharacters = 117,
wordWrapColumn = 118,
wordWrapOverride1 = 119,
wordWrapOverride2 = 120,
wrappingIndent = 121,
wrappingStrategy = 122,
showDeprecated = 123,
inlayHints = 124,
editorClassName = 125,
pixelRatio = 126,
tabFocusMode = 127,
layoutInfo = 128,
wrappingInfo = 129
bracketPairColorization = 12,
codeLens = 13,
codeLensFontFamily = 14,
codeLensFontSize = 15,
colorDecorators = 16,
columnSelection = 17,
comments = 18,
contextmenu = 19,
copyWithSyntaxHighlighting = 20,
cursorBlinking = 21,
cursorSmoothCaretAnimation = 22,
cursorStyle = 23,
cursorSurroundingLines = 24,
cursorSurroundingLinesStyle = 25,
cursorWidth = 26,
disableLayerHinting = 27,
disableMonospaceOptimizations = 28,
domReadOnly = 29,
dragAndDrop = 30,
emptySelectionClipboard = 31,
extraEditorClassName = 32,
fastScrollSensitivity = 33,
find = 34,
fixedOverflowWidgets = 35,
folding = 36,
foldingStrategy = 37,
foldingHighlight = 38,
foldingImportsByDefault = 39,
unfoldOnClickAfterEndOfLine = 40,
fontFamily = 41,
fontInfo = 42,
fontLigatures = 43,
fontSize = 44,
fontWeight = 45,
formatOnPaste = 46,
formatOnType = 47,
glyphMargin = 48,
gotoLocation = 49,
hideCursorInOverviewRuler = 50,
highlightActiveIndentGuide = 51,
hover = 52,
inDiffEditor = 53,
inlineSuggest = 54,
letterSpacing = 55,
lightbulb = 56,
lineDecorationsWidth = 57,
lineHeight = 58,
lineNumbers = 59,
lineNumbersMinChars = 60,
linkedEditing = 61,
links = 62,
matchBrackets = 63,
minimap = 64,
mouseStyle = 65,
mouseWheelScrollSensitivity = 66,
mouseWheelZoom = 67,
multiCursorMergeOverlapping = 68,
multiCursorModifier = 69,
multiCursorPaste = 70,
occurrencesHighlight = 71,
overviewRulerBorder = 72,
overviewRulerLanes = 73,
padding = 74,
parameterHints = 75,
peekWidgetDefaultFocus = 76,
definitionLinkOpensInPeek = 77,
quickSuggestions = 78,
quickSuggestionsDelay = 79,
readOnly = 80,
renameOnType = 81,
renderControlCharacters = 82,
renderIndentGuides = 83,
renderFinalNewline = 84,
renderLineHighlight = 85,
renderLineHighlightOnlyWhenFocus = 86,
renderValidationDecorations = 87,
renderWhitespace = 88,
revealHorizontalRightPadding = 89,
roundedSelection = 90,
rulers = 91,
scrollbar = 92,
scrollBeyondLastColumn = 93,
scrollBeyondLastLine = 94,
scrollPredominantAxis = 95,
selectionClipboard = 96,
selectionHighlight = 97,
selectOnLineNumbers = 98,
showFoldingControls = 99,
showUnused = 100,
snippetSuggestions = 101,
smartSelect = 102,
smoothScrolling = 103,
stickyTabStops = 104,
stopRenderingLineAfter = 105,
suggest = 106,
suggestFontSize = 107,
suggestLineHeight = 108,
suggestOnTriggerCharacters = 109,
suggestSelection = 110,
tabCompletion = 111,
tabIndex = 112,
unusualLineTerminators = 113,
useShadowDOM = 114,
useTabStops = 115,
wordSeparators = 116,
wordWrap = 117,
wordWrapBreakAfterCharacters = 118,
wordWrapBreakBeforeCharacters = 119,
wordWrapColumn = 120,
wordWrapOverride1 = 121,
wordWrapOverride2 = 122,
wrappingIndent = 123,
wrappingStrategy = 124,
showDeprecated = 125,
inlayHints = 126,
editorClassName = 127,
pixelRatio = 128,
tabFocusMode = 129,
layoutInfo = 130,
wrappingInfo = 131
}
/**

View File

@@ -30,7 +30,7 @@ export const editorActiveLineNumber = registerColor('editorLineNumber.activeFore
export const editorRuler = registerColor('editorRuler.foreground', { dark: '#5A5A5A', light: Color.lightgrey, hc: Color.white }, nls.localize('editorRuler', 'Color of the editor rulers.'));
export const editorCodeLensForeground = registerColor('editorCodeLens.foreground', { dark: '#999999', light: '#999999', hc: '#999999' }, nls.localize('editorCodeLensForeground', 'Foreground color of editor CodeLens'));
export const editorCodeLensForeground = registerColor('editorCodeLens.foreground', { dark: '#999999', light: '#919191', hc: '#999999' }, nls.localize('editorCodeLensForeground', 'Foreground color of editor CodeLens'));
export const editorBracketMatchBackground = registerColor('editorBracketMatch.background', { dark: '#0064001a', light: '#0064001a', hc: '#0064001a' }, nls.localize('editorBracketMatchBackground', 'Background color behind matching brackets'));
export const editorBracketMatchBorder = registerColor('editorBracketMatch.border', { dark: '#888', light: '#B9B9B9', hc: contrastBorder }, nls.localize('editorBracketMatchBorder', 'Color for matching brackets boxes'));
@@ -52,6 +52,15 @@ export const overviewRulerError = registerColor('editorOverviewRuler.errorForegr
export const overviewRulerWarning = registerColor('editorOverviewRuler.warningForeground', { dark: editorWarningForeground, light: editorWarningForeground, hc: editorWarningBorder }, nls.localize('overviewRuleWarning', 'Overview ruler marker color for warnings.'));
export const overviewRulerInfo = registerColor('editorOverviewRuler.infoForeground', { dark: editorInfoForeground, light: editorInfoForeground, hc: editorInfoBorder }, nls.localize('overviewRuleInfo', 'Overview ruler marker color for infos.'));
export const editorBracketHighlightingForeground1 = registerColor('editorBracketHighlight.foreground1', { dark: '#FFD700', light: '#0431FAFF', hc: '#FFD700' }, nls.localize('editorBracketHighlightForeground1', 'Foreground color of brackets (1).'));
export const editorBracketHighlightingForeground2 = registerColor('editorBracketHighlight.foreground2', { dark: '#DA70D6', light: '#319331FF', hc: '#DA70D6' }, nls.localize('editorBracketHighlightForeground2', 'Foreground color of brackets (2).'));
export const editorBracketHighlightingForeground3 = registerColor('editorBracketHighlight.foreground3', { dark: '#179FFF', light: '#7B3814FF', hc: '#87CEFA' }, nls.localize('editorBracketHighlightForeground3', 'Foreground color of brackets (3).'));
export const editorBracketHighlightingForeground4 = registerColor('editorBracketHighlight.foreground4', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketHighlightForeground4', 'Foreground color of brackets (4).'));
export const editorBracketHighlightingForeground5 = registerColor('editorBracketHighlight.foreground5', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketHighlightForeground5', 'Foreground color of brackets (5).'));
export const editorBracketHighlightingForeground6 = registerColor('editorBracketHighlight.foreground6', { dark: '#00000000', light: '#00000000', hc: '#00000000' }, nls.localize('editorBracketHighlightForeground6', 'Foreground color of brackets (6).'));
export const editorBracketHighlightingUnexpectedBracketForeground = registerColor('editorBracketHighlight.unexpectedBracket.foreground', { dark: new Color(new RGBA(255, 18, 18, 0.8)), light: new Color(new RGBA(255, 18, 18, 0.8)), hc: new Color(new RGBA(255, 50, 50, 1)) }, nls.localize('editorBracketHighlightUnexpectedBracketForeground', 'Foreground color of unexpected brackets.'));
// contains all color rules that used to defined in editor/browser/widget/editor.css
registerThemingParticipant((theme, collector) => {
const background = theme.getColor(editorBackground);

View File

@@ -8,7 +8,7 @@ const enum Constants {
}
export class ColorZone {
_colorZoneBrand: void;
_colorZoneBrand: void = undefined;
public readonly from: number;
public readonly to: number;
@@ -35,7 +35,7 @@ export class ColorZone {
* A zone in the overview ruler
*/
export class OverviewRulerZone {
_overviewRulerZoneBrand: void;
_overviewRulerZoneBrand: void = undefined;
public readonly startLineNumber: number;
public readonly endLineNumber: number;

View File

@@ -14,7 +14,7 @@ export interface IViewLines {
}
export abstract class RestrictedRenderingContext {
_restrictedRenderingContextBrand: void;
_restrictedRenderingContextBrand: void = undefined;
public readonly viewportData: ViewportData;
@@ -64,7 +64,7 @@ export abstract class RestrictedRenderingContext {
}
export class RenderingContext extends RestrictedRenderingContext {
_renderingContextBrand: void;
_renderingContextBrand: void = undefined;
private readonly _viewLines: IViewLines;

View File

@@ -9,7 +9,7 @@ import { InlineDecoration, InlineDecorationType } from 'vs/editor/common/viewMod
import { LinePartMetadata } from 'vs/editor/common/viewLayout/viewLineRenderer';
export class LineDecoration {
_lineDecorationBrand: void;
_lineDecorationBrand: void = undefined;
constructor(
public readonly startColumn: number,
@@ -96,23 +96,24 @@ export class LineDecoration {
}
public static compare(a: LineDecoration, b: LineDecoration): number {
if (a.startColumn === b.startColumn) {
if (a.endColumn === b.endColumn) {
const typeCmp = LineDecoration._typeCompare(a.type, b.type);
if (typeCmp === 0) {
if (a.className < b.className) {
return -1;
}
if (a.className > b.className) {
return 1;
}
return 0;
}
return typeCmp;
}
if (a.startColumn !== b.startColumn) {
return a.startColumn - b.startColumn;
}
if (a.endColumn !== b.endColumn) {
return a.endColumn - b.endColumn;
}
return a.startColumn - b.startColumn;
const typeCmp = LineDecoration._typeCompare(a.type, b.type);
if (typeCmp !== 0) {
return typeCmp;
}
if (a.className !== b.className) {
return a.className < b.className ? -1 : 1;
}
return 0;
}
}

View File

@@ -29,7 +29,7 @@ export const enum LinePartMetadata {
}
class LinePart {
_linePartBrand: void;
_linePartBrand: void = undefined;
/**
* last char index of this token (not inclusive).
@@ -357,7 +357,7 @@ export const enum ForeignElementType {
}
export class RenderLineOutput {
_renderLineOutputBrand: void;
_renderLineOutputBrand: void = undefined;
readonly characterMapping: CharacterMapping;
readonly containsRTL: boolean;
@@ -738,6 +738,8 @@ function _applyRenderWhitespace(input: RenderLineInput, lineContent: string, len
if (tokenIndex < tokensLength) {
tokenType = tokens[tokenIndex].type;
tokenEndIndex = tokens[tokenIndex].endIndex;
} else {
break;
}
}
}

View File

@@ -4,14 +4,15 @@
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, markAsSingleton } from 'vs/base/common/lifecycle';
import { RGBA8 } from 'vs/editor/common/core/rgba';
import { ColorId, TokenizationRegistry } from 'vs/editor/common/modes';
export class MinimapTokensColorTracker {
export class MinimapTokensColorTracker extends Disposable {
private static _INSTANCE: MinimapTokensColorTracker | null = null;
public static getInstance(): MinimapTokensColorTracker {
if (!this._INSTANCE) {
this._INSTANCE = new MinimapTokensColorTracker();
this._INSTANCE = markAsSingleton(new MinimapTokensColorTracker());
}
return this._INSTANCE;
}
@@ -23,12 +24,13 @@ export class MinimapTokensColorTracker {
public readonly onDidChange: Event<void> = this._onDidChange.event;
private constructor() {
super();
this._updateColorMap();
TokenizationRegistry.onDidChange(e => {
this._register(TokenizationRegistry.onDidChange(e => {
if (e.changedColorMap) {
this._updateColorMap();
}
});
}));
}
private _updateColorMap(): void {

View File

@@ -10,6 +10,8 @@ import { CharacterClassifier } from 'vs/editor/common/core/characterClassifier';
import { ILineBreaksComputerFactory } from 'vs/editor/common/viewModel/splitLinesCollection';
import { FontInfo } from 'vs/editor/common/config/fontInfo';
import { ILineBreaksComputer, LineBreakData } from 'vs/editor/common/viewModel/viewModel';
import { LineInjectedText } from 'vs/editor/common/model/textModelEvents';
import { InjectedTextOptions } from 'vs/editor/common/model';
const enum CharacterClass {
NONE = 0,
@@ -75,22 +77,25 @@ export class MonospaceLineBreaksComputerFactory implements ILineBreaksComputerFa
tabSize = tabSize | 0; //@perf
wrappingColumn = +wrappingColumn; //@perf
let requests: string[] = [];
let previousBreakingData: (LineBreakData | null)[] = [];
const requests: string[] = [];
const injectedTexts: (LineInjectedText[] | null)[] = [];
const previousBreakingData: (LineBreakData | null)[] = [];
return {
addRequest: (lineText: string, previousLineBreakData: LineBreakData | null) => {
addRequest: (lineText: string, injectedText: LineInjectedText[] | null, previousLineBreakData: LineBreakData | null) => {
requests.push(lineText);
injectedTexts.push(injectedText);
previousBreakingData.push(previousLineBreakData);
},
finalize: () => {
const columnsForFullWidthChar = fontInfo.typicalFullwidthCharacterWidth / fontInfo.typicalHalfwidthCharacterWidth; //@perf
let result: (LineBreakData | null)[] = [];
for (let i = 0, len = requests.length; i < len; i++) {
const injectedText = injectedTexts[i];
const previousLineBreakData = previousBreakingData[i];
if (previousLineBreakData) {
if (previousLineBreakData && !previousLineBreakData.injectionOptions && !injectedText) {
result[i] = createLineBreaksFromPreviousLineBreaks(this.classifier, previousLineBreakData, requests[i], tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent);
} else {
result[i] = createLineBreaks(this.classifier, requests[i], tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent);
result[i] = createLineBreaks(this.classifier, requests[i], injectedText, tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent);
}
}
arrPool1.length = 0;
@@ -353,14 +358,36 @@ function createLineBreaksFromPreviousLineBreaks(classifier: WrappingCharacterCla
return previousBreakingData;
}
function createLineBreaks(classifier: WrappingCharacterClassifier, lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent): LineBreakData | null {
function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: string, injectedTexts: LineInjectedText[] | null, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent): LineBreakData | null {
const lineText = LineInjectedText.applyInjectedText(_lineText, injectedTexts);
let injectionOptions: InjectedTextOptions[] | null;
let injectionOffsets: number[] | null;
if (injectedTexts && injectedTexts.length > 0) {
injectionOptions = injectedTexts.map(t => t.options);
injectionOffsets = injectedTexts.map(text => text.column - 1);
} else {
injectionOptions = null;
injectionOffsets = null;
}
if (firstLineBreakColumn === -1) {
return null;
if (!injectionOptions) {
return null;
}
// creating a `LineBreakData` with an invalid `breakOffsetsVisibleColumn` is OK
// because `breakOffsetsVisibleColumn` will never be used because it contains injected text
return new LineBreakData([lineText.length], [], 0, injectionOffsets, injectionOptions);
}
const len = lineText.length;
if (len <= 1) {
return null;
if (!injectionOptions) {
return null;
}
// creating a `LineBreakData` with an invalid `breakOffsetsVisibleColumn` is OK
// because `breakOffsetsVisibleColumn` will never be used because it contains injected text
return new LineBreakData([lineText.length], [], 0, injectionOffsets, injectionOptions);
}
const wrappedTextIndentLength = computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent);
@@ -430,7 +457,7 @@ function createLineBreaks(classifier: WrappingCharacterClassifier, lineText: str
prevCharCodeClass = charCodeClass;
}
if (breakingOffsetsCount === 0) {
if (breakingOffsetsCount === 0 && (!injectedTexts || injectedTexts.length === 0)) {
return null;
}
@@ -438,7 +465,7 @@ function createLineBreaks(classifier: WrappingCharacterClassifier, lineText: str
breakingOffsets[breakingOffsetsCount] = len;
breakingOffsetsVisibleColumn[breakingOffsetsCount] = visibleColumn;
return new LineBreakData(breakingOffsets, breakingOffsetsVisibleColumn, wrappedTextIndentLength);
return new LineBreakData(breakingOffsets, breakingOffsetsVisibleColumn, wrappedTextIndentLength, injectionOffsets, injectionOptions);
}
function computeCharWidth(charCode: number, visibleColumn: number, tabSize: number, columnsForFullWidthChar: number): number {

View File

@@ -6,7 +6,7 @@
import { toUint32 } from 'vs/base/common/uint';
export class PrefixSumIndexOfResult {
_prefixSumIndexOfResultBrand: void;
_prefixSumIndexOfResultBrand: void = undefined;
index: number;
remainder: number;
@@ -85,9 +85,9 @@ export class PrefixSumComputer {
return true;
}
public removeValues(startIndex: number, cnt: number): boolean {
public removeValues(startIndex: number, count: number): boolean {
startIndex = toUint32(startIndex);
cnt = toUint32(cnt);
count = toUint32(count);
const oldValues = this.values;
const oldPrefixSum = this.prefixSum;
@@ -96,18 +96,18 @@ export class PrefixSumComputer {
return false;
}
let maxCnt = oldValues.length - startIndex;
if (cnt >= maxCnt) {
cnt = maxCnt;
let maxCount = oldValues.length - startIndex;
if (count >= maxCount) {
count = maxCount;
}
if (cnt === 0) {
if (count === 0) {
return false;
}
this.values = new Uint32Array(oldValues.length - cnt);
this.values = new Uint32Array(oldValues.length - count);
this.values.set(oldValues.subarray(0, startIndex), 0);
this.values.set(oldValues.subarray(startIndex + cnt), startIndex);
this.values.set(oldValues.subarray(startIndex + count), startIndex);
this.prefixSum = new Uint32Array(this.values.length);
if (startIndex - 1 < this.prefixSumValidIndex[0]) {
@@ -119,23 +119,23 @@ export class PrefixSumComputer {
return true;
}
public getTotalValue(): number {
public getTotalSum(): number {
if (this.values.length === 0) {
return 0;
}
return this._getAccumulatedValue(this.values.length - 1);
return this._getPrefixSum(this.values.length - 1);
}
public getAccumulatedValue(index: number): number {
public getPrefixSum(index: number): number {
if (index < 0) {
return 0;
}
index = toUint32(index);
return this._getAccumulatedValue(index);
return this._getPrefixSum(index);
}
private _getAccumulatedValue(index: number): number {
private _getPrefixSum(index: number): number {
if (index <= this.prefixSumValidIndex[0]) {
return this.prefixSum[index];
}
@@ -157,11 +157,11 @@ export class PrefixSumComputer {
return this.prefixSum[index];
}
public getIndexOf(accumulatedValue: number): PrefixSumIndexOfResult {
accumulatedValue = Math.floor(accumulatedValue); //@perf
public getIndexOf(sum: number): PrefixSumIndexOfResult {
sum = Math.floor(sum); //@perf
// Compute all sums (to get a fully valid prefixSum)
this.getTotalValue();
this.getTotalSum();
let low = 0;
let high = this.values.length - 1;
@@ -175,15 +175,15 @@ export class PrefixSumComputer {
midStop = this.prefixSum[mid];
midStart = midStop - this.values[mid];
if (accumulatedValue < midStart) {
if (sum < midStart) {
high = mid - 1;
} else if (accumulatedValue >= midStop) {
} else if (sum >= midStop) {
low = mid + 1;
} else {
break;
}
}
return new PrefixSumIndexOfResult(mid, accumulatedValue - midStart);
return new PrefixSumIndexOfResult(mid, sum - midStart);
}
}

View File

@@ -5,17 +5,18 @@
import * as arrays from 'vs/base/common/arrays';
import { WrappingIndent } from 'vs/editor/common/config/editorOptions';
import { LineTokens } from 'vs/editor/common/core/lineTokens';
import { IViewLineTokens, LineTokens } from 'vs/editor/common/core/lineTokens';
import { Position } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
import { EndOfLinePreference, IActiveIndentGuideInfo, IModelDecoration, IModelDeltaDecoration, ITextModel, PositionNormalizationAffinity } from 'vs/editor/common/model';
import { EndOfLinePreference, IActiveIndentGuideInfo, IModelDecoration, IModelDeltaDecoration, ITextModel, PositionAffinity } from 'vs/editor/common/model';
import { ModelDecorationOptions, ModelDecorationOverviewRulerOptions } from 'vs/editor/common/model/textModel';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
import { PrefixSumIndexOfResult } from 'vs/editor/common/viewModel/prefixSumComputer';
import { ICoordinatesConverter, ILineBreaksComputer, IOverviewRulerDecorations, LineBreakData, ViewLineData } from 'vs/editor/common/viewModel/viewModel';
import { ICoordinatesConverter, InjectedText, ILineBreaksComputer, IOverviewRulerDecorations, LineBreakData, SingleLineInlineDecoration, ViewLineData } from 'vs/editor/common/viewModel/viewModel';
import { IDisposable } from 'vs/base/common/lifecycle';
import { FontInfo } from 'vs/editor/common/config/fontInfo';
import { EditorTheme } from 'vs/editor/common/view/viewContext';
import { LineInjectedText } from 'vs/editor/common/model/textModelEvents';
export interface ILineBreaksComputerFactory {
createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent): ILineBreaksComputer;
@@ -44,9 +45,11 @@ export interface ISplitLine {
getViewLinesData(model: ISimpleModel, modelLineNumber: number, fromOuputLineIndex: number, toOutputLineIndex: number, globalStartIndex: number, needed: boolean[], result: Array<ViewLineData | null>): void;
getModelColumnOfViewPosition(outputLineIndex: number, outputColumn: number): number;
getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number): Position;
getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number, affinity?: PositionAffinity): Position;
getViewLineNumberOfModelPosition(deltaLineNumber: number, inputColumn: number): number;
normalizePosition(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number, outputPosition: Position, affinity: PositionNormalizationAffinity): Position;
normalizePosition(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number, outputPosition: Position, affinity: PositionAffinity): Position;
getInjectedTextAt(outputLineIndex: number, column: number): InjectedText | null;
}
export interface IViewModelLinesCollection extends IDisposable {
@@ -59,9 +62,9 @@ export interface IViewModelLinesCollection extends IDisposable {
createLineBreaksComputer(): ILineBreaksComputer;
onModelFlushed(): void;
onModelLinesDeleted(versionId: number, fromLineNumber: number, toLineNumber: number): viewEvents.ViewLinesDeletedEvent | null;
onModelLinesInserted(versionId: number, fromLineNumber: number, toLineNumber: number, lineBreaks: (LineBreakData | null)[]): viewEvents.ViewLinesInsertedEvent | null;
onModelLineChanged(versionId: number, lineNumber: number, lineBreakData: LineBreakData | null): [boolean, viewEvents.ViewLinesChangedEvent | null, viewEvents.ViewLinesInsertedEvent | null, viewEvents.ViewLinesDeletedEvent | null];
onModelLinesDeleted(versionId: number | null, fromLineNumber: number, toLineNumber: number): viewEvents.ViewLinesDeletedEvent | null;
onModelLinesInserted(versionId: number | null, fromLineNumber: number, toLineNumber: number, lineBreaks: (LineBreakData | null)[]): viewEvents.ViewLinesInsertedEvent | null;
onModelLineChanged(versionId: number | null, lineNumber: number, lineBreakData: LineBreakData | null): [boolean, viewEvents.ViewLinesChangedEvent | null, viewEvents.ViewLinesInsertedEvent | null, viewEvents.ViewLinesDeletedEvent | null];
acceptVersionId(versionId: number): void;
getViewLineCount(): number;
@@ -77,7 +80,9 @@ export interface IViewModelLinesCollection extends IDisposable {
getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: EditorTheme): IOverviewRulerDecorations;
getDecorationsInRange(range: Range, ownerId: number, filterOutValidation: boolean): IModelDecoration[];
normalizePosition(position: Position, affinity: PositionNormalizationAffinity): Position;
getInjectedTextAt(viewPosition: Position): InjectedText | null;
normalizePosition(position: Position, affinity: PositionAffinity): Position;
/**
* Gets the column at which indentation stops at a given line.
* @internal
@@ -113,12 +118,12 @@ export class CoordinatesConverter implements ICoordinatesConverter {
// Model -> View conversion and related methods
public convertModelPositionToViewPosition(modelPosition: Position): Position {
return this._lines.convertModelPositionToViewPosition(modelPosition.lineNumber, modelPosition.column);
public convertModelPositionToViewPosition(modelPosition: Position, affinity?: PositionAffinity): Position {
return this._lines.convertModelPositionToViewPosition(modelPosition.lineNumber, modelPosition.column, affinity);
}
public convertModelRangeToViewRange(modelRange: Range): Range {
return this._lines.convertModelRangeToViewRange(modelRange);
public convertModelRangeToViewRange(modelRange: Range, affinity?: PositionAffinity): Range {
return this._lines.convertModelRangeToViewRange(modelRange, affinity);
}
public modelPositionIsVisible(modelPosition: Position): boolean {
@@ -221,6 +226,7 @@ class LineNumberMapper {
export class SplitLinesCollection implements IViewModelLinesCollection {
private readonly _editorId: number;
private readonly model: ITextModel;
private _validModelVersionId: number;
@@ -239,6 +245,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
private hiddenAreasIds!: string[];
constructor(
editorId: number,
model: ITextModel,
domLineBreaksComputerFactory: ILineBreaksComputerFactory,
monospaceLineBreaksComputerFactory: ILineBreaksComputerFactory,
@@ -248,6 +255,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
wrappingColumn: number,
wrappingIndent: WrappingIndent,
) {
this._editorId = editorId;
this.model = model;
this._validModelVersionId = -1;
this._domLineBreaksComputerFactory = domLineBreaksComputerFactory;
@@ -276,11 +284,15 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
this.hiddenAreasIds = [];
}
let linesContent = this.model.getLinesContent();
const linesContent = this.model.getLinesContent();
const injectedTextDecorations = this.model.getInjectedTextDecorations(this._editorId);
const lineCount = linesContent.length;
const lineBreaksComputer = this.createLineBreaksComputer();
const injectedTextQueue = new arrays.ArrayQueue(LineInjectedText.fromDecorations(injectedTextDecorations));
for (let i = 0; i < lineCount; i++) {
lineBreaksComputer.addRequest(linesContent[i], previousLineBreaks ? previousLineBreaks[i] : null);
const lineInjectedText = injectedTextQueue.takeWhile(t => t.lineNumber === i + 1);
lineBreaksComputer.addRequest(linesContent[i], lineInjectedText, previousLineBreaks ? previousLineBreaks[i] : null);
}
const linesBreaks = lineBreaksComputer.finalize();
@@ -488,8 +500,8 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
this._constructLines(/*resetHiddenAreas*/true, null);
}
public onModelLinesDeleted(versionId: number, fromLineNumber: number, toLineNumber: number): viewEvents.ViewLinesDeletedEvent | null {
if (versionId <= this._validModelVersionId) {
public onModelLinesDeleted(versionId: number | null, fromLineNumber: number, toLineNumber: number): viewEvents.ViewLinesDeletedEvent | null {
if (!versionId || versionId <= this._validModelVersionId) {
// Here we check for versionId in case the lines were reconstructed in the meantime.
// We don't want to apply stale change events on top of a newer read model state.
return null;
@@ -504,8 +516,8 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
return new viewEvents.ViewLinesDeletedEvent(outputFromLineNumber, outputToLineNumber);
}
public onModelLinesInserted(versionId: number, fromLineNumber: number, _toLineNumber: number, lineBreaks: (LineBreakData | null)[]): viewEvents.ViewLinesInsertedEvent | null {
if (versionId <= this._validModelVersionId) {
public onModelLinesInserted(versionId: number | null, fromLineNumber: number, _toLineNumber: number, lineBreaks: (LineBreakData | null)[]): viewEvents.ViewLinesInsertedEvent | null {
if (!versionId || versionId <= this._validModelVersionId) {
// Here we check for versionId in case the lines were reconstructed in the meantime.
// We don't want to apply stale change events on top of a newer read model state.
return null;
@@ -537,8 +549,8 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
return new viewEvents.ViewLinesInsertedEvent(outputFromLineNumber, outputFromLineNumber + totalOutputLineCount - 1);
}
public onModelLineChanged(versionId: number, lineNumber: number, lineBreakData: LineBreakData | null): [boolean, viewEvents.ViewLinesChangedEvent | null, viewEvents.ViewLinesInsertedEvent | null, viewEvents.ViewLinesDeletedEvent | null] {
if (versionId <= this._validModelVersionId) {
public onModelLineChanged(versionId: number | null, lineNumber: number, lineBreakData: LineBreakData | null): [boolean, viewEvents.ViewLinesChangedEvent | null, viewEvents.ViewLinesInsertedEvent | null, viewEvents.ViewLinesDeletedEvent | null] {
if (versionId !== null && versionId <= this._validModelVersionId) {
// Here we check for versionId in case the lines were reconstructed in the meantime.
// We don't want to apply stale change events on top of a newer read model state.
return [false, null, null, null];
@@ -833,7 +845,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
return new Range(start.lineNumber, start.column, end.lineNumber, end.column);
}
public convertModelPositionToViewPosition(_modelLineNumber: number, _modelColumn: number): Position {
public convertModelPositionToViewPosition(_modelLineNumber: number, _modelColumn: number, affinity: PositionAffinity = PositionAffinity.None): Position {
const validPosition = this.model.validatePosition(new Position(_modelLineNumber, _modelColumn));
const inputLineNumber = validPosition.lineNumber;
@@ -853,26 +865,27 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
let r: Position;
if (lineIndexChanged) {
r = this.lines[lineIndex].getViewPositionOfModelPosition(deltaLineNumber, this.model.getLineMaxColumn(lineIndex + 1));
r = this.lines[lineIndex].getViewPositionOfModelPosition(deltaLineNumber, this.model.getLineMaxColumn(lineIndex + 1), affinity);
} else {
r = this.lines[inputLineNumber - 1].getViewPositionOfModelPosition(deltaLineNumber, inputColumn);
r = this.lines[inputLineNumber - 1].getViewPositionOfModelPosition(deltaLineNumber, inputColumn, affinity);
}
// console.log('in -> out ' + inputLineNumber + ',' + inputColumn + ' ===> ' + r.lineNumber + ',' + r);
return r;
}
public convertModelRangeToViewRange(modelRange: Range): Range {
let start = this.convertModelPositionToViewPosition(modelRange.startLineNumber, modelRange.startColumn);
let end = this.convertModelPositionToViewPosition(modelRange.endLineNumber, modelRange.endColumn);
if (modelRange.startLineNumber === modelRange.endLineNumber && start.lineNumber !== end.lineNumber) {
// This is a single line range that ends up taking more lines due to wrapping
if (end.column === this.getViewLineMinColumn(end.lineNumber)) {
// the end column lands on the first column of the next line
return new Range(start.lineNumber, start.column, end.lineNumber - 1, this.getViewLineMaxColumn(end.lineNumber - 1));
}
/**
* @param affinity The affinity in case of an empty range. Has no effect for non-empty ranges.
*/
public convertModelRangeToViewRange(modelRange: Range, affinity: PositionAffinity = PositionAffinity.Left): Range {
if (modelRange.isEmpty()) {
const start = this.convertModelPositionToViewPosition(modelRange.startLineNumber, modelRange.startColumn, affinity);
return Range.fromPositions(start);
} else {
const start = this.convertModelPositionToViewPosition(modelRange.startLineNumber, modelRange.startColumn, PositionAffinity.Right);
const end = this.convertModelPositionToViewPosition(modelRange.endLineNumber, modelRange.endColumn, PositionAffinity.Left);
return new Range(start.lineNumber, start.column, end.lineNumber, end.column);
}
return new Range(start.lineNumber, start.column, end.lineNumber, end.column);
}
private _getViewLineNumberForModelPosition(inputLineNumber: number, inputColumn: number): number {
@@ -980,7 +993,16 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
return finalResult;
}
normalizePosition(position: Position, affinity: PositionNormalizationAffinity): Position {
public getInjectedTextAt(position: Position): InjectedText | null {
const viewLineNumber = this._toValidViewLineNumber(position.lineNumber);
const r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1);
const lineIndex = r.index;
const remainder = r.remainder;
return this.lines[lineIndex].getInjectedTextAt(remainder, position.column);
}
normalizePosition(position: Position, affinity: PositionAffinity): Position {
const viewLineNumber = this._toValidViewLineNumber(position.lineNumber);
const r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1);
const lineIndex = r.index;
@@ -1056,7 +1078,8 @@ class VisibleIdentitySplitLine implements ISplitLine {
1,
lineContent.length + 1,
0,
lineTokens.inflate()
lineTokens.inflate(),
null
);
}
@@ -1080,9 +1103,13 @@ class VisibleIdentitySplitLine implements ISplitLine {
return deltaLineNumber;
}
public normalizePosition(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number, outputPosition: Position, affinity: PositionNormalizationAffinity): Position {
public normalizePosition(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number, outputPosition: Position, affinity: PositionAffinity): Position {
return outputPosition;
}
public getInjectedTextAt(_outputLineIndex: number, _outputColumn: number): InjectedText | null {
return null;
}
}
class InvisibleIdentitySplitLine implements ISplitLine {
@@ -1146,7 +1173,11 @@ class InvisibleIdentitySplitLine implements ISplitLine {
throw new Error('Not supported');
}
public normalizePosition(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number, outputPosition: Position, affinity: PositionNormalizationAffinity): Position {
public normalizePosition(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number, outputPosition: Position, affinity: PositionAffinity): Position {
throw new Error('Not supported');
}
public getInjectedTextAt(_outputLineIndex: number, _outputColumn: number): InjectedText | null {
throw new Error('Not supported');
}
}
@@ -1182,28 +1213,40 @@ export class SplitLine implements ISplitLine {
}
private getInputStartOffsetOfOutputLineIndex(outputLineIndex: number): number {
return LineBreakData.getInputOffsetOfOutputPosition(this._lineBreakData.breakOffsets, outputLineIndex, 0);
return this._lineBreakData.getInputOffsetOfOutputPosition(outputLineIndex, 0);
}
private getInputEndOffsetOfOutputLineIndex(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number {
if (outputLineIndex + 1 === this._lineBreakData.breakOffsets.length) {
return model.getLineMaxColumn(modelLineNumber) - 1;
}
return LineBreakData.getInputOffsetOfOutputPosition(this._lineBreakData.breakOffsets, outputLineIndex + 1, 0);
return this._lineBreakData.getInputOffsetOfOutputPosition(outputLineIndex + 1, 0);
}
public getViewLineContent(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): string {
if (!this._isVisible) {
throw new Error('Not supported');
}
let startOffset = this.getInputStartOffsetOfOutputLineIndex(outputLineIndex);
let endOffset = this.getInputEndOffsetOfOutputLineIndex(model, modelLineNumber, outputLineIndex);
let r = model.getValueInRange({
startLineNumber: modelLineNumber,
startColumn: startOffset + 1,
endLineNumber: modelLineNumber,
endColumn: endOffset + 1
});
// These offsets refer to model text with injected text.
const startOffset = outputLineIndex > 0 ? this._lineBreakData.breakOffsets[outputLineIndex - 1] : 0;
const endOffset = outputLineIndex < this._lineBreakData.breakOffsets.length
? this._lineBreakData.breakOffsets[outputLineIndex]
// This case might not be possible anyway, but we clamp the value to be on the safe side.
: this._lineBreakData.breakOffsets[this._lineBreakData.breakOffsets.length - 1];
let r: string;
if (this._lineBreakData.injectionOffsets !== null) {
const injectedTexts = this._lineBreakData.injectionOffsets.map((offset, idx) => new LineInjectedText(0, 0, offset + 1, this._lineBreakData.injectionOptions![idx], 0));
r = LineInjectedText.applyInjectedText(model.getLineContent(modelLineNumber), injectedTexts).substring(startOffset, endOffset);
} else {
r = model.getValueInRange({
startLineNumber: modelLineNumber,
startColumn: startOffset + 1,
endLineNumber: modelLineNumber,
endColumn: endOffset + 1
});
}
if (outputLineIndex > 0) {
r = spaces(this._lineBreakData.wrappedTextIndentLength) + r;
@@ -1213,11 +1256,18 @@ export class SplitLine implements ISplitLine {
}
public getViewLineLength(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number {
// TODO @hediet make this method a member of LineBreakData.
if (!this._isVisible) {
throw new Error('Not supported');
}
let startOffset = this.getInputStartOffsetOfOutputLineIndex(outputLineIndex);
let endOffset = this.getInputEndOffsetOfOutputLineIndex(model, modelLineNumber, outputLineIndex);
// These offsets refer to model text with injected text.
const startOffset = outputLineIndex > 0 ? this._lineBreakData.breakOffsets[outputLineIndex - 1] : 0;
const endOffset = outputLineIndex < this._lineBreakData.breakOffsets.length
? this._lineBreakData.breakOffsets[outputLineIndex]
// This case might not be possible anyway, but we clamp the value to be on the safe side.
: this._lineBreakData.breakOffsets[this._lineBreakData.breakOffsets.length - 1];
let r = endOffset - startOffset;
if (outputLineIndex > 0) {
@@ -1252,33 +1302,77 @@ export class SplitLine implements ISplitLine {
if (!this._isVisible) {
throw new Error('Not supported');
}
const lineBreakData = this._lineBreakData;
const deltaStartIndex = (outputLineIndex > 0 ? lineBreakData.wrappedTextIndentLength : 0);
let startOffset = this.getInputStartOffsetOfOutputLineIndex(outputLineIndex);
let endOffset = this.getInputEndOffsetOfOutputLineIndex(model, modelLineNumber, outputLineIndex);
const injectionOffsets = lineBreakData.injectionOffsets;
const injectionOptions = lineBreakData.injectionOptions;
let lineContent = model.getValueInRange({
startLineNumber: modelLineNumber,
startColumn: startOffset + 1,
endLineNumber: modelLineNumber,
endColumn: endOffset + 1
});
let lineContent: string;
let tokens: IViewLineTokens;
let inlineDecorations: null | SingleLineInlineDecoration[];
if (injectionOffsets) {
const lineTokens = model.getLineTokens(modelLineNumber).withInserted(injectionOffsets.map((offset, idx) => ({
offset,
text: injectionOptions![idx].content,
tokenMetadata: LineTokens.defaultTokenMetadata
})));
if (outputLineIndex > 0) {
lineContent = spaces(this._lineBreakData.wrappedTextIndentLength) + lineContent;
const lineStartOffsetInUnwrappedLine = outputLineIndex > 0 ? lineBreakData.breakOffsets[outputLineIndex - 1] : 0;
const lineEndOffsetInUnwrappedLine = lineBreakData.breakOffsets[outputLineIndex];
lineContent = lineTokens.getLineContent().substring(lineStartOffsetInUnwrappedLine, lineEndOffsetInUnwrappedLine);
tokens = lineTokens.sliceAndInflate(lineStartOffsetInUnwrappedLine, lineEndOffsetInUnwrappedLine, deltaStartIndex);
inlineDecorations = new Array<SingleLineInlineDecoration>();
let totalInjectedTextLengthBefore = 0;
for (let i = 0; i < injectionOffsets.length; i++) {
const length = injectionOptions![i].content.length;
const injectedTextStartOffsetInUnwrappedLine = injectionOffsets[i] + totalInjectedTextLengthBefore;
const injectedTextEndOffsetInUnwrappedLine = injectionOffsets[i] + totalInjectedTextLengthBefore + length;
if (injectedTextStartOffsetInUnwrappedLine > lineEndOffsetInUnwrappedLine) {
// Injected text only starts in later wrapped lines.
break;
}
if (lineStartOffsetInUnwrappedLine < injectedTextEndOffsetInUnwrappedLine) {
// Injected text ends after or in this line (but also starts in or before this line).
const options = injectionOptions![i];
if (options.inlineClassName) {
const offset = (outputLineIndex > 0 ? lineBreakData.wrappedTextIndentLength : 0);
const start = offset + Math.max(injectedTextStartOffsetInUnwrappedLine - lineStartOffsetInUnwrappedLine, 0);
const end = offset + Math.min(injectedTextEndOffsetInUnwrappedLine - lineStartOffsetInUnwrappedLine, lineEndOffsetInUnwrappedLine);
if (start !== end) {
inlineDecorations.push(new SingleLineInlineDecoration(start, end, options.inlineClassName, options.inlineClassNameAffectsLetterSpacing!));
}
}
}
totalInjectedTextLengthBefore += length;
}
} else {
const startOffset = this.getInputStartOffsetOfOutputLineIndex(outputLineIndex);
const endOffset = this.getInputEndOffsetOfOutputLineIndex(model, modelLineNumber, outputLineIndex);
const lineTokens = model.getLineTokens(modelLineNumber);
lineContent = model.getValueInRange({
startLineNumber: modelLineNumber,
startColumn: startOffset + 1,
endLineNumber: modelLineNumber,
endColumn: endOffset + 1
});
tokens = lineTokens.sliceAndInflate(startOffset, endOffset, deltaStartIndex);
inlineDecorations = null;
}
let minColumn = (outputLineIndex > 0 ? this._lineBreakData.wrappedTextIndentLength + 1 : 1);
let maxColumn = lineContent.length + 1;
let continuesWithWrappedLine = (outputLineIndex + 1 < this.getViewLineCount());
let deltaStartIndex = 0;
if (outputLineIndex > 0) {
deltaStartIndex = this._lineBreakData.wrappedTextIndentLength;
lineContent = spaces(lineBreakData.wrappedTextIndentLength) + lineContent;
}
let lineTokens = model.getLineTokens(modelLineNumber);
const startVisibleColumn = (outputLineIndex === 0 ? 0 : this._lineBreakData.breakOffsetsVisibleColumn[outputLineIndex - 1]);
const minColumn = (outputLineIndex > 0 ? lineBreakData.wrappedTextIndentLength + 1 : 1);
const maxColumn = lineContent.length + 1;
const continuesWithWrappedLine = (outputLineIndex + 1 < this.getViewLineCount());
const startVisibleColumn = (outputLineIndex === 0 ? 0 : lineBreakData.breakOffsetsVisibleColumn[outputLineIndex - 1]);
return new ViewLineData(
lineContent,
@@ -1286,7 +1380,8 @@ export class SplitLine implements ISplitLine {
minColumn,
maxColumn,
startVisibleColumn,
lineTokens.sliceAndInflate(startOffset, endOffset, deltaStartIndex)
tokens,
inlineDecorations
);
}
@@ -1317,14 +1412,14 @@ export class SplitLine implements ISplitLine {
adjustedColumn -= this._lineBreakData.wrappedTextIndentLength;
}
}
return LineBreakData.getInputOffsetOfOutputPosition(this._lineBreakData.breakOffsets, outputLineIndex, adjustedColumn) + 1;
return this._lineBreakData.getInputOffsetOfOutputPosition(outputLineIndex, adjustedColumn) + 1;
}
public getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number): Position {
public getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number, affinity: PositionAffinity = PositionAffinity.None): Position {
if (!this._isVisible) {
throw new Error('Not supported');
}
let r = LineBreakData.getOutputPositionOfInputOffset(this._lineBreakData.breakOffsets, inputColumn - 1);
let r = this._lineBreakData.getOutputPositionOfInputOffset(inputColumn - 1, affinity);
let outputLineIndex = r.outputLineIndex;
let outputColumn = r.outputOffset + 1;
@@ -1340,24 +1435,39 @@ export class SplitLine implements ISplitLine {
if (!this._isVisible) {
throw new Error('Not supported');
}
const r = LineBreakData.getOutputPositionOfInputOffset(this._lineBreakData.breakOffsets, inputColumn - 1);
const r = this._lineBreakData.getOutputPositionOfInputOffset(inputColumn - 1);
return (deltaLineNumber + r.outputLineIndex);
}
public normalizePosition(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number, outputPosition: Position, affinity: PositionNormalizationAffinity): Position {
if (affinity === PositionNormalizationAffinity.Left) {
public normalizePosition(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number, outputPosition: Position, affinity: PositionAffinity): Position {
if (this._lineBreakData.injectionOffsets !== null) {
const baseViewLineNumber = outputPosition.lineNumber - outputLineIndex;
const offsetInUnwrappedLine = this._lineBreakData.outputPositionToOffsetInUnwrappedLine(outputLineIndex, outputPosition.column - 1);
const normalizedOffsetInUnwrappedLine = this._lineBreakData.normalizeOffsetAroundInjections(offsetInUnwrappedLine, affinity);
if (normalizedOffsetInUnwrappedLine !== offsetInUnwrappedLine) {
// injected text caused a change
return this._lineBreakData.getOutputPositionOfOffsetInUnwrappedLine(normalizedOffsetInUnwrappedLine, affinity).toPosition(baseViewLineNumber, this._lineBreakData.wrappedTextIndentLength);
}
}
if (affinity === PositionAffinity.Left) {
if (outputLineIndex > 0 && outputPosition.column === this._getViewLineMinColumn(outputLineIndex)) {
return new Position(outputPosition.lineNumber - 1, this.getViewLineMaxColumn(model, modelLineNumber, outputLineIndex - 1));
}
}
else if (affinity === PositionNormalizationAffinity.Right) {
else if (affinity === PositionAffinity.Right) {
const maxOutputLineIndex = this.getViewLineCount() - 1;
if (outputLineIndex < maxOutputLineIndex && outputPosition.column === this.getViewLineMaxColumn(model, modelLineNumber, outputLineIndex)) {
return new Position(outputPosition.lineNumber + 1, this._getViewLineMinColumn(outputLineIndex + 1));
}
}
return outputPosition;
}
public getInjectedTextAt(outputLineIndex: number, outputColumn: number): InjectedText | null {
return this._lineBreakData.getInjectedText(outputLineIndex, outputColumn - 1);
}
}
let _spaces: string[] = [''];
@@ -1477,7 +1587,7 @@ export class IdentityLinesCollection implements IViewModelLinesCollection {
public createLineBreaksComputer(): ILineBreaksComputer {
let result: null[] = [];
return {
addRequest: (lineText: string, previousLineBreakData: LineBreakData | null) => {
addRequest: (lineText: string, injectedText: LineInjectedText[] | null, previousLineBreakData: LineBreakData | null) => {
result.push(null);
},
finalize: () => {
@@ -1489,15 +1599,15 @@ export class IdentityLinesCollection implements IViewModelLinesCollection {
public onModelFlushed(): void {
}
public onModelLinesDeleted(_versionId: number, fromLineNumber: number, toLineNumber: number): viewEvents.ViewLinesDeletedEvent | null {
public onModelLinesDeleted(_versionId: number | null, fromLineNumber: number, toLineNumber: number): viewEvents.ViewLinesDeletedEvent | null {
return new viewEvents.ViewLinesDeletedEvent(fromLineNumber, toLineNumber);
}
public onModelLinesInserted(_versionId: number, fromLineNumber: number, toLineNumber: number, lineBreaks: (LineBreakData | null)[]): viewEvents.ViewLinesInsertedEvent | null {
public onModelLinesInserted(_versionId: number | null, fromLineNumber: number, toLineNumber: number, lineBreaks: (LineBreakData | null)[]): viewEvents.ViewLinesInsertedEvent | null {
return new viewEvents.ViewLinesInsertedEvent(fromLineNumber, toLineNumber);
}
public onModelLineChanged(_versionId: number, lineNumber: number, lineBreakData: LineBreakData | null): [boolean, viewEvents.ViewLinesChangedEvent | null, viewEvents.ViewLinesInsertedEvent | null, viewEvents.ViewLinesDeletedEvent | null] {
public onModelLineChanged(_versionId: number | null, lineNumber: number, lineBreakData: LineBreakData | null): [boolean, viewEvents.ViewLinesChangedEvent | null, viewEvents.ViewLinesInsertedEvent | null, viewEvents.ViewLinesDeletedEvent | null] {
return [false, new viewEvents.ViewLinesChangedEvent(lineNumber, lineNumber), null, null];
}
@@ -1550,7 +1660,8 @@ export class IdentityLinesCollection implements IViewModelLinesCollection {
1,
lineContent.length + 1,
0,
lineTokens.inflate()
lineTokens.inflate(),
null
);
}
@@ -1593,13 +1704,18 @@ export class IdentityLinesCollection implements IViewModelLinesCollection {
return this.model.getDecorationsInRange(range, ownerId, filterOutValidation);
}
normalizePosition(position: Position, affinity: PositionNormalizationAffinity): Position {
normalizePosition(position: Position, affinity: PositionAffinity): Position {
return this.model.normalizePosition(position, affinity);
}
public getLineIndentColumn(lineNumber: number): number {
return this.model.getLineIndentColumn(lineNumber);
}
public getInjectedTextAt(position: Position): InjectedText | null {
// Identity lines collection does not support injected text.
return null;
}
}
class OverviewRulerDecorations {

View File

@@ -9,7 +9,7 @@ import { IViewLineTokens } from 'vs/editor/common/core/lineTokens';
import { IPosition, Position } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
import { INewScrollPosition, ScrollType } from 'vs/editor/common/editorCommon';
import { EndOfLinePreference, IActiveIndentGuideInfo, IModelDecorationOptions, TextModelResolvedOptions, ITextModel } from 'vs/editor/common/model';
import { EndOfLinePreference, IActiveIndentGuideInfo, IModelDecorationOptions, TextModelResolvedOptions, ITextModel, InjectedTextOptions, PositionAffinity } from 'vs/editor/common/model';
import { VerticalRevealType } from 'vs/editor/common/view/viewEvents';
import { IPartialViewLinesViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData';
import { IEditorWhitespace, IWhitespaceChangeAccessor } from 'vs/editor/common/viewLayout/linesLayout';
@@ -17,6 +17,7 @@ import { EditorTheme } from 'vs/editor/common/view/viewContext';
import { ICursorSimpleModel, PartialCursorState, CursorState, IColumnSelectData, EditOperationType, CursorConfiguration } from 'vs/editor/common/controller/cursorCommon';
import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents';
import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler';
import { LineInjectedText } from 'vs/editor/common/model/textModelEvents';
export interface IViewWhitespaceViewportData {
readonly id: string;
@@ -26,7 +27,7 @@ export interface IViewWhitespaceViewportData {
}
export class Viewport {
readonly _viewportBrand: void;
readonly _viewportBrand: void = undefined;
readonly top: number;
readonly left: number;
@@ -81,8 +82,11 @@ export interface ICoordinatesConverter {
validateViewRange(viewRange: Range, expectedModelRange: Range): Range;
// Model -> View conversion and related methods
convertModelPositionToViewPosition(modelPosition: Position): Position;
convertModelRangeToViewRange(modelRange: Range): Range;
convertModelPositionToViewPosition(modelPosition: Position, affinity?: PositionAffinity): Position;
/**
* @param affinity Only has an effect if the range is empty.
*/
convertModelRangeToViewRange(modelRange: Range, affinity?: PositionAffinity): Range;
modelPositionIsVisible(modelPosition: Position): boolean;
getModelLineViewLineCount(modelLineNumber: number): number;
}
@@ -95,53 +99,201 @@ export class OutputPosition {
this.outputLineIndex = outputLineIndex;
this.outputOffset = outputOffset;
}
toString(): string {
return `${this.outputLineIndex}:${this.outputOffset}`;
}
toPosition(baseLineNumber: number, wrappedTextIndentLength: number): Position {
const delta = (this.outputLineIndex > 0 ? wrappedTextIndentLength : 0);
return new Position(baseLineNumber + this.outputLineIndex, delta + this.outputOffset + 1);
}
}
export class LineBreakData {
constructor(
public breakOffsets: number[],
public breakOffsetsVisibleColumn: number[],
public wrappedTextIndentLength: number
public wrappedTextIndentLength: number,
public injectionOffsets: number[] | null,
public injectionOptions: InjectedTextOptions[] | null
) { }
public static getInputOffsetOfOutputPosition(breakOffsets: number[], outputLineIndex: number, outputOffset: number): number {
public getInputOffsetOfOutputPosition(outputLineIndex: number, outputOffset: number): number {
let inputOffset = 0;
if (outputLineIndex === 0) {
return outputOffset;
inputOffset = outputOffset;
} else {
return breakOffsets[outputLineIndex - 1] + outputOffset;
inputOffset = this.breakOffsets[outputLineIndex - 1] + outputOffset;
}
if (this.injectionOffsets !== null) {
for (let i = 0; i < this.injectionOffsets.length; i++) {
if (inputOffset > this.injectionOffsets[i]) {
if (inputOffset < this.injectionOffsets[i] + this.injectionOptions![i].content.length) {
// `inputOffset` is within injected text
inputOffset = this.injectionOffsets[i];
} else {
inputOffset -= this.injectionOptions![i].content.length;
}
} else {
break;
}
}
}
return inputOffset;
}
public static getOutputPositionOfInputOffset(breakOffsets: number[], inputOffset: number): OutputPosition {
public getOutputPositionOfInputOffset(inputOffset: number, affinity: PositionAffinity = PositionAffinity.None): OutputPosition {
let delta = 0;
if (this.injectionOffsets !== null) {
for (let i = 0; i < this.injectionOffsets.length; i++) {
if (inputOffset < this.injectionOffsets[i]) {
break;
}
if (affinity !== PositionAffinity.Right && inputOffset === this.injectionOffsets[i]) {
break;
}
delta += this.injectionOptions![i].content.length;
}
}
inputOffset += delta;
return this.getOutputPositionOfOffsetInUnwrappedLine(inputOffset, affinity);
}
public getOutputPositionOfOffsetInUnwrappedLine(inputOffset: number, affinity: PositionAffinity = PositionAffinity.None): OutputPosition {
let low = 0;
let high = breakOffsets.length - 1;
let high = this.breakOffsets.length - 1;
let mid = 0;
let midStart = 0;
while (low <= high) {
mid = low + ((high - low) / 2) | 0;
const midStop = breakOffsets[mid];
midStart = mid > 0 ? breakOffsets[mid - 1] : 0;
const midStop = this.breakOffsets[mid];
midStart = mid > 0 ? this.breakOffsets[mid - 1] : 0;
if (inputOffset < midStart) {
high = mid - 1;
} else if (inputOffset >= midStop) {
low = mid + 1;
if (affinity === PositionAffinity.Left) {
if (inputOffset <= midStart) {
high = mid - 1;
} else if (inputOffset > midStop) {
low = mid + 1;
} else {
break;
}
} else {
break;
if (inputOffset < midStart) {
high = mid - 1;
} else if (inputOffset >= midStop) {
low = mid + 1;
} else {
break;
}
}
}
return new OutputPosition(mid, inputOffset - midStart);
}
public outputPositionToOffsetInUnwrappedLine(outputLineIndex: number, outputOffset: number): number {
let result = (outputLineIndex > 0 ? this.breakOffsets[outputLineIndex - 1] : 0) + outputOffset;
if (outputLineIndex > 0) {
result -= this.wrappedTextIndentLength;
}
return result;
}
public normalizeOffsetAroundInjections(offsetInUnwrappedLine: number, affinity: PositionAffinity): number {
const injectedText = this.getInjectedTextAtOffset(offsetInUnwrappedLine);
if (!injectedText) {
return offsetInUnwrappedLine;
}
if (affinity === PositionAffinity.None) {
if (offsetInUnwrappedLine === injectedText.offsetInUnwrappedLine + injectedText.length) {
// go to the end of this injected text
return injectedText.offsetInUnwrappedLine + injectedText.length;
} else {
// go to the start of this injected text
return injectedText.offsetInUnwrappedLine;
}
}
if (affinity === PositionAffinity.Right) {
let result = injectedText.offsetInUnwrappedLine + injectedText.length;
let index = injectedText.injectedTextIndex;
// traverse all injected text that touch eachother
while (index + 1 < this.injectionOffsets!.length && this.injectionOffsets![index + 1] === this.injectionOffsets![index]) {
result += this.injectionOptions![index + 1].content.length;
index++;
}
return result;
}
// affinity is left
let result = injectedText.offsetInUnwrappedLine;
let index = injectedText.injectedTextIndex;
// traverse all injected text that touch eachother
while (index - 1 >= 0 && this.injectionOffsets![index - 1] === this.injectionOffsets![index]) {
result -= this.injectionOptions![index - 1].content.length;
index++;
}
return result;
}
public getInjectedText(outputLineIndex: number, outputOffset: number): InjectedText | null {
const offset = this.outputPositionToOffsetInUnwrappedLine(outputLineIndex, outputOffset);
const injectedText = this.getInjectedTextAtOffset(offset);
if (!injectedText) {
return null;
}
return {
options: this.injectionOptions![injectedText.injectedTextIndex]
};
}
private getInjectedTextAtOffset(offsetInUnwrappedLine: number): { injectedTextIndex: number, offsetInUnwrappedLine: number, length: number } | undefined {
const injectionOffsets = this.injectionOffsets;
const injectionOptions = this.injectionOptions;
if (injectionOffsets !== null) {
let totalInjectedTextLengthBefore = 0;
for (let i = 0; i < injectionOffsets.length; i++) {
const length = injectionOptions![i].content.length;
const injectedTextStartOffsetInUnwrappedLine = injectionOffsets[i] + totalInjectedTextLengthBefore;
const injectedTextEndOffsetInUnwrappedLine = injectionOffsets[i] + totalInjectedTextLengthBefore + length;
if (injectedTextStartOffsetInUnwrappedLine > offsetInUnwrappedLine) {
// Injected text starts later.
break; // All later injected texts have an even larger offset.
}
if (offsetInUnwrappedLine <= injectedTextEndOffsetInUnwrappedLine) {
// Injected text ends after or with the given position (but also starts with or before it).
return {
injectedTextIndex: i,
offsetInUnwrappedLine: injectedTextStartOffsetInUnwrappedLine,
length
};
}
totalInjectedTextLengthBefore += length;
}
}
return undefined;
}
}
export interface ILineBreaksComputer {
/**
* Pass in `previousLineBreakData` if the only difference is in breaking columns!!!
*/
addRequest(lineText: string, previousLineBreakData: LineBreakData | null): void;
addRequest(lineText: string, injectedText: LineInjectedText[] | null, previousLineBreakData: LineBreakData | null): void;
finalize(): (LineBreakData | null)[];
}
@@ -190,6 +342,8 @@ export interface IViewModel extends ICursorSimpleModel {
invalidateMinimapColorCache(): void;
getValueInRange(range: Range, eol: EndOfLinePreference): string;
getInjectedTextAt(viewPosition: Position): InjectedText | null;
getModelLineMaxColumn(modelLineNumber: number): number;
validateModelPosition(modelPosition: IPosition): Position;
validateModelRange(range: IRange): Range;
@@ -234,6 +388,10 @@ export interface IViewModel extends ICursorSimpleModel {
//#endregion
}
export class InjectedText {
constructor(public readonly options: InjectedTextOptions) { }
}
export class MinimapLinesRenderingData {
public readonly tabSize: number;
public readonly data: Array<ViewLineData | null>;
@@ -248,7 +406,7 @@ export class MinimapLinesRenderingData {
}
export class ViewLineData {
_viewLineDataBrand: void;
_viewLineDataBrand: void = undefined;
/**
* The content at this view line.
@@ -275,13 +433,19 @@ export class ViewLineData {
*/
public readonly tokens: IViewLineTokens;
/**
* Additional inline decorations for this line.
*/
public readonly inlineDecorations: readonly SingleLineInlineDecoration[] | null;
constructor(
content: string,
continuesWithWrappedLine: boolean,
minColumn: number,
maxColumn: number,
startVisibleColumn: number,
tokens: IViewLineTokens
tokens: IViewLineTokens,
inlineDecorations: readonly SingleLineInlineDecoration[] | null
) {
this.content = content;
this.continuesWithWrappedLine = continuesWithWrappedLine;
@@ -289,6 +453,7 @@ export class ViewLineData {
this.maxColumn = maxColumn;
this.startVisibleColumn = startVisibleColumn;
this.tokens = tokens;
this.inlineDecorations = inlineDecorations;
}
}
@@ -344,7 +509,7 @@ export class ViewLineRenderingData {
tokens: IViewLineTokens,
inlineDecorations: InlineDecoration[],
tabSize: number,
startVisibleColumn: number
startVisibleColumn: number,
) {
this.minColumn = minColumn;
this.maxColumn = maxColumn;
@@ -391,8 +556,26 @@ export class InlineDecoration {
}
}
export class SingleLineInlineDecoration {
constructor(
public readonly startOffset: number,
public readonly endOffset: number,
public readonly inlineClassName: string,
public readonly inlineClassNameAffectsLetterSpacing: boolean
) {
}
toInlineDecoration(lineNumber: number): InlineDecoration {
return new InlineDecoration(
new Range(lineNumber, this.startOffset + 1, lineNumber, this.endOffset + 1),
this.inlineClassName,
this.inlineClassNameAffectsLetterSpacing ? InlineDecorationType.RegularAffectingLetterSpacing : InlineDecorationType.Regular
);
}
}
export class ViewModelDecoration {
_viewModelDecorationBrand: void;
_viewModelDecorationBrand: void = undefined;
public readonly range: Range;
public readonly options: IModelDecorationOptions;

View File

@@ -7,7 +7,7 @@ import { IDisposable } from 'vs/base/common/lifecycle';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import * as editorCommon from 'vs/editor/common/editorCommon';
import { IModelDecoration, ITextModel } from 'vs/editor/common/model';
import { IModelDecoration, ITextModel, PositionAffinity } from 'vs/editor/common/model';
import { IViewModelLinesCollection } from 'vs/editor/common/viewModel/splitLinesCollection';
import { ICoordinatesConverter, InlineDecoration, InlineDecorationType, ViewModelDecoration } from 'vs/editor/common/viewModel/viewModel';
import { filterValidationDecorations } from 'vs/editor/common/config/editorOptions';
@@ -81,11 +81,13 @@ export class ViewModelDecorations implements IDisposable {
const options = modelDecoration.options;
let viewRange: Range;
if (options.isWholeLine) {
const start = this._coordinatesConverter.convertModelPositionToViewPosition(new Position(modelRange.startLineNumber, 1));
const end = this._coordinatesConverter.convertModelPositionToViewPosition(new Position(modelRange.endLineNumber, this.model.getLineMaxColumn(modelRange.endLineNumber)));
const start = this._coordinatesConverter.convertModelPositionToViewPosition(new Position(modelRange.startLineNumber, 1), PositionAffinity.Left);
const end = this._coordinatesConverter.convertModelPositionToViewPosition(new Position(modelRange.endLineNumber, this.model.getLineMaxColumn(modelRange.endLineNumber)), PositionAffinity.Right);
viewRange = new Range(start.lineNumber, start.column, end.lineNumber, end.column);
} else {
viewRange = this._coordinatesConverter.convertModelRangeToViewRange(modelRange);
// For backwards compatibility reasons, we want injected text before any decoration.
// Thus, move decorations to the right.
viewRange = this._coordinatesConverter.convertModelRangeToViewRange(modelRange, PositionAffinity.Right);
}
r = new ViewModelDecoration(viewRange, options);
this._decorationsCache[id] = r;

View File

@@ -35,7 +35,7 @@ export class ViewModelEventDispatcher extends Disposable {
public emitOutgoingEvent(e: OutgoingViewModelEvent): void {
this._addOutgoingEvent(e);
this._emitOugoingEvents();
this._emitOutgoingEvents();
}
private _addOutgoingEvent(e: OutgoingViewModelEvent): void {
@@ -49,7 +49,7 @@ export class ViewModelEventDispatcher extends Disposable {
this._outgoingEvents.push(e);
}
private _emitOugoingEvents(): void {
private _emitOutgoingEvents(): void {
while (this._outgoingEvents.length > 0) {
if (this._collector || this._isConsumingViewEventQueue) {
// right now collecting or emitting view events, so let's postpone emitting
@@ -104,7 +104,7 @@ export class ViewModelEventDispatcher extends Disposable {
this._emitMany(viewEvents);
}
}
this._emitOugoingEvents();
this._emitOutgoingEvents();
}
public emitSingleViewEvent(event: ViewEvent): void {

View File

@@ -12,7 +12,7 @@ import { IPosition, Position } from 'vs/editor/common/core/position';
import { ISelection, Selection } from 'vs/editor/common/core/selection';
import { IRange, Range } from 'vs/editor/common/core/range';
import { IConfiguration, IViewState, ScrollType, ICursorState, ICommand, INewScrollPosition } from 'vs/editor/common/editorCommon';
import { EndOfLinePreference, IActiveIndentGuideInfo, ITextModel, TrackedRangeStickiness, TextModelResolvedOptions, IIdentifiedSingleEditOperation, ICursorStateComputer, PositionNormalizationAffinity } from 'vs/editor/common/model';
import { EndOfLinePreference, IActiveIndentGuideInfo, ITextModel, TrackedRangeStickiness, TextModelResolvedOptions, IIdentifiedSingleEditOperation, ICursorStateComputer, PositionAffinity } from 'vs/editor/common/model';
import { ModelDecorationOverviewRulerOptions, ModelDecorationMinimapOptions } from 'vs/editor/common/model/textModel';
import * as textModelEvents from 'vs/editor/common/model/textModelEvents';
import { ColorId, LanguageId, TokenizationRegistry } from 'vs/editor/common/modes';
@@ -21,12 +21,12 @@ import { MinimapTokensColorTracker } from 'vs/editor/common/viewModel/minimapTok
import * as viewEvents from 'vs/editor/common/view/viewEvents';
import { ViewLayout } from 'vs/editor/common/viewLayout/viewLayout';
import { IViewModelLinesCollection, IdentityLinesCollection, SplitLinesCollection, ILineBreaksComputerFactory } from 'vs/editor/common/viewModel/splitLinesCollection';
import { ICoordinatesConverter, ILineBreaksComputer, IOverviewRulerDecorations, IViewModel, MinimapLinesRenderingData, ViewLineData, ViewLineRenderingData, ViewModelDecoration } from 'vs/editor/common/viewModel/viewModel';
import { ICoordinatesConverter, InjectedText, ILineBreaksComputer, IOverviewRulerDecorations, IViewModel, MinimapLinesRenderingData, ViewLineData, ViewLineRenderingData, ViewModelDecoration } from 'vs/editor/common/viewModel/viewModel';
import { ViewModelDecorations } from 'vs/editor/common/viewModel/viewModelDecorations';
import { RunOnceScheduler } from 'vs/base/common/async';
import * as platform from 'vs/base/common/platform';
import { EditorTheme } from 'vs/editor/common/view/viewContext';
import { Cursor } from 'vs/editor/common/controller/cursor';
import { CursorsController } from 'vs/editor/common/controller/cursor';
import { PartialCursorState, CursorState, IColumnSelectData, EditOperationType, CursorConfiguration } from 'vs/editor/common/controller/cursorCommon';
import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents';
import { IWhitespaceChangeAccessor } from 'vs/editor/common/viewLayout/linesLayout';
@@ -52,7 +52,7 @@ export class ViewModel extends Disposable implements IViewModel {
private readonly _lines: IViewModelLinesCollection;
public readonly coordinatesConverter: ICoordinatesConverter;
public readonly viewLayout: ViewLayout;
private readonly _cursor: Cursor;
private readonly _cursor: CursorsController;
private readonly _decorations: ViewModelDecorations;
constructor(
@@ -90,6 +90,7 @@ export class ViewModel extends Disposable implements IViewModel {
const wrappingIndent = options.get(EditorOption.wrappingIndent);
this._lines = new SplitLinesCollection(
this._editorId,
this.model,
domLineBreaksComputerFactory,
monospaceLineBreaksComputerFactory,
@@ -103,7 +104,7 @@ export class ViewModel extends Disposable implements IViewModel {
this.coordinatesConverter = this._lines.createCoordinatesConverter();
this._cursor = this._register(new Cursor(model, this, this.coordinatesConverter, this.cursorConfig));
this._cursor = this._register(new CursorsController(model, this, this.coordinatesConverter, this.cursorConfig));
this.viewLayout = this._register(new ViewLayout(this._configuration, this.getLineCount(), scheduleAtNextAnimationFrame));
@@ -250,7 +251,7 @@ export class ViewModel extends Disposable implements IViewModel {
private _registerModelEvents(): void {
this._register(this.model.onDidChangeRawContentFast((e) => {
this._register(this.model.onDidChangeContentOrInjectedText((e) => {
try {
const eventsCollector = this._eventDispatcher.beginEmitViewEvents();
@@ -258,20 +259,29 @@ export class ViewModel extends Disposable implements IViewModel {
let hadModelLineChangeThatChangedLineMapping = false;
const changes = e.changes;
const versionId = e.versionId;
const versionId = (e instanceof textModelEvents.ModelRawContentChangedEvent ? e.versionId : null);
// Do a first pass to compute line mappings, and a second pass to actually interpret them
const lineBreaksComputer = this._lines.createLineBreaksComputer();
for (const change of changes) {
switch (change.changeType) {
case textModelEvents.RawContentChangedType.LinesInserted: {
for (const line of change.detail) {
lineBreaksComputer.addRequest(line, null);
for (let lineIdx = 0; lineIdx < change.detail.length; lineIdx++) {
const line = change.detail[lineIdx];
let injectedText = change.injectedTexts[lineIdx];
if (injectedText) {
injectedText = injectedText.filter(element => (!element.ownerId || element.ownerId === this._editorId));
}
lineBreaksComputer.addRequest(line, injectedText, null);
}
break;
}
case textModelEvents.RawContentChangedType.LineChanged: {
lineBreaksComputer.addRequest(change.detail, null);
let injectedText: textModelEvents.LineInjectedText[] | null = null;
if (change.injectedText) {
injectedText = change.injectedText.filter(element => (!element.ownerId || element.ownerId === this._editorId));
}
lineBreaksComputer.addRequest(change.detail, injectedText, null);
break;
}
}
@@ -336,7 +346,10 @@ export class ViewModel extends Disposable implements IViewModel {
}
}
}
this._lines.acceptVersionId(versionId);
if (versionId !== null) {
this._lines.acceptVersionId(versionId);
}
this.viewLayout.onHeightMaybeChanged();
if (!hadOtherModelChange && hadModelLineChangeThatChangedLineMapping) {
@@ -637,6 +650,10 @@ export class ViewModel extends Disposable implements IViewModel {
return this._decorations.getDecorationsViewportData(visibleRange).decorations;
}
public getInjectedTextAt(viewPosition: Position): InjectedText | null {
return this._lines.getInjectedTextAt(viewPosition);
}
public getViewLineRenderingData(visibleRange: Range, lineNumber: number): ViewLineRenderingData {
let mightContainRTL = this.model.mightContainRTL();
let mightContainNonBasicASCII = this.model.mightContainNonBasicASCII();
@@ -645,6 +662,15 @@ export class ViewModel extends Disposable implements IViewModel {
let allInlineDecorations = this._decorations.getDecorationsViewportData(visibleRange).inlineDecorations;
let inlineDecorations = allInlineDecorations[lineNumber - visibleRange.startLineNumber];
if (lineData.inlineDecorations) {
inlineDecorations = [
...inlineDecorations,
...lineData.inlineDecorations.map(d =>
d.toInlineDecoration(lineNumber)
)
];
}
return new ViewLineRenderingData(
lineData.minColumn,
lineData.maxColumn,
@@ -1038,7 +1064,7 @@ export class ViewModel extends Disposable implements IViewModel {
}
}
normalizePosition(position: Position, affinity: PositionNormalizationAffinity): Position {
normalizePosition(position: Position, affinity: PositionAffinity): Position {
return this._lines.normalizePosition(position, affinity);
}