diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts new file mode 100644 index 00000000000..99cac3379e4 --- /dev/null +++ b/editors/code/src/commands.ts @@ -0,0 +1,17 @@ +import * as applySourceChange from './commands/apply_source_change'; +import * as extendSelection from './commands/extend_selection'; +import * as joinLines from './commands/join_lines'; +import * as matchingBrace from './commands/matching_brace'; +import * as parentModule from './commands/parent_module'; +import * as runnables from './commands/runnables'; +import * as syntaxTree from './commands/syntaxTree'; + +export { + applySourceChange, + extendSelection, + joinLines, + matchingBrace, + parentModule, + runnables, + syntaxTree +} diff --git a/editors/code/src/commands/apply_source_change.ts b/editors/code/src/commands/apply_source_change.ts new file mode 100644 index 00000000000..dcbbb2b0984 --- /dev/null +++ b/editors/code/src/commands/apply_source_change.ts @@ -0,0 +1,58 @@ +import * as vscode from 'vscode'; +import * as lc from 'vscode-languageclient' + +import { Server } from '../server'; + +interface FileSystemEdit { + type: string; + uri?: string; + src?: string; + dst?: string; +} + +export interface SourceChange { + label: string, + sourceFileEdits: lc.TextDocumentEdit[], + fileSystemEdits: FileSystemEdit[], + cursorPosition?: lc.TextDocumentPositionParams, +} + +export async function handle(change: SourceChange) { + console.log(`applySOurceChange ${JSON.stringify(change)}`) + let wsEdit = new vscode.WorkspaceEdit() + for (let sourceEdit of change.sourceFileEdits) { + let uri = Server.client.protocol2CodeConverter.asUri(sourceEdit.textDocument.uri) + let edits = Server.client.protocol2CodeConverter.asTextEdits(sourceEdit.edits) + wsEdit.set(uri, edits) + } + let created; + let moved; + for (let fsEdit of change.fileSystemEdits) { + if (fsEdit.type == "createFile") { + let uri = vscode.Uri.parse(fsEdit.uri!) + wsEdit.createFile(uri) + created = uri + } else if (fsEdit.type == "moveFile") { + let src = vscode.Uri.parse(fsEdit.src!) + let dst = vscode.Uri.parse(fsEdit.dst!) + wsEdit.renameFile(src, dst) + moved = dst + } else { + console.error(`unknown op: ${JSON.stringify(fsEdit)}`) + } + } + let toOpen = created || moved + let toReveal = change.cursorPosition + await vscode.workspace.applyEdit(wsEdit) + if (toOpen) { + let doc = await vscode.workspace.openTextDocument(toOpen) + await vscode.window.showTextDocument(doc) + } else if (toReveal) { + let uri = Server.client.protocol2CodeConverter.asUri(toReveal.textDocument.uri) + let position = Server.client.protocol2CodeConverter.asPosition(toReveal.position) + let editor = vscode.window.activeTextEditor; + if (!editor || editor.document.uri.toString() != uri.toString()) return + if (!editor.selection.isEmpty) return + editor!.selection = new vscode.Selection(position, position) + } +} diff --git a/editors/code/src/commands/extend_selection.ts b/editors/code/src/commands/extend_selection.ts new file mode 100644 index 00000000000..b90828ba9ba --- /dev/null +++ b/editors/code/src/commands/extend_selection.ts @@ -0,0 +1,29 @@ +import * as vscode from 'vscode'; + +import { TextDocumentIdentifier, Range } from "vscode-languageclient"; +import { Server } from '../server'; + +interface ExtendSelectionParams { + textDocument: TextDocumentIdentifier; + selections: Range[]; +} + +interface ExtendSelectionResult { + selections: Range[]; +} + +export async function handle() { + let editor = vscode.window.activeTextEditor + if (editor == null || editor.document.languageId != "rust") return + let request: ExtendSelectionParams = { + textDocument: { uri: editor.document.uri.toString() }, + selections: editor.selections.map((s) => { + return Server.client.code2ProtocolConverter.asRange(s) + }) + } + let response = await Server.client.sendRequest("m/extendSelection", request) + editor.selections = response.selections.map((range: Range) => { + let r = Server.client.protocol2CodeConverter.asRange(range) + return new vscode.Selection(r.start, r.end) + }) +} diff --git a/editors/code/src/commands/join_lines.ts b/editors/code/src/commands/join_lines.ts new file mode 100644 index 00000000000..7ae7b9d7601 --- /dev/null +++ b/editors/code/src/commands/join_lines.ts @@ -0,0 +1,21 @@ +import * as vscode from 'vscode'; + +import { TextDocumentIdentifier, Range } from "vscode-languageclient"; +import { Server } from '../server'; +import { handle as applySourceChange, SourceChange } from './apply_source_change'; + +interface JoinLinesParams { + textDocument: TextDocumentIdentifier; + range: Range; +} + +export async function handle() { + let editor = vscode.window.activeTextEditor + if (editor == null || editor.document.languageId != "rust") return + let request: JoinLinesParams = { + textDocument: { uri: editor.document.uri.toString() }, + range: Server.client.code2ProtocolConverter.asRange(editor.selection), + } + let change = await Server.client.sendRequest("m/joinLines", request) + await applySourceChange(change) +} diff --git a/editors/code/src/commands/matching_brace.ts b/editors/code/src/commands/matching_brace.ts new file mode 100644 index 00000000000..572c15ce830 --- /dev/null +++ b/editors/code/src/commands/matching_brace.ts @@ -0,0 +1,27 @@ +import * as vscode from 'vscode'; + +import { TextDocumentIdentifier, Position } from "vscode-languageclient"; +import { Server } from '../server'; + +interface FindMatchingBraceParams { + textDocument: TextDocumentIdentifier; + offsets: Position[]; +} + +export async function handle() { + let editor = vscode.window.activeTextEditor + if (editor == null || editor.document.languageId != "rust") return + let request: FindMatchingBraceParams = { + textDocument: { uri: editor.document.uri.toString() }, + offsets: editor.selections.map((s) => { + return Server.client.code2ProtocolConverter.asPosition(s.active) + }) + } + let response = await Server.client.sendRequest("m/findMatchingBrace", request) + editor.selections = editor.selections.map((sel, idx) => { + let active = Server.client.protocol2CodeConverter.asPosition(response[idx]) + let anchor = sel.isEmpty ? active : sel.anchor + return new vscode.Selection(anchor, active) + }) + editor.revealRange(editor.selection) +}; diff --git a/editors/code/src/commands/parent_module.ts b/editors/code/src/commands/parent_module.ts new file mode 100644 index 00000000000..dae60bfb4a3 --- /dev/null +++ b/editors/code/src/commands/parent_module.ts @@ -0,0 +1,22 @@ +import * as vscode from 'vscode'; + +import { TextDocumentIdentifier, Location } from "vscode-languageclient"; +import { Server } from '../server'; + +export async function handle() { + let editor = vscode.window.activeTextEditor + if (editor == null || editor.document.languageId != "rust") return + let request: TextDocumentIdentifier = { + uri: editor.document.uri.toString() + } + let response = await Server.client.sendRequest("m/parentModule", request) + let loc = response[0] + if (loc == null) return + let uri = Server.client.protocol2CodeConverter.asUri(loc.uri) + let range = Server.client.protocol2CodeConverter.asRange(loc.range) + + let doc = await vscode.workspace.openTextDocument(uri) + let e = await vscode.window.showTextDocument(doc) + e.selection = new vscode.Selection(range.start, range.start) + e.revealRange(range, vscode.TextEditorRevealType.InCenter) +} diff --git a/editors/code/src/commands/runnables.ts b/editors/code/src/commands/runnables.ts new file mode 100644 index 00000000000..45c16497dbe --- /dev/null +++ b/editors/code/src/commands/runnables.ts @@ -0,0 +1,88 @@ +import * as vscode from 'vscode'; +import * as lc from 'vscode-languageclient' +import { Server } from '../server'; + +interface RunnablesParams { + textDocument: lc.TextDocumentIdentifier, + position?: lc.Position, +} + +interface Runnable { + range: lc.Range; + label: string; + bin: string; + args: string[]; + env: { [index: string]: string }, +} + +class RunnableQuickPick implements vscode.QuickPickItem { + label: string; + description?: string | undefined; + detail?: string | undefined; + picked?: boolean | undefined; + + constructor(public runnable: Runnable) { + this.label = runnable.label + } +} + +interface CargoTaskDefinition extends vscode.TaskDefinition { + type: 'cargo'; + label: string; + command: string; + args: Array; + env?: { [key: string]: string }; +} + +function createTask(spec: Runnable): vscode.Task { + const TASK_SOURCE = 'Rust'; + let definition: CargoTaskDefinition = { + type: 'cargo', + label: 'cargo', + command: spec.bin, + args: spec.args, + env: spec.env + } + + let execCmd = `${definition.command} ${definition.args.join(' ')}`; + let execOption: vscode.ShellExecutionOptions = { + cwd: '.', + env: definition.env, + }; + let exec = new vscode.ShellExecution(`clear; ${execCmd}`, execOption); + + let f = vscode.workspace.workspaceFolders![0] + let t = new vscode.Task(definition, f, definition.label, TASK_SOURCE, exec, ['$rustc']); + return t; +} + +let prevRunnable: RunnableQuickPick | undefined = undefined +export async function handle() { + let editor = vscode.window.activeTextEditor + if (editor == null || editor.document.languageId != "rust") return + let textDocument: lc.TextDocumentIdentifier = { + uri: editor.document.uri.toString() + } + let params: RunnablesParams = { + textDocument, + position: Server.client.code2ProtocolConverter.asPosition(editor.selection.active) + } + let runnables = await Server.client.sendRequest('m/runnables', params) + let items: RunnableQuickPick[] = [] + if (prevRunnable) { + items.push(prevRunnable) + } + for (let r of runnables) { + if (prevRunnable && JSON.stringify(prevRunnable.runnable) == JSON.stringify(r)) { + continue + } + items.push(new RunnableQuickPick(r)) + } + let item = await vscode.window.showQuickPick(items) + if (item) { + item.detail = "rerun" + prevRunnable = item + let task = createTask(item.runnable) + return await vscode.tasks.executeTask(task) + } +} diff --git a/editors/code/src/commands/syntaxTree.ts b/editors/code/src/commands/syntaxTree.ts new file mode 100644 index 00000000000..d5daa9302c6 --- /dev/null +++ b/editors/code/src/commands/syntaxTree.ts @@ -0,0 +1,38 @@ +import * as vscode from 'vscode'; +import { TextDocumentIdentifier } from 'vscode-languageclient'; + +import { Server } from '../server'; + +export const syntaxTreeUri = vscode.Uri.parse('ra-lsp://syntaxtree'); + +export class TextDocumentContentProvider implements vscode.TextDocumentContentProvider { + public eventEmitter = new vscode.EventEmitter() + public syntaxTree: string = "Not available" + + public provideTextDocumentContent(uri: vscode.Uri): vscode.ProviderResult { + let editor = vscode.window.activeTextEditor; + if (editor == null) return "" + let request: SyntaxTreeParams = { + textDocument: { uri: editor.document.uri.toString() } + }; + return Server.client.sendRequest("m/syntaxTree", request); + } + + get onDidChange(): vscode.Event { + return this.eventEmitter.event + } +} + +interface SyntaxTreeParams { + textDocument: TextDocumentIdentifier; +} + +type SyntaxTreeResult = string; + +// Opens the virtual file that will show the syntax tree +// +// The contents of the file come from the `TextDocumentContentProvider` +export async function handle() { + let document = await vscode.workspace.openTextDocument(syntaxTreeUri) + return vscode.window.showTextDocument(document, vscode.ViewColumn.Two, true) +} diff --git a/editors/code/src/events.ts b/editors/code/src/events.ts new file mode 100644 index 00000000000..b143bb2565c --- /dev/null +++ b/editors/code/src/events.ts @@ -0,0 +1,7 @@ +import * as changeActiveTextEditor from './events/change_active_text_editor' +import * as changeTextDocument from './events/change_text_document'; + +export { + changeActiveTextEditor, + changeTextDocument +} \ No newline at end of file diff --git a/editors/code/src/events/change_active_text_editor.ts b/editors/code/src/events/change_active_text_editor.ts new file mode 100644 index 00000000000..bbdd5309869 --- /dev/null +++ b/editors/code/src/events/change_active_text_editor.ts @@ -0,0 +1,14 @@ +import { TextEditor } from "vscode"; +import { TextDocumentIdentifier } from "vscode-languageclient"; + +import { Server } from "../server"; +import { Decoration } from "../highlighting"; + +export async function handle(editor: TextEditor | undefined) { + if (!Server.config.highlightingOn || !editor || editor.document.languageId != 'rust') return + let params: TextDocumentIdentifier = { + uri: editor.document.uri.toString() + } + let decorations = await Server.client.sendRequest("m/decorationsRequest", params) + Server.highlighter.setHighlights(editor, decorations) +} \ No newline at end of file diff --git a/editors/code/src/events/change_text_document.ts b/editors/code/src/events/change_text_document.ts new file mode 100644 index 00000000000..83ee6c9ee99 --- /dev/null +++ b/editors/code/src/events/change_text_document.ts @@ -0,0 +1,19 @@ +import * as vscode from 'vscode'; + +import { syntaxTreeUri, TextDocumentContentProvider } from '../commands/syntaxTree'; + +export function createHandler(textDocumentContentProvider: TextDocumentContentProvider) { + return (event: vscode.TextDocumentChangeEvent) => { + let doc = event.document + if (doc.languageId != "rust") return + afterLs(() => { + textDocumentContentProvider.eventEmitter.fire(syntaxTreeUri); + }) + } +} + +// We need to order this after LS updates, but there's no API for that. +// Hence, good old setTimeout. +function afterLs(f: () => any) { + setTimeout(f, 10) +} diff --git a/editors/code/src/extension.ts b/editors/code/src/extension.ts index fde6a480d85..595fb98fe3f 100644 --- a/editors/code/src/extension.ts +++ b/editors/code/src/extension.ts @@ -1,434 +1,45 @@ -'use strict'; import * as vscode from 'vscode'; -import * as lc from 'vscode-languageclient' -let client: lc.LanguageClient; - -let uris = { - syntaxTree: vscode.Uri.parse('ra-lsp://syntaxtree') -} - -let highlightingOn = true; +import * as commands from './commands' +import * as events from './events' +import { Server } from './server'; +import { TextDocumentContentProvider } from './commands/syntaxTree'; export function activate(context: vscode.ExtensionContext) { - let applyHighlightingOn = () => { - let config = vscode.workspace.getConfiguration('ra-lsp'); - if (config.has('highlightingOn')) { - highlightingOn = config.get('highlightingOn') as boolean; - }; - - if (!highlightingOn) { - removeHighlights(); - } - }; - - // Apply the highlightingOn config now and whenever the config changes - applyHighlightingOn(); - vscode.workspace.onDidChangeConfiguration(_ => { - applyHighlightingOn(); - }); - - let textDocumentContentProvider = new TextDocumentContentProvider() - let dispose = (disposable: vscode.Disposable) => { + function disposeOnDeactivation(disposable: vscode.Disposable) { context.subscriptions.push(disposable); } - let registerCommand = (name: string, f: any) => { - dispose(vscode.commands.registerCommand(name, f)) + + function registerCommand(name: string, f: any) { + disposeOnDeactivation(vscode.commands.registerCommand(name, f)) } - registerCommand('ra-lsp.syntaxTree', () => openDoc(uris.syntaxTree)) - registerCommand('ra-lsp.extendSelection', async () => { - let editor = vscode.window.activeTextEditor - if (editor == null || editor.document.languageId != "rust") return - let request: ExtendSelectionParams = { - textDocument: { uri: editor.document.uri.toString() }, - selections: editor.selections.map((s) => { - return client.code2ProtocolConverter.asRange(s) - }) - } - let response = await client.sendRequest("m/extendSelection", request) - editor.selections = response.selections.map((range) => { - let r = client.protocol2CodeConverter.asRange(range) - return new vscode.Selection(r.start, r.end) - }) - }) - registerCommand('ra-lsp.matchingBrace', async () => { - let editor = vscode.window.activeTextEditor - if (editor == null || editor.document.languageId != "rust") return - let request: FindMatchingBraceParams = { - textDocument: { uri: editor.document.uri.toString() }, - offsets: editor.selections.map((s) => { - return client.code2ProtocolConverter.asPosition(s.active) - }) - } - let response = await client.sendRequest("m/findMatchingBrace", request) - editor.selections = editor.selections.map((sel, idx) => { - let active = client.protocol2CodeConverter.asPosition(response[idx]) - let anchor = sel.isEmpty ? active : sel.anchor - return new vscode.Selection(anchor, active) - }) - editor.revealRange(editor.selection) - }) - registerCommand('ra-lsp.joinLines', async () => { - let editor = vscode.window.activeTextEditor - if (editor == null || editor.document.languageId != "rust") return - let request: JoinLinesParams = { - textDocument: { uri: editor.document.uri.toString() }, - range: client.code2ProtocolConverter.asRange(editor.selection), - } - let change = await client.sendRequest("m/joinLines", request) - await applySourceChange(change) - }) - registerCommand('ra-lsp.parentModule', async () => { - let editor = vscode.window.activeTextEditor - if (editor == null || editor.document.languageId != "rust") return - let request: lc.TextDocumentIdentifier = { - uri: editor.document.uri.toString() - } - let response = await client.sendRequest("m/parentModule", request) - let loc = response[0] - if (loc == null) return - let uri = client.protocol2CodeConverter.asUri(loc.uri) - let range = client.protocol2CodeConverter.asRange(loc.range) + registerCommand('ra-lsp.syntaxTree', commands.syntaxTree.handle) + registerCommand('ra-lsp.extendSelection', commands.extendSelection.handle); + registerCommand('ra-lsp.matchingBrace', commands.matchingBrace.handle); + registerCommand('ra-lsp.joinLines', commands.joinLines.handle); + registerCommand('ra-lsp.parentModule', commands.parentModule.handle); + registerCommand('ra-lsp.run', commands.runnables.handle); + registerCommand('ra-lsp.applySourceChange', commands.applySourceChange.handle); - let doc = await vscode.workspace.openTextDocument(uri) - let e = await vscode.window.showTextDocument(doc) - e.selection = new vscode.Selection(range.start, range.start) - e.revealRange(range, vscode.TextEditorRevealType.InCenter) - }) - - let prevRunnable: RunnableQuickPick | undefined = undefined - registerCommand('ra-lsp.run', async () => { - let editor = vscode.window.activeTextEditor - if (editor == null || editor.document.languageId != "rust") return - let textDocument: lc.TextDocumentIdentifier = { - uri: editor.document.uri.toString() - } - let params: RunnablesParams = { - textDocument, - position: client.code2ProtocolConverter.asPosition(editor.selection.active) - } - let runnables = await client.sendRequest('m/runnables', params) - let items: RunnableQuickPick[] = [] - if (prevRunnable) { - items.push(prevRunnable) - } - for (let r of runnables) { - if (prevRunnable && JSON.stringify(prevRunnable.runnable) == JSON.stringify(r)) { - continue - } - items.push(new RunnableQuickPick(r)) - } - let item = await vscode.window.showQuickPick(items) - if (item) { - item.detail = "rerun" - prevRunnable = item - let task = createTask(item.runnable) - return await vscode.tasks.executeTask(task) - } - }) - registerCommand('ra-lsp.applySourceChange', applySourceChange) - - dispose(vscode.workspace.registerTextDocumentContentProvider( + let textDocumentContentProvider = new TextDocumentContentProvider() + disposeOnDeactivation(vscode.workspace.registerTextDocumentContentProvider( 'ra-lsp', textDocumentContentProvider )) - startServer() - vscode.workspace.onDidChangeTextDocument((event: vscode.TextDocumentChangeEvent) => { - let doc = event.document - if (doc.languageId != "rust") return - afterLs(() => { - textDocumentContentProvider.eventEmitter.fire(uris.syntaxTree) - }) - }, null, context.subscriptions) - vscode.window.onDidChangeActiveTextEditor(async (editor) => { - if (!highlightingOn || !editor || editor.document.languageId != 'rust') return - let params: lc.TextDocumentIdentifier = { - uri: editor.document.uri.toString() - } - let decorations = await client.sendRequest("m/decorationsRequest", params) - setHighlights(editor, decorations) - }) -} -// We need to order this after LS updates, but there's no API for that. -// Hence, good old setTimeout. -function afterLs(f: () => any) { - setTimeout(f, 10) + Server.start() + + vscode.workspace.onDidChangeTextDocument( + events.changeTextDocument.createHandler(textDocumentContentProvider), + null, + context.subscriptions) + vscode.window.onDidChangeActiveTextEditor(events.changeActiveTextEditor.handle) } export function deactivate(): Thenable { - if (!client) { + if (!Server.client) { return Promise.resolve(); } - return client.stop(); -} - -function startServer() { - let run: lc.Executable = { - command: "ra_lsp_server", - options: { cwd: "." } - } - let serverOptions: lc.ServerOptions = { - run, - debug: run - }; - - let clientOptions: lc.LanguageClientOptions = { - documentSelector: [{ scheme: 'file', language: 'rust' }], - }; - - client = new lc.LanguageClient( - 'ra-lsp', - 'rust-analyzer languge server', - serverOptions, - clientOptions, - ); - client.onReady().then(() => { - client.onNotification( - "m/publishDecorations", - (params: PublishDecorationsParams) => { - let editor = vscode.window.visibleTextEditors.find( - (editor) => editor.document.uri.toString() == params.uri - ) - if (!highlightingOn || !editor) return; - setHighlights( - editor, - params.decorations, - ) - } - ) - }) - client.start(); -} - -async function openDoc(uri: vscode.Uri) { - let document = await vscode.workspace.openTextDocument(uri) - return vscode.window.showTextDocument(document, vscode.ViewColumn.Two, true) -} - -class TextDocumentContentProvider implements vscode.TextDocumentContentProvider { - public eventEmitter = new vscode.EventEmitter() - public syntaxTree: string = "Not available" - - public provideTextDocumentContent(uri: vscode.Uri): vscode.ProviderResult { - let editor = vscode.window.activeTextEditor; - if (editor == null) return "" - let request: SyntaxTreeParams = { - textDocument: { uri: editor.document.uri.toString() } - }; - return client.sendRequest("m/syntaxTree", request); - } - - get onDidChange(): vscode.Event { - return this.eventEmitter.event - } -} - -let decorations: { [index: string]: vscode.TextEditorDecorationType } = {}; - -function initDecorations() { - const decor = (obj: any) => vscode.window.createTextEditorDecorationType({ color: obj }) - decorations = { - background: decor("#3F3F3F"), - error: vscode.window.createTextEditorDecorationType({ - borderColor: "red", - borderStyle: "none none dashed none", - }), - comment: decor("#7F9F7F"), - string: decor("#CC9393"), - keyword: decor("#F0DFAF"), - function: decor("#93E0E3"), - parameter: decor("#94BFF3"), - builtin: decor("#DD6718"), - text: decor("#DCDCCC"), - attribute: decor("#BFEBBF"), - literal: decor("#DFAF8F"), - } -} - -function removeHighlights() { - for (let tag in decorations) { - decorations[tag].dispose(); - } - - decorations = {}; -} - -function setHighlights( - editor: vscode.TextEditor, - highlights: Array -) { - // Initialize decorations if necessary - // - // Note: decoration objects need to be kept around so we can dispose them - // if the user disables syntax highlighting - if (Object.keys(decorations).length === 0) { - initDecorations(); - } - - let byTag: Map = new Map() - for (let tag in decorations) { - byTag.set(tag, []) - } - - for (let d of highlights) { - if (!byTag.get(d.tag)) { - console.log(`unknown tag ${d.tag}`) - continue - } - byTag.get(d.tag)!.push( - client.protocol2CodeConverter.asRange(d.range) - ) - } - - for (let tag of byTag.keys()) { - let dec: vscode.TextEditorDecorationType = decorations[tag] - let ranges = byTag.get(tag)! - editor.setDecorations(dec, ranges) - } -} - -interface SyntaxTreeParams { - textDocument: lc.TextDocumentIdentifier; -} - -type SyntaxTreeResult = string - -interface ExtendSelectionParams { - textDocument: lc.TextDocumentIdentifier; - selections: lc.Range[]; -} - -interface ExtendSelectionResult { - selections: lc.Range[]; -} - -interface FindMatchingBraceParams { - textDocument: lc.TextDocumentIdentifier; - offsets: lc.Position[]; -} - -interface JoinLinesParams { - textDocument: lc.TextDocumentIdentifier; - range: lc.Range; -} - -interface PublishDecorationsParams { - uri: string, - decorations: Decoration[], -} - -interface RunnablesParams { - textDocument: lc.TextDocumentIdentifier, - position?: lc.Position, -} - -interface Runnable { - range: lc.Range; - label: string; - bin: string; - args: string[]; - env: { [index: string]: string }, -} - -class RunnableQuickPick implements vscode.QuickPickItem { - label: string; - description?: string | undefined; - detail?: string | undefined; - picked?: boolean | undefined; - - constructor(public runnable: Runnable) { - this.label = runnable.label - } -} - -interface Decoration { - range: lc.Range, - tag: string, -} - - -interface CargoTaskDefinition extends vscode.TaskDefinition { - type: 'cargo'; - label: string; - command: string; - args: Array; - env?: { [key: string]: string }; -} - -function createTask(spec: Runnable): vscode.Task { - const TASK_SOURCE = 'Rust'; - let definition: CargoTaskDefinition = { - type: 'cargo', - label: 'cargo', - command: spec.bin, - args: spec.args, - env: spec.env - } - - let execCmd = `${definition.command} ${definition.args.join(' ')}`; - let execOption: vscode.ShellExecutionOptions = { - cwd: '.', - env: definition.env, - }; - let exec = new vscode.ShellExecution(`clear; ${execCmd}`, execOption); - - let f = vscode.workspace.workspaceFolders![0] - let t = new vscode.Task(definition, f, definition.label, TASK_SOURCE, exec, ['$rustc']); - return t; -} - -interface FileSystemEdit { - type: string; - uri?: string; - src?: string; - dst?: string; -} - -interface SourceChange { - label: string, - sourceFileEdits: lc.TextDocumentEdit[], - fileSystemEdits: FileSystemEdit[], - cursorPosition?: lc.TextDocumentPositionParams, -} - -async function applySourceChange(change: SourceChange) { - console.log(`applySOurceChange ${JSON.stringify(change)}`) - let wsEdit = new vscode.WorkspaceEdit() - for (let sourceEdit of change.sourceFileEdits) { - let uri = client.protocol2CodeConverter.asUri(sourceEdit.textDocument.uri) - let edits = client.protocol2CodeConverter.asTextEdits(sourceEdit.edits) - wsEdit.set(uri, edits) - } - let created; - let moved; - for (let fsEdit of change.fileSystemEdits) { - if (fsEdit.type == "createFile") { - let uri = vscode.Uri.parse(fsEdit.uri!) - wsEdit.createFile(uri) - created = uri - } else if (fsEdit.type == "moveFile") { - let src = vscode.Uri.parse(fsEdit.src!) - let dst = vscode.Uri.parse(fsEdit.dst!) - wsEdit.renameFile(src, dst) - moved = dst - } else { - console.error(`unknown op: ${JSON.stringify(fsEdit)}`) - } - } - let toOpen = created || moved - let toReveal = change.cursorPosition - await vscode.workspace.applyEdit(wsEdit) - if (toOpen) { - let doc = await vscode.workspace.openTextDocument(toOpen) - await vscode.window.showTextDocument(doc) - } else if (toReveal) { - let uri = client.protocol2CodeConverter.asUri(toReveal.textDocument.uri) - let position = client.protocol2CodeConverter.asPosition(toReveal.position) - let editor = vscode.window.activeTextEditor; - if (!editor || editor.document.uri.toString() != uri.toString()) return - if (!editor.selection.isEmpty) return - editor!.selection = new vscode.Selection(position, position) - } + return Server.client.stop(); } diff --git a/editors/code/src/highlighting.ts b/editors/code/src/highlighting.ts new file mode 100644 index 00000000000..169ddb0df08 --- /dev/null +++ b/editors/code/src/highlighting.ts @@ -0,0 +1,78 @@ +import * as vscode from 'vscode'; +import * as lc from 'vscode-languageclient' + +import { Server } from './server'; + +export interface Decoration { + range: lc.Range, + tag: string, +} + +export class Highlighter { + private decorations: { [index: string]: vscode.TextEditorDecorationType }; + constructor() { + this.decorations = {}; + } + + removeHighlights() { + for (let tag in this.decorations) { + this.decorations[tag].dispose(); + } + + this.decorations = {}; + } + + setHighlights( + editor: vscode.TextEditor, + highlights: Array + ) { + // Initialize decorations if necessary + // + // Note: decoration objects need to be kept around so we can dispose them + // if the user disables syntax highlighting + if (Object.keys(this.decorations).length === 0) { + this.initDecorations(); + } + + let byTag: Map = new Map() + for (let tag in this.decorations) { + byTag.set(tag, []) + } + + for (let d of highlights) { + if (!byTag.get(d.tag)) { + console.log(`unknown tag ${d.tag}`) + continue + } + byTag.get(d.tag)!.push( + Server.client.protocol2CodeConverter.asRange(d.range) + ) + } + + for (let tag of byTag.keys()) { + let dec: vscode.TextEditorDecorationType = this.decorations[tag] + let ranges = byTag.get(tag)! + editor.setDecorations(dec, ranges) + } + } + + private initDecorations() { + const decor = (obj: any) => vscode.window.createTextEditorDecorationType({ color: obj }) + this.decorations = { + background: decor("#3F3F3F"), + error: vscode.window.createTextEditorDecorationType({ + borderColor: "red", + borderStyle: "none none dashed none", + }), + comment: decor("#7F9F7F"), + string: decor("#CC9393"), + keyword: decor("#F0DFAF"), + function: decor("#93E0E3"), + parameter: decor("#94BFF3"), + builtin: decor("#DD6718"), + text: decor("#DCDCCC"), + attribute: decor("#BFEBBF"), + literal: decor("#DFAF8F"), + } + } +} diff --git a/editors/code/src/server.ts b/editors/code/src/server.ts new file mode 100644 index 00000000000..c1c95e00842 --- /dev/null +++ b/editors/code/src/server.ts @@ -0,0 +1,74 @@ +import * as vscode from 'vscode'; +import * as lc from 'vscode-languageclient' + +import { Highlighter, Decoration } from './highlighting'; + +export class Config { + highlightingOn = true; + + constructor() { + vscode.workspace.onDidChangeConfiguration(_ => this.userConfigChanged()); + this.userConfigChanged(); + } + + userConfigChanged() { + let config = vscode.workspace.getConfiguration('ra-lsp'); + if (config.has('highlightingOn')) { + this.highlightingOn = config.get('highlightingOn') as boolean; + }; + + if (!this.highlightingOn) { + Server.highlighter.removeHighlights(); + } + } +} + +export class Server { + static highlighter = new Highlighter(); + static config = new Config(); + static client: lc.LanguageClient; + + + static start() { + let run: lc.Executable = { + command: "ra_lsp_server", + options: { cwd: "." } + } + let serverOptions: lc.ServerOptions = { + run, + debug: run + }; + + let clientOptions: lc.LanguageClientOptions = { + documentSelector: [{ scheme: 'file', language: 'rust' }], + }; + + Server.client = new lc.LanguageClient( + 'ra-lsp', + 'rust-analyzer languge server', + serverOptions, + clientOptions, + ); + Server.client.onReady().then(() => { + Server.client.onNotification( + "m/publishDecorations", + (params: PublishDecorationsParams) => { + let editor = vscode.window.visibleTextEditors.find( + (editor) => editor.document.uri.toString() == params.uri + ) + if (!Server.config.highlightingOn || !editor) return; + Server.highlighter.setHighlights( + editor, + params.decorations, + ) + } + ) + }) + Server.client.start(); + } +} + +interface PublishDecorationsParams { + uri: string, + decorations: Decoration[], +}