提交 bbe82060 编写于 作者: J Johannes Rieken

debt - remove unused snippet code

上级 9532abde
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as strings from 'vs/base/common/strings';
import { Range } from 'vs/editor/common/core/range';
import { Marker, Variable, Placeholder, Text, SnippetParser, walk } from 'vs/editor/contrib/snippet/common/snippetParser';
export interface IIndentationNormalizer {
normalizeIndentation(str: string): string;
}
export interface IPlaceHolder {
id: string;
value: string;
occurences: Range[];
}
export interface ICodeSnippet {
lines: string[];
placeHolders: IPlaceHolder[];
finishPlaceHolderIndex: number;
}
export interface ISnippetVariableResolver {
resolve(name: string): string;
}
export class CodeSnippet implements ICodeSnippet {
static fromTextmate(template: string, variableResolver?: ISnippetVariableResolver): CodeSnippet {
const marker = new SnippetParser(true, false).parse(template);
const snippet = new CodeSnippet();
_resolveSnippetVariables(marker, variableResolver);
_fillCodeSnippetFromMarker(snippet, marker);
return snippet;
}
static fromInternal(template: string): CodeSnippet {
const marker = new SnippetParser(false, true).parse(template);
const snippet = new CodeSnippet();
_fillCodeSnippetFromMarker(snippet, marker);
return snippet;
}
static none(template: string): CodeSnippet {
const snippet = new CodeSnippet();
snippet.lines = template.split(/\r\n|\n|\r/);
return snippet;
}
static fixEmmetFinalTabstop(template: string): string {
let matchFinalStops = template.match(/\$\{0\}|\$0/g);
if (!matchFinalStops || matchFinalStops.length === 1) {
return template;
}
// string to string conversion that tries to fix the
// snippet in-place
let marker = new SnippetParser(true, false).parse(template);
let maxIndex = -Number.MIN_VALUE;
// find highest placeholder index
walk(marker, candidate => {
if (candidate instanceof Placeholder) {
let index = Number(candidate.index);
if (index > maxIndex) {
maxIndex = index;
}
}
return true;
});
// rewrite final tabstops
walk(marker, candidate => {
if (candidate instanceof Placeholder) {
if (candidate.isFinalTabstop) {
candidate.index = String(++maxIndex);
}
}
return true;
});
// write back as string
function toSnippetString(marker: Marker): string {
if (marker instanceof Text) {
return marker.string;
} else if (marker instanceof Placeholder) {
if (marker.defaultValue.length > 0) {
return `\${${marker.index}:${marker.defaultValue.map(toSnippetString).join('')}}`;
} else {
return `\$${marker.index}`;
}
} else if (marker instanceof Variable) {
if (marker.defaultValue.length > 0) {
return `\${${marker.name}:${marker.defaultValue.map(toSnippetString).join('')}}`;
} else {
return `\$${marker.name}`;
}
} else {
throw new Error('unexpected marker: ' + marker);
}
}
return marker.map(toSnippetString).join('');
}
static fromEmmet(template: string): CodeSnippet {
CodeSnippet.fixEmmetFinalTabstop(template);
let matchFinalStops = template.match(/\$\{0\}|\$0/g);
if (!matchFinalStops || matchFinalStops.length === 1) {
return CodeSnippet.fromTextmate(template);
}
// Emmet sometimes returns snippets with multiple ${0}
// In such cases, replace ${0} with incremental tab stops
const snippetMarkers: Marker[] = new SnippetParser(true, false).parse(template) || [];
let getMaxTabStop = (markers: Marker[]): number => {
let currentMaxTabStop = -1;
markers.forEach(marker => {
if (marker instanceof Placeholder && /^\d+$/.test(marker['index'])) {
let currentTabStop = Number(marker['index']);
let nestedMaxTabStop = getMaxTabStop(marker['defaultValue'] || []);
currentMaxTabStop = Math.max(currentMaxTabStop, currentTabStop, nestedMaxTabStop);
}
});
return currentMaxTabStop;
};
let maxTabStop = getMaxTabStop(snippetMarkers);
let setNextTabStop = (markers: Marker[]) => {
markers.forEach(marker => {
if (marker instanceof Placeholder) {
if (marker['index'] === '0') {
marker['index'] = ++maxTabStop + '';
}
setNextTabStop(marker['defaultValue'] || []);
}
});
};
setNextTabStop(snippetMarkers);
const snippet = new CodeSnippet();
_fillCodeSnippetFromMarker(snippet, snippetMarkers);
return snippet;
}
public lines: string[] = [];
public placeHolders: IPlaceHolder[] = [];
public finishPlaceHolderIndex: number = -1;
get isInsertOnly(): boolean {
return this.placeHolders.length === 0;
}
get isSingleTabstopOnly(): boolean {
if (this.placeHolders.length !== 1) {
return false;
}
const [placeHolder] = this.placeHolders;
if (placeHolder.value !== '' || placeHolder.occurences.length !== 1) {
return false;
}
const [placeHolderRange] = placeHolder.occurences;
if (!Range.isEmpty(placeHolderRange)) {
return false;
}
return true;
}
private extractLineIndentation(str: string, maxColumn: number = Number.MAX_VALUE): string {
var fullIndentation = strings.getLeadingWhitespace(str);
if (fullIndentation.length > maxColumn - 1) {
return fullIndentation.substring(0, maxColumn - 1);
}
return fullIndentation;
}
public bind(referenceLine: string, deltaLine: number, firstLineDeltaColumn: number, config: IIndentationNormalizer): ICodeSnippet {
const resultLines: string[] = [];
const resultPlaceHolders: IPlaceHolder[] = [];
const referenceIndentation = this.extractLineIndentation(referenceLine, firstLineDeltaColumn + 1);
// Compute resultLines & keep deltaColumns as a reference for adjusting placeholders
const deltaColumns: number[] = [];
for (let i = 0, len = this.lines.length; i < len; i++) {
let originalLine = this.lines[i];
if (i === 0) {
deltaColumns[i + 1] = firstLineDeltaColumn;
resultLines[i] = originalLine;
} else {
let originalLineIndentation = this.extractLineIndentation(originalLine);
let remainingLine = originalLine.substr(originalLineIndentation.length);
let indentation = config.normalizeIndentation(referenceIndentation + originalLineIndentation);
deltaColumns[i + 1] = indentation.length - originalLineIndentation.length;
resultLines[i] = indentation + remainingLine;
}
}
// Compute resultPlaceHolders
for (const originalPlaceHolder of this.placeHolders) {
let resultOccurences: Range[] = [];
for (let { startLineNumber, startColumn, endLineNumber, endColumn } of originalPlaceHolder.occurences) {
if (startColumn > 1 || startLineNumber === 1) {
// placeholders that aren't at the beginning of new snippet lines
// will be moved by how many characters the indentation has been
// adjusted
startColumn = startColumn + deltaColumns[startLineNumber];
endColumn = endColumn + deltaColumns[endLineNumber];
} else {
// placeholders at the beginning of new snippet lines
// will be indented by the reference indentation
startColumn += referenceIndentation.length;
endColumn += referenceIndentation.length;
}
resultOccurences.push(new Range(
startLineNumber + deltaLine,
startColumn,
endLineNumber + deltaLine,
endColumn,
));
}
resultPlaceHolders.push({
id: originalPlaceHolder.id,
value: originalPlaceHolder.value,
occurences: resultOccurences
});
}
return {
lines: resultLines,
placeHolders: resultPlaceHolders,
finishPlaceHolderIndex: this.finishPlaceHolderIndex
};
}
}
// --- parsing
interface ISnippetParser {
parse(input: string): CodeSnippet;
}
interface IParsedLinePlaceHolderInfo {
id: string;
value: string;
startColumn: number;
endColumn: number;
}
interface IParsedLine {
line: string;
placeHolders: IParsedLinePlaceHolderInfo[];
}
function _resolveSnippetVariables(marker: Marker[], resolver: ISnippetVariableResolver) {
if (resolver) {
const stack = [...marker];
while (stack.length > 0) {
const marker = stack.shift();
if (marker instanceof Variable) {
try {
marker.resolvedValue = resolver.resolve(marker.name);
} catch (e) {
//
}
if (marker.isDefined) {
continue;
}
}
if (marker instanceof Variable || marker instanceof Placeholder) {
// 'recurse'
stack.unshift(...marker.defaultValue);
}
}
}
}
function _isFinishPlaceHolder(v: IPlaceHolder) {
return (v.id === '' && v.value === '') || v.id === '0';
}
function _fillCodeSnippetFromMarker(snippet: CodeSnippet, marker: Marker[]) {
let placeHolders: { [id: string]: IPlaceHolder } = Object.create(null);
let hasFinishPlaceHolder = false;
const stack = [...marker];
snippet.lines = [''];
while (stack.length > 0) {
const marker = stack.shift();
if (marker instanceof Text) {
// simple text
let lines = marker.string.split(/\r\n|\n|\r/);
snippet.lines[snippet.lines.length - 1] += lines.shift();
snippet.lines.push(...lines);
} else if (marker instanceof Placeholder) {
let placeHolder = placeHolders[marker.index];
if (!placeHolder) {
placeHolders[marker.index] = placeHolder = {
id: marker.index,
value: Marker.toString(marker.defaultValue),
occurences: []
};
snippet.placeHolders.push(placeHolder);
}
hasFinishPlaceHolder = hasFinishPlaceHolder || _isFinishPlaceHolder(placeHolder);
const line = snippet.lines.length;
const column = snippet.lines[line - 1].length + 1;
placeHolder.occurences.push(new Range(
line,
column,
line,
column + Marker.toString(marker.defaultValue).length // TODO multiline placeholders!
));
stack.unshift(...marker.defaultValue);
} else if (marker instanceof Variable) {
if (!marker.isDefined) {
// contine as placeholder
// THIS is because of us having falsy
// advertised ${foo} as placeholder syntax
stack.unshift(new Placeholder(marker.name, marker.defaultValue.length === 0
? [new Text(marker.name)]
: marker.defaultValue));
} else if (marker.resolvedValue) {
// contine with the value
stack.unshift(new Text(marker.resolvedValue));
} else {
// continue with default values
stack.unshift(...marker.defaultValue);
}
}
if (stack.length === 0 && !hasFinishPlaceHolder) {
stack.push(new Placeholder('0', []));
}
}
// Named variables (e.g. {greeting} and {greeting:Hello}) are sorted first, followed by
// tab-stops and numeric variables (e.g. $1, $2, ${3:foo}) which are sorted in ascending order
snippet.placeHolders.sort((a, b) => {
let nonIntegerId = (v: IPlaceHolder) => !(/^\d+$/).test(v.id);
// Sort finish placeholder last
if (_isFinishPlaceHolder(a)) {
return 1;
} else if (_isFinishPlaceHolder(b)) {
return -1;
}
// Sort named placeholders first
if (nonIntegerId(a) && nonIntegerId(b)) {
return 0;
} else if (nonIntegerId(a)) {
return -1;
} else if (nonIntegerId(b)) {
return 1;
}
if (a.id === b.id) {
return 0;
}
return Number(a.id) < Number(b.id) ? -1 : 1;
});
if (snippet.placeHolders.length > 0) {
snippet.finishPlaceHolderIndex = snippet.placeHolders.length - 1;
snippet.placeHolders[snippet.finishPlaceHolderIndex].id = '';
}
}
......@@ -5,11 +5,9 @@
'use strict';
import { basename, dirname, normalize } from 'vs/base/common/paths';
import { ICommonCodeEditor, IModel } from 'vs/editor/common/editorCommon';
import { basename, dirname } from 'vs/base/common/paths';
import { IModel } from 'vs/editor/common/editorCommon';
import { Selection } from 'vs/editor/common/core/selection';
import { ISnippetVariableResolver } from './snippet';
export class EditorSnippetVariableResolver {
......@@ -67,66 +65,3 @@ export class EditorSnippetVariableResolver {
}
}
}
export class SnippetVariablesResolver implements ISnippetVariableResolver {
private readonly _editor: ICommonCodeEditor;
constructor(editor: ICommonCodeEditor) {
this._editor = editor;
}
resolve(name: string): string {
const model = this._editor.getModel();
if (!model) {
throw new Error();
}
switch (name) {
case 'SELECTION':
case 'TM_SELECTED_TEXT': return this._tmSelectedText();
case 'TM_CURRENT_LINE': return this._tmCurrentLine();
case 'TM_CURRENT_WORD': return this._tmCurrentWord();
case 'TM_LINE_INDEX': return this._tmLineIndex();
case 'TM_LINE_NUMBER': return this._tmLineNumber();
case 'TM_FILENAME': return this._tmFilename();
case 'TM_DIRECTORY': return this._tmDirectory();
case 'TM_FILEPATH': return this._tmFilepath();
}
return undefined;
}
private _tmCurrentLine(): string {
const { positionLineNumber } = this._editor.getSelection();
return this._editor.getModel().getValueInRange({ startLineNumber: positionLineNumber, startColumn: 1, endLineNumber: positionLineNumber, endColumn: Number.MAX_VALUE });
}
private _tmCurrentWord(): string {
const word = this._editor.getModel().getWordAtPosition(this._editor.getPosition());
return word ? word.word : '';
}
private _tmFilename(): string {
return basename(this._editor.getModel().uri.fsPath);
}
private _tmDirectory(): string {
const dir = dirname(normalize(this._editor.getModel().uri.fsPath));
return dir !== '.' ? dir : '';
}
private _tmFilepath(): string {
return this._editor.getModel().uri.fsPath;
}
private _tmLineIndex(): string {
return String(this._editor.getSelection().positionLineNumber - 1);
}
private _tmLineNumber(): string {
return String(this._editor.getSelection().positionLineNumber);
}
private _tmSelectedText(): string {
return this._editor.getModel().getValueInRange(this._editor.getSelection());
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import { Range } from 'vs/editor/common/core/range';
import { CodeSnippet, ICodeSnippet, ISnippetVariableResolver } from 'vs/editor/contrib/snippet/common/snippet';
suite('Editor Contrib - Snippets', () => {
function assertInternalAndTextmate(internal: string, textmate: string, callback: (snippet: ICodeSnippet) => any) {
callback(CodeSnippet.fromInternal(internal));
callback(CodeSnippet.fromTextmate(textmate));
}
test('Support tab stop order', () => {
assertInternalAndTextmate(
'finished:{{}}, second:{{2:name}}, first:{{1:}}, third:{{3:}}',
'finished:$0, second:${2:name}, first:$1, third:$3',
snippet => {
assert.deepEqual(snippet.lines, ['finished:, second:name, first:, third:']);
assert.equal(snippet.placeHolders.length, 4);
assert.equal(snippet.placeHolders[0].id, '1');
assert.equal(snippet.placeHolders[0].value, '');
assert.equal(snippet.placeHolders[1].id, '2');
assert.equal(snippet.placeHolders[1].value, 'name');
assert.equal(snippet.placeHolders[2].id, '3');
assert.equal(snippet.placeHolders[2].value, '');
assert.equal(snippet.placeHolders[3].id, '');
assert.equal(snippet.placeHolders[3].value, '');
assert.equal(snippet.finishPlaceHolderIndex, 3);
});
});
test('Support tab stop order with implicit finish', () => {
assertInternalAndTextmate(
't2:{{2:}}, t1:{{1:}}',
't2:$2, t1:$1',
snippet => {
assert.deepEqual(snippet.lines, ['t2:, t1:']);
assert.equal(snippet.placeHolders.length, 3);
assert.equal(snippet.placeHolders[0].id, '1');
assert.equal(snippet.placeHolders[0].value, '');
assert.equal(snippet.placeHolders[1].id, '2');
assert.equal(snippet.placeHolders[1].value, '');
assert.equal(snippet.finishPlaceHolderIndex, 2);
});
});
test('Support tab stop order with no finish', () => {
assertInternalAndTextmate(
't2:{{2:second}}, t3:{{3:last}}, t1:{{1:first}}',
't2:${2:second}, t3:${3:last}, t1:${1:first}',
snippet => {
assert.deepEqual(snippet.lines, ['t2:second, t3:last, t1:first']);
assert.equal(snippet.placeHolders.length, 4);
assert.equal(snippet.placeHolders[0].id, '1');
assert.equal(snippet.placeHolders[0].value, 'first');
assert.equal(snippet.placeHolders[1].id, '2');
assert.equal(snippet.placeHolders[1].value, 'second');
assert.equal(snippet.placeHolders[2].id, '3');
assert.equal(snippet.placeHolders[2].value, 'last');
assert.equal(snippet.finishPlaceHolderIndex, 3);
});
});
test('Support tab stop order which does not affect named variable id\'s', () => {
assertInternalAndTextmate(
'{{first}}-{{2:}}-{{second}}-{{1:}}',
'${first}-${2}-${second}-${1}',
snippet => {
assert.deepEqual(snippet.lines, ['first--second-']);
assert.equal(snippet.placeHolders.length, 5);
assert.equal(snippet.placeHolders[0].id, 'first');
assert.equal(snippet.placeHolders[1].id, 'second');
assert.equal(snippet.placeHolders[2].id, '1');
assert.equal(snippet.placeHolders[3].id, '2');
}
);
});
test('nested placeholder', () => {
let snippet = CodeSnippet.fromTextmate([
'<div${1: id="${2:some_id}"}>',
'\t$0',
'</div>'
].join('\n'));
assert.equal(snippet.placeHolders.length, 3);
assert.equal(snippet.finishPlaceHolderIndex, 2);
let [first, second, third] = snippet.placeHolders;
assert.equal(third.id, 0);
assert.equal(third.occurences.length, 1);
assert.deepEqual(third.occurences[0], new Range(2, 2, 2, 2));
assert.equal(second.id, 2);
assert.equal(second.occurences.length, 1);
assert.deepEqual(second.occurences[0], new Range(1, 10, 1, 17));
assert.equal(first.id, '1');
assert.equal(first.occurences.length, 1);
assert.deepEqual(first.occurences[0], new Range(1, 5, 1, 18));
});
test('bug #17541:[snippets] Support default text in mirrors', () => {
var external = [
'begin{${1:enumerate}}',
'\t$0',
'end{$1}'
].join('\n');
var internal = [
'begin\\{{{1:enumerate}}\\}',
'\t{{}}',
'end\\{{{1:}}\\}'
].join('\n');
assertInternalAndTextmate(internal, external, snippet => {
assert.deepEqual(snippet.lines, [
'begin{enumerate}',
'\t',
'end{enumerate}'
]);
assert.equal(snippet.placeHolders.length, 2);
assert.equal(snippet.placeHolders[0].id, '1');
assert.equal(snippet.placeHolders[0].occurences.length, 2);
assert.deepEqual(snippet.placeHolders[0].occurences[0], new Range(1, 7, 1, 16));
assert.deepEqual(snippet.placeHolders[0].occurences[1], new Range(3, 5, 3, 14));
assert.equal(snippet.placeHolders[1].id, '');
assert.equal(snippet.placeHolders[1].occurences.length, 1);
assert.deepEqual(snippet.placeHolders[1].occurences[0], new Range(2, 2, 2, 2));
});
});
test('bug #7093: Snippet default value is only populated for first variable reference', () => {
var internal = 'logger.error({ logContext: lc, errorContext: `{{1:err}}`, error: {{1:}} });';
var external = 'logger.error({ logContext: lc, errorContext: `${1:err}`, error: $1 });';
assertInternalAndTextmate(internal, external, snippet => {
assert.equal(snippet.lines.length, 1);
assert.equal(snippet.lines[0], 'logger.error({ logContext: lc, errorContext: `err`, error: err });');
});
});
test('bug #17487:[snippets] four backslashes are required to get one backslash in the inserted text', () => {
var external = [
'\\begin{${1:enumerate}}',
'\t$0',
'\\end{$1}'
].join('\n');
var internal = [
'\\begin\\{{{1:enumerate}}\\}',
'\t{{}}',
'\\end\\{{{1:}}\\}'
].join('\n');
assertInternalAndTextmate(internal, external, snippet => {
assert.deepEqual(snippet.lines, [
'\\begin{enumerate}',
'\t',
'\\end{enumerate}'
]);
assert.equal(snippet.placeHolders.length, 2);
assert.equal(snippet.placeHolders[0].id, '1');
assert.equal(snippet.placeHolders[0].occurences.length, 2);
assert.deepEqual(snippet.placeHolders[0].occurences[0], new Range(1, 8, 1, 17));
assert.deepEqual(snippet.placeHolders[0].occurences[1], new Range(3, 6, 3, 15));
assert.equal(snippet.placeHolders[1].id, '');
assert.equal(snippet.placeHolders[1].occurences.length, 1);
assert.deepEqual(snippet.placeHolders[1].occurences[0], new Range(2, 2, 2, 2));
});
});
test('issue #3552: Snippet Converted Not Working for literal Dollar Sign', () => {
let external = '\n\\$scope.\\$broadcast(\'scroll.infiniteScrollComplete\');\n';
let snippet = CodeSnippet.fromTextmate(external);
assert.equal(snippet.placeHolders.length, 1);
assert.equal(snippet.finishPlaceHolderIndex, 0);
assert.deepEqual(snippet.lines, ['', '$scope.$broadcast(\'scroll.infiniteScrollComplete\');', '']);
});
test('bind, adjust indentation', () => {
// don't move placeholder at the beginning of the line
let snippet = CodeSnippet.fromTextmate([
'afterEach((done) => {',
'\t${1}test${2}',
'})'
].join('\n'));
// replace tab-stop with two spaces
let boundSnippet = snippet.bind('', 0, 0, {
normalizeIndentation(str: string): string {
return str.replace(/\t/g, ' ');
}
});
let [first, second] = boundSnippet.placeHolders;
assert.equal(first.occurences.length, 1);
assert.equal(first.occurences[0].startColumn, 3);
assert.equal(second.occurences.length, 1);
assert.equal(second.occurences[0].startColumn, 7);
// keep tab-stop, identity
boundSnippet = snippet.bind('', 0, 0, {
normalizeIndentation(str: string): string {
return str;
}
});
[first, second] = boundSnippet.placeHolders;
assert.equal(first.occurences.length, 1);
assert.equal(first.occurences[0].startColumn, 2);
assert.equal(second.occurences.length, 1);
assert.equal(second.occurences[0].startColumn, 6);
});
test('issue #11890: Bad cursor position 1/2', () => {
let snippet = CodeSnippet.fromTextmate([
'afterEach((done) => {',
'${1}\ttest${2}',
'})'
].join('\n'));
let boundSnippet = snippet.bind('', 0, 0, {
normalizeIndentation(str: string): string {
return str.replace(/\t/g, ' ');
}
});
assert.equal(boundSnippet.lines[1], ' test');
assert.equal(boundSnippet.placeHolders.length, 3);
assert.equal(boundSnippet.finishPlaceHolderIndex, 2);
let [first, second] = boundSnippet.placeHolders;
assert.equal(first.occurences.length, 1);
assert.equal(first.occurences[0].startColumn, 1);
assert.equal(second.occurences.length, 1);
assert.equal(second.occurences[0].startColumn, 7);
});
test('issue #11890: Bad cursor position 2/2', () => {
let snippet = CodeSnippet.fromTextmate('${1}\ttest');
let boundSnippet = snippet.bind('abc abc abc prefix3', 0, 12, {
normalizeIndentation(str: string): string {
return str.replace(/\t/g, ' ');
}
});
assert.equal(boundSnippet.lines[0], '\ttest');
assert.equal(boundSnippet.placeHolders.length, 2);
assert.equal(boundSnippet.finishPlaceHolderIndex, 1);
let [first, second] = boundSnippet.placeHolders;
assert.equal(first.occurences.length, 1);
assert.equal(first.occurences[0].startColumn, 13);
assert.equal(second.occurences.length, 1);
assert.equal(second.occurences[0].startColumn, 18);
});
test('issue #17989: Bad selection', () => {
let snippet = CodeSnippet.fromTextmate('${1:HoldMeTight}');
let boundSnippet = snippet.bind('abc abc abc prefix3', 0, 12, {
normalizeIndentation(str: string): string {
return str.replace(/\t/g, ' ');
}
});
assert.equal(boundSnippet.lines[0], 'HoldMeTight');
assert.equal(boundSnippet.placeHolders.length, 2);
assert.equal(boundSnippet.finishPlaceHolderIndex, 1);
let [first, second] = boundSnippet.placeHolders;
assert.equal(first.occurences.length, 1);
assert.equal(first.occurences[0].startColumn, 13);
assert.equal(second.occurences.length, 1);
assert.equal(second.occurences[0].startColumn, 24);
});
test('variables, simple', () => {
const resolver: ISnippetVariableResolver = {
resolve(name) {
return name.split('').reverse().join('');
}
};
// simple
let snippet = CodeSnippet.fromTextmate('$FOO', resolver);
assert.equal(snippet.lines[0], 'OOF');
assert.equal(snippet.placeHolders.length, 1);
assert.equal(snippet.placeHolders[0].occurences[0].endColumn, 4);
snippet = CodeSnippet.fromTextmate('${FOO:BAR}', resolver);
assert.equal(snippet.lines[0], 'OOF');
assert.equal(snippet.placeHolders.length, 1);
assert.equal(snippet.placeHolders[0].occurences[0].endColumn, 4);
// placeholder
snippet = CodeSnippet.fromTextmate('${1:$FOO}bar$1', resolver);
assert.equal(snippet.lines[0], 'OOFbarOOF');
assert.equal(snippet.placeHolders.length, 2);
assert.equal(snippet.placeHolders[0].occurences.length, 2);
assert.equal(snippet.placeHolders[0].occurences[0].startColumn, 1);
assert.equal(snippet.placeHolders[0].occurences[0].endColumn, 4);
assert.equal(snippet.placeHolders[0].occurences[1].startColumn, 7);
assert.equal(snippet.placeHolders[0].occurences[1].endColumn, 10);
assert.equal(snippet.placeHolders[1].occurences.length, 1);
snippet = CodeSnippet.fromTextmate('${1:${FOO:abc}}bar$1', resolver);
assert.equal(snippet.lines[0], 'OOFbarOOF');
});
test('variables, evil resolver', () => {
let snippet = CodeSnippet.fromTextmate('$FOO', { resolve(): string { throw new Error(); } });
assert.equal(snippet.lines[0], 'FOO');
});
test('variables, default', () => {
let snippet = CodeSnippet.fromTextmate('$FOO', { resolve(): string { return undefined; } });
assert.equal(snippet.lines[0], 'FOO');
snippet = CodeSnippet.fromTextmate('$FOO', { resolve(): string { return ''; } });
assert.equal(snippet.lines[0], '');
snippet = CodeSnippet.fromTextmate('${FOO:BAR}', { resolve(): string { return undefined; } });
assert.equal(snippet.lines[0], 'BAR');
snippet = CodeSnippet.fromTextmate('${FOO:BAR}', { resolve(): string { return ''; } });
assert.equal(snippet.lines[0], 'BAR');
});
});
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import { Range } from 'vs/editor/common/core/range';
import { Position } from 'vs/editor/common/core/position';
import { Selection } from 'vs/editor/common/core/selection';
import { CodeSnippet } from 'vs/editor/contrib/snippet/common/snippet';
import { SnippetController } from 'vs/editor/contrib/snippet/common/snippetController';
import { MockCodeEditor, withMockCodeEditor } from 'vs/editor/test/common/mocks/mockCodeEditor';
import { Cursor } from 'vs/editor/common/controller/cursor';
class TestSnippetController extends SnippetController {
isInSnippetMode(): boolean {
return !!this._currentController;
}
}
suite('SnippetController', () => {
function snippetTest(cb: (editor: MockCodeEditor, cursor: Cursor, codeSnippet: CodeSnippet, snippetController: TestSnippetController) => void, lines?: string[]): void {
if (!lines) {
lines = [
'function test() {',
'\tvar x = 3;',
'\tvar arr = [];',
'\t',
'}'
];
};
withMockCodeEditor(lines, {}, (editor, cursor) => {
editor.getModel().updateOptions({
insertSpaces: false
});
let snippetController = editor.registerAndInstantiateContribution<TestSnippetController>(TestSnippetController);
let codeSnippet = CodeSnippet.fromInternal([
'for (var {{index}}; {{index}} < {{array}}.length; {{index}}++) {',
'\tvar element = {{array}}[{{index}}];',
'\t{{}}',
'}'
].join('\n'));
cb(editor, cursor, codeSnippet, snippetController);
snippetController.dispose();
});
}
test('Simple accepted', () => {
snippetTest((editor, cursor, codeSnippet, snippetController) => {
editor.setPosition({ lineNumber: 4, column: 2 });
snippetController.run(codeSnippet, 0, 0);
assert.equal(editor.getModel().getLineContent(4), '\tfor (var index; index < array.length; index++) {');
assert.equal(editor.getModel().getLineContent(5), '\t\tvar element = array[index];');
assert.equal(editor.getModel().getLineContent(6), '\t\t');
assert.equal(editor.getModel().getLineContent(7), '\t}');
editor.trigger('test', 'type', { text: 'i' });
assert.equal(editor.getModel().getLineContent(4), '\tfor (var i; i < array.length; i++) {');
assert.equal(editor.getModel().getLineContent(5), '\t\tvar element = array[i];');
assert.equal(editor.getModel().getLineContent(6), '\t\t');
assert.equal(editor.getModel().getLineContent(7), '\t}');
snippetController.jumpToNextPlaceholder();
editor.trigger('test', 'type', { text: 'arr' });
assert.equal(editor.getModel().getLineContent(4), '\tfor (var i; i < arr.length; i++) {');
assert.equal(editor.getModel().getLineContent(5), '\t\tvar element = arr[i];');
assert.equal(editor.getModel().getLineContent(6), '\t\t');
assert.equal(editor.getModel().getLineContent(7), '\t}');
snippetController.jumpToPrevPlaceholder();
editor.trigger('test', 'type', { text: 'j' });
assert.equal(editor.getModel().getLineContent(4), '\tfor (var j; j < arr.length; j++) {');
assert.equal(editor.getModel().getLineContent(5), '\t\tvar element = arr[j];');
assert.equal(editor.getModel().getLineContent(6), '\t\t');
assert.equal(editor.getModel().getLineContent(7), '\t}');
snippetController.acceptSnippet();
assert.deepEqual(editor.getPosition(), new Position(6, 3));
});
});
test('Simple canceled', () => {
snippetTest((editor, cursor, codeSnippet, snippetController) => {
editor.setPosition({ lineNumber: 4, column: 2 });
snippetController.run(codeSnippet, 0, 0);
assert.equal(editor.getModel().getLineContent(4), '\tfor (var index; index < array.length; index++) {');
assert.equal(editor.getModel().getLineContent(5), '\t\tvar element = array[index];');
assert.equal(editor.getModel().getLineContent(6), '\t\t');
assert.equal(editor.getModel().getLineContent(7), '\t}');
snippetController.leaveSnippet();
assert.deepEqual(editor.getPosition(), new Position(4, 16));
});
});
test('Stops when deleting lines above', () => {
snippetTest((editor, cursor, codeSnippet, snippetController) => {
editor.setPosition({ lineNumber: 4, column: 2 });
snippetController.run(codeSnippet, 0, 0);
editor.getModel().applyEdits([{
forceMoveMarkers: false,
identifier: null,
isAutoWhitespaceEdit: false,
range: new Range(1, 1, 3, 1),
text: null
}]);
assert.equal(snippetController.isInSnippetMode(), false);
});
});
test('Stops when deleting lines below', () => {
snippetTest((editor, cursor, codeSnippet, snippetController) => {
editor.setPosition({ lineNumber: 4, column: 2 });
snippetController.run(codeSnippet, 0, 0);
editor.getModel().applyEdits([{
forceMoveMarkers: false,
identifier: null,
isAutoWhitespaceEdit: false,
range: new Range(8, 1, 8, 100),
text: null
}]);
assert.equal(snippetController.isInSnippetMode(), false);
});
});
test('Stops when inserting lines above', () => {
snippetTest((editor, cursor, codeSnippet, snippetController) => {
editor.setPosition({ lineNumber: 4, column: 2 });
snippetController.run(codeSnippet, 0, 0);
editor.getModel().applyEdits([{
forceMoveMarkers: false,
identifier: null,
isAutoWhitespaceEdit: false,
range: new Range(1, 100, 1, 100),
text: '\nHello'
}]);
assert.equal(snippetController.isInSnippetMode(), false);
});
});
test('Stops when inserting lines below', () => {
snippetTest((editor, cursor, codeSnippet, snippetController) => {
editor.setPosition({ lineNumber: 4, column: 2 });
snippetController.run(codeSnippet, 0, 0);
editor.getModel().applyEdits([{
forceMoveMarkers: false,
identifier: null,
isAutoWhitespaceEdit: false,
range: new Range(8, 100, 8, 100),
text: '\nHello'
}]);
assert.equal(snippetController.isInSnippetMode(), false);
});
});
test('Stops when calling model.setValue()', () => {
snippetTest((editor, cursor, codeSnippet, snippetController) => {
editor.setPosition({ lineNumber: 4, column: 2 });
snippetController.run(codeSnippet, 0, 0);
editor.getModel().setValue('goodbye');
assert.equal(snippetController.isInSnippetMode(), false);
});
});
test('Stops when undoing', () => {
snippetTest((editor, cursor, codeSnippet, snippetController) => {
editor.setPosition({ lineNumber: 4, column: 2 });
snippetController.run(codeSnippet, 0, 0);
editor.getModel().undo();
assert.equal(snippetController.isInSnippetMode(), false);
});
});
test('Stops when moving cursor outside', () => {
snippetTest((editor, cursor, codeSnippet, snippetController) => {
editor.setPosition({ lineNumber: 4, column: 2 });
snippetController.run(codeSnippet, 0, 0);
editor.setPosition({ lineNumber: 1, column: 1 });
assert.equal(snippetController.isInSnippetMode(), false);
});
});
test('Stops when disconnecting editor model', () => {
snippetTest((editor, cursor, codeSnippet, snippetController) => {
editor.setPosition({ lineNumber: 4, column: 2 });
snippetController.run(codeSnippet, 0, 0);
editor.setModel(null);
assert.equal(snippetController.isInSnippetMode(), false);
});
});
test('Stops when disposing editor', () => {
snippetTest((editor, cursor, codeSnippet, snippetController) => {
editor.setPosition({ lineNumber: 4, column: 2 });
snippetController.run(codeSnippet, 0, 0);
snippetController.dispose();
assert.equal(snippetController.isInSnippetMode(), false);
});
});
test('Final tabstop with multiple selections', () => {
snippetTest((editor, cursor, codeSnippet, snippetController) => {
editor.setSelections([
new Selection(1, 1, 1, 1),
new Selection(2, 1, 2, 1),
]);
codeSnippet = CodeSnippet.fromInternal('foo{{}}');
snippetController.run(codeSnippet, 0, 0);
assert.equal(editor.getSelections().length, 2);
const [first, second] = editor.getSelections();
assert.ok(first.equalsRange({ startLineNumber: 1, startColumn: 4, endLineNumber: 1, endColumn: 4 }), first.toString());
assert.ok(second.equalsRange({ startLineNumber: 2, startColumn: 4, endLineNumber: 2, endColumn: 4 }), second.toString());
});
snippetTest((editor, cursor, codeSnippet, snippetController) => {
editor.setSelections([
new Selection(1, 1, 1, 1),
new Selection(2, 1, 2, 1),
]);
codeSnippet = CodeSnippet.fromInternal('foo{{}}bar');
snippetController.run(codeSnippet, 0, 0);
assert.equal(editor.getSelections().length, 2);
const [first, second] = editor.getSelections();
assert.ok(first.equalsRange({ startLineNumber: 1, startColumn: 4, endLineNumber: 1, endColumn: 4 }), first.toString());
assert.ok(second.equalsRange({ startLineNumber: 2, startColumn: 4, endLineNumber: 2, endColumn: 4 }), second.toString());
});
snippetTest((editor, cursor, codeSnippet, snippetController) => {
editor.setSelections([
new Selection(1, 1, 1, 1),
new Selection(1, 5, 1, 5),
]);
codeSnippet = CodeSnippet.fromInternal('foo{{}}bar');
snippetController.run(codeSnippet, 0, 0);
assert.equal(editor.getSelections().length, 2);
const [first, second] = editor.getSelections();
assert.ok(first.equalsRange({ startLineNumber: 1, startColumn: 4, endLineNumber: 1, endColumn: 4 }), first.toString());
assert.ok(second.equalsRange({ startLineNumber: 1, startColumn: 14, endLineNumber: 1, endColumn: 14 }), second.toString());
});
snippetTest((editor, cursor, codeSnippet, snippetController) => {
editor.setSelections([
new Selection(1, 1, 1, 1),
new Selection(1, 5, 1, 5),
]);
codeSnippet = CodeSnippet.fromInternal('foo\n{{}}\nbar');
snippetController.run(codeSnippet, 0, 0);
assert.equal(editor.getSelections().length, 2);
const [first, second] = editor.getSelections();
assert.ok(first.equalsRange({ startLineNumber: 2, startColumn: 1, endLineNumber: 2, endColumn: 1 }), first.toString());
assert.ok(second.equalsRange({ startLineNumber: 4, startColumn: 1, endLineNumber: 4, endColumn: 1 }), second.toString());
});
snippetTest((editor, cursor, codeSnippet, snippetController) => {
editor.setSelections([
new Selection(1, 1, 1, 1),
new Selection(1, 5, 1, 5),
]);
codeSnippet = CodeSnippet.fromInternal('foo\n{{}}\nbar');
snippetController.run(codeSnippet, 0, 0);
assert.equal(editor.getSelections().length, 2);
const [first, second] = editor.getSelections();
assert.ok(first.equalsRange({ startLineNumber: 2, startColumn: 1, endLineNumber: 2, endColumn: 1 }), first.toString());
assert.ok(second.equalsRange({ startLineNumber: 4, startColumn: 1, endLineNumber: 4, endColumn: 1 }), second.toString());
});
snippetTest((editor, cursor, codeSnippet, snippetController) => {
editor.setSelections([
new Selection(2, 7, 2, 7),
]);
codeSnippet = CodeSnippet.fromInternal('xo{{}}r');
snippetController.run(codeSnippet, 1, 0);
assert.equal(editor.getSelections().length, 1);
assert.ok(editor.getSelection().equalsRange({ startLineNumber: 2, startColumn: 8, endColumn: 8, endLineNumber: 2 }));
});
});
test('Final tabstop, #11742 simple', () => {
snippetTest((editor, cursor, codeSnippet, controller) => {
editor.setSelection(new Selection(1, 19, 1, 19));
codeSnippet = CodeSnippet.fromTextmate('{{% url_**$1** %}}');
controller.run(codeSnippet, 2, 0);
assert.equal(editor.getSelections().length, 1);
assert.ok(editor.getSelection().equalsRange({ startLineNumber: 1, startColumn: 27, endLineNumber: 1, endColumn: 27 }));
assert.equal(editor.getModel().getValue(), 'example example {{% url_**** %}}');
}, ['example example sc']);
snippetTest((editor, cursor, codeSnippet, controller) => {
editor.setSelection(new Selection(1, 3, 1, 3));
codeSnippet = CodeSnippet.fromTextmate([
'afterEach((done) => {',
'\t${1}test',
'});'
].join('\n'));
controller.run(codeSnippet, 2, 0);
assert.equal(editor.getSelections().length, 1);
assert.ok(editor.getSelection().equalsRange({ startLineNumber: 2, startColumn: 2, endLineNumber: 2, endColumn: 2 }), editor.getSelection().toString());
assert.equal(editor.getModel().getValue(), 'afterEach((done) => {\n\ttest\n});');
}, ['af']);
snippetTest((editor, cursor, codeSnippet, controller) => {
editor.setSelection(new Selection(1, 3, 1, 3));
codeSnippet = CodeSnippet.fromTextmate([
'afterEach((done) => {',
'${1}\ttest',
'});'
].join('\n'));
controller.run(codeSnippet, 2, 0);
assert.equal(editor.getSelections().length, 1);
assert.ok(editor.getSelection().equalsRange({ startLineNumber: 2, startColumn: 1, endLineNumber: 2, endColumn: 1 }), editor.getSelection().toString());
assert.equal(editor.getModel().getValue(), 'afterEach((done) => {\n\ttest\n});');
}, ['af']);
snippetTest((editor, cursor, codeSnippet, controller) => {
editor.setSelection(new Selection(1, 9, 1, 9));
codeSnippet = CodeSnippet.fromTextmate([
'aft${1}er'
].join('\n'));
controller.run(codeSnippet, 8, 0);
assert.equal(editor.getModel().getValue(), 'after');
assert.equal(editor.getSelections().length, 1);
assert.ok(editor.getSelection().equalsRange({ startLineNumber: 1, startColumn: 4, endLineNumber: 1, endColumn: 4 }), editor.getSelection().toString());
}, ['afterone']);
});
test('Final tabstop, #11742 different indents', () => {
snippetTest((editor, cursor, codeSnippet, controller) => {
editor.setSelections([
new Selection(2, 4, 2, 4),
new Selection(1, 3, 1, 3)
]);
codeSnippet = CodeSnippet.fromTextmate([
'afterEach((done) => {',
'\t${0}test',
'});'
].join('\n'));
controller.run(codeSnippet, 2, 0);
assert.equal(editor.getSelections().length, 2);
const [first, second] = editor.getSelections();
assert.ok(first.equalsRange({ startLineNumber: 5, startColumn: 3, endLineNumber: 5, endColumn: 3 }), first.toString());
assert.ok(second.equalsRange({ startLineNumber: 2, startColumn: 2, endLineNumber: 2, endColumn: 2 }), second.toString());
}, ['af', '\taf']);
});
test('Final tabstop, #11890 stay at the beginning', () => {
snippetTest((editor, cursor, codeSnippet, controller) => {
editor.setSelections([
new Selection(1, 5, 1, 5)
]);
codeSnippet = CodeSnippet.fromTextmate([
'afterEach((done) => {',
'${1}\ttest',
'});'
].join('\n'));
controller.run(codeSnippet, 2, 0);
assert.equal(editor.getSelections().length, 1);
const [first] = editor.getSelections();
assert.ok(first.equalsRange({ startLineNumber: 2, startColumn: 3, endLineNumber: 2, endColumn: 3 }), first.toString());
}, [' af']);
});
test('Final tabstop, no tabstop', () => {
snippetTest((editor, cursor, codeSnippet, controller) => {
editor.setSelections([
new Selection(1, 3, 1, 3)
]);
codeSnippet = CodeSnippet.fromTextmate('afterEach');
controller.run(codeSnippet, 2, 0);
assert.ok(editor.getSelection().equalsRange({ startLineNumber: 1, startColumn: 10, endLineNumber: 1, endColumn: 10 }));
}, ['af', '\taf']);
});
test('Multiple cursor and overwriteBefore/After, issue #11060', () => {
snippetTest((editor, cursor, codeSnippet, controller) => {
editor.setSelections([
new Selection(1, 7, 1, 7),
new Selection(2, 4, 2, 4)
]);
codeSnippet = CodeSnippet.fromTextmate('_foo');
controller.run(codeSnippet, 1, 0);
assert.equal(editor.getModel().getValue(), 'this._foo\nabc_foo');
}, ['this._', 'abc']);
snippetTest((editor, cursor, codeSnippet, controller) => {
editor.setSelections([
new Selection(1, 7, 1, 7),
new Selection(2, 4, 2, 4)
]);
codeSnippet = CodeSnippet.fromTextmate('XX');
controller.run(codeSnippet, 1, 0);
assert.equal(editor.getModel().getValue(), 'this.XX\nabcXX');
}, ['this._', 'abc']);
snippetTest((editor, cursor, codeSnippet, controller) => {
editor.setSelections([
new Selection(1, 7, 1, 7),
new Selection(2, 4, 2, 4),
new Selection(3, 5, 3, 5)
]);
codeSnippet = CodeSnippet.fromTextmate('_foo');
controller.run(codeSnippet, 1, 0);
assert.equal(editor.getModel().getValue(), 'this._foo\nabc_foo\ndef_foo');
}, ['this._', 'abc', 'def_']);
snippetTest((editor, cursor, codeSnippet, controller) => {
editor.setSelections([
new Selection(1, 7, 1, 7),
new Selection(2, 4, 2, 4),
new Selection(3, 6, 3, 6)
]);
codeSnippet = CodeSnippet.fromTextmate('._foo');
controller.run(codeSnippet, 2, 0);
assert.equal(editor.getModel().getValue(), 'this._foo\nabc._foo\ndef._foo');
}, ['this._', 'abc', 'def._']);
});
test('Multiple cursor and overwriteBefore/After, #16277', () => {
snippetTest((editor, cursor, codeSnippet, controller) => {
editor.setSelections([
new Selection(1, 5, 1, 5),
new Selection(2, 5, 2, 5),
]);
codeSnippet = CodeSnippet.fromTextmate('document');
controller.run(codeSnippet, 3, 0);
assert.equal(editor.getModel().getValue(), '{document}\n{document && true}');
}, ['{foo}', '{foo && true}']);
});
test('Insert snippet twice, #19449', () => {
snippetTest((editor, cursor, codeSnippet, controller) => {
editor.setSelections([
new Selection(1, 1, 1, 1)
]);
codeSnippet = CodeSnippet.fromTextmate('for (var ${1:i}=0; ${1:i}<len; ${1:i}++) { $0 }');
controller.run(codeSnippet, 0, 0);
assert.equal(editor.getModel().getValue(), 'for (var i=0; i<len; i++) { }for (var i=0; i<len; i++) { }');
}, ['for (var i=0; i<len; i++) { }']);
snippetTest((editor, cursor, codeSnippet, controller) => {
editor.setSelections([
new Selection(1, 1, 1, 1)
]);
codeSnippet = CodeSnippet.fromTextmate('for (let ${1:i}=0; ${1:i}<len; ${1:i}++) { $0 }');
controller.run(codeSnippet, 0, 0);
assert.equal(editor.getModel().getValue(), 'for (let i=0; i<len; i++) { }for (var i=0; i<len; i++) { }');
}, ['for (var i=0; i<len; i++) { }']);
});
});
......@@ -7,12 +7,12 @@
import { ICommonCodeEditor } from 'vs/editor/common/editorCommon';
import strings = require('vs/base/common/strings');
import snippets = require('vs/editor/contrib/snippet/common/snippet');
import { Range } from 'vs/editor/common/core/range';
import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2';
import { LanguageId, LanguageIdentifier } from 'vs/editor/common/modes';
import { Position } from 'vs/editor/common/core/position';
import { CoreEditingCommands } from "vs/editor/common/controller/coreCommands";
import { SnippetParser, walk, Placeholder, Variable, Text, Marker } from 'vs/editor/contrib/snippet/common/snippetParser';
import emmet = require('emmet');
......@@ -94,7 +94,7 @@ export class EditorAccessor implements emmet.Editor {
return;
}
const tweakedValue = snippets.CodeSnippet.fixEmmetFinalTabstop(value);
const tweakedValue = EditorAccessor.fixEmmetFinalTabstop(value);
// let selection define the typing range
this._editor.setSelection(range);
......@@ -104,6 +104,64 @@ export class EditorAccessor implements emmet.Editor {
// SnippetController.get(this._editor).runWithReplaceRange(codeSnippet, range);
}
private static fixEmmetFinalTabstop(template: string): string {
let matchFinalStops = template.match(/\$\{0\}|\$0/g);
if (!matchFinalStops || matchFinalStops.length === 1) {
return template;
}
// string to string conversion that tries to fix the
// snippet in-place
let marker = new SnippetParser(true, false).parse(template);
let maxIndex = -Number.MIN_VALUE;
// find highest placeholder index
walk(marker, candidate => {
if (candidate instanceof Placeholder) {
let index = Number(candidate.index);
if (index > maxIndex) {
maxIndex = index;
}
}
return true;
});
// rewrite final tabstops
walk(marker, candidate => {
if (candidate instanceof Placeholder) {
if (candidate.isFinalTabstop) {
candidate.index = String(++maxIndex);
}
}
return true;
});
// write back as string
function toSnippetString(marker: Marker): string {
if (marker instanceof Text) {
return SnippetParser.escape(marker.string);
} else if (marker instanceof Placeholder) {
if (marker.defaultValue.length > 0) {
return `\${${marker.index}:${marker.defaultValue.map(toSnippetString).join('')}}`;
} else {
return `\$${marker.index}`;
}
} else if (marker instanceof Variable) {
if (marker.defaultValue.length > 0) {
return `\${${marker.name}:${marker.defaultValue.map(toSnippetString).join('')}}`;
} else {
return `\$${marker.name}`;
}
} else {
throw new Error('unexpected marker: ' + marker);
}
}
return marker.map(toSnippetString).join('');
}
public getRangeToReplace(value: string, start: number, end: number): Range {
//console.log('value', value);
let startPosition = this.getPositionFromOffset(start);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册