Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] gq #1106

Merged
merged 9 commits into from
Dec 4, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@
"before": ["j", "j"],
"after": ["<esc>"]
}
],
]
}
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ The following is a subset of the supported configurations; the full list is desc
* Copy indent from current line when starting a new line
* Type: Boolean (Default: `true`)

#### textwidth
* Width to word-wrap to when using `gq`.
* Type: number (Default: `80`)

## Configure

Vim options are loaded in the following sequence:
Expand Down
10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,11 @@
"type": "array",
"description": "Non-recursive keybinding overrides to use for insert mode."
},
"vim.textwidth": {
"type": "number",
"description": "Width to word-wrap to when using gq.",
"default": 80
},
"vim.useSolidBlockCursor": {
"type": "boolean",
"description": "Use a non blinking block cursor.",
Expand Down Expand Up @@ -345,6 +350,7 @@
"copy-paste": "^1.3.0",
"diff-match-patch": "^1.0.0",
"lodash": "^4.12.0",
"typescript": "^2.2.0-dev.20161203",
"vscode": "^0.11.16"
},
"devDependencies": {
Expand All @@ -358,7 +364,7 @@
"gulp-typings": "^2.0.0",
"merge-stream": "^1.0.0",
"tslint": "^3.10.2",
"typescript": "2.0.0",
"typescript": "^2.2.0-dev.20161203",
"typings": "^1.4.0"
}
}
}
213 changes: 213 additions & 0 deletions src/actions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3931,6 +3931,219 @@ class ActionJoinVisualMode extends BaseCommand {
}
}

interface CommentTypeSingle {
singleLine: true;

start: string;
}

interface CommentTypeMultiLine {
singleLine: false;

start: string;
inner: string;
final: string;
}

type CommentType = CommentTypeSingle | CommentTypeMultiLine;

@RegisterAction
class ActionVisualReflowParagraph extends BaseCommand {
modes = [ModeName.Visual, ModeName.VisualLine];
keys = ["g", "q"];

public static CommentTypes: CommentType[] = [
{ singleLine: true, start: "//" },
{ singleLine: true, start: "--" },
{ singleLine: true, start: "#" },
{ singleLine: true, start: ";" },
{ singleLine: false, start: "/**", inner: "*", final: "*/" },
{ singleLine: false, start: "/*", inner: "*", final: "*/" },
{ singleLine: false, start: "{-", inner: "-", final: "-}" },

// Needs to come last, since everything starts with the emtpy string!
{ singleLine: true, start: "" },
];

public getIndentationLevel(s: string): number {
for (const line of s.split("\n")) {
const result = line.match(/^\s+/g);
const indentLevel = result ? result[0].length : 0;

if (indentLevel !== line.length) {
return indentLevel;
}
}

return 0;
}

public reflowParagraph(s: string, indentLevel: number): string {
const maximumLineLength = Configuration.getInstance().textwidth - indentLevel - 2;
const indent = Array(indentLevel + 1).join(" ");

// Chunk the lines by commenting style.

let chunksToReflow: {
commentType: CommentType;
content: string;
}[] = [];

for (const line of s.split("\n")) {
let lastChunk: { commentType: CommentType; content: string} | undefined = chunksToReflow[chunksToReflow.length - 1];
const trimmedLine = line.trim();

// See what comment type they are using.

let commentType: CommentType | undefined;

for (const type of ActionVisualReflowParagraph.CommentTypes) {
if (line.trim().startsWith(type.start)) {
commentType = type;

break;
}

// If they're currently in a multiline comment, see if they continued it.
if (lastChunk && type.start === lastChunk.commentType.start && !type.singleLine) {
if (line.trim().startsWith(type.inner)) {
commentType = type;

break;
}

if (line.trim().endsWith(type.final)) {
commentType = type;

break;
}
}
}

if (!commentType) { break; } // will never happen, just to satisfy typechecker.

// Did they start a new comment type?
if (!lastChunk || commentType.start !== lastChunk.commentType.start) {
chunksToReflow.push({
commentType,
content: `${ trimmedLine.substr(commentType.start.length).trim() }`
});

continue;
}

// Parse out commenting style, gather words.

lastChunk = chunksToReflow[chunksToReflow.length - 1];

if (lastChunk.commentType.singleLine) { // is it a continuation of a comment like "//"
lastChunk.content += `\n${ trimmedLine.substr(lastChunk.commentType.start.length).trim() }`;

} else { // are we in the middle of a multiline comment like "/*"
if (trimmedLine.endsWith(lastChunk.commentType.final)) {
if (trimmedLine.length > lastChunk.commentType.final.length) {
lastChunk.content += `\n${ trimmedLine.substr(
lastChunk.commentType.inner.length,
trimmedLine.length - lastChunk.commentType.final.length
).trim() }`;
}

} else if (trimmedLine.startsWith(lastChunk.commentType.inner)) {
lastChunk.content += `\n${ trimmedLine.substr(lastChunk.commentType.inner.length).trim() }`;
} else if (trimmedLine.startsWith(lastChunk.commentType.start)) {
lastChunk.content += `\n${ trimmedLine.substr(lastChunk.commentType.start.length).trim() }`;
}
}
}

// Reflow each chunk.
let result: string[] = [];

for (const { commentType, content } of chunksToReflow) {
let lines: string[];

if (commentType.singleLine) {
lines = [``];
} else {
lines = [``, ``];
}

for (const line of content.trim().split("\n")) {

// Preserve newlines.

if (line.trim() === "") {
for (let i = 0; i < 2; i++) {
lines.push(``);
}

continue;
}

// Add word by word, wrapping when necessary.

for (const word of line.split(/\s+/)) {
if (word === "") { continue; }

if (lines[lines.length - 1].length + word.length + 1 < maximumLineLength) {
lines[lines.length - 1] += ` ${ word }`;
} else {
lines.push(` ${ word }`);
}
}
}

if (!commentType.singleLine) {
lines.push(``);
}

if (commentType.singleLine) {
if (lines.length > 1 && lines[0].trim() === "") { lines = lines.slice(1); }
if (lines.length > 1 && lines[lines.length - 1].trim() === "") { lines = lines.slice(0, -1); }
}

for (let i = 0; i < lines.length; i++) {
if (commentType.singleLine) {
lines[i] = `${ indent }${ commentType.start }${ lines[i] }`;
} else {
if (i === 0) {
lines[i] = `${ indent }${ commentType.start }${ lines[i] }`;
} else if (i === lines.length - 1) {
lines[i] = `${ indent } ${ commentType.final }`;
} else {
lines[i] = `${ indent } ${ commentType.inner }${ lines[i] }`;
}
}
}

result = result.concat(lines);
}

// Gather up multiple empty lines into single empty lines.

return result.join("\n");
}

public async exec(position: Position, vimState: VimState): Promise<VimState> {
const rangeStart = Position.EarlierOf(vimState.cursorPosition, vimState.cursorStartPosition);
const rangeStop = Position.LaterOf(vimState.cursorPosition, vimState.cursorStartPosition);

let textToReflow = TextEditor.getText(new vscode.Range(rangeStart, rangeStop));
let indentLevel = this.getIndentationLevel(textToReflow);

textToReflow = this.reflowParagraph(textToReflow, indentLevel);

vimState.recordedState.transformations.push({
type: "replaceText",
text: textToReflow,
start: vimState.cursorStartPosition,
end: vimState.cursorPosition,
});

return vimState;
}
}

@RegisterAction
class ActionJoinNoWhitespace extends BaseCommand {
modes = [ModeName.Normal];
Expand Down
2 changes: 1 addition & 1 deletion src/cmd_line/subparsers/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function parseWriteCommandArgs(args : string) : WriteCommand {
scanner.ignore();
while (!scanner.isAtEof) {
let c = scanner.next();
if (c !== ' ' || c !== '\t') {
if (c !== ' ' && c !== '\t') {
continue;
}
scanner.backup();
Expand Down
2 changes: 1 addition & 1 deletion src/cmd_line/subparsers/writequit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function parseWriteQuitCommandArgs(args : string) : WriteQuitCommand {
scanner.ignore();
while (!scanner.isAtEof) {
let c = scanner.next();
if (c !== ' ' || c !== '\t') {
if (c !== ' ' && c !== '\t') {
continue;
}
scanner.backup();
Expand Down
1 change: 1 addition & 0 deletions src/configuration/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export class Configuration {
useSystemClipboard = false;
useCtrlKeys = false;
scroll = 20;
textwidth = 80;
hlsearch = false;
ignorecase = true;
smartcase = true;
Expand Down
2 changes: 1 addition & 1 deletion src/mode/modeHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1518,7 +1518,7 @@ export class ModeHandler implements vscode.Disposable {

rangesToDraw.push.apply(rangesToDraw, searchState.matchRanges);

const { pos, match } = searchState.getNextSearchMatchPosition(vimState.cursorPosition);
const { pos, match } = searchState.getNextSearchMatchPosition(vimState.cursorPosition);

if (match) {
rangesToDraw.push(new vscode.Range(
Expand Down