2024-06-29 18:10:07 -07:00
|
|
|
import {
|
|
|
|
completeFromList,
|
|
|
|
hasNextSnippetField,
|
|
|
|
pickedCompletion,
|
|
|
|
snippetCompletion,
|
|
|
|
} from '@codemirror/autocomplete'
|
|
|
|
import {
|
|
|
|
Facet,
|
|
|
|
StateEffect,
|
|
|
|
StateField,
|
|
|
|
Extension,
|
|
|
|
Annotation,
|
|
|
|
Transaction,
|
|
|
|
} from '@codemirror/state'
|
|
|
|
import {
|
|
|
|
EditorView,
|
|
|
|
Tooltip,
|
|
|
|
Decoration,
|
|
|
|
DecorationSet,
|
|
|
|
} from '@codemirror/view'
|
|
|
|
import { URI } from 'vscode-uri'
|
2023-09-05 16:02:27 -07:00
|
|
|
import {
|
|
|
|
DiagnosticSeverity,
|
|
|
|
CompletionItemKind,
|
|
|
|
CompletionTriggerKind,
|
|
|
|
} from 'vscode-languageserver-protocol'
|
|
|
|
|
2024-04-15 17:18:32 -07:00
|
|
|
import { deferExecution } from 'lib/utils'
|
2023-09-05 16:02:27 -07:00
|
|
|
import type {
|
|
|
|
Completion,
|
|
|
|
CompletionContext,
|
|
|
|
CompletionResult,
|
|
|
|
} from '@codemirror/autocomplete'
|
|
|
|
import type { PublishDiagnosticsParams } from 'vscode-languageserver-protocol'
|
|
|
|
import type { ViewUpdate, PluginValue } from '@codemirror/view'
|
|
|
|
import type * as LSP from 'vscode-languageserver-protocol'
|
2024-02-19 12:33:16 -08:00
|
|
|
import { LanguageServerClient } from 'editor/plugins/lsp'
|
2023-09-05 16:02:27 -07:00
|
|
|
import { Marked } from '@ts-stack/markdown'
|
2024-02-19 12:33:16 -08:00
|
|
|
import { posToOffset } from 'editor/plugins/lsp/util'
|
2024-04-15 17:18:32 -07:00
|
|
|
import { Program, ProgramMemory } from 'lang/wasm'
|
2024-06-29 18:10:07 -07:00
|
|
|
import { codeManager, editorManager } from 'lib/singletons'
|
2024-04-15 17:18:32 -07:00
|
|
|
import type { UnitLength } from 'wasm-lib/kcl/bindings/UnitLength'
|
|
|
|
import { UpdateUnitsResponse } from 'wasm-lib/kcl/bindings/UpdateUnitsResponse'
|
|
|
|
import { UpdateCanExecuteResponse } from 'wasm-lib/kcl/bindings/UpdateCanExecuteResponse'
|
2024-06-29 18:10:07 -07:00
|
|
|
import { copilotPluginEvent } from './copilot'
|
|
|
|
import { codeManagerUpdateEvent } from 'lang/codeManager'
|
|
|
|
import {
|
|
|
|
modelingMachineEvent,
|
|
|
|
updateOutsideEditorEvent,
|
|
|
|
setDiagnosticsEvent,
|
|
|
|
} from 'editor/manager'
|
|
|
|
import { SemanticToken, getTag } from 'editor/plugins/lsp/semantic_token'
|
|
|
|
import { highlightingFor } from '@codemirror/language'
|
2024-02-19 12:33:16 -08:00
|
|
|
|
|
|
|
const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '')
|
2024-06-29 18:10:07 -07:00
|
|
|
export const docPathFacet = Facet.define<string, string>({
|
|
|
|
combine: useLast,
|
|
|
|
})
|
2024-02-19 12:33:16 -08:00
|
|
|
export const languageId = Facet.define<string, string>({ combine: useLast })
|
|
|
|
export const workspaceFolders = Facet.define<
|
|
|
|
LSP.WorkspaceFolder[],
|
|
|
|
LSP.WorkspaceFolder[]
|
|
|
|
>({ combine: useLast })
|
2023-09-05 16:02:27 -07:00
|
|
|
|
2024-06-29 18:10:07 -07:00
|
|
|
enum LspAnnotation {
|
|
|
|
SemanticTokens = 'semantic-tokens',
|
|
|
|
}
|
|
|
|
|
|
|
|
const lspEvent = Annotation.define<LspAnnotation>()
|
|
|
|
export const lspSemanticTokensEvent = lspEvent.of(LspAnnotation.SemanticTokens)
|
|
|
|
|
2023-09-05 16:02:27 -07:00
|
|
|
const CompletionItemKindMap = Object.fromEntries(
|
|
|
|
Object.entries(CompletionItemKind).map(([key, value]) => [value, key])
|
|
|
|
) as Record<CompletionItemKind, string>
|
|
|
|
|
2024-04-15 17:18:32 -07:00
|
|
|
const changesDelay = 600
|
2024-06-29 18:10:07 -07:00
|
|
|
|
|
|
|
const addToken = StateEffect.define<SemanticToken>({
|
|
|
|
map: (token: SemanticToken, change) => ({
|
|
|
|
...token,
|
|
|
|
from: change.mapPos(token.from),
|
|
|
|
to: change.mapPos(token.to),
|
|
|
|
}),
|
|
|
|
})
|
|
|
|
|
|
|
|
export const semanticTokenField = StateField.define<DecorationSet>({
|
|
|
|
create() {
|
|
|
|
return Decoration.none
|
|
|
|
},
|
|
|
|
update(highlights, tr) {
|
|
|
|
// Nothing can come before this line, this is very important!
|
|
|
|
// It makes sure the highlights are updated correctly for the changes.
|
|
|
|
highlights = highlights.map(tr.changes)
|
|
|
|
|
|
|
|
const isSemanticTokensEvent = tr.annotation(lspSemanticTokensEvent.type)
|
|
|
|
if (!isSemanticTokensEvent) {
|
|
|
|
return highlights
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if any of the changes are addToken
|
|
|
|
const hasAddToken = tr.effects.some((e) => e.is(addToken))
|
|
|
|
if (hasAddToken) {
|
|
|
|
highlights = highlights.update({
|
|
|
|
filter: (from, to) => false,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const e of tr.effects)
|
|
|
|
if (e.is(addToken)) {
|
|
|
|
const tag = getTag(e.value)
|
|
|
|
const className = tag
|
|
|
|
? highlightingFor(tr.startState, [tag])
|
|
|
|
: undefined
|
|
|
|
|
|
|
|
if (e.value.from < e.value.to && tag) {
|
|
|
|
if (className) {
|
|
|
|
highlights = highlights.update({
|
|
|
|
add: [
|
|
|
|
Decoration.mark({ class: className }).range(
|
|
|
|
e.value.from,
|
|
|
|
e.value.to
|
|
|
|
),
|
|
|
|
],
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return highlights
|
|
|
|
},
|
|
|
|
provide: (f) => EditorView.decorations.from(f),
|
|
|
|
})
|
|
|
|
|
|
|
|
export enum TransactionAnnotation {
|
|
|
|
Diagnostics = 'diagnostics',
|
|
|
|
Remote = 'remote',
|
|
|
|
UserSelect = 'user.select',
|
|
|
|
UserInput = 'user.input',
|
|
|
|
UserMove = 'user.move',
|
|
|
|
UserDelete = 'user.delete',
|
|
|
|
UserUndo = 'user.undo',
|
|
|
|
UserRedo = 'user.redo',
|
|
|
|
|
|
|
|
Copoilot = 'copilot',
|
|
|
|
OutsideEditor = 'outsideEditor',
|
|
|
|
CodeManager = 'codeManager',
|
|
|
|
ModelingMachine = 'modelingMachineEvent',
|
|
|
|
LspSemanticTokens = 'lspSemanticTokensEvent',
|
|
|
|
|
|
|
|
PickedCompletion = 'pickedCompletion',
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface TransactionInfo {
|
|
|
|
annotations: TransactionAnnotation[]
|
|
|
|
time: number | null
|
|
|
|
docChanged: boolean
|
|
|
|
addToHistory: boolean
|
|
|
|
inSnippet: boolean
|
|
|
|
}
|
|
|
|
|
|
|
|
export const updateInfo = (update: ViewUpdate): TransactionInfo[] => {
|
|
|
|
let transactionInfos: TransactionInfo[] = []
|
|
|
|
|
|
|
|
for (const tr of update.transactions) {
|
|
|
|
let annotations: TransactionAnnotation[] = []
|
|
|
|
|
|
|
|
if (tr.isUserEvent('select')) {
|
|
|
|
annotations.push(TransactionAnnotation.UserSelect)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tr.isUserEvent('input')) {
|
|
|
|
annotations.push(TransactionAnnotation.UserInput)
|
|
|
|
}
|
|
|
|
if (tr.isUserEvent('delete')) {
|
|
|
|
annotations.push(TransactionAnnotation.UserDelete)
|
|
|
|
}
|
|
|
|
if (tr.isUserEvent('undo')) {
|
|
|
|
annotations.push(TransactionAnnotation.UserUndo)
|
|
|
|
}
|
|
|
|
if (tr.isUserEvent('redo')) {
|
|
|
|
annotations.push(TransactionAnnotation.UserRedo)
|
|
|
|
}
|
|
|
|
if (tr.isUserEvent('move')) {
|
|
|
|
annotations.push(TransactionAnnotation.UserMove)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tr.annotation(pickedCompletion) !== undefined) {
|
|
|
|
annotations.push(TransactionAnnotation.PickedCompletion)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tr.annotation(copilotPluginEvent.type) !== undefined) {
|
|
|
|
annotations.push(TransactionAnnotation.Copoilot)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tr.annotation(updateOutsideEditorEvent.type) !== undefined) {
|
|
|
|
annotations.push(TransactionAnnotation.OutsideEditor)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tr.annotation(codeManagerUpdateEvent.type) !== undefined) {
|
|
|
|
annotations.push(TransactionAnnotation.CodeManager)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tr.annotation(modelingMachineEvent.type) !== undefined) {
|
|
|
|
annotations.push(TransactionAnnotation.ModelingMachine)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tr.annotation(lspSemanticTokensEvent.type) !== undefined) {
|
|
|
|
annotations.push(TransactionAnnotation.LspSemanticTokens)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tr.annotation(setDiagnosticsEvent.type) !== undefined) {
|
|
|
|
annotations.push(TransactionAnnotation.Diagnostics)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tr.annotation(Transaction.remote) !== undefined) {
|
|
|
|
annotations.push(TransactionAnnotation.Remote)
|
|
|
|
}
|
|
|
|
|
|
|
|
transactionInfos.push({
|
|
|
|
annotations,
|
|
|
|
time: tr.annotation(Transaction.time) || null,
|
|
|
|
docChanged: tr.docChanged,
|
|
|
|
addToHistory: tr.annotation(Transaction.addToHistory) || false,
|
|
|
|
inSnippet: hasNextSnippetField(update.state),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return transactionInfos
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface RelevantUpdate {
|
|
|
|
overall: boolean
|
|
|
|
userSelect: boolean
|
|
|
|
time: number | null
|
|
|
|
}
|
|
|
|
|
|
|
|
export const relevantUpdate = (update: ViewUpdate): RelevantUpdate => {
|
|
|
|
const infos = updateInfo(update)
|
|
|
|
// Make sure we are not in a snippet
|
|
|
|
if (infos.some((info) => info.inSnippet)) {
|
|
|
|
return {
|
|
|
|
overall: false,
|
|
|
|
userSelect: false,
|
|
|
|
time: null,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
overall: infos.some(
|
|
|
|
(info) =>
|
|
|
|
info.docChanged ||
|
|
|
|
info.annotations.includes(TransactionAnnotation.UserInput) ||
|
|
|
|
info.annotations.includes(TransactionAnnotation.UserDelete) ||
|
|
|
|
info.annotations.includes(TransactionAnnotation.UserUndo) ||
|
|
|
|
info.annotations.includes(TransactionAnnotation.UserRedo) ||
|
|
|
|
info.annotations.includes(TransactionAnnotation.UserMove)
|
|
|
|
),
|
|
|
|
userSelect: infos.some((info) =>
|
|
|
|
info.annotations.includes(TransactionAnnotation.UserSelect)
|
|
|
|
),
|
|
|
|
time: infos.length ? infos[0].time : null,
|
|
|
|
}
|
|
|
|
}
|
2024-04-15 17:18:32 -07:00
|
|
|
|
2023-09-05 16:02:27 -07:00
|
|
|
export class LanguageServerPlugin implements PluginValue {
|
|
|
|
public client: LanguageServerClient
|
|
|
|
private documentVersion: number
|
2024-04-15 17:18:32 -07:00
|
|
|
private foldingRanges: LSP.FoldingRange[] | null = null
|
2024-06-29 18:10:07 -07:00
|
|
|
|
|
|
|
private previousSemanticTokens: SemanticToken[] = []
|
|
|
|
|
2024-04-15 17:18:32 -07:00
|
|
|
private _defferer = deferExecution((code: string) => {
|
|
|
|
try {
|
2024-04-17 20:18:07 -07:00
|
|
|
// Update the state (not the editor) with the new code.
|
2024-04-15 17:18:32 -07:00
|
|
|
this.client.textDocumentDidChange({
|
|
|
|
textDocument: {
|
2024-06-29 18:10:07 -07:00
|
|
|
uri: this.getDocUri(),
|
2024-04-15 17:18:32 -07:00
|
|
|
version: this.documentVersion++,
|
|
|
|
},
|
|
|
|
contentChanges: [{ text: code }],
|
|
|
|
})
|
2024-04-22 17:21:24 -07:00
|
|
|
|
2024-06-29 18:10:07 -07:00
|
|
|
this.requestSemanticTokens(this.view)
|
2024-04-15 17:18:32 -07:00
|
|
|
} catch (e) {
|
|
|
|
console.error(e)
|
|
|
|
}
|
|
|
|
}, changesDelay)
|
2023-09-05 16:02:27 -07:00
|
|
|
|
2024-02-19 12:33:16 -08:00
|
|
|
constructor(
|
|
|
|
client: LanguageServerClient,
|
|
|
|
private view: EditorView,
|
|
|
|
private allowHTMLContent: boolean
|
|
|
|
) {
|
|
|
|
this.client = client
|
2023-09-05 16:02:27 -07:00
|
|
|
this.documentVersion = 0
|
|
|
|
|
|
|
|
this.client.attachPlugin(this)
|
|
|
|
|
2024-02-11 12:59:00 +11:00
|
|
|
this.initialize({
|
2024-06-29 18:10:07 -07:00
|
|
|
documentText: this.getDocText(),
|
2023-09-05 16:02:27 -07:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-06-29 18:10:07 -07:00
|
|
|
private getDocPath(view = this.view) {
|
|
|
|
return view.state.facet(docPathFacet)
|
|
|
|
}
|
|
|
|
private getDocText(view = this.view) {
|
|
|
|
return view.state.doc.toString()
|
|
|
|
}
|
2024-04-22 17:21:24 -07:00
|
|
|
|
2024-06-29 18:10:07 -07:00
|
|
|
private getDocUri(view = this.view) {
|
|
|
|
return URI.file(this.getDocPath(view)).toString()
|
|
|
|
}
|
|
|
|
|
|
|
|
private getLanguageId(view = this.view) {
|
|
|
|
return view.state.facet(languageId)
|
|
|
|
}
|
|
|
|
|
|
|
|
update(viewUpdate: ViewUpdate) {
|
|
|
|
const isRelevant = relevantUpdate(viewUpdate)
|
|
|
|
if (!isRelevant.overall) {
|
2024-04-22 17:21:24 -07:00
|
|
|
return
|
|
|
|
}
|
2023-09-21 16:13:22 -07:00
|
|
|
|
2024-06-29 18:10:07 -07:00
|
|
|
// If the doc didn't change we can return early.
|
|
|
|
if (!viewUpdate.docChanged) {
|
|
|
|
return
|
|
|
|
}
|
2024-04-22 17:21:24 -07:00
|
|
|
|
2024-02-11 12:59:00 +11:00
|
|
|
this.sendChange({
|
2024-06-29 18:10:07 -07:00
|
|
|
documentText: viewUpdate.state.doc.toString(),
|
2023-09-21 16:13:22 -07:00
|
|
|
})
|
2023-09-05 16:02:27 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
destroy() {
|
|
|
|
this.client.detachPlugin(this)
|
|
|
|
}
|
|
|
|
|
|
|
|
async initialize({ documentText }: { documentText: string }) {
|
|
|
|
if (this.client.initializePromise) {
|
|
|
|
await this.client.initializePromise
|
|
|
|
}
|
2024-06-29 18:10:07 -07:00
|
|
|
|
2023-09-05 16:02:27 -07:00
|
|
|
this.client.textDocumentDidOpen({
|
|
|
|
textDocument: {
|
2024-06-29 18:10:07 -07:00
|
|
|
uri: this.getDocUri(),
|
|
|
|
languageId: this.getLanguageId(),
|
2023-09-05 16:02:27 -07:00
|
|
|
text: documentText,
|
|
|
|
version: this.documentVersion,
|
|
|
|
},
|
|
|
|
})
|
2024-06-29 18:10:07 -07:00
|
|
|
|
|
|
|
this.requestSemanticTokens(this.view)
|
2023-09-05 16:02:27 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
async sendChange({ documentText }: { documentText: string }) {
|
|
|
|
if (!this.client.ready) return
|
2023-09-21 16:13:22 -07:00
|
|
|
|
2024-04-15 17:18:32 -07:00
|
|
|
this._defferer(documentText)
|
2023-09-05 16:02:27 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
requestDiagnostics(view: EditorView) {
|
2024-06-29 18:10:07 -07:00
|
|
|
this.sendChange({ documentText: this.getDocText() })
|
2023-09-05 16:02:27 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
async requestHoverTooltip(
|
|
|
|
view: EditorView,
|
|
|
|
{ line, character }: { line: number; character: number }
|
|
|
|
): Promise<Tooltip | null> {
|
|
|
|
if (
|
|
|
|
!this.client.ready ||
|
|
|
|
!this.client.getServerCapabilities().hoverProvider
|
|
|
|
)
|
|
|
|
return null
|
|
|
|
|
2024-06-29 18:10:07 -07:00
|
|
|
this.sendChange({ documentText: this.getDocText() })
|
2023-09-05 16:02:27 -07:00
|
|
|
const result = await this.client.textDocumentHover({
|
2024-06-29 18:10:07 -07:00
|
|
|
textDocument: { uri: this.getDocUri() },
|
2023-09-05 16:02:27 -07:00
|
|
|
position: { line, character },
|
|
|
|
})
|
|
|
|
if (!result) return null
|
|
|
|
const { contents, range } = result
|
|
|
|
let pos = posToOffset(view.state.doc, { line, character })!
|
|
|
|
let end: number | undefined
|
|
|
|
if (range) {
|
|
|
|
pos = posToOffset(view.state.doc, range.start)!
|
|
|
|
end = posToOffset(view.state.doc, range.end)
|
|
|
|
}
|
|
|
|
if (pos === null) return null
|
|
|
|
const dom = document.createElement('div')
|
|
|
|
dom.classList.add('documentation')
|
2024-06-22 15:50:16 -07:00
|
|
|
dom.classList.add('hover-tooltip')
|
2024-05-02 16:31:33 -07:00
|
|
|
dom.style.zIndex = '99999999'
|
2023-09-05 16:02:27 -07:00
|
|
|
if (this.allowHTMLContent) dom.innerHTML = formatContents(contents)
|
|
|
|
else dom.textContent = formatContents(contents)
|
|
|
|
return { pos, end, create: (view) => ({ dom }), above: true }
|
|
|
|
}
|
|
|
|
|
2024-04-15 17:18:32 -07:00
|
|
|
async getFoldingRanges(): Promise<LSP.FoldingRange[] | null> {
|
|
|
|
if (
|
|
|
|
!this.client.ready ||
|
|
|
|
!this.client.getServerCapabilities().foldingRangeProvider
|
|
|
|
)
|
|
|
|
return null
|
|
|
|
const result = await this.client.textDocumentFoldingRange({
|
2024-06-29 18:10:07 -07:00
|
|
|
textDocument: { uri: this.getDocUri() },
|
2024-04-15 17:18:32 -07:00
|
|
|
})
|
|
|
|
|
|
|
|
return result || null
|
|
|
|
}
|
|
|
|
|
|
|
|
async updateFoldingRanges() {
|
|
|
|
const foldingRanges = await this.getFoldingRanges()
|
|
|
|
if (foldingRanges === null) return
|
|
|
|
// Update the folding ranges.
|
|
|
|
this.foldingRanges = foldingRanges
|
|
|
|
}
|
|
|
|
|
|
|
|
// In the future if codemirrors foldService accepts async folding ranges
|
|
|
|
// then we will not have to store these and we can call getFoldingRanges
|
|
|
|
// here.
|
|
|
|
foldingRange(
|
|
|
|
lineStart: number,
|
|
|
|
lineEnd: number
|
|
|
|
): { from: number; to: number } | null {
|
|
|
|
if (this.foldingRanges === null) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 0; i < this.foldingRanges.length; i++) {
|
|
|
|
const { startLine, endLine } = this.foldingRanges[i]
|
|
|
|
if (startLine === lineEnd) {
|
|
|
|
const range = {
|
|
|
|
// Set the fold start to the end of the first line
|
|
|
|
// With this, the fold will not include the first line
|
|
|
|
from: startLine,
|
|
|
|
to: endLine,
|
|
|
|
}
|
|
|
|
|
|
|
|
return range
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
async updateUnits(units: UnitLength): Promise<UpdateUnitsResponse | null> {
|
|
|
|
if (this.client.name !== 'kcl') return null
|
|
|
|
if (!this.client.ready) return null
|
|
|
|
|
|
|
|
return await this.client.updateUnits({
|
|
|
|
textDocument: {
|
2024-06-29 18:10:07 -07:00
|
|
|
uri: this.getDocUri(),
|
2024-04-15 17:18:32 -07:00
|
|
|
},
|
2024-06-29 18:10:07 -07:00
|
|
|
text: this.getDocText(),
|
2024-04-15 17:18:32 -07:00
|
|
|
units,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
async updateCanExecute(
|
|
|
|
canExecute: boolean
|
|
|
|
): Promise<UpdateCanExecuteResponse | null> {
|
|
|
|
if (this.client.name !== 'kcl') return null
|
|
|
|
if (!this.client.ready) return null
|
|
|
|
|
|
|
|
let response = await this.client.updateCanExecute({
|
|
|
|
canExecute,
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!canExecute && response.isExecuting) {
|
|
|
|
// We want to wait until the server is not busy before we reply to the
|
|
|
|
// caller.
|
|
|
|
while (response.isExecuting) {
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
|
|
response = await this.client.updateCanExecute({
|
|
|
|
canExecute,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return response
|
|
|
|
}
|
|
|
|
|
|
|
|
async requestFormatting() {
|
|
|
|
if (
|
|
|
|
!this.client.ready ||
|
|
|
|
!this.client.getServerCapabilities().documentFormattingProvider
|
|
|
|
)
|
|
|
|
return null
|
|
|
|
|
2024-05-10 16:51:54 -07:00
|
|
|
this.client.textDocumentDidChange({
|
|
|
|
textDocument: {
|
2024-06-29 18:10:07 -07:00
|
|
|
uri: this.getDocUri(),
|
2024-05-10 16:51:54 -07:00
|
|
|
version: this.documentVersion++,
|
|
|
|
},
|
2024-06-29 18:10:07 -07:00
|
|
|
contentChanges: [{ text: this.getDocText() }],
|
2024-04-15 17:18:32 -07:00
|
|
|
})
|
|
|
|
|
|
|
|
const result = await this.client.textDocumentFormatting({
|
2024-06-29 18:10:07 -07:00
|
|
|
textDocument: { uri: this.getDocUri() },
|
2024-04-15 17:18:32 -07:00
|
|
|
options: {
|
|
|
|
tabSize: 2,
|
|
|
|
insertSpaces: true,
|
|
|
|
insertFinalNewline: true,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!result) return null
|
|
|
|
|
|
|
|
for (let i = 0; i < result.length; i++) {
|
2024-06-29 18:10:07 -07:00
|
|
|
const { newText } = result[i]
|
|
|
|
codeManager.updateCodeStateEditor(newText)
|
2024-04-15 17:18:32 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-05 16:02:27 -07:00
|
|
|
async requestCompletion(
|
|
|
|
context: CompletionContext,
|
|
|
|
{ line, character }: { line: number; character: number },
|
|
|
|
{
|
|
|
|
triggerKind,
|
|
|
|
triggerCharacter,
|
|
|
|
}: {
|
|
|
|
triggerKind: CompletionTriggerKind
|
|
|
|
triggerCharacter: string | undefined
|
|
|
|
}
|
|
|
|
): Promise<CompletionResult | null> {
|
|
|
|
if (
|
|
|
|
!this.client.ready ||
|
|
|
|
!this.client.getServerCapabilities().completionProvider
|
|
|
|
)
|
|
|
|
return null
|
|
|
|
|
2024-02-11 12:59:00 +11:00
|
|
|
this.sendChange({
|
2023-09-05 16:02:27 -07:00
|
|
|
documentText: context.state.doc.toString(),
|
|
|
|
})
|
|
|
|
|
|
|
|
const result = await this.client.textDocumentCompletion({
|
2024-06-29 18:10:07 -07:00
|
|
|
textDocument: { uri: this.getDocUri() },
|
2023-09-05 16:02:27 -07:00
|
|
|
position: { line, character },
|
|
|
|
context: {
|
|
|
|
triggerKind,
|
|
|
|
triggerCharacter,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!result) return null
|
|
|
|
|
|
|
|
const items = 'items' in result ? result.items : result
|
|
|
|
|
|
|
|
let options = items.map(
|
|
|
|
({
|
|
|
|
detail,
|
|
|
|
label,
|
|
|
|
labelDetails,
|
|
|
|
kind,
|
|
|
|
textEdit,
|
|
|
|
documentation,
|
|
|
|
deprecated,
|
|
|
|
insertText,
|
|
|
|
insertTextFormat,
|
|
|
|
sortText,
|
|
|
|
filterText,
|
|
|
|
}) => {
|
|
|
|
const completion: Completion & {
|
|
|
|
filterText: string
|
|
|
|
sortText?: string
|
|
|
|
apply: string
|
|
|
|
} = {
|
|
|
|
label,
|
|
|
|
detail: labelDetails ? labelDetails.detail : detail,
|
|
|
|
apply: label,
|
|
|
|
type: kind && CompletionItemKindMap[kind].toLowerCase(),
|
|
|
|
sortText: sortText ?? label,
|
|
|
|
filterText: filterText ?? label,
|
|
|
|
}
|
|
|
|
if (documentation) {
|
2023-09-06 21:27:30 -04:00
|
|
|
completion.info = () => {
|
|
|
|
const htmlString = formatContents(documentation)
|
|
|
|
const htmlNode = document.createElement('div')
|
|
|
|
htmlNode.style.display = 'contents'
|
|
|
|
htmlNode.innerHTML = htmlString
|
|
|
|
return { dom: htmlNode }
|
|
|
|
}
|
2023-09-05 16:02:27 -07:00
|
|
|
}
|
|
|
|
|
2024-04-12 13:28:58 -07:00
|
|
|
if (insertText && insertTextFormat === 2) {
|
|
|
|
return snippetCompletion(insertText, completion)
|
|
|
|
}
|
|
|
|
|
2023-09-05 16:02:27 -07:00
|
|
|
return completion
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
return completeFromList(options)(context)
|
|
|
|
}
|
|
|
|
|
2024-06-29 18:10:07 -07:00
|
|
|
parseSemanticTokens(view: EditorView, data: number[]) {
|
|
|
|
// decode the lsp semantic token types
|
|
|
|
const tokens = []
|
|
|
|
for (let i = 0; i < data.length; i += 5) {
|
|
|
|
tokens.push({
|
|
|
|
deltaLine: data[i],
|
|
|
|
startChar: data[i + 1],
|
|
|
|
length: data[i + 2],
|
|
|
|
tokenType: data[i + 3],
|
|
|
|
modifiers: data[i + 4],
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// convert the tokens into an array of {to, from, type} objects
|
|
|
|
const tokenTypes =
|
|
|
|
this.client.getServerCapabilities().semanticTokensProvider!.legend
|
|
|
|
.tokenTypes
|
|
|
|
const tokenModifiers =
|
|
|
|
this.client.getServerCapabilities().semanticTokensProvider!.legend
|
|
|
|
.tokenModifiers
|
|
|
|
const tokenRanges: any = []
|
|
|
|
let curLine = 0
|
|
|
|
let prevStart = 0
|
|
|
|
for (let i = 0; i < tokens.length; i++) {
|
|
|
|
const token = tokens[i]
|
|
|
|
const tokenType = tokenTypes[token.tokenType]
|
|
|
|
// get a list of modifiers
|
|
|
|
const tokenModifier = []
|
|
|
|
for (let j = 0; j < tokenModifiers.length; j++) {
|
|
|
|
if (token.modifiers & (1 << j)) {
|
|
|
|
tokenModifier.push(tokenModifiers[j])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (token.deltaLine !== 0) prevStart = 0
|
|
|
|
|
|
|
|
const tokenRange = {
|
|
|
|
from: posToOffset(view.state.doc, {
|
|
|
|
line: curLine + token.deltaLine,
|
|
|
|
character: prevStart + token.startChar,
|
|
|
|
})!,
|
|
|
|
to: posToOffset(view.state.doc, {
|
|
|
|
line: curLine + token.deltaLine,
|
|
|
|
character: prevStart + token.startChar + token.length,
|
|
|
|
})!,
|
|
|
|
type: tokenType,
|
|
|
|
modifiers: tokenModifier,
|
|
|
|
}
|
|
|
|
tokenRanges.push(tokenRange)
|
|
|
|
|
|
|
|
curLine += token.deltaLine
|
|
|
|
prevStart += token.startChar
|
|
|
|
}
|
|
|
|
|
|
|
|
// sort by from
|
|
|
|
tokenRanges.sort((a: any, b: any) => a.from - b.from)
|
|
|
|
return tokenRanges
|
|
|
|
}
|
|
|
|
|
|
|
|
async requestSemanticTokens(view: EditorView) {
|
|
|
|
if (
|
|
|
|
!this.client.ready ||
|
|
|
|
!this.client.getServerCapabilities().semanticTokensProvider
|
|
|
|
) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
const result = await this.client.textDocumentSemanticTokensFull({
|
|
|
|
textDocument: { uri: this.getDocUri() },
|
|
|
|
})
|
|
|
|
if (!result) return null
|
|
|
|
|
|
|
|
const { data } = result
|
|
|
|
this.previousSemanticTokens = this.parseSemanticTokens(view, data)
|
|
|
|
|
|
|
|
const effects: StateEffect<SemanticToken | Extension>[] =
|
|
|
|
this.previousSemanticTokens.map((tokenRange: any) =>
|
|
|
|
addToken.of(tokenRange)
|
|
|
|
)
|
|
|
|
|
|
|
|
view.dispatch({
|
|
|
|
effects,
|
|
|
|
|
|
|
|
annotations: [lspSemanticTokensEvent, Transaction.addToHistory.of(false)],
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-04-17 20:18:07 -07:00
|
|
|
async processNotification(notification: LSP.NotificationMessage) {
|
2023-09-05 16:02:27 -07:00
|
|
|
try {
|
|
|
|
switch (notification.method) {
|
|
|
|
case 'textDocument/publishDiagnostics':
|
2024-06-29 18:10:07 -07:00
|
|
|
if (notification === undefined) break
|
|
|
|
if (notification.params === undefined) break
|
|
|
|
if (!notification.params) break
|
|
|
|
const params = notification.params as PublishDiagnosticsParams
|
|
|
|
if (!params) break
|
2024-06-11 19:23:35 -04:00
|
|
|
console.log(
|
|
|
|
'[lsp] [window/publishDiagnostics]',
|
|
|
|
this.client.getName(),
|
2024-06-29 18:10:07 -07:00
|
|
|
params
|
2024-06-11 19:23:35 -04:00
|
|
|
)
|
2024-04-19 14:24:40 -07:00
|
|
|
// this is sometimes slower than our actual typing.
|
2024-06-11 19:23:35 -04:00
|
|
|
this.processDiagnostics(params)
|
2024-02-19 12:33:16 -08:00
|
|
|
break
|
|
|
|
case 'window/logMessage':
|
|
|
|
console.log(
|
|
|
|
'[lsp] [window/logMessage]',
|
|
|
|
this.client.getName(),
|
|
|
|
notification.params
|
|
|
|
)
|
|
|
|
break
|
|
|
|
case 'window/showMessage':
|
|
|
|
console.log(
|
|
|
|
'[lsp] [window/showMessage]',
|
|
|
|
this.client.getName(),
|
|
|
|
notification.params
|
|
|
|
)
|
|
|
|
break
|
2024-04-15 17:18:32 -07:00
|
|
|
case 'kcl/astUpdated':
|
|
|
|
// The server has updated the AST, we should update elsewhere.
|
|
|
|
let updatedAst = notification.params as Program
|
|
|
|
console.log('[lsp]: Updated AST', updatedAst)
|
|
|
|
|
|
|
|
// Update the folding ranges, since the AST has changed.
|
|
|
|
// This is a hack since codemirror does not support async foldService.
|
|
|
|
// When they do we can delete this.
|
|
|
|
this.updateFoldingRanges()
|
|
|
|
break
|
|
|
|
case 'kcl/memoryUpdated':
|
|
|
|
// The server has updated the memory, we should update elsewhere.
|
|
|
|
let updatedMemory = notification.params as ProgramMemory
|
|
|
|
console.log('[lsp]: Updated Memory', updatedMemory)
|
|
|
|
break
|
2023-09-05 16:02:27 -07:00
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error(error)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
processDiagnostics(params: PublishDiagnosticsParams) {
|
2024-06-29 18:10:07 -07:00
|
|
|
if (params.uri !== this.getDocUri()) return
|
2023-09-05 16:02:27 -07:00
|
|
|
|
|
|
|
const diagnostics = params.diagnostics
|
|
|
|
.map(({ range, message, severity }) => ({
|
|
|
|
from: posToOffset(this.view.state.doc, range.start)!,
|
|
|
|
to: posToOffset(this.view.state.doc, range.end)!,
|
|
|
|
severity: (
|
|
|
|
{
|
|
|
|
[DiagnosticSeverity.Error]: 'error',
|
|
|
|
[DiagnosticSeverity.Warning]: 'warning',
|
|
|
|
[DiagnosticSeverity.Information]: 'info',
|
|
|
|
[DiagnosticSeverity.Hint]: 'info',
|
|
|
|
} as const
|
|
|
|
)[severity!],
|
|
|
|
message,
|
|
|
|
}))
|
|
|
|
.filter(
|
|
|
|
({ from, to }) =>
|
|
|
|
from !== null && to !== null && from !== undefined && to !== undefined
|
|
|
|
)
|
|
|
|
.sort((a, b) => {
|
|
|
|
switch (true) {
|
|
|
|
case a.from < b.from:
|
|
|
|
return -1
|
|
|
|
case a.from > b.from:
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
return 0
|
|
|
|
})
|
|
|
|
|
2024-06-29 18:10:07 -07:00
|
|
|
editorManager.addDiagnostics(diagnostics)
|
2023-09-05 16:02:27 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function formatContents(
|
|
|
|
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
|
|
|
|
): string {
|
|
|
|
if (Array.isArray(contents)) {
|
|
|
|
return contents.map((c) => formatContents(c) + '\n\n').join('')
|
|
|
|
} else if (typeof contents === 'string') {
|
|
|
|
return Marked.parse(contents)
|
|
|
|
} else {
|
|
|
|
return Marked.parse(contents.value)
|
|
|
|
}
|
|
|
|
}
|