turbo-editors/turbo-corepublic Fork 0
v1.0.0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

actions_file.go · 338 lines · 9.6 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 18h ago1// Everything the File menu does: opening, saving and closing files, and
2// leaving the editor.
3package app
4
5import (
6 "fmt"
7 "path/filepath"
8
📦 Turbo Core f3ade8d k33g 11h ago9 "rickub.com/turbo-editors/turbo-core/buffer"
10 "rickub.com/turbo-editors/turbo-core/editor"
11 "rickub.com/turbo-editors/turbo-core/ui"
🛟 Updated. 28d5985 k33g 18h ago12)
13
14// untitledName is what a window with no file yet is called.
15const untitledName = "Untitled"
16
17// NewFile opens an empty window.
18func (a *App) NewFile() {
19 a.openBuffer(buffer.New())
20}
21
22// OpenFile asks for a file and opens it.
23func (a *App) OpenFile() {
24 dialog := NewFileDialog("Open", a.currentDirectory(), a.screenRect())
25
26 a.pushModal(dialog.Dialog(), func(result ui.Result) {
27 if result == ui.ResultOK {
28 a.Open(dialog.Path())
29 }
30 })
31}
32
33// Open reads a file into a new window, or brings its window forward when it is
34// already open.
35func (a *App) Open(path string) {
36 if path == "" {
37 return
38 }
39 if window := a.windowFor(path); window != nil {
40 a.desktop.Focus(window)
41 return
42 }
43
44 buf, err := buffer.Open(path)
45 if err != nil {
46 a.ShowMessage("Cannot open", err.Error())
47 return
48 }
49 a.openBuffer(buf)
50}
51
52// windowFor returns the window already editing a path, if there is one.
53func (a *App) windowFor(path string) *ui.Window {
54 wanted, err := filepath.Abs(path)
55 if err != nil {
56 wanted = path
57 }
58
59 for _, window := range a.desktop.Windows() {
60 view, ok := editorViewOf(window)
61 if !ok || view.Buffer().Path() == "" {
62 continue
63 }
64 if existing, err := filepath.Abs(view.Buffer().Path()); err == nil && existing == wanted {
65 return window
66 }
67 }
68 return nil
69}
70
71// openBuffer puts a buffer in a new window and tells the language server.
72func (a *App) openBuffer(buf *buffer.Buffer) {
73 view := editor.NewView(buf, a.clipboard)
74 window := ui.NewWindow(windowTitle(buf), view)
75 window.SetBounds(a.newWindowBounds())
76 window.OnClose = func() bool { a.closeWindow(window); return true }
77
78 view.OnChange = func() { a.viewChanged(window, view) }
79 view.OnCompletionRequest = a.RequestCompletion
80
81 a.desktop.Add(window)
82 a.windowsOpened++
83 a.language.DidOpen(buf.Path(), buf.Text())
84}
85
86// windowTitle returns what a buffer's window is called: its file name, with a
87// star while there are unsaved changes.
88func windowTitle(buf *buffer.Buffer) string {
89 name := untitledName
90 if buf.Path() != "" {
91 name = filepath.Base(buf.Path())
92 }
93 if buf.Modified() {
94 return name + " *"
95 }
96 return name
97}
98
99// viewChanged runs after every edit: it retitles the window and tells the
100// language server what the file now says.
101func (a *App) viewChanged(window *ui.Window, view *editor.View) {
102 window.SetTitle(windowTitle(view.Buffer()))
103 a.language.DidChange(view.Buffer().Path(), view.Buffer().Text())
104 a.refreshCompletionPrefix()
105 a.noteEdit()
106}
107
108// newWindowBounds returns where the next window goes: the whole desktop, with
109// a cascade offset so a second window does not hide the first.
110func (a *App) newWindowBounds() ui.Rect {
111 area := a.desktopRect()
112 offset := (a.windowsOpened % maxWindowOffsets) * newWindowOffset
113
114 return ui.Rect{
115 X: area.X + offset,
116 Y: area.Y + offset,
117 W: max(area.W-offset-ui.ShadowWidth, ui.MinWindowWidth),
118 H: max(area.H-offset, ui.MinWindowHeight),
119 }
120}
121
122// currentDirectory returns where a file dialog should open: beside the file
123// being edited, or the working directory when there is none.
124func (a *App) currentDirectory() string {
125 if view := a.activeView(); view != nil && view.Buffer().Path() != "" {
126 return view.Buffer().Path()
127 }
128 return ""
129}
130
131// SaveFile writes the front window's file, asking for a name if it has none.
132func (a *App) SaveFile() {
133 view := a.activeView()
134 if view == nil {
135 return
136 }
137 if view.Buffer().Path() == "" {
138 a.SaveFileAs()
139 return
140 }
141 a.save(view, view.Buffer().Path())
142}
143
144// SaveFileAs asks for a name and writes the front window's file to it.
145func (a *App) SaveFileAs() {
146 view := a.activeView()
147 if view == nil {
148 return
149 }
150
151 dialog := NewFileDialog("Save as", a.currentDirectory(), a.screenRect())
152 a.pushModal(dialog.Dialog(), func(result ui.Result) {
153 if result == ui.ResultOK {
154 a.save(view, dialog.Path())
155 }
156 })
157}
158
159// save writes a view's buffer to a path and reports how it went.
160func (a *App) save(view *editor.View, path string) {
161 if path == "" {
162 return
163 }
164 // Remembered before the write, because SaveAs rewrites the buffer's path;
165 // it is the only record of which document the server had open until now.
166 previous := view.Buffer().Path()
167 if err := view.Buffer().SaveAs(path); err != nil {
168 a.ShowMessage("Cannot save", err.Error())
169 return
170 }
171 if renamed(previous, path) {
172 a.language.DidClose(previous)
173 }
174 a.afterSave(view, path)
175}
176
177// renamed reports whether a save gave the buffer a genuinely different file,
178// rather than writing the one it already had. Compared absolute, because the
179// Save As dialog and the command line spell the same file differently.
180//
181// Without the close this decides on, a Save As under a new name leaves the
182// old document open on the server for as long as the editor runs — a ghost
183// that keeps its diagnostics and shadows the file if it is ever reopened.
184func renamed(previous, path string) bool {
185 return previous != "" && pathKey(previous) != pathKey(path)
186}
187
188// afterSave brings everything that watches a file up to date once it has been
189// written, whichever of the two ways wrote it.
190//
191// The File menu's save and automatic saving differ only in how they report a
192// failure; everything after a successful write is the same. It is one function
193// so that a step added to one cannot go missing from the other — re-reading the
194// project's settings is exactly such a step, and would have been missing from
195// autosave.
196func (a *App) afterSave(view *editor.View, path string) {
197 view.RefreshSyntax()
198 if window := a.windowOf(view); window != nil {
199 window.SetTitle(windowTitle(view.Buffer()))
200 }
201 a.announceSaved(path, view.Buffer().Text())
202 a.refreshTree()
203
204 // The message comes before the settings are re-read, so that re-reading can
205 // replace it: "the settings you just saved are now in force" is worth more
206 // than "saved", and a settings file that no longer parses is worth much
207 // more than either.
208 a.Message("Saved " + filepath.Base(path))
209 a.reapplySettings(path)
210}
211
212// announceSaved tells the language server about a write — or about the
213// document itself, when this save is the first time there is a name to tell.
214//
215// A window that started Untitled was skipped by every didOpen so far: it had
216// no path to announce. A server ignores didChange and didSave for a document
217// it was never told is open, so sending only didSave here would leave that
218// window without completion, hover or diagnostics until the editor is
219// restarted and the file is opened with its name — which is exactly how the
220// defect was found.
221func (a *App) announceSaved(path, text string) {
222 if a.language.Knows(path) {
223 a.language.DidSave(path, text)
224 return
225 }
226 a.language.DidOpen(path, text)
227}
228
229// windowOf returns the window holding a view.
230func (a *App) windowOf(view *editor.View) *ui.Window {
231 for _, window := range a.desktop.Windows() {
232 if window.Content() == ui.Widget(view) {
233 return window
234 }
235 }
236 return nil
237}
238
239// CloseFile closes the front window, offering to save it first.
240func (a *App) CloseFile() {
241 window := a.desktop.Active()
242 if window == nil {
243 return
244 }
245 a.closeWindow(window)
246}
247
248// closeWindow closes a window, asking about unsaved changes first.
249func (a *App) closeWindow(window *ui.Window) {
250 if view, isTerminal := a.terminalView(window); isTerminal {
251 a.closeTerminal(window, view)
252 return
253 }
254 if a.isTreeWindow(window) {
255 a.closeTree()
256 return
257 }
258 // An agent window holds a running conversation, not unsaved work, so
259 // closing it asks nothing — the same bargain a terminal makes.
260 if a.isAgentWindow(window) {
261 a.closeAgent(window)
262 return
263 }
264
265 view, ok := editorViewOf(window)
266 if !ok || !view.Buffer().Modified() {
267 a.discard(window)
268 return
269 }
270 // With autosave on there is nothing to ask about: the file was going to be
271 // written anyway, so closing writes it and gets out of the way.
272 if a.savedByAutosave(window) {
273 a.discard(window)
274 return
275 }
276
277 question := fmt.Sprintf("%s has unsaved changes. Save them?", window.Title())
278 a.pushModal(NewConfirmDialog("Close", question, a.screenRect()), func(result ui.Result) {
279 switch result {
280 case ui.ResultOK:
281 a.SaveFile()
282 a.discard(window)
283 case ui.ResultNo:
284 a.discard(window)
285 }
286 })
287}
288
289// discard removes a window without asking anything.
290func (a *App) discard(window *ui.Window) {
291 if view, ok := editorViewOf(window); ok {
292 a.language.DidClose(view.Buffer().Path())
293 }
294 a.desktop.Remove(window)
295 a.completion.Hide()
296}
297
298// Quit leaves the editor, offering to save each modified file on the way.
299func (a *App) Quit() {
300 for _, window := range a.desktop.Windows() {
301 view, ok := editorViewOf(window)
302 if !ok || !view.Buffer().Modified() {
303 continue
304 }
305 // Autosave writes what it can; a window it cannot write — one that has
306 // never been named — is still a real question.
307 if a.savedByAutosave(window) {
308 continue
309 }
310 a.confirmQuit(window)
311 return
312 }
313
314 // Every shell goes with the editor: a window is the only way back to one,
315 // and leaving them running would strand the processes.
316 a.closeTerminals()
317 a.closeAgents()
318 a.cancelAutosave()
319 a.quitting = true
320}
321
322// confirmQuit asks about one modified window, then tries to quit again.
323func (a *App) confirmQuit(window *ui.Window) {
324 question := fmt.Sprintf("%s has unsaved changes. Save them?", window.Title())
325
326 a.pushModal(NewConfirmDialog("Exit", question, a.screenRect()), func(result ui.Result) {
327 switch result {
328 case ui.ResultOK:
329 a.desktop.Focus(window)
330 a.SaveFile()
331 a.discard(window)
332 a.Quit()
333 case ui.ResultNo:
334 a.discard(window)
335 a.Quit()
336 }
337 })
338}