-
Notifications
You must be signed in to change notification settings - Fork 26
/
extension.ts
203 lines (195 loc) · 7.5 KB
/
extension.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
import * as vscode from 'vscode'
import * as Parser from 'web-tree-sitter'
import * as path from 'path'
import * as scopes from './scopes'
import * as colors from './colors'
// Be sure to declare the language in package.json and include a minimalist grammar.
const languages: {[id: string]: {module: string, color: colors.ColorFunction, parser?: Parser}} = {
'go': {module: 'tree-sitter-go', color: colors.colorGo},
'cpp': {module: 'tree-sitter-cpp', color: colors.colorCpp},
'rust': {module: 'tree-sitter-rust', color: colors.colorRust},
'ruby': {module: 'tree-sitter-ruby', color: colors.colorRuby},
'verilog': {module: 'tree-sitter-verilog', color: colors.colorVerilog},
'typescript': {module: 'tree-sitter-typescript', color: colors.colorTypescript},
// TODO there is a separate JS grammar now
'javascript': {module: 'tree-sitter-javascript', color: colors.colorTypescript},
}
// Create decoration types from scopes lazily
const decorationCache = new Map<string, vscode.TextEditorDecorationType>()
function decoration(scope: string): vscode.TextEditorDecorationType|undefined {
// If we've already created a decoration for `scope`, use it
if (decorationCache.has(scope)) {
return decorationCache.get(scope)
}
// If `scope` is defined in the current theme, create a decoration for it
const textmate = scopes.find(scope)
if (textmate) {
const decoration = createDecorationFromTextmate(textmate)
decorationCache.set(scope, decoration)
return decoration
}
// Otherwise, give up, there is no color available for this scope
return undefined
}
function createDecorationFromTextmate(themeStyle: scopes.TextMateRuleSettings): vscode.TextEditorDecorationType {
let options: vscode.DecorationRenderOptions = {}
options.rangeBehavior = vscode.DecorationRangeBehavior.OpenOpen
if (themeStyle.foreground) {
options.color = themeStyle.foreground
}
if (themeStyle.background) {
options.backgroundColor = themeStyle.background
}
if (themeStyle.fontStyle) {
let parts: string[] = themeStyle.fontStyle.split(" ")
parts.forEach((part) => {
switch (part) {
case "italic":
options.fontStyle = "italic"
break
case "bold":
options.fontWeight = "bold"
break
case "underline":
options.textDecoration = "underline"
break
default:
break
}
})
}
return vscode.window.createTextEditorDecorationType(options)
}
// Load styles from the current active theme
async function loadStyles() {
await scopes.load()
// Clear old styles
for (const style of decorationCache.values()) {
style.dispose()
}
decorationCache.clear()
}
// For some reason this crashes if we put it inside activate
const initParser = Parser.init() // TODO this isn't a field, suppress package member coloring like Go
// Called when the extension is first activated by user opening a file with the appropriate language
export async function activate(context: vscode.ExtensionContext) {
console.log("Activating tree-sitter...")
// Parse of all visible documents
const trees: {[uri: string]: Parser.Tree} = {}
async function open(editor: vscode.TextEditor) {
const language = languages[editor.document.languageId]
if (language == null) return
if (language.parser == null) {
const absolute = path.join(context.extensionPath, 'parsers', language.module + '.wasm')
const wasm = path.relative(process.cwd(), absolute)
const lang = await Parser.Language.load(wasm)
const parser = new Parser()
parser.setLanguage(lang)
language.parser = parser
}
const t = language.parser.parse(editor.document.getText()) // TODO don't use getText, use Parser.Input
trees[editor.document.uri.toString()] = t
colorUri(editor.document.uri)
}
// NOTE: if you make this an async function, it seems to cause edit anomalies
function edit(edit: vscode.TextDocumentChangeEvent) {
const language = languages[edit.document.languageId]
if (language == null || language.parser == null) return
updateTree(language.parser, edit)
colorUri(edit.document.uri)
}
function updateTree(parser: Parser, edit: vscode.TextDocumentChangeEvent) {
if (edit.contentChanges.length == 0) return
const old = trees[edit.document.uri.toString()]
for (const e of edit.contentChanges) {
const startIndex = e.rangeOffset
const oldEndIndex = e.rangeOffset + e.rangeLength
const newEndIndex = e.rangeOffset + e.text.length
const startPos = edit.document.positionAt(startIndex)
const oldEndPos = edit.document.positionAt(oldEndIndex)
const newEndPos = edit.document.positionAt(newEndIndex)
const startPosition = asPoint(startPos)
const oldEndPosition = asPoint(oldEndPos)
const newEndPosition = asPoint(newEndPos)
const delta = {startIndex, oldEndIndex, newEndIndex, startPosition, oldEndPosition, newEndPosition}
old.edit(delta)
}
const t = parser.parse(edit.document.getText(), old) // TODO don't use getText, use Parser.Input
trees[edit.document.uri.toString()] = t
}
function asPoint(pos: vscode.Position): Parser.Point {
return {row: pos.line, column: pos.character}
}
function close(doc: vscode.TextDocument) {
delete trees[doc.uri.toString()]
}
function colorUri(uri: vscode.Uri) {
for (const editor of vscode.window.visibleTextEditors) {
if (editor.document.uri == uri) {
colorEditor(editor)
}
}
}
const warnedScopes = new Set<string>()
function colorEditor(editor: vscode.TextEditor) {
const t = trees[editor.document.uri.toString()]
if (t == null) return
const language = languages[editor.document.languageId]
if (language == null) return
const scopes = language.color(t, visibleLines(editor))
for (const scope of scopes.keys()) {
const dec = decoration(scope)
if (dec) {
const ranges = scopes.get(scope)!.map(range)
editor.setDecorations(dec, ranges)
} else if (!warnedScopes.has(scope)) {
console.warn(scope, 'was not found in the current theme')
warnedScopes.add(scope)
}
}
for (const scope of decorationCache.keys()) {
if (!scopes.has(scope)) {
const dec = decorationCache.get(scope)!
editor.setDecorations(dec, [])
}
}
}
async function colorAllOpen() {
for (const editor of vscode.window.visibleTextEditors) {
await open(editor)
}
}
// Load active color theme
async function onChangeConfiguration(event: vscode.ConfigurationChangeEvent) {
let colorizationNeedsReload: boolean = event.affectsConfiguration("workbench.colorTheme")
|| event.affectsConfiguration("editor.tokenColorCustomizations")
if (colorizationNeedsReload) {
await loadStyles()
colorAllOpen()
}
}
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(onChangeConfiguration))
context.subscriptions.push(vscode.window.onDidChangeVisibleTextEditors(colorAllOpen))
context.subscriptions.push(vscode.workspace.onDidChangeTextDocument(edit))
context.subscriptions.push(vscode.workspace.onDidCloseTextDocument(close))
context.subscriptions.push(vscode.window.onDidChangeTextEditorVisibleRanges(change => colorEditor(change.textEditor)))
// Don't wait for the initial color, it takes too long to inspect the themes and causes VSCode extension host to hang
async function activateLazily() {
await loadStyles()
await initParser
colorAllOpen()
}
activateLazily()
}
function visibleLines(editor: vscode.TextEditor) {
return editor.visibleRanges.map(range => {
const start = range.start.line
const end = range.end.line
return {start, end}
})
}
function range(x: colors.Range): vscode.Range {
return new vscode.Range(x.start.row, x.start.column, x.end.row, x.end.column)
}
// this method is called when your extension is deactivated
export function deactivate() {}