feat: fully support onEnter edits inside comments (#823)

- select with range
- multiple cursor
This commit is contained in:
Myriad-Dreamin 2024-11-14 22:21:14 +08:00 committed by GitHub
parent a2eb405430
commit 8b3a0e986a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 168 additions and 23 deletions

View file

@ -20,8 +20,8 @@ use crate::{prelude::*, syntax::node_ancestors, SyntaxRequest};
pub struct OnEnterRequest {
/// The path of the document to get folding ranges for.
pub path: PathBuf,
/// The source code position to request for.
pub position: LspPosition,
/// The source code range to request for.
pub range: LspRange,
}
impl SyntaxRequest for OnEnterRequest {
@ -33,7 +33,8 @@ impl SyntaxRequest for OnEnterRequest {
position_encoding: PositionEncoding,
) -> Option<Self::Response> {
let root = LinkedNode::new(source.root());
let cursor = lsp_to_typst::position(self.position, position_encoding, source)?;
let rng = lsp_to_typst::range(self.range, position_encoding, source)?;
let cursor = rng.end;
let leaf = root.leaf_at_compat(cursor)?;
let worker = OnEnterWorker {
@ -42,13 +43,13 @@ impl SyntaxRequest for OnEnterRequest {
};
if matches!(leaf.kind(), SyntaxKind::LineComment) {
return worker.enter_line_doc_comment(&leaf, cursor);
return worker.enter_line_doc_comment(&leaf, rng);
}
let math_node =
node_ancestors(&leaf).find(|node| matches!(node.kind(), SyntaxKind::Equation));
if let Some(mn) = math_node {
return worker.enter_block_math(mn, cursor);
return worker.enter_block_math(mn, rng);
}
None
@ -68,7 +69,11 @@ impl OnEnterWorker<'_> {
" ".repeat(indent_size)
}
fn enter_line_doc_comment(&self, leaf: &LinkedNode, cursor: usize) -> Option<Vec<TextEdit>> {
fn enter_line_doc_comment(
&self,
leaf: &LinkedNode,
rng: Range<usize>,
) -> Option<Vec<TextEdit>> {
let skipper = |n: &LinkedNode| {
matches!(
n.kind(),
@ -100,8 +105,6 @@ impl OnEnterWorker<'_> {
let indent = self.indent_of(leaf.offset());
// todo: remove_trailing_whitespace
let rng = cursor..cursor;
let edit = TextEdit {
range: typst_to_lsp::range(rng, self.source, self.position_encoding),
new_text: format!("\n{indent}{comment_prefix} $0"),
@ -110,9 +113,13 @@ impl OnEnterWorker<'_> {
Some(vec![edit])
}
fn enter_block_math(&self, math_node: &LinkedNode<'_>, cursor: usize) -> Option<Vec<TextEdit>> {
fn enter_block_math(
&self,
math_node: &LinkedNode<'_>,
rng: Range<usize>,
) -> Option<Vec<TextEdit>> {
let o = math_node.range();
if !o.contains(&cursor) {
if !o.contains(&rng.end) {
return None;
}
@ -124,7 +131,6 @@ impl OnEnterWorker<'_> {
}
let indent = self.indent_of(o.start);
let rng = cursor..cursor;
let edit = TextEdit {
range: typst_to_lsp::range(rng, self.source, self.position_encoding),
// todo: read indent configuration

View file

@ -818,13 +818,10 @@ impl LanguageState {
run_query!(req_id, self.Symbol(pattern))
}
fn on_enter(
&mut self,
req_id: RequestId,
params: TextDocumentPositionParams,
) -> ScheduledResult {
let (path, position) = as_path_pos(params);
run_query!(req_id, self.OnEnter(path, position))
fn on_enter(&mut self, req_id: RequestId, params: OnEnterParams) -> ScheduledResult {
let path = as_path(params.text_document);
let range = params.range;
run_query!(req_id, self.OnEnter(path, range))
}
fn will_rename_files(
@ -1119,9 +1116,22 @@ struct ExportOpts {
page: PageSelection,
}
/// A parameter for the `experimental/onEnter` command.
///
/// @since 3.17.0
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OnEnterParams {
/// The text document.
pub text_document: TextDocumentIdentifier,
/// The visible document range for which `onEnter` edits should be computed.
pub range: Range,
}
struct OnEnter;
impl lsp_types::request::Request for OnEnter {
type Params = TextDocumentPositionParams;
type Params = OnEnterParams;
type Result = Option<Vec<TextEdit>>;
const METHOD: &'static str = "experimental/onEnter";
}

View file

@ -317,6 +317,12 @@
"Do not use semantic tokens for syntax highlighting"
]
},
"tinymist.typingContinueCommentsOnNewline": {
"title": "Continue Comments on Newline",
"markdownDescription": "Whether to prefix newlines after comments with the corresponding comment prefix.",
"type": "boolean",
"default": true
},
"tinymist.onEnterEvent": {
"title": "Handling on enter events",
"description": "Enable or disable [experimental/onEnter](https://github.com/rust-lang/rust-analyzer/blob/master/docs/dev/lsp-extensions.md#on-enter) (LSP onEnter feature) to allow automatic insertion of characters on enter, such as `///` for comments. Note: restarting the editor is required to change this setting.",

View file

@ -78,6 +78,19 @@ export async function doActivate(context: ExtensionContext): Promise<void> {
extensionState.features.dragAndDrop = config.dragAndDrop === "enable";
extensionState.features.onEnter = !!config.onEnterEvent;
extensionState.features.renderDocs = config.renderDocs === "enable";
// Configures advanced language configuration
tinymist.configureLanguage(config["typingContinueCommentsOnNewline"]);
context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration("tinymist.typingContinueCommentsOnNewline")) {
const config = loadTinymistConfig();
// Update language configuration
tinymist.configureLanguage(config["typingContinueCommentsOnNewline"]);
}
}),
);
// Initializes language client
const client = initClient(context, config);
setClient(client);
@ -108,6 +121,7 @@ export async function doActivate(context: ExtensionContext): Promise<void> {
previewSetIsTinymist();
previewActivate(context, false);
}
// Starts language client
return await startClient(client, context);
}

View file

@ -5,9 +5,22 @@ import { applySnippetTextEdits } from "./snippets";
import { activeTypstEditor } from "./util";
import { extensionState } from "./state";
const onEnter = new lc.RequestType<lc.TextDocumentPositionParams, lc.TextEdit[], void>(
"experimental/onEnter",
);
/**
* A parameter literal used in requests to pass a text document and a range inside that
* document.
*/
export interface OnEnterParams {
/**
* The text document.
*/
textDocument: lc.TextDocumentIdentifier;
/**
* The range inside the text document.
*/
range: lc.Range;
}
const onEnter = new lc.RequestType<OnEnterParams, lc.TextEdit[], void>("experimental/onEnter");
export async function onEnterHandler() {
try {
@ -19,6 +32,8 @@ export async function onEnterHandler() {
await vscode.commands.executeCommand("default:type", { text: "\n" });
}
// The code copied from https://github.com/rust-lang/rust-analyzer/blob/fc98e0657abf3ce07eed513e38274c89bbb2f8ad/editors/code/src/commands.ts#L199
// doesn't work, so we change `onEnter` to pass the `range` instead of `position` to the server.
async function handleKeypress() {
if (!extensionState.features.onEnter) return false;
@ -29,7 +44,7 @@ async function handleKeypress() {
const lcEdits = await client
.sendRequest(onEnter, {
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
position: client.code2ProtocolConverter.asPosition(editor.selection.active),
range: client.code2ProtocolConverter.asRange(editor.selection),
})
.catch((_error: any) => {
// client.handleFailedRequest(OnEnterRequest.type, error, null);

View file

@ -1,3 +1,4 @@
import * as vscode from "vscode";
import { LanguageClient, SymbolInformation } from "vscode-languageclient/node";
import { spawnSync } from "child_process";
import { resolve } from "path";
@ -71,6 +72,99 @@ export const tinymist = {
client.outputChannel.show();
}
},
/**
* The code is borrowed from https://github.com/rust-lang/rust-analyzer/blob/fc98e0657abf3ce07eed513e38274c89bbb2f8ad/editors/code/src/config.ts#L98
* Last checked time: 2024-11-14
*
* Sets up additional language configuration that's impossible to do via a
* separate language-configuration.json file. See [1] for more information.
*
* [1]: https://github.com/Microsoft/vscode/issues/11514#issuecomment-244707076
*/
configureLang: undefined as vscode.Disposable | undefined,
configureLanguage(typingContinueCommentsOnNewline: boolean) {
// Only need to dispose of the config if there's a change
if (this.configureLang) {
this.configureLang.dispose();
this.configureLang = undefined;
}
let onEnterRules: vscode.OnEnterRule[] = [
{
// Carry indentation from the previous line
// if it's only whitespace
beforeText: /^\s+$/,
action: { indentAction: vscode.IndentAction.None },
},
{
// After the end of a function/field chain,
// with the semicolon on the same line
beforeText: /^\s+\..*;/,
action: { indentAction: vscode.IndentAction.Outdent },
},
{
// After the end of a function/field chain,
// with semicolon detached from the rest
beforeText: /^\s+;/,
previousLineText: /^\s+\..*/,
action: { indentAction: vscode.IndentAction.Outdent },
},
];
if (typingContinueCommentsOnNewline) {
const indentAction = vscode.IndentAction.None;
onEnterRules = [
...onEnterRules,
{
// Doc single-line comment
// e.g. ///|
beforeText: /^\s*\/{3}.*$/,
action: { indentAction, appendText: "/// " },
},
{
// Parent doc single-line comment
// e.g. //!|
beforeText: /^\s*\/{2}\!.*$/,
action: { indentAction, appendText: "//! " },
},
{
// Begins an auto-closed multi-line comment (standard or parent doc)
// e.g. /** | */ or /*! | */
beforeText: /^\s*\/\*(\*|\!)(?!\/)([^\*]|\*(?!\/))*$/,
afterText: /^\s*\*\/$/,
action: {
indentAction: vscode.IndentAction.IndentOutdent,
appendText: " * ",
},
},
{
// Begins a multi-line comment (standard or parent doc)
// e.g. /** ...| or /*! ...|
beforeText: /^\s*\/\*(\*|\!)(?!\/)([^\*]|\*(?!\/))*$/,
action: { indentAction, appendText: " * " },
},
{
// Continues a multi-line comment
// e.g. * ...|
beforeText: /^(\ \ )*\ \*(\ ([^\*]|\*(?!\/))*)?$/,
action: { indentAction, appendText: "* " },
},
{
// Dedents after closing a multi-line comment
// e.g. */|
beforeText: /^(\ \ )*\ \*\/\s*$/,
action: { indentAction, removeText: 1 },
},
];
}
console.log("Setting up language configuration", typingContinueCommentsOnNewline);
this.configureLang = vscode.languages.setLanguageConfiguration("typst", {
onEnterRules,
});
},
};
/// kill the probe task after 60s