-
Notifications
You must be signed in to change notification settings - Fork 30.5k
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
Changes from 5 commits
dd0558f
0a893ff
20ede81
f0fd429
86b7ffa
53ba601
27aaa97
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,11 +8,115 @@ 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 { CharacterMapping } from 'vs/editor/common/viewLayout/viewLineRenderer'; | ||
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 containsRTL = options.containsRTL; | ||
|
||
let result = `<div>`; | ||
const characterMapping = new CharacterMapping(text.length + 1, viewLineTokens.length); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to build up character mapping here |
||
|
||
let charIndex = options.startOffset; | ||
let tabsCharDelta = 0; | ||
let charOffsetInPart = 0; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 charOffsetInPart |
||
|
||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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++) { | ||
characterMapping.setPartData(charIndex, tokenIndex, charOffsetInPart); | ||
const charCode = text.charCodeAt(charIndex); | ||
|
||
switch (charCode) { | ||
case CharCode.Tab: | ||
let insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize; | ||
tabsCharDelta += insertSpacesCount - 1; | ||
charOffsetInPart += insertSpacesCount - 1; | ||
while (insertSpacesCount > 0) { | ||
partContent += ' '; | ||
partContentCnt++; | ||
insertSpacesCount--; | ||
} | ||
break; | ||
|
||
case CharCode.Space: | ||
partContent += ' '; | ||
partContentCnt++; | ||
break; | ||
|
||
case CharCode.LessThan: | ||
partContent += '<'; | ||
partContentCnt++; | ||
break; | ||
|
||
case CharCode.GreaterThan: | ||
partContent += '>'; | ||
partContentCnt++; | ||
break; | ||
|
||
case CharCode.Ampersand: | ||
partContent += '&'; | ||
partContentCnt++; | ||
break; | ||
|
||
case CharCode.Null: | ||
partContent += '�'; | ||
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 += '​'; | ||
partContentCnt++; | ||
break; | ||
|
||
default: | ||
partContent += String.fromCharCode(charCode); | ||
partContentCnt++; | ||
} | ||
|
||
charOffsetInPart++; | ||
} | ||
|
||
characterMapping.setPartLength(tokenIndex, partContentCnt); | ||
let style = tokenType.split(' ').map(type => rules[type]).join(''); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please leave a todo here, we should clean this up once I merge the minimap branch where a view line token holds on to the metadata, it should be straight-forward with those changes to read here the font style and the foreground color directly |
||
if (containsRTL) { | ||
result += `<span dir="ltr" style="${style}">${partContent}</span>`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. perhaps this is overkill. I have a feeling the software where we paste should figure out by itself what to do with RTL characters, so I suggest to ignore RTL here for now. |
||
} else { | ||
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) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
@@ -558,4 +560,97 @@ 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 { | ||
let rules: { [key: string]: string } = {}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please leave a TODO here so we clean this up once the minimap branch gets merged in (same as above) |
||
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; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would use null for richText as a signal that it should not be set (instead of undefined + optional).
IMHO null is a lot more clear and prevents bugs where we forget by accident to add a second argument....