Skip to content

Commit 67c9d5b

Browse files
UrtsiSantsisonnyp
andauthored
Add search to code views (#853)
Co-authored-by: Sonny Piers <sonny@fastmail.net>
1 parent 79bd0bd commit 67c9d5b

File tree

7 files changed

+307
-4
lines changed

7 files changed

+307
-4
lines changed

.vscode/settings.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@
2020
"[yaml]": {
2121
"editor.defaultFormatter": "esbenp.prettier-vscode"
2222
},
23-
"vala.languageServerPath": "${workspaceFolder}/.flatpak/vala-language-server.sh",
2423
"[json]": {
2524
"editor.defaultFormatter": "esbenp.prettier-vscode"
2625
},
2726
"mesonbuild.configureOnOpen": false,
2827
"mesonbuild.buildFolder": "_build",
29-
"mesonbuild.mesonPath": "${workspaceFolder}/.flatpak/meson.sh"
28+
"mesonbuild.mesonPath": "${workspaceFolder}/.flatpak/meson.sh",
29+
"vala.languageServerPath": "${workspaceFolder}/.flatpak/vala-language-server.sh"
3030
}

data/app.metainfo.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
<li>Open the Library on start if there are no sessions to restore</li>
5656
<li>Restore scroll and cusor positions on format and Run</li>
5757
<li>Add "Copy" and "Select All" to Console</li>
58+
<!-- <li>Add find text support</li> -->
5859
<li>Add Vala formatter support</li>
5960
<li>Add WebP image format support</li>
6061
<li>Use Biome instead of prettier for JavaScript formatter</li>

src/shortcutsWindow.blp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ ShortcutsWindow window {
4242
title: _("Redo");
4343
}
4444

45+
// ShortcutsShortcut {
46+
// accelerator: "<Control>F";
47+
// title: _("Find");
48+
// }
4549
ShortcutsShortcut {
4650
accelerator: "<Control>W";
4751
title: _("Close Window");

src/widgets/CodeFind.blp

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using Gtk 4.0;
2+
3+
template $CodeFind: Revealer {
4+
transition-type: slide_up;
5+
reveal-child: false;
6+
7+
Box {
8+
orientation: vertical;
9+
10+
Separator {}
11+
12+
Box {
13+
spacing: 8;
14+
halign: center;
15+
margin-start: 8;
16+
margin-end: 8;
17+
margin-top: 8;
18+
margin-bottom: 8;
19+
20+
Box {
21+
valign: center;
22+
width-request: 220;
23+
css-name: "entry";
24+
25+
Image {
26+
icon-name: 'edit-find-symbolic';
27+
}
28+
29+
Text text_search_term {
30+
hexpand: true;
31+
vexpand: true;
32+
width-chars: 10;
33+
max-width-chars: 10;
34+
}
35+
36+
Label label_info {
37+
label: "";
38+
xalign: 1;
39+
opacity: 0.5;
40+
}
41+
}
42+
43+
Box {
44+
valign: center;
45+
46+
styles [
47+
"linked"
48+
]
49+
50+
Button button_previous {
51+
icon-name: "up";
52+
tooltip-text: _("Move to previous match (Ctrl+Shift+G)");
53+
sensitive: false;
54+
}
55+
56+
Button button_next {
57+
icon-name: "down";
58+
tooltip-text: _("Move to next match (Ctrl+G)");
59+
sensitive: false;
60+
}
61+
}
62+
}
63+
}
64+
}

src/widgets/CodeFind.js

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import GObject from "gi://GObject";
2+
import Gdk from "gi://Gdk";
3+
import Gtk from "gi://Gtk";
4+
import GtkSource from "gi://GtkSource";
5+
6+
import Template from "./CodeFind.blp" with { type: "uri" };
7+
8+
class Search extends Gtk.Revealer {
9+
#previous_search_term = "";
10+
#buffer;
11+
#search_settings;
12+
#search_context;
13+
14+
constructor(params = {}) {
15+
super(params);
16+
17+
this.#buffer = this._source_view.buffer;
18+
19+
this.#initSearch();
20+
this.#trackOccurrences();
21+
this.#connectButtons();
22+
this.#addControllers();
23+
}
24+
25+
set source_view(obj) {
26+
this._source_view = obj;
27+
}
28+
29+
get source_view() {
30+
return this._source_view;
31+
}
32+
33+
#initSearch() {
34+
this.#search_settings = new GtkSource.SearchSettings({
35+
case_sensitive: false,
36+
wrap_around: true,
37+
});
38+
39+
this.#search_context = new GtkSource.SearchContext({
40+
buffer: this.#buffer,
41+
settings: this.#search_settings,
42+
highlight: true,
43+
});
44+
45+
this.#search_context.connect("notify::occurrences-count", () => {
46+
this.#updateInfo();
47+
});
48+
49+
this.#buffer.connect("mark-set", (_buffer, _iter, mark) => {
50+
const mark_name = mark.get_name();
51+
if (mark_name === "insert" || mark_name === "selection_bound") {
52+
this.#updateInfo();
53+
}
54+
});
55+
56+
this._text_search_term.connect("notify", () => {
57+
this.#search_settings.search_text = this._text_search_term.text;
58+
const [, , iter] = this.#buffer.get_selection_bounds();
59+
const [found, ,] = this.#search_context.forward(iter);
60+
this._button_previous.sensitive = found;
61+
this._button_next.sensitive = found;
62+
});
63+
}
64+
65+
#trackOccurrences() {
66+
this.#search_context.connect("notify::occurrences-count", () => {
67+
this.#updateInfo();
68+
});
69+
70+
this.#buffer.connect("mark-set", (_buffer, _iter, mark) => {
71+
const mark_name = mark.get_name();
72+
if (mark_name === "insert" || mark_name === "selection_bound") {
73+
this.#updateInfo();
74+
}
75+
});
76+
}
77+
78+
#connectButtons() {
79+
this._button_previous.connect("clicked", () => {
80+
this.#search(false);
81+
});
82+
83+
this._button_next.connect("clicked", () => {
84+
this.#search(true);
85+
});
86+
}
87+
88+
#addControllers() {
89+
const controller_key_source_view = new Gtk.EventControllerKey();
90+
this._source_view.add_controller(controller_key_source_view);
91+
controller_key_source_view.connect(
92+
"key-pressed",
93+
(_controller, keyval, _keycode, state) => {
94+
if (
95+
state & Gdk.ModifierType.CONTROL_MASK &&
96+
(keyval === Gdk.KEY_f || keyval === Gdk.KEY_F)
97+
) {
98+
const selected_text = this.#getSelectedText();
99+
if (selected_text) {
100+
this._text_search_term.text = selected_text;
101+
} else {
102+
this._text_search_term.text = this.#previous_search_term;
103+
}
104+
this.reveal_child = true;
105+
this._text_search_term.grab_focus();
106+
} else if (this.reveal_child && keyval === Gdk.KEY_Escape) {
107+
this.#endSearch(false);
108+
} else if (
109+
this.reveal_child &&
110+
state & Gdk.ModifierType.CONTROL_MASK &&
111+
(keyval === Gdk.KEY_g || keyval === Gdk.KEY_G)
112+
) {
113+
this.#search(!(state & Gdk.ModifierType.SHIFT_MASK));
114+
}
115+
},
116+
);
117+
118+
const controller_key = new Gtk.EventControllerKey();
119+
this.add_controller(controller_key);
120+
121+
controller_key.connect(
122+
"key-pressed",
123+
(_controller, keyval, _keycode, state) => {
124+
if (
125+
state & Gdk.ModifierType.CONTROL_MASK &&
126+
(keyval === Gdk.KEY_g || keyval === Gdk.KEY_G)
127+
) {
128+
this.#search(!(state & Gdk.ModifierType.SHIFT_MASK));
129+
} else if (
130+
state & Gdk.ModifierType.SHIFT_MASK &&
131+
keyval === Gdk.KEY_Return
132+
) {
133+
this.#search(false);
134+
} else if (keyval === Gdk.KEY_Escape) {
135+
this.#endSearch(true);
136+
}
137+
},
138+
);
139+
140+
this._text_search_term.connect("activate", () => {
141+
this.#search(true);
142+
});
143+
}
144+
145+
#endSearch(grabFocus) {
146+
this.reveal_child = false;
147+
this.#previous_search_term = this._text_search_term.text;
148+
this._text_search_term.text = "";
149+
if (grabFocus) {
150+
this._source_view.grab_focus();
151+
}
152+
}
153+
154+
#getSelectedText() {
155+
const [, match_start, match_end] = this.#buffer.get_selection_bounds();
156+
return this.#buffer.get_text(match_start, match_end, true);
157+
}
158+
159+
#search(isForward) {
160+
let found, match_start, match_end, iter;
161+
if (isForward) {
162+
[, , iter] = this.#buffer.get_selection_bounds();
163+
[found, match_start, match_end] = this.#search_context.forward(iter);
164+
} else {
165+
[, iter] = this.#buffer.get_selection_bounds();
166+
[found, match_start, match_end] = this.#search_context.backward(iter);
167+
}
168+
if (found) {
169+
this.#selectOccurence(match_start, match_end);
170+
}
171+
}
172+
173+
#selectOccurence(match_start, match_end) {
174+
this.#buffer.select_range(match_start, match_end);
175+
this._source_view.scroll_mark_onscreen(this.#buffer.get_insert());
176+
}
177+
178+
#updateInfo() {
179+
if (this._text_search_term.text) {
180+
const [, match_start, match_end] = this.#buffer.get_selection_bounds();
181+
const count = this.#search_context.get_occurrences_count();
182+
const index = this.#search_context.get_occurrence_position(
183+
match_start,
184+
match_end,
185+
);
186+
187+
if (count < 1) {
188+
this._label_info.label = "";
189+
} else if (index === -1) {
190+
this._label_info.label = `${count} occurences`;
191+
} else {
192+
this._label_info.label = `${index} of ${count}`;
193+
}
194+
} else {
195+
this._label_info.label = "";
196+
}
197+
}
198+
}
199+
200+
export default GObject.registerClass(
201+
{
202+
GTypeName: "CodeFind",
203+
Template,
204+
InternalChildren: [
205+
"text_search_term",
206+
"label_info",
207+
"button_previous",
208+
"button_next",
209+
],
210+
Properties: {
211+
source_view: GObject.ParamSpec.object(
212+
"source-view",
213+
"SourceView",
214+
"The SourceView that will be searched",
215+
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
216+
Gtk.Widget.$gtype,
217+
),
218+
},
219+
},
220+
Search,
221+
);

src/widgets/CodeView.blp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ using Gtk 4.0;
22
using GtkSource 5;
33

44
template $CodeView: Gtk.Widget {
5-
layout-manager: BinLayout {};
5+
layout-manager: BoxLayout {
6+
orientation: vertical;
7+
};
68

79
vexpand: true;
810

@@ -24,4 +26,9 @@ template $CodeView: Gtk.Widget {
2426
tab-width: 2;
2527
}
2628
}
29+
30+
$CodeFind code_find {
31+
vexpand: false;
32+
source-view: source_view;
33+
}
2734
}

src/widgets/CodeView.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import WorkbenchHoverProvider from "../WorkbenchHoverProvider.js";
1313
import { registerClass } from "../overrides.js";
1414
import { getItersAtRange } from "../lsp/sourceview.js";
1515

16+
import "./CodeFind.js";
17+
1618
Source.init();
1719

1820
const scheme_manager = Source.StyleSchemeManager.get_default();
@@ -23,6 +25,10 @@ class CodeView extends Gtk.Widget {
2325
super(params);
2426
this.source_view = this._source_view;
2527
this.buffer = this._source_view.buffer;
28+
// TODO: Investigate why the Blueprint defintion does not behave as intended
29+
// transition-type: slide_up;
30+
// https://github.com/workbenchdev/Workbench/pull/853/files#r1443560736
31+
this._code_find.transition_type = Gtk.RevealerTransitionType.SLIDE_UP;
2632

2733
try {
2834
this.language = language_manager.get_language(language_id);
@@ -198,7 +204,7 @@ export default registerClass(
198204
Signals: {
199205
changed: {},
200206
},
201-
InternalChildren: ["source_view"],
207+
InternalChildren: ["source_view", "code_find"],
202208
},
203209
CodeView,
204210
);

0 commit comments

Comments
 (0)