Merge branch 'master' into aeschli/ts-sem

This commit is contained in:
Martin Aeschlimann 2020-01-10 15:40:03 +01:00
commit fbad0c368e
76 changed files with 2429 additions and 1029 deletions

View file

@ -18,7 +18,7 @@ export default class TsConfigProvider {
return [];
}
const configs = new Map<string, TSConfig>();
for (const config of await vscode.workspace.findFiles('**/tsconfig*.json', '**/node_modules/**')) {
for (const config of await vscode.workspace.findFiles('**/tsconfig*.json', '**/{node_modules,.*}/**')) {
const root = vscode.workspace.getWorkspaceFolder(config);
if (root) {
configs.set(config.fsPath, {

View file

@ -13,6 +13,7 @@ const webviewId = 'myWebview';
const testDocument = join(vscode.workspace.rootPath || '', './bower.json');
// TODO: Re-enable after https://github.com/microsoft/vscode/issues/88415
suite.skip('Webview tests', () => {
const disposables: vscode.Disposable[] = [];

View file

@ -57,12 +57,3 @@ export function toUint32(v: number): number {
}
return v | 0;
}
export function toUint32Array(arr: number[]): Uint32Array {
const len = arr.length;
const r = new Uint32Array(len);
for (let i = 0; i < len; i++) {
r[i] = toUint32(arr[i]);
}
return r;
}

View file

@ -191,7 +191,8 @@ export class CodeApplication extends Disposable {
webPreferences.nodeIntegration = false;
// Verify URLs being loaded
if (isValidWebviewSource(params.src) && isValidWebviewSource(webPreferences.preload)) {
// https://github.com/electron/electron/issues/21553
if (isValidWebviewSource(params.src) && isValidWebviewSource((webPreferences as { preloadURL: string }).preloadURL)) {
return;
}

View file

@ -136,14 +136,16 @@ export class IndentGuidesOverlay extends DynamicViewOverlay {
const indent = indents[lineIndex];
let result = '';
const leftMostVisiblePosition = ctx.visibleRangeForPosition(new Position(lineNumber, 1));
let left = leftMostVisiblePosition ? leftMostVisiblePosition.left : 0;
for (let i = 1; i <= indent; i++) {
const className = (containsActiveIndentGuide && i === activeIndentLevel ? 'cigra' : 'cigr');
result += `<div class="${className}" style="left:${left}px;height:${lineHeight}px;width:${indentWidth}px"></div>`;
left += indentWidth;
if (left > scrollWidth || (this._maxIndentLeft > 0 && left > this._maxIndentLeft)) {
break;
if (indent >= 1) {
const leftMostVisiblePosition = ctx.visibleRangeForPosition(new Position(lineNumber, 1));
let left = leftMostVisiblePosition ? leftMostVisiblePosition.left : 0;
for (let i = 1; i <= indent; i++) {
const className = (containsActiveIndentGuide && i === activeIndentLevel ? 'cigra' : 'cigr');
result += `<div class="${className}" style="left:${left}px;height:${lineHeight}px;width:${indentWidth}px"></div>`;
left += indentWidth;
if (left > scrollWidth || (this._maxIndentLeft > 0 && left > this._maxIndentLeft)) {
break;
}
}
}

View file

@ -213,6 +213,7 @@ export class ViewLine implements IVisibleLine {
lineData.tokens,
actualInlineDecorations,
lineData.tabSize,
lineData.startVisibleColumn,
options.spaceWidth,
options.stopRenderingLineAfter,
options.renderWhitespace,

View file

@ -50,6 +50,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { withNullAsUndefined } from 'vs/base/common/types';
import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer';
let EDITOR_ID = 0;
@ -1329,7 +1330,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
model.onBeforeAttached();
const viewModel = new ViewModel(this._id, this._configuration, model, (callback) => dom.scheduleAtNextAnimationFrame(callback));
const viewModel = new ViewModel(this._id, this._configuration, model, MonospaceLineBreaksComputerFactory.create(this._configuration.options), (callback) => dom.scheduleAtNextAnimationFrame(callback));
listenersToRemove.push(model.onDidChangeDecorations((e) => this._onDidChangeModelDecorations.fire(e)));
listenersToRemove.push(model.onDidChangeLanguage((e) => {

View file

@ -2144,6 +2144,7 @@ class InlineViewZonesComputer extends ViewZonesComputer {
lineTokens,
actualDecorations,
tabSize,
0,
fontInfo.spaceWidth,
options.get(EditorOption.stopRenderingLineAfter),
options.get(EditorOption.renderWhitespace),

View file

@ -780,6 +780,7 @@ export class DiffReview extends Disposable {
lineTokens,
[],
tabSize,
0,
fontInfo.spaceWidth,
options.get(EditorOption.stopRenderingLineAfter),
options.get(EditorOption.renderWhitespace),

View file

@ -264,19 +264,14 @@ export interface IEditorOptions {
wrappingIndent?: 'none' | 'same' | 'indent' | 'deepIndent';
/**
* Configure word wrapping characters. A break will be introduced before these characters.
* Defaults to '{([+'.
* Defaults to '([{‘“〈《「『【〔([{「£¥$£¥+'.
*/
wordWrapBreakBeforeCharacters?: string;
/**
* Configure word wrapping characters. A break will be introduced after these characters.
* Defaults to ' \t})]?|&,;'.
* Defaults to ' \t})]?|/&.,;¢°′″‰℃、。。、¢,.:;?!%・・ゝゞヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻ァィゥェォャュョッー”〉》」』】〕)]}」'.
*/
wordWrapBreakAfterCharacters?: string;
/**
* Configure word wrapping characters. A break will be introduced after these characters only if no `wordWrapBreakBeforeCharacters` or `wordWrapBreakAfterCharacters` were found.
* Defaults to '.'.
*/
wordWrapBreakObtrusiveCharacters?: string;
/**
* Performance guard: Stop rendering a line after x characters.
* Defaults to 10000.
@ -3154,7 +3149,6 @@ export const enum EditorOption {
wordWrap,
wordWrapBreakAfterCharacters,
wordWrapBreakBeforeCharacters,
wordWrapBreakObtrusiveCharacters,
wordWrapColumn,
wordWrapMinified,
wrappingIndent,
@ -3681,16 +3675,12 @@ export const EditorOptions = {
)),
wordWrapBreakAfterCharacters: register(new EditorStringOption(
EditorOption.wordWrapBreakAfterCharacters, 'wordWrapBreakAfterCharacters',
' \t})]?|/&,;¢°′″‰℃、。。、¢,.:;?!%・・ゝゞヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻ァィゥェォャュョッー”〉》」』】〕)]}」',
' \t})]?|/&.,;¢°′″‰℃、。。、¢,.:;?!%・・ゝゞヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻ァィゥェォャュョッー”〉》」』】〕)]}」',
)),
wordWrapBreakBeforeCharacters: register(new EditorStringOption(
EditorOption.wordWrapBreakBeforeCharacters, 'wordWrapBreakBeforeCharacters',
'([{‘“〈《「『【〔([{「£¥$£¥+'
)),
wordWrapBreakObtrusiveCharacters: register(new EditorStringOption(
EditorOption.wordWrapBreakObtrusiveCharacters, 'wordWrapBreakObtrusiveCharacters',
'.'
)),
wordWrapColumn: register(new EditorIntOption(
EditorOption.wordWrapColumn, 'wordWrapColumn',
80, 1, Constants.MAX_SAFE_SMALL_INTEGER,

View file

@ -12,14 +12,14 @@ export class CharacterClassifier<T extends number> {
/**
* Maintain a compact (fully initialized ASCII map for quickly classifying ASCII characters - used more often in code).
*/
private _asciiMap: Uint8Array;
protected _asciiMap: Uint8Array;
/**
* The entire map (sparse array).
*/
private _map: Map<number, number>;
protected _map: Map<number, number>;
private _defaultValue: number;
protected _defaultValue: number;
constructor(_defaultValue: T) {
let defaultValue = toUint8(_defaultValue);

View file

@ -511,7 +511,93 @@ export class PieceTreeBase {
}
public getLinesContent(): string[] {
return this.getContentOfSubTree(this.root).split(/\r\n|\r|\n/);
let lines: string[] = [];
let linesLength = 0;
let currentLine = '';
let danglingCR = false;
this.iterate(this.root, node => {
if (node === SENTINEL) {
return true;
}
const piece = node.piece;
let pieceLength = piece.length;
if (pieceLength === 0) {
return true;
}
const buffer = this._buffers[piece.bufferIndex].buffer;
const lineStarts = this._buffers[piece.bufferIndex].lineStarts;
const pieceStartLine = piece.start.line;
const pieceEndLine = piece.end.line;
let pieceStartOffset = lineStarts[pieceStartLine] + piece.start.column;
if (danglingCR) {
if (buffer.charCodeAt(pieceStartOffset) === CharCode.LineFeed) {
// pretend the \n was in the previous piece..
pieceStartOffset++;
pieceLength--;
}
lines[linesLength++] = currentLine;
currentLine = '';
danglingCR = false;
if (pieceLength === 0) {
return true;
}
}
if (pieceStartLine === pieceEndLine) {
// this piece has no new lines
if (!this._EOLNormalized && buffer.charCodeAt(pieceStartOffset + pieceLength - 1) === CharCode.CarriageReturn) {
danglingCR = true;
currentLine += buffer.substr(pieceStartOffset, pieceLength - 1);
} else {
currentLine += buffer.substr(pieceStartOffset, pieceLength);
}
return true;
}
// add the text before the first line start in this piece
currentLine += (
this._EOLNormalized
? buffer.substring(pieceStartOffset, Math.max(pieceStartOffset, lineStarts[pieceStartLine + 1] - this._EOLLength))
: buffer.substring(pieceStartOffset, lineStarts[pieceStartLine + 1]).replace(/(\r\n|\r|\n)$/, '')
);
lines[linesLength++] = currentLine;
for (let line = pieceStartLine + 1; line < pieceEndLine; line++) {
currentLine = (
this._EOLNormalized
? buffer.substring(lineStarts[line], lineStarts[line + 1] - this._EOLLength)
: buffer.substring(lineStarts[line], lineStarts[line + 1]).replace(/(\r\n|\r|\n)$/, '')
);
lines[linesLength++] = currentLine;
}
if (!this._EOLNormalized && buffer.charCodeAt(lineStarts[pieceEndLine] + piece.end.column - 1) === CharCode.CarriageReturn) {
danglingCR = true;
if (piece.end.column === 0) {
// The last line ended with a \r, let's undo the push, it will be pushed by next iteration
linesLength--;
} else {
currentLine = buffer.substr(lineStarts[pieceEndLine], piece.end.column - 1);
}
} else {
currentLine = buffer.substr(lineStarts[pieceEndLine], piece.end.column);
}
return true;
});
if (danglingCR) {
lines[linesLength++] = currentLine;
currentLine = '';
}
lines[linesLength++] = currentLine;
return lines;
}
public getLength(): number {
@ -728,7 +814,7 @@ export class PieceTreeBase {
// #endregion
// #region Piece Table
insert(offset: number, value: string, eolNormalized: boolean = false): void {
public insert(offset: number, value: string, eolNormalized: boolean = false): void {
this._EOLNormalized = this._EOLNormalized && eolNormalized;
this._lastVisitedLine.lineNumber = 0;
this._lastVisitedLine.value = '';
@ -826,7 +912,7 @@ export class PieceTreeBase {
this.computeBufferMetadata();
}
delete(offset: number, cnt: number): void {
public delete(offset: number, cnt: number): void {
this._lastVisitedLine.lineNumber = 0;
this._lastVisitedLine.value = '';
@ -899,7 +985,7 @@ export class PieceTreeBase {
this.computeBufferMetadata();
}
insertContentToNodeLeft(value: string, node: TreeNode) {
private insertContentToNodeLeft(value: string, node: TreeNode) {
// we are inserting content to the beginning of node
let nodesToDel: TreeNode[] = [];
if (this.shouldCheckCRLF() && this.endWithCR(value) && this.startWithLF(node)) {
@ -934,7 +1020,7 @@ export class PieceTreeBase {
this.deleteNodes(nodesToDel);
}
insertContentToNodeRight(value: string, node: TreeNode) {
private insertContentToNodeRight(value: string, node: TreeNode) {
// we are inserting to the right of this node.
if (this.adjustCarriageReturnFromNext(value, node)) {
// move \n to the new node.
@ -952,9 +1038,9 @@ export class PieceTreeBase {
this.validateCRLFWithPrevNode(newNode);
}
positionInBuffer(node: TreeNode, remainder: number): BufferCursor;
positionInBuffer(node: TreeNode, remainder: number, ret: BufferCursor): null;
positionInBuffer(node: TreeNode, remainder: number, ret?: BufferCursor): BufferCursor | null {
private positionInBuffer(node: TreeNode, remainder: number): BufferCursor;
private positionInBuffer(node: TreeNode, remainder: number, ret: BufferCursor): null;
private positionInBuffer(node: TreeNode, remainder: number, ret?: BufferCursor): BufferCursor | null {
let piece = node.piece;
let bufferIndex = node.piece.bufferIndex;
let lineStarts = this._buffers[bufferIndex].lineStarts;
@ -1002,7 +1088,7 @@ export class PieceTreeBase {
};
}
getLineFeedCnt(bufferIndex: number, start: BufferCursor, end: BufferCursor): number {
private getLineFeedCnt(bufferIndex: number, start: BufferCursor, end: BufferCursor): number {
// we don't need to worry about start: abc\r|\n, or abc|\r, or abc|\n, or abc|\r\n doesn't change the fact that, there is one line break after start.
// now let's take care of end: abc\r|\n, if end is in between \r and \n, we need to add line feed count by 1
if (end.column === 0) {
@ -1032,18 +1118,18 @@ export class PieceTreeBase {
}
}
offsetInBuffer(bufferIndex: number, cursor: BufferCursor): number {
private offsetInBuffer(bufferIndex: number, cursor: BufferCursor): number {
let lineStarts = this._buffers[bufferIndex].lineStarts;
return lineStarts[cursor.line] + cursor.column;
}
deleteNodes(nodes: TreeNode[]): void {
private deleteNodes(nodes: TreeNode[]): void {
for (let i = 0; i < nodes.length; i++) {
rbDelete(this, nodes[i]);
}
}
createNewPieces(text: string): Piece[] {
private createNewPieces(text: string): Piece[] {
if (text.length > AverageBufferSize) {
// the content is large, operations like substring, charCode becomes slow
// so here we split it into smaller chunks, just like what we did for CR/LF normalization
@ -1128,11 +1214,11 @@ export class PieceTreeBase {
return [newPiece];
}
getLinesRawContent(): string {
public getLinesRawContent(): string {
return this.getContentOfSubTree(this.root);
}
getLineRawContent(lineNumber: number, endOffset: number = 0): string {
public getLineRawContent(lineNumber: number, endOffset: number = 0): string {
let x = this.root;
let ret = '';
@ -1204,7 +1290,7 @@ export class PieceTreeBase {
return ret;
}
computeBufferMetadata() {
private computeBufferMetadata() {
let x = this.root;
let lfCnt = 1;
@ -1222,7 +1308,7 @@ export class PieceTreeBase {
}
// #region node operations
getIndexOf(node: TreeNode, accumulatedValue: number): { index: number, remainder: number } {
private getIndexOf(node: TreeNode, accumulatedValue: number): { index: number, remainder: number } {
let piece = node.piece;
let pos = this.positionInBuffer(node, accumulatedValue);
let lineCnt = pos.line - piece.start.line;
@ -1239,7 +1325,7 @@ export class PieceTreeBase {
return { index: lineCnt, remainder: pos.column };
}
getAccumulatedValue(node: TreeNode, index: number) {
private getAccumulatedValue(node: TreeNode, index: number) {
if (index < 0) {
return 0;
}
@ -1253,7 +1339,7 @@ export class PieceTreeBase {
}
}
deleteNodeTail(node: TreeNode, pos: BufferCursor) {
private deleteNodeTail(node: TreeNode, pos: BufferCursor) {
const piece = node.piece;
const originalLFCnt = piece.lineFeedCnt;
const originalEndOffset = this.offsetInBuffer(piece.bufferIndex, piece.end);
@ -1277,7 +1363,7 @@ export class PieceTreeBase {
updateTreeMetadata(this, node, size_delta, lf_delta);
}
deleteNodeHead(node: TreeNode, pos: BufferCursor) {
private deleteNodeHead(node: TreeNode, pos: BufferCursor) {
const piece = node.piece;
const originalLFCnt = piece.lineFeedCnt;
const originalStartOffset = this.offsetInBuffer(piece.bufferIndex, piece.start);
@ -1299,7 +1385,7 @@ export class PieceTreeBase {
updateTreeMetadata(this, node, size_delta, lf_delta);
}
shrinkNode(node: TreeNode, start: BufferCursor, end: BufferCursor) {
private shrinkNode(node: TreeNode, start: BufferCursor, end: BufferCursor) {
const piece = node.piece;
const originalStartPos = piece.start;
const originalEndPos = piece.end;
@ -1334,7 +1420,7 @@ export class PieceTreeBase {
this.validateCRLFWithPrevNode(newNode);
}
appendToNode(node: TreeNode, value: string): void {
private appendToNode(node: TreeNode, value: string): void {
if (this.adjustCarriageReturnFromNext(value, node)) {
value += '\n';
}
@ -1374,7 +1460,7 @@ export class PieceTreeBase {
updateTreeMetadata(this, node, value.length, lf_delta);
}
nodeAt(offset: number): NodePosition {
private nodeAt(offset: number): NodePosition {
let x = this.root;
let cache = this._searchCache.get(offset);
if (cache) {
@ -1409,7 +1495,7 @@ export class PieceTreeBase {
return null!;
}
nodeAt2(lineNumber: number, column: number): NodePosition {
private nodeAt2(lineNumber: number, column: number): NodePosition {
let x = this.root;
let nodeStartOffset = 0;
@ -1476,7 +1562,7 @@ export class PieceTreeBase {
return null!;
}
nodeCharCodeAt(node: TreeNode, offset: number): number {
private nodeCharCodeAt(node: TreeNode, offset: number): number {
if (node.piece.lineFeedCnt < 1) {
return -1;
}
@ -1485,7 +1571,7 @@ export class PieceTreeBase {
return buffer.buffer.charCodeAt(newOffset);
}
offsetOfNode(node: TreeNode): number {
private offsetOfNode(node: TreeNode): number {
if (!node) {
return 0;
}
@ -1504,11 +1590,11 @@ export class PieceTreeBase {
// #endregion
// #region CRLF
shouldCheckCRLF() {
private shouldCheckCRLF() {
return !(this._EOLNormalized && this._EOL === '\n');
}
startWithLF(val: string | TreeNode): boolean {
private startWithLF(val: string | TreeNode): boolean {
if (typeof val === 'string') {
return val.charCodeAt(0) === 10;
}
@ -1532,7 +1618,7 @@ export class PieceTreeBase {
return this._buffers[piece.bufferIndex].buffer.charCodeAt(startOffset) === 10;
}
endWithCR(val: string | TreeNode): boolean {
private endWithCR(val: string | TreeNode): boolean {
if (typeof val === 'string') {
return val.charCodeAt(val.length - 1) === 13;
}
@ -1544,7 +1630,7 @@ export class PieceTreeBase {
return this.nodeCharCodeAt(val, val.piece.length - 1) === 13;
}
validateCRLFWithPrevNode(nextNode: TreeNode) {
private validateCRLFWithPrevNode(nextNode: TreeNode) {
if (this.shouldCheckCRLF() && this.startWithLF(nextNode)) {
let node = nextNode.prev();
if (this.endWithCR(node)) {
@ -1553,7 +1639,7 @@ export class PieceTreeBase {
}
}
validateCRLFWithNextNode(node: TreeNode) {
private validateCRLFWithNextNode(node: TreeNode) {
if (this.shouldCheckCRLF() && this.endWithCR(node)) {
let nextNode = node.next();
if (this.startWithLF(nextNode)) {
@ -1562,7 +1648,7 @@ export class PieceTreeBase {
}
}
fixCRLF(prev: TreeNode, next: TreeNode) {
private fixCRLF(prev: TreeNode, next: TreeNode) {
let nodesToDel: TreeNode[] = [];
// update node
let lineStarts = this._buffers[prev.piece.bufferIndex].lineStarts;
@ -1617,7 +1703,7 @@ export class PieceTreeBase {
}
}
adjustCarriageReturnFromNext(value: string, node: TreeNode): boolean {
private adjustCarriageReturnFromNext(value: string, node: TreeNode): boolean {
if (this.shouldCheckCRLF() && this.endWithCR(value)) {
let nextNode = node.next();
if (this.startWithLF(nextNode)) {
@ -1667,7 +1753,7 @@ export class PieceTreeBase {
return callback(node) && this.iterate(node.right, callback);
}
getNodeContent(node: TreeNode) {
private getNodeContent(node: TreeNode) {
if (node === SENTINEL) {
return '';
}
@ -1695,7 +1781,7 @@ export class PieceTreeBase {
* /
* z
*/
rbInsertRight(node: TreeNode | null, p: Piece): TreeNode {
private rbInsertRight(node: TreeNode | null, p: Piece): TreeNode {
let z = new TreeNode(p, NodeColor.Red);
z.left = SENTINEL;
z.right = SENTINEL;
@ -1727,7 +1813,7 @@ export class PieceTreeBase {
* \
* z
*/
rbInsertLeft(node: TreeNode | null, p: Piece): TreeNode {
private rbInsertLeft(node: TreeNode | null, p: Piece): TreeNode {
let z = new TreeNode(p, NodeColor.Red);
z.left = SENTINEL;
z.right = SENTINEL;
@ -1751,7 +1837,7 @@ export class PieceTreeBase {
return z;
}
getContentOfSubTree(node: TreeNode): string {
private getContentOfSubTree(node: TreeNode): string {
let str = '';
this.iterate(node, node => {

View file

@ -481,6 +481,7 @@ export interface CompletionItem {
export interface CompletionList {
suggestions: CompletionItem[];
incomplete?: boolean;
isDetailsResolved?: boolean;
dispose?(): void;
}

View file

@ -53,9 +53,14 @@ export interface ITextEditorModel extends IEditorModel {
createSnapshot(this: ITextEditorModel): ITextSnapshot | null;
/**
* Signals if this model is readonly or not.
* Signals if this model is readonly or not.
*/
isReadonly(): boolean;
/**
* Figure out if this model is resolved or not.
*/
isResolved(): this is IResolvedTextEditorModel;
}
export interface IResolvedTextEditorModel extends ITextEditorModel {

View file

@ -66,6 +66,7 @@ export class RenderLineInput {
public readonly lineTokens: IViewLineTokens;
public readonly lineDecorations: LineDecoration[];
public readonly tabSize: number;
public readonly startVisibleColumn: number;
public readonly spaceWidth: number;
public readonly stopRenderingLineAfter: number;
public readonly renderWhitespace: RenderWhitespace;
@ -89,6 +90,7 @@ export class RenderLineInput {
lineTokens: IViewLineTokens,
lineDecorations: LineDecoration[],
tabSize: number,
startVisibleColumn: number,
spaceWidth: number,
stopRenderingLineAfter: number,
renderWhitespace: 'none' | 'boundary' | 'selection' | 'all',
@ -106,6 +108,7 @@ export class RenderLineInput {
this.lineTokens = lineTokens;
this.lineDecorations = lineDecorations;
this.tabSize = tabSize;
this.startVisibleColumn = startVisibleColumn;
this.spaceWidth = spaceWidth;
this.stopRenderingLineAfter = stopRenderingLineAfter;
this.renderWhitespace = (
@ -154,6 +157,7 @@ export class RenderLineInput {
&& this.containsRTL === other.containsRTL
&& this.fauxIndentLength === other.fauxIndentLength
&& this.tabSize === other.tabSize
&& this.startVisibleColumn === other.startVisibleColumn
&& this.spaceWidth === other.spaceWidth
&& this.stopRenderingLineAfter === other.stopRenderingLineAfter
&& this.renderWhitespace === other.renderWhitespace
@ -368,7 +372,9 @@ class ResolvedRenderLineInput {
public readonly isOverflowing: boolean,
public readonly parts: LinePart[],
public readonly containsForeignElements: ForeignElementType,
public readonly fauxIndentLength: number,
public readonly tabSize: number,
public readonly startVisibleColumn: number,
public readonly containsRTL: boolean,
public readonly spaceWidth: number,
public readonly renderWhitespace: RenderWhitespace,
@ -395,7 +401,7 @@ function resolveRenderLineInput(input: RenderLineInput): ResolvedRenderLineInput
let tokens = transformAndRemoveOverflowing(input.lineTokens, input.fauxIndentLength, len);
if (input.renderWhitespace === RenderWhitespace.All || input.renderWhitespace === RenderWhitespace.Boundary || (input.renderWhitespace === RenderWhitespace.Selection && !!input.selectionsOnLine)) {
tokens = _applyRenderWhitespace(lineContent, len, input.continuesWithWrappedLine, tokens, input.fauxIndentLength, input.tabSize, useMonospaceOptimizations, input.selectionsOnLine, input.renderWhitespace === RenderWhitespace.Boundary);
tokens = _applyRenderWhitespace(lineContent, len, input.continuesWithWrappedLine, tokens, input.fauxIndentLength, input.tabSize, input.startVisibleColumn, useMonospaceOptimizations, input.selectionsOnLine, input.renderWhitespace === RenderWhitespace.Boundary);
}
let containsForeignElements = ForeignElementType.None;
if (input.lineDecorations.length > 0) {
@ -425,7 +431,9 @@ function resolveRenderLineInput(input: RenderLineInput): ResolvedRenderLineInput
isOverflowing,
tokens,
containsForeignElements,
input.fauxIndentLength,
input.tabSize,
input.startVisibleColumn,
input.containsRTL,
input.spaceWidth,
input.renderWhitespace,
@ -537,7 +545,7 @@ function splitLargeTokens(lineContent: string, tokens: LinePart[], onlyAtSpaces:
* Moreover, a token is created for every visual indent because on some fonts the glyphs used for rendering whitespace (&rarr; or &middot;) do not have the same width as &nbsp;.
* The rendering phase will generate `style="width:..."` for these tokens.
*/
function _applyRenderWhitespace(lineContent: string, len: number, continuesWithWrappedLine: boolean, tokens: LinePart[], fauxIndentLength: number, tabSize: number, useMonospaceOptimizations: boolean, selections: LineRange[] | null, onlyBoundary: boolean): LinePart[] {
function _applyRenderWhitespace(lineContent: string, len: number, continuesWithWrappedLine: boolean, tokens: LinePart[], fauxIndentLength: number, tabSize: number, startVisibleColumn: number, useMonospaceOptimizations: boolean, selections: LineRange[] | null, onlyBoundary: boolean): LinePart[] {
let result: LinePart[] = [], resultLen = 0;
let tokenIndex = 0;
@ -555,21 +563,10 @@ function _applyRenderWhitespace(lineContent: string, len: number, continuesWithW
lastNonWhitespaceIndex = strings.lastNonWhitespaceIndex(lineContent);
}
let tmpIndent = 0;
for (let charIndex = 0; charIndex < fauxIndentLength; charIndex++) {
const chCode = lineContent.charCodeAt(charIndex);
if (chCode === CharCode.Tab) {
tmpIndent = tabSize;
} else if (strings.isFullWidthCharacter(chCode)) {
tmpIndent += 2;
} else {
tmpIndent++;
}
}
tmpIndent = tmpIndent % tabSize;
let wasInWhitespace = false;
let currentSelectionIndex = 0;
let currentSelection = selections && selections[currentSelectionIndex];
let tmpIndent = startVisibleColumn % tabSize;
for (let charIndex = fauxIndentLength; charIndex < len; charIndex++) {
const chCode = lineContent.charCodeAt(charIndex);
@ -729,7 +726,9 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render
const len = input.len;
const isOverflowing = input.isOverflowing;
const parts = input.parts;
const fauxIndentLength = input.fauxIndentLength;
const tabSize = input.tabSize;
const startVisibleColumn = input.startVisibleColumn;
const containsRTL = input.containsRTL;
const spaceWidth = input.spaceWidth;
const renderWhitespace = input.renderWhitespace;
@ -738,7 +737,7 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render
const characterMapping = new CharacterMapping(len + 1, parts.length);
let charIndex = 0;
let tabsCharDelta = 0;
let visibleColumn = startVisibleColumn;
let charOffsetInPart = 0;
let prevPartContentCnt = 0;
@ -764,18 +763,14 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render
let partContentCnt = 0;
{
let _charIndex = charIndex;
let _tabsCharDelta = tabsCharDelta;
let _visibleColumn = visibleColumn;
for (; _charIndex < partEndIndex; _charIndex++) {
const charCode = lineContent.charCodeAt(_charIndex);
if (charCode === CharCode.Tab) {
let insertSpacesCount = tabSize - (_charIndex + _tabsCharDelta) % tabSize;
_tabsCharDelta += insertSpacesCount - 1;
partContentCnt += insertSpacesCount;
} else {
// must be CharCode.Space
partContentCnt++;
const charWidth = (charCode === CharCode.Tab ? (tabSize - (_visibleColumn % tabSize)) : 1) | 0;
partContentCnt += charWidth;
if (_charIndex >= fauxIndentLength) {
_visibleColumn += charWidth;
}
}
}
@ -793,29 +788,30 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render
for (; charIndex < partEndIndex; charIndex++) {
characterMapping.setPartData(charIndex, partIndex, charOffsetInPart, partAbsoluteOffset);
const charCode = lineContent.charCodeAt(charIndex);
let charWidth: number;
if (charCode === CharCode.Tab) {
let insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize;
tabsCharDelta += insertSpacesCount - 1;
charOffsetInPart += insertSpacesCount - 1;
if (insertSpacesCount > 0) {
if (!canUseHalfwidthRightwardsArrow || insertSpacesCount > 1) {
sb.write1(0x2192); // RIGHTWARDS ARROW
} else {
sb.write1(0xFFEB); // HALFWIDTH RIGHTWARDS ARROW
}
insertSpacesCount--;
charWidth = (tabSize - (visibleColumn % tabSize)) | 0;
if (!canUseHalfwidthRightwardsArrow || charWidth > 1) {
sb.write1(0x2192); // RIGHTWARDS ARROW
} else {
sb.write1(0xFFEB); // HALFWIDTH RIGHTWARDS ARROW
}
while (insertSpacesCount > 0) {
for (let space = 2; space <= charWidth; space++) {
sb.write1(0xA0); // &nbsp;
insertSpacesCount--;
}
} else {
// must be CharCode.Space
} else { // must be CharCode.Space
charWidth = 1;
sb.write1(0xB7); // &middot;
}
charOffsetInPart++;
charOffsetInPart += charWidth;
if (charIndex >= fauxIndentLength) {
visibleColumn += charWidth;
}
}
prevPartContentCnt = partContentCnt;
@ -833,63 +829,59 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render
characterMapping.setPartData(charIndex, partIndex, charOffsetInPart, partAbsoluteOffset);
const charCode = lineContent.charCodeAt(charIndex);
let producedCharacters = 1;
let charWidth = 1;
switch (charCode) {
case CharCode.Tab:
let insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize;
tabsCharDelta += insertSpacesCount - 1;
charOffsetInPart += insertSpacesCount - 1;
while (insertSpacesCount > 0) {
producedCharacters = (tabSize - (visibleColumn % tabSize));
charWidth = producedCharacters;
for (let space = 1; space <= producedCharacters; space++) {
sb.write1(0xA0); // &nbsp;
partContentCnt++;
insertSpacesCount--;
}
break;
case CharCode.Space:
sb.write1(0xA0); // &nbsp;
partContentCnt++;
break;
case CharCode.LessThan:
sb.appendASCIIString('&lt;');
partContentCnt++;
break;
case CharCode.GreaterThan:
sb.appendASCIIString('&gt;');
partContentCnt++;
break;
case CharCode.Ampersand:
sb.appendASCIIString('&amp;');
partContentCnt++;
break;
case CharCode.Null:
sb.appendASCIIString('&#00;');
partContentCnt++;
break;
case CharCode.UTF8_BOM:
case CharCode.LINE_SEPARATOR_2028:
sb.write1(0xFFFD);
partContentCnt++;
break;
default:
if (strings.isFullWidthCharacter(charCode)) {
tabsCharDelta++;
charWidth++;
}
if (renderControlCharacters && charCode < 32) {
sb.write1(9216 + charCode);
partContentCnt++;
} else {
sb.write1(charCode);
partContentCnt++;
}
}
charOffsetInPart++;
charOffsetInPart += producedCharacters;
partContentCnt += producedCharacters;
if (charIndex >= fauxIndentLength) {
visibleColumn += charWidth;
}
}
prevPartContentCnt = partContentCnt;

View file

@ -1,293 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CharCode } from 'vs/base/common/charCode';
import * as strings from 'vs/base/common/strings';
import { WrappingIndent } from 'vs/editor/common/config/editorOptions';
import { CharacterClassifier } from 'vs/editor/common/core/characterClassifier';
import { toUint32Array } from 'vs/base/common/uint';
import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer';
import { ILineMapperFactory, ILineMapping, OutputPosition } from 'vs/editor/common/viewModel/splitLinesCollection';
const enum CharacterClass {
NONE = 0,
BREAK_BEFORE = 1,
BREAK_AFTER = 2,
BREAK_OBTRUSIVE = 3,
BREAK_IDEOGRAPHIC = 4 // for Han and Kana.
}
class WrappingCharacterClassifier extends CharacterClassifier<CharacterClass> {
constructor(BREAK_BEFORE: string, BREAK_AFTER: string, BREAK_OBTRUSIVE: string) {
super(CharacterClass.NONE);
for (let i = 0; i < BREAK_BEFORE.length; i++) {
this.set(BREAK_BEFORE.charCodeAt(i), CharacterClass.BREAK_BEFORE);
}
for (let i = 0; i < BREAK_AFTER.length; i++) {
this.set(BREAK_AFTER.charCodeAt(i), CharacterClass.BREAK_AFTER);
}
for (let i = 0; i < BREAK_OBTRUSIVE.length; i++) {
this.set(BREAK_OBTRUSIVE.charCodeAt(i), CharacterClass.BREAK_OBTRUSIVE);
}
}
public get(charCode: number): CharacterClass {
// Initialize CharacterClass.BREAK_IDEOGRAPHIC for these Unicode ranges:
// 1. CJK Unified Ideographs (0x4E00 -- 0x9FFF)
// 2. CJK Unified Ideographs Extension A (0x3400 -- 0x4DBF)
// 3. Hiragana and Katakana (0x3040 -- 0x30FF)
if (
(charCode >= 0x3040 && charCode <= 0x30FF)
|| (charCode >= 0x3400 && charCode <= 0x4DBF)
|| (charCode >= 0x4E00 && charCode <= 0x9FFF)
) {
return CharacterClass.BREAK_IDEOGRAPHIC;
}
return super.get(charCode);
}
}
export class CharacterHardWrappingLineMapperFactory implements ILineMapperFactory {
private readonly classifier: WrappingCharacterClassifier;
constructor(breakBeforeChars: string, breakAfterChars: string, breakObtrusiveChars: string) {
this.classifier = new WrappingCharacterClassifier(breakBeforeChars, breakAfterChars, breakObtrusiveChars);
}
// TODO@Alex -> duplicated in lineCommentCommand
private static nextVisibleColumn(currentVisibleColumn: number, tabSize: number, isTab: boolean, columnSize: number): number {
currentVisibleColumn = +currentVisibleColumn; //@perf
tabSize = +tabSize; //@perf
columnSize = +columnSize; //@perf
if (isTab) {
return currentVisibleColumn + (tabSize - (currentVisibleColumn % tabSize));
}
return currentVisibleColumn + columnSize;
}
public createLineMapping(lineText: string, tabSize: number, breakingColumn: number, columnsForFullWidthChar: number, hardWrappingIndent: WrappingIndent): ILineMapping | null {
if (breakingColumn === -1) {
return null;
}
tabSize = +tabSize; //@perf
breakingColumn = +breakingColumn; //@perf
columnsForFullWidthChar = +columnsForFullWidthChar; //@perf
hardWrappingIndent = +hardWrappingIndent; //@perf
let wrappedTextIndentVisibleColumn = 0;
let wrappedTextIndent = '';
let firstNonWhitespaceIndex = -1;
if (hardWrappingIndent !== WrappingIndent.None) {
firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineText);
if (firstNonWhitespaceIndex !== -1) {
// Track existing indent
wrappedTextIndent = lineText.substring(0, firstNonWhitespaceIndex);
for (let i = 0; i < firstNonWhitespaceIndex; i++) {
wrappedTextIndentVisibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(wrappedTextIndentVisibleColumn, tabSize, lineText.charCodeAt(i) === CharCode.Tab, 1);
}
// Increase indent of continuation lines, if desired
let numberOfAdditionalTabs = 0;
if (hardWrappingIndent === WrappingIndent.Indent) {
numberOfAdditionalTabs = 1;
} else if (hardWrappingIndent === WrappingIndent.DeepIndent) {
numberOfAdditionalTabs = 2;
}
for (let i = 0; i < numberOfAdditionalTabs; i++) {
wrappedTextIndent += '\t';
wrappedTextIndentVisibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(wrappedTextIndentVisibleColumn, tabSize, true, 1);
}
// Force sticking to beginning of line if no character would fit except for the indentation
if (wrappedTextIndentVisibleColumn + columnsForFullWidthChar > breakingColumn) {
wrappedTextIndent = '';
wrappedTextIndentVisibleColumn = 0;
}
}
}
let classifier = this.classifier;
let lastBreakingOffset = 0; // Last 0-based offset in the lineText at which a break happened
let breakingLengths: number[] = []; // The length of each broken-up line text
let breakingLengthsIndex: number = 0; // The count of breaks already done
let visibleColumn = 0; // Visible column since the beginning of the current line
let niceBreakOffset = -1; // Last index of a character that indicates a break should happen before it (more desirable)
let niceBreakVisibleColumn = 0; // visible column if a break were to be later introduced before `niceBreakOffset`
let obtrusiveBreakOffset = -1; // Last index of a character that indicates a break should happen before it (less desirable)
let obtrusiveBreakVisibleColumn = 0; // visible column if a break were to be later introduced before `obtrusiveBreakOffset`
let len = lineText.length;
for (let i = 0; i < len; i++) {
// At this point, there is a certainty that the character before `i` fits on the current line,
// but the character at `i` might not fit
let charCode = lineText.charCodeAt(i);
let charCodeIsTab = (charCode === CharCode.Tab);
let charCodeClass = classifier.get(charCode);
if (strings.isLowSurrogate(charCode)/* && i + 1 < len */) {
// A surrogate pair must always be considered as a single unit, so it is never to be broken
// => advance visibleColumn by 1 and advance to next char code...
visibleColumn = visibleColumn + 1;
continue;
}
if (charCodeClass === CharacterClass.BREAK_BEFORE) {
// This is a character that indicates that a break should happen before it
// Since we are certain the character before `i` fits, there's no extra checking needed,
// just mark it as a nice breaking opportunity
niceBreakOffset = i;
niceBreakVisibleColumn = wrappedTextIndentVisibleColumn;
}
// CJK breaking : before break
if (charCodeClass === CharacterClass.BREAK_IDEOGRAPHIC && i > 0) {
let prevCode = lineText.charCodeAt(i - 1);
let prevClass = classifier.get(prevCode);
if (prevClass !== CharacterClass.BREAK_BEFORE) { // Kinsoku Shori: Don't break after a leading character, like an open bracket
niceBreakOffset = i;
niceBreakVisibleColumn = wrappedTextIndentVisibleColumn;
}
}
let charColumnSize = 1;
if (strings.isFullWidthCharacter(charCode)) {
charColumnSize = columnsForFullWidthChar;
}
// Advance visibleColumn with character at `i`
visibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(visibleColumn, tabSize, charCodeIsTab, charColumnSize);
if (visibleColumn > breakingColumn && i !== 0) {
// We need to break at least before character at `i`:
// - break before niceBreakLastOffset if it exists (and re-establish a correct visibleColumn by using niceBreakVisibleColumn + charAt(i))
// - otherwise, break before obtrusiveBreakLastOffset if it exists (and re-establish a correct visibleColumn by using obtrusiveBreakVisibleColumn + charAt(i))
// - otherwise, break before i (and re-establish a correct visibleColumn by charAt(i))
let breakBeforeOffset: number;
let restoreVisibleColumnFrom: number;
if (niceBreakOffset !== -1 && niceBreakVisibleColumn <= breakingColumn) {
// We will break before `niceBreakLastOffset`
breakBeforeOffset = niceBreakOffset;
restoreVisibleColumnFrom = niceBreakVisibleColumn;
} else if (obtrusiveBreakOffset !== -1 && obtrusiveBreakVisibleColumn <= breakingColumn) {
// We will break before `obtrusiveBreakLastOffset`
breakBeforeOffset = obtrusiveBreakOffset;
restoreVisibleColumnFrom = obtrusiveBreakVisibleColumn;
} else {
// We will break before `i`
breakBeforeOffset = i;
restoreVisibleColumnFrom = wrappedTextIndentVisibleColumn;
}
// Break before character at `breakBeforeOffset`
breakingLengths[breakingLengthsIndex++] = breakBeforeOffset - lastBreakingOffset;
lastBreakingOffset = breakBeforeOffset;
// Re-establish visibleColumn by taking character at `i` into account
visibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(restoreVisibleColumnFrom, tabSize, charCodeIsTab, charColumnSize);
// Reset markers
niceBreakOffset = -1;
niceBreakVisibleColumn = 0;
obtrusiveBreakOffset = -1;
obtrusiveBreakVisibleColumn = 0;
}
// At this point, there is a certainty that the character at `i` fits on the current line
if (niceBreakOffset !== -1) {
// Advance niceBreakVisibleColumn
niceBreakVisibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(niceBreakVisibleColumn, tabSize, charCodeIsTab, charColumnSize);
}
if (obtrusiveBreakOffset !== -1) {
// Advance obtrusiveBreakVisibleColumn
obtrusiveBreakVisibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(obtrusiveBreakVisibleColumn, tabSize, charCodeIsTab, charColumnSize);
}
if (charCodeClass === CharacterClass.BREAK_AFTER && (hardWrappingIndent === WrappingIndent.None || i >= firstNonWhitespaceIndex)) {
// This is a character that indicates that a break should happen after it
niceBreakOffset = i + 1;
niceBreakVisibleColumn = wrappedTextIndentVisibleColumn;
}
// CJK breaking : after break
if (charCodeClass === CharacterClass.BREAK_IDEOGRAPHIC && i < len - 1) {
let nextCode = lineText.charCodeAt(i + 1);
let nextClass = classifier.get(nextCode);
if (nextClass !== CharacterClass.BREAK_AFTER) { // Kinsoku Shori: Don't break before a trailing character, like a period
niceBreakOffset = i + 1;
niceBreakVisibleColumn = wrappedTextIndentVisibleColumn;
}
}
if (charCodeClass === CharacterClass.BREAK_OBTRUSIVE) {
// This is an obtrusive character that indicates that a break should happen after it
obtrusiveBreakOffset = i + 1;
obtrusiveBreakVisibleColumn = wrappedTextIndentVisibleColumn;
}
}
if (breakingLengthsIndex === 0) {
return null;
}
// Add last segment
breakingLengths[breakingLengthsIndex++] = len - lastBreakingOffset;
return new CharacterHardWrappingLineMapping(
new PrefixSumComputer(toUint32Array(breakingLengths)),
wrappedTextIndent
);
}
}
export class CharacterHardWrappingLineMapping implements ILineMapping {
private readonly _prefixSums: PrefixSumComputer;
private readonly _wrappedLinesIndent: string;
constructor(prefixSums: PrefixSumComputer, wrappedLinesIndent: string) {
this._prefixSums = prefixSums;
this._wrappedLinesIndent = wrappedLinesIndent;
}
public getOutputLineCount(): number {
return this._prefixSums.getCount();
}
public getWrappedLinesIndent(): string {
return this._wrappedLinesIndent;
}
public getInputOffsetOfOutputPosition(outputLineIndex: number, outputOffset: number): number {
if (outputLineIndex === 0) {
return outputOffset;
} else {
return this._prefixSums.getAccumulatedValue(outputLineIndex - 1) + outputOffset;
}
}
public getOutputPositionOfInputOffset(inputOffset: number): OutputPosition {
let r = this._prefixSums.getIndexOf(inputOffset);
return new OutputPosition(r.index, r.remainder);
}
}

View file

@ -0,0 +1,468 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CharCode } from 'vs/base/common/charCode';
import * as strings from 'vs/base/common/strings';
import { WrappingIndent, IComputedEditorOptions, EditorOption } from 'vs/editor/common/config/editorOptions';
import { CharacterClassifier } from 'vs/editor/common/core/characterClassifier';
import { ILineBreaksComputerFactory, LineBreakData, ILineBreaksComputer } from 'vs/editor/common/viewModel/splitLinesCollection';
const enum CharacterClass {
NONE = 0,
BREAK_BEFORE = 1,
BREAK_AFTER = 2,
BREAK_IDEOGRAPHIC = 3 // for Han and Kana.
}
class WrappingCharacterClassifier extends CharacterClassifier<CharacterClass> {
constructor(BREAK_BEFORE: string, BREAK_AFTER: string) {
super(CharacterClass.NONE);
for (let i = 0; i < BREAK_BEFORE.length; i++) {
this.set(BREAK_BEFORE.charCodeAt(i), CharacterClass.BREAK_BEFORE);
}
for (let i = 0; i < BREAK_AFTER.length; i++) {
this.set(BREAK_AFTER.charCodeAt(i), CharacterClass.BREAK_AFTER);
}
}
public get(charCode: number): CharacterClass {
if (charCode >= 0 && charCode < 256) {
return <CharacterClass>this._asciiMap[charCode];
} else {
// Initialize CharacterClass.BREAK_IDEOGRAPHIC for these Unicode ranges:
// 1. CJK Unified Ideographs (0x4E00 -- 0x9FFF)
// 2. CJK Unified Ideographs Extension A (0x3400 -- 0x4DBF)
// 3. Hiragana and Katakana (0x3040 -- 0x30FF)
if (
(charCode >= 0x3040 && charCode <= 0x30FF)
|| (charCode >= 0x3400 && charCode <= 0x4DBF)
|| (charCode >= 0x4E00 && charCode <= 0x9FFF)
) {
return CharacterClass.BREAK_IDEOGRAPHIC;
}
return <CharacterClass>(this._map.get(charCode) || this._defaultValue);
}
}
}
export class MonospaceLineBreaksComputerFactory implements ILineBreaksComputerFactory {
public static create(options: IComputedEditorOptions): MonospaceLineBreaksComputerFactory {
return new MonospaceLineBreaksComputerFactory(
options.get(EditorOption.wordWrapBreakBeforeCharacters),
options.get(EditorOption.wordWrapBreakAfterCharacters)
);
}
private readonly classifier: WrappingCharacterClassifier;
constructor(breakBeforeChars: string, breakAfterChars: string) {
this.classifier = new WrappingCharacterClassifier(breakBeforeChars, breakAfterChars);
}
public createLineBreaksComputer(tabSize: number, wrappingColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent): ILineBreaksComputer {
tabSize = tabSize | 0; //@perf
wrappingColumn = +wrappingColumn; //@perf
columnsForFullWidthChar = +columnsForFullWidthChar; //@perf
let requests: string[] = [];
let previousBreakingData: (LineBreakData | null)[] = [];
return {
addRequest: (lineText: string, previousLineBreakData: LineBreakData | null) => {
requests.push(lineText);
previousBreakingData.push(previousLineBreakData);
},
finalize: () => {
let result: (LineBreakData | null)[] = [];
for (let i = 0, len = requests.length; i < len; i++) {
const previousLineBreakData = previousBreakingData[i];
if (previousLineBreakData) {
result[i] = createLineBreaksFromPreviousLineBreaks(this.classifier, previousLineBreakData, requests[i], tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent);
} else {
result[i] = createLineBreaks(this.classifier, requests[i], tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent);
}
}
return result;
}
};
}
}
let arrPool1: number[] = [];
let arrPool2: number[] = [];
function createLineBreaksFromPreviousLineBreaks(classifier: WrappingCharacterClassifier, previousBreakingData: LineBreakData, lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent): LineBreakData | null {
if (firstLineBreakColumn === -1) {
return null;
}
const len = lineText.length;
if (len <= 1) {
return null;
}
const prevBreakingOffsets = previousBreakingData.breakOffsets;
const prevBreakingOffsetsVisibleColumn = previousBreakingData.breakOffsetsVisibleColumn;
const wrappedTextIndentLength = computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent);
const wrappedLineBreakColumn = firstLineBreakColumn - wrappedTextIndentLength;
let breakingOffsets: number[] = arrPool1;
let breakingOffsetsVisibleColumn: number[] = arrPool2;
let breakingOffsetsCount: number = 0;
let breakingColumn = firstLineBreakColumn;
const prevLen = prevBreakingOffsets.length;
let prevIndex = 0;
if (prevIndex >= 0) {
let bestDistance = Math.abs(prevBreakingOffsetsVisibleColumn[prevIndex] - breakingColumn);
while (prevIndex + 1 < prevLen) {
const distance = Math.abs(prevBreakingOffsetsVisibleColumn[prevIndex + 1] - breakingColumn);
if (distance >= bestDistance) {
break;
}
bestDistance = distance;
prevIndex++;
}
}
while (prevIndex < prevLen) {
// Allow for prevIndex to be -1 (for the case where we hit a tab when walking backwards from the first break)
const prevBreakOffset = prevIndex < 0 ? 0 : prevBreakingOffsets[prevIndex];
const prevBreakoffsetVisibleColumn = prevIndex < 0 ? 0 : prevBreakingOffsetsVisibleColumn[prevIndex];
let breakOffset = 0;
let breakOffsetVisibleColumn = 0;
let forcedBreakOffset = 0;
let forcedBreakOffsetVisibleColumn = 0;
// initially, we search as much as possible to the right (if it fits)
if (prevBreakoffsetVisibleColumn <= breakingColumn) {
let visibleColumn = prevBreakoffsetVisibleColumn;
let prevCharCode = lineText.charCodeAt(prevBreakOffset - 1);
let prevCharCodeClass = classifier.get(prevCharCode);
let entireLineFits = true;
for (let i = prevBreakOffset; i < len; i++) {
const charStartOffset = i;
const charCode = lineText.charCodeAt(i);
let charCodeClass: number;
let charWidth: number;
if (strings.isHighSurrogate(charCode)) {
// A surrogate pair must always be considered as a single unit, so it is never to be broken
i++;
charCodeClass = CharacterClass.NONE;
charWidth = 2;
} else {
charCodeClass = classifier.get(charCode);
charWidth = computeCharWidth(charCode, visibleColumn, tabSize, columnsForFullWidthChar);
}
if (canBreak(prevCharCode, prevCharCodeClass, charCode, charCodeClass)) {
breakOffset = charStartOffset;
breakOffsetVisibleColumn = visibleColumn;
}
visibleColumn += charWidth;
// check if adding character at `i` will go over the breaking column
if (visibleColumn > breakingColumn) {
// We need to break at least before character at `i`:
forcedBreakOffset = charStartOffset;
forcedBreakOffsetVisibleColumn = visibleColumn - charWidth;
if (visibleColumn - breakOffsetVisibleColumn > wrappedLineBreakColumn) {
// Cannot break at `breakOffset` => reset it if it was set
breakOffset = 0;
}
entireLineFits = false;
break;
}
prevCharCode = charCode;
prevCharCodeClass = charCodeClass;
}
if (entireLineFits) {
// there is no more need to break => stop the outer loop!
if (breakingOffsetsCount > 0) {
// Add last segment
breakingOffsets[breakingOffsetsCount] = prevBreakingOffsets[prevBreakingOffsets.length - 1];
breakingOffsetsVisibleColumn[breakingOffsetsCount] = prevBreakingOffsetsVisibleColumn[prevBreakingOffsets.length - 1];
breakingOffsetsCount++;
}
break;
}
}
if (breakOffset === 0) {
// must search left
let visibleColumn = prevBreakoffsetVisibleColumn;
let charCode = lineText.charCodeAt(prevBreakOffset);
let charCodeClass = classifier.get(charCode);
let hitATabCharacter = false;
for (let i = prevBreakOffset - 1; i >= 0; i--) {
const charStartOffset = i + 1;
const prevCharCode = lineText.charCodeAt(i);
if (prevCharCode === CharCode.Tab) {
// cannot determine the width of a tab when going backwards, so we must go forwards
hitATabCharacter = true;
break;
}
let prevCharCodeClass: number;
let prevCharWidth: number;
if (strings.isLowSurrogate(prevCharCode)) {
// A surrogate pair must always be considered as a single unit, so it is never to be broken
i--;
prevCharCodeClass = CharacterClass.NONE;
prevCharWidth = 2;
} else {
prevCharCodeClass = classifier.get(prevCharCode);
prevCharWidth = (strings.isFullWidthCharacter(prevCharCode) ? columnsForFullWidthChar : 1);
}
if (visibleColumn <= breakingColumn) {
if (forcedBreakOffset === 0) {
forcedBreakOffset = charStartOffset;
forcedBreakOffsetVisibleColumn = visibleColumn;
}
if (visibleColumn <= breakingColumn - wrappedLineBreakColumn) {
// went too far!
break;
}
if (canBreak(prevCharCode, prevCharCodeClass, charCode, charCodeClass)) {
breakOffset = charStartOffset;
breakOffsetVisibleColumn = visibleColumn;
break;
}
}
visibleColumn -= prevCharWidth;
charCode = prevCharCode;
charCodeClass = prevCharCodeClass;
}
if (breakOffset !== 0) {
const remainingWidthOfNextLine = wrappedLineBreakColumn - (forcedBreakOffsetVisibleColumn - breakOffsetVisibleColumn);
if (remainingWidthOfNextLine <= tabSize) {
const charCodeAtForcedBreakOffset = lineText.charCodeAt(forcedBreakOffset);
let charWidth: number;
if (strings.isHighSurrogate(charCodeAtForcedBreakOffset)) {
// A surrogate pair must always be considered as a single unit, so it is never to be broken
charWidth = 2;
} else {
charWidth = computeCharWidth(charCodeAtForcedBreakOffset, forcedBreakOffsetVisibleColumn, tabSize, columnsForFullWidthChar);
}
if (remainingWidthOfNextLine - charWidth < 0) {
// it is not worth it to break at breakOffset, it just introduces an extra needless line!
breakOffset = 0;
}
}
}
if (hitATabCharacter) {
// cannot determine the width of a tab when going backwards, so we must go forwards from the previous break
prevIndex--;
continue;
}
}
if (breakOffset === 0) {
// Could not find a good breaking point
breakOffset = forcedBreakOffset;
breakOffsetVisibleColumn = forcedBreakOffsetVisibleColumn;
}
breakingOffsets[breakingOffsetsCount] = breakOffset;
breakingOffsetsVisibleColumn[breakingOffsetsCount] = breakOffsetVisibleColumn;
breakingOffsetsCount++;
breakingColumn = breakOffsetVisibleColumn + wrappedLineBreakColumn;
while (prevIndex < 0 || (prevIndex < prevLen && prevBreakingOffsetsVisibleColumn[prevIndex] < breakOffsetVisibleColumn)) {
prevIndex++;
}
let bestDistance = Math.abs(prevBreakingOffsetsVisibleColumn[prevIndex] - breakingColumn);
while (prevIndex + 1 < prevLen) {
const distance = Math.abs(prevBreakingOffsetsVisibleColumn[prevIndex + 1] - breakingColumn);
if (distance >= bestDistance) {
break;
}
bestDistance = distance;
prevIndex++;
}
}
if (breakingOffsetsCount === 0) {
return null;
}
// Doing here some object reuse which ends up helping a huge deal with GC pauses!
breakingOffsets.length = breakingOffsetsCount;
breakingOffsetsVisibleColumn.length = breakingOffsetsCount;
arrPool1 = previousBreakingData.breakOffsets;
arrPool2 = previousBreakingData.breakOffsetsVisibleColumn;
previousBreakingData.breakOffsets = breakingOffsets;
previousBreakingData.breakOffsetsVisibleColumn = breakingOffsetsVisibleColumn;
previousBreakingData.wrappedTextIndentLength = wrappedTextIndentLength;
return previousBreakingData;
}
function createLineBreaks(classifier: WrappingCharacterClassifier, lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent): LineBreakData | null {
if (firstLineBreakColumn === -1) {
return null;
}
const len = lineText.length;
if (len <= 1) {
return null;
}
const wrappedTextIndentLength = computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent);
const wrappedLineBreakColumn = firstLineBreakColumn - wrappedTextIndentLength;
let breakingOffsets: number[] = [];
let breakingOffsetsVisibleColumn: number[] = [];
let breakingOffsetsCount: number = 0;
let breakOffset = 0;
let breakOffsetVisibleColumn = 0;
let breakingColumn = firstLineBreakColumn;
let prevCharCode = lineText.charCodeAt(0);
let prevCharCodeClass = classifier.get(prevCharCode);
let visibleColumn = computeCharWidth(prevCharCode, 0, tabSize, columnsForFullWidthChar);
let startOffset = 1;
if (strings.isHighSurrogate(prevCharCode)) {
// A surrogate pair must always be considered as a single unit, so it is never to be broken
visibleColumn += 1;
prevCharCode = lineText.charCodeAt(1);
prevCharCodeClass = classifier.get(prevCharCode);
startOffset++;
}
for (let i = startOffset; i < len; i++) {
const charStartOffset = i;
const charCode = lineText.charCodeAt(i);
let charCodeClass: number;
let charWidth: number;
if (strings.isHighSurrogate(charCode)) {
// A surrogate pair must always be considered as a single unit, so it is never to be broken
i++;
charCodeClass = CharacterClass.NONE;
charWidth = 2;
} else {
charCodeClass = classifier.get(charCode);
charWidth = computeCharWidth(charCode, visibleColumn, tabSize, columnsForFullWidthChar);
}
if (canBreak(prevCharCode, prevCharCodeClass, charCode, charCodeClass)) {
breakOffset = charStartOffset;
breakOffsetVisibleColumn = visibleColumn;
}
visibleColumn += charWidth;
// check if adding character at `i` will go over the breaking column
if (visibleColumn > breakingColumn) {
// We need to break at least before character at `i`:
if (breakOffset === 0 || visibleColumn - breakOffsetVisibleColumn > wrappedLineBreakColumn) {
// Cannot break at `breakOffset`, must break at `i`
breakOffset = charStartOffset;
breakOffsetVisibleColumn = visibleColumn - charWidth;
}
breakingOffsets[breakingOffsetsCount] = breakOffset;
breakingOffsetsVisibleColumn[breakingOffsetsCount] = breakOffsetVisibleColumn;
breakingOffsetsCount++;
breakingColumn = breakOffsetVisibleColumn + wrappedLineBreakColumn;
breakOffset = 0;
}
prevCharCode = charCode;
prevCharCodeClass = charCodeClass;
}
if (breakingOffsetsCount === 0) {
return null;
}
// Add last segment
breakingOffsets[breakingOffsetsCount] = len;
breakingOffsetsVisibleColumn[breakingOffsetsCount] = visibleColumn;
return new LineBreakData(breakingOffsets, breakingOffsetsVisibleColumn, wrappedTextIndentLength);
}
function computeCharWidth(charCode: number, visibleColumn: number, tabSize: number, columnsForFullWidthChar: number): number {
if (charCode === CharCode.Tab) {
return (tabSize - (visibleColumn % tabSize));
}
if (strings.isFullWidthCharacter(charCode)) {
return columnsForFullWidthChar;
}
return 1;
}
function tabCharacterWidth(visibleColumn: number, tabSize: number): number {
return (tabSize - (visibleColumn % tabSize));
}
/**
* Kinsoku Shori : Don't break after a leading character, like an open bracket
* Kinsoku Shori : Don't break before a trailing character, like a period
*/
function canBreak(prevCharCode: number, prevCharCodeClass: CharacterClass, charCode: number, charCodeClass: CharacterClass): boolean {
return (
charCode !== CharCode.Space
&& (
(prevCharCodeClass === CharacterClass.BREAK_AFTER)
|| (prevCharCodeClass === CharacterClass.BREAK_IDEOGRAPHIC && charCodeClass !== CharacterClass.BREAK_AFTER)
|| (charCodeClass === CharacterClass.BREAK_BEFORE)
|| (charCodeClass === CharacterClass.BREAK_IDEOGRAPHIC && prevCharCodeClass !== CharacterClass.BREAK_BEFORE)
)
);
}
function computeWrappedTextIndentLength(lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent): number {
let wrappedTextIndentLength = 0;
if (wrappingIndent !== WrappingIndent.None) {
const firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineText);
if (firstNonWhitespaceIndex !== -1) {
// Track existing indent
for (let i = 0; i < firstNonWhitespaceIndex; i++) {
const charWidth = (lineText.charCodeAt(i) === CharCode.Tab ? tabCharacterWidth(wrappedTextIndentLength, tabSize) : 1);
wrappedTextIndentLength += charWidth;
}
// Increase indent of continuation lines, if desired
const numberOfAdditionalTabs = (wrappingIndent === WrappingIndent.DeepIndent ? 2 : wrappingIndent === WrappingIndent.Indent ? 1 : 0);
for (let i = 0; i < numberOfAdditionalTabs; i++) {
const charWidth = tabCharacterWidth(wrappedTextIndentLength, tabSize);
wrappedTextIndentLength += charWidth;
}
// Force sticking to beginning of line if no character would fit except for the indentation
if (wrappedTextIndentLength + columnsForFullWidthChar > firstLineBreakColumn) {
wrappedTextIndentLength = 0;
}
}
}
return wrappedTextIndentLength;
}

View file

@ -187,73 +187,3 @@ export class PrefixSumComputer {
return new PrefixSumIndexOfResult(mid, accumulatedValue - midStart);
}
}
export class PrefixSumComputerWithCache {
private readonly _actual: PrefixSumComputer;
private _cacheAccumulatedValueStart: number = 0;
private _cache: PrefixSumIndexOfResult[] | null = null;
constructor(values: Uint32Array) {
this._actual = new PrefixSumComputer(values);
this._bustCache();
}
private _bustCache(): void {
this._cacheAccumulatedValueStart = 0;
this._cache = null;
}
public insertValues(insertIndex: number, insertValues: Uint32Array): void {
if (this._actual.insertValues(insertIndex, insertValues)) {
this._bustCache();
}
}
public changeValue(index: number, value: number): void {
if (this._actual.changeValue(index, value)) {
this._bustCache();
}
}
public removeValues(startIndex: number, cnt: number): void {
if (this._actual.removeValues(startIndex, cnt)) {
this._bustCache();
}
}
public getTotalValue(): number {
return this._actual.getTotalValue();
}
public getAccumulatedValue(index: number): number {
return this._actual.getAccumulatedValue(index);
}
public getIndexOf(accumulatedValue: number): PrefixSumIndexOfResult {
accumulatedValue = Math.floor(accumulatedValue); //@perf
if (this._cache !== null) {
let cacheIndex = accumulatedValue - this._cacheAccumulatedValueStart;
if (cacheIndex >= 0 && cacheIndex < this._cache.length) {
// Cache hit!
return this._cache[cacheIndex];
}
}
// Cache miss!
return this._actual.getIndexOf(accumulatedValue);
}
/**
* Gives a hint that a lot of requests are about to come in for these accumulated values.
*/
public warmUpCache(accumulatedValueStart: number, accumulatedValueEnd: number): void {
let newCache: PrefixSumIndexOfResult[] = [];
for (let accumulatedValue = accumulatedValueStart; accumulatedValue <= accumulatedValueEnd; accumulatedValue++) {
newCache[accumulatedValue - accumulatedValueStart] = this.getIndexOf(accumulatedValue);
}
this._cache = newCache;
this._cacheAccumulatedValueStart = accumulatedValueStart;
}
}

View file

@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
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 { Position } from 'vs/editor/common/core/position';
@ -10,13 +11,12 @@ import { IRange, Range } from 'vs/editor/common/core/range';
import { EndOfLinePreference, IActiveIndentGuideInfo, IModelDecoration, IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model';
import { ModelDecorationOptions, ModelDecorationOverviewRulerOptions } from 'vs/editor/common/model/textModel';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
import { PrefixSumComputerWithCache } from 'vs/editor/common/viewModel/prefixSumComputer';
import { PrefixSumIndexOfResult } from 'vs/editor/common/viewModel/prefixSumComputer';
import { ICoordinatesConverter, IOverviewRulerDecorations, ViewLineData } from 'vs/editor/common/viewModel/viewModel';
import { ITheme } from 'vs/platform/theme/common/themeService';
import { IDisposable } from 'vs/base/common/lifecycle';
export class OutputPosition {
_outputPositionBrand: void;
outputLineIndex: number;
outputOffset: number;
@ -26,15 +26,56 @@ export class OutputPosition {
}
}
export interface ILineMapping {
getOutputLineCount(): number;
getWrappedLinesIndent(): string;
getInputOffsetOfOutputPosition(outputLineIndex: number, outputOffset: number): number;
getOutputPositionOfInputOffset(inputOffset: number): OutputPosition;
export class LineBreakData {
constructor(
public breakOffsets: number[],
public breakOffsetsVisibleColumn: number[],
public wrappedTextIndentLength: number
) { }
public static getInputOffsetOfOutputPosition(breakOffsets: number[], outputLineIndex: number, outputOffset: number): number {
if (outputLineIndex === 0) {
return outputOffset;
} else {
return breakOffsets[outputLineIndex - 1] + outputOffset;
}
}
public static getOutputPositionOfInputOffset(breakOffsets: number[], inputOffset: number): OutputPosition {
let low = 0;
let high = 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;
if (inputOffset < midStart) {
high = mid - 1;
} else if (inputOffset >= midStop) {
low = mid + 1;
} else {
break;
}
}
return new OutputPosition(mid, inputOffset - midStart);
}
}
export interface ILineMapperFactory {
createLineMapping(lineText: string, tabSize: number, wrappingColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent): ILineMapping | null;
export interface ILineBreaksComputer {
/**
* Pass in `previousLineBreakData` if the only difference is in breaking columns!!!
*/
addRequest(lineText: string, previousLineBreakData: LineBreakData | null): void;
finalize(): (LineBreakData | null)[];
}
export interface ILineBreaksComputerFactory {
createLineBreaksComputer(tabSize: number, wrappingColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent): ILineBreaksComputer;
}
export interface ISimpleModel {
@ -50,6 +91,7 @@ export interface ISplitLine {
isVisible(): boolean;
setVisible(isVisible: boolean): ISplitLine;
getLineBreakData(): LineBreakData | null;
getViewLineCount(): number;
getViewLineContent(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): string;
getViewLineLength(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number;
@ -71,14 +113,14 @@ export interface IViewModelLinesCollection extends IDisposable {
getHiddenAreas(): Range[];
setHiddenAreas(_ranges: Range[]): boolean;
createLineBreaksComputer(): ILineBreaksComputer;
onModelFlushed(): void;
onModelLinesDeleted(versionId: number, fromLineNumber: number, toLineNumber: number): viewEvents.ViewLinesDeletedEvent | null;
onModelLinesInserted(versionId: number, fromLineNumber: number, toLineNumber: number, text: string[]): viewEvents.ViewLinesInsertedEvent | null;
onModelLineChanged(versionId: number, lineNumber: number, newText: string): [boolean, viewEvents.ViewLinesChangedEvent | null, viewEvents.ViewLinesInsertedEvent | null, 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];
acceptVersionId(versionId: number): void;
getViewLineCount(): number;
warmUpLookupCache(viewStartLineNumber: number, viewEndLineNumber: number): void;
getActiveIndentGuide(viewLineNumber: number, minLineNumber: number, maxLineNumber: number): IActiveIndentGuideInfo;
getViewLinesIndentGuides(viewStartLineNumber: number, viewEndLineNumber: number): number[];
getViewLineContent(viewLineNumber: number): string;
@ -107,9 +149,7 @@ export class CoordinatesConverter implements ICoordinatesConverter {
}
public convertViewRangeToModelRange(viewRange: Range): Range {
let start = this._lines.convertViewPositionToModelPosition(viewRange.startLineNumber, viewRange.startColumn);
let end = this._lines.convertViewPositionToModelPosition(viewRange.endLineNumber, viewRange.endColumn);
return new Range(start.lineNumber, start.column, end.lineNumber, end.column);
return this._lines.convertViewRangeToModelRange(viewRange);
}
public validateViewPosition(viewPosition: Position, expectedModelPosition: Position): Position {
@ -117,9 +157,7 @@ export class CoordinatesConverter implements ICoordinatesConverter {
}
public validateViewRange(viewRange: Range, expectedModelRange: Range): Range {
const validViewStart = this._lines.validateViewPosition(viewRange.startLineNumber, viewRange.startColumn, expectedModelRange.getStartPosition());
const validViewEnd = this._lines.validateViewPosition(viewRange.endLineNumber, viewRange.endColumn, expectedModelRange.getEndPosition());
return new Range(validViewStart.lineNumber, validViewStart.column, validViewEnd.lineNumber, validViewEnd.column);
return this._lines.validateViewRange(viewRange, expectedModelRange);
}
// Model -> View conversion and related methods
@ -135,7 +173,6 @@ export class CoordinatesConverter implements ICoordinatesConverter {
public modelPositionIsVisible(modelPosition: Position): boolean {
return this._lines.modelPositionIsVisible(modelPosition.lineNumber, modelPosition.column);
}
}
const enum IndentGuideRepeatOption {
@ -144,6 +181,89 @@ const enum IndentGuideRepeatOption {
BlockAll = 2
}
class LineNumberMapper {
private _counts: number[];
private _isValid: boolean;
private _validEndIndex: number;
private _modelToView: number[];
private _viewToModel: number[];
constructor(viewLineCounts: number[]) {
this._counts = viewLineCounts;
this._isValid = false;
this._validEndIndex = -1;
this._modelToView = [];
this._viewToModel = [];
}
private _invalidate(index: number): void {
this._isValid = false;
this._validEndIndex = Math.min(this._validEndIndex, index - 1);
}
private _ensureValid(): void {
if (this._isValid) {
return;
}
for (let i = this._validEndIndex + 1, len = this._counts.length; i < len; i++) {
const viewLineCount = this._counts[i];
const viewLinesAbove = (i > 0 ? this._modelToView[i - 1] : 0);
this._modelToView[i] = viewLinesAbove + viewLineCount;
for (let j = 0; j < viewLineCount; j++) {
this._viewToModel[viewLinesAbove + j] = i;
}
}
// trim things
this._modelToView.length = this._counts.length;
this._viewToModel.length = this._modelToView[this._modelToView.length - 1];
// mark as valid
this._isValid = true;
this._validEndIndex = this._counts.length - 1;
}
public changeValue(index: number, value: number): void {
if (this._counts[index] === value) {
// no change
return;
}
this._counts[index] = value;
this._invalidate(index);
}
public removeValues(start: number, deleteCount: number): void {
this._counts.splice(start, deleteCount);
this._invalidate(start);
}
public insertValues(insertIndex: number, insertArr: number[]): void {
this._counts = arrays.arrayInsert(this._counts, insertIndex, insertArr);
this._invalidate(insertIndex);
}
public getTotalValue(): number {
this._ensureValid();
return this._viewToModel.length;
}
public getAccumulatedValue(index: number): number {
this._ensureValid();
return this._modelToView[index];
}
public getIndexOf(accumulatedValue: number): PrefixSumIndexOfResult {
this._ensureValid();
const modelLineIndex = this._viewToModel[accumulatedValue];
const viewLinesAbove = (modelLineIndex > 0 ? this._modelToView[modelLineIndex - 1] : 0);
return new PrefixSumIndexOfResult(modelLineIndex, accumulatedValue - viewLinesAbove);
}
}
export class SplitLinesCollection implements IViewModelLinesCollection {
private readonly model: ITextModel;
@ -155,13 +275,13 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
private tabSize: number;
private lines!: ISplitLine[];
private prefixSumComputer!: PrefixSumComputerWithCache;
private prefixSumComputer!: LineNumberMapper;
private readonly linePositionMapperFactory: ILineMapperFactory;
private readonly linePositionMapperFactory: ILineBreaksComputerFactory;
private hiddenAreasIds!: string[];
constructor(model: ITextModel, linePositionMapperFactory: ILineMapperFactory, tabSize: number, wrappingColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent) {
constructor(model: ITextModel, linePositionMapperFactory: ILineBreaksComputerFactory, tabSize: number, wrappingColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent) {
this.model = model;
this._validModelVersionId = -1;
this.tabSize = tabSize;
@ -170,7 +290,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
this.wrappingIndent = wrappingIndent;
this.linePositionMapperFactory = linePositionMapperFactory;
this._constructLines(true);
this._constructLines(/*resetHiddenAreas*/true, null);
}
public dispose(): void {
@ -181,19 +301,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
return new CoordinatesConverter(this);
}
private _ensureValidState(): void {
let modelVersion = this.model.getVersionId();
if (modelVersion !== this._validModelVersionId) {
// This is pretty bad, it means we lost track of the model...
throw new Error(`ViewModel is out of sync with Model!`);
}
if (this.lines.length !== this.model.getLineCount()) {
// This is pretty bad, it means we lost track of the model...
this._constructLines(false);
}
}
private _constructLines(resetHiddenAreas: boolean): void {
private _constructLines(resetHiddenAreas: boolean, previousLineBreaks: ((LineBreakData | null)[]) | null): void {
this.lines = [];
if (resetHiddenAreas) {
@ -201,8 +309,14 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
}
let linesContent = this.model.getLinesContent();
let lineCount = linesContent.length;
let values = new Uint32Array(lineCount);
const lineCount = linesContent.length;
const lineBreaksComputer = this.linePositionMapperFactory.createLineBreaksComputer(this.tabSize, this.wrappingColumn, this.columnsForFullWidthChar, this.wrappingIndent);
for (let i = 0; i < lineCount; i++) {
lineBreaksComputer.addRequest(linesContent[i], previousLineBreaks ? previousLineBreaks[i] : null);
}
const linesBreaks = lineBreaksComputer.finalize();
let values: number[] = [];
let hiddenAreas = this.hiddenAreasIds.map((areaId) => this.model.getDecorationRange(areaId)!).sort(Range.compareRangesUsingStarts);
let hiddenAreaStart = 1, hiddenAreaEnd = 0;
@ -220,14 +334,14 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
}
let isInHiddenArea = (lineNumber >= hiddenAreaStart && lineNumber <= hiddenAreaEnd);
let line = createSplitLine(this.linePositionMapperFactory, linesContent[i], this.tabSize, this.wrappingColumn, this.columnsForFullWidthChar, this.wrappingIndent, !isInHiddenArea);
let line = createSplitLine(linesBreaks[i], !isInHiddenArea);
values[i] = line.getViewLineCount();
this.lines[i] = line;
}
this._validModelVersionId = this.model.getVersionId();
this.prefixSumComputer = new PrefixSumComputerWithCache(values);
this.prefixSumComputer = new LineNumberMapper(values);
}
public getHiddenAreas(): Range[] {
@ -351,7 +465,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
}
this.tabSize = newTabSize;
this._constructLines(false);
this._constructLines(/*resetHiddenAreas*/false, null);
return true;
}
@ -361,17 +475,31 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
return false;
}
const onlyWrappingColumnChanged = (this.wrappingIndent === wrappingIndent && this.wrappingColumn !== wrappingColumn && this.columnsForFullWidthChar === columnsForFullWidthChar);
this.wrappingIndent = wrappingIndent;
this.wrappingColumn = wrappingColumn;
this.columnsForFullWidthChar = columnsForFullWidthChar;
this._constructLines(false);
let previousLineBreaks: ((LineBreakData | null)[]) | null = null;
if (onlyWrappingColumnChanged) {
previousLineBreaks = [];
for (let i = 0, len = this.lines.length; i < len; i++) {
previousLineBreaks[i] = this.lines[i].getLineBreakData();
}
}
this._constructLines(/*resetHiddenAreas*/false, previousLineBreaks);
return true;
}
public createLineBreaksComputer(): ILineBreaksComputer {
return this.linePositionMapperFactory.createLineBreaksComputer(this.tabSize, this.wrappingColumn, this.columnsForFullWidthChar, this.wrappingIndent);
}
public onModelFlushed(): void {
this._constructLines(true);
this._constructLines(/*resetHiddenAreas*/true, null);
}
public onModelLinesDeleted(versionId: number, fromLineNumber: number, toLineNumber: number): viewEvents.ViewLinesDeletedEvent | null {
@ -390,7 +518,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
return new viewEvents.ViewLinesDeletedEvent(outputFromLineNumber, outputToLineNumber);
}
public onModelLinesInserted(versionId: number, fromLineNumber: number, _toLineNumber: number, text: string[]): viewEvents.ViewLinesInsertedEvent | null {
public onModelLinesInserted(versionId: number, fromLineNumber: number, _toLineNumber: number, lineBreaks: (LineBreakData | null)[]): viewEvents.ViewLinesInsertedEvent | null {
if (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.
@ -411,10 +539,10 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
let totalOutputLineCount = 0;
let insertLines: ISplitLine[] = [];
let insertPrefixSumValues = new Uint32Array(text.length);
let insertPrefixSumValues: number[] = [];
for (let i = 0, len = text.length; i < len; i++) {
let line = createSplitLine(this.linePositionMapperFactory, text[i], this.tabSize, this.wrappingColumn, this.columnsForFullWidthChar, this.wrappingIndent, !isInHiddenArea);
for (let i = 0, len = lineBreaks.length; i < len; i++) {
let line = createSplitLine(lineBreaks[i], !isInHiddenArea);
insertLines.push(line);
let outputLineCount = line.getViewLineCount();
@ -430,7 +558,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
return new viewEvents.ViewLinesInsertedEvent(outputFromLineNumber, outputFromLineNumber + totalOutputLineCount - 1);
}
public onModelLineChanged(versionId: number, lineNumber: number, newText: string): [boolean, viewEvents.ViewLinesChangedEvent | null, viewEvents.ViewLinesInsertedEvent | null, viewEvents.ViewLinesDeletedEvent | null] {
public onModelLineChanged(versionId: number, lineNumber: number, lineBreakData: LineBreakData | null): [boolean, viewEvents.ViewLinesChangedEvent | null, viewEvents.ViewLinesInsertedEvent | null, viewEvents.ViewLinesDeletedEvent | null] {
if (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.
@ -441,7 +569,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
let oldOutputLineCount = this.lines[lineIndex].getViewLineCount();
let isVisible = this.lines[lineIndex].isVisible();
let line = createSplitLine(this.linePositionMapperFactory, newText, this.tabSize, this.wrappingColumn, this.columnsForFullWidthChar, this.wrappingIndent, isVisible);
let line = createSplitLine(lineBreakData, isVisible);
this.lines[lineIndex] = line;
let newOutputLineCount = this.lines[lineIndex].getViewLineCount();
@ -488,7 +616,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
}
public getViewLineCount(): number {
this._ensureValidState();
return this.prefixSumComputer.getTotalValue();
}
@ -496,22 +623,14 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
if (viewLineNumber < 1) {
return 1;
}
let viewLineCount = this.getViewLineCount();
const viewLineCount = this.getViewLineCount();
if (viewLineNumber > viewLineCount) {
return viewLineCount;
}
return viewLineNumber;
}
/**
* Gives a hint that a lot of requests are about to come in for these line numbers.
*/
public warmUpLookupCache(viewStartLineNumber: number, viewEndLineNumber: number): void {
this.prefixSumComputer.warmUpCache(viewStartLineNumber - 1, viewEndLineNumber - 1);
return viewLineNumber | 0;
}
public getActiveIndentGuide(viewLineNumber: number, minLineNumber: number, maxLineNumber: number): IActiveIndentGuideInfo {
this._ensureValidState();
viewLineNumber = this._toValidViewLineNumber(viewLineNumber);
minLineNumber = this._toValidViewLineNumber(minLineNumber);
maxLineNumber = this._toValidViewLineNumber(maxLineNumber);
@ -531,7 +650,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
}
public getViewLinesIndentGuides(viewStartLineNumber: number, viewEndLineNumber: number): number[] {
this._ensureValidState();
viewStartLineNumber = this._toValidViewLineNumber(viewStartLineNumber);
viewEndLineNumber = this._toValidViewLineNumber(viewEndLineNumber);
@ -602,7 +720,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
}
public getViewLineContent(viewLineNumber: number): string {
this._ensureValidState();
viewLineNumber = this._toValidViewLineNumber(viewLineNumber);
let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1);
let lineIndex = r.index;
@ -612,7 +729,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
}
public getViewLineLength(viewLineNumber: number): number {
this._ensureValidState();
viewLineNumber = this._toValidViewLineNumber(viewLineNumber);
let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1);
let lineIndex = r.index;
@ -622,7 +738,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
}
public getViewLineMinColumn(viewLineNumber: number): number {
this._ensureValidState();
viewLineNumber = this._toValidViewLineNumber(viewLineNumber);
let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1);
let lineIndex = r.index;
@ -632,7 +747,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
}
public getViewLineMaxColumn(viewLineNumber: number): number {
this._ensureValidState();
viewLineNumber = this._toValidViewLineNumber(viewLineNumber);
let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1);
let lineIndex = r.index;
@ -642,7 +756,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
}
public getViewLineData(viewLineNumber: number): ViewLineData {
this._ensureValidState();
viewLineNumber = this._toValidViewLineNumber(viewLineNumber);
let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1);
let lineIndex = r.index;
@ -652,7 +765,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
}
public getViewLinesData(viewStartLineNumber: number, viewEndLineNumber: number, needed: boolean[]): ViewLineData[] {
this._ensureValidState();
viewStartLineNumber = this._toValidViewLineNumber(viewStartLineNumber);
viewEndLineNumber = this._toValidViewLineNumber(viewEndLineNumber);
@ -691,7 +803,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
}
public validateViewPosition(viewLineNumber: number, viewColumn: number, expectedModelPosition: Position): Position {
this._ensureValidState();
viewLineNumber = this._toValidViewLineNumber(viewLineNumber);
let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1);
@ -719,8 +830,13 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
return this.convertModelPositionToViewPosition(expectedModelPosition.lineNumber, expectedModelPosition.column);
}
public validateViewRange(viewRange: Range, expectedModelRange: Range): Range {
const validViewStart = this.validateViewPosition(viewRange.startLineNumber, viewRange.startColumn, expectedModelRange.getStartPosition());
const validViewEnd = this.validateViewPosition(viewRange.endLineNumber, viewRange.endColumn, expectedModelRange.getEndPosition());
return new Range(validViewStart.lineNumber, validViewStart.column, validViewEnd.lineNumber, validViewEnd.column);
}
public convertViewPositionToModelPosition(viewLineNumber: number, viewColumn: number): Position {
this._ensureValidState();
viewLineNumber = this._toValidViewLineNumber(viewLineNumber);
let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1);
@ -732,8 +848,13 @@ export class SplitLinesCollection implements IViewModelLinesCollection {
return this.model.validatePosition(new Position(lineIndex + 1, inputColumn));
}
public convertViewRangeToModelRange(viewRange: Range): Range {
const start = this.convertViewPositionToModelPosition(viewRange.startLineNumber, viewRange.startColumn);
const end = this.convertViewPositionToModelPosition(viewRange.endLineNumber, viewRange.endColumn);
return new Range(start.lineNumber, start.column, end.lineNumber, end.column);
}
public convertModelPositionToViewPosition(_modelLineNumber: number, _modelColumn: number): Position {
this._ensureValidState();
const validPosition = this.model.validatePosition(new Position(_modelLineNumber, _modelColumn));
const inputLineNumber = validPosition.lineNumber;
@ -898,6 +1019,10 @@ class VisibleIdentitySplitLine implements ISplitLine {
return InvisibleIdentitySplitLine.INSTANCE;
}
public getLineBreakData(): LineBreakData | null {
return null;
}
public getViewLineCount(): number {
return 1;
}
@ -926,6 +1051,7 @@ class VisibleIdentitySplitLine implements ISplitLine {
false,
1,
lineContent.length + 1,
0,
lineTokens.inflate()
);
}
@ -968,6 +1094,10 @@ class InvisibleIdentitySplitLine implements ISplitLine {
return VisibleIdentitySplitLine.INSTANCE;
}
public getLineBreakData(): LineBreakData | null {
return null;
}
public getViewLineCount(): number {
return 0;
}
@ -1011,18 +1141,11 @@ class InvisibleIdentitySplitLine implements ISplitLine {
export class SplitLine implements ISplitLine {
private readonly positionMapper: ILineMapping;
private readonly outputLineCount: number;
private readonly wrappedIndent: string;
private readonly wrappedIndentLength: number;
private readonly _lineBreakData: LineBreakData;
private _isVisible: boolean;
constructor(positionMapper: ILineMapping, isVisible: boolean) {
this.positionMapper = positionMapper;
this.wrappedIndent = this.positionMapper.getWrappedLinesIndent();
this.wrappedIndentLength = this.wrappedIndent.length;
this.outputLineCount = this.positionMapper.getOutputLineCount();
constructor(lineBreakData: LineBreakData, isVisible: boolean) {
this._lineBreakData = lineBreakData;
this._isVisible = isVisible;
}
@ -1035,22 +1158,26 @@ export class SplitLine implements ISplitLine {
return this;
}
public getLineBreakData(): LineBreakData | null {
return this._lineBreakData;
}
public getViewLineCount(): number {
if (!this._isVisible) {
return 0;
}
return this.outputLineCount;
return this._lineBreakData.breakOffsets.length;
}
private getInputStartOffsetOfOutputLineIndex(outputLineIndex: number): number {
return this.positionMapper.getInputOffsetOfOutputPosition(outputLineIndex, 0);
return LineBreakData.getInputOffsetOfOutputPosition(this._lineBreakData.breakOffsets, outputLineIndex, 0);
}
private getInputEndOffsetOfOutputLineIndex(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number {
if (outputLineIndex + 1 === this.outputLineCount) {
if (outputLineIndex + 1 === this._lineBreakData.breakOffsets.length) {
return model.getLineMaxColumn(modelLineNumber) - 1;
}
return this.positionMapper.getInputOffsetOfOutputPosition(outputLineIndex + 1, 0);
return LineBreakData.getInputOffsetOfOutputPosition(this._lineBreakData.breakOffsets, outputLineIndex + 1, 0);
}
public getViewLineContent(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): string {
@ -1067,7 +1194,7 @@ export class SplitLine implements ISplitLine {
});
if (outputLineIndex > 0) {
r = this.wrappedIndent + r;
r = spaces(this._lineBreakData.wrappedTextIndentLength) + r;
}
return r;
@ -1082,7 +1209,7 @@ export class SplitLine implements ISplitLine {
let r = endOffset - startOffset;
if (outputLineIndex > 0) {
r = this.wrappedIndent.length + r;
r = this._lineBreakData.wrappedTextIndentLength + r;
}
return r;
@ -1093,7 +1220,7 @@ export class SplitLine implements ISplitLine {
throw new Error('Not supported');
}
if (outputLineIndex > 0) {
return this.wrappedIndentLength + 1;
return this._lineBreakData.wrappedTextIndentLength + 1;
}
return 1;
}
@ -1121,25 +1248,28 @@ export class SplitLine implements ISplitLine {
});
if (outputLineIndex > 0) {
lineContent = this.wrappedIndent + lineContent;
lineContent = spaces(this._lineBreakData.wrappedTextIndentLength) + lineContent;
}
let minColumn = (outputLineIndex > 0 ? this.wrappedIndentLength + 1 : 1);
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.wrappedIndentLength;
deltaStartIndex = this._lineBreakData.wrappedTextIndentLength;
}
let lineTokens = model.getLineTokens(modelLineNumber);
const startVisibleColumn = (outputLineIndex === 0 ? 0 : this._lineBreakData.breakOffsetsVisibleColumn[outputLineIndex - 1]);
return new ViewLineData(
lineContent,
continuesWithWrappedLine,
minColumn,
maxColumn,
startVisibleColumn,
lineTokens.sliceAndInflate(startOffset, endOffset, deltaStartIndex)
);
}
@ -1165,25 +1295,25 @@ export class SplitLine implements ISplitLine {
}
let adjustedColumn = outputColumn - 1;
if (outputLineIndex > 0) {
if (adjustedColumn < this.wrappedIndentLength) {
if (adjustedColumn < this._lineBreakData.wrappedTextIndentLength) {
adjustedColumn = 0;
} else {
adjustedColumn -= this.wrappedIndentLength;
adjustedColumn -= this._lineBreakData.wrappedTextIndentLength;
}
}
return this.positionMapper.getInputOffsetOfOutputPosition(outputLineIndex, adjustedColumn) + 1;
return LineBreakData.getInputOffsetOfOutputPosition(this._lineBreakData.breakOffsets, outputLineIndex, adjustedColumn) + 1;
}
public getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number): Position {
if (!this._isVisible) {
throw new Error('Not supported');
}
let r = this.positionMapper.getOutputPositionOfInputOffset(inputColumn - 1);
let r = LineBreakData.getOutputPositionOfInputOffset(this._lineBreakData.breakOffsets, inputColumn - 1);
let outputLineIndex = r.outputLineIndex;
let outputColumn = r.outputOffset + 1;
if (outputLineIndex > 0) {
outputColumn += this.wrappedIndentLength;
outputColumn += this._lineBreakData.wrappedTextIndentLength;
}
// console.log('in -> out ' + deltaLineNumber + ',' + inputColumn + ' ===> ' + (deltaLineNumber+outputLineIndex) + ',' + outputColumn);
@ -1194,21 +1324,33 @@ export class SplitLine implements ISplitLine {
if (!this._isVisible) {
throw new Error('Not supported');
}
const r = this.positionMapper.getOutputPositionOfInputOffset(inputColumn - 1);
const r = LineBreakData.getOutputPositionOfInputOffset(this._lineBreakData.breakOffsets, inputColumn - 1);
return (deltaLineNumber + r.outputLineIndex);
}
}
function createSplitLine(linePositionMapperFactory: ILineMapperFactory, text: string, tabSize: number, wrappingColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, isVisible: boolean): ISplitLine {
let positionMapper = linePositionMapperFactory.createLineMapping(text, tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent);
if (positionMapper === null) {
let _spaces: string[] = [''];
function spaces(count: number): string {
if (count >= _spaces.length) {
for (let i = 1; i <= count; i++) {
_spaces[i] = _makeSpaces(i);
}
}
return _spaces[count];
}
function _makeSpaces(count: number): string {
return new Array(count + 1).join(' ');
}
function createSplitLine(lineBreakData: LineBreakData | null, isVisible: boolean): ISplitLine {
if (lineBreakData === null) {
// No mapping needed
if (isVisible) {
return VisibleIdentitySplitLine.INSTANCE;
}
return InvisibleIdentitySplitLine.INSTANCE;
} else {
return new SplitLine(positionMapper, isVisible);
return new SplitLine(lineBreakData, isVisible);
}
}
@ -1298,6 +1440,18 @@ export class IdentityLinesCollection implements IViewModelLinesCollection {
return false;
}
public createLineBreaksComputer(): ILineBreaksComputer {
let result: null[] = [];
return {
addRequest: (lineText: string, previousLineBreakData: LineBreakData | null) => {
result.push(null);
},
finalize: () => {
return result;
}
};
}
public onModelFlushed(): void {
}
@ -1305,11 +1459,11 @@ export class IdentityLinesCollection implements IViewModelLinesCollection {
return new viewEvents.ViewLinesDeletedEvent(fromLineNumber, toLineNumber);
}
public onModelLinesInserted(_versionId: number, fromLineNumber: number, toLineNumber: number, _text: string[]): viewEvents.ViewLinesInsertedEvent | null {
public onModelLinesInserted(_versionId: number, fromLineNumber: number, toLineNumber: number, lineBreaks: (LineBreakData | null)[]): viewEvents.ViewLinesInsertedEvent | null {
return new viewEvents.ViewLinesInsertedEvent(fromLineNumber, toLineNumber);
}
public onModelLineChanged(_versionId: number, lineNumber: number, _newText: string): [boolean, viewEvents.ViewLinesChangedEvent | null, viewEvents.ViewLinesInsertedEvent | null, viewEvents.ViewLinesDeletedEvent | null] {
public onModelLineChanged(_versionId: number, lineNumber: number, lineBreakData: LineBreakData | null): [boolean, viewEvents.ViewLinesChangedEvent | null, viewEvents.ViewLinesInsertedEvent | null, viewEvents.ViewLinesDeletedEvent | null] {
return [false, new viewEvents.ViewLinesChangedEvent(lineNumber, lineNumber), null, null];
}
@ -1320,9 +1474,6 @@ export class IdentityLinesCollection implements IViewModelLinesCollection {
return this.model.getLineCount();
}
public warmUpLookupCache(_viewStartLineNumber: number, _viewEndLineNumber: number): void {
}
public getActiveIndentGuide(viewLineNumber: number, _minLineNumber: number, _maxLineNumber: number): IActiveIndentGuideInfo {
return {
startLineNumber: viewLineNumber,
@ -1364,6 +1515,7 @@ export class IdentityLinesCollection implements IViewModelLinesCollection {
false,
1,
lineContent.length + 1,
0,
lineTokens.inflate()
);
}

View file

@ -174,6 +174,10 @@ export class ViewLineData {
* The maximum allowed column at this view line.
*/
public readonly maxColumn: number;
/**
* The visible column at the start of the line (after the fauxIndent).
*/
public readonly startVisibleColumn: number;
/**
* The tokens at this view line.
*/
@ -184,12 +188,14 @@ export class ViewLineData {
continuesWithWrappedLine: boolean,
minColumn: number,
maxColumn: number,
startVisibleColumn: number,
tokens: IViewLineTokens
) {
this.content = content;
this.continuesWithWrappedLine = continuesWithWrappedLine;
this.minColumn = minColumn;
this.maxColumn = maxColumn;
this.startVisibleColumn = startVisibleColumn;
this.tokens = tokens;
}
}
@ -231,6 +237,10 @@ export class ViewLineRenderingData {
* The tab size for this view model.
*/
public readonly tabSize: number;
/**
* The visible column at the start of the line (after the fauxIndent)
*/
public readonly startVisibleColumn: number;
constructor(
minColumn: number,
@ -241,7 +251,8 @@ export class ViewLineRenderingData {
mightContainNonBasicASCII: boolean,
tokens: IViewLineTokens,
inlineDecorations: InlineDecoration[],
tabSize: number
tabSize: number,
startVisibleColumn: number
) {
this.minColumn = minColumn;
this.maxColumn = maxColumn;
@ -254,6 +265,7 @@ export class ViewLineRenderingData {
this.tokens = tokens;
this.inlineDecorations = inlineDecorations;
this.tabSize = tabSize;
this.startVisibleColumn = startVisibleColumn;
}
public static isBasicASCII(lineContent: string, mightContainNonBasicASCII: boolean): boolean {

View file

@ -18,8 +18,7 @@ import { tokenizeLineToHTML } from 'vs/editor/common/modes/textToHtmlTokenizer';
import { MinimapTokensColorTracker } from 'vs/editor/common/viewModel/minimapTokensColorTracker';
import * as viewEvents from 'vs/editor/common/view/viewEvents';
import { ViewLayout } from 'vs/editor/common/viewLayout/viewLayout';
import { CharacterHardWrappingLineMapperFactory } from 'vs/editor/common/viewModel/characterHardWrappingLineMapper';
import { IViewModelLinesCollection, IdentityLinesCollection, SplitLinesCollection } from 'vs/editor/common/viewModel/splitLinesCollection';
import { IViewModelLinesCollection, IdentityLinesCollection, SplitLinesCollection, ILineBreaksComputerFactory } from 'vs/editor/common/viewModel/splitLinesCollection';
import { ICoordinatesConverter, IOverviewRulerDecorations, IViewModel, MinimapLinesRenderingData, ViewLineData, ViewLineRenderingData, ViewModelDecoration } from 'vs/editor/common/viewModel/viewModel';
import { ViewModelDecorations } from 'vs/editor/common/viewModel/viewModelDecorations';
import { ITheme } from 'vs/platform/theme/common/themeService';
@ -43,7 +42,13 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel
public readonly viewLayout: ViewLayout;
private readonly decorations: ViewModelDecorations;
constructor(editorId: number, configuration: editorCommon.IConfiguration, model: ITextModel, scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) {
constructor(
editorId: number,
configuration: editorCommon.IConfiguration,
model: ITextModel,
lineMapperFactory: ILineBreaksComputerFactory,
scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable
) {
super();
this.editorId = editorId;
@ -63,20 +68,11 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel
const options = this.configuration.options;
const wrappingInfo = options.get(EditorOption.wrappingInfo);
const fontInfo = options.get(EditorOption.fontInfo);
const wordWrapBreakAfterCharacters = options.get(EditorOption.wordWrapBreakAfterCharacters);
const wordWrapBreakBeforeCharacters = options.get(EditorOption.wordWrapBreakBeforeCharacters);
const wordWrapBreakObtrusiveCharacters = options.get(EditorOption.wordWrapBreakObtrusiveCharacters);
const wrappingIndent = options.get(EditorOption.wrappingIndent);
let hardWrappingLineMapperFactory = new CharacterHardWrappingLineMapperFactory(
wordWrapBreakBeforeCharacters,
wordWrapBreakAfterCharacters,
wordWrapBreakObtrusiveCharacters
);
this.lines = new SplitLinesCollection(
this.model,
hardWrappingLineMapperFactory,
lineMapperFactory,
this.model.getOptions().tabSize,
wrappingInfo.wrappingColumn,
fontInfo.typicalFullwidthCharacterWidth / fontInfo.typicalHalfwidthCharacterWidth,
@ -200,8 +196,26 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel
const changes = e.changes;
const versionId = e.versionId;
for (let j = 0, lenJ = changes.length; j < lenJ; j++) {
const change = changes[j];
// 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);
}
break;
}
case textModelEvents.RawContentChangedType.LineChanged: {
lineBreaksComputer.addRequest(change.detail, null);
break;
}
}
}
const lineBreaks = lineBreaksComputer.finalize();
let lineBreaksOffset = 0;
for (const change of changes) {
switch (change.changeType) {
case textModelEvents.RawContentChangedType.Flush: {
@ -222,7 +236,10 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel
break;
}
case textModelEvents.RawContentChangedType.LinesInserted: {
const linesInsertedEvent = this.lines.onModelLinesInserted(versionId, change.fromLineNumber, change.toLineNumber, change.detail);
const insertedLineBreaks = lineBreaks.slice(lineBreaksOffset, lineBreaksOffset + change.detail.length);
lineBreaksOffset += change.detail.length;
const linesInsertedEvent = this.lines.onModelLinesInserted(versionId, change.fromLineNumber, change.toLineNumber, insertedLineBreaks);
if (linesInsertedEvent !== null) {
eventsCollector.emit(linesInsertedEvent);
this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber);
@ -231,7 +248,10 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel
break;
}
case textModelEvents.RawContentChangedType.LineChanged: {
const [lineMappingChanged, linesChangedEvent, linesInsertedEvent, linesDeletedEvent] = this.lines.onModelLineChanged(versionId, change.lineNumber, change.detail);
const changedLineBreakData = lineBreaks[lineBreaksOffset];
lineBreaksOffset++;
const [lineMappingChanged, linesChangedEvent, linesInsertedEvent, linesDeletedEvent] = this.lines.onModelLineChanged(versionId, change.lineNumber, changedLineBreakData);
hadModelLineChangeThatChangedLineMapping = lineMappingChanged;
if (linesChangedEvent) {
eventsCollector.emit(linesChangedEvent);
@ -475,8 +495,6 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel
* Gives a hint that a lot of requests are about to come in for these line numbers.
*/
public setViewport(startLineNumber: number, endLineNumber: number, centeredLineNumber: number): void {
this.lines.warmUpLookupCache(startLineNumber, endLineNumber);
this.viewportStartLine = startLineNumber;
let position = this.coordinatesConverter.convertViewPositionToModelPosition(new Position(startLineNumber, this.getLineMinColumn(startLineNumber)));
this.viewportStartLineTrackedRange = this.model._setTrackedRange(this.viewportStartLineTrackedRange, new Range(position.lineNumber, position.column, position.lineNumber, position.column), TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges);
@ -546,7 +564,8 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel
mightContainNonBasicASCII,
lineData.tokens,
inlineDecorations,
tabSize
tabSize,
lineData.startVisibleColumn
);
}

View file

@ -381,7 +381,6 @@ export class LineCommentCommand implements editorCommon.ICommand {
return res;
}
// TODO@Alex -> duplicated in characterHardWrappingLineMapper
private static nextVisibleColumn(currentVisibleColumn: number, tabSize: number, isTab: boolean, columnSize: number): number {
if (isTab) {
return currentVisibleColumn + (tabSize - (currentVisibleColumn % tabSize));

View file

@ -47,6 +47,9 @@ export class CompletionItem {
idx?: number;
word?: string;
//
readonly isDetailsResolved: boolean;
constructor(
readonly position: IPosition,
readonly completion: modes.CompletionItem,
@ -70,6 +73,8 @@ export class CompletionItem {
this.editReplaceEnd = new Position(completion.range.replace.endLineNumber, completion.range.replace.endColumn);
}
this.isDetailsResolved = container.isDetailsResolved || typeof provider.resolveCompletionItem === 'undefined';
// create the suggestion resolver
const { resolveCompletionItem } = provider;
if (typeof resolveCompletionItem !== 'function') {

View file

@ -125,6 +125,7 @@ export class Colorizer {
[],
tabSize,
0,
0,
-1,
'none',
false,
@ -193,6 +194,7 @@ function _fakeColorize(lines: string[], tabSize: number): string {
[],
tabSize,
0,
0,
-1,
'none',
false,
@ -230,6 +232,7 @@ function _actualColorize(lines: string[], tabSize: number, tokenizationSupport:
[],
tabSize,
0,
0,
-1,
'none',
false,

View file

@ -80,6 +80,10 @@ export class SimpleModel implements IResolvedTextEditorModel {
public dispose(): void {
this._onDispose.fire();
}
public isResolved(): boolean {
return true;
}
}
export interface IOpenEditorDelegate {

View file

@ -14,6 +14,7 @@ import { TextModel } from 'vs/editor/common/model/textModel';
import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl';
import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration';
import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer';
function testCommand(lines: string[], selections: Selection[], edits: IIdentifiedSingleEditOperation[], expectedLines: string[], expectedSelections: Selection[]): void {
withTestCodeEditor(lines, {}, (editor, cursor) => {
@ -200,7 +201,7 @@ suite('SideEditing', () => {
function _runTest(selection: Selection, editRange: Range, editText: string, editForceMoveMarkers: boolean, expected: Selection, msg: string): void {
const model = TextModel.createFromString(LINES.join('\n'));
const config = new TestConfiguration({});
const viewModel = new ViewModel(0, config, model, null!);
const viewModel = new ViewModel(0, config, model, MonospaceLineBreaksComputerFactory.create(config.options), null!);
const cursor = new Cursor(config, model, viewModel);
cursor.setSelections('tests', [selection]);

View file

@ -25,6 +25,7 @@ import { IRelaxedTextModelCreationOptions, createTextModel } from 'vs/editor/tes
import { MockMode } from 'vs/editor/test/common/mocks/mockMode';
import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration';
import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules';
import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer';
const H = Handler;
@ -152,7 +153,7 @@ suite('Editor Controller - Cursor', () => {
thisModel = createTextModel(text);
thisConfiguration = new TestConfiguration({});
thisViewModel = new ViewModel(0, thisConfiguration, thisModel, null!);
thisViewModel = new ViewModel(0, thisConfiguration, thisModel, MonospaceLineBreaksComputerFactory.create(thisConfiguration.options), null!);
thisCursor = new Cursor(thisConfiguration, thisModel, thisViewModel);
});
@ -776,7 +777,7 @@ suite('Editor Controller - Cursor', () => {
'var newer = require("gulp-newer");',
].join('\n'));
const config = new TestConfiguration({});
const viewModel = new ViewModel(0, config, model, null!);
const viewModel = new ViewModel(0, config, model, MonospaceLineBreaksComputerFactory.create(config.options), null!);
const cursor = new Cursor(config, model, viewModel);
moveTo(cursor, 1, 4, false);
@ -816,7 +817,7 @@ suite('Editor Controller - Cursor', () => {
'<property id="SomeThing" key="SomeKey" value="00X"/>',
].join('\n'));
const config = new TestConfiguration({});
const viewModel = new ViewModel(0, config, model, null!);
const viewModel = new ViewModel(0, config, model, MonospaceLineBreaksComputerFactory.create(config.options), null!);
const cursor = new Cursor(config, model, viewModel);
moveTo(cursor, 10, 10, false);
@ -880,7 +881,7 @@ suite('Editor Controller - Cursor', () => {
'<property id="SomeThing" key="SomeKey" value="00X"/>',
].join('\n'));
const config = new TestConfiguration({});
const viewModel = new ViewModel(0, config, model, null!);
const viewModel = new ViewModel(0, config, model, MonospaceLineBreaksComputerFactory.create(config.options), null!);
const cursor = new Cursor(config, model, viewModel);
moveTo(cursor, 10, 10, false);
@ -929,7 +930,7 @@ suite('Editor Controller - Cursor', () => {
'var newer = require("gulp-newer");',
].join('\n'));
const config = new TestConfiguration({});
const viewModel = new ViewModel(0, config, model, null!);
const viewModel = new ViewModel(0, config, model, MonospaceLineBreaksComputerFactory.create(config.options), null!);
const cursor = new Cursor(config, model, viewModel);
moveTo(cursor, 1, 4, false);
@ -2074,7 +2075,7 @@ suite('Editor Controller - Regression tests', () => {
wordWrap: 'wordWrapColumn',
wordWrapColumn: 100
});
const viewModel = new ViewModel(0, config, model, null!);
const viewModel = new ViewModel(0, config, model, MonospaceLineBreaksComputerFactory.create(config.options), null!);
const cursor = new Cursor(config, model, viewModel);
moveTo(cursor, 1, 43, false);
@ -3834,7 +3835,7 @@ function usingCursor(opts: ICursorOpts, callback: (model: TextModel, cursor: Cur
let model = createTextModel(opts.text.join('\n'), opts.modelOpts, opts.languageIdentifier);
model.forceTokenization(model.getLineCount());
let config = new TestConfiguration(opts.editorOpts || {});
let viewModel = new ViewModel(0, config, model, null!);
let viewModel = new ViewModel(0, config, model, MonospaceLineBreaksComputerFactory.create(config.options), null!);
let cursor = new Cursor(config, model, viewModel);
callback(model, cursor);

View file

@ -13,6 +13,7 @@ import { Selection } from 'vs/editor/common/core/selection';
import { TextModel } from 'vs/editor/common/model/textModel';
import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl';
import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration';
import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer';
suite('Cursor move command test', () => {
@ -32,7 +33,7 @@ suite('Cursor move command test', () => {
thisModel = TextModel.createFromString(text);
thisConfiguration = new TestConfiguration({});
thisViewModel = new ViewModel(0, thisConfiguration, thisModel, null!);
thisViewModel = new ViewModel(0, thisConfiguration, thisModel, MonospaceLineBreaksComputerFactory.create(thisConfiguration.options), null!);
thisCursor = new Cursor(thisConfiguration, thisModel, thisViewModel);
});

View file

@ -38,6 +38,7 @@ suite('viewLineRenderer.renderLine', () => {
[],
tabSize,
0,
0,
-1,
'none',
false,
@ -88,6 +89,7 @@ suite('viewLineRenderer.renderLine', () => {
[],
tabSize,
0,
0,
-1,
'none',
false,
@ -140,6 +142,7 @@ suite('viewLineRenderer.renderLine', () => {
]),
[],
4,
0,
10,
6,
'boundary',
@ -232,6 +235,7 @@ suite('viewLineRenderer.renderLine', () => {
lineParts,
[],
4,
0,
10,
-1,
'boundary',
@ -295,6 +299,7 @@ suite('viewLineRenderer.renderLine', () => {
lineParts,
[],
4,
0,
10,
-1,
'none',
@ -358,6 +363,7 @@ suite('viewLineRenderer.renderLine', () => {
lineParts,
[],
4,
0,
10,
-1,
'none',
@ -398,6 +404,7 @@ suite('viewLineRenderer.renderLine', () => {
lineParts,
[],
4,
0,
10,
-1,
'none',
@ -429,6 +436,7 @@ suite('viewLineRenderer.renderLine', () => {
lineParts,
[],
4,
0,
10,
-1,
'none',
@ -530,6 +538,7 @@ suite('viewLineRenderer.renderLine', () => {
lineParts,
[],
4,
0,
10,
-1,
'none',
@ -569,6 +578,7 @@ suite('viewLineRenderer.renderLine', () => {
lineParts,
[],
4,
0,
10,
-1,
'none',
@ -599,6 +609,7 @@ suite('viewLineRenderer.renderLine', () => {
lineParts,
[],
4,
0,
10,
-1,
'none',
@ -646,6 +657,7 @@ suite('viewLineRenderer.renderLine', () => {
lineParts,
[],
4,
0,
10,
-1,
'none',
@ -728,6 +740,7 @@ suite('viewLineRenderer.renderLine 2', () => {
createViewLineTokens(tokens),
[],
4,
0,
10,
-1,
renderWhitespace,
@ -754,6 +767,7 @@ suite('viewLineRenderer.renderLine 2', () => {
createViewLineTokens([createPart(21, 3)]),
[new LineDecoration(1, 22, 'link', InlineDecorationType.Regular)],
4,
0,
10,
-1,
'none',
@ -794,6 +808,7 @@ suite('viewLineRenderer.renderLine 2', () => {
new LineDecoration(13, 51, 'detected-link', InlineDecorationType.Regular)
],
4,
0,
10,
-1,
'none',
@ -1209,6 +1224,7 @@ suite('viewLineRenderer.renderLine 2', () => {
new LineDecoration(2, 8, 'c', InlineDecorationType.Regular),
],
4,
0,
10,
-1,
'none',
@ -1250,6 +1266,7 @@ suite('viewLineRenderer.renderLine 2', () => {
createViewLineTokens([createPart(4, 3)]),
[new LineDecoration(1, 2, 'before', InlineDecorationType.Before)],
4,
0,
10,
-1,
'all',
@ -1283,6 +1300,7 @@ suite('viewLineRenderer.renderLine 2', () => {
createViewLineTokens([createPart(4, 3)]),
[new LineDecoration(2, 3, 'before', InlineDecorationType.Before)],
4,
0,
10,
-1,
'all',
@ -1317,6 +1335,7 @@ suite('viewLineRenderer.renderLine 2', () => {
createViewLineTokens([createPart(0, 3)]),
[new LineDecoration(1, 2, 'before', InlineDecorationType.Before)],
4,
0,
10,
-1,
'all',
@ -1347,6 +1366,7 @@ suite('viewLineRenderer.renderLine 2', () => {
createViewLineTokens([createPart(7, 3)]),
[new LineDecoration(7, 8, 'inline-folded', InlineDecorationType.After)],
2,
0,
10,
10000,
'none',
@ -1381,6 +1401,7 @@ suite('viewLineRenderer.renderLine 2', () => {
new LineDecoration(0, 1, 'after', InlineDecorationType.After),
],
2,
0,
10,
10000,
'none',
@ -1414,6 +1435,7 @@ suite('viewLineRenderer.renderLine 2', () => {
new LineDecoration(3, 3, 'ced-TextEditorDecorationType2-5e9b9b3f-4 ced-TextEditorDecorationType2-4', InlineDecorationType.After),
],
4,
0,
10,
10000,
'none',
@ -1445,6 +1467,7 @@ suite('viewLineRenderer.renderLine 2', () => {
createViewLineTokens([createPart(15, 3)]),
[],
4,
0,
10,
10000,
'none',
@ -1475,6 +1498,7 @@ suite('viewLineRenderer.renderLine 2', () => {
createViewLineTokens([createPart(15, 3)]),
[],
4,
0,
10,
10000,
'all',
@ -1511,6 +1535,7 @@ suite('viewLineRenderer.renderLine 2', () => {
createViewLineTokens([createPart(53, 3)]),
[],
4,
0,
10,
10000,
'none',
@ -1541,6 +1566,7 @@ suite('viewLineRenderer.renderLine 2', () => {
createViewLineTokens([createPart(100, 3)]),
[],
4,
0,
10,
10000,
'none',
@ -1573,6 +1599,7 @@ suite('viewLineRenderer.renderLine 2', () => {
createViewLineTokens([createPart(105, 3)]),
[],
4,
0,
10,
10000,
'none',
@ -1604,6 +1631,7 @@ suite('viewLineRenderer.renderLine 2', () => {
createViewLineTokens([createPart(59, 3)]),
[],
4,
0,
10,
10000,
'boundary',
@ -1633,6 +1661,7 @@ suite('viewLineRenderer.renderLine 2', () => {
createViewLineTokens([createPart(194, 3)]),
[],
4,
0,
10,
10000,
'none',
@ -1666,6 +1695,7 @@ suite('viewLineRenderer.renderLine 2', () => {
createViewLineTokens([createPart(194, 3)]),
[],
4,
0,
10,
10000,
'none',
@ -1695,6 +1725,7 @@ suite('viewLineRenderer.renderLine 2', () => {
createViewLineTokens(parts),
[],
tabSize,
0,
10,
-1,
'none',

View file

@ -1,127 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { WrappingIndent } from 'vs/editor/common/config/editorOptions';
import { CharacterHardWrappingLineMapperFactory } from 'vs/editor/common/viewModel/characterHardWrappingLineMapper';
import { ILineMapperFactory, ILineMapping } from 'vs/editor/common/viewModel/splitLinesCollection';
function assertLineMapping(factory: ILineMapperFactory, tabSize: number, breakAfter: number, annotatedText: string, wrappingIndent = WrappingIndent.None): ILineMapping | null {
// Create version of `annotatedText` with line break markers removed
let rawText = '';
let currentLineIndex = 0;
let lineIndices: number[] = [];
for (let i = 0, len = annotatedText.length; i < len; i++) {
if (annotatedText.charAt(i) === '|') {
currentLineIndex++;
} else {
rawText += annotatedText.charAt(i);
lineIndices[rawText.length - 1] = currentLineIndex;
}
}
const mapper = factory.createLineMapping(rawText, tabSize, breakAfter, 2, wrappingIndent);
// Insert line break markers again, according to algorithm
let actualAnnotatedText = '';
if (mapper) {
let previousLineIndex = 0;
for (let i = 0, len = rawText.length; i < len; i++) {
let r = mapper.getOutputPositionOfInputOffset(i);
if (previousLineIndex !== r.outputLineIndex) {
previousLineIndex = r.outputLineIndex;
actualAnnotatedText += '|';
}
actualAnnotatedText += rawText.charAt(i);
}
} else {
// No wrapping
actualAnnotatedText = rawText;
}
assert.equal(actualAnnotatedText, annotatedText);
return mapper;
}
suite('Editor ViewModel - CharacterHardWrappingLineMapper', () => {
test('CharacterHardWrappingLineMapper', () => {
let factory = new CharacterHardWrappingLineMapperFactory('(', ')', '.');
// Empty string
assertLineMapping(factory, 4, 5, '');
// No wrapping if not necessary
assertLineMapping(factory, 4, 5, 'aaa');
assertLineMapping(factory, 4, 5, 'aaaaa');
assertLineMapping(factory, 4, -1, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
// Acts like hard wrapping if no char found
assertLineMapping(factory, 4, 5, 'aaaaa|a');
// Honors obtrusive wrapping character
assertLineMapping(factory, 4, 5, 'aaaaa|.');
assertLineMapping(factory, 4, 5, 'aaaaa|a.|aaa.|aa');
assertLineMapping(factory, 4, 5, 'aaaaa|a..|aaa.|aa');
assertLineMapping(factory, 4, 5, 'aaaaa|a...|aaa.|aa');
assertLineMapping(factory, 4, 5, 'aaaaa|a....|aaa.|aa');
// Honors tabs when computing wrapping position
assertLineMapping(factory, 4, 5, '\t');
assertLineMapping(factory, 4, 5, '\ta|aa');
assertLineMapping(factory, 4, 5, '\ta|\ta|a');
assertLineMapping(factory, 4, 5, 'aa\ta');
assertLineMapping(factory, 4, 5, 'aa\ta|a');
// Honors wrapping before characters (& gives it priority)
assertLineMapping(factory, 4, 5, 'aaa.|aa');
assertLineMapping(factory, 4, 5, 'aaa|(.aa');
// Honors wrapping after characters (& gives it priority)
assertLineMapping(factory, 4, 5, 'aaa))|).aaa');
assertLineMapping(factory, 4, 5, 'aaa))|)|.aaaa');
assertLineMapping(factory, 4, 5, 'aaa)|()|.aaa');
assertLineMapping(factory, 4, 5, 'aaa(|()|.aaa');
assertLineMapping(factory, 4, 5, 'aa.(|()|.aaa');
assertLineMapping(factory, 4, 5, 'aa.|(.)|.aaa');
});
test('CharacterHardWrappingLineMapper - CJK and Kinsoku Shori', () => {
let factory = new CharacterHardWrappingLineMapperFactory('(', ')', '.');
assertLineMapping(factory, 4, 5, 'aa \u5b89|\u5b89');
assertLineMapping(factory, 4, 5, '\u3042 \u5b89|\u5b89');
assertLineMapping(factory, 4, 5, '\u3042\u3042|\u5b89\u5b89');
assertLineMapping(factory, 4, 5, 'aa |\u5b89)\u5b89|\u5b89');
assertLineMapping(factory, 4, 5, 'aa \u3042|\u5b89\u3042)|\u5b89');
assertLineMapping(factory, 4, 5, 'aa |(\u5b89aa|\u5b89');
});
test('CharacterHardWrappingLineMapper - WrappingIndent.Same', () => {
let factory = new CharacterHardWrappingLineMapperFactory('', ' ', '');
assertLineMapping(factory, 4, 38, ' *123456789012345678901234567890123456|7890', WrappingIndent.Same);
});
test('issue #16332: Scroll bar overlaying on top of text', () => {
let factory = new CharacterHardWrappingLineMapperFactory('', ' ', '');
assertLineMapping(factory, 4, 24, 'a/ very/long/line/of/tex|t/that/expands/beyon|d/your/typical/line/|of/code/', WrappingIndent.Indent);
});
test('issue #35162: wrappingIndent not consistently working', () => {
let factory = new CharacterHardWrappingLineMapperFactory('', ' ', '');
let mapper = assertLineMapping(factory, 4, 24, ' t h i s |i s |a l |o n |g l |i n |e', WrappingIndent.Indent);
assert.equal(mapper!.getWrappedLinesIndent(), ' \t');
});
test('issue #75494: surrogate pairs', () => {
let factory = new CharacterHardWrappingLineMapperFactory('', ' ', '');
assertLineMapping(factory, 4, 49, '🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇|👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬', WrappingIndent.Same);
});
test('CharacterHardWrappingLineMapper - WrappingIndent.DeepIndent', () => {
let factory = new CharacterHardWrappingLineMapperFactory('', ' ', '');
let mapper = assertLineMapping(factory, 4, 26, ' W e A r e T e s t |i n g D e |e p I n d |e n t a t |i o n', WrappingIndent.DeepIndent);
assert.equal(mapper!.getWrappedLinesIndent(), ' \t\t');
});
});

View file

@ -0,0 +1,240 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { WrappingIndent, EditorOptions } from 'vs/editor/common/config/editorOptions';
import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer';
import { ILineBreaksComputerFactory, LineBreakData } from 'vs/editor/common/viewModel/splitLinesCollection';
function parseAnnotatedText(annotatedText: string): { text: string; indices: number[]; } {
let text = '';
let currentLineIndex = 0;
let indices: number[] = [];
for (let i = 0, len = annotatedText.length; i < len; i++) {
if (annotatedText.charAt(i) === '|') {
currentLineIndex++;
} else {
text += annotatedText.charAt(i);
indices[text.length - 1] = currentLineIndex;
}
}
return { text: text, indices: indices };
}
function toAnnotatedText(text: string, lineBreakData: LineBreakData | null): string {
// Insert line break markers again, according to algorithm
let actualAnnotatedText = '';
if (lineBreakData) {
let previousLineIndex = 0;
for (let i = 0, len = text.length; i < len; i++) {
let r = LineBreakData.getOutputPositionOfInputOffset(lineBreakData.breakOffsets, i);
if (previousLineIndex !== r.outputLineIndex) {
previousLineIndex = r.outputLineIndex;
actualAnnotatedText += '|';
}
actualAnnotatedText += text.charAt(i);
}
} else {
// No wrapping
actualAnnotatedText = text;
}
return actualAnnotatedText;
}
function getLineBreakData(factory: ILineBreaksComputerFactory, tabSize: number, breakAfter: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, text: string, previousLineBreakData: LineBreakData | null): LineBreakData | null {
const lineBreaksComputer = factory.createLineBreaksComputer(tabSize, breakAfter, columnsForFullWidthChar, wrappingIndent);
const previousLineBreakDataClone = previousLineBreakData ? new LineBreakData(previousLineBreakData.breakOffsets.slice(0), previousLineBreakData.breakOffsetsVisibleColumn.slice(0), previousLineBreakData.wrappedTextIndentLength) : null;
lineBreaksComputer.addRequest(text, previousLineBreakDataClone);
return lineBreaksComputer.finalize()[0];
}
function assertLineBreaks(factory: ILineBreaksComputerFactory, tabSize: number, breakAfter: number, annotatedText: string, wrappingIndent = WrappingIndent.None): LineBreakData | null {
// Create version of `annotatedText` with line break markers removed
const text = parseAnnotatedText(annotatedText).text;
const lineBreakData = getLineBreakData(factory, tabSize, breakAfter, 2, wrappingIndent, text, null);
const actualAnnotatedText = toAnnotatedText(text, lineBreakData);
assert.equal(actualAnnotatedText, annotatedText);
return lineBreakData;
}
suite('Editor ViewModel - MonospaceLineBreaksComputer', () => {
test('MonospaceLineBreaksComputer', () => {
let factory = new MonospaceLineBreaksComputerFactory('(', '\t).');
// Empty string
assertLineBreaks(factory, 4, 5, '');
// No wrapping if not necessary
assertLineBreaks(factory, 4, 5, 'aaa');
assertLineBreaks(factory, 4, 5, 'aaaaa');
assertLineBreaks(factory, 4, -1, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
// Acts like hard wrapping if no char found
assertLineBreaks(factory, 4, 5, 'aaaaa|a');
// Honors wrapping character
assertLineBreaks(factory, 4, 5, 'aaaaa|.');
assertLineBreaks(factory, 4, 5, 'aaaaa|a.|aaa.|aa');
assertLineBreaks(factory, 4, 5, 'aaaaa|a..|aaa.|aa');
assertLineBreaks(factory, 4, 5, 'aaaaa|a...|aaa.|aa');
assertLineBreaks(factory, 4, 5, 'aaaaa|a....|aaa.|aa');
// Honors tabs when computing wrapping position
assertLineBreaks(factory, 4, 5, '\t');
assertLineBreaks(factory, 4, 5, '\t|aaa');
assertLineBreaks(factory, 4, 5, '\t|a\t|aa');
assertLineBreaks(factory, 4, 5, 'aa\ta');
assertLineBreaks(factory, 4, 5, 'aa\t|aa');
// Honors wrapping before characters (& gives it priority)
assertLineBreaks(factory, 4, 5, 'aaa.|aa');
assertLineBreaks(factory, 4, 5, 'aaa(.|aa');
// Honors wrapping after characters (& gives it priority)
assertLineBreaks(factory, 4, 5, 'aaa))|).aaa');
assertLineBreaks(factory, 4, 5, 'aaa))|).|aaaa');
assertLineBreaks(factory, 4, 5, 'aaa)|().|aaa');
assertLineBreaks(factory, 4, 5, 'aaa(|().|aaa');
assertLineBreaks(factory, 4, 5, 'aa.(|().|aaa');
assertLineBreaks(factory, 4, 5, 'aa.(.|).aaa');
});
function assertIncrementalLineBreaks(factory: ILineBreaksComputerFactory, text: string, tabSize: number, breakAfter1: number, annotatedText1: string, breakAfter2: number, annotatedText2: string, wrappingIndent = WrappingIndent.None): void {
// sanity check the test
assert.equal(text, parseAnnotatedText(annotatedText1).text);
assert.equal(text, parseAnnotatedText(annotatedText2).text);
// check that the direct mapping is ok for 1
const directLineBreakData1 = getLineBreakData(factory, tabSize, breakAfter1, 2, wrappingIndent, text, null);
assert.equal(toAnnotatedText(text, directLineBreakData1), annotatedText1);
// check that the direct mapping is ok for 2
const directLineBreakData2 = getLineBreakData(factory, tabSize, breakAfter2, 2, wrappingIndent, text, null);
assert.equal(toAnnotatedText(text, directLineBreakData2), annotatedText2);
// check that going from 1 to 2 is ok
const lineBreakData2from1 = getLineBreakData(factory, tabSize, breakAfter2, 2, wrappingIndent, text, directLineBreakData1);
assert.equal(toAnnotatedText(text, lineBreakData2from1), annotatedText2);
assert.deepEqual(lineBreakData2from1, directLineBreakData2);
// check that going from 2 to 1 is ok
const lineBreakData1from2 = getLineBreakData(factory, tabSize, breakAfter1, 2, wrappingIndent, text, directLineBreakData2);
assert.equal(toAnnotatedText(text, lineBreakData1from2), annotatedText1);
assert.deepEqual(lineBreakData1from2, directLineBreakData1);
}
test('MonospaceLineBreaksComputer incremental 1', () => {
let factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);
assertIncrementalLineBreaks(
factory, 'just some text and more', 4,
10, 'just some |text and |more',
15, 'just some text |and more'
);
assertIncrementalLineBreaks(
factory, 'Cu scripserit suscipiantur eos, in affert pericula contentiones sed, cetero sanctus et pro. Ius vidit magna regione te, sit ei elaboraret liberavisse. Mundi verear eu mea, eam vero scriptorem in, vix in menandri assueverit. Natum definiebas cu vim. Vim doming vocibus efficiantur id. In indoctum deseruisse voluptatum vim, ad debitis verterem sed.', 4,
47, 'Cu scripserit suscipiantur eos, in affert |pericula contentiones sed, cetero sanctus et |pro. Ius vidit magna regione te, sit ei |elaboraret liberavisse. Mundi verear eu mea, |eam vero scriptorem in, vix in menandri |assueverit. Natum definiebas cu vim. Vim |doming vocibus efficiantur id. In indoctum |deseruisse voluptatum vim, ad debitis verterem |sed.',
142, 'Cu scripserit suscipiantur eos, in affert pericula contentiones sed, cetero sanctus et pro. Ius vidit magna regione te, sit ei elaboraret |liberavisse. Mundi verear eu mea, eam vero scriptorem in, vix in menandri assueverit. Natum definiebas cu vim. Vim doming vocibus efficiantur |id. In indoctum deseruisse voluptatum vim, ad debitis verterem sed.',
);
assertIncrementalLineBreaks(
factory, 'An his legere persecuti, oblique delicata efficiantur ex vix, vel at graecis officiis maluisset. Et per impedit voluptua, usu discere maiorum at. Ut assum ornatus temporibus vis, an sea melius pericula. Ea dicunt oblique phaedrum nam, eu duo movet nobis. His melius facilis eu, vim malorum temporibus ne. Nec no sale regione, meliore civibus placerat id eam. Mea alii fabulas definitionem te, agam volutpat ad vis, et per bonorum nonumes repudiandae.', 4,
57, 'An his legere persecuti, oblique delicata efficiantur ex |vix, vel at graecis officiis maluisset. Et per impedit |voluptua, usu discere maiorum at. Ut assum ornatus |temporibus vis, an sea melius pericula. Ea dicunt |oblique phaedrum nam, eu duo movet nobis. His melius |facilis eu, vim malorum temporibus ne. Nec no sale |regione, meliore civibus placerat id eam. Mea alii |fabulas definitionem te, agam volutpat ad vis, et per |bonorum nonumes repudiandae.',
58, 'An his legere persecuti, oblique delicata efficiantur ex |vix, vel at graecis officiis maluisset. Et per impedit |voluptua, usu discere maiorum at. Ut assum ornatus |temporibus vis, an sea melius pericula. Ea dicunt oblique |phaedrum nam, eu duo movet nobis. His melius facilis eu, |vim malorum temporibus ne. Nec no sale regione, meliore |civibus placerat id eam. Mea alii fabulas definitionem |te, agam volutpat ad vis, et per bonorum nonumes |repudiandae.'
);
assertIncrementalLineBreaks(
factory, '\t\t"owner": "vscode",', 4,
14, '\t\t"owner|": |"vscod|e",',
16, '\t\t"owner":| |"vscode"|,',
WrappingIndent.Same
);
assertIncrementalLineBreaks(
factory, '🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇&👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬', 4,
51, '🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇&|👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬',
50, '🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇|&|👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬',
WrappingIndent.Same
);
assertIncrementalLineBreaks(
factory, '🐇👬&🌞🌖', 4,
5, '🐇👬&|🌞🌖',
4, '🐇👬|&|🌞🌖',
WrappingIndent.Same
);
assertIncrementalLineBreaks(
factory, '\t\tfunc(\'🌞🏇🍼🌞🏇🍼🐇&👬🌖🌞👬🌖🌞🏇🍼🐇👬\', WrappingIndent.Same);', 4,
26, '\t\tfunc|(\'🌞🏇🍼🌞🏇🍼🐇&|👬🌖🌞👬🌖🌞🏇🍼🐇|👬\', |WrappingIndent.|Same);',
27, '\t\tfunc|(\'🌞🏇🍼🌞🏇🍼🐇&|👬🌖🌞👬🌖🌞🏇🍼🐇|👬\', |WrappingIndent.|Same);',
WrappingIndent.Same
);
assertIncrementalLineBreaks(
factory, 'factory, "xtxtfunc(x"🌞🏇🍼🌞🏇🍼🐇&👬🌖🌞👬🌖🌞🏇🍼🐇👬x"', 4,
16, 'factory, |"xtxtfunc|(x"🌞🏇🍼🌞🏇🍼|🐇&|👬🌖🌞👬🌖🌞🏇🍼|🐇👬x"',
17, 'factory, |"xtxtfunc|(x"🌞🏇🍼🌞🏇🍼🐇|&👬🌖🌞👬🌖🌞🏇🍼|🐇👬x"',
WrappingIndent.Same
);
});
test('MonospaceLineBreaksComputer - CJK and Kinsoku Shori', () => {
let factory = new MonospaceLineBreaksComputerFactory('(', '\t)');
assertLineBreaks(factory, 4, 5, 'aa \u5b89|\u5b89');
assertLineBreaks(factory, 4, 5, '\u3042 \u5b89|\u5b89');
assertLineBreaks(factory, 4, 5, '\u3042\u3042|\u5b89\u5b89');
assertLineBreaks(factory, 4, 5, 'aa |\u5b89)\u5b89|\u5b89');
assertLineBreaks(factory, 4, 5, 'aa \u3042|\u5b89\u3042)|\u5b89');
assertLineBreaks(factory, 4, 5, 'aa |(\u5b89aa|\u5b89');
});
test('MonospaceLineBreaksComputer - WrappingIndent.Same', () => {
let factory = new MonospaceLineBreaksComputerFactory('', '\t ');
assertLineBreaks(factory, 4, 38, ' *123456789012345678901234567890123456|7890', WrappingIndent.Same);
});
test('issue #16332: Scroll bar overlaying on top of text', () => {
let factory = new MonospaceLineBreaksComputerFactory('', '\t ');
assertLineBreaks(factory, 4, 24, 'a/ very/long/line/of/tex|t/that/expands/beyon|d/your/typical/line/|of/code/', WrappingIndent.Indent);
});
test('issue #35162: wrappingIndent not consistently working', () => {
let factory = new MonospaceLineBreaksComputerFactory('', '\t ');
let mapper = assertLineBreaks(factory, 4, 24, ' t h i s |i s |a l |o n |g l |i n |e', WrappingIndent.Indent);
assert.equal(mapper!.wrappedTextIndentLength, ' '.length);
});
test('issue #75494: surrogate pairs', () => {
let factory = new MonospaceLineBreaksComputerFactory('\t', ' ');
assertLineBreaks(factory, 4, 49, '🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼|🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼|🐇👬', WrappingIndent.Same);
});
test('issue #75494: surrogate pairs overrun 1', () => {
const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);
assertLineBreaks(factory, 4, 4, '🐇👬|&|🌞🌖', WrappingIndent.Same);
});
test('issue #75494: surrogate pairs overrun 2', () => {
const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);
assertLineBreaks(factory, 4, 17, 'factory, |"xtxtfunc|(x"🌞🏇🍼🌞🏇🍼🐇|&👬🌖🌞👬🌖🌞🏇🍼|🐇👬x"', WrappingIndent.Same);
});
test('MonospaceLineBreaksComputer - WrappingIndent.DeepIndent', () => {
let factory = new MonospaceLineBreaksComputerFactory('', '\t ');
let mapper = assertLineBreaks(factory, 4, 26, ' W e A r e T e s t |i n g D e |e p I n d |e n t a t |i o n', WrappingIndent.DeepIndent);
assert.equal(mapper!.wrappedTextIndentLength, ' '.length);
});
test('issue #33366: Word wrap algorithm behaves differently around punctuation', () => {
const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue);
assertLineBreaks(factory, 4, 23, 'this is a line of |text, text that sits |on a line', WrappingIndent.Same);
});
});

View file

@ -4,9 +4,18 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { toUint32Array } from 'vs/base/common/uint';
import { toUint32 } from 'vs/base/common/uint';
import { PrefixSumComputer, PrefixSumIndexOfResult } from 'vs/editor/common/viewModel/prefixSumComputer';
function toUint32Array(arr: number[]): Uint32Array {
const len = arr.length;
const r = new Uint32Array(len);
for (let i = 0; i < len; i++) {
r[i] = toUint32(arr[i]);
}
return r;
}
suite('Editor ViewModel - PrefixSumComputer', () => {
test('PrefixSumComputer', () => {

View file

@ -9,14 +9,12 @@ import { IViewLineTokens } from 'vs/editor/common/core/lineTokens';
import { Position } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
import { TokenizationResult2 } from 'vs/editor/common/core/token';
import { toUint32Array } from 'vs/base/common/uint';
import { EndOfLinePreference } from 'vs/editor/common/model';
import { TextModel } from 'vs/editor/common/model/textModel';
import * as modes from 'vs/editor/common/modes';
import { NULL_STATE } from 'vs/editor/common/modes/nullMode';
import { CharacterHardWrappingLineMapperFactory, CharacterHardWrappingLineMapping } from 'vs/editor/common/viewModel/characterHardWrappingLineMapper';
import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer';
import { ILineMapping, ISimpleModel, SplitLine, SplitLinesCollection } from 'vs/editor/common/viewModel/splitLinesCollection';
import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer';
import { LineBreakData, ISimpleModel, SplitLine, SplitLinesCollection } from 'vs/editor/common/viewModel/splitLinesCollection';
import { ViewLineData } from 'vs/editor/common/viewModel/viewModel';
import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
@ -25,7 +23,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions';
suite('Editor ViewModel - SplitLinesCollection', () => {
test('SplitLine', () => {
let model1 = createModel('My First LineMy Second LineAnd another one');
let line1 = createSplitLine([13, 14, 15], '');
let line1 = createSplitLine([13, 14, 15], [13, 13 + 14, 13 + 14 + 15], 0);
assert.equal(line1.getViewLineCount(), 3);
assert.equal(line1.getViewLineContent(model1, 1, 0), 'My First Line');
@ -54,38 +52,38 @@ suite('Editor ViewModel - SplitLinesCollection', () => {
}
model1 = createModel('My First LineMy Second LineAnd another one');
line1 = createSplitLine([13, 14, 15], '\t');
line1 = createSplitLine([13, 14, 15], [13, 13 + 14, 13 + 14 + 15], 4);
assert.equal(line1.getViewLineCount(), 3);
assert.equal(line1.getViewLineContent(model1, 1, 0), 'My First Line');
assert.equal(line1.getViewLineContent(model1, 1, 1), '\tMy Second Line');
assert.equal(line1.getViewLineContent(model1, 1, 2), '\tAnd another one');
assert.equal(line1.getViewLineContent(model1, 1, 1), ' My Second Line');
assert.equal(line1.getViewLineContent(model1, 1, 2), ' And another one');
assert.equal(line1.getViewLineMaxColumn(model1, 1, 0), 14);
assert.equal(line1.getViewLineMaxColumn(model1, 1, 1), 16);
assert.equal(line1.getViewLineMaxColumn(model1, 1, 2), 17);
for (let col = 1; col <= 14; col++) {
assert.equal(line1.getModelColumnOfViewPosition(0, col), col, 'getInputColumnOfOutputPosition(0, ' + col + ')');
}
for (let col = 1; col <= 1; col++) {
assert.equal(line1.getModelColumnOfViewPosition(1, 1), 13 + col, 'getInputColumnOfOutputPosition(1, ' + col + ')');
}
for (let col = 2; col <= 16; col++) {
assert.equal(line1.getModelColumnOfViewPosition(1, col), 13 + col - 1, 'getInputColumnOfOutputPosition(1, ' + col + ')');
}
for (let col = 1; col <= 1; col++) {
assert.equal(line1.getModelColumnOfViewPosition(2, col), 13 + 14 + col, 'getInputColumnOfOutputPosition(2, ' + col + ')');
}
for (let col = 2; col <= 17; col++) {
assert.equal(line1.getModelColumnOfViewPosition(2, col), 13 + 14 + col - 1, 'getInputColumnOfOutputPosition(2, ' + col + ')');
assert.equal(line1.getViewLineMaxColumn(model1, 1, 1), 19);
assert.equal(line1.getViewLineMaxColumn(model1, 1, 2), 20);
let actualViewColumnMapping: number[][] = [];
for (let lineIndex = 0; lineIndex < line1.getViewLineCount(); lineIndex++) {
let actualLineViewColumnMapping: number[] = [];
for (let col = 1; col <= line1.getViewLineMaxColumn(model1, 1, lineIndex); col++) {
actualLineViewColumnMapping.push(line1.getModelColumnOfViewPosition(lineIndex, col));
}
actualViewColumnMapping.push(actualLineViewColumnMapping);
}
assert.deepEqual(actualViewColumnMapping, [
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
[14, 14, 14, 14, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28],
[28, 28, 28, 28, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43],
]);
for (let col = 1; col <= 13; col++) {
assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(0, col), 'getOutputPositionOfInputPosition(' + col + ')');
assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(0, col), '6.getOutputPositionOfInputPosition(' + col + ')');
}
for (let col = 1 + 13; col <= 14 + 13; col++) {
assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(1, 1 + col - 13), 'getOutputPositionOfInputPosition(' + col + ')');
assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(1, 4 + col - 13), '7.getOutputPositionOfInputPosition(' + col + ')');
}
for (let col = 1 + 13 + 14; col <= 15 + 14 + 13; col++) {
assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(2, 1 + col - 13 - 14), 'getOutputPositionOfInputPosition(' + col + ')');
assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(2, 4 + col - 13 - 14), '8.getOutputPositionOfInputPosition(' + col + ')');
}
});
@ -95,13 +93,11 @@ suite('Editor ViewModel - SplitLinesCollection', () => {
const fontInfo = config.options.get(EditorOption.fontInfo);
const wordWrapBreakAfterCharacters = config.options.get(EditorOption.wordWrapBreakAfterCharacters);
const wordWrapBreakBeforeCharacters = config.options.get(EditorOption.wordWrapBreakBeforeCharacters);
const wordWrapBreakObtrusiveCharacters = config.options.get(EditorOption.wordWrapBreakObtrusiveCharacters);
const wrappingIndent = config.options.get(EditorOption.wrappingIndent);
const hardWrappingLineMapperFactory = new CharacterHardWrappingLineMapperFactory(
const lineBreaksComputerFactory = new MonospaceLineBreaksComputerFactory(
wordWrapBreakBeforeCharacters,
wordWrapBreakAfterCharacters,
wordWrapBreakObtrusiveCharacters
wordWrapBreakAfterCharacters
);
const model = TextModel.createFromString([
@ -115,7 +111,7 @@ suite('Editor ViewModel - SplitLinesCollection', () => {
const linesCollection = new SplitLinesCollection(
model,
hardWrappingLineMapperFactory,
lineBreaksComputerFactory,
model.getOptions().tabSize,
wrappingInfo.wrappingColumn,
fontInfo.typicalFullwidthCharacterWidth / fontInfo.typicalHalfwidthCharacterWidth,
@ -616,12 +612,12 @@ suite('SplitLinesCollection', () => {
]
},
{
content: ' world");',
minColumn: 4,
maxColumn: 12,
content: ' world");',
minColumn: 13,
maxColumn: 21,
tokens: [
{ endIndex: 9, value: 15 },
{ endIndex: 11, value: 16 },
{ endIndex: 18, value: 15 },
{ endIndex: 20, value: 16 },
]
},
{
@ -658,28 +654,28 @@ suite('SplitLinesCollection', () => {
]
},
{
content: ' world, this is a ',
minColumn: 4,
maxColumn: 21,
content: ' world, this is a ',
minColumn: 13,
maxColumn: 30,
tokens: [
{ endIndex: 20, value: 28 },
{ endIndex: 29, value: 28 },
]
},
{
content: ' somewhat longer ',
minColumn: 4,
content: ' somewhat longer ',
minColumn: 13,
maxColumn: 29,
tokens: [
{ endIndex: 28, value: 28 },
]
},
{
content: ' line");',
minColumn: 13,
maxColumn: 20,
tokens: [
{ endIndex: 19, value: 28 },
]
},
{
content: ' line");',
minColumn: 4,
maxColumn: 11,
tokens: [
{ endIndex: 8, value: 28 },
{ endIndex: 10, value: 29 },
{ endIndex: 17, value: 28 },
{ endIndex: 19, value: 29 },
]
},
{
@ -749,13 +745,11 @@ suite('SplitLinesCollection', () => {
const fontInfo = configuration.options.get(EditorOption.fontInfo);
const wordWrapBreakAfterCharacters = configuration.options.get(EditorOption.wordWrapBreakAfterCharacters);
const wordWrapBreakBeforeCharacters = configuration.options.get(EditorOption.wordWrapBreakBeforeCharacters);
const wordWrapBreakObtrusiveCharacters = configuration.options.get(EditorOption.wordWrapBreakObtrusiveCharacters);
const wrappingIndent = configuration.options.get(EditorOption.wrappingIndent);
const factory = new CharacterHardWrappingLineMapperFactory(
const factory = new MonospaceLineBreaksComputerFactory(
wordWrapBreakBeforeCharacters,
wordWrapBreakAfterCharacters,
wordWrapBreakObtrusiveCharacters
wordWrapBreakAfterCharacters
);
const linesCollection = new SplitLinesCollection(
@ -778,15 +772,16 @@ function pos(lineNumber: number, column: number): Position {
return new Position(lineNumber, column);
}
function createSplitLine(splitLengths: number[], wrappedLinesPrefix: string, isVisible: boolean = true): SplitLine {
return new SplitLine(createLineMapping(splitLengths, wrappedLinesPrefix), isVisible);
function createSplitLine(splitLengths: number[], breakingOffsetsVisibleColumn: number[], wrappedTextIndentWidth: number, isVisible: boolean = true): SplitLine {
return new SplitLine(createLineBreakData(splitLengths, breakingOffsetsVisibleColumn, wrappedTextIndentWidth), isVisible);
}
function createLineMapping(breakingLengths: number[], wrappedLinesPrefix: string): ILineMapping {
return new CharacterHardWrappingLineMapping(
new PrefixSumComputer(toUint32Array(breakingLengths)),
wrappedLinesPrefix
);
function createLineBreakData(breakingLengths: number[], breakingOffsetsVisibleColumn: number[], wrappedTextIndentWidth: number): LineBreakData {
let sums: number[] = [];
for (let i = 0; i < breakingLengths.length; i++) {
sums[i] = (i > 0 ? sums[i - 1] : 0) + breakingLengths[i];
}
return new LineBreakData(sums, breakingOffsetsVisibleColumn, wrappedTextIndentWidth);
}
function createModel(text: string): ISimpleModel {

View file

@ -7,6 +7,7 @@ import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { TextModel } from 'vs/editor/common/model/textModel';
import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl';
import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration';
import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer';
export function testViewModel(text: string[], options: IEditorOptions, callback: (viewModel: ViewModel, model: TextModel) => void): void {
const EDITOR_ID = 1;
@ -15,7 +16,7 @@ export function testViewModel(text: string[], options: IEditorOptions, callback:
let model = TextModel.createFromString(text.join('\n'));
let viewModel = new ViewModel(EDITOR_ID, configuration, model, null!);
let viewModel = new ViewModel(EDITOR_ID, configuration, model, MonospaceLineBreaksComputerFactory.create(configuration.options), null!);
callback(viewModel, model);

10
src/vs/monaco.d.ts vendored
View file

@ -2709,19 +2709,14 @@ declare namespace monaco.editor {
wrappingIndent?: 'none' | 'same' | 'indent' | 'deepIndent';
/**
* Configure word wrapping characters. A break will be introduced before these characters.
* Defaults to '{([+'.
* Defaults to '([{‘“〈《「『【〔([{「£¥$£¥+'.
*/
wordWrapBreakBeforeCharacters?: string;
/**
* Configure word wrapping characters. A break will be introduced after these characters.
* Defaults to ' \t})]?|&,;'.
* Defaults to ' \t})]?|/&.,;¢°′″‰℃、。。、¢,.:;?!%・・ゝゞヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻ァィゥェォャュョッー”〉》」』】〕)]}」'.
*/
wordWrapBreakAfterCharacters?: string;
/**
* Configure word wrapping characters. A break will be introduced after these characters only if no `wordWrapBreakBeforeCharacters` or `wordWrapBreakAfterCharacters` were found.
* Defaults to '.'.
*/
wordWrapBreakObtrusiveCharacters?: string;
/**
* Performance guard: Stop rendering a line after x characters.
* Defaults to 10000.
@ -4854,6 +4849,7 @@ declare namespace monaco.languages {
export interface CompletionList {
suggestions: CompletionItem[];
incomplete?: boolean;
isDetailsResolved?: boolean;
dispose?(): void;
}

1
src/vs/vscode.d.ts vendored
View file

@ -1193,7 +1193,6 @@ declare module 'vscode' {
* A complex edit that will be applied in one transaction on a TextEditor.
* This holds a description of the edits and if the edits are valid (i.e. no overlapping regions, document was not changed in the meantime, etc.)
* they can be applied on a [document](#TextDocument) associated with a [text editor](#TextEditor).
*
*/
export interface TextEditorEdit {
/**

View file

@ -1504,4 +1504,15 @@ declare module 'vscode' {
export const onDidChangeActiveColorTheme: Event<ColorTheme>;
}
//#endregion
//#region https://github.com/microsoft/vscode/issues/39441
export interface CompletionList {
isDetailsResolved?: boolean;
}
//#endregion
}

View file

@ -367,6 +367,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha
return {
suggestions: result.b.map(d => MainThreadLanguageFeatures._inflateSuggestDto(result.a, d)),
incomplete: result.c,
isDetailsResolved: result.d,
dispose: () => typeof result.x === 'number' && this._proxy.$releaseCompletionItems(handle, result.x)
};
});

View file

@ -1012,6 +1012,7 @@ export interface ISuggestResultDto {
a: { insert: IRange, replace: IRange; };
b: ISuggestDataDto[];
c?: boolean;
d?: boolean;
}
export interface ISignatureHelpDto {

View file

@ -787,7 +787,8 @@ class SuggestAdapter {
x: pid,
b: [],
a: { replace: typeConvert.Range.from(replaceRange), insert: typeConvert.Range.from(insertRange) },
c: list.isIncomplete || undefined
c: list.isIncomplete || undefined,
d: list.isDetailsResolved || undefined
};
for (let i = 0; i < list.items.length; i++) {

View file

@ -1395,7 +1395,7 @@ export class CompletionItem implements vscode.CompletionItem {
export class CompletionList {
isIncomplete?: boolean;
isDetailsResolved?: boolean;
items: vscode.CompletionItem[];
constructor(items: vscode.CompletionItem[] = [], isIncomplete: boolean = false) {

View file

@ -190,10 +190,19 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe
private parseIpAddress(hex: string): string {
let result = '';
for (let i = hex.length - 2; (i >= 0); i -= 2) {
result += parseInt(hex.substr(i, 2), 16);
if (i !== 0) {
result += '.';
if (hex.length === 8) {
for (let i = hex.length - 2; i >= 0; i -= 2) {
result += parseInt(hex.substr(i, 2), 16);
if (i !== 0) {
result += '.';
}
}
} else {
for (let i = hex.length - 4; i >= 0; i -= 4) {
result += parseInt(hex.substr(i, 4), 16).toString(16);
if (i !== 0) {
result += ':';
}
}
}
return result;

View file

@ -4,16 +4,22 @@
*--------------------------------------------------------------------------------------------*/
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { Disposable, DisposableStore, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
import { IFilesConfigurationService, AutoSaveMode, IAutoSaveConfiguration } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { SaveReason, IEditorIdentifier, IEditorInput, GroupIdentifier } from 'vs/workbench/common/editor';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { withNullAsUndefined } from 'vs/base/common/types';
import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService';
export class EditorAutoSave extends Disposable implements IWorkbenchContribution {
// Auto save: after delay
private autoSaveAfterDelay: number | undefined;
private readonly pendingAutoSavesAfterDelay = new Map<IWorkingCopy, IDisposable>();
// Auto save: focus change & window change
private lastActiveEditor: IEditorInput | undefined = undefined;
private lastActiveGroupId: GroupIdentifier | undefined = undefined;
private lastActiveEditorControlDisposable = this._register(new DisposableStore());
@ -22,17 +28,22 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution
@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService,
@IHostService private readonly hostService: IHostService,
@IEditorService private readonly editorService: IEditorService,
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService
) {
super();
// Figure out initial auto save config
this.onAutoSaveConfigurationChange(filesConfigurationService.getAutoSaveConfiguration(), false);
this.registerListeners();
}
private registerListeners(): void {
this._register(this.hostService.onDidChangeFocus(focused => this.onWindowFocusChange(focused)));
this._register(this.editorService.onDidActiveEditorChange(() => this.onDidActiveEditorChange()));
this._register(this.filesConfigurationService.onAutoSaveConfigurationChange(() => this.onAutoSaveConfigurationChange()));
this._register(this.filesConfigurationService.onAutoSaveConfigurationChange(config => this.onAutoSaveConfigurationChange(config, true)));
this._register(this.workingCopyService.onDidChangeDirty(workingCopy => this.onDidWorkingCopyChangeDirty(workingCopy)));
}
private onWindowFocusChange(focused: boolean): void {
@ -85,24 +96,55 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution
}
}
private onAutoSaveConfigurationChange(): void {
let reason: SaveReason | undefined = undefined;
switch (this.filesConfigurationService.getAutoSaveMode()) {
case AutoSaveMode.ON_FOCUS_CHANGE:
reason = SaveReason.FOCUS_CHANGE;
break;
case AutoSaveMode.ON_WINDOW_CHANGE:
reason = SaveReason.WINDOW_CHANGE;
break;
case AutoSaveMode.AFTER_SHORT_DELAY:
case AutoSaveMode.AFTER_LONG_DELAY:
reason = SaveReason.AUTO;
break;
}
private onAutoSaveConfigurationChange(config: IAutoSaveConfiguration, fromEvent: boolean): void {
// Update auto save after delay config
this.autoSaveAfterDelay = (typeof config.autoSaveDelay === 'number') && config.autoSaveDelay > 0 ? config.autoSaveDelay : undefined;
// Trigger a save-all when auto save is enabled
if (reason) {
this.editorService.saveAll({ reason });
if (fromEvent) {
let reason: SaveReason | undefined = undefined;
switch (this.filesConfigurationService.getAutoSaveMode()) {
case AutoSaveMode.ON_FOCUS_CHANGE:
reason = SaveReason.FOCUS_CHANGE;
break;
case AutoSaveMode.ON_WINDOW_CHANGE:
reason = SaveReason.WINDOW_CHANGE;
break;
case AutoSaveMode.AFTER_SHORT_DELAY:
case AutoSaveMode.AFTER_LONG_DELAY:
reason = SaveReason.AUTO;
break;
}
if (reason) {
this.editorService.saveAll({ reason });
}
}
}
private onDidWorkingCopyChangeDirty(workingCopy: IWorkingCopy): void {
if (typeof this.autoSaveAfterDelay !== 'number') {
return; // auto save after delay must be enabled
}
if (workingCopy.capabilities & WorkingCopyCapabilities.Untitled) {
return; // we never auto save untitled working copies
}
// Clear any running auto save operation
dispose(this.pendingAutoSavesAfterDelay.get(workingCopy));
this.pendingAutoSavesAfterDelay.delete(workingCopy);
// Working copy got dirty - start auto save
if (workingCopy.isDirty()) {
const handle = setTimeout(() => {
if (workingCopy.isDirty()) {
workingCopy.save({ reason: SaveReason.AUTO });
}
}, this.autoSaveAfterDelay);
this.pendingAutoSavesAfterDelay.set(workingCopy, toDisposable(() => clearTimeout(handle)));
}
}
}

View file

@ -3,12 +3,15 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { EditorInput, ITextEditorModel, IModeSupport } from 'vs/workbench/common/editor';
import { EditorInput, ITextEditorModel, IModeSupport, GroupIdentifier, isTextEditor } from 'vs/workbench/common/editor';
import { URI } from 'vs/base/common/uri';
import { IReference } from 'vs/base/common/lifecycle';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel';
import { basename } from 'vs/base/common/resources';
import { ITextFileSaveOptions, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import type { IEditorViewState } from 'vs/editor/common/editorCommon';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
/**
* A read-only text editor input whos contents are made of the provided resource that points to an existing
@ -26,7 +29,9 @@ export class ResourceEditorInput extends EditorInput implements IModeSupport {
private description: string | undefined,
private readonly resource: URI,
private preferredMode: string | undefined,
@ITextModelService private readonly textModelResolverService: ITextModelService
@ITextModelService private readonly textModelResolverService: ITextModelService,
@ITextFileService private readonly textFileService: ITextFileService,
@IEditorService private readonly editorService: IEditorService
) {
super();
@ -104,6 +109,28 @@ export class ResourceEditorInput extends EditorInput implements IModeSupport {
return model;
}
async saveAs(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise<boolean> {
// Preserve view state by opening the editor first. In addition
// this allows the user to review the contents of the editor.
let viewState: IEditorViewState | undefined = undefined;
const editor = await this.editorService.openEditor(this, undefined, group);
if (isTextEditor(editor)) {
viewState = editor.getViewState();
}
// Save as
const target = await this.textFileService.saveAs(this.resource, undefined, options);
if (!target) {
return false; // save cancelled
}
// Open the target
await this.editorService.openEditor({ resource: target, options: { viewState, pinned: true } }, group);
return true;
}
matches(otherInput: unknown): boolean {
if (super.matches(otherInput) === true) {
return true;

View file

@ -13,6 +13,7 @@ import * as nls from 'vs/nls';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { FileOperation, IFileService } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
import * as colorRegistry from 'vs/platform/theme/common/colorRegistry';
@ -25,7 +26,7 @@ import { CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CONTEXT_HAS_CUSTOM_EDITORS,
import { CustomEditorModelManager } from 'vs/workbench/contrib/customEditor/common/customEditorModelManager';
import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput';
import { IWebviewService, webviewHasOwnEditFunctionsContext } from 'vs/workbench/contrib/webview/browser/webview';
import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorService, IOpenEditorOverride } from 'vs/workbench/services/editor/common/editorService';
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { CustomFileEditorInput } from './customEditorInput';
@ -81,8 +82,10 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ
constructor(
@IContextKeyService contextKeyService: IContextKeyService,
@IWorkingCopyService workingCopyService: IWorkingCopyService,
@IFileService fileService: IFileService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IEditorService private readonly editorService: IEditorService,
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IWebviewService private readonly webviewService: IWebviewService,
@ -112,6 +115,13 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ
this._webviewHasOwnEditFunctions = webviewHasOwnEditFunctionsContext.bindTo(contextKeyService);
this._register(this.editorService.onDidActiveEditorChange(() => this.updateContexts()));
this._register(fileService.onAfterOperation(e => {
if (e.isOperation(FileOperation.MOVE)) {
this.handleMovedFileInOpenedFileEditors(e.resource, e.target.resource);
}
}));
this.updateContexts();
}
@ -262,6 +272,31 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ
this._focusedCustomEditorIsEditable.set(activeControl?.input instanceof CustomFileEditorInput);
this._webviewHasOwnEditFunctions.set(true);
}
private handleMovedFileInOpenedFileEditors(oldResource: URI, newResource: URI): void {
for (const group of this.editorGroupService.groups) {
for (const editor of group.editors) {
if (!(editor instanceof CustomFileEditorInput)) {
continue;
}
const editorInfo = this._editorInfoStore.get(editor.viewType);
if (!editorInfo) {
continue;
}
if (!editorInfo.matches(newResource)) {
continue;
}
const replacement = this.createInput(newResource, editor.viewType, group);
this.editorService.replaceEditors([{
editor: editor,
replacement: replacement,
}], group);
}
}
}
}
export const customEditorsAssociationsKey = 'workbench.experimental.editorAssociations';

View file

@ -202,9 +202,9 @@ async function deleteFiles(textFileService: ITextFileService, dialogService: IDi
let { message, detail } = getMoveToTrashMessage(distinctElements);
detail += detail ? '\n' : '';
if (isWindows) {
detail += nls.localize('undoBin', "You can restore from the Recycle Bin.");
detail += distinctElements.length > 1 ? nls.localize('undoBinFiles', "You can restore these files from the Recycle Bin.") : nls.localize('undoBin', "You can restore this file from the Recycle Bin.");
} else {
detail += nls.localize('undoTrash', "You can restore from the Trash.");
detail += distinctElements.length > 1 ? nls.localize('undoTrashFiles', "You can restore these files from the Trash.") : nls.localize('undoTrash', "You can restore this file from the Trash.");
}
confirmDeletePromise = dialogService.confirm({

View file

@ -69,14 +69,16 @@ export class FileEditorInput extends TextEditorInput implements IFileEditorInput
private registerListeners(): void {
// Model changes
// Dirty changes
this._register(this.textFileService.models.onModelDirty(e => this.onDirtyStateChange(e)));
this._register(this.textFileService.models.onModelSaveError(e => this.onDirtyStateChange(e)));
this._register(this.textFileService.models.onModelSaved(e => this.onDirtyStateChange(e)));
this._register(this.textFileService.models.onModelReverted(e => this.onDirtyStateChange(e)));
this._register(this.textFileService.models.onModelOrphanedChanged(e => this.onModelOrphanedChanged(e)));
// Label changes
this._register(this.labelService.onDidChangeFormatters(() => FileEditorInput.MEMOIZER.clear()));
this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(() => FileEditorInput.MEMOIZER.clear()));
this._register(this.textFileService.models.onModelOrphanedChanged(e => this.onModelOrphanedChanged(e)));
}
private onDirtyStateChange(e: TextFileModelChangeEvent): void {

View file

@ -0,0 +1,113 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { Event } from 'vs/base/common/event';
import { toResource } from 'vs/base/test/common/utils';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { workbenchInstantiationService, TestTextFileService, TestFileService, TestFilesConfigurationService, TestEnvironmentService } from 'vs/workbench/test/workbenchTestServices';
import { ITextFileService, IResolvedTextFileEditorModel, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
import { IFileService } from 'vs/platform/files/common/files';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor';
import { Registry } from 'vs/platform/registry/common/platform';
import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { EditorInput } from 'vs/workbench/common/editor';
import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput';
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager';
import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart';
import { EditorService } from 'vs/workbench/services/editor/browser/editorService';
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
import { EditorAutoSave } from 'vs/workbench/browser/parts/editor/editorAutoSave';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
class ServiceAccessor {
constructor(
@IEditorService public editorService: IEditorService,
@IEditorGroupsService public editorGroupService: IEditorGroupsService,
@ITextFileService public textFileService: TestTextFileService,
@IFileService public fileService: TestFileService,
@IUntitledTextEditorService public untitledTextEditorService: IUntitledTextEditorService,
@IConfigurationService public configurationService: TestConfigurationService
) {
}
}
suite('EditorAutoSave', () => {
let disposables: IDisposable[] = [];
setup(() => {
disposables.push(Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
EditorDescriptor.create(
TextFileEditor,
TextFileEditor.ID,
'Text File Editor'
),
[new SyncDescriptor<EditorInput>(FileEditorInput)]
));
});
teardown(() => {
dispose(disposables);
disposables = [];
});
test('editor auto saves after short delay if configured', async function () {
const instantiationService = workbenchInstantiationService();
const configurationService = new TestConfigurationService();
configurationService.setUserConfiguration('files', { autoSave: 'afterDelay', autoSaveDelay: 1 });
instantiationService.stub(IConfigurationService, configurationService);
instantiationService.stub(IFilesConfigurationService, new TestFilesConfigurationService(
<IContextKeyService>instantiationService.createInstance(MockContextKeyService),
configurationService,
TestEnvironmentService
));
const part = instantiationService.createInstance(EditorPart);
part.create(document.createElement('div'));
part.layout(400, 300);
instantiationService.stub(IEditorGroupsService, part);
const editorService: EditorService = instantiationService.createInstance(EditorService);
instantiationService.stub(IEditorService, editorService);
const accessor = instantiationService.createInstance(ServiceAccessor);
const editorAutoSave = instantiationService.createInstance(EditorAutoSave);
const resource = toResource.call(this, '/path/index.txt');
const model = await accessor.textFileService.models.loadOrCreate(resource) as IResolvedTextFileEditorModel;
model.textEditorModel.setValue('Super Good');
assert.ok(model.isDirty());
await awaitModelSaved(model);
assert.ok(!model.isDirty());
part.dispose();
editorAutoSave.dispose();
(<TextFileEditorModelManager>accessor.textFileService.models).clear();
(<TextFileEditorModelManager>accessor.textFileService.models).dispose();
});
function awaitModelSaved(model: ITextFileEditorModel): Promise<void> {
return new Promise(c => {
Event.once(model.onDidChangeDirty)(c);
});
}
});

View file

@ -18,15 +18,19 @@ import { LOG_SCHEME } from 'vs/workbench/contrib/output/common/output';
import { IFileOutputChannelDescriptor } from 'vs/workbench/services/output/common/output';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
export class LogViewerInput extends ResourceEditorInput {
static readonly ID = 'workbench.editorinputs.output';
constructor(private readonly outputChannelDescriptor: IFileOutputChannelDescriptor,
@ITextModelService textModelResolverService: ITextModelService
constructor(
private readonly outputChannelDescriptor: IFileOutputChannelDescriptor,
@ITextModelService textModelResolverService: ITextModelService,
@ITextFileService textFileService: ITextFileService,
@IEditorService editorService: IEditorService
) {
super(basename(outputChannelDescriptor.file.path), dirname(outputChannelDescriptor.file.path), URI.from({ scheme: LOG_SCHEME, path: outputChannelDescriptor.id }), undefined, textModelResolverService);
super(basename(outputChannelDescriptor.file.path), dirname(outputChannelDescriptor.file.path), URI.from({ scheme: LOG_SCHEME, path: outputChannelDescriptor.id }), undefined, textModelResolverService, textFileService, editorService);
}
getTypeId(): string {

View file

@ -21,6 +21,8 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService
import { writeTransientState } from 'vs/workbench/contrib/codeEditor/browser/toggleWordWrap';
import { mergeSort } from 'vs/base/common/arrays';
import product from 'vs/platform/product/common/product';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
export class PerfviewContrib {
@ -44,14 +46,18 @@ export class PerfviewInput extends ResourceEditorInput {
static readonly Uri = URI.from({ scheme: 'perf', path: 'Startup Performance' });
constructor(
@ITextModelService textModelResolverService: ITextModelService
@ITextModelService textModelResolverService: ITextModelService,
@ITextFileService textFileService: ITextFileService,
@IEditorService editorService: IEditorService
) {
super(
localize('name', "Startup Performance"),
undefined,
PerfviewInput.Uri,
undefined,
textModelResolverService
textModelResolverService,
textFileService,
editorService
);
}

View file

@ -0,0 +1,165 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.search-editor {
display: flex;
flex-direction: column;
}
.search-editor .search-results {
flex: 1;
}
.search-editor .query-container {
margin: 0px 12px 12px 2px;
padding-top: 6px;
}
.search-editor .search-widget .toggle-replace-button {
position: absolute;
top: 0;
left: 0;
width: 16px;
height: 100%;
box-sizing: border-box;
background-position: center center;
background-repeat: no-repeat;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.search-editor .search-widget .search-container,
.search-editor .search-widget .replace-container {
margin-left: 18px;
display: flex;
align-items: center;
}
.search-editor .search-widget .monaco-findInput {
display: inline-block;
vertical-align: middle;
width: 100%;
}
.search-editor .search-widget .monaco-inputbox > .wrapper {
height: 100%;
}
.search-editor .search-widget .monaco-inputbox > .wrapper > .mirror,
.search-editor .search-widget .monaco-inputbox > .wrapper > textarea.input {
padding: 3px;
padding-left: 4px;
}
.search-editor .search-widget .monaco-inputbox > .wrapper > .mirror {
max-height: 134px;
}
/* NOTE: height is also used in searchWidget.ts as a constant*/
.search-editor .search-widget .monaco-inputbox > .wrapper > textarea.input {
overflow: initial;
height: 24px; /* set initial height before measure */
}
.search-editor .monaco-inputbox > .wrapper > textarea.input {
scrollbar-width: none; /* Firefox: hide scrollbar */
}
.search-editor .monaco-inputbox > .wrapper > textarea.input::-webkit-scrollbar {
display: none;
}
.search-editor .search-widget .context-lines-input {
display: none;
}
.search-editor .search-widget.show-context .context-lines-input {
display: inherit;
margin-left: 5px;
margin-right: 2px;
max-width: 50px;
}
.search-editor .search-widget .replace-container {
margin-top: 6px;
position: relative;
display: inline-flex;
}
.search-editor .search-widget .replace-input {
position: relative;
display: flex;
vertical-align: middle;
width: auto !important;
height: 25px;
}
.search-editor .search-widget .replace-input > .controls {
position: absolute;
top: 3px;
right: 2px;
}
.search-editor .search-widget .replace-container.disabled {
display: none;
}
.search-editor .search-widget .replace-container .monaco-action-bar {
margin-left: 0;
}
.search-editor .search-widget .replace-container .monaco-action-bar {
height: 25px;
}
.search-editor .search-widget .replace-container .monaco-action-bar .action-item .codicon {
background-repeat: no-repeat;
width: 25px;
height: 25px;
margin-right: 0;
display: flex;
align-items: center;
justify-content: center;
}
.search-editor .includes-excludes {
min-height: 1em;
position: relative;
margin: 0 0 0 17px;
}
.search-editor .includes-excludes .expand {
position: absolute;
right: -2px;
cursor: pointer;
width: 25px;
height: 16px;
z-index: 2; /* Force it above the search results message, which has a negative top margin */
}
.search-editor .includes-excludes .file-types {
display: none;
}
.search-editor .includes-excludes.expanded .file-types {
display: inherit;
}
.search-editor .includes-excludes.expanded .file-types:last-child {
padding-bottom: 10px;
}
.search-editor .includes-excludes.expanded h4 {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
padding: 4px 0 0;
margin: 0;
font-size: 11px;
font-weight: normal;
}

View file

@ -10,7 +10,7 @@ import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox';
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
import { IInputValidator, HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Event as CommonEvent, Emitter } from 'vs/base/common/event';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { attachInputBoxStyler, attachCheckboxStyler } from 'vs/platform/theme/common/styler';
@ -170,6 +170,7 @@ export class PatternInputWidget extends Widget {
case KeyCode.Escape:
this._onCancel.fire();
return;
case KeyCode.Tab: case KeyCode.Tab | KeyMod.Shift: return;
default:
if (this.searchConfig.searchOnType) {
this._onCancel.fire();

View file

@ -121,9 +121,10 @@ export class ReplaceService implements IReplaceService {
revealIfVisible: true
}
}).then(editor => {
const input = editor?.input;
const disposable = fileMatch.onDispose(() => {
if (editor && editor.input) {
editor.input.dispose();
if (input) {
input.dispose();
}
disposable.dispose();
});

View file

@ -39,7 +39,7 @@ import { ExplorerFolderContext, ExplorerRootContext, FilesExplorerFocusCondition
import { OpenAnythingHandler } from 'vs/workbench/contrib/search/browser/openAnythingHandler';
import { OpenSymbolHandler } from 'vs/workbench/contrib/search/browser/openSymbolHandler';
import { registerContributions as replaceContributions } from 'vs/workbench/contrib/search/browser/replaceContributions';
import { clearHistoryCommand, ClearSearchResultsAction, CloseReplaceAction, CollapseDeepestExpandedLevelAction, copyAllCommand, copyMatchCommand, copyPathCommand, FocusNextInputAction, FocusNextSearchResultAction, FocusPreviousInputAction, FocusPreviousSearchResultAction, focusSearchListCommand, getSearchView, openSearchView, OpenSearchViewletAction, RefreshAction, RemoveAction, ReplaceAction, ReplaceAllAction, ReplaceAllInFolderAction, ReplaceInFilesAction, toggleCaseSensitiveCommand, toggleRegexCommand, toggleWholeWordCommand, FindInFilesCommand, ToggleSearchOnTypeAction, OpenResultsInEditorAction, RerunEditorSearchAction, RerunEditorSearchWithContextAction, ExpandAllAction } from 'vs/workbench/contrib/search/browser/searchActions';
import { clearHistoryCommand, ClearSearchResultsAction, CloseReplaceAction, CollapseDeepestExpandedLevelAction, copyAllCommand, copyMatchCommand, copyPathCommand, FocusNextInputAction, FocusNextSearchResultAction, FocusPreviousInputAction, FocusPreviousSearchResultAction, focusSearchListCommand, getSearchView, openSearchView, OpenSearchViewletAction, RefreshAction, RemoveAction, ReplaceAction, ReplaceAllAction, ReplaceAllInFolderAction, ReplaceInFilesAction, toggleCaseSensitiveCommand, toggleRegexCommand, toggleWholeWordCommand, FindInFilesCommand, ToggleSearchOnTypeAction, OpenResultsInEditorAction, RerunEditorSearchAction, RerunEditorSearchWithContextAction, ExpandAllAction, OpenSearchEditorAction } from 'vs/workbench/contrib/search/browser/searchActions';
import { SearchPanel } from 'vs/workbench/contrib/search/browser/searchPanel';
import { SearchView, SearchViewPosition } from 'vs/workbench/contrib/search/browser/searchView';
import { registerContributions as searchWidgetContributions } from 'vs/workbench/contrib/search/browser/searchWidget';
@ -56,6 +56,9 @@ import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/ex
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { assertType } from 'vs/base/common/types';
import { SearchViewPaneContainer } from 'vs/workbench/contrib/search/browser/searchViewlet';
import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor';
import { SearchEditor, SearchEditorInput } from 'vs/workbench/contrib/search/browser/searchEditor';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
registerSingleton(ISearchWorkbenchService, SearchWorkbenchService, true);
registerSingleton(ISearchHistoryService, SearchHistoryService, true);
@ -647,6 +650,11 @@ registry.registerWorkbenchAction(
'Search: Open Results in Editor', category,
ContextKeyExpr.and(Constants.EnableSearchEditorPreview));
registry.registerWorkbenchAction(
SyncActionDescriptor.create(OpenSearchEditorAction, OpenSearchEditorAction.ID, OpenSearchEditorAction.LABEL),
'Search: Open new Search Editor', category,
ContextKeyExpr.and(Constants.EnableSearchEditorPreview));
const searchEditorCategory = nls.localize({ comment: ['The name of the tabbed search view'], key: 'searcheditor' }, "Search Editor");
registry.registerWorkbenchAction(
SyncActionDescriptor.create(RerunEditorSearchAction, RerunEditorSearchAction.ID, RerunEditorSearchAction.LABEL,
@ -828,6 +836,17 @@ configurationRegistry.registerConfiguration({
default: false,
description: nls.localize('search.enableSearchEditorPreview', "Experimental: When enabled, allows opening workspace search results in an editor.")
},
'search.searchEditorPreview.doubleClickBehaviour': {
type: 'string',
enum: ['selectWord', 'goToLocation', 'openLocationToSide'],
default: 'goToLocation',
enumDescriptions: [
nls.localize('search.searchEditorPreview.doubleClickBehaviour.selectWord', "Double clicking selects the word under the cursor."),
nls.localize('search.searchEditorPreview.doubleClickBehaviour.goToLocation', "Double clicking opens the result in the active editor group."),
nls.localize('search.searchEditorPreview.doubleClickBehaviour.openLocationToSide', "Double clicking opens the result in the editor group to the side, creating one if it does not yet exist."),
],
markdownDescription: nls.localize('search.searchEditorPreview.doubleClickBehaviour', "Configure effect of double clicking a result in a Search Editor.\n\n `#search.enableSearchEditorPreview#` must be enabled for this setting to have an effect.")
},
'search.sortOrder': {
'type': 'string',
'enum': [SearchSortOrder.Default, SearchSortOrder.FileNames, SearchSortOrder.Type, SearchSortOrder.Modified, SearchSortOrder.CountDescending, SearchSortOrder.CountAscending],
@ -872,3 +891,15 @@ MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {
},
order: 2
});
Registry.as<IEditorRegistry>(EditorExtensions.Editors).registerEditor(
EditorDescriptor.create(
SearchEditor,
SearchEditor.ID,
nls.localize('defaultPreferencesEditor', "Search Editor")
),
[
new SyncDescriptor(SearchEditorInput)
]
);

View file

@ -29,7 +29,7 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { SearchViewPaneContainer } from 'vs/workbench/contrib/search/browser/searchViewlet';
import { SearchPanel } from 'vs/workbench/contrib/search/browser/searchPanel';
import { ITreeNavigator } from 'vs/base/browser/ui/tree/tree';
import { createEditorFromSearchResult, refreshActiveEditorSearch } from 'vs/workbench/contrib/search/browser/searchEditor';
import { createEditorFromSearchResult, refreshActiveEditorSearch, openNewSearchEditor } from 'vs/workbench/contrib/search/browser/searchEditor';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
@ -508,6 +508,30 @@ export class CancelSearchAction extends Action {
}
}
export class OpenSearchEditorAction extends Action {
static readonly ID: string = Constants.OpenNewEditorCommandId;
static readonly LABEL = nls.localize('search.openNewEditor', "Open new Search Editor");
constructor(id: string, label: string,
@IEditorService private editorService: IEditorService,
@IConfigurationService private configurationService: IConfigurationService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super(id, label);
}
get enabled(): boolean {
return true;
}
async run() {
if (this.configurationService.getValue<ISearchConfigurationProperties>('search').enableSearchEditorPreview) {
await openNewSearchEditor(this.editorService, this.instantiationService);
}
}
}
export class OpenResultsInEditorAction extends Action {
static readonly ID: string = Constants.OpenInEditorCommandId;
@ -518,7 +542,8 @@ export class OpenResultsInEditorAction extends Action {
@IPanelService private panelService: IPanelService,
@ILabelService private labelService: ILabelService,
@IEditorService private editorService: IEditorService,
@IConfigurationService private configurationService: IConfigurationService
@IConfigurationService private configurationService: IConfigurationService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super(id, label, 'codicon-go-to-file');
}
@ -535,7 +560,7 @@ export class OpenResultsInEditorAction extends Action {
async run() {
const searchView = getSearchView(this.viewletService, this.panelService);
if (searchView && this.configurationService.getValue<ISearchConfigurationProperties>('search').enableSearchEditorPreview) {
await createEditorFromSearchResult(searchView.searchResult, searchView.searchIncludePattern.getValue(), searchView.searchExcludePattern.getValue(), this.labelService, this.editorService);
await createEditorFromSearchResult(searchView.searchResult, searchView.searchIncludePattern.getValue(), searchView.searchExcludePattern.getValue(), this.labelService, this.editorService, this.instantiationService);
}
}
}

View file

@ -3,26 +3,356 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Match, searchMatchComparer, FileMatch, SearchResult, SearchModel } from 'vs/workbench/contrib/search/common/searchModel';
import { repeat } from 'vs/base/common/strings';
import { ILabelService } from 'vs/platform/label/common/label';
import * as DOM from 'vs/base/browser/dom';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { coalesce, flatten } from 'vs/base/common/arrays';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { URI } from 'vs/base/common/uri';
import { ITextQuery, IPatternInfo, ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search';
import { CancellationToken } from 'vs/base/common/cancellation';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import * as network from 'vs/base/common/network';
import { repeat } from 'vs/base/common/strings';
import { assertIsDefined } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import 'vs/css!./media/searchEditor';
import { isCodeEditor } from 'vs/editor/browser/editorBrowser';
import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget';
import type { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { Range } from 'vs/editor/common/core/range';
import { ITextModel, TrackedRangeStickiness, EndOfLinePreference } from 'vs/editor/common/model';
import { EndOfLinePreference, TrackedRangeStickiness } from 'vs/editor/common/model';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { localize } from 'vs/nls';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILabelService } from 'vs/platform/label/common/label';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { searchEditorFindMatch, searchEditorFindMatchBorder } from 'vs/platform/theme/common/colorRegistry';
import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorInput, EditorOptions } from 'vs/workbench/common/editor';
import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput';
import { ExcludePatternInputWidget, PatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget';
import { SearchWidget } from 'vs/workbench/contrib/search/browser/searchWidget';
import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder';
import { getOutOfWorkspaceEditorResources } from 'vs/workbench/contrib/search/common/search';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { searchEditorFindMatch, searchEditorFindMatchBorder } from 'vs/platform/theme/common/colorRegistry';
import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput';
import { localize } from 'vs/nls';
import { isCodeEditor } from 'vs/editor/browser/editorBrowser';
import { FileMatch, Match, searchMatchComparer, SearchModel, SearchResult } from 'vs/workbench/contrib/search/common/searchModel';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IPatternInfo, ISearchConfigurationProperties, ITextQuery } from 'vs/workbench/services/search/common/search';
import { Delayer } from 'vs/base/common/async';
const RESULT_LINE_REGEX = /^(\s+)(\d+)(:| )(\s+)(.*)$/;
type SearchConfiguration = {
query: string,
includes: string,
excludes: string,
contextLines: number,
wholeWord: boolean,
caseSensitive: boolean,
regexp: boolean,
useIgnores: boolean
};
let searchEditorInputInstances = 0;
export class SearchEditorInput extends EditorInput {
static readonly ID: string = 'workbench.editorinputs.searchEditorInput';
public config: SearchConfiguration;
private instanceNumber: number = searchEditorInputInstances++;
constructor(
config: SearchConfiguration | undefined,
@IModelService private readonly modelService: IModelService,
@IModeService private readonly modeService: IModeService,
) {
super();
if (config === undefined) {
this.config = { query: '', includes: '', excludes: '', contextLines: 0, wholeWord: false, caseSensitive: false, regexp: false, useIgnores: true };
} else {
this.config = config;
}
const searchResultMode = this.modeService.create('search-result');
this.modelService.createModel('', searchResultMode, this.getResource());
}
getTypeId(): string {
return SearchEditorInput.ID;
}
getResource(): URI {
return URI.from({ scheme: 'code-search', fragment: `${this.instanceNumber}` });
}
getName(): string {
return this.config.query ? localize('searchTitle.withQuery', "Search: {0}", this.config.query) : localize('searchTitle', "Search");
}
setConfig(config: SearchConfiguration) {
this.config = config;
this._onDidChangeLabel.fire();
}
async resolve() {
return null;
}
dispose() {
this.modelService.destroyModel(this.getResource());
super.dispose();
}
}
export class SearchEditor extends BaseEditor {
static readonly ID: string = 'workbench.editor.searchEditor';
private queryEditorWidget!: SearchWidget;
private searchResultEditor!: CodeEditorWidget;
private queryEditorContainer!: HTMLElement;
private dimension?: DOM.Dimension;
private inputPatternIncludes!: PatternInputWidget;
private inputPatternExcludes!: ExcludePatternInputWidget;
private includesExcludesContainer!: HTMLElement;
private toggleQueryDetailsButton!: HTMLElement;
private runSearchDelayer = new Delayer(300);
private pauseSearching: boolean = false;
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService,
@IModelService private readonly modelService: IModelService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@ILabelService private readonly labelService: ILabelService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextViewService private readonly contextViewService: IContextViewService,
@ICommandService private readonly commandService: ICommandService,
) {
super(SearchEditor.ID, telemetryService, themeService, storageService);
}
createEditor(parent: HTMLElement) {
DOM.addClass(parent, 'search-editor');
// Query
this.queryEditorContainer = DOM.append(parent, DOM.$('.query-container'));
this.queryEditorWidget = this._register(this.instantiationService.createInstance(SearchWidget, this.queryEditorContainer, { _hideReplaceToggle: true, showContextToggle: true }));
this._register(this.queryEditorWidget.onReplaceToggled(() => this.reLayout()));
this._register(this.queryEditorWidget.onDidHeightChange(() => this.reLayout()));
this.queryEditorWidget.onSearchSubmit(() => this.runSearch());
this.queryEditorWidget.searchInput.onDidOptionChange(() => this.runSearch());
this.queryEditorWidget.onDidToggleContext(() => this.runSearch());
// Includes/Excludes Dropdown
this.includesExcludesContainer = DOM.append(this.queryEditorContainer, DOM.$('.includes-excludes'));
// // Toggle query details button
this.toggleQueryDetailsButton = DOM.append(this.includesExcludesContainer, DOM.$('.expand.codicon.codicon-ellipsis', { tabindex: 0, role: 'button', title: localize('moreSearch', "Toggle Search Details") }));
this._register(DOM.addDisposableListener(this.toggleQueryDetailsButton, DOM.EventType.CLICK, e => {
DOM.EventHelper.stop(e);
this.toggleQueryDetails();
}));
this._register(DOM.addDisposableListener(this.toggleQueryDetailsButton, DOM.EventType.KEY_UP, (e: KeyboardEvent) => {
const event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
DOM.EventHelper.stop(e);
this.toggleQueryDetails();
}
}));
this._register(DOM.addDisposableListener(this.toggleQueryDetailsButton, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
const event = new StandardKeyboardEvent(e);
if (event.equals(KeyMod.Shift | KeyCode.Tab)) {
if (this.queryEditorWidget.isReplaceActive()) {
this.queryEditorWidget.focusReplaceAllAction();
} else {
this.queryEditorWidget.isReplaceShown() ? this.queryEditorWidget.replaceInput.focusOnPreserve() : this.queryEditorWidget.focusRegexAction();
}
DOM.EventHelper.stop(e);
}
}));
// // Includes
const folderIncludesList = DOM.append(this.includesExcludesContainer, DOM.$('.file-types.includes'));
const filesToIncludeTitle = localize('searchScope.includes', "files to include");
DOM.append(folderIncludesList, DOM.$('h4', undefined, filesToIncludeTitle));
this.inputPatternIncludes = this._register(this.instantiationService.createInstance(PatternInputWidget, folderIncludesList, this.contextViewService, {
ariaLabel: localize('label.includes', 'Search Include Patterns'),
}));
this.inputPatternIncludes.onSubmit(_triggeredOnType => this.runSearch());
// // Excludes
const excludesList = DOM.append(this.includesExcludesContainer, DOM.$('.file-types.excludes'));
const excludesTitle = localize('searchScope.excludes', "files to exclude");
DOM.append(excludesList, DOM.$('h4', undefined, excludesTitle));
this.inputPatternExcludes = this._register(this.instantiationService.createInstance(ExcludePatternInputWidget, excludesList, this.contextViewService, {
ariaLabel: localize('label.excludes', 'Search Exclude Patterns'),
}));
this.inputPatternExcludes.onSubmit(_triggeredOnType => this.runSearch());
this.inputPatternExcludes.onChangeIgnoreBox(() => this.runSearch());
// Editor
const searchResultContainer = DOM.append(parent, DOM.$('.search-results'));
const configuration: IEditorOptions = this.configurationService.getValue('editor', { overrideIdentifier: 'search-result' });
const options: ICodeEditorWidgetOptions = {};
this.searchResultEditor = this._register(this.instantiationService.createInstance(CodeEditorWidget, searchResultContainer, configuration, options));
this.searchResultEditor.onMouseUp(e => {
if (e.event.detail === 2) {
const behaviour = this.configurationService.getValue<ISearchConfigurationProperties>('search').searchEditorPreview.doubleClickBehaviour;
const position = e.target.position;
if (position && behaviour !== 'selectWord') {
const line = this.searchResultEditor.getModel()?.getLineContent(position.lineNumber) ?? '';
if (line.match(RESULT_LINE_REGEX)) {
this.searchResultEditor.setSelection(Range.fromPositions(position));
this.commandService.executeCommand(behaviour === 'goToLocation' ? 'editor.action.goToDeclaration' : 'editor.action.openDeclarationToTheSide');
}
}
}
});
}
private async runSearch() {
if (!this.pauseSearching) {
this.runSearchDelayer.trigger(() => this.doRunSearch());
}
}
private async doRunSearch() {
const startInput = this.input;
const config: SearchConfiguration = {
caseSensitive: this.queryEditorWidget.searchInput.getCaseSensitive(),
contextLines: this.queryEditorWidget.contextLines(),
excludes: this.inputPatternExcludes.getValue(),
includes: this.inputPatternIncludes.getValue(),
query: this.queryEditorWidget.searchInput.getValue(),
regexp: this.queryEditorWidget.searchInput.getRegex(),
wholeWord: this.queryEditorWidget.searchInput.getWholeWords(),
useIgnores: this.inputPatternExcludes.useExcludesAndIgnoreFiles()
};
const content: IPatternInfo = {
pattern: config.query,
isRegExp: config.regexp,
isCaseSensitive: config.caseSensitive,
isWordMatch: config.wholeWord,
};
const options: ITextQueryBuilderOptions = {
_reason: 'searchEditor',
extraFileResources: this.instantiationService.invokeFunction(getOutOfWorkspaceEditorResources),
maxResults: 10000,
disregardIgnoreFiles: !config.useIgnores,
disregardExcludeSettings: !config.useIgnores,
excludePattern: config.excludes,
includePattern: config.includes,
previewOptions: {
matchLines: 1,
charsPerLine: 1000
},
afterContext: config.contextLines,
beforeContext: config.contextLines,
isSmartCase: this.configurationService.getValue<ISearchConfigurationProperties>('search').smartCase,
expandPatterns: true
};
const folderResources = this.contextService.getWorkspace().folders;
let query: ITextQuery;
try {
const queryBuilder = this.instantiationService.createInstance(QueryBuilder);
query = queryBuilder.text(content, folderResources.map(folder => folder.uri), options);
}
catch (err) {
return;
}
const searchModel = this.instantiationService.createInstance(SearchModel);
await searchModel.search(query);
if (this.input !== startInput) {
searchModel.dispose();
return;
}
(assertIsDefined(this._input) as SearchEditorInput).setConfig(config);
const labelFormatter = (uri: URI): string => this.labelService.getUriLabel(uri, { relative: true });
const results = serializeSearchResultForEditor(searchModel.searchResult, config.includes, config.excludes, config.contextLines, labelFormatter, false);
const textModel = assertIsDefined(this.searchResultEditor.getModel());
textModel.setValue(results.text.join(lineDelimiter));
textModel.deltaDecorations([], results.matchRanges.map(range => ({ range, options: { className: 'searchEditorFindMatch', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } })));
searchModel.dispose();
}
layout(dimension: DOM.Dimension) {
this.dimension = dimension;
this.reLayout();
}
focusInput() {
this.queryEditorWidget.focus();
}
private reLayout() {
if (this.dimension) {
this.queryEditorWidget.setWidth(this.dimension.width - 28 /* container margin */);
this.searchResultEditor.layout({ height: this.dimension.height - DOM.getTotalHeight(this.queryEditorContainer), width: this.dimension.width });
this.inputPatternExcludes.setWidth(this.dimension.width - 28 /* container margin */);
this.inputPatternIncludes.setWidth(this.dimension.width - 28 /* container margin */);
}
}
async setInput(newInput: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise<void> {
if (!(newInput instanceof SearchEditorInput)) { return; }
this.pauseSearching = true;
// TODO: Manage model lifecycle in SearchEditorInput
const model = this.modelService.getModel(newInput.getResource());
this.searchResultEditor.setModel(model);
this.queryEditorWidget.setValue(newInput.config.query, true);
this.queryEditorWidget.searchInput.setCaseSensitive(newInput.config.caseSensitive);
this.queryEditorWidget.searchInput.setRegex(newInput.config.regexp);
this.queryEditorWidget.searchInput.setWholeWords(newInput.config.wholeWord);
this.queryEditorWidget.setContextLines(newInput.config.contextLines);
this.inputPatternExcludes.setValue(newInput.config.excludes);
this.inputPatternIncludes.setValue(newInput.config.includes);
this.inputPatternExcludes.setUseExcludesAndIgnoreFiles(newInput.config.useIgnores);
this.focusInput();
await super.setInput(newInput, options, token);
this.pauseSearching = false;
}
toggleQueryDetails(): void {
const cls = 'expanded';
const shouldShow = !DOM.hasClass(this.includesExcludesContainer, cls);
if (shouldShow) {
this.toggleQueryDetailsButton.setAttribute('aria-expanded', 'true');
DOM.addClass(this.includesExcludesContainer, cls);
} else {
this.toggleQueryDetailsButton.setAttribute('aria-expanded', 'false');
DOM.removeClass(this.includesExcludesContainer, cls);
}
this.reLayout();
}
getModel() {
return this.searchResultEditor.getModel();
}
}
// Using \r\n on Windows inserts an extra newline between results.
const lineDelimiter = '\n';
@ -221,8 +551,8 @@ const searchHeaderToContentPattern = (header: string[]): SearchHeader => {
return query;
};
const serializeSearchResultForEditor = (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, contextLines: number, labelFormatter: (x: URI) => string): SearchResultSerialization => {
const header = contentPatternToSearchResultHeader(searchResult.query, rawIncludePattern, rawExcludePattern, contextLines);
const serializeSearchResultForEditor = (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, contextLines: number, labelFormatter: (x: URI) => string, includeHeader: boolean): SearchResultSerialization => {
const header = includeHeader ? contentPatternToSearchResultHeader(searchResult.query, rawIncludePattern, rawExcludePattern, contextLines) : [];
const allResults =
flattenSearchResultSerializations(
flatten(
@ -290,20 +620,29 @@ export const refreshActiveEditorSearch =
await searchModel.search(query);
const labelFormatter = (uri: URI): string => labelService.getUriLabel(uri, { relative: true });
const results = serializeSearchResultForEditor(searchModel.searchResult, contentPattern.includes, contentPattern.excludes, contextLines, labelFormatter);
const results = serializeSearchResultForEditor(searchModel.searchResult, contentPattern.includes, contentPattern.excludes, contextLines, labelFormatter, false);
textModel.setValue(results.text.join(lineDelimiter));
textModel.deltaDecorations([], results.matchRanges.map(range => ({ range, options: { className: 'searchEditorFindMatch', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } })));
};
export const openNewSearchEditor =
async (editorService: IEditorService, instantiationService: IInstantiationService) => {
await editorService.openEditor(instantiationService.createInstance(SearchEditorInput, undefined), { pinned: true });
};
export const createEditorFromSearchResult =
async (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, labelService: ILabelService, editorService: IEditorService) => {
const searchTerm = searchResult.query?.contentPattern.pattern.replace(/[^\w-_. ]/g, '') || 'Search';
async (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, labelService: ILabelService, editorService: IEditorService, instantiationService: IInstantiationService) => {
if (!searchResult.query) {
console.error('Expected searchResult.query to be defined. Got', searchResult);
return;
}
const searchTerm = searchResult.query.contentPattern.pattern.replace(/[^\w-_. ]/g, '') || 'Search';
const labelFormatter = (uri: URI): string => labelService.getUriLabel(uri, { relative: true });
const results = serializeSearchResultForEditor(searchResult, rawIncludePattern, rawExcludePattern, 0, labelFormatter);
const results = serializeSearchResultForEditor(searchResult, rawIncludePattern, rawExcludePattern, 0, labelFormatter, false);
const contents = results.text.join(lineDelimiter);
let possible = {
contents,
@ -312,6 +651,7 @@ export const createEditorFromSearchResult =
};
let id = 0;
let existing = editorService.getOpened(possible);
while (existing) {
if (existing instanceof UntitledTextEditorInput) {
@ -325,10 +665,23 @@ export const createEditorFromSearchResult =
existing = editorService.getOpened(possible);
}
const editor = await editorService.openEditor(possible);
const control = editor?.getControl()!;
const model = control.getModel() as ITextModel;
const editor = await editorService.openEditor(
instantiationService.createInstance(
SearchEditorInput,
{
query: searchResult.query.contentPattern.pattern,
regexp: !!searchResult.query.contentPattern.isRegExp,
caseSensitive: !!searchResult.query.contentPattern.isCaseSensitive,
wholeWord: !!searchResult.query.contentPattern.isWordMatch,
includes: rawIncludePattern,
excludes: rawExcludePattern,
contextLines: 0,
useIgnores: !searchResult.query.userDisabledExcludesAndIgnoreFiles,
}),
{ pinned: true }) as SearchEditor;
const model = assertIsDefined(editor.getModel());
model.setValue(contents);
model.deltaDecorations([], results.matchRanges.map(range => ({ range, options: { className: 'searchEditorFindMatch', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } })));
};

View file

@ -1559,7 +1559,7 @@ export class SearchView extends ViewPane {
this.messageDisposables.push(dom.addDisposableListener(openInEditorLink, dom.EventType.CLICK, (e: MouseEvent) => {
dom.EventHelper.stop(e, false);
createEditorFromSearchResult(this.searchResult, this.searchIncludePattern.getValue(), this.searchExcludePattern.getValue(), this.labelService, this.editorService);
createEditorFromSearchResult(this.searchResult, this.searchIncludePattern.getValue(), this.searchExcludePattern.getValue(), this.labelService, this.editorService, this.instantiationService);
}));
} else {

View file

@ -9,7 +9,7 @@ import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { Button, IButtonOptions } from 'vs/base/browser/ui/button/button';
import { FindInput, IFindInputOptions } from 'vs/base/browser/ui/findinput/findInput';
import { ReplaceInput } from 'vs/base/browser/ui/findinput/replaceInput';
import { IMessage } from 'vs/base/browser/ui/inputbox/inputBox';
import { IMessage, InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import { Widget } from 'vs/base/browser/ui/widget';
import { Action } from 'vs/base/common/actions';
import { Delayer } from 'vs/base/common/async';
@ -34,6 +34,7 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility';
import { isMacintosh } from 'vs/base/common/platform';
import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox';
/** Specified in searchview.css */
export const SingleLineInputHeight = 24;
@ -47,6 +48,8 @@ export interface ISearchWidgetOptions {
searchHistory?: string[];
replaceHistory?: string[];
preserveCase?: boolean;
_hideReplaceToggle?: boolean; // TODO: Search Editor's replace experience
showContextToggle?: boolean;
}
class ReplaceAllAction extends Action {
@ -151,7 +154,13 @@ export class SearchWidget extends Widget {
private _onDidHeightChange = this._register(new Emitter<void>());
readonly onDidHeightChange: Event<void> = this._onDidHeightChange.event;
private readonly _onDidToggleContext = new Emitter<void>();
readonly onDidToggleContext: Event<void> = this._onDidToggleContext.event;
private temporarilySkipSearchOnChange = false;
private showContextCheckbox!: Checkbox;
private contextLinesInput!: InputBox;
private _contextLineInputDelayer: Delayer<void>;
constructor(
container: HTMLElement,
@ -168,7 +177,10 @@ export class SearchWidget extends Widget {
this.replaceActive = Constants.ReplaceActiveKey.bindTo(this.contextKeyService);
this.searchInputBoxFocused = Constants.SearchInputBoxFocusedKey.bindTo(this.contextKeyService);
this.replaceInputBoxFocused = Constants.ReplaceInputBoxFocusedKey.bindTo(this.contextKeyService);
this._replaceHistoryDelayer = new Delayer<void>(500);
this._contextLineInputDelayer = new Delayer<void>(300);
this._searchDelayer = this._register(new Delayer<void>(this.searchConfiguration.searchOnTypeDebouncePeriod));
this.render(container, options);
@ -275,7 +287,9 @@ export class SearchWidget extends Widget {
this.domNode = dom.append(container, dom.$('.search-widget'));
this.domNode.style.position = 'relative';
this.renderToggleReplaceButton(this.domNode);
if (!options._hideReplaceToggle) {
this.renderToggleReplaceButton(this.domNode);
}
this.renderSearchInput(this.domNode, options);
this.renderReplaceInput(this.domNode, options);
@ -356,6 +370,36 @@ export class SearchWidget extends Widget {
this.ignoreGlobalFindBufferOnNextFocus = false;
}));
this._register(this.searchInputFocusTracker.onDidBlur(() => this.searchInputBoxFocused.set(false)));
this.showContextCheckbox = new Checkbox({ isChecked: false, title: nls.localize('showContext', "Show Context"), actionClassName: 'codicon-list-selection' });
this._register(this.showContextCheckbox.onChange(() => {
dom.toggleClass(parent, 'show-context', this.showContextCheckbox.checked);
this._onDidToggleContext.fire();
}));
if (options.showContextToggle) {
this.contextLinesInput = new InputBox(searchInputContainer, this.contextViewService, { type: 'number' });
dom.addClass(this.contextLinesInput.element, 'context-lines-input');
this.contextLinesInput.value = '2';
this._register(this.contextLinesInput.onDidChange(() => {
if (this.contextLinesInput.value.includes('-')) {
this.contextLinesInput.value = '0';
}
this._contextLineInputDelayer.trigger(() => this._onDidToggleContext.fire());
}));
dom.append(searchInputContainer, this.showContextCheckbox.domNode);
}
}
public setContextLines(lines: number) {
if (!this.contextLinesInput) { return; }
if (lines === 0) {
this.showContextCheckbox.checked = false;
} else {
this.showContextCheckbox.checked = true;
this.contextLinesInput.value = '' + lines;
}
}
private renderReplaceInput(parent: HTMLElement, options: ISearchWidgetOptions): void {
@ -580,6 +624,10 @@ export class SearchWidget extends Widget {
this._onSearchSubmit.fire(triggeredOnType);
}
contextLines() {
return this.showContextCheckbox.checked ? +this.contextLinesInput.value : 0;
}
dispose(): void {
this.setReplaceAllActionState(false);
super.dispose();

View file

@ -16,6 +16,7 @@ export const CopyPathCommandId = 'search.action.copyPath';
export const CopyMatchCommandId = 'search.action.copyMatch';
export const CopyAllCommandId = 'search.action.copyAll';
export const OpenInEditorCommandId = 'search.action.openInEditor';
export const OpenNewEditorCommandId = 'search.action.openNewEditor';
export const RerunEditorSearchCommandId = 'search.action.rerunEditorSearch';
export const RerunEditorSearchWithContextCommandId = 'search.action.rerunEditorSearchWithContext';
export const ClearSearchHistoryCommandId = 'search.action.clearHistory';

View file

@ -543,15 +543,13 @@ export class FolderMatch extends Disposable {
replace(match: FileMatch): Promise<any> {
return this.replaceService.replace([match]).then(() => {
this.doRemove(match, false, true);
this.doRemove(match);
});
}
replaceAll(): Promise<any> {
const matches = this.matches();
return this.replaceService.replace(matches).then(() => {
matches.forEach(match => this.doRemove(match, false, true));
});
return this.replaceService.replace(matches).then(() => this.doRemove(matches));
}
matches(): FileMatch[] {

View file

@ -146,7 +146,7 @@ export class SnippetCompletionProvider implements CompletionItemProvider {
i = to;
}
}
return { suggestions };
return { suggestions, isDetailsResolved: true };
});
}

View file

@ -11,6 +11,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService';
import * as marked from 'vs/base/common/marked/marked';
import { Schemas } from 'vs/base/common/network';
import { isEqual } from 'vs/base/common/resources';
import { EndOfLinePreference } from 'vs/editor/common/model';
export class WalkThroughModel extends EditorModel {
@ -111,7 +112,7 @@ export class WalkThroughInput extends EditorInput {
return '';
};
const markdown = ref.object.textEditorModel.getLinesContent().join('\n');
const markdown = ref.object.textEditorModel.getValue(EndOfLinePreference.LF);
marked(markdown, { renderer });
return Promise.all(snippets)

View file

@ -39,6 +39,7 @@ import { Dimension, size } from 'vs/base/browser/dom';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { CancellationToken } from 'vs/base/common/cancellation';
import { domEvent } from 'vs/base/browser/event';
import { EndOfLinePreference } from 'vs/editor/common/model';
export const WALK_THROUGH_FOCUS = new RawContextKey<boolean>('interactivePlaygroundFocus', false);
@ -278,7 +279,7 @@ export class WalkThroughPart extends BaseEditor {
return;
}
const content = model.main.textEditorModel.getLinesContent().join('\n');
const content = model.main.textEditorModel.getValue(EndOfLinePreference.LF);
if (!strings.endsWith(input.getResource().path, '.md')) {
this.content.innerHTML = content;
this.updateSizeClasses();
@ -421,7 +422,8 @@ export class WalkThroughPart extends BaseEditor {
horizontal: 'auto',
useShadows: true,
verticalHasArrows: false,
horizontalHasArrows: false
horizontalHasArrows: false,
alwaysConsumeMouseWheel: false
},
overviewRulerLanes: 3,
fixedOverflowWidgets: true,

View file

@ -13,6 +13,8 @@ import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorIn
import { KeybindingsEditorModel } from 'vs/workbench/services/preferences/common/keybindingsEditorModel';
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
import { Settings2EditorModel } from 'vs/workbench/services/preferences/common/preferencesModels';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
export class PreferencesEditorInput extends SideBySideEditorInput {
static readonly ID: string = 'workbench.editorinputs.preferencesEditorInput';
@ -28,10 +30,13 @@ export class PreferencesEditorInput extends SideBySideEditorInput {
export class DefaultPreferencesEditorInput extends ResourceEditorInput {
static readonly ID = 'workbench.editorinputs.defaultpreferences';
constructor(defaultSettingsResource: URI,
@ITextModelService textModelResolverService: ITextModelService
constructor(
defaultSettingsResource: URI,
@ITextModelService textModelResolverService: ITextModelService,
@ITextFileService textFileService: ITextFileService,
@IEditorService editorService: IEditorService
) {
super(nls.localize('settingsEditorName', "Default Settings"), '', defaultSettingsResource, undefined, textModelResolverService);
super(nls.localize('settingsEditorName', "Default Settings"), '', defaultSettingsResource, undefined, textModelResolverService, textFileService, editorService);
}
getTypeId(): string {

View file

@ -45,11 +45,15 @@ export interface Tunnel {
closeable?: boolean;
}
export function MakeAddress(host: string, port: number): string {
function ToLocalHost(host: string): string {
if (host === '127.0.0.1') {
host = 'localhost';
}
return host + ':' + port;
return host;
}
export function MakeAddress(host: string, port: number): string {
return ToLocalHost(host) + ':' + port;
}
export class TunnelModel extends Disposable {
@ -201,7 +205,13 @@ export class TunnelModel extends Disposable {
return;
}
if (this._candidateFinder) {
this._candidates = await this._candidateFinder();
this._candidates = (await this._candidateFinder()).map(value => {
return {
host: ToLocalHost(value.host),
port: value.port,
detail: value.detail
};
});
}
}
@ -286,7 +296,9 @@ class RemoteExplorerService implements IRemoteExplorerService {
}
getEditableData(tunnelItem: ITunnelItem | undefined): IEditableData | undefined {
return (this._editable && (!tunnelItem || (this._editable.tunnelItem?.remotePort === tunnelItem.remotePort) && (this._editable.tunnelItem.remoteHost === tunnelItem.remoteHost))) ?
return (this._editable &&
((!tunnelItem && (tunnelItem === this._editable.tunnelItem)) ||
(tunnelItem && (this._editable.tunnelItem?.remotePort === tunnelItem.remotePort) && (this._editable.tunnelItem.remoteHost === tunnelItem.remoteHost)))) ?
this._editable.data : undefined;
}

View file

@ -335,6 +335,7 @@ export interface ISearchConfigurationProperties {
searchOnType: boolean;
searchOnTypeDebouncePeriod: number;
enableSearchEditorPreview: boolean;
searchEditorPreview: { doubleClickBehaviour: 'selectWord' | 'goToLocation' | 'openLocationToSide' };
searchEditorPreviewForceAbsolutePaths: boolean;
sortOrder: SearchSortOrder;
}

View file

@ -26,7 +26,7 @@ export class BrowserTextFileService extends AbstractTextFileService {
}
private doBeforeShutdownSync(): boolean {
if (this.models.getAll().some(model => model.hasState(ModelState.PENDING_SAVE) || model.hasState(ModelState.PENDING_AUTO_SAVE))) {
if (this.models.getAll().some(model => model.hasState(ModelState.PENDING_SAVE))) {
return true; // files are pending to be saved: veto
}

View file

@ -9,7 +9,7 @@ import { Emitter, AsyncEmitter } from 'vs/base/common/event';
import * as platform from 'vs/base/common/platform';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { IResult, ITextFileOperationResult, ITextFileService, ITextFileStreamContent, ITextFileEditorModelManager, ITextFileEditorModel, ModelState, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, FileOperationWillRunEvent, FileOperationDidRunEvent, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles';
import { SaveReason, IRevertOptions } from 'vs/workbench/common/editor';
import { SaveReason, IRevertOptions, IEncodingSupport } from 'vs/workbench/common/editor';
import { ILifecycleService, ShutdownReason, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IFileService, FileOperationError, FileOperationResult, HotExitConfiguration, IFileStatWithMetadata, ICreateFileOptions, FileOperation } from 'vs/platform/files/common/files';
@ -37,6 +37,7 @@ import { ITextResourceConfigurationService } from 'vs/editor/common/services/tex
import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { CancellationToken } from 'vs/base/common/cancellation';
import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService';
/**
* The workbench file service implementation implements the raw file service spec and adds additional methods on top.
@ -76,7 +77,8 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
@IFileDialogService private readonly fileDialogService: IFileDialogService,
@IEditorService private readonly editorService: IEditorService,
@ITextResourceConfigurationService protected readonly textResourceConfigurationService: ITextResourceConfigurationService,
@IFilesConfigurationService protected readonly filesConfigurationService: IFilesConfigurationService
@IFilesConfigurationService protected readonly filesConfigurationService: IFilesConfigurationService,
@ITextModelService private readonly textModelService: ITextModelService
) {
super();
@ -676,68 +678,77 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
return this.getFileModels(resources).filter(model => model.isDirty());
}
async saveAs(resource: URI, targetResource?: URI, options?: ITextFileSaveOptions): Promise<URI | undefined> {
async saveAs(source: URI, target?: URI, options?: ITextFileSaveOptions): Promise<URI | undefined> {
// Get to target resource
if (!targetResource) {
let dialogPath = resource;
if (resource.scheme === Schemas.untitled) {
dialogPath = this.suggestFileName(resource);
if (!target) {
let dialogPath = source;
if (source.scheme === Schemas.untitled) {
dialogPath = this.suggestFileName(source);
}
targetResource = await this.promptForPath(resource, dialogPath, options ? options.availableFileSystems : undefined);
target = await this.promptForPath(source, dialogPath, options ? options.availableFileSystems : undefined);
}
if (!targetResource) {
if (!target) {
return; // user canceled
}
// Just save if target is same as models own resource
if (resource.toString() === targetResource.toString()) {
await this.save(resource, options);
if (source.toString() === target.toString()) {
await this.save(source, options);
return resource;
return source;
}
// Do it
return this.doSaveAs(resource, targetResource, options);
return this.doSaveAs(source, target, options);
}
private async doSaveAs(resource: URI, target: URI, options?: ITextFileSaveOptions): Promise<URI> {
private async doSaveAs(source: URI, target: URI, options?: ITextFileSaveOptions): Promise<URI> {
let success = false;
// Retrieve text model from provided resource if any
let model: ITextFileEditorModel | UntitledTextEditorModel | undefined;
if (this.fileService.canHandleResource(resource)) {
model = this._models.get(resource);
} else if (resource.scheme === Schemas.untitled && this.untitledTextEditorService.exists(resource)) {
model = await this.untitledTextEditorService.createOrGet({ resource }).resolve();
// If the source is an existing text file model, we can directly
// use that model to copy the contents to the target destination
const textFileModel = this._models.get(source);
if (textFileModel && textFileModel.isResolved()) {
success = await this.doSaveAsTextFile(textFileModel, source, target, options);
}
// We have a model: Use it (can be null e.g. if this file is binary and not a text file or was never opened before)
let result: boolean;
if (model) {
result = await this.doSaveTextFileAs(model, resource, target, options);
// Otherwise if the source can be handled by the file service
// we can simply invoke the copy() function to save as
else if (this.fileService.canHandleResource(source)) {
await this.fileService.copy(source, target);
success = true;
}
// Otherwise we can only copy
else {
await this.fileService.copy(resource, target);
// Finally, if the source does not seem to be a file, we have to
// try to resolve a text model from the resource to get at the
// contents and additional meta data (e.g. encoding).
else if (this.textModelService.hasTextModelContentProvider(source.scheme)) {
const modelReference = await this.textModelService.createModelReference(source);
success = await this.doSaveAsTextFile(modelReference.object, source, target, options);
result = true;
modelReference.dispose(); // free up our use of the reference
}
// Return early if the operation was not running
if (!result) {
return target;
// Revert the source if result is success
if (success) {
await this.revert(source);
}
// Revert the source
await this.revert(resource);
return target;
}
private async doSaveTextFileAs(sourceModel: ITextFileEditorModel | UntitledTextEditorModel, resource: URI, target: URI, options?: ITextFileSaveOptions): Promise<boolean> {
private async doSaveAsTextFile(sourceModel: IResolvedTextEditorModel, source: URI, target: URI, options?: ITextFileSaveOptions): Promise<boolean> {
// Find source encoding if any
let sourceModelEncoding: string | undefined = undefined;
const sourceModelWithEncodingSupport = (sourceModel as unknown as IEncodingSupport);
if (typeof sourceModelWithEncodingSupport.getEncoding === 'function') {
sourceModelEncoding = sourceModelWithEncodingSupport.getEncoding();
}
// Prefer an existing model if it is already loaded for the given target resource
let targetExists: boolean = false;
@ -764,7 +775,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
}
}
targetModel = await this.models.loadOrCreate(target, { encoding: sourceModel.getEncoding(), mode });
targetModel = await this.models.loadOrCreate(target, { encoding: sourceModelEncoding, mode });
}
try {
@ -785,7 +796,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
}
// take over model value, encoding and mode (only if more specific) from source model
targetModel.updatePreferredEncoding(sourceModel.getEncoding());
targetModel.updatePreferredEncoding(sourceModelEncoding);
if (sourceModel.isResolved() && targetModel.isResolved()) {
this.modelService.updateModel(targetModel.textEditorModel, createTextBufferFactoryFromSnapshot(sourceModel.createSnapshot()));
@ -809,7 +820,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
) {
await this.fileService.del(target);
return this.doSaveTextFileAs(sourceModel, resource, target, options);
return this.doSaveAsTextFile(sourceModel, source, target, options);
}
throw error;

View file

@ -24,13 +24,12 @@ import { RunOnceScheduler, timeout } from 'vs/base/common/async';
import { ITextBufferFactory } from 'vs/editor/common/model';
import { hash } from 'vs/base/common/hash';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { toDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { isEqual, isEqualOrParent, extname, basename, joinPath } from 'vs/base/common/resources';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Schemas } from 'vs/base/common/network';
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { IFilesConfigurationService, IAutoSaveConfiguration } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
export interface IBackupMetaData {
mtime: number;
@ -92,10 +91,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
private lastResolvedFileStat: IFileStatWithMetadata | undefined;
private autoSaveAfterMillies: number | undefined;
private autoSaveAfterMilliesEnabled: boolean | undefined;
private readonly autoSaveDisposable = this._register(new MutableDisposable());
private readonly saveSequentializer = new SaveSequentializer();
private lastSaveAttemptTime = 0;
@ -128,8 +123,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
) {
super(modelService, modeService);
this.updateAutoSaveConfiguration(filesConfigurationService.getAutoSaveConfiguration());
// Make known to working copy service
this._register(this.workingCopyService.registerWorkingCopy(this));
@ -138,7 +131,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
private registerListeners(): void {
this._register(this.fileService.onFileChanges(e => this.onFileChanges(e)));
this._register(this.filesConfigurationService.onAutoSaveConfigurationChange(config => this.updateAutoSaveConfiguration(config)));
this._register(this.filesConfigurationService.onFilesAssociationChange(e => this.onFilesAssociationChange()));
this._register(this.onDidStateChange(e => this.onStateChange(e)));
}
@ -206,13 +198,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
}
private updateAutoSaveConfiguration(config: IAutoSaveConfiguration): void {
const autoSaveAfterMilliesEnabled = (typeof config.autoSaveDelay === 'number') && config.autoSaveDelay > 0;
this.autoSaveAfterMilliesEnabled = autoSaveAfterMilliesEnabled;
this.autoSaveAfterMillies = autoSaveAfterMilliesEnabled ? config.autoSaveDelay : undefined;
}
private onFilesAssociationChange(): void {
if (!this.isResolved()) {
return;
@ -258,9 +243,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
return false;
}
// Cancel any running auto-save
this.autoSaveDisposable.clear();
// Unset flags
const wasDirty = this.dirty;
const undo = this.setDirty(false);
@ -474,13 +456,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
this.createTextEditorModel(value, resource, this.preferredMode);
// We restored a backup so we have to set the model as being dirty
// We also want to trigger auto save if it is enabled to simulate the exact same behaviour
// you would get if manually making the model dirty (fixes https://github.com/Microsoft/vscode/issues/16977)
if (fromBackup) {
this.doMakeDirty();
if (this.autoSaveAfterMilliesEnabled) {
this.doAutoSave(this.versionId);
}
}
// Ensure we are not tracking a stale state
@ -536,9 +513,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// The contents changed as a matter of Undo and the version reached matches the saved one
// In this case we clear the dirty flag and emit a SAVED event to indicate this state.
// Note: we currently only do this check when auto-save is turned off because there you see
// a dirty indicator that you want to get rid of when undoing to the saved version.
if (!this.autoSaveAfterMilliesEnabled && this.isResolved() && this.textEditorModel.getAlternativeVersionId() === this.bufferSavedVersionId) {
if (this.isResolved() && this.textEditorModel.getAlternativeVersionId() === this.bufferSavedVersionId) {
this.logService.trace('onModelContentChanged() - model content changed back to last saved version', this.resource);
// Clear flags
@ -559,15 +534,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Mark as dirty
this.doMakeDirty();
// Start auto save process unless we are in conflict resolution mode and unless it is disabled
if (this.autoSaveAfterMilliesEnabled) {
if (!this.inConflictMode) {
this.doAutoSave(this.versionId);
} else {
this.logService.trace('makeDirty() - prevented save because we are in conflict resolution mode', this.resource);
}
}
// Handle content change events
this.contentChangeEventScheduler.schedule();
}
@ -593,27 +559,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
}
private doAutoSave(versionId: number): void {
this.logService.trace(`doAutoSave() - enter for versionId ${versionId}`, this.resource);
// Cancel any currently running auto saves to make this the one that succeeds
this.autoSaveDisposable.clear();
// Create new save timer and store it for disposal as needed
const handle = setTimeout(() => {
// Clear the timeout now that we are running
this.autoSaveDisposable.clear();
// Only trigger save if the version id has not changed meanwhile
if (versionId === this.versionId) {
this.doSave(versionId, { reason: SaveReason.AUTO }); // Very important here to not return the promise because if the timeout promise is canceled it will bubble up the error otherwise - do not change
}
}, this.autoSaveAfterMillies);
this.autoSaveDisposable.value = toDisposable(() => clearTimeout(handle));
}
async save(options: ITextFileSaveOptions = Object.create(null)): Promise<boolean> {
if (!this.isResolved()) {
return false;
@ -621,9 +566,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
this.logService.trace('save() - enter', this.resource);
// Cancel any currently running auto saves to make this the one that succeeds
this.autoSaveDisposable.clear();
await this.doSave(this.versionId, options);
return true;
@ -676,8 +618,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
// Push all edit operations to the undo stack so that the user has a chance to
// Ctrl+Z back to the saved version. We only do this when auto-save is turned off
if (!this.autoSaveAfterMilliesEnabled && this.isResolved()) {
// Ctrl+Z back to the saved version.
if (this.isResolved()) {
this.textEditorModel.pushStackElement();
}
@ -953,8 +895,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
return this.inOrphanMode;
case ModelState.PENDING_SAVE:
return this.saveSequentializer.hasPendingSave();
case ModelState.PENDING_AUTO_SAVE:
return !!this.autoSaveDisposable.value;
case ModelState.SAVED:
return !this.dirty;
}

View file

@ -257,11 +257,6 @@ export const enum ModelState {
*/
PENDING_SAVE,
/**
* A model is marked for being saved after a specific timeout.
*/
PENDING_AUTO_SAVE,
/**
* A model is in conflict mode when changes cannot be saved because the
* underlying file has changed. Models in conflict mode are always dirty.

View file

@ -41,6 +41,7 @@ import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/d
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { assign } from 'vs/base/common/objects';
import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
export class NativeTextFileService extends AbstractTextFileService {
@ -62,9 +63,10 @@ export class NativeTextFileService extends AbstractTextFileService {
@ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService,
@IElectronService private readonly electronService: IElectronService,
@IProductService private readonly productService: IProductService,
@IFilesConfigurationService filesConfigurationService: IFilesConfigurationService
@IFilesConfigurationService filesConfigurationService: IFilesConfigurationService,
@ITextModelService textModelService: ITextModelService
) {
super(contextService, fileService, untitledTextEditorService, lifecycleService, instantiationService, modeService, modelService, environmentService, notificationService, backupFileService, historyService, dialogService, fileDialogService, editorService, textResourceConfigurationService, filesConfigurationService);
super(contextService, fileService, untitledTextEditorService, lifecycleService, instantiationService, modeService, modelService, environmentService, notificationService, backupFileService, historyService, dialogService, fileDialogService, editorService, textResourceConfigurationService, filesConfigurationService, textModelService);
}
private _encoding: EncodingOracle | undefined;

View file

@ -175,6 +175,10 @@ export class TextModelResolverService implements ITextModelService {
}
hasTextModelContentProvider(scheme: string): boolean {
if (scheme === network.Schemas.untitled || scheme === network.Schemas.inMemory) {
return true; // we handle untitled:// and inMemory:// within
}
return this.resourceModelCollection.hasTextModelContentProvider(scheme);
}
}

View file

@ -192,6 +192,15 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
configurationRegistry.notifyConfigurationSchemaUpdated(themeSettingsConfiguration, tokenColorCustomizationConfiguration);
let colorThemeSetting = this.configurationService.getValue<string>(COLOR_THEME_SETTING);
if (colorThemeSetting !== this.currentColorTheme.settingsId) {
const theme = await this.colorThemeStore.findThemeDataBySettingsId(colorThemeSetting, undefined);
if (theme) {
this.setColorTheme(theme.id, undefined);
return;
}
}
if (this.currentColorTheme.isLoaded) {
const themeData = await this.colorThemeStore.findThemeData(this.currentColorTheme.id);
if (!themeData) {
@ -216,6 +225,15 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
iconThemeSettingSchema.enumDescriptions = [iconThemeSettingSchema.enumDescriptions![0], ...event.themes.map(t => t.description || '')];
configurationRegistry.notifyConfigurationSchemaUpdated(themeSettingsConfiguration);
let iconThemeSetting = this.configurationService.getValue<string | null>(ICON_THEME_SETTING);
if (iconThemeSetting !== this.currentIconTheme.settingsId) {
const theme = await this.iconThemeStore.findThemeBySettingsId(iconThemeSetting);
if (theme) {
this.setFileIconTheme(theme.id, undefined);
return;
}
}
if (this.currentIconTheme.isLoaded) {
const theme = await this.iconThemeStore.findThemeData(this.currentIconTheme.id);
if (!theme) {

View file

@ -9,19 +9,25 @@ import { Event, Emitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { Disposable, IDisposable, toDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle';
import { TernarySearchTree } from 'vs/base/common/map';
import { ISaveOptions } from 'vs/workbench/common/editor';
export const enum WorkingCopyCapabilities {
/**
* Signals that the working copy requires
* additional input when saving, e.g. an
* associated path to save to.
* associated path to save to.
*/
Untitled = 1 << 1
}
export interface IWorkingCopy {
readonly resource: URI;
readonly capabilities: WorkingCopyCapabilities;
//#region Dirty Tracking
readonly onDidChangeDirty: Event<void>;
@ -31,9 +37,11 @@ export interface IWorkingCopy {
//#endregion
readonly resource: URI;
//#region Save
readonly capabilities: WorkingCopyCapabilities;
save(options?: ISaveOptions): Promise<boolean>;
//#endregion
}
export const IWorkingCopyService = createDecorator<IWorkingCopyService>('workingCopyService');

View file

@ -9,6 +9,7 @@ import { URI } from 'vs/base/common/uri';
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { TestWorkingCopyService } from 'vs/workbench/test/workbenchTestServices';
import type { ISaveOptions } from 'vs/workbench/common/editor';
suite('WorkingCopyService', () => {
@ -41,6 +42,10 @@ suite('WorkingCopyService', () => {
return this.dirty;
}
async save(options?: ISaveOptions): Promise<boolean> {
return true;
}
dispose(): void {
this._onDispose.fire();

View file

@ -213,7 +213,8 @@ export class TestTextFileService extends NativeTextFileService {
@ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService,
@IElectronService electronService: IElectronService,
@IProductService productService: IProductService,
@IFilesConfigurationService filesConfigurationService: IFilesConfigurationService
@IFilesConfigurationService filesConfigurationService: IFilesConfigurationService,
@ITextModelService textModelService: ITextModelService
) {
super(
contextService,
@ -233,7 +234,8 @@ export class TestTextFileService extends NativeTextFileService {
textResourceConfigurationService,
electronService,
productService,
filesConfigurationService
filesConfigurationService,
textModelService
);
}