diff --git a/package.json b/package.json index c52280d6..42e404cf 100644 --- a/package.json +++ b/package.json @@ -67,23 +67,37 @@ "vscode-test": "^0.4.3" }, "contributes": { - "languages": [ - { - "id": "rust", - "extensions": [ - ".rs" - ], - "configuration": "./language-configuration.json" - } - ], - "snippets": [ + "colors": [{ + "id": "rust.typeHintColor", + "description": "Specifies the foreground color of a type hint", + "defaults": { + "dark": "#A0A0A0F0", + "light": "#747474", + "highContrast": "#BEBEBE" + } + }, { - "language": "rust", - "path": "./snippets/rust.json" + "id": "rust.typeHintBackgroundColor", + "description": "Specifies the foreground color of a type hint", + "defaults": { + "dark": "#70707020", + "light": "#74747410", + "highContrast": "#BEBEBE" + } } ], - "commands": [ - { + "languages": [{ + "id": "rust", + "extensions": [ + ".rs" + ], + "configuration": "./language-configuration.json" + }], + "snippets": [{ + "language": "rust", + "path": "./snippets/rust.json" + }], + "commands": [{ "command": "rls.update", "title": "Update the RLS", "description": "Use Rustup to update Rust, the RLS, and required data", @@ -96,52 +110,47 @@ "category": "Rust" } ], - "taskDefinitions": [ - { - "type": "cargo", - "properties": { - "subcommand": { - "type": "string" - } - }, - "required": [ - "subcommand" - ] - } - ], - "problemMatchers": [ - { - "name": "rustc", - "owner": "rust", - "fileLocation": [ - "relative", - "${workspaceRoot}" - ], - "pattern": [ - { - "regexp": "^(warning|warn|error)(\\[(.*)\\])?: (.*)$", - "severity": 1, - "message": 4, - "code": 3 - }, - { - "regexp": "^([\\s->=]*(.*):(\\d*):(\\d*)|.*)$", - "file": 2, - "line": 3, - "column": 4 - }, - { - "regexp": "^.*$" - }, - { - "regexp": "^([\\s->=]*(.*):(\\d*):(\\d*)|.*)$", - "file": 2, - "line": 3, - "column": 4 - } - ] - } - ], + "taskDefinitions": [{ + "type": "cargo", + "properties": { + "subcommand": { + "type": "string" + } + }, + "required": [ + "subcommand" + ] + }], + "problemMatchers": [{ + "name": "rustc", + "owner": "rust", + "fileLocation": [ + "relative", + "${workspaceRoot}" + ], + "pattern": [{ + "regexp": "^(warning|warn|error)(\\[(.*)\\])?: (.*)$", + "severity": 1, + "message": 4, + "code": 3 + }, + { + "regexp": "^([\\s->=]*(.*):(\\d*):(\\d*)|.*)$", + "file": 2, + "line": 3, + "column": 4 + }, + { + "regexp": "^.*$" + }, + { + "regexp": "^([\\s->=]*(.*):(\\d*):(\\d*)|.*)$", + "file": 2, + "line": 3, + "column": 4 + } + ] + }], "configuration": { "type": "object", "title": "Rust configuration", @@ -422,8 +431,27 @@ "default": true, "description": "Show additional context in hover tooltips when available. This is often the type local variable declaration.", "scope": "resource" + }, + "rust-type_hints.enable": { + "type": "boolean", + "default": true, + "desciption": "Show type hints", + "scope": "resource" + }, + "rust-type_hints.max_length": { + "type": "integer", + "default": 40, + "desciption": "Maximum length of type hints", + "scope": "resource" + }, + "rust-type_hints.shortening": { + "type": "string", + "enum": ["greedy", "none"], + "default": "greedy", + "desciption": "\"greedy\" will remove all namespace parts from type names, as well as lifetime specifiers, from the hints", + "scope": "resource" } } } } -} +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index d66a2912..d92e9c59 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,13 @@ import * as child_process from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; +import { + LanguageClient, + LanguageClientOptions, + NotificationType, + ServerOptions, +} from 'vscode-languageclient'; + import { commands, Disposable, @@ -14,15 +21,9 @@ import { WorkspaceFolder, WorkspaceFoldersChangeEvent, } from 'vscode'; -import { - LanguageClient, - LanguageClientOptions, - NotificationType, - ServerOptions, -} from 'vscode-languageclient'; - import { RLSConfiguration } from './configuration'; import { SignatureHelpProvider } from './providers/signatureHelpProvider'; +import { Decorator } from './providers/typeAnnotationsProvider'; import { checkForRls, ensureToolchain, rustupUpdate } from './rustup'; import { startSpinner, stopSpinner } from './spinner'; import { activateTaskProvider, Execution, runRlsCommand } from './tasks'; @@ -50,6 +51,24 @@ export async function activate(context: ExtensionContext) { workspace.onDidChangeWorkspaceFolders(e => whenChangingWorkspaceFolders(e, context), ); + window.onDidChangeActiveTextEditor(editor => { + if (editor === undefined || editor === null) { + return; + } + const decorator = Decorator.getInstance(); + if (decorator === undefined) { + return; + } + console.log('Attempting to decorate after editor change.'); + decorator.decorate(editor); + }); + workspace.onDidChangeTextDocument(textDocumentChange => { + for (const editor of window.visibleTextEditors) { + if (editor.document === textDocumentChange.document) { + Decorator.getInstance()!.decorate(editor); + } + } + }); } export async function deactivate() { @@ -256,6 +275,7 @@ class ClientWorkspace { clientOptions, ); + Decorator.getInstance(this.lc); const selector = this.config.multiProjectEnabled ? { language: 'rust', scheme: 'file', pattern } : { language: 'rust' }; @@ -330,12 +350,18 @@ class ClientWorkspace { const runningProgress: Set = new Set(); await this.lc.onReady(); stopSpinner('RLS'); - this.lc.onNotification( new NotificationType('window/progress'), progress => { if (progress.done) { runningProgress.delete(progress.id); + const decorator = Decorator.getInstance(); + if (decorator !== undefined) { + console.log('Starting Decoration after progress.done'); + for (const editor of window.visibleTextEditors) { + decorator.decorate(editor); + } + } } else { runningProgress.add(progress.id); } @@ -517,7 +543,10 @@ function configureLanguage(): Disposable { // e.g. /** | */ or /*! | */ beforeText: /^\s*\/\*(\*|\!)(?!\/)([^\*]|\*(?!\/))*$/, afterText: /^\s*\*\/$/, - action: { indentAction: IndentAction.IndentOutdent, appendText: ' * ' }, + action: { + indentAction: IndentAction.IndentOutdent, + appendText: ' * ', + }, }, { // Begins a multi-line comment (standard or parent doc) diff --git a/src/providers/typeAnnotationsProvider.ts b/src/providers/typeAnnotationsProvider.ts new file mode 100644 index 00000000..7afbe682 --- /dev/null +++ b/src/providers/typeAnnotationsProvider.ts @@ -0,0 +1,204 @@ +import * as vscode from 'vscode'; +import { HoverRequest, LanguageClient } from 'vscode-languageclient'; +import { FullType, GreedySimplifier } from './typeNameShortener'; + +const typeHintDecorationType = vscode.window.createTextEditorDecorationType({ + before: { + color: new vscode.ThemeColor('rust.typeHintColor'), + backgroundColor: new vscode.ThemeColor('rust.typeHintBackgroundColor'), + }, +}); + +const MULTIPLE_DECLARATIONS = '(&?(mut\\s+)?\\w+(\\s*:\\s*\\w+)?\\s*,?\\s*)+'; +const SIMPLE_DECLARATION = /((let|for)(\s+mut)?\s+(\w+))\s*[=;]/; +const TUPLE_UNPACKING: RegExp = new RegExp( + '(let\\s+|for\\s+|if let[^=]+)(\\(' + MULTIPLE_DECLARATIONS + '\\))', +); +const MATCH_CASE: RegExp = new RegExp( + '\\(' + MULTIPLE_DECLARATIONS + '\\)[)\\s]*=>', +); +const CLOSURE_PARAMETERS: RegExp = new RegExp( + '\\|' + MULTIPLE_DECLARATIONS + '\\|', +); +const INNER_DECLARATION: RegExp = new RegExp('&?\\s*(mut\\s+)?\\w+'); + +function unpack_arguments(line: string): number[] { + const result: number[] = []; + const args = line.split(','); + let count = 0; + for (const arg of args) { + const inner = arg.match(INNER_DECLARATION); + if (!arg.includes(':') && inner && inner.index !== undefined) { + result.push(count + inner.index + inner[0].length); + } + count += arg.length + 1; + } + return result; +} + +function get_next_position( + lineNumber: number, + substring: string, + currentCharCount: number, +): vscode.Position[] { + const match = substring.match(SIMPLE_DECLARATION); + if (match && match.index) { + return [ + new vscode.Position( + lineNumber, + currentCharCount + match.index + match[1].length, + ), + ]; + } + const declarationPositions = []; + const closureMatch = substring.match(CLOSURE_PARAMETERS); + if (closureMatch && closureMatch.index) { + for (const character of unpack_arguments(closureMatch[0].substr(1))) { + declarationPositions.push( + new vscode.Position( + lineNumber, + currentCharCount + closureMatch.index + 1 + character, + ), + ); + } + return declarationPositions; + } + const tupleUnpacking = substring.match(TUPLE_UNPACKING); + if (tupleUnpacking && tupleUnpacking.index) { + for (const character of unpack_arguments(tupleUnpacking[2].substr(1))) { + declarationPositions.push( + new vscode.Position( + lineNumber, + currentCharCount + + tupleUnpacking.index + + tupleUnpacking[1].length + + 1 + + character, + ), + ); + } + return declarationPositions; + } + if (substring.includes('=>')) { + const matchArm = substring.match(MATCH_CASE); + if (matchArm && matchArm.index) { + for (const character of unpack_arguments(matchArm[0].substr(1))) { + declarationPositions.push( + new vscode.Position( + lineNumber, + currentCharCount + matchArm.index + 1 + character, + ), + ); + } + } + } + return declarationPositions; +} + +const SHORTENER_REGEX = /<[^<]*?(<\.\.\.>)?[^<]*?>/; +const ENABLED = vscode.workspace + .getConfiguration() + .get('rust-type_hints.enabled', true); +const MAX_LENGTH = vscode.workspace + .getConfiguration() + .get('rust-type_hints.max_length', 40); +const SHORTENER = vscode.workspace + .getConfiguration() + .get('rust-type_hints.shortening', 'greedy'); +export class Decorator { + private static instance?: Decorator; + private lc: LanguageClient; + + constructor(lc: LanguageClient) { + this.lc = lc; + } + + public static getInstance(lc?: LanguageClient): Decorator | undefined { + if (lc !== undefined) { + if (Decorator.instance === undefined) { + Decorator.instance = new Decorator(lc); + } else { + if (Decorator.instance) { + Decorator.instance.lc = lc; + } + } + } + return Decorator.instance; + } + + public async decorate(editor: vscode.TextEditor): Promise { + if (editor.document.languageId !== 'rust' || !ENABLED) { + return; + } + try { + const text = editor.document.getText(); + const lines = text.split('\n'); + const declarationPositions: vscode.Position[] = []; + for (let i = 0; i < lines.length; i++) { + let line = lines[i].split('//')[0].split('#')[0]; + if (line.trim().startsWith('impl')) { + continue; + } + let newPositions: vscode.Position[] = []; + let count = 0; + do { + newPositions = get_next_position(i, line, count); + for (const position of newPositions) { + declarationPositions.push(position); + } + const last = newPositions[newPositions.length - 1]; + if (last) { + line = line.substr(last.character); + count += last.character; + } + } while (newPositions.length > 0); + } + const hints: vscode.DecorationOptions[] = []; + for (const position of declarationPositions) { + try { + const hover = await this.lc.sendRequest( + HoverRequest.type, + this.lc.code2ProtocolConverter.asTextDocumentPositionParams( + editor.document, + position.translate(0, -1), + ), + ); + if (hover) { + const content = hover.contents; + // @ts-ignore + const simplified = content[0].value; + if (!/^(?:\w+::)*\w+(?:<.*>)?$/m.test(simplified)) { + continue; + } + const type = new FullType(simplified); + let hint = ''; + switch (SHORTENER) { + case 'none': + hint = type.stringify(); + break; + case 'greedy': + hint = GreedySimplifier.simplify(type).stringify(); + break; + } + while (hint.length > MAX_LENGTH) { + const replacement = hint.replace(SHORTENER_REGEX, '<...>'); + if (replacement === hint) { + break; + } + hint = replacement; + } + hints.push({ + range: new vscode.Range(position, position), + renderOptions: { before: { contentText: ': ' + hint } }, + }); + } + } catch (e) { + continue; + } + } + editor.setDecorations(typeHintDecorationType, hints); + } catch (e) { + return; + } + } +} diff --git a/src/providers/typeNameShortener.ts b/src/providers/typeNameShortener.ts new file mode 100644 index 00000000..9dfc911c --- /dev/null +++ b/src/providers/typeNameShortener.ts @@ -0,0 +1,158 @@ +function splitSingleBracketLevel( + str: string, + brackets: string = '{}', + delimiter = ',', + level = 1, +): string[] { + const result: string[] = []; + const bracketStart = brackets[0]; + const bracketEnd = brackets[brackets.length - 1]; + let count = 0; + let latestDelimiter = 0; + for (let i = 0; i < str.length; i++) { + switch (str[i]) { + case bracketStart: + count++; + if (count === level) { + latestDelimiter = i + 1; + } + break; + case bracketEnd: + count--; + if (count === level - 1) { + result.push(str.substring(latestDelimiter, i)); + latestDelimiter = i + 1; + return result; + } + break; + case delimiter: + if (count === level) { + result.push(str.substring(latestDelimiter, i)); + latestDelimiter = i + 1; + } + break; + case '[': + count++; + break; + case ']': + count--; + } + } + const end = str.substring(latestDelimiter); + if (end.length > 0) { + result.push(end); + } + return result; +} + +function getPostFix(str: string, brackets = '<>'): string { + let count = 0; + const bracketStart = brackets[0]; + const bracketEnd = brackets[brackets.length - 1]; + for (let i = 0; i < str.length; i++) { + switch (str[i]) { + case bracketStart: + count++; + break; + case bracketEnd: + count--; + if (count === 0) { + return str.substring(i + 1); + } + break; + } + } + return ''; +} + +class SemiFullType { + public prefix: string; + public postfix: string; + public children: FullType[]; + public constructor( + prefix: string, + postfix: string, + children: FullType[] = [], + ) { + this.prefix = prefix; + this.postfix = postfix; + this.children = children; + } + + public stringify(): string { + return ( + this.prefix + + (this.children.length === 0 + ? '' + : '<' + + this.children.map(child => child.stringify()).join(', ') + + '>') + + this.postfix + ); + } +} + +// tslint:disable-next-line: max-classes-per-file +export class FullType { + public parts: SemiFullType[] = []; + + public constructor(parseable: string) { + if (parseable === '') { + return; + } + const typeParts = splitSingleBracketLevel(parseable, '<>', '+', 0); + for (const typePart of typeParts) { + let index: number | undefined = typePart.indexOf('<'); + index = index < 0 ? undefined : index; + const prefix = typePart.substring(0, index); + const args = index !== undefined ? typePart.substring(index) : ''; + const postfix = getPostFix(args); + const part: SemiFullType = new SemiFullType(prefix.trim(), postfix); + const childrenStr = splitSingleBracketLevel(args, '<>'); + for (let childStr of childrenStr) { + if (childStr.trim().startsWith('[closure@')) { + childStr = '[closure]'; + } + part.children.push(new FullType(childStr)); + } + this.parts.push(part); + } + } + + public stringify(): string { + return this.parts.map(part => part.stringify()).join(' + '); + } +} + +// tslint:disable-next-line: max-classes-per-file +export class GreedySimplifier { + protected static prefixRegex: RegExp = /(&(mut)?\s*)/; + + public static simplify(fullType: FullType): FullType { + const returnValue: FullType = new FullType(''); + for (const part of fullType.parts) { + if (part.prefix.startsWith("'")) { + continue; + } + const prefixOption = part.prefix.match(GreedySimplifier.prefixRegex); + const prefix = prefixOption !== null ? prefixOption[0] : ''; + const nameSplit = part.prefix.substring(prefix.length).split('::'); + const semiType: SemiFullType = new SemiFullType( + prefix + + (part.prefix.includes(' as ') + ? part.prefix.split(' as ')[0] + ' as ' + : '') + + nameSplit[nameSplit.length - 1], + part.postfix, + ); + for (const subType of part.children) { + const simplifiedSubType = this.simplify(subType); + if (simplifiedSubType.parts.length > 0) { + semiType.children.push(simplifiedSubType); + } + } + returnValue.parts.push(semiType); + } + return returnValue; + } +}