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

Copy formatted code to clipboard #20242

Merged
merged 7 commits into from
Feb 16, 2017
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
5 changes: 4 additions & 1 deletion src/vs/editor/browser/controller/input/textAreaWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@ class ClipboardEventWrapper implements IClipboardEvent {
return false;
}

public setTextData(text: string): void {
public setTextData(text: string, richText: string): void {
if (this._event.clipboardData) {
this._event.clipboardData.setData('text/plain', text);
if (richText !== null) {
this._event.clipboardData.setData('text/html', richText);
}
this._event.preventDefault();
return;
}
Expand Down
40 changes: 8 additions & 32 deletions src/vs/editor/common/controller/textAreaHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import { KeyCode } from 'vs/base/common/keyCodes';
import { Disposable } from 'vs/base/common/lifecycle';
import { IClipboardEvent, ICompositionEvent, IKeyboardEventWrapper, ISimpleModel, ITextAreaWrapper, ITypeData, TextAreaState, TextAreaStrategy, createTextAreaState } from 'vs/editor/common/controller/textAreaState';
import { Range } from 'vs/editor/common/core/range';
import { Position } from 'vs/editor/common/core/position';
import { EndOfLinePreference } from 'vs/editor/common/editorCommon';
import { InternalEditorOptions } from 'vs/editor/common/editorCommon';

const enum ReadFromTextArea {
Type,
Expand Down Expand Up @@ -324,9 +323,13 @@ export class TextAreaHandler extends Disposable {
// ------------- Clipboard operations

private _ensureClipboardGetsEditorSelection(e: IClipboardEvent): void {
let whatToCopy = this._getPlainTextToCopy();
let whatToCopy = this.model.getPlainTextToCopy(this.selections, this.Browser.enableEmptySelectionClipboard);
if (e.canUseTextData()) {
e.setTextData(whatToCopy);
let whatHTMLToCopy = null;
if (!this.Browser.isEdgeOrIE && (whatToCopy.length < 65536 || InternalEditorOptions.forceCopyWithSyntaxHighlighting)) {
whatHTMLToCopy = this.model.getHTMLToCopy(this.selections, this.Browser.enableEmptySelectionClipboard);
}
e.setTextData(whatToCopy, whatHTMLToCopy);
} else {
this.setTextAreaState('copy or cut', this.textAreaState.fromText(whatToCopy), false);
}
Expand All @@ -344,31 +347,4 @@ export class TextAreaHandler extends Disposable {
this.lastCopiedValueIsFromEmptySelection = (selections.length === 1 && selections[0].isEmpty());
}
}

private _getPlainTextToCopy(): string {
let newLineCharacter = this.model.getEOL();
let selections = this.selections;

if (selections.length === 1) {
let range: Range = selections[0];
if (range.isEmpty()) {
if (this.Browser.enableEmptySelectionClipboard) {
let modelLineNumber = this.model.coordinatesConverter.convertViewPositionToModelPosition(new Position(range.startLineNumber, 1)).lineNumber;
return this.model.getModelLineContent(modelLineNumber) + newLineCharacter;
} else {
return '';
}
}

return this.model.getValueInRange(range, EndOfLinePreference.TextDefined);
} else {
selections = selections.slice(0).sort(Range.compareRangesUsingStarts);
let result: string[] = [];
for (let i = 0; i < selections.length; i++) {
result.push(this.model.getValueInRange(selections[i], EndOfLinePreference.TextDefined));
}

return result.join(newLineCharacter);
}
}
}
}
4 changes: 3 additions & 1 deletion src/vs/editor/common/controller/textAreaState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Constants } from 'vs/editor/common/core/uint';

export interface IClipboardEvent {
canUseTextData(): boolean;
setTextData(text: string): void;
setTextData(text: string, richText: string): void;
getTextData(): string;
}

Expand Down Expand Up @@ -56,6 +56,8 @@ export interface ISimpleModel {
getValueInRange(range: Range, eol: EndOfLinePreference): string;
getModelLineContent(lineNumber: number): string;
getLineCount(): number;
getPlainTextToCopy(ranges: Range[], enableEmptySelectionClipboard: boolean): string;
getHTMLToCopy(ranges: Range[], enableEmptySelectionClipboard: boolean): string;

coordinatesConverter: {
convertViewPositionToModelPosition(viewPosition: Position): Position;
Expand Down
4 changes: 4 additions & 0 deletions src/vs/editor/common/editorCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1004,6 +1004,10 @@ export class EditorContribOptions {
* Internal configuration options (transformed or computed) for the editor.
*/
export class InternalEditorOptions {
/**
* @internal
*/
static forceCopyWithSyntaxHighlighting: boolean = false;
readonly _internalEditorOptionsBrand: void;

readonly lineHeight: number; // todo: move to fontInfo
Expand Down
90 changes: 90 additions & 0 deletions src/vs/editor/common/modes/textToHtmlTokenizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,101 @@ import * as strings from 'vs/base/common/strings';
import { IState, ITokenizationSupport, TokenizationRegistry, LanguageId } from 'vs/editor/common/modes';
import { NULL_STATE, nullTokenize2 } from 'vs/editor/common/modes/nullMode';
import { LineTokens } from 'vs/editor/common/core/lineTokens';
import { CharCode } from 'vs/base/common/charCode';
import { ViewLineToken } from 'vs/editor/common/core/viewLineToken';

export function tokenizeToString(text: string, languageId: string): string {
return _tokenizeToString(text, _getSafeTokenizationSupport(languageId));
}

export function tokenizeLineToHTML(text: string, viewLineTokens: ViewLineToken[], rules: { [key: string]: string }, options: { startOffset: number, endOffset: number, tabSize: number, containsRTL: boolean }): string {
let tabSize = options.tabSize;
let result = `<div>`;
let charIndex = options.startOffset;
let tabsCharDelta = 0;

for (let tokenIndex = 0, lenJ = viewLineTokens.length; tokenIndex < lenJ; tokenIndex++) {
const token = viewLineTokens[tokenIndex];
const tokenEndIndex = token.endIndex;

if (token.endIndex < options.startOffset) {
continue;
}

const tokenType = token.type;
let partContentCnt = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since there's no need to build up character mapping, there's also no need to keep track of partContentCnt

let partContent = '';

for (; charIndex < tokenEndIndex && charIndex < options.endOffset; charIndex++) {
const charCode = text.charCodeAt(charIndex);

switch (charCode) {
case CharCode.Tab:
let insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize;
tabsCharDelta += insertSpacesCount - 1;
while (insertSpacesCount > 0) {
partContent += '&nbsp;';
partContentCnt++;
insertSpacesCount--;
}
break;

case CharCode.Space:
partContent += '&nbsp;';
partContentCnt++;
break;

case CharCode.LessThan:
partContent += '&lt;';
partContentCnt++;
break;

case CharCode.GreaterThan:
partContent += '&gt;';
partContentCnt++;
break;

case CharCode.Ampersand:
partContent += '&amp;';
partContentCnt++;
break;

case CharCode.Null:
partContent += '&#00;';
partContentCnt++;
break;

case CharCode.UTF8_BOM:
case CharCode.LINE_SEPARATOR_2028:
partContent += '\ufffd';
partContentCnt++;
break;

case CharCode.CarriageReturn:
// zero width space, because carriage return would introduce a line break
partContent += '&#8203';
partContentCnt++;
break;

default:
partContent += String.fromCharCode(charCode);
partContentCnt++;
}
}

// TODO: adopt new view line tokens.
let style = tokenType.split(' ').map(type => rules[type]).join('');
result += `<span style="${style}">${partContent}</span>`;

if (token.endIndex > options.endOffset) {
break;
}
}

result += `</div>`;
return result;
}

function _getSafeTokenizationSupport(languageId: string): ITokenizationSupport {
let tokenizationSupport = TokenizationRegistry.get(languageId);
if (tokenizationSupport) {
Expand Down
4 changes: 2 additions & 2 deletions src/vs/editor/common/standalone/themes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export const vs: IThemeRule[] = [

/* -------------------------------- Begin vs-dark tokens -------------------------------- */
export const vs_dark: IThemeRule[] = [
{ token: '', foreground: 'D4D4D4' },
{ token: '', foreground: 'D4D4D4', background: '1E1E1E' },
{ token: 'invalid', foreground: 'f44747' },
{ token: 'emphasis', fontStyle: 'italic' },
{ token: 'strong', fontStyle: 'bold' },
Expand Down Expand Up @@ -130,7 +130,7 @@ export const vs_dark: IThemeRule[] = [

/* -------------------------------- Begin hc-black tokens -------------------------------- */
export const hc_black: IThemeRule[] = [
{ token: '', foreground: 'FFFFFF' },
{ token: '', foreground: 'FFFFFF', background: '000000' },
{ token: 'invalid', foreground: 'f44747' },
{ token: 'emphasis', fontStyle: 'italic' },
{ token: 'strong', fontStyle: 'bold' },
Expand Down
3 changes: 3 additions & 0 deletions src/vs/editor/common/viewModel/viewModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ export interface IViewModel extends IEventEmitter {
getModelLineContent(modelLineNumber: number): string;
getModelLineMaxColumn(modelLineNumber: number): number;
validateModelPosition(modelPosition: IPosition): Position;

getPlainTextToCopy(ranges: Range[], enableEmptySelectionClipboard: boolean): string;
getHTMLToCopy(ranges: Range[], enableEmptySelectionClipboard: boolean): string;
}

export class ViewLineRenderingData {
Expand Down
96 changes: 96 additions & 0 deletions src/vs/editor/common/viewModel/viewModelImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
import * as editorCommon from 'vs/editor/common/editorCommon';
import { TokenizationRegistry } from 'vs/editor/common/modes';
import { tokenizeLineToHTML } from 'vs/editor/common/modes/textToHtmlTokenizer';
import { ViewModelCursors } from 'vs/editor/common/viewModel/viewModelCursors';
import { ViewModelDecorations } from 'vs/editor/common/viewModel/viewModelDecorations';
import { ViewLineRenderingData, ViewModelDecoration, IViewModel, ICoordinatesConverter } from 'vs/editor/common/viewModel/viewModel';
Expand Down Expand Up @@ -558,4 +560,98 @@ export class ViewModel extends EventEmitter implements IViewModel {
public validateModelPosition(position: editorCommon.IPosition): Position {
return this.model.validatePosition(position);
}

public getPlainTextToCopy(ranges: Range[], enableEmptySelectionClipboard: boolean): string {
let newLineCharacter = this.getEOL();

if (ranges.length === 1) {
let range: Range = ranges[0];
if (range.isEmpty()) {
if (enableEmptySelectionClipboard) {
let modelLineNumber = this.coordinatesConverter.convertViewPositionToModelPosition(new Position(range.startLineNumber, 1)).lineNumber;
return this.getModelLineContent(modelLineNumber) + newLineCharacter;
} else {
return '';
}
}

return this.getValueInRange(range, editorCommon.EndOfLinePreference.TextDefined);
} else {
ranges = ranges.slice(0).sort(Range.compareRangesUsingStarts);
let result: string[] = [];
for (let i = 0; i < ranges.length; i++) {
result.push(this.getValueInRange(ranges[i], editorCommon.EndOfLinePreference.TextDefined));
}

return result.join(newLineCharacter);
}
}

public getHTMLToCopy(ranges: Range[], enableEmptySelectionClipboard: boolean): string {
// TODO: adopt new view line tokens.
let rules: { [key: string]: string } = {};
let colorMap = TokenizationRegistry.getColorMap();
for (let i = 1, len = colorMap.length; i < len; i++) {
let color = colorMap[i];
if (/^(?:[0-9a-fA-F]{3}){1,2}$/.test(color)) {
color = '#' + color;
}
rules[`mtk${i}`] = `color: ${color};`;
}
rules['mtki'] = 'font-style: italic;';
rules['mtkb'] = 'font-weight: bold;';
rules['mtku'] = 'text-decoration: underline;';

let defaultForegroundColor = /^(?:[0-9a-fA-F]{3}){1,2}$/.test(colorMap[1]) ? '#' + colorMap[1] : colorMap[1];
let defaultBackgroundColor = /^(?:[0-9a-fA-F]{3}){1,2}$/.test(colorMap[2]) ? '#' + colorMap[2] : colorMap[2];

let output = `<div style="color: ${defaultForegroundColor}; background-color: ${defaultBackgroundColor}">`;

if (ranges.length === 1) {
let range: Range = ranges[0];

if (range.isEmpty()) {
if (enableEmptySelectionClipboard) {
let modelLineNumber = this.coordinatesConverter.convertViewPositionToModelPosition(new Position(range.startLineNumber, 1)).lineNumber;
let viewLineStart = new Position(range.startLineNumber, 1);
let viewLineEnd = new Position(range.startLineNumber, this.getLineMaxColumn(range.startLineNumber));
let startOffset = this.coordinatesConverter.convertViewPositionToModelPosition(viewLineStart).column - 1;
let endOffset = this.coordinatesConverter.convertViewPositionToModelPosition(viewLineEnd).column - 1;
let viewLineRenderingData = this.getViewLineRenderingData(new Range(viewLineStart.lineNumber, viewLineStart.column, viewLineEnd.lineNumber, viewLineEnd.column), modelLineNumber);
let html = tokenizeLineToHTML(this.getModelLineContent(modelLineNumber),
viewLineRenderingData.tokens,
rules,
{
startOffset: startOffset,
endOffset: endOffset,
tabSize: this.getTabSize(),
containsRTL: this.model.mightContainRTL()
});
output += `${html}`;
} else {
return '';
}
} else {
for (let i = 0, lineCount = range.endLineNumber - range.startLineNumber; i <= lineCount; i++) {
let viewLineRenderingData = this.getViewLineRenderingData(range, range.startLineNumber + i);
let lineContent = viewLineRenderingData.content;
let startOffset = i === 0 ? range.startColumn - 1 : 0;
let endOffset = i === lineCount ? range.endColumn - 1 : lineContent.length;

let html = tokenizeLineToHTML(lineContent, viewLineRenderingData.tokens, rules,
{
startOffset: startOffset,
endOffset: endOffset,
tabSize: this.getTabSize(),
containsRTL: this.model.mightContainRTL()
});
output += `${html}`;
}
}
}

output += '</div>';

return output;
}
}
41 changes: 41 additions & 0 deletions src/vs/editor/contrib/clipboard/browser/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ function conditionalEditorAction(testCommand: string) {
return editorAction;
}

function conditionalCopyWithSyntaxHighlighting() {
if (browser.isEdgeOrIE || !browser.supportsExecCommand('copy')) {
return () => { };
}

return editorAction;
}

abstract class ExecCommandAction extends EditorAction {

private browserCommand: string;
Expand Down Expand Up @@ -136,3 +144,36 @@ class ExecCommandPasteAction extends ExecCommandAction {
});
}
}

@conditionalCopyWithSyntaxHighlighting()
class ExecCommandCopyWithSyntaxHighlightingAction extends ExecCommandAction {

constructor() {
super('copy', {
id: 'editor.action.clipboardCopyWithSyntaxHighlightingAction',
label: nls.localize('actions.clipboard.copyWithSyntaxHighlightingLabel', "Copy With Syntax Highlighting"),
alias: 'Copy With Syntax Highlighting',
precondition: null,
kbOpts: {
kbExpr: EditorContextKeys.TextFocus,
primary: null
},
menuOpts: {
group: CLIPBOARD_CONTEXT_MENU_GROUP,
order: 2
}
});
}

public run(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor): void {
var enableEmptySelectionClipboard = editor.getConfiguration().contribInfo.emptySelectionClipboard && browser.enableEmptySelectionClipboard;

if (!enableEmptySelectionClipboard && editor.getSelection().isEmpty()) {
return;
}

editorCommon.InternalEditorOptions.forceCopyWithSyntaxHighlighting = true;
super.run(accessor, editor);
editorCommon.InternalEditorOptions.forceCopyWithSyntaxHighlighting = false;
}
}
Loading