mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-15 10:58:31 -05:00
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:
@@ -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.") }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
480
src/vs/editor/common/model/bracketPairColorizer/ast.ts
Normal file
480
src/vs/editor/common/model/bracketPairColorizer/ast.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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}; }`);
|
||||
}
|
||||
});
|
||||
120
src/vs/editor/common/model/bracketPairColorizer/brackets.ts
Normal file
120
src/vs/editor/common/model/bracketPairColorizer/brackets.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
230
src/vs/editor/common/model/bracketPairColorizer/length.ts
Normal file
230
src/vs/editor/common/model/bracketPairColorizer/length.ts
Normal 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;
|
||||
}
|
||||
123
src/vs/editor/common/model/bracketPairColorizer/nodeReader.ts
Normal file
123
src/vs/editor/common/model/bracketPairColorizer/nodeReader.ts
Normal 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;
|
||||
}
|
||||
153
src/vs/editor/common/model/bracketPairColorizer/parser.ts
Normal file
153
src/vs/editor/common/model/bracketPairColorizer/parser.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
349
src/vs/editor/common/model/bracketPairColorizer/tokenizer.ts
Normal file
349
src/vs/editor/common/model/bracketPairColorizer/tokenizer.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
29
src/vs/editor/common/model/decorationProvider.ts
Normal file
29
src/vs/editor/common/model/decorationProvider.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -254,7 +254,7 @@ export interface CompleteEnterAction {
|
||||
* @internal
|
||||
*/
|
||||
export class StandardAutoClosingPairConditional {
|
||||
_standardAutoClosingPairConditionalBrand: void;
|
||||
_standardAutoClosingPairConditionalBrand: void = undefined;
|
||||
|
||||
readonly open: string;
|
||||
readonly close: string;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user