Skip to content

Commit c6c7c09

Browse files
feat: change selections
1 parent a9d6d1f commit c6c7c09

File tree

7 files changed

+151
-73
lines changed

7 files changed

+151
-73
lines changed

crates/tinymist/src/cmd.rs

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ use crate::lsp::query::{run_query, LspClientExt};
2626
use crate::tool::ast::AstRepr;
2727
use crate::tool::package::InitTask;
2828

29+
mod pin_focus;
30+
2931
/// See [`ProjectTask`].
3032
#[derive(Debug, Clone, Default, Deserialize)]
3133
struct ExportOpts {
@@ -63,6 +65,8 @@ struct ExportSyntaxRangeOpts {
6365
}
6466

6567
/// Here are implemented the handlers for each command.
68+
/// Some command group are placed in the sub crates:
69+
/// - [`pin_focus`] implements commands for pinning and focusing documents.
6670
impl ServerState {
6771
/// Export the current document as PDF file(s).
6872
pub fn export_pdf(&mut self, req_id: RequestId, mut args: Vec<JsonValue>) -> ScheduledResult {
@@ -277,35 +281,6 @@ impl ServerState {
277281
just_ok(JsonValue::Null)
278282
}
279283

280-
/// Pin main file to some path.
281-
pub fn pin_document(&mut self, mut args: Vec<JsonValue>) -> AnySchedulableResponse {
282-
let entry = get_arg!(args[0] as Option<PathBuf>).map(From::from);
283-
284-
let update_result = self.pin_main_file(entry.clone());
285-
update_result.map_err(|err| internal_error(format!("could not pin file: {err}")))?;
286-
287-
log::info!("file pinned: {entry:?}");
288-
just_ok(JsonValue::Null)
289-
}
290-
291-
/// Focus main file to some path.
292-
pub fn focus_document(&mut self, mut args: Vec<JsonValue>) -> AnySchedulableResponse {
293-
let entry = get_arg!(args[0] as Option<PathBuf>).map(From::from);
294-
295-
if !self.ever_manual_focusing {
296-
self.ever_manual_focusing = true;
297-
log::info!("first manual focusing is coming");
298-
}
299-
300-
let ok = self.focus_main_file(entry.clone());
301-
let ok = ok.map_err(|err| internal_error(format!("could not focus file: {err}")))?;
302-
303-
if ok {
304-
log::info!("file focused: {entry:?}");
305-
}
306-
just_ok(JsonValue::Null)
307-
}
308-
309284
/// Starts a preview instance.
310285
#[cfg(feature = "preview")]
311286
pub fn do_start_preview(

crates/tinymist/src/cmd/pin_focus.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//! Tinymist LSP commands: pinning and focusing documents.
2+
3+
use super::*;
4+
5+
type Selections = Vec<LspRange>;
6+
7+
/// Extra options for the focus command.
8+
#[derive(Debug, Clone, Default, Deserialize)]
9+
struct FocusDocOpts {
10+
/// An optional list of selections to be set after focusing. The first
11+
/// selection is the primary one.
12+
#[serde(default)]
13+
selections: Option<Selections>,
14+
}
15+
16+
impl ServerState {
17+
/// Pins main file to some path.
18+
pub fn pin_document(&mut self, mut args: Vec<JsonValue>) -> AnySchedulableResponse {
19+
let entry = get_arg!(args[0] as Option<PathBuf>).map(From::from);
20+
let opts = get_arg_or_default!(args[1] as FocusDocOpts);
21+
22+
let update_result = self.pin_main_file(entry.clone());
23+
update_result.map_err(|err| internal_error(format!("could not pin file: {err}")))?;
24+
25+
self.do_change_selections(opts.selections);
26+
27+
log::info!("file pinned: {entry:?}");
28+
just_ok(JsonValue::Null)
29+
}
30+
31+
/// Focuses main file to some path.
32+
pub fn focus_document(&mut self, mut args: Vec<JsonValue>) -> AnySchedulableResponse {
33+
let entry = get_arg!(args[0] as Option<PathBuf>).map(From::from);
34+
35+
if !self.ever_manual_focusing {
36+
self.ever_manual_focusing = true;
37+
log::info!("first manual focusing is coming");
38+
}
39+
40+
let ok = self.focus_main_file(entry.clone());
41+
let ok = ok.map_err(|err| internal_error(format!("could not focus file: {err}")))?;
42+
43+
if ok {
44+
log::info!("file focused: {entry:?}");
45+
}
46+
just_ok(JsonValue::Null)
47+
}
48+
49+
/// Changes the selections.
50+
pub fn change_selections(&mut self, mut args: Vec<JsonValue>) -> AnySchedulableResponse {
51+
let selections = get_arg!(args[0] as Selections);
52+
self.do_change_selections(Some(selections));
53+
just_ok(JsonValue::Null)
54+
}
55+
}

crates/tinymist/src/input.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use lsp_types::*;
22
use reflexo_typst::Bytes;
3-
use tinymist_query::{to_typst_range, PositionEncoding};
3+
use tinymist_query::{to_typst_range, LspRange, PositionEncoding};
44
use tinymist_std::error::prelude::*;
55
use tinymist_std::ImmutPath;
66
use typst::{diag::FileResult, syntax::Source};
@@ -193,6 +193,16 @@ impl ServerState {
193193
Ok(false) => {}
194194
}
195195
}
196+
197+
/// Does the selection change. It accepts an optional list of selections.
198+
/// The first selection is the primary one.
199+
pub fn do_change_selections(&mut self, selections: Option<Vec<LspRange>>) -> Option<()> {
200+
let selections = selections?;
201+
let primary_selection = selections.into_iter().next()?;
202+
203+
self.focusing_selection = Some(primary_selection);
204+
Some(())
205+
}
196206
}
197207

198208
/// Task input resolution.

crates/tinymist/src/server.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use lsp_types::request::ShowMessageRequest;
77
use lsp_types::*;
88
use reflexo::debug_loc::LspPosition;
99
use sync_ls::*;
10-
use tinymist_query::{OnExportRequest, ServerInfoResponse};
10+
use tinymist_query::{LspRange, OnExportRequest, ServerInfoResponse};
1111
use tinymist_std::error::prelude::*;
1212
use tinymist_std::ImmutPath;
1313
use tinymist_task::ProjectTask;
@@ -76,7 +76,11 @@ pub struct ServerState {
7676
pub pinning_by_browsing_preview: bool,
7777
/// The client focusing file.
7878
pub focusing: Option<ImmutPath>,
79-
/// The client focusing position.
79+
/// The client focusing range. There might be multiple ranges selected by
80+
/// the client at the same time, but we only record the primary one.
81+
pub focusing_selection: Option<LspRange>,
82+
/// The client focusing position, implicitly. It is inferred from the LSP
83+
/// requests so may be inaccurate.
8084
pub implicit_position: Option<LspPosition>,
8185
/// The client ever focused implicitly by activities.
8286
pub ever_focusing_by_activities: bool,
@@ -136,6 +140,7 @@ impl ServerState {
136140
pinning_by_preview: false,
137141
pinning_by_browsing_preview: false,
138142
focusing: None,
143+
focusing_selection: None,
139144
implicit_position: None,
140145
formatter,
141146
user_action: UserActionTask,
@@ -273,6 +278,7 @@ impl ServerState {
273278
.with_command("tinymist.doClearCache", State::clear_cache)
274279
.with_command("tinymist.pinMain", State::pin_document)
275280
.with_command("tinymist.focusMain", State::focus_document)
281+
.with_command("tinymist.changeSelections", State::change_selections)
276282
.with_command("tinymist.doInitTemplate", State::init_template)
277283
.with_command("tinymist.doGetTemplateEntry", State::get_template_entry)
278284
.with_command_("tinymist.interactCodeContext", State::interact_code_context)

editors/vscode/src/extension.shared.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ export async function tinymistActivate(
103103

104104
configureEditorAndLanguage(context, trait);
105105

106+
// todo: a better model is `focusing.transfer(newClient)`, since the old request must not be cancelled and must
107+
// be sent to the new client.
108+
extensionState.mut.focusing.reset();
106109
// Initializes language client
107110
if (extensionState.features.lsp) {
108111
const executable = tinymist.probeEnvPath("tinymist.serverPath", config.serverPath);

editors/vscode/src/extension.ts

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,11 @@ async function languageActivate(context: IContext) {
152152
// Find first document to focus
153153
const editor = window.activeTextEditor;
154154
if (isTypstDocument(editor?.document)) {
155-
focusDocPath(editor.document);
155+
focusDoc(editor.document, editor);
156156
} else {
157157
window.visibleTextEditors.forEach((editor) => {
158158
if (isTypstDocument(editor.document)) {
159-
focusDocPath(editor.document);
159+
focusDoc(editor.document, editor);
160160
}
161161
});
162162
}
@@ -170,28 +170,16 @@ async function languageActivate(context: IContext) {
170170
// if (langId === "plaintext") {
171171
// console.log("plaintext", langId, editor?.document.uri.fsPath);
172172
// }
173-
if (!isTypstDocument(editor?.document)) {
174-
// console.log("not typst", langId, editor?.document.uri.fsPath);
175-
return focusDocPath(undefined);
176-
}
177-
return focusDocPath(editor?.document);
173+
return focusDoc(isTypstDocument(editor?.document) ? editor?.document : undefined, editor);
178174
}),
179-
);
180-
context.subscriptions.push(
181175
vscode.workspace.onDidOpenTextDocument((doc: vscode.TextDocument) => {
182176
if (doc.isUntitled && window.activeTextEditor?.document === doc) {
183-
if (isTypstDocument(doc)) {
184-
return focusDocPath(doc);
185-
} else {
186-
return focusDocPath(undefined);
187-
}
177+
return focusDoc(isTypstDocument(doc) ? doc : undefined, window.activeTextEditor);
188178
}
189179
}),
190-
);
191-
context.subscriptions.push(
192180
vscode.workspace.onDidCloseTextDocument((doc: vscode.TextDocument) => {
193181
if (extensionState.mut.focusing.doc === doc) {
194-
focusDocPath(undefined);
182+
focusDoc(undefined);
195183
}
196184
}),
197185
);
@@ -244,21 +232,15 @@ async function openExternal(target: string): Promise<void> {
244232
await vscode.env.openExternal(uri);
245233
}
246234

247-
function focusDocPath(doc: vscode.TextDocument | undefined) {
248-
const fsPath = doc
249-
? doc.isUntitled
250-
? "/untitled/" + doc.uri.fsPath
251-
: doc.uri.fsPath
252-
: undefined;
253-
235+
function focusDoc(doc: vscode.TextDocument | undefined, editor?: TextEditor | undefined) {
254236
// Changes focus state.
255-
extensionState.mut.focusing.focusMain(doc, fsPath);
237+
extensionState.mut.focusing.focusMain(doc, editor);
256238

257239
// Changes status bar.
258240
const formatString = statusBarFormatString();
259241
triggerStatusBar(
260242
// Shows the status bar only the last focusing file is not closed (opened)
261-
!!formatString && !!(fsPath || extensionState.mut.focusing.doc?.isClosed === false),
243+
!!formatString && !!(extensionState.mut.focusing.doc?.isClosed === false),
262244
);
263245
}
264246

editors/vscode/src/focus.ts

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,83 @@ import * as vscode from "vscode";
33

44
export class FocusState {
55
private mainTimeout: NodeJS.Timeout | undefined = undefined;
6-
private mainPath: string | undefined = undefined;
76
private mainDoc: vscode.TextDocument | undefined = undefined;
7+
private subscribedSelection: boolean = false;
8+
private lastSyncPath?: string;
89

10+
get doc() {
11+
return this.mainDoc;
12+
}
13+
14+
/**
15+
* Resets the state of the focus. This is called before the extension is
16+
* (re-)activated.
17+
*/
918
reset() {
1019
if (this.mainTimeout) {
1120
clearTimeout(this.mainTimeout);
1221
}
22+
this.lastSyncPath = undefined;
1323
}
1424

15-
// Informs the server after a while. This is not done intemediately to avoid
16-
// the cases that the user removes the file from the editor and then opens
17-
// another one in a short time.
18-
focusMain(doc?: vscode.TextDocument, fsPath?: string) {
19-
if (this.mainTimeout) {
20-
clearTimeout(this.mainTimeout);
21-
}
25+
/**
26+
* Informs the server after a while. This is not done intemediately to avoid
27+
* the cases that the user removes the file from the editor and then opens
28+
* another one in a short time.
29+
*/
30+
focusMain(doc?: vscode.TextDocument, editor?: vscode.TextEditor) {
2231
this.mainDoc = doc;
2332
if (this.mainDoc?.isClosed) {
2433
this.mainDoc = undefined;
2534
}
26-
this.mainPath = fsPath;
35+
36+
this.subscribeChange();
37+
this.lspChangeSelect(doc, editor ? () => editor.selections : undefined);
38+
}
39+
40+
/**
41+
* Lazily subscribes to the selection change event.
42+
*/
43+
private subscribeChange() {
44+
if (this.subscribedSelection) {
45+
return;
46+
}
47+
this.subscribedSelection = true;
48+
49+
vscode.window.onDidChangeTextEditorSelection((event) => {
50+
if (this.mainDoc && uriEquals(event.textEditor.document.uri, this.mainDoc.uri)) {
51+
this.lspChangeSelect(event.textEditor.document, () => event.selections);
52+
}
53+
});
54+
}
55+
56+
private lspChangeSelect(doc?: vscode.TextDocument, select?: () => readonly vscode.Selection[]) {
57+
if (this.mainTimeout) {
58+
clearTimeout(this.mainTimeout);
59+
}
2760
this.mainTimeout = setTimeout(async () => {
28-
tinymist.executeCommand("tinymist.focusMain", [fsPath]);
61+
const fsPath = doc
62+
? doc.isUntitled
63+
? "/untitled/" + doc.uri.fsPath
64+
: doc.uri.fsPath
65+
: undefined;
66+
const opts = select ? { selections: await this.convertLspSelections(select()) } : undefined;
67+
68+
if (this.lastSyncPath === fsPath) {
69+
tinymist.executeCommand("tinymist.changeSelections", [opts?.selections]);
70+
} else {
71+
tinymist.executeCommand("tinymist.focusMain", [fsPath, opts]);
72+
}
2973
}, 100);
3074
}
3175

32-
get path() {
33-
return this.mainPath;
34-
}
35-
get doc() {
36-
return this.mainDoc;
76+
async convertLspSelections(selections: readonly vscode.Selection[]) {
77+
const client = await tinymist.getClient();
78+
79+
return selections.map((s) => client.code2ProtocolConverter.asRange(s));
3780
}
3881
}
82+
83+
function uriEquals(lhs: vscode.Uri, rhs: vscode.Uri) {
84+
return lhs.toString(true) === rhs.toString(true);
85+
}

0 commit comments

Comments
 (0)