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