4602: Add boolean literal semantic token type to package.json r=matklad a=lnicola
Closes #4583.
CC @GrayJack
4603: Add self keyword semantic token type r=matklad a=lnicola
Not sure if this is warranted a new token type or just a modifier.
---
CC #4583, @GrayJack
Co-authored-by: Laurențiu Nicola <lnicola@dend.ro>
/// Returns an edit which should be applied when opening a new line, fixing
/// up minor stuff like continuing the comment.
- pub fn on_enter(&self, position: FilePosition) -> Cancelable<Option<SourceChange>> {
+ /// The edit will be a snippet (with `$0`).
+ pub fn on_enter(&self, position: FilePosition) -> Cancelable<Option<TextEdit>> {
self.with_db(|db| typing::on_enter(&db, position))
}
}
<span class="keyword">trait</span> <span class="trait declaration">Bar</span> {
- <span class="keyword">fn</span> <span class="function declaration">bar</span>(&<span class="keyword">self</span>) -> <span class="builtin_type">i32</span>;
+ <span class="keyword">fn</span> <span class="function declaration">bar</span>(&<span class="self_keyword">self</span>) -> <span class="builtin_type">i32</span>;
}
<span class="keyword">impl</span> <span class="trait">Bar</span> <span class="keyword">for</span> <span class="struct">Foo</span> {
- <span class="keyword">fn</span> <span class="function declaration">bar</span>(&<span class="keyword">self</span>) -> <span class="builtin_type">i32</span> {
- <span class="keyword">self</span>.<span class="field">x</span>
+ <span class="keyword">fn</span> <span class="function declaration">bar</span>(&<span class="self_keyword">self</span>) -> <span class="builtin_type">i32</span> {
+ <span class="self_keyword">self</span>.<span class="field">x</span>
}
}
<span class="keyword">use</span> <span class="enum">Option</span>::*;
<span class="keyword">impl</span><<span class="type_param declaration">T</span>> <span class="enum">Option</span><<span class="type_param">T</span>> {
- <span class="keyword">fn</span> <span class="function declaration">and</span><<span class="type_param declaration">U</span>>(<span class="keyword">self</span>, <span class="variable declaration">other</span>: <span class="enum">Option</span><<span class="type_param">U</span>>) -> <span class="enum">Option</span><(<span class="type_param">T</span>, <span class="type_param">U</span>)> {
+ <span class="keyword">fn</span> <span class="function declaration">and</span><<span class="type_param declaration">U</span>>(<span class="self_keyword">self</span>, <span class="variable declaration">other</span>: <span class="enum">Option</span><<span class="type_param">U</span>>) -> <span class="enum">Option</span><(<span class="type_param">T</span>, <span class="type_param">U</span>)> {
<span class="keyword control">match</span> <span class="variable">other</span> {
<span class="enum_variant">None</span> => <span class="macro">unimplemented!</span>(),
<span class="variable declaration">Nope</span> => <span class="variable">Nope</span>,
T![for] if !is_child_of_impl(element) => h | HighlightModifier::ControlFlow,
T![unsafe] => h | HighlightModifier::Unsafe,
T![true] | T![false] => HighlightTag::BoolLiteral.into(),
+ T![self] => HighlightTag::SelfKeyword.into(),
_ => h,
}
}
Macro,
Module,
NumericLiteral,
+ SelfKeyword,
SelfType,
Static,
StringLiteral,
HighlightTag::Macro => "macro",
HighlightTag::Module => "module",
HighlightTag::NumericLiteral => "numeric_literal",
+ HighlightTag::SelfKeyword => "self_keyword",
HighlightTag::SelfType => "self_type",
HighlightTag::Static => "static",
HighlightTag::StringLiteral => "string_literal",
};
use ra_text_edit::TextEdit;
-use crate::{SourceChange, SourceFileEdit};
-
-pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<SourceChange> {
+pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<TextEdit> {
let parse = db.parse(position.file_id);
let file = parse.tree();
let comment = file
let inserted = format!("\n{}{} $0", indent, prefix);
let edit = TextEdit::insert(position.offset, inserted);
- let mut res = SourceChange::from(SourceFileEdit { edit, file_id: position.file_id });
- res.is_snippet = true;
- Some(res)
+ Some(edit)
}
fn followed_by_comment(comment: &ast::Comment) -> bool {
let (analysis, file_id) = single_file(&before);
let result = analysis.on_enter(FilePosition { offset, file_id }).unwrap()?;
- assert_eq!(result.source_file_edits.len(), 1);
let mut actual = before.to_string();
- result.source_file_edits[0].edit.apply(&mut actual);
+ result.apply(&mut actual);
Some(actual)
}
experimental: Some(json!({
"joinLines": true,
"ssr": true,
+ "onEnter": true,
+ "parentModule": true,
})),
}
}
use std::{collections::HashMap, path::PathBuf};
use lsp_types::request::Request;
-use lsp_types::{Location, Position, Range, TextDocumentIdentifier};
+use lsp_types::{Position, Range, TextDocumentIdentifier};
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
#[serde(rename_all = "camelCase")]
pub struct ExpandMacroParams {
pub text_document: TextDocumentIdentifier,
- pub position: Option<Position>,
+ pub position: Position,
}
#[derive(Deserialize, Serialize, Debug)]
impl Request for ParentModule {
type Params = lsp_types::TextDocumentPositionParams;
- type Result = Vec<Location>;
- const METHOD: &'static str = "rust-analyzer/parentModule";
+ type Result = Option<lsp_types::GotoDefinitionResponse>;
+ const METHOD: &'static str = "experimental/parentModule";
}
pub enum JoinLines {}
impl Request for OnEnter {
type Params = lsp_types::TextDocumentPositionParams;
- type Result = Option<SnippetWorkspaceEdit>;
- const METHOD: &'static str = "rust-analyzer/onEnter";
+ type Result = Option<Vec<SnippetTextEdit>>;
+ const METHOD: &'static str = "experimental/onEnter";
}
pub enum Runnables {}
let _p = profile("handle_expand_macro");
let file_id = from_proto::file_id(&world, ¶ms.text_document.uri)?;
let line_index = world.analysis().file_line_index(file_id)?;
- let offset = params.position.map(|p| from_proto::offset(&line_index, p));
+ let offset = from_proto::offset(&line_index, params.position);
- match offset {
- None => Ok(None),
- Some(offset) => {
- let res = world.analysis().expand_macro(FilePosition { file_id, offset })?;
- Ok(res.map(|it| lsp_ext::ExpandedMacro { name: it.name, expansion: it.expansion }))
- }
- }
+ let res = world.analysis().expand_macro(FilePosition { file_id, offset })?;
+ Ok(res.map(|it| lsp_ext::ExpandedMacro { name: it.name, expansion: it.expansion }))
}
pub fn handle_selection_range(
pub fn handle_on_enter(
world: WorldSnapshot,
params: lsp_types::TextDocumentPositionParams,
-) -> Result<Option<lsp_ext::SnippetWorkspaceEdit>> {
+) -> Result<Option<Vec<lsp_ext::SnippetTextEdit>>> {
let _p = profile("handle_on_enter");
let position = from_proto::file_position(&world, params)?;
- match world.analysis().on_enter(position)? {
- None => Ok(None),
- Some(source_change) => to_proto::snippet_workspace_edit(&world, source_change).map(Some),
- }
+ let edit = match world.analysis().on_enter(position)? {
+ None => return Ok(None),
+ Some(it) => it,
+ };
+ let line_index = world.analysis().file_line_index(position.file_id)?;
+ let line_endings = world.file_line_endings(position.file_id);
+ let edit = to_proto::snippet_text_edit_vec(&line_index, line_endings, true, edit);
+ Ok(Some(edit))
}
// Don't forget to add new trigger characters to `ServerCapabilities` in `caps.rs`.
None => return Ok(None),
Some(it) => it,
};
- let res = to_proto::goto_definition_response(
- &world,
- FileRange { file_id: position.file_id, range: nav_info.range },
- nav_info.info,
- )?;
+ let src = FileRange { file_id: position.file_id, range: nav_info.range };
+ let res = to_proto::goto_definition_response(&world, Some(src), nav_info.info)?;
Ok(Some(res))
}
None => return Ok(None),
Some(it) => it,
};
- let res = to_proto::goto_definition_response(
- &world,
- FileRange { file_id: position.file_id, range: nav_info.range },
- nav_info.info,
- )?;
+ let src = FileRange { file_id: position.file_id, range: nav_info.range };
+ let res = to_proto::goto_definition_response(&world, Some(src), nav_info.info)?;
Ok(Some(res))
}
None => return Ok(None),
Some(it) => it,
};
- let res = to_proto::goto_definition_response(
- &world,
- FileRange { file_id: position.file_id, range: nav_info.range },
- nav_info.info,
- )?;
+ let src = FileRange { file_id: position.file_id, range: nav_info.range };
+ let res = to_proto::goto_definition_response(&world, Some(src), nav_info.info)?;
Ok(Some(res))
}
pub fn handle_parent_module(
world: WorldSnapshot,
params: lsp_types::TextDocumentPositionParams,
-) -> Result<Vec<Location>> {
+) -> Result<Option<lsp_types::GotoDefinitionResponse>> {
let _p = profile("handle_parent_module");
let position = from_proto::file_position(&world, params)?;
- world
- .analysis()
- .parent_module(position)?
- .into_iter()
- .map(|it| to_proto::location(&world, it.file_range()))
- .collect::<Result<Vec<_>>>()
+ let navs = world.analysis().parent_module(position)?;
+ let res = to_proto::goto_definition_response(&world, None, navs)?;
+ Ok(Some(res))
}
pub fn handle_runnables(
(BUILTIN_TYPE, "builtinType"),
(ENUM_MEMBER, "enumMember"),
(LIFETIME, "lifetime"),
+ (SELF_KEYWORD, "selfKeyword"),
(TYPE_ALIAS, "typeAlias"),
(UNION, "union"),
(UNRESOLVED_REFERENCE, "unresolvedReference"),
text_edit.into_iter().map(|indel| self::text_edit(line_index, line_endings, indel)).collect()
}
+pub(crate) fn snippet_text_edit_vec(
+ line_index: &LineIndex,
+ line_endings: LineEndings,
+ is_snippet: bool,
+ text_edit: TextEdit,
+) -> Vec<lsp_ext::SnippetTextEdit> {
+ text_edit
+ .into_iter()
+ .map(|indel| self::snippet_text_edit(line_index, line_endings, is_snippet, indel))
+ .collect()
+}
+
pub(crate) fn completion_item(
line_index: &LineIndex,
line_endings: LineEndings,
HighlightTag::TypeAlias => semantic_tokens::TYPE_ALIAS,
HighlightTag::Trait => lsp_types::SemanticTokenType::INTERFACE,
HighlightTag::BuiltinType => semantic_tokens::BUILTIN_TYPE,
+ HighlightTag::SelfKeyword => semantic_tokens::SELF_KEYWORD,
HighlightTag::SelfType => lsp_types::SemanticTokenType::TYPE,
HighlightTag::Field => lsp_types::SemanticTokenType::PROPERTY,
HighlightTag::Function => lsp_types::SemanticTokenType::FUNCTION,
pub(crate) fn location_link(
world: &WorldSnapshot,
- src: FileRange,
+ src: Option<FileRange>,
target: NavigationTarget,
) -> Result<lsp_types::LocationLink> {
- let src_location = location(world, src)?;
+ let origin_selection_range = match src {
+ Some(src) => {
+ let line_index = world.analysis().file_line_index(src.file_id)?;
+ let range = range(&line_index, src.range);
+ Some(range)
+ }
+ None => None,
+ };
let (target_uri, target_range, target_selection_range) = location_info(world, target)?;
let res = lsp_types::LocationLink {
- origin_selection_range: Some(src_location.range),
+ origin_selection_range,
target_uri,
target_range,
target_selection_range,
pub(crate) fn goto_definition_response(
world: &WorldSnapshot,
- src: FileRange,
+ src: Option<FileRange>,
targets: Vec<NavigationTarget>,
) -> Result<lsp_types::GotoDefinitionResponse> {
if world.config.client_caps.location_link {
text_document: server.doc_id("src/m0.rs"),
position: Position { line: 0, character: 5 },
},
- json!({
- "documentChanges": [
- {
- "edits": [
- {
- "insertTextFormat": 2,
- "newText": "\n/// $0",
- "range": {
- "end": { "character": 5, "line": 0 },
- "start": { "character": 5, "line": 0 }
- }
- }
- ],
- "textDocument": { "uri": "file:///[..]src/m0.rs", "version": null }
+ json!([{
+ "insertTextFormat": 2,
+ "newText": "\n/// $0",
+ "range": {
+ "end": { "character": 5, "line": 0 },
+ "start": { "character": 5, "line": 0 }
}
- ]
- }),
+ }]),
);
let elapsed = start.elapsed();
assert!(elapsed.as_millis() < 2000, "typing enter took {:?}", elapsed);
text_document: server.doc_id("src/main.rs"),
position: Position { line: 0, character: 8 },
},
- json!({
- "documentChanges": [
- {
- "edits": [
- {
- "insertTextFormat": 2,
- "newText": "\r\n/// $0",
- "range": {
- "end": { "line": 0, "character": 8 },
- "start": { "line": 0, "character": 8 }
- }
- }
- ],
- "textDocument": { "uri": "file:///[..]src/main.rs", "version": null }
+ json!([{
+ "insertTextFormat": 2,
+ "newText": "\r\n/// $0",
+ "range": {
+ "end": { "line": 0, "character": 8 },
+ "start": { "line": 0, "character": 8 }
}
- ]
- }),
+ }]),
);
}
* Is a fixed two-level structure enough?
* Should we devise a general way to encode custom interaction protocols for GUI refactorings?
+## Parent Module
+
+**Issue:** https://github.com/microsoft/language-server-protocol/issues/1002
+
+**Server Capability:** `{ "parentModule": boolean }`
+
+This request is send from client to server to handle "Goto Parent Module" editor action.
+
+**Method:** `experimental/parentModule`
+
+**Request:** `TextDocumentPositionParams`
+
+**Response:** `Location | Location[] | LocationLink[] | null`
+
+
+### Example
+
+```rust
+// src/main.rs
+mod foo;
+// src/foo.rs
+
+/* cursor here*/
+```
+
+`experimental/parentModule` returns a single `Link` to the `mod foo;` declaration.
+
+### Unresolved Question
+
+* An alternative would be to use a more general "gotoSuper" request, which would work for super methods, super classes and super modules.
+ This is the approach IntelliJ Rust is takeing.
+ However, experience shows that super module (which generally has a feeling of navigation between files) should be separate.
+ If you want super module, but the cursor happens to be inside an overriden function, the behavior with single "gotoSuper" request is surprising.
+
## Join Lines
**Issue:** https://github.com/microsoft/language-server-protocol/issues/992
}
```
-**Response:**
-
-```typescript
-TextEdit[]
-```
+**Response:** `TextEdit[]`
### Example
Currently this is left to editor's discretion, but it might be useful to specify on the server via snippets.
However, it then becomes unclear how it works with multi cursor.
+## On Enter
+
+**Issue:** https://github.com/microsoft/language-server-protocol/issues/1001
+
+**Server Capability:** `{ "onEnter": boolean }`
+
+This request is send from client to server to handle <kbd>Enter</kbd> keypress.
+
+**Method:** `experimental/onEnter`
+
+**Request:**: `TextDocumentPositionParams`
+
+**Response:**
+
+```typescript
+SnippetTextEdit[]
+```
+
+### Example
+
+```rust
+fn main() {
+ // Some /*cursor here*/ docs
+ let x = 92;
+}
+```
+
+`experimental/onEnter` returns the following snippet
+
+```rust
+fn main() {
+ // Some
+ // $0 docs
+ let x = 92;
+}
+```
+
+The primary goal of `onEnter` is to handle automatic indentation when opening a new line.
+This is not yet implemented.
+The secondary goal is to handle fixing up syntax, like continuing doc strings and comments, and escaping `\n` in string literals.
+
+As proper cursor positioning is raison-d'etat for `onEnter`, it uses `SnippetTextEdit`.
+
+### Unresolved Question
+
+* How to deal with synchronicity of the request?
+ One option is to require the client to block until the server returns the response.
+ Another option is to do a OT-style merging of edits from client and server.
+ A third option is to do a record-replay: client applies heuristic on enter immediatelly, then applies all user's keypresses.
+ When the server is ready with the response, the client rollbacks all the changes and applies the recorded actions on top of the correct response.
+* How to deal with multiple carets?
+* Should we extend this to arbitrary typed events and not just `onEnter`?
+
## Structural Search Replace (SSR)
**Server Capability:** `{ "ssr": boolean }`
```typescript
interface ExpandMacroParams {
textDocument: TextDocumentIdentifier,
- position?: Position,
+ position: Position,
}
```
"id": "lifetime",
"description": "Style for lifetimes"
},
+ {
+ "id": "selfKeyword",
+ "description": "Style for the self keyword",
+ "superType": "keyword"
+ },
{
"id": "typeAlias",
"description": "Style for type aliases",
--- /dev/null
+import * as vscode from 'vscode';
+
+import { Ctx, Disposable } from './ctx';
+import { RustEditor, isRustEditor } from './util';
+
+// FIXME: consider implementing this via the Tree View API?
+// https://code.visualstudio.com/api/extension-guides/tree-view
+export class AstInspector implements vscode.HoverProvider, vscode.DefinitionProvider, Disposable {
+ private readonly astDecorationType = vscode.window.createTextEditorDecorationType({
+ borderColor: new vscode.ThemeColor('rust_analyzer.syntaxTreeBorder'),
+ borderStyle: "solid",
+ borderWidth: "2px",
+ });
+ private rustEditor: undefined | RustEditor;
+
+ // Lazy rust token range -> syntax tree file range.
+ private readonly rust2Ast = new Lazy(() => {
+ const astEditor = this.findAstTextEditor();
+ if (!this.rustEditor || !astEditor) return undefined;
+
+ const buf: [vscode.Range, vscode.Range][] = [];
+ for (let i = 0; i < astEditor.document.lineCount; ++i) {
+ const astLine = astEditor.document.lineAt(i);
+
+ // Heuristically look for nodes with quoted text (which are token nodes)
+ const isTokenNode = astLine.text.lastIndexOf('"') >= 0;
+ if (!isTokenNode) continue;
+
+ const rustRange = this.parseRustTextRange(this.rustEditor.document, astLine.text);
+ if (!rustRange) continue;
+
+ buf.push([rustRange, this.findAstNodeRange(astLine)]);
+ }
+ return buf;
+ });
+
+ constructor(ctx: Ctx) {
+ ctx.pushCleanup(vscode.languages.registerHoverProvider({ scheme: 'rust-analyzer' }, this));
+ ctx.pushCleanup(vscode.languages.registerDefinitionProvider({ language: "rust" }, this));
+ vscode.workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, ctx.subscriptions);
+ vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions);
+ vscode.window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, ctx.subscriptions);
+
+ ctx.pushCleanup(this);
+ }
+ dispose() {
+ this.setRustEditor(undefined);
+ }
+
+ private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
+ if (this.rustEditor && event.document.uri.toString() === this.rustEditor.document.uri.toString()) {
+ this.rust2Ast.reset();
+ }
+ }
+
+ private onDidCloseTextDocument(doc: vscode.TextDocument) {
+ if (this.rustEditor && doc.uri.toString() === this.rustEditor.document.uri.toString()) {
+ this.setRustEditor(undefined);
+ }
+ }
+
+ private onDidChangeVisibleTextEditors(editors: vscode.TextEditor[]) {
+ if (!this.findAstTextEditor()) {
+ this.setRustEditor(undefined);
+ return;
+ }
+ this.setRustEditor(editors.find(isRustEditor));
+ }
+
+ private findAstTextEditor(): undefined | vscode.TextEditor {
+ return vscode.window.visibleTextEditors.find(it => it.document.uri.scheme === 'rust-analyzer');
+ }
+
+ private setRustEditor(newRustEditor: undefined | RustEditor) {
+ if (this.rustEditor && this.rustEditor !== newRustEditor) {
+ this.rustEditor.setDecorations(this.astDecorationType, []);
+ this.rust2Ast.reset();
+ }
+ this.rustEditor = newRustEditor;
+ }
+
+ // additional positional params are omitted
+ provideDefinition(doc: vscode.TextDocument, pos: vscode.Position): vscode.ProviderResult<vscode.DefinitionLink[]> {
+ if (!this.rustEditor || doc.uri.toString() !== this.rustEditor.document.uri.toString()) return;
+
+ const astEditor = this.findAstTextEditor();
+ if (!astEditor) return;
+
+ const rust2AstRanges = this.rust2Ast.get()?.find(([rustRange, _]) => rustRange.contains(pos));
+ if (!rust2AstRanges) return;
+
+ const [rustFileRange, astFileRange] = rust2AstRanges;
+
+ astEditor.revealRange(astFileRange);
+ astEditor.selection = new vscode.Selection(astFileRange.start, astFileRange.end);
+
+ return [{
+ targetRange: astFileRange,
+ targetUri: astEditor.document.uri,
+ originSelectionRange: rustFileRange,
+ targetSelectionRange: astFileRange,
+ }];
+ }
+
+ // additional positional params are omitted
+ provideHover(doc: vscode.TextDocument, hoverPosition: vscode.Position): vscode.ProviderResult<vscode.Hover> {
+ if (!this.rustEditor) return;
+
+ const astFileLine = doc.lineAt(hoverPosition.line);
+
+ const rustFileRange = this.parseRustTextRange(this.rustEditor.document, astFileLine.text);
+ if (!rustFileRange) return;
+
+ this.rustEditor.setDecorations(this.astDecorationType, [rustFileRange]);
+ this.rustEditor.revealRange(rustFileRange);
+
+ const rustSourceCode = this.rustEditor.document.getText(rustFileRange);
+ const astFileRange = this.findAstNodeRange(astFileLine);
+
+ return new vscode.Hover(["```rust\n" + rustSourceCode + "\n```"], astFileRange);
+ }
+
+ private findAstNodeRange(astLine: vscode.TextLine): vscode.Range {
+ const lineOffset = astLine.range.start;
+ const begin = lineOffset.translate(undefined, astLine.firstNonWhitespaceCharacterIndex);
+ const end = lineOffset.translate(undefined, astLine.text.trimEnd().length);
+ return new vscode.Range(begin, end);
+ }
+
+ private parseRustTextRange(doc: vscode.TextDocument, astLine: string): undefined | vscode.Range {
+ const parsedRange = /(\d+)\.\.(\d+)/.exec(astLine);
+ if (!parsedRange) return;
+
+ const [begin, end] = parsedRange
+ .slice(1)
+ .map(off => this.positionAt(doc, +off));
+
+ return new vscode.Range(begin, end);
+ }
+
+ // Memoize the last value, otherwise the CPU is at 100% single core
+ // with quadratic lookups when we build rust2Ast cache
+ cache?: { doc: vscode.TextDocument; offset: number; line: number };
+
+ positionAt(doc: vscode.TextDocument, targetOffset: number): vscode.Position {
+ if (doc.eol === vscode.EndOfLine.LF) {
+ return doc.positionAt(targetOffset);
+ }
+
+ // Dirty workaround for crlf line endings
+ // We are still in this prehistoric era of carriage returns here...
+
+ let line = 0;
+ let offset = 0;
+
+ const cache = this.cache;
+ if (cache?.doc === doc && cache.offset <= targetOffset) {
+ ({ line, offset } = cache);
+ }
+
+ while (true) {
+ const lineLenWithLf = doc.lineAt(line).text.length + 1;
+ if (offset + lineLenWithLf > targetOffset) {
+ this.cache = { doc, offset, line };
+ return doc.positionAt(targetOffset + line);
+ }
+ offset += lineLenWithLf;
+ line += 1;
+ }
+ }
+}
+
+class Lazy<T> {
+ val: undefined | T;
+
+ constructor(private readonly compute: () => undefined | T) { }
+
+ get() {
+ return this.val ?? (this.val = this.compute());
+ }
+
+ reset() {
+ this.val = undefined;
+ }
+}
--- /dev/null
+import * as vscode from 'vscode';
+import * as lc from 'vscode-languageclient';
+import * as ra from './lsp_ext';
+
+import { Ctx, Cmd } from './ctx';
+import { applySnippetWorkspaceEdit, applySnippetTextEdits } from './snippets';
+import { spawnSync } from 'child_process';
+import { RunnableQuickPick, selectRunnable, createTask } from './run';
+import { AstInspector } from './ast_inspector';
+import { isRustDocument, sleep, isRustEditor } from './util';
+
+export * from './ast_inspector';
+export * from './run';
+
+export function analyzerStatus(ctx: Ctx): Cmd {
+ const tdcp = new class implements vscode.TextDocumentContentProvider {
+ readonly uri = vscode.Uri.parse('rust-analyzer-status://status');
+ readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
+
+ provideTextDocumentContent(_uri: vscode.Uri): vscode.ProviderResult<string> {
+ if (!vscode.window.activeTextEditor) return '';
+
+ return ctx.client.sendRequest(ra.analyzerStatus, null);
+ }
+
+ get onDidChange(): vscode.Event<vscode.Uri> {
+ return this.eventEmitter.event;
+ }
+ }();
+
+ let poller: NodeJS.Timer | undefined = undefined;
+
+ ctx.pushCleanup(
+ vscode.workspace.registerTextDocumentContentProvider(
+ 'rust-analyzer-status',
+ tdcp,
+ ),
+ );
+
+ ctx.pushCleanup({
+ dispose() {
+ if (poller !== undefined) {
+ clearInterval(poller);
+ }
+ },
+ });
+
+ return async () => {
+ if (poller === undefined) {
+ poller = setInterval(() => tdcp.eventEmitter.fire(tdcp.uri), 1000);
+ }
+ const document = await vscode.workspace.openTextDocument(tdcp.uri);
+ return vscode.window.showTextDocument(document, vscode.ViewColumn.Two, true);
+ };
+}
+
+export function matchingBrace(ctx: Ctx): Cmd {
+ return async () => {
+ const editor = ctx.activeRustEditor;
+ const client = ctx.client;
+ if (!editor || !client) return;
+
+ const response = await client.sendRequest(ra.matchingBrace, {
+ textDocument: { uri: editor.document.uri.toString() },
+ positions: editor.selections.map(s =>
+ client.code2ProtocolConverter.asPosition(s.active),
+ ),
+ });
+ editor.selections = editor.selections.map((sel, idx) => {
+ const active = client.protocol2CodeConverter.asPosition(
+ response[idx],
+ );
+ const anchor = sel.isEmpty ? active : sel.anchor;
+ return new vscode.Selection(anchor, active);
+ });
+ editor.revealRange(editor.selection);
+ };
+}
+
+export function joinLines(ctx: Ctx): Cmd {
+ return async () => {
+ const editor = ctx.activeRustEditor;
+ const client = ctx.client;
+ if (!editor || !client) return;
+
+ const items: lc.TextEdit[] = await client.sendRequest(ra.joinLines, {
+ ranges: editor.selections.map((it) => client.code2ProtocolConverter.asRange(it)),
+ textDocument: { uri: editor.document.uri.toString() },
+ });
+ editor.edit((builder) => {
+ client.protocol2CodeConverter.asTextEdits(items).forEach((edit) => {
+ builder.replace(edit.range, edit.newText);
+ });
+ });
+ };
+}
+
+export function onEnter(ctx: Ctx): Cmd {
+ async function handleKeypress() {
+ const editor = ctx.activeRustEditor;
+ const client = ctx.client;
+
+ if (!editor || !client) return false;
+
+ const lcEdits = await client.sendRequest(ra.onEnter, {
+ textDocument: { uri: editor.document.uri.toString() },
+ position: client.code2ProtocolConverter.asPosition(
+ editor.selection.active,
+ ),
+ }).catch(_error => {
+ // client.logFailedRequest(OnEnterRequest.type, error);
+ return null;
+ });
+ if (!lcEdits) return false;
+
+ const edits = client.protocol2CodeConverter.asTextEdits(lcEdits);
+ await applySnippetTextEdits(editor, edits);
+ return true;
+ }
+
+ return async () => {
+ if (await handleKeypress()) return;
+
+ await vscode.commands.executeCommand('default:type', { text: '\n' });
+ };
+}
+
+export function parentModule(ctx: Ctx): Cmd {
+ return async () => {
+ const editor = ctx.activeRustEditor;
+ const client = ctx.client;
+ if (!editor || !client) return;
+
+ const response = await client.sendRequest(ra.parentModule, {
+ textDocument: { uri: editor.document.uri.toString() },
+ position: client.code2ProtocolConverter.asPosition(
+ editor.selection.active,
+ ),
+ });
+ const loc = response[0];
+ if (!loc) return;
+
+ const uri = client.protocol2CodeConverter.asUri(loc.targetUri);
+ const range = client.protocol2CodeConverter.asRange(loc.targetRange);
+
+ const doc = await vscode.workspace.openTextDocument(uri);
+ const e = await vscode.window.showTextDocument(doc);
+ e.selection = new vscode.Selection(range.start, range.start);
+ e.revealRange(range, vscode.TextEditorRevealType.InCenter);
+ };
+}
+
+export function ssr(ctx: Ctx): Cmd {
+ return async () => {
+ const client = ctx.client;
+ if (!client) return;
+
+ const options: vscode.InputBoxOptions = {
+ value: "() ==>> ()",
+ prompt: "Enter request, for example 'Foo($a:expr) ==> Foo::new($a)' ",
+ validateInput: async (x: string) => {
+ try {
+ await client.sendRequest(ra.ssr, { query: x, parseOnly: true });
+ } catch (e) {
+ return e.toString();
+ }
+ return null;
+ }
+ };
+ const request = await vscode.window.showInputBox(options);
+ if (!request) return;
+
+ const edit = await client.sendRequest(ra.ssr, { query: request, parseOnly: false });
+
+ await vscode.workspace.applyEdit(client.protocol2CodeConverter.asWorkspaceEdit(edit));
+ };
+}
+
+export function serverVersion(ctx: Ctx): Cmd {
+ return async () => {
+ const { stdout } = spawnSync(ctx.serverPath, ["--version"], { encoding: "utf8" });
+ const commitHash = stdout.slice(`rust-analyzer `.length).trim();
+ const { releaseTag } = ctx.config.package;
+
+ void vscode.window.showInformationMessage(
+ `rust-analyzer version: ${releaseTag ?? "unreleased"} (${commitHash})`
+ );
+ };
+}
+
+export function toggleInlayHints(ctx: Ctx): Cmd {
+ return async () => {
+ await vscode
+ .workspace
+ .getConfiguration(`${ctx.config.rootSection}.inlayHints`)
+ .update('enable', !ctx.config.inlayHints.enable, vscode.ConfigurationTarget.Workspace);
+ };
+}
+
+export function run(ctx: Ctx): Cmd {
+ let prevRunnable: RunnableQuickPick | undefined;
+
+ return async () => {
+ const item = await selectRunnable(ctx, prevRunnable);
+ if (!item) return;
+
+ item.detail = 'rerun';
+ prevRunnable = item;
+ const task = createTask(item.runnable);
+ return await vscode.tasks.executeTask(task);
+ };
+}
+
+// Opens the virtual file that will show the syntax tree
+//
+// The contents of the file come from the `TextDocumentContentProvider`
+export function syntaxTree(ctx: Ctx): Cmd {
+ const tdcp = new class implements vscode.TextDocumentContentProvider {
+ readonly uri = vscode.Uri.parse('rust-analyzer://syntaxtree/tree.rast');
+ readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
+ constructor() {
+ vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions);
+ vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, ctx.subscriptions);
+ }
+
+ private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
+ if (isRustDocument(event.document)) {
+ // We need to order this after language server updates, but there's no API for that.
+ // Hence, good old sleep().
+ void sleep(10).then(() => this.eventEmitter.fire(this.uri));
+ }
+ }
+ private onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) {
+ if (editor && isRustEditor(editor)) {
+ this.eventEmitter.fire(this.uri);
+ }
+ }
+
+ provideTextDocumentContent(uri: vscode.Uri, ct: vscode.CancellationToken): vscode.ProviderResult<string> {
+ const rustEditor = ctx.activeRustEditor;
+ if (!rustEditor) return '';
+
+ // When the range based query is enabled we take the range of the selection
+ const range = uri.query === 'range=true' && !rustEditor.selection.isEmpty
+ ? ctx.client.code2ProtocolConverter.asRange(rustEditor.selection)
+ : null;
+
+ const params = { textDocument: { uri: rustEditor.document.uri.toString() }, range, };
+ return ctx.client.sendRequest(ra.syntaxTree, params, ct);
+ }
+
+ get onDidChange(): vscode.Event<vscode.Uri> {
+ return this.eventEmitter.event;
+ }
+ };
+
+ void new AstInspector(ctx);
+
+ ctx.pushCleanup(vscode.workspace.registerTextDocumentContentProvider('rust-analyzer', tdcp));
+ ctx.pushCleanup(vscode.languages.setLanguageConfiguration("ra_syntax_tree", {
+ brackets: [["[", ")"]],
+ }));
+
+ return async () => {
+ const editor = vscode.window.activeTextEditor;
+ const rangeEnabled = !!editor && !editor.selection.isEmpty;
+
+ const uri = rangeEnabled
+ ? vscode.Uri.parse(`${tdcp.uri.toString()}?range=true`)
+ : tdcp.uri;
+
+ const document = await vscode.workspace.openTextDocument(uri);
+
+ tdcp.eventEmitter.fire(uri);
+
+ void await vscode.window.showTextDocument(document, {
+ viewColumn: vscode.ViewColumn.Two,
+ preserveFocus: true
+ });
+ };
+}
+
+
+// Opens the virtual file that will show the syntax tree
+//
+// The contents of the file come from the `TextDocumentContentProvider`
+export function expandMacro(ctx: Ctx): Cmd {
+ function codeFormat(expanded: ra.ExpandedMacro): string {
+ let result = `// Recursive expansion of ${expanded.name}! macro\n`;
+ result += '// ' + '='.repeat(result.length - 3);
+ result += '\n\n';
+ result += expanded.expansion;
+
+ return result;
+ }
+
+ const tdcp = new class implements vscode.TextDocumentContentProvider {
+ uri = vscode.Uri.parse('rust-analyzer://expandMacro/[EXPANSION].rs');
+ eventEmitter = new vscode.EventEmitter<vscode.Uri>();
+ async provideTextDocumentContent(_uri: vscode.Uri): Promise<string> {
+ const editor = vscode.window.activeTextEditor;
+ const client = ctx.client;
+ if (!editor || !client) return '';
+
+ const position = editor.selection.active;
+
+ const expanded = await client.sendRequest(ra.expandMacro, {
+ textDocument: { uri: editor.document.uri.toString() },
+ position,
+ });
+
+ if (expanded == null) return 'Not available';
+
+ return codeFormat(expanded);
+ }
+
+ get onDidChange(): vscode.Event<vscode.Uri> {
+ return this.eventEmitter.event;
+ }
+ }();
+
+ ctx.pushCleanup(
+ vscode.workspace.registerTextDocumentContentProvider(
+ 'rust-analyzer',
+ tdcp,
+ ),
+ );
+
+ return async () => {
+ const document = await vscode.workspace.openTextDocument(tdcp.uri);
+ tdcp.eventEmitter.fire(tdcp.uri);
+ return vscode.window.showTextDocument(
+ document,
+ vscode.ViewColumn.Two,
+ true,
+ );
+ };
+}
+
+export function collectGarbage(ctx: Ctx): Cmd {
+ return async () => ctx.client.sendRequest(ra.collectGarbage, null);
+}
+
+export function showReferences(ctx: Ctx): Cmd {
+ return (uri: string, position: lc.Position, locations: lc.Location[]) => {
+ const client = ctx.client;
+ if (client) {
+ vscode.commands.executeCommand(
+ 'editor.action.showReferences',
+ vscode.Uri.parse(uri),
+ client.protocol2CodeConverter.asPosition(position),
+ locations.map(client.protocol2CodeConverter.asLocation),
+ );
+ }
+ };
+}
+
+export function applyActionGroup(_ctx: Ctx): Cmd {
+ return async (actions: { label: string; edit: vscode.WorkspaceEdit }[]) => {
+ const selectedAction = await vscode.window.showQuickPick(actions);
+ if (!selectedAction) return;
+ await applySnippetWorkspaceEdit(selectedAction.edit);
+ };
+}
+
+export function applySnippetWorkspaceEditCommand(_ctx: Ctx): Cmd {
+ return async (edit: vscode.WorkspaceEdit) => {
+ await applySnippetWorkspaceEdit(edit);
+ };
+}
+++ /dev/null
-import * as vscode from 'vscode';
-
-import * as ra from '../rust-analyzer-api';
-import { Ctx, Cmd } from '../ctx';
-
-// Shows status of rust-analyzer (for debugging)
-export function analyzerStatus(ctx: Ctx): Cmd {
- let poller: NodeJS.Timer | undefined = undefined;
- const tdcp = new TextDocumentContentProvider(ctx);
-
- ctx.pushCleanup(
- vscode.workspace.registerTextDocumentContentProvider(
- 'rust-analyzer-status',
- tdcp,
- ),
- );
-
- ctx.pushCleanup({
- dispose() {
- if (poller !== undefined) {
- clearInterval(poller);
- }
- },
- });
-
- return async () => {
- if (poller === undefined) {
- poller = setInterval(() => tdcp.eventEmitter.fire(tdcp.uri), 1000);
- }
- const document = await vscode.workspace.openTextDocument(tdcp.uri);
- return vscode.window.showTextDocument(document, vscode.ViewColumn.Two, true);
- };
-}
-
-class TextDocumentContentProvider implements vscode.TextDocumentContentProvider {
- readonly uri = vscode.Uri.parse('rust-analyzer-status://status');
- readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
-
- constructor(private readonly ctx: Ctx) {
- }
-
- provideTextDocumentContent(_uri: vscode.Uri): vscode.ProviderResult<string> {
- if (!vscode.window.activeTextEditor) return '';
-
- return this.ctx.client.sendRequest(ra.analyzerStatus, null);
- }
-
- get onDidChange(): vscode.Event<vscode.Uri> {
- return this.eventEmitter.event;
- }
-}
+++ /dev/null
-import * as vscode from 'vscode';
-import * as ra from '../rust-analyzer-api';
-
-import { Ctx, Cmd } from '../ctx';
-
-// Opens the virtual file that will show the syntax tree
-//
-// The contents of the file come from the `TextDocumentContentProvider`
-export function expandMacro(ctx: Ctx): Cmd {
- const tdcp = new TextDocumentContentProvider(ctx);
- ctx.pushCleanup(
- vscode.workspace.registerTextDocumentContentProvider(
- 'rust-analyzer',
- tdcp,
- ),
- );
-
- return async () => {
- const document = await vscode.workspace.openTextDocument(tdcp.uri);
- tdcp.eventEmitter.fire(tdcp.uri);
- return vscode.window.showTextDocument(
- document,
- vscode.ViewColumn.Two,
- true,
- );
- };
-}
-
-function codeFormat(expanded: ra.ExpandedMacro): string {
- let result = `// Recursive expansion of ${expanded.name}! macro\n`;
- result += '// ' + '='.repeat(result.length - 3);
- result += '\n\n';
- result += expanded.expansion;
-
- return result;
-}
-
-class TextDocumentContentProvider
- implements vscode.TextDocumentContentProvider {
- uri = vscode.Uri.parse('rust-analyzer://expandMacro/[EXPANSION].rs');
- eventEmitter = new vscode.EventEmitter<vscode.Uri>();
-
- constructor(private readonly ctx: Ctx) {
- }
-
- async provideTextDocumentContent(_uri: vscode.Uri): Promise<string> {
- const editor = vscode.window.activeTextEditor;
- const client = this.ctx.client;
- if (!editor || !client) return '';
-
- const position = editor.selection.active;
-
- const expanded = await client.sendRequest(ra.expandMacro, {
- textDocument: { uri: editor.document.uri.toString() },
- position,
- });
-
- if (expanded == null) return 'Not available';
-
- return codeFormat(expanded);
- }
-
- get onDidChange(): vscode.Event<vscode.Uri> {
- return this.eventEmitter.event;
- }
-}
+++ /dev/null
-import * as vscode from 'vscode';
-import * as lc from 'vscode-languageclient';
-import * as ra from '../rust-analyzer-api';
-
-import { Ctx, Cmd } from '../ctx';
-import * as sourceChange from '../source_change';
-import { assert } from '../util';
-
-export * from './analyzer_status';
-export * from './matching_brace';
-export * from './join_lines';
-export * from './on_enter';
-export * from './parent_module';
-export * from './syntax_tree';
-export * from './expand_macro';
-export * from './runnables';
-export * from './ssr';
-export * from './server_version';
-export * from './toggle_inlay_hints';
-
-export function collectGarbage(ctx: Ctx): Cmd {
- return async () => ctx.client.sendRequest(ra.collectGarbage, null);
-}
-
-export function showReferences(ctx: Ctx): Cmd {
- return (uri: string, position: lc.Position, locations: lc.Location[]) => {
- const client = ctx.client;
- if (client) {
- vscode.commands.executeCommand(
- 'editor.action.showReferences',
- vscode.Uri.parse(uri),
- client.protocol2CodeConverter.asPosition(position),
- locations.map(client.protocol2CodeConverter.asLocation),
- );
- }
- };
-}
-
-export function applySourceChange(ctx: Ctx): Cmd {
- return async (change: ra.SourceChange) => {
- await sourceChange.applySourceChange(ctx, change);
- };
-}
-
-export function applyActionGroup(_ctx: Ctx): Cmd {
- return async (actions: { label: string; edit: vscode.WorkspaceEdit }[]) => {
- const selectedAction = await vscode.window.showQuickPick(actions);
- if (!selectedAction) return;
- await applySnippetWorkspaceEdit(selectedAction.edit);
- };
-}
-
-export function applySnippetWorkspaceEditCommand(_ctx: Ctx): Cmd {
- return async (edit: vscode.WorkspaceEdit) => {
- await applySnippetWorkspaceEdit(edit);
- };
-}
-
-export async function applySnippetWorkspaceEdit(edit: vscode.WorkspaceEdit) {
- assert(edit.entries().length === 1, `bad ws edit: ${JSON.stringify(edit)}`);
- const [uri, edits] = edit.entries()[0];
-
- const editor = vscode.window.visibleTextEditors.find((it) => it.document.uri.toString() === uri.toString());
- if (!editor) return;
-
- let selection: vscode.Selection | undefined = undefined;
- let lineDelta = 0;
- await editor.edit((builder) => {
- for (const indel of edits) {
- const parsed = parseSnippet(indel.newText);
- if (parsed) {
- const [newText, [placeholderStart, placeholderLength]] = parsed;
- const prefix = newText.substr(0, placeholderStart);
- const lastNewline = prefix.lastIndexOf('\n');
-
- const startLine = indel.range.start.line + lineDelta + countLines(prefix);
- const startColumn = lastNewline === -1 ?
- indel.range.start.character + placeholderStart
- : prefix.length - lastNewline - 1;
- const endColumn = startColumn + placeholderLength;
- selection = new vscode.Selection(
- new vscode.Position(startLine, startColumn),
- new vscode.Position(startLine, endColumn),
- );
- builder.replace(indel.range, newText);
- } else {
- lineDelta = countLines(indel.newText) - (indel.range.end.line - indel.range.start.line);
- builder.replace(indel.range, indel.newText);
- }
- }
- });
- if (selection) editor.selection = selection;
-}
-
-function parseSnippet(snip: string): [string, [number, number]] | undefined {
- const m = snip.match(/\$(0|\{0:([^}]*)\})/);
- if (!m) return undefined;
- const placeholder = m[2] ?? "";
- const range: [number, number] = [m.index!!, placeholder.length];
- const insert = snip.replace(m[0], placeholder);
- return [insert, range];
-}
-
-function countLines(text: string): number {
- return (text.match(/\n/g) || []).length;
-}
+++ /dev/null
-import * as ra from '../rust-analyzer-api';
-import * as lc from 'vscode-languageclient';
-
-import { Ctx, Cmd } from '../ctx';
-
-export function joinLines(ctx: Ctx): Cmd {
- return async () => {
- const editor = ctx.activeRustEditor;
- const client = ctx.client;
- if (!editor || !client) return;
-
- const items: lc.TextEdit[] = await client.sendRequest(ra.joinLines, {
- ranges: editor.selections.map((it) => client.code2ProtocolConverter.asRange(it)),
- textDocument: { uri: editor.document.uri.toString() },
- });
- editor.edit((builder) => {
- client.protocol2CodeConverter.asTextEdits(items).forEach((edit) => {
- builder.replace(edit.range, edit.newText);
- });
- });
- };
-}
+++ /dev/null
-import * as vscode from 'vscode';
-import * as ra from '../rust-analyzer-api';
-
-import { Ctx, Cmd } from '../ctx';
-
-export function matchingBrace(ctx: Ctx): Cmd {
- return async () => {
- const editor = ctx.activeRustEditor;
- const client = ctx.client;
- if (!editor || !client) return;
-
- const response = await client.sendRequest(ra.matchingBrace, {
- textDocument: { uri: editor.document.uri.toString() },
- positions: editor.selections.map(s =>
- client.code2ProtocolConverter.asPosition(s.active),
- ),
- });
- editor.selections = editor.selections.map((sel, idx) => {
- const active = client.protocol2CodeConverter.asPosition(
- response[idx],
- );
- const anchor = sel.isEmpty ? active : sel.anchor;
- return new vscode.Selection(anchor, active);
- });
- editor.revealRange(editor.selection);
- };
-}
+++ /dev/null
-import * as vscode from 'vscode';
-import * as ra from '../rust-analyzer-api';
-
-import { Cmd, Ctx } from '../ctx';
-import { applySnippetWorkspaceEdit } from '.';
-
-async function handleKeypress(ctx: Ctx) {
- const editor = ctx.activeRustEditor;
- const client = ctx.client;
-
- if (!editor || !client) return false;
-
- const change = await client.sendRequest(ra.onEnter, {
- textDocument: { uri: editor.document.uri.toString() },
- position: client.code2ProtocolConverter.asPosition(
- editor.selection.active,
- ),
- }).catch(_error => {
- // client.logFailedRequest(OnEnterRequest.type, error);
- return null;
- });
- if (!change) return false;
-
- const workspaceEdit = client.protocol2CodeConverter.asWorkspaceEdit(change);
- await applySnippetWorkspaceEdit(workspaceEdit);
- return true;
-}
-
-export function onEnter(ctx: Ctx): Cmd {
- return async () => {
- if (await handleKeypress(ctx)) return;
-
- await vscode.commands.executeCommand('default:type', { text: '\n' });
- };
-}
+++ /dev/null
-import * as vscode from 'vscode';
-import * as ra from '../rust-analyzer-api';
-
-import { Ctx, Cmd } from '../ctx';
-
-export function parentModule(ctx: Ctx): Cmd {
- return async () => {
- const editor = ctx.activeRustEditor;
- const client = ctx.client;
- if (!editor || !client) return;
-
- const response = await client.sendRequest(ra.parentModule, {
- textDocument: { uri: editor.document.uri.toString() },
- position: client.code2ProtocolConverter.asPosition(
- editor.selection.active,
- ),
- });
- const loc = response[0];
- if (loc == null) return;
-
- const uri = client.protocol2CodeConverter.asUri(loc.uri);
- const range = client.protocol2CodeConverter.asRange(loc.range);
-
- const doc = await vscode.workspace.openTextDocument(uri);
- const e = await vscode.window.showTextDocument(doc);
- e.selection = new vscode.Selection(range.start, range.start);
- e.revealRange(range, vscode.TextEditorRevealType.InCenter);
- };
-}
+++ /dev/null
-import * as vscode from 'vscode';
-import * as lc from 'vscode-languageclient';
-import * as ra from '../rust-analyzer-api';
-
-import { Ctx, Cmd } from '../ctx';
-import { startDebugSession, getDebugConfiguration } from '../debug';
-
-const quickPickButtons = [{ iconPath: new vscode.ThemeIcon("save"), tooltip: "Save as a launch.json configurtation." }];
-
-async function selectRunnable(ctx: Ctx, prevRunnable?: RunnableQuickPick, debuggeeOnly = false, showButtons: boolean = true): Promise<RunnableQuickPick | undefined> {
- const editor = ctx.activeRustEditor;
- const client = ctx.client;
- if (!editor || !client) return;
-
- const textDocument: lc.TextDocumentIdentifier = {
- uri: editor.document.uri.toString(),
- };
-
- const runnables = await client.sendRequest(ra.runnables, {
- textDocument,
- position: client.code2ProtocolConverter.asPosition(
- editor.selection.active,
- ),
- });
- const items: RunnableQuickPick[] = [];
- if (prevRunnable) {
- items.push(prevRunnable);
- }
- for (const r of runnables) {
- if (
- prevRunnable &&
- JSON.stringify(prevRunnable.runnable) === JSON.stringify(r)
- ) {
- continue;
- }
-
- if (debuggeeOnly && (r.label.startsWith('doctest') || r.label.startsWith('cargo'))) {
- continue;
- }
- items.push(new RunnableQuickPick(r));
- }
-
- if (items.length === 0) {
- // it is the debug case, run always has at least 'cargo check ...'
- // see crates\rust-analyzer\src\main_loop\handlers.rs, handle_runnables
- vscode.window.showErrorMessage("There's no debug target!");
- return;
- }
-
- return await new Promise((resolve) => {
- const disposables: vscode.Disposable[] = [];
- const close = (result?: RunnableQuickPick) => {
- resolve(result);
- disposables.forEach(d => d.dispose());
- };
-
- const quickPick = vscode.window.createQuickPick<RunnableQuickPick>();
- quickPick.items = items;
- quickPick.title = "Select Runnable";
- if (showButtons) {
- quickPick.buttons = quickPickButtons;
- }
- disposables.push(
- quickPick.onDidHide(() => close()),
- quickPick.onDidAccept(() => close(quickPick.selectedItems[0])),
- quickPick.onDidTriggerButton((_button) => {
- (async () => await makeDebugConfig(ctx, quickPick.activeItems[0]))();
- close();
- }),
- quickPick.onDidChangeActive((active) => {
- if (showButtons && active.length > 0) {
- if (active[0].label.startsWith('cargo')) {
- // save button makes no sense for `cargo test` or `cargo check`
- quickPick.buttons = [];
- } else if (quickPick.buttons.length === 0) {
- quickPick.buttons = quickPickButtons;
- }
- }
- }),
- quickPick
- );
- quickPick.show();
- });
-}
-
-export function run(ctx: Ctx): Cmd {
- let prevRunnable: RunnableQuickPick | undefined;
-
- return async () => {
- const item = await selectRunnable(ctx, prevRunnable);
- if (!item) return;
-
- item.detail = 'rerun';
- prevRunnable = item;
- const task = createTask(item.runnable);
- return await vscode.tasks.executeTask(task);
- };
-}
-
-export function runSingle(ctx: Ctx): Cmd {
- return async (runnable: ra.Runnable) => {
- const editor = ctx.activeRustEditor;
- if (!editor) return;
-
- const task = createTask(runnable);
- task.group = vscode.TaskGroup.Build;
- task.presentationOptions = {
- reveal: vscode.TaskRevealKind.Always,
- panel: vscode.TaskPanelKind.Dedicated,
- clear: true,
- };
-
- return vscode.tasks.executeTask(task);
- };
-}
-
-export function debug(ctx: Ctx): Cmd {
- let prevDebuggee: RunnableQuickPick | undefined;
-
- return async () => {
- const item = await selectRunnable(ctx, prevDebuggee, true);
- if (!item) return;
-
- item.detail = 'restart';
- prevDebuggee = item;
- return await startDebugSession(ctx, item.runnable);
- };
-}
-
-export function debugSingle(ctx: Ctx): Cmd {
- return async (config: ra.Runnable) => {
- await startDebugSession(ctx, config);
- };
-}
-
-async function makeDebugConfig(ctx: Ctx, item: RunnableQuickPick): Promise<void> {
- const scope = ctx.activeRustEditor?.document.uri;
- if (!scope) return;
-
- const debugConfig = await getDebugConfiguration(ctx, item.runnable);
- if (!debugConfig) return;
-
- const wsLaunchSection = vscode.workspace.getConfiguration("launch", scope);
- const configurations = wsLaunchSection.get<any[]>("configurations") || [];
-
- const index = configurations.findIndex(c => c.name === debugConfig.name);
- if (index !== -1) {
- const answer = await vscode.window.showErrorMessage(`Launch configuration '${debugConfig.name}' already exists!`, 'Cancel', 'Update');
- if (answer === "Cancel") return;
-
- configurations[index] = debugConfig;
- } else {
- configurations.push(debugConfig);
- }
-
- await wsLaunchSection.update("configurations", configurations);
-}
-
-export function newDebugConfig(ctx: Ctx): Cmd {
- return async () => {
- const item = await selectRunnable(ctx, undefined, true, false);
- if (!item) return;
-
- await makeDebugConfig(ctx, item);
- };
-}
-
-class RunnableQuickPick implements vscode.QuickPickItem {
- public label: string;
- public description?: string | undefined;
- public detail?: string | undefined;
- public picked?: boolean | undefined;
-
- constructor(public runnable: ra.Runnable) {
- this.label = runnable.label;
- }
-}
-
-interface CargoTaskDefinition extends vscode.TaskDefinition {
- type: 'cargo';
- label: string;
- command: string;
- args: string[];
- env?: { [key: string]: string };
-}
-
-function createTask(spec: ra.Runnable): vscode.Task {
- const TASK_SOURCE = 'Rust';
- const definition: CargoTaskDefinition = {
- type: 'cargo',
- label: spec.label,
- command: spec.bin,
- args: spec.extraArgs ? [...spec.args, '--', ...spec.extraArgs] : spec.args,
- env: spec.env,
- };
-
- const execOption: vscode.ShellExecutionOptions = {
- cwd: spec.cwd || '.',
- env: definition.env,
- };
- const exec = new vscode.ShellExecution(
- definition.command,
- definition.args,
- execOption,
- );
-
- const f = vscode.workspace.workspaceFolders![0];
- const t = new vscode.Task(
- definition,
- f,
- definition.label,
- TASK_SOURCE,
- exec,
- ['$rustc'],
- );
- t.presentationOptions.clear = true;
- return t;
-}
+++ /dev/null
-import * as vscode from "vscode";
-import { spawnSync } from "child_process";
-import { Ctx, Cmd } from '../ctx';
-
-export function serverVersion(ctx: Ctx): Cmd {
- return async () => {
- const { stdout } = spawnSync(ctx.serverPath, ["--version"], { encoding: "utf8" });
- const commitHash = stdout.slice(`rust-analyzer `.length).trim();
- const { releaseTag } = ctx.config.package;
-
- void vscode.window.showInformationMessage(
- `rust-analyzer version: ${releaseTag ?? "unreleased"} (${commitHash})`
- );
- };
-}
+++ /dev/null
-import * as vscode from 'vscode';
-import * as ra from "../rust-analyzer-api";
-
-import { Ctx, Cmd } from '../ctx';
-
-export function ssr(ctx: Ctx): Cmd {
- return async () => {
- const client = ctx.client;
- if (!client) return;
-
- const options: vscode.InputBoxOptions = {
- value: "() ==>> ()",
- prompt: "Enter request, for example 'Foo($a:expr) ==> Foo::new($a)' ",
- validateInput: async (x: string) => {
- try {
- await client.sendRequest(ra.ssr, { query: x, parseOnly: true });
- } catch (e) {
- return e.toString();
- }
- return null;
- }
- };
- const request = await vscode.window.showInputBox(options);
- if (!request) return;
-
- const edit = await client.sendRequest(ra.ssr, { query: request, parseOnly: false });
-
- await vscode.workspace.applyEdit(client.protocol2CodeConverter.asWorkspaceEdit(edit));
- };
-}
+++ /dev/null
-import * as vscode from 'vscode';
-import * as ra from '../rust-analyzer-api';
-
-import { Ctx, Cmd, Disposable } from '../ctx';
-import { isRustDocument, RustEditor, isRustEditor, sleep } from '../util';
-
-const AST_FILE_SCHEME = "rust-analyzer";
-
-// Opens the virtual file that will show the syntax tree
-//
-// The contents of the file come from the `TextDocumentContentProvider`
-export function syntaxTree(ctx: Ctx): Cmd {
- const tdcp = new TextDocumentContentProvider(ctx);
-
- void new AstInspector(ctx);
-
- ctx.pushCleanup(vscode.workspace.registerTextDocumentContentProvider(AST_FILE_SCHEME, tdcp));
- ctx.pushCleanup(vscode.languages.setLanguageConfiguration("ra_syntax_tree", {
- brackets: [["[", ")"]],
- }));
-
- return async () => {
- const editor = vscode.window.activeTextEditor;
- const rangeEnabled = !!editor && !editor.selection.isEmpty;
-
- const uri = rangeEnabled
- ? vscode.Uri.parse(`${tdcp.uri.toString()}?range=true`)
- : tdcp.uri;
-
- const document = await vscode.workspace.openTextDocument(uri);
-
- tdcp.eventEmitter.fire(uri);
-
- void await vscode.window.showTextDocument(document, {
- viewColumn: vscode.ViewColumn.Two,
- preserveFocus: true
- });
- };
-}
-
-class TextDocumentContentProvider implements vscode.TextDocumentContentProvider {
- readonly uri = vscode.Uri.parse('rust-analyzer://syntaxtree/tree.rast');
- readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
-
-
- constructor(private readonly ctx: Ctx) {
- vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions);
- vscode.window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, ctx.subscriptions);
- }
-
- private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
- if (isRustDocument(event.document)) {
- // We need to order this after language server updates, but there's no API for that.
- // Hence, good old sleep().
- void sleep(10).then(() => this.eventEmitter.fire(this.uri));
- }
- }
- private onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) {
- if (editor && isRustEditor(editor)) {
- this.eventEmitter.fire(this.uri);
- }
- }
-
- provideTextDocumentContent(uri: vscode.Uri, ct: vscode.CancellationToken): vscode.ProviderResult<string> {
- const rustEditor = this.ctx.activeRustEditor;
- if (!rustEditor) return '';
-
- // When the range based query is enabled we take the range of the selection
- const range = uri.query === 'range=true' && !rustEditor.selection.isEmpty
- ? this.ctx.client.code2ProtocolConverter.asRange(rustEditor.selection)
- : null;
-
- const params = { textDocument: { uri: rustEditor.document.uri.toString() }, range, };
- return this.ctx.client.sendRequest(ra.syntaxTree, params, ct);
- }
-
- get onDidChange(): vscode.Event<vscode.Uri> {
- return this.eventEmitter.event;
- }
-}
-
-
-// FIXME: consider implementing this via the Tree View API?
-// https://code.visualstudio.com/api/extension-guides/tree-view
-class AstInspector implements vscode.HoverProvider, vscode.DefinitionProvider, Disposable {
- private readonly astDecorationType = vscode.window.createTextEditorDecorationType({
- borderColor: new vscode.ThemeColor('rust_analyzer.syntaxTreeBorder'),
- borderStyle: "solid",
- borderWidth: "2px",
-
- });
- private rustEditor: undefined | RustEditor;
-
- // Lazy rust token range -> syntax tree file range.
- private readonly rust2Ast = new Lazy(() => {
- const astEditor = this.findAstTextEditor();
- if (!this.rustEditor || !astEditor) return undefined;
-
- const buf: [vscode.Range, vscode.Range][] = [];
- for (let i = 0; i < astEditor.document.lineCount; ++i) {
- const astLine = astEditor.document.lineAt(i);
-
- // Heuristically look for nodes with quoted text (which are token nodes)
- const isTokenNode = astLine.text.lastIndexOf('"') >= 0;
- if (!isTokenNode) continue;
-
- const rustRange = this.parseRustTextRange(this.rustEditor.document, astLine.text);
- if (!rustRange) continue;
-
- buf.push([rustRange, this.findAstNodeRange(astLine)]);
- }
- return buf;
- });
-
- constructor(ctx: Ctx) {
- ctx.pushCleanup(vscode.languages.registerHoverProvider({ scheme: AST_FILE_SCHEME }, this));
- ctx.pushCleanup(vscode.languages.registerDefinitionProvider({ language: "rust" }, this));
- vscode.workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, ctx.subscriptions);
- vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, ctx.subscriptions);
- vscode.window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, ctx.subscriptions);
-
- ctx.pushCleanup(this);
- }
- dispose() {
- this.setRustEditor(undefined);
- }
-
- private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) {
- if (this.rustEditor && event.document.uri.toString() === this.rustEditor.document.uri.toString()) {
- this.rust2Ast.reset();
- }
- }
-
- private onDidCloseTextDocument(doc: vscode.TextDocument) {
- if (this.rustEditor && doc.uri.toString() === this.rustEditor.document.uri.toString()) {
- this.setRustEditor(undefined);
- }
- }
-
- private onDidChangeVisibleTextEditors(editors: vscode.TextEditor[]) {
- if (!this.findAstTextEditor()) {
- this.setRustEditor(undefined);
- return;
- }
- this.setRustEditor(editors.find(isRustEditor));
- }
-
- private findAstTextEditor(): undefined | vscode.TextEditor {
- return vscode.window.visibleTextEditors.find(it => it.document.uri.scheme === AST_FILE_SCHEME);
- }
-
- private setRustEditor(newRustEditor: undefined | RustEditor) {
- if (this.rustEditor && this.rustEditor !== newRustEditor) {
- this.rustEditor.setDecorations(this.astDecorationType, []);
- this.rust2Ast.reset();
- }
- this.rustEditor = newRustEditor;
- }
-
- // additional positional params are omitted
- provideDefinition(doc: vscode.TextDocument, pos: vscode.Position): vscode.ProviderResult<vscode.DefinitionLink[]> {
- if (!this.rustEditor || doc.uri.toString() !== this.rustEditor.document.uri.toString()) return;
-
- const astEditor = this.findAstTextEditor();
- if (!astEditor) return;
-
- const rust2AstRanges = this.rust2Ast.get()?.find(([rustRange, _]) => rustRange.contains(pos));
- if (!rust2AstRanges) return;
-
- const [rustFileRange, astFileRange] = rust2AstRanges;
-
- astEditor.revealRange(astFileRange);
- astEditor.selection = new vscode.Selection(astFileRange.start, astFileRange.end);
-
- return [{
- targetRange: astFileRange,
- targetUri: astEditor.document.uri,
- originSelectionRange: rustFileRange,
- targetSelectionRange: astFileRange,
- }];
- }
-
- // additional positional params are omitted
- provideHover(doc: vscode.TextDocument, hoverPosition: vscode.Position): vscode.ProviderResult<vscode.Hover> {
- if (!this.rustEditor) return;
-
- const astFileLine = doc.lineAt(hoverPosition.line);
-
- const rustFileRange = this.parseRustTextRange(this.rustEditor.document, astFileLine.text);
- if (!rustFileRange) return;
-
- this.rustEditor.setDecorations(this.astDecorationType, [rustFileRange]);
- this.rustEditor.revealRange(rustFileRange);
-
- const rustSourceCode = this.rustEditor.document.getText(rustFileRange);
- const astFileRange = this.findAstNodeRange(astFileLine);
-
- return new vscode.Hover(["```rust\n" + rustSourceCode + "\n```"], astFileRange);
- }
-
- private findAstNodeRange(astLine: vscode.TextLine): vscode.Range {
- const lineOffset = astLine.range.start;
- const begin = lineOffset.translate(undefined, astLine.firstNonWhitespaceCharacterIndex);
- const end = lineOffset.translate(undefined, astLine.text.trimEnd().length);
- return new vscode.Range(begin, end);
- }
-
- private parseRustTextRange(doc: vscode.TextDocument, astLine: string): undefined | vscode.Range {
- const parsedRange = /(\d+)\.\.(\d+)/.exec(astLine);
- if (!parsedRange) return;
-
- const [begin, end] = parsedRange
- .slice(1)
- .map(off => this.positionAt(doc, +off));
-
- return new vscode.Range(begin, end);
- }
-
- // Memoize the last value, otherwise the CPU is at 100% single core
- // with quadratic lookups when we build rust2Ast cache
- cache?: { doc: vscode.TextDocument; offset: number; line: number };
-
- positionAt(doc: vscode.TextDocument, targetOffset: number): vscode.Position {
- if (doc.eol === vscode.EndOfLine.LF) {
- return doc.positionAt(targetOffset);
- }
-
- // Dirty workaround for crlf line endings
- // We are still in this prehistoric era of carriage returns here...
-
- let line = 0;
- let offset = 0;
-
- const cache = this.cache;
- if (cache?.doc === doc && cache.offset <= targetOffset) {
- ({ line, offset } = cache);
- }
-
- while (true) {
- const lineLenWithLf = doc.lineAt(line).text.length + 1;
- if (offset + lineLenWithLf > targetOffset) {
- this.cache = { doc, offset, line };
- return doc.positionAt(targetOffset + line);
- }
- offset += lineLenWithLf;
- line += 1;
- }
- }
-}
-
-class Lazy<T> {
- val: undefined | T;
-
- constructor(private readonly compute: () => undefined | T) { }
-
- get() {
- return this.val ?? (this.val = this.compute());
- }
-
- reset() {
- this.val = undefined;
- }
-}
+++ /dev/null
-import * as vscode from 'vscode';
-import { Ctx, Cmd } from '../ctx';
-
-export function toggleInlayHints(ctx: Ctx): Cmd {
- return async () => {
- await vscode
- .workspace
- .getConfiguration(`${ctx.config.rootSection}.inlayHints`)
- .update('enable', !ctx.config.inlayHints.enable, vscode.ConfigurationTarget.Workspace);
- };
-}
import * as os from "os";
import * as vscode from 'vscode';
import * as path from 'path';
-import * as ra from './rust-analyzer-api';
+import * as ra from './lsp_ext';
import { Cargo } from './cargo';
import { Ctx } from "./ctx";
import * as lc from "vscode-languageclient";
import * as vscode from 'vscode';
-import * as ra from './rust-analyzer-api';
+import * as ra from './lsp_ext';
import { Ctx, Disposable } from './ctx';
import { sendRequestWithRetry, isRustDocument, RustDocument, RustEditor, sleep } from './util';
--- /dev/null
+/**
+ * This file mirrors `crates/rust-analyzer/src/req.rs` declarations.
+ */
+
+import * as lc from "vscode-languageclient";
+
+export const analyzerStatus = new lc.RequestType<null, string, void>("rust-analyzer/analyzerStatus");
+
+export const collectGarbage = new lc.RequestType<null, null, void>("rust-analyzer/collectGarbage");
+
+export interface SyntaxTreeParams {
+ textDocument: lc.TextDocumentIdentifier;
+ range: lc.Range | null;
+}
+export const syntaxTree = new lc.RequestType<SyntaxTreeParams, string, void>("rust-analyzer/syntaxTree");
+
+
+export interface ExpandMacroParams {
+ textDocument: lc.TextDocumentIdentifier;
+ position: lc.Position;
+}
+export interface ExpandedMacro {
+ name: string;
+ expansion: string;
+}
+export const expandMacro = new lc.RequestType<ExpandMacroParams, ExpandedMacro | null, void>("rust-analyzer/expandMacro");
+
+export interface MatchingBraceParams {
+ textDocument: lc.TextDocumentIdentifier;
+ positions: lc.Position[];
+}
+export const matchingBrace = new lc.RequestType<MatchingBraceParams, lc.Position[], void>("experimental/matchingBrace");
+
+export const parentModule = new lc.RequestType<lc.TextDocumentPositionParams, lc.LocationLink[], void>("experimental/parentModule");
+
+export interface JoinLinesParams {
+ textDocument: lc.TextDocumentIdentifier;
+ ranges: lc.Range[];
+}
+export const joinLines = new lc.RequestType<JoinLinesParams, lc.TextEdit[], void>("experimental/joinLines");
+
+export const onEnter = new lc.RequestType<lc.TextDocumentPositionParams, lc.TextEdit[], void>("experimental/onEnter");
+
+export interface RunnablesParams {
+ textDocument: lc.TextDocumentIdentifier;
+ position: lc.Position | null;
+}
+export interface Runnable {
+ range: lc.Range;
+ label: string;
+ bin: string;
+ args: string[];
+ extraArgs: string[];
+ env: { [key: string]: string };
+ cwd: string | null;
+}
+export const runnables = new lc.RequestType<RunnablesParams, Runnable[], void>("rust-analyzer/runnables");
+
+export type InlayHint = InlayHint.TypeHint | InlayHint.ParamHint | InlayHint.ChainingHint;
+
+export namespace InlayHint {
+ export const enum Kind {
+ TypeHint = "TypeHint",
+ ParamHint = "ParameterHint",
+ ChainingHint = "ChainingHint",
+ }
+ interface Common {
+ range: lc.Range;
+ label: string;
+ }
+ export type TypeHint = Common & { kind: Kind.TypeHint };
+ export type ParamHint = Common & { kind: Kind.ParamHint };
+ export type ChainingHint = Common & { kind: Kind.ChainingHint };
+}
+export interface InlayHintsParams {
+ textDocument: lc.TextDocumentIdentifier;
+}
+export const inlayHints = new lc.RequestType<InlayHintsParams, InlayHint[], void>("rust-analyzer/inlayHints");
+
+export interface SsrParams {
+ query: string;
+ parseOnly: boolean;
+}
+export const ssr = new lc.RequestType<SsrParams, lc.WorkspaceEdit, void>('experimental/ssr');
ctx.registerCommand('runSingle', commands.runSingle);
ctx.registerCommand('debugSingle', commands.debugSingle);
ctx.registerCommand('showReferences', commands.showReferences);
- ctx.registerCommand('applySourceChange', commands.applySourceChange);
ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand);
ctx.registerCommand('applyActionGroup', commands.applyActionGroup);
--- /dev/null
+import * as vscode from 'vscode';
+import * as lc from 'vscode-languageclient';
+import * as ra from './lsp_ext';
+
+import { Ctx, Cmd } from './ctx';
+import { startDebugSession, getDebugConfiguration } from './debug';
+
+const quickPickButtons = [{ iconPath: new vscode.ThemeIcon("save"), tooltip: "Save as a launch.json configurtation." }];
+
+export async function selectRunnable(ctx: Ctx, prevRunnable?: RunnableQuickPick, debuggeeOnly = false, showButtons: boolean = true): Promise<RunnableQuickPick | undefined> {
+ const editor = ctx.activeRustEditor;
+ const client = ctx.client;
+ if (!editor || !client) return;
+
+ const textDocument: lc.TextDocumentIdentifier = {
+ uri: editor.document.uri.toString(),
+ };
+
+ const runnables = await client.sendRequest(ra.runnables, {
+ textDocument,
+ position: client.code2ProtocolConverter.asPosition(
+ editor.selection.active,
+ ),
+ });
+ const items: RunnableQuickPick[] = [];
+ if (prevRunnable) {
+ items.push(prevRunnable);
+ }
+ for (const r of runnables) {
+ if (
+ prevRunnable &&
+ JSON.stringify(prevRunnable.runnable) === JSON.stringify(r)
+ ) {
+ continue;
+ }
+
+ if (debuggeeOnly && (r.label.startsWith('doctest') || r.label.startsWith('cargo'))) {
+ continue;
+ }
+ items.push(new RunnableQuickPick(r));
+ }
+
+ if (items.length === 0) {
+ // it is the debug case, run always has at least 'cargo check ...'
+ // see crates\rust-analyzer\src\main_loop\handlers.rs, handle_runnables
+ vscode.window.showErrorMessage("There's no debug target!");
+ return;
+ }
+
+ return await new Promise((resolve) => {
+ const disposables: vscode.Disposable[] = [];
+ const close = (result?: RunnableQuickPick) => {
+ resolve(result);
+ disposables.forEach(d => d.dispose());
+ };
+
+ const quickPick = vscode.window.createQuickPick<RunnableQuickPick>();
+ quickPick.items = items;
+ quickPick.title = "Select Runnable";
+ if (showButtons) {
+ quickPick.buttons = quickPickButtons;
+ }
+ disposables.push(
+ quickPick.onDidHide(() => close()),
+ quickPick.onDidAccept(() => close(quickPick.selectedItems[0])),
+ quickPick.onDidTriggerButton((_button) => {
+ (async () => await makeDebugConfig(ctx, quickPick.activeItems[0]))();
+ close();
+ }),
+ quickPick.onDidChangeActive((active) => {
+ if (showButtons && active.length > 0) {
+ if (active[0].label.startsWith('cargo')) {
+ // save button makes no sense for `cargo test` or `cargo check`
+ quickPick.buttons = [];
+ } else if (quickPick.buttons.length === 0) {
+ quickPick.buttons = quickPickButtons;
+ }
+ }
+ }),
+ quickPick
+ );
+ quickPick.show();
+ });
+}
+
+export function runSingle(ctx: Ctx): Cmd {
+ return async (runnable: ra.Runnable) => {
+ const editor = ctx.activeRustEditor;
+ if (!editor) return;
+
+ const task = createTask(runnable);
+ task.group = vscode.TaskGroup.Build;
+ task.presentationOptions = {
+ reveal: vscode.TaskRevealKind.Always,
+ panel: vscode.TaskPanelKind.Dedicated,
+ clear: true,
+ };
+
+ return vscode.tasks.executeTask(task);
+ };
+}
+
+export function debug(ctx: Ctx): Cmd {
+ let prevDebuggee: RunnableQuickPick | undefined;
+
+ return async () => {
+ const item = await selectRunnable(ctx, prevDebuggee, true);
+ if (!item) return;
+
+ item.detail = 'restart';
+ prevDebuggee = item;
+ return await startDebugSession(ctx, item.runnable);
+ };
+}
+
+export function debugSingle(ctx: Ctx): Cmd {
+ return async (config: ra.Runnable) => {
+ await startDebugSession(ctx, config);
+ };
+}
+
+async function makeDebugConfig(ctx: Ctx, item: RunnableQuickPick): Promise<void> {
+ const scope = ctx.activeRustEditor?.document.uri;
+ if (!scope) return;
+
+ const debugConfig = await getDebugConfiguration(ctx, item.runnable);
+ if (!debugConfig) return;
+
+ const wsLaunchSection = vscode.workspace.getConfiguration("launch", scope);
+ const configurations = wsLaunchSection.get<any[]>("configurations") || [];
+
+ const index = configurations.findIndex(c => c.name === debugConfig.name);
+ if (index !== -1) {
+ const answer = await vscode.window.showErrorMessage(`Launch configuration '${debugConfig.name}' already exists!`, 'Cancel', 'Update');
+ if (answer === "Cancel") return;
+
+ configurations[index] = debugConfig;
+ } else {
+ configurations.push(debugConfig);
+ }
+
+ await wsLaunchSection.update("configurations", configurations);
+}
+
+export function newDebugConfig(ctx: Ctx): Cmd {
+ return async () => {
+ const item = await selectRunnable(ctx, undefined, true, false);
+ if (!item) return;
+
+ await makeDebugConfig(ctx, item);
+ };
+}
+
+export class RunnableQuickPick implements vscode.QuickPickItem {
+ public label: string;
+ public description?: string | undefined;
+ public detail?: string | undefined;
+ public picked?: boolean | undefined;
+
+ constructor(public runnable: ra.Runnable) {
+ this.label = runnable.label;
+ }
+}
+
+interface CargoTaskDefinition extends vscode.TaskDefinition {
+ type: 'cargo';
+ label: string;
+ command: string;
+ args: string[];
+ env?: { [key: string]: string };
+}
+
+export function createTask(spec: ra.Runnable): vscode.Task {
+ const TASK_SOURCE = 'Rust';
+ const definition: CargoTaskDefinition = {
+ type: 'cargo',
+ label: spec.label,
+ command: spec.bin,
+ args: spec.extraArgs ? [...spec.args, '--', ...spec.extraArgs] : spec.args,
+ env: spec.env,
+ };
+
+ const execOption: vscode.ShellExecutionOptions = {
+ cwd: spec.cwd || '.',
+ env: definition.env,
+ };
+ const exec = new vscode.ShellExecution(
+ definition.command,
+ definition.args,
+ execOption,
+ );
+
+ const f = vscode.workspace.workspaceFolders![0];
+ const t = new vscode.Task(
+ definition,
+ f,
+ definition.label,
+ TASK_SOURCE,
+ exec,
+ ['$rustc'],
+ );
+ t.presentationOptions.clear = true;
+ return t;
+}
+++ /dev/null
-/**
- * This file mirrors `crates/rust-analyzer/src/req.rs` declarations.
- */
-
-import * as lc from "vscode-languageclient";
-
-type Option<T> = null | T;
-type Vec<T> = T[];
-type FxHashMap<K extends PropertyKey, V> = Record<K, V>;
-
-function request<TParams, TResult>(method: string) {
- return new lc.RequestType<TParams, TResult, unknown>(`rust-analyzer/${method}`);
-}
-function notification<TParam>(method: string) {
- return new lc.NotificationType<TParam>(method);
-}
-
-
-export const analyzerStatus = request<null, string>("analyzerStatus");
-
-
-export const collectGarbage = request<null, null>("collectGarbage");
-
-
-export interface SyntaxTreeParams {
- textDocument: lc.TextDocumentIdentifier;
- range: Option<lc.Range>;
-}
-export const syntaxTree = request<SyntaxTreeParams, string>("syntaxTree");
-
-
-export interface ExpandMacroParams {
- textDocument: lc.TextDocumentIdentifier;
- position: Option<lc.Position>;
-}
-export interface ExpandedMacro {
- name: string;
- expansion: string;
-}
-export const expandMacro = request<ExpandMacroParams, Option<ExpandedMacro>>("expandMacro");
-
-
-export interface MatchingBraceParams {
- textDocument: lc.TextDocumentIdentifier;
- positions: lc.Position[];
-}
-export const matchingBrace = new lc.RequestType<MatchingBraceParams, lc.Position[], unknown>('experimental/matchingBrace');
-
-export interface PublishDecorationsParams {
- uri: string;
- decorations: Vec<Decoration>;
-}
-export interface Decoration {
- range: lc.Range;
- tag: string;
- bindingHash: Option<string>;
-}
-export const decorationsRequest = request<lc.TextDocumentIdentifier, Vec<Decoration>>("decorationsRequest");
-
-
-export const parentModule = request<lc.TextDocumentPositionParams, Vec<lc.Location>>("parentModule");
-
-
-export interface JoinLinesParams {
- textDocument: lc.TextDocumentIdentifier;
- ranges: lc.Range[];
-}
-export const joinLines = new lc.RequestType<JoinLinesParams, lc.TextEdit[], unknown>('experimental/joinLines');
-
-
-export const onEnter = request<lc.TextDocumentPositionParams, Option<lc.WorkspaceEdit>>("onEnter");
-
-export interface RunnablesParams {
- textDocument: lc.TextDocumentIdentifier;
- position: Option<lc.Position>;
-}
-export interface Runnable {
- range: lc.Range;
- label: string;
- bin: string;
- args: Vec<string>;
- extraArgs: Vec<string>;
- env: FxHashMap<string, string>;
- cwd: Option<string>;
-}
-export const runnables = request<RunnablesParams, Vec<Runnable>>("runnables");
-
-export type InlayHint = InlayHint.TypeHint | InlayHint.ParamHint | InlayHint.ChainingHint;
-
-export namespace InlayHint {
- export const enum Kind {
- TypeHint = "TypeHint",
- ParamHint = "ParameterHint",
- ChainingHint = "ChainingHint",
- }
- interface Common {
- range: lc.Range;
- label: string;
- }
- export type TypeHint = Common & { kind: Kind.TypeHint };
- export type ParamHint = Common & { kind: Kind.ParamHint };
- export type ChainingHint = Common & { kind: Kind.ChainingHint };
-}
-export interface InlayHintsParams {
- textDocument: lc.TextDocumentIdentifier;
-}
-export const inlayHints = request<InlayHintsParams, Vec<InlayHint>>("inlayHints");
-
-
-export interface SsrParams {
- query: string;
- parseOnly: boolean;
-}
-export const ssr = new lc.RequestType<SsrParams, lc.WorkspaceEdit, unknown>('experimental/ssr');
-
-
-export const publishDecorations = notification<PublishDecorationsParams>("publishDecorations");
-
-
-export interface SourceChange {
- label: string;
- workspaceEdit: lc.WorkspaceEdit;
- cursorPosition: Option<lc.TextDocumentPositionParams>;
-}
--- /dev/null
+import * as vscode from 'vscode';
+
+import { assert } from './util';
+
+export async function applySnippetWorkspaceEdit(edit: vscode.WorkspaceEdit) {
+ assert(edit.entries().length === 1, `bad ws edit: ${JSON.stringify(edit)}`);
+ const [uri, edits] = edit.entries()[0];
+
+ const editor = vscode.window.visibleTextEditors.find((it) => it.document.uri.toString() === uri.toString());
+ if (!editor) return;
+ await applySnippetTextEdits(editor, edits);
+}
+
+export async function applySnippetTextEdits(editor: vscode.TextEditor, edits: vscode.TextEdit[]) {
+ let selection: vscode.Selection | undefined = undefined;
+ let lineDelta = 0;
+ await editor.edit((builder) => {
+ for (const indel of edits) {
+ const parsed = parseSnippet(indel.newText);
+ if (parsed) {
+ const [newText, [placeholderStart, placeholderLength]] = parsed;
+ const prefix = newText.substr(0, placeholderStart);
+ const lastNewline = prefix.lastIndexOf('\n');
+
+ const startLine = indel.range.start.line + lineDelta + countLines(prefix);
+ const startColumn = lastNewline === -1 ?
+ indel.range.start.character + placeholderStart
+ : prefix.length - lastNewline - 1;
+ const endColumn = startColumn + placeholderLength;
+ selection = new vscode.Selection(
+ new vscode.Position(startLine, startColumn),
+ new vscode.Position(startLine, endColumn),
+ );
+ builder.replace(indel.range, newText);
+ } else {
+ lineDelta = countLines(indel.newText) - (indel.range.end.line - indel.range.start.line);
+ builder.replace(indel.range, indel.newText);
+ }
+ }
+ });
+ if (selection) editor.selection = selection;
+}
+
+function parseSnippet(snip: string): [string, [number, number]] | undefined {
+ const m = snip.match(/\$(0|\{0:([^}]*)\})/);
+ if (!m) return undefined;
+ const placeholder = m[2] ?? "";
+ const range: [number, number] = [m.index!!, placeholder.length];
+ const insert = snip.replace(m[0], placeholder);
+ return [insert, range];
+}
+
+function countLines(text: string): number {
+ return (text.match(/\n/g) || []).length;
+}
+++ /dev/null
-import * as vscode from 'vscode';
-import * as lc from 'vscode-languageclient';
-import * as ra from './rust-analyzer-api';
-
-import { Ctx } from './ctx';
-
-export async function applySourceChange(ctx: Ctx, change: ra.SourceChange) {
- const client = ctx.client;
- if (!client) return;
-
- const wsEdit = client.protocol2CodeConverter.asWorkspaceEdit(
- change.workspaceEdit,
- );
- let created;
- let moved;
- if (change.workspaceEdit.documentChanges) {
- for (const docChange of change.workspaceEdit.documentChanges) {
- if (lc.CreateFile.is(docChange)) {
- created = docChange.uri;
- } else if (lc.RenameFile.is(docChange)) {
- moved = docChange.newUri;
- }
- }
- }
- const toOpen = created || moved;
- const toReveal = change.cursorPosition;
- await vscode.workspace.applyEdit(wsEdit);
- if (toOpen) {
- const toOpenUri = vscode.Uri.parse(toOpen);
- const doc = await vscode.workspace.openTextDocument(toOpenUri);
- await vscode.window.showTextDocument(doc);
- } else if (toReveal) {
- const uri = client.protocol2CodeConverter.asUri(
- toReveal.textDocument.uri,
- );
- const position = client.protocol2CodeConverter.asPosition(
- toReveal.position,
- );
- const editor = vscode.window.activeTextEditor;
- if (!editor || !editor.selection.isEmpty) {
- return;
- }
-
- if (editor.document.uri !== uri) {
- const doc = await vscode.workspace.openTextDocument(uri);
- await vscode.window.showTextDocument(doc);
- }
- editor.selection = new vscode.Selection(position, position);
- editor.revealRange(
- new vscode.Range(position, position),
- vscode.TextEditorRevealType.Default,
- );
- }
-}